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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Reference Time Information characteristic (0x2A14) implementation.
3Provides information about the reference time source, including its type,
4accuracy, and time since last update.
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"""
14from __future__ import annotations
16from enum import IntEnum
18import msgspec
20from ..context import CharacteristicContext
21from .base import BaseCharacteristic
22from .utils import DataParser
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
31class TimeSource(IntEnum):
32 """Time source enumeration per Bluetooth SIG specification."""
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
44class ReferenceTimeInformationData(msgspec.Struct):
45 """Reference Time Information characteristic data structure."""
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
53class ReferenceTimeInformationCharacteristic(BaseCharacteristic[ReferenceTimeInformationData]):
54 """Reference Time Information characteristic (0x2A14).
56 Represents information about the reference time source including type,
57 accuracy, and time elapsed since last synchronization.
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)
65 Used by Current Time Service (0x1805).
66 """
68 expected_length: int = 4 # Time Source(1) + Time Accuracy(1) + Days(1) + Hours(1)
69 min_length: int = 4
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.
76 Args:
77 data: Raw characteristic data (4 bytes)
78 ctx: Optional characteristic context
79 validate: Whether to validate ranges (default True)
81 Returns:
82 ReferenceTimeInformationData with all fields
84 Raises:
85 ValueError: If data is insufficient or contains invalid values
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)
92 # Parse Time Accuracy (1 byte) - no validation needed, all values 0-255 valid
93 time_accuracy = DataParser.parse_int8(data, 1, signed=False)
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)
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)
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 )
109 def _encode_value(self, data: ReferenceTimeInformationData) -> bytearray:
110 """Encode Reference Time Information data to bytes.
112 Args:
113 data: ReferenceTimeInformationData to encode
115 Returns:
116 Encoded reference time information (4 bytes)
118 Raises:
119 ValueError: If data contains invalid values
121 """
122 result = bytearray()
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)
129 # Encode Time Accuracy (1 byte) - all values 0-255 valid
130 result.append(data.time_accuracy)
132 # Encode Days Since Update (1 byte) - all values 0-255 valid
133 result.append(data.days_since_update)
135 # Encode Hours Since Update (1 byte)
136 _validate_hours_since_update(data.hours_since_update)
137 result.append(data.hours_since_update)
139 return result
142def _validate_time_source(time_source_raw: int) -> TimeSource:
143 """Validate time source value.
145 Args:
146 time_source_raw: Raw time source value (0-255)
148 Returns:
149 TimeSource enum value
151 Raises:
152 ValueError: If time source is in reserved range (8-254)
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
160def _validate_hours_since_update(hours: int) -> None:
161 """Validate hours since update value.
163 Args:
164 hours: Hours since update value (0-255)
166 Raises:
167 ValueError: If hours is invalid (24-254)
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 )