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

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +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 min_length: int = 4 

70 

71 def _decode_value( 

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

73 ) -> ReferenceTimeInformationData: 

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

75 

76 Args: 

77 data: Raw characteristic data (4 bytes) 

78 ctx: Optional characteristic context 

79 validate: Whether to validate ranges (default True) 

80 

81 Returns: 

82 ReferenceTimeInformationData with all fields 

83 

84 Raises: 

85 ValueError: If data is insufficient or contains invalid values 

86 

87 """ 

88 # Parse Time Source (1 byte) 

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

90 time_source = _validate_time_source(time_source_raw) 

91 

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

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

94 

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

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

97 

98 # Parse Hours Since Update (1 byte) 

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

100 _validate_hours_since_update(hours_since_update) 

101 

102 return ReferenceTimeInformationData( 

103 time_source=time_source, 

104 time_accuracy=time_accuracy, 

105 days_since_update=days_since_update, 

106 hours_since_update=hours_since_update, 

107 ) 

108 

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

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

111 

112 Args: 

113 data: ReferenceTimeInformationData to encode 

114 

115 Returns: 

116 Encoded reference time information (4 bytes) 

117 

118 Raises: 

119 ValueError: If data contains invalid values 

120 

121 """ 

122 result = bytearray() 

123 

124 # Encode Time Source (1 byte) 

125 time_source_value = int(data.time_source) 

126 _validate_time_source(time_source_value) # Validate before encoding 

127 result.append(time_source_value) 

128 

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

130 result.append(data.time_accuracy) 

131 

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

133 result.append(data.days_since_update) 

134 

135 # Encode Hours Since Update (1 byte) 

136 _validate_hours_since_update(data.hours_since_update) 

137 result.append(data.hours_since_update) 

138 

139 return result 

140 

141 

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

143 """Validate time source value. 

144 

145 Args: 

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

147 

148 Returns: 

149 TimeSource enum value 

150 

151 Raises: 

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

153 

154 """ 

155 if TIME_SOURCE_MAX < time_source_raw < HOURS_SINCE_UPDATE_OUT_OF_RANGE: 

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

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

158 

159 

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

161 """Validate hours since update value. 

162 

163 Args: 

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

165 

166 Raises: 

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

168 

169 """ 

170 if hours > HOURS_SINCE_UPDATE_MAX and hours != HOURS_SINCE_UPDATE_OUT_OF_RANGE: 

171 raise ValueError( 

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

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

174 )