Files
zorruno-homeassistant/packages/battery_notes_alerts.yaml
T
2026-08-30 00:25:42 +12:00

636 lines
25 KiB
YAML

#:########################################################################################:#
# Battery Notes Alerts Package #
#:########################################################################################:#
#
# TITLE:
# Battery Notes Alerts
#
# FILE:
# packages/battery_notes_alerts.yaml
#
# VERSION:
# V1.1.0 2026-08-26
#
# PURPOSE:
# Sends immediate Pushover alerts when a non-temporary Battery Notes device newly
# reports a low battery or becomes unavailable, plus a Thursday weekly digest.
#
# Immediate alerts are limited to one alert of each type per device per calendar day.
#
# SAFETY / PERFORMANCE DESIGN:
# - No automation-level templated variables.
# - Low battery detection uses the native Battery Notes threshold event.
# - Weekly Battery Notes checks use raise_events: false.
# - Immediate automations never scan all Home Assistant states.
# - Immediate automations never call device_entities().
# - The offline watcher does only cheap filtering on state_changed events.
# - Offline alerts are inhibited for 10 minutes after Home Assistant starts.
# - Weekly list processing is bounded to 100 returned items per category.
# - Devices with "Temporary" anywhere in the Battery Notes note are always ignored.
#
# CONTROLS:
# - input_boolean.battery_notes_alerts_master_enable
# Master enable/disable for all notifications from this package.
#
# - Battery Notes - Immediate Low Alert
# May also be independently enabled/disabled in Automations.
#
# - Battery Notes - Immediate Offline Alert
# May also be independently enabled/disabled in Automations.
#
# - Battery Notes - Weekly Digest
# May also be independently enabled/disabled in Automations.
#
# OPERATION:
# - LOW:
# Native Battery Notes threshold event only.
# Reminder-generated events are ignored.
# Maximum one low alert per device per calendar day.
#
# - OFFLINE:
# Battery Notes Battery+ entities changing from a valid state to
# unknown/unavailable.
# Suppressed for 10 minutes after Home Assistant starts.
# Maximum one offline alert per device per calendar day.
#
# - WEEKLY:
# Thursday at 19:00.
# Reports current low batteries and batteries not reporting for 2+ days.
# Battery Notes devices marked Temporary are excluded.
#
# DEPENDENCIES:
# - Battery Notes integration
# - packages/view_road_announcement_router.yaml
#
# VERSION HISTORY:
# - V1.1.0 2026-08-26: Complete redesign using native Battery Notes events/actions.
# Removes high-frequency state scanning and top-level templates.
# Adds Temporary filtering, daily per-device alert suppression,
# startup offline suppression and master enable control.
# - V1.0.2 2026-08-25: RETIRED - unsafe high-frequency architecture.
# - V1.0.1 2026-08-25: RETIRED - unsafe high-frequency architecture.
# - V1.0.0 2026-08-25: RETIRED - unsafe high-frequency architecture.
#
#:########################################################################################:#
#:########################################################################################:#
# HELPERS #
#:########################################################################################:#
input_boolean:
# Master safety switch.
#
# No "initial" value is specified intentionally:
# - First creation defaults to OFF.
# - Thereafter Home Assistant restores the previous state.
battery_notes_alerts_master_enable:
name: "Battery Notes Alerts - Master Enable"
icon: mdi:battery-alert
# Internal startup guard.
#
# This MUST start false on every Home Assistant restart.
battery_notes_offline_alerts_armed:
name: "Battery Notes Alerts - Offline Armed"
icon: mdi:shield-clock
initial: false
input_text:
# Daily de-duplication caches.
#
# Format:
# YYYY-MM-DD|xxxxxxxxxx|xxxxxxxxxx|...
#
# Only the final 10 characters of the device identifier are stored.
# A maximum of the 20 most recent device tokens is retained.
#
# No initial value is set so the cache survives Home Assistant restarts.
battery_notes_low_alert_cache:
name: "Battery Notes - Low Alert Cache"
max: 255
icon: mdi:battery-alert-variant
battery_notes_offline_alert_cache:
name: "Battery Notes - Offline Alert Cache"
max: 255
icon: mdi:battery-off-outline
#:########################################################################################:#
# AUTOMATIONS #
#:########################################################################################:#
automation:
#:######################################################################################:#
# Offline Alert Startup Guard #
#:######################################################################################:#
- id: battery_notes_arm_offline_alerts
alias: "Battery Notes - Arm Offline Alerts"
description: >
Arms Battery Notes offline notifications 10 minutes after Home Assistant starts.
This prevents normal startup/integration initialisation from being interpreted as
devices suddenly going offline.
mode: restart
triggers:
- trigger: homeassistant
event: start
actions:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.battery_notes_offline_alerts_armed
- delay: "00:10:00"
- action: input_boolean.turn_on
target:
entity_id: input_boolean.battery_notes_offline_alerts_armed
#:######################################################################################:#
# Immediate Low Battery Alert #
#:######################################################################################:#
- id: battery_notes_immediate_low_alert
alias: "Battery Notes - Immediate Low Alert"
description: >
Sends one immediate Pushover alert when a non-temporary Battery Notes device
genuinely crosses into its low-battery state. Reminder-generated Battery Notes
events are ignored. A device can generate at most one low alert per calendar day.
mode: queued
max: 10
triggers:
- trigger: event
event_type: battery_notes_battery_threshold
event_data:
battery_low: true
reminder: false
conditions:
# Master control.
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
# Temporary Battery Notes devices must never generate notifications.
- condition: template
value_template: >-
{% set note = trigger.event.data.note | default('', true) | lower %}
{{ 'temporary' not in note }}
# Require some stable identifier for daily de-duplication.
- condition: template
value_template: >-
{{ (trigger.event.data.device_id | default('', true)) != ''
or (trigger.event.data.source_entity_id | default('', true)) != '' }}
actions:
# IMPORTANT:
# This de-duplication check is deliberately an ACTION condition rather than an
# automation-level condition. Because this automation is mode: queued, two events
# arriving together are serialised before checking/updating the cache.
- condition: template
value_template: >-
{% set identifier =
trigger.event.data.device_id | default('', true)
or trigger.event.data.source_entity_id | default('', true) %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_low_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{{ token not in previous }}
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: >-
{% set name =
trigger.event.data.device_name
| default('Unknown Battery Device', true) %}
{% set level =
trigger.event.data.battery_level
| default(-1, true)
| float(-1) %}
{% set battery_type =
trigger.event.data.battery_type_and_quantity
| default('Not recorded', true) %}
New Alert: {{ name }}, Battery Low{% if level >= 0 %}
{{ level | round(0) | int }}%{% endif %} {{ battery_type }}
long_announcement: >-
{% set name =
trigger.event.data.device_name
| default('Unknown Battery Device', true) %}
{% set level =
trigger.event.data.battery_level
| default(-1, true)
| float(-1) %}
{% set battery_type =
trigger.event.data.battery_type_and_quantity
| default('Not recorded', true) %}
New Alert: {{ name }}, Battery Low{% if level >= 0 %}
{{ level | round(0) | int }}%{% endif %} {{ battery_type }}
# Record this device only after the notification action has completed.
- action: input_text.set_value
target:
entity_id: input_text.battery_notes_low_alert_cache
data:
value: >-
{% set identifier =
trigger.event.data.device_id | default('', true)
or trigger.event.data.source_entity_id | default('', true) %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_low_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{% set updated = (previous + [token])[-20:] %}
{{ today ~ '|' ~ updated | join('|') }}
#:######################################################################################:#
# Immediate Offline Alert #
#:######################################################################################:#
- id: battery_notes_immediate_offline_alert
alias: "Battery Notes - Immediate Offline Alert"
description: >
Sends one immediate Pushover alert when a non-temporary Battery Notes Battery+
entity genuinely changes from an available state to unknown/unavailable.
Alerts are inhibited for the first 10 minutes after Home Assistant starts.
A device can generate at most one offline alert per calendar day.
mode: queued
max: 20
triggers:
- trigger: event
event_type: state_changed
conditions:
# Keep the first checks extremely cheap because state_changed is a global event.
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
- condition: state
entity_id: input_boolean.battery_notes_offline_alerts_armed
state: "on"
# Only Battery Notes generated Battery+ entities are interesting.
# No regex and no global state lookup is required.
- condition: template
value_template: >-
{% set entity_id = trigger.event.data.entity_id | default('', true) %}
{{ (entity_id.startswith('sensor.')
and entity_id.endswith('_battery_plus'))
or
(entity_id.startswith('binary_sensor.')
and entity_id.endswith('_battery_plus_low')) }}
# Require a genuine transition from available -> unavailable.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{{ old is not none
and new is not none
and old.state not in ['unknown', 'unavailable']
and new.state in ['unknown', 'unavailable'] }}
# Temporary devices are ignored.
#
# Prefer the new state's note but fall back to the old state's attributes because
# integrations do not always retain every attribute on an unavailable state.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set note =
new.attributes.get(
'note',
old.attributes.get('note', '')
)
| default('', true)
| lower %}
{{ 'temporary' not in note }}
actions:
# As with low alerts, do this after entering the queued action sequence so two
# related Battery+ entities becoming unavailable together cannot both notify.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set identifier =
new.attributes.get('device_id', '')
or old.attributes.get('device_id', '')
or new.entity_id %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_offline_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{{ token not in previous }}
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set name =
new.attributes.get('device_name')
or old.attributes.get('device_name')
or new.attributes.get('friendly_name')
or old.attributes.get('friendly_name')
or new.name %}
{% set battery_type =
new.attributes.get('battery_type_and_quantity')
or old.attributes.get('battery_type_and_quantity')
or 'Not recorded' %}
New Alert: {{ name }}, Battery Unavailable {{ battery_type }}
long_announcement: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set name =
new.attributes.get('device_name')
or old.attributes.get('device_name')
or new.attributes.get('friendly_name')
or old.attributes.get('friendly_name')
or new.name %}
{% set battery_type =
new.attributes.get('battery_type_and_quantity')
or old.attributes.get('battery_type_and_quantity')
or 'Not recorded' %}
New Alert: {{ name }}, Battery Unavailable {{ battery_type }}
- action: input_text.set_value
target:
entity_id: input_text.battery_notes_offline_alert_cache
data:
value: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set identifier =
new.attributes.get('device_id', '')
or old.attributes.get('device_id', '')
or new.entity_id %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_offline_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{% set updated = (previous + [token])[-20:] %}
{{ today ~ '|' ~ updated | join('|') }}
#:######################################################################################:#
# Weekly Digest #
#:######################################################################################:#
- id: battery_notes_weekly_digest
alias: "Battery Notes - Weekly Digest"
description: >
Sends one Battery Notes summary every Thursday at 19:00. Reports current low
batteries and batteries which have not reported for at least two days.
Temporary Battery Notes devices are excluded.
mode: single
triggers:
- trigger: time
at: "19:00:00"
conditions:
- condition: time
weekday:
- thu
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
actions:
# IMPORTANT:
# raise_events MUST remain false.
#
# This retrieves the current low list without creating Battery Notes threshold
# reminder events.
- action: battery_notes.check_battery_low
data:
raise_events: false
response_variable: battery_low_response
# Ask Battery Notes for devices which have not reported for at least two days.
# Again, retrieve a response only; do not raise one event per device.
- action: battery_notes.check_battery_last_reported
data:
days_last_reported: 2
raise_events: false
response_variable: battery_report_response
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Weekly Battery Digest"
short_announcement: "Weekly Battery Digest"
long_announcement: >-
{# ------------------------------------------------------------------------- #}
{# Obtain response arrays. #}
{# #}
{# check_battery_battery_low is confirmed from this HA installation. #}
{# Two possible last-reported response keys are accepted defensively. #}
{# ------------------------------------------------------------------------- #}
{% set low_items =
battery_low_response.get(
'check_battery_battery_low',
battery_low_response.get('check_battery_low', [])
) %}
{% set report_items =
battery_report_response.get(
'check_battery_last_reported',
battery_report_response.get(
'check_battery_battery_last_reported',
[]
)
) %}
{# ------------------------------------------------------------------------- #}
{# Resolve the Battery Notes note. #}
{# #}
{# The low action response on this installation does not include "note", #}
{# so fall back to the attributes of Battery Notes entities. #}
{# This lookup occurs ONCE PER WEEK ONLY and is bounded below. #}
{# ------------------------------------------------------------------------- #}
{% macro battery_note(item) -%}
{% set ns =
namespace(
note=item.get('note', '') | default('', true)
) %}
{% if not ns.note %}
{% set item_device_id =
item.get('device_id', '') | default('', true) %}
{% set item_source_id =
item.get('source_entity_id', '') | default('', true) %}
{% for entity_id in integration_entities('battery_notes') %}
{% if not ns.note %}
{% set entity_device_id =
state_attr(entity_id, 'device_id')
| default('', true) %}
{% set entity_source_id =
state_attr(entity_id, 'source_entity_id')
| default('', true) %}
{% if
(item_device_id
and entity_device_id == item_device_id)
or
(item_source_id
and entity_source_id == item_source_id) %}
{% set found_note =
state_attr(entity_id, 'note')
| default('', true) %}
{% if found_note %}
{% set ns.note = found_note %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{{ ns.note }}
{%- endmacro %}
{# ------------------------------------------------------------------------- #}
{# Build bounded problem lists, excluding Temporary devices. #}
{# ------------------------------------------------------------------------- #}
{% set summary = namespace(low=[], not_reporting=[]) %}
{% for item in low_items[:100] %}
{% set note = battery_note(item) | trim | lower %}
{% if 'temporary' not in note %}
{% set name =
item.get('device_name', 'Unknown Battery Device')
| default('Unknown Battery Device', true) %}
{% set level =
item.get('battery_level', -1)
| default(-1, true)
| float(-1) %}
{% set battery_type =
item.get('battery_type_and_quantity', 'Not recorded')
| default('Not recorded', true) %}
{% if level >= 0 %}
{% set line =
name
~ ' - '
~ (level | round(0) | int)
~ '% - '
~ battery_type %}
{% else %}
{% set line =
name
~ ' - '
~ battery_type %}
{% endif %}
{% set summary.low = summary.low + [line] %}
{% endif %}
{% endfor %}
{% for item in report_items[:100] %}
{% set note = battery_note(item) | trim | lower %}
{% if 'temporary' not in note %}
{% set name =
item.get('device_name', 'Unknown Battery Device')
| default('Unknown Battery Device', true) %}
{% set days =
item.get('battery_last_reported_days', 0)
| default(0, true)
| int(0) %}
{% set last_level =
item.get('battery_last_reported_level', -1)
| default(-1, true)
| float(-1) %}
{% set battery_type =
item.get('battery_type_and_quantity', 'Not recorded')
| default('Not recorded', true) %}
{% if last_level >= 0 %}
{% set line =
name
~ ' - '
~ days
~ ' days - last '
~ (last_level | round(0) | int)
~ '% - '
~ battery_type %}
{% else %}
{% set line =
name
~ ' - '
~ days
~ ' days - '
~ battery_type %}
{% endif %}
{% set summary.not_reporting =
summary.not_reporting + [line] %}
{% endif %}
{% endfor %}
Weekly Battery Digest
{% if summary.low %}
Low Battery: {{ summary.low | count }}
{% for line in summary.low %}
• {{ line }}
{% endfor %}
{% else %}
Low Battery: 0
{% endif %}
{% if summary.not_reporting %}
Not Reporting (2+ days): {{ summary.not_reporting | count }}
{% for line in summary.not_reporting %}
• {{ line }}
{% endfor %}
{% else %}
Not Reporting (2+ days): 0
{% endif %}
{% if not summary.low and not summary.not_reporting %}
All monitored Battery Notes devices are OK.
{% endif %}