Coverage for src / bluetooth_sig / types / advertising / result.py: 85%

39 statements  

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

1"""Final composed advertising data result types.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7import msgspec 

8 

9from bluetooth_sig.types.advertising.ad_structures import ( 

10 AdvertisingDataStructures, 

11 ExtendedAdvertisingData, 

12) 

13from bluetooth_sig.types.company import ManufacturerData 

14from bluetooth_sig.types.uuid import BluetoothUUID 

15 

16 

17class AdvertisingData(msgspec.Struct, kw_only=True): 

18 """Complete BLE advertising data with device information and metadata. 

19 

20 Attributes: 

21 raw_data: Raw bytes from the advertising packet 

22 ad_structures: Parsed AD structures organized by category 

23 extended: Extended advertising data (BLE 5.0+) 

24 rssi: Received signal strength indicator in dBm 

25 """ 

26 

27 raw_data: bytes 

28 ad_structures: AdvertisingDataStructures = msgspec.field(default_factory=AdvertisingDataStructures) 

29 extended: ExtendedAdvertisingData = msgspec.field(default_factory=ExtendedAdvertisingData) 

30 rssi: int | None = None 

31 

32 @property 

33 def is_extended_advertising(self) -> bool: 

34 """Check if this advertisement uses extended advertising.""" 

35 return bool(self.extended.extended_payload) or bool(self.extended.auxiliary_packets) 

36 

37 @property 

38 def total_payload_size(self) -> int: 

39 """Get total payload size including extended data.""" 

40 base_size = len(self.raw_data) 

41 if self.extended.extended_payload: 

42 base_size += len(self.extended.extended_payload) 

43 for aux_packet in self.extended.auxiliary_packets: 

44 base_size += len(aux_packet.payload) 

45 return base_size 

46 

47 

48class AdvertisementData(msgspec.Struct, kw_only=True): 

49 """Complete parsed advertisement with PDU structures and interpreted data. 

50 

51 This is the unified result from Device.update_advertisement(), containing 

52 both low-level AD structures and high-level vendor-specific interpretation. 

53 

54 The interpreted_data field is typed as Any to maintain msgspec.Struct compatibility 

55 while supporting generic vendor-specific result types at runtime. 

56 

57 Attributes: 

58 ad_structures: Parsed AD structures (manufacturer_data, service_data, etc.) 

59 interpreted_data: Vendor-specific typed result (e.g., sensor readings), or None 

60 interpreter_name: Name of the interpreter used (e.g., "BTHome", "Xiaomi"), or None 

61 rssi: Received signal strength indicator in dBm 

62 

63 Example:: 

64 # Using connection manager (recommended) 

65 ad_data = BleakConnectionManager.convert_advertisement(bleak_advertisement) 

66 result = device.update_advertisement(ad_data) 

67 

68 # Access low-level AD structures 

69 print(result.ad_structures.core.manufacturer_data) # {0x0499: b'...'} 

70 print(result.ad_structures.properties.flags) 

71 

72 # Access vendor-specific interpreted data 

73 if result.interpreted_data: 

74 print(f"Interpreter: {result.interpreter_name}") 

75 print(f"Temperature: {result.interpreted_data.temperature}") 

76 

77 """ 

78 

79 ad_structures: AdvertisingDataStructures = msgspec.field(default_factory=AdvertisingDataStructures) 

80 interpreted_data: Any = None 

81 interpreter_name: str | None = None 

82 rssi: int | None = None 

83 

84 @property 

85 def manufacturer_data(self) -> dict[int, ManufacturerData]: 

86 """Convenience accessor for manufacturer data (company_id → ManufacturerData).""" 

87 return self.ad_structures.core.manufacturer_data 

88 

89 @property 

90 def service_data(self) -> dict[BluetoothUUID, bytes]: 

91 """Convenience accessor for service data (UUID → payload).""" 

92 return self.ad_structures.core.service_data 

93 

94 @property 

95 def local_name(self) -> str: 

96 """Convenience accessor for device local name.""" 

97 return self.ad_structures.core.local_name 

98 

99 @property 

100 def has_interpretation(self) -> bool: 

101 """Check if vendor-specific interpretation was applied.""" 

102 return self.interpreted_data is not None