diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py index 03e30ce..12aa6e5 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 @@ -166,6 +168,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 +229,7 @@ class ProtocolRequestHelper: # Pack 56-byte RepeaterStats (layout matches firmware) stats = struct.pack( " 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 in amps, 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) / 1000.0 + except (TypeError, ValueError): + continue + return 0.000 + + @staticmethod + def _battery_power(readings) -> int: + """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") + if power is not None: + try: + return int(float(power) / 1000.0) + except (TypeError, ValueError): + continue + return 0 + @staticmethod def _encode_environment_reading(channel: int, reading) -> bytes: """Encode a temperature/humidity reading as CayenneLPP, or b"" if none. 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.