Compare commits
3 Commits
v2.0.0-alp
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 937c79a19b | |||
| 149f11a18e | |||
| 38a85fce74 |
@@ -2,7 +2,7 @@ FROM python:3.13-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG SILLYHOME_FUTURE_REF=main
|
ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.7
|
||||||
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.4"
|
version: "2.0.0-alpha.7"
|
||||||
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
|
||||||
|
|||||||
74
app/main.py
74
app/main.py
@@ -16,6 +16,7 @@ from app.core.models import (
|
|||||||
BackupBundle,
|
BackupBundle,
|
||||||
ControlProfile,
|
ControlProfile,
|
||||||
ControlState,
|
ControlState,
|
||||||
|
EntityState,
|
||||||
HandoffMode,
|
HandoffMode,
|
||||||
LearningState,
|
LearningState,
|
||||||
RuntimeState,
|
RuntimeState,
|
||||||
@@ -49,7 +50,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.4",
|
version="2.0.0-alpha.7",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,25 +189,76 @@ def _load_initial_ha_states(client: FutureHaClient) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def _ha_listener_loop(client: FutureHaClient) -> None:
|
async def _ha_listener_loop(client: FutureHaClient) -> None:
|
||||||
|
routed_triggers: set[str] = set()
|
||||||
|
next_route_refresh = 0.0
|
||||||
|
pending_unrouted: list[StateEvent] = []
|
||||||
|
last_unrouted_flush = 0.0
|
||||||
while True:
|
while True:
|
||||||
runtime = stores.runtime()
|
await asyncio.to_thread(_set_websocket_status, "connecting")
|
||||||
runtime.websocket_status = "connecting"
|
|
||||||
stores.save_runtime(runtime)
|
|
||||||
try:
|
try:
|
||||||
async for event in client.listen_state_events():
|
async for event in client.listen_state_events():
|
||||||
runtime = stores.runtime()
|
loop_time = asyncio.get_running_loop().time()
|
||||||
runtime.websocket_status = "connected"
|
if loop_time >= next_route_refresh:
|
||||||
stores.save_runtime(runtime)
|
routed_triggers = await asyncio.to_thread(_routed_trigger_ids)
|
||||||
event_core.process_state_event(event, execute=_execute_ha_decision)
|
next_route_refresh = loop_time + 5
|
||||||
|
await asyncio.to_thread(_set_websocket_status, "connected")
|
||||||
|
if event.entity_id in routed_triggers:
|
||||||
|
if pending_unrouted:
|
||||||
|
await asyncio.to_thread(_merge_unrouted_events, pending_unrouted)
|
||||||
|
pending_unrouted = []
|
||||||
|
await asyncio.to_thread(
|
||||||
|
event_core.process_state_event,
|
||||||
|
event,
|
||||||
|
execute=_execute_ha_decision,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pending_unrouted.append(event)
|
||||||
|
if len(pending_unrouted) >= 100 or loop_time - last_unrouted_flush >= 2:
|
||||||
|
await asyncio.to_thread(_merge_unrouted_events, pending_unrouted)
|
||||||
|
pending_unrouted = []
|
||||||
|
last_unrouted_flush = loop_time
|
||||||
|
await asyncio.sleep(0)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
runtime = stores.runtime()
|
await asyncio.to_thread(_set_websocket_status, f"reconnecting: {exc}")
|
||||||
runtime.websocket_status = f"reconnecting: {exc}"
|
|
||||||
stores.save_runtime(runtime)
|
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_websocket_status(status: str) -> None:
|
||||||
|
runtime = stores.runtime()
|
||||||
|
if runtime.websocket_status == status:
|
||||||
|
return
|
||||||
|
runtime.websocket_status = status
|
||||||
|
stores.save_runtime(runtime)
|
||||||
|
|
||||||
|
|
||||||
|
def _routed_trigger_ids() -> set[str]:
|
||||||
|
result: set[str] = set()
|
||||||
|
for profile in stores.learning().profiles.values():
|
||||||
|
for pattern in profile.patterns:
|
||||||
|
if pattern.trigger_entity_id is not None:
|
||||||
|
result.add(pattern.trigger_entity_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_unrouted_events(events: list[StateEvent]) -> None:
|
||||||
|
if not events:
|
||||||
|
return
|
||||||
|
runtime = stores.runtime()
|
||||||
|
for event in events:
|
||||||
|
runtime.entities[event.entity_id] = EntityState(
|
||||||
|
entity_id=event.entity_id,
|
||||||
|
domain=event.entity_id.split(".", 1)[0],
|
||||||
|
state=event.new_state,
|
||||||
|
changed_at=event.changed_at,
|
||||||
|
area_name=event.attributes.get("area_name"),
|
||||||
|
device_id=event.attributes.get("device_id"),
|
||||||
|
friendly_name=event.attributes.get("friendly_name"),
|
||||||
|
)
|
||||||
|
stores.save_runtime(runtime)
|
||||||
|
|
||||||
|
|
||||||
def _execute_ha_decision(decision: object) -> bool:
|
def _execute_ha_decision(decision: object) -> bool:
|
||||||
if ha_client is None or not hasattr(decision, "actuator_entity_id"):
|
if ha_client is None or not hasattr(decision, "actuator_entity_id"):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-future"
|
name = "sillyhome-future"
|
||||||
version = "2.0.0-alpha.4"
|
version = "2.0.0-alpha.7"
|
||||||
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.4"
|
assert config["version"] == "2.0.0-alpha.7"
|
||||||
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
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_health_and_backup_roundtrip(tmp_path, monkeypatch) -> None: # type: ig
|
|||||||
|
|
||||||
assert stores is not None
|
assert stores is not None
|
||||||
assert health.status_code == 200
|
assert health.status_code == 200
|
||||||
assert health.json()["version"] == "2.0.0-alpha.4"
|
assert health.json()["version"] == "2.0.0-alpha.7"
|
||||||
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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user