Skip to content

Commit

Permalink
Merge pull request #752 from custom-components/new-HA_BLE
Browse files Browse the repository at this point in the history
Add new HA BLE format
  • Loading branch information
Ernst79 authored Mar 18, 2022
2 parents c3e5051 + 0646280 commit 48e0474
Show file tree
Hide file tree
Showing 41 changed files with 1,019 additions and 401 deletions.
25 changes: 18 additions & 7 deletions custom_components/ble_monitor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
)
import homeassistant.util.dt as dt_util
from homeassistant.util import dt

from .ble_parser import BleParser
from .const import (
AUTO_BINARY_SENSOR_LIST,
AUTO_MANUFACTURER_DICT,
AUTO_SENSOR_LIST,
AES128KEY24_REGEX,
AES128KEY32_REGEX,
CONF_ACTIVE_SCAN,
Expand Down Expand Up @@ -75,6 +78,7 @@
DOMAIN,
PLATFORMS,
MAC_REGEX,
MANUFACTURER_DICT,
MEASUREMENT_DICT,
REPORT_UNKNOWN_LIST,
SERVICE_CLEANUP_ENTRIES,
Expand Down Expand Up @@ -567,7 +571,7 @@ def __init__(self, config, dataqueue):
self.tracker_whitelist = []
self.report_unknown = False
self.report_unknown_whitelist = []
self.last_bt_reset = dt_util.now()
self.last_bt_reset = dt.now()
if self.config[CONF_REPORT_UNKNOWN]:
if self.config[CONF_REPORT_UNKNOWN] != "Off":
self.report_unknown = self.config[CONF_REPORT_UNKNOWN]
Expand Down Expand Up @@ -649,10 +653,17 @@ def process_hci_events(self, data, gateway_id=DOMAIN):
if sensor_msg:
measurements = list(sensor_msg.keys())
device_type = sensor_msg["type"]
sensor_list = (
MEASUREMENT_DICT[device_type][0] + MEASUREMENT_DICT[device_type][1]
)
binary_list = MEASUREMENT_DICT[device_type][2] + ["battery"]
if device_type in MANUFACTURER_DICT:
sensor_list = (
MEASUREMENT_DICT[device_type][0] + MEASUREMENT_DICT[device_type][1]
)
binary_list = MEASUREMENT_DICT[device_type][2] + ["battery"]
elif device_type in AUTO_MANUFACTURER_DICT:
sensor_list = AUTO_SENSOR_LIST
binary_list = AUTO_BINARY_SENSOR_LIST + ["battery"]
else:
return

measuring = any(x in measurements for x in sensor_list)
binary = any(x in measurements for x in binary_list)
if binary == measuring:
Expand Down Expand Up @@ -741,7 +752,7 @@ def run(self):
if (interface_is_ok[hci] is False) and (self.config[CONF_BT_AUTO_RESTART] is True):
interfaces_to_reset.append(hci)
if interfaces_to_reset:
ts_now = dt_util.now()
ts_now = dt.now()
if (ts_now - self.last_bt_reset).seconds > 60:
for iface in interfaces_to_reset:
_LOGGER.error(
Expand Down
101 changes: 65 additions & 36 deletions custom_components/ble_monitor/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from homeassistant.util import dt

from .helper import (
identifier_normalize,
Expand All @@ -29,6 +29,8 @@
)

from .const import (
AUTO_MANUFACTURER_DICT,
AUTO_BINARY_SENSOR_LIST,
CONF_PERIOD,
CONF_RESTORE_STATE,
CONF_DEVICE_RESTORE_STATE,
Expand Down Expand Up @@ -65,6 +67,7 @@
"mode",
"sector_timer",
"number_of_sectors",
"weight",
]


Expand Down Expand Up @@ -101,34 +104,47 @@ def __init__(self, blemonitor, add_entities):
async def async_run(self, hass):
"""Entities updater loop."""

async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None):
device_sensors = MEASUREMENT_DICT[sensortype][2]
if key not in sensors_by_key:
sensors = []
for sensor in device_sensors:
description = [item for item in BINARY_SENSOR_TYPES if item.key is sensor][0]
sensors.insert(
device_sensors.index(sensor),
globals()[description.sensor_class](
async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None, data={}):
if sensortype in AUTO_MANUFACTURER_DICT:
sensors = {}
for measurement in AUTO_BINARY_SENSOR_LIST:
if measurement in data:
if key not in sensors_by_key:
sensors_by_key[key] = {}
if measurement not in sensors_by_key[key]:
description = [item for item in BINARY_SENSOR_TYPES if item.key is measurement][0]
sensors[measurement] = globals()[description.sensor_class](
self.config, key, sensortype, firmware, description, manufacturer
)
self.add_entities([sensors[measurement]])
sensors_by_key[key].update(sensors)
else:
sensors = sensors_by_key[key]
else:
device_sensors = MEASUREMENT_DICT[sensortype][2]
if key not in sensors_by_key:
sensors = {}
sensors_by_key[key] = {}
for measurement in device_sensors:
description = [item for item in BINARY_SENSOR_TYPES if item.key is measurement][0]
sensors[measurement] = globals()[description.sensor_class](
self.config, key, sensortype, firmware, description, manufacturer
),
)
if len(sensors) != 0:
)
self.add_entities([sensors[measurement]])
sensors_by_key[key] = sensors
self.add_entities(sensors)
else:
sensors = sensors_by_key[key]
else:
sensors = sensors_by_key[key]
return sensors

