2026-03-02 19:30:20 +01:00
|
|
|
"""Home Assistant data router — read states & control entities."""
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
2026-03-02 19:30:20 +01:00
|
|
|
from typing import Any, Dict, Optional
|
2026-03-02 01:48:51 +01:00
|
|
|
|
2026-03-02 22:41:16 +01:00
|
|
|
from fastapi import APIRouter
|
2026-03-02 19:30:20 +01:00
|
|
|
from pydantic import BaseModel
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
from server.cache import cache
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
from server.config import get_settings
|
2026-03-02 19:30:20 +01:00
|
|
|
from server.services.ha_service import call_ha_service, fetch_ha_data
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api", tags=["homeassistant"])
|
|
|
|
|
|
|
|
|
|
CACHE_KEY = "ha"
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 19:30:20 +01:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# GET /api/ha — read-only, no auth needed
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-02 01:48:51 +01:00
|
|
|
@router.get("/ha")
|
|
|
|
|
async def get_ha() -> Dict[str, Any]:
|
2026-03-02 19:30:20 +01:00
|
|
|
"""Return Home Assistant entity data (cached)."""
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
cached = await cache.get(CACHE_KEY)
|
|
|
|
|
if cached is not None:
|
|
|
|
|
return cached
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data: Dict[str, Any] = await fetch_ha_data(
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
get_settings().ha_url,
|
|
|
|
|
get_settings().ha_token,
|
2026-03-02 01:48:51 +01:00
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.exception("Failed to fetch Home Assistant data")
|
|
|
|
|
return {"error": True, "message": str(exc)}
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news,
Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead
of ENV variables. Two-layer config: ENV seeds DB on first start, then DB
is source of truth. Auto-migration system on startup.
Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service,
admin router (protected), test_connections per integration, config.py rewrite.
Frontend: react-router v6, login page, admin layout with sidebar, 8 settings
pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword),
shared IntegrationForm + TestButton components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:37:30 +01:00
|
|
|
await cache.set(CACHE_KEY, data, get_settings().ha_cache_ttl)
|
2026-03-02 01:48:51 +01:00
|
|
|
return data
|
2026-03-02 19:30:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# POST /api/ha/control — requires auth
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class HAControlRequest(BaseModel):
|
|
|
|
|
entity_id: str
|
|
|
|
|
action: str # toggle, turn_on, turn_off, open, close, stop
|
|
|
|
|
data: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/ha/control")
|
|
|
|
|
async def control_ha(
|
|
|
|
|
body: HAControlRequest,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""Control a Home Assistant entity (toggle light, open cover, etc.)."""
|
|
|
|
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
|
|
|
|
if not settings.ha_url or not settings.ha_token:
|
|
|
|
|
return {"ok": False, "error": "Home Assistant not configured"}
|
|
|
|
|
|
|
|
|
|
result = await call_ha_service(
|
|
|
|
|
url=settings.ha_url,
|
|
|
|
|
token=settings.ha_token,
|
|
|
|
|
entity_id=body.entity_id,
|
|
|
|
|
action=body.action,
|
|
|
|
|
service_data=body.data,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Invalidate cache so the next GET reflects the new state
|
|
|
|
|
if result.get("ok"):
|
|
|
|
|
await cache.invalidate(CACHE_KEY)
|
|
|
|
|
logger.info("[HA] Cache invalidated after %s on %s", body.action, body.entity_id)
|
|
|
|
|
|
|
|
|
|
return result
|