16 — Listeners — Subsystem Test Map¶
The listeners subsystem wires HA state-change events to manager actions:
lifecycle (auto-finalize), job-progress ticks, job-metrics watch maps,
dock-events, path-blockers (mid-job rule re-evaluation), discovery passes,
pause-timeout escalation, and pose sampling (external-run pose time-series
capture for room auto-attribution) — plus the registration/teardown plumbing.
Covered by 80 tests across 7 integration files, with the pose sampler
covered separately by tests/unit/test_pose_sampler.py (14 tests).
Source: custom_components/eufy_vacuum/listeners/
Architecture reference: docs/dev/04-listeners.md
Coverage map¶
| Source module | Stmts | Cov | Test files | Layer |
|---|---|---|---|---|
lifecycle.py |
121 | 95% | test_listeners_state_driven.py, test_listeners_active.py, test_listeners_registration.py |
integration |
path_blockers.py |
103 | 99% | test_listeners_state_driven.py, test_listeners_path_blockers.py |
integration |
job_metrics.py |
88 | 89% | test_listeners_active.py, test_listeners_job_metrics_negative.py |
integration |
dock_events.py |
64 | 90% | test_listeners_active.py, test_listeners_state_driven.py |
integration |
discovery.py |
72 | 99% | test_listeners_timers.py |
integration |
pause_timeout.py |
53 | 84% | test_listeners_timers.py |
integration |
_common.py |
72 | 94% | test_listeners_common.py |
integration |
job_progress.py |
43 | 95% | test_listeners_active.py |
integration |
pose_sampler.py |
137 | 89% | test_pose_sampler.py (unit) |
unit |
What's tested¶
- Registration / teardown (
test_listeners_registration) — each listener family registers its state-change subscriptions and unregisters cleanly. - State-driven actions (
test_listeners_state_driven) — a vacuum state change drives lifecycle auto-finalize and path-blocker re-evaluation. - Active watch maps (
test_listeners_active) — dock-event recording, job-metrics entity watch construction. - Timers (
test_listeners_timers) — the discovery pass and pause-timeout escalation fire on their timer callbacks. - Shared helpers (
test_listeners_common) —_common.pydispatch utilities. - Path-blocker actions (
test_listeners_path_blockers) — a matched mid-job blocker rule drives the pause / cancel / event action and the watcher-build filtering that drops malformed/disabled/non-blocker rules. - Job-metrics negative/guard paths (
test_listeners_job_metrics_negative) — the metrics-change handler's entry-miss / no-state / unavailable / manager-gone and value-parse (ValueError) guards. - Pose sampling (
test_pose_sampler, unit) — external-run pose time-series capture: parked/docked nulling ofcurrent_room/anchorvia MQTTtask_status(with the poserobot_dockedflag as fallback), cadence resolution from the adapter'sroom_attributionblock, and the external-only + live-pose-only gating.
How it's tested¶
The manager / manager_with_services fixtures plus hass.states.async_set
to drive events and hass.async_block_till_done() to flush. The unsubscribe
teardown excepts are best-effort (# pragma: no cover).
Known gaps¶
The uncovered lines are dominated by best-effort teardown/guard branches that
are intentionally left uncovered (# pragma: no cover on the unsubscribe
excepts), early-return guards on malformed/duplicate events, and a few
adapter-config sub-branches:
lifecycle.py(95%) — the remaining misses are early-return guards (no manager / no matched vacuum) and the executor-job wrapper around the tracker'send_job(defensive). The mid-jobmapping_tracker.start_jobbranch and the post-job mop-water amendment registration (gated on a finalized mop job) are now covered bytest_listeners_active.py. The auto-finalize except is# pragma: no cover - best-effort auto-finalize.path_blockers.py(99%) — watcher-build filter branches that drop malformed/disabled/non-blocker rules (73, 76, 78, 81) and event-dedup guards (109, 111, 115, 131, 139: empty/unwatched entity, no new_state, unchanged state, non-dict report, already-paused). All defensive filtering.dock_events.py(90%) — event-dedup guards (69 no new_state, 75 unchanged value, 79 unwatched entity, 83 manager gone). Thelast_dry_startdry-duration capture sub-branch (98-105) is now covered bytest_listeners_state_driven.py; the remaining misses are the defensive dedup guards.job_metrics.py(97%) — the metrics-change handler's value-parse path (94-95 capability-read except, 106/110/113 entry-miss / no-state / unavailable guards, 118/122-123 manager-gone andValueErrorguards). The happy-path record-and-counter-sample below is covered; the misses are guards._common.py(94%) — the broad-except fallbacks inget_adapter_vocab(45-46) andget_adapter_value(72-73), plus the non-dict traversal guard (67). Defensive registry-lookup safety nets.pause_timeout.py(94%) — two guards inside the tick (56 manager gone, 63 skip "unknown" map_id).discovery.py(99%) — only line 150, the body of the periodic safety-net_on_tickcallback (_run_pass()); the timer fires it but no test advances the adapter-configured interval. Trivial.job_progress.py(95%) — only line 68, thecontinuethat skips the "unknown" map_id during a tick.pose_sampler.py(90%) — the misses are the no-task_status/ unreadable-state fallback to the poserobot_dockedflag and the not-attribution / no-live-map vacuum skips (defensive gating). The external-only sampling and the parked/docked nulling happy paths are covered bytests/unit/test_pose_sampler.py.
These are deliberately uncovered at the ~90% meaningful-coverage ceiling: the teardown/guard branches are defensive, and the sequenced-phase / mapping-tracker branches in lifecycle exercise paths that require a full integration boot with a live tracker rather than the per-unit fixtures used here.