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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""Electric Current 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
13class ElectricCurrentStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
14 """Data class for electric current statistics."""
16 minimum: float # Minimum current in Amperes
17 maximum: float # Maximum current in Amperes
18 average: float # Average current in Amperes
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 )
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 )
44class ElectricCurrentStatisticsCharacteristic(BaseCharacteristic):
45 """Electric Current Statistics characteristic (0x2AF1).
47 org.bluetooth.characteristic.electric_current_statistics
49 Electric Current Statistics characteristic.
51 Provides statistical current data (min, max, average over time).
52 """
54 # Override since decode_value returns structured ElectricCurrentStatisticsData
55 _manual_value_type: ValueType | str | None = ValueType.DICT
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).
60 Args:
61 data: Raw bytes from the characteristic read.
62 ctx: Optional CharacteristicContext providing surrounding context (may be None).
64 Returns:
65 ElectricCurrentStatisticsData with 'minimum', 'maximum', and 'average' current values in Amperes.
67 Raises:
68 ValueError: If data is insufficient.
70 """
71 if len(data) < 6:
72 raise ValueError("Electric current statistics data must be at least 6 bytes")
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)
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 )
85 def encode_value(self, data: ElectricCurrentStatisticsData) -> bytearray:
86 """Encode electric current statistics value back to bytes.
88 Args:
89 data: ElectricCurrentStatisticsData instance
91 Returns:
92 Encoded bytes representing the current statistics (3x uint16, 0.01 A resolution)
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)
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))
106 return result