Coverage for src / bluetooth_sig / gatt / characteristics / event_statistics.py: 93%
55 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"""Event Statistics characteristic implementation."""
3from __future__ import annotations
5import math
7import msgspec
9from ..constants import UINT16_MAX
10from ..context import CharacteristicContext
11from .base import BaseCharacteristic
12from .utils import DataParser
14_TIME_EXP_BASE = 1.1
15_TIME_EXP_OFFSET = 64
18def _decode_time_exponential(raw: int) -> float:
19 """Decode Time Exponential 8 raw value to seconds."""
20 if raw == 0:
21 return 0.0
22 return _TIME_EXP_BASE ** (raw - _TIME_EXP_OFFSET)
25def _encode_time_exponential(seconds: float) -> int:
26 """Encode seconds to Time Exponential 8 raw value."""
27 if seconds <= 0.0:
28 return 0
29 n = round(math.log(seconds) / math.log(_TIME_EXP_BASE) + _TIME_EXP_OFFSET)
30 return max(1, min(n, 0xFD))
33class EventStatisticsData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
34 """Data class for event statistics.
36 Event count (uint16), average event duration (uint16, seconds),
37 time elapsed since last event (Time Exponential 8, seconds),
38 and sensing duration (Time Exponential 8, seconds).
39 """
41 number_of_events: int # Count 16 (unitless)
42 average_event_duration: int # Time Second 16 (seconds, integer)
43 time_elapsed_since_last_event: float # Time Exponential 8 (seconds)
44 sensing_duration: float # Time Exponential 8 (seconds)
46 def __post_init__(self) -> None:
47 """Validate data fields."""
48 if not 0 <= self.number_of_events <= UINT16_MAX:
49 raise ValueError(f"Number of events {self.number_of_events} is outside valid range (0 to {UINT16_MAX})")
50 if not 0 <= self.average_event_duration <= UINT16_MAX:
51 raise ValueError(
52 f"Average event duration {self.average_event_duration} s is outside valid range (0 to {UINT16_MAX})"
53 )
54 if self.time_elapsed_since_last_event < 0.0:
55 raise ValueError(f"Time elapsed {self.time_elapsed_since_last_event} s cannot be negative")
56 if self.sensing_duration < 0.0:
57 raise ValueError(f"Sensing duration {self.sensing_duration} s cannot be negative")
60class EventStatisticsCharacteristic(BaseCharacteristic[EventStatisticsData]):
61 """Event Statistics characteristic (0x2AF4).
63 org.bluetooth.characteristic.event_statistics
65 Statistics for events: count (uint16), average duration (uint16, 1 s),
66 time since last event (Time Exponential 8), sensing duration
67 (Time Exponential 8).
68 """
70 expected_length: int = 6 # 2 x uint16 + 2 x uint8
71 min_length: int = 6
72 expected_type = EventStatisticsData
74 def _decode_value(
75 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
76 ) -> EventStatisticsData:
77 """Parse event statistics.
79 Args:
80 data: Raw bytes (6 bytes).
81 ctx: Optional CharacteristicContext.
82 validate: Whether to validate ranges (default True).
84 Returns:
85 EventStatisticsData.
87 """
88 count = DataParser.parse_int16(data, 0, signed=False)
89 avg_dur = DataParser.parse_int16(data, 2, signed=False)
90 elapsed_raw = DataParser.parse_int8(data, 4, signed=False)
91 duration_raw = DataParser.parse_int8(data, 5, signed=False)
93 return EventStatisticsData(
94 number_of_events=count,
95 average_event_duration=avg_dur,
96 time_elapsed_since_last_event=_decode_time_exponential(elapsed_raw),
97 sensing_duration=_decode_time_exponential(duration_raw),
98 )
100 def _encode_value(self, data: EventStatisticsData) -> bytearray:
101 """Encode event statistics.
103 Args:
104 data: EventStatisticsData instance.
106 Returns:
107 Encoded bytes (6 bytes).
109 """
110 elapsed_raw = _encode_time_exponential(data.time_elapsed_since_last_event)
111 duration_raw = _encode_time_exponential(data.sensing_duration)
113 if not 0 <= data.number_of_events <= UINT16_MAX:
114 raise ValueError(f"Event count {data.number_of_events} exceeds uint16 range")
115 if not 0 <= data.average_event_duration <= UINT16_MAX:
116 raise ValueError(f"Average duration {data.average_event_duration} exceeds uint16 range")
118 result = bytearray()
119 result.extend(DataParser.encode_int16(data.number_of_events, signed=False))
120 result.extend(DataParser.encode_int16(data.average_event_duration, signed=False))
121 result.extend(DataParser.encode_int8(elapsed_raw, signed=False))
122 result.extend(DataParser.encode_int8(duration_raw, signed=False))
123 return result