File: manufacturer_data.py

package info (click to toggle)
python-aiohomekit 3.2.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,620 kB
  • sloc: python: 16,560; sh: 14; makefile: 8
file content (123 lines) | stat: -rw-r--r-- 3,747 bytes parent folder | download
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)