How I Centralized Notifications Across 28 Home Assistant Automations

After several months with one central Home Assistant notification script, I can route 28 automations by presence, room occupancy, priority, and night mode without teaching every automation about my phones and speakers.

An editorial scene with a dark-screen smartphone and a smart speaker arranged around a small central home-automation hub in a quiet room.
Editorial image generated for Technology, Applied.; not a depiction of Lukas' home or devices. Editorial image: AI-generated

At some point, notifications stopped being a small detail in my Home Assistant setup. Every automation knew where its own message had to go. Some targeted my phone, some used a speaker, and several carried variations of the same delivery logic.

That worked while the number of automations was still manageable. Then the maintenance cost became hard to ignore. Why were my Echo speakers talking when nobody was home? Should a message arrive through push, speech, or both? And what would happen the next time I changed phones and had to replace the target in every relevant automation?

I eventually moved the delivery logic into one central script. The 28 automations connected to it now know the event and the information they want to send. They no longer need to know the concrete phone, speaker, or routing rule that will deliver it.

I have used this setup for several months. It is still imperfect, and the complete script below carries some historical baggage. Even so, it has become one of the best investments I have made in my smart home.

The repeated logic was the real problem

Before I introduced this abstraction, each automation handled its own notification. The same questions appeared again and again: Should this be a push message? Should a speaker announce it? Should both happen? Is anybody home to hear it?

The more automations I added, the more often I duplicated those answers. A change to a target device meant finding every place that knew about it. Replacing a smartphone was the clearest example: the notification target had changed, but the underlying events had not. Editing all of those automations would have been maintenance work created entirely by the architecture.

My software-development instinct was to separate two responsibilities:

  • An automation knows what happened, how relevant it is, and what information should be communicated.
  • A central script knows the available delivery channels, concrete target devices, and the household context that decides whether a message should be delivered now.

This was not about sending less useful information by definition. It was about deciding in one place how useful information reaches the right person.

My most important lesson: normal operation should be quiet

While consolidating the logic, I noticed a second problem: a smart home can become far too talkative.

Not every event deserves a message. Normal states usually do not need an active notification. Deviations and genuine action requirements matter more. If the system reports too much, I become used to the noise and pay less attention when a message is actually important.

That changed the question I ask when building an automation. I no longer start with how should I send this? I start with do I need to receive this at all? Some status information is still helpful to me, such as the washing machine finishing. Plenty of other events do not justify an interruption.

My VELUX KLF-200 self-healing automation is a good example of how that judgement keeps evolving. During the first phase, I wanted to know when recovery started and when it succeeded. That helped me observe the new automation. Over time, however, even a successful self-healing run can become unnecessary noise. I am considering keeping only the failure notification, because the entire point of automation is that I do not have to think about a successful repair attempt.

For me, fewer notifications mean fewer interruptions and more attention for the messages that remain.

One script, two responsibilities

The individual automation still performs its actual job. At the appropriate point, it calls the notification script and passes a set of parameters. The automation decides the message, its priority, and whether speech or push should be considered. The script then evaluates the current context and owns the final delivery decision.

The conceptual path is:

Relevance -> urgency -> context -> channel -> destination
Multiple household events entering one central notification script, which evaluates presence, room occupancy, priority, and night mode before routing to push notifications or voice announcements.
English source-derived adaptation of the German architecture graphic. It explains the routing model; it is not runtime evidence.

In the current version, the calling automation still chooses more than pure relevance. A later version could let the script derive the channel itself and reduce the number of parameters. That would be a stronger abstraction, but it is not what I run today.

Home Assistant’s own script documentation describes fields as inputs available to callers and queued mode as sequential execution in start order. Those platform features are the basis for my interface; the routing policy itself is my implementation.

Presence chooses the channel

The first routing decision is whether a person is home.

When I am away, the script sends a push notification to my phone. When I am home, it normally uses speech instead and skips the duplicate push. If I have already heard that the washing machine is finished, I do not need the same information waiting on my lock screen.

The decision is per person. One person can be at home and hear the announcement while another receives a push message elsewhere. We both get the same information through the channel most likely to reach us at that moment.

Home Assistant represents this through person and zone state. The official Person integration documentation explains how device trackers contribute to person state, while the Companion App location documentation describes how the mobile app creates a device tracker and sends location updates. That is external platform behavior; my source-supported experience is the routing result in my own setup: I receive noticeably fewer push notifications.

Room presence chooses the speaker

Person presence only tells the script whether speech is appropriate. I later added another layer to decide where the speech should play.

