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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +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)
70 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> ReferenceTimeInformationData:
71 """Decode Reference Time Information data from bytes.
73 Args:
74 data: Raw characteristic data (4 bytes)
75 ctx: Optional characteristic context
77 Returns:
78 ReferenceTimeInformationData with all fields
80 Raises:
81 ValueError: If data is insufficient or contains invalid values
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 )
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)
94 # Parse Time Accuracy (1 byte) - no validation needed, all values 0-255 valid
95 time_accuracy = DataParser.parse_int8(data, 1, signed=False)
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)
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)
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 )
111 def _encode_value(self, data: ReferenceTimeInformationData) -> bytearray:
112 """Encode Reference Time Information data to bytes.
114 Args:
115 data: ReferenceTimeInformationData to encode
117 Returns:
118 Encoded reference time information (4 bytes)
120 Raises:
121 ValueError: If data contains invalid values
123 """
124 result = bytearray()
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)
131 # Encode Time Accuracy (1 byte) - all values 0-255 valid
132 result.append(data.time_accuracy)
134 # Encode Days Since Update (1 byte) - all values 0-255 valid
135 result.append(data.days_since_update)
137 # Encode Hours Since Update (1 byte)
138 _validate_hours_since_update(data.hours_since_update)
139 result.append(data.hours_since_update)
141 return result
144def _validate_time_source(time_source_raw: int) -> TimeSource:
145 """Validate time source value.
147 Args:
148 time_source_raw: Raw time source value (0-255)
150 Returns:
151 TimeSource enum value
153 Raises:
154 ValueError: If time source is in reserved range (8-254)
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
162def _validate_hours_since_update(hours: int) -> None:
163 """Validate hours since update value.
165 Args:
166 hours: Hours since update value (0-255)
168 Raises:
169 ValueError: If hours is invalid (24-254)
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 )