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

1"""Supported Power Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import SINT16_MAX, SINT16_MIN, UINT16_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12 

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

14 """Data class for supported power range. 

15 

16 Per FTMS v1.0: Minimum Power (sint16) + Maximum Power (sint16) + 

17 Minimum Increment (uint16), all in Watts. 

18 """ 

19 

20 minimum: int # Minimum power in Watts 

21 maximum: int # Maximum power in Watts 

22 minimum_increment: int # Minimum power increment in Watts 

23 

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") 

28 

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 ) 

37 

38 

39class SupportedPowerRangeCharacteristic(BaseCharacteristic[SupportedPowerRangeData]): 

40 """Supported Power Range characteristic (0x2AD8). 

41 

42 org.bluetooth.characteristic.supported_power_range 

43 

44 Specifies minimum power, maximum power, and minimum power increment 

45 supported by a fitness machine (FTMS v1.0). 

46 """ 

47 

48 expected_length: int = 6 # 2x sint16 + 1x uint16 

49 min_length: int = 6 

50 

51 def _decode_value( 

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

53 ) -> SupportedPowerRangeData: 

54 """Parse supported power range data. 

55 

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) 

62 

63 return SupportedPowerRangeData( 

64 minimum=min_power_raw, 

65 maximum=max_power_raw, 

66 minimum_increment=min_increment_raw, 

67 ) 

68 

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__}") 

73 

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") 

80 

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)) 

85 

86 return result