mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-10-30 19:51:59 +01:00
fix(companion): report battery and storage to companion clients
The previous commit fixed battery_mv in the STATS_TYPE_CORE frame, but that is not the path companion apps use to display battery. CMD_GET_BATT_AND_STORAGE goes through `CompanionFrameServer._get_batt_and_storage()`, an openhop_core extension hook documented as "Default: all zeros" that the repeater never overrode — so apps showed 0% regardless of what any sensor reported. Override it in the repeater's frame server: battery from the same sensor lookup added for core stats, storage from the filesystem holding the configured storage_dir. Both are wired in from main.py alongside stats_getter. Found by deploying the previous fix and observing that Home Assistant showed the modem battery correctly while the phone app still showed 0% — two different paths, only one of which had been fixed.
This commit is contained in:
1 parent
0a2eeea8ee
commit
a3f8ec18e7
3 files changed
+85
-1
No files matched your search
@@ -10,7 +10,9 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
import os
|
||||
import shutil
|
||||
from typing import Callable, Optional
|
||||
|
||||
from openhop_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
|
||||
from openhop_core.companion.frame_server import CompanionFrameServer as _BaseFrameServer
|
||||
@@ -38,6 +40,8 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
local_hash: Optional[int] = None,
|
||||
stats_getter=None,
|
||||
control_handler=None,
|
||||
batt_getter: Optional[Callable[[], int]] = None,
|
||||
storage_dir: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
bridge=bridge,
|
||||
@@ -53,6 +57,37 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
control_handler=control_handler,
|
||||
)
|
||||
self.sqlite_handler = sqlite_handler
|
||||
self.batt_getter = batt_getter
|
||||
self.storage_dir = storage_dir
|
||||
|
||||
def _get_batt_and_storage(self) -> tuple[int, int, int]:
|
||||
"""Report battery millivolts and storage usage to companion clients.
|
||||
|
||||
The base-class hook returns zeros, so companion apps show no battery and
|
||||
no storage for a Python repeater. Battery comes from whichever sensor
|
||||
plug-in publishes one (an attached UPS HAT, or the battery of an
|
||||
openHop modem the repeater drives); storage is the filesystem holding
|
||||
the repeater's data directory.
|
||||
"""
|
||||
millivolts = 0
|
||||
if self.batt_getter is not None:
|
||||
try:
|
||||
millivolts = int(self.batt_getter() or 0)
|
||||
except Exception: # pragma: no cover - never break the companion link
|
||||
logger.debug("battery lookup failed", exc_info=True)
|
||||
millivolts = 0
|
||||
if millivolts < 0 or millivolts > 0xFFFF:
|
||||
millivolts = 0
|
||||
|
||||
used_kb = total_kb = 0
|
||||
if self.storage_dir:
|
||||
try:
|
||||
usage = shutil.disk_usage(self.storage_dir)
|
||||
total_kb = min(usage.total // 1024, 0xFFFFFFFF)
|
||||
used_kb = min((usage.total - usage.free) // 1024, 0xFFFFFFFF)
|
||||
except OSError:
|
||||
logger.debug("storage lookup failed for %s", self.storage_dir, exc_info=True)
|
||||
return millivolts, used_kb, total_kb
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start persistence before accepting companion client connections."""
|
||||
|
||||
@@ -957,6 +957,8 @@ class RepeaterDaemon:
|
||||
sqlite_handler=sqlite_handler,
|
||||
local_hash=self.local_hash,
|
||||
stats_getter=self._get_companion_stats,
|
||||
batt_getter=self._companion_battery_mv,
|
||||
storage_dir=self._companion_storage_dir(),
|
||||
control_handler=(
|
||||
self.discovery_helper.control_handler if self.discovery_helper else None
|
||||
),
|
||||
@@ -1198,6 +1200,8 @@ class RepeaterDaemon:
|
||||
sqlite_handler=sqlite_handler,
|
||||
local_hash=self.local_hash,
|
||||
stats_getter=self._get_companion_stats,
|
||||
batt_getter=self._companion_battery_mv,
|
||||
storage_dir=self._companion_storage_dir(),
|
||||
control_handler=(
|
||||
self.discovery_helper.control_handler if self.discovery_helper else None
|
||||
),
|
||||
@@ -1441,6 +1445,14 @@ class RepeaterDaemon:
|
||||
|
||||
return stats
|
||||
|
||||
def _companion_storage_dir(self) -> "str | None":
|
||||
"""Filesystem path companion clients should see storage figures for."""
|
||||
try:
|
||||
path = (self.config.get("storage", {}) or {}).get("storage_dir")
|
||||
return path if path and os.path.isdir(path) else None
|
||||
except Exception: # pragma: no cover - defensive
|
||||
return None
|
||||
|
||||
def _companion_battery_mv(self) -> int:
|
||||
"""Battery voltage in mV for the companion core-stats frame.
|
||||
|
||||
|
||||
@@ -76,3 +76,40 @@ def test_never_raises_when_the_sensor_manager_misbehaves():
|
||||
daemon = RepeaterDaemon(_base_config(), radio=object())
|
||||
daemon.sensor_manager = SimpleNamespace(get_summary=boom)
|
||||
assert daemon._companion_battery_mv() == 0
|
||||
|
||||
|
||||
def test_frame_server_reports_battery_and_storage(tmp_path):
|
||||
"""The companion BATT_AND_STORAGE hook must return real values, not zeros."""
|
||||
from repeater.companion.frame_server import CompanionFrameServer
|
||||
|
||||
fs = CompanionFrameServer.__new__(CompanionFrameServer)
|
||||
fs.batt_getter = lambda: 4221
|
||||
fs.storage_dir = str(tmp_path)
|
||||
|
||||
mv, used_kb, total_kb = fs._get_batt_and_storage()
|
||||
assert mv == 4221
|
||||
assert total_kb > 0
|
||||
assert 0 <= used_kb <= total_kb
|
||||
|
||||
|
||||
def test_frame_server_battery_survives_a_broken_getter(tmp_path):
|
||||
from repeater.companion.frame_server import CompanionFrameServer
|
||||
|
||||
fs = CompanionFrameServer.__new__(CompanionFrameServer)
|
||||
|
||||
def boom():
|
||||
raise RuntimeError("sensor exploded")
|
||||
|
||||
fs.batt_getter = boom
|
||||
fs.storage_dir = str(tmp_path)
|
||||
mv, _, _ = fs._get_batt_and_storage()
|
||||
assert mv == 0
|
||||
|
||||
|
||||
def test_frame_server_without_getters_returns_zeros():
|
||||
from repeater.companion.frame_server import CompanionFrameServer
|
||||
|
||||
fs = CompanionFrameServer.__new__(CompanionFrameServer)
|
||||
fs.batt_getter = None
|
||||
fs.storage_dir = None
|
||||
assert fs._get_batt_and_storage() == (0, 0, 0)
|
||||
Reference in new issue
Block a user