# Reticulum for Home Assistant A Home Assistant custom integration that connects Home Assistant to a [Reticulum](https://reticulum.network/) network over TCP and exchanges [LXMF](https://github.com/markqvist/LXMF) messages with users. The headline feature: **a user can chat with the Home Assistant Assist agent over Reticulum**. Any LXMF client (Sideband, MeshChat, Nomad Network) can send a message to Home Assistant's address; the text is routed through the Assist conversation pipeline and the answer is sent straight back — over LoRa, packet radio, I2P, TCP, or anything else Reticulum runs on. It also exposes a `notify` entity, a rich service API, sensors, and bus events so LXMF messaging can drive and be driven by automations. --- ## How it fits together ``` ┌────────────────────────┐ TCP ┌─────────────────────────┐ │ Home Assistant (k8s) │ TCPClientInterface│ Neighbouring machine │ │ │ ─────────────────► │ Reticulum instance │ │ reticulum integration │ │ (TCPServerInterface) │ │ • runs its own RNS │ ◄───────────────── │ + any other interfaces │ │ • LXMF delivery dest. │ │ (LoRa / RNode / I2P …) │ │ • Assist bridge │ └─────────────────────────┘ └────────────────────────┘ ``` Home Assistant runs a **standalone, non-shared** Reticulum instance inside its own process and reaches the wider network through the single `TCPClientInterface` pointing at the neighbouring stack. No shared-instance socket is used, which is what you want inside a container/pod. --- ## Requirements - Home Assistant 2024.11 or newer. - A reachable Reticulum instance on another machine with a `TCPServerInterface` enabled. Example `~/.reticulum/config` on that machine: ``` [interfaces] [[TCP Server Interface]] type = TCPServerInterface enabled = yes listen_ip = 0.0.0.0 listen_port = 4242 ``` In Kubernetes, expose that port to the HA pod (Service / NodePort / host networking as appropriate). Do **not** enable `kiss_framing` on either side. The integration declares its Python dependencies (`rns`, `lxmf`) in `manifest.json`; Home Assistant installs them automatically on first setup. --- ## Installation ### HACS (custom repository) 1. HACS → ⋮ → **Custom repositories**. 2. Add this repository URL, category **Integration**. 3. Install **Reticulum**, then restart Home Assistant. ### Manual Copy `custom_components/reticulum` into your Home Assistant `config/custom_components/` directory and restart. --- ## Configuration **Settings → Devices & Services → Add Integration → Reticulum.** | Field | Meaning | |-------|---------| | Target host | Host/IP of the neighbouring Reticulum TCP server | | Target port | Its `listen_port` (default `4242`) | | Display name | Name announced to other peers (e.g. `Home Assistant`) | | Interface name | Label for the connection in the generated RNS config | On first start the integration generates a stable identity and announces it. Find your **LXMF address** on the *LXMF address* sensor, or in the log: ``` Reticulum ready. LXMF address: a1b2c3… (Home Assistant) ``` Add that address as a contact in Sideband/MeshChat and send a message — Assist will answer. ### Options (⚙ → Configure) - **Route incoming messages to Assist** – turn the chat bridge on/off. - **Assist agent / language** – pick the conversation agent from the dropdown. > **Want free-form chat?** The default *Home Assistant* agent only recognises > device-control commands and will answer anything else with "Sorry, I > couldn't understand that". To actually chat, install an LLM conversation > integration (OpenAI, Google Generative AI, Anthropic, Ollama, …), then > select that agent here. - **Reply to any sender** / **Allowed sender addresses** – restrict who Assist will answer. - **Default recipient** – address used by the `notify.reticulum` entity. - **Default delivery method** – `direct`, `opportunistic`, or `propagated`. - **Announce interval** – periodic re-announce (seconds; `0` disables). - **Propagation node** + **sync interval** – optional store-and-forward node so messages queue while HA is offline. - **Reticulum log level** – `0`–`7`. --- ## Entities | Entity | Description | |--------|-------------| | `sensor.reticulum_lxmf_address` | This HA's LXMF address (share this with users) | | `sensor.reticulum_messages_received` / `_sent` / `_failed` | Counters | | `sensor.reticulum_known_peers` | Count + `peers` attribute (address, name, hops) | | `sensor.reticulum_last_message` | Last inbound text + source/title attributes | | `sensor.reticulum_last_message_time` | Timestamp of last inbound message | | `sensor.reticulum_received` / `_transmitted` | Interface RX/TX byte counters | | `sensor.reticulum_bitrate` | Interface bitrate | | `sensor.reticulum_rssi` / `_snr` / `_link_quality` | Physical-link telemetry (disabled by default) | | `binary_sensor.reticulum_connected` | Whether the TCP interface is online | | `button.reticulum_announce` | Announce now | | `button.reticulum_sync_propagation_node` | Pull queued messages now | | `notify.reticulum_message` | Send a message to the default recipient | The telemetry sensors read from the outbound interface. RX/TX bytes and bitrate work for the TCP link. **RSSI, SNR and link quality only carry data when the underlying interface is a physical one (RNode/LoRa)** — they stay unavailable on a plain TCP link, so they are disabled by default. Enable them from the device page if you run a physical interface. --- ## Services ### `reticulum.send_message` Send to any destination. Returns the message hash. ```yaml action: reticulum.send_message data: destination: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 content: "The garage door is still open." title: "Home Assistant" method: direct # direct | opportunistic | propagated ``` Other services: `reticulum.announce`, `reticulum.request_path`, `reticulum.set_propagation_node`, `reticulum.sync_propagation`. --- ## Events - `reticulum_message_received` — `{content, title, source, destination, timestamp, signature_validated, fields}` - `reticulum_message_delivered` — `{message_hash, destination}` - `reticulum_message_failed` — `{message_hash, destination}` - `reticulum_announce_received` — `{destination, display_name, stamp_cost, hops}` ### Example automation — forward every inbound message to a mobile push ```yaml alias: Reticulum → phone trigger: - platform: event event_type: reticulum_message_received action: - action: notify.mobile_app_phone data: title: "Reticulum: {{ trigger.event.data.source[:8] }}" message: "{{ trigger.event.data.content }}" ``` ### Device triggers (automation UI) Instead of raw event triggers you can pick these from the automation editor under the Reticulum device ("When… → Reticulum → …"): - **Message received** - **Message delivered** - **Message delivery failed** - **Announce received (peer heard)** Each wraps the matching bus event, so `trigger.event.data` carries the same payload described above. ### Example automation — alert over Reticulum when a door opens ```yaml alias: Door alert over Reticulum trigger: - platform: state entity_id: binary_sensor.front_door to: "on" action: - action: reticulum.send_message data: destination: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 content: "Front door opened at {{ now().strftime('%H:%M') }}" ``` --- ## Notes & limitations - Reticulum uses a **single process-wide instance**. Only one Reticulum config entry is allowed, and because RNS/LXMF cannot be cleanly torn down inside a running process, the running stack is reused across reloads. Changing the host/port (or recovering from a failed first start) therefore requires a **full Home Assistant restart**, not just an integration reload — a reload reuses the already-running stack. Option changes that don't touch the stack apply on reload. - The identity is stored in `config/reticulum/identity` — back it up to keep the same address. - RNS and LXMF are standalone-daemon-style libraries — both install SIGINT/SIGTERM handlers and `atexit` persistence in their constructors. To embed them cleanly the integration suppresses those signal handlers during init (so Home Assistant keeps ownership of SIGINT/SIGTERM — important under Kubernetes) and unregisters their blocking `atexit` persistence, persisting RNS/LXMF state itself, off the event loop, on the `homeassistant_stop` event. RNS still spawns a couple of non-daemon worker threads, so on shutdown you may see a "non-daemonic threads" notice and a short delay before the process exits (within the pod's termination grace period). - Assist replies use the standard conversation pipeline, so whichever agent you select (built-in intents, a local LLM, a cloud LLM, …) is what answers. ## License MIT