Rework dashboard loading and cache entity metadata
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-17 00:41:50 +02:00
parent 98a2b2cc38
commit 4db4276b95
8 changed files with 224 additions and 56 deletions

View File

@@ -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],