Compare commits
1 Commits
90e1222a2f
...
f8099406e8
| Author | SHA1 | Date | |
|---|---|---|---|
| f8099406e8 |
File diff suppressed because it is too large
Load Diff
@@ -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 }}"
|
||||
@@ -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 }}"
|
||||
Reference in New Issue
Block a user