From 6cb3166a3a8564a5885d30b3926e519316566238 Mon Sep 17 00:00:00 2001 From: lbibass <27320050+lbibass@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:08:55 -0400 Subject: [PATCH 1/3] enable battery percentage on repeater stats page --- repeater/handler_helpers/protocol_request.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py index 03e30ce..77a8e14 100644 --- a/repeater/handler_helpers/protocol_request.py +++ b/repeater/handler_helpers/protocol_request.py @@ -166,6 +166,10 @@ class ProtocolRequestHelper: # n_sent_flood, n_sent_direct, n_recv_flood, n_recv_direct, # uint16 err_events, int16 last_snr (×4), uint16 n_direct_dups, n_flood_dups, # uint32 total_rx_air_time_secs, n_recv_errors → 56 bytes + + # Battery Readings: uses first configured sensor that reports bus/pack voltage. + readings = self._get_sensor_readings() + batt = int(min(max(self._battery_voltage(readings) * 1000, 0), 0xFFFF)) # Uptime: use engine start_time when available (fixes wrong "20521 days" from time.time()) if self.engine and hasattr(self.engine, "start_time"): @@ -223,7 +227,7 @@ class ProtocolRequestHelper: # Pack 56-byte RepeaterStats (layout matches firmware) stats = struct.pack( " bytes: From 30fe28fb6a44638ab2127071fe9316c756692ccf Mon Sep 17 00:00:00 2001 From: lbibass <27320050+lbibass@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:40:20 -0400 Subject: [PATCH 2/3] added current and power to telemetry page. --- repeater/handler_helpers/protocol_request.py | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py index 77a8e14..296e773 100644 --- a/repeater/handler_helpers/protocol_request.py +++ b/repeater/handler_helpers/protocol_request.py @@ -23,6 +23,8 @@ from openhop_core.protocol.cayenne_lpp import ( encode_relative_humidity, encode_temperature, encode_voltage, + encode_current, + encode_power ) from openhop_core.protocol.constants import TELEM_PERM_ENVIRONMENT @@ -327,7 +329,32 @@ class ProtocolRequestHelper: except (TypeError, ValueError): continue return 0.0 - + def _battery_current(readings) -> float: + """First configured sensor's current, else 0.000A.""" + for reading in readings: + if not reading.get("ok"): + continue + data = reading.get("data") or {} + current = data.get("current_ma") + if current is not None: + try: + return float(current) + except (TypeError, ValueError): + continue + return 0.000 + def _battery_power(readings) -> int: + """First configured sensor's power, else 0W.""" + for reading in readings: + if not reading.get("ok"): + continue + data = reading.get("data") or {} + power = data.get("power_mw")/1000 + if power is not None: + try: + return int(power) + except (TypeError, ValueError): + continue + return 0 @staticmethod def _encode_environment_reading(channel: int, reading) -> bytes: From b955f1d0a3eda03e4377b7c65fb1971cb64f513a Mon Sep 17 00:00:00 2001 From: lbibass <27320050+lbibass@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:41:01 -0400 Subject: [PATCH 3/3] added current and power to telemetry page --- repeater/handler_helpers/protocol_request.py | 38 +++++++++++++--- ...test_handler_helpers_path_protocol_text.py | 44 ++++++++++++++++--- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py index 296e773..12aa6e5 100644 --- a/repeater/handler_helpers/protocol_request.py +++ b/repeater/handler_helpers/protocol_request.py @@ -287,10 +287,19 @@ class ProtocolRequestHelper: # else 0.0 V (voltage-only floor, mirroring the companion self-telemetry). lpp = bytearray(encode_voltage(TELEM_CHANNEL_SELF, self._battery_voltage(readings))) - # Environment sensors: each gets the next channel starting at 2, in the - # order they are configured (firmware querySensors channel assignment). + # INA219 power telemetry and environment sensors: the power telemetry + # channels are emitted first, then environment sensors continue on the + # next available channel. if perm_mask & TELEM_PERM_ENVIRONMENT: channel = TELEM_CHANNEL_SELF + 1 + if self._has_sensor_data(readings, "bus_voltage_v"): + lpp.extend(encode_voltage(channel, self._battery_voltage(readings))) + if self._has_sensor_data(readings, "current_ma"): + lpp.extend(encode_current(channel, self._battery_current(readings))) + if self._has_sensor_data(readings, "power_mw"): + lpp.extend(encode_power(channel, self._battery_power(readings))) + channel += 1 + for reading in readings: entry = self._encode_environment_reading(channel, reading) if entry: @@ -329,8 +338,21 @@ class ProtocolRequestHelper: except (TypeError, ValueError): continue return 0.0 + + @staticmethod + def _has_sensor_data(readings, key: str) -> bool: + """Return True when any reading exposes the requested sensor field.""" + for reading in readings: + if not reading.get("ok"): + continue + data = reading.get("data") or {} + if data.get(key) is not None: + return True + return False + + @staticmethod def _battery_current(readings) -> float: - """First configured sensor's current, else 0.000A.""" + """First configured sensor's current in amps, else 0.000A.""" for reading in readings: if not reading.get("ok"): continue @@ -338,20 +360,22 @@ class ProtocolRequestHelper: current = data.get("current_ma") if current is not None: try: - return float(current) + return float(current) / 1000.0 except (TypeError, ValueError): continue return 0.000 + + @staticmethod def _battery_power(readings) -> int: - """First configured sensor's power, else 0W.""" + """First configured sensor's power in watts, else 0W.""" for reading in readings: if not reading.get("ok"): continue data = reading.get("data") or {} - power = data.get("power_mw")/1000 + power = data.get("power_mw") if power is not None: try: - return int(power) + return int(float(power) / 1000.0) except (TypeError, ValueError): continue return 0 diff --git a/tests/test_handler_helpers_path_protocol_text.py b/tests/test_handler_helpers_path_protocol_text.py index 8da7643..cc77ba7 100644 --- a/tests/test_handler_helpers_path_protocol_text.py +++ b/tests/test_handler_helpers_path_protocol_text.py @@ -682,6 +682,8 @@ from openhop_core.node.handlers.protocol_request import ( # noqa: E402 ) from openhop_core.protocol.cayenne_lpp import ( # noqa: E402 TELEM_CHANNEL_SELF, + encode_current, + encode_power, encode_relative_humidity, encode_temperature, encode_voltage, @@ -722,8 +724,11 @@ def test_telemetry_base_voltage_from_ups_sensor(): lpp = helper._handle_get_telemetry(admin, 0, b"\x00") - # No environment values on the UPS reading -> voltage entry only. - assert lpp == encode_voltage(TELEM_CHANNEL_SELF, 12.6) + # Base voltage stays on channel 1, and the additional INA219 voltage view + # is emitted on the next channel when environment telemetry is allowed. + assert lpp == encode_voltage(TELEM_CHANNEL_SELF, 12.6) + encode_voltage( + TELEM_CHANNEL_SELF + 1, 12.6 + ) def test_telemetry_admin_full_mask_includes_environment_sensors(): @@ -734,7 +739,7 @@ def test_telemetry_admin_full_mask_includes_environment_sensors(): """ sm = _FakeSensorManager( [ - _reading(bus_voltage_v=12.6), + _reading(bus_voltage_v=12.6, current_ma=500.0, power_mw=6000.0), _reading(temperature_c=21.5, humidity_pct=55.0), ] ) @@ -748,8 +753,11 @@ def test_telemetry_admin_full_mask_includes_environment_sensors(): expected = ( encode_voltage(TELEM_CHANNEL_SELF, 12.6) - + encode_temperature(TELEM_CHANNEL_SELF + 1, 21.5) - + encode_relative_humidity(TELEM_CHANNEL_SELF + 1, 55.0) + + encode_voltage(TELEM_CHANNEL_SELF + 1, 12.6) + + encode_current(TELEM_CHANNEL_SELF + 2, 0.5) + + encode_power(TELEM_CHANNEL_SELF + 3, 6) + + encode_temperature(TELEM_CHANNEL_SELF + 4, 21.5) + + encode_relative_humidity(TELEM_CHANNEL_SELF + 4, 55.0) ) assert lpp == expected @@ -767,6 +775,32 @@ def test_telemetry_guest_forced_to_base_only(): assert lpp == encode_voltage(TELEM_CHANNEL_SELF, 0.0) +def test_telemetry_includes_ina219_power_channels_before_environment_sensors(): + """INA219 voltage/current/power values are emitted before env sensors.""" + sm = _FakeSensorManager( + [ + _reading(bus_voltage_v=12.6, current_ma=500.0, power_mw=6000.0), + _reading(temperature_c=21.5, humidity_pct=55.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.6) + + encode_voltage(TELEM_CHANNEL_SELF + 1, 12.6) + + encode_current(TELEM_CHANNEL_SELF + 2, 0.5) + + encode_power(TELEM_CHANNEL_SELF + 3, 6) + + encode_temperature(TELEM_CHANNEL_SELF + 4, 21.5) + + encode_relative_humidity(TELEM_CHANNEL_SELF + 4, 55.0) + ) + assert lpp == expected + + def test_telemetry_inverse_mask_gates_environment(): """An inverse mask that clears the environment bit strips env sensors.