My Aqara FP2 and SONOFF presence sensors report whether individual rooms are occupied. The script builds its speaker targets from those states. An empty room stays quiet; an occupied room receives the announcement. If several rooms are occupied, the message plays in each of them.

Home Assistant device page showing an Aqara FP2 with four presence sensor entities and recent occupancy activity.
Original German source screenshot of the FP2 device and room-presence entities. Privacy/redaction review is still open.

I do not need exact indoor positioning for this. A simple occupied/unoccupied state is enough. I have observed occasional delay after leaving a room, so a speaker there can still announce a message briefly after the room becomes empty. In practice, that has not bothered me. Missing an important notification because no room appeared occupied would be worse, and that has not happened so far.

Coverage is not complete. The bedroom does not yet have a presence sensor, so its speaker announces independently of room occupancy. That is a deliberate exception caused by missing hardware, not a behavior I want to hide behind a generic architecture diagram.

Night mode, priority, and an always-push override

Channel alone is not enough. The script also evaluates priority.

I activate a night mode manually through the bedtime voice routine. Once it is active, normal-priority speech is suppressed. High-priority messages can still be announced, and they play on all configured speakers regardless of room-presence state. That increases the chance that I hear an urgent alert.

A mailbox event is the kind of message I do not want at night. A water-leak sensor is different. The night-mode filter lets me express that distinction centrally instead of repeating time conditions in every caller.

There is also an immer_per_pushnachricht_senden input—literally, “always send by push.” I use it when a notification should leave a history on the phone even if somebody is home. Changes to the alarm-system state are my main example because I want to look back at them and avoid accidentally walking into an armed system.

I use that override sparingly. If every automation enables it, I have merely moved notification overload into the central script rather than solving it.

The complete productive script

This is the complete script from my source article. I anonymized personal information and installation details there before publication. It still contains my German technical identifiers—such as benachrichtigungstext, prioritaet, hoch, and input_boolean.nachtmodus—because changing them would change the interface or logic used by the calling automations.

I translated only reader-facing aliases, field labels, descriptions, and explanatory message text. The block is therefore source-derived and semantically preserved, but it is not byte-for-byte identical to the German configuration.

Before using it, replace the installation-specific persons, entities, speakers, notify actions, device target, and HOME_ASSISTANT_ADRESSE placeholder deliberately. This is my productive configuration, not a universal drop-in blueprint.

alias: Notification
description: ''
mode: queued
fields:
  benachrichtigungstext:
    selector:
      text: null
    name: Notification text
    description: Notification content
    required: true
  an_sprachassistenten_senden:
    selector:
      boolean: {}
    name: Send to voice assistants
    required: true
    default: true
  immer_per_pushnachricht_senden:
    selector:
      boolean: {}
    name: Always send as push notification
    required: true
  an_lametric_senden:
    selector:
      boolean: {}
    name: Send to LaMetric
    default: true
    required: true
  bild_entity:
    selector:
      text: null
    name: Image entity
    required: false
    description: Image entity, for example image.eingang_person
  prioritaet:
    selector:
      select:
        options:
          - normal
          - hoch
        mode: dropdown
    name: Priority
    description: Relevance for voice assistants in night mode
    required: false
    default: normal

