mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-11 03:12:51 +02:00
fix: throttle only pyMC modem sensor polling
This commit is contained in:
+3
-3
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user