Coverage for src / bluetooth_sig / gatt / characteristics / voltage_statistics.py: 91%
64 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Voltage Statistics characteristic implementation."""
3from __future__ import annotations
5import math
7import msgspec
9from ..constants import UINT8_MAX, UINT16_MAX
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
14_VOLTAGE_RESOLUTION = 1 / 64.0 # 1/64 V per raw unit
15_MAX_VOLTAGE = UINT16_MAX * _VOLTAGE_RESOLUTION # ~1023.98 V
16_TIME_EXP_BASE = 1.1
17_TIME_EXP_OFFSET = 64
20def _decode_time_exponential(raw: int) -> float:
21 """Decode Time Exponential 8 raw value to seconds."""
22 if raw == 0:
23 return 0.0
24 return _TIME_EXP_BASE ** (raw - _TIME_EXP_OFFSET)
27def _encode_time_exponential(seconds: float) -> int:
28 """Encode seconds to Time Exponential 8 raw value."""
29 if seconds <= 0.0:
30 return 0
31 n = round(math.log(seconds) / math.log(_TIME_EXP_BASE) + _TIME_EXP_OFFSET)
32 return max(1, min(n, 0xFD))
35class VoltageStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
36 """Data class for voltage statistics.
38 Five fields per spec: Average (uint16, 1/64 V), Standard Deviation
39 (uint16, 1/64 V), Minimum (uint16, 1/64 V), Maximum (uint16, 1/64 V),
40 and Sensing Duration (uint8, Time Exponential 8).
41 """
43 average: float # Average voltage in Volts
44 standard_deviation: float # Standard deviation in Volts
45 minimum: float # Minimum voltage in Volts
46 maximum: float # Maximum voltage in Volts
47 sensing_duration: float # Sensing duration in seconds (exponential encoding)
49 def __post_init__(self) -> None:
50 """Validate voltage statistics data."""
51 if self.minimum > self.maximum:
52 raise ValueError(f"Minimum voltage {self.minimum} V cannot be greater than maximum {self.maximum} V")
54 for name, voltage in [
55 ("average", self.average),
56 ("standard_deviation", self.standard_deviation),
57 ("minimum", self.minimum),
58 ("maximum", self.maximum),
59 ]:
60 if not 0.0 <= voltage <= _MAX_VOLTAGE:
61 raise ValueError(
62 f"{name.capitalize()} voltage {voltage} V is outside valid range (0.0 to {_MAX_VOLTAGE:.2f} V)"
63 )
64 if self.sensing_duration < 0.0:
65 raise ValueError(f"Sensing duration {self.sensing_duration} s cannot be negative")
68class VoltageStatisticsCharacteristic(BaseCharacteristic[VoltageStatisticsData]):
69 """Voltage Statistics characteristic (0x2B1A).
71 org.bluetooth.characteristic.voltage_statistics
73 Statistics for Voltage measurements: average, standard deviation,
74 minimum, maximum (all uint16, 1/64 V), and sensing duration
75 (Time Exponential 8).
76 """
78 expected_length: int = 9 # 4x uint16 + uint8
79 min_length: int = 9
81 def _decode_value(
82 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
83 ) -> VoltageStatisticsData:
84 """Parse voltage statistics data (4x uint16 + uint8)."""
85 avg_raw = DataParser.parse_int16(data, 0, signed=False)
86 std_raw = DataParser.parse_int16(data, 2, signed=False)
87 min_raw = DataParser.parse_int16(data, 4, signed=False)
88 max_raw = DataParser.parse_int16(data, 6, signed=False)
89 dur_raw = DataParser.parse_int8(data, 8, signed=False)
91 return VoltageStatisticsData(
92 average=avg_raw / 64.0,
93 standard_deviation=std_raw / 64.0,
94 minimum=min_raw / 64.0,
95 maximum=max_raw / 64.0,
96 sensing_duration=_decode_time_exponential(dur_raw),
97 )
99 def _encode_value(self, data: VoltageStatisticsData) -> bytearray:
100 """Encode voltage statistics value back to bytes."""
101 if not isinstance(data, VoltageStatisticsData):
102 raise TypeError(f"Voltage statistics data must be a VoltageStatisticsData, got {type(data).__name__}")
104 avg_raw = round(data.average * 64)
105 std_raw = round(data.standard_deviation * 64)
106 min_raw = round(data.minimum * 64)
107 max_raw = round(data.maximum * 64)
108 dur_raw = _encode_time_exponential(data.sensing_duration)
110 # pylint: disable=duplicate-code
111 for name, value in [
112 ("average", avg_raw),
113 ("standard_deviation", std_raw),
114 ("minimum", min_raw),
115 ("maximum", max_raw),
116 ]:
117 if not 0 <= value <= UINT16_MAX:
118 raise ValueError(f"Voltage {name} value {value} exceeds uint16 range")
119 if not 0 <= dur_raw <= UINT8_MAX:
120 raise ValueError(f"Duration raw {dur_raw} exceeds uint8 range")
122 result = bytearray()
123 result.extend(DataParser.encode_int16(avg_raw, signed=False))
124 result.extend(DataParser.encode_int16(std_raw, signed=False))
125 result.extend(DataParser.encode_int16(min_raw, signed=False))
126 result.extend(DataParser.encode_int16(max_raw, signed=False))
127 result.extend(DataParser.encode_int8(dur_raw, signed=False))
129 return result