sequence:
  - if:
      - condition: or
        conditions:
          - condition: zone
            entity_id: person.lukas
            zone: zone.home
          - condition: zone
            entity_id: person.bewohner_2
            zone: zone.home
      - condition: template
        value_template: '{{ an_sprachassistenten_senden }}'
      - condition: or
        conditions:
          - condition: state
            entity_id: input_boolean.nachtmodus
            state: 'off'
          - condition: template
            value_template: '{{ prioritaet | default(''normal'') == ''hoch'' }}'
      - condition: or
        conditions:
          - condition: or
            conditions:
              - condition: time
                after: '06:00:00'
                before: '22:00:00'
                weekday:
                  - fri
                  - thu
                  - wed
                  - tue
                  - mon
              - condition: time
                after: '09:00:00'
                before: '22:00:00'
                weekday:
                  - sat
                  - sun
          - condition: template
            value_template: '{{ prioritaet | default(''normal'') == ''hoch'' }}'
    then:
      - variables:
          room_routes:
            - occupancy: binary_sensor.arbeitszimmer_belegung
              speaker: media_player.studio_arbeitszimmer
              platform: alexa
            - occupancy: binary_sensor.essbereich_belegung
              speaker: media_player.essbereich
              platform: alexa
            - occupancy: binary_sensor.badezimmer_belegung
              speaker: media_player.badezimmer
              platform: alexa
            - occupancy: binary_sensor.wohnzimmer_belegung
              speaker: media_player.wohnzimmer
              platform: alexa
            - occupancy: binary_sensor.gaeste_wc_belegung
              speaker: media_player.buro
              platform: google

          alexa_targets: >-
            {% set urgent = prioritaet | default('normal') == 'hoch' %}{% set ns
            = namespace(out=[]) %}{% for r in room_routes %}{% if r.platform ==
            'alexa' and (urgent or is_state(r.occupancy, 'on')) %}{% set ns.out
            = ns.out + [r.speaker] %}{% endif %}{% endfor %}{{ ns.out | unique |
            list }}

          google_targets: >-
            {% set urgent = prioritaet | default('normal') == 'hoch' %}{% set ns
            = namespace(out=[]) %}{% for r in room_routes %}{% if r.platform ==
            'google' and (urgent or is_state(r.occupancy, 'on')) %}{% set ns.out
            = ns.out + [r.speaker] %}{% endif %}{% endfor %}{{ ns.out | unique |
            list }}

      - choose:
          - conditions:
              - condition: template
                value_template: '{{ alexa_targets | count > 0 }}'
            sequence:
              - repeat:
                  for_each: '{{ alexa_targets }}'
                  sequence:
                    - action: notify.alexa_media
                      data:
                        message: '{{ benachrichtigungstext }}'
                        data:
                          type: announce
                        target: '{{ repeat.item }}'

      - choose:
          - conditions:
              - condition: template
                value_template: '{{ google_targets | count > 0 }}'
            sequence:
              - repeat:
                  for_each: '{{ google_targets }}'
                  sequence:
                    - action: tts.cloud_say
                      data:
                        entity_id: '{{ repeat.item }}'
                        message: '{{ benachrichtigungstext }}'
                        cache: false

      - action: media_player.play_media
        data:
          announce: true
          media:
            media_content_id: media-source://tts/cloud?message="{{ benachrichtigungstext }}"
            media_content_type: music
            metadata: {}
        target:
          device_id: DEVICE_ID_LAUTSPRECHER

  - if:
      - condition: and
        conditions:
          - condition: or
            conditions:
              - condition: not
                conditions:
                  - condition: zone
                    entity_id: person.lukas
                    zone: zone.home
              - condition: template
                value_template: '{{ immer_per_pushnachricht_senden }}'
          - condition: template
            value_template: >-
              {% set bild = bild_entity | default('') | trim %}{% set picture =
              state_attr(bild, 'entity_picture') | default('') | trim %}{{ bild
              | length == 0 or picture | length == 0 }}
    then:
      - action: notify.mobile_app_lukas_iphone
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'
        enabled: false

      - action: notify.mobile_app_lukas_iphone_17
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'

  - if:
      - condition: and
        conditions:
          - condition: or
            conditions:
              - condition: not
                conditions:
                  - condition: zone
                    entity_id: person.lukas
                    zone: zone.home
              - condition: template
                value_template: '{{ immer_per_pushnachricht_senden }}'
          - condition: template
            value_template: >-
              {% set bild = bild_entity | default('') | trim %}{% set picture =
              state_attr(bild, 'entity_picture') | default('') | trim %}{{ bild
              | length > 0 and picture | length > 0 }}
    then:
      - action: notify.mobile_app_lukas_iphone
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'
          data:
            image: >
              {% set bild = bild_entity | default('') | trim %}{% set image_path
              = state_attr(bild, 'entity_picture') %}
              http://HOME_ASSISTANT_ADRESSE{{ image_path }}
        enabled: false

      - action: notify.mobile_app_lukas_iphone_17
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'
          data:
            image: >
              {% set bild = bild_entity | default('') | trim %}{% set image_path
              = state_attr(bild, 'entity_picture') %}
              http://HOME_ASSISTANT_ADRESSE{{ image_path }}

  - if:
      - condition: and
        conditions:
          - condition: or
            conditions:
              - condition: not
                conditions:
                  - condition: zone
                    entity_id: person.bewohner_2
                    zone: zone.home
              - condition: template
                value_template: '{{ immer_per_pushnachricht_senden }}'
          - condition: template
            value_template: >-
              {% set bild = bild_entity | default('') | trim %}{% set picture =
              state_attr(bild, 'entity_picture') | default('') | trim %}{{ bild
              | length == 0 or picture | length == 0 }}
    then:
      - action: notify.mobile_app_bewohner_2_alt
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'
        enabled: false

      - action: notify.mobile_app_bewohner_2
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'

  - if:
      - condition: and
        conditions:
          - condition: or
            conditions:
              - condition: not
                conditions:
                  - condition: zone
                    entity_id: person.bewohner_2
                    zone: zone.home
              - condition: template
                value_template: '{{ immer_per_pushnachricht_senden }}'
          - condition: template
            value_template: >-
              {% set bild = bild_entity | default('') | trim %}{% set picture =
              state_attr(bild, 'entity_picture') | default('') | trim %}{{ bild
              | length > 0 and picture | length > 0 }}
    then:
      - action: notify.mobile_app_bewohner_2_alt
        metadata: {}
        data:
          title: Smart Home
          message: '{{ benachrichtigungstext }}'
          image: >
            {% set bild = bild_entity | default('') | trim %}{% set image_path =
            state_attr(bild, 'entity_picture') %}
            http://HOME_ASSISTANT_ADRESSE{{ image_path }}
        enabled: false

      - action: notify.mobile_app_bewohner_2
        metadata: {}
        data:
          data:
            image: >
              {% set bild = bild_entity | default('') | trim %}{% set image_path
              = state_attr(bild, 'entity_picture') %}
              http://HOME_ASSISTANT_ADRESSE{{ image_path }}
          message: '{{ benachrichtigungstext }}'
          title: Smart Home

