bugfixes
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import statistics
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
@@ -42,6 +43,37 @@ class Device:
|
||||
default_factory=lambda: deque(maxlen=500)
|
||||
)
|
||||
_last_count_at: float = 0.0 # for de-duplicating multi-meter DLL bursts
|
||||
# arrival times of counted telegrams, for estimating the send interval
|
||||
rx_times: Deque[float] = field(default_factory=lambda: deque(maxlen=64))
|
||||
|
||||
@property
|
||||
def intervals(self) -> List[float]:
|
||||
t = list(self.rx_times)
|
||||
return [b - a for a, b in zip(t, t[1:])]
|
||||
|
||||
@property
|
||||
def interval_samples(self) -> int:
|
||||
return max(0, len(self.rx_times) - 1)
|
||||
|
||||
@property
|
||||
def interval_estimate(self) -> Optional[float]:
|
||||
"""Best estimate of the transmit period: median of observed gaps.
|
||||
|
||||
Median is robust to missed telegrams (which show up as gaps that are
|
||||
multiples of the true period) and to the odd retransmission burst.
|
||||
"""
|
||||
gaps = self.intervals
|
||||
return statistics.median(gaps) if gaps else None
|
||||
|
||||
@property
|
||||
def interval_last(self) -> Optional[float]:
|
||||
gaps = self.intervals
|
||||
return gaps[-1] if gaps else None
|
||||
|
||||
@property
|
||||
def interval_min(self) -> Optional[float]:
|
||||
gaps = self.intervals
|
||||
return min(gaps) if gaps else None
|
||||
|
||||
@property
|
||||
def encryption(self) -> str:
|
||||
@@ -113,6 +145,20 @@ class DeviceStore:
|
||||
def ordered(self) -> List[Device]:
|
||||
return [self.devices[i] for i in self.order]
|
||||
|
||||
@staticmethod
|
||||
def _note_telegram(dev: Device, now: float) -> None:
|
||||
"""Count one telegram and record its arrival, de-duplicating the
|
||||
multi-line burst a single telegram produces (DLL[+DLL]+ELL+TPL+JSON,
|
||||
all within microseconds) so counts and the interval stay accurate.
|
||||
|
||||
Driven by *any* telegram-level event, so a DLL line we fail to parse
|
||||
can't silently zero out the count and send-interval.
|
||||
"""
|
||||
if dev._last_count_at == 0.0 or (now - dev._last_count_at) > COUNT_DEDUP_WINDOW:
|
||||
dev.count += 1
|
||||
dev.rx_times.append(now)
|
||||
dev._last_count_at = now
|
||||
|
||||
def _get_or_create(self, device_id: str, now: float) -> Tuple[Device, bool]:
|
||||
dev = self.devices.get(device_id)
|
||||
if dev is None:
|
||||
@@ -134,11 +180,7 @@ class DeviceStore:
|
||||
if ev.get("rssi") is not None:
|
||||
dev.rssi = ev["rssi"]
|
||||
dev.last_seen = now
|
||||
# Count once per telegram, ignoring the duplicate DLL burst from a
|
||||
# second matching meter.
|
||||
if is_new or (now - dev._last_count_at) > COUNT_DEDUP_WINDOW:
|
||||
dev.count += 1
|
||||
dev._last_count_at = now
|
||||
self._note_telegram(dev, now)
|
||||
return dev, is_new
|
||||
|
||||
def apply_ell(self, ev: dict, device_id: Optional[str]) -> Optional[Device]:
|
||||
@@ -192,4 +234,8 @@ class DeviceStore:
|
||||
if from_key:
|
||||
dev.key_decrypted = True
|
||||
dev.last_seen = now
|
||||
# Also drive the counter from JSON, so telegrams still count even if
|
||||
# their DLL line failed to parse. The dedup window collapses the DLL
|
||||
# and its JSON (same telegram) into one.
|
||||
self._note_telegram(dev, now)
|
||||
return dev, is_new
|
||||
|
||||
Reference in New Issue
Block a user