02 — HA Events Reference¶
The integration fires events on the Home Assistant event bus at specific points in a cleaning job's lifecycle. You can listen to any of these events in an automation using the event trigger platform. All payloads are plain dictionaries — no custom objects to unwrap.
eufy_vacuum_job_finished¶
When it fires¶
Fires after a cleaning job has been finalized. This covers every path to job completion:
- The robot finishes normally and returns to the dock (auto-finalization via the lifecycle listener in
listeners/lifecycle.py) - You call
eufy_vacuum.cancel_active_joband cancellation succeeds - A paused job times out and is auto-cancelled
- A path blocker is configured with
cancel_and_eventand triggers a cancellation - You call
eufy_vacuum.finalize_learning_jobdirectly
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum, e.g. vacuum.alfred |
map_id |
str |
Map ID the job ran on, as a string |
job_id |
str \| null |
Internal job identifier assigned at job start |
status |
str |
Outcome of the job — completed, cancelled, failed, or interrupted |
reason_detail |
str \| null |
Human-readable lifecycle message, e.g. "pause_timeout". On the auto-finalize paths (lifecycle/pause-timeout/path-blocker) it falls back to the status string when no lifecycle message is present, so a clean completion reports "completed" rather than null there. Only the eufy_vacuum.finalize_learning_job service path uses the lifecycle message alone and yields null for clean completions. |
used_for_learning |
bool \| null |
Whether this job was included in the learning system's stats; null when learning is not active |
finalized_at |
str \| null |
ISO 8601 timestamp of finalization |
room_count |
int \| null |
Number of rooms that were queued in the job |
duration_minutes |
float \| null |
Wall-clock duration of the job in minutes, net of pauses and recharges. Same value used by the post-job summary banner in the panel. Present only on the auto-finalize paths (lifecycle/pause-timeout/path-blocker) — omitted from the payload when the job is finalized via the eufy_vacuum.finalize_learning_job service. |
actual_cleaning_minutes |
float \| null |
Time the robot actually spent cleaning, derived from the Returning state transition. Excludes the return-to-dock trip. Only set for single-room jobs; null for multi-room jobs (where it would not be meaningful). Present only on the auto-finalize paths (lifecycle/pause-timeout/path-blocker) — omitted from the payload when the job is finalized via the eufy_vacuum.finalize_learning_job service. |
job_path |
str \| null |
Filesystem path to the saved completed-job JSON file, or null if learning is not enabled |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_job_finished
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Send a push notification with the outcome. Check trigger.event.data.status to vary the message between completed and cancelled jobs. duration_minutes and room_count are useful for one-line summaries without having to read the saved job file.
trigger:
- platform: event
event_type: eufy_vacuum_job_finished
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: notify.mobile_app_your_phone
data:
title: "Alfred finished"
message: >
Job {{ trigger.event.data.status }} —
{{ trigger.event.data.room_count }} room(s).
eufy_vacuum_room_started¶
When it fires¶
Fires when the integration determines the robot has begun cleaning a new room. There are several firing sites, distinguished by source:
source: "job_start"— fired immediately after a job is started, for the first room in the queuesource: "counter_plateau"— fired when the live cleaned-area counter plateaus, signalling the robot has moved on to the next room (the primary live-rollover path for Eufy)source: "timing_rollover"— fired when the previous room's timing threshold is exceeded and the integration advances to the next room in the queuesource: "bounds_exit_early"— fired when a confident coordinate signal advances to the next room before the timing threshold is reachedsource: "native_signal"— the Roborock native current-room rollover path: the device reports the live room directly, suppressing the counter/timing rollover for that job
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on |
job_id |
str |
Job identifier |
room_id |
str |
Room ID as a string |
room_name |
str |
Human-readable room name |
started_at |
str \| null |
ISO 8601 timestamp of when the room started |
source |
str |
One of "job_start", "counter_plateau", "timing_rollover", "bounds_exit_early", or "native_signal" |
completed_room_ids |
list[int] |
List of room IDs already completed in this job |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_room_started
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Log each room start to a helper or push a live update. You can filter to a specific room by adding room_id to event_data.
trigger:
- platform: event
event_type: eufy_vacuum_room_started
event_data:
vacuum_entity_id: "vacuum.alfred"
room_id: "3"
action:
- service: notify.mobile_app_your_phone
data:
message: "Alfred started cleaning {{ trigger.event.data.room_name }}"
eufy_vacuum_room_finished¶
When it fires¶
Fires when the integration marks a room complete and advances to the next one. This is the same _maybe_roll_current_room_by_timing path that also fires eufy_vacuum_room_started for the following room. The rollover happens because the live cleaned-area counter plateaued (source: "counter_plateau", the primary live path for Eufy), because the room's timing threshold was exceeded (source: "timing_rollover"), or because the device reported the live room directly via the Roborock native current-room rollover (source: "native_signal", which suppresses the counter/timing rollover for that job). A legacy source: "bounds_exit_early" value still exists in the finalize code but is dormant — the coordinate-based fast-rollover producer that once set it (MappingTracker._signal_fast_rollover) was removed with the mapping split, so it no longer fires in current builds.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on |
job_id |
str \| null |
Job identifier |
room_id |
str |
ID of the room that was just completed |
room_name |
str |
Human-readable name of the completed room |
completed_at |
str |
ISO 8601 timestamp of completion |
source |
str |
One of "counter_plateau", "timing_rollover", "bounds_exit_early", or "native_signal" |
actual_duration_minutes |
float |
How long the robot spent in the room, in minutes, rounded to 2 decimal places |
confidence |
float \| null |
Confidence score from the timing estimate, or null if no estimate was available |
completed_room_ids |
list[int] |
Full list of room IDs now completed in this job (includes the room just finished) |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_room_finished
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Build a running log of actual cleaning durations per room to compare against learning estimates.
trigger:
- platform: event
event_type: eufy_vacuum_room_finished
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: logbook.log
data:
name: "Alfred room done"
message: >
{{ trigger.event.data.room_name }}
in {{ trigger.event.data.actual_duration_minutes }} min
eufy_vacuum_run_incomplete¶
When it fires¶
Fires from finalize_learning_job (in learning/services.py) after a job that ended with status cancelled, failed, or interrupted — but only when at least one queued room was not cleaned. If the job completed normally, or if all queued rooms were cleaned before the job ended, this event does not fire.
The integration derives missed rooms by computing the difference between the rooms that were queued at job start and the rooms recorded as completed in active_job_state.completed_room_ids.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
job_id |
str |
Job identifier |
outcome_status |
str |
Why the job ended — cancelled, failed, or interrupted |
missed_room_ids |
list[int] |
IDs of rooms that were queued but not cleaned |
missed_rooms |
list[dict] |
One entry per missed room, each with room_id (int) and name (str) |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_run_incomplete
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Automatically re-queue missed rooms using the eufy_vacuum.retry_missed_rooms service. This is the canonical pattern documented in the source.
trigger:
- platform: event
event_type: eufy_vacuum_run_incomplete
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: eufy_vacuum.retry_missed_rooms
data:
vacuum_entity_id: "{{ trigger.event.data.vacuum_entity_id }}"
You can also gate on outcome to only retry cancelled jobs, not failed ones:
condition:
- condition: template
value_template: "{{ trigger.event.data.outcome_status == 'cancelled' }}"
eufy_vacuum_external_run_pending¶
When it fires¶
Fires when an app-started (external) clean finishes and is captured as a
pending review record under learning/<slug>/external_jobs/. Subscribe to surface
a notification prompting the user to confirm which rooms it cleaned (the card's
"External Jobs" subtab). See the
external-run ingestion dev doc.
Payload fields¶
| Field | Description |
|---|---|
vacuum_entity_id |
The vacuum that ran. |
map_id |
The map the run cleaned. |
record_path |
Path to the pending record JSON. |
segment_count |
Number of detected cleaning segments. |
detection_ts |
When detection first fired (the pending record id basis). |
eufy_vacuum_room_completed¶
When it fires¶
Fires from the mapping tracker when the device's native current-room signal indicates the robot has left a room — confirmed through a confidence/dwell debounce (CONFIDENCE_THRESHOLD = 0.85 in mapping/tracker.py) — and carries that room's dwell duration. This is distinct from the timing-rollover path that fires eufy_vacuum_room_finished. It requires the adapter to expose the native current-room signal (the active_cleaning_target entity); the earlier coordinate/boundary-box mechanism — and its robot_position_x / robot_position_y requirement — was removed with the mapping split.
Because it is driven by the device's own current-room signal rather than learned timing, it can fire for rooms the learning system has no history for, and it fires independently of whether the room was part of the current queue.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on |
room_id |
str |
ID of the room whose boundary was exited, as a string |
room_name |
str |
Human-readable room name |
confidence |
float |
Coordinate-tracking confidence score for the room exit |
duration_seconds |
float |
How long the robot was inside the room's boundary, in seconds, rounded to 1 decimal place |
entered_at |
str \| null |
ISO 8601 UTC timestamp of when the robot entered the room's boundary, or null if unknown |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_room_completed
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Use as a position-accurate room-exit signal when the interactive map is configured. Pairs well with eufy_vacuum_room_finished for cross-validation — if one fires but not the other, the coordinate or timing model may need review.
eufy_vacuum_job_progress_tick¶
When it fires¶
Fires on a fixed 5-second interval from the job-progress listener (listeners/job_progress.py) for every managed vacuum/map that has an active job. The tick fires only while the active job's status is started or paused — it stops once the job is finalized. On each tick the listener recomputes the job progress snapshot (the same path that can fire eufy_vacuum_stall_detected) and then emits this event so dashboards and automations can refresh on a heartbeat rather than polling a service.
The payload deliberately carries no job state — it is a pull signal. Use it as a trigger to call get_job_progress_snapshot, get_dashboard_snapshot, or another state-inspection service for the current values.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum with the active job |
map_id |
str |
Map ID the active job is running on, as a string |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_job_progress_tick
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Drive a live progress refresh. Trigger on the tick, then call eufy_vacuum.get_job_progress_snapshot (with response_variable) to pull the current room, completed rooms, and completion percentage into a helper or notification.
trigger:
- platform: event
event_type: eufy_vacuum_job_progress_tick
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: eufy_vacuum.get_job_progress_snapshot
data:
vacuum_entity_id: "{{ trigger.event.data.vacuum_entity_id }}"
map_id: "{{ trigger.event.data.map_id }}"
response_variable: progress
eufy_vacuum_stall_detected¶
When it fires¶
Fires from ActiveJobTracker.detect_run_anomalies (in jobs/active_job.py), which get_job_progress_snapshot() invokes on every dashboard poll / progress tick. The event fires when both of the following are true:
- The integration is already in
awaiting_bounds_exitstate for the current room — meaning the room's timing threshold was met but it has not yet rolled over (no counter plateau or native-signal completion has advanced past it) - The robot has been in the room for at least 2× the learned timing threshold for that room (
_STALL_RATIO)
The tracker records which rooms have already triggered this event per job via _stall_notified_room_ids on the active job, so it fires at most once per room per job regardless of how many dashboard polls occur.
This event requires the learning system to have timing data for the room. If no learned threshold exists, the stall check is skipped.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on |
room_id |
int |
ID of the stalled room (integer, not a string) |
room_name |
str |
Human-readable name of the stalled room |
elapsed_minutes |
float |
How long the robot has been in the room, rounded to 1 decimal place |
expected_minutes |
float |
The learned timing threshold for the room, rounded to 1 decimal place |
stall_ratio |
float |
elapsed_minutes / expected_minutes, rounded to 2 decimal places — always >= 2.0 when this event fires |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_stall_detected
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Alert when the robot is stuck or taking unusually long in one room, then decide whether to intervene.
trigger:
- platform: event
event_type: eufy_vacuum_stall_detected
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: notify.mobile_app_your_phone
data:
title: "Alfred may be stuck"
message: >
Stalled in {{ trigger.event.data.room_name }}
({{ trigger.event.data.elapsed_minutes }} min,
expected {{ trigger.event.data.expected_minutes }} min,
ratio {{ trigger.event.data.stall_ratio }}x)
Soft tier —
running_long. Below the 2× stall there is a softer "this room is taking a while" band that does not fire its own event. Theget_job_progress_snapshotresponse (and eacheufy_vacuum_job_progress_tick) carriesrunning_long(bool),running_long_room_id(int | null), andrunning_long_ratio(float | null). It is set when the current room has run betweenrunning_long_ratio(default 1.5×, from the adapter'sanomalyblock) andstall_ratio(default 2.0×) of its learned threshold with no pending counter transition — i.e. genuinely lingering rather than mid-roll. It is disjoint from the stall event by band, so a room is at most one ofrunning_longor stalled at a time. Poll the snapshot to surface it; there is no event-bus trigger for the soft tier.
eufy_vacuum_room_skipped¶
When it fires¶
Fires when the live job queue advances past a queued room that was never cleaned — a non-sequential advance. The integration computes this in ActiveJobTracker.detect_run_anomalies (in jobs/active_job.py), invoked by get_job_progress_snapshot(), as the conservative "skipped" set: any room positioned strictly before the current room in queue order that is not in completed_room_ids.
For Eufy this is almost never observed: Eufy's sequential counter rollover keeps completed_room_ids a contiguous prefix of the queue, so there is no room "left behind" to attribute. The hook exists for position-reliable brands or transition-detection paths that can legitimately jump forward, and to future-proof the queue model. There is no false-positive heuristic — if the skip can't be proven from the queue order, the event does not fire.
The reliable, post-run signal for rooms that ended up uncleaned remains
eufy_vacuum_run_incomplete, derived at finalization.eufy_vacuum_room_skippedis the live, mid-run counterpart and is intentionally conservative.
The integration tracks which rooms have already fired this event per job via _skipped_notified_room_ids on the active job, so it fires at most once per room per job regardless of how many snapshot polls occur.
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on, as a string |
job_id |
str \| null |
Job identifier |
room_id |
int |
ID of the skipped room (integer, not a string) |
room_name |
str |
Human-readable name of the skipped room, or "Room {id}" if unknown |
completed_room_ids |
list[int] |
Room IDs completed in this job at the time of the skip |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_room_skipped
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: Get a live heads-up that a room was passed over mid-run, rather than waiting for the post-run eufy_vacuum_run_incomplete summary. Note this is rare for Eufy — most "missed room" automations should still key off eufy_vacuum_run_incomplete.
trigger:
- platform: event
event_type: eufy_vacuum_room_skipped
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: notify.mobile_app_your_phone
data:
title: "Alfred skipped a room"
message: "Passed over {{ trigger.event.data.room_name }} mid-run"
eufy_vacuum_path_blocked¶
When it fires¶
Fires when a monitored entity (a door sensor, a binary sensor, or any state-tracked entity configured as a path blocker) changes state while a job is active, and that state change affects at least one remaining room in the queue. The integration computes which rooms are directly blocked (the room itself is behind the blocker) and which are indirectly blocked (the only access path to that room passes through a blocked room).
This event fires once per unique blocking signature. If the same combination of blocker entity, state, and affected rooms is already recorded on the active job, the event is suppressed to prevent duplicate firings on rapid state fluctuations.
The event also carries the outcome of whatever path_block_action was configured for the job (event_only, pause_and_event, or cancel_and_event). When the action is cancel_and_event and the cancellation succeeds, eufy_vacuum_job_finished fires first, then eufy_vacuum_path_blocked fires with action_taken: "cancelled".
Payload fields¶
| Field | Type | Description |
|---|---|---|
vacuum_entity_id |
str |
Entity ID of the vacuum |
map_id |
str |
Map ID the job is running on |
job_id |
str \| null |
Job identifier |
trigger_entity_id |
str |
Entity ID of the blocker that changed state |
trigger_entity_state |
str |
New state of the triggering entity |
affected_remaining_room_ids |
list[str] |
IDs (as strings) of all remaining rooms that are now blocked (directly or indirectly) |
affected_remaining_room_names |
list[str] |
Human-readable names of those rooms |
directly_blocked_room_ids |
list[str] |
Rooms whose own access is directly blocked by the triggering entity |
indirectly_blocked_room_ids |
list[str] |
Rooms blocked because their access path passes through a directly blocked room |
remaining_room_ids |
list[str] |
All remaining (unfinished) room IDs in the current queue at the time of the event |
reason_codes |
list[str] |
Deduplicated set of reason codes from the affected rooms' block configurations |
affected_rooms |
list[dict] |
Full detail list of affected rooms, each containing room_id, name, and reason |
requires_attention |
bool |
Always true |
event_scope |
str |
Always "active_job_path_blocked" |
path_block_action |
str |
The configured action — event_only, pause_and_event, or cancel_and_event |
action_taken |
str |
What actually happened — event_only, paused, pause_failed, already_paused, cancelled, or cancel_failed |
action_result |
dict |
Present only when an action was attempted; contains the result from pause_active_job or cancel_active_job |
Example trigger¶
trigger:
- platform: event
event_type: eufy_vacuum_path_blocked
event_data:
vacuum_entity_id: "vacuum.alfred"
Practical use: When using event_only mode (you want manual control), send a notification listing which rooms are now unreachable so you can decide to pause, re-route, or cancel.
trigger:
- platform: event
event_type: eufy_vacuum_path_blocked
event_data:
vacuum_entity_id: "vacuum.alfred"
action:
- service: notify.mobile_app_your_phone
data:
title: "Alfred: path blocked"
message: >
{{ trigger.event.data.trigger_entity_id }} went
{{ trigger.event.data.trigger_entity_state }}.
Affected rooms:
{{ trigger.event.data.affected_remaining_room_names | join(', ') }}