Skip to content

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_job and cancellation succeeds
  • A paused job times out and is auto-cancelled
  • A path blocker is configured with cancel_and_event and triggers a cancellation
  • You call eufy_vacuum.finalize_learning_job directly

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 queue
  • source: "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 queue
  • source: "bounds_exit_early" — fired when a confident coordinate signal advances to the next room before the timing threshold is reached
  • source: "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:

  1. The integration is already in awaiting_bounds_exit state 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)
  2. 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. The get_job_progress_snapshot response (and each eufy_vacuum_job_progress_tick) carries running_long (bool), running_long_room_id (int | null), and running_long_ratio (float | null). It is set when the current room has run between running_long_ratio (default 1.5×, from the adapter's anomaly block) and stall_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 of running_long or 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_skipped is 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(', ') }}