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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Supported Inclination Range characteristic implementation."""
3from __future__ import annotations
5import msgspec
7from ..constants import SINT16_MAX, SINT16_MIN, UINT16_MAX
8from ..context import CharacteristicContext
9from .base import BaseCharacteristic
10from .utils import DataParser
12# Resolution: M=1, d=-1, b=0 -> 0.1 percentage points
13_RESOLUTION = 0.1
16class SupportedInclinationRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
17 """Data class for supported inclination range.
19 All values are in percentage with 0.1% resolution.
20 Min/max may be negative (decline).
21 """
23 minimum: float # Minimum inclination in %
24 maximum: float # Maximum inclination in %
25 minimum_increment: float # Minimum increment in %
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})")
43class SupportedInclinationRangeCharacteristic(BaseCharacteristic[SupportedInclinationRangeData]):
44 """Supported Inclination Range characteristic (0x2AD5).
46 org.bluetooth.characteristic.supported_inclination_range
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 """
53 # Validation attributes
54 expected_length: int = 6 # 2 x sint16 + 1 x uint16
55 min_length: int = 6
57 def _decode_value(
58 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
59 ) -> SupportedInclinationRangeData:
60 """Parse supported inclination range data.
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).
67 Returns:
68 SupportedInclinationRangeData with minimum, maximum, and increment.
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)
75 return SupportedInclinationRangeData(
76 minimum=min_raw * _RESOLUTION,
77 maximum=max_raw * _RESOLUTION,
78 minimum_increment=inc_raw * _RESOLUTION,
79 )
81 def _encode_value(self, data: SupportedInclinationRangeData) -> bytearray:
82 """Encode supported inclination range to bytes.
84 Args:
85 data: SupportedInclinationRangeData instance.
87 Returns:
88 Encoded bytes (2 x sint16 + 1 x uint16, little-endian).
90 """
91 if not isinstance(data, SupportedInclinationRangeData):
92 raise TypeError(f"Expected SupportedInclinationRangeData, got {type(data).__name__}")
94 min_raw = round(data.minimum / _RESOLUTION)
95 max_raw = round(data.maximum / _RESOLUTION)
96 inc_raw = round(data.minimum_increment / _RESOLUTION)
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})")
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