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

1"""Temperature Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import SINT16_MAX, SINT16_MIN, TEMPERATURE_RESOLUTION 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12 

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

14 """Data class for temperature range. 

15 

16 Each value is a temperature in degrees Celsius with 0.01°C resolution. 

17 """ 

18 

19 minimum: float # Minimum temperature in °C 

20 maximum: float # Maximum temperature in °C 

21 

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 ) 

33 

34 

35class TemperatureRangeCharacteristic(BaseCharacteristic[TemperatureRangeData]): 

36 """Temperature Range characteristic (0x2B10). 

37 

38 org.bluetooth.characteristic.temperature_range 

39 

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 """ 

43 

44 # Validation attributes 

45 expected_length: int = 4 # 2 x sint16 

46 min_length: int = 4 

47 

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). 

52 

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). 

57 

58 Returns: 

59 TemperatureRangeData with minimum and maximum temperature values in °C. 

60 

61 """ 

62 min_raw = DataParser.parse_int16(data, 0, signed=True) 

63 max_raw = DataParser.parse_int16(data, 2, signed=True) 

64 

65 return TemperatureRangeData( 

66 minimum=min_raw * TEMPERATURE_RESOLUTION, 

67 maximum=max_raw * TEMPERATURE_RESOLUTION, 

68 ) 

69 

70 def _encode_value(self, data: TemperatureRangeData) -> bytearray: 

71 """Encode temperature range to bytes. 

72 

73 Args: 

74 data: TemperatureRangeData instance. 

75 

76 Returns: 

77 Encoded bytes (2 x sint16, little-endian). 

78 

79 """ 

80 if not isinstance(data, TemperatureRangeData): 

81 raise TypeError(f"Expected TemperatureRangeData, got {type(data).__name__}") 

82 

83 min_raw = round(data.minimum / TEMPERATURE_RESOLUTION) 

84 max_raw = round(data.maximum / TEMPERATURE_RESOLUTION) 

85 

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") 

89 

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