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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Supported Speed Range characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import UINT16_MAX
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
12# Resolution: M=1, d=-2, b=0 → 0.01 km/h
13_RESOLUTION = 0.01
16class SupportedSpeedRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for supported speed range.
19 All values are in kilometres per hour with 0.01 km/h resolution.
20 """
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
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})")
40class SupportedSpeedRangeCharacteristic(BaseCharacteristic[SupportedSpeedRangeData]):
41 """Supported Speed Range characteristic (0x2AD4).
43 org.bluetooth.characteristic.supported_speed_range
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 """
50 # Validation attributes
51 expected_length: int = 6 # 3 x uint16
52 min_length: int = 6
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).
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).
64 Returns:
65 SupportedSpeedRangeData with minimum, maximum, and increment values.
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)
72 return SupportedSpeedRangeData(
73 minimum=min_raw * _RESOLUTION,
74 maximum=max_raw * _RESOLUTION,
75 minimum_increment=inc_raw * _RESOLUTION,
76 )
78 def _encode_value(self, data: SupportedSpeedRangeData) -> bytearray:
79 """Encode supported speed range to bytes.
81 Args:
82 data: SupportedSpeedRangeData instance.
84 Returns:
85 Encoded bytes (3 x uint16, little-endian).
87 """
88 if not isinstance(data, SupportedSpeedRangeData):
89 raise TypeError(f"Expected SupportedSpeedRangeData, got {type(data).__name__}")
91 min_raw = round(data.minimum / _RESOLUTION)
92 max_raw = round(data.maximum / _RESOLUTION)
93 inc_raw = round(data.minimum_increment / _RESOLUTION)
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")
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