from __future__ import annotations import json import sqlite3 from datetime import datetime, timezone from pathlib import Path from threading import RLock from app.ha.models import HaEntitySummary class DashboardCache: def __init__(self, path: str | Path) -> None: self._path = Path(path).resolve() self._path.parent.mkdir(parents=True, exist_ok=True) self._lock = RLock() self._init() def load_entities_payload(self) -> dict[str, object]: with self._lock, self._connect() as connection: rows = connection.execute( "select entity_id, payload from ha_entities order by entity_id" ).fetchall() updated_at = self._get_meta(connection, "ha_entities_updated_at") groups_json = self._get_meta(connection, "discovery_groups") or "[]" try: groups = json.loads(groups_json) except ValueError: groups = [] return { "updated_at": updated_at, "discovery_groups": groups if isinstance(groups, list) else [], "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, *, entities: list[HaEntitySummary], discovery_groups: list[dict[str, object]], ) -> None: now = datetime.now(timezone.utc).isoformat() rows = [ (entity.entity_id, entity.model_dump_json()) for entity in entities ] with self._lock, self._connect() as connection: connection.execute("delete from ha_entities") connection.executemany( "insert into ha_entities(entity_id, payload) values (?, ?)", rows, ) self._set_meta(connection, "ha_entities_updated_at", now) self._set_meta( connection, "discovery_groups", json.dumps(discovery_groups, ensure_ascii=True, sort_keys=True), ) def _init(self) -> None: with self._connect() as connection: connection.execute( """ create table if not exists ha_entities ( entity_id text primary key, payload text not null ) """ ) connection.execute( """ create table if not exists cache_meta ( key text primary key, value text ) """ ) def _connect(self) -> sqlite3.Connection: return sqlite3.connect(self._path, timeout=30) @staticmethod def _get_meta(connection: sqlite3.Connection, key: str) -> str | None: row = connection.execute( "select value from cache_meta where key = ?", (key,), ).fetchone() return str(row[0]) if row is not None and row[0] is not None else None @staticmethod def _set_meta(connection: sqlite3.Connection, key: str, value: str) -> None: connection.execute( """ insert into cache_meta(key, value) values (?, ?) on conflict(key) do update set value = excluded.value """, (key, value), )