Coverage for src / bluetooth_sig / gatt / characteristics / preferred_units.py: 100%

45 statements  

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

1"""Preferred Units characteristic (0x2B46).""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ...registry.uuids.units import units_registry 

8from ...types.gatt_enums import CharacteristicRole 

9from ...types.registry.units import UnitInfo 

10from ...types.uuid import BluetoothUUID 

11from ..context import CharacteristicContext 

12from ..exceptions import InsufficientDataError 

13from .base import BaseCharacteristic 

14from .utils.data_parser import DataParser 

15 

16 

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

18 """Preferred Units data structure.""" 

19 

20 units: list[BluetoothUUID] 

21 

22 

23class PreferredUnitsCharacteristic(BaseCharacteristic[PreferredUnitsData]): 

24 """Preferred Units characteristic (0x2B46). 

25 

26 org.bluetooth.characteristic.preferred_units 

27 

28 The Preferred Units characteristic is the list of units the user prefers. 

29 Each unit is represented by a 16-bit Bluetooth UUID from the Bluetooth SIG units registry. 

30 """ 

31 

32 _manual_role = CharacteristicRole.INFO 

33 # Variable length: minimum 0 bytes (empty list), multiples of 2 bytes (16-bit UUIDs) 

34 min_length = 0 

35 allow_variable_length = True 

36 

37 def _decode_value( 

38 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

39 ) -> PreferredUnitsData: 

40 """Decode Preferred Units from raw bytes. 

41 

42 Args: 

43 data: Raw bytes from BLE characteristic (variable length, multiples of 2) 

44 ctx: Optional context for parsing 

45 validate: Whether to validate ranges (default True) 

46 

47 Returns: 

48 PreferredUnitsData: Parsed preferred units as Bluetooth UUID objects 

49 

50 Raises: 

51 InsufficientDataError: If data length is not a multiple of 2 

52 """ 

53 if len(data) % 2 != 0: 

54 raise InsufficientDataError("Preferred Units", data, len(data) + (len(data) % 2)) 

55 

56 units: list[BluetoothUUID] = [] 

57 for i in range(0, len(data), 2): 

58 unit_value = DataParser.parse_int16(data, i, signed=False) 

59 unit_uuid = BluetoothUUID(unit_value) 

60 units.append(unit_uuid) 

61 

62 return PreferredUnitsData(units=units) 

63 

64 def _encode_value(self, data: PreferredUnitsData) -> bytearray: 

65 """Encode Preferred Units to raw bytes. 

66 

67 Args: 

68 data: PreferredUnitsData to encode 

69 

70 Returns: 

71 bytearray: Encoded bytes 

72 """ 

73 result = bytearray() 

74 for unit_uuid in data.units: 

75 # Extract 16-bit short form value from UUID for encoding 

76 unit_value = int(unit_uuid.short_form, 16) 

77 result.extend(DataParser.encode_int16(unit_value, signed=False)) 

78 return result 

79 

80 def get_units(self, data: PreferredUnitsData) -> list[UnitInfo]: 

81 """Get unit information for the preferred units. 

82 

83 Args: 

84 data: PreferredUnitsData containing unit UUIDs 

85 

86 Returns: 

87 List of UnitInfo objects, with placeholder UnitInfo for unrecognized UUIDs 

88 """ 

89 units: list[UnitInfo] = [] 

90 for unit_uuid in data.units: 

91 unit_info = units_registry.get_unit_info(unit_uuid) 

92 if unit_info: 

93 units.append(unit_info) 

94 else: 

95 # Create a placeholder UnitInfo for unknown units 

96 units.append( 

97 UnitInfo( 

98 uuid=unit_uuid, 

99 name=f"Unknown Unit ({unit_uuid.short_form})", 

100 id=f"unknown.{unit_uuid.short_form.lower()}", 

101 ) 

102 ) 

103 return units 

104 

105 def validate_units(self, data: PreferredUnitsData) -> list[str]: 

106 """Validate that all units in the data are recognized Bluetooth SIG units. 

107 

108 Args: 

109 data: PreferredUnitsData to validate 

110 

111 Returns: 

112 List of validation errors (empty if all units are valid) 

113 """ 

114 errors: list[str] = [] 

115 for i, unit_uuid in enumerate(data.units): 

116 if not units_registry.is_unit_uuid(unit_uuid): 

117 errors.append(f"Unit at index {i} ({unit_uuid.short_form}) is not a recognized Bluetooth SIG unit") 

118 return errors