Package: Smart Notifications
Version: 1.0.0
Description: Helpers and logic for the dynamic notification router
Executive Summary
The Smart Notifications package is a dynamic, user-centric routing engine for Home Assistant. Unlike static notification groups, this system allows users to manage their own subscriptions to different notification categories (e.g., Security, System, Info) directly from the dashboard. It abstracts the complexity of device management, automatically handling device services (Android/iOS) and presence detection, ensuring alerts are delivered only when relevant and desired.
Process Description (Non-Technical)
The system operates on a "Publish-Subscribe" model adapted for a smart home:
- User Onboarding: An administrator selects a Home Assistant
personentity and "initializes" them. The system automatically generates a suite of configuration "helpers" (switches) for every notification category defined in the system. - Preference Management: Users interact with a dashboard calling these switches. If a user wants to mute "Security" alerts but keep "Energy" alerts, they simply toggle the respective switches.
- Smart Routing: When an automation needs to send a notification, it calls the
notify_smart_masterscript with acategory(e.g.,security) and the message. - Delivery Logic: The router iterates through all registered users. For each user, it checks:
- Subscription: Is the switch for this category ON?
- Presence: If the alert is "local only" or the user is away (and presence checks are enabled), the message is suppressed.
- Platform: It formats the message specifically for the user's defined device type (iOS vs Android), handling critical alerts and actionable notifications appropriate to the platform.
Dashboard Connections
This package powers the following dashboard views:
- System: This view provides deep insights into the Proxmox 'Halo' virtualization node. It features real-time resource monitoring (CPU, RAM) using
mini-graph-card, and critical power controls (Reboot, Shutdown) protected by confirmation dialogs. It also offers bulk management for guest VMs/containers and tracks system update status, ensuring the infrastructure host is healthy and up-to-date. (Uses 1 entities) - Living Room: The Living Room dashboard is a media and comfort hub. It features in-depth environmental monitoring (Radon, VOCs, CO2) via Airthings Wave, displaying historical trends. Entertainment controls are central, with remotes for the TV and Soundbar, plus power management for the media wall. The view also includes specific controls for the fireplace, air purifier modes, and various lighting scenes, alongside standard occupancy settings. (Uses 1 entities)
- Notifications Management: The Notification Center dashboard provides a comprehensive interface for managing the smart home's notification system. Administrators can add or remove users for mobile app notifications and define notification categories (e.g., 'Garage', 'Electricity'). The view allows for granular control over subscriptions, enabling individual users to opt-in or out of specific notification types, and includes tools to map and monitor notification-related automations. (Uses 9 entities)
- Room Management: The Room Management dashboard serves as the administrative backend for the home's room logic. It allows users to initialize new rooms (creating necessary helper entities) or delete existing ones. It features a dynamic "Configured Rooms" section powered by
auto-entities, which automatically lists all configured rooms and provides collapsible controls for their automation modes, occupancy sensors, and timeouts. (Uses 2 entities)
Architecture Diagram
When a notification request triggers the notify_smart_master router, the system begins a parallel evaluation for every registered user. It retrieves the user's specific settings—checking if they have subscribed to the message's category, verifying if a 'Presence Check' is required, and determining their current device platform (iOS or Android). Only if the subscription is active and the presence criteria are met does the router format the payload (adjusting for platform-specifics like critical volume or click actions) and dispatch it to the specific mobile app service.
sequenceDiagram
participant Auto as Automation
participant Router as Script: Smart Master
participant Config as User Switches
participant Logic as Logic Filter
participant App as Mobile App
Auto->>Router: Call (Title, Msg, Category="security")
loop For Each User
Router->>Config: Check State: switch.user_security_notification
Config-->>Router: State "ON"
Router->>Logic: Check Presence Requirement
alt User is Home OR Presence User Not Required
Logic-->>Router: Allowed
Router->>Router: Format Data (Android/iOS)
Router->>App: Send Notification
else User Away AND Required
Logic-->>Router: Blocked
end
end
Configuration (Source Code)
# ------------------------------------------------------------------------------
# Package: Smart Notification System
# Version: 1.0.0
# Description: Helpers and logic for the dynamic notification router
# Dependencies: notify.*, input_select.notify_mgmt_service, script.notify_smart_master
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# 1. HELPERS (Input Text, Select)
# ------------------------------------------------------------------------------
input_text:
# RETAINED: Only retained helpers for categories and deletion IDs
## MASTER LIST FOR THE CATEGORIES
notify_mgmt_categories:
name: "Categories (comma-separated)"
initial: "info,system,security,alarm,doorbell"
# TO BE REMOVED
notify_delete_category:
name: "Category to Delete"
icon: mdi:archive-remove
notify_add_category:
name: "New Category Name"
icon: mdi:playlist-plus
input_select:
# Dropdown for selecting a category to delete
notify_delete_category:
name: "Category to Delete"
icon: mdi:archive-remove
options:
- "-select-"
# Dropdown populated with existing Home Assistant Persons
notify_mgmt_person_select:
name: "Select Person to Initialize"
icon: mdi:account-circle
options:
- "-select-" # Placeholder before population
# List of the Notify services for devices
notify_mgmt_service:
name: "Notify Service"
icon: mdi:cellphone-message
options:
- "-select-"
# Dropdown for mobile platform
notify_mgmt_platform:
name: "Platform"
options:
- android
- ios
icon: mdi:apple-ios
# Dropdown for deleting users
notify_mgmt_delete_user_select:
name: "Select User to Delete"
icon: mdi:account-remove
options:
- "-select-"
# ------------------------------------------------------------------------------
# 2. INPUT BOOLEANS (Flags & Maintenance)
# ------------------------------------------------------------------------------
input_boolean:
# Global Category Settings (Generated dynamically, but good to have base logic ready)
# Note: Individual category switches are MQTT switches now, not input_booleans.
# Removed: notify_flag_person_list_refresh (Not used, we trigger automation directly)
# ... (Rest of the file remains the same)
# ------------------------------------------------------------------------------
# 3. SCRIPTS & AUTOMATIONS (Logic)
# ------------------------------------------------------------------------------
script:
# --- CREATE USER (Pulls Name and ID from Person Entity) ---
create_notify_user:
alias: "System: Create Notification User"
icon: mdi:account-plus
mode: single
sequence:
- variables:
# Get selected person name (e.g., "Evis")
user_name: "{{ states('input_select.notify_mgmt_person_select') }}"
# CRITICAL: Create slug (e.g., "Evis" -> "Evis")
user_slug: "{{ user_name | slugify }}"
# Ensure we link back to the native HA person entity
person_entity: "person.{{ user_name | slugify }}"
notify_service: "{{ states('input_select.notify_mgmt_service') }}"
platform: "{{ states('input_select.notify_mgmt_platform') }}"
categories: "{{ states('input_text.notify_mgmt_categories') }}"
- repeat:
for_each: "{{ categories.split(',') | map('trim') | list }}"
sequence:
# A. Create User Switch
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ user_slug }}_{{ repeat.item }}/config"
payload: >-
{
"name": "{{ repeat.item|capitalize }} Notification",
"default_entity_id": "switch.{{ repeat.item }}_notification_{{ user_slug }}",
"unique_id": "notify_switch_{{ user_slug }}_{{ repeat.item }}",
"icon": "mdi:bell-ring",
"command_topic": "notify/{{ user_slug }}/{{ repeat.item }}/set",
"state_topic": "notify/{{ user_slug }}/{{ repeat.item }}/state",
"availability_topic": "notify/{{ user_slug }}/availability",
"payload_available": "online",
"json_attributes_topic": "notify/{{ user_slug }}/attributes",
"device": {
"identifiers": ["notify_user_{{ user_slug }}"],
"name": "{{ user_name }} |",
"manufacturer": "Home Assistant",
"model": "Notification Profile"
}
}
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/attributes"
payload: >-
{ "user_slug": "{{ user_slug }}", "linked_person": "{{ person_entity }}" }
- if:
- condition: template
value_template: >-
{{ states('switch.' ~ repeat.item ~ '_notification_' ~ user_slug) in ['unknown', 'unavailable', 'none'] }}
then:
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/{{ repeat.item }}/state"
payload: "ON"
# B. Create GLOBAL CATEGORY SETTING Switch (Unchanged)
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_category_{{ repeat.item }}_local/config"
payload: >-
{
"name": "Notify {{ repeat.item|capitalize }}: When Home",
"name": "Notify {{ repeat.item|capitalize }}: When Home",
"unique_id": "notify_category_{{ repeat.item }}_local",
"icon": "mdi:home-account",
"command_topic": "notify/settings/{{ repeat.item }}/local_only/set",
"state_topic": "notify/settings/{{ repeat.item }}/local_only/state",
"device": {
"identifiers": ["notify_system_settings"],
"name": "Cat |",
"manufacturer": "Home Assistant",
"model": "Global Rules"
}
}
- if:
- condition: template
value_template: >-
{{ states('switch.notify_category_' ~ repeat.item ~ '_local') in ['unknown', 'unavailable', 'none'] }}
then:
- service: mqtt.publish
data:
retain: true
topic: "notify/settings/{{ repeat.item }}/local_only/state"
payload: "OFF"
# C. Create Helpers
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/text/notify_{{ user_slug }}_service/config"
payload: >-
{
"name": "{{ user_name }} Notify Service",
"name": "{{ user_name }} Notify Service",
"unique_id": "notify_service_{{ user_slug }}",
"icon": "mdi:cellphone",
"command_topic": "notify/{{ user_slug }}/service/set",
"state_topic": "notify/{{ user_slug }}/service/state",
"availability_topic": "notify/{{ user_slug }}/availability",
"device": { "identifiers": ["notify_user_{{ user_slug }}"] }
}
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/service/state"
payload: "{{ notify_service }}"
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/select/notify_{{ user_slug }}_platform/config"
payload: >-
{
"name": "{{ user_name }} Phone Platform",
"name": "{{ user_name }} Phone Platform",
"unique_id": "notify_platform_{{ user_slug }}",
"icon": "mdi:apple-ios",
"options": ["android", "ios"],
"command_topic": "notify/{{ user_slug }}/platform/set",
"state_topic": "notify/{{ user_slug }}/platform/state",
"availability_topic": "notify/{{ user_slug }}/availability",
"device": { "identifiers": ["notify_user_{{ user_slug }}"] }
}
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/platform/state"
payload: "{{ platform }}"
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/availability"
payload: "online"
# Refresh Lists
- delay: "00:00:02"
- service: automation.trigger
target:
entity_id: automation.system_populate_notify_services
data:
skip_condition: true
# Reset Dropdowns
- service: input_select.select_option
target:
entity_id:
- input_select.notify_mgmt_person_select
- input_select.notify_mgmt_service
data:
option: "-select-"
# --- DELETE USER (Updated to use Dropdown) ---
delete_notify_user:
alias: "System: Delete Notification User"
icon: mdi:account-remove
mode: single
sequence:
- variables:
# Get slug from dropdown
user_slug: "{{ states('input_select.notify_mgmt_delete_user_select') }}"
categories: "{{ states('input_text.notify_mgmt_categories') }}"
- repeat:
for_each: "{{ categories.split(',') | map('trim') | list }}"
sequence:
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ user_slug }}_{{ repeat.item }}/config"
payload: ""
# Delete Helpers
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/text/notify_{{ user_slug }}_service/config"
payload: ""
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/select/notify_{{ user_slug }}_platform/config"
payload: ""
# Set Offline
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/availability"
payload: "offline"
# Refresh Lists
- delay: "00:00:02"
- service: automation.trigger
target:
entity_id: automation.system_populate_notify_services
data:
skip_condition: true
# Reset Dropdown
- service: input_select.select_option
target:
entity_id: input_select.notify_mgmt_delete_user_select
data:
option: "-select-"
# --- DELETE CATEGORY ---
delete_notify_category_global:
alias: "System: Delete Notification Category"
icon: mdi:archive-remove
mode: single
sequence:
- variables:
# NOW USES THE INPUT SELECT
category: "{{ states('input_select.notify_delete_category') | trim | lower }}"
current_list: "{{ states('input_text.notify_mgmt_categories') }}"
# Prevent deleting if 'select' is chosen or empty
- condition: template
value_template: "{{ category not in ['-select-', 'select', 'unknown', 'unavailable', ''] }}"
# Remove from Master List (Text Input)
- service: input_text.set_value
target:
entity_id: input_text.notify_mgmt_categories
data:
value: >-
{{ current_list.split(',')
| map('trim')
| reject('eq', category)
| join(',') }}
# Remove Global Switch Config
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_category_{{ category }}_local/config"
payload: ""
# Remove User Switches for this category
- repeat:
for_each: >-
{{ states.switch
| selectattr('entity_id', 'search', '^switch\.' ~ category ~ '_notification_')
| map(attribute='attributes.user_slug')
| reject('undefined')
| unique
| list }}
sequence:
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ repeat.item }}_{{ category }}/config"
payload: ""
# Refresh Lists (updates dropdowns immediately)
- delay: "00:00:01"
- service: automation.trigger
target:
entity_id: automation.system_populate_notify_services
data:
skip_condition: true
# Reset dropdown to safe default
- service: input_select.select_option
target:
entity_id: input_select.notify_delete_category
data:
option: "-select-"
# --- ADD NEW CATEGORY (Global + All Users) ---
add_notify_category_global:
alias: "System: Add Notification Category"
icon: mdi:playlist-plus
mode: single
sequence:
- variables:
new_category: "{{ states('input_text.notify_add_category') | trim | lower }}"
current_list: "{{ states('input_text.notify_mgmt_categories') }}"
# 1. Update Master List (if not exists)
- if:
- condition: template
value_template: "{{ new_category not in current_list.split(',') }}"
then:
- service: input_text.set_value
target:
entity_id: input_text.notify_mgmt_categories
data:
value: "{{ current_list ~ ',' ~ new_category if current_list|length > 0 else new_category }}"
# 2. Create Global Setting Switch
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_category_{{ new_category }}_local/config"
payload: >-
{
"name": "Notify {{ new_category|capitalize }}: When Home",
"default_entity_id": "switch.notify_category_{{ new_category }}_local",
"unique_id": "notify_category_{{ new_category }}_local",
"icon": "mdi:home-account",
"command_topic": "notify/settings/{{ new_category }}/local_only/set",
"state_topic": "notify/settings/{{ new_category }}/local_only/state",
"device": {
"identifiers": ["notify_system_settings"],
"name": "Cat |",
"manufacturer": "Home Assistant",
"model": "Global Rules"
}
}
# Init to OFF
- service: mqtt.publish
data:
retain: true
topic: "notify/settings/{{ new_category }}/local_only/state"
payload: "OFF"
# 3. Add Switch to ALL Existing Users
- repeat:
for_each: >-
{{ states.text
| selectattr('entity_id', 'search', '^text\.notify_service_')
| map(attribute='object_id')
| map('replace', 'notify_service_', '')
| list }}
sequence:
- variables:
user_slug: "{{ repeat.item }}"
# Create the switch for this user
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ user_slug }}_{{ new_category }}/config"
payload: >-
{
"name": "{{ new_category|capitalize }} Notification",
"object_id": "{{ new_category }}_notification_{{ user_slug }}",
"default_entity_id": "switch.{{ new_category }}_notification_{{ user_slug }}",
"unique_id": "notify_switch_{{ user_slug }}_{{ new_category }}",
"icon": "mdi:bell-ring",
"command_topic": "notify/{{ user_slug }}/{{ new_category }}/set",
"state_topic": "notify/{{ user_slug }}/{{ new_category }}/state",
"availability_topic": "notify/{{ user_slug }}/availability",
"payload_available": "online",
"json_attributes_topic": "notify/{{ user_slug }}/attributes",
"device": {
"identifiers": ["notify_user_{{ user_slug }}"],
"manufacturer": "Home Assistant",
"model": "Notification Profile"
}
}
# Set State ON
- service: mqtt.publish
data:
retain: true
topic: "notify/{{ user_slug }}/{{ new_category }}/state"
payload: "ON"
# 4. Add Switch to ALL Smart Speakers
- variables:
all_speakers: >-
{{ states.switch
| selectattr('attributes.speaker_slug', 'defined')
| map(attribute='attributes.speaker_slug')
| unique
| list }}
- repeat:
for_each: "{{ all_speakers }}"
sequence:
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/speaker_{{ repeat.item }}_notify_{{ new_category }}/config"
payload: >-
{{
{
"name": new_category | title,
"default_entity_id": "switch.speaker_" ~ repeat.item ~ "_notify_" ~ new_category,
"unique_id": "speaker_notif_" ~ repeat.item ~ "_" ~ new_category ~ "_v4",
"icon": "mdi:bell-ring",
"command_topic": "speaker/" ~ repeat.item ~ "/notify_" ~ new_category ~ "/set",
"state_topic": "speaker/" ~ repeat.item ~ "/notify_" ~ new_category ~ "/state",
"payload_on": "ON",
"payload_off": "OFF",
"device": { "identifiers": ["smart_speaker_" ~ repeat.item] }
} | to_json
}}
# Default to ON
- service: mqtt.publish
data:
retain: true
topic: "speaker/{{ repeat.item }}/notify_{{ new_category }}/state"
payload: "ON"
# Refresh Lists
- delay: "00:00:01"
- service: automation.trigger
target:
entity_id: automation.system_populate_notify_services
data:
skip_condition: true
# --- MASTER ROUTER (Unchanged) ---
notify_smart_master:
alias: "Notify: Master Router"
icon: mdi:message-badge-outline
mode: parallel
fields:
category:
description: "Notification Category"
default: "info"
title:
description: "Title"
message:
description: "Body"
image:
description: "Image Path"
tag:
description: "Grouping tag"
actions:
description: "List of buttons"
clickAction:
description: "Link click"
sticky:
description: "Sticky"
critical:
description: "Critical alert"
presence_only:
description: "Override: Force check presence"
default: null
source_room:
description: "Slug of the room where event originated (for suppression)"
default: null
room:
description: "Override: Specific room to target (bypass occupancy)"
default: null
sequence:
- variables:
global_presence_setting: >-
{{ states('switch.notify_category_' ~ category | default('info') ~ '_local') == 'on' }}
use_presence_check: >-
{% set pres = presence_only | default(none) %}
{{ pres if pres is not none else global_presence_setting }}
- repeat:
for_each: >-
{{ states.switch
| selectattr('entity_id', 'search', '\.' ~ category | default('info') ~ '_notification_')
| selectattr('state', 'eq', 'on')
| map(attribute='entity_id')
| list }}
sequence:
- variables:
match_id: "{{ repeat.item }}"
# Fallback: Extract from Entity ID
slug_from_id: >-
{{ repeat.item
| replace('switch.', '')
| replace(category | default('info') ~ '_notify_', '')
| replace('_notification', '') }}
user_slug: "{{ state_attr(repeat.item, 'user_slug') | default(slug_from_id, true) }}"
# Fetch Service
target_service: "{{ states('text.notify_service_' ~ user_slug) }}"
# Fetch Linked Person for Presence Check
linked_person: "{{ state_attr(repeat.item, 'linked_person') }}"
- service: system_log.write
data:
message: "DEBUG MOBILE {{ user_slug }} | Service: {{ target_service }} | Match: {{ match_id }}"
level: warning
- if:
- condition: template
value_template: >-
{{
target_service is not none and target_service not in ['unknown', 'unavailable', '']
and (
use_presence_check == false
or (linked_person is not none and is_state(linked_person, 'home'))
)
}}
then:
- service: "{{ target_service }}"
data:
title: "{{ title | default('Notification') }}"
message: "{{ message }}"
data:
tag: "{{ tag | default('') }}"
image: "{{ image | default('') }}"
sticky: "{{ sticky | default(false) }}"
push:
sound:
name: "default"
critical: "{{ 1 if critical | default(false) else 0 }}"
volume: "{{ 1.0 if critical | default(false) else 0.5 }}"
ttl: "{{ 0 if critical | default(false) else 0 }}"
priority: "{{ 'high' if critical | default(false) else 'normal' }}"
clickAction: >-
{% set clk = clickAction | default('') %}
{% if target_platform == 'ios' and 'app://' in (clk|string) %}
unifi-protect://
{% else %}
{{ clk }}
{% endif %}
actions: "{{ actions | default([]) }}"
# --- 2. SMART SPEAKER ROUTING ---
- variables:
# Define Priority based on Critical flag or Category
# Categories 'alarm', 'security', 'doorbell', 'smoke' are inherently Critical
is_critical: >-
{{ critical | default(false) or category in ['alarm', 'security', 'doorbell', 'smoke'] }}
- repeat:
# Find all speakers subscribed to this category
for_each: >-
{{ states.switch
| selectattr('entity_id', 'search', '^switch\.speaker_.*_notify_' ~ category | default('info'))
| selectattr('state', 'eq', 'on')
| map(attribute='entity_id')
| list }}
sequence:
- variables:
# Extract Slug: switch.speaker_{slug}_notify_{category}
config_entity: "{{ repeat.item }}"
# Regex to pull slug
speaker_slug: >-
{{ config_entity
| replace('switch.speaker_', '')
| replace('_notify_' ~ category | default('info'), '') }}
# Get Config Entity (now attached to Quiet Mode Switch)
speaker_config_entity: "switch.speaker_{{ speaker_slug }}_quiet_mode"
# Attributes are on the switch now
# Attributes are on the switch now
media_player: "{{ state_attr(speaker_config_entity, 'entity_id') }}"
# Get Linked Areas
linked_areas: "{{ state_attr(media_player, 'linked_areas') | default([]) }}"
# Status Tests
dnd_switch: "{{ speaker_config_entity | default('') }}"
# Check if Quiet Mode is ON
is_dnd_active: "{{ is_state(dnd_switch, 'on') }}"
# Room State Checks (Sleep/DND)
# We check if ANY linked room is in a restrictive state
is_room_quiet: >-
{% set ns = namespace(quiet=false) %}
{% for area in linked_areas %}
{% set state = states('select.area_' ~ area ~ '_state') %}
{% if state in ['Sleep', 'DND'] %}
{% set ns.quiet = true %}
{% endif %}
{% endfor %}
{{ ns.quiet }}
# Occupancy Check
is_occupied: >-
{% set ns = namespace(occupied=false) %}
{% set areas = linked_areas if linked_areas is iterable and linked_areas is not string else [] %}
{% for area in areas %}
{# Debug Sensor Logic #}
{% set s1 = 'binary_sensor.area_' ~ area ~ '_occupancy' %}
{% set s2 = 'binary_sensor.' ~ area ~ '_occupancy' %}
{% set v1 = states(s1) %}
{% set v2 = states(s2) %}
{# Logic #}
{% if v1 == 'on' or v2 == 'on' %}
{% set ns.occupied = true %}
{% endif %}
{% endfor %}
{{ ns.occupied }}
# Extended Debug Info
debug_occ_trace: >-
{% set areas = linked_areas if linked_areas is iterable and linked_areas is not string else [] %}
{% set ns = namespace(log=[]) %}
{% for area in areas %}
{% set s1 = 'binary_sensor.area_' ~ area ~ '_occupancy' %}
{% set s2 = 'binary_sensor.' ~ area ~ '_occupancy' %}
{% set ns.log = ns.log + [s1 ~ '=' ~ states(s1) ~ ' / ' ~ s2 ~ '=' ~ states(s2)] %}
{% endfor %}
{{ ns.log | join(', ') }}
# Source Suppression Check
# If source_room is provided, and matches a linked room, and I am in that room?
# Design: "If source_room is Occupied, start simple: If source matches linked room, suppress"
# Actually User said: "coffee ready... you in kitchen... silence"
# So: If source_room is in linked_rooms AND binary_sensor.room_{source_room}_occupancy is ON -> Suppress
is_at_source: >-
{% if source_room is defined and source_room and source_room in linked_areas and is_state('binary_sensor.area_' ~ source_room ~ '_occupancy', 'on') %}
true
{% else %}
false
{% endif %}
# Targeted Override: If 'room' var is passed, force speak in that room (bypass occupancy)
# Targeted Override: Check if my linked rooms match the requested targets
is_targeted: >-
{% if room is defined and room not in [none, '', 'unknown'] %}
{% set targets = (room | string).split(',') | map('trim') | list %}
{% set ns = namespace(match=false) %}
{% for r in linked_areas %}
{% if r in targets %}
{% set ns.match = true %}
{% endif %}
{% endfor %}
{{ ns.match }}
{% else %}
false
{% endif %}
- service: system_log.write
data:
message: >-
DEBUG SPEAKER {{ speaker_slug }} |
Media: {{ media_player }} |
Targeted: {{ is_targeted }} |
Room: {{ room }} |
Linked: {{ linked_rooms }} |
Occ: {{ is_occupied }} |
DND: {{ is_dnd_active }} |
Quiet: {{ is_room_quiet }}
level: warning
# LOGIC GATE
- if:
- condition: template
value_template: >-
{% set source_bool = is_at_source | bool %}
{% set allow_priority = is_critical or (not is_dnd_active and not is_room_quiet) %}
{# Logic Gate: Exclusive Targeting vs Occupancy #}
{% set global_target_defined = room is defined and room not in [none, '', 'unknown'] %}
{% if global_target_defined %}
{# If targeting is active, ONLY allow targeted speakers #}
{% set allow_occupancy = is_targeted %}
{% else %}
{# Standard Mode: Play if Critical OR Occupied #}
{% set allow_occupancy = is_critical or is_occupied %}
{% endif %}
{% set allow_source = not source_bool %}
{{ allow_priority and allow_occupancy and allow_source }}
then:
- service: system_log.write
data:
message: "DEBUG: Speaking to {{ media_player }} (Method: Cloud Say). Msg: {{ message }}"
level: warning
- service: tts.cloud_say
target:
entity_id: "{{ media_player }}"
data:
message: "{{ message }}"
# Optionally add chimes for critical?
options:
voice: "Katherine" # Customize as needed
# --- DEBUG DIAGNOSTICS ---
debug_notify_diagnostics:
alias: "System: Debug Notification Targets"
icon: mdi:bug
fields:
category:
description: "Category to test"
default: "info"
sequence:
- variables:
cat: "{{ category | default('info') }}"
# 1. Dump ALL Switches that look related
all_switches: >-
{{ states.switch | map(attribute='entity_id') | select('search', 'notify|speaker') | list }}
# 2. Test Mobile Regex
mobile_regex: "_{{ cat }}_notification"
mobile_matches: >-
{{ states.switch
| selectattr('entity_id', 'search', mobile_regex)
| map(attribute='entity_id') | list }}
# 3. Test Speaker Regex
speaker_regex: "^switch\\.speaker_.*_notify_{{ cat }}"
speaker_matches: >-
{{ states.switch
| selectattr('entity_id', 'search', speaker_regex)
| map(attribute='entity_id') | list }}
- service: system_log.write
data:
level: warning
message: >-
DEBUG DIAGNOSTICS (Category: {{ cat }})
--------------------------------------------------
Regex Mobile: '{{ mobile_regex }}' -> Found: {{ mobile_matches }}
Regex Speaker: '{{ speaker_regex }}' -> Found: {{ speaker_matches }}
--------------------------------------------------
ALL CANDIDATES: {{ all_switches }}
- service: system_log.write
data:
level: warning
message: >-
DEBUG DIAGNOSTICS (Category: {{ cat }})
--------------------------------------------------
Regex Mobile: '{{ mobile_regex }}' -> Found: {{ mobile_matches }}
Regex Speaker: '{{ speaker_regex }}' -> Found: {{ speaker_matches }}
--------------------------------------------------
ALL CANDIDATES: {{ all_switches }}
# --- PURGE GHOSTS ---
purge_notification_ghosts:
alias: "System: Purge Legacy Notification Ghosts"
icon: mdi:ghost-off
mode: single
sequence:
- variables:
categories: "{{ states('input_text.notify_mgmt_categories').split(',') | map('trim') | list + ['action', 'doorbell', 'doorbell2', 'network'] }}"
users: >-
{{ states.text
| selectattr('entity_id', 'search', '^text\.notify_service_')
| map(attribute='object_id')
| map('replace', 'notify_service_', '')
| list }}
- repeat:
for_each: "{{ users }}"
sequence:
- variables:
user: "{{ repeat.item }}"
- repeat:
for_each: "{{ categories }}"
sequence:
- variables:
cat: "{{ repeat.item }}"
# PATTERN 1: switch.Evis_garden_notification
# Topic: homeassistant/switch/Evis_garden_notification/config (Guess)
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/{{ user }}_{{ cat }}_notification/config"
payload: ""
# PATTERN 2: switch.Evis_garden_notify
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/{{ user }}_{{ cat }}_notify/config"
payload: ""
# PATTERN 3: switch.notify_garden_Evis (Old Flipped)
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ cat }}_{{ user }}/config"
payload: ""
# PATTERN 4: switch.notify_Evis_garden (Legacy Loop)
- service: mqtt.publish
data:
retain: true
topic: "homeassistant/switch/notify_{{ user }}_{{ cat }}/config"
# Wait! Pattern 4 IS THE CURRENT CORRECT TOPIC.
# DO NOT DELETE THIS unless object_id matches bad pattern?
# We cannot target object_id here, only topic.
# IF the ghost shares the topic with the new entity, deleting it kills the new entity too.
# BUT create_notify_user will restore it.
# Safer to NOT touch Pattern 4.
payload: "" # SKIP THIS ONE
automation:
- alias: "System: Populate Notify Services"
id: populate_notify_services_auto
trigger:
- platform: homeassistant
event: start
- platform: time_pattern
hours: "/1"
action:
# Use this to populate the new person selector list
- service: input_select.set_options
target:
entity_id: input_select.notify_mgmt_person_select
data:
options: >
{% set person_names = states.person | map(attribute='attributes.friendly_name') | list | sort %}
{{ ['-select-'] + person_names + ['Guest'] }} # Always include 'Guest' option
- service: input_select.set_options
target:
entity_id: input_select.notify_mgmt_service
data:
options: >
{% set ns = namespace(services=[]) %}
{% set trackers = integration_entities('mobile_app') | select('search', '^device_tracker\.') | list %}
{% for tracker in trackers %}
{% set dev_id = device_id(tracker) %}
{% set dev_name = device_attr(dev_id, 'name') %}
{% if dev_name %}
{% set service_name = 'notify.mobile_app_' ~ dev_name | slugify %}
{% set ns.services = ns.services + [service_name] %}
{% endif %}
{% endfor %}
{{ (['-select-'] + ns.services + ['notify.mobile_app_unknown']) | unique | sort }}
# 3. Populate Delete User list
# Scans for existing 'text.notify_service_*' entities to find valid user slugs
- service: input_select.set_options
target:
entity_id: input_select.notify_mgmt_delete_user_select
data:
options: >
{% set ns = namespace(users=[]) %}
{% set user_helpers = states.text | selectattr('object_id', 'search', '^notify_service_') | list %}
{% for helper in user_helpers %}
{% set slug = helper.object_id.replace('notify_service_', '') %}
{% set ns.users = ns.users + [slug] %}
{% endfor %}
{{ (['-select-'] + ns.users + ['unknown']) | unique | sort }}
# 4. Populate Delete Category List (NEW)
- service: input_select.set_options
target:
entity_id: input_select.notify_delete_category
data:
options: >
{% set cats = states('input_text.notify_mgmt_categories').split(',')
| map('trim')
| select('ne', '')
| list
| sort %}
{{ ['-select-'] + cats }}
- alias: "System: MQTT State Persistence"
id: system_mqtt_state_persistence
mode: parallel
trigger:
- platform: mqtt
topic: "notify/#"
condition:
- condition: template
value_template: "{{ trigger.topic.endswith('/set') }}"
action:
- service: mqtt.publish
data:
topic: "{{ trigger.topic[:-4] }}/state"
payload: "{{ trigger.payload }}"
retain: true
