1 Commits

Author SHA1 Message Date
bf832b49f4 Filter autopilot actuator candidates 2026-06-18 18:43:53 +02:00
6 changed files with 46 additions and 9 deletions

View File

@@ -2,7 +2,7 @@ FROM python:3.13-slim
WORKDIR /app 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 \ RUN python -m pip install --no-cache-dir \
"http://192.168.6.31:3000/Otto/sillyhome-future/archive/${SILLYHOME_FUTURE_REF}.tar.gz" "http://192.168.6.31:3000/Otto/sillyhome-future/archive/${SILLYHOME_FUTURE_REF}.tar.gz"

View File

@@ -1,5 +1,5 @@
name: SillyHome Future name: SillyHome Future
version: "2.0.0-alpha.14" version: "2.0.0-alpha.15"
slug: sillyhome_future slug: sillyhome_future
description: Event-first SillyHome v2 test controller description: Event-first SillyHome v2 test controller
url: http://192.168.6.31:3000/Otto/sillyhome-future url: http://192.168.6.31:3000/Otto/sillyhome-future

View File

@@ -155,7 +155,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
app = FastAPI( app = FastAPI(
title="SillyHome Future API", title="SillyHome Future API",
description="SillyHome v2 event-core side project.", description="SillyHome v2 event-core side project.",
version="2.0.0-alpha.14", version="2.0.0-alpha.15",
lifespan=lifespan, lifespan=lifespan,
) )
@@ -199,7 +199,7 @@ def dashboard_data() -> dict[str, object]:
actuator_entities = [ actuator_entities = [
entity entity
for entity in runtime.entities.values() 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 { return {
"websocket_status": runtime.websocket_status, "websocket_status": runtime.websocket_status,
@@ -954,7 +954,8 @@ def _run_autopilot_once() -> AutopilotRunResult:
learning.candidates = { learning.candidates = {
candidate_id: candidate candidate_id: candidate
for candidate_id, candidate in learning.candidates.items() 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) candidates = _generate_candidates(settings)
for candidate in candidates: for candidate in candidates:
@@ -974,7 +975,7 @@ def _generate_candidates(settings: AutopilotSettings) -> list[CandidateRecommend
actuators = [ actuators = [
entity entity
for entity in runtime.entities.values() 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] ][:150]
triggers = [ triggers = [
entity 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) 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]: def _entity_tokens(entity: EntityState) -> set[str]:
raw = f"{entity.entity_id} {entity.friendly_name or ''}".lower() raw = f"{entity.entity_id} {entity.friendly_name or ''}".lower()
return {part for part in raw.replace(".", "_").split("_") if len(part) >= 4} return {part for part in raw.replace(".", "_").split("_") if len(part) >= 4}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "sillyhome-future" name = "sillyhome-future"
version = "2.0.0-alpha.14" version = "2.0.0-alpha.15"
description = "SillyHome v2 event-core prototype" description = "SillyHome v2 event-core prototype"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [

View File

@@ -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")) config = yaml.safe_load(Path("addon/config.yaml").read_text(encoding="utf-8"))
assert config["slug"] == "sillyhome_future" 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"] is True
assert config["ingress_port"] == 8099 assert config["ingress_port"] == 8099
assert config["homeassistant_api"] is True assert config["homeassistant_api"] is True

View File

@@ -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()) restore = client.post("/v2/backup/restore", json=backup.json())
assert health.status_code == 200 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 backup.status_code == 200
assert restore.status_code == 200 assert restore.status_code == 200
assert restore.json() == {"status": "restored"} assert restore.json() == {"status": "restored"}
@@ -47,6 +47,12 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None
state="off", state="off",
area_name="Storage", 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 evaluations.json()
assert autopilot.status_code == 200 assert autopilot.status_code == 200
assert autopilot.json()["candidates"] 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 candidates.status_code == 200
assert accepted.status_code == 200 assert accepted.status_code == 200
assert jobs.status_code == 200 assert jobs.status_code == 200