_LOGGER.debug("Binary entities updater loop started!")
sensors_by_key = {}
sensors = []
sensors = {}
batt = {} # batteries
mibeacon_cnt = 0
hpriority = []
ts_last = dt_util.now()
ts_last = dt.now()
ts_now = ts_last
data = None
data = {}
await asyncio.sleep(0)

# Set up binary sensors of configured devices on startup when sensortype is available in device registry
Expand All @@ -145,17 +161,17 @@ async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None):
firmware = dev.sw_version
if sensortype and firmware:
sensors = await async_add_binary_sensor(
key, sensortype, firmware, dev.manufacturer
key, sensortype, firmware, dev.manufacturer, data
)
else:
continue
else:
pass
else:
sensors = []
sensors = {}

# Set up new binary sensors when first BLE advertisement is received
sensors = []
sensors = {}
while True:
try:
advevent = await asyncio.wait_for(self.dataqueue.get(), 1)
Expand All @@ -179,19 +195,23 @@ async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None):
sensortype = data["type"]
firmware = data["firmware"]
manufacturer = data["manufacturer"] if "manufacturer" in data else None
device_sensors = MEASUREMENT_DICT[sensortype][2]
sensors = await async_add_binary_sensor(key, sensortype, firmware, manufacturer)
sensors = await async_add_binary_sensor(key, sensortype, firmware, manufacturer, data)
device_sensors = sensors.keys()

if data["data"] is False:
data = None
continue

# store found readings per device
if "battery" in MEASUREMENT_DICT[sensortype][0]:
# battery attribute
if sensortype in AUTO_MANUFACTURER_DICT or (
sensortype in MANUFACTURER_DICT and (
"battery" in MEASUREMENT_DICT[sensortype][0]
)
):
if "battery" in data:
batt[key] = int(data["battery"])
batt_attr = batt[key]
for entity in sensors:
for entity in sensors.values():
getattr(entity, "_extra_state_attributes")[
ATTR_BATTERY_LEVEL
] = batt_attr
Expand All @@ -202,10 +222,11 @@ async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None):
batt_attr = batt[key]
except KeyError:
batt_attr = None

