Files
sillyhome-next-dev/app/api/v1/actuators.py

123 lines
4.5 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from app.actuators.lifecycle import ActuatorReconciliationService
from app.actuators.models import ActuatorRecord, ManualOverride, ReconciliationState
from app.actuators.store import ActuatorStore
from app.dependencies import get_ha_reader
from app.ha.discovery import EntityRole
from app.ha.models import HaEntitySummary
from app.ha.reader import HaReader
router = APIRouter(prefix="/v1/actuators", tags=["actuators"])
class ConfigureActuatorRequest(BaseModel):
actuator_entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$")
enabled: bool = True
class OverrideRequest(BaseModel):
numeric_entity_id: str | None = Field(default=None, pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$")
context_entity_ids: list[str] = Field(default_factory=list)
note: str | None = Field(default=None, max_length=300)
clear: bool = False
@router.get("/discovery", response_model=list[HaEntitySummary])
def discover_actuators(ha_reader: HaReader = Depends(get_ha_reader)) -> list[HaEntitySummary]:
entities = {entity.entity_id: entity for entity in ha_reader.read_entities()}
discovered = ha_reader.discover()
actuator_ids = sorted(
entity.entity_id for entity in discovered if entity.role is EntityRole.ACTUATOR
)
return [entities[entity_id] for entity_id in actuator_ids if entity_id in entities]
@router.get("", response_model=list[ActuatorRecord])
def list_configured(request: Request) -> list[ActuatorRecord]:
return _service(request).list_configured()
@router.post("", response_model=ActuatorRecord, status_code=201)
def configure(payload: ConfigureActuatorRequest, request: Request) -> ActuatorRecord:
try:
return _service(request).configure_actuator(
payload.actuator_entity_id,
enabled=payload.enabled,
)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.get("/{actuator_entity_id}", response_model=ActuatorRecord)
def get_actuator(actuator_entity_id: str, request: Request) -> ActuatorRecord:
try:
return _service(request).get_actuator(actuator_entity_id)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.delete("/{actuator_entity_id}", status_code=204)
def delete_actuator(actuator_entity_id: str, request: Request) -> None:
_service(request).delete_actuator(actuator_entity_id)
@router.post("/{actuator_entity_id}/override", response_model=ActuatorRecord)
def set_override(
actuator_entity_id: str,
payload: OverrideRequest,
request: Request,
) -> ActuatorRecord:
override = None if payload.clear else ManualOverride(
numeric_entity_id=payload.numeric_entity_id,
context_entity_ids=payload.context_entity_ids,
note=payload.note,
)
try:
return _service(request).set_override(actuator_entity_id, override)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.post("/{actuator_entity_id}/reconcile", response_model=ActuatorRecord)
def reconcile_actuator(
actuator_entity_id: str,
request: Request,
) -> ActuatorRecord:
try:
return _service(request).reconcile_actuator(actuator_entity_id, trigger="manual")
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.get("/reconciliation/state", response_model=ReconciliationState)
def get_reconciliation_state(request: Request) -> ReconciliationState:
store = getattr(request.app.state, "actuator_store", None)
if not isinstance(store, ActuatorStore):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Actuator Store nicht initialisiert.",
)
return store.load_reconciliation_state()
@router.post("/reconciliation/run", response_model=ReconciliationState)
def run_reconciliation(
request: Request,
trigger: str = Query(default="manual", pattern=r"^[a-z0-9_-]{1,32}$"),
) -> ReconciliationState:
return _service(request).reconcile_all(trigger=trigger)
def _service(request: Request) -> ActuatorReconciliationService:
service = getattr(request.app.state, "actuator_service", None)
if not isinstance(service, ActuatorReconciliationService):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Actuator-Reconciliation nicht initialisiert.",
)
return service