Coverage for src / bluetooth_sig / gatt / characteristics / reference_time_information.py: 98%

53 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""Reference Time Information characteristic (0x2A14) implementation. 

2 

3Provides information about the reference time source, including its type, 

4accuracy, and time since last update. 

5 

6Based on Bluetooth SIG GATT Specification: 

7- Reference Time Information: 4 bytes (Time Source + Time Accuracy + Days Since Update + Hours Since Update) 

8- Time Source: uint8 (0=Unknown, 1=NTP, 2=GPS, etc.) 

9- Time Accuracy: uint8 (0-253 in 125ms steps, 254=out of range, 255=unknown) 

10- Days Since Update: uint8 (0-254, 255 means >=255 days) 

11- Hours Since Update: uint8 (0-23, 255 means >=255 days) 

12""" 

13 

14from __future__ import annotations 

15 

16from enum import IntEnum 

17 

18import msgspec 

19 

20from ..context import CharacteristicContext 

21from .base import BaseCharacteristic 

22from .utils import DataParser 

23 

24# Bluetooth SIG Reference Time Information characteristic constants 

25REFERENCE_TIME_INFO_LENGTH = 4 # Total characteristic length in bytes 

26TIME_SOURCE_MAX = 7 # Maximum valid time source value (0-7) 

27HOURS_SINCE_UPDATE_MAX = 23 # Maximum valid hours value (0-23) 

28HOURS_SINCE_UPDATE_OUT_OF_RANGE = 255 # Special value indicating >=255 days 

29 

30 

31class TimeSource(IntEnum): 

32 """Time source enumeration per Bluetooth SIG specification.""" 

33 

34 UNKNOWN = 0 

35 NETWORK_TIME_PROTOCOL = 1 

36 GPS = 2 

37 RADIO_TIME_SIGNAL = 3 

38 MANUAL = 4 

39 ATOMIC_CLOCK = 5 

40 CELLULAR_NETWORK = 6 

41 NOT_SYNCHRONIZED = 7 

42 

43 

44class ReferenceTimeInformationData(msgspec.Struct): 

45 """Reference Time Information characteristic data structure.""" 

46 

47 time_source: TimeSource 

48 time_accuracy: int # 0-253 (in 125ms steps), 254=out of range, 255=unknown 

49 days_since_update: int # 0-254, 255 means >=255 days 

50 hours_since_update: int # 0-23, 255 means >=255 days 

51 

52 

53class ReferenceTimeInformationCharacteristic(BaseCharacteristic[ReferenceTimeInformationData]): 

54 """Reference Time Information characteristic (0x2A14). 

55 

56 Represents information about the reference time source including type, 

57 accuracy, and time elapsed since last synchronization. 

58 

59 Structure (4 bytes): 

60 - Time Source: uint8 (0=Unknown, 1=NTP, 2=GPS, 3=Radio, 4=Manual, 5=Atomic, 6=Cellular, 7=Not Sync) 

61 - Time Accuracy: uint8 (0-253 in 125ms steps, 254=out of range >31.625s, 255=unknown) 

62 - Days Since Update: uint8 (0-254 days, 255 means >=255 days) 

63 - Hours Since Update: uint8 (0-23 hours, 255 means >=255 days) 

64 

65 Used by Current Time Service (0x1805). 

66 """ 

67 

68 expected_length: int = 4 # Time Source(1) + Time Accuracy(1) + Days(1) + Hours(1) 

69 

70 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> ReferenceTimeInformationData: 

71 """Decode Reference Time Information data from bytes. 

72 

73 Args: 

74 data: Raw characteristic data (4 bytes) 

75 ctx: Optional characteristic context 

76 

77 Returns: 

78 ReferenceTimeInformationData with all fields 

79 

80 Raises: 

81 ValueError: If data is insufficient or contains invalid values 

82 

83 """ 

84 if len(data) < REFERENCE_TIME_INFO_LENGTH: 

85 raise ValueError( 

86 f"Insufficient data for Reference Time Information: " 

87 f"expected {REFERENCE_TIME_INFO_LENGTH} bytes, got {len(data)}" 

88 ) 

89 

90 # Parse Time Source (1 byte) 

91 time_source_raw = DataParser.parse_int8(data, 0, signed=False) 

92 time_source = _validate_time_source(time_source_raw) 

93 

94 # Parse Time Accuracy (1 byte) - no validation needed, all values 0-255 valid 

95 time_accuracy = DataParser.parse_int8(data, 1, signed=False) 

96 

97 # Parse Days Since Update (1 byte) - no validation needed, all values 0-255 valid 

98 days_since_update = DataParser.parse_int8(data, 2, signed=False) 

99 

100 # Parse Hours Since Update (1 byte) 

101 hours_since_update = DataParser.parse_int8(data, 3, signed=False) 

102 _validate_hours_since_update(hours_since_update) 

103 

104 return ReferenceTimeInformationData( 

105 time_source=time_source, 

106 time_accuracy=time_accuracy, 

107 days_since_update=days_since_update, 

108 hours_since_update=hours_since_update, 

109 ) 

110 

111 def _encode_value(self, data: ReferenceTimeInformationData) -> bytearray: 

112 """Encode Reference Time Information data to bytes. 

113 

114 Args: 

115 data: ReferenceTimeInformationData to encode 

116 

117 Returns: 

118 Encoded reference time information (4 bytes) 

119 

120 Raises: 

121 ValueError: If data contains invalid values 

122 

123 """ 

124 result = bytearray() 

125 

126 # Encode Time Source (1 byte) 

127 time_source_value = int(data.time_source) 

128 _validate_time_source(time_source_value) # Validate before encoding 

129 result.append(time_source_value) 

130 

131 # Encode Time Accuracy (1 byte) - all values 0-255 valid 

132 result.append(data.time_accuracy) 

133 

134 # Encode Days Since Update (1 byte) - all values 0-255 valid 

135 result.append(data.days_since_update) 

136 

137 # Encode Hours Since Update (1 byte) 

138 _validate_hours_since_update(data.hours_since_update) 

139 result.append(data.hours_since_update) 

140 

141 return result 

142 

143 

144def _validate_time_source(time_source_raw: int) -> TimeSource: 

145 """Validate time source value. 

146 

147 Args: 

148 time_source_raw: Raw time source value (0-255) 

149 

150 Returns: 

151 TimeSource enum value 

152 

153 Raises: 

154 ValueError: If time source is in reserved range (8-254) 

155 

156 """ 

157 if TIME_SOURCE_MAX < time_source_raw < HOURS_SINCE_UPDATE_OUT_OF_RANGE: 

158 raise ValueError(f"Invalid time source: {time_source_raw} (valid range: 0-{TIME_SOURCE_MAX})") 

159 return TimeSource(time_source_raw) if time_source_raw <= TIME_SOURCE_MAX else TimeSource.UNKNOWN 

160 

161 

162def _validate_hours_since_update(hours: int) -> None: 

163 """Validate hours since update value. 

164 

165 Args: 

166 hours: Hours since update value (0-255) 

167 

168 Raises: 

169 ValueError: If hours is invalid (24-254) 

170 

171 """ 

172 if hours > HOURS_SINCE_UPDATE_MAX and hours != HOURS_SINCE_UPDATE_OUT_OF_RANGE: 

173 raise ValueError( 

174 f"Invalid hours since update: {hours} " 

175 f"(valid range: 0-{HOURS_SINCE_UPDATE_MAX} or {HOURS_SINCE_UPDATE_OUT_OF_RANGE} for >=255 days)" 

176 )