#!/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()