From bf832b49f4095b22f0ecf6892950a31d72567416 Mon Sep 17 00:00:00 2001 From: Otto Date: Thu, 18 Jun 2026 18:43:53 +0200 Subject: [PATCH] Filter autopilot actuator candidates --- addon/Dockerfile | 2 +- addon/config.yaml | 2 +- app/main.py | 35 +++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- tests/test_addon_config.py | 2 +- tests/test_future_api.py | 12 +++++++++++- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/addon/Dockerfile b/addon/Dockerfile index a06e05a..51ddfbe 100644 --- a/addon/Dockerfile +++ b/addon/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.13-slim WORKDIR /app -ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.14 +ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.15 RUN python -m pip install --no-cache-dir \ "http://192.168.6.31:3000/Otto/sillyhome-future/archive/${SILLYHOME_FUTURE_REF}.tar.gz" diff --git a/addon/config.yaml b/addon/config.yaml index 0fe0b32..49f4255 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Future -version: "2.0.0-alpha.14" +version: "2.0.0-alpha.15" slug: sillyhome_future description: Event-first SillyHome v2 test controller url: http://192.168.6.31:3000/Otto/sillyhome-future diff --git a/app/main.py b/app/main.py index 3f06d32..60ca55a 100644 --- a/app/main.py +++ b/app/main.py @@ -155,7 +155,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="SillyHome Future API", description="SillyHome v2 event-core side project.", - version="2.0.0-alpha.14", + version="2.0.0-alpha.15", lifespan=lifespan, ) @@ -199,7 +199,7 @@ def dashboard_data() -> dict[str, object]: actuator_entities = [ entity for entity in runtime.entities.values() - if entity.domain in {"light", "switch", "fan", "cover", "humidifier"} + if _is_good_actuator(entity.entity_id, entity.friendly_name) ] return { "websocket_status": runtime.websocket_status, @@ -954,7 +954,8 @@ def _run_autopilot_once() -> AutopilotRunResult: learning.candidates = { candidate_id: candidate for candidate_id, candidate in learning.candidates.items() - if _is_good_trigger_id(candidate.trigger_entity_id) + if _is_good_actuator(candidate.actuator_entity_id, None) + and _is_good_trigger_id(candidate.trigger_entity_id) } candidates = _generate_candidates(settings) for candidate in candidates: @@ -974,7 +975,7 @@ def _generate_candidates(settings: AutopilotSettings) -> list[CandidateRecommend actuators = [ entity for entity in runtime.entities.values() - if entity.domain in {"light", "switch", "fan", "cover", "humidifier"} + if _is_good_actuator(entity.entity_id, entity.friendly_name) ][:150] triggers = [ entity @@ -1056,6 +1057,32 @@ def _is_good_trigger_id(entity_id: str) -> bool: return not any(token in raw for token in bad) and any(token in raw for token in good) +def _is_good_actuator(entity_id: str, friendly_name: str | None) -> bool: + domain = entity_id.split(".", 1)[0] + if domain not in {"light", "switch", "fan", "cover", "humidifier"}: + return False + if domain != "switch": + return True + raw = f"{entity_id} {friendly_name or ''}".lower() + bad = { + "alarm", + "battery", + "batterie", + "detection", + "linkquality", + "low", + "motion", + "occupancy", + "people", + "presence", + "problem", + "tamper", + "trigger", + "update", + } + return not any(token in raw for token in bad) + + def _entity_tokens(entity: EntityState) -> set[str]: raw = f"{entity.entity_id} {entity.friendly_name or ''}".lower() return {part for part in raw.replace(".", "_").split("_") if len(part) >= 4} diff --git a/pyproject.toml b/pyproject.toml index 9b3d3f9..99f7538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-future" -version = "2.0.0-alpha.14" +version = "2.0.0-alpha.15" description = "SillyHome v2 event-core prototype" requires-python = ">=3.11" dependencies = [ diff --git a/tests/test_addon_config.py b/tests/test_addon_config.py index 6b72989..76d2bab 100644 --- a/tests/test_addon_config.py +++ b/tests/test_addon_config.py @@ -9,7 +9,7 @@ def test_addon_config_declares_future_addon() -> None: config = yaml.safe_load(Path("addon/config.yaml").read_text(encoding="utf-8")) assert config["slug"] == "sillyhome_future" - assert config["version"] == "2.0.0-alpha.14" + assert config["version"] == "2.0.0-alpha.15" assert config["ingress"] is True assert config["ingress_port"] == 8099 assert config["homeassistant_api"] is True diff --git a/tests/test_future_api.py b/tests/test_future_api.py index 4a27bfd..2ff7118 100644 --- a/tests/test_future_api.py +++ b/tests/test_future_api.py @@ -19,7 +19,7 @@ def test_health_and_backup_roundtrip(tmp_path, monkeypatch) -> None: # type: ig restore = client.post("/v2/backup/restore", json=backup.json()) assert health.status_code == 200 - assert health.json()["version"] == "2.0.0-alpha.14" + assert health.json()["version"] == "2.0.0-alpha.15" assert backup.status_code == 200 assert restore.status_code == 200 assert restore.json() == {"status": "restored"} @@ -47,6 +47,12 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None state="off", area_name="Storage", ), + "switch.storage_motion": EntityState( + entity_id="switch.storage_motion", + domain="switch", + state="off", + area_name="Storage", + ), } ) ) @@ -162,6 +168,10 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None assert evaluations.json() assert autopilot.status_code == 200 assert autopilot.json()["candidates"] + assert all( + candidate["actuator_entity_id"] != "switch.storage_motion" + for candidate in autopilot.json()["candidates"] + ) assert candidates.status_code == 200 assert accepted.status_code == 200 assert jobs.status_code == 200