Coverage for src / bluetooth_sig / gatt / characteristics / templates / epoch_date.py: 92%
24 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"""Epoch-date template returning ``datetime.date`` for BLE date characteristics.
3Wraps a 24-bit unsigned integer (days since 1970-01-01) and converts to/from
4:class:`datetime.date` so callers receive a proper Python date type.
5"""
7from __future__ import annotations
9from datetime import date, timedelta
11from ...context import CharacteristicContext
12from ...exceptions import InsufficientDataError
13from ..utils.extractors import UINT24, RawExtractor
14from .base import CodingTemplate
16_EPOCH = date(1970, 1, 1)
19class EpochDateTemplate(CodingTemplate[date]):
20 """Template for epoch-day date characteristics that return ``datetime.date``.
22 The raw wire value is a 24-bit unsigned integer counting the number of
23 days elapsed since 1970-01-01 (the Unix epoch).
25 Pipeline Integration:
26 bytes → [UINT24 extractor] → day_count → date(1970,1,1) + timedelta(days=…)
28 Examples:
29 >>> template = EpochDateTemplate()
30 >>> template.decode_value(bytearray([0x61, 0x4D, 0x00]))
31 datetime.date(2024, 2, 19)
32 """
34 @property
35 def data_size(self) -> int:
36 """Size: 3 bytes (uint24)."""
37 return 3
39 @property
40 def extractor(self) -> RawExtractor:
41 """Return uint24 extractor for pipeline access."""
42 return UINT24
44 def decode_value(
45 self,
46 data: bytearray,
47 offset: int = 0,
48 ctx: CharacteristicContext | None = None,
49 *,
50 validate: bool = True,
51 ) -> date:
52 """Decode 24-bit day count to ``datetime.date``.
54 Args:
55 data: Raw bytes from BLE characteristic.
56 offset: Starting offset in data buffer.
57 ctx: Optional context for parsing.
58 validate: Whether to validate data length (default True).
60 Returns:
61 ``datetime.date`` representing the decoded date.
63 Raises:
64 InsufficientDataError: If data too short.
66 """
67 if validate and len(data) < offset + self.data_size:
68 raise InsufficientDataError("EpochDate", data[offset:], self.data_size)
70 days = UINT24.extract(data, offset)
71 return _EPOCH + timedelta(days=days)
73 def encode_value(self, value: date | int, *, validate: bool = True) -> bytearray:
74 """Encode ``datetime.date`` (or raw day count) to 3 bytes.
76 Args:
77 value: ``datetime.date`` or integer day count since epoch.
78 validate: Whether to validate (default True).
80 Returns:
81 Encoded bytes (3 bytes, little-endian).
83 """
84 days = (value - _EPOCH).days if isinstance(value, date) else int(value)
86 if validate and days < 0:
87 raise ValueError(f"Date {value} is before epoch (1970-01-01)")
89 return UINT24.pack(days)