93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Broadcast Home Assistant sensor values over Meshtastic on a fixed interval."""
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import requests
|
|
|
|
# --- Config: add-on options are written here by the Supervisor ---------------
|
|
with open("/data/options.json", encoding="utf-8") as fh:
|
|
OPTS = json.load(fh)
|
|
|
|
CONNECTION = OPTS.get("connection", "tcp")
|
|
HOST = OPTS.get("host", "")
|
|
SERIAL_PORT = OPTS.get("serial_port", "/dev/ttyUSB0")
|
|
CHANNEL_INDEX = int(OPTS.get("channel_index", 0))
|
|
INTERVAL = int(OPTS.get("interval", 900))
|
|
PREFIX = OPTS.get("prefix", "") or ""
|
|
SENSORS = OPTS.get("sensors", [])
|
|
|
|
# --- HA core API via the Supervisor proxy (token injected by Supervisor) -----
|
|
HA_API = "http://supervisor/core/api"
|
|
HEADERS = {"Authorization": f"Bearer {os.environ['SUPERVISOR_TOKEN']}"}
|
|
|
|
UNAVAILABLE = (None, "unknown", "unavailable", "none", "")
|
|
|
|
|
|
def log(msg):
|
|
print(msg, file=sys.stdout, flush=True)
|
|
|
|
|
|
def get_state(entity_id):
|
|
try:
|
|
r = requests.get(f"{HA_API}/states/{entity_id}", headers=HEADERS, timeout=10)
|
|
if r.status_code == 200:
|
|
return r.json().get("state")
|
|
log(f"[warn] {entity_id}: HTTP {r.status_code}")
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f"[warn] {entity_id}: {exc}")
|
|
return None
|
|
|
|
|
|
def build_message():
|
|
parts = []
|
|
for s in SENSORS:
|
|
state = get_state(s["entity_id"])
|
|
if state not in UNAVAILABLE:
|
|
label = s.get("label", "")
|
|
unit = s.get("unit", "")
|
|
parts.append(f"{label}:{state}{unit}" if label else f"{state}{unit}")
|
|
body = " ".join(parts)
|
|
return f"{PREFIX} {body}".strip() if PREFIX else body
|
|
|
|
|
|
def connect():
|
|
import meshtastic
|
|
if CONNECTION == "serial":
|
|
import meshtastic.serial_interface
|
|
log(f"Connecting to node via serial {SERIAL_PORT}")
|
|
return meshtastic.serial_interface.SerialInterface(devPath=SERIAL_PORT)
|
|
import meshtastic.tcp_interface
|
|
log(f"Connecting to node via TCP {HOST}")
|
|
return meshtastic.tcp_interface.TCPInterface(hostname=HOST)
|
|
|
|
|
|
def main():
|
|
log(f"Broadcaster up: {CONNECTION}, channel {CHANNEL_INDEX}, every {INTERVAL}s")
|
|
iface = None
|
|
while True:
|
|
msg = build_message()
|
|
if not msg:
|
|
log("[warn] no sensor values available — skipping this cycle")
|
|
else:
|
|
try:
|
|
if iface is None:
|
|
iface = connect()
|
|
iface.sendText(msg, channelIndex=CHANNEL_INDEX)
|
|
log(f"sent ({len(msg.encode('utf-8'))} bytes): {msg}")
|
|
except Exception as exc: # noqa: BLE001
|
|
log(f"[error] send failed, will reconnect: {exc}")
|
|
try:
|
|
if iface:
|
|
iface.close()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
iface = None
|
|
time.sleep(INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|