From 214b384b70390c55caac72e1fb87d4b03190cb7e Mon Sep 17 00:00:00 2001 From: Otto Date: Thu, 18 Jun 2026 01:02:29 +0200 Subject: [PATCH] Release v1.6.0 dashboard architecture cleanup --- CHANGELOG.md | 13 +++++ addon/config.yaml | 2 +- app/actuators/cache_db.py | 32 +++++++++++ app/api/v1/actuators.py | 44 ++++++++++++--- app/main.py | 2 +- app/static/index.html | 99 ++++++++++++++-------------------- docs/V1_6_0_OPERATING_GUIDE.md | 36 +++++++++++++ pyproject.toml | 2 +- tests/api/test_actuators.py | 29 +++++++++- tests/test_dashboard.py | 24 +++++---- 10 files changed, 201 insertions(+), 82 deletions(-) create mode 100644 docs/V1_6_0_OPERATING_GUIDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 132c91a..9e45ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 1.6.0 - 2026-06-18 +- `/v1/actuators/dashboard/system` und `/dashboard/start` lesen fuer + Cache-Status nur noch SQLite-Metadaten statt den kompletten Entity-Cache zu + materialisieren. +- Aktor-Summaries lesen benoetigte Entity-Metadaten gezielt aus SQLite anhand + der Aktor-IDs. +- Ingress-Dashboard bereinigt: weniger Erklaertexte, kein Ablauf-Menue, kein + Versions-Chip im Einrichtungsbereich. +- Detailansicht ergaenzt Zurueck-Navigation, Aktualisieren und Auswahl eines + anderen beobachteten Geraets. +- Frontend bleibt Anzeige- und Bedienebene; Backend liefert schlanke + View-Daten, Worker aktualisieren HA-/Discovery-Cache im Hintergrund. + ## 1.5.4 - 2026-06-18 - Add-on-Start vertraut Ingress-Proxy-Headern nicht mehr blind. Uvicorn loggt damit den direkten Docker-/Ingress-Peer statt LAN-IPs aus `X-Forwarded-For`. diff --git a/addon/config.yaml b/addon/config.yaml index fdfbdbd..60a748f 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "1.5.4" +version: "1.6.0" 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/actuators/cache_db.py b/app/actuators/cache_db.py index 23b7b26..6fe6153 100644 --- a/app/actuators/cache_db.py +++ b/app/actuators/cache_db.py @@ -33,6 +33,38 @@ class DashboardCache: "entities": [json.loads(row[1]) for row in rows], } + def load_status(self) -> dict[str, object]: + with self._lock, self._connect() as connection: + updated_at = self._get_meta(connection, "ha_entities_updated_at") + groups_json = self._get_meta(connection, "discovery_groups") or "[]" + entity_count = connection.execute("select count(*) from ha_entities").fetchone()[0] + try: + groups = json.loads(groups_json) + except ValueError: + groups = [] + return { + "updated_at": updated_at, + "discovery_groups": groups if isinstance(groups, list) else [], + "entity_count": int(entity_count or 0), + } + + def load_entity_map(self, entity_ids: set[str]) -> dict[str, HaEntitySummary]: + if not entity_ids: + return {} + placeholders = ",".join("?" for _ in entity_ids) + with self._lock, self._connect() as connection: + rows = connection.execute( + f"select entity_id, payload from ha_entities where entity_id in ({placeholders})", + tuple(sorted(entity_ids)), + ).fetchall() + result: dict[str, HaEntitySummary] = {} + for entity_id, payload in rows: + try: + result[str(entity_id)] = HaEntitySummary.model_validate(json.loads(payload)) + except (TypeError, ValueError): + continue + return result + def save_entities_payload( self, *, diff --git a/app/api/v1/actuators.py b/app/api/v1/actuators.py index ca67329..34eb409 100644 --- a/app/api/v1/actuators.py +++ b/app/api/v1/actuators.py @@ -326,13 +326,10 @@ def _dashboard_overview( include_background: bool, include_actuators: bool, ) -> DashboardOverview: - cache_payload = _load_entity_cache_payload(request) - raw_entities = cache_payload.get("entities", []) - if not isinstance(raw_entities, list): - raw_entities = [] - raw_updated_at = cache_payload.get("updated_at") + cache_status = _load_entity_cache_status(request) + raw_updated_at = cache_status.get("updated_at") updated_at = raw_updated_at if isinstance(raw_updated_at, str) else None - raw_groups = cache_payload.get("discovery_groups", []) + raw_groups = cache_status.get("discovery_groups", []) cached_groups = [ DashboardDiscoveryGroup.model_validate(group) for group in raw_groups @@ -350,6 +347,9 @@ def _dashboard_overview( job_p95_duration_ms, slow_job_count, performance_status = _performance_status(jobs) anomaly_count = sum(record.anomaly_count for record in actuators) critical_anomaly_count = sum(record.critical_anomaly_count for record in actuators) + entity_count = cache_status.get("entity_count") + if not isinstance(entity_count, int): + entity_count = 0 return DashboardOverview( system=DashboardSystemStatus( websocket_status=getattr(ws_status, "status", "unavailable"), @@ -373,9 +373,9 @@ def _dashboard_overview( critical_anomaly_count=critical_anomaly_count, ), cache=EntityCacheStatus( - available=bool(raw_entities), + available=bool(entity_count), updated_at=updated_at, - entity_count=len(raw_entities), + entity_count=entity_count, ), actuators=actuators, discovery_groups=cached_groups, @@ -846,6 +846,11 @@ def _load_cached_entity_map( ) -> dict[str, HaEntitySummary]: if not entity_ids: return {} + cache = getattr(request.app.state, "dashboard_cache", None) + if isinstance(cache, DashboardCache): + cached_result = cache.load_entity_map(entity_ids) + if cached_result: + return cached_result payload = _load_entity_cache_payload(request) raw_entities = payload.get("entities", []) if not isinstance(raw_entities, list): @@ -864,6 +869,29 @@ def _load_cached_entity_map( return result +def _load_entity_cache_status(request: Request) -> dict[str, object]: + cache = getattr(request.app.state, "dashboard_cache", None) + if isinstance(cache, DashboardCache): + status_payload = cache.load_status() + if status_payload.get("entity_count"): + return status_payload + path = _entity_cache_path(request) + if not path.exists(): + return {} + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, TypeError, ValueError): + return {} + if not isinstance(payload, dict): + return {} + raw_entities = payload.get("entities", []) + return { + "updated_at": payload.get("updated_at"), + "discovery_groups": payload.get("discovery_groups", []), + "entity_count": len(raw_entities) if isinstance(raw_entities, list) else 0, + } + + def _load_entity_cache_payload(request: Request) -> dict[str, object]: cache = getattr(request.app.state, "dashboard_cache", None) if isinstance(cache, DashboardCache): diff --git a/app/main.py b/app/main.py index 57f97ea..5129f62 100644 --- a/app/main.py +++ b/app/main.py @@ -117,7 +117,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.4", + version="1.6.0", lifespan=lifespan, ) app.state.settings = load_settings() diff --git a/app/static/index.html b/app/static/index.html index cc847da..820ed19 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -52,6 +52,8 @@ .panel-title { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; } .toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:10px 0; } .toolbar button { margin-top:0; } + .detail-tools { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:8px; margin:10px 0; align-items:end; } + .detail-tools button { margin-top:0; } details.collapsible > summary, .manual-context > summary, .group-panel > summary { cursor:pointer; font-weight:800; color:#eaf1f8; } @@ -144,10 +146,9 @@
SH -

