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:
@@ -1,17 +1,17 @@
|
||||
"""Button platform for Reticulum."""
|
||||
"""Button platform for Reticulum (per identity)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import ReticulumEntity
|
||||
from .reticulum_client import ReticulumError, ReticulumManager
|
||||
from .const import DOMAIN, SUBENTRY_TYPE_IDENTITY
|
||||
from .entity import ReticulumIdentityEntity
|
||||
from .reticulum_client import ReticulumError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,25 +21,33 @@ async def async_setup_entry(
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Reticulum buttons."""
|
||||
manager: ReticulumManager = hass.data[DOMAIN][entry.entry_id].manager
|
||||
async_add_entities(
|
||||
[
|
||||
ReticulumAnnounceButton(manager, entry.entry_id),
|
||||
ReticulumSyncButton(manager, entry.entry_id),
|
||||
]
|
||||
)
|
||||
"""Set up per-identity buttons."""
|
||||
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(
|
||||
[
|
||||
ReticulumAnnounceButton(manager, entry, subentry),
|
||||
ReticulumSyncButton(manager, entry, subentry),
|
||||
ReticulumRegenerateButton(manager, entry, subentry),
|
||||
],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class ReticulumAnnounceButton(ReticulumEntity, ButtonEntity):
|
||||
"""Announce our LXMF destination on demand."""
|
||||
class ReticulumAnnounceButton(ReticulumIdentityEntity, ButtonEntity):
|
||||
"""Announce this identity's LXMF destination."""
|
||||
|
||||
_attr_translation_key = "announce"
|
||||
_attr_icon = "mdi:bullhorn"
|
||||
|
||||
def __init__(self, manager: ReticulumManager, entry_id: str) -> None:
|
||||
super().__init__(manager, entry_id)
|
||||
self._attr_unique_id = f"{entry_id}_announce"
|
||||
def __init__(self, manager, entry, subentry) -> None:
|
||||
super().__init__(manager, entry, subentry)
|
||||
self._attr_unique_id = f"{subentry.subentry_id}_announce"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
try:
|
||||
@@ -48,18 +56,42 @@ class ReticulumAnnounceButton(ReticulumEntity, ButtonEntity):
|
||||
_LOGGER.warning("Announce failed: %s", err)
|
||||
|
||||
|
||||
class ReticulumSyncButton(ReticulumEntity, ButtonEntity):
|
||||
"""Pull queued messages from the configured propagation node."""
|
||||
class ReticulumSyncButton(ReticulumIdentityEntity, ButtonEntity):
|
||||
"""Pull queued messages from the propagation node."""
|
||||
|
||||
_attr_translation_key = "sync"
|
||||
_attr_icon = "mdi:sync"
|
||||
|
||||
def __init__(self, manager: ReticulumManager, entry_id: str) -> None:
|
||||
super().__init__(manager, entry_id)
|
||||
self._attr_unique_id = f"{entry_id}_sync"
|
||||
def __init__(self, manager, entry, subentry) -> None:
|
||||
super().__init__(manager, entry, subentry)
|
||||
self._attr_unique_id = f"{subentry.subentry_id}_sync"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
try:
|
||||
await self._manager.async_sync_propagation()
|
||||
except ReticulumError as err:
|
||||
_LOGGER.warning("Propagation sync failed: %s", err)
|
||||
|
||||
|
||||
class ReticulumRegenerateButton(ReticulumIdentityEntity, ButtonEntity):
|
||||
"""Generate a brand-new identity/address for this identity."""
|
||||
|
||||
_attr_translation_key = "regenerate"
|
||||
_attr_icon = "mdi:key-change"
|
||||
_attr_device_class = ButtonDeviceClass.RESTART
|
||||
|
||||
def __init__(self, manager, entry, subentry) -> None:
|
||||
super().__init__(manager, entry, subentry)
|
||||
self._attr_unique_id = f"{subentry.subentry_id}_regenerate"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
try:
|
||||
address = await self._manager.async_regenerate_identity()
|
||||
except ReticulumError as err:
|
||||
_LOGGER.warning("Identity regeneration failed: %s", err)
|
||||
return
|
||||
_LOGGER.info(
|
||||
"Identity '%s' regenerated; new address %s",
|
||||
self._manager.display_name,
|
||||
address,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user