04 โ Automation Examples¶
This section gives ready-to-use Home Assistant automation YAML for the most common eufy_vacuum patterns. Every example is built from the actual event names, service names, and payload fields used in the integration โ nothing here is invented.
The automation surface (what you can call)¶
The integration registers ~145 services, but most are dashboard-card getters and one-time setup CRUD. For automations you touch a small, high-level surface โ you say what and when, and the integration owns how (payload assembly, room ordering, availability gating, mixed-mode water safety). There are deliberately no per-setting entities to poke; the intelligence is not meant to live in your automation.
What an automation writes (per room):
| Entity | Purpose |
|---|---|
switch.<vacuum>_<room>_selected_for_cleaning |
Include/exclude the room. On = enabled = in the payload. |
number.<vacuum>_<room>_order |
Clean order for that room. |
Per-room settings (mode, fan, water, intensity, passes, edge-mop) are not pokeable
entities โ set them with the update_room_fields service (one call per room) or by applying
a profile.
The verbs, by job:
| Job | Services |
|---|---|
| Start a clean | start_selected_rooms, start_run_profile, apply_run_profile |
| Clean a zone | clean_saved_zone, clean_saved_zones (named saved zones); start_zone_clean (free-form rectangles) |
| Configure rooms | update_room_fields, apply_room_profile |
| Control in-flight | pause_active_job, resume_active_job, cancel_active_job |
| Dock / upkeep | wash_mop, dry_mop, stop_dry_mop, empty_dust, reset_maintenance, battery_rebaseline |
| Learning | set_learning_processing, process_pending_runs |
| Monitoring | set_stall_capture (arm the stall photo for a vacuum) |
Read state (each returns a response_variable) |
get_start_status, get_active_job, get_job_progress_snapshot, get_lifecycle_state, get_vacuum_capabilities, get_saved_run_profiles |
The normal sequence is always the same shape:
- Select โ flip the room switches, or
update_room_fields(enable + settings in one call), or apply a profile. - Start โ
start_selected_rooms(ad-hoc) orstart_run_profile(a saved routine). - React (optional) โ trigger on an
eufy_vacuum_*event, or poll aget_*getter.
Every example below is a worked case of that shape. Full per-field docs for every service
live in Developer Tools โ Actions in Home Assistant (and in services.yaml).
1. Retry Missed Rooms¶
What it does: When a run ends early (cancelled, failed, or interrupted) and at
least one queued room was not cleaned, the integration fires
eufy_vacuum_run_incomplete. This automation catches that event and immediately
calls eufy_vacuum.retry_missed_rooms to re-queue only the skipped rooms and
start a new job.
automation:
alias: "Vacuum โ retry missed rooms"
description: >
Re-queue and restart only the rooms that were skipped after an
incomplete run. Triggered by eufy_vacuum_run_incomplete.
trigger:
- platform: event
event_type: eufy_vacuum_run_incomplete
event_data:
vacuum_entity_id: vacuum.alfred
condition: []
action:
- service: eufy_vacuum.retry_missed_rooms
data:
vacuum_entity_id: "{{ trigger.event.data.vacuum_entity_id }}"
# map_id is optional โ the service reads it from the incomplete run log.
# confirm_reduced_run defaults to true, which is the right default for
# automation use since there is no interactive confirmation step.
Customization points:
- Replace
vacuum.alfredin the triggerevent_datafilter with your vacuum entity ID. Filtering byvacuum_entity_idin the trigger is recommended when you have more than one vacuum so the automation does not fire for the wrong device. - You can set
map_idexplicitly if you need to override the map that was active during the incomplete run. In most cases you can omit it. - Set
confirm_reduced_run: falseif you want the retry to abort rather than proceed when blocker rules are still active. The default (true) keeps things fully automated. - Add a
path_block_actionfield (event_only,pause_and_event, orcancel_and_event) to control what happens if a path-blocking rule fires during the retry run itself.
Caveats:
- The service returns
{"started": false, "reason": "no_missed_rooms"}if the incomplete run log is absent or already cleared. In a pure event-triggered automation this is harmless, but you can add await_for_triggeror aconditionchecking theeufy_vacuum_run_incompletepayload'smissed_room_idslist if you want to guard against edge cases. retry_missed_roomsclears the incomplete run log on success. Calling it twice for the same event will result in a no-op on the second call.
2. Stall Response¶
What it does: When the vacuum has been in a room for at least twice its learned
duration threshold and is still waiting to exit, the integration fires
eufy_vacuum_stall_detected. This automation sends a mobile notification that
includes the room name, how long the vacuum has been there, and how long it was
expected to take.
The event fires at most once per room per job, so you will not receive duplicate alerts for the same stall.
automation:
alias: "Vacuum โ stall alert"
description: >
Send a notification when the vacuum stalls in a room for longer than
its learned threshold.
trigger:
- platform: event
event_type: eufy_vacuum_stall_detected
event_data:
vacuum_entity_id: vacuum.alfred
condition: []
action:
- service: notify.mobile_app_your_phone
data:
title: "Vacuum stalled โ {{ trigger.event.data.room_name }}"
message: >
{{ trigger.event.data.vacuum_entity_id }} has been in
{{ trigger.event.data.room_name }} for
{{ trigger.event.data.elapsed_minutes | round(1) }} min
(expected {{ trigger.event.data.expected_minutes | round(1) }} min,
ratio {{ trigger.event.data.stall_ratio | round(2) }}).
It may need attention.
Event payload fields used:
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
string | Entity ID of the vacuum |
map_id |
string | Map the job is running on |
room_id |
int | Room where the stall was detected |
room_name |
string | Display name of the stalled room |
elapsed_minutes |
float | Minutes the vacuum has been in this room |
expected_minutes |
float | Learned expected duration for this room |
stall_ratio |
float | elapsed / expected โ always >= 2.0 at fire time |
Customization points:
- Replace
notify.mobile_app_your_phonewith your notification service. - Add an
ifcondition ontrigger.event.data.stall_ratioto suppress notifications below a threshold you consider acceptable (for example, only alert whenstall_ratio > 3). - You can also automatically pause or cancel the job from this automation by
calling
eufy_vacuum.pause_active_joboreufy_vacuum.cancel_active_jobwith thevacuum_entity_idandmap_idfrom the event payload.
3. Scheduled Clean with a Run Profile¶
What it does: Runs a saved run profile on a fixed schedule using
eufy_vacuum.start_run_profile. The service applies the profile's room
selection and per-room settings, rebuilds the queue, and starts the job through
the same protected start flow used by the dashboard card.
If the profile carries an ordered steps list โ room groups separated by
charge_wait or wait stops โ start_run_profile runs the whole sequence as
one job: it cleans a group, docks and charges to the target (or holds for the
wait), then continues with the next group. Nothing about the automation changes;
the same service call runs a plain profile or a stepped one.
automation:
alias: "Vacuum โ weekday morning full clean"
description: >
Start the 'Full House' run profile every weekday at 9 AM.
trigger:
- platform: time
at: "09:00:00"
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: eufy_vacuum.start_run_profile
data:
vacuum_entity_id: vacuum.alfred
profile_id: full_house
confirm_reduced_run: true
path_block_action: pause_and_event
# map_id is optional โ defaults to the vacuum's current active map.
# Set it explicitly only if you need to target a stored secondary map.
Finding your profile_id: Call eufy_vacuum.get_saved_run_profiles with
just your vacuum_entity_id (the map auto-resolves). The response lists every
saved run profile; use the id field from the profile you want.
Customization points:
- Adjust the
attime andweekdaylist to match your schedule. - Set
confirm_reduced_run: falseif you want the job to abort rather than start with a reduced set of rooms when blockers are active. The default isfalseforstart_run_profileโ set it totrueexplicitly if you want fully unattended operation. path_block_actioncontrols what happens mid-run if a blocker rule fires.pause_and_eventis a good default for attended schedules (you get an alert and the job pauses). Usecancel_and_eventfor overnight or unattended runs.- Add
pause_timeout_minutes_override: 30to cap how long the job may remain paused before it auto-cancels. Set it to0to disable auto-cancel for this run. - To define a stepped profile programmatically, call
eufy_vacuum.set_run_profile_stepswith theprofile_idand an orderedstepslist. A step is a room group ({type: room_group, rooms: [...]}), a charge stop ({type: charge_wait, target_battery_percent: 95}, valid 1โ100), a dry/hold stop ({type: wait, wait_minutes: 20}, valid 1โ1440), or a zone step ({type: zone, zone_ids: [...]}, naming saved zones cleaned together in one phase). The write is strict, not forgiving: the list must contain at least one room group, a sequence may not start or end with a charge/wait stop, and an out-of-range or malformed step is refused rather than silently dropped or clamped. Unlike the other services in this recipe, a refusal here is a raised error, not a returned payload โ the service call fails withServiceValidationError(aborting the automation action if you don't guard it) and the reason (invalid_steps,no_room_group,leading_break_unsupported,trailing_break_unsupported, โฆ) is only available as free text inside the exception message. The manager computes a richer refusal internally โ including arejected_stepslist naming each bad entry by index and reason โ but the service wrapper discards it before raising, so that detail is not exposed by this service today; you'll need to fix the malformed step(s) by inspecting your ownstepspayload rather than reading it back from the integration. - To watch the charge/wait phase from an automation, read
eufy_vacuum.get_job_progress_snapshot. It exposescharge_phase_active,charge_target_percent,charge_eta_minutes(andcharge_eta_source), pluswait_phase_activeandwait_minutes. Theeufy_vacuum_job_progress_tickevent fires every 5 s during an active job as a "refresh now" trigger for re-reading the snapshot.
Caveats:
- If the profile's run is reduced by active blocker rules and
confirm_reduced_runisfalse(the default), the service returns{"started": false, "reason": "confirmation_required"}(alongsidemessage,warning,preflight, andconfirm_token) and the job does not start. An automation that inspects the response must teststarted == falseandreason == "confirmation_required"โ there is nostatusfield. Setconfirm_reduced_run: truefor fully automated schedules. start_run_profilerequires the vacuum to be docked and idle. If a job is already in progress (or the start is otherwise blocked), it returns a structured refusal โ{"started": false, "reason": ..., "message": ...}โ rather than raising an error.
4. Post-Job Notification¶
What it does: Sends a summary notification every time a job finishes. The
eufy_vacuum_job_finished event includes a room_count and a status field so
you can see at a glance whether the run completed normally or was cancelled.
automation:
alias: "Vacuum โ job finished summary"
description: >
Send a notification when any job finishes, including the room count
and final status.
trigger:
- platform: event
event_type: eufy_vacuum_job_finished
event_data:
vacuum_entity_id: vacuum.alfred
condition: []
action:
- service: notify.mobile_app_your_phone
data:
title: >
Vacuum job {{ trigger.event.data.status }}
message: >
{{ trigger.event.data.vacuum_entity_id }} finished
{% if trigger.event.data.room_count is not none %}
cleaning {{ trigger.event.data.room_count }} room(s).
{% else %}
a cleaning job.
{% endif %}
Status: {{ trigger.event.data.status }}.
{% if trigger.event.data.reason_detail %}
Detail: {{ trigger.event.data.reason_detail }}.
{% endif %}
Finalized at: {{ trigger.event.data.finalized_at }}.
Event payload fields used:
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
string | Entity ID of the vacuum |
map_id |
string | Map the job ran on |
job_id |
string or null | Unique job identifier |
status |
string | Outcome: completed, cancelled, failed, interrupted |
reason_detail |
string or null | Human-readable lifecycle message |
used_for_learning |
bool or null | Whether the job was used to update learned stats |
finalized_at |
string or null | ISO timestamp when the job was finalized |
room_count |
int or null | Number of rooms in the job |
duration_minutes |
float or null | Total wall-clock duration of the job in minutes |
actual_cleaning_minutes |
float or null | Minutes the vacuum spent actively cleaning |
job_path |
string or null | Path to the archived job file |
One payload asymmetry:
duration_minutesandactual_cleaning_minutesride the listener-fired paths (normal auto-finalization, pause-timeout cancel, path-block cancel). A job finalized manually via thefinalize_learning_jobservice fires a reduced payload without those two keys โ they are absent, not null โ so guard withis definedif you finalize manually.
Customization points:
- Filter on
statusin the triggerevent_datato only notify for specific outcomes โ for example,status: cancelledto alert when a job did not complete. - Use
job_idto look up more detail witheufy_vacuum.get_active_jobor to cross-reference learning history. room_countisnullwhen the job record does not include it (rare, typically manual finalization). Guard against it withis not noneas shown above.
5. Path Blocked Alert¶
What it does: When a blocker rule fires during an active job and one or more
remaining rooms become inaccessible, the integration fires
eufy_vacuum_path_blocked. This automation sends a notification listing which
rooms are affected so you can decide whether to intervene.
The event fires at most once per unique blocked-room signature per job, so you will not receive duplicate alerts for the same blocking state.
automation:
alias: "Vacuum โ path blocked alert"
description: >
Notify when a path-blocking rule fires during an active job and
remaining rooms can no longer be reached.
trigger:
- platform: event
event_type: eufy_vacuum_path_blocked
event_data:
vacuum_entity_id: vacuum.alfred
condition: []
action:
- service: notify.mobile_app_your_phone
data:
title: "Vacuum path blocked"
message: >
{{ trigger.event.data.vacuum_entity_id }} โ path blocked
during job {{ trigger.event.data.job_id }}.
Affected rooms:
{{ trigger.event.data.affected_remaining_room_names | join(', ') }}.
Trigger entity: {{ trigger.event.data.trigger_entity_id }}
({{ trigger.event.data.trigger_entity_state }}).
Action taken: {{ trigger.event.data.action_taken }}.
Event payload fields used:
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
string | Entity ID of the vacuum |
map_id |
string | Map the job is running on |
job_id |
string or null | Unique job identifier |
trigger_entity_id |
string or null | HA entity whose state change triggered the block |
trigger_entity_state |
string or null | New state of the trigger entity |
affected_remaining_room_ids |
list of strings | IDs of rooms now blocked or inaccessible |
affected_remaining_room_names |
list of strings | Display names of those rooms |
directly_blocked_room_ids |
list of strings | Rooms blocked by a direct blocker rule |
indirectly_blocked_room_ids |
list of strings | Rooms blocked by access dependency on a blocked room |
remaining_room_ids |
list of strings | All remaining room IDs in the job at fire time |
reason_codes |
list of strings | Unique reason strings from affected rooms |
affected_rooms |
list of dicts | Full per-room block detail (source, reason, rule IDs) |
requires_attention |
bool | Always true when this event fires |
event_scope |
string | Always "active_job_path_blocked" |
path_block_action |
string | The path_block_action that was configured for this run |
action_taken |
string | What the integration actually did: event_only, already_paused, paused, pause_failed, cancelled, cancel_failed, or cancel_suppressed_recheck (a cancel was configured, but at action time the triggering rule no longer matched with a known entity state, so the irreversible cancel was suppressed) |
indeterminate_rules |
list of dicts | Blocker rules whose entity read unavailable/unknown at evaluation time โ held (neither blocking nor clearing), listed for diagnostics as {rule_id, entity_id, room_id} |
action_result |
dict | Present only when a pause/cancel was attempted โ the raw result of that call |
Customization points:
- The
path_block_actionthe integration takes (pause, cancel, or event-only) is configured when you start the job viastart_run_profileorstart_selected_rooms, not in this automation. This automation is the notification layer that runs regardless of which action was taken. - Use
trigger.event.data.action_takenin your message to communicate whether the vacuum was paused or cancelled automatically. - If you set
path_block_action: event_onlyon the job, you can use this automation to take your own action โ for example, callingeufy_vacuum.cancel_active_jobafter sending a notification. - You can also filter notifications by
trigger.event.data.directly_blocked_room_idsto distinguish a hard block (a rule matched) from an indirect block (a downstream room became unreachable because its access-gating room was blocked).
6. Ad-hoc Room Clean (custom selection)¶
What it does: Builds a one-off clean from a specific set of rooms โ the modern
replacement for hand-assembling a vacuum.send_command payload from a wall of helper
entities. The integration builds the queue, resolves the map, applies mixed-mode water
safety, and gates on availability; the automation only says which rooms and how.
Entity-driven โ when the rooms are already configured the way you want and you only
need to pick which run (on = enabled = in the payload):
automation:
alias: "Vacuum โ quick downstairs"
trigger:
- platform: state
entity_id: input_button.clean_downstairs
action:
# 1. Select rooms
- service: homeassistant.turn_on
target:
entity_id:
- switch.alfred_kitchen_selected_for_cleaning
- switch.alfred_hallway_selected_for_cleaning
# 2. Start them
- service: eufy_vacuum.start_selected_rooms
data:
vacuum_entity_id: vacuum.alfred
confirm_reduced_run: true
Service-driven โ when the automation should also set per-room behavior in the same
run. update_room_fields sets enabled and any settings in one call, so no
per-setting entities are needed and no switch toggle is required (enabled: true selects
the room):
action:
- service: eufy_vacuum.update_room_fields
data:
vacuum_entity_id: vacuum.alfred
room_id: 5 # Kitchen
enabled: true
clean_mode: vacuum_mop # vacuum | mop | vacuum_mop
fan_speed: Max
water_level: High
- service: eufy_vacuum.update_room_fields
data:
vacuum_entity_id: vacuum.alfred
room_id: 4 # Hallway
enabled: true
clean_mode: vacuum # carpet-safe: vacuum only
- service: eufy_vacuum.start_selected_rooms
data:
vacuum_entity_id: vacuum.alfred
confirm_reduced_run: true
Customization points:
confirm_reduced_run: truekeeps it unattended โ the run proceeds even if blocker rules would reduce the room set. Omit it (defaultfalse) and the service returns{"started": false, "reason": "confirmation_required", "confirm_token": ...}instead of starting, so a supervised automation can inspect the response and decide.update_room_fieldschanges only the fields you pass; unset fields keep their stored values. It re-derivesprofile_nameafter the change โ snapping it to the preset that matches the resulting fields, orcustomwhen the room no longer matches any preset.- Water-on-carpet is enforced at payload time regardless of what you pass โ a carpet
room is never wet-mopped even if you set a
water_level. - Add
strict_order: truetostart_selected_roomsto force the vacuum to honor thenumber.<room>_ordersequence instead of path-optimizing. start_selected_roomsneeds the vacuum docked/idle; if a job is already running it returns{"started": false, "reason": ..., "message": ...}instead of starting. Guard with aconditiononstates('vacuum.alfred') in ['docked','idle']if the trigger might fire mid-run.
Finding a room_id: call eufy_vacuum.get_vacuum_maps (or get_queue_state) with a
response_variable โ the response lists each room's room_id and name. These are the
device's own room numbers (the same ones the Eufy app uses).
7. Staged Clean with Native Waits (charge break in automation land)¶
What it does: Runs a multi-phase clean โ vacuum, charge, then mop โ where the
automation owns the pauses instead of a stepped run profile. Each phase is a normal
start_selected_rooms call; between phases you use Home Assistant's own wait primitives.
This is the inverse of a stepped run profile: there, the integration owns the charge_wait
break internally and the whole thing is one job. Here you orchestrate it in the open, and
every primitive you need is already exposed:
| Between-phase wait | How |
|---|---|
| Phase finished | wait_for_trigger on eufy_vacuum_job_finished |
| Charge break | the vacuum docks automatically after a clean โ just wait_template on the battery level |
| Timed hold | plain delay |
automation:
alias: "Vacuum โ vacuum, charge, then mop the main floor"
trigger:
- platform: time
at: "09:00:00"
action:
# --- Phase 1: vacuum ---
- service: eufy_vacuum.update_room_fields
data: { vacuum_entity_id: vacuum.alfred, room_id: 5, enabled: true, clean_mode: vacuum }
- service: eufy_vacuum.update_room_fields
data: { vacuum_entity_id: vacuum.alfred, room_id: 7, enabled: true, clean_mode: vacuum }
- service: eufy_vacuum.start_selected_rooms
data: { vacuum_entity_id: vacuum.alfred, confirm_reduced_run: true }
# Wait for phase 1 to finish (the vacuum docks itself at the end)
- wait_for_trigger:
- platform: event
event_type: eufy_vacuum_job_finished
event_data: { vacuum_entity_id: vacuum.alfred }
timeout: "01:30:00"
continue_on_timeout: false
# --- Charge break: wait for the level (swap for `delay:` to make it a timed hold) ---
- wait_template: "{{ state_attr('vacuum.alfred', 'battery_level') | int(0) >= 90 }}"
timeout: "02:00:00"
continue_on_timeout: false
# --- Phase 2: mop the same rooms ---
- service: eufy_vacuum.update_room_fields
data: { vacuum_entity_id: vacuum.alfred, room_id: 5, enabled: true, clean_mode: mop, water_level: High }
- service: eufy_vacuum.update_room_fields
data: { vacuum_entity_id: vacuum.alfred, room_id: 7, enabled: true, clean_mode: mop, water_level: High }
- service: eufy_vacuum.start_selected_rooms
data: { vacuum_entity_id: vacuum.alfred, confirm_reduced_run: true }
Learning still works โ and it's actually cleaner. Each phase goes out through
start_selected_rooms, so it is a dispatched job (started by the integration, not the
app). Every phase is captured and feeds per-room learning normally, with no attribution
guesswork โ the integration sent the rooms, so it knows exactly what ran. And because the
charge happens between two separate jobs, it is invisible to either job's timing: the
vacuum phase learns vacuum timing, the mop phase learns mop timing, and the dead charge
time is learned by nobody. A stepped run profile has to deliberately exclude the
charge_wait phase from its timing; this pattern never has that problem, because there is
no charge inside a job to exclude.
Choosing between this and a stepped run profile:
| Stepped run profile | Native waits (this) | |
|---|---|---|
| Records | One composite vac โ charge โ mop job |
One record per phase |
| Charge timing | Excluded via break-phase logic | Free โ charge is dead time between jobs |
| Orchestration | Owned by the integration | Owned by your automation (transparent, debuggable) |
| Room learning | Yes | Yes, per phase |
Both feed learning. Pick the profile when you want one tidy record; pick native waits when you want to see and control each phase.
Customization points:
- Swap the
wait_templatefordelay: "00:20:00"to make it a timed hold instead of a charge break. continue_on_timeout: falseaborts the sequence if a phase never finishes, so a stuck vacuum doesn't fall through into the mop phase. Raise thetimeoutvalues to fit your home.- Confirm the battery attribute on your vacuum entity โ most report
battery_level, but check Developer Tools if thewait_templatenever releases.
Caveats:
- The
enabled/selected flag persists โ a run does not clear it. The example re-cleans the same rooms, so re-enabling them for phase 2 is harmless. If a later phase cleans a different set of rooms, first turn the previous phase's rooms off (homeassistant.turn_offon theirswitch.<vac>_<room>_selected_for_cleaning, orupdate_room_fieldswithenabled: false) so they don't run again. start_selected_roomsneeds the vacuum docked/idle. After a charge break it will be docked, so phase 2 starts cleanly โ but a phase that doesn't return to the dock first should be gated onstates('vacuum.alfred') in ['docked','idle'].
8. Clean a Saved Zone on a Trigger¶
What it does: Fires a saved zone โ a named region you drew once ("under the table", "the litter corner", "the entryway") โ as a one-off clean when something happens. This is the automation form of tapping Clean on a saved zone: a precise sub-room clean, no whole-room run.
Use clean_saved_zone for one zone, or clean_saved_zones for several at once (the device
cleans the whole set in a single run).
automation:
alias: "Vacuum โ clean under the table after dinner"
description: >
Clean the 'under the table' saved zone every evening at 8 PM.
trigger:
- platform: time
at: "20:00:00"
condition:
# Fire-and-forget zone cleans only run when their map is the ACTIVE map, and
# need the vacuum free โ gate on a docked/idle vacuum to avoid a refusal.
- condition: state
entity_id: vacuum.alfred
state:
- docked
- idle
action:
- service: eufy_vacuum.clean_saved_zone
data:
vacuum_entity_id: vacuum.alfred
map_id: "6" # the zone's map โ must be the currently active map
zone_id: zone_abc123 # the saved zone to clean
clean_times: 1 # optional passes (defaults to the device default)
Cleaning several zones at once โ swap the service and pass a list:
- service: eufy_vacuum.clean_saved_zones
data:
vacuum_entity_id: vacuum.alfred
map_id: "6"
zone_ids:
- zone_abc123 # under the table
- zone_def456 # the entryway
clean_times: 1
Finding your map_id and zone_id: call eufy_vacuum.get_vacuum_maps with a
response_variable for the map ids; each saved zone's zone_id is shown in the card's
Saved Zones panel (and in the get_map_segments response). Per-brand caps apply to a
batch โ Eufy up to 10 zones, Roborock up to 5.
Customization points:
- Any trigger works โ a time, an
input_button, a person leaving, abinary_sensor(a litter box, a rain sensor). The zone clean is just the action. clean_timesis optional; omit it to use the device default, or raise it for a dirtier spot.- To fold a zone into a whole-room run instead of firing it alone โ "vacuum the kitchen,
then hit the stove zone, then mop" โ don't use these services; build a stepped run
profile with a zone step and start it with
start_run_profile(Recipe 3). See Zones โ Add a zone to a run.
Caveats:
- Fire-and-forget. These are not tracked as a room-queue job โ there is no job record,
no per-room learning, and no
eufy_vacuum_job_finishedevent for the zone clean. (A zone run inside a stepped profile, by contrast, is tracked and learned.) - Active-map only. A saved zone cleans only when its map is the vacuum's current active map; on a different map the service refuses. On a single-map home this is never an issue.
- The vacuum must be free to take the command โ hence the docked/idle condition above.
9. Pre-Run Conditions¶
Start when, not charge first. The integration owns what happens inside a job โ the sequencer runs a job's room groups, charge/wait stops, and zone steps as one choreographed unit (Recipe 3). Home Assistant owns whether and when a job starts. So the things you might picture as "steps before the run" โ charge to full first, then run ยท only run off-peak ยท hold until 9 AM โ aren't internal steps at all. They're conditions on your start automation. That keeps each job a clean, self-contained unit the learning system can trust, and puts the "when" where it belongs: in HA's triggers and conditions, out in the open.
Three patterns cover almost every "before the run" wish:
| Wish | Express it as |
|---|---|
| Run at / after a time | a time trigger (or a delay) before the start call โ Recipe 3 |
| Only run once charged | the vacuum is already docked and charging; wait for the battery level, then start โ no internal "leading charge" needed, the dock does the charging and HA just waits |
| Only run off-peak | a time window (and/or battery) condition guarding the start |
automation:
alias: "Vacuum โ overnight run, but only once charged and off-peak"
description: >
At 1 AM, wait until the battery is topped up, then start the full-house
profile โ but only during the off-peak window.
trigger:
- platform: time
at: "01:00:00"
condition:
# Off-peak window guard (skip entirely outside it).
- condition: time
after: "00:00:00"
before: "06:00:00"
action:
# "Charge first" is just waiting for the level โ the vacuum charges on its
# own dock; the automation holds until it's ready, then starts.
- wait_template: "{{ state_attr('vacuum.alfred', 'battery_level') | int(0) >= 95 }}"
timeout: "03:00:00"
continue_on_timeout: false
- service: eufy_vacuum.start_run_profile
data:
vacuum_entity_id: vacuum.alfred
profile_id: full_house
confirm_reduced_run: true
Why not a "leading charge step" inside the job? Because a charge before any cleaning isn't choreography โ it's a start pre-condition, and the vacuum already charges itself on the dock. Modelling it as an internal step would bury a "when to start" decision inside the job, where it can't see your automation's triggers and would muddy the job's timing. A charge between two cleaning phases (vacuum โ charge โ mop) genuinely is intra-job choreography โ that one belongs to the sequencer, as a stepped run profile (Recipe 3) or as native between-phase waits (Recipe 7).
Customization points:
- Swap the
wait_templatebattery threshold to taste, or drop it if you don't care about the level. - Replace the time window with any condition โ an energy-price sensor, a "nobody home"
presence check, a
suncondition. continue_on_timeout: falsemeans a vacuum that never reaches the level simply doesn't run, rather than starting under-charged.
Caveats:
wait_templateneeds the battery attribute to actually change โ confirm your vacuum reportsbattery_level(most do) in Developer Tools, or the wait will sit until timeout.- Keep the
timeoutsane so a stuck automation eventually releases instead of hanging for a day.
10. Send the Stall Photo to Your Phone¶
What it does: Recipe 2 tells you that the vacuum stalled and in which room. This one sends you the picture โ the room's shape with the robot's position and the path it took either side of the stall โ so you can see whether it is wedged under the couch or just slow, without opening Home Assistant.
Turn it on first. Stall capture is off by default and armed per vacuum. Call
eufy_vacuum.set_stall_capture once (Developer Tools โ
Actions, or the switch in the card's Rooms panel):
Once armed, each detected stall renders a PNG, raises a persistent notification, and fires
eufy_vacuum_stall_captured carrying the file path.
The image is a file, not a URL. It is written to
config/eufy_vacuum/learning/<vacuum>/stall/<map_id>.png and deliberately not under
www/, because that directory is served at /local/ with no authentication โ putting it
there would publish a cropped floor plan of your home at a fetchable URL on every stall. So
use a notifier that takes a filesystem path. Telegram does:
automation:
alias: "Vacuum โ send the stall photo"
description: >
When a stall capture lands, send the rendered room picture to Telegram
using the path from the event payload.
trigger:
- platform: event
event_type: eufy_vacuum_stall_captured
event_data:
vacuum_entity_id: vacuum.alfred
condition: []
action:
- service: notify.telegram
data:
message: "{{ trigger.event.data.message }}"
data:
photo:
- file: "{{ trigger.event.data.image_path }}"
caption: >
{{ trigger.event.data.room_name }} โ
map {{ trigger.event.data.map_id }}
Telegram will only read files from directories you have allowed, so add the capture
directory to configuration.yaml once:
If you use the Home Assistant companion app instead, its image attachment takes a
URL, not a path โ so the file has to be reachable over HTTP, which means copying it into
www/. That re-introduces exactly the exposure the feature avoids: anything under www/
is served at /local/ without authentication, so a copy of your floor plan becomes
fetchable by anyone who can reach your Home Assistant URL and guess the filename. If you
accept that trade for one vacuum, do it explicitly with a shell_command:
# configuration.yaml
shell_command:
copy_stall_capture: "cp '{{ source }}' /config/www/stall-latest.png"
action:
- service: shell_command.copy_stall_capture
data:
source: "{{ trigger.event.data.image_path }}"
- service: notify.mobile_app_your_phone
data:
title: "Alfred may be stuck"
message: "{{ trigger.event.data.message }}"
data:
image: "/local/stall-latest.png?v={{ now().timestamp() | int }}"
The ?v= query string is not decoration โ the file is overwritten in place, so without a
changing URL the phone shows the previous capture from its cache.
Event payload fields used:
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
string | Entity ID of the vacuum |
map_id |
string | Map the job was running on |
room_id |
int | Room the robot stalled in |
room_name |
string | Display name of that room |
image_path |
string | Absolute path to the PNG just written |
message |
string | The same one-line text as the persistent notification |
Customization points:
- Drop the
event_datafilter to cover every vacuum, and put{{ trigger.event.data.vacuum_entity_id }}in the message. - Pair this with Recipe 2: trigger the text alert on
eufy_vacuum_stall_detectedfor speed, and the picture oneufy_vacuum_stall_capturedwhen it lands. - Add a
eufy_vacuum.pause_active_jobaction if you would rather the robot stop and wait while you look at the photo.
Caveats:
- Nothing fires if capture is not armed. With the switch off you still get
eufy_vacuum_stall_detectedand the card's anomaly reporting, but no picture and noeufy_vacuum_stall_captured. "Notification but no photo" means the capture, not the detection. - No picture is a valid outcome. If the optional imaging library is missing, or the map render data is unusable, the capture is skipped silently rather than raising an error โ so treat this automation as best-effort, not as a guaranteed companion to every stall.
- One file per vacuum per map, overwritten each time. The path is stable enough to hard-code, but there is no history โ send or copy the image when the event arrives rather than reading it later.
- A stall fabricated by the maintainer-only
dev_inject_stallservice produces a capture that looks exactly like a real one. That service is unsupported and should not be running on a real install.