Coverage for src / bluetooth_sig / gatt / characteristics / supported_heart_rate_range.py: 97%

32 statements  

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

1"""Supported Heart Rate Range characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..constants import UINT8_MAX 

8from ..context import CharacteristicContext 

9from .base import BaseCharacteristic 

10from .utils import DataParser 

11 

12 

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

14 """Data class for supported heart rate range. 

15 

16 All values are in beats per minute (BPM), integer precision. 

17 """ 

18 

19 minimum: int # Minimum heart rate in BPM 

20 maximum: int # Maximum heart rate in BPM 

21 minimum_increment: int # Minimum increment in BPM 

22 

23 def __post_init__(self) -> None: 

24 """Validate heart rate range data.""" 

25 if self.minimum > self.maximum: 

26 raise ValueError(f"Minimum heart rate {self.minimum} BPM cannot be greater than maximum {self.maximum} BPM") 

27 for name, val in [ 

28 ("minimum", self.minimum), 

29 ("maximum", self.maximum), 

30 ("minimum_increment", self.minimum_increment), 

31 ]: 

32 if not 0 <= val <= UINT8_MAX: 

33 raise ValueError(f"{name} {val} BPM is outside valid range (0 to {UINT8_MAX})") 

34 

35 

36class SupportedHeartRateRangeCharacteristic(BaseCharacteristic[SupportedHeartRateRangeData]): 

37 """Supported Heart Rate Range characteristic (0x2AD7). 

38 

39 org.bluetooth.characteristic.supported_heart_rate_range 

40 

41 Represents the heart rate range supported by a fitness machine. 

42 Three fields: minimum heart rate, maximum heart rate, and minimum 

43 increment. Each is a uint8 in beats per minute (no scaling). 

44 """ 

45 

46 # Validation attributes 

47 expected_length: int = 3 # 3 x uint8 

48 min_length: int = 3 

49 

50 def _decode_value( 

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

52 ) -> SupportedHeartRateRangeData: 

53 """Parse supported heart rate range data. 

54 

55 Args: 

56 data: Raw bytes from the characteristic read. 

57 ctx: Optional CharacteristicContext (may be None). 

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

59 

60 Returns: 

61 SupportedHeartRateRangeData with minimum, maximum, and increment. 

62 

63 """ 

64 min_raw = DataParser.parse_int8(data, 0, signed=False) 

65 max_raw = DataParser.parse_int8(data, 1, signed=False) 

66 inc_raw = DataParser.parse_int8(data, 2, signed=False) 

67 

68 return SupportedHeartRateRangeData( 

69 minimum=min_raw, 

70 maximum=max_raw, 

71 minimum_increment=inc_raw, 

72 ) 

73 

74 def _encode_value(self, data: SupportedHeartRateRangeData) -> bytearray: 

75 """Encode supported heart rate range to bytes. 

76 

77 Args: 

78 data: SupportedHeartRateRangeData instance. 

79 

80 Returns: 

81 Encoded bytes (3 x uint8). 

82 

83 """ 

84 if not isinstance(data, SupportedHeartRateRangeData): 

85 raise TypeError(f"Expected SupportedHeartRateRangeData, got {type(data).__name__}") 

86 

87 result = bytearray() 

88 result.extend(DataParser.encode_int8(data.minimum, signed=False)) 

89 result.extend(DataParser.encode_int8(data.maximum, signed=False)) 

90 result.extend(DataParser.encode_int8(data.minimum_increment, signed=False)) 

91 return result