Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!--
SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
SPDX-FileCopyrightText: 2025 Ylian Saint-Hilaire @ylianst
SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle

SPDX-License-Identifier: MIT
-->
Expand Down Expand Up @@ -149,6 +150,36 @@ it is still busy with receiving / sending other mesh messages.
```
</details>

<details>
<summary>React to every message in a channel with a 👋 emoji</summary>

```yaml
- id: '1757011496522'
alias: React to every message in a channel with a 👋 emoji
description: ''
triggers:
- domain: meshtastic
device_id: f1837e8872a6de5d76ed0d6abd5462e6
type: channel_message.received
entity_id: meshtastic.gateway_brig_channel_foo
trigger: device
conditions:
- condition: template
value_template: '{{ trigger.event.data.emoji == 0 }}'
alias: Do not react to reactions
actions:
- action: meshtastic.broadcast_channel_message
metadata: {}
data:
ack: false
emoji: true
channel: meshtastic.gateway_brig_channel_foo
message: 👋
reply_id: '{{ trigger.event.data.message_id }}'
mode: single
```
</details>

<details>
<summary>Advanced: Handling incoming text messages from any node without notification platform and its entities</summary>

Expand Down Expand Up @@ -219,6 +250,9 @@ trigger:
channel: 0
gateway: 862525748
message: Sample Message
reply_id: 0
emoji: 0
message_id: 2770141804
```

From contains the node id of the sender of the message, to will have the node id of the gateway for direct messages, or a gateway channel id if the message is directed at the channel.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
# SPDX-FileCopyrightText: 2025 Ovidiu D. Nițan @ov1d1u
# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -251,7 +252,8 @@ async def send_mesh_packet( # noqa: PLR0913
*,
from_node: int | None = None,
ack: bool = False,
reply_id: int | None = None,
reply_id: int = 0,
emoji: int = 0,
want_response: bool = False,
out_callback: Callable[[Packet], Awaitable[None]] | None = None,
ack_callback: Callable[[Packet[mesh_pb2.Routing]], Awaitable[None]] | None = None,
Expand All @@ -265,8 +267,8 @@ async def send_mesh_packet( # noqa: PLR0913
)
mesh_packet.decoded.portnum = port_num
mesh_packet.decoded.want_response = want_response
if reply_id is not None:
mesh_packet.decoded.reply_id = reply_id
mesh_packet.decoded.reply_id = reply_id
mesh_packet.decoded.emoji = emoji
mesh_packet.id = self._generate_packet_id()
if from_node is not None:
mesh_packet.__setattr__("from", from_node)
Expand Down
5 changes: 4 additions & 1 deletion custom_components/meshtastic/aiomeshtastic/interface.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
# SPDX-FileCopyrightText: 2025 Hendrik @novag
# SPDX-FileCopyrightText: 2025 Ovidiu D. Nițan @ov1d1u
# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -1060,7 +1061,8 @@ async def send_text_message( # noqa: PLR0912, PLR0913
want_ack: bool = False,
channel_index: int | None = None,
priority: MeshPacket.Priority | None = None,
reply_id: int | None = None,
reply_id: int = 0,
emoji: int = 0,
on_message_sent: Callable[[Packet], Awaitable[None]] | None = None,
) -> None:
if isinstance(destination, MeshNode):
Expand Down Expand Up @@ -1109,6 +1111,7 @@ async def out_callback(packet: Packet) -> None:
want_response=False,
ack=want_ack,
reply_id=reply_id,
emoji=emoji,
out_callback=out_callback,
)

Expand Down
42 changes: 36 additions & 6 deletions custom_components/meshtastic/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
# SPDX-FileCopyrightText: 2025 Ovidiu D. Nițan @ov1d1u
# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -211,12 +212,15 @@ def _transform_node_info(self, node_info: Mapping[str, Any]) -> Mapping[str, Any

return transformed

