fix: throttle only pyMC modem sensor polling

This commit is contained in:
Yellowcooln
2026-06-15 18:34:05 -04:00
parent f2b7d254a7
commit 1c186d7feb
4 changed files with 63 additions and 29 deletions
+3 -3
View File
@@ -225,9 +225,8 @@ sensors:
# Master switch for sensor subsystem
enabled: true
# Poll interval for all configured sensors. Keep modem-backed sensors at 60s
# or slower so their HTTP stats reads don't contend with modem traffic.
poll_interval_seconds: 60.0
# Poll interval for all configured sensors
poll_interval_seconds: 10.0
# If true, missing Python packages for a sensor may be installed at runtime
# using "python -m pip install ...".
@@ -302,6 +301,7 @@ sensors:
# endpoint: "/api/stats"
# username: "admin"
# password: "" # modem HTTP password; may match pymc_tcp.token
# poll_interval_seconds: 60.0 # avoid contending with modem traffic
# timeout_seconds: 2.0
+50 -22
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import importlib
import logging
import threading
import time
from typing import Any, Dict, List, Optional
from .registry import SensorRegistry
@@ -112,53 +113,80 @@ class SensorManager:
def read_all(self) -> List[Dict[str, Any]]:
readings: List[Dict[str, Any]] = []
for sensor in self.sensors:
try:
readings.append(sensor.read())
except Exception as exc:
self.log.warning("Sensor manager caught read error for %s: %s", sensor.name, exc)
readings.append(
{
"name": sensor.name,
"type": getattr(sensor, "sensor_type", "sensor"),
"ok": False,
"timestamp": None,
"data": {},
"error": f"{type(exc).__name__}: {exc}",
}
)
readings.append(self._read_sensor(sensor))
return readings
def _read_sensor(self, sensor) -> Dict[str, Any]:
try:
return sensor.read()
except Exception as exc:
self.log.warning("Sensor manager caught read error for %s: %s", sensor.name, exc)
return {
"name": sensor.name,
"type": getattr(sensor, "sensor_type", "sensor"),
"ok": False,
"timestamp": None,
"data": {},
"error": f"{type(exc).__name__}: {exc}",
}
@staticmethod
def _sensor_poll_interval(sensor, default_interval: float) -> float:
raw = getattr(sensor, "poll_interval_seconds", default_interval)
try:
interval = float(raw)
except (TypeError, ValueError):
interval = default_interval
return max(0.1, interval)
def _poll_loop(self) -> None:
"""Background thread: poll sensors at configured interval and cache readings."""
section = self.config.get("sensors", {})
poll_interval = 60.0
poll_interval = 30.0
if isinstance(section, dict):
try:
poll_interval = float(section.get("poll_interval_seconds", 60.0))
poll_interval = float(section.get("poll_interval_seconds", 30.0))
except (TypeError, ValueError):
pass
self.log.debug("Sensor polling loop started (interval=%.1f sec)", poll_interval)
next_read_at: Dict[str, float] = {}
latest_by_name: Dict[str, Dict[str, Any]] = {}
while not self._stop_event.is_set():
now = time.monotonic()
next_due_in = poll_interval
try:
readings = self.read_all()
with self._readings_lock:
self._latest_readings = readings
updated = False
for sensor in self.sensors:
name = sensor.name
interval = self._sensor_poll_interval(sensor, poll_interval)
due_at = next_read_at.get(name, 0.0)
if now >= due_at:
latest_by_name[name] = self._read_sensor(sensor)
next_read_at[name] = now + interval
updated = True
next_due_in = min(next_due_in, max(0.0, next_read_at[name] - now))
if updated:
readings = [latest_by_name[s.name] for s in self.sensors if s.name in latest_by_name]
with self._readings_lock:
self._latest_readings = readings
except Exception as exc:
self.log.warning("Sensor poll cycle failed: %s", exc)
# Wait for next poll or stop signal
self._stop_event.wait(poll_interval)
self._stop_event.wait(max(0.1, next_due_in))
self.log.debug("Sensor polling loop stopped")
def get_summary(self) -> Dict[str, Any]:
section = self.config.get("sensors", {})
poll_interval = 60.0
poll_interval = 30.0
if isinstance(section, dict):
try:
poll_interval = float(section.get("poll_interval_seconds", 60.0))
poll_interval = float(section.get("poll_interval_seconds", 30.0))
except (TypeError, ValueError):
pass
+1
View File
@@ -19,6 +19,7 @@ class PymcModemSensor(SensorBase):
def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, log=None):
super().__init__(name=name, config=config, log=log)
self.poll_interval_seconds = float(self.settings.get("poll_interval_seconds", 60.0))
self.timeout_seconds = float(self.settings.get("timeout_seconds", 2.0))
self.endpoint = str(self.settings.get("endpoint", "/api/stats") or "/api/stats")
self.url = self._build_url()
+9 -4
View File
@@ -108,12 +108,17 @@ def test_sensor_manager_summary_reflects_sensor_config():
assert summary["loaded"] == 1
def test_sensor_manager_default_poll_interval_is_sixty_seconds():
config = {"sensors": {"enabled": True, "definitions": []}}
def test_pymc_modem_sensor_defaults_to_sixty_second_poll_interval():
sensor = PymcModemSensor("modem", {"settings": {"host": "192.168.0.205"}})
manager = SensorManager(config, registry=_TestRegistry)
assert sensor.poll_interval_seconds == 60.0
assert manager.get_summary()["poll_interval_seconds"] == 60.0
def test_sensor_manager_uses_sensor_specific_poll_interval():
sensor = _DummySensor("demo", {"settings": {"value": 7}})
setattr(sensor, "poll_interval_seconds", 60.0)
assert SensorManager._sensor_poll_interval(sensor, 10.0) == 60.0
def test_sensor_manager_loads_and_reads_sensors_without_stopping_on_failure():