Files
zorruno-homeassistant/packages/appliance_remaining_time_predictor.yaml
T
2026-08-15 14:58:19 +12:00

443 lines
21 KiB
YAML

################################################################################
# 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) }}