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

1"""Epoch-date template returning ``datetime.date`` for BLE date characteristics. 

2 

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""" 

6 

7from __future__ import annotations 

8 

9from datetime import date, timedelta 

10 

11from ...context import CharacteristicContext 

12from ...exceptions import InsufficientDataError 

13from ..utils.extractors import UINT24, RawExtractor 

14from .base import CodingTemplate 

15 

16_EPOCH = date(1970, 1, 1) 

17 

18 

19class EpochDateTemplate(CodingTemplate[date]): 

20 """Template for epoch-day date characteristics that return ``datetime.date``. 

21 

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). 

24 

25 Pipeline Integration: 

26 bytes → [UINT24 extractor] → day_count → date(1970,1,1) + timedelta(days=…) 

27 

28 Examples: 

29 >>> template = EpochDateTemplate() 

30 >>> template.decode_value(bytearray([0x61, 0x4D, 0x00])) 

31 datetime.date(2024, 2, 19) 

32 """ 

33 

34 @property 

35 def data_size(self) -> int: 

36 """Size: 3 bytes (uint24).""" 

37 return 3 

38 

39 @property 

40 def extractor(self) -> RawExtractor: 

41 """Return uint24 extractor for pipeline access.""" 

42 return UINT24 

43 

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``. 

53 

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). 

59 

60 Returns: 

61 ``datetime.date`` representing the decoded date. 

62 

63 Raises: 

64 InsufficientDataError: If data too short. 

65 

66 """ 

67 if validate and len(data) < offset + self.data_size: 

68 raise InsufficientDataError("EpochDate", data[offset:], self.data_size) 

69 

70 days = UINT24.extract(data, offset) 

71 return _EPOCH + timedelta(days=days) 

72 

73 def encode_value(self, value: date | int, *, validate: bool = True) -> bytearray: 

74 """Encode ``datetime.date`` (or raw day count) to 3 bytes. 

75 

76 Args: 

77 value: ``datetime.date`` or integer day count since epoch. 

78 validate: Whether to validate (default True). 

79 

80 Returns: 

81 Encoded bytes (3 bytes, little-endian). 

82 

83 """ 

84 days = (value - _EPOCH).days if isinstance(value, date) else int(value) 

85 

86 if validate and days < 0: 

87 raise ValueError(f"Date {value} is before epoch (1970-01-01)") 

88 

89 return UINT24.pack(days)