Coverage for src / bluetooth_sig / gatt / characteristics / supported_resistance_level_range.py: 95%

40 statements  

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

1"""Supported Resistance Level Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT8_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12# Resolution: M=1, d=1, b=0 -> 10 unitless per raw unit 

13_RESOLUTION = 10.0 

14 

15 

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

17 """Data class for supported resistance level range. 

18 

19 All values are unitless with resolution of 10 per raw unit. 

20 """ 

21 

22 minimum: float # Minimum resistance level (unitless) 

23 maximum: float # Maximum resistance level (unitless) 

24 minimum_increment: float # Minimum increment (unitless) 

25 

26 def __post_init__(self) -> None: 

27 """Validate resistance level range data.""" 

28 if self.minimum > self.maximum: 

29 raise ValueError(f"Minimum resistance level {self.minimum} cannot be greater than maximum {self.maximum}") 

30 max_value = UINT8_MAX * _RESOLUTION 

31 for name, val in [ 

32 ("minimum", self.minimum), 

33 ("maximum", self.maximum), 

34 ("minimum_increment", self.minimum_increment), 

35 ]: 

36 if not 0.0 <= val <= max_value: 

37 raise ValueError(f"{name} {val} is outside valid range (0.0 to {max_value})") 

38 

39 

40class SupportedResistanceLevelRangeCharacteristic( 

41 BaseCharacteristic[SupportedResistanceLevelRangeData], 

42): 

43 """Supported Resistance Level Range characteristic (0x2AD6). 

44 

45 org.bluetooth.characteristic.supported_resistance_level_range 

46 

47 Represents the resistance level range supported by a fitness machine. 

48 Three fields: minimum resistance level, maximum resistance level, 

49 and minimum increment. Each is a uint8 with M=1, d=1, b=0 (resolution 10). 

50 """ 

51 

52 # Validation attributes 

53 expected_length: int = 3 # 3 x uint8 

54 min_length: int = 3 

55 

56 def _decode_value( 

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

58 ) -> SupportedResistanceLevelRangeData: 

59 """Parse supported resistance level range data. 

60 

61 Args: 

62 data: Raw bytes from the characteristic read. 

63 ctx: Optional CharacteristicContext (may be None). 

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

65 

66 Returns: 

67 SupportedResistanceLevelRangeData with minimum, maximum, and 

68 increment values. 

69 

70 """ 

71 min_raw = DataParser.parse_int8(data, 0, signed=False) 

72 max_raw = DataParser.parse_int8(data, 1, signed=False) 

73 inc_raw = DataParser.parse_int8(data, 2, signed=False) 

74 

75 return SupportedResistanceLevelRangeData( 

76 minimum=min_raw * _RESOLUTION, 

77 maximum=max_raw * _RESOLUTION, 

78 minimum_increment=inc_raw * _RESOLUTION, 

79 ) 

80 

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

82 """Encode supported resistance level range to bytes. 

83 

84 Args: 

85 data: SupportedResistanceLevelRangeData instance. 

86 

87 Returns: 

88 Encoded bytes (3 x uint8). 

89 

90 """ 

91 if not isinstance(data, SupportedResistanceLevelRangeData): 

92 raise TypeError(f"Expected SupportedResistanceLevelRangeData, 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 in [ 

99 ("minimum", min_raw), 

100 ("maximum", max_raw), 

101 ("increment", inc_raw), 

102 ]: 

103 if not 0 <= value <= UINT8_MAX: 

104 raise ValueError(f"Resistance {name} raw value {value} exceeds uint8 range (0 to {UINT8_MAX})") 

105 

106 result = bytearray() 

107 result.extend(DataParser.encode_int8(min_raw, signed=False)) 

108 result.extend(DataParser.encode_int8(max_raw, signed=False)) 

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

110 return result