Coverage for src / bluetooth_sig / gatt / characteristics / high_intensity_exercise_threshold.py: 87%

46 statements  

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

1"""High Intensity Exercise Threshold characteristic (0x2B4D).""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from ..context import CharacteristicContext 

8from .base import BaseCharacteristic 

9from .utils import DataParser 

10 

11# Field selector values 

12_FIELD_SELECTOR_ENERGY = 1 

13_FIELD_SELECTOR_MET = 2 

14_FIELD_SELECTOR_HR_PERCENTAGE = 3 

15 

16# Minimum data lengths for each field selector 

17_MIN_LENGTH_ENERGY = 3 # 1 byte selector + 2 bytes uint16 

18_MIN_LENGTH_MET = 2 # 1 byte selector + 1 byte uint8 

19_MIN_LENGTH_HR = 2 # 1 byte selector + 1 byte uint8 

20 

21 

22class HighIntensityExerciseThresholdData(msgspec.Struct): 

23 """High Intensity Exercise Threshold parsed data. 

24 

25 Attributes: 

26 field_selector: Field selector indicating which threshold is present (1, 2, or 3) 

27 threshold_energy_expenditure: Energy expenditure in joules (field_selector=1) 

28 threshold_metabolic_equivalent: Metabolic equivalent in MET (field_selector=2) 

29 threshold_percentage_max_heart_rate: Heart rate percentage (field_selector=3) 

30 """ 

31 

32 field_selector: int 

33 threshold_energy_expenditure: int | None = None 

34 threshold_metabolic_equivalent: float | None = None 

35 threshold_percentage_max_heart_rate: int | None = None 

36 

37 

38class HighIntensityExerciseThresholdCharacteristic(BaseCharacteristic[HighIntensityExerciseThresholdData]): 

39 """High Intensity Exercise Threshold characteristic (0x2B4D). 

40 

41 org.bluetooth.characteristic.high_intensity_exercise_threshold 

42 

43 High Intensity Exercise Threshold characteristic with conditional fields. 

44 

45 Structure (variable length): 

46 - Field Selector (uint8): 1 byte - determines which threshold field follows 

47 - 1 = Threshold as Energy Expenditure per Hour (uint16, 0 or 2 bytes) 

48 - 2 = Threshold as Metabolic Equivalent (uint8, 0 or 1 byte) 

49 - 3 = Threshold as Percentage of Maximum Heart Rate (uint8, 0 or 1 byte) 

50 

51 Total payload: 1-3 bytes 

52 """ 

53 

54 # YAML specifies variable fields based on Field Selector value 

55 min_length: int | None = 1 

56 max_length: int | None = 3 

57 

58 def _decode_value( 

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

60 ) -> HighIntensityExerciseThresholdData: 

61 """Parse High Intensity Exercise Threshold with conditional fields. 

62 

63 Args: 

64 data: Raw bytes (1-3 bytes) 

65 ctx: Optional context 

66 validate: Whether to validate ranges (default True) 

67 

68 Returns: 

69 HighIntensityExerciseThresholdData with field_selector and optional threshold 

70 """ 

71 field_selector = int(data[0]) 

72 threshold_energy = None 

73 threshold_met = None 

74 threshold_hr = None 

75 

76 # Parse optional threshold field based on selector 

77 if field_selector == _FIELD_SELECTOR_ENERGY and len(data) >= _MIN_LENGTH_ENERGY: 

78 # Energy Expenditure per Hour (uint16, resolution 1000 J) 

79 threshold = DataParser.parse_int16(data, 1, signed=False) 

80 threshold_energy = threshold * 1000 # Convert to joules 

81 elif field_selector == _FIELD_SELECTOR_MET and len(data) >= _MIN_LENGTH_MET: 

82 # Metabolic Equivalent (uint8, resolution 0.1 MET) 

83 threshold = int(data[1]) 

84 threshold_met = threshold * 0.1 

85 elif field_selector == _FIELD_SELECTOR_HR_PERCENTAGE and len(data) >= _MIN_LENGTH_HR: 

86 # Percentage of Maximum Heart Rate (uint8) 

87 threshold = int(data[1]) 

88 threshold_hr = threshold 

89 

90 return HighIntensityExerciseThresholdData( 

91 field_selector=field_selector, 

92 threshold_energy_expenditure=threshold_energy, 

93 threshold_metabolic_equivalent=threshold_met, 

94 threshold_percentage_max_heart_rate=threshold_hr, 

95 ) 

96 

97 def _encode_value(self, data: HighIntensityExerciseThresholdData) -> bytearray: 

98 """Encode High Intensity Exercise Threshold. 

99 

100 Args: 

101 data: HighIntensityExerciseThresholdData instance 

102 

103 Returns: 

104 Encoded bytes (1-3 bytes) 

105 """ 

106 result = bytearray([data.field_selector]) 

107 

108 if data.field_selector == _FIELD_SELECTOR_ENERGY and data.threshold_energy_expenditure is not None: 

109 # Convert joules back to 1000 J units 

110 energy_value = int(data.threshold_energy_expenditure / 1000) 

111 result.extend(DataParser.encode_int16(energy_value, signed=False)) 

112 elif data.field_selector == _FIELD_SELECTOR_MET and data.threshold_metabolic_equivalent is not None: 

113 # Convert 0.1 MET units back to uint8 

114 met_value = int(data.threshold_metabolic_equivalent / 0.1) 

115 result.append(met_value) 

116 elif ( 

117 data.field_selector == _FIELD_SELECTOR_HR_PERCENTAGE 

118 and data.threshold_percentage_max_heart_rate is not None 

119 ): 

120 hr_value = int(data.threshold_percentage_max_heart_rate) 

121 result.append(hr_value) 

122 

123 return result