Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new HA BLE format #752

Merged
merged 33 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2b2457d
Add new HA BLE format
Ernst79 Mar 2, 2022
186a0b3
Merge branch 'master' into new-HA_BLE
Ernst79 Mar 2, 2022
9701a6e
Add examples
Ernst79 Mar 2, 2022
aeab29e
Fix pressure test
Ernst79 Mar 2, 2022
73d2540
Add opening and switch binary sensors to HA BLE
Ernst79 Mar 3, 2022
9bf8862
udpate test HA BLE
Ernst79 Mar 3, 2022
8a4e698
Fix binary sensors HA BLE
Ernst79 Mar 3, 2022
a33f6c7
Add binary tests
Ernst79 Mar 3, 2022
9068bab
Add tests
Ernst79 Mar 3, 2022
b40367a
Add mac address
Ernst79 Mar 3, 2022
57e44b6
Merge branch 'master' into new-HA_BLE
Ernst79 Mar 4, 2022
f4f6239
Add encryption to HA BLE
Ernst79 Mar 6, 2022
a2ab0c6
Merge branch 'master' into new-HA_BLE
Ernst79 Mar 6, 2022
61e590d
Fix MAC in HA BLE
Ernst79 Mar 6, 2022
3e739a3
Add HA BLE encrypted test
Ernst79 Mar 6, 2022
b30ec86
Fix test
Ernst79 Mar 6, 2022
a1fa35b
Add packet id to HA BLE
Ernst79 Mar 8, 2022
cda85b2
Fix HA BLE test
Ernst79 Mar 8, 2022
59ae51e
Merge branch 'master' into new-HA_BLE
Ernst79 Mar 11, 2022
88c9f51
Automatic sensor adding for HA BLE
Ernst79 Mar 12, 2022
d6a5da7
Fix for wrong encryption key
Ernst79 Mar 12, 2022
2d511d3
fix for not encrypted adv
Ernst79 Mar 12, 2022
ddd1ed6
Update version
Ernst79 Mar 12, 2022
00ced03
Fix for no data error
Ernst79 Mar 13, 2022
6427b60
Fix HA BLE
Ernst79 Mar 13, 2022
095c0d2
Add Inkbird IBS-P01R
Ernst79 Mar 15, 2022
4b10860
Add sensirion SHT40 and miscale update
Ernst79 Mar 15, 2022
8b4d9d8
Fix MiScale test
Ernst79 Mar 15, 2022
db486b4
Fix sensirion SHT40 test
Ernst79 Mar 15, 2022
ca88318
Merge branch 'Mi-Scale-update' into new-HA_BLE
Ernst79 Mar 15, 2022
59bea69
update bparasite
Ernst79 Mar 15, 2022
7a9ed1c
Fix time format error
Ernst79 Mar 17, 2022
0646280
Fix iBBq-6 and MiScale v2
Ernst79 Mar 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading