Coverage for src / bluetooth_sig / gatt / characteristics / templates / time_duration.py: 94%
68 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"""Time-duration template returning ``timedelta`` for BLE time characteristics.
3Wraps a numeric extractor and converts raw integer counts (seconds,
4milliseconds, hours, …) into :class:`datetime.timedelta` instances so
5that callers receive a proper Python time type instead of a plain ``int``.
6"""
8from __future__ import annotations
10import math
11from datetime import timedelta
13from ...context import CharacteristicContext
14from ...exceptions import InsufficientDataError
15from ..utils.extractors import (
16 UINT8,
17 UINT16,
18 UINT24,
19 UINT32,
20 RawExtractor,
21)
22from .base import CodingTemplate
25class TimeDurationTemplate(CodingTemplate[timedelta]):
26 r"""Template for time-duration characteristics that return ``timedelta``.
28 Encodes/decodes a raw integer count in a given time unit (seconds,
29 milliseconds, hours, ...) to/from a ``timedelta``.
31 Pipeline Integration:
32 bytes -> [extractor] -> raw_int -> x scale -> timedelta(seconds=...)
34 Examples:
35 >>> template = TimeDurationTemplate.seconds_uint16()
36 >>> template.decode_value(bytearray([0x2A, 0x00]))
37 datetime.timedelta(seconds=42)
38 >>>
39 >>> template.encode_value(timedelta(seconds=42))
40 bytearray(b'*\\x00')
41 """
43 def __init__(
44 self,
45 extractor: RawExtractor,
46 *,
47 seconds_per_unit: float = 1.0,
48 ) -> None:
49 """Initialise with extractor and time-unit conversion factor.
51 Args:
52 extractor: Raw extractor defining byte size and signedness.
53 seconds_per_unit: How many seconds one raw count represents.
54 E.g. ``1.0`` for seconds, ``0.001`` for ms,
55 ``3600.0`` for hours, ``360.0`` for deci-hours.
57 """
58 self._extractor = extractor
59 self._seconds_per_unit = seconds_per_unit
61 @property
62 def data_size(self) -> int:
63 """Return byte size required for encoding."""
64 return self._extractor.byte_size
66 @property
67 def extractor(self) -> RawExtractor:
68 """Return extractor for pipeline access."""
69 return self._extractor
71 def decode_value(
72 self,
73 data: bytearray,
74 offset: int = 0,
75 ctx: CharacteristicContext | None = None,
76 *,
77 validate: bool = True,
78 ) -> timedelta:
79 """Decode bytes to ``timedelta``.
81 Args:
82 data: Raw bytes from BLE characteristic.
83 offset: Starting offset in data buffer.
84 ctx: Optional context for parsing.
85 validate: Whether to validate data length (default True).
87 Returns:
88 ``timedelta`` representing the decoded duration.
90 Raises:
91 InsufficientDataError: If data too short for required byte size.
93 """
94 if validate and len(data) < offset + self.data_size:
95 raise InsufficientDataError("TimeDuration", data[offset:], self.data_size)
97 raw = self._extractor.extract(data, offset)
98 return timedelta(seconds=raw * self._seconds_per_unit)
100 def encode_value(self, value: timedelta | int | float, *, validate: bool = True) -> bytearray:
101 """Encode ``timedelta`` (or numeric seconds) to bytes.
103 Args:
104 value: ``timedelta``, or a numeric value treated as the raw count.
105 validate: Whether to validate (default True).
107 Returns:
108 Encoded bytes.
110 """
111 raw = round(value.total_seconds() / self._seconds_per_unit) if isinstance(value, timedelta) else int(value)
113 return self._extractor.pack(raw)
115 # -----------------------------------------------------------------
116 # Factory methods
117 # -----------------------------------------------------------------
119 @classmethod
120 def seconds_uint8(cls) -> TimeDurationTemplate:
121 """1-byte unsigned, 1-second resolution."""
122 return cls(UINT8, seconds_per_unit=1.0)
124 @classmethod
125 def seconds_uint16(cls) -> TimeDurationTemplate:
126 """2-byte unsigned, 1-second resolution."""
127 return cls(UINT16, seconds_per_unit=1.0)
129 @classmethod
130 def seconds_uint24(cls) -> TimeDurationTemplate:
131 """3-byte unsigned, 1-second resolution."""
132 return cls(UINT24, seconds_per_unit=1.0)
134 @classmethod
135 def seconds_uint32(cls) -> TimeDurationTemplate:
136 """4-byte unsigned, 1-second resolution."""
137 return cls(UINT32, seconds_per_unit=1.0)
139 @classmethod
140 def milliseconds_uint24(cls) -> TimeDurationTemplate:
141 """3-byte unsigned, 1-millisecond resolution."""
142 return cls(UINT24, seconds_per_unit=0.001)
144 @classmethod
145 def hours_uint24(cls) -> TimeDurationTemplate:
146 """3-byte unsigned, 1-hour resolution."""
147 return cls(UINT24, seconds_per_unit=3600.0)
149 @classmethod
150 def decihours_uint8(cls) -> TimeDurationTemplate:
151 """1-byte unsigned, 0.1-hour (6-minute) resolution."""
152 return cls(UINT8, seconds_per_unit=360.0)
155class TimeExponentialTemplate(CodingTemplate[timedelta]):
156 """Template for exponentially-encoded time (Time Exponential 8).
158 Encoding: ``value = 1.1^(N - 64)`` seconds.
159 Special values: ``0x00`` = 0 s, ``0xFE`` = device lifetime, ``0xFF`` = unknown.
160 """
162 @property
163 def data_size(self) -> int:
164 """Size: 1 byte."""
165 return 1
167 @property
168 def extractor(self) -> RawExtractor:
169 """Return uint8 extractor for pipeline access."""
170 return UINT8
172 def decode_value(
173 self,
174 data: bytearray,
175 offset: int = 0,
176 ctx: CharacteristicContext | None = None,
177 *,
178 validate: bool = True,
179 ) -> timedelta:
180 """Decode exponentially-encoded time to ``timedelta``.
182 Args:
183 data: Raw bytes from BLE characteristic.
184 offset: Starting offset in data buffer.
185 ctx: Optional context for parsing.
186 validate: Whether to validate data length (default True).
188 Returns:
189 ``timedelta`` representing the decoded duration.
191 Raises:
192 InsufficientDataError: If data too short.
194 """
195 if validate and len(data) < offset + self.data_size:
196 raise InsufficientDataError("TimeExponential8", data[offset:], 1)
198 raw = UINT8.extract(data, offset)
199 if raw == 0:
200 return timedelta(seconds=0)
201 seconds = 1.1 ** (raw - 64)
202 return timedelta(seconds=seconds)
204 def encode_value(self, value: timedelta | float, *, validate: bool = True) -> bytearray:
205 """Encode a time duration using exponential encoding.
207 Args:
208 value: ``timedelta`` or numeric seconds.
209 validate: Whether to validate (default True).
211 Returns:
212 Encoded byte.
214 """
215 seconds = value.total_seconds() if isinstance(value, timedelta) else float(value)
217 if seconds <= 0.0:
218 return UINT8.pack(0)
220 n = round(math.log(seconds) / math.log(1.1) + 64)
221 n = max(1, min(n, 0xFD))
222 return UINT8.pack(n)