Coverage for src / bluetooth_sig / gatt / characteristics / temperature_measurement.py: 98%
56 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"""Temperature Measurement characteristic implementation."""
3from __future__ import annotations
5from datetime import datetime
6from enum import IntFlag
8import msgspec
10from bluetooth_sig.types.units import TemperatureUnit
12from ..context import CharacteristicContext
13from .base import BaseCharacteristic
14from .temperature_type import TemperatureType
15from .utils import IEEE11073Parser
18class TemperatureMeasurementFlags(IntFlag):
19 """Temperature Measurement flags as per Bluetooth SIG specification."""
21 CELSIUS_UNIT = 0x00
22 FAHRENHEIT_UNIT = 0x01
23 TIMESTAMP_PRESENT = 0x02
24 TEMPERATURE_TYPE_PRESENT = 0x04
27class TemperatureMeasurementData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
28 """Parsed temperature measurement data."""
30 temperature: float
31 unit: TemperatureUnit
32 flags: TemperatureMeasurementFlags
33 timestamp: datetime | None = None
34 temperature_type: TemperatureType | None = None
36 def __post_init__(self) -> None:
37 """Validate temperature measurement data."""
38 if self.unit not in (TemperatureUnit.CELSIUS, TemperatureUnit.FAHRENHEIT):
39 raise ValueError(f"Temperature unit must be CELSIUS or FAHRENHEIT, got {self.unit}")
42class TemperatureMeasurementCharacteristic(BaseCharacteristic[TemperatureMeasurementData]):
43 """Temperature Measurement characteristic (0x2A1C).
45 Used in Health Thermometer Service for medical temperature readings.
46 Different from Environmental Temperature (0x2A6E).
47 """
49 # Declarative validation attributes
50 min_length: int | None = 5 # Flags(1) + Temperature(4) minimum
51 max_length: int | None = 13 # + Timestamp(7) + TemperatureType(1) maximum
52 allow_variable_length: bool = True # Variable optional fields
54 def _decode_value(
55 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
56 ) -> TemperatureMeasurementData: # pylint: disable=too-many-locals
57 """Parse temperature measurement data according to Bluetooth specification.
59 Format: Flags(1) + Temperature Value(4) + [Timestamp(7)] + [Temperature Type(1)].
60 Temperature is medfloat32 (IEEE 11073 medical float format).
62 Args:
63 data: Raw bytearray from BLE characteristic.
64 ctx: Optional context providing surrounding characteristic metadata when available.
65 validate: Whether to validate ranges (default True)
67 Returns:
68 TemperatureMeasurementData containing parsed temperature data with metadata.
70 """
71 flags = TemperatureMeasurementFlags(data[0])
73 # Parse temperature value (medfloat32 - IEEE 11073 medical float format)
74 temp_value = IEEE11073Parser.parse_float32(data, 1)
76 # Check temperature unit flag (bit 0)
77 unit = (
78 TemperatureUnit.FAHRENHEIT
79 if TemperatureMeasurementFlags.FAHRENHEIT_UNIT in flags
80 else TemperatureUnit.CELSIUS
81 )
83 # Parse optional fields
84 timestamp: datetime | None = None
85 temperature_type: TemperatureType | None = None
86 offset = 5
88 if TemperatureMeasurementFlags.TIMESTAMP_PRESENT in flags and len(data) >= offset + 7:
89 timestamp = IEEE11073Parser.parse_timestamp(data, offset)
90 offset += 7
92 if TemperatureMeasurementFlags.TEMPERATURE_TYPE_PRESENT in flags and len(data) >= offset + 1:
93 temperature_type = TemperatureType(data[offset])
95 # Create immutable struct with all values
96 return TemperatureMeasurementData(
97 temperature=temp_value,
98 unit=unit,
99 flags=flags,
100 timestamp=timestamp,
101 temperature_type=temperature_type,
102 )
104 def _encode_value(self, data: TemperatureMeasurementData) -> bytearray:
105 """Encode temperature measurement value back to bytes.
107 Args:
108 data: TemperatureMeasurementData containing temperature measurement data
110 Returns:
111 Encoded bytes representing the temperature measurement
113 """
114 # Build flags
115 flags = TemperatureMeasurementFlags(0)
116 if data.unit == TemperatureUnit.FAHRENHEIT:
117 flags |= TemperatureMeasurementFlags.FAHRENHEIT_UNIT
118 if data.timestamp is not None:
119 flags |= TemperatureMeasurementFlags.TIMESTAMP_PRESENT
120 if data.temperature_type is not None:
121 flags |= TemperatureMeasurementFlags.TEMPERATURE_TYPE_PRESENT
123 # Start with flags byte
124 result = bytearray([int(flags)])
126 # Add temperature value (medfloat32 - IEEE 11073 medical float format)
127 temp_bytes = IEEE11073Parser.encode_float32(data.temperature)
128 result.extend(temp_bytes)
130 # Add optional timestamp (7 bytes) if present
131 if data.timestamp is not None:
132 result.extend(IEEE11073Parser.encode_timestamp(data.timestamp))
134 # Add optional temperature type (1 byte) if present
135 if data.temperature_type is not None:
136 result.append(int(data.temperature_type))
138 return result