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