Files
ha-reticulum/README.md
claude 027ff685b7 Multiple identities via config subentries + regenerate + editable name
Refactor from one-entry-one-identity to a hub config entry (shared RNS stack)
plus config subentries of type "identity", each an independent LXMF identity
with its own LXMRouter, address, device and entities. This is required because
LXMRouter allows only one delivery identity per instance.

- ReticulumStack: owns the single process-wide RNS instance, peer discovery and
  interface telemetry (hub device).
- IdentityManager: one per subentry, its own LXMRouter/identity/destination and
  delivery callback (identity device). Signal handlers suppressed and atexit
  handlers tamed for each router too.
- Config subentry flow (add / reconfigure identity); a first identity is seeded
  on stack creation. Editable announce display name = subentry title.
- Per-identity "Regenerate identity" button: new address + re-announce
  (clears the router's single delivery destination first).
- Services and events gain an "identity" field to target/distinguish identities.
- Per-identity assist bridge, greeting-when-assist-off, allow-list, notify,
  announce/sync buttons, message counters; hub-level connectivity/telemetry/peers.

Verified against current HA config_entries/entity_platform/selector and RNS/LXMF
sources. Docs and en/ru translations updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:29:05 +03:00

10 KiB
Raw Permalink Blame History

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 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 (07).

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.

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

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, 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.

License

MIT