diff --git a/README.md b/README.md
index 6ce5b18..ebd53ff 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
@@ -149,6 +150,36 @@ it is still busy with receiving / sending other mesh messages.
```
+
+React to every message in a channel with a 👋 emoji
+
+```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
+```
+
+
Advanced: Handling incoming text messages from any node without notification platform and its entities
@@ -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.
diff --git a/custom_components/meshtastic/aiomeshtastic/connection/__init__.py b/custom_components/meshtastic/aiomeshtastic/connection/__init__.py
index d0a5640..feeb88e 100644
--- a/custom_components/meshtastic/aiomeshtastic/connection/__init__.py
+++ b/custom_components/meshtastic/aiomeshtastic/connection/__init__.py
@@ -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
@@ -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,
@@ -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)
diff --git a/custom_components/meshtastic/aiomeshtastic/interface.py b/custom_components/meshtastic/aiomeshtastic/interface.py
index c6fc13a..f33d3d5 100644
--- a/custom_components/meshtastic/aiomeshtastic/interface.py
+++ b/custom_components/meshtastic/aiomeshtastic/interface.py
@@ -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
@@ -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):
@@ -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,
)
diff --git a/custom_components/meshtastic/api.py b/custom_components/meshtastic/api.py
index 29458a9..1922d31 100644
--- a/custom_components/meshtastic/api.py
+++ b/custom_components/meshtastic/api.py
@@ -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
@@ -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
@@ -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:
@@ -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,
@@ -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:
diff --git a/custom_components/meshtastic/const.py b/custom_components/meshtastic/const.py
index 017e6d1..314486f 100644
--- a/custom_components/meshtastic/const.py
+++ b/custom_components/meshtastic/const.py
@@ -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
@@ -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"
@@ -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
@@ -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
diff --git a/custom_components/meshtastic/logbook.py b/custom_components/meshtastic/logbook.py
index 1ecc479..17133a5 100644
--- a/custom_components/meshtastic/logbook.py
+++ b/custom_components/meshtastic/logbook.py
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
+# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT
@@ -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,
@@ -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:
@@ -138,11 +155,9 @@ 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:
@@ -150,17 +165,13 @@ async def _on_text_message(
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):
diff --git a/custom_components/meshtastic/services.py b/custom_components/meshtastic/services.py
index 7777ff3..c9b9eee 100644
--- a/custom_components/meshtastic/services.py
+++ b/custom_components/meshtastic/services.py
@@ -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
@@ -36,6 +37,7 @@
ATTR_SERVICE_BROADCAST_CHANNEL_MESSAGE_DATA_MESSAGE,
ATTR_SERVICE_DATA_ACK,
ATTR_SERVICE_DATA_CHANNEL,
+ ATTR_SERVICE_DATA_EMOJI,
ATTR_SERVICE_DATA_FROM,
ATTR_SERVICE_DATA_REPLY_ID,
ATTR_SERVICE_DATA_TO,
@@ -62,7 +64,8 @@
vol.Optional(ATTR_SERVICE_DATA_FROM): cv.string,
vol.Optional(ATTR_SERVICE_DATA_CHANNEL): cv.string,
vol.Required(ATTR_SERVICE_DATA_ACK, default=False): cv.boolean,
- vol.Optional(ATTR_SERVICE_DATA_REPLY_ID): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_REPLY_ID, default=0): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_EMOJI, default=False): cv.boolean,
}
)
@@ -71,7 +74,8 @@
vol.Required(ATTR_SERVICE_DATA_TO): cv.string,
vol.Required(ATTR_SERVICE_SEND_DIRECT_MESSAGE_DATA_MESSAGE): cv.string,
vol.Required(ATTR_SERVICE_DATA_ACK, default=True): cv.boolean,
- vol.Optional(ATTR_SERVICE_DATA_REPLY_ID): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_REPLY_ID, default=0): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_EMOJI, default=False): cv.boolean,
}
)
@@ -80,7 +84,8 @@
vol.Required(ATTR_SERVICE_BROADCAST_CHANNEL_MESSAGE_DATA_CHANNEL): cv.string,
vol.Required(ATTR_SERVICE_BROADCAST_CHANNEL_MESSAGE_DATA_MESSAGE): cv.string,
vol.Required(ATTR_SERVICE_DATA_ACK, default=True): cv.boolean,
- vol.Optional(ATTR_SERVICE_DATA_REPLY_ID): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_REPLY_ID, default=0): cv.positive_int,
+ vol.Required(ATTR_SERVICE_DATA_EMOJI, default=False): cv.boolean,
}
)
@@ -292,7 +297,8 @@ async def handle_service_call(call: ServiceCall) -> ServiceResponse | object:
text=text,
destination_id=to_node_id,
want_ack=call.data[ATTR_SERVICE_DATA_ACK],
- reply_id=call.data.get(ATTR_SERVICE_DATA_REPLY_ID, None),
+ reply_id=call.data[ATTR_SERVICE_DATA_REPLY_ID],
+ emoji=1 if call.data[ATTR_SERVICE_DATA_EMOJI] else 0,
)
return None
@@ -328,7 +334,8 @@ async def handle_service_call(call: ServiceCall) -> ServiceResponse | object:
text=text,
channel_index=channel_index,
want_ack=call.data[ATTR_SERVICE_DATA_ACK],
- reply_id=call.data.get(ATTR_SERVICE_DATA_REPLY_ID, None),
+ reply_id=call.data[ATTR_SERVICE_DATA_REPLY_ID],
+ emoji=1 if call.data[ATTR_SERVICE_DATA_EMOJI] else 0,
)
return None
@@ -372,7 +379,8 @@ async def handler(call: ServiceCall, to: int, channel_index: int | None) -> None
destination_id=to,
channel_index=channel_index,
want_ack=call.data[ATTR_SERVICE_DATA_ACK],
- reply_id=call.data.get(ATTR_SERVICE_DATA_REPLY_ID, None),
+ reply_id=call.data[ATTR_SERVICE_DATA_REPLY_ID],
+ emoji=1 if call.data[ATTR_SERVICE_DATA_EMOJI] else 0,
)
_service_handlers[entry.entry_id][SERVICE_SEND_TEXT] = await _build_default_handler(hass, client, handler)
diff --git a/custom_components/meshtastic/services.yaml b/custom_components/meshtastic/services.yaml
index 7820875..06203a9 100644
--- a/custom_components/meshtastic/services.yaml
+++ b/custom_components/meshtastic/services.yaml
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
+# SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
#
# SPDX-License-Identifier: MIT
@@ -37,8 +38,11 @@ send_text:
reply_id:
required: false
selector:
- text:
- multiline: false
+ template:
+ emoji:
+ required: true
+ selector:
+ boolean:
send_direct_message:
fields:
@@ -60,8 +64,11 @@ send_direct_message:
reply_id:
required: false
selector:
- text:
- multiline: false
+ template:
+ emoji:
+ required: true
+ selector:
+ boolean:
broadcast_channel_message:
fields:
@@ -85,8 +92,11 @@ broadcast_channel_message:
reply_id:
required: false
selector:
- text:
- multiline: false
+ template:
+ emoji:
+ required: true
+ selector:
+ boolean:
request_telemetry:
fields:
diff --git a/custom_components/meshtastic/translations/en.json b/custom_components/meshtastic/translations/en.json
index 8713c8c..def7543 100644
--- a/custom_components/meshtastic/translations/en.json
+++ b/custom_components/meshtastic/translations/en.json
@@ -179,7 +179,11 @@
},
"reply_id": {
"name": "Reply ID",
- "description": "A message ID to reply to."
+ "description": "If set and non-zero, this message is intended to be a reply to a previously sent message with the defined ID. Using a constant value for this field is not very useful, you probably want to use a template such as '{{ trigger.event.data.message_id }}'."
+ },
+ "emoji": {
+ "name": "Emoji",
+ "description": "If true, then what is in the payload should be treated as an emoji, like giving a message a heart or poop emoji."
}
}
},
@@ -201,7 +205,11 @@
},
"reply_id": {
"name": "Reply ID",
- "description": "A message ID to reply to."
+ "description": "If set and non-zero, this message is intended to be a reply to a previously sent message with the defined ID. Using a constant value for this field is not very useful, you probably want to use a template such as '{{ trigger.event.data.message_id }}'."
+ },
+ "emoji": {
+ "name": "Emoji",
+ "description": "If true, then what is in the payload should be treated as an emoji, like giving a message a heart or poop emoji."
}
}
},
@@ -223,7 +231,11 @@
},
"reply_id": {
"name": "Reply ID",
- "description": "A message ID to reply to."
+ "description": "If set and non-zero, this message is intended to be a reply to a previously sent message with the defined ID. Using a constant value for this field is not very useful, you probably want to use a template such as '{{ trigger.event.data.message_id }}'."
+ },
+ "emoji": {
+ "name": "Emoji",
+ "description": "If true, then what is in the payload should be treated as an emoji, like giving a message a heart or poop emoji."
}
}
},
diff --git a/custom_components/meshtastic/translations/en.json.license b/custom_components/meshtastic/translations/en.json.license
index ca40bd5..5e213e3 100644
--- a/custom_components/meshtastic/translations/en.json.license
+++ b/custom_components/meshtastic/translations/en.json.license
@@ -1,3 +1,4 @@
SPDX-FileCopyrightText: 2024-2025 Pascal Brogle @broglep
+SPDX-FileCopyrightText: 2025 Zdeněk Biberle @zdenek-biberle
SPDX-License-Identifier: MIT