diff --git a/README.md b/README.md index fb3d949..2a9504d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ nach einer ausdrücklichen Freigabe ausführen. [`docs/V1_5_0_OPERATING_GUIDE.md`](docs/V1_5_0_OPERATING_GUIDE.md) - Version 1.5.1 Stabilisierung der Dashboard-Ladepfade: [`docs/V1_5_1_OPERATING_GUIDE.md`](docs/V1_5_1_OPERATING_GUIDE.md) +- Version 1.5.2 Rollback-Speicher und HA-Timeouts: + [`docs/V1_5_2_OPERATING_GUIDE.md`](docs/V1_5_2_OPERATING_GUIDE.md) - Arbeitsregeln für Coding-Agenten: [`AGENTS.md`](AGENTS.md) ## Reifegrad diff --git a/addon/config.yaml b/addon/config.yaml index 6378ab0..9355215 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "1.5.1" +version: "1.5.2" slug: sillyhome_next description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren url: http://192.168.6.31:3000/pino/sillyhome-next diff --git a/app/api/v1/actuators.py b/app/api/v1/actuators.py index 9c4df23..c28c47f 100644 --- a/app/api/v1/actuators.py +++ b/app/api/v1/actuators.py @@ -447,7 +447,7 @@ def get_actuator_detail(actuator_entity_id: str, request: Request) -> ActuatorRe } compact_snapshots = [ snapshot.model_copy(update={"patterns": []}) - for snapshot in record.behavior.model_snapshots[-5:] + for snapshot in record.behavior.model_snapshots[-3:] ] compact_behavior = record.behavior.model_copy( update={ diff --git a/app/behavior/engine.py b/app/behavior/engine.py index 7680594..913604b 100644 --- a/app/behavior/engine.py +++ b/app/behavior/engine.py @@ -32,6 +32,8 @@ from app.ha.models import HaEntitySummary from app.ha.reader import HaReader _MAX_PATTERNS = 500 +_MAX_MODEL_SNAPSHOTS = 3 +_MAX_SNAPSHOT_PATTERNS = 120 _MAX_EXECUTION_EVENTS = 100 _ACTION_LOGBOOK_TOLERANCE = timedelta(seconds=10) _CONTEXT_TRIGGER_TOLERANCE = timedelta(seconds=3) @@ -937,6 +939,11 @@ class BehaviorEngine: record: ActuatorRecord, behavior: BehaviorState, ) -> ActuatorRecord: + behavior = behavior.model_copy( + update={ + "model_snapshots": _compact_model_snapshots(behavior.model_snapshots), + } + ) updated = record.model_copy( update={ "behavior": behavior, @@ -1132,10 +1139,19 @@ def _next_model_snapshots( high_confidence_sample_count=trusted_actions, average_confidence=round(average_confidence, 4), incorrect_feedback_count=incorrect_feedback_count, - patterns=patterns, + patterns=patterns[-_MAX_SNAPSHOT_PATTERNS:], reason=reason, ) - return [*existing, snapshot][-10:] + return _compact_model_snapshots([*existing, snapshot]) + + +def _compact_model_snapshots(existing: list[ModelSnapshot]) -> list[ModelSnapshot]: + return [ + snapshot.model_copy( + update={"patterns": snapshot.patterns[-_MAX_SNAPSHOT_PATTERNS:]} + ) + for snapshot in existing[-_MAX_MODEL_SNAPSHOTS:] + ] def _average(values: list[float]) -> float: diff --git a/app/config.py b/app/config.py index 2bfcec5..cb391b8 100644 --- a/app/config.py +++ b/app/config.py @@ -21,6 +21,7 @@ class Settings: prediction_interval_seconds: int = 60 execution_cooldown_seconds: int = 900 timezone: str = "Europe/Berlin" + ha_timeout_seconds: int = 25 @property def ha_configured(self) -> bool: @@ -55,4 +56,5 @@ def load_settings() -> Settings: 60, int(os.getenv("SILLYHOME_EXECUTION_COOLDOWN_SECONDS", "900")) ), timezone=os.getenv("SILLYHOME_TIMEZONE", "Europe/Berlin"), + ha_timeout_seconds=max(5, int(os.getenv("SILLYHOME_HA_TIMEOUT_SECONDS", "25"))), ) diff --git a/app/main.py b/app/main.py index 29f446e..fad38dd 100644 --- a/app/main.py +++ b/app/main.py @@ -60,6 +60,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: settings=HaClientSettings( url=cast(str, settings.ha_url), token=cast(str, settings.ha_token), + timeout_seconds=settings.ha_timeout_seconds, ) ) app.state.ha_reader = HaReader(client=client) @@ -105,7 +106,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="SillyHome Next API", description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.", - version="1.5.1", + version="1.5.2", lifespan=lifespan, ) app.state.settings = load_settings() @@ -213,8 +214,8 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: try: async with websockets.connect( ws_url, - ping_interval=20, - ping_timeout=10, + ping_interval=30, + ping_timeout=30, ) as websocket: auth_required_msg = await websocket.recv() auth_required_data = json.loads(auth_required_msg) diff --git a/docs/V1_5_2_OPERATING_GUIDE.md b/docs/V1_5_2_OPERATING_GUIDE.md new file mode 100644 index 0000000..75532b9 --- /dev/null +++ b/docs/V1_5_2_OPERATING_GUIDE.md @@ -0,0 +1,32 @@ +# SillyHome Next v1.5.2 Operating Guide + +v1.5.2 begrenzt den Rollback-Speicher und entschärft Home-Assistant-Timeouts, +die in den Add-on-Logs sichtbar wurden. + +## Rollback-Speicher + +- Pro Aktor bleiben maximal 3 Modell-Snapshots erhalten. +- Pro Snapshot bleiben maximal 120 Muster erhalten. +- Beim Speichern eines Aktors werden ältere oder zu große Snapshots automatisch + gekappt. +- Der kompakte Detail-Endpunkt liefert ebenfalls maximal 3 Rollback-Snapshots + und keine Musterlisten. + +Damit bleibt Rollback nutzbar, ohne dass die JSON-Dateien mit alten Modellen +stark wachsen. + +## Home-Assistant-Zugriffe + +- REST-Zugriffe auf Home Assistant haben jetzt standardmäßig 25 Sekunden + Timeout statt 10 Sekunden. +- Der Wert ist über `SILLYHOME_HA_TIMEOUT_SECONDS` konfigurierbar. +- WebSocket-Keepalive wurde auf 30 Sekunden Ping-Intervall und 30 Sekunden + Ping-Timeout entschärft. + +## Log-Einordnung + +- `GET ... HTTP/1.1` ist bei Uvicorn/HA-Ingress normal und kein Fehler. +- `Zeitüberschreitung beim Zugriff auf Home Assistant` bedeutet, dass HA selbst + zu langsam geantwortet hat oder der Ingress/Netzpfad verzögert war. +- `keepalive ping timeout` bedeutet, dass die HA-WebSocket-Verbindung nicht + rechtzeitig geantwortet hat. SillyHome reconnectet automatisch. diff --git a/pyproject.toml b/pyproject.toml index f355321..b585d95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "1.5.1" +version = "1.5.2" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [ diff --git a/tests/test_main.py b/tests/test_main.py index 994aa15..367c31f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -92,11 +92,11 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None: except asyncio.CancelledError: pass - connect.assert_called_once_with( - "ws://homeassistant:8123/api/websocket", - ping_interval=20, - ping_timeout=10, - ) + connect.assert_called_once_with( + "ws://homeassistant:8123/api/websocket", + ping_interval=30, + ping_timeout=30, + ) assert fake_ws.sent == [ {"type": "auth", "access_token": "test-token"}, {"id": 1, "type": "subscribe_events", "event_type": "state_changed"},