Coverage for src / bluetooth_sig / types / company.py: 91%
34 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Company identifier types for Bluetooth SIG manufacturer IDs.
3Provides a unified type that encapsulates both the numeric company ID
4and its resolved human-readable name from the Bluetooth SIG registry.
5"""
7from __future__ import annotations
9import msgspec
11from bluetooth_sig.gatt.constants import SIZE_UINT16
12from bluetooth_sig.registry.company_identifiers import company_identifiers_registry
15class CompanyIdentifier(msgspec.Struct, kw_only=True, frozen=True):
16 """Bluetooth SIG company identifier with resolved name.
18 Encapsulates both the numeric company ID and its resolved name,
19 providing a single source of truth for manufacturer identification
20 in advertising data.
22 Attributes:
23 id: Numeric company identifier (16-bit unsigned integer).
24 name: Resolved company name from Bluetooth SIG registry.
25 Falls back to hex format if not found in registry.
27 Example::
28 # Direct construction
29 apple = CompanyIdentifier(id=0x004C, name="Apple, Inc.")
31 # Using factory method (recommended)
32 apple = CompanyIdentifier.from_id(0x004C)
33 assert apple.id == 0x004C
34 assert apple.name == "Apple, Inc."
36 # Unknown company ID
37 unknown = CompanyIdentifier.from_id(0xFFFF)
38 assert unknown.id == 0xFFFF
39 assert unknown.name == "Unknown (0xFFFF)"
41 """
43 id: int
44 name: str
46 @classmethod
47 def from_id(cls, company_id: int) -> CompanyIdentifier:
48 """Create CompanyIdentifier from numeric ID with registry lookup.
50 Args:
51 company_id: Manufacturer company identifier (e.g., 0x004C for Apple).
53 Returns:
54 CompanyIdentifier with resolved name from registry.
56 Example::
57 >>> company = CompanyIdentifier.from_id(0x004C)
58 >>> company.id
59 76
60 >>> company.name
61 'Apple, Inc.'
63 """
64 name = company_identifiers_registry.get_company_name(company_id)
65 if not name:
66 name = f"Unknown (0x{company_id:04X})"
67 return cls(id=company_id, name=name)
69 def __str__(self) -> str:
70 """String representation showing name and hex ID."""
71 return f"{self.name} (0x{self.id:04X})"
73 def __repr__(self) -> str:
74 """Developer representation."""
75 return f"CompanyIdentifier(id=0x{self.id:04X}, name={self.name!r})"
78class ManufacturerData(msgspec.Struct, kw_only=True, frozen=True):
79 r"""Manufacturer-specific advertising data.
81 Attributes:
82 company: Resolved company identifier with ID and name.
83 payload: Raw manufacturer-specific data bytes.
85 Example::
86 # Parse from raw bytes
87 mfr_data = ManufacturerData.from_bytes(b"\x4c\x00\x02\x15...")
88 print(mfr_data.company.name) # "Apple, Inc."
89 print(mfr_data.payload.hex()) # "0215..."
91 """
93 company: CompanyIdentifier
94 payload: bytes
96 @classmethod
97 def from_bytes(cls, data: bytes) -> ManufacturerData:
98 """Parse manufacturer data from raw AD structure bytes.
100 Args:
101 data: Raw bytes with company ID (little-endian uint16) followed by payload.
103 Returns:
104 Parsed ManufacturerData with resolved company info.
106 Raises:
107 ValueError: If data is too short to contain company ID.
109 """
110 if len(data) < SIZE_UINT16:
111 raise ValueError(f"Manufacturer data too short: {len(data)} bytes, need at least {SIZE_UINT16}")
113 company_id = int.from_bytes(data[:2], byteorder="little", signed=False)
114 payload = data[2:]
115 company = CompanyIdentifier.from_id(company_id)
117 return cls(company=company, payload=payload)
119 @classmethod
120 def from_id_and_payload(cls, company_id: int, payload: bytes) -> ManufacturerData:
121 """Create manufacturer data from company ID and payload.
123 Args:
124 company_id: Numeric company identifier.
125 payload: Raw manufacturer-specific data bytes.
127 Returns:
128 ManufacturerData with resolved company info.
130 """
131 company = CompanyIdentifier.from_id(company_id)
132 return cls(company=company, payload=payload)
134 def to_bytes(self) -> bytes:
135 """Encode manufacturer data to wire format.
137 Returns:
138 Encoded bytes: company ID (little-endian uint16) + payload.
140 """
141 return self.company.id.to_bytes(SIZE_UINT16, byteorder="little") + self.payload