Coverage for src/bluetooth_sig/gatt/characteristics/temperature_measurement.py: 98%
57 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +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 .utils import IEEE11073Parser
17class TemperatureMeasurementFlags(IntFlag):
18 """Temperature Measurement flags as per Bluetooth SIG specification."""
20 CELSIUS_UNIT = 0x00
21 FAHRENHEIT_UNIT = 0x01
22 TIMESTAMP_PRESENT = 0x02
23 TEMPERATURE_TYPE_PRESENT = 0x04
26class TemperatureMeasurementData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
27 """Parsed temperature measurement data."""
29 temperature: float
30 unit: TemperatureUnit
31 flags: TemperatureMeasurementFlags
32 timestamp: datetime | None = None
33 temperature_type: int | None = None
35 def __post_init__(self) -> None:
36 """Validate temperature measurement data."""
37 if self.unit not in (TemperatureUnit.CELSIUS, TemperatureUnit.FAHRENHEIT):
38 raise ValueError(f"Temperature unit must be CELSIUS or FAHRENHEIT, got {self.unit}")
41class TemperatureMeasurementCharacteristic(BaseCharacteristic):
42 """Temperature Measurement characteristic (0x2A1C).
44 Used in Health Thermometer Service for medical temperature readings.
45 Different from Environmental Temperature (0x2A6E).
46 """
48 # Declarative validation attributes
49 min_length: int | None = 5 # Flags(1) + Temperature(4) minimum
50 max_length: int | None = 13 # + Timestamp(7) + TemperatureType(1) maximum
51 allow_variable_length: bool = True # Variable optional fields
53 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> TemperatureMeasurementData: # pylint: disable=too-many-locals
54 """Parse temperature measurement data according to Bluetooth specification.
56 Format: Flags(1) + Temperature Value(4) + [Timestamp(7)] + [Temperature Type(1)].
57 Temperature is medfloat32 (IEEE 11073 medical float format).
59 Args:
60 data: Raw bytearray from BLE characteristic.
61 ctx: Optional context providing surrounding characteristic metadata when available.
63 Returns:
64 TemperatureMeasurementData containing parsed temperature data with metadata.
66 """
67 if len(data) < 5:
68 raise ValueError("Temperature Measurement data must be at least 5 bytes")
70 flags = TemperatureMeasurementFlags(data[0])
72 # Parse temperature value (medfloat32 - IEEE 11073 medical float format)
73 temp_value = IEEE11073Parser.parse_float32(data, 1)
75 # Check temperature unit flag (bit 0)
76 unit = (
77 TemperatureUnit.FAHRENHEIT
78 if TemperatureMeasurementFlags.FAHRENHEIT_UNIT in flags
79 else TemperatureUnit.CELSIUS
80 )
82 # Parse optional fields
83 timestamp: datetime | None = None
84 temperature_type: int | None = None
85 offset = 5
87 if TemperatureMeasurementFlags.TIMESTAMP_PRESENT in flags and len(data) >= offset + 7:
88 timestamp = IEEE11073Parser.parse_timestamp(data, offset)
89 offset += 7
91 if TemperatureMeasurementFlags.TEMPERATURE_TYPE_PRESENT in flags and len(data) >= offset + 1:
92 temperature_type = data[offset]
94 # Create immutable struct with all values
95 return TemperatureMeasurementData(
96 temperature=temp_value,
97 unit=unit,
98 flags=flags,
99 timestamp=timestamp,
100 temperature_type=temperature_type,
101 )
103 def encode_value(self, data: TemperatureMeasurementData) -> bytearray:
104 """Encode temperature measurement value back to bytes.
106 Args:
107 data: TemperatureMeasurementData containing temperature measurement data
109 Returns:
110 Encoded bytes representing the temperature measurement
112 """
113 # Build flags
114 flags = TemperatureMeasurementFlags(0)
115 if data.unit == TemperatureUnit.FAHRENHEIT:
116 flags |= TemperatureMeasurementFlags.FAHRENHEIT_UNIT
117 if data.timestamp is not None:
118 flags |= TemperatureMeasurementFlags.TIMESTAMP_PRESENT
119 if data.temperature_type is not None:
120 flags |= TemperatureMeasurementFlags.TEMPERATURE_TYPE_PRESENT
122 # Start with flags byte
123 result = bytearray([int(flags)])
125 # Add temperature value (medfloat32 - IEEE 11073 medical float format)
126 temp_bytes = IEEE11073Parser.encode_float32(data.temperature)
127 result.extend(temp_bytes)
129 # Add optional timestamp (7 bytes) if present
130 if data.timestamp is not None:
131 result.extend(IEEE11073Parser.encode_timestamp(data.timestamp))
133 # Add optional temperature type (1 byte) if present
134 if data.temperature_type is not None:
135 result.append(int(data.temperature_type))
137 return result