Coverage for src / bluetooth_sig / gatt / characteristics / voltage_specification.py: 85%
39 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"""Voltage Specification characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ...types.gatt_enums import ValueType
8from ..constants import UINT16_MAX
9from ..context import CharacteristicContext
10from .base import BaseCharacteristic
11from .utils import DataParser
14class VoltageSpecificationData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
15 """Data class for voltage specification."""
17 minimum: float # Minimum voltage in Volts
18 maximum: float # Maximum voltage in Volts
20 def __post_init__(self) -> None:
21 """Validate voltage specification data."""
22 if self.minimum > self.maximum:
23 raise ValueError(f"Minimum voltage {self.minimum} V cannot be greater than maximum {self.maximum} V")
25 # Validate range for uint16 with 1/64 V resolution (0 to ~1024 V)
26 max_voltage_value = UINT16_MAX / 64.0 # ~1024 V
27 if not 0.0 <= self.minimum <= max_voltage_value:
28 raise ValueError(
29 f"Minimum voltage {self.minimum} V is outside valid range (0.0 to {max_voltage_value:.2f} V)"
30 )
31 if not 0.0 <= self.maximum <= max_voltage_value:
32 raise ValueError(
33 f"Maximum voltage {self.maximum} V is outside valid range (0.0 to {max_voltage_value:.2f} V)"
34 )
37class VoltageSpecificationCharacteristic(BaseCharacteristic[VoltageSpecificationData]):
38 """Voltage Specification characteristic (0x2B19).
40 org.bluetooth.characteristic.voltage_specification
42 Voltage Specification characteristic.
44 Specifies minimum and maximum voltage values for electrical
45 specifications.
46 """
48 min_length = 4
49 # Override since decode_value returns structured VoltageSpecificationData
50 _manual_value_type: ValueType | str | None = ValueType.DICT
52 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> VoltageSpecificationData:
53 """Parse voltage specification data (2x uint16 in units of 1/64 V).
55 Args:
56 data: Raw bytes from the characteristic read.
57 ctx: Optional CharacteristicContext providing surrounding context (may be None).
59 Returns:
60 VoltageSpecificationData with 'minimum' and 'maximum' voltage specification values in Volts.
62 Raises:
63 ValueError: If data is insufficient.
65 """
66 if len(data) < 4:
67 raise ValueError("Voltage specification data must be at least 4 bytes")
69 # Convert 2x uint16 (little endian) to voltage specification in Volts
70 min_voltage_raw = DataParser.parse_int16(data, 0, signed=False)
71 max_voltage_raw = DataParser.parse_int16(data, 2, signed=False)
73 return VoltageSpecificationData(minimum=min_voltage_raw / 64.0, maximum=max_voltage_raw / 64.0)
75 def _encode_value(self, data: VoltageSpecificationData) -> bytearray:
76 """Encode voltage specification value back to bytes.
78 Args:
79 data: VoltageSpecificationData instance with 'minimum' and 'maximum' voltage values in Volts
81 Returns:
82 Encoded bytes representing the voltage specification (2x uint16, 1/64 V resolution)
84 """
85 if not isinstance(data, VoltageSpecificationData):
86 raise TypeError(f"Voltage specification data must be a VoltageSpecificationData, got {type(data).__name__}")
87 # Convert Volts to raw values (multiply by 64 for 1/64 V resolution)
88 min_voltage_raw = round(data.minimum * 64)
89 max_voltage_raw = round(data.maximum * 64)
91 # Validate range for uint16 (0 to UINT16_MAX)
92 # pylint: disable=duplicate-code
93 # NOTE: This uint16 validation and encoding pattern is shared with VoltageStatisticsCharacteristic.
94 # Both characteristics encode voltage values using the same 1/64V resolution and uint16 little-endian format
95 # per Bluetooth SIG spec. Consolidation not practical as each has different field structures.
96 for name, value in [("minimum", min_voltage_raw), ("maximum", max_voltage_raw)]:
97 if not 0 <= value <= UINT16_MAX:
98 raise ValueError(f"Voltage {name} value {value} exceeds uint16 range")
100 # Encode as 2 uint16 values (little endian)
101 result = bytearray()
102 result.extend(DataParser.encode_int16(min_voltage_raw, signed=False))
103 result.extend(DataParser.encode_int16(max_voltage_raw, signed=False))
105 return result