Thought it might be time to give something back to the community, not sure if this will help anyone, but here it is.
My notifications were always a bit rough and inconsistent, and every time I set up a new automation I’d have to remind myself how the notify side of things worked. I looked at a few HACS options for centralised notification handling, but my needs were pretty simple and everything I found felt over-engineered for what I was after.
So I came up with the following. It’s been running for about a week without any issues, at least none I’ve noticed. There’s probably a more elegant way to do it, but this is where I landed.
Home Assistant Notification Router
Central automation for all Home Assistant notifications and TTS announcements. Other automations never call notify services directly, they fire a router_notify event and this automation handles everything from there.
How It Works
- An automation fires a
router_notifyevent with a payload - The router checks master and notification switches are on
- Variables are resolved; targets, services, AI tone
- Optionally enriches the message via AI (info/warning only)
- Sends push notifications to resolved targets
- Creates a persistent notification if critical
- Speaks via TTS if announce is true (or severity is critical)
Prerequisites
The following (or equivalent) should exist in your HA instance:
| Entity | Purpose |
|---|---|
input_boolean.automation_master |
Global automation on/off switch |
input_boolean.automation_notification |
Notification-specific on/off switch |
sensor.time_of_day |
Returns NIGHT during quiet hours |
sensor.house_status |
Returns VACANT when nobody is home |
ai_task.google_ai_task |
AI Task integration, I’m just using Google, looking to make this local when I purchase some better hardware |
tts.piper |
TTS engine, I use Piper |
media_player.ma_all_house |
Media player (group) for TTS, I use Music Assistant |
Trigger Payload
Fire the router_notify event from any automation using:
- event: router_notify
event_data:
title: "Your Title"
message: "Your message here"
severity: warning
announce: true
ai_enrich: true
tag: my_tag
target:
- person1
- person2
- person3
- etc
Payload Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title |
string | No | [HA Name] |
Notification title |
message |
string | Yes | — | The core message content |
severity |
string | No | info |
info, warning, or critical |
announce |
bool | No | false |
Whether to speak via TTS |
tag |
string | No | general |
Notification tag (used for deduplication) |
target |
string or list | No | all |
Who to notify, see targets below |
ai_enrich |
bool | No | false |
Enrich message with AI (info/warning only) |
Targets
| Target | Devices notified |
|---|---|
all |
notify.notify (all mobiles) + Living Room TV |
person1 |
Person 1 phone only |
person2 |
Person 2 phone only |
person3 |
Person 3 phone only |
Targets are stackable as a list. Example: notify two people but not all:
target:
- person1
- person2
Severity Behaviour
| Severity | Push Notifications | Persistent Notification | TTS | AI Enrichment | AI Tone |
|---|---|---|---|---|---|
info |
Targeted or all | If announce: true + not night + occupied |
Optional | Witty and lighthearted | |
warning |
Targeted or all | If announce: true + not night + occupied |
Optional | Clear and slightly urgent | |
critical |
All devices always | Always (ignores time/occupancy) | — |
TTS Suppression
TTS will not fire for info/warning when:
sensor.time_of_dayreturnsNIGHT, orsensor.house_statusreturnsVACANT
Critical severity bypasses both of these checks and always speaks.
AI Enrichment
When ai_enrich: true, the original message is passed to Google AI Task and rewritten as a short, natural-language announcement (max ~20 words). The tone adapts to severity:
- info → witty and lighthearted
- warning → clear and slightly urgent
If the AI call fails or returns nothing, the original message is used as a fallback, the notification will still send.
AI enrichment is disabled for critical severity, those messages are always sent as-is.
Scenario Examples
| Scenario | severity | target | announce | ai_enrich | Result |
|---|---|---|---|---|---|
| Motion at front door | warning |
person1 |
true |
false |
Push to Person 1 + TTS if not night/vacant |
| Washing machine done | info |
all |
true |
true |
AI-enriched push to everyone + TTS |
| Smoke alarm | critical |
— | — | — | Push all + persistent notification + TTS always |
| Package delivered | info |
person1 |
false |
true |
AI-enriched push to Person 1 only, no TTS |
| Back door left open | warning |
all |
false |
false |
Push to everyone, no TTS |
| System status update | info |
person1 |
false |
false |
Simple push to Person 1 |
Notification Deduplication
Notifications use the tag field. If a second notification fires with the same tag before the first is dismissed, it will silently replace the existing one rather than alerting again.
Adding a New Target
- Add the new notify service to the
notify_servicesvariable block in the automation - Add a matching
eliffor the new target name - Update this document
Example: adding `person4’, can name it anything:
{% elif t == 'person4' %}
{% set ns.services = ns.services + ['notify.mobile_app_person4_phone'] %}
YAML
alias: Notification Router
description: >
Central notification router. Trigger via event 'router_notify' with data:
title, message, severity (info/warning/critical), announce (bool), tag,
ai_enrich (bool), target (string or list: Person1/Person2/Person3/all)
triggers:
- trigger: event
event_type: router_notify
conditions:
- condition: state
entity_id: input_boolean.automation_master
state: "on"
- condition: state
entity_id: input_boolean.automation_notification
state: "on"
actions:
- variables:
title: "{{ trigger.event.data.title | default('HA Name') }}"
message: "{{ trigger.event.data.message | default('') }}"
severity: "{{ trigger.event.data.severity | default('info') | lower }}"
announce: "{{ trigger.event.data.announce | default(false) | bool }}"
tag: "{{ trigger.event.data.tag | default('general') }}"
ai_enrich: "{{ trigger.event.data.ai_enrich | default(false) | bool }}"
target_list: >-
{% set raw = trigger.event.data.get('target', 'all') %} {% if raw is
string %}
{{ [raw] }}
{% elif raw | length == 0 %}
{{ ['all'] }}
{% else %}
{{ raw }}
{% endif %}
announce_allowed: |-
{{ announce
and states('sensor.time_of_day') | upper != 'NIGHT'
and states('sensor.house_status') | upper != 'VACANT' }}
ai_tone: >-
{% if severity == 'warning' %}clear and slightly urgent {% else %}witty
and lighthearted{% endif %}
notify_services: |-
{% if severity == 'critical' %}
{{ ['notify.notify', 'notify.livingroomtv'] }}
{% else %}
{% set ns = namespace(services=[]) %}
{% for t in target_list %}
{% if t == 'all' %}
{% set ns.services = ns.services + ['notify.notify', 'notify.livingroomtv'] %}
{% elif t == 'person1' %}
{% set ns.services = ns.services + ['notify.mobile_app_1'] %}
{% elif t == 'person2' %}
{% set ns.services = ns.services + ['notify.mobile_app_2'] %}
{% elif t == 'person3' %}
{% set ns.services = ns.services + ['notify.mobile_app_2'] %}
{% endif %}
{% endfor %}
{{ ns.services }}
{% endif %}
- variables:
final_message: "{{ message }}"
- choose:
- conditions:
- condition: template
value_template: "{{ severity in ['info', 'warning'] and ai_enrich }}"
sequence:
- action: ai_task.generate_data
data:
task_name: "{{ title }}"
entity_id: ai_task.google_ai_task
instructions: >-
Generate a single short {{ ai_tone }} announcement for a smart
home.
Event: "{{ message }}"
Requirements: - Maximum 20 words - Natural spoken language, no
markdown - Suitable for both voice announcement and phone
notification
response_variable: ai_response
- variables:
final_message: "{{ ai_response.data | default(message) }}"
- repeat:
count: "{{ notify_services | length }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: >-
{{ notify_services[repeat.index - 1] ==
'notify.livingroomtv' }}
sequence:
- action: notify.livingroomtv
data:
title: "{{ title }}"
message: "{{ final_message }}"
data:
tag: "{{ tag }}"
position: top-right
default:
- action: "{{ notify_services[repeat.index - 1] }}"
data:
title: "{{ title }}"
message: "{{ final_message }}"
data:
tag: "{{ tag }}"
- choose:
- conditions:
- condition: template
value_template: "{{ severity == 'critical' }}"
sequence:
- action: persistent_notification.create
data:
title: 🚨 {{ title }}
message: "{{ final_message }}"
notification_id: router_{{ tag }}
- choose:
- conditions:
- condition: template
value_template: "{{ severity == 'critical' or announce_allowed }}"
sequence:
- action: tts.speak
target:
entity_id: tts.piper
data:
cache: true
media_player_entity_id: media_player.ma_all_house
message: "{{ final_message }}"
mode: queued