From 1881ad9b933fbf82a387b103bd91f7169f16c232 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:39:22 -0400 Subject: [PATCH] feat: derive modem battery percent from voltage --- repeater/sensors/pymc_modem.py | 33 +++++++++++++++++++++++++++++++++ tests/test_sensors.py | 2 ++ 2 files changed, 35 insertions(+) diff --git a/repeater/sensors/pymc_modem.py b/repeater/sensors/pymc_modem.py index 6a3c6c7..39f3b4a 100644 --- a/repeater/sensors/pymc_modem.py +++ b/repeater/sensors/pymc_modem.py @@ -11,6 +11,23 @@ from .base import SensorBase from .registry import SensorRegistry +def _single_cell_voltage_to_percent(voltage_v: float) -> int: + """Piecewise linear SoC estimate for a single Li-ion/LiPo cell.""" + if voltage_v >= 4.20: + return 100 + if voltage_v >= 4.00: + return int(85 + (voltage_v - 4.00) / 0.20 * 15) + if voltage_v >= 3.80: + return int(60 + (voltage_v - 3.80) / 0.20 * 25) + if voltage_v >= 3.70: + return int(40 + (voltage_v - 3.70) / 0.10 * 20) + if voltage_v >= 3.50: + return int(15 + (voltage_v - 3.50) / 0.20 * 25) + if voltage_v >= 3.00: + return int((voltage_v - 3.00) / 0.50 * 15) + return 0 + + @SensorRegistry.register("pymc_modem") class PymcModemSensor(SensorBase): """Read diagnostics exposed by a pyMC modem HTTP API.""" @@ -126,13 +143,29 @@ class PymcModemSensor(SensorBase): for key in ( "battery_voltage_mv", "battery_voltage_v", + "battery_percent", + "battery_percentage", "solar_charge_rate_percent_per_hour", ): if key in payload: out[key] = payload[key] + if "battery_percent" not in out: + battery_voltage_v = self._battery_voltage_v(payload) + if battery_voltage_v is not None: + out["battery_percent"] = _single_cell_voltage_to_percent(battery_voltage_v) + return {key: value for key, value in out.items() if value is not None} + def _battery_voltage_v(self, payload: Dict[str, Any]) -> Optional[float]: + voltage_v = self._float(payload.get("battery_voltage_v")) + if voltage_v is not None: + return voltage_v + voltage_mv = self._float(payload.get("battery_voltage_mv")) + if voltage_mv is not None: + return voltage_mv / 1000.0 + return None + @staticmethod def _first_dict(*values: Any) -> Dict[str, Any]: for value in values: diff --git a/tests/test_sensors.py b/tests/test_sensors.py index d060fea..288677c 100644 --- a/tests/test_sensors.py +++ b/tests/test_sensors.py @@ -225,6 +225,7 @@ def test_pymc_modem_sensor_reads_modem_stats(monkeypatch): assert reading["data"]["satellites_in_view"] == 14 assert reading["data"]["datetime_utc"] == "2026-06-14T18:25:30+00:00" assert reading["data"]["battery_voltage_mv"] == 4112 + assert reading["data"]["battery_percent"] == 93 def test_pymc_modem_sensor_accepts_stats_without_gps_coordinates(monkeypatch): @@ -264,6 +265,7 @@ def test_pymc_modem_sensor_accepts_stats_without_gps_coordinates(monkeypatch): assert reading["data"]["source"] == "pymc_modem" assert reading["data"]["battery_voltage_mv"] == 3681 assert reading["data"]["battery_voltage_v"] == 3.681 + assert reading["data"]["battery_percent"] == 37 assert reading["data"]["solar_charge_rate_percent_per_hour"] == 9.568 assert reading["data"]["gps_enabled"] is True assert reading["data"]["gps_seen"] is False