236 lines
8.4 KiB
Python
236 lines
8.4 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.exceptions import HaClientError
|
|
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
|
|
pause_matching_automations: bool = False
|
|
restore_paused_automations: bool = False
|
|
|
|
|
|
class AutomationControlRequest(BaseModel):
|
|
automation_entity_id: str = Field(pattern=r"^automation\.[a-z0-9_]+$")
|
|
enabled: bool
|
|
|
|
|
|
class ManualAssignmentRequest(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=500)
|
|
|
|
|
|
@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("/context-options", response_model=list[HaEntitySummary])
|
|
def context_options(
|
|
request: Request,
|
|
actuator_entity_id: str | None = Query(default=None, pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$"),
|
|
) -> list[HaEntitySummary]:
|
|
if actuator_entity_id is None:
|
|
return []
|
|
try:
|
|
return _service(request).suggest_context_options(actuator_entity_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@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,
|
|
pause_matching_automations=payload.pause_matching_automations,
|
|
restore_paused_automations=payload.restore_paused_automations,
|
|
)
|
|
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.post("/{actuator_entity_id}/assignment", response_model=ActuatorRecord)
|
|
def set_manual_assignment(
|
|
actuator_entity_id: str,
|
|
payload: ManualAssignmentRequest,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
record = _service(request).set_manual_assignment(
|
|
actuator_entity_id,
|
|
numeric_entity_id=payload.numeric_entity_id,
|
|
context_entity_ids=payload.context_entity_ids,
|
|
note=payload.note,
|
|
)
|
|
_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
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post(
|
|
"/{actuator_entity_id}/related-automations/refresh",
|
|
response_model=ActuatorRecord,
|
|
)
|
|
def refresh_related_automations(
|
|
actuator_entity_id: str,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
return _behavior(request).refresh_related_automations(actuator_entity_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except (ValueError, HaClientError) as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post(
|
|
"/{actuator_entity_id}/related-automations/control",
|
|
response_model=ActuatorRecord,
|
|
)
|
|
def control_related_automation(
|
|
actuator_entity_id: str,
|
|
payload: AutomationControlRequest,
|
|
request: Request,
|
|
) -> ActuatorRecord:
|
|
try:
|
|
return _behavior(request).set_automation_enabled(
|
|
actuator_entity_id,
|
|
payload.automation_entity_id,
|
|
enabled=payload.enabled,
|
|
)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except (ValueError, HaClientError) 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
|