Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
2265f2b282 Update HA version badge to 2025.12.1 2025-12-09 18:14:27 +00:00
Carlo Costanzo
ea8d57393a More changes to #1470 -
Refactor Dreame vacuum orchestration to implement continuous phased cleaning cycles. Update logic for room queue management and notifications, enhancing automation for idle periods and phase transitions.
2025-12-09 13:14:07 -05:00
3 changed files with 110 additions and 148 deletions

View File

@@ -45,7 +45,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
| [lightning.yaml](lightning.yaml) | Blitzortung lightning counter monitoring with snoozeable push actions. | `sensor.blitzortung_lightning_counter`, `input_boolean.snooze_lightning`, notify engine actions |
| [phynplus.yaml](phynplus.yaml) | Tie the Phyn Plus smart shutoff into HA notifications, automations, and valve overrides. | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `script.notify_engine_two_button` |
| [powerwall.yaml](powerwall.yaml) | Track Tesla Powerwall grid status and shed loads automatically when off-grid. | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `script.notify_engine` |
| [vacuum.yaml](vacuum.yaml) | Dreame (ex-Neato) vacuum orchestration with maintenance reminders and reset helpers. | `sensor.l10s_vacuum_task_status`, `sensor.l10s_vacuum_sensor_dirty_left`, `button.l10s_vacuum_reset_sensor` |
| [vacuum.yaml](vacuum.yaml) | Dreame (ex-Neato) vacuum orchestration with continuous phased sweep/mop cycles. | `input_select.l10s_vacuum_phase`, `input_text.l10s_vacuum_room_queue`, `sensor.l10s_vacuum_task_status` |
| [hass_agent_homepc.yaml](hass_agent_homepc.yaml) | Mirrors PC lock/unlock state from HASS.Agent to the office lamp for instant desk presence cues. | `sensor.carlo_homepc_carlo_homepc_sessionstate`, `switch.office_lamp_switch` |
| [finance.yaml](finance.yaml) | Yahoo Finance sensor bundle for portfolio glances and Lovelace cards. | `sensor.tsla`, `sensor.aapl`, `sensor.amzn`, `sensor.msft` |
@@ -60,8 +60,8 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
![Nest Climate Control](../www/custom_ui/floorplan/images/branding/Nest_Climate_Control.png)
### Dreame vacuum automations
- Logic lives in [vacuum.yaml](vacuum.yaml): weekday sweeping/weekend mopping, room-queue segment cleaning that pauses/docks on arrival, resumes after charging, and per-room notifications/briefing summaries.
- Uses the Dreame HACS integration with map-based segments and daily resets to cover the whole house without repeating rooms.
- Logic lives in [vacuum.yaml](vacuum.yaml): continuous four-phase loop (sweep main, sweep baths, mop main, mop baths) driven by `input_select.l10s_vacuum_phase` and `input_text.l10s_vacuum_room_queue`, with per-room notifications and automatic reseeding between phases.
- Uses the Dreame HACS integration with segment IDs to enforce bathrooms last in each sweep/mop pass, dock on arrival, and auto-run if idle for 3+ days.
![Dreame Automations](../www/custom_ui/floorplan/images/branding/Dreame%20Automations.png)
### Blog & video deep dives

View File

