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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +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
12class HighIntensityExerciseThresholdData(msgspec.Struct):
13 """High Intensity Exercise Threshold parsed data.
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 """
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
28class HighIntensityExerciseThresholdCharacteristic(BaseCharacteristic[HighIntensityExerciseThresholdData]):
29 """High Intensity Exercise Threshold characteristic (0x2B4D).
31 org.bluetooth.characteristic.high_intensity_exercise_threshold
33 High Intensity Exercise Threshold characteristic with conditional fields.
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)
41 Total payload: 1–3 bytes
42 """
44 # YAML specifies variable fields based on Field Selector value
45 min_length: int | None = 1
46 max_length: int | None = 3
48 def _decode_value(
49 self, data: bytearray, ctx: CharacteristicContext | None = None
50 ) -> HighIntensityExerciseThresholdData:
51 """Parse High Intensity Exercise Threshold with conditional fields.
53 Args:
54 data: Raw bytes (1–3 bytes)
55 ctx: Optional context
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
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
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 )
86 def _encode_value(self, data: HighIntensityExerciseThresholdData) -> bytearray:
87 """Encode High Intensity Exercise Threshold.
89 Args:
90 data: HighIntensityExerciseThresholdData instance
92 Returns:
93 Encoded bytes (1–3 bytes)
94 """
95 result = bytearray([data.field_selector])
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)
109 return result