diff --git a/custom_components/ble_monitor/ble_parser/__init__.py b/custom_components/ble_monitor/ble_parser/__init__.py
index 6753916d..0a0538fe 100644
--- a/custom_components/ble_monitor/ble_parser/__init__.py
+++ b/custom_components/ble_monitor/ble_parser/__init__.py
@@ -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)
diff --git a/custom_components/ble_monitor/ble_parser/holyiot.py b/custom_components/ble_monitor/ble_parser/holyiot.py
index 97552a55..ca16dd5a 100644
--- a/custom_components/ble_monitor/ble_parser/holyiot.py
+++ b/custom_components/ble_monitor/ble_parser/holyiot.py
@@ -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: 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(
diff --git a/custom_components/ble_monitor/const.py b/custom_components/ble_monitor/const.py
index 67efa9d8..906e11bb 100644
--- a/custom_components/ble_monitor/const.py
+++ b/custom_components/ble_monitor/const.py
@@ -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',
diff --git a/custom_components/ble_monitor/test/test_holyiot.py b/custom_components/ble_monitor/test/test_holyiot.py
index 45a4a337..399cf991 100644
--- a/custom_components/ble_monitor/test/test_holyiot.py
+++ b/custom_components/ble_monitor/test/test_holyiot.py
@@ -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
diff --git a/docs/_devices/HolyIOT.md b/docs/_devices/HolyIOT.md
index ec078ea1..c753f10d 100644
--- a/docs/_devices/HolyIOT.md
+++ b/docs/_devices/HolyIOT.md
@@ -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.
---