Coverage for src / bluetooth_sig / gatt / characteristics / voltage_statistics.py: 86%
44 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 Statistics 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 VoltageStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
15 """Data class for voltage statistics."""
17 minimum: float # Minimum voltage in Volts
18 maximum: float # Maximum voltage in Volts
19 average: float # Average voltage in Volts
21 def __post_init__(self) -> None:
22 """Validate voltage statistics data."""
23 # Validate logical order
24 if self.minimum > self.maximum:
25 raise ValueError(f"Minimum voltage {self.minimum} V cannot be greater than maximum {self.maximum} V")
26 if not self.minimum <= self.average <= self.maximum:
27 raise ValueError(
28 f"Average voltage {self.average} V must be between "
29 f"minimum {self.minimum} V and maximum {self.maximum} V"
30 )
32 # Validate range for uint16 with 1/64 V resolution (0 to ~1024 V)
33 max_voltage_value = UINT16_MAX / 64.0 # ~1024 V
34 for name, voltage in [
35 ("minimum", self.minimum),
36 ("maximum", self.maximum),
37 ("average", self.average),
38 ]:
39 if not 0.0 <= voltage <= max_voltage_value:
40 raise ValueError(
41 f"{name.capitalize()} voltage {voltage} V is outside valid range (0.0 to {max_voltage_value:.2f} V)"
42 )
45class VoltageStatisticsCharacteristic(BaseCharacteristic[VoltageStatisticsData]):
46 """Voltage Statistics characteristic (0x2B1A).
48 org.bluetooth.characteristic.voltage_statistics
50 Voltage Statistics characteristic.
52 Provides statistical voltage data over time.
53 """
55 # Override since decode_value returns structured VoltageStatisticsData
56 _manual_value_type: ValueType | str | None = ValueType.DICT
57 expected_length: int = 6 # Minimum(2) + Maximum(2) + Average(2)
59 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> VoltageStatisticsData:
60 """Parse voltage statistics data (3x uint16 in units of 1/64 V).
62 Args:
63 data: Raw bytes from the characteristic read.
64 ctx: Optional CharacteristicContext providing surrounding context (may be None).
66 Returns:
67 VoltageStatisticsData with 'minimum', 'maximum', and 'average' voltage values in Volts.
69 # `ctx` is intentionally unused for this characteristic; mark as used for linters.
70 del ctx
71 Raises:
72 ValueError: If data is insufficient.
74 """
75 if len(data) < 6:
76 raise ValueError("Voltage statistics data must be at least 6 bytes")
78 # Convert 3x uint16 (little endian) to voltage statistics in Volts
79 min_voltage_raw = DataParser.parse_int16(data, 0, signed=False)
80 max_voltage_raw = DataParser.parse_int16(data, 2, signed=False)
81 avg_voltage_raw = DataParser.parse_int16(data, 4, signed=False)
83 return VoltageStatisticsData(
84 minimum=min_voltage_raw / 64.0,
85 maximum=max_voltage_raw / 64.0,
86 average=avg_voltage_raw / 64.0,
87 )
89 def _encode_value(self, data: VoltageStatisticsData) -> bytearray:
90 """Encode voltage statistics value back to bytes.
92 Args:
93 data: VoltageStatisticsData instance with 'minimum', 'maximum', and 'average' voltage values in Volts
95 Returns:
96 Encoded bytes representing the voltage statistics (3x uint16, 1/64 V resolution)
98 """
99 if not isinstance(data, VoltageStatisticsData):
100 raise TypeError(f"Voltage statistics data must be a VoltageStatisticsData, got {type(data).__name__}")
102 # Convert Volts to raw values (multiply by 64 for 1/64 V resolution)
103 min_voltage_raw = round(data.minimum * 64)
104 max_voltage_raw = round(data.maximum * 64)
105 avg_voltage_raw = round(data.average * 64)
107 # Validate range for uint16 (0 to UINT16_MAX)
108 # pylint: disable=duplicate-code
109 # NOTE: This uint16 validation and encoding pattern is shared with VoltageSpecificationCharacteristic.
110 # Both characteristics encode voltage values using the same 1/64V resolution and uint16 little-endian format
111 # per Bluetooth SIG spec. Consolidation not practical as each has different field structures (2 vs 3 values).
112 for name, value in [
113 ("minimum", min_voltage_raw),
114 ("maximum", max_voltage_raw),
115 ("average", avg_voltage_raw),
116 ]:
117 if not 0 <= value <= UINT16_MAX:
118 raise ValueError(f"Voltage {name} value {value} exceeds uint16 range")
120 # Encode as 3 uint16 values (little endian)
121 result = bytearray()
122 result.extend(DataParser.encode_int16(min_voltage_raw, signed=False))
123 result.extend(DataParser.encode_int16(max_voltage_raw, signed=False))
124 result.extend(DataParser.encode_int16(avg_voltage_raw, signed=False))
126 return result