Update appliance monitoring configuration

This commit is contained in:
root
2026-06-09 22:20:06 +12:00
parent 0c38ec96f0
commit a8d2a15942
7 changed files with 3775 additions and 0 deletions
+763
View File
@@ -0,0 +1,763 @@
################################################################################
# PACKAGE: Appliance Monitor Processor
################################################################################
#
# Version: 1.2
# Date: 2026-06-09
#
# Purpose:
# Shared appliance power monitoring processor.
#
# This script is called by appliances_status_monitoring.yaml. It reads the
# previous appliance monitor state from the appliance monitor MQTT sensor,
# calculates the next state and cycle totals, publishes retained JSON state
# back to MQTT, and calls the View Road announcement router when a cycle
# finishes.
#
# Design:
# - One shared processing script.
# - MQTT retained JSON state per appliance.
# - One MQTT sensor per appliance.
# - Timestamp-based energy and cost calculation.
# - Rolling average state detection.
#
# Default values:
# debug_flow: false
# resolution: 7
# standby_power: 2
# operating_power_min: 12
# paid_tariff_start: "00:00"
# paid_tariff_end: "21:00"
# paid_tariff_cost: 0.2023
# offpeak_tariff_cost: 0.0
# max_sample_gap_seconds: 900
# min_cycle_notify_seconds: 300
#
# States:
# Off
# Standby
# Operating
# Finished
#
# Notes:
# Version 1.2:
# - Added min_cycle_notify_seconds.
# - Finished cycles shorter than this are still recorded, but do not send
# notifications.
# - Added MQTT attributes showing whether notification was allowed or
# suppressed.
#
# Version 1.1:
# - Accepts lower-case retained states from previous package versions, such
# as standby, operating, and finished.
#
################################################################################
# FINISHED ANNOUNCEMENT
# The finishing script calls the package view_road_announcement_router.yaml.
# If you want to use your own announcement system, it will occur at the end
# at am_cycle_notify_allowed being true, so change to your own announcement
# method there.
#
# Announcements can use:
# am_announcement_channels
# am_announcement_title
# am_short_announcement
# am_long_announcement
# am_matrix_indicator
#
################################################################################
script:
appliance_monitor_process_sample:
alias: Appliance Monitor - Process Sample
icon: mdi:meter-electric
mode: queued
max: 50
fields:
appliance_id:
name: Appliance ID
description: Short unique ID, for example washing_machine.
selector:
text:
appliance_name:
name: Appliance Name
description: Human readable appliance name.
selector:
text:
appliance_action:
name: Appliance Action
description: Human readable action, for example washing.
selector:
text:
power_w:
name: Power W
description: Current instantaneous appliance power in watts.
selector:
number:
min: 0
max: 10000
step: 0.1
unit_of_measurement: W
monitor_sensor:
name: Monitor Sensor
description: MQTT monitor sensor used to read previous state.
selector:
entity:
domain: sensor
state_topic:
name: MQTT State Topic
description: Retained MQTT topic used to publish monitor state JSON.
selector:
text:
min_cycle_notify_seconds:
name: Minimum Cycle Notify Seconds
description: Finished cycles shorter than this are recorded but do not send notifications.
default: 300
selector:
number:
min: 0
max: 86400
step: 1
unit_of_measurement: s
sequence:
##########################################################################
# CORE DEFAULTS AND PASSED VALUES
##########################################################################
- variables:
am_appliance_id: "{{ appliance_id | default('appliance', true) | string }}"
am_appliance_name: "{{ appliance_name | default('Appliance', true) | string }}"
am_appliance_action: "{{ appliance_action | default('running', true) | string }}"
am_power_w: "{{ power_w | default(0, true) | float(0) }}"
am_monitor_sensor: "{{ monitor_sensor | default('', true) | string }}"
am_state_topic: "{{ state_topic | default('', true) | string }}"
am_debug_flow: "{{ debug_flow | default(false, true) | bool(false) }}"
am_resolution: "{{ resolution | default(7, true) | int(7) }}"
am_standby_power: "{{ standby_power | default(2, true) | float(2) }}"
am_operating_power_min: "{{ operating_power_min | default(12, true) | float(12) }}"
am_paid_tariff_start: "{{ paid_tariff_start | default('00:00', true) | string }}"
am_paid_tariff_end: "{{ paid_tariff_end | default('21:00', true) | string }}"
am_paid_tariff_cost: "{{ paid_tariff_cost | default(0.2023, true) | float(0.2023) }}"
am_offpeak_tariff_cost: "{{ offpeak_tariff_cost | default(0.0, true) | float(0.0) }}"
am_max_sample_gap_seconds: "{{ max_sample_gap_seconds | default(900, true) | int(900) }}"
am_min_cycle_notify_seconds: "{{ min_cycle_notify_seconds | default(300, true) | int(300) }}"
##########################################################################
# ANNOUNCEMENT DEFAULTS AND PASSED VALUES
##########################################################################
- variables:
am_announcement_channels: >-
{{ announcement_channels | default('pushover_zorruno, sony_tv_lounge, lounge_google_home_voice, led_matrix_1', true) | string }}
am_short_announcement: >-
{{ short_announcement | default((am_appliance_action | title) ~ ' complete', true) | string }}
am_long_announcement_override: >-
{{ long_announcement | default('', true) | string }}
am_announcement_title: >-
{{ title | default(am_appliance_name ~ ' Notification', true) | string }}
am_matrix_indicator: "{{ matrix_indicator | default('', true) | string }}"
##########################################################################
# PREVIOUS STATE FROM MQTT MONITOR SENSOR ATTRIBUTES
##########################################################################
- variables:
am_now_ts: "{{ as_timestamp(now()) }}"
am_previous_state: >-
{%- set s = states(am_monitor_sensor) | string | lower | trim -%}
{{ {'operating': 'Operating', 'finished': 'Finished', 'standby': 'Standby', 'off': 'Off'}.get(s, 'Off') }}
am_last_power_w: "{{ state_attr(am_monitor_sensor, 'last_power_w') | float(am_power_w) }}"
am_last_sample_ts: "{{ state_attr(am_monitor_sensor, 'last_sample_ts') | float(0) }}"
am_existing_cycle_start_ts: "{{ state_attr(am_monitor_sensor, 'cycle_start_ts') | float(0) }}"
am_existing_cycle_stop_ts: "{{ state_attr(am_monitor_sensor, 'cycle_stop_ts') | float(0) }}"
am_existing_cycle_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_energy_kwh') | float(0) }}"
am_existing_cycle_paid_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_paid_energy_kwh') | float(0) }}"
am_existing_cycle_free_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_free_energy_kwh') | float(0) }}"
am_existing_cycle_cost: "{{ state_attr(am_monitor_sensor, 'cycle_cost') | float(0) }}"
am_previous_recent_samples_json: >-
{% set raw = state_attr(am_monitor_sensor, 'recent_samples_json') | string | trim %}
{{ raw if raw.startswith('[') else '[]' }}
##########################################################################
# ELAPSED TIME
##########################################################################
- variables:
am_elapsed_sec_raw: >-
{% if am_last_sample_ts | float(0) > 0 %}
{{ (am_now_ts | float(0)) - (am_last_sample_ts | float(0)) }}
{% else %}
0
{% endif %}
am_effective_elapsed_sec: >-
{% set elapsed = am_elapsed_sec_raw | float(0) %}
{% if elapsed <= 0 %}
0
{% elif elapsed > (am_max_sample_gap_seconds | int(900)) %}
{{ am_max_sample_gap_seconds | int(900) }}
{% else %}
{{ elapsed }}
{% endif %}
##########################################################################
# ROLLING AVERAGE
##########################################################################
- variables:
am_previous_recent_samples_list: >-
{% set raw_json = state_attr(am_monitor_sensor, 'recent_samples_json') %}
{% set raw_list = state_attr(am_monitor_sensor, 'recent_samples') %}
{% if raw_json is string %}
{{ raw_json | from_json(default=[]) }}
{% elif raw_json is iterable and raw_json is not string %}
{{ raw_json | list }}
{% elif raw_list is iterable and raw_list is not string %}
{{ raw_list | list }}
{% else %}
[]
{% endif %}
am_recent_samples_list: >-
{% set old = am_previous_recent_samples_list %}
{% set ns = namespace(items=[am_power_w | float(0)]) %}
{% for item in old %}
{% if loop.index < (am_resolution | int(7)) %}
{% set ns.items = ns.items + [item | float(0)] %}
{% endif %}
{% endfor %}
{{ ns.items }}
am_recent_samples_json: >-
{{ am_recent_samples_list | to_json }}
am_average_power_w: >-
{% set samples = am_recent_samples_list %}
{% if samples | count > 0 %}
{{ ((samples | sum) / (samples | count)) | round(3) }}
{% else %}
0
{% endif %}
##########################################################################
# STATE MACHINE
##########################################################################
- variables:
am_new_state: >-
{% set avg = am_average_power_w | float(0) %}
{% set standby = am_standby_power | float(2) %}
{% set operate = am_operating_power_min | float(12) %}
{% set prev = am_previous_state | string %}
{% if prev == 'Operating' and avg < operate %}
Finished
{% elif avg < standby %}
Off
{% elif avg >= operate %}
Operating
{% elif avg >= standby and prev in ['Off', 'Finished'] %}
Standby
{% else %}
{{ prev }}
{% endif %}
am_state_changed: "{{ (am_new_state | string) != (am_previous_state | string) }}"
am_cycle_started: "{{ am_new_state == 'Operating' and am_previous_state != 'Operating' }}"
am_cycle_finished: "{{ am_new_state == 'Finished' and am_previous_state == 'Operating' }}"
##########################################################################
# ACCUMULATION WINDOW
##########################################################################
- variables:
am_accumulation_seconds: >-
{% set elapsed = am_effective_elapsed_sec | float(0) %}
{% if elapsed <= 0 %}
0
{% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
{{ elapsed }}
{% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
{{ elapsed / 2 }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
{{ elapsed / 2 }}
{% else %}
0
{% endif %}
am_accumulation_average_power_w: >-
{% set last = am_last_power_w | float(0) %}
{% set current = am_power_w | float(0) %}
{% set operate = am_operating_power_min | float(12) %}
{% if am_previous_state == 'Operating' and am_new_state == 'Operating' %}
{{ ((last + current) / 2) | round(3) }}
{% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
{{ ((operate + current) / 2) | round(3) }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
{{ ((last + operate) / 2) | round(3) }}
{% else %}
0
{% endif %}
am_accumulation_start_ts: >-
{% set elapsed = am_effective_elapsed_sec | float(0) %}
{% set accum = am_accumulation_seconds | float(0) %}
{% if accum <= 0 %}
{{ am_now_ts | float(0) }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
{{ (am_now_ts | float(0)) - elapsed }}
{% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
{{ (am_now_ts | float(0)) - accum }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
{{ (am_now_ts | float(0)) - elapsed }}
{% else %}
{{ am_now_ts | float(0) }}
{% endif %}
am_accumulation_stop_ts: >-
{% set elapsed = am_effective_elapsed_sec | float(0) %}
{% set accum = am_accumulation_seconds | float(0) %}
{% if accum <= 0 %}
{{ am_now_ts | float(0) }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
{{ am_now_ts | float(0) }}
{% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
{{ am_now_ts | float(0) }}
{% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
{{ (am_now_ts | float(0)) - elapsed + accum }}
{% else %}
{{ am_now_ts | float(0) }}
{% endif %}
am_interval_mid_ts: >-
{% set start = am_accumulation_start_ts | float(0) %}
{% set accum = am_accumulation_seconds | float(0) %}
{% if accum > 0 %}
{{ start + (accum / 2) }}
{% else %}
{{ am_now_ts | float(0) }}
{% endif %}
##########################################################################
# TARIFF CALCULATION
##########################################################################
- variables:
am_paid_start_minutes: >-
{% set p = (am_paid_tariff_start | string).split(':') %}
{% set hh = p[0] if p | count > 0 else 0 %}
{% set mm = p[1] if p | count > 1 else 0 %}
{{ (hh | int(0)) * 60 + (mm | int(0)) }}
am_paid_end_minutes: >-
{% set p = (am_paid_tariff_end | string).split(':') %}
{% set hh = p[0] if p | count > 0 else 0 %}
{% set mm = p[1] if p | count > 1 else 0 %}
{{ (hh | int(0)) * 60 + (mm | int(0)) }}
am_interval_mid_minutes: >-
{% set h = am_interval_mid_ts | float(0) | timestamp_custom('%H', true) | int(0) %}
{% set m = am_interval_mid_ts | float(0) | timestamp_custom('%M', true) | int(0) %}
{{ h * 60 + m }}
am_is_paid_interval: >-
{% set start = am_paid_start_minutes | int(0) %}
{% set end = am_paid_end_minutes | int(0) %}
{% set mid = am_interval_mid_minutes | int(0) %}
{% if start == end %}
true
{% elif start < end %}
{{ mid >= start and mid < end }}
{% else %}
{{ mid >= start or mid < end }}
{% endif %}
am_tariff_rate: >-
{% if am_is_paid_interval | bool(false) %}
{{ am_paid_tariff_cost | float(0.2023) }}
{% else %}
{{ am_offpeak_tariff_cost | float(0.0) }}
{% endif %}
##########################################################################
# INTERVAL ENERGY AND COST
##########################################################################
- variables:
am_interval_energy_kwh: >-
{{
(
(am_accumulation_average_power_w | float(0))
* (am_accumulation_seconds | float(0))
/ 3600000
) | round(6)
}}
am_interval_cost: >-
{{ ((am_interval_energy_kwh | float(0)) * (am_tariff_rate | float(0))) | round(6) }}
am_should_accumulate: "{{ (am_accumulation_seconds | float(0)) > 0 }}"
##########################################################################
# CYCLE TOTAL BASES
##########################################################################
- variables:
am_cycle_energy_base_kwh: >-
{% if am_cycle_started | bool(false) %}
0
{% else %}
{{ am_existing_cycle_energy_kwh | float(0) }}
{% endif %}
am_cycle_paid_energy_base_kwh: >-
{% if am_cycle_started | bool(false) %}
0
{% else %}
{{ am_existing_cycle_paid_energy_kwh | float(0) }}
{% endif %}
am_cycle_free_energy_base_kwh: >-
{% if am_cycle_started | bool(false) %}
0
{% else %}
{{ am_existing_cycle_free_energy_kwh | float(0) }}
{% endif %}
am_cycle_cost_base: >-
{% if am_cycle_started | bool(false) %}
0
{% else %}
{{ am_existing_cycle_cost | float(0) }}
{% endif %}
##########################################################################
# CYCLE TOTALS
##########################################################################
- variables:
am_cycle_energy_new_kwh: >-
{% set add_kwh = am_interval_energy_kwh | float(0) if am_should_accumulate | bool(false) else 0 %}
{{ ((am_cycle_energy_base_kwh | float(0)) + add_kwh) | round(6) }}
am_cycle_cost_new: >-
{% set add_cost = am_interval_cost | float(0) if am_should_accumulate | bool(false) else 0 %}
{{ ((am_cycle_cost_base | float(0)) + add_cost) | round(6) }}
am_cycle_paid_energy_new_kwh: >-
{% set add_kwh = am_interval_energy_kwh | float(0) if (am_should_accumulate | bool(false) and am_is_paid_interval | bool(false)) else 0 %}
{{ ((am_cycle_paid_energy_base_kwh | float(0)) + add_kwh) | round(6) }}
am_cycle_free_energy_new_kwh: >-
{% set add_kwh = am_interval_energy_kwh | float(0) if (am_should_accumulate | bool(false) and not (am_is_paid_interval | bool(false))) else 0 %}
{{ ((am_cycle_free_energy_base_kwh | float(0)) + add_kwh) | round(6) }}
##########################################################################
# CYCLE TIMES
##########################################################################
- variables:
am_cycle_start_ts_new: >-
{% if am_cycle_started | bool(false) %}
{{ am_accumulation_start_ts | float(0) }}
{% elif am_existing_cycle_start_ts | float(0) > 0 %}
{{ am_existing_cycle_start_ts | float(0) }}
{% else %}
{{ am_now_ts | float(0) }}
{% endif %}
am_cycle_stop_ts_new: >-
{% if am_cycle_finished | bool(false) %}
{{ am_accumulation_stop_ts | float(0) }}
{% elif am_cycle_started | bool(false) %}
{{ am_accumulation_start_ts | float(0) }}
{% elif am_existing_cycle_stop_ts | float(0) > 0 %}
{{ am_existing_cycle_stop_ts | float(0) }}
{% else %}
{{ am_now_ts | float(0) }}
{% endif %}
am_cycle_duration_seconds_new: >-
{% set start = am_cycle_start_ts_new | float(0) %}
{% if am_new_state == 'Operating' %}
{% set stop = am_now_ts | float(0) %}
{% else %}
{% set stop = am_cycle_stop_ts_new | float(0) %}
{% endif %}
{% if start > 0 and stop > start %}
{{ (stop - start) | int(0) }}
{% else %}
0
{% endif %}
##########################################################################
# FORMATTED VALUES
##########################################################################
- variables:
am_cycle_energy_formatted: >-
{% set kwh = am_cycle_energy_new_kwh | float(0) %}
{% if kwh >= 1 %}
{{ '%.1f' | format(kwh) }}kWh
{% elif kwh < 0.001 %}
<1Wh
{% else %}
{{ '%.1f' | format(kwh * 1000) }}Wh
{% endif %}
am_cycle_cost_formatted: >-
{% set val = am_cycle_cost_new | float(0) %}
{% if val < 0.01 %}
<1c
{% elif val < 1 %}
{{ (val * 100) | round(0) | int(0) }}c
{% else %}
${{ '%.2f' | format(val) }}
{% endif %}
am_cycle_offpeak_savings: >-
{% set val = ((am_cycle_energy_new_kwh | float(0)) * (am_paid_tariff_cost | float(0.2023))) - (am_cycle_cost_new | float(0)) %}
{{ ([0, val] | max) | round(4) }}
am_cycle_offpeak_savings_formatted: >-
{% set val = am_cycle_offpeak_savings | float(0) %}
{% if val < 0.01 %}
<1c
{% elif val < 1 %}
{{ (val * 100) | round(0) | int(0) }}c
{% else %}
${{ '%.2f' | format(val) }}
{% endif %}
am_cycle_duration_formatted: >-
{% set d = am_cycle_duration_seconds_new | int(0) %}
{% set h = (d // 3600) | int(0) %}
{% set m = ((d % 3600) // 60) | int(0) %}
{% if h < 1 %}
{{ '%02dmin' | format(m) }}
{% else %}
{{ '%02dh %02dmin' | format(h, m) }}
{% endif %}
am_cycle_start_time_formatted: >-
{{ am_cycle_start_ts_new | float(0) | timestamp_custom('%H:%M', true) }}
am_cycle_stop_time_formatted: >-
{{ am_cycle_stop_ts_new | float(0) | timestamp_custom('%H:%M', true) }}
am_cycle_start_local: >-
{{ am_cycle_start_ts_new | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}
am_cycle_stop_local: >-
{{ am_cycle_stop_ts_new | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}
am_last_sample_local: >-
{{ am_now_ts | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}
##########################################################################
# SUMMARY STRINGS
##########################################################################
- variables:
am_summary_short: >-
Your {{ am_appliance_action }} is complete.
am_summary: >-
Your {{ am_appliance_action }} is complete.
It started at {{ am_cycle_start_time_formatted | trim }},
finished at {{ am_cycle_stop_time_formatted | trim }},
and used {{ am_cycle_energy_formatted | trim }},
taking {{ am_cycle_duration_formatted | trim }}
at a cost of {{ am_cycle_cost_formatted | trim }}
(Off-peak savings of {{ am_cycle_offpeak_savings_formatted | trim }}).
am_last_summary_new: >-
{% if am_cycle_started | bool(false) %}
Cycle running
{% elif am_cycle_finished | bool(false) %}
{{ am_summary }}
{% else %}
{{ state_attr(am_monitor_sensor, 'last_summary') | default('No cycle recorded', true) }}
{% endif %}
am_long_announcement: >-
{% if am_long_announcement_override | trim | length > 0 %}
{{ am_long_announcement_override }}
{% else %}
{{ am_summary }}
{% endif %}
##########################################################################
# NOTIFICATION DECISION
##########################################################################
#
# Very short cycles are still recorded, but no announcement is sent.
# Default minimum is 300 seconds / 5 minutes.
#
##########################################################################
- variables:
am_cycle_notify_allowed: >-
{{
am_cycle_finished | bool(false)
and (am_cycle_duration_seconds_new | int(0)) >= (am_min_cycle_notify_seconds | int(300))
}}
am_cycle_notification_suppressed: >-
{{
am_cycle_finished | bool(false)
and not (am_cycle_notify_allowed | bool(false))
}}
am_cycle_notification_suppressed_reason: >-
{% if am_cycle_notification_suppressed | bool(false) %}
Cycle was shorter than {{ am_min_cycle_notify_seconds | int(300) }} seconds
{% endif %}
##########################################################################
# PUBLISH RETAINED MQTT STATE
##########################################################################
- action: mqtt.publish
data:
topic: "{{ am_state_topic }}"
retain: true
qos: 0
payload: >-
{{
dict(
appliance_id=am_appliance_id,
appliance_name=am_appliance_name,
appliance_action=am_appliance_action,
state=am_new_state,
previous_state=am_previous_state,
state_changed=am_state_changed | bool(false),
power_w=am_power_w | float(0),
last_power_w=am_power_w | float(0),
previous_power_w=am_last_power_w | float(0),
average_power_w=am_average_power_w | float(0),
recent_samples=am_recent_samples_list,
recent_samples_json=am_recent_samples_json,
last_sample_ts=am_now_ts | float(0),
last_sample_local=am_last_sample_local,
cycle_start_ts=am_cycle_start_ts_new | float(0),
cycle_stop_ts=am_cycle_stop_ts_new | float(0),
cycle_start_local=am_cycle_start_local,
cycle_stop_local=am_cycle_stop_local,
cycle_duration_seconds=am_cycle_duration_seconds_new | int(0),
cycle_duration_formatted=am_cycle_duration_formatted | trim,
min_cycle_notify_seconds=am_min_cycle_notify_seconds | int(300),
cycle_notify_allowed=am_cycle_notify_allowed | bool(false),
cycle_notification_suppressed=am_cycle_notification_suppressed | bool(false),
cycle_notification_suppressed_reason=am_cycle_notification_suppressed_reason | trim,
cycle_energy_kwh=am_cycle_energy_new_kwh | float(0),
cycle_paid_energy_kwh=am_cycle_paid_energy_new_kwh | float(0),
cycle_free_energy_kwh=am_cycle_free_energy_new_kwh | float(0),
cycle_energy_formatted=am_cycle_energy_formatted | trim,
cycle_cost=am_cycle_cost_new | float(0),
cycle_cost_formatted=am_cycle_cost_formatted | trim,
cycle_offpeak_savings=am_cycle_offpeak_savings | float(0),
cycle_offpeak_savings_formatted=am_cycle_offpeak_savings_formatted | trim,
paid_tariff_start=am_paid_tariff_start,
paid_tariff_end=am_paid_tariff_end,
paid_tariff_cost=am_paid_tariff_cost | float(0),
offpeak_tariff_cost=am_offpeak_tariff_cost | float(0),
accumulation_seconds=am_accumulation_seconds | float(0),
accumulation_average_power_w=am_accumulation_average_power_w | float(0),
interval_energy_kwh=am_interval_energy_kwh | float(0),
interval_cost=am_interval_cost | float(0),
is_paid_interval=am_is_paid_interval | bool(false),
summary_short=am_summary_short,
summary=am_summary,
last_summary=am_last_summary_new
) | to_json
}}
##########################################################################
# DEBUG LOGBOOK ENTRY
##########################################################################
- if:
- condition: template
value_template: "{{ am_debug_flow | bool(false) }}"
then:
- action: logbook.log
data:
name: "Appliance Monitor"
message: >-
{{ am_appliance_name }}: {{ am_previous_state }} -> {{ am_new_state }},
power {{ am_power_w | round(1) }}W,
average {{ am_average_power_w | round(1) }}W,
cycle {{ am_cycle_energy_new_kwh | round(3) }}kWh,
cost {{ am_cycle_cost_new | round(4) }}
##########################################################################
# SHORT CYCLE SUPPRESSION LOG
##########################################################################
- if:
- condition: template
value_template: "{{ am_cycle_notification_suppressed | bool(false) }}"
then:
- action: logbook.log
data:
name: "Appliance Monitor"
message: >-
{{ am_appliance_name }} finished after
{{ am_cycle_duration_formatted | trim }}, below the
{{ am_min_cycle_notify_seconds | int(300) }} second notification
threshold. No announcement was sent.
##########################################################################
# FINISHED ANNOUNCEMENT
##########################################################################
# This script is in additional package view_road_announcement_router.yaml.
# If you want to use your own announcement system, it will occur here
# at am_cycle_notify_allowed being true.
#
# Change to your own announcement method here if needed.
##########################################################################
- if:
- condition: template
value_template: "{{ am_cycle_notify_allowed | bool(false) }}"
then:
- action: script.view_road_announcement
data:
channels: "{{ am_announcement_channels }}"
title: "{{ am_announcement_title }}"
short_announcement: "{{ am_short_announcement }}"
long_announcement: "{{ am_long_announcement }}"
payload: ""
indicator: "{{ am_matrix_indicator }}"
+255
View File
@@ -0,0 +1,255 @@
################################################################################
# PACKAGE: Appliances Status Monitoring
################################################################################
#
# Version: 1.4
# Date: 2026-06-09
#
# Purpose:
# Appliance status monitoring package for multiple appliances.
#
# This package defines a central appliance registry and one automation that:
#
# 1. Publishes MQTT discovery config for each appliance monitor sensor.
# 2. Samples appliance power sensors once per minute.
# 3. Calls script.appliance_monitor_process_sample.
#
# Processor package required:
# appliance_monitor_processor.yaml
#
# Announcement router required:
# view_road_announcement_router.yaml
#
# MQTT:
# MQTT is used only as retained state storage for appliance monitor sensors.
# Sensor entities are created using MQTT discovery.
#
# Adding another appliance:
# Add one entry to the am_appliances list.
#
# Version 1.4:
# Added min_cycle_notify_seconds support.
# Default is 300 seconds, so cycles shorter than 5 minutes are recorded but
# do not send finished notifications.
# Per-appliance overrides can be set in the am_appliances list.
#
# Version 1.3:
# Removed the is_number gate before calling the processor. The processor is
# now always called once per appliance per sample, and power is safely converted
# to 0 if the source sensor is unavailable or non-numeric.
#
################################################################################
automation:
- id: appliances_status_monitoring_process_samples
alias: Appliances Status Monitoring - Process Samples
description: Publish MQTT discovery, sample appliance power sensors, and process appliance status/cycle data.
mode: queued
max: 20
trigger:
- platform: homeassistant
event: start
id: ha_start
- platform: time_pattern
minutes: "/1"
id: periodic_sample
variables:
##########################################################################
# MQTT LOCAL CONTROLS
##########################################################################
mqtt_device_name: "appliance_monitor"
mqtt_local_status_topic: !secret mqtt_status_main_topic
mqtt_discovery_prefix: "homeassistant"
##########################################################################
# DEFAULTS
##########################################################################
am_default_debug_flow: false
am_default_resolution: 2
am_default_standby_power: 2
am_default_operating_power_min: 12
am_default_min_cycle_notify_seconds: 300
#am_default_announcement_channels: "pushover_zorruno, sony_tv_lounge, lounge_google_home_voice, led_matrix_1"
am_default_announcement_channels: "pushover_zorruno"
# Optional appliance-specific overrides available:
# resolution: 4
# The number of samples for the average, with one sample per minute.
#
# standby_power: 3
# Below this value, the appliance will show "Off".
# Between this value and operating_power_min, it will show "Standby".
#
# operating_power_min: 20
# The minimum average power needed for the appliance to show "Operating".
#
# min_cycle_notify_seconds: 600
# Finished cycles shorter than this are recorded, but no notification
# is sent. Default is 300 seconds.
#
# announcement_channels: "pushover_zorruno"
# Announcement channels for this appliance.
#
# long_announcement: "The device has finished. Please empty it when convenient."
# Override the long announcement.
#
# min_cycle_notify_seconds: 600
# Minimum time for an appliance to be operating before a completion announcement generated.
##########################################################################
# APPLIANCE REGISTRY
##########################################################################
am_appliances:
- appliance_id: washing_machine
appliance_name: "Washing Machine"
appliance_action: "washing2"
power_entity: "sensor.esp_laundrywashpow_load_power"
icon: "mdi:washing-machine"
short_announcement: "/s/sWashing complete"
matrix_indicator: "wash,1,15,10,6,5,1,0"
# Optional washing-machine-specific overrides
resolution: 5
standby_power: 3
operating_power_min: 20
announcement_channels: "pushover_zorruno"
- appliance_id: dryer
appliance_name: "Dryer"
appliance_action: "drying2"
power_entity: "sensor.esp_laundrydrypow_load_power"
icon: "mdi:tumble-dryer"
short_announcement: "/s/sDryer complete"
matrix_indicator: "dry,1,15,10,6,5,1,0"
# Optional dryer-specific overrides
resolution: 4
standby_power: 3
operating_power_min: 20
announcement_channels: "pushover_zorruno"
- appliance_id: main_dishwasher
appliance_name: "Main Dishwasher"
appliance_action: "dishwashing2"
power_entity: "sensor.esp_maindishwasherpower_power"
icon: "mdi:dishwasher"
short_announcement: "/sDishwasher complete"
matrix_indicator: "dish,1,15,10,6,5,1,0"
action:
##########################################################################
# PUBLISH / REFRESH MQTT DISCOVERY CONFIG
##########################################################################
- repeat:
for_each: "{{ am_appliances }}"
sequence:
- variables:
am_state_topic: >-
{{ mqtt_local_status_topic }}/{{ mqtt_device_name }}/{{ repeat.item.appliance_id }}/state
am_discovery_topic: >-
{{ mqtt_discovery_prefix }}/sensor/{{ mqtt_device_name }}_{{ repeat.item.appliance_id }}/config
am_unique_id: >-
{{ repeat.item.appliance_id }}_monitor
am_object_id: >-
{{ mqtt_device_name }}_{{ repeat.item.appliance_id }}_monitor
- action: mqtt.publish
data:
topic: "{{ am_discovery_topic }}"
retain: true
qos: 0
payload: >-
{{
dict(
name=repeat.item.appliance_name ~ ' Monitor',
unique_id=am_unique_id,
object_id=am_object_id,
icon=repeat.item.icon | default('mdi:power-plug'),
state_topic=am_state_topic,
value_template='{{ value_json.state | default("Off") }}',
json_attributes_topic=am_state_topic,
device=dict(
identifiers=[mqtt_device_name],
name='Appliance Monitor',
manufacturer='View Road Home Assistant',
model='Shared appliance power monitor'
)
) | to_json
}}
##########################################################################
# PROCESS SAMPLES
##########################################################################
#
# Always process every appliance. The processor receives power_w as a safe
# float value, so unavailable/non-numeric sensors become 0 W.
#
##########################################################################
- repeat:
for_each: "{{ am_appliances }}"
sequence:
- variables:
am_state_topic: >-
{{ mqtt_local_status_topic }}/{{ mqtt_device_name }}/{{ repeat.item.appliance_id }}/state
am_monitor_sensor: >-
sensor.{{ mqtt_device_name }}_{{ repeat.item.appliance_id }}_monitor
am_title: >-
{{ repeat.item.title | default(repeat.item.appliance_name ~ ' Notification', true) }}
am_announcement_channels: >-
{{ repeat.item.announcement_channels | default(am_default_announcement_channels, true) }}
am_debug_flow: >-
{{ repeat.item.debug_flow | default(am_default_debug_flow, true) }}
am_resolution: >-
{{ repeat.item.resolution | default(am_default_resolution, true) }}
am_standby_power: >-
{{ repeat.item.standby_power | default(am_default_standby_power, true) }}
am_operating_power_min: >-
{{ repeat.item.operating_power_min | default(am_default_operating_power_min, true) }}
am_min_cycle_notify_seconds: >-
{{ repeat.item.min_cycle_notify_seconds | default(am_default_min_cycle_notify_seconds, true) }}
am_power_w: >-
{{ states(repeat.item.power_entity) | float(0) }}
am_long_announcement: >-
{{ repeat.item.long_announcement | default('', true) }}
- action: script.appliance_monitor_process_sample
data:
appliance_id: "{{ repeat.item.appliance_id }}"
appliance_name: "{{ repeat.item.appliance_name }}"
appliance_action: "{{ repeat.item.appliance_action }}"
power_w: "{{ am_power_w }}"
monitor_sensor: "{{ am_monitor_sensor }}"
state_topic: "{{ am_state_topic }}"
debug_flow: "{{ am_debug_flow }}"
resolution: "{{ am_resolution }}"
standby_power: "{{ am_standby_power }}"
operating_power_min: "{{ am_operating_power_min }}"
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
announcement_channels: "{{ am_announcement_channels }}"
short_announcement: "{{ repeat.item.short_announcement }}"
long_announcement: "{{ am_long_announcement }}"
title: "{{ am_title }}"
matrix_indicator: "{{ repeat.item.matrix_indicator }}"