SillyHome Next

+

SillyHome

-

Arbeitsdashboard für gelernte Home-Assistant-Bedienung: Geräte auswählen, Lernstand prüfen, Freigaben steuern.

-

Sicherer Start: Zuerst wird nur beobachtet und vorhergesagt. Ohne deine spätere Freigabe wird nichts geschaltet.

+

Geräte, Lernen, Freigaben und Systemzustand.

@@ -156,7 +157,6 @@ -
Seite bereit, Status folgt ...
@@ -165,11 +165,9 @@
-

Steuerung

- v1 +

Discovery & Einrichtung

-

Wähle eine Lampe, einen Rollladen oder einen anderen unterstützten Aktor. Du wählst keine Sensoren und erstellst keine Regeln.

- +
@@ -199,16 +197,15 @@
- + -

Noch kein Aktor ausgewählt.

+

Bereit.

Vorschläge anzeigen -

Vorschläge können Home Assistant stark abfragen und werden deshalb nicht beim Start geladen.

@@ -218,7 +215,6 @@

Beobachtete Geräte

-

Öffne „Details“, um Lernfortschritt, aktuelle Vorhersage und den automatisch gefundenen Kontext zu sehen.

@@ -226,8 +222,10 @@
-

Lernfortschritt und Freigabe

-

Die Freigabe erscheint erst, wenn genug eindeutig zugeordnete Handlungen gelernt wurden. Vorher bleibt das Gerät sicher im Beobachtungsmodus.

+
+

Details

+ +
Öffne bei einem beobachteten Gerät die Details.
@@ -235,7 +233,6 @@

System & Cache

-

Die Startansicht nutzt lokale Summaries und Cache-Daten. Home-Assistant-Discovery lädt erst bei Bedarf.

@@ -259,40 +256,9 @@ -

Die API speichert stabile technische Werte. Die Oberfläche übersetzt sie in die gewählte Sprache.

- -
-

Performance-Standard

-

Startansichten dürfen maximal 3 Sekunden brauchen. Schwere Daten werden nur nach Menüwechsel oder bei Bearbeitung geladen.

- -
-
- So gehst du vor -
-
- 1 -

Aktor auswählen

-

Wo? Links im Feld „Gerät auswählen“.

-

Was passiert? SillyHome ordnet Raum, Sensoren, Zustände und vorhandene Historie automatisch zu.

-
-
- 2 -

Wie gewohnt bedienen

-

Wo? Weiterhin in Home Assistant, an Schaltern oder über deine bisherigen Bedienwege.

-

Was passiert? SillyHome lernt deine Handlungen und zeigt Vorhersagen an, schaltet aber noch nicht selbst.

-
-
- 3 -

Später freigeben

-

Wo? In den Details des ausgewählten Geräts, sobald genug Verhalten gelernt wurde.

-

Was passiert? Erst dann darf SillyHome passende Vorhersagen automatisch ausführen. Die Freigabe kann jederzeit gestoppt werden.

-
-
-
-