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>
This commit is contained in:
claude
2026-07-22 20:29:05 +03:00
parent c3b98043f7
commit 027ff685b7
16 changed files with 1572 additions and 1069 deletions

View File

@@ -1,8 +1,8 @@
"""Notify platform for Reticulum.
"""Notify platform for Reticulum (per identity).
Exposes a ``notify`` entity that sends an LXMF message to the configured
default recipient. For arbitrary destinations use the ``reticulum.send_message``
service instead.
Each identity exposes a ``notify`` entity that sends an LXMF message from that
identity to its configured default recipient. For arbitrary destinations use
the ``reticulum.send_message`` service.
"""
from __future__ import annotations
@@ -13,9 +13,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONF_DEFAULT_RECIPIENT, CONF_DELIVERY_METHOD, DEFAULT_DELIVERY_METHOD, DOMAIN
from .entity import ReticulumEntity
from .reticulum_client import ReticulumError, ReticulumManager
from .const import (
CONF_DEFAULT_RECIPIENT,
CONF_DELIVERY_METHOD,
DEFAULT_DELIVERY_METHOD,
DOMAIN,
SUBENTRY_TYPE_IDENTITY,
)
from .entity import ReticulumIdentityEntity
from .reticulum_client import ReticulumError
async def async_setup_entry(
@@ -23,32 +29,42 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Reticulum notify entity."""
manager: ReticulumManager = hass.data[DOMAIN][entry.entry_id].manager
async_add_entities([ReticulumNotify(manager, entry)])
"""Set up per-identity notify entities."""
runtime = hass.data[DOMAIN][entry.entry_id]
for subentry in entry.subentries.values():
if subentry.subentry_type != SUBENTRY_TYPE_IDENTITY:
continue
manager = runtime.identities.get(subentry.subentry_id)
if manager is None:
continue
async_add_entities(
[ReticulumNotify(manager, entry, subentry)],
config_subentry_id=subentry.subentry_id,
)
class ReticulumNotify(ReticulumEntity, NotifyEntity):
"""Send LXMF messages to the default recipient."""
class ReticulumNotify(ReticulumIdentityEntity, NotifyEntity):
"""Send LXMF messages from this identity to its default recipient."""
_attr_translation_key = "message"
_attr_icon = "mdi:message-fast"
_attr_supported_features = NotifyEntityFeature.TITLE
def __init__(self, manager: ReticulumManager, entry: ConfigEntry) -> None:
super().__init__(manager, entry.entry_id)
self._entry = entry
self._attr_unique_id = f"{entry.entry_id}_notify"
def __init__(self, manager, entry, subentry) -> None:
super().__init__(manager, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_notify"
async def async_send_message(self, message: str, title: str | None = None) -> None:
recipient = self._entry.options.get(CONF_DEFAULT_RECIPIENT)
recipient = self._subentry.data.get(CONF_DEFAULT_RECIPIENT)
if not recipient:
raise ServiceValidationError(
"No default recipient configured. Set one in the Reticulum "
"integration options, or use the reticulum.send_message service "
"No default recipient configured for this identity. Set one in "
"the identity's options, or use the reticulum.send_message service "
"with an explicit destination."
)
method = self._entry.options.get(CONF_DELIVERY_METHOD, DEFAULT_DELIVERY_METHOD)
method = self._subentry.data.get(
CONF_DELIVERY_METHOD, DEFAULT_DELIVERY_METHOD
)
try:
await self._manager.async_send_message(
recipient, message, title=title or "", method=method