07 — Mapping — Subsystem Test Map¶
The mapping subsystem turns map images into room data: an image-segmentation
stack (segment_primitives, segmenter_engines), a room tracker (tracker)
that fires room_completed off the device's native current-room, and the
service orchestrator (mapping_services).
A second authoring path lets a human draw rooms directly: segment_primitives
rasterises composer shapes into polygons, and mapping_services holds many named
custom layouts per map alongside the CV store, selected by a segmentation_mode
pointer flip.
A third path skips authoring entirely: the map_state_source reader
(map_source, map_source_runtime, map_source_coordinator) normalizes the
provider's OWN segmentation + live pose into VA-owned room data (bbox/name, dock/robot
anchors, area, current room, overlay layers), so rooms are auto-derived from the device's
authoritative map rather than learned from drifting samples or hand-drawn.
The image primitives are near-fully covered; the tracker and the service
orchestrator have both their pure helpers (unit) and hass-bound bodies
(integration) covered; and the real detect_room_segments CV pipeline runs
end to end against a synthetic image.
Mapping split. The run-derived inference lineage — the
trace_capture→trace_store→trace_segmentation→trace_reviewpipeline, room-boundary derivation, and image-segment suggestion — was retired, andMappingManagerand the learned bounding-box storeroom_bounds.py(RoomBoundsStore) were removed; room tracking now reads the device's native current-room. The module table and coverage counts below are re-stamped byscripts/update_test_docs.pyon the next digest and may lag this structural change.
Source: custom_components/eufy_vacuum/mapping/
Architecture reference: docs/dev/11-mapping-system.md
Coverage map¶
| Source module | Stmts | Cov | Test file | Layer |
|---|---|---|---|---|
boundary.py |
15 | 90% | via zone_membership in tests/integration/test_mapping_services.py (only point_in_polygon survives) |
integration (pure geometry) |
segment_primitives.py |
280 | 93% | tests/unit/test_mapping_segment_primitives.py |
unit (pure + numpy/scipy) |
segmenter_engines.py |
132 | 100% | tests/unit/test_mapping_segmenter_engines.py |
unit (pure) |
tracker.py |
231 | 85% | test_mapping_tracker.py + test_mapping_tracker_events.py |
unit + integration |
mapping_services.py |
1154 | 85% | test_mapping_services_helpers.py + test_mapping_services.py + test_mapping_services_handlers.py |
unit + integration |
map_source.py |
422 | 94% | tests/unit/test_map_source.py |
unit (pure) |
map_source_runtime.py |
509 | 89% | tests/unit/test_map_source_runtime.py + tests/unit/test_map_source_collectors.py |
unit (pure) |
map_source_coordinator.py |
279 | 90% | test_manager_compare_sources.py + test_manager_live_pose.py + test_manager_map_source_refresh.py |
integration |
roborock_raw_map.py |
142 | 90% | tests/unit/test_roborock_raw_map.py |
unit (pure) |
What's tested¶
The image stack¶
segment_primitives(SP) — pure geometry (rdp,polygon_area,compactness,aspect_ratio, …), the numpy/scipy mask primitives (mask_to_polygon,mask_iou, transforms,mask_edge_band,estimate_alignment,normalized_color_features), andrasterize_primitives— the composer rasteriser that turns rect/circle/polygon shapes into a mask (fill + orderedsubtractops) for custom segments.segmenter_engines(SE) — the engine registry, tuning validation, and the no-image/noop unavailable paths. The CV pipeline body (detect_room_segments) is exercised only through its failure paths.
The tracker¶
tracker(MT, unit) — the pure_RoomConfidenceStatemachine (reset_room, movement increment / saturate,reset_job).tracker(MTE, integration) — the job lifecycle: register/unregister,start_job/end_job(seed / reset the confidence state), pause/resume sampling,_handle_position_update(per-tick confidence advance when a job is active, else the passive dock-drift JSONL log), the native current-room resolution (_detect_current_room/_get_raw_position), and the confidence-threshold room-exit firingeufy_vacuum_room_completed.
The service orchestrator¶
mapping_services(MS, unit) —_apply_segment_adjustments,_build_segments_response, and the module-local geometry helpers.MSH, integration — the service handlers viaasync_register_mapping_services:get_map_segments,adjust_map_segment,set_segment_room_link(set/clear/1:1),set_companion_anchor,delete_map_image.
Custom & multi-layout segmentation¶
The human-authored alternative to CV, exercised end to end through the services.
- Authoring (
test_mapping_services.py) —set_custom_segmentsrasterises composer primitives into room polygons (replace-all; refuses withno_custom_backdropuntil a backdrop image exists), and a custom room link survives a re-save.set_segmentation_modeis a pure pointer flip that never re-runs the segmenter and losslessly switches the served store (cv ↔ custom). - Named layouts (
LAYOUT-*,test_mapping_services.py) — thecustom_layoutscollection lifecycle: a legacy singlecustom_segmentsstore migrates into one default layout (custom-resolved links/anchors move onto the layout, CV's stay on the map bucket); create / rename / set-active / delete (create flips to custom and activates, delete-active reassigns, delete-last flips back to CV); and set-active with zero layouts auto-creates one. - Per-layout isolation — the same segment id may link to different rooms on
two layouts (
test_per_layout_segment_isolation), and companion anchors including the reserveddockspot are per-layout (LAYOUT-6) — neither bleeds across. - Furnished render (
FURN-*,test_mapping_furnished_render.py) — the per-layout furnished-art contract end to end:set_furnished_art_placement(home/room scope, 4dp round-trip, scale clamp to[0.05, 20], clear-on-all-null, missing-room-id guard),set_furnished_render_mode(layout vs per-room, blank room_id → layout level),set_room_viewport, theupload_map_imageart_scopevariant routing (custom_<id>_home_art/_room_<rid>ontohome_art/rooms, never the backdrop), theresolve_furnished_renderprojection, thedelete_custom_layoutart sweep, and per-layout isolation — all through the real service registry.
The map_state_source reader¶
The brand-agnostic read of the provider's own segmentation + live pose into
VA-owned room data (architecture: docs/dev/map-state-source.md).
The pure extraction/normalization is unit-tested without Home Assistant; the
manager-facing seams (delegators into MapSourceCoordinator) are integration-tested.
map_source(MS-*, unit,test_map_source.py) — the pure core:rooms_from_room_pixels(per-room bbox+name, Y-flip, catch-all rid 32 filtered, malformed/short buffers degrade to[]),normalize_renderedclamp+flip,anchors_from_storage(dock/robot normalize, non-numeric coords skipped),rooms_from_parsed_map(Roborock parser path, flagged approximate), per-room area (pixel_count × (res_cm/100)²), andbuild_map_source_result's presence gate (absent-with-reason vs populated, withextraoverlay layers merged in).map_source_runtime(MSR-*+MSC-*, unit,test_map_source_runtime.py+test_map_source_collectors.py) — the HA-aware glue tested with injected plain data:eufy_result_from_store(the#136version guard, presence gate, extraction, degradation), the Roborockfind_mapdata/find_roomlike_collectiondefensive introspector (duck-typing, cycle-safety, attr denylist), and the candidate collectors (eufy_inmem_candidates,roborock_candidates,image_entity_object) that gather roots fromhass.data[domain]/ per-entryruntime_data/ the image entity, each degrading cleanly when a source is absent.roborock_raw_map(RRD-*, unit,test_roborock_raw_map.py) — the pure Roborock v1 raw-map segment decoder, no HA/device:decode_roborock_v1_segments(a well-formed IMAGE block → resolved room-id raster + dims + ids; no-IMAGE / empty / truncated / garbage / dims-exceed-data →None, never raises),resolve_rid's per-pixel encoding (byte >> 3only when the low 3 bits == 7;0xFFcatch-all; scan/wall/outside → 0; the0x08obstacle-bit collision a naive shift would mislabel as room 1),roborock_render_datawrapping a decoded raster in the sharedeufy_room_pixels_v1payload (b64 round-trip,flip_y,catch_all_rid, room_names), the..._from_candidatesintrospection bridges (BFS to aMapContent, absent-marker on no match),raster_room_bboxes(per-room normalized bbox withflip_yhonored so raw top rows land at the rendered bottom — the Ivy finding), andgeometry_drift, the on-device decode self-validator overlaying the parser's bboxes against the raster (aligned / IoU / centre-drift, flip detection,only_parserset-diff).map_source_coordinator(CMP-*+LP-*, integration,test_manager_compare_sources.py+test_manager_live_pose.py) — the manager delegators intoMapSourceCoordinator:async_compare_map_sources(the verify probe — not_configured / memory_not_configured guards, flags-only when only one source is present,compare_map_datareached when both are, diagnostics breadcrumb passthrough) and the live-pose seamasync_get_map_live_pose/_apply_inmem_pose_to_result(robot/dock/heading/trail overlaid on present pose, base overlays preserved when pose is absent or its read raises,no_geomreason when geometry is missing).map_source_coordinatordispatcher + backends (MSD-*/GLM-*/RND-*/RIP-*/LPG-*, integration,test_manager_map_source_refresh.py) — the pre-warmasync_refresh_map_state_sourcerouting (storage / memory-primary / memory / unknown-backend / error-degrade, plus the live-image presence gate), the storage backend's mtime-cache hit + version-mismatch warn + no-device marker, the memory-primary scan with its content-version cache and the three.storagefallbacks (memory miss / convert-None / present-False),get_live_mapdata_obj(the zone-dispatch object locator across both backends,Noneon absence/raise),async_get_map_render_data(memory-primary vs.storagevs unknown-format, plus the Roborockroborock_raw_map_v1format dispatching to the raw-map render bridge with room names from the manager's stored rooms), and the live-pose read layer (_read_inmem_pose,_load_live_pose_geomread + mtime cache + no-map-data). The_msrparsers stay unit-tested; these pin the coordinator's own dispatch / cache / fallback branching, each fallback asserted via an assert-not-called guard so the intended branch is proven, not just non-crashing.
How it's tested¶
Four patterns, same as elsewhere in the suite:
- Pure import (Recipe C) — the geometry, engine registry, and the service helpers. No fixtures.
tmp_pathfilesystem — thetrackerfile helpers (the dock-drift JSONL) get an isolated config dir. The hass is aMagicMockwithconfig.config_dir = str(tmp_path):- numpy/scipy arrays — the
segment_primitivesmask tests build real arrays. numpy ships transitively via HA core; scipy and Pillow are explicit test deps (seerequirements_test.txt). Scipy-only paths still guard withpytest.importorskip("scipy.ndimage")so the file degrades gracefully if the stack is ever absent.
Image/CV pipeline (IMG, integration — brand-agnostic)¶
The framework's segment plumbing (analyze_map_image, get_map_segments) is
tested against a registered fake segmenter engine that returns a canned SegmentationResult
— not a concrete brand engine. This proves the framework drives any adapter's
CV pipeline without coupling to Eufy's. The real Eufy CV segmentor
(detect_room_segments, HSV masks, EufyCVSegmenter) is tested solo in
tests/adapters/eufy/test_segmentor.py (prefix ECV), where brand code
belongs. Framework-level tests stay engine-agnostic; brand CV tests live with the brand code.
Non-segment service handlers (MSV, integration)¶
The surviving read/display handlers — delete_map_image, set_live_map_rotation,
set_map_overlay_visibility, and get_map_render_data — exercised through
async_register_mapping_services. (The room-bounds-snapshot, boundary-trace,
dock, trace-capture, mapping-package, and trace-review handlers were retired with
the mapping split.)
Known gaps¶
Every module sits at 92–100%. The genuinely-untested behaviours were closed in
the 2026-06 gap pass (legacy-calibration migration, dock optional-field writes,
assist-image selection, delete-variant retain + already-gone paths, the
adjust-segment vertex-merge / reset-to-zero branches, the short/similar-segment
merge branches, the RDP fallback / non-unit zoom / alignment-recovery primitives,
the degraded _reshape diagnostics, and the sliver area guard). What remains is
uncovered on purpose,
per the ~90% held-ceiling policy — defensive guards, not coverage debt:
mapping_services.py(91%) — the_handle_analyze_map_imagetuning-override / light-assist wiring (thetuningdict +assist_image_pathblock inside that handler) is deferred: a robust test fights the filesystem-probing image store plus phac's shared config dir — not worth a fragile test. The rest is defensive (theOSErrordelete branch in_handle_delete_map_image, non-dict guards, the tracker-absent else, and the schema-unreachable coerce guards in_build_segments_response).tracker.py(93%) —# pragma: no covercapabilities-read / dock-drift JSONL I/O except-blocks, malformed-line resilience, and trivial early-return guards. The transition-room skip in_detect_current_roomis a redundant defensive short-circuit (its normal path is covered bytest_room_completed_event), deliberately left.segment_primitives.py(94%) — empty-mask divide-by-zero returns, optional-dependency guards, and unreachable malformed-edge artifacts.map_source_coordinator.py(95%, up from 45%) —_stat_mtime's realos.stat(monkeypatched in the dispatch tests), theexcept OSErrorstat-failure guard, the memory-backend diagnostics-log branch, and a few->partials in the candidate loop / render / geom-cache edges. Defensive guards + real-IO; the dispatch, cache, and fallback behaviour is fully covered.
Extending¶
- A new geometry/engine behavior? Pure unit test — add a target to
the matching file. Use
tmp_pathif it touches disk. - A scipy/numpy image path? Build real arrays; gate scipy-only code with
pytest.importorskip. - A service handler? That's the integration pass — register via
async_register_mapping_serviceswith a real hass +tmp_pathconfig dir. - Re-measure across all mapping test files together for the true per-module number.