async def _publish_event_text_message_out(
async def _publish_event_text_message_out( # noqa: PLR0913
self,
text: str,
message_id: int,
destination_id: int | str = MeshInterface.BROADCAST_ADDR,
channel_index: int | None = None,
*,
reply_id: int,
emoji: int,
) -> None:
if destination_id == MeshInterface.BROADCAST_NUM or channel_index is not None:
to_channel = channel_index
Expand All @@ -233,25 +237,33 @@ async def _publish_event_text_message_out(
"to": {"node": to_node, "channel": to_channel},
"gateway": gateway_id,
"message": text,
"reply_id": reply_id,
"emoji": emoji,
},
)

event_data["message_id"] = message_id
self._hass.bus.async_fire(EVENT_MESHTASTIC_API_TEXT_MESSAGE_OUT, event_data)

async def send_text(
async def send_text( # noqa: PLR0913
self,
text: str,
destination_id: int | str = MeshInterface.BROADCAST_ADDR,
*,
want_ack: bool = False,
channel_index: int | None = None,
reply_id: int | None = None,
reply_id: int = 0,
emoji: int = 0,
) -> bool:
async def _on_message_sent(packet: Packet) -> None:
# publish event so that outgoing messages are recorded to logbook
await self._publish_event_text_message_out(
text, packet.mesh_packet.id, destination_id=destination_id, channel_index=channel_index
text,
packet.mesh_packet.id,
destination_id=destination_id,
channel_index=channel_index,
reply_id=reply_id,
emoji=emoji,
)

try:
Expand All @@ -262,6 +274,7 @@ async def _on_message_sent(packet: Packet) -> None:
want_ack=want_ack,
channel_index=channel_index,
reply_id=reply_id,
emoji=emoji,
on_message_sent=_on_message_sent,
),
timeout=30,
Expand Down Expand Up @@ -310,17 +323,34 @@ async def _on_text_message(self, node: MeshNode, packet: Packet) -> None:
to_channel = None
to_node = packet.to_id

data = packet.data
if data is None:
self._logger.debug("No decoded data in text message packet, ignoring")
return

payload = packet.app_payload
if payload is None:
self._logger.debug("No payload in text message packet, ignoring")
return

mesh_packet = packet.mesh_packet
if mesh_packet is None:
self._logger.debug("No mesh packet in text message packet, ignoring")
return

event_data = self._build_event_data(
node.id,
{
"from": packet.from_id,
"to": {"node": to_node, "channel": to_channel},
"gateway": self.get_own_node()["num"],
"message": packet.app_payload,
"message": payload,
"reply_id": data.reply_id,
"emoji": data.emoji,
},
)

event_data["message_id"] = packet.mesh_packet.id
event_data["message_id"] = mesh_packet.id
self._hass.bus.async_fire(EVENT_MESHTASTIC_API_TEXT_MESSAGE, event_data)

async def _on_telemetry(self, node: MeshNode, telemetry: dict[str, Any]) -> None:
Expand Down
26 changes: 17 additions & 9 deletions custom_components/meshtastic/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
# SPDX-FileCopyrightText: 2025 Ovidiu D. Nițan @ov1d1u
# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -67,6 +68,7 @@ class ConfigOptionNotifyPlatformNodes(enum.StrEnum):
ATTR_SERVICE_DATA_FROM = "from"
ATTR_SERVICE_DATA_ACK = "ack"
ATTR_SERVICE_DATA_REPLY_ID = "reply_id"
ATTR_SERVICE_DATA_EMOJI = "emoji"


ATTR_SERVICE_SEND_TEXT_DATA_TEXT = "text"
Expand All @@ -92,12 +94,19 @@ class MeshtasticDomainEventType(enum.StrEnum):


EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: Final = "message"
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE_ID: Final = "message_id"
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_REPLY_ID: Final = "reply_id"
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_EMOJI: Final = "emoji"


class MeshtasticDomainEventData(TypedDict):
CONF_DEVICE_ID: str
CONF_ENTITY_ID: str | None
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: str
type: MeshtasticDomainEventType
device_id: str
entity_id: str | None
message: str
message_id: int
reply_id: int
emoji: int


