Skip to content

Commit

Permalink
Merge pull request #35 from custom-components/multiple_interfaces
Browse files Browse the repository at this point in the history
Multiple interfaces + Battery entities
  • Loading branch information
Magalex2x14 authored Feb 14, 2020
2 parents 5d75b18 + e02f989 commit fd323a2
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 49 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down Expand Up @@ -86,6 +86,7 @@ sensor:
use_median: False
active_scan: False
hci_interface: 0
batt_entities: False
```
Expand Down Expand Up @@ -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_ <sensor_mac_address>`. Default value: False
## Frequently asked questions
Expand Down
18 changes: 10 additions & 8 deletions custom_components/mitemp_bt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +18,7 @@
DEFAULT_USE_MEDIAN = False
DEFAULT_ACTIVE_SCAN = False
DEFAULT_HCI_INTERFACE = 0
DEFAULT_BATT_ENTITIES = False


"""Fixed constants."""
Expand All @@ -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]
}
133 changes: 107 additions & 26 deletions custom_components/mitemp_bt/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import statistics as sts
import struct
from threading import Thread
from time import sleep

import aioblescan as aiobs
import voluptuous as vol

from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_BATTERY,
TEMP_CELSIUS,
ATTR_BATTERY_LEVEL,
)
Expand All @@ -29,13 +31,15 @@
DEFAULT_USE_MEDIAN,
DEFAULT_ACTIVE_SCAN,
DEFAULT_HCI_INTERFACE,
DEFAULT_BATT_ENTITIES,
CONF_ROUNDING,
CONF_DECIMALS,
CONF_PERIOD,
CONF_LOG_SPIKES,
CONF_USE_MEDIAN,
CONF_ACTIVE_SCAN,
CONF_HCI_INTERFACE,
CONF_BATT_ENTITIES,
CONF_TMIN,
CONF_TMAX,
CONF_HMIN,
Expand All @@ -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,
}
)

Expand Down Expand Up @@ -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."""
Expand All @@ -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):
Expand Down Expand Up @@ -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]
Expand All @@ -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):
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading

0 comments on commit fd323a2

Please sign in to comment.