Skip to content

Commit

Permalink
Merge pull request #6 from custom-components/Vars_to_configuration
Browse files Browse the repository at this point in the history
Vars to configuration
  • Loading branch information
Ernst79 authored Nov 27, 2019
2 parents 94a6b02 + 28bac97 commit da40f48
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 94 deletions.
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Xiaomi Mi Temperature and Humidity Monitor (BLE) sensor platform
This custom component is an alternative for the standard build in [mitemp_bt](https://www.home-assistant.io/integrations/mitemp_bt/) integration that is available in Home Assistant. Unlike the original `mitemp_bt` integration, which is getting its data by polling the device with a default five-minute interval, this custom component is parsing the Bluetooth Low Energy packets payload that is emitted each second by the sensor. The packets payload contains temperature/humidity and battery data. Advantage of this integration is that it doesn't affect the battery as much as the built-in integration. It also solves connection issues some people have with the standard integration.
This custom component is an alternative for the standard build in [mitemp_bt](https://www.home-assistant.io/integrations/mitemp_bt/) integration that is available in Home Assistant. Unlike the original `mitemp_bt` integration, which is getting its data by polling the device with a default five-minute interval, this custom component is parsing the Bluetooth Low Energy packets payload that is emitted each second by the sensor. The packets payload contains temperature/humidity and battery data. Advantage of this integration is that it doesn't affect the battery as much as the built-in integration. It also solves connection issues some people have with the standard integration. Currently only LYWSDCGQ sensor compatibility confirmed (round body, segment LCD).

![sensor](/sensor.jpg)
![LYWSDCGQ](/sensor.jpg)

## HOW TO INSTALL
**1. Install bluez-hcidump (not needed on HASSio):**
Expand Down Expand Up @@ -45,5 +45,51 @@ sensor:

IMPORTANT. If you used the standard Home Assistant built ['mitemp_bt'](https://www.home-assistant.io/integrations/mitemp_bt/) integration, make sure you delete the additional parameters, like `mac:` and `monitored_conditions:`.

An example of `configuration.yaml` with all optional parameters is:

```yaml
sensor:
- platform: mitemp_bt
rounding: True
decimals: 2
period: 60
log_spikes: False
use_median: False
hcitool_active: False
```


### Configuration Variables

**rounding**

(boolean)(Optional) Enable/disable rounding of the average of all measurements taken within the number seconds specified with 'period'. Default value: True

**decimals**

(positive integer)(Optional) Number of decimal places to round if rounding is enabled. Default value: 2

**period**

(positive integer)(Optional) The period in seconds during which the sensor readings are collected and transmitted to Home Assistant after averaging. Default value: 60

**log_spikes**

(boolean)(Optional) Puts information about each erroneous spike in the Home Assistant log. Default value: False

*There are reports (pretty rare) that some sensors tend to sometimes produce erroneous values that differ markedly from the actual ones. Therefore, if you see inexplicable sharp peaks or dips on the temperature or humidity graph, I recommend that you enable this option so that you can see in the log which values were qualified as erroneous. The component discards values that exceeds the sensor’s measurement capabilities. These discarded values are given in the log records when this option is enabled. If erroneous values are within the measurement capabilities (-40..60°C and 0..100%H), there are no messages in the log. If your sensor is showing this, there is no other choice but to calculate the average as the median (next option).*

**use_median**

(boolean)(Optional) Use median as sensor output instead of mean (helps with "spiky" sensors). Please note that both the median and the mean values in any case are present as the sensor state attributes. Default value: False

*The difference between the mean and the median is that the median is **selected** from the sensor readings, and not calculated as the average. That is, the median resolution is equal to the resolution of the sensor (one tenth of a degree or percent), while the mean allows you to slightly increase the resolution (the longer the measurement period, the larger the number of values will be averaged, and the higher the resolution can be achieved, if necessary with disabled rounding).*

**hcitool_active**

(boolean)(Optional) In active mode hcitool sends scan requests, which is most often not required, but slightly increases the sensor battery consumption. 'Passive mode' means that you are not sending any request to the sensor but you are just reciving the advertisements sent by the BLE devices. This parameter is a subject for experiment. See the hcitool docs, --passive switch. Default value: False



## Credits
Credits and a big thanks should be given to [@tsymbaliuk](https://community.home-assistant.io/u/tsymbaliuk) and [@Magalex](https://community.home-assistant.io/u/Magalex). The main python code for this component was originally developed by [@tsymbaliuk](https://community.home-assistant.io/u/tsymbaliuk) and later modified by [@Magalex](https://community.home-assistant.io/u/Magalex).
25 changes: 25 additions & 0 deletions custom_components/mitemp_bt/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Constants for the Xiaomi Mi temperature and humidity monitor integration."""

# Configuration options
CONF_ROUNDING = "rounding"
CONF_DECIMALS = "decimals"
CONF_PERIOD = "period"
CONF_LOG_SPIKES = "log_spikes"
CONF_USE_MEDIAN = "use_median"
CONF_HCITOOL_ACTIVE = "hcitool_active"

# Default values for configuration options
DEFAULT_ROUNDING = True
DEFAULT_DECIMALS = 2
DEFAULT_PERIOD = 60
DEFAULT_LOG_SPIKES = False
DEFAULT_USE_MEDIAN = False
DEFAULT_HCITOOL_ACTIVE = False

"""Fixed constants."""

# Sensor measurement limits to exclude erroneous spikes from the results
CONF_TMIN = -40.0
CONF_TMAX = 60.0
CONF_HMIN = 0.0
CONF_HMAX = 99.9
160 changes: 72 additions & 88 deletions custom_components/mitemp_bt/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import sys
import tempfile
import voluptuous as vol


from homeassistant.const import (
Expand All @@ -14,45 +15,46 @@
TEMP_CELSIUS,
ATTR_BATTERY_LEVEL,
)
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_point_in_utc_time
import homeassistant.util.dt as dt_util

from .const import (
DEFAULT_ROUNDING,
DEFAULT_DECIMALS,
DEFAULT_PERIOD,
DEFAULT_LOG_SPIKES,
DEFAULT_USE_MEDIAN,
DEFAULT_HCITOOL_ACTIVE,
CONF_ROUNDING,
CONF_DECIMALS,
CONF_PERIOD,
CONF_LOG_SPIKES,
CONF_USE_MEDIAN,
CONF_HCITOOL_ACTIVE,
CONF_TMIN,
CONF_TMAX,
CONF_HMIN,
CONF_HMAX,
)

_LOGGER = logging.getLogger(__name__)


# ----------------------
# SOME OPTIONS TO ADJUST
# ----------------------
# Enable/disable rounding of the average of all measurements taken
# within CONF_MITEMPBT_PERIOD seconds
CONF_MITEMPBT_ROUNDING = True
# To how many decimal places to round if rounding is enabled
CONF_MITEMPBT_DECIMALS = 2
# The period in seconds during which the sensor readings are
# collected and transmitted to HA after averaging
CONF_MITEMPBT_PERIOD = 60
#
# Sensor measurement limits to exclude erroneous spikes from the results
CONF_MITEMPBT_TMIN = -40.0
CONF_MITEMPBT_TMAX = 60.0
CONF_MITEMPBT_HMIN = 0.0
CONF_MITEMPBT_HMAX = 99.9
# Sensor measurement limits to exclude erroneous spikes from the results
CONF_MITEMPBT_LOG_SPIKES = False
# Use median as sensor output instead of mean (helps with "spiky" sensors)
CONF_MITEMPBT_USE_MEDIAN = False
# Please note that both the median and the average in any case are present
# as the sensor state attributes
#
CONF_MITEMPBT_HCITOOL_ACTIVE = False
# In active mode hcitool sends scan requests, which is most often
# not required, but slightly increases the sensor battery consumption.
# 'Passive mode' means that you are not sending any request to the sensor
# but you are just reciving the advertisements sent by the BLE devices.
# This parameter is a subject for experiment.
# See the hcitool docs, --passive switch.
# ----------------------
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ROUNDING, default=DEFAULT_ROUNDING): cv.boolean,
vol.Optional(CONF_DECIMALS, default=DEFAULT_DECIMALS): cv.positive_int,
vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.positive_int,
vol.Optional(CONF_LOG_SPIKES, default=DEFAULT_LOG_SPIKES): cv.boolean,
vol.Optional(CONF_USE_MEDIAN, default=DEFAULT_USE_MEDIAN): cv.boolean,
vol.Optional(
CONF_HCITOOL_ACTIVE, default=DEFAULT_HCITOOL_ACTIVE
): cv.boolean,
}
)


def parse_raw_data(data):
Expand Down Expand Up @@ -150,8 +152,9 @@ class BLEScanner:
hcidump = None
tempf = tempfile.SpooledTemporaryFile()

def start(self):
def start(self, config):
"""Start receiving broadcasts."""
hcitool_active = config[CONF_HCITOOL_ACTIVE]
_LOGGER.debug("Temp dir used: %s", tempfile.gettempdir())
_LOGGER.debug("Start receiving broadcasts")
devnull = (
Expand All @@ -160,13 +163,10 @@ def start(self):
else open(os.devnull, "wb")
)
hcitoolcmd = ["hcitool", "lescan", "--duplicates", "--passive"]
if CONF_MITEMPBT_HCITOOL_ACTIVE:
if hcitool_active:
hcitoolcmd = ["hcitool", "lescan", "--duplicates"]
# sudo setcap 'cap_net_raw+ep' `readlink -f \`which hcidump\``
self.hcitool = subprocess.Popen(
hcitoolcmd,
stdout=devnull,
stderr=devnull,
hcitoolcmd, stdout=devnull, stderr=devnull
)
self.hcidump = subprocess.Popen(
["hcidump", "--raw", "hci"], stdout=self.tempf, stderr=None
Expand Down Expand Up @@ -208,15 +208,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the sensor platform."""
_LOGGER.debug("Starting")
scanner = BLEScanner()
scanner.start()
scanner.start(config)

sensors_by_mac = {}

def discover_ble_devices():
def discover_ble_devices(config):
"""Discover Bluetooth LE devices."""
_LOGGER.debug("Discovering Bluetooth LE devices")
rounding = config[CONF_ROUNDING]
decimals = config[CONF_DECIMALS]
log_spikes = config[CONF_LOG_SPIKES]
use_median = config[CONF_USE_MEDIAN]

_LOGGER.debug("Stopping")
scanner.stop()

_LOGGER.debug("Analyzing")
hum_m_data = {}
temp_m_data = {}
Expand All @@ -230,37 +236,26 @@ def discover_ble_devices():

# store found readings per device
if "temperature" in data:
if (
CONF_MITEMPBT_TMAX
>= data["temperature"]
>= CONF_MITEMPBT_TMIN
):
if CONF_TMAX >= data["temperature"] >= CONF_TMIN:
if data["mac"] not in temp_m_data:
temp_m_data[data["mac"]] = []
temp_m_data[data["mac"]].append(
data["temperature"]
)
temp_m_data[data["mac"]].append(data["temperature"])
macs[data["mac"]] = data["mac"]
elif CONF_MITEMPBT_LOG_SPIKES:
elif log_spikes:
_LOGGER.error(
"Temperature spike: %s",
(data["temperature"]),
"Temperature spike: %s (%s)", data["temperature"],
data["mac"]
)
if "humidity" in data:
if (
CONF_MITEMPBT_HMAX
>= data["humidity"]
>= CONF_MITEMPBT_HMIN
):
if CONF_HMAX >= data["humidity"] >= CONF_HMIN:
if data["mac"] not in hum_m_data:
hum_m_data[data["mac"]] = []
hum_m_data[data["mac"]].append(
data["humidity"]
)
hum_m_data[data["mac"]].append(data["humidity"])
macs[data["mac"]] = data["mac"]
elif CONF_MITEMPBT_LOG_SPIKES:
elif log_spikes:
_LOGGER.error(
"Humidity spike: %s", data["humidity"]
"Humidity spike: %s (%s)", data["humidity"],
data["mac"]
)
if "battery" in data:
batt[data["mac"]] = int(data["battery"])
Expand Down Expand Up @@ -293,27 +288,23 @@ def discover_ble_devices():
humstate_mean = None
tempstate_median = None
humstate_median = None
if CONF_MITEMPBT_USE_MEDIAN:
if use_median:
textattr = "last median of"
else:
textattr = "last mean of"
if mac in temp_m_data:
try:
if CONF_MITEMPBT_ROUNDING:
if rounding:
tempstate_median = round(
statistics.median(temp_m_data[mac]),
CONF_MITEMPBT_DECIMALS,
statistics.median(temp_m_data[mac]), decimals
)
tempstate_mean = round(
statistics.mean(temp_m_data[mac]),
CONF_MITEMPBT_DECIMALS,
statistics.mean(temp_m_data[mac]), decimals
)
else:
tempstate_median = statistics.median(
temp_m_data[mac]
)
tempstate_median = statistics.median(temp_m_data[mac])
tempstate_mean = statistics.mean(temp_m_data[mac])
if CONF_MITEMPBT_USE_MEDIAN:
if use_median:
setattr(sensors[0], "_state", tempstate_median)
else:
setattr(sensors[0], "_state", tempstate_mean)
Expand All @@ -336,21 +327,17 @@ def discover_ble_devices():
continue
if mac in hum_m_data:
try:
if CONF_MITEMPBT_ROUNDING:
if rounding:
humstate_median = round(
statistics.median(hum_m_data[mac]),
CONF_MITEMPBT_DECIMALS,
statistics.median(hum_m_data[mac]), decimals
)
humstate_mean = round(
statistics.mean(hum_m_data[mac]),
CONF_MITEMPBT_DECIMALS,
statistics.mean(hum_m_data[mac]), decimals
)
else:
humstate_median = statistics.median(
hum_m_data[mac]
)
humstate_median = statistics.median(hum_m_data[mac])
humstate_mean = statistics.mean(hum_m_data[mac])
if CONF_MITEMPBT_USE_MEDIAN:
if use_median:
setattr(sensors[1], "_state", humstate_median)
else:
setattr(sensors[1], "_state", humstate_mean)
Expand All @@ -367,26 +354,23 @@ def discover_ble_devices():
except AttributeError:
_LOGGER.info("Sensor %s not yet ready for update", mac)
except ZeroDivisionError:
_LOGGER.error(
"Division by zero while humidity averaging!"
)
_LOGGER.error("Division by zero while humidity averaging!")
continue
scanner.start()
scanner.start(config)
return []

def update_ble(now):
"""Lookup Bluetooth LE devices and update status."""
period = config[CONF_PERIOD]
_LOGGER.debug("update_ble called")

try:
discover_ble_devices()
discover_ble_devices(config)
except RuntimeError as error:
_LOGGER.error("Error during Bluetooth LE scan: %s", error)

track_point_in_utc_time(
hass,
update_ble,
dt_util.utcnow() + timedelta(seconds=CONF_MITEMPBT_PERIOD),
hass, update_ble, dt_util.utcnow() + timedelta(seconds=period)
)

update_ble(dt_util.utcnow())
Expand Down
Loading

0 comments on commit da40f48

Please sign in to comment.