# 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.** You configure the shared **stack** once (the RNS connection), and a first LXMF **identity** is created automatically. Additional identities are added as **sub-entries** from the integration page. | Stack field | Meaning | |-------|---------| | Target host | Host/IP of the neighbouring Reticulum TCP server | | Target port | Its `listen_port` (default `4242`) | | Interface name | Label for the connection in the generated RNS config | | First identity name | Announce name of the first identity | ### Identities (sub-entries) Each identity is an independent LXMF address with its own settings, its own device, and its own entities. Add one via the integration's **⋮ → Add identity** (or reconfigure an existing one). Typical uses: - an **assistant** identity (Assist enabled) that users chat with; - a **receive-only** identity (Assist disabled) whose messages are handled by automations via the `reticulum_message_received` event. Per-identity settings: - **Name** – contact name announced to peers (also the identity's HA name). Change it any time by reconfiguring the identity; future announces use it. - **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. To chat, install an LLM conversation integration > (OpenAI, Google Generative AI, Anthropic, Ollama, …) and select it here. - **Reply to any sender** / **Allowed sender addresses** – restrict who is answered. - **Default recipient** – address this identity's notify entity sends to. - **Auto-reply (when Assist is off)** – message sent automatically in reply to incoming messages only when Assist routing is off. Blank = disabled. - **Default delivery method**, **announce interval**, **propagation node** + **sync interval**. Stack-level options (**⚙ → Configure**): **Reticulum log level** (`0`–`7`). Find each identity's **LXMF address** on its *LXMF address* sensor, or in the log (`Reticulum identity '…' ready: a1b2c3…`). Add it as a contact in Sideband/MeshChat and message it. --- ## Entities **Hub (stack) device — `Reticulum`:** | Entity | Description | |--------|-------------| | `binary_sensor` Connected | Whether the TCP interface is online | | `sensor` Known peers | Count + `peers` attribute (address, name, hops) | | `sensor` Received / Transmitted | Interface RX/TX byte counters | | `sensor` Bitrate | Interface bitrate | | `sensor` RSSI / SNR / Link quality | Physical-link telemetry (disabled by default) | **Per identity device (one per identity):** | Entity | Description | |--------|-------------| | `sensor` LXMF address | This identity's address (share it with users) | | `sensor` Messages received / sent / failed | Counters | | `sensor` Last message | Last inbound text + source/title attributes | | `button` Announce | Announce this identity now | | `button` Sync propagation node | Pull queued messages now | | `button` Regenerate identity | Generate a brand-new address for this identity | | `notify` Send message | Sends an LXMF message from this identity to its **default recipient** (`action: notify.send_message`); for arbitrary addresses use the `reticulum.send_message` service | RSSI, SNR and link quality only carry data on a physical interface (RNode/LoRa) — they stay unavailable on a plain TCP link, so they are disabled by default. > **Regenerate identity** replaces this identity's address with a fresh one and > re-announces. The old address stops working — contacts must re-add the new one. --- ## 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 identity: "Home Assistant" # optional: which identity to send from (name or # LXMF address); defaults to the first identity ``` Other services: `reticulum.announce`, `reticulum.request_path`, `reticulum.set_propagation_node`, `reticulum.sync_propagation`. Each accepts an optional `identity` to pick which identity to act through. --- ## Events - `reticulum_message_received` — `{content, title, source, destination, timestamp, signature_validated, fields, identity, identity_name, local_address}` - `reticulum_message_delivered` — `{message_hash, destination, identity, identity_name, local_address}` - `reticulum_message_failed` — `{message_hash, destination, identity, identity_name, local_address}` - `reticulum_announce_received` — `{destination, display_name, stamp_cost, hops}` `identity` is the subentry id and `identity_name` the identity's name, so automations can tell which identity received/sent a message. ### 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**, so only one Reticulum config entry (the stack) is allowed; add multiple **identities** as sub-entries under it. Because RNS/LXMF cannot be cleanly torn down inside a running process, the running stack and each identity's router are reused across reloads. Changing the host/port (or recovering from a failed first start) therefore requires a **full Home Assistant restart**, not just a reload. - Each identity is stored in `config/reticulum/identities//identity` — back it up to keep the same address. **Regenerating** an identity overwrites it with a new 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