mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-10-31 04:01:59 +01:00
fix(companion): report real battery voltage in core stats
`_get_companion_stats` hardcoded `battery_mv: 0`, so every MeshCore companion client connected to a Python repeater showed no battery, even when a sensor plug-in was reporting one. The protocol side is already complete: the frame server packs `battery_mv` into the STATS_TYPE_CORE frame and clients request it via CMD_GET_BATT_AND_STORAGE. Surface the first sensor reading that publishes a usable battery voltage, accepting either `battery_voltage_mv` or `battery_voltage_v`. This covers an attached UPS HAT as well as the battery of an openHop modem the repeater is driving, which is the case that prompted this: a solar-powered modem whose battery was invisible to the companion app right up until it went flat. Returns 0 when no sensor reports a battery, which is what clients already expect from a mains-powered node, and the lookup never raises — stats must not be able to take down the companion connection.
This commit is contained in:
1 parent
ca3d4ecb8f
commit
0a2eeea8ee
2 files changed
+125
-1
No files matched your search
+47
-1
@@ -1441,6 +1441,52 @@ class RepeaterDaemon:
|
||||
|
||||
return stats
|
||||
|
||||
def _companion_battery_mv(self) -> int:
|
||||
"""Battery voltage in mV for the companion core-stats frame.
|
||||
|
||||
MeshCore companion clients show the battery of the device they are
|
||||
connected to. A Python repeater has no battery of its own, but a sensor
|
||||
plug-in often reports one -- an attached UPS HAT, or the battery of an
|
||||
openHop modem the repeater is driving -- so surface the first sensor
|
||||
publishing a usable voltage instead of always reporting 0.
|
||||
|
||||
Returns 0 when nothing is available, which is what clients already
|
||||
expect for a mains-powered node.
|
||||
"""
|
||||
if not self.sensor_manager:
|
||||
return 0
|
||||
try:
|
||||
readings = (self.sensor_manager.get_summary() or {}).get("readings") or []
|
||||
except Exception: # pragma: no cover - defensive; stats must never raise
|
||||
logger.debug("companion battery lookup failed", exc_info=True)
|
||||
return 0
|
||||
|
||||
for reading in readings:
|
||||
if not isinstance(reading, dict) or not reading.get("ok"):
|
||||
continue
|
||||
data = reading.get("data")
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
millivolts = data.get("battery_voltage_mv")
|
||||
if millivolts is None:
|
||||
volts = data.get("battery_voltage_v")
|
||||
if volts is None:
|
||||
continue
|
||||
try:
|
||||
millivolts = float(volts) * 1000.0
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
try:
|
||||
millivolts = round(float(millivolts))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
# The core-stats frame packs battery_mv as an unsigned 16-bit int.
|
||||
if 0 < millivolts <= 0xFFFF:
|
||||
return millivolts
|
||||
return 0
|
||||
|
||||
async def _get_companion_stats(self, stats_type: int) -> dict:
|
||||
"""Return stats dict for companion CMD_GET_STATS (format expected by frame_server + meshcore_py)."""
|
||||
from repeater.companion.constants import (
|
||||
@@ -1459,7 +1505,7 @@ class RepeaterDaemon:
|
||||
queue_len += getattr(getattr(bridge, "message_queue", None), "count", 0) or 0
|
||||
if stats_type == STATS_TYPE_CORE:
|
||||
return {
|
||||
"battery_mv": 0,
|
||||
"battery_mv": self._companion_battery_mv(),
|
||||
"uptime_secs": uptime_secs,
|
||||
"errors": 0,
|
||||
"queue_len": min(255, queue_len),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Companion core-stats battery reporting.
|
||||
|
||||
The MeshCore companion frame packs ``battery_mv``; clients show it as the
|
||||
connected device's battery. The repeater used to hardcode 0, so companion apps
|
||||
always showed no battery even when a sensor plug-in was reporting one.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from repeater.main import RepeaterDaemon
|
||||
|
||||
|
||||
def _base_config():
|
||||
return {
|
||||
"repeater": {
|
||||
"node_name": "node-test",
|
||||
"mode": "forward",
|
||||
"latitude": 1.0,
|
||||
"longitude": 2.0,
|
||||
},
|
||||
"logging": {"level": "INFO"},
|
||||
}
|
||||
|
||||
|
||||
def _daemon(readings=None):
|
||||
daemon = RepeaterDaemon(_base_config(), radio=object())
|
||||
daemon.sensor_manager = (
|
||||
None if readings is None else SimpleNamespace(get_summary=lambda: {"readings": readings})
|
||||
)
|
||||
return daemon
|
||||
|
||||
|
||||
def _reading(name, data, ok=True):
|
||||
return {"name": name, "type": name, "ok": ok, "timestamp": "t", "data": data}
|
||||
|
||||
|
||||
def test_returns_zero_without_a_sensor_manager():
|
||||
assert _daemon()._companion_battery_mv() == 0
|
||||
|
||||
|
||||
def test_reads_battery_voltage_mv():
|
||||
daemon = _daemon([_reading("modem", {"battery_voltage_mv": 4213})])
|
||||
assert daemon._companion_battery_mv() == 4213
|
||||
|
||||
|
||||
def test_derives_millivolts_from_volts():
|
||||
daemon = _daemon([_reading("modem", {"battery_voltage_v": 3.72})])
|
||||
assert daemon._companion_battery_mv() == 3720
|
||||
|
||||
|
||||
def test_skips_failed_and_batteryless_readings():
|
||||
daemon = _daemon(
|
||||
[
|
||||
_reading("broken", {"battery_voltage_mv": 4100}, ok=False),
|
||||
_reading("system-health", {"cpu_percent": 12.0}),
|
||||
_reading("modem", {"battery_voltage_mv": 3980}),
|
||||
]
|
||||
)
|
||||
assert daemon._companion_battery_mv() == 3980
|
||||
|
||||
|
||||
def test_ignores_out_of_range_and_unparseable_values():
|
||||
for data in (
|
||||
{"battery_voltage_mv": 0},
|
||||
{"battery_voltage_mv": 70000},
|
||||
{"battery_voltage_mv": "n/a"},
|
||||
{"battery_voltage_v": None},
|
||||
):
|
||||
assert _daemon([_reading("modem", data)])._companion_battery_mv() == 0
|
||||
|
||||
|
||||
def test_never_raises_when_the_sensor_manager_misbehaves():
|
||||
def boom():
|
||||
raise RuntimeError("sensor manager exploded")
|
||||
|
||||
daemon = RepeaterDaemon(_base_config(), radio=object())
|
||||
daemon.sensor_manager = SimpleNamespace(get_summary=boom)
|
||||
assert daemon._companion_battery_mv() == 0
|
||||
Reference in new issue
Block a user