Coverage for src / bluetooth_sig / gatt / characteristics / high_intensity_exercise_threshold.py: 88%
51 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"""High Intensity Exercise Threshold characteristic (0x2B4D)."""
3from __future__ import annotations
5import msgspec
7from ..context import CharacteristicContext
8from .base import BaseCharacteristic
9from .utils import DataParser
11# Field selector values
12_FIELD_SELECTOR_ENERGY = 1
13_FIELD_SELECTOR_MET = 2
14_FIELD_SELECTOR_HR_PERCENTAGE = 3
15_FIELD_SELECTOR_HR = 4
17# Minimum data lengths for each field selector
18_MIN_LENGTH_ENERGY = 3 # 1 byte selector + 2 bytes uint16
19_MIN_LENGTH_MET = 2 # 1 byte selector + 1 byte uint8
20_MIN_LENGTH_HR = 2 # 1 byte selector + 1 byte uint8
23class HighIntensityExerciseThresholdData(msgspec.Struct):
24 """High Intensity Exercise Threshold parsed data.
26 Attributes:
27 field_selector: Field selector indicating which threshold is present (1, 2, or 3)
28 threshold_energy_expenditure: Energy expenditure in joules (field_selector=1)
29 threshold_metabolic_equivalent: Metabolic equivalent in MET (field_selector=2)
30 threshold_percentage_max_heart_rate: Heart rate percentage (field_selector=3)
31 """
33 field_selector: int
34 threshold_energy_expenditure: int | None = None
35 threshold_metabolic_equivalent: float | None = None
36 threshold_percentage_max_heart_rate: int | None = None
37 threshold_heart_rate: int | None = None
40class HighIntensityExerciseThresholdCharacteristic(BaseCharacteristic[HighIntensityExerciseThresholdData]):
41 """High Intensity Exercise Threshold characteristic (0x2B4D).
43 org.bluetooth.characteristic.high_intensity_exercise_threshold
45 High Intensity Exercise Threshold characteristic with conditional fields.
47 Structure (variable length):
48 - Field Selector (uint8): 1 byte - determines which threshold field follows
49 - 0 = No field selected
50 - 1 = Threshold as Energy Expenditure per Hour (uint16, 2 bytes)
51 - 2 = Threshold as Metabolic Equivalent (uint8, 1 byte)
52 - 3 = Threshold as Percentage of Maximum Heart Rate (uint8, 1 byte)
53 - 4 = Threshold as Heart Rate (uint8, 1 byte)
55 Total payload: 1-3 bytes
56 """
58 # YAML specifies variable fields based on Field Selector value
59 min_length: int | None = 1
60 max_length: int | None = 3
62 def _decode_value(
63 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
64 ) -> HighIntensityExerciseThresholdData:
65 """Parse High Intensity Exercise Threshold with conditional fields.
67 Args:
68 data: Raw bytes (1-3 bytes)
69 ctx: Optional context
70 validate: Whether to validate ranges (default True)
72 Returns:
73 HighIntensityExerciseThresholdData with field_selector and optional threshold
74 """
75 field_selector = int(data[0])
76 threshold_energy = None
77 threshold_met = None
78 threshold_hr_pct = None
79 threshold_hr = None
81 # Parse optional threshold field based on selector
82 if field_selector == _FIELD_SELECTOR_ENERGY and len(data) >= _MIN_LENGTH_ENERGY:
83 # Energy Expenditure per Hour (uint16, resolution 1000 J)
84 threshold = DataParser.parse_int16(data, 1, signed=False)
85 threshold_energy = threshold * 1000 # Convert to joules
86 elif field_selector == _FIELD_SELECTOR_MET and len(data) >= _MIN_LENGTH_MET:
87 # Metabolic Equivalent (uint8, resolution 0.1 MET)
88 threshold = int(data[1])
89 threshold_met = threshold * 0.1
90 elif field_selector == _FIELD_SELECTOR_HR_PERCENTAGE and len(data) >= _MIN_LENGTH_HR:
91 # Percentage of Maximum Heart Rate (uint8)
92 threshold_hr_pct = int(data[1])
93 elif field_selector == _FIELD_SELECTOR_HR and len(data) >= _MIN_LENGTH_HR:
94 # Heart Rate (uint8, BPM)
95 threshold_hr = int(data[1])
97 return HighIntensityExerciseThresholdData(
98 field_selector=field_selector,
99 threshold_energy_expenditure=threshold_energy,
100 threshold_metabolic_equivalent=threshold_met,
101 threshold_percentage_max_heart_rate=threshold_hr_pct,
102 threshold_heart_rate=threshold_hr,
103 )
105 def _encode_value(self, data: HighIntensityExerciseThresholdData) -> bytearray:
106 """Encode High Intensity Exercise Threshold.
108 Args:
109 data: HighIntensityExerciseThresholdData instance
111 Returns:
112 Encoded bytes (1-3 bytes)
113 """
114 result = bytearray([data.field_selector])
116 if data.field_selector == _FIELD_SELECTOR_ENERGY and data.threshold_energy_expenditure is not None:
117 # Convert joules back to 1000 J units
118 energy_value = int(data.threshold_energy_expenditure / 1000)
119 result.extend(DataParser.encode_int16(energy_value, signed=False))
120 elif data.field_selector == _FIELD_SELECTOR_MET and data.threshold_metabolic_equivalent is not None:
121 # Convert 0.1 MET units back to uint8
122 met_value = int(data.threshold_metabolic_equivalent / 0.1)
123 result.append(met_value)
124 elif (
125 data.field_selector == _FIELD_SELECTOR_HR_PERCENTAGE
126 and data.threshold_percentage_max_heart_rate is not None
127 ):
128 result.append(int(data.threshold_percentage_max_heart_rate))
129 elif data.field_selector == _FIELD_SELECTOR_HR and data.threshold_heart_rate is not None:
130 result.append(int(data.threshold_heart_rate))
132 return result