diff --git a/README.md b/README.md index 3362bfd99..a1c5d31d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Xiaomi BLE Monitor 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 constantly emitted by the sensor. The packets payload may contain temperature/humidity/battery and other 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 constantly emitted by the sensor. The packets payload may contain temperature/humidity/battery and other 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 (due to passivity and the ability to collect data from multiple bt-interfaces simultaneously). Supported sensors: @@ -86,6 +86,7 @@ sensor: use_median: False active_scan: False hci_interface: 0 + batt_entities: False ``` @@ -124,7 +125,21 @@ sensor: #### hci_interface - (positive integer)(Optional) This parameter is used to select the bt-interface used. 0 for hci0, 1 for hci1 and so on. On most systems, the interface is hci0. Default value: 0 + (positive integer or list of positive integers)(Optional) This parameter is used to select the bt-interface used. 0 for hci0, 1 for hci1 and so on. On most systems, the interface is hci0. In addition, if you need to collect data from several interfaces, you can specify a list of interfaces: + + ```yaml + sensor: + - platform: mitemp_bt + hci_interface: + - 0 + - 1 + ``` + + Default value: 0 + +#### batt_entities + + (boolean)(Optional) By default, the battery information will be presented only as a sensor attribute called `battery level`. If you set this parameter to `True`, then the battery sensor entity will be additionally created - `sensor.mi_batt_ `. Default value: False ## Frequently asked questions diff --git a/custom_components/mitemp_bt/const.py b/custom_components/mitemp_bt/const.py index 77b45607c..b5495ca74 100644 --- a/custom_components/mitemp_bt/const.py +++ b/custom_components/mitemp_bt/const.py @@ -8,6 +8,7 @@ CONF_USE_MEDIAN = "use_median" CONF_ACTIVE_SCAN = "active_scan" CONF_HCI_INTERFACE = "hci_interface" +CONF_BATT_ENTITIES = "batt_entities" # Default values for configuration options DEFAULT_ROUNDING = True @@ -17,6 +18,7 @@ DEFAULT_USE_MEDIAN = False DEFAULT_ACTIVE_SCAN = False DEFAULT_HCI_INTERFACE = 0 +DEFAULT_BATT_ENTITIES = False """Fixed constants.""" @@ -36,13 +38,13 @@ '205D01': ["HHCCPOT002", 1] } -# Sensor type indexes dictionary -# Temperature, Humidity, Moisture, Conductivity, Illuminance -# Measurement type T H M C I 9 - no measurement +# Sensor type indexes dictionary +# Temperature, Humidity, Moisture, Conductivity, Illuminance, Battery +# Measurement type T H M C I B 9 - no measurement MMTS_DICT = { - 'HHCCJCY01' : [0, 9, 1, 2, 3], - 'HHCCPOT002': [9, 9, 0, 1, 9], - 'LYWSDCGQ' : [0, 1, 9, 9, 9], - 'LYWSD02' : [0, 1, 9, 9, 9], - 'CGG1' : [0, 1, 9, 9, 9] + 'HHCCJCY01' : [0, 9, 1, 2, 3, 9], + 'HHCCPOT002': [9, 9, 0, 1, 9, 9], + 'LYWSDCGQ' : [0, 1, 9, 9, 9, 2], + 'LYWSD02' : [0, 1, 9, 9, 9, 9], + 'CGG1' : [0, 1, 9, 9, 9, 2] } diff --git a/custom_components/mitemp_bt/sensor.py b/custom_components/mitemp_bt/sensor.py index c981462e2..199b5fa2b 100644 --- a/custom_components/mitemp_bt/sensor.py +++ b/custom_components/mitemp_bt/sensor.py @@ -5,6 +5,7 @@ import statistics as sts import struct from threading import Thread +from time import sleep import aioblescan as aiobs import voluptuous as vol @@ -12,6 +13,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_BATTERY, TEMP_CELSIUS, ATTR_BATTERY_LEVEL, ) @@ -29,6 +31,7 @@ DEFAULT_USE_MEDIAN, DEFAULT_ACTIVE_SCAN, DEFAULT_HCI_INTERFACE, + DEFAULT_BATT_ENTITIES, CONF_ROUNDING, CONF_DECIMALS, CONF_PERIOD, @@ -36,6 +39,7 @@ CONF_USE_MEDIAN, CONF_ACTIVE_SCAN, CONF_HCI_INTERFACE, + CONF_BATT_ENTITIES, CONF_TMIN, CONF_TMAX, CONF_HMIN, @@ -55,8 +59,9 @@ vol.Optional(CONF_USE_MEDIAN, default=DEFAULT_USE_MEDIAN): cv.boolean, vol.Optional(CONF_ACTIVE_SCAN, default=DEFAULT_ACTIVE_SCAN): cv.boolean, vol.Optional( - CONF_HCI_INTERFACE, default=DEFAULT_HCI_INTERFACE - ): cv.positive_int, + CONF_HCI_INTERFACE, default=[DEFAULT_HCI_INTERFACE] + ): vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_BATT_ENTITIES, default=DEFAULT_BATT_ENTITIES): cv.boolean, } ) @@ -107,12 +112,15 @@ def run(self): ) btctrl.send_scan_request() _LOGGER.debug("HCIdump thread: start main event_loop") - self._event_loop.run_forever() - _LOGGER.debug("HCIdump thread: main event_loop stopped, finishing") - btctrl.stop_scan_request() - conn.close() - self._event_loop.close() - _LOGGER.debug("HCIdump thread: Run finished") + try: + self._event_loop.run_forever() + finally: + _LOGGER.debug("HCIdump thread: main event_loop stopped, finishing") + btctrl.stop_scan_request() + conn.close() + self._event_loop.run_until_complete(asyncio.sleep(0)) + self._event_loop.close() + _LOGGER.debug("HCIdump thread: Run finished") def join(self, timeout=3): """Join HCIdump thread.""" @@ -121,8 +129,9 @@ def join(self, timeout=3): self._event_loop.call_soon_threadsafe(self._event_loop.stop) except AttributeError as error: _LOGGER.debug("%s", error) - Thread.join(self, timeout) - _LOGGER.debug("HCIdump thread: joined") + finally: + Thread.join(self, timeout) + _LOGGER.debug("HCIdump thread: joined") def reverse_mac(rmac): @@ -220,7 +229,7 @@ def parse_raw_message(data): } # loop through xiaomi payload - # assume that the data may have several values ​​of different types, + # assume that the data may have several values of different types, # although I did not notice this behavior with my LYWSDCGQ sensors while True: xvalue_typecode = data[xdata_point:xdata_point + 2] @@ -244,31 +253,37 @@ def parse_raw_message(data): class BLEScanner: """BLE scanner.""" - dumpthread = None + dumpthreads = [] hcidump_data = [] def start(self, config): """Start receiving broadcasts.""" active_scan = config[CONF_ACTIVE_SCAN] - hci_interface = config[CONF_HCI_INTERFACE] + hci_interfaces = config[CONF_HCI_INTERFACE] self.hcidump_data.clear() - _LOGGER.debug("Spawning HCIdump thread.") - self.dumpthread = HCIdump( - dumplist=self.hcidump_data, - interface=hci_interface, - active=int(active_scan is True), - ) - _LOGGER.debug("Starting HCIdump thread.") - self.dumpthread.start() + _LOGGER.debug("Spawning HCIdump thread(s).") + for hci_int in hci_interfaces: + dumpthread = HCIdump( + dumplist=self.hcidump_data, + interface=hci_int, + active=int(active_scan is True), + ) + self.dumpthreads.append(dumpthread) + _LOGGER.debug("Starting HCIdump thread for hci%s", hci_int) + dumpthread.start() + _LOGGER.debug("HCIdump threads count = %s", len(self.dumpthreads)) + def stop(self): - """Stop HCIdump thread.""" - self.dumpthread.join() + """Stop HCIdump thread(s).""" + for dumpthread in self.dumpthreads: + dumpthread.join() + self.dumpthreads.clear() def shutdown_handler(self, event): """Run homeassistant_stop event handler.""" _LOGGER.debug("Running homeassistant_stop event handler: %s", event) - self.dumpthread.join() + self.stop() def setup_platform(hass, config, add_entities, discovery_info=None): @@ -278,6 +293,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.bus.listen("homeassistant_stop", scanner.shutdown_handler) scanner.start(config) sensors_by_mac = {} + sleep(1) def calc_update_state(entity_to_update, sensor_mac, config, measurements_list): """Averages according to options and updates the entity state.""" @@ -348,7 +364,9 @@ def discover_ble_devices(config): else: prev_packet = None if prev_packet == packet: + # _LOGGER.debug("DUPLICATE: %s, IGNORING!", data) continue + # _LOGGER.debug("NEW DATA: %s", data) lpacket[data["mac"]] = packet # store found readings per device if "temperature" in data: @@ -400,7 +418,7 @@ def discover_ble_devices(config): # fixed entity index for every measurement type # according to the sensor implementation - t_i, h_i, m_i, c_i, i_i = MMTS_DICT[stype[mac]] + t_i, h_i, m_i, c_i, i_i, b_i = MMTS_DICT[stype[mac]] # if necessary, create a list of entities # according to the sensor implementation @@ -422,6 +440,10 @@ def discover_ble_devices(config): sensors = [None] * 2 sensors[t_i] = TemperatureSensor(mac) sensors[h_i] = HumiditySensor(mac) + + if config[CONF_BATT_ENTITIES] and (b_i != 9): + sensors.insert(b_i, BatterySensor(mac)) + except IndexError as error: _LOGGER.error( "Sensor implementation error for %s, %s!", stype[mac], mac @@ -439,7 +461,16 @@ def discover_ble_devices(config): sts.mean(rssi[mac]) ) getattr(sensor, "_device_state_attributes")["sensor type"] = stype[mac] - if mac in batt: + if mac in batt: + if config[CONF_BATT_ENTITIES]: + try: + setattr(sensors[b_i], "_state", batt[mac]) + sensors[b_i].async_schedule_update_ha_state() + except AttributeError: + _LOGGER.debug("BatterySensor %s not yet ready for update", mac) + for sensor in sensors: + if isinstance(sensor, BatterySensor): + continue getattr(sensor, "_device_state_attributes")[ ATTR_BATTERY_LEVEL ] = batt[mac] @@ -769,3 +800,53 @@ def unique_id(self) -> str: def force_update(self): """Force update.""" return True + + +class BatterySensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, mac): + """Initialize the sensor.""" + self._state = None + self._unique_id = "batt_" + mac + self._device_state_attributes = {} + + @property + def name(self): + """Return the name of the sensor.""" + return "mi {}".format(self._unique_id) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "%" + + @property + def device_class(self): + """Return the unit of measurement.""" + return DEVICE_CLASS_BATTERY + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def force_update(self): + """Force update.""" + return True diff --git a/faq.md b/faq.md index 0b3041bd3..e4ac55f78 100644 --- a/faq.md +++ b/faq.md @@ -85,6 +85,8 @@ sensor: device_class: "battery" ``` +Or (since v0.5.4) you can set option `batt_entities` to `True` - the battery sensor entity will be created automatically for each device reporting battery status. + ## RECEPTION ISSUES ### My sensor doesn't receive any readings from my sensors anymore or only occasionally @@ -104,6 +106,7 @@ Especially SSD devices are known to affect the Bluetooth reception, try to place - The quality of your Bluetooth transceiver. The range of the built-in Bluetooth tranceiver of a Raspberry Pi is known to be limited. Try using an external Bluetooth transceiver to increase the range, e.g. with an external antenna. +It is also worth noting that starting from v0.5.5, a component can receive data from multiple interfaces simultaneously (see the `hci_interface` option). ## OTHER ISSUES diff --git a/info.md b/info.md index ee490b6e8..97bce5b3b 100644 --- a/info.md +++ b/info.md @@ -2,34 +2,38 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) {% if prerelease %} + ### NB!: This is a Beta version! + {% endif %} -{% if installed %} -# Changes since 0.4 +{% if installed or pending_update %} -The component does not use external utilities anymore, we get access to data directly from python, from a separate tread. +# Changes since 0.5.3 New configuration options: -- active_scan: False -- hci_interface: 0 (integer number, 0 as default for hci0, 1 for hci1 and so on) +- batt_entities: False -Deprecated configuration options: +(boolean)(Optional) By default, the battery information will be presented only as a sensor attribute called `battery level`. If you set this parameter to `True`, then the battery sensor entity will be additionally created - `sensor.mi_batt_ `. Default value: False -- hcidump_active is deprecated and __must be removed__ from `configuration.yaml`) +Changed: -NB: +Added the ability to specify a list for the `hci_interface` configuration option. This makes it possible to collect data from multiple interfaces simultaneously: -Since the component now uses direct access to the HCI interface, python must have the appropriate rights, see paragraph 1 of the HOW TO INSTALL section [below](#how-to-install). - -In addition, we began to collect [Frequently Asked Questions](https://github.com/custom-components/sensor.mitemp_bt/blob/master/faq.md). Please read it before creating a new issue. +```yaml + sensor: + - platform: mitemp_bt + hci_interface: + - 0 + - 1 + ``` --- {% endif %} # Xiaomi BLE Monitor 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 constantly emitted by the sensor. The packets payload may contain temperature/humidity/battery and other 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 constantly emitted by the sensor. The packets payload may contain temperature/humidity/battery and other 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 (due to passivity and the ability to collect data from multiple bt-interfaces simultaneously). ![supported sensors](https://raw.github.com/custom-components/sensor.mitemp_bt/master/sensors.jpg) @@ -115,6 +119,7 @@ sensor: use_median: False active_scan: False hci_interface: 0 + batt_entities: False ``` ### Configuration Variables @@ -151,7 +156,21 @@ sensor: #### hci_interface - (positive integer)(Optional) This parameter is used to select the bt-interface used. 0 for hci0, 1 for hci1 and so on. On most systems, the interface is hci0. Default value: 0 + (positive integer or list of positive integers)(Optional) This parameter is used to select the bt-interface used. 0 for hci0, 1 for hci1 and so on. On most systems, the interface is hci0. In addition, if you need to collect data from multiple interfaces simultaneously, you can specify a list of interfaces: + + ```yaml + sensor: + - platform: mitemp_bt + hci_interface: + - 0 + - 1 + ``` + + Default value: 0 + +#### batt_entities + + (boolean)(Optional) By default, the battery information will be presented only as a sensor attribute called `battery level`. If you set this parameter to `True`, then the battery sensor entity will be additionally created - `sensor.mi_batt_ `. Default value: False ## Frequently asked questions