Skip to content

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_capturetrace_storetrace_segmentationtrace_review pipeline, room-boundary derivation, and image-segment suggestion — was retired, and MappingManager and the learned bounding-box store room_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 by scripts/update_test_docs.py on 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), and rasterize_primitives — the composer rasteriser that turns rect/circle/polygon shapes into a mask (fill + ordered subtract ops) 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 _RoomConfidenceState machine (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 firing eufy_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 via async_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_segments rasterises composer primitives into room polygons (replace-all; refuses with no_custom_backdrop until a backdrop image exists), and a custom room link survives a re-save. set_segmentation_mode is 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) — the custom_layouts collection lifecycle: a legacy single custom_segments store 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 reserved dock spot 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, the upload_map_image art_scope variant routing (custom_<id>_home_art / _room_<rid> onto home_art/rooms, never the backdrop), the resolve_furnished_render projection, the delete_custom_layout art 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_rendered clamp+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)²), and build_map_source_result's presence gate (absent-with-reason vs populated, with extra overlay 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 #136 version guard, presence gate, extraction, degradation), the Roborock find_mapdata / find_roomlike_collection defensive introspector (duck-typing, cycle-safety, attr denylist), and the candidate collectors (eufy_inmem_candidates, roborock_candidates, image_entity_object) that gather roots from hass.data[domain] / per-entry runtime_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 >> 3 only when the low 3 bits == 7; 0xFF catch-all; scan/wall/outside → 0; the 0x08 obstacle-bit collision a naive shift would mislabel as room 1), roborock_render_data wrapping a decoded raster in the shared eufy_room_pixels_v1 payload (b64 round-trip, flip_y, catch_all_rid, room_names), the ..._from_candidates introspection bridges (BFS to a MapContent, absent-marker on no match), raster_room_bboxes (per-room normalized bbox with flip_y honored so raw top rows land at the rendered bottom — the Ivy finding), and geometry_drift, the on-device decode self-validator overlaying the parser's bboxes against the raster (aligned / IoU / centre-drift, flip detection, only_parser set-diff).
  • map_source_coordinator (CMP-* + LP-*, integration, test_manager_compare_sources.py + test_manager_live_pose.py) — the manager delegators into MapSourceCoordinator: async_compare_map_sources (the verify probe — not_configured / memory_not_configured guards, flags-only when only one source is present, compare_map_data reached when both are, diagnostics breadcrumb passthrough) and the live-pose seam async_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_geom reason when geometry is missing).
  • map_source_coordinator dispatcher + backends (MSD-* / GLM-* / RND-* / RIP-* / LPG-*, integration, test_manager_map_source_refresh.py) — the pre-warm async_refresh_map_state_source routing (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 .storage fallbacks (memory miss / convert-None / present-False), get_live_mapdata_obj (the zone-dispatch object locator across both backends, None on absence/raise), async_get_map_render_data (memory-primary vs .storage vs unknown-format, plus the Roborock roborock_raw_map_v1 format 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_geom read + mtime cache + no-map-data). The _msr parsers 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:

  1. Pure import (Recipe C) — the geometry, engine registry, and the service helpers. No fixtures.
  2. tmp_path filesystem — the tracker file helpers (the dock-drift JSONL) get an isolated config dir. The hass is a MagicMock with config.config_dir = str(tmp_path):
    hass = MagicMock(); hass.config.config_dir = str(tmp_path)
    tracker = MappingTracker(hass)
    
  3. numpy/scipy arrays — the segment_primitives mask tests build real arrays. numpy ships transitively via HA core; scipy and Pillow are explicit test deps (see requirements_test.txt). Scipy-only paths still guard with pytest.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 SegmentationResultnot 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_image tuning-override / light-assist wiring (the tuning dict + assist_image_path block 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 (the OSError delete 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 cover capabilities-read / dock-drift JSONL I/O except-blocks, malformed-line resilience, and trivial early-return guards. The transition-room skip in _detect_current_room is a redundant defensive short-circuit (its normal path is covered by test_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 real os.stat (monkeypatched in the dispatch tests), the except OSError stat-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

  1. A new geometry/engine behavior? Pure unit test — add a target to the matching file. Use tmp_path if it touches disk.
  2. A scipy/numpy image path? Build real arrays; gate scipy-only code with pytest.importorskip.
  3. A service handler? That's the integration pass — register via async_register_mapping_services with a real hass + tmp_path config dir.
  4. Re-measure across all mapping test files together for the true per-module number.