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. - Decrypts channel packets (AES-CTR) using the channel PSK.
- Exposes the latest text message as a sensor (with from/to/time attributes). - Exposes the latest text message as a sensor (with from/to/time attributes).
- Parses `LABEL:VALUE` messages into individual temperature/humidity sensors. - 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 - Auto-creates entities via MQTT Discovery; resolves the HA broker through the
Supervisor (no manual credentials required). 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). `to`, and timestamp as attributes).
- **Parsed sensors** — values pulled out of structured messages such as - **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. `🏠 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. 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`, 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`. 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 ## Topics used
- Discovery: `{discovery_prefix}/sensor/{device}/{label}/config` - Discovery: `{discovery_prefix}/sensor/{device}/{label}/config`
- State: `meshtastic_receiver/{device}/{label}` - State: `meshtastic_receiver/{device}/{label}`
- Last message: `meshtastic_receiver/{device}/last_message` (+ `/attributes`) - Last message: `meshtastic_receiver/{device}/last_message` (+ `/attributes`)
- Diagnostics: `meshtastic_receiver/{device}/{last_seen,rssi,snr,hops_away}`
- Availability: `meshtastic_receiver/{device}/status` - Availability: `meshtastic_receiver/{device}/status`
## Privacy note ## Privacy note
+87 -1
View File
@@ -14,6 +14,7 @@ import os
import re import re
import sys import sys
import time import time
from datetime import datetime, timezone
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import requests 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). # 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+)?)") 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. # Set once the HA broker client is connected.
ha_client = None ha_client = None
@@ -158,10 +189,62 @@ def publish_discovery():
json.dumps(cfg), json.dumps(cfg),
retain=True, 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 -------------------------------------------------------- # --- 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): def handle_text(text, from_id, to_id):
# HA sensor states are limited to 255 chars. # HA sensor states are limited to 255 chars.
ha_client.publish(f"{BASE_TOPIC}/last_message", text[:255], retain=True) 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 mp = env.packet
from_id = getattr(mp, "from") from_id = getattr(mp, "from")
# Link diagnostics + last-seen update for every packet on the channel.
publish_metadata(mp)
if mp.HasField("decoded"): if mp.HasField("decoded"):
data = mp.decoded data = mp.decoded
elif mp.HasField("encrypted"): elif mp.HasField("encrypted"):