1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
|
"""Shelly BLE manufacturer data parsing."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
LOGGER = logging.getLogger(__name__)
ALLTERCO_MFID = 0x0BA9
# Block types in manufacturer data
BLOCK_TYPE_FLAGS = 0x01
BLOCK_TYPE_MAC = 0x0A
BLOCK_TYPE_MODEL = 0x0B
# Shelly bitfield flags (block type 0x01)
FLAG_DISCOVERABLE = 1 << 0
FLAG_AUTH_ENABLED = 1 << 1
FLAG_RPC_OVER_BLE_ENABLED = 1 << 2
FLAG_BUZZER_ENABLED = 1 << 3
FLAG_IN_PAIRING_MODE = 1 << 4
def parse_shelly_manufacturer_data(
manufacturer_data: dict[int, bytes],
) -> dict[str, int | str] | None:
"""Parse Shelly manufacturer data from BLE advertisement.
Args:
manufacturer_data: Manufacturer data from BLE advertisement
Returns:
Dict with parsed data (flags, mac, model) or None if invalid
"""
if ALLTERCO_MFID not in manufacturer_data:
return None
data = manufacturer_data[ALLTERCO_MFID]
if len(data) < 1:
return None
result: dict[str, int | str] = {}
offset = 0
# Parse blocks
while offset < len(data):
block_type = data[offset]
offset += 1
if block_type == BLOCK_TYPE_FLAGS:
# 2 bytes of flags
if offset + 2 > len(data):
break
flags = int.from_bytes(data[offset : offset + 2], byteorder="little")
result["flags"] = flags
offset += 2
elif block_type == BLOCK_TYPE_MAC:
# 6 bytes MAC address
if offset + 6 > len(data):
break
mac_bytes = data[offset : offset + 6]
# Format as standard MAC address
result["mac"] = ":".join(f"{b:02X}" for b in mac_bytes)
offset += 6
elif block_type == BLOCK_TYPE_MODEL:
# 2 bytes model ID
if offset + 2 > len(data):
break
model_id = int.from_bytes(data[offset : offset + 2], byteorder="little")
result["model_id"] = model_id
offset += 2
else:
# Unknown block type - can't continue parsing
LOGGER.debug("Unknown block type in manufacturer data: 0x%02X", block_type)
break
return result if result else None
def has_rpc_over_ble(manufacturer_data: dict[int, bytes]) -> bool:
"""Check if device has RPC-over-BLE enabled.
Args:
manufacturer_data: Manufacturer data from BLE advertisement
Returns:
True if RPC-over-BLE is enabled
"""
parsed = parse_shelly_manufacturer_data(manufacturer_data)
if not parsed or "flags" not in parsed:
return False
flags = parsed["flags"]
if TYPE_CHECKING:
assert isinstance(flags, int)
return bool(flags & FLAG_RPC_OVER_BLE_ENABLED)
|