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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Preferred Units characteristic (0x2B46)."""
3from __future__ import annotations
5import msgspec
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
16class PreferredUnitsData(msgspec.Struct, frozen=True, kw_only=True):
17 """Preferred Units data structure."""
19 units: list[BluetoothUUID]
22class PreferredUnitsCharacteristic(BaseCharacteristic[PreferredUnitsData]):
23 """Preferred Units characteristic (0x2B46).
25 org.bluetooth.characteristic.preferred_units
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 """
31 # Variable length: minimum 0 bytes (empty list), multiples of 2 bytes (16-bit UUIDs)
32 min_length = 0
33 allow_variable_length = True
35 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> PreferredUnitsData:
36 """Decode Preferred Units from raw bytes.
38 Args:
39 data: Raw bytes from BLE characteristic (variable length, multiples of 2)
40 ctx: Optional context for parsing
42 Returns:
43 PreferredUnitsData: Parsed preferred units as Bluetooth UUID objects
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))
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)
57 return PreferredUnitsData(units=units)
59 def _encode_value(self, data: PreferredUnitsData) -> bytearray:
60 """Encode Preferred Units to raw bytes.
62 Args:
63 data: PreferredUnitsData to encode
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
74 def get_units(self, data: PreferredUnitsData) -> list[UnitInfo]:
75 """Get unit information for the preferred units.
77 Args:
78 data: PreferredUnitsData containing unit UUIDs
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
99 def validate_units(self, data: PreferredUnitsData) -> list[str]:
100 """Validate that all units in the data are recognized Bluetooth SIG units.
102 Args:
103 data: PreferredUnitsData to validate
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