feat: add unload reminder to Washing Machine

- Send a reminder N minutes after cycle completion, with optional repeats
- Auto-cancel reminders when a new cycle starts or a door/lid sensor opens
- Gate the completion timestamp on configured door sensors being closed
- Track completion time (cct) and reminder count (urc) in persistent state
This commit is contained in:
2026-05-27 13:09:55 +03:00
parent 34cf5b1f7a
commit ad6f30ce3c
2 changed files with 212 additions and 3 deletions
+196 -2
View File
@@ -65,6 +65,67 @@ blueprint:
selector:
boolean:
# -------------------------------------------------------------------------
# Unload Reminder
# -------------------------------------------------------------------------
# Optional reminder to unload the appliance after the cycle completes.
# Purely time-based: no door/state tracking required.
unload_reminder_group:
name: "Unload Reminder"
collapsed: true
input:
unload_reminder_delay:
name: Unload Reminder Delay (minutes)
description: >
Send a reminder this many minutes after the cycle completes
if the appliance has not been emptied yet.
Set to 0 to disable unload reminders entirely.
default: 0
selector:
number:
min: 0
max: 180
unit_of_measurement: minutes
mode: slider
unload_reminder_repeat_count:
name: Unload Reminder Repeat Count
description: >
Total number of unload reminders to send (including the first).
Set to 1 for a single reminder.
default: 1
selector:
number:
min: 1
max: 10
mode: slider
unload_reminder_repeat_interval:
name: Unload Reminder Repeat Interval (minutes)
description: >
Time between repeated unload reminders.
Only used when Repeat Count > 1.
default: 15
selector:
number:
min: 5
max: 120
unit_of_measurement: minutes
mode: slider
unload_reminder_door_sensors:
name: Unload Reminder Door Sensors (optional)
description: >
Optional list of door / lid binary sensors. When any of them
opens after the cycle completes, pending unload reminders are
cancelled (treated as "user has unloaded").
Leave empty to keep reminders purely time-based.
default: []
selector:
entity:
domain: binary_sensor
multiple: true
# -------------------------------------------------------------------------
# Persistent State Configuration
# -------------------------------------------------------------------------
@@ -143,6 +204,17 @@ blueprint:
text:
multiline: true
message_unload_reminder:
name: "Unload Reminder Message"
description: >
Reminder sent after the cycle completes if the appliance has not
been unloaded within the configured delay.
Variables: `{appliance_name}`, `{minutes}` (elapsed since completion)
default: "🧺 {appliance_name}: прошло {minutes} мин. — пора достать вещи!"
selector:
text:
multiline: true
message_almost_done:
name: "Almost Done Message"
description: >
@@ -389,6 +461,18 @@ trigger:
entity_id: !input power_sensor
id: "power_update"
# Periodic tick for time-based checks (e.g., unload reminder)
- platform: time_pattern
minutes: "/1"
id: "reminder_tick"
# Door / lid opened after completion (cancels unload reminder)
# Note: Uses multiple selector, so empty list means trigger is skipped
- platform: state
entity_id: !input unload_reminder_door_sensors
to: 'on'
id: "door_opened"
# =============================================================================
# CONDITIONS
# =============================================================================
@@ -412,6 +496,8 @@ variables:
state_cycle_start_time: 'cst' # Cycle start timestamp
state_energy_samples: 'esmp' # Energy sample accumulator (Wh)
state_last_sample_time: 'lst' # Last power sample timestamp
state_cycle_completion_time: 'cct' # Cycle completion timestamp (for unload reminder)
state_unload_reminder_count: 'urc' # Number of unload reminders already sent
# ---------------------------------------------------------------------------
# Input Variables
@@ -431,6 +517,10 @@ variables:
show_estimated_end_time: !input show_estimated_end_time
power_sensor: !input power_sensor
energy_cost_per_kwh: !input energy_cost_per_kwh
unload_reminder_delay: !input unload_reminder_delay
unload_reminder_repeat_count: !input unload_reminder_repeat_count
unload_reminder_repeat_interval: !input unload_reminder_repeat_interval
unload_reminder_door_sensors: !input unload_reminder_door_sensors
enable_debug_notifications: !input enable_debug_notifications
# ---------------------------------------------------------------------------
@@ -438,6 +528,7 @@ variables:
# ---------------------------------------------------------------------------
message_start_template: !input message_start
message_completed_template: !input message_completed
message_unload_reminder_template: !input message_unload_reminder
message_almost_done_template: !input message_almost_done
message_preparation_template: !input message_preparation
message_error_template: !input message_error
@@ -619,7 +710,9 @@ action:
state_notification_about_start_sent: true,
state_cycle_start_time: now().isoformat(),
state_energy_samples: 0,
state_last_sample_time: now().isoformat()
state_last_sample_time: now().isoformat(),
state_cycle_completion_time: '',
state_unload_reminder_count: 0
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
@@ -686,13 +779,17 @@ action:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set any_door_open = (unload_reminder_door_sensors | select('is_state', 'on') | list | length) > 0 %}
{% set arm_reminder = (unload_reminder_delay | int(0)) > 0 and not any_door_open %}
{% set new_automation_state = (automation_state | combine({
state_notification_about_remaining_time_sent: false,
state_notification_about_start_sent: false,
state_notification_about_preparation_sent: false,
state_notification_about_pause_sent: false,
state_was_paused: false,
state_energy_samples: 0
state_energy_samples: 0,
state_cycle_completion_time: (now().isoformat() if arm_reminder else ''),
state_unload_reminder_count: 0
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
@@ -973,3 +1070,100 @@ action:
state_last_sample_time: now().isoformat()
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# -----------------------------------------------------------------------
# CASE 10: Unload Reminder
# -----------------------------------------------------------------------
# Triggered when: Cycle completion timestamp is recorded and the
# configured delay (plus any repeat intervals) has elapsed without a
# new cycle starting. CASE 1 clears the timestamp on a new cycle.
# Action: Remind the user to unload the appliance.
- conditions:
- condition: template
value_template: >
{%- set cct_val = automation_state.get(state_cycle_completion_time, '') -%}
{%- set count_val = automation_state.get(state_unload_reminder_count, 0) | int(0) -%}
{{ (unload_reminder_delay | int(0)) > 0
and cct_val not in ['', 'unknown', 'unavailable', 'none']
and count_val < (unload_reminder_repeat_count | int(1))
and ((now() - (cct_val | as_datetime)).total_seconds() / 60)
>= ((unload_reminder_delay | int(0)) + count_val * (unload_reminder_repeat_interval | int(0))) }}
sequence:
- variables:
cct_val: "{{ automation_state.get(state_cycle_completion_time, '') }}"
count_val: "{{ automation_state.get(state_unload_reminder_count, 0) | int(0) }}"
new_reminder_count: "{{ (count_val | int(0)) + 1 }}"
elapsed_minutes: >
{{ ((now() - (cct_val | as_datetime)).total_seconds() / 60) | round(0) | int }}
# Render the message template with available variables
message: >
{% set tpl = message_unload_reminder_template %}
{{ tpl | replace('{appliance_name}', appliance_name)
| replace('{minutes}', elapsed_minutes | string) }}
# Send unload reminder notification
- service: notify.send_message
target:
entity_id: !input notify_target
data:
message: "{{ message }}"
# Increment reminder count; clear completion time when last reminder is sent
- service: input_text.set_value
target:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set new_automation_state = (automation_state | combine({
state_unload_reminder_count: new_reminder_count | int(0),
state_cycle_completion_time: ('' if (new_reminder_count | int(0)) >= (unload_reminder_repeat_count | int(1)) else cct_val)
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Debug notification for unload reminder
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "{appliance_name} - UNLOAD REMINDER"
message: >
Action: UNLOAD REMINDER
Time: {{ now().strftime('%H:%M:%S') }}
Reminder #: {{ new_reminder_count }} of {{ unload_reminder_repeat_count }}
Elapsed: {{ elapsed_minutes }} min
# -----------------------------------------------------------------------
# CASE 11: Door Opened After Completion (cancel unload reminder)
# -----------------------------------------------------------------------
# Triggered when: A configured door / lid sensor opens while a cycle
# completion timestamp is pending. Treated as "user has unloaded".
# Action: Clear cct and urc so no further reminders fire.
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'door_opened'
and automation_state.get(state_cycle_completion_time, '') not in ['', 'unknown', 'unavailable', 'none'] }}
sequence:
- service: input_text.set_value
target:
entity_id: "{{ automation_state_entity }}"
data:
value: >
{% set new_automation_state = (automation_state | combine({
state_cycle_completion_time: '',
state_unload_reminder_count: 0
})) %}
{{ automation_state_global | combine({ automation_state_key: new_automation_state }) | tojson }}
# Debug notification for door-cancelled reminder
- choose:
- conditions: "{{ enable_debug_notifications }}"
sequence:
- service: persistent_notification.create
data:
title: "{appliance_name} - UNLOAD DETECTED"
message: >
Action: UNLOAD REMINDER CANCELLED
Time: {{ now().strftime('%H:%M:%S') }}
Door: {{ trigger.entity_id }}