Files
ha-reticulum/README.md
Artem Eliseev 7902a2529e Add a brand icon
Since HA 2026.x the brands component serves
/api/brands/integration/<domain>/<image> from a custom integration's own
brand/ directory before falling back to the brands CDN, so without one the
integration renders as the grey placeholder. Add brand/icon.png (256x256) and
brand/icon@2x.png (512x512) - a mesh lattice, drawn by
make_brand_icons.py in the applications repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 21:43:17 +03:00

267 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<subentry_id>/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.
## Brand icon
`custom_components/reticulum/brand/icon.png` (256x256) and `icon@2x.png`
(512x512) are the integration's own icon. Since HA 2026.x the `brands`
component serves `/api/brands/integration/reticulum/<image>` from a custom
integration's `brand/` directory before falling back to the brands CDN, so
without them the integration shows the grey placeholder. Detection is
`Integration.has_branding`, which only checks that a top-level `brand`
directory exists - it is a cached property, so Home Assistant has to be
restarted after the files land.
## License
MIT