@@ -3,8 +3,8 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Dreame Vacuum Orchestration - Room queue, away/on-demand runs
# Weekday sweep, weekend mop, bathrooms last, notifications
# Dreame Vacuum Orchestration - Continuous phased sweep/mop with away/on-demand
# Phases: sweep main, sweep baths, mop main, mop baths; notifications + idle auto-start
# -------------------------------------------------------------------
######################################################################
@@ -12,29 +12,31 @@
######################################################################
input_boolean:
l10s_vacuum_weekday_cycle_active:
name: L10s Weekday Cleaning Active
icon: mdi:robot-vacuum
l10s_vacuum_on_demand:
name: Dream Clean (On-Demand)
icon: mdi:rocket-launch
name: Dreame Clean (On-Demand)
icon: mdi:robot-vacuum
input_datetime:
l10s_vacuum_last_weekday_cycle:
name: L10s Last Weekday Cleaning Cycle
has_date: true
has_time: true
input_select:
l10s_vacuum_phase:
name: L10s Vacuum Phase
options:
- sweep_main
- sweep_bath
- mop_main
- mop_bath
initial: sweep_main
icon: mdi:playlist-check
input_text:
l10s_vacuum_room_queue:
name: L10s Vacuum Room Queue
# Room order (id:name): 14 Kitchen, 12 Dining, 10 Living, 7 Master Bedroom, 15 Foyer, 9 Stacey Office,
# 17 Formal Dining, 13 Hallway, 8 Justin Bedroom, 6 Paige Bedroom, 4 Master Bathroom, 2 Office, 1 Pool Bath, 3 Kids Bathroom.
icon: mdi:format-list-bulleted
max: 255
l10s_vacuum_room_catalog:
name: L10s Vacuum Room Catalog
# Room order (id:name): 14 Kitchen, 12 Dining, 10 Living, 7 Master Bedroom, 15 Foyer, 9 Stacey Office,
# 17 Formal Dining, 13 Hallway, 8 Justin Bedroom, 6 Paige Bedroom, 4 Master Bathroom, 2 Office, 1 Pool Bath, 3 Kids Bathroom.
initial: "14,12,10,7,15,9,17,13,8,6,4,2,1,3"
initial: "6,7,8,9,10,12,13,14,15,17,2,4,1,3"
icon: mdi:map
max: 255
l10s_vacuum_rooms_cleaned_today:
@@ -52,54 +54,39 @@ script:
mode: single
sequence:
- variables:
# Weekday runs are sweeping (vacuum only), weekend runs are mopping
cleaning_mode: "{{ 'mopping' if now().weekday() in [5, 6] else 'sweeping' }}"
catalog_raw: "{{ states('input_text.l10s_vacuum_room_catalog') | default('', true) | string | replace(' ', '') }}"
queue_raw: "{{ states('input_text.l10s_vacuum_room_queue') | default('', true) | string | replace(' ', '') }}"
last_reset_raw: "{{ states('input_datetime.l10s_vacuum_last_weekday_cycle') }}"
last_reset_date_str: >
{% set dt = as_datetime(last_reset_raw, default=None) %}
{{ dt.date().isoformat() if dt is not none else '' }}
catalog_ints: "{{ catalog_raw | regex_findall('[0-9]+') | map('int') | select('gt', 0) | list }}"
queue_ints: "{{ queue_raw | regex_findall('[0-9]+') | map('int') | select('gt', 0) | list }}"
# Seed if queue is empty AND last reset was not today
can_seed_today: "{{ last_reset_date_str == '' or last_reset_date_str != now().date().isoformat() }}"
will_seed: >
{% set empty_queue = queue_ints | length == 0 %}
{% set on_demand = is_state('input_boolean.l10s_vacuum_on_demand', 'on') %}
{{ (empty_queue or (on_demand and queue_ints | length <= 1)) and catalog_ints | length > 0 and (can_seed_today or on_demand) }}
seeded_queue_list: "{{ catalog_ints if will_seed else queue_ints }}"
valid_queue_list: "{{ seeded_queue_list }}"
# Define bathroom IDs for mopping separation
bath_ids: [1, 3, 4]
nonbath_list: "{{ valid_queue_list | reject('in', bath_ids) | list }}"
bath_list: "{{ valid_queue_list | select('in', bath_ids) | list }}"
# Prioritize non-bathrooms first, then bathrooms
segments_to_clean: >
{% if nonbath_list | length > 0 %}
{{ nonbath_list }}
{% elif bath_list | length > 0 %}
{{ bath_list }}
main_ids: "{{ catalog_ints | reject('in', bath_ids) | list }}"
phase_order: ['sweep_main', 'sweep_bath', 'mop_main', 'mop_bath']
phase_state: "{{ states('input_select.l10s_vacuum_phase') }}"
phase: "{{ phase_state if phase_state in phase_order else 'sweep_main' }}"
cleaning_mode: "{{ 'mopping' if 'mop_' in phase else 'sweeping' }}"
queue_raw: "{{ states('input_text.l10s_vacuum_room_queue') | default('', true) | string | replace(' ', '') }}"
queue_ints: "{{ queue_raw | regex_findall('[0-9]+') | map('int') | select('gt', 0) | list }}"
phase_segments: >
{% if phase == 'sweep_main' %}
{{ main_ids }}
{% elif phase == 'sweep_bath' %}
{{ bath_ids }}
{% elif phase == 'mop_main' %}
{{ main_ids }}
{% else %}
[]
{{ bath_ids }}
{% endif %}
segments_to_clean: "{{ queue_ints if queue_ints | length > 0 else phase_segments }}"
# 1. Seed the queue if necessary
- choose:
- conditions:
- condition: template
value_template: "{{ will_seed }}"
value_template: "{{ queue_ints | length == 0 and phase_segments | length > 0 }}"
sequence:
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_room_queue
data:
value: "{{ catalog_raw }}"
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.l10s_vacuum_last_weekday_cycle
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
value: "{{ phase_segments | join(',') }}"
default: []
# 2. Check if there is anything to clean and stop if not
@@ -122,9 +109,6 @@ script:
entity_id: vacuum.l10s_vacuum
data:
fan_speed: Standard
- service: input_boolean.turn_on
target:
entity_id: input_boolean.l10s_vacuum_weekday_cycle_active
- service: dreame_vacuum.vacuum_clean_segment
target:
entity_id: vacuum.l10s_vacuum
@@ -138,36 +122,30 @@ script:
automation:
- alias: 'Away Vacuum: Reset Queue (Mon/Sat)'
id: 93a6e7dc-9c32-4d53-9f7c-651cd60f4b84
- alias: 'Vacuum: Reset Cleaned List at 5am'
id: 18f7b6d3-c02c-4ec1-88b3-0c3b8b4c6f7b
trigger:
- platform: time
at: '08:55:00'
condition:
- condition: time
weekday:
- mon
- sat
at: '05:00:00'
action:
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_room_queue
data:
value: "{{ states('input_text.l10s_vacuum_room_catalog') }}"
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_rooms_cleaned_today
data:
value: ""
- service: input_boolean.turn_off
target:
entity_id: input_boolean.l10s_vacuum_weekday_cycle_active
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.l10s_vacuum_last_weekday_cycle
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- service: input_boolean.turn_off
- alias: 'Vacuum: Auto-Start if Idle 3 Days'
id: c6b3f1e8-9a3f-4098-9b9e-1c7f2d6f1d11
trigger:
- platform: time
at: '16:00:00'
condition:
- condition: template
value_template: >
{% set last = state_attr('script.l10s_vacuum_start_next_room','last_triggered') %}
{{ last is none or (now() - last).days >= 3 }}
action:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.l10s_vacuum_on_demand
@@ -202,9 +180,6 @@ automation:
entity_id: input_boolean.l10s_vacuum_on_demand
to: 'off'
condition:
- condition: state
entity_id: input_boolean.l10s_vacuum_weekday_cycle_active
state: 'on'
- condition: template
value_template: >
{{ is_state('vacuum.l10s_vacuum', 'cleaning') or is_state('vacuum.l10s_vacuum', 'returning') or is_state('vacuum.l10s_vacuum', 'paused') }}
@@ -223,52 +198,47 @@ automation:
- platform: state
entity_id: sensor.l10s_vacuum_current_room
for: '00:03:00'
- platform: state
entity_id: vacuum.l10s_vacuum
to: 'cleaning'
for: '00:03:00'
variables:
room_map: {14:'kitchen',12:'dining-room',10:'living room',7:'master-bedroom',15:'foyer',9:'stacey-office',17:'formal-dining',13:'hallway',8:'justin-bedroom',6:'paige-bedroom',4:'master-bathroom',2:'office',1:'pool-bath',3:'kids-bathroom'}
catalog_raw: "{{ states('input_text.l10s_vacuum_room_catalog') | default('', true) | string | replace(' ', '') }}"
catalog_ints: "{{ catalog_raw | regex_findall('[0-9]+') | map('int') | select('gt', 0) | list }}"
queue_raw: "{{ states('input_text.l10s_vacuum_room_queue') | default('', true) | string | replace(' ', '') }}"
queue_ints: "{{ queue_raw | regex_findall('[0-9]+') | map('int') | select('gt', 0) | list | default([], true) }}"
working_queue: "{{ queue_ints if queue_ints | length > 0 else catalog_ints }}"
queue_ints: "{{ queue_raw | regex_findall('[0-9]+') | map('int') | list | default([], true) }}"
current_room_id: "{{ trigger.to_state.attributes.room_id | default(state_attr('sensor.l10s_vacuum_current_room', 'room_id'), true) | int(0) }}"
matched_room_id: "{{ current_room_id if current_room_id > 0 and current_room_id in (working_queue | default([], true)) else 0 }}"
remaining_rooms: "{{ working_queue | reject('equalto', matched_room_id) | list | join(',') }}"
remaining_value: >
{% set rem = remaining_rooms | string %}
{% if rem | length == 0 and working_queue | length > 1 %}
{{ working_queue | join(',') }}
matched_room_id: "{{ current_room_id if current_room_id > 0 and current_room_id in (queue_ints | default([], true)) else 0 }}"
remaining_list: >
{% set rem = [] %}
{% set removed = namespace(done=false) %}
{% for r in queue_ints %}
{% if not removed.done and r == matched_room_id %}
{% set removed.done = true %}
{% else %}
{% set rem = rem + [r] %}
{% endif %}
{% endfor %}
{{ rem }}
{% endif %}
remaining_value_str: >
{% set rv = remaining_value %}
{% if rv is string %}
{{ rv }}
{% elif rv is iterable %}
{{ rv | map('string') | join(',') }}
{% else %}
{{ rv | string }}
{% endif %}
remaining_rooms: "{{ remaining_list | join(',') }}"
remaining_count: "{{ remaining_list | length }}"
phase_order: ['sweep_main', 'sweep_bath', 'mop_main', 'mop_bath']
phase_state: "{{ states('input_select.l10s_vacuum_phase') }}"
phase: "{{ phase_state if phase_state in phase_order else 'sweep_main' }}"
phase_index: "{{ phase_order.index(phase) if phase in phase_order else 0 }}"
has_next_phase: "{{ phase_index < (phase_order | length) - 1 }}"
next_phase: "{{ phase_order[phase_index + 1] if has_next_phase else '' }}"
condition:
# Only run if there's actually a queue and a room was successfully matched to the start of the queue
- condition: template
value_template: "{{ working_queue | length > 0 }}"
- condition: template
value_template: "{{ matched_room_id != 0 }}"
value_template: "{{ queue_ints | length > 0 }}"
- condition: template
value_template: "{{ matched_room_id != 0 }}"
- condition: state
entity_id: vacuum.l10s_vacuum
state: 'cleaning'
action:
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_room_queue
data:
value: "{{ remaining_value_str }}"
value: "{{ remaining_rooms }}"
- variables:
cleaned_raw: "{{ states('input_text.l10s_vacuum_rooms_cleaned_today') | default('', true) | string }}"
cleaned_parts: "{{ cleaned_raw | regex_findall('[^,]+') | map('trim') | reject('equalto','') | list }}"
@@ -280,7 +250,6 @@ automation:
{% else %}
{{ parts | join(', ') }}
{% endif %}
remaining_count: "{{ remaining_value_str | regex_findall('[^,]+') | length if remaining_value_str | length > 0 else 0 }}"
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_rooms_cleaned_today
@@ -299,46 +268,39 @@ automation:
- choose:
- conditions:
- condition: template
value_template: "{{ remaining_rooms | length > 0 }}"
value_template: "{{ remaining_count > 0 }}"
sequence:
- service: script.l10s_vacuum_start_next_room
- conditions:
- condition: template
value_template: "{{ remaining_rooms | length == 0 }}"
value_template: "{{ remaining_count == 0 and has_next_phase }}"
sequence:
- service: input_boolean.turn_off
- service: input_select.select_option
target:
entity_id: input_boolean.l10s_vacuum_weekday_cycle_active
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.l10s_vacuum_last_weekday_cycle
entity_id: input_select.l10s_vacuum_phase
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- alias: 'Away Vacuum: Cycle Complete'
id: 8fa7779a-957b-49a3-84e7-36ca93c2e0d2
trigger:
- platform: state
entity_id: sensor.l10s_vacuum_task_status
to: 'completed'
- platform: state
entity_id: vacuum.l10s_vacuum
to: 'docked'
for: 00:05:00
condition:
- condition: template
value_template: "{{ is_state('sensor.l10s_vacuum_task_status', 'completed') }}"
- condition: template
value_template: "{{ (states('input_text.l10s_vacuum_room_queue') | replace(' ', '')) | length == 0 }}"
action:
- service: input_boolean.turn_off
option: "{{ next_phase }}"
- service: input_text.set_value
target:
entity_id: input_boolean.l10s_vacuum_weekday_cycle_active
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.l10s_vacuum_last_weekday_cycle
entity_id: input_text.l10s_vacuum_room_queue
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
value: ""
- service: script.l10s_vacuum_start_next_room
- conditions:
- condition: template
value_template: "{{ remaining_count == 0 and not has_next_phase }}"
sequence:
- service: input_select.select_option
target:
entity_id: input_select.l10s_vacuum_phase
data:
option: "sweep_main"
- service: input_text.set_value
target:
entity_id: input_text.l10s_vacuum_room_queue
data:
value: ""
- service: script.l10s_vacuum_start_next_room
- alias: 'Vacuum Sensor Cleaning Silencer'
id: 6548de52-a4a4-4df2-9d66-9c2c15577a7f

View File

@@ -1,13 +1,13 @@
<svg width="68.5" height="20" viewBox="0 0 685 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="2025.12.1">
<title>2025.12.1</title>
<linearGradient id="GGkaK" x2="0" y2="100%">
<linearGradient id="zseUi" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="wwoCE"><rect width="685" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#wwoCE)">
<mask id="nBbtk"><rect width="685" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#nBbtk)">
<rect width="685" height="200" fill="#08C" x="0"/>
<rect width="685" height="200" fill="url(#GGkaK)"/>
<rect width="685" height="200" fill="url(#zseUi)"/>
</g>
<g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="65" y="148" textLength="570" fill="#000" opacity="0.25">2025.12.1</text>

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 815 B