From e1ee7fcd2407ddc795a7a33c54c31acc966d9ef3 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sun, 12 Apr 2026 18:59:44 -0700 Subject: [PATCH] Add default precision --- app/fanout/mqtt_ha.py | 63 ++++++++++++++++++++++++++++---------- frontend/package-lock.json | 4 +-- tests/test_mqtt_ha.py | 23 +++++++++++++- 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/app/fanout/mqtt_ha.py b/app/fanout/mqtt_ha.py index 5dc9f16..27801f8 100644 --- a/app/fanout/mqtt_ha.py +++ b/app/fanout/mqtt_ha.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) # ── Repeater telemetry sensor definitions ───────────────────────────────── -_REPEATER_SENSORS: list[dict[str, str | None]] = [ +_REPEATER_SENSORS: list[dict[str, Any]] = [ { "field": "battery_volts", "name": "Battery Voltage", @@ -34,6 +34,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": "voltage", "state_class": "measurement", "unit": "V", + "precision": 2, }, { "field": "noise_floor_dbm", @@ -42,6 +43,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": "signal_strength", "state_class": "measurement", "unit": "dBm", + "precision": 0, }, { "field": "last_rssi_dbm", @@ -50,6 +52,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": "signal_strength", "state_class": "measurement", "unit": "dBm", + "precision": 0, }, { "field": "last_snr_db", @@ -58,6 +61,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "measurement", "unit": "dB", + "precision": 1, }, { "field": "packets_received", @@ -66,6 +70,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "total_increasing", "unit": None, + "precision": 0, }, { "field": "packets_sent", @@ -74,6 +79,7 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "total_increasing", "unit": None, + "precision": 0, }, { "field": "uptime_seconds", @@ -82,24 +88,25 @@ _REPEATER_SENSORS: list[dict[str, str | None]] = [ "device_class": "duration", "state_class": None, "unit": "s", + "precision": 0, }, ] # ── LPP sensor metadata ───────────────────────────────────────────────── -_LPP_HA_META: dict[str, dict[str, str | None]] = { - "temperature": {"device_class": "temperature", "unit": "°C"}, - "humidity": {"device_class": "humidity", "unit": "%"}, - "barometer": {"device_class": "atmospheric_pressure", "unit": "hPa"}, - "voltage": {"device_class": "voltage", "unit": "V"}, - "current": {"device_class": "current", "unit": "mA"}, - "luminosity": {"device_class": "illuminance", "unit": "lux"}, - "power": {"device_class": "power", "unit": "W"}, - "energy": {"device_class": "energy", "unit": "kWh"}, - "distance": {"device_class": "distance", "unit": "mm"}, - "concentration": {"device_class": None, "unit": "ppm"}, - "direction": {"device_class": None, "unit": "°"}, - "altitude": {"device_class": None, "unit": "m"}, +_LPP_HA_META: dict[str, dict[str, Any]] = { + "temperature": {"device_class": "temperature", "unit": "°C", "precision": 1}, + "humidity": {"device_class": "humidity", "unit": "%", "precision": 1}, + "barometer": {"device_class": "atmospheric_pressure", "unit": "hPa", "precision": 1}, + "voltage": {"device_class": "voltage", "unit": "V", "precision": 2}, + "current": {"device_class": "current", "unit": "mA", "precision": 1}, + "luminosity": {"device_class": "illuminance", "unit": "lux", "precision": 0}, + "power": {"device_class": "power", "unit": "W", "precision": 1}, + "energy": {"device_class": "energy", "unit": "kWh", "precision": 2}, + "distance": {"device_class": "distance", "unit": "mm", "precision": 0}, + "concentration": {"device_class": None, "unit": "ppm", "precision": 0}, + "direction": {"device_class": None, "unit": "°", "precision": 0}, + "altitude": {"device_class": None, "unit": "m", "precision": 1}, } @@ -141,6 +148,8 @@ def _lpp_discovery_configs( cfg["device_class"] = meta["device_class"] if meta.get("unit"): cfg["unit_of_measurement"] = meta["unit"] + if meta.get("precision") is not None: + cfg["suggested_display_precision"] = meta["precision"] topic = f"homeassistant/sensor/meshcore_{nid}/{object_id}/config" configs.append((topic, cfg)) @@ -150,7 +159,7 @@ def _lpp_discovery_configs( # ── Local radio sensor definitions ──────────────────────────────────────── -_RADIO_SENSORS: list[dict[str, str | None]] = [ +_RADIO_SENSORS: list[dict[str, Any]] = [ { "field": "noise_floor_dbm", "name": "Noise Floor", @@ -158,14 +167,16 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": "signal_strength", "state_class": "measurement", "unit": "dBm", + "precision": 0, }, { - "field": "battery_mv", + "field": "battery_volts", "name": "Battery", "object_id": "battery", "device_class": "voltage", "state_class": "measurement", - "unit": "mV", + "unit": "V", + "precision": 2, }, { "field": "uptime_secs", @@ -174,6 +185,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": "duration", "state_class": None, "unit": "s", + "precision": 0, }, { "field": "last_rssi", @@ -182,6 +194,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": "signal_strength", "state_class": "measurement", "unit": "dBm", + "precision": 0, }, { "field": "last_snr", @@ -190,6 +203,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "measurement", "unit": "dB", + "precision": 1, }, { "field": "tx_air_secs", @@ -198,6 +212,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": "duration", "state_class": "total_increasing", "unit": "s", + "precision": 0, }, { "field": "rx_air_secs", @@ -206,6 +221,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": "duration", "state_class": "total_increasing", "unit": "s", + "precision": 0, }, { "field": "packets_recv", @@ -214,6 +230,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "total_increasing", "unit": None, + "precision": 0, }, { "field": "packets_sent", @@ -222,6 +239,7 @@ _RADIO_SENSORS: list[dict[str, str | None]] = [ "device_class": None, "state_class": "total_increasing", "unit": None, + "precision": 0, }, ] @@ -343,6 +361,8 @@ def _radio_discovery_configs( cfg["state_class"] = sensor["state_class"] if sensor["unit"]: cfg["unit_of_measurement"] = sensor["unit"] + if sensor.get("precision") is not None: + cfg["suggested_display_precision"] = sensor["precision"] topic = f"homeassistant/sensor/meshcore_{nid}/{sensor['object_id']}/config" configs.append((topic, cfg)) @@ -376,6 +396,8 @@ def _repeater_discovery_configs( cfg["state_class"] = sensor["state_class"] if sensor["unit"]: cfg["unit_of_measurement"] = sensor["unit"] + if sensor.get("precision") is not None: + cfg["suggested_display_precision"] = sensor["precision"] # 10 hours — margin over the 8-hour auto-collect cycle cfg["expire_after"] = 36000 @@ -632,6 +654,13 @@ class MqttHaModule(FanoutModule): field = sensor["field"] if field is not None: payload[field] = data.get(field) + + # Normalize battery from millivolts to volts for consistency with + # repeater battery and the discovery config (unit: V, precision: 2). + battery_mv = data.get("battery_mv") + if battery_mv is not None: + payload["battery_volts"] = battery_mv / 1000.0 + await self._publisher.publish(f"{self._prefix}/{nid}/health", payload) async def on_contact(self, data: dict) -> None: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9da983d..95cb3a4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "remoteterm-meshcore-frontend", - "version": "3.8.0", + "version": "3.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "remoteterm-meshcore-frontend", - "version": "3.8.0", + "version": "3.11.0", "dependencies": { "@codemirror/lang-python": "^6.2.1", "@codemirror/theme-one-dark": "^6.1.3", diff --git a/tests/test_mqtt_ha.py b/tests/test_mqtt_ha.py index 13f6af7..3868e98 100644 --- a/tests/test_mqtt_ha.py +++ b/tests/test_mqtt_ha.py @@ -104,6 +104,21 @@ class TestRadioDiscovery: for _, cfg in configs[1:]: assert cfg["expire_after"] == 120 + def test_sensor_configs_have_display_precision(self): + configs = _radio_discovery_configs("mc", "aabbccddeeff", "R") + # All sensor configs (skip the binary_sensor at index 0) + for _, cfg in configs[1:]: + assert "suggested_display_precision" in cfg + assert isinstance(cfg["suggested_display_precision"], int) + + def test_battery_sensor_uses_volts(self): + configs = _radio_discovery_configs("mc", "aabbccddeeff", "R") + battery_cfgs = [(t, c) for t, c in configs if "battery" in t] + assert len(battery_cfgs) == 1 + _, cfg = battery_cfgs[0] + assert cfg["unit_of_measurement"] == "V" + assert cfg["suggested_display_precision"] == 2 + class TestRepeaterDiscovery: def test_produces_sensor_per_field(self): @@ -124,6 +139,11 @@ class TestRepeaterDiscovery: for _, cfg in configs: assert cfg["expire_after"] == 36000 + def test_sensors_have_display_precision(self): + configs = _repeater_discovery_configs("mc", "ccdd", "Rep1", None) + for _, cfg in configs: + assert "suggested_display_precision" in cfg + class TestContactTrackerDiscovery: def test_config_shape(self): @@ -263,7 +283,7 @@ class TestMqttHaHealth: payload = health_calls[-1][0][1] assert payload["connected"] is True assert payload["noise_floor_dbm"] == -110 - assert payload["battery_mv"] == 4150 + assert payload["battery_volts"] == 4.15 assert payload["uptime_secs"] == 3600 assert payload["last_rssi"] == -85 assert payload["packets_recv"] == 500 @@ -524,6 +544,7 @@ class TestLppDiscoveryConfigs: assert cfg["unit_of_measurement"] == "°C" assert cfg["state_class"] == "measurement" assert cfg["expire_after"] == 36000 + assert cfg["suggested_display_precision"] == 1 assert "lpp_temperature_ch1" in cfg["value_template"] def test_unknown_sensor_type_no_device_class(self):