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 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), )