From c5dc495b52d2f17a0ce7ee10c382b8b7562cacab Mon Sep 17 00:00:00 2001 From: Marco Ragonesi Date: Wed, 29 Apr 2026 23:42:36 +0200 Subject: [PATCH 1/2] feat: add message_tts parameter for separate TTS and text messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional message_tts field to the send service schema. When provided, voice channels (Google Home, Alexa) use message_tts instead of message, while text channels (Telegram, Mobile App) keep using message. Falls back to message if message_tts is not set. Also supported per-target via target_data[alias][message_tts]. Use case: send emoji/markdown-rich text to Telegram while sending clean, readable speech to voice assistants from a single service call. Example: action: universal_notifier.send data: message: '✅ R2D2 ha finito! Batteria: 85% 🤖' message_tts: 'R2D2 ha terminato le pulizie. Batteria all ottantacinque percento.' targets: [telegram, google_home] --- custom_components/universal_notifier/__init__.py | 8 ++++++-- custom_components/universal_notifier/const.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/universal_notifier/__init__.py b/custom_components/universal_notifier/__init__.py index 4e38e41..15b9c93 100644 --- a/custom_components/universal_notifier/__init__.py +++ b/custom_components/universal_notifier/__init__.py @@ -20,7 +20,7 @@ CONF_CHANNELS, CONF_ASSISTANT_NAME, CONF_DATE_FORMAT, CONF_GREETINGS, CONF_TIME_SLOTS, CONF_DND, CONF_BOLD_PREFIX, # Service keys (Inputs) - CONF_MESSAGE, CONF_TITLE, CONF_TARGETS, CONF_DATA, CONF_TARGET_DATA, + CONF_MESSAGE, CONF_MESSAGE_TTS, CONF_TITLE, CONF_TARGETS, CONF_DATA, CONF_TARGET_DATA, CONF_PRIORITY, CONF_SKIP_GREETING, CONF_INCLUDE_TIME, CONF_PRIORITY_VOLUME, CONF_OVERRIDE_GREETINGS, CONF_PERSON_ENTITIES, # Inner Channel keys @@ -123,6 +123,7 @@ async def _apply_resume(hass: HomeAssistant, entity_id: str, target_volume: floa SEND_SERVICE_SCHEMA = vol.Schema({ vol.Required(CONF_MESSAGE): cv.string, vol.Required(CONF_TARGETS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MESSAGE_TTS): cv.string, vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_DATA): dict, vol.Optional(CONF_TARGET_DATA): dict, @@ -223,6 +224,7 @@ async def async_send_notification(call: ServiceCall): """Handler principale del servizio 'send'.""" # 1. Parsing Input global_raw_message = call.data.get(CONF_MESSAGE, "") + global_raw_tts_message = call.data.get(CONF_MESSAGE_TTS) # None se non fornito global_title = call.data.get(CONF_TITLE) runtime_data = call.data.get(CONF_DATA, {}) target_specific_data = call.data.get(CONF_TARGET_DATA, {}) @@ -280,6 +282,7 @@ async def async_send_notification(call: ServiceCall): specific_data = target_specific_data[target_alias].copy() _LOGGER.debug(f"UniNotifier: Specific Data {specific_data}") target_raw_message = specific_data.pop(CONF_MESSAGE, global_raw_message) + target_raw_tts_message = specific_data.pop(CONF_MESSAGE_TTS, global_raw_tts_message) dynamic_entities = specific_data.pop(CONF_ENTITY_ID, channel_conf.get(CONF_TARGET)) if isinstance(dynamic_entities, str): @@ -328,7 +331,8 @@ async def async_send_notification(call: ServiceCall): final_msg = target_raw_message else: if is_voice_channel: - clean_msg = clean_text_for_tts(str(target_raw_message)) + tts_source = target_raw_tts_message if target_raw_tts_message is not None else target_raw_message + clean_msg = clean_text_for_tts(str(tts_source)) clean_greet = clean_text_for_tts(current_greeting) full_spoken_text = "" if final_title: diff --git a/custom_components/universal_notifier/const.py b/custom_components/universal_notifier/const.py index cd2ddc7..d50e640 100644 --- a/custom_components/universal_notifier/const.py +++ b/custom_components/universal_notifier/const.py @@ -14,6 +14,7 @@ # --- Chiavi Parametri Servizio (Service Call) --- CONF_MESSAGE = "message" +CONF_MESSAGE_TTS = "message_tts" CONF_TITLE = "title" CONF_TARGETS = "targets" CONF_DATA = "data" From 8accb04cea40a223971315a04dd9d17cf8086e68 Mon Sep 17 00:00:00 2001 From: Marco Ragonesi Date: Thu, 30 Apr 2026 00:08:52 +0200 Subject: [PATCH 2/2] fix: pass parse_mode to notify services to match message formatting When building text messages with HTML or MarkdownV2 formatting, the parse_mode was stripped before reaching the notify service call. This caused Telegram to reject messages silently when the bot's default parse_mode (e.g. markdownv2) didn't match the tags in the formatted text (e.g. HTML tags). Now parse_mode is injected into service_payload['data'] for notify channels so Telegram renders formatting correctly. --- custom_components/universal_notifier/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/custom_components/universal_notifier/__init__.py b/custom_components/universal_notifier/__init__.py index 15b9c93..2c0f2b7 100644 --- a/custom_components/universal_notifier/__init__.py +++ b/custom_components/universal_notifier/__init__.py @@ -447,6 +447,13 @@ async def async_send_notification(call: ServiceCall): else: service_payload.update(all_additional_data) + # Passa parse_mode ai canali notify non-voce così Telegram usa + # lo stesso formato (HTML/MarkdownV2) con cui è stato costruito il testo. + if not is_voice_channel and not is_command_message and srv_domain == "notify" and parse_mode: + if "data" not in service_payload: + service_payload["data"] = {} + service_payload["data"].setdefault("parse_mode", parse_mode) + #################################################################### physical_players = [] # J. DISPATCH LOGIC: CODA PER VOCE vs IMMEDIATO