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

37 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +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 

11 

12 

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

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

15 

16 minimum: float # Minimum current in Amperes 

17 maximum: float # Maximum current in Amperes 

18 average: float # Average current in Amperes 

19 

20 def __post_init__(self) -> None: 

21 """Validate current statistics data.""" 

22 # Validate logical order 

23 if self.minimum > self.maximum: 

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

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

26 raise ValueError( 

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

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

29 ) 

30 

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

32 max_current_value = UINT16_MAX * 0.01 

33 for name, current in [ 

34 ("minimum", self.minimum), 

35 ("maximum", self.maximum), 

36 ("average", self.average), 

37 ]: 

38 if not 0.0 <= current <= max_current_value: 

39 raise ValueError( 

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

41 ) 

42 

43 

44class ElectricCurrentStatisticsCharacteristic(BaseCharacteristic): 

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

46 

47 org.bluetooth.characteristic.electric_current_statistics 

48 

49 Electric Current Statistics characteristic. 

50 

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

52 """ 

53 

54 # Override since decode_value returns structured ElectricCurrentStatisticsData 

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

56 

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

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

59 

60 Args: 

61 data: Raw bytes from the characteristic read. 

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

63 

64 Returns: 

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

66 

67 Raises: 

68 ValueError: If data is insufficient. 

69 

70 """ 

71 if len(data) < 6: 

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

73 

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

75 min_current_raw = int.from_bytes(data[:2], byteorder="little", signed=False) 

76 max_current_raw = int.from_bytes(data[2:4], byteorder="little", signed=False) 

77 avg_current_raw = int.from_bytes(data[4:6], byteorder="little", signed=False) 

78 

79 return ElectricCurrentStatisticsData( 

80 minimum=min_current_raw * 0.01, 

81 maximum=max_current_raw * 0.01, 

82 average=avg_current_raw * 0.01, 

83 ) 

84 

85 def encode_value(self, data: ElectricCurrentStatisticsData) -> bytearray: 

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

87 

88 Args: 

89 data: ElectricCurrentStatisticsData instance 

90 

91 Returns: 

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

93 

94 """ 

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

96 min_current_raw = round(data.minimum * 100) 

97 max_current_raw = round(data.maximum * 100) 

98 avg_current_raw = round(data.average * 100) 

99 

100 # Encode as 3 uint16 values (little endian) 

101 result = bytearray() 

102 result.extend(min_current_raw.to_bytes(2, byteorder="little", signed=False)) 

103 result.extend(max_current_raw.to_bytes(2, byteorder="little", signed=False)) 

104 result.extend(avg_current_raw.to_bytes(2, byteorder="little", signed=False)) 

105 

106 return result