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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +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
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
22class HighIntensityExerciseThresholdData(msgspec.Struct):
23 """High Intensity Exercise Threshold parsed data.
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 """
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
38class HighIntensityExerciseThresholdCharacteristic(BaseCharacteristic[HighIntensityExerciseThresholdData]):
39 """High Intensity Exercise Threshold characteristic (0x2B4D).
41 org.bluetooth.characteristic.high_intensity_exercise_threshold
43 High Intensity Exercise Threshold characteristic with conditional fields.
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)
51 Total payload: 1-3 bytes
52 """
54 # YAML specifies variable fields based on Field Selector value
55 min_length: int | None = 1
56 max_length: int | None = 3
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.
63 Args:
64 data: Raw bytes (1-3 bytes)
65 ctx: Optional context
66 validate: Whether to validate ranges (default True)
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
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
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 )
97 def _encode_value(self, data: HighIntensityExerciseThresholdData) -> bytearray:
98 """Encode High Intensity Exercise Threshold.
100 Args:
101 data: HighIntensityExerciseThresholdData instance
103 Returns:
104 Encoded bytes (1-3 bytes)
105 """
106 result = bytearray([data.field_selector])
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)
123 return result