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

1"""Voltage Statistics characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ...types.gatt_enums import ValueType 

8from ..constants import UINT16_MAX 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

14class VoltageStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

15 """Data class for voltage statistics.""" 

16 

17 minimum: float # Minimum voltage in Volts 

18 maximum: float # Maximum voltage in Volts 

19 average: float # Average voltage in Volts 

20 

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 ) 

31 

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 ) 

43 

44 

45class VoltageStatisticsCharacteristic(BaseCharacteristic[VoltageStatisticsData]): 

46 """Voltage Statistics characteristic (0x2B1A). 

47 

48 org.bluetooth.characteristic.voltage_statistics 

49 

50 Voltage Statistics characteristic. 

51 

52 Provides statistical voltage data over time. 

53 """ 

54 

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) 

58 

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). 

61 

62 Args: 

63 data: Raw bytes from the characteristic read. 

64 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

65 

66 Returns: 

67 VoltageStatisticsData with 'minimum', 'maximum', and 'average' voltage values in Volts. 

68 

69 # `ctx` is intentionally unused for this characteristic; mark as used for linters. 

70 del ctx 

71 Raises: 

72 ValueError: If data is insufficient. 

73 

74 """ 

75 if len(data) < 6: 

76 raise ValueError("Voltage statistics data must be at least 6 bytes") 

77 

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) 

82 

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 ) 

88 

89 def _encode_value(self, data: VoltageStatisticsData) -> bytearray: 

90 """Encode voltage statistics value back to bytes. 

91 

92 Args: 

93 data: VoltageStatisticsData instance with 'minimum', 'maximum', and 'average' voltage values in Volts 

94 

95 Returns: 

96 Encoded bytes representing the voltage statistics (3x uint16, 1/64 V resolution) 

97 

98 """ 

99 if not isinstance(data, VoltageStatisticsData): 

100 raise TypeError(f"Voltage statistics data must be a VoltageStatisticsData, got {type(data).__name__}") 

101 

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) 

106 

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") 

119 

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)) 

125 

126 return result