- messages_sent now increments when a message is queued (async_send_message), not only when a delivery proof returns. LXMessage's delivery callback only fires on a returned proof (LXMessage.py ~566), which many peers/methods never send, so the "Messages sent" sensor was stuck at 0 for messages that were actually delivered. The delivery callback now only emits the event. - Implement the previously-inert "greeting" option: send it automatically the first time each sender contacts HA (tracked per sender in RuntimeData). - Clarify the notify entity (rename to "Send message" / "Отправить сообщение") and document greeting + notify in strings/translations/README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reticulum for Home Assistant
A Home Assistant custom integration that connects Home Assistant to a Reticulum network over TCP and exchanges 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
TCPServerInterfaceenabled. Example~/.reticulum/configon that machine:[interfaces] [[TCP Server Interface]] type = TCPServerInterface enabled = yes listen_ip = 0.0.0.0 listen_port = 4242In Kubernetes, expose that port to the HA pod (Service / NodePort / host networking as appropriate). Do not enable
kiss_framingon 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)
- HACS → ⋮ → Custom repositories.
- Add this repository URL, category Integration.
- 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 the
notify.reticulum_send_messageentity sends to (see Entities below). - Greeting – optional message sent automatically the first time each sender contacts Home Assistant (once per sender, reset on restart). Blank = disabled.
- Default delivery method –
direct,opportunistic, orpropagated. - Announce interval – periodic re-announce (seconds;
0disables). - 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_send_message |
notify entity that sends an LXMF message to the default recipient set in options — use it in automations/scripts (action: notify.send_message) or the Developer Tools; for arbitrary addresses use the reticulum.send_message service |
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.
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
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
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
atexitpersistence 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 blockingatexitpersistence, persisting RNS/LXMF state itself, off the event loop, on thehomeassistant_stopevent. 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