added more diagnostics
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user