Coverage for src / bluetooth_sig / gatt / characteristics / temperature_range.py: 92%
36 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 Range characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import SINT16_MAX, SINT16_MIN, TEMPERATURE_RESOLUTION
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
13class TemperatureRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
14 """Data class for temperature range.
16 Each value is a temperature in degrees Celsius with 0.01°C resolution.
17 """
19 minimum: float # Minimum temperature in °C
20 maximum: float # Maximum temperature in °C
22 def __post_init__(self) -> None:
23 """Validate temperature range data."""
24 if self.minimum > self.maximum:
25 raise ValueError(f"Minimum temperature {self.minimum} °C cannot be greater than maximum {self.maximum} °C")
26 temp_min = SINT16_MIN * TEMPERATURE_RESOLUTION
27 temp_max = SINT16_MAX * TEMPERATURE_RESOLUTION
28 for name, val in [("minimum", self.minimum), ("maximum", self.maximum)]:
29 if not temp_min <= val <= temp_max:
30 raise ValueError(
31 f"{name.capitalize()} temperature {val} °C is outside valid range ({temp_min} to {temp_max})"
32 )
35class TemperatureRangeCharacteristic(BaseCharacteristic[TemperatureRangeData]):
36 """Temperature Range characteristic (0x2B10).
38 org.bluetooth.characteristic.temperature_range
40 Represents a temperature range as a pair of Temperature values.
41 Each field is a sint16, M=1 d=-2 b=0 (resolution 0.01°C).
42 """
44 # Validation attributes
45 expected_length: int = 4 # 2 x sint16
46 min_length: int = 4
48 def _decode_value(
49 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
50 ) -> TemperatureRangeData:
51 """Parse temperature range data (2 x sint16, 0.01°C resolution).
53 Args:
54 data: Raw bytes from the characteristic read.
55 ctx: Optional CharacteristicContext (may be None).
56 validate: Whether to validate ranges (default True).
58 Returns:
59 TemperatureRangeData with minimum and maximum temperature values in °C.
61 """
62 min_raw = DataParser.parse_int16(data, 0, signed=True)
63 max_raw = DataParser.parse_int16(data, 2, signed=True)
65 return TemperatureRangeData(
66 minimum=min_raw * TEMPERATURE_RESOLUTION,
67 maximum=max_raw * TEMPERATURE_RESOLUTION,
68 )
70 def _encode_value(self, data: TemperatureRangeData) -> bytearray:
71 """Encode temperature range to bytes.
73 Args:
74 data: TemperatureRangeData instance.
76 Returns:
77 Encoded bytes (2 x sint16, little-endian).
79 """
80 if not isinstance(data, TemperatureRangeData):
81 raise TypeError(f"Expected TemperatureRangeData, got {type(data).__name__}")
83 min_raw = round(data.minimum / TEMPERATURE_RESOLUTION)
84 max_raw = round(data.maximum / TEMPERATURE_RESOLUTION)
86 for name, value in [("minimum", min_raw), ("maximum", max_raw)]:
87 if not SINT16_MIN <= value <= SINT16_MAX:
88 raise ValueError(f"Temperature {name} raw value {value} exceeds sint16 range")
90 result = bytearray()
91 result.extend(DataParser.encode_int16(min_raw, signed=True))
92 result.extend(DataParser.encode_int16(max_raw, signed=True))
93 return result