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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
|
from __future__ import annotations
from dataclasses import dataclass
import logging
import struct
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from aiohomekit.controller.abstract import AbstractDescription
from aiohomekit.model.categories import Categories
from aiohomekit.model.status_flags import StatusFlags
UNPACK_HHBB = struct.Struct("<HHBB").unpack
UNPACK_HH = struct.Struct("<HH").unpack
APPLE_MANUFACTURER_ID = 76
HOMEKIT_ADVERTISEMENT_TYPE = 0x06
HOMEKIT_ENCRYPTED_NOTIFICATION_TYPE = 0x11
logger = logging.getLogger(__name__)
@dataclass
class HomeKitAdvertisement(AbstractDescription):
setup_hash: bytes
address: str
state_num: int
@classmethod
def from_manufacturer_data(
cls, name: str, address: str, manufacturer_data: dict[int, bytes]
) -> HomeKitAdvertisement:
if not (data := manufacturer_data.get(APPLE_MANUFACTURER_ID)):
raise ValueError("Not an Apple device")
if data[0] != HOMEKIT_ADVERTISEMENT_TYPE:
raise ValueError("Not a HomeKit device")
sf = data[2]
device_id = ":".join(
data[3:9].hex()[0 + i : 2 + i] for i in range(0, 12, 2)
).lower()
acid, gsn, cn, cv = UNPACK_HHBB(data[9:15])
sh = data[15:19]
return cls(
name=name,
id=device_id,
category=Categories(acid),
status_flags=StatusFlags(sf),
config_num=cn,
state_num=gsn,
setup_hash=sh,
address=address,
)
@classmethod
def from_cache(
cls, address: str, id: str, config_num: int, state_num: int
) -> HomeKitAdvertisement:
"""Create a HomeKitAdvertisement from a cache entry."""
return cls(
name=address,
id=id,
category=Categories(0),
status_flags=StatusFlags(0),
config_num=config_num,
state_num=state_num,
setup_hash=b"",
address=address,
)
@classmethod
def from_advertisement(
cls, device: BLEDevice, advertisement_data: AdvertisementData
) -> HomeKitAdvertisement:
if not (mfr_data := advertisement_data.manufacturer_data):
raise ValueError("No manufacturer data")
return cls.from_manufacturer_data(device.name, device.address, mfr_data)
@dataclass
class HomeKitEncryptedNotification:
name: str
address: str
id: str
advertising_identifier: bytes
encrypted_payload: bytes
@classmethod
def from_manufacturer_data(
cls, name: str, address: str, manufacturer_data: dict[int, bytes]
) -> HomeKitAdvertisement:
if not (data := manufacturer_data.get(APPLE_MANUFACTURER_ID)):
raise ValueError("Not an Apple device")
if data[0] != HOMEKIT_ENCRYPTED_NOTIFICATION_TYPE:
raise ValueError("Not a HomeKit encrypted notification")
advertising_identifier = data[2:8]
device_id = ":".join(
advertising_identifier.hex()[0 + i : 2 + i] for i in range(0, 12, 2)
).lower()
encrypted_payload = data[8:]
return cls(
name=name,
id=device_id,
address=address,
advertising_identifier=advertising_identifier,
encrypted_payload=encrypted_payload,
)
@classmethod
def from_advertisement(
cls, device: BLEDevice, advertisement_data: AdvertisementData
) -> HomeKitAdvertisement:
if not (mfr_data := advertisement_data.manufacturer_data):
raise ValueError("No manufacturer data")
return cls.from_manufacturer_data(device.name, device.address, mfr_data)
|