Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions custom_components/ble_monitor/ble_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ def parse_advertisement(
# UUID16 = HolyIOT
sensor_data = parse_holyiot(self, service_data, mac)
break
elif uuid16 == 0x180A and len(service_data) == 13:
# UUID16 = Device Information service (used by HolyIOT B1)
sensor_data = parse_holyiot(self, service_data, mac)
break
elif uuid16 in [0xAA20, 0xAA21, 0xAA22] and local_name == "ECo":
# UUID16 = Relsib
sensor_data = parse_relsib(self, service_data, mac)
Expand Down
19 changes: 19 additions & 0 deletions custom_components/ble_monitor/ble_parser/holyiot.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ def parse_holyiot(self, data: str, mac: bytes):
"battery": batt
}
)
elif msg_length == 13:
# HolyIOT B1 (B1-B, B1-S, holyiot-25055 board) status frame on
# 0x180A service data: <frame type> <MAC> <settings byte> <battery %>
# frame type: 0x01 = beacon mode, 0x02 = iBeacon mode (observed);
# the settings byte changes with adv interval/tx power config (0x04 -> 0xf8),
# meaning not fully reverse-engineered - not parsed.
device_type = "HolyIOT Beacon"
if data[4] not in (0x01, 0x02):
return None
holyiot_mac = data[5:11]
if holyiot_mac != mac:
_LOGGER.debug(
"HolyIOT MAC address doesn't match data MAC address. Data: %s with source mac: %s and HolyIOT mac: %s",
data.hex(),
mac,
holyiot_mac,
)
return None
result.update({"battery": data[12]})
else:
if self.report_unknown == "HolyIOT":
_LOGGER.info(
Expand Down
1 change: 1 addition & 0 deletions custom_components/ble_monitor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2458,6 +2458,7 @@ class BLEMonitorBinarySensorEntityDescription(
'BeckettLinkPro' : 'Beckett',
'GenisysGas' : 'Beckett',
'HHCCJCY10' : 'HHCC',
'HolyIOT Beacon' : 'HolyIOT',
'HolyIOT BLE tracker' : 'HolyIOT',
'JTYJGD03MI' : 'Honeywell',
'Supramatic E4 BS' : 'Hörmann',
Expand Down
58 changes: 58 additions & 0 deletions custom_components/ble_monitor/test/test_holyiot.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,61 @@ def test_holyiot_button(self):
assert sensor_msg["battery"] == 100
assert sensor_msg["button"] == "toggle"
assert sensor_msg["rssi"] == -52

def test_holyiot_b1_b_battery(self):
"""Test HolyIOT parser for B1-B beacon (real capture)."""
data_string = "043e49020102016eee4ce590e13d0201061affffff0215fda50693a4e24fb1afcfc6eb07647825271b4cb9c9110942312d420000000000000000000000000c160a1801e190e54cee6e0461b9"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "HolyIOT"
assert sensor_msg["type"] == "HolyIOT Beacon"
assert sensor_msg["mac"] == "E190E54CEE6E"
assert sensor_msg["packet"] == "no packet id"
assert sensor_msg["data"]
assert sensor_msg["battery"] == 97
assert sensor_msg["rssi"] == -71

def test_holyiot_b1_s_battery(self):
"""Test HolyIOT parser for B1-S beacon (real capture)."""
data_string = "043e4902010201bd483e2817fb3d0201061affffff0215fda50693a4e24fb1afcfc6eb07647825271b4cb9c9110942312d530000000000000000000000000c160a1801fb17283e48bd0462b9"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "HolyIOT"
assert sensor_msg["type"] == "HolyIOT Beacon"
assert sensor_msg["mac"] == "FB17283E48BD"
assert sensor_msg["packet"] == "no packet id"
assert sensor_msg["data"]
assert sensor_msg["battery"] == 98
assert sensor_msg["rssi"] == -71

def test_holyiot_beacon_ibeacon_mode(self):
"""Test HolyIOT parser for the iBeacon-mode frame (type byte 0x02, real capture)."""
data_string = "043e49020102016eee4ce590e13d0201061affffff02156b1e44d99f274c8eb3f10a5d82e49c7300010001c9110942312d420000000000000000000000000c160a1802e190e54cee6ef85fb0"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "HolyIOT"
assert sensor_msg["type"] == "HolyIOT Beacon"
assert sensor_msg["mac"] == "E190E54CEE6E"
assert sensor_msg["packet"] == "no packet id"
assert sensor_msg["data"]
assert sensor_msg["battery"] == 95
assert sensor_msg["rssi"] == -80

def test_holyiot_b1_mac_mismatch(self):
"""Test HolyIOT parser drops B1 frames with a foreign MAC in payload."""
data_string = "043e49020102016eee4ce590e13d0201061affffff0215fda50693a4e24fb1afcfc6eb07647825271b4cb9c9110942312d420000000000000000000000000c160a1801e090e54cee6e0461b9"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg is None
1 change: 1 addition & 0 deletions docs/_devices/HolyIOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ encryption_key: true
custom_firmware:
notes:
- Supported measurement types depend on the model.
- New-generation HolyIOT beacons (e.g. B1-B, B1-S; managed by the NexBeacon pro app) broadcast their battery level. Button events (single, double, triple, long press) are only sent over a BLE connection (Nordic UART service) and cannot be captured from advertisements.
- After each button press, the sensor state shows 'Toggle'. It will return to `no press` after the time set with the [reset_timer](configuration_params#reset_timer) option.
---