diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d5780..0a46e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.7.21 - 2026-06-17 +- Dashboard-Ladepfad getrennt: beobachtete Geräte laden sofort über + `/v1/actuators/summary`; Status, Discovery und Vorschläge laufen unabhängig + nachgelagert und blockieren die Übersicht nicht mehr. +- Systemstatus nutzt Timeouts und bleibt auch bei langsamem ML-/HA-Status + bedienbar. +- HA-Entity-Metadaten werden als JSON-Cache gespeichert und für Friendly Name, + Raum und Gerät in schlanken Summaries wiederverwendet. +- Anleitung, Gerätegruppen und manuelle Kontextauswahl sind aufklappbar und + kompakter für Smartphone- und Desktopansichten. + ## 0.7.20 - 2026-06-17 - Dashboard-Übersicht ist kompatibel mit dem leichten Summary-Format und greift nicht mehr auf `record.behavior.status` aus dem Vollformat zu. diff --git a/addon/config.yaml b/addon/config.yaml index 351d7c1..b3b9df8 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "0.7.20" +version: "0.7.21" 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 45cbb8d..5e8eb69 100644 --- a/app/api/v1/actuators.py +++ b/app/api/v1/actuators.py @@ -1,5 +1,10 @@ from __future__ import annotations +import json +import os +from datetime import datetime, timezone +from pathlib import Path + from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from pydantic import BaseModel, Field @@ -7,6 +12,7 @@ from app.actuators.lifecycle import ActuatorReconciliationService from app.actuators.models import ActuatorRecord, ReconciliationState from app.actuators.store import ActuatorStore from app.behavior.engine import BehaviorEngine +from app.config import Settings from app.dependencies import get_ha_reader from app.ha.discovery import DiscoveredEntity, EntityRole from app.ha.exceptions import HaClientError @@ -58,6 +64,9 @@ class ActuatorSuggestion(BaseModel): class ActuatorSummary(BaseModel): actuator_entity_id: str domain: str + friendly_name: str | None = None + area_name: str | None = None + device_name: str | None = None enabled: bool behavior_mode: str behavior_status: str @@ -71,8 +80,18 @@ class ActuatorSummary(BaseModel): @router.get("/discovery", response_model=list[HaEntitySummary]) -def discover_actuators(ha_reader: HaReader = Depends(get_ha_reader)) -> list[HaEntitySummary]: - entities = {entity.entity_id: entity for entity in ha_reader.read_entities()} +def discover_actuators( + request: Request, + refresh: bool = Query(default=False), + ha_reader: HaReader = Depends(get_ha_reader), +) -> list[HaEntitySummary]: + cached_entities = [] if refresh else _load_cached_entities(request) + if cached_entities: + entities = {entity.entity_id: entity for entity in cached_entities} + else: + fresh_entities = list(ha_reader.read_entities()) + _save_cached_entities(request, fresh_entities) + entities = {entity.entity_id: entity for entity in fresh_entities} discovered = ha_reader.discover() actuator_ids = _deduplicate_actuator_ids( [ @@ -160,10 +179,26 @@ def context_options( @router.get("/summary", response_model=list[ActuatorSummary]) def list_configured_summary(request: Request) -> list[ActuatorSummary]: + entity_map = {entity.entity_id: entity for entity in _load_cached_entities(request)} return [ ActuatorSummary( actuator_entity_id=record.actuator_entity_id, domain=record.actuator_entity_id.split(".", 1)[0], + friendly_name=( + entity_map[record.actuator_entity_id].friendly_name + if record.actuator_entity_id in entity_map + else None + ), + area_name=( + entity_map[record.actuator_entity_id].area_name + if record.actuator_entity_id in entity_map + else None + ), + device_name=( + entity_map[record.actuator_entity_id].device_name + if record.actuator_entity_id in entity_map + else None + ), enabled=record.enabled, behavior_mode=record.behavior.mode.value, behavior_status=record.behavior.status.value, @@ -377,6 +412,45 @@ def _behavior(request: Request) -> BehaviorEngine: return engine +def _entity_cache_path(request: Request) -> Path: + store = getattr(request.app.state, "actuator_store", None) + settings = getattr(request.app.state, "settings", None) + if isinstance(store, ActuatorStore): + base_dir = store._root + elif isinstance(settings, Settings): + base_dir = Path(settings.actuator_store).resolve().parent + else: + base_dir = Path(".").resolve() + return Path(os.getenv("SILLYHOME_ENTITY_CACHE", base_dir / "ha_entity_cache.json")) + + +def _load_cached_entities(request: Request) -> list[HaEntitySummary]: + path = _entity_cache_path(request) + if not path.exists(): + return [] + try: + payload = json.loads(path.read_text(encoding="utf-8")) + raw_entities = payload.get("entities", []) + return [HaEntitySummary.model_validate(entity) for entity in raw_entities] + except (OSError, TypeError, ValueError): + return [] + + +def _save_cached_entities(request: Request, entities: list[HaEntitySummary]) -> None: + path = _entity_cache_path(request) + path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "updated_at": datetime.now(timezone.utc).isoformat(), + "entities": [entity.model_dump(mode="json") for entity in entities], + } + temporary = path.with_suffix(".json.tmp") + temporary.write_text( + json.dumps(payload, ensure_ascii=True, sort_keys=True) + "\n", + encoding="utf-8", + ) + os.replace(temporary, path) + + def _deduplicate_actuator_ids( discovered: list[tuple[str, str]], entities: dict[str, HaEntitySummary], diff --git a/app/main.py b/app/main.py index 7bee66a..58b70bd 100644 --- a/app/main.py +++ b/app/main.py @@ -105,7 +105,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="SillyHome Next API", description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.", - version="0.7.20", + version="0.7.21", lifespan=lifespan, ) app.state.settings = load_settings() diff --git a/app/static/index.html b/app/static/index.html index a329d8a..d5edd55 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -6,27 +6,40 @@ SillyHome Next