Coverage for src / bluetooth_sig / gatt / characteristics / relative_value_in_a_period_of_day.py: 93%
41 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"""Relative Value in a Period of Day characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import UINT8_MAX
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
12_PERCENTAGE_RESOLUTION = 0.5 # Percentage 8: M=1, d=0, b=-1 -> 0.5%
13_TIME_DECIHOUR_RESOLUTION = 0.1 # Time Decihour 8: M=1, d=-1, b=0 -> 0.1 hr
16class RelativeValueInAPeriodOfDayData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for relative value in a period of day.
19 Combines a percentage (0.5% resolution) with a time-of-day range
20 (start/end in hours, 0.1 hr resolution).
21 """
23 relative_value: float # Percentage (0.5% resolution)
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 max_pct = UINT8_MAX * _PERCENTAGE_RESOLUTION
30 if not 0.0 <= self.relative_value <= max_pct:
31 raise ValueError(f"Relative value {self.relative_value}% is outside valid range (0.0 to {max_pct})")
32 max_time = UINT8_MAX * _TIME_DECIHOUR_RESOLUTION
33 for name, val in [("start_time", self.start_time), ("end_time", self.end_time)]:
34 if not 0.0 <= val <= max_time:
35 raise ValueError(f"{name} {val} hr is outside valid range (0.0 to {max_time})")
38class RelativeValueInAPeriodOfDayCharacteristic(
39 BaseCharacteristic[RelativeValueInAPeriodOfDayData],
40):
41 """Relative Value in a Period of Day characteristic (0x2B0B).
43 org.bluetooth.characteristic.relative_value_in_a_period_of_day
45 Represents a relative value within a period of the day. Fields:
46 Percentage 8 (uint8, 0.5%), start time (uint8, 0.1 hr),
47 end time (uint8, 0.1 hr).
48 """
50 expected_length: int = 3 # 3 x uint8
51 min_length: int = 3
52 expected_type = RelativeValueInAPeriodOfDayData
54 def _decode_value(
55 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
56 ) -> RelativeValueInAPeriodOfDayData:
57 """Parse relative value in a period of day.
59 Args:
60 data: Raw bytes (3 bytes).
61 ctx: Optional CharacteristicContext.
62 validate: Whether to validate ranges (default True).
64 Returns:
65 RelativeValueInAPeriodOfDayData.
67 """
68 pct_raw = DataParser.parse_int8(data, 0, signed=False)
69 start_raw = DataParser.parse_int8(data, 1, signed=False)
70 end_raw = DataParser.parse_int8(data, 2, signed=False)
72 return RelativeValueInAPeriodOfDayData(
73 relative_value=pct_raw * _PERCENTAGE_RESOLUTION,
74 start_time=start_raw * _TIME_DECIHOUR_RESOLUTION,
75 end_time=end_raw * _TIME_DECIHOUR_RESOLUTION,
76 )
78 def _encode_value(self, data: RelativeValueInAPeriodOfDayData) -> bytearray:
79 """Encode relative value in a period of day.
81 Args:
82 data: RelativeValueInAPeriodOfDayData instance.
84 Returns:
85 Encoded bytes (3 bytes).
87 """
88 pct_raw = round(data.relative_value / _PERCENTAGE_RESOLUTION)
89 start_raw = round(data.start_time / _TIME_DECIHOUR_RESOLUTION)
90 end_raw = round(data.end_time / _TIME_DECIHOUR_RESOLUTION)
92 for name, value in [
93 ("percentage", pct_raw),
94 ("start_time", start_raw),
95 ("end_time", end_raw),
96 ]:
97 if not 0 <= value <= UINT8_MAX:
98 raise ValueError(f"{name} raw value {value} exceeds uint8 range")
100 result = bytearray()
101 result.extend(DataParser.encode_int8(pct_raw, signed=False))
102 result.extend(DataParser.encode_int8(start_raw, signed=False))
103 result.extend(DataParser.encode_int8(end_raw, signed=False))
104 return result