max: 10

The an_lametric_senden field is a technical leftover. An earlier version displayed notifications on my LaMetric; the field no longer has any effect. The fixed speech time windows are another remnant from before night mode became the better control. Disabled old mobile targets show the same history after device changes.

The image-notification branch also reflects my source configuration. It constructs a URL from the image entity’s entity_picture and the HOME_ASSISTANT_ADRESSE placeholder. Current Home Assistant Companion documentation recommends authenticated media_source paths for attachments where possible and documents image-entity proxy paths. Treat the official notification attachment guidance as the current reference before adapting this part to your own network.

A real caller: the washing-machine leak sensor

The water-leak automation shows the interface more clearly than another abstract explanation. It detects moisture and passes the urgent message to script.benachrichtigung. It requests speech, forces a push notification, and assigns high priority so night mode does not silence it.

Home Assistant water-leak automation calling the central notification script with speech, always-push, and high-priority options enabled.
The real water-leak caller in Lukas' Home Assistant setup. Original German source screenshot; human media review is still required.
alias: Washing machine water leak
description: >-
  Immediately warns by voice announcement and push notification when the water
  leak sensor at the washing machine detects moisture.
triggers:
  - trigger: state
    entity_id: binary_sensor.waschmaschine_wasserleck
    to: 'on'
conditions: []
actions:
  - action: script.benachrichtigung
    data:
      benachrichtigungstext: >-
        Warning! The water leak sensor at the washing machine has detected
        moisture. Please check immediately.
      an_sprachassistenten_senden: true
      immer_per_pushnachricht_senden: true
      an_lametric_senden: false
      prioritaet: hoch
mode: single

The caller does not name a phone or speaker. It knows the event, message, urgency, and requested behavior. The central script knows the devices.

My productive script is not perfect

I could have cleaned everything before showing it. That would make the YAML look better, but it would also erase part of what several months of real operation produced.

The unused LaMetric parameter should go. The fixed weekday and weekend speech windows should go as well because night mode now represents the household context more accurately. Some disabled notification targets remain from older phones. Several rooms still lack the occupancy hardware that would let the routing design work consistently everywhere.

The missing hardware is especially instructive. I do not buy an attractive device and then search for a purpose. I start with a problem and add hardware when the use case justifies it. Until that happens, the script has explicit exceptions.

There is also a broader design improvement still ahead: callers currently state both relevance and parts of the desired channel. If they supplied only relevance, the central script could choose the channel completely on its own. That is a reasonable next step, but presenting it as already implemented would misrepresent the configuration above.

What several months and 28 automations changed

The strongest proof came when I replaced my smartphone. I did not have to open 28 automations. I changed the notification target in one script, and I even delegated that bounded code edit to AI. I spent more time setting up the phone itself than replacing the relevant Home Assistant entity.

The script also gave me a consistent vocabulary for notifications. Priority is more than a routing flag: choosing it forces me to decide how important an event really is. Presence and room occupancy reduce duplicate or irrelevant delivery. Night mode lets the household stay quiet without weakening genuinely urgent alerts.

I would choose this architecture again despite its technical debt. My central lesson remains simple: an automation should know what event warrants a notification. It should not need to know the device on which that notification will eventually appear.