Prepare SillyHome Next 1.0.0 dashboard and API rework
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 01:13:43 +02:00
parent 4db4276b95
commit 7ba9807a4e
11 changed files with 412 additions and 44 deletions

View File

@@ -14,7 +14,7 @@ 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.discovery import DiscoveredEntity, EntityRole, discover_entities
from app.ha.exceptions import HaClientError
from app.ha.models import HaEntitySummary
from app.ha.reader import HaReader
@@ -79,6 +79,35 @@ class ActuatorSummary(BaseModel):
updated_at: str
class EntityCacheStatus(BaseModel):
available: bool
updated_at: str | None = None
entity_count: int = 0
class DashboardSystemStatus(BaseModel):
api_status: str = "ok"
websocket_status: str = "unavailable"
websocket_error: str | None = None
reconciliation_last_completed_at: str | None = None
configured_actuators: int = 0
trained_models: int = 0
review_required: int = 0
class DashboardDiscoveryGroup(BaseModel):
category: str
role: str
count: int
class DashboardOverview(BaseModel):
system: DashboardSystemStatus
cache: EntityCacheStatus
actuators: list[ActuatorSummary]
discovery_groups: list[DashboardDiscoveryGroup]
@router.get("/discovery", response_model=list[HaEntitySummary])
def discover_actuators(
request: Request,
@@ -92,7 +121,7 @@ def discover_actuators(
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()
discovered = discover_entities(list(entities.values()))
actuator_ids = _deduplicate_actuator_ids(
[
(entity.entity_id, entity.category)
@@ -110,7 +139,7 @@ def suggest_actuators(
ha_reader: HaReader = Depends(get_ha_reader),
) -> list[ActuatorSuggestion]:
entities = {entity.entity_id: entity for entity in ha_reader.read_entities()}
discovered = {entity.entity_id: entity for entity in ha_reader.discover()}
discovered = {entity.entity_id: entity for entity in discover_entities(list(entities.values()))}
configured_ids = {record.actuator_entity_id for record in _service(request).list_configured()}
actuator_ids = _deduplicate_actuator_ids(
[
@@ -222,6 +251,53 @@ def list_configured_summary(request: Request) -> list[ActuatorSummary]:
]
@router.get("/dashboard", response_model=DashboardOverview)
def dashboard_overview(request: Request) -> 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")
updated_at = raw_updated_at if isinstance(raw_updated_at, str) else None
cached_entities = [
HaEntitySummary.model_validate(entity)
for entity in raw_entities
if isinstance(entity, dict)
]
discovered = discover_entities(cached_entities) if cached_entities else []
group_counts: dict[tuple[str, str], int] = {}
for entity in discovered:
key = (entity.category, entity.role.value)
group_counts[key] = group_counts.get(key, 0) + 1
reconciliation = _reconciliation_state_or_default(request)
ws_status = getattr(request.app.state, "ws_status", None)
actuators = list_configured_summary(request)
return DashboardOverview(
system=DashboardSystemStatus(
websocket_status=getattr(ws_status, "status", "unavailable"),
websocket_error=getattr(ws_status, "error", None),
reconciliation_last_completed_at=(
reconciliation.last_completed_at.isoformat()
if reconciliation.last_completed_at is not None
else None
),
configured_actuators=len(actuators),
trained_models=reconciliation.trained_models,
review_required=reconciliation.review_required,
),
cache=EntityCacheStatus(
available=bool(cached_entities),
updated_at=updated_at,
entity_count=len(cached_entities),
),
actuators=actuators,
discovery_groups=[
DashboardDiscoveryGroup(category=category, role=role, count=count)
for (category, role), count in sorted(group_counts.items())
],
)
@router.get("", response_model=list[ActuatorRecord])
def list_configured(request: Request) -> list[ActuatorRecord]:
return _service(request).list_configured()
@@ -412,6 +488,16 @@ def _behavior(request: Request) -> BehaviorEngine:
return engine
def _reconciliation_state_or_default(request: Request) -> ReconciliationState:
store = getattr(request.app.state, "actuator_store", None)
if not isinstance(store, ActuatorStore):
return ReconciliationState()
try:
return store.load_reconciliation_state()
except ValueError:
return ReconciliationState(last_summary="Reconciliation-Status ist unlesbar.")
def _entity_cache_path(request: Request) -> Path:
store = getattr(request.app.state, "actuator_store", None)
settings = getattr(request.app.state, "settings", None)
@@ -425,17 +511,27 @@ def _entity_cache_path(request: Request) -> Path:
def _load_cached_entities(request: Request) -> list[HaEntitySummary]:
path = _entity_cache_path(request)
if not path.exists():
payload = _load_entity_cache_payload(request)
raw_entities = payload.get("entities", [])
if not isinstance(raw_entities, list):
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):
except ValueError:
return []
def _load_entity_cache_payload(request: Request) -> dict[str, object]:
path = _entity_cache_path(request)
if not path.exists():
return {}
try:
payload = json.loads(path.read_text(encoding="utf-8"))
return payload if isinstance(payload, dict) else {}
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)