# Primary user facing event
Expand All @@ -110,12 +119,11 @@ class MeshtasticDomainEventData(TypedDict):


class MeshtasticDomainMessageLogEventData(TypedDict):
CONF_DEVICE_ID: str
CONF_ENTITY_ID: str
EVENT_MESHTASTIC_MESSAGE_LOG_EVENT_DATA_ATTR_MESSAGE: str
EVENT_MESHTASTIC_MESSAGE_LOG_EVENT_DATA_ATTR_FROM_NAME: str
EVENT_MESHTASTIC_MESSAGE_LOG_EVENT_DATA_ATTR_PKI: bool
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: str
device_id: str
entity_id: str
message: str
from_name: str
pki: bool


# Event used for logbook
Expand Down
35 changes: 23 additions & 12 deletions custom_components/meshtastic/logbook.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -29,7 +30,10 @@
from .const import (
DOMAIN,
EVENT_MESHTASTIC_DOMAIN_EVENT,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_EMOJI,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE_ID,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_REPLY_ID,
EVENT_MESHTASTIC_DOMAIN_MESSAGE_LOG,
EVENT_MESHTASTIC_MESSAGE_LOG_EVENT_DATA_ATTR_FROM_NAME,
EVENT_MESHTASTIC_MESSAGE_LOG_EVENT_DATA_ATTR_MESSAGE,
Expand Down Expand Up @@ -113,6 +117,19 @@ def _publish_message_log_event( # noqa: PLR0913
}
hass.bus.async_fire(event_type=EVENT_MESHTASTIC_DOMAIN_MESSAGE_LOG, event_data=message_log_event_data)

def _build_domain_event_data(
event_type: MeshtasticDomainEventType, device_id: str, event_data: Mapping[str, Any], data: Mapping[str, Any]
) -> MeshtasticDomainEventData:
return {
CONF_TYPE: event_type,
CONF_DEVICE_ID: device_id,
CONF_ENTITY_ID: None,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: data["message"],
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE_ID: event_data["message_id"],
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_REPLY_ID: data["reply_id"],
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_EMOJI: data["emoji"],
}

async def _on_text_message(
event: Event, *, produce_domain_event: bool = True, produce_log_event: bool = True
) -> None:
Expand All @@ -138,29 +155,23 @@ async def _on_text_message(

if produce_domain_event:
if from_device:
domain_event_data: MeshtasticDomainEventData = {
CONF_DEVICE_ID: from_device.id,
CONF_TYPE: MeshtasticDomainEventType.MESSAGE_SENT,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: message,
}
domain_event_data = _build_domain_event_data(
MeshtasticDomainEventType.MESSAGE_SENT, from_device.id, event_data, data
)
if to_channel_entity_id:
domain_event_data[CONF_ENTITY_ID] = to_channel_entity_id
if to_dm_entity_id:
domain_event_data[CONF_ENTITY_ID] = to_dm_entity_id
hass.bus.async_fire(event_type=EVENT_MESHTASTIC_DOMAIN_EVENT, event_data=domain_event_data)

if to_device:
domain_event_data: MeshtasticDomainEventData = {
CONF_DEVICE_ID: to_device.id,
CONF_TYPE: MeshtasticDomainEventType.MESSAGE_RECEIVED,
EVENT_MESHTASTIC_DOMAIN_EVENT_DATA_ATTR_MESSAGE: message,
}

domain_event_data = _build_domain_event_data(
MeshtasticDomainEventType.MESSAGE_RECEIVED, to_device.id, event_data, data
)
if to_channel_entity_id:
domain_event_data[CONF_ENTITY_ID] = to_channel_entity_id
if to_dm_entity_id:
domain_event_data[CONF_ENTITY_ID] = to_dm_entity_id

hass.bus.async_fire(event_type=EVENT_MESHTASTIC_DOMAIN_EVENT, event_data=domain_event_data)

if produce_log_event and (to_dm_entity_id or to_channel_entity_id):
Expand Down
Loading