131 lines
4.6 KiB
Python
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),
|
|
)
|