Files
sillyhome-next/app/actuators/cache_db.py
Otto 214b384b70
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
Release v1.6.0 dashboard architecture cleanup
2026-06-18 01:02:29 +02:00

131 lines
4.6 KiB
Python

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