from __future__ import annotations from datetime import datetime, timezone from enum import StrEnum from pydantic import BaseModel, Field from app.ha.discovery import EntityRole class AssignmentSource(StrEnum): NONE = "none" AUTOMATIC = "automatic" MANUAL = "manual" class LifecycleStatus(StrEnum): PENDING_ASSIGNMENT = "pending_assignment" REVIEW_REQUIRED = "review_required" PENDING_HISTORY = "pending_history" TRAINED = "trained" STALE = "stale" INVALID = "invalid" ORPHANED = "orphaned" ARCHIVED = "archived" class BehaviorMode(StrEnum): SHADOW = "shadow" ACTIVE = "active" PAUSED = "paused" class BehaviorStatus(StrEnum): COLLECTING = "collecting" TRAINED = "trained" BLOCKED = "blocked" class AssignmentCandidate(BaseModel): entity_id: str domain: str role: EntityRole device_class: str | None = None state_class: str | None = None unit_of_measurement: str | None = None friendly_name: str | None = None area_name: str | None = None device_name: str | None = None score: float = Field(ge=0.0) confidence: float = Field(ge=0.0, le=1.0) auto_accepted: bool = False evidence: list[str] = Field(default_factory=list) class AssignmentSelection(BaseModel): selected_numeric_entity_id: str | None = None selected_context_entity_ids: list[str] = Field(default_factory=list) source: AssignmentSource = AssignmentSource.NONE confidence: float = Field(default=0.0, ge=0.0, le=1.0) review_required: bool = True reason: str = "Noch keine Zuordnung vorhanden." class ManualOverride(BaseModel): numeric_entity_id: str | None = None context_entity_ids: list[str] = Field(default_factory=list) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) note: str | None = None class LifecycleAuditEntry(BaseModel): at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) action: str = Field(min_length=1, max_length=120) reason: str = Field(min_length=1, max_length=500) class ModelLifecycleState(BaseModel): model_id: str status: LifecycleStatus = LifecycleStatus.PENDING_ASSIGNMENT last_reconciled_at: datetime | None = None last_trained_at: datetime | None = None last_history_signature: str | None = None last_history_point_count: int = Field(default=0, ge=0) reason: str = "Noch keine Trainingsdaten ausgewertet." next_action: str = "Aktor auswählen; Kontext und Historie werden automatisch geprüft." audit: list[LifecycleAuditEntry] = Field(default_factory=list) class BehaviorPattern(BaseModel): target_state: str = Field(min_length=1, max_length=100) minute_of_day: int = Field(ge=0, le=1439) weekday: int = Field(ge=0, le=6) context_states: dict[str, str] = Field(default_factory=dict) source: str = Field(default="observed", max_length=40) weight: float = Field(default=1.0, ge=0.1, le=1.0) observed_at: datetime class BehaviorPrediction(BaseModel): target_state: str confidence: float = Field(ge=0.0, le=1.0) generated_at: datetime reason: str matching_patterns: int = Field(default=0, ge=0) executed: bool = False class ExecutionEvent(BaseModel): target_state: str executed_at: datetime class BehaviorState(BaseModel): mode: BehaviorMode = BehaviorMode.SHADOW status: BehaviorStatus = BehaviorStatus.COLLECTING approved_at: datetime | None = None sample_count: int = Field(default=0, ge=0) high_confidence_sample_count: int = Field(default=0, ge=0) patterns: list[BehaviorPattern] = Field(default_factory=list) prediction: BehaviorPrediction | None = None last_trained_at: datetime | None = None last_evaluated_at: datetime | None = None last_executed_at: datetime | None = None execution_events: list[ExecutionEvent] = Field(default_factory=list) reason: str = "Historische Aktorhandlungen werden analysiert." class ActuatorRecord(BaseModel): actuator_entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$") enabled: bool = True created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) assignment: AssignmentSelection = Field(default_factory=AssignmentSelection) manual_override: ManualOverride | None = None numeric_candidates: list[AssignmentCandidate] = Field(default_factory=list) context_candidates: list[AssignmentCandidate] = Field(default_factory=list) lifecycle: ModelLifecycleState behavior: BehaviorState = Field(default_factory=BehaviorState) class ReconciliationState(BaseModel): last_started_at: datetime | None = None last_completed_at: datetime | None = None last_trigger: str | None = None running: bool = False configured_actuators: int = Field(default=0, ge=0) review_required: int = Field(default=0, ge=0) trained_models: int = Field(default=0, ge=0) last_summary: str = "Noch keine Reconciliation ausgeführt." def model_id_for_actuator(actuator_entity_id: str) -> str: return f"actuator.{actuator_entity_id}"