From 40a4e4f245a36f0e1b1704bf5988598fa58d379a Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:55:48 -0400 Subject: [PATCH] fix(sensors): normalize modem power telemetry --- repeater/handler_helpers/protocol_request.py | 7 ++- repeater/sensors/openhop_modem.py | 29 +++++++-- ...test_handler_helpers_path_protocol_text.py | 35 +++++++++++ tests/test_openhop_modem_sensor.py | 63 +++++++++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py index b537987..13882c4 100644 --- a/repeater/handler_helpers/protocol_request.py +++ b/repeater/handler_helpers/protocol_request.py @@ -358,8 +358,11 @@ class ProtocolRequestHelper: power = data.get("power_mw") if power is not None: try: - out.extend(encode_power(channel, int(float(power) / 1000.0))) - except (TypeError, ValueError): + power_mw = float(power) + watts = int(power_mw / 1000.0) + if power_mw >= 0.0 and watts <= 0xFFFF: + out.extend(encode_power(channel, watts)) + except (TypeError, ValueError, OverflowError): pass return bytes(out) diff --git a/repeater/sensors/openhop_modem.py b/repeater/sensors/openhop_modem.py index 4883ec3..ea61818 100644 --- a/repeater/sensors/openhop_modem.py +++ b/repeater/sensors/openhop_modem.py @@ -197,17 +197,38 @@ class OpenHopModemSensor(SensorBase): "battery_percent", "battery_percentage", "solar_charge_rate_percent_per_hour", - "bus_voltage_v", - "current_ma", - "power_mw", ): if key in payload: out[key] = payload[key] - for key in ("battery_voltage_mv", "battery_voltage_v", "current_ma", "power_mw"): + for key in ("bus_voltage_v", "current_ma", "power_mw"): + value = self._float(payload.get(key)) + if value is not None: + out[key] = value + + for key in ("battery_voltage_mv", "battery_voltage_v"): if out.get(key) is None and system.get(key) is not None: out[key] = system[key] + station_g3_fallbacks = { + "bus_voltage_v": ("bus_voltage_v", "station_g3_input_voltage_v"), + "current_ma": ("current_ma", "station_g3_current_ma"), + "power_mw": ("power_mw",), + } + for output_key, system_keys in station_g3_fallbacks.items(): + if out.get(output_key) is not None: + continue + for system_key in system_keys: + value = self._float(system.get(system_key)) + if value is not None: + out[output_key] = value + break + + if out.get("power_mw") is None: + power_w = self._float(system.get("station_g3_power_w")) + if power_w is not None: + out["power_mw"] = power_w * 1000.0 + if "battery_percent" not in out: battery_voltage_v = self._float(out.get("battery_voltage_v")) if battery_voltage_v is None: diff --git a/tests/test_handler_helpers_path_protocol_text.py b/tests/test_handler_helpers_path_protocol_text.py index 27bc271..1c74c38 100644 --- a/tests/test_handler_helpers_path_protocol_text.py +++ b/tests/test_handler_helpers_path_protocol_text.py @@ -774,6 +774,41 @@ def test_telemetry_admin_full_mask_includes_environment_sensors(): assert lpp == expected +def test_station_g3_subwatt_power_uses_lpp_one_watt_resolution(): + sm = _FakeSensorManager([_reading(bus_voltage_v=12.46, current_ma=59.7, power_mw=744.0)]) + helper = ProtocolRequestHelper( + identity_manager=MagicMock(), packet_injector=AsyncMock(), sensor_manager=sm + ) + admin = SimpleNamespace(is_guest=lambda: False) + + lpp = helper._handle_get_telemetry(admin, 0, b"\x00") + + expected = ( + encode_voltage(TELEM_CHANNEL_SELF, 12.46) + + encode_voltage(TELEM_CHANNEL_SELF + 1, 12.46) + + encode_current(TELEM_CHANNEL_SELF + 1, 0.0597) + + encode_power(TELEM_CHANNEL_SELF + 1, 0) + ) + assert lpp == expected + + +def test_negative_power_is_preserved_as_current_but_omitted_from_unsigned_lpp_power(): + sm = _FakeSensorManager([_reading(bus_voltage_v=12.46, current_ma=-59.7, power_mw=-744.0)]) + helper = ProtocolRequestHelper( + identity_manager=MagicMock(), packet_injector=AsyncMock(), sensor_manager=sm + ) + admin = SimpleNamespace(is_guest=lambda: False) + + lpp = helper._handle_get_telemetry(admin, 0, b"\x00") + + expected = ( + encode_voltage(TELEM_CHANNEL_SELF, 12.46) + + encode_voltage(TELEM_CHANNEL_SELF + 1, 12.46) + + encode_current(TELEM_CHANNEL_SELF + 1, -0.0597) + ) + assert lpp == expected + + def test_telemetry_bme280_emits_temperature_humidity_and_pressure_on_one_channel(): """A BME280-shaped reading emits all three measured values on one channel. diff --git a/tests/test_openhop_modem_sensor.py b/tests/test_openhop_modem_sensor.py index be2f2c9..d560dc0 100644 --- a/tests/test_openhop_modem_sensor.py +++ b/tests/test_openhop_modem_sensor.py @@ -137,6 +137,69 @@ def test_openhop_modem_sensor_accepts_legacy_token_flag(): assert data["token_configured"] is True +def test_openhop_modem_sensor_prefers_top_level_power_telemetry(): + sensor = OpenHopModemSensor("modem", {"settings": {"base_url": "http://modem.local"}}) + + data = sensor._normalize_payload( + { + "bus_voltage_v": 12.46, + "current_ma": 59.7, + "power_mw": 744.0, + "system": { + "station_g3_input_voltage_v": 11.0, + "station_g3_current_ma": 50.0, + "station_g3_power_w": 0.55, + }, + } + ) + + assert data["bus_voltage_v"] == 12.46 + assert data["current_ma"] == 59.7 + assert data["power_mw"] == 744.0 + + +def test_openhop_modem_sensor_falls_back_to_legacy_station_g3_power_fields(): + sensor = OpenHopModemSensor("modem", {"settings": {"base_url": "http://modem.local"}}) + + data = sensor._normalize_payload( + { + "bus_voltage_v": None, + "current_ma": None, + "power_mw": None, + "system": { + "station_g3_input_voltage_v": "12.46", + "station_g3_current_ma": "-59.7", + "station_g3_power_w": "-0.744", + }, + } + ) + + assert data["bus_voltage_v"] == 12.46 + assert data["current_ma"] == -59.7 + assert data["power_mw"] == -744.0 + + +def test_openhop_modem_sensor_omits_invalid_power_telemetry(): + sensor = OpenHopModemSensor("modem", {"settings": {"base_url": "http://modem.local"}}) + + data = sensor._normalize_payload( + { + "bus_voltage_v": "invalid", + "current_ma": "invalid", + "power_mw": "invalid", + "system": { + "station_g3_input_voltage_v": None, + "station_g3_current_ma": None, + "station_g3_power_w": None, + }, + } + ) + + assert "bus_voltage_v" not in data + assert "current_ma" not in data + assert "power_mw" not in data + + def test_legacy_pymc_modem_module_and_registry_create_canonical_sensor(): assert PymcModemSensor is OpenHopModemSensor legacy = SensorRegistry.create(