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

40 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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 

12class HighIntensityExerciseThresholdData(msgspec.Struct): 

13 """High Intensity Exercise Threshold parsed data. 

14 

15 Attributes: 

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

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

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

19 threshold_percentage_max_heart_rate: Heart rate percentage (field_selector=3) 

20 """ 

21 

22 field_selector: int 

23 threshold_energy_expenditure: int | None = None 

24 threshold_metabolic_equivalent: float | None = None 

25 threshold_percentage_max_heart_rate: int | None = None 

26 

27 

28class HighIntensityExerciseThresholdCharacteristic(BaseCharacteristic[HighIntensityExerciseThresholdData]): 

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

30 

31 org.bluetooth.characteristic.high_intensity_exercise_threshold 

32 

33 High Intensity Exercise Threshold characteristic with conditional fields. 

34 

35 Structure (variable length): 

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

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

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

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

40 

41 Total payload: 1–3 bytes 

42 """ 

43 

44 # YAML specifies variable fields based on Field Selector value 

45 min_length: int | None = 1 

46 max_length: int | None = 3 

47 

48 def _decode_value( 

49 self, data: bytearray, ctx: CharacteristicContext | None = None 

50 ) -> HighIntensityExerciseThresholdData: 

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

52 

53 Args: 

54 data: Raw bytes (1–3 bytes) 

55 ctx: Optional context 

56 

57 Returns: 

58 HighIntensityExerciseThresholdData with field_selector and optional threshold 

59 """ 

60 field_selector = int(data[0]) 

61 threshold_energy = None 

62 threshold_met = None 

63 threshold_hr = None 

64 

65 # Parse optional threshold field based on selector 

66 if field_selector == 1 and len(data) >= 3: 

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

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

69 threshold_energy = threshold * 1000 # Convert to joules 

70 elif field_selector == 2 and len(data) >= 2: 

71 # Metabolic Equivalent (uint8, resolution 0.1 MET) 

72 threshold = int(data[1]) 

73 threshold_met = threshold * 0.1 

74 elif field_selector == 3 and len(data) >= 2: 

75 # Percentage of Maximum Heart Rate (uint8) 

76 threshold = int(data[1]) 

77 threshold_hr = threshold 

78 

79 return HighIntensityExerciseThresholdData( 

80 field_selector=field_selector, 

81 threshold_energy_expenditure=threshold_energy, 

82 threshold_metabolic_equivalent=threshold_met, 

83 threshold_percentage_max_heart_rate=threshold_hr, 

84 ) 

85 

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

87 """Encode High Intensity Exercise Threshold. 

88 

89 Args: 

90 data: HighIntensityExerciseThresholdData instance 

91 

92 Returns: 

93 Encoded bytes (1–3 bytes) 

94 """ 

95 result = bytearray([data.field_selector]) 

96 

97 if data.field_selector == 1 and data.threshold_energy_expenditure is not None: 

98 # Convert joules back to 1000 J units 

99 energy_value = int(data.threshold_energy_expenditure / 1000) 

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

101 elif data.field_selector == 2 and data.threshold_metabolic_equivalent is not None: 

102 # Convert 0.1 MET units back to uint8 

103 met_value = int(data.threshold_metabolic_equivalent / 0.1) 

104 result.append(met_value) 

105 elif data.field_selector == 3 and data.threshold_percentage_max_heart_rate is not None: 

106 hr_value = int(data.threshold_percentage_max_heart_rate) 

107 result.append(hr_value) 

108 

109 return result