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>
This commit is contained in:
Sam 2026-03-02 19:30:20 +01:00
parent 4e7c1909ee
commit f5b7c53f18
6 changed files with 683 additions and 168 deletions

View file

@ -1,15 +1,17 @@
"""Home Assistant data router."""
"""Home Assistant data router — read states & control entities."""
from __future__ import annotations
import logging
from typing import Any, Dict
from typing import Any, Dict, Optional
from fastapi import APIRouter
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 fetch_ha_data
from server.services.ha_service import call_ha_service, fetch_ha_data
logger = logging.getLogger(__name__)
@ -18,22 +20,18 @@ 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.
"""Return Home Assistant entity data (cached)."""
The exact shape depends on what ``fetch_ha_data`` returns; on failure an
error stub is returned instead::
{ "error": true, "message": "..." }
"""
# --- cache hit? -----------------------------------------------------------
cached = await cache.get(CACHE_KEY)
if cached is not None:
return cached
# --- cache miss -----------------------------------------------------------
try:
data: Dict[str, Any] = await fetch_ha_data(
get_settings().ha_url,
@ -45,3 +43,41 @@ async def get_ha() -> Dict[str, Any]:
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