diff --git a/Common/Washing Machine/README.md b/Common/Washing Machine/README.md index 4edce27..1676e14 100644 --- a/Common/Washing Machine/README.md +++ b/Common/Washing Machine/README.md @@ -7,6 +7,7 @@ This blueprint monitors washing machine or dryer appliances and sends notificati - Start notification with cycle duration, estimated end time, and mode details - Completion notification (reminder to unload clothes) with energy report - "Almost done" notification (configurable minutes before end) +- Unload reminder (configurable delay, optional repeats) — auto-cancelled when a new cycle starts or a configured door/lid sensor opens - Pause/Resume notifications (detect when cycle is paused or resumed) - Error message notifications - Preparation mode notification (e.g., for dryer prep) @@ -39,6 +40,8 @@ The automation tracks the appliance through these states: | `cst` | Cycle Start Time (ISO timestamp) | | `esmp` | Energy Samples accumulator (Wh) | | `lst` | Last Sample Time (ISO timestamp) | +| `cct` | Cycle Completion Time (ISO timestamp, drives unload reminder) | +| `urc` | Unload Reminder Count (number of reminders already sent) | ## Message Template Variables @@ -49,7 +52,7 @@ All message templates support these placeholder variables (use single braces): | `{appliance_name}` | Device name (e.g., "Washing Machine") | | `{remaining}` | Remaining time as string (e.g., "01:30:00") | | `{estimated_end}` | Estimated completion time (e.g., "14:30") | -| `{minutes}` | Remaining minutes as number (e.g., 90) | +| `{minutes}` | Remaining minutes (almost-done) or elapsed minutes since completion (unload reminder) | | `{error}` | Error message text (only in error notification) | | `{tub_count}` | Tub clean counter value (only in tub clean notification) | | `{tub_threshold}` | Tub clean threshold (only in tub clean notification) | @@ -64,6 +67,18 @@ All message templates support these placeholder variables (use single braces): - Notification service entity - (Optional) Power sensor for energy tracking +## Unload Reminder + +If `Unload Reminder Delay` is greater than 0, the automation: + +1. Records the completion timestamp (`cct`) when the cycle finishes — but **only if** every door sensor in `Unload Reminder Door Sensors` is currently closed (or the list is empty). +2. Sends a reminder N minutes later (where N = delay), then optionally repeats every `Unload Reminder Repeat Interval` minutes up to `Unload Reminder Repeat Count` total. +3. Cancels all pending reminders when: + - A new cycle starts (start handler clears `cct` and `urc`), **or** + - Any of the configured door / lid sensors transitions to `on` (treated as "user has unloaded"). + +The check runs once per minute via a `time_pattern` trigger. The door sensor list is optional — leave it empty to keep reminders purely time-based. + ## Note Default messages are in Russian for LG ThinQ integration. Customize messages in the "Messages" section for your language. diff --git a/Common/Washing Machine/blueprint.yaml b/Common/Washing Machine/blueprint.yaml index d310d7c..b771dea 100644 --- a/Common/Washing Machine/blueprint.yaml +++ b/Common/Washing Machine/blueprint.yaml @@ -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 }}