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

1"""Company identifier types for Bluetooth SIG manufacturer IDs. 

2 

3Provides a unified type that encapsulates both the numeric company ID 

4and its resolved human-readable name from the Bluetooth SIG registry. 

5""" 

6 

7from __future__ import annotations 

8 

9import msgspec 

10 

11from bluetooth_sig.gatt.constants import SIZE_UINT16 

12from bluetooth_sig.registry.company_identifiers import company_identifiers_registry 

13 

14 

15class CompanyIdentifier(msgspec.Struct, kw_only=True, frozen=True): 

16 """Bluetooth SIG company identifier with resolved name. 

17 

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. 

21 

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. 

26 

27 Example:: 

28 # Direct construction 

29 apple = CompanyIdentifier(id=0x004C, name="Apple, Inc.") 

30 

31 # Using factory method (recommended) 

32 apple = CompanyIdentifier.from_id(0x004C) 

33 assert apple.id == 0x004C 

34 assert apple.name == "Apple, Inc." 

35 

36 # Unknown company ID 

37 unknown = CompanyIdentifier.from_id(0xFFFF) 

38 assert unknown.id == 0xFFFF 

39 assert unknown.name == "Unknown (0xFFFF)" 

40 

41 """ 

42 

43 id: int 

44 name: str 

45 

46 @classmethod 

47 def from_id(cls, company_id: int) -> CompanyIdentifier: 

48 """Create CompanyIdentifier from numeric ID with registry lookup. 

49 

50 Args: 

51 company_id: Manufacturer company identifier (e.g., 0x004C for Apple). 

52 

53 Returns: 

54 CompanyIdentifier with resolved name from registry. 

55 

56 Example:: 

57 >>> company = CompanyIdentifier.from_id(0x004C) 

58 >>> company.id 

59 76 

60 >>> company.name 

61 'Apple, Inc.' 

62 

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) 

68 

69 def __str__(self) -> str: 

70 """String representation showing name and hex ID.""" 

71 return f"{self.name} (0x{self.id:04X})" 

72 

73 def __repr__(self) -> str: 

74 """Developer representation.""" 

75 return f"CompanyIdentifier(id=0x{self.id:04X}, name={self.name!r})" 

76 

77 

78class ManufacturerData(msgspec.Struct, kw_only=True, frozen=True): 

79 r"""Manufacturer-specific advertising data. 

80 

81 Attributes: 

82 company: Resolved company identifier with ID and name. 

83 payload: Raw manufacturer-specific data bytes. 

84 

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..." 

90 

91 """ 

92 

93 company: CompanyIdentifier 

94 payload: bytes 

95 

96 @classmethod 

97 def from_bytes(cls, data: bytes) -> ManufacturerData: 

98 """Parse manufacturer data from raw AD structure bytes. 

99 

100 Args: 

101 data: Raw bytes with company ID (little-endian uint16) followed by payload. 

102 

103 Returns: 

104 Parsed ManufacturerData with resolved company info. 

105 

106 Raises: 

107 ValueError: If data is too short to contain company ID. 

108 

109 """ 

110 if len(data) < SIZE_UINT16: 

111 raise ValueError(f"Manufacturer data too short: {len(data)} bytes, need at least {SIZE_UINT16}") 

112 

113 company_id = int.from_bytes(data[:2], byteorder="little", signed=False) 

114 payload = data[2:] 

115 company = CompanyIdentifier.from_id(company_id) 

116 

117 return cls(company=company, payload=payload) 

118 

119 @classmethod 

120 def from_id_and_payload(cls, company_id: int, payload: bytes) -> ManufacturerData: 

121 """Create manufacturer data from company ID and payload. 

122 

123 Args: 

124 company_id: Numeric company identifier. 

125 payload: Raw manufacturer-specific data bytes. 

126 

127 Returns: 

128 ManufacturerData with resolved company info. 

129 

130 """ 

131 company = CompanyIdentifier.from_id(company_id) 

132 return cls(company=company, payload=payload) 

133 

134 def to_bytes(self) -> bytes: 

135 """Encode manufacturer data to wire format. 

136 

137 Returns: 

138 Encoded bytes: company ID (little-endian uint16) + payload. 

139 

140 """ 

141 return self.company.id.to_bytes(SIZE_UINT16, byteorder="little") + self.payload