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

1"""Energy in a Period of Day characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT8_MAX, UINT24_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

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 

14 

15 

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. 

18 

19 Energy value in kWh (integer, uint24) with a time-of-day range 

20 (0.1 hr resolution). 

21 """ 

22 

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) 

26 

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

35 

36 

37class EnergyInAPeriodOfDayCharacteristic( 

38 BaseCharacteristic[EnergyInAPeriodOfDayData], 

39): 

40 """Energy in a Period of Day characteristic (0x2AF3). 

41 

42 org.bluetooth.characteristic.energy_in_a_period_of_day 

43 

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

48 

49 expected_length: int = 5 # uint24 + 2 x uint8 

50 min_length: int = 5 

51 

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. 

56 

57 Args: 

58 data: Raw bytes (5 bytes). 

59 ctx: Optional CharacteristicContext. 

60 validate: Whether to validate ranges (default True). 

61 

62 Returns: 

63 EnergyInAPeriodOfDayData. 

64 

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) 

69 

70 return EnergyInAPeriodOfDayData( 

71 energy=energy, 

72 start_time=start_raw * _TIME_DECIHOUR_RESOLUTION, 

73 end_time=end_raw * _TIME_DECIHOUR_RESOLUTION, 

74 ) 

75 

76 def _encode_value(self, data: EnergyInAPeriodOfDayData) -> bytearray: 

77 """Encode energy in a period of day. 

78 

79 Args: 

80 data: EnergyInAPeriodOfDayData instance. 

81 

82 Returns: 

83 Encoded bytes (5 bytes). 

84 

85 """ 

86 start_raw = round(data.start_time / _TIME_DECIHOUR_RESOLUTION) 

87 end_raw = round(data.end_time / _TIME_DECIHOUR_RESOLUTION) 

88 

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

92 

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