Compare commits
2 Commits
v2.0.0-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bf832b49f4 | |||
| 8057751c11 |
@@ -2,7 +2,7 @@ FROM python:3.13-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.13
|
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"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Future
|
name: SillyHome Future
|
||||||
version: "2.0.0-alpha.13"
|
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
|
||||||
|
|||||||
68
app/main.py
68
app/main.py
@@ -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.13",
|
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,
|
||||||
@@ -951,6 +951,12 @@ def _run_autopilot_once() -> AutopilotRunResult:
|
|||||||
if not settings.enabled:
|
if not settings.enabled:
|
||||||
return AutopilotRunResult(candidates=[], trained_models=[], evaluations=[])
|
return AutopilotRunResult(candidates=[], trained_models=[], evaluations=[])
|
||||||
learning = stores.learning()
|
learning = stores.learning()
|
||||||
|
learning.candidates = {
|
||||||
|
candidate_id: candidate
|
||||||
|
for candidate_id, candidate in learning.candidates.items()
|
||||||
|
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:
|
||||||
learning.candidates[candidate.candidate_id] = candidate
|
learning.candidates[candidate.candidate_id] = candidate
|
||||||
@@ -969,12 +975,12 @@ 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
|
||||||
for entity in runtime.entities.values()
|
for entity in runtime.entities.values()
|
||||||
if entity.domain in {"binary_sensor", "sensor"}
|
if entity.domain == "binary_sensor" and _is_good_trigger_id(entity.entity_id)
|
||||||
][:500]
|
][:500]
|
||||||
result: list[CandidateRecommendation] = []
|
result: list[CandidateRecommendation] = []
|
||||||
for actuator in actuators:
|
for actuator in actuators:
|
||||||
@@ -1023,6 +1029,60 @@ def _candidate_score(actuator: EntityState, trigger: EntityState) -> tuple[float
|
|||||||
return 0.0, ""
|
return 0.0, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _is_good_trigger_id(entity_id: str) -> bool:
|
||||||
|
raw = entity_id.lower()
|
||||||
|
bad = {
|
||||||
|
"battery",
|
||||||
|
"batterie",
|
||||||
|
"low",
|
||||||
|
"update",
|
||||||
|
"problem",
|
||||||
|
"connectivity",
|
||||||
|
"linkquality",
|
||||||
|
"tamper",
|
||||||
|
}
|
||||||
|
good = {
|
||||||
|
"door",
|
||||||
|
"tuer",
|
||||||
|
"ture",
|
||||||
|
"window",
|
||||||
|
"fenster",
|
||||||
|
"motion",
|
||||||
|
"pir",
|
||||||
|
"occupancy",
|
||||||
|
"presence",
|
||||||
|
"kontakt",
|
||||||
|
"contact",
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-future"
|
name = "sillyhome-future"
|
||||||
version = "2.0.0-alpha.13"
|
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 = [
|
||||||
|
|||||||
@@ -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.13"
|
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
|
||||||
|
|||||||
@@ -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.13"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user