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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Service data parser for BLE advertisements.
3Parses service data payloads using registered GATT characteristic classes,
4enabling automatic interpretation of SIG-standard and custom characteristics
5from advertisement service data.
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"""
13from __future__ import annotations
15from typing import Any, ClassVar
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
24class ServiceDataParser:
25 r"""Parser for service data from BLE advertisements.
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.
31 Example::
32 parser = ServiceDataParser()
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
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)
46 """
48 # Cache characteristic instances by UUID for performance
49 _char_cache: ClassVar[dict[str, BaseCharacteristic[Any]]] = {}
51 @classmethod
52 def get_characteristic(cls, uuid: BluetoothUUID) -> BaseCharacteristic[Any] | None:
53 """Get a cached characteristic instance for the given UUID.
55 Args:
56 uuid: The characteristic UUID to look up
58 Returns:
59 Characteristic instance if UUID is registered, None otherwise
61 """
62 normalized = uuid.normalized
63 if normalized in cls._char_cache:
64 return cls._char_cache[normalized]
66 char_instance = CharacteristicRegistry.get_characteristic(uuid)
67 if char_instance is not None:
68 cls._char_cache[normalized] = char_instance
70 return char_instance
72 @classmethod
73 def clear_cache(cls) -> None:
74 """Clear the characteristic instance cache (for testing)."""
75 cls._char_cache.clear()
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.
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
96 Returns:
97 CharacteristicContext populated with device info
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)
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 )
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.
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.
127 Use ctx.validate=False to suppress validation exceptions.
129 Args:
130 service_data: Mapping of service UUID to raw payload bytes
131 ctx: Optional context for parsing
133 Returns:
134 Mapping of UUID to parsed values (only includes recognised UUIDs)
136 Raises:
137 CharacteristicParseError: If parsing fails (when validate=True)
138 SpecialValueDetectedError: If a special value sentinel is detected
140 """
141 results: dict[BluetoothUUID, Any] = {}
143 for uuid, data in service_data.items():
144 char_instance = self.get_characteristic(uuid)
145 if char_instance is None:
146 continue
148 results[uuid] = char_instance.parse_value(data, ctx)
150 return results