Coverage for src / bluetooth_sig / gatt / characteristics / relative_value_in_a_temperature_range.py: 90%
42 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 Temperature Range characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import SINT16_MAX, SINT16_MIN, 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_TEMPERATURE_RESOLUTION = 0.01 # Temperature: M=1, d=-2, b=0 -> 0.01 C
16class RelativeValueInATemperatureRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for relative value in a temperature range.
19 Combines a percentage (0.5% resolution) with a temperature range
20 (min/max in degrees Celsius, 0.01 C resolution).
21 """
23 relative_value: float # Percentage (0.5% resolution)
24 minimum_temperature: float # Minimum temperature in C
25 maximum_temperature: float # Maximum temperature in C
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 if self.minimum_temperature > self.maximum_temperature:
33 raise ValueError(
34 f"Minimum temperature {self.minimum_temperature} C cannot exceed maximum {self.maximum_temperature} C"
35 )
38class RelativeValueInATemperatureRangeCharacteristic(
39 BaseCharacteristic[RelativeValueInATemperatureRangeData],
40):
41 """Relative Value in a Temperature Range characteristic (0x2B0C).
43 org.bluetooth.characteristic.relative_value_in_a_temperature_range
45 Represents a relative value within a temperature range. Fields:
46 Percentage 8 (uint8, 0.5%), min temperature (sint16, 0.01 C),
47 max temperature (sint16, 0.01 C).
48 """
50 expected_length: int = 5 # uint8 + 2 x sint16
51 min_length: int = 5
52 expected_type = RelativeValueInATemperatureRangeData
54 def _decode_value(
55 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
56 ) -> RelativeValueInATemperatureRangeData:
57 """Parse relative value in a temperature range.
59 Args:
60 data: Raw bytes (5 bytes).
61 ctx: Optional CharacteristicContext.
62 validate: Whether to validate ranges (default True).
64 Returns:
65 RelativeValueInATemperatureRangeData.
67 """
68 pct_raw = DataParser.parse_int8(data, 0, signed=False)
69 min_raw = DataParser.parse_int16(data, 1, signed=True)
70 max_raw = DataParser.parse_int16(data, 3, signed=True)
72 return RelativeValueInATemperatureRangeData(
73 relative_value=pct_raw * _PERCENTAGE_RESOLUTION,
74 minimum_temperature=min_raw * _TEMPERATURE_RESOLUTION,
75 maximum_temperature=max_raw * _TEMPERATURE_RESOLUTION,
76 )
78 def _encode_value(self, data: RelativeValueInATemperatureRangeData) -> bytearray:
79 """Encode relative value in a temperature range.
81 Args:
82 data: RelativeValueInATemperatureRangeData instance.
84 Returns:
85 Encoded bytes (5 bytes).
87 """
88 pct_raw = round(data.relative_value / _PERCENTAGE_RESOLUTION)
89 min_raw = round(data.minimum_temperature / _TEMPERATURE_RESOLUTION)
90 max_raw = round(data.maximum_temperature / _TEMPERATURE_RESOLUTION)
92 if not 0 <= pct_raw <= UINT8_MAX:
93 raise ValueError(f"Percentage raw {pct_raw} exceeds uint8 range")
94 if not SINT16_MIN <= min_raw <= SINT16_MAX:
95 raise ValueError(f"Min temperature raw {min_raw} exceeds sint16 range")
96 if not SINT16_MIN <= max_raw <= SINT16_MAX:
97 raise ValueError(f"Max temperature raw {max_raw} exceeds sint16 range")
99 result = bytearray()
100 result.extend(DataParser.encode_int8(pct_raw, signed=False))
101 result.extend(DataParser.encode_int16(min_raw, signed=True))
102 result.extend(DataParser.encode_int16(max_raw, signed=True))
103 return result