RNS.Reticulum.__init__ sets its internal singleton before it can fail partway through init, which left our module-level _RNS_INSTANCE cache out of sync (None) while RNS believed it was already running. Every setup retry then hit RNS's reinit guard and raised "Attempt to reinitialise Reticulum, when it was already running". Use RNS.Reticulum.get_instance() as the source of truth and adopt an existing instance instead of re-creating it (with an OSError fallback that also adopts). Also cache and reuse the LXMF router, delivery destination, identity and announce handler across reloads so a reload never spawns a duplicate router or stacks announce handlers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
222 lines
8.2 KiB
Markdown
222 lines
8.2 KiB
Markdown
# 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 a specific conversation agent, or leave
|
||
blank for the default pipeline.
|
||
- **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.
|
||
- 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
|