Coverage for src / bluetooth_sig / gatt / characteristics / energy_in_a_period_of_day.py: 95%
37 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Energy in a Period of Day characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import UINT8_MAX, UINT24_MAX
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
12_TIME_DECIHOUR_RESOLUTION = 0.1 # Time Decihour 8: M=1, d=-1, b=0 -> 0.1 hr
13# Energy: M=1, d=0, b=0 -> 1 kWh per raw uint24 unit
16class EnergyInAPeriodOfDayData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for energy in a period of day.
19 Energy value in kWh (integer, uint24) with a time-of-day range
20 (0.1 hr resolution).
21 """
23 energy: int # Energy in kWh (integer, uint24)
24 start_time: float # Start time in hours (0.1 hr resolution)
25 end_time: float # End time in hours (0.1 hr resolution)
27 def __post_init__(self) -> None:
28 """Validate data fields."""
29 if not 0 <= self.energy <= UINT24_MAX:
30 raise ValueError(f"Energy {self.energy} kWh is outside valid range (0 to {UINT24_MAX})")
31 max_time = UINT8_MAX * _TIME_DECIHOUR_RESOLUTION
32 for name, val in [("start_time", self.start_time), ("end_time", self.end_time)]:
33 if not 0.0 <= val <= max_time:
34 raise ValueError(f"{name} {val} hr is outside valid range (0.0 to {max_time})")
37class EnergyInAPeriodOfDayCharacteristic(
38 BaseCharacteristic[EnergyInAPeriodOfDayData],
39):
40 """Energy in a Period of Day characteristic (0x2AF3).
42 org.bluetooth.characteristic.energy_in_a_period_of_day
44 Represents an energy measurement within a time-of-day range. Fields:
45 Energy (uint24, 1 kWh), start time (uint8, 0.1 hr),
46 end time (uint8, 0.1 hr).
47 """
49 expected_length: int = 5 # uint24 + 2 x uint8
50 min_length: int = 5
52 def _decode_value(
53 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
54 ) -> EnergyInAPeriodOfDayData:
55 """Parse energy in a period of day.
57 Args:
58 data: Raw bytes (5 bytes).
59 ctx: Optional CharacteristicContext.
60 validate: Whether to validate ranges (default True).
62 Returns:
63 EnergyInAPeriodOfDayData.
65 """
66 energy = DataParser.parse_int24(data, 0, signed=False)
67 start_raw = DataParser.parse_int8(data, 3, signed=False)
68 end_raw = DataParser.parse_int8(data, 4, signed=False)
70 return EnergyInAPeriodOfDayData(
71 energy=energy,
72 start_time=start_raw * _TIME_DECIHOUR_RESOLUTION,
73 end_time=end_raw * _TIME_DECIHOUR_RESOLUTION,
74 )
76 def _encode_value(self, data: EnergyInAPeriodOfDayData) -> bytearray:
77 """Encode energy in a period of day.
79 Args:
80 data: EnergyInAPeriodOfDayData instance.
82 Returns:
83 Encoded bytes (5 bytes).
85 """
86 start_raw = round(data.start_time / _TIME_DECIHOUR_RESOLUTION)
87 end_raw = round(data.end_time / _TIME_DECIHOUR_RESOLUTION)
89 for name, value in [("start_time", start_raw), ("end_time", end_raw)]:
90 if not 0 <= value <= UINT8_MAX:
91 raise ValueError(f"{name} raw value {value} exceeds uint8 range")
93 result = bytearray()
94 result.extend(DataParser.encode_int24(data.energy, signed=False))
95 result.extend(DataParser.encode_int8(start_raw, signed=False))
96 result.extend(DataParser.encode_int8(end_raw, signed=False))
97 return result