Coverage for src / bluetooth_sig / gatt / characteristics / temperature_8_statistics.py: 95%
61 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"""Temperature 8 Statistics characteristic implementation."""
3from __future__ import annotations
5import math
7import msgspec
9from ..constants import SINT8_MAX, SINT8_MIN, UINT8_MAX
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
14_TEMPERATURE_8_RESOLUTION = 0.5 # Temperature 8: M=1, d=0, b=-1 -> 0.5 C
15_TIME_EXP_BASE = 1.1
16_TIME_EXP_OFFSET = 64
19def _decode_time_exponential(raw: int) -> float:
20 """Decode Time Exponential 8 raw value to seconds."""
21 if raw == 0:
22 return 0.0
23 return _TIME_EXP_BASE ** (raw - _TIME_EXP_OFFSET)
26def _encode_time_exponential(seconds: float) -> int:
27 """Encode seconds to Time Exponential 8 raw value."""
28 if seconds <= 0.0:
29 return 0
30 n = round(math.log(seconds) / math.log(_TIME_EXP_BASE) + _TIME_EXP_OFFSET)
31 return max(1, min(n, 0xFD))
34class Temperature8StatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
35 """Data class for temperature 8 statistics.
37 Four temperature values (0.5 C resolution) and a sensing duration
38 encoded as Time Exponential 8 (seconds).
39 """
41 average: float # Average temperature in C
42 standard_deviation: float # Standard deviation in C
43 minimum: float # Minimum temperature in C
44 maximum: float # Maximum temperature in C
45 sensing_duration: float # Sensing duration in seconds (exponential encoding)
47 def __post_init__(self) -> None:
48 """Validate data fields."""
49 min_temp = SINT8_MIN * _TEMPERATURE_8_RESOLUTION
50 max_temp = SINT8_MAX * _TEMPERATURE_8_RESOLUTION
51 for name, val in [
52 ("average", self.average),
53 ("standard_deviation", self.standard_deviation),
54 ("minimum", self.minimum),
55 ("maximum", self.maximum),
56 ]:
57 if not min_temp <= val <= max_temp:
58 raise ValueError(f"{name} {val} C is outside valid range ({min_temp} to {max_temp})")
59 if self.sensing_duration < 0.0:
60 raise ValueError(f"Sensing duration {self.sensing_duration} s cannot be negative")
63class Temperature8StatisticsCharacteristic(
64 BaseCharacteristic[Temperature8StatisticsData],
65):
66 """Temperature 8 Statistics characteristic (0x2B0F).
68 org.bluetooth.characteristic.temperature_8_statistics
70 Statistics for Temperature 8 measurements: average, standard deviation,
71 minimum, maximum (all sint8, 0.5 C), and sensing duration
72 (Time Exponential 8).
73 """
75 expected_length: int = 5 # 4 x sint8 + uint8
76 min_length: int = 5
78 def _decode_value(
79 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
80 ) -> Temperature8StatisticsData:
81 """Parse temperature 8 statistics.
83 Args:
84 data: Raw bytes (5 bytes).
85 ctx: Optional CharacteristicContext.
86 validate: Whether to validate ranges (default True).
88 Returns:
89 Temperature8StatisticsData.
91 """
92 avg_raw = DataParser.parse_int8(data, 0, signed=True)
93 std_raw = DataParser.parse_int8(data, 1, signed=True)
94 min_raw = DataParser.parse_int8(data, 2, signed=True)
95 max_raw = DataParser.parse_int8(data, 3, signed=True)
96 dur_raw = DataParser.parse_int8(data, 4, signed=False)
98 return Temperature8StatisticsData(
99 average=avg_raw * _TEMPERATURE_8_RESOLUTION,
100 standard_deviation=std_raw * _TEMPERATURE_8_RESOLUTION,
101 minimum=min_raw * _TEMPERATURE_8_RESOLUTION,
102 maximum=max_raw * _TEMPERATURE_8_RESOLUTION,
103 sensing_duration=_decode_time_exponential(dur_raw),
104 )
106 def _encode_value(self, data: Temperature8StatisticsData) -> bytearray:
107 """Encode temperature 8 statistics.
109 Args:
110 data: Temperature8StatisticsData instance.
112 Returns:
113 Encoded bytes (5 bytes).
115 """
116 avg_raw = round(data.average / _TEMPERATURE_8_RESOLUTION)
117 std_raw = round(data.standard_deviation / _TEMPERATURE_8_RESOLUTION)
118 min_raw = round(data.minimum / _TEMPERATURE_8_RESOLUTION)
119 max_raw = round(data.maximum / _TEMPERATURE_8_RESOLUTION)
120 dur_raw = _encode_time_exponential(data.sensing_duration)
122 for name, value in [
123 ("average", avg_raw),
124 ("standard_deviation", std_raw),
125 ("minimum", min_raw),
126 ("maximum", max_raw),
127 ]:
128 if not SINT8_MIN <= value <= SINT8_MAX:
129 raise ValueError(f"{name} raw {value} exceeds sint8 range")
130 if not 0 <= dur_raw <= UINT8_MAX:
131 raise ValueError(f"Duration raw {dur_raw} exceeds uint8 range")
133 result = bytearray()
134 result.extend(DataParser.encode_int8(avg_raw, signed=True))
135 result.extend(DataParser.encode_int8(std_raw, signed=True))
136 result.extend(DataParser.encode_int8(min_raw, signed=True))
137 result.extend(DataParser.encode_int8(max_raw, signed=True))
138 result.extend(DataParser.encode_int8(dur_raw, signed=False))
139 return result