Coverage for src / bluetooth_sig / gatt / characteristics / temperature_8_in_a_period_of_day.py: 91%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Temperature 8 in a Period of Day characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import SINT8_MAX, SINT8_MIN, UINT8_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12_TEMPERATURE_8_RESOLUTION = 0.5 # Temperature 8: M=1, d=0, b=-1 -> 0.5 C 

13_TIME_DECIHOUR_RESOLUTION = 0.1 # Time Decihour 8: M=1, d=-1, b=0 -> 0.1 hr 

14 

15 

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

17 """Data class for temperature 8 in a period of day. 

18 

19 Temperature in degrees Celsius (0.5 C resolution) with a 

20 time-of-day range (0.1 hr resolution). 

21 """ 

22 

23 temperature: float # Temperature in C (0.5 C 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) 

26 

27 def __post_init__(self) -> None: 

28 """Validate data fields.""" 

29 min_temp = SINT8_MIN * _TEMPERATURE_8_RESOLUTION 

30 max_temp = SINT8_MAX * _TEMPERATURE_8_RESOLUTION 

31 if not min_temp <= self.temperature <= max_temp: 

32 raise ValueError(f"Temperature {self.temperature} C is outside valid range ({min_temp} to {max_temp})") 

33 max_time = UINT8_MAX * _TIME_DECIHOUR_RESOLUTION 

34 for name, val in [("start_time", self.start_time), ("end_time", self.end_time)]: 

35 if not 0.0 <= val <= max_time: 

36 raise ValueError(f"{name} {val} hr is outside valid range (0.0 to {max_time})") 

37 

38 

39class Temperature8InAPeriodOfDayCharacteristic( 

40 BaseCharacteristic[Temperature8InAPeriodOfDayData], 

41): 

42 """Temperature 8 in a Period of Day characteristic (0x2B0E). 

43 

44 org.bluetooth.characteristic.temperature_8_in_a_period_of_day 

45 

46 Represents a temperature reading within a time-of-day range. Fields: 

47 Temperature 8 (sint8, 0.5 C), start time (uint8, 0.1 hr), 

48 end time (uint8, 0.1 hr). 

49 """ 

50 

51 expected_length: int = 3 # sint8 + 2 x uint8 

52 min_length: int = 3 

53 

54 def _decode_value( 

55 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

56 ) -> Temperature8InAPeriodOfDayData: 

57 """Parse temperature 8 in a period of day. 

58 

59 Args: 

60 data: Raw bytes (3 bytes). 

61 ctx: Optional CharacteristicContext. 

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

63 

64 Returns: 

65 Temperature8InAPeriodOfDayData. 

66 

67 """ 

68 temp_raw = DataParser.parse_int8(data, 0, signed=True) 

69 start_raw = DataParser.parse_int8(data, 1, signed=False) 

70 end_raw = DataParser.parse_int8(data, 2, signed=False) 

71 

72 return Temperature8InAPeriodOfDayData( 

73 temperature=temp_raw * _TEMPERATURE_8_RESOLUTION, 

74 start_time=start_raw * _TIME_DECIHOUR_RESOLUTION, 

75 end_time=end_raw * _TIME_DECIHOUR_RESOLUTION, 

76 ) 

77 

78 def _encode_value(self, data: Temperature8InAPeriodOfDayData) -> bytearray: 

79 """Encode temperature 8 in a period of day. 

80 

81 Args: 

82 data: Temperature8InAPeriodOfDayData instance. 

83 

84 Returns: 

85 Encoded bytes (3 bytes). 

86 

87 """ 

88 temp_raw = round(data.temperature / _TEMPERATURE_8_RESOLUTION) 

89 start_raw = round(data.start_time / _TIME_DECIHOUR_RESOLUTION) 

90 end_raw = round(data.end_time / _TIME_DECIHOUR_RESOLUTION) 

91 

92 if not SINT8_MIN <= temp_raw <= SINT8_MAX: 

93 raise ValueError(f"Temperature raw {temp_raw} exceeds sint8 range") 

94 for name, value in [("start_time", start_raw), ("end_time", end_raw)]: 

95 if not 0 <= value <= UINT8_MAX: 

96 raise ValueError(f"{name} raw value {value} exceeds uint8 range") 

97 

98 result = bytearray() 

99 result.extend(DataParser.encode_int8(temp_raw, signed=True)) 

100 result.extend(DataParser.encode_int8(start_raw, signed=False)) 

101 result.extend(DataParser.encode_int8(end_raw, signed=False)) 

102 return result