Coverage for src / bluetooth_sig / gatt / characteristics / electric_current_statistics.py: 90%

39 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""Electric Current 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 ElectricCurrentStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

15 """Data class for electric current statistics.""" 

16 

17 minimum: float # Minimum current in Amperes 

18 maximum: float # Maximum current in Amperes 

19 average: float # Average current in Amperes 

20 

21 def __post_init__(self) -> None: 

22 """Validate current statistics data.""" 

23 # Validate logical order 

24 if self.minimum > self.maximum: 

25 raise ValueError(f"Minimum current {self.minimum} A cannot be greater than maximum {self.maximum} A") 

26 if not self.minimum <= self.average <= self.maximum: 

27 raise ValueError( 

28 f"Average current {self.average} A must be between " 

29 f"minimum {self.minimum} A and maximum {self.maximum} A" 

30 ) 

31 

32 # Validate range for uint16 with 0.01 A resolution (0 to 655.35 A) 

33 max_current_value = UINT16_MAX * 0.01 

34 for name, current in [ 

35 ("minimum", self.minimum), 

36 ("maximum", self.maximum), 

37 ("average", self.average), 

38 ]: 

39 if not 0.0 <= current <= max_current_value: 

40 raise ValueError( 

41 f"{name.capitalize()} current {current} A is outside valid range (0.0 to {max_current_value} A)" 

42 ) 

43 

44 

45class ElectricCurrentStatisticsCharacteristic(BaseCharacteristic[ElectricCurrentStatisticsData]): 

46 """Electric Current Statistics characteristic (0x2AF1). 

47 

48 org.bluetooth.characteristic.electric_current_statistics 

49 

50 Electric Current Statistics characteristic. 

51 

52 Provides statistical current data (min, max, average over time). 

53 """ 

54 

55 # Validation attributes 

56 expected_length: int = 6 # 3x uint16 

57 

58 # Override since decode_value returns structured ElectricCurrentStatisticsData 

59 _manual_value_type: ValueType | str | None = ValueType.DICT 

60 

61 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> ElectricCurrentStatisticsData: 

62 """Parse current statistics data (3x uint16 in units of 0.01 A). 

63 

64 Args: 

65 data: Raw bytes from the characteristic read. 

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

67 

68 Returns: 

69 ElectricCurrentStatisticsData with 'minimum', 'maximum', and 'average' current values in Amperes. 

70 

71 Raises: 

72 ValueError: If data is insufficient. 

73 

74 """ 

75 if len(data) < 6: 

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

77 

78 # Convert 3x uint16 (little endian) to current statistics in Amperes 

79 min_current_raw = DataParser.parse_int16(data, 0, signed=False) 

80 max_current_raw = DataParser.parse_int16(data, 2, signed=False) 

81 avg_current_raw = DataParser.parse_int16(data, 4, signed=False) 

82 

83 return ElectricCurrentStatisticsData( 

84 minimum=min_current_raw * 0.01, 

85 maximum=max_current_raw * 0.01, 

86 average=avg_current_raw * 0.01, 

87 ) 

88 

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

90 """Encode electric current statistics value back to bytes. 

91 

92 Args: 

93 data: ElectricCurrentStatisticsData instance 

94 

95 Returns: 

96 Encoded bytes representing the current statistics (3x uint16, 0.01 A resolution) 

97 

98 """ 

99 # Convert Amperes to raw values (multiply by 100 for 0.01 A resolution) 

100 min_current_raw = round(data.minimum * 100) 

101 max_current_raw = round(data.maximum * 100) 

102 avg_current_raw = round(data.average * 100) 

103 

104 # Encode as 3 uint16 values (little endian) 

105 result = bytearray() 

106 result.extend(DataParser.encode_int16(min_current_raw, signed=False)) 

107 result.extend(DataParser.encode_int16(max_current_raw, signed=False)) 

108 result.extend(DataParser.encode_int16(avg_current_raw, signed=False)) 

109 

110 return result