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

1"""Temperature Measurement characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from datetime import datetime 

6from enum import IntFlag 

7 

8import msgspec 

9 

10from bluetooth_sig.types.units import TemperatureUnit 

11 

12from ..context import CharacteristicContext 

13from .base import BaseCharacteristic 

14from .temperature_type import TemperatureType 

15from .utils import IEEE11073Parser 

16 

17 

18class TemperatureMeasurementFlags(IntFlag): 

19 """Temperature Measurement flags as per Bluetooth SIG specification.""" 

20 

21 CELSIUS_UNIT = 0x00 

22 FAHRENHEIT_UNIT = 0x01 

23 TIMESTAMP_PRESENT = 0x02 

24 TEMPERATURE_TYPE_PRESENT = 0x04 

25 

26 

27class TemperatureMeasurementData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

28 """Parsed temperature measurement data.""" 

29 

30 temperature: float 

31 unit: TemperatureUnit 

32 flags: TemperatureMeasurementFlags 

33 timestamp: datetime | None = None 

34 temperature_type: TemperatureType | None = None 

35 

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}") 

40 

41 

42class TemperatureMeasurementCharacteristic(BaseCharacteristic[TemperatureMeasurementData]): 

43 """Temperature Measurement characteristic (0x2A1C). 

44 

45 Used in Health Thermometer Service for medical temperature readings. 

46 Different from Environmental Temperature (0x2A6E). 

47 """ 

48 

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 

53 

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. 

58 

59 Format: Flags(1) + Temperature Value(4) + [Timestamp(7)] + [Temperature Type(1)]. 

60 Temperature is medfloat32 (IEEE 11073 medical float format). 

61 

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) 

66 

67 Returns: 

68 TemperatureMeasurementData containing parsed temperature data with metadata. 

69 

70 """ 

71 flags = TemperatureMeasurementFlags(data[0]) 

72 

73 # Parse temperature value (medfloat32 - IEEE 11073 medical float format) 

74 temp_value = IEEE11073Parser.parse_float32(data, 1) 

75 

76 # Check temperature unit flag (bit 0) 

77 unit = ( 

78 TemperatureUnit.FAHRENHEIT 

79 if TemperatureMeasurementFlags.FAHRENHEIT_UNIT in flags 

80 else TemperatureUnit.CELSIUS 

81 ) 

82 

83 # Parse optional fields 

84 timestamp: datetime | None = None 

85 temperature_type: TemperatureType | None = None 

86 offset = 5 

87 

88 if TemperatureMeasurementFlags.TIMESTAMP_PRESENT in flags and len(data) >= offset + 7: 

89 timestamp = IEEE11073Parser.parse_timestamp(data, offset) 

90 offset += 7 

91 

92 if TemperatureMeasurementFlags.TEMPERATURE_TYPE_PRESENT in flags and len(data) >= offset + 1: 

93 temperature_type = TemperatureType(data[offset]) 

94 

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 ) 

103 

104 def _encode_value(self, data: TemperatureMeasurementData) -> bytearray: 

105 """Encode temperature measurement value back to bytes. 

106 

107 Args: 

108 data: TemperatureMeasurementData containing temperature measurement data 

109 

110 Returns: 

111 Encoded bytes representing the temperature measurement 

112 

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 

122 

123 # Start with flags byte 

124 result = bytearray([int(flags)]) 

125 

126 # Add temperature value (medfloat32 - IEEE 11073 medical float format) 

127 temp_bytes = IEEE11073Parser.encode_float32(data.temperature) 

128 result.extend(temp_bytes) 

129 

130 # Add optional timestamp (7 bytes) if present 

131 if data.timestamp is not None: 

132 result.extend(IEEE11073Parser.encode_timestamp(data.timestamp)) 

133 

134 # Add optional temperature type (1 byte) if present 

135 if data.temperature_type is not None: 

136 result.append(int(data.temperature_type)) 

137 

138 return result