Coverage for src / bluetooth_sig / gatt / characteristics / supported_power_range.py: 80%
41 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Supported Power 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
13class SupportedPowerRangeData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
14 """Data class for supported power range.
16 Per FTMS v1.0: Minimum Power (sint16) + Maximum Power (sint16) +
17 Minimum Increment (uint16), all in Watts.
18 """
20 minimum: int # Minimum power in Watts
21 maximum: int # Maximum power in Watts
22 minimum_increment: int # Minimum power increment in Watts
24 def __post_init__(self) -> None:
25 """Validate power range data."""
26 if self.minimum > self.maximum:
27 raise ValueError(f"Minimum power {self.minimum} W cannot be greater than maximum {self.maximum} W")
29 if not SINT16_MIN <= self.minimum <= SINT16_MAX:
30 raise ValueError(f"Minimum power {self.minimum} W is outside valid range ({SINT16_MIN} to {SINT16_MAX} W)")
31 if not SINT16_MIN <= self.maximum <= SINT16_MAX:
32 raise ValueError(f"Maximum power {self.maximum} W is outside valid range ({SINT16_MIN} to {SINT16_MAX} W)")
33 if not 0 <= self.minimum_increment <= UINT16_MAX:
34 raise ValueError(
35 f"Minimum increment {self.minimum_increment} W is outside valid range (0 to {UINT16_MAX} W)"
36 )
39class SupportedPowerRangeCharacteristic(BaseCharacteristic[SupportedPowerRangeData]):
40 """Supported Power Range characteristic (0x2AD8).
42 org.bluetooth.characteristic.supported_power_range
44 Specifies minimum power, maximum power, and minimum power increment
45 supported by a fitness machine (FTMS v1.0).
46 """
48 expected_length: int = 6 # 2x sint16 + 1x uint16
49 min_length: int = 6
51 def _decode_value(
52 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
53 ) -> SupportedPowerRangeData:
54 """Parse supported power range data.
56 Layout: Minimum Power (sint16) + Maximum Power (sint16) +
57 Minimum Increment (uint16) = 6 bytes.
58 """
59 min_power_raw = DataParser.parse_int16(data, 0, signed=True)
60 max_power_raw = DataParser.parse_int16(data, 2, signed=True)
61 min_increment_raw = DataParser.parse_int16(data, 4, signed=False)
63 return SupportedPowerRangeData(
64 minimum=min_power_raw,
65 maximum=max_power_raw,
66 minimum_increment=min_increment_raw,
67 )
69 def _encode_value(self, data: SupportedPowerRangeData) -> bytearray:
70 """Encode supported power range value back to bytes."""
71 if not isinstance(data, SupportedPowerRangeData):
72 raise TypeError(f"Supported power range data must be a SupportedPowerRangeData, got {type(data).__name__}")
74 if not SINT16_MIN <= data.minimum <= SINT16_MAX:
75 raise ValueError(f"Minimum power {data.minimum} exceeds sint16 range")
76 if not SINT16_MIN <= data.maximum <= SINT16_MAX:
77 raise ValueError(f"Maximum power {data.maximum} exceeds sint16 range")
78 if not 0 <= data.minimum_increment <= UINT16_MAX:
79 raise ValueError(f"Minimum increment {data.minimum_increment} exceeds uint16 range")
81 result = bytearray()
82 result.extend(DataParser.encode_int16(data.minimum, signed=True))
83 result.extend(DataParser.encode_int16(data.maximum, signed=True))
84 result.extend(DataParser.encode_int16(data.minimum_increment, signed=False))
86 return result