636 lines
25 KiB
YAML
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 %} |