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

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 .utils import IEEE11073Parser 

15 

16 

17class TemperatureMeasurementFlags(IntFlag): 

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

19 

20 CELSIUS_UNIT = 0x00 

21 FAHRENHEIT_UNIT = 0x01 

22 TIMESTAMP_PRESENT = 0x02 

23 TEMPERATURE_TYPE_PRESENT = 0x04 

24 

25 

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

27 """Parsed temperature measurement data.""" 

28 

29 temperature: float 

30 unit: TemperatureUnit 

31 flags: TemperatureMeasurementFlags 

32 timestamp: datetime | None = None 

33 temperature_type: int | None = None 

34 

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

39 

40 

41class TemperatureMeasurementCharacteristic(BaseCharacteristic): 

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

43 

44 Used in Health Thermometer Service for medical temperature readings. 

45 Different from Environmental Temperature (0x2A6E). 

46 """ 

47 

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 

52 

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. 

55 

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

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

58 

59 Args: 

60 data: Raw bytearray from BLE characteristic. 

61 ctx: Optional context providing surrounding characteristic metadata when available. 

62 

63 Returns: 

64 TemperatureMeasurementData containing parsed temperature data with metadata. 

65 

66 """ 

67 if len(data) < 5: 

68 raise ValueError("Temperature Measurement data must be at least 5 bytes") 

69 

70 flags = TemperatureMeasurementFlags(data[0]) 

71 

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

73 temp_value = IEEE11073Parser.parse_float32(data, 1) 

74 

75 # Check temperature unit flag (bit 0) 

76 unit = ( 

77 TemperatureUnit.FAHRENHEIT 

78 if TemperatureMeasurementFlags.FAHRENHEIT_UNIT in flags 

79 else TemperatureUnit.CELSIUS 

80 ) 

81 

82 # Parse optional fields 

83 timestamp: datetime | None = None 

84 temperature_type: int | None = None 

85 offset = 5 

86 

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

88 timestamp = IEEE11073Parser.parse_timestamp(data, offset) 

89 offset += 7 

90 

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

92 temperature_type = data[offset] 

93 

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 ) 

102 

103 def encode_value(self, data: TemperatureMeasurementData) -> bytearray: 

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

105 

106 Args: 

107 data: TemperatureMeasurementData containing temperature measurement data 

108 

109 Returns: 

110 Encoded bytes representing the temperature measurement 

111 

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 

121 

122 # Start with flags byte 

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

124 

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

126 temp_bytes = IEEE11073Parser.encode_float32(data.temperature) 

127 result.extend(temp_bytes) 

128 

129 # Add optional timestamp (7 bytes) if present 

130 if data.timestamp is not None: 

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

132 

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

134 if data.temperature_type is not None: 

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

136 

137 return result