Simple AI Enabled System Wide Notification Router

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

  1. An automation fires a router_notify event with a payload
  2. The router checks master and notification switches are on
  3. Variables are resolved; targets, services, AI tone
  4. Optionally enriches the message via AI (info/warning only)
  5. Sends push notifications to resolved targets
  6. Creates a persistent notification if critical
  7. 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 :x: If announce: true + not night + occupied Optional Witty and lighthearted
warning Targeted or all :x: If announce: true + not night + occupied Optional Clear and slightly urgent
critical All devices always :white_check_mark: Always (ignores time/occupancy) :x:

TTS Suppression

TTS will not fire for info/warning when:

  • sensor.time_of_day returns NIGHT, or
  • sensor.house_status returns VACANT

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

  1. Add the new notify service to the notify_services variable block in the automation
  2. Add a matching elif for the new target name
  3. 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

4 Likes