# schedule an immediate update of binary sensors
for measurement in device_sensors:
if measurement in data:
entity = sensors[device_sensors.index(measurement)]
entity = sensors[measurement]
entity.collect(data, batt_attr)
if entity.pending_update is True:
entity.async_schedule_update_ha_state(True)
Expand All @@ -214,7 +235,7 @@ async def async_add_binary_sensor(key, sensortype, firmware, manufacturer=None):
):
hpriority.append(entity)
data = None
ts_now = dt_util.now()
ts_now = dt.now()
if ts_now - ts_last < timedelta(seconds=self.period):
continue
ts_last = ts_now
Expand Down Expand Up @@ -254,7 +275,10 @@ def __init__(
self._device_firmware = firmware
self._device_manufacturer = manufacturer \
if manufacturer is not None \
else MANUFACTURER_DICT[devtype]
else MANUFACTURER_DICT.get(
devtype,
AUTO_MANUFACTURER_DICT.get(devtype, None)
)

self._extra_state_attributes = {
'sensor_type': devtype,
Expand Down Expand Up @@ -398,7 +422,7 @@ def collect(self, data, batt_attr=None):
self._extra_state_attributes[ATTR_BATTERY_LEVEL] = batt_attr
if "motion timer" in data:
if data["motion timer"] == 1:
self._extra_state_attributes["last_motion"] = dt_util.now()
self._extra_state_attributes["last_motion"] = dt.now()
# dirty hack for kettle status
if self._device_type in KETTLES:
if self._newstate == 0:
Expand All @@ -412,7 +436,8 @@ def collect(self, data, batt_attr=None):
else:
self._extra_state_attributes["status"] = self._newstate
if self.entity_description.key == "opening":
self._extra_state_attributes["status"] = data["status"]
if "status" in data:
self._extra_state_attributes["status"] = data["status"]
if self.entity_description.key == "lock":
self._extra_state_attributes["action"] = data["action"]
self._extra_state_attributes["method"] = data["method"]
Expand All @@ -437,6 +462,10 @@ def collect(self, data, batt_attr=None):
self._extra_state_attributes["sector_timer"] = data["sector timer"]
if "number of sectors" in data:
self._extra_state_attributes["number_of_sectors"] = data["number of sectors"]
if self.entity_description.key == "weight removed":
if "stabilized" in data:
if data["stabilized"] and data["weight removed"]:
self._extra_state_attributes["weight"] = data["non-stabilized weight"]

async def async_update(self):
"""Update sensor state and attribute."""
Expand All @@ -454,7 +483,7 @@ def __init__(self, config, key, devtype, firmware, description, manufacturer=Non
def reset_state(self, event=None):
"""Reset state of the sensor."""
# check if the latest update of the timer is longer than the set timer value
if dt_util.now() - self._start_timer >= timedelta(seconds=self._reset_timer):
if dt.now() - self._start_timer >= timedelta(seconds=self._reset_timer):
self._state = False
self.schedule_update_ha_state(False)

Expand All @@ -464,15 +493,15 @@ async def async_update(self):
if self._reset_timer > 0:
try:
# if there is a last_motion attribute, check the timer
now = dt_util.now()
self._start_timer = self._extra_state_attributes["last_motion"]
now = dt.now()
self._start_timer = dt.parse_datetime(self._extra_state_attributes["last_motion"])

if now - self._start_timer >= timedelta(seconds=self._reset_timer):
self._state = False
else:
self._state = True
async_call_later(self.hass, self._reset_timer, self.reset_state)
except KeyError:
except (KeyError, ValueError):
self._state = self._newstate
else:
self._state = self._newstate
10 changes: 7 additions & 3 deletions custom_components/ble_monitor/ble_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .const import GATT_CHARACTERISTICS
from .govee import parse_govee
from .ha_ble import parse_ha_ble
from .ha_ble_legacy import parse_ha_ble_legacy
from .ibeacon import parse_ibeacon
from .inkbird import parse_inkbird
from .inode import parse_inode
Expand Down Expand Up @@ -136,10 +137,13 @@ def parse_data(self, data):
else:
sensor_data = parse_atc(self, service_data, mac, rssi)
break
elif uuid16 == 0x181B or uuid16 == 0x181D:
elif uuid16 in [0x181B, 0x181D]:
# UUID16 = Body Composition and Weight Scale (used by Mi Scale)
sensor_data = parse_miscale(self, service_data, mac, rssi)
break
elif uuid16 in [0x181C, 0x181E]:
# UUID16 = User Data and Bond Management (used by BLE HA)
sensor_data = parse_ha_ble(self, service_data, uuid16, mac, rssi)
elif uuid16 in [0xAA20, 0xAA21, 0xAA22] and complete_local_name == "ECo":
# UUID16 = Relsib
sensor_data = parse_relsib(self, service_data, mac, rssi)
Expand All @@ -165,8 +169,8 @@ def parse_data(self, data):
sensor_data = parse_switchbot(self, service_data, mac, rssi)
break
elif uuid16 in GATT_CHARACTERISTICS and shortened_local_name == "HA_BLE":
# HA BLE
sensor_data = parse_ha_ble(self, service_data_list, mac, rssi)
# HA BLE legacy (deprecated)
sensor_data = parse_ha_ble_legacy(self, service_data_list, mac, rssi)
break
elif uuid16 == 0x2A6E or uuid16 == 0x2A6F:
# UUID16 = Temperature and Humidity (used by Teltonika)
Expand Down
5 changes: 4 additions & 1 deletion custom_components/ble_monitor/ble_parser/altbeacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

DEVICE_TYPE: Final = "AltBeacon"


def parse_altbeacon(self, data: str, comp_id: int, source_mac: str, rssi: float):
if len(data) >= 27:
uuid = data[6:22]
Expand Down Expand Up @@ -71,8 +72,10 @@ def parse_altbeacon(self, data: str, comp_id: int, source_mac: str, rssi: float)


def to_uuid(uuid: str) -> str:
"""Return formatted UUID"""
return str(UUID(''.join('{:02X}'.format(x) for x in uuid)))


def to_mac(addr: str) -> str:
return ':'.join('{:02x}'.format(x) for x in addr).upper()
"""Return formatted MAC address"""
return ':'.join(f'{i:02X}' for i in addr)
6 changes: 3 additions & 3 deletions custom_components/ble_monitor/ble_parser/atc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


def parse_atc(self, data, source_mac, rssi):
"""Check for adstruc length"""
"""Parse ATC BLE advertisements"""
device_type = "ATC"
msg_length = len(data)
if msg_length == 19:
Expand Down Expand Up @@ -178,5 +178,5 @@ def decrypt_atc(self, data, atc_mac):


def to_mac(addr: int):
"""Convert MAC address."""
return ':'.join('{:02x}'.format(x) for x in addr).upper()
"""Return formatted MAC address"""
return ':'.join(f'{i:02X}' for i in addr)
4 changes: 2 additions & 2 deletions custom_components/ble_monitor/ble_parser/bluemaestro.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ def parse_bluemaestro(self, data, source_mac, rssi):


def to_mac(addr: int):
"""Convert MAC address."""
return ':'.join('{:02x}'.format(x) for x in addr).upper()
"""Return formatted MAC address"""
return ':'.join(f'{i:02X}' for i in addr)
2 changes: 1 addition & 1 deletion custom_components/ble_monitor/ble_parser/bparasite.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def parse_bparasite(self, data, source_mac, rssi):

result.update({
"rssi": rssi,
"mac": ''.join('{:02X}'.format(x) for x in bpara_mac),
"mac": ''.join('{:02X}'.format(x) for x in bpara_mac[:]),
"type": device_type,
"packet": packet_id,
"firmware": firmware,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ble_monitor/ble_parser/brifit.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ def parse_brifit(self, data, source_mac, rssi):


def to_mac(addr: int):
"""Convert MAC address."""
"""Return formatted MAC address"""
return ':'.join(f'{i:02X}' for i in addr)
Loading

0 comments on commit 48e0474

Please sign in to comment.