Files
sillyhome-future/app/core/models.py
2026-06-18 17:27:25 +02:00

244 lines
7.7 KiB
Python

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