added more diagnostics

This commit is contained in:
Oliver Walter
2026-07-01 01:27:24 +02:00
parent ddbbd27dbe
commit 30948ebb0a
3 changed files with 102 additions and 1 deletions
+1
View File
@@ -7,5 +7,6 @@
- Decrypts channel packets (AES-CTR) using the channel PSK.
- Exposes the latest text message as a sensor (with from/to/time attributes).
- Parses `LABEL:VALUE` messages into individual temperature/humidity sensors.
- Link diagnostics per node: Last Seen, RSSI, SNR, and Hops Away.
- Auto-creates entities via MQTT Discovery; resolves the HA broker through the
Supervisor (no manual credentials required).
+14
View File
@@ -7,6 +7,8 @@ the results in Home Assistant via MQTT Discovery:
`to`, and timestamp as attributes).
- **Parsed sensors** — values pulled out of structured messages such as
`🏠 T1:20.8°C T2:26.7°C H1:83.7%` become individual numeric sensors.
- **Link diagnostics** — `Last Seen`, `RSSI`, `SNR`, and `Hops Away`, derived
from each packet's metadata (shown under the device's Diagnostic section).
This is the receiving counterpart to the **Meshtastic Sensor Broadcaster** add-on.
@@ -44,11 +46,23 @@ ignored). For example:
produces `T1=20.8`, `T2=26.7`, `T3=26.6`, `H1=83.7`, `H2=54.4`, `H3=55.5`,
each published to the sensor configured with that `label`.
## Link diagnostics
These are created automatically (no config) from each received packet:
| Sensor | Source | Notes |
|--------|--------|-------|
| `Last Seen` | packet `rx_time` | Updates on every packet on the channel. |
| `RSSI` | packet `rx_rssi` (dBm) | Signal at the gateway. Omitted when the gateway originated the packet (value 0). |
| `SNR` | packet `rx_snr` (dB) | Published alongside RSSI. |
| `Hops Away` | `hop_start - hop_limit` | Number of hops the packet traveled. Requires firmware that reports `hop_start`. |
## Topics used
- Discovery: `{discovery_prefix}/sensor/{device}/{label}/config`
- State: `meshtastic_receiver/{device}/{label}`
- Last message: `meshtastic_receiver/{device}/last_message` (+ `/attributes`)
- Diagnostics: `meshtastic_receiver/{device}/{last_seen,rssi,snr,hops_away}`
- Availability: `meshtastic_receiver/{device}/status`
## Privacy note
+87 -1
View File
@@ -14,6 +14,7 @@ import os
import re
import sys
import time
from datetime import datetime, timezone
import paho.mqtt.client as mqtt
import requests
@@ -62,6 +63,36 @@ CHANNEL_KEY = expand_psk(CHANNEL_PSK)
# Matches "T1:20.8", "H2:54.4", "P:1013" etc. (unit suffix is ignored here).
PAIR_RE = re.compile(r"([A-Za-z][A-Za-z0-9_]*)\s*:\s*(-?\d+(?:\.\d+)?)")
# Built-in per-node diagnostic sensors derived from packet metadata.
DIAGNOSTICS = [
{
"key": "last_seen",
"name": "Last Seen",
"device_class": "timestamp",
"icon": "mdi:clock-outline",
},
{
"key": "rssi",
"name": "RSSI",
"device_class": "signal_strength",
"unit": "dBm",
"state_class": "measurement",
},
{
"key": "snr",
"name": "SNR",
"unit": "dB",
"state_class": "measurement",
"icon": "mdi:signal",
},
{
"key": "hops_away",
"name": "Hops Away",
"state_class": "measurement",
"icon": "mdi:transit-connection-variant",
},
]
# Set once the HA broker client is connected.
ha_client = None
@@ -158,10 +189,62 @@ def publish_discovery():
json.dumps(cfg),
retain=True,
)
log(f"[ha] published discovery for last_message + {len(SENSORS)} sensors")
for d in DIAGNOSTICS:
key = d["key"]
cfg = {
"name": d["name"],
"unique_id": f"{DEVICE_UID}_{key}",
"state_topic": f"{BASE_TOPIC}/{key}",
"availability_topic": AVAIL_TOPIC,
"entity_category": "diagnostic",
"device": device_block(),
}
if d.get("unit"):
cfg["unit_of_measurement"] = d["unit"]
if d.get("device_class"):
cfg["device_class"] = d["device_class"]
if d.get("state_class"):
cfg["state_class"] = d["state_class"]
if d.get("icon"):
cfg["icon"] = d["icon"]
ha_client.publish(
f"{DISCOVERY_PREFIX}/sensor/{DEVICE_UID}/{key}/config",
json.dumps(cfg),
retain=True,
)
log(
f"[ha] published discovery for last_message + {len(SENSORS)} sensors "
f"+ {len(DIAGNOSTICS)} diagnostics"
)
# --- Message handling --------------------------------------------------------
def publish_metadata(mp):
"""Publish link-quality diagnostics from a received MeshPacket."""
# Last seen: prefer the gateway's rx_time, fall back to now.
ts = (
datetime.fromtimestamp(mp.rx_time, tz=timezone.utc)
if mp.rx_time
else datetime.now(timezone.utc)
)
ha_client.publish(f"{BASE_TOPIC}/last_seen", ts.isoformat(), retain=True)
# RSSI/SNR are 0 for packets the gateway originated itself — only publish
# genuine over-the-air measurements.
if mp.rx_rssi != 0:
ha_client.publish(f"{BASE_TOPIC}/rssi", mp.rx_rssi, retain=True)
ha_client.publish(f"{BASE_TOPIC}/snr", round(mp.rx_snr, 2), retain=True)
# hop_start is the original TTL; hops taken = hop_start - hop_limit.
# hop_start == 0 means older firmware that didn't report it.
if mp.hop_start:
ha_client.publish(
f"{BASE_TOPIC}/hops_away", mp.hop_start - mp.hop_limit, retain=True
)
def handle_text(text, from_id, to_id):
# HA sensor states are limited to 255 chars.
ha_client.publish(f"{BASE_TOPIC}/last_message", text[:255], retain=True)
@@ -212,6 +295,9 @@ def on_src_message(client, userdata, msg):
mp = env.packet
from_id = getattr(mp, "from")
# Link diagnostics + last-seen update for every packet on the channel.
publish_metadata(mp)
if mp.HasField("decoded"):
data = mp.decoded
elif mp.HasField("encrypted"):