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

43 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

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

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

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

8from ...types.registry.units import UnitInfo 

9from ...types.uuid import BluetoothUUID 

10from ..context import CharacteristicContext 

11from ..exceptions import InsufficientDataError 

12from .base import BaseCharacteristic 

13from .utils.data_parser import DataParser 

14 

15 

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

17 """Preferred Units data structure.""" 

18 

19 units: list[BluetoothUUID] 

20 

21 

22class PreferredUnitsCharacteristic(BaseCharacteristic[PreferredUnitsData]): 

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

24 

25 org.bluetooth.characteristic.preferred_units 

26 

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

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

29 """ 

30 

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

32 min_length = 0 

33 allow_variable_length = True 

34 

35 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> PreferredUnitsData: 

36 """Decode Preferred Units from raw bytes. 

37 

38 Args: 

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

40 ctx: Optional context for parsing 

41 

42 Returns: 

43 PreferredUnitsData: Parsed preferred units as Bluetooth UUID objects 

44 

45 Raises: 

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

47 """ 

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

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

50 

51 units: list[BluetoothUUID] = [] 

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

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

54 unit_uuid = BluetoothUUID(unit_value) 

55 units.append(unit_uuid) 

56 

57 return PreferredUnitsData(units=units) 

58 

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

60 """Encode Preferred Units to raw bytes. 

61 

62 Args: 

63 data: PreferredUnitsData to encode 

64 

65 Returns: 

66 bytearray: Encoded bytes 

67 """ 

68 result = bytearray() 

69 for unit_uuid in data.units: 

70 unit_value = unit_uuid.int_value 

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

72 return result 

73 

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

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

76 

77 Args: 

78 data: PreferredUnitsData containing unit UUIDs 

79 

80 Returns: 

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

82 """ 

83 units: list[UnitInfo] = [] 

84 for unit_uuid in data.units: 

85 unit_info = units_registry.get_unit_info(unit_uuid) 

86 if unit_info: 

87 units.append(unit_info) 

88 else: 

89 # Create a placeholder UnitInfo for unknown units 

90 units.append( 

91 UnitInfo( 

92 uuid=unit_uuid, 

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

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

95 ) 

96 ) 

97 return units 

98 

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

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

101 

102 Args: 

103 data: PreferredUnitsData to validate 

104 

105 Returns: 

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

107 """ 

108 errors: list[str] = [] 

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

110 if not units_registry.is_unit_uuid(unit_uuid): 

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

112 return errors