Coverage for src / bluetooth_sig / gatt / characteristics / supported_inclination_range.py: 91%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Supported Inclination Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import SINT16_MAX, SINT16_MIN, UINT16_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12# Resolution: M=1, d=-1, b=0 -> 0.1 percentage points 

13_RESOLUTION = 0.1 

14 

15 

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

17 """Data class for supported inclination range. 

18 

19 All values are in percentage with 0.1% resolution. 

20 Min/max may be negative (decline). 

21 """ 

22 

23 minimum: float # Minimum inclination in % 

24 maximum: float # Maximum inclination in % 

25 minimum_increment: float # Minimum increment in % 

26 

27 def __post_init__(self) -> None: 

28 """Validate inclination range data.""" 

29 if self.minimum > self.maximum: 

30 raise ValueError(f"Minimum inclination {self.minimum}% cannot be greater than maximum {self.maximum}%") 

31 min_value = SINT16_MIN * _RESOLUTION 

32 max_value = SINT16_MAX * _RESOLUTION 

33 for name, val in [("minimum", self.minimum), ("maximum", self.maximum)]: 

34 if not min_value <= val <= max_value: 

35 raise ValueError( 

36 f"{name.capitalize()} inclination {val}% is outside valid range ({min_value} to {max_value})" 

37 ) 

38 inc_max = UINT16_MAX * _RESOLUTION 

39 if not 0.0 <= self.minimum_increment <= inc_max: 

40 raise ValueError(f"Minimum increment {self.minimum_increment}% is outside valid range (0.0 to {inc_max})") 

41 

42 

43class SupportedInclinationRangeCharacteristic(BaseCharacteristic[SupportedInclinationRangeData]): 

44 """Supported Inclination Range characteristic (0x2AD5). 

45 

46 org.bluetooth.characteristic.supported_inclination_range 

47 

48 Represents the inclination range supported by a fitness machine. 

49 Three fields: minimum inclination (sint16), maximum inclination (sint16), 

50 and minimum increment (uint16). All scaled M=1, d=-1, b=0 (0.1% resolution). 

51 """ 

52 

53 # Validation attributes 

54 expected_length: int = 6 # 2 x sint16 + 1 x uint16 

55 min_length: int = 6 

56 

57 def _decode_value( 

58 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

59 ) -> SupportedInclinationRangeData: 

60 """Parse supported inclination range data. 

61 

62 Args: 

63 data: Raw bytes from the characteristic read. 

64 ctx: Optional CharacteristicContext (may be None). 

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

66 

67 Returns: 

68 SupportedInclinationRangeData with minimum, maximum, and increment. 

69 

70 """ 

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

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

73 inc_raw = DataParser.parse_int16(data, 4, signed=False) 

74 

75 return SupportedInclinationRangeData( 

76 minimum=min_raw * _RESOLUTION, 

77 maximum=max_raw * _RESOLUTION, 

78 minimum_increment=inc_raw * _RESOLUTION, 

79 ) 

80 

81 def _encode_value(self, data: SupportedInclinationRangeData) -> bytearray: 

82 """Encode supported inclination range to bytes. 

83 

84 Args: 

85 data: SupportedInclinationRangeData instance. 

86 

87 Returns: 

88 Encoded bytes (2 x sint16 + 1 x uint16, little-endian). 

89 

90 """ 

91 if not isinstance(data, SupportedInclinationRangeData): 

92 raise TypeError(f"Expected SupportedInclinationRangeData, got {type(data).__name__}") 

93 

94 min_raw = round(data.minimum / _RESOLUTION) 

95 max_raw = round(data.maximum / _RESOLUTION) 

96 inc_raw = round(data.minimum_increment / _RESOLUTION) 

97 

98 for name, value, lo, hi in [ 

99 ("minimum", min_raw, SINT16_MIN, SINT16_MAX), 

100 ("maximum", max_raw, SINT16_MIN, SINT16_MAX), 

101 ("increment", inc_raw, 0, UINT16_MAX), 

102 ]: 

103 if not lo <= value <= hi: 

104 raise ValueError(f"Inclination {name} raw value {value} exceeds range ({lo} to {hi})") 

105 

106 result = bytearray() 

107 result.extend(DataParser.encode_int16(min_raw, signed=True)) 

108 result.extend(DataParser.encode_int16(max_raw, signed=True)) 

109 result.extend(DataParser.encode_int16(inc_raw, signed=False)) 

110 return result