From 9899d628ac2404e278c50bf9ec4d2cbf1e70f746 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 3 Sep 2026 19:18:06 +0300 Subject: [PATCH] feat: drop cross-space vacuum map routes from a single-space export User-Visible: no Issue: #162 --- custom_components/houseplan/import_export.py | 14 +++++ tests_backend/test_ha_import_export.py | 55 ++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/custom_components/houseplan/import_export.py b/custom_components/houseplan/import_export.py index 7d42d7bf..87df662e 100644 --- a/custom_components/houseplan/import_export.py +++ b/custom_components/houseplan/import_export.py @@ -576,6 +576,20 @@ def create_export( and ref[len("marker:"):] not in selected_ids: marker.pop("value_source", None) dropped_marker_links += 1 + # #162: a robot map routed into ANOTHER space cannot travel + # with a single-space export — its target would not exist on + # import, and a route with a dead space resolves to nothing. + # Same neutralisation contract as the marker refs above: drop + # it and count it, so the preview states the loss up front. + vacuum = marker.get("vacuum") + routes = vacuum.get("map_routes") if isinstance(vacuum, dict) else None + if isinstance(routes, list): + kept_routes = [ + route for route in routes + if isinstance(route, dict) and str(route.get("space")) == str(space_id) + ] + dropped_marker_links += len(routes) - len(kept_routes) + vacuum["map_routes"] = kept_routes or None config = { "spaces": [_json_copy(space)], "markers": _json_copy(selected_markers), diff --git a/tests_backend/test_ha_import_export.py b/tests_backend/test_ha_import_export.py index d93caf68..38729eda 100644 --- a/tests_backend/test_ha_import_export.py +++ b/tests_backend/test_ha_import_export.py @@ -2488,3 +2488,58 @@ def test_apply_schema_accepts_only_an_opaque_preview_token() -> None: "expected_config_rev": 1, "expected_layout_rev": 1, }) + + +def test_issue_162_space_export_drops_map_routes_of_other_spaces(tmp_path: Path) -> None: + """Маршрут в чужое пространство нельзя увезти: его цели в экспорте нет.""" + config = _config() + config["markers"][0]["vacuum"] = { + "source": "camera.robot", + "map_routes": [ + {"id": "vr1", "source": "camera.robot", "map_id": "m1", + "space": "ground", "calibration": [1, 0, 0, 0, 1, 0]}, + {"id": "vr2", "source": "camera.robot", "map_id": "m2", + "space": "other", "calibration": [2, 0, 0, 0, 2, 0]}, + ], + } + document, _ = create_export( + SimpleNamespace(instance_id="instance-a"), {"config": config}, + {"layout": {"lamp": {"s": "ground", "x": 0.5, "y": 0.5}}}, + kind="space", space_id="ground", card_version="1.61.0", config_root=tmp_path, + ) + vacuum = document["payload"]["config"]["markers"][0]["vacuum"] + assert [route["id"] for route in vacuum["map_routes"]] == ["vr1"] + assert vacuum["source"] == "camera.robot", "корневой источник остаётся" + assert document["transfer"]["dropped_marker_links"] == 1 + + +def test_issue_162_full_export_round_trips_every_map_route(tmp_path: Path) -> None: + config = _config() + routes = [ + {"id": "vr1", "source": "camera.robot", "map_id": "m1", + "space": "ground", "calibration": [1, 0, 0, 0, 1, 0]}, + {"id": "vr2", "source": "camera.other", "map_id": "m2", + "space": "other", "calibration": [2, 0, 0, 0, 2, 0]}, + ] + config["markers"][0]["vacuum"] = {"source": "camera.robot", "map_routes": routes} + document, _ = create_export( + SimpleNamespace(instance_id="instance-a"), {"config": config}, + {"layout": {}}, kind="full", card_version="1.61.0", config_root=tmp_path, + ) + assert document["payload"]["config"]["markers"][0]["vacuum"]["map_routes"] == routes + assert document["transfer"]["dropped_marker_links"] == 0 + + +def test_issue_162_space_export_keeps_legacy_calibration_untouched(tmp_path: Path) -> None: + config = _config() + config["markers"][0]["vacuum"] = { + "source": "camera.robot", "calibration": {"m1": [1, 0, 0, 0, 1, 0]}, + } + document, _ = create_export( + SimpleNamespace(instance_id="instance-a"), {"config": config}, + {"layout": {"lamp": {"s": "ground", "x": 0.5, "y": 0.5}}}, + kind="space", space_id="ground", card_version="1.61.0", config_root=tmp_path, + ) + vacuum = document["payload"]["config"]["markers"][0]["vacuum"] + assert vacuum == {"source": "camera.robot", "calibration": {"m1": [1, 0, 0, 0, 1, 0]}} + assert document["transfer"]["dropped_marker_links"] == 0