from __future__ import annotations from datetime import datetime, timezone from enum import StrEnum from pydantic import BaseModel, Field class HandoffMode(StrEnum): SHADOW = "shadow" CANDIDATE = "candidate" CONTROLLED = "controlled" CONFLICT = "conflict" ROLLBACK = "rollback" class SafetyStage(StrEnum): OBSERVE = "observe" DRY_RUN = "dry_run" ACTIVE = "active" BLOCKED = "blocked" class EntityState(BaseModel): entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$") domain: str state: str | None = None changed_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) area_name: str | None = None device_id: str | None = None friendly_name: str | None = None class StateEvent(BaseModel): entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$") new_state: str | None = None changed_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) attributes: dict[str, str] = Field(default_factory=dict) class BehaviorPatternV2(BaseModel): actuator_entity_id: str target_state: str trigger_entity_id: str | None = None trigger_state: str | None = None context: dict[str, str] = Field(default_factory=dict) support: int = Field(default=1, ge=1) confidence: float = Field(default=0.0, ge=0.0, le=1.0) source: str = Field(default="observed", max_length=40) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class LearningProfile(BaseModel): actuator_entity_id: str patterns: list[BehaviorPatternV2] = Field(default_factory=list) feedback_positive: int = Field(default=0, ge=0) feedback_negative: int = Field(default=0, ge=0) model_version: str = "empty" class ControlProfile(BaseModel): actuator_entity_id: str stage: SafetyStage = SafetyStage.OBSERVE min_confidence: float = Field(default=0.82, ge=0.0, le=1.0) manual_block: bool = False cooldown_seconds: int = Field(default=900, ge=0) handoff_mode: HandoffMode = HandoffMode.SHADOW related_automation_ids: list[str] = Field(default_factory=list) paused_automation_ids: list[str] = Field(default_factory=list) dry_run_events: int = Field(default=0, ge=0) dry_run_successes: int = Field(default=0, ge=0) dry_run_failures: int = Field(default=0, ge=0) active_ready: bool = False active_readiness_reason: str = "Noch nicht bewertet." class RoomProfile(BaseModel): room_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$") name: str actuator_entity_ids: list[str] = Field(default_factory=list) context_entity_ids: list[str] = Field(default_factory=list) class SceneProfile(BaseModel): scene_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$") name: str actuator_entity_ids: list[str] = Field(default_factory=list) trigger_entity_id: str | None = None confidence: float = Field(default=0.0, ge=0.0, le=1.0) class Decision(BaseModel): actuator_entity_id: str target_state: str | None = None confidence: float = Field(default=0.0, ge=0.0, le=1.0) allowed: bool = False executed: bool = False dry_run: bool = False reason: str blockers: list[str] = Field(default_factory=list) trigger_entity_id: str | None = None created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class AuditEvent(BaseModel): event_id: str kind: str = Field(max_length=40) entity_id: str | None = None message: str = Field(max_length=700) decision: Decision | None = None created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class RuntimeState(BaseModel): entities: dict[str, EntityState] = Field(default_factory=dict) audit: list[AuditEvent] = Field(default_factory=list) websocket_status: str = "unavailable" class LearningState(BaseModel): profiles: dict[str, LearningProfile] = Field(default_factory=dict) rooms: dict[str, RoomProfile] = Field(default_factory=dict) scenes: dict[str, SceneProfile] = Field(default_factory=dict) class ControlState(BaseModel): profiles: dict[str, ControlProfile] = Field(default_factory=dict) global_enabled: bool = True class BackupBundle(BaseModel): exported_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) runtime: RuntimeState learning: LearningState control: ControlState