Coverage for src / bluetooth_sig / gatt / characteristics / intermediate_temperature.py: 90%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Intermediate Temperature characteristic (0x2A1E). 

2 

3Reports the intermediate temperature measurement. 

4 

5References: 

6 Bluetooth SIG Assigned Numbers / GATT Service Specifications 

7""" 

8 

9from __future__ import annotations 

10 

11from datetime import datetime 

12from enum import IntFlag 

13 

14import msgspec 

15 

16from bluetooth_sig.types.units import TemperatureUnit 

17 

18from ..context import CharacteristicContext 

19from .base import BaseCharacteristic 

20from .temperature_type import TemperatureType 

21from .utils import IEEE11073Parser 

22 

23 

24class IntermediateTemperatureFlags(IntFlag): 

25 """Intermediate Temperature flags as per Bluetooth SIG specification.""" 

26 

27 CELSIUS_UNIT = 0x00 

28 FAHRENHEIT_UNIT = 0x01 

29 TIMESTAMP_PRESENT = 0x02 

30 TEMPERATURE_TYPE_PRESENT = 0x04 

31 

32 

33class IntermediateTemperatureData(msgspec.Struct, frozen=True, kw_only=True): 

34 """Parsed intermediate temperature data.""" 

35 

36 temperature: float 

37 unit: TemperatureUnit 

38 flags: IntermediateTemperatureFlags 

39 timestamp: datetime | None = None 

40 temperature_type: TemperatureType | None = None 

41 

42 

43class IntermediateTemperatureCharacteristic(BaseCharacteristic[IntermediateTemperatureData]): 

44 """Intermediate Temperature characteristic (0x2A1E). 

45 

46 org.bluetooth.characteristic.intermediate_temperature 

47 

48 Same structure as Temperature Measurement: Flags(1) + Temperature(4) + [Timestamp(7)] + [Type(1)]. 

49 """ 

50 

51 min_length: int | None = 5 

52 max_length: int | None = 13 

53 allow_variable_length: bool = True 

54 

55 def _decode_value( 

56 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

57 ) -> IntermediateTemperatureData: 

58 """Parse intermediate temperature data. 

59 

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

61 """ 

62 flags = IntermediateTemperatureFlags(data[0]) 

63 temp_value = IEEE11073Parser.parse_float32(data, 1) 

64 unit = ( 

65 TemperatureUnit.FAHRENHEIT 

66 if IntermediateTemperatureFlags.FAHRENHEIT_UNIT in flags 

67 else TemperatureUnit.CELSIUS 

68 ) 

69 

70 timestamp: datetime | None = None 

71 temperature_type: TemperatureType | None = None 

72 offset = 5 

73 

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

75 timestamp = IEEE11073Parser.parse_timestamp(data, offset) 

76 offset += 7 

77 

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

79 temperature_type = TemperatureType(data[offset]) 

80 

81 return IntermediateTemperatureData( 

82 temperature=temp_value, 

83 unit=unit, 

84 flags=flags, 

85 timestamp=timestamp, 

86 temperature_type=temperature_type, 

87 ) 

88 

89 def _encode_value(self, data: IntermediateTemperatureData) -> bytearray: 

90 """Encode intermediate temperature value back to bytes.""" 

91 flags = IntermediateTemperatureFlags(0) 

92 if data.unit == TemperatureUnit.FAHRENHEIT: 

93 flags |= IntermediateTemperatureFlags.FAHRENHEIT_UNIT 

94 if data.timestamp is not None: 

95 flags |= IntermediateTemperatureFlags.TIMESTAMP_PRESENT 

96 if data.temperature_type is not None: 

97 flags |= IntermediateTemperatureFlags.TEMPERATURE_TYPE_PRESENT 

98 

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

100 result.extend(IEEE11073Parser.encode_float32(data.temperature)) 

101 

102 if data.timestamp is not None: 

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

104 

105 if data.temperature_type is not None: 

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

107 

108 return result