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