various tidyups
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
################################################################################
|
||||
# Appliance Remaining Time Predictor Package
|
||||
################################################################################
|
||||
#
|
||||
# File: appliance_remaining_time_predictor.yaml
|
||||
# Version: 1.2.0
|
||||
# Date: 2026-08-03
|
||||
#
|
||||
# Purpose:
|
||||
# - Record recent valid cycle durations for the washing machine, dryer, and
|
||||
# main dishwasher.
|
||||
# - Estimate minutes remaining while each appliance monitor is Operating.
|
||||
# - Retain the latest ten valid durations across Home Assistant restarts.
|
||||
# - Preserve the previous completed cycle's timestamps, energy, and cost while
|
||||
# a new cycle is running.
|
||||
#
|
||||
# Prediction method:
|
||||
# - Reject cycles outside appliance-specific duration limits to avoid false
|
||||
# starts and stale cycles.
|
||||
# - Keep the ten most recent valid durations.
|
||||
# - With five or more samples, discard the shortest and longest duration before
|
||||
# calculating the average. This limits the effect of unusual loads while
|
||||
# retaining recent behavior.
|
||||
# - Subtract the current cycle elapsed time from the predicted total duration.
|
||||
#
|
||||
# Version history:
|
||||
# - 1.2.0 (2026-08-06): Learn durations to the monitor's Finished transition,
|
||||
# matching the completion announcement instead of the inferred power stop.
|
||||
# - 1.1.0 (2026-08-03): Preserved last-completed cycle display values.
|
||||
# - 1.0.0 (2026-08-03): Initial three-appliance implementation.
|
||||
################################################################################
|
||||
|
||||
input_text:
|
||||
appliance_predictor_washing_machine_durations:
|
||||
name: "Appliance Predictor Washing Machine Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_dryer_durations:
|
||||
name: "Appliance Predictor Dryer Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_main_dishwasher_durations:
|
||||
name: "Appliance Predictor Main Dishwasher Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_washing_machine_last_completed:
|
||||
name: "Appliance Predictor Washing Machine Last Completed"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_dryer_last_completed:
|
||||
name: "Appliance Predictor Dryer Last Completed"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_main_dishwasher_last_completed:
|
||||
name: "Appliance Predictor Main Dishwasher Last Completed"
|
||||
max: 255
|
||||
|
||||
script:
|
||||
appliance_predictor_record_duration:
|
||||
alias: "Appliance Predictor - Record Duration"
|
||||
description: >
|
||||
Validates and appends one completed cycle duration to the selected
|
||||
appliance's retained ten-cycle history.
|
||||
mode: queued
|
||||
max: 20
|
||||
fields:
|
||||
appliance_id:
|
||||
name: "Appliance ID"
|
||||
description: "washing_machine, dryer, or main_dishwasher"
|
||||
example: washing_machine
|
||||
duration_seconds:
|
||||
name: "Duration seconds"
|
||||
description: "Completed cycle duration in seconds."
|
||||
example: 3600
|
||||
sequence:
|
||||
- variables:
|
||||
duration: "{{ duration_seconds | int(0) }}"
|
||||
history_entity: >-
|
||||
{% if appliance_id == 'washing_machine' %}
|
||||
input_text.appliance_predictor_washing_machine_durations
|
||||
{% elif appliance_id == 'dryer' %}
|
||||
input_text.appliance_predictor_dryer_durations
|
||||
{% elif appliance_id == 'main_dishwasher' %}
|
||||
input_text.appliance_predictor_main_dishwasher_durations
|
||||
{% endif %}
|
||||
minimum_duration: >-
|
||||
{{ 900 if appliance_id == 'washing_machine' else 1800 }}
|
||||
maximum_duration: >-
|
||||
{{ 14400 if appliance_id == 'washing_machine' else 21600 }}
|
||||
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
history_entity | trim | length > 0
|
||||
and duration >= (minimum_duration | int(0))
|
||||
and duration <= (maximum_duration | int(0))
|
||||
}}
|
||||
|
||||
- variables:
|
||||
updated_history: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states(history_entity).split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ns.values = ns.values + [duration] %}
|
||||
{{ ns.values[-10:] | join(',') }}
|
||||
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ history_entity | trim }}"
|
||||
data:
|
||||
value: "{{ updated_history | trim }}"
|
||||
|
||||
automation:
|
||||
- id: appliance_predictor_record_completed_cycles
|
||||
alias: "Appliance Predictor - Record Completed Cycles"
|
||||
description: >
|
||||
Records valid cycle durations when one of the three appliance monitors
|
||||
changes to Finished.
|
||||
mode: queued
|
||||
max: 10
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- sensor.appliance_monitor_washing_machine_monitor
|
||||
- sensor.appliance_monitor_dryer_monitor
|
||||
- sensor.appliance_monitor_main_dishwasher_monitor
|
||||
to: "Finished"
|
||||
actions:
|
||||
- variables:
|
||||
appliance_id: >-
|
||||
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
|
||||
washing_machine
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
|
||||
dryer
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
|
||||
main_dishwasher
|
||||
{% endif %}
|
||||
duration: >-
|
||||
{{
|
||||
as_timestamp(trigger.to_state.last_changed)
|
||||
- (trigger.to_state.attributes.cycle_start_ts | float(0))
|
||||
}}
|
||||
completed_record_entity: >-
|
||||
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
|
||||
input_text.appliance_predictor_washing_machine_last_completed
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
|
||||
input_text.appliance_predictor_dryer_last_completed
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
|
||||
input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
{% endif %}
|
||||
completed_record: >-
|
||||
{{
|
||||
[
|
||||
trigger.to_state.attributes.cycle_start_ts | float(0),
|
||||
trigger.to_state.attributes.cycle_stop_ts | float(0),
|
||||
trigger.to_state.attributes.cycle_energy_kwh | float(0),
|
||||
trigger.to_state.attributes.cycle_cost | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ completed_record_entity | trim }}"
|
||||
data:
|
||||
value: "{{ completed_record | trim }}"
|
||||
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: "{{ appliance_id | trim }}"
|
||||
duration_seconds: "{{ duration }}"
|
||||
|
||||
- id: appliance_predictor_seed_empty_histories
|
||||
alias: "Appliance Predictor - Seed Empty Histories"
|
||||
description: >
|
||||
Seeds an empty history from the retained last completed monitor cycle
|
||||
after Home Assistant starts. Existing learned histories are not changed.
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
actions:
|
||||
- delay: "00:00:30"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: washing_machine
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: dryer
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: main_dishwasher
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_washing_machine_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_washing_machine_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_dryer_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_dryer_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_dryer_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_main_dishwasher_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
template:
|
||||
- triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/1"
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- sensor.appliance_monitor_washing_machine_monitor
|
||||
- sensor.appliance_monitor_dryer_monitor
|
||||
- sensor.appliance_monitor_main_dishwasher_monitor
|
||||
- input_text.appliance_predictor_washing_machine_durations
|
||||
- input_text.appliance_predictor_dryer_durations
|
||||
- input_text.appliance_predictor_main_dishwasher_durations
|
||||
- input_text.appliance_predictor_washing_machine_last_completed
|
||||
- input_text.appliance_predictor_dryer_last_completed
|
||||
- input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
sensor:
|
||||
- name: "Washing Machine Estimated Time Remaining"
|
||||
unique_id: washing_machine_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_washing_machine_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
|
||||
- name: "Dryer Estimated Time Remaining"
|
||||
unique_id: dryer_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_dryer_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_dryer_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
|
||||
- name: "Main Dishwasher Estimated Time Remaining"
|
||||
unique_id: main_dishwasher_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_main_dishwasher_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
Reference in New Issue
Block a user