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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Preferred Units characteristic (0x2B46)."""
3from __future__ import annotations
5import msgspec
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
17class PreferredUnitsData(msgspec.Struct, frozen=True, kw_only=True):
18 """Preferred Units data structure."""
20 units: list[BluetoothUUID]
23class PreferredUnitsCharacteristic(BaseCharacteristic[PreferredUnitsData]):
24 """Preferred Units characteristic (0x2B46).
26 org.bluetooth.characteristic.preferred_units
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 """
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
37 def _decode_value(
38 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
39 ) -> PreferredUnitsData:
40 """Decode Preferred Units from raw bytes.
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)
47 Returns:
48 PreferredUnitsData: Parsed preferred units as Bluetooth UUID objects
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))
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)
62 return PreferredUnitsData(units=units)
64 def _encode_value(self, data: PreferredUnitsData) -> bytearray:
65 """Encode Preferred Units to raw bytes.
67 Args:
68 data: PreferredUnitsData to encode
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
80 def get_units(self, data: PreferredUnitsData) -> list[UnitInfo]:
81 """Get unit information for the preferred units.
83 Args:
84 data: PreferredUnitsData containing unit UUIDs
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
105 def validate_units(self, data: PreferredUnitsData) -> list[str]:
106 """Validate that all units in the data are recognized Bluetooth SIG units.
108 Args:
109 data: PreferredUnitsData to validate
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