mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
feat: record vacuum trails under the route that produced them
User-Visible: no Issue: #162
This commit is contained in:
@@ -729,3 +729,80 @@ def test_overlapping_refreshes_leave_one_subscription_teardown_zero():
|
||||
_run_isolated(scenario())
|
||||
finally:
|
||||
trails.async_track_state_change_event = old_track
|
||||
|
||||
|
||||
def test_refresh_watches_every_route_source_not_only_the_root(monkeypatch):
|
||||
"""#162: карты одного робота могут идти через разные камеры."""
|
||||
import asyncio
|
||||
|
||||
tracked = []
|
||||
old_track = trails.async_track_state_change_event
|
||||
trails.async_track_state_change_event = lambda hass, ents, cb: (
|
||||
tracked.append(sorted(ents)) or (lambda: None))
|
||||
try:
|
||||
markers = [{
|
||||
"id": "m1", "binding": "entity:vacuum.x50", "space": "floor1",
|
||||
"vacuum": {
|
||||
"source": "camera.floor1",
|
||||
"map_routes": [
|
||||
{"id": "vr1", "source": "camera.floor1", "map_id": "a", "space": "floor1"},
|
||||
{"id": "vr2", "source": "camera.floor2", "map_id": "b", "space": "floor2"},
|
||||
],
|
||||
},
|
||||
}]
|
||||
|
||||
class CS:
|
||||
async def async_load(self):
|
||||
return {"config": {"markers": markers}}
|
||||
|
||||
class RT:
|
||||
config_store = CS()
|
||||
|
||||
hass = Hass({
|
||||
"vacuum.x50": S("docked", {}),
|
||||
"camera.floor1": S("idle", {}),
|
||||
"camera.floor2": S("idle", {}),
|
||||
})
|
||||
rec = trails.TrailRecorder(hass, RT())
|
||||
_run_isolated(rec.async_refresh())
|
||||
assert sorted(rec.pairs) == ["camera.floor1", "camera.floor2"]
|
||||
assert rec.pairs["camera.floor2"] == [("m1", "vacuum.x50")]
|
||||
assert tracked == [["camera.floor1", "camera.floor2", "vacuum.x50"]]
|
||||
assert [r["id"] for r in rec.routes_by_marker["m1"]] == ["vr1", "vr2"]
|
||||
finally:
|
||||
trails.async_track_state_change_event = old_track
|
||||
|
||||
|
||||
def test_sample_files_the_point_under_its_route():
|
||||
states = {
|
||||
"vacuum.x50": S("cleaning", {}),
|
||||
"camera.floor2": S("idle", {"vacuum_position": {"x": 5, "y": 6}, "map_name": "b"}),
|
||||
}
|
||||
rec = trails.TrailRecorder(Hass(states), None)
|
||||
rec.pairs = {"camera.floor2": [("m1", "vacuum.x50")]}
|
||||
rec.routes_by_marker = {"m1": [
|
||||
{"id": "vr1", "source": "camera.floor1", "map_id": "b", "space": "floor1"},
|
||||
{"id": "vr2", "source": "camera.floor2", "map_id": "b", "space": "floor2"},
|
||||
]}
|
||||
rec._on_state(E("camera.floor2"))
|
||||
run = rec.book.data["m1"]["current"]
|
||||
assert run["route_id"] == "vr2", "источник, а не только id карты, выбирает маршрут"
|
||||
assert run["source"] == "camera.floor2"
|
||||
assert run["points"] == [[5.0, 6.0]]
|
||||
|
||||
|
||||
def test_sample_without_a_matching_route_records_legacy_shaped_run():
|
||||
states = {
|
||||
"vacuum.x50": S("cleaning", {}),
|
||||
"camera.map": S("idle", {"vacuum_position": {"x": 1, "y": 2}, "map_name": "неизвестная"}),
|
||||
}
|
||||
rec = trails.TrailRecorder(Hass(states), None)
|
||||
rec.pairs = {"camera.map": [("m1", "vacuum.x50")]}
|
||||
rec.routes_by_marker = {"m1": [
|
||||
{"id": "vr1", "source": "camera.map", "map_id": "b", "space": "floor1"},
|
||||
]}
|
||||
rec._on_state(E("camera.map"))
|
||||
run = rec.book.data["m1"]["current"]
|
||||
assert "route_id" not in run, "чужой маршрут прогону не приписывается"
|
||||
assert run["map_id"] == "неизвестная"
|
||||
assert run["points"] == [[1.0, 2.0]]
|
||||
|
||||
@@ -143,3 +143,74 @@ def test_junk_store_data_tolerated():
|
||||
assert b.data == {}
|
||||
b.on_point("m", "0", 1, 1, 1.0)
|
||||
assert b.data["m"]["current"]["points"] == [[1, 1]]
|
||||
|
||||
|
||||
# --- #162: маршруты карт и пространств ---------------------------------------
|
||||
|
||||
def test_route_change_starts_a_new_run_even_on_the_same_map_id():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "default", 1.0, 1.0, 100.0, route_id="vr_a", source="camera.a")
|
||||
b.on_point("m", "default", 2.0, 2.0, 101.0, route_id="vr_b", source="camera.b")
|
||||
rec = b.data["m"]
|
||||
assert rec["previous"]["route_id"] == "vr_a"
|
||||
assert rec["current"]["route_id"] == "vr_b"
|
||||
assert rec["current"]["source"] == "camera.b"
|
||||
assert rec["current"]["points"] == [[2.0, 2.0]]
|
||||
|
||||
|
||||
def test_same_route_keeps_one_run():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "default", 1.0, 1.0, 100.0, route_id="vr_a", source="camera.a")
|
||||
b.on_point("m", "default", 2.0, 2.0, 101.0, route_id="vr_a", source="camera.a")
|
||||
assert "previous" not in b.data["m"]
|
||||
assert b.data["m"]["current"]["points"] == [[1.0, 1.0], [2.0, 2.0]]
|
||||
|
||||
|
||||
def test_run_without_route_stays_legacy_shaped():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "0", 1.0, 2.0, 100.0)
|
||||
run = b.data["m"]["current"]
|
||||
assert "route_id" not in run and "source" not in run
|
||||
assert set(run) == {"map_id", "started", "ended", "points"}
|
||||
|
||||
|
||||
def test_legacy_run_resumes_by_map_id_as_before():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "0", 1.0, 2.0, 100.0)
|
||||
b.end_run("m", 200.0)
|
||||
assert b.on_point("m", "0", 3.0, 4.0, 300.0)
|
||||
assert "previous" not in b.data["m"]
|
||||
assert b.data["m"]["current"]["points"] == [[1.0, 2.0], [3.0, 4.0]]
|
||||
|
||||
|
||||
def test_route_run_does_not_resume_into_another_route():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "default", 1.0, 2.0, 100.0, route_id="vr_a")
|
||||
b.end_run("m", 200.0)
|
||||
b.on_point("m", "default", 3.0, 4.0, 300.0, route_id="vr_b")
|
||||
assert b.data["m"]["previous"]["route_id"] == "vr_a"
|
||||
assert b.data["m"]["current"]["route_id"] == "vr_b"
|
||||
|
||||
|
||||
def test_drop_unknown_routes_touches_only_runs_that_name_a_route():
|
||||
b = TrailBook()
|
||||
b.on_point("m", "m1", 1.0, 1.0, 100.0, route_id="vr_gone")
|
||||
b.on_point("m", "m2", 2.0, 2.0, 101.0, route_id="vr_live")
|
||||
assert b.drop_unknown_routes("m", {"vr_live"}) is True
|
||||
assert "previous" not in b.data["m"]
|
||||
assert b.data["m"]["current"]["route_id"] == "vr_live"
|
||||
assert b.drop_unknown_routes("m", {"vr_live"}) is False
|
||||
legacy = TrailBook()
|
||||
legacy.on_point("m", "m1", 1.0, 1.0, 100.0)
|
||||
assert legacy.drop_unknown_routes("m", set()) is False, "легаси-прогон не трогаем"
|
||||
assert legacy.data["m"]["current"]["points"] == [[1.0, 1.0]]
|
||||
assert TrailBook().drop_unknown_routes("нет такого", {"vr"}) is False
|
||||
|
||||
|
||||
def test_same_run_identity_contract():
|
||||
same = ns["same_run_identity"]
|
||||
assert same({"route_id": "vr_a", "map_id": "x"}, "vr_a", "y") is True
|
||||
assert same({"route_id": "vr_a", "map_id": "x"}, "vr_b", "x") is False
|
||||
assert same({"map_id": "x"}, "vr_a", "x") is True, "легаси-прогон опознаётся картой"
|
||||
assert same({"map_id": "x"}, "", "y") is False
|
||||
assert same(None, "vr", "x") is False
|
||||
|
||||
Reference in New Issue
Block a user