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

1"""Relative Value in a Temperature Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import SINT16_MAX, SINT16_MIN, UINT8_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

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 

14 

15 

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. 

18 

19 Combines a percentage (0.5% resolution) with a temperature range 

20 (min/max in degrees Celsius, 0.01 C resolution). 

21 """ 

22 

23 relative_value: float # Percentage (0.5% resolution) 

24 minimum_temperature: float # Minimum temperature in C 

25 maximum_temperature: float # Maximum temperature in C 

26 

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 ) 

36 

37 

38class RelativeValueInATemperatureRangeCharacteristic( 

39 BaseCharacteristic[RelativeValueInATemperatureRangeData], 

40): 

41 """Relative Value in a Temperature Range characteristic (0x2B0C). 

42 

43 org.bluetooth.characteristic.relative_value_in_a_temperature_range 

44 

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

49 

50 expected_length: int = 5 # uint8 + 2 x sint16 

51 min_length: int = 5 

52 expected_type = RelativeValueInATemperatureRangeData 

53 

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. 

58 

59 Args: 

60 data: Raw bytes (5 bytes). 

61 ctx: Optional CharacteristicContext. 

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

63 

64 Returns: 

65 RelativeValueInATemperatureRangeData. 

66 

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) 

71 

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 ) 

77 

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

79 """Encode relative value in a temperature range. 

80 

81 Args: 

82 data: RelativeValueInATemperatureRangeData instance. 

83 

84 Returns: 

85 Encoded bytes (5 bytes). 

86 

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) 

91 

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

98 

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