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 ProposalStatus(StrEnum): DRAFT = "draft" APPROVED = "approved" REJECTED = "rejected" class JobStatus(StrEnum): QUEUED = "queued" RUNNING = "running" SUCCEEDED = "succeeded" FAILED = "failed" class CandidateStatus(StrEnum): PROPOSED = "proposed" ACCEPTED = "accepted" DISMISSED = "dismissed" 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 AutomationProposal(BaseModel): proposal_id: str name: str trigger_entity_id: str trigger_state: str | None = None actuator_entity_id: str target_state: str status: ProposalStatus = ProposalStatus.DRAFT revision: int = 1 created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) decided_at: datetime | None = None class ModelRecord(BaseModel): model_id: str actuator_entity_id: str pattern_count: int confidence: float = Field(default=0.0, ge=0.0, le=1.0) trained_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class ModelEvaluation(BaseModel): evaluation_id: str model_id: str actuator_entity_id: str score: float = Field(default=0.0, ge=0.0, le=1.0) coverage: float = Field(default=0.0, ge=0.0, le=1.0) dry_run_success_rate: float = Field(default=0.0, ge=0.0, le=1.0) feedback_score: float = Field(default=0.0, ge=0.0, le=1.0) verdict: str reasons: list[str] = Field(default_factory=list) evaluated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class JobQueueItem(BaseModel): job_id: str kind: str status: JobStatus = JobStatus.QUEUED message: str = "" created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) finished_at: datetime | None = None class SensorWeightOverride(BaseModel): actuator_entity_id: str sensor_weights: dict[str, float] = Field(default_factory=dict) note: str | None = Field(default=None, max_length=500) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class AutopilotSettings(BaseModel): enabled: bool = True interval_seconds: int = Field(default=3600, ge=300) min_candidate_confidence: float = Field(default=0.65, ge=0.0, le=1.0) auto_train: bool = True auto_evaluate: bool = True auto_activate: bool = False last_run_at: datetime | None = None class CandidateRecommendation(BaseModel): candidate_id: str actuator_entity_id: str trigger_entity_id: str trigger_state: str | None = None target_state: str confidence: float = Field(default=0.0, ge=0.0, le=1.0) reason: str status: CandidateStatus = CandidateStatus.PROPOSED created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class HistoryAnalysis(BaseModel): entity_id: str samples: int last_state: str | None = None changed_at: datetime | None = None unique_states: int = 0 transitions: int = 0 recommendation: str 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) automation_proposals: dict[str, AutomationProposal] = Field(default_factory=dict) models: dict[str, ModelRecord] = Field(default_factory=dict) model_evaluations: dict[str, ModelEvaluation] = Field(default_factory=dict) jobs: dict[str, JobQueueItem] = Field(default_factory=dict) weight_overrides: dict[str, SensorWeightOverride] = Field(default_factory=dict) candidates: dict[str, CandidateRecommendation] = Field(default_factory=dict) class ControlState(BaseModel): profiles: dict[str, ControlProfile] = Field(default_factory=dict) global_enabled: bool = True autopilot: AutopilotSettings = Field(default_factory=AutopilotSettings) class BackupBundle(BaseModel): exported_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) runtime: RuntimeState learning: LearningState control: ControlState