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

40 statements  

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

1"""Supported Speed Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT16_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12# Resolution: M=1, d=-2, b=0 → 0.01 km/h 

13_RESOLUTION = 0.01 

14 

15 

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

17 """Data class for supported speed range. 

18 

19 All values are in kilometres per hour with 0.01 km/h resolution. 

20 """ 

21 

22 minimum: float # Minimum speed in km/h 

23 maximum: float # Maximum speed in km/h 

24 minimum_increment: float # Minimum increment in km/h 

25 

26 def __post_init__(self) -> None: 

27 """Validate speed range data.""" 

28 if self.minimum > self.maximum: 

29 raise ValueError(f"Minimum speed {self.minimum} km/h cannot be greater than maximum {self.maximum} km/h") 

30 max_value = UINT16_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} km/h is outside valid range (0.0 to {max_value})") 

38 

39 

40class SupportedSpeedRangeCharacteristic(BaseCharacteristic[SupportedSpeedRangeData]): 

41 """Supported Speed Range characteristic (0x2AD4). 

42 

43 org.bluetooth.characteristic.supported_speed_range 

44 

45 Represents the speed range supported by a fitness machine. 

46 Three fields: minimum speed, maximum speed, and minimum increment. 

47 Each is a uint16 with M=1, d=-2, b=0 (0.01 km/h resolution). 

48 """ 

49 

50 # Validation attributes 

51 expected_length: int = 6 # 3 x uint16 

52 min_length: int = 6 

53 

54 def _decode_value( 

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

56 ) -> SupportedSpeedRangeData: 

57 """Parse supported speed range data (3 x uint16, 0.01 km/h resolution). 

58 

59 Args: 

60 data: Raw bytes from the characteristic read. 

61 ctx: Optional CharacteristicContext (may be None). 

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

63 

64 Returns: 

65 SupportedSpeedRangeData with minimum, maximum, and increment values. 

66 

67 """ 

68 min_raw = DataParser.parse_int16(data, 0, signed=False) 

69 max_raw = DataParser.parse_int16(data, 2, signed=False) 

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

71 

72 return SupportedSpeedRangeData( 

73 minimum=min_raw * _RESOLUTION, 

74 maximum=max_raw * _RESOLUTION, 

75 minimum_increment=inc_raw * _RESOLUTION, 

76 ) 

77 

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

79 """Encode supported speed range to bytes. 

80 

81 Args: 

82 data: SupportedSpeedRangeData instance. 

83 

84 Returns: 

85 Encoded bytes (3 x uint16, little-endian). 

86 

87 """ 

88 if not isinstance(data, SupportedSpeedRangeData): 

89 raise TypeError(f"Expected SupportedSpeedRangeData, got {type(data).__name__}") 

90 

91 min_raw = round(data.minimum / _RESOLUTION) 

92 max_raw = round(data.maximum / _RESOLUTION) 

93 inc_raw = round(data.minimum_increment / _RESOLUTION) 

94 

95 for name, value in [("minimum", min_raw), ("maximum", max_raw), ("increment", inc_raw)]: 

96 if not 0 <= value <= UINT16_MAX: 

97 raise ValueError(f"Speed {name} raw value {value} exceeds uint16 range") 

98 

99 result = bytearray() 

100 result.extend(DataParser.encode_int16(min_raw, signed=False)) 

101 result.extend(DataParser.encode_int16(max_raw, signed=False)) 

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

103 return result