Coverage for src / bluetooth_sig / advertising / service_data_parser.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Service data parser for BLE advertisements. 

2 

3Parses service data payloads using registered GATT characteristic classes, 

4enabling automatic interpretation of SIG-standard and custom characteristics 

5from advertisement service data. 

6 

7This bridges the advertising and GATT layers: 

8- Advertisement Service Data (UUID → raw bytes) is extracted by AdvertisingPDUParser 

9- This module maps UUIDs to characteristic classes via CharacteristicRegistry 

10- Characteristic classes decode bytes using their standard parse_value() method 

11""" 

12 

13from __future__ import annotations 

14 

15from typing import Any, ClassVar 

16 

17from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic 

18from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry 

19from bluetooth_sig.types.company import ManufacturerData 

20from bluetooth_sig.types.context import CharacteristicContext, DeviceInfo 

21from bluetooth_sig.types.uuid import BluetoothUUID 

22 

23 

24class ServiceDataParser: 

25 r"""Parser for service data from BLE advertisements. 

26 

27 Uses registered GATT characteristic classes to decode service data payloads. 

28 Both SIG-standard and custom-registered characteristics are supported via 

29 the unified CharacteristicRegistry. 

30 

31 Example:: 

32 parser = ServiceDataParser() 

33 

34 # Parse all service data from an advertisement 

35 service_data = {BluetoothUUID("2A6E"): b"\xe8\x03"} # Temperature 

36 results = parser.parse(service_data) 

37 # results = {BluetoothUUID("2A6E"): 10.0} # Parsed temperature in °C 

38 

39 # Parse with context 

40 ctx = ServiceDataParser.build_context( 

41 device_name="MySensor", 

42 device_address="AA:BB:CC:DD:EE:FF", 

43 ) 

44 results = parser.parse(service_data, ctx) 

45 

46 """ 

47 

48 # Cache characteristic instances by UUID for performance 

49 _char_cache: ClassVar[dict[str, BaseCharacteristic[Any]]] = {} 

50 

51 @classmethod 

52 def get_characteristic(cls, uuid: BluetoothUUID) -> BaseCharacteristic[Any] | None: 

53 """Get a cached characteristic instance for the given UUID. 

54 

55 Args: 

56 uuid: The characteristic UUID to look up 

57 

58 Returns: 

59 Characteristic instance if UUID is registered, None otherwise 

60 

61 """ 

62 normalized = uuid.normalized 

63 if normalized in cls._char_cache: 

64 return cls._char_cache[normalized] 

65 

66 char_instance = CharacteristicRegistry.get_characteristic(uuid) 

67 if char_instance is not None: 

68 cls._char_cache[normalized] = char_instance 

69 

70 return char_instance 

71 

72 @classmethod 

73 def clear_cache(cls) -> None: 

74 """Clear the characteristic instance cache (for testing).""" 

75 cls._char_cache.clear() 

76 

77 @staticmethod 

78 def build_context( # pylint: disable=too-many-arguments,too-many-positional-arguments # Context builder needs all ad fields 

79 device_name: str = "", 

80 device_address: str = "", 

81 manufacturer_data: dict[int, bytes] | None = None, 

82 service_uuids: list[BluetoothUUID] | None = None, 

83 advertisement: bytes = b"", 

84 validate: bool = True, 

85 ) -> CharacteristicContext: 

86 """Build a CharacteristicContext from advertisement information. 

87 

88 Args: 

89 device_name: Device local name from advertisement 

90 device_address: Device MAC address 

91 manufacturer_data: Manufacturer data from advertisement 

92 service_uuids: Service UUIDs from advertisement 

93 advertisement: Raw advertisement bytes 

94 validate: Whether to perform validation during parsing 

95 

96 Returns: 

97 CharacteristicContext populated with device info 

98 

99 """ 

100 mfr_data_converted: dict[int, ManufacturerData] = {} 

101 if manufacturer_data: 

102 for company_id, payload in manufacturer_data.items(): 

103 mfr_data_converted[company_id] = ManufacturerData.from_id_and_payload(company_id, payload) 

104 

105 return CharacteristicContext( 

106 device_info=DeviceInfo( 

107 name=device_name, 

108 address=device_address, 

109 manufacturer_data=mfr_data_converted, 

110 service_uuids=service_uuids or [], 

111 ), 

112 advertisement=advertisement, 

113 validate=validate, 

114 ) 

115 

116 def parse( 

117 self, 

118 service_data: dict[BluetoothUUID, bytes], 

119 ctx: CharacteristicContext | None = None, 

120 ) -> dict[BluetoothUUID, Any]: 

121 """Parse service data using registered characteristic classes. 

122 

123 Iterates through service data entries, looking up each UUID in the 

124 CharacteristicRegistry. For recognised UUIDs, calls parse_value() 

125 to decode the payload. Unrecognised UUIDs are skipped. 

126 

127 Use ctx.validate=False to suppress validation exceptions. 

128 

129 Args: 

130 service_data: Mapping of service UUID to raw payload bytes 

131 ctx: Optional context for parsing 

132 

133 Returns: 

134 Mapping of UUID to parsed values (only includes recognised UUIDs) 

135 

136 Raises: 

137 CharacteristicParseError: If parsing fails (when validate=True) 

138 SpecialValueDetectedError: If a special value sentinel is detected 

139 

140 """ 

141 results: dict[BluetoothUUID, Any] = {} 

142 

143 for uuid, data in service_data.items(): 

144 char_instance = self.get_characteristic(uuid) 

145 if char_instance is None: 

146 continue 

147 

148 results[uuid] = char_instance.parse_value(data, ctx) 

149 

150 return results