Count sent messages at send time; implement first-contact greeting

- 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>
This commit is contained in:
claude
2026-07-22 19:28:26 +03:00
parent 3be19028b1
commit 9ae6bb09c4
7 changed files with 32 additions and 12 deletions

View File

@@ -103,7 +103,10 @@ will answer.
> select that agent here. > select that agent here.
- **Reply to any sender** / **Allowed sender addresses** restrict who Assist - **Reply to any sender** / **Allowed sender addresses** restrict who Assist
will answer. will answer.
- **Default recipient** address used by the `notify.reticulum` entity. - **Default recipient** address the `notify.reticulum_send_message` entity
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`, or `propagated`. - **Default delivery method** `direct`, `opportunistic`, or `propagated`.
- **Announce interval** periodic re-announce (seconds; `0` disables). - **Announce interval** periodic re-announce (seconds; `0` disables).
- **Propagation node** + **sync interval** optional store-and-forward node so - **Propagation node** + **sync interval** optional store-and-forward node so
@@ -127,7 +130,7 @@ will answer.
| `binary_sensor.reticulum_connected` | Whether the TCP interface is online | | `binary_sensor.reticulum_connected` | Whether the TCP interface is online |
| `button.reticulum_announce` | Announce now | | `button.reticulum_announce` | Announce now |
| `button.reticulum_sync_propagation_node` | Pull queued messages now | | `button.reticulum_sync_propagation_node` | Pull queued messages now |
| `notify.reticulum_message` | Send a message to the default recipient | | `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 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 work for the TCP link. **RSSI, SNR and link quality only carry data when the

View File

@@ -80,6 +80,8 @@ class RuntimeData:
# Maps a peer address -> the HA conversation_id we opened for it, so # Maps a peer address -> the HA conversation_id we opened for it, so
# each remote user keeps its own Assist conversation context. # each remote user keeps its own Assist conversation context.
self.conversation_ids: dict[str, str] = {} self.conversation_ids: dict[str, str] = {}
# Senders we've already greeted this session (first-contact greeting).
self.greeted: set[str] = set()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -236,6 +238,12 @@ def _make_incoming_handler(
_LOGGER.debug("Ignoring message from non-allowed sender %s", source) _LOGGER.debug("Ignoring message from non-allowed sender %s", source)
return return
# First-contact greeting: send the configured greeting once per sender.
greeting = (options.get(CONF_GREETING) or "").strip()
if greeting and source not in runtime.greeted:
runtime.greeted.add(source)
await _reply(runtime.manager, source, greeting, "Home Assistant")
if not options.get(CONF_ENABLE_ASSIST, True): if not options.get(CONF_ENABLE_ASSIST, True):
return return

View File

@@ -12,5 +12,5 @@
"loggers": ["RNS", "LXMF"], "loggers": ["RNS", "LXMF"],
"requirements": ["rns>=0.9.0", "lxmf>=0.6.0"], "requirements": ["rns>=0.9.0", "lxmf>=0.6.0"],
"single_config_entry": true, "single_config_entry": true,
"version": "1.0.5" "version": "1.0.6"
} }

View File

@@ -362,9 +362,15 @@ class ReticulumManager:
) -> str: ) -> str:
"""Send an LXMF message. Returns the message hash (hex).""" """Send an LXMF message. Returns the message hash (hex)."""
dest_hash = self._normalise_hash(destination) dest_hash = self._normalise_hash(destination)
return await self.hass.async_add_executor_job( message_hash = await self.hass.async_add_executor_job(
self._send, dest_hash, content, title, method, fields self._send, dest_hash, content, title, method, fields
) )
# Count at send time. The delivery callback only fires on a returned
# proof, which many peers/methods never send, so counting there would
# leave this stuck at 0 even for messages that were delivered fine.
self.state.messages_sent += 1
self._push_state()
return message_hash
def _send( def _send(
self, self,
@@ -535,10 +541,10 @@ class ReticulumManager:
@callback @callback
def _on_outbound_delivered(self, message: Any) -> None: def _on_outbound_delivered(self, message: Any) -> None:
# messages_sent is counted at send time (see async_send_message); this
# callback only confirms delivery, so it just emits the event.
import RNS # noqa: PLC0415 import RNS # noqa: PLC0415
self.state.messages_sent += 1
self._push_state()
_fire_event( _fire_event(
self.hass, self.hass,
"delivered", "delivered",

View File

@@ -37,7 +37,7 @@
"allow_all_senders": "Reply to any sender", "allow_all_senders": "Reply to any sender",
"allowed_identities": "Allowed sender addresses", "allowed_identities": "Allowed sender addresses",
"default_recipient": "Default recipient address (for the notify entity)", "default_recipient": "Default recipient address (for the notify entity)",
"greeting": "Greeting message", "greeting": "First-contact greeting message",
"delivery_method": "Default delivery method", "delivery_method": "Default delivery method",
"announce_interval": "Announce interval", "announce_interval": "Announce interval",
"propagation_node": "Propagation node address", "propagation_node": "Propagation node address",
@@ -48,6 +48,7 @@
"enable_assist": "When enabled, incoming LXMF messages are answered by the Assist conversation agent and the reply is sent back over Reticulum.", "enable_assist": "When enabled, incoming LXMF messages are answered by the Assist conversation agent and the reply is sent back over Reticulum.",
"allowed_identities": "One address per line (or comma separated). Only used when 'Reply to any sender' is off.", "allowed_identities": "One address per line (or comma separated). Only used when 'Reply to any sender' is off.",
"default_recipient": "16-byte hex LXMF address that the notify.reticulum entity sends to.", "default_recipient": "16-byte hex LXMF address that the notify.reticulum entity sends to.",
"greeting": "Sent automatically the first time each sender messages Home Assistant (once per sender per restart). Leave blank to disable.",
"announce_interval": "How often (seconds) to announce our address. 0 disables periodic announces.", "announce_interval": "How often (seconds) to announce our address. 0 disables periodic announces.",
"propagation_node": "Optional LXMF propagation node for store-and-forward delivery.", "propagation_node": "Optional LXMF propagation node for store-and-forward delivery.",
"sync_interval": "How often (seconds) to pull queued messages from the propagation node. 0 disables." "sync_interval": "How often (seconds) to pull queued messages from the propagation node. 0 disables."
@@ -79,7 +80,7 @@
"sync": { "name": "Sync propagation node" } "sync": { "name": "Sync propagation node" }
}, },
"notify": { "notify": {
"message": { "name": "Message" } "message": { "name": "Send message" }
} }
}, },
"selector": { "selector": {

View File

@@ -37,7 +37,7 @@
"allow_all_senders": "Reply to any sender", "allow_all_senders": "Reply to any sender",
"allowed_identities": "Allowed sender addresses", "allowed_identities": "Allowed sender addresses",
"default_recipient": "Default recipient address (for the notify entity)", "default_recipient": "Default recipient address (for the notify entity)",
"greeting": "Greeting message", "greeting": "First-contact greeting message",
"delivery_method": "Default delivery method", "delivery_method": "Default delivery method",
"announce_interval": "Announce interval", "announce_interval": "Announce interval",
"propagation_node": "Propagation node address", "propagation_node": "Propagation node address",
@@ -48,6 +48,7 @@
"enable_assist": "When enabled, incoming LXMF messages are answered by the Assist conversation agent and the reply is sent back over Reticulum.", "enable_assist": "When enabled, incoming LXMF messages are answered by the Assist conversation agent and the reply is sent back over Reticulum.",
"allowed_identities": "One address per line (or comma separated). Only used when 'Reply to any sender' is off.", "allowed_identities": "One address per line (or comma separated). Only used when 'Reply to any sender' is off.",
"default_recipient": "16-byte hex LXMF address that the notify.reticulum entity sends to.", "default_recipient": "16-byte hex LXMF address that the notify.reticulum entity sends to.",
"greeting": "Sent automatically the first time each sender messages Home Assistant (once per sender per restart). Leave blank to disable.",
"announce_interval": "How often (seconds) to announce our address. 0 disables periodic announces.", "announce_interval": "How often (seconds) to announce our address. 0 disables periodic announces.",
"propagation_node": "Optional LXMF propagation node for store-and-forward delivery.", "propagation_node": "Optional LXMF propagation node for store-and-forward delivery.",
"sync_interval": "How often (seconds) to pull queued messages from the propagation node. 0 disables." "sync_interval": "How often (seconds) to pull queued messages from the propagation node. 0 disables."
@@ -79,7 +80,7 @@
"sync": { "name": "Sync propagation node" } "sync": { "name": "Sync propagation node" }
}, },
"notify": { "notify": {
"message": { "name": "Message" } "message": { "name": "Send message" }
} }
}, },
"selector": { "selector": {

View File

@@ -37,7 +37,7 @@
"allow_all_senders": "Отвечать любому отправителю", "allow_all_senders": "Отвечать любому отправителю",
"allowed_identities": "Разрешённые адреса отправителей", "allowed_identities": "Разрешённые адреса отправителей",
"default_recipient": "Адрес получателя по умолчанию (для сущности notify)", "default_recipient": "Адрес получателя по умолчанию (для сущности notify)",
"greeting": "Приветственное сообщение", "greeting": "Приветствие при первом обращении",
"delivery_method": "Способ доставки по умолчанию", "delivery_method": "Способ доставки по умолчанию",
"announce_interval": "Интервал анонсов", "announce_interval": "Интервал анонсов",
"propagation_node": "Адрес узла ретрансляции (propagation node)", "propagation_node": "Адрес узла ретрансляции (propagation node)",
@@ -48,6 +48,7 @@
"enable_assist": "Если включено, входящие сообщения LXMF обрабатываются агентом Assist, а ответ отправляется обратно через Reticulum.", "enable_assist": "Если включено, входящие сообщения LXMF обрабатываются агентом Assist, а ответ отправляется обратно через Reticulum.",
"allowed_identities": "По одному адресу в строке (или через запятую). Используется только если «Отвечать любому отправителю» выключено.", "allowed_identities": "По одному адресу в строке (или через запятую). Используется только если «Отвечать любому отправителю» выключено.",
"default_recipient": "16-байтовый hex-адрес LXMF, на который отправляет сущность notify.reticulum.", "default_recipient": "16-байтовый hex-адрес LXMF, на который отправляет сущность notify.reticulum.",
"greeting": "Отправляется автоматически при первом сообщении от каждого отправителя (один раз на отправителя до перезапуска). Оставьте пустым, чтобы отключить.",
"announce_interval": "Как часто (в секундах) анонсировать наш адрес. 0 отключает периодические анонсы.", "announce_interval": "Как часто (в секундах) анонсировать наш адрес. 0 отключает периодические анонсы.",
"propagation_node": "Необязательный узел ретрансляции LXMF для доставки по принципу store-and-forward.", "propagation_node": "Необязательный узел ретрансляции LXMF для доставки по принципу store-and-forward.",
"sync_interval": "Как часто (в секундах) забирать сообщения из очереди узла ретрансляции. 0 отключает." "sync_interval": "Как часто (в секундах) забирать сообщения из очереди узла ретрансляции. 0 отключает."
@@ -79,7 +80,7 @@
"sync": { "name": "Синхронизировать узел ретрансляции" } "sync": { "name": "Синхронизировать узел ретрансляции" }
}, },
"notify": { "notify": {
"message": { "name": "Сообщение" } "message": { "name": "Отправить сообщение" }
} }
}, },
"selector": { "selector": {