daily-briefing/server/routers/homeassistant.py
Sam f5b7c53f18 HA controls + wider layout: toggle lights/switches, cover controls, more sensors
- Backend: call_ha_service() for controlling entities via HA REST API
- Backend: POST /api/ha/control with JWT auth + cache invalidation
- Backend: Parse switches, binary_sensors, humidity, climate entities
- Frontend: HA card now xl:col-span-2 (double width)
- Frontend: Interactive toggles for lights/switches, cover up/stop/down
- Frontend: Temperature + humidity sensors, climate display, binary sensors
- Frontend: Two-column internal layout (controls left, sensors right)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:30:20 +01:00

83 lines
2.4 KiB
Python

"""Home Assistant data router — read states & control entities."""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from server.auth import require_admin
from server.cache import cache
from server.config import get_settings
from server.services.ha_service import call_ha_service, fetch_ha_data
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["homeassistant"])
CACHE_KEY = "ha"
# ---------------------------------------------------------------------------
# GET /api/ha — read-only, no auth needed
# ---------------------------------------------------------------------------
@router.get("/ha")
async def get_ha() -> Dict[str, Any]:
"""Return Home Assistant entity data (cached)."""
cached = await cache.get(CACHE_KEY)
if cached is not None:
return cached
try:
data: Dict[str, Any] = await fetch_ha_data(
get_settings().ha_url,
get_settings().ha_token,
)
except Exception as exc:
logger.exception("Failed to fetch Home Assistant data")
return {"error": True, "message": str(exc)}
await cache.set(CACHE_KEY, data, get_settings().ha_cache_ttl)
return data
# ---------------------------------------------------------------------------
# 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,
admin_user: str = Depends(require_admin), # noqa: ARG001
) -> 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