146 lines
5.3 KiB
Python
146 lines
5.3 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, ReconciliationState
|
|
from app.actuators.store import ActuatorStore
|
|
from app.behavior.engine import BehaviorEngine
|
|
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 ActivationRequest(BaseModel):
|
|
active: bool
|
|
|
|
|
|
@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:
|
|
record = _service(request).configure_actuator(
|
|
payload.actuator_entity_id,
|
|
enabled=payload.enabled,
|
|
)
|
|
_behavior(request).train(record.actuator_entity_id)
|
|
return _behavior(request).evaluate(record.actuator_entity_id)
|
|
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}/reconcile", response_model=ActuatorRecord)
|
|
def reconcile_actuator(
|
|
actuator_entity_id: str,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
_service(request).reconcile_actuator(actuator_entity_id, trigger="manual")
|
|
_behavior(request).train(actuator_entity_id)
|
|
return _behavior(request).evaluate(actuator_entity_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/{actuator_entity_id}/evaluate", response_model=ActuatorRecord)
|
|
def evaluate_actuator(
|
|
actuator_entity_id: str,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
return _behavior(request).evaluate(actuator_entity_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/{actuator_entity_id}/activation", response_model=ActuatorRecord)
|
|
def set_activation(
|
|
actuator_entity_id: str,
|
|
payload: ActivationRequest,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
return _behavior(request).set_active(actuator_entity_id, active=payload.active)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, 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:
|
|
state = _service(request).reconcile_all(trigger=trigger)
|
|
_behavior(request).train_all()
|
|
_behavior(request).evaluate_all()
|
|
return state
|
|
|
|
|
|
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
|
|
|
|
|
|
def _behavior(request: Request) -> BehaviorEngine:
|
|
engine = getattr(request.app.state, "behavior_engine", None)
|
|
if not isinstance(engine, BehaviorEngine):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Verhaltenslernen ist nicht initialisiert.",
|
|
)
|
|
return engine
|