410 lines
15 KiB
Python
410 lines
15 KiB
Python
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 SafetyStage(StrEnum):
|
|
OBSERVE = "observe"
|
|
SUGGEST = "suggest"
|
|
SHADOW = "shadow"
|
|
PARTIAL = "partial"
|
|
ACTIVE = "active"
|
|
|
|
|
|
class JobStatus(StrEnum):
|
|
PENDING = "pending"
|
|
RUNNING = "running"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
class FeedbackKind(StrEnum):
|
|
CORRECT = "correct"
|
|
WRONG = "wrong"
|
|
TOO_EARLY = "too_early"
|
|
TOO_LATE = "too_late"
|
|
NEVER_AUTOMATE = "never_automate"
|
|
|
|
|
|
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)
|
|
manual_weight: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
effective_weight: float = Field(default=1.0, 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 SensorWeightGroup(BaseModel):
|
|
group_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
|
|
name: str = Field(min_length=1, max_length=120)
|
|
entity_ids: list[str] = Field(default_factory=list)
|
|
weight: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
|
|
|
|
class ManualOverride(BaseModel):
|
|
numeric_entity_id: str | None = None
|
|
context_entity_ids: list[str] = Field(default_factory=list)
|
|
sensor_weights: dict[str, float] = Field(default_factory=dict)
|
|
sensor_weight_groups: list[SensorWeightGroup] = 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)
|
|
trigger_entity_id: str | None = None
|
|
trigger_from_state: str | None = None
|
|
trigger_to_state: str | None = None
|
|
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
|
|
execution_reason: str = "Vorhersage wurde noch nicht ausgeführt."
|
|
|
|
|
|
class DecisionFactor(BaseModel):
|
|
entity_id: str | None = None
|
|
label: str
|
|
factor_type: str = Field(max_length=40)
|
|
state: str | None = None
|
|
weight: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
contribution: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
evidence: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class SimulationOutcome(BaseModel):
|
|
scenario_id: str = Field(pattern=r"^[a-z0-9_.-]{1,120}$")
|
|
actuator_entity_id: str
|
|
sensor_states: dict[str, str] = Field(default_factory=dict)
|
|
sensor_weights: dict[str, float] = Field(default_factory=dict)
|
|
prediction: BehaviorPrediction | None = None
|
|
decision_factors: list[DecisionFactor] = Field(default_factory=list)
|
|
would_execute: bool = False
|
|
blockers: list[str] = Field(default_factory=list)
|
|
score: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
recommendation: str = Field(default="", max_length=700)
|
|
|
|
|
|
class DecisionTrace(BaseModel):
|
|
trace_id: str = Field(pattern=r"^[a-z0-9_.-]{1,120}$")
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
trigger_entity_id: str | None = None
|
|
trigger_state: str | None = None
|
|
target_state: str | None = None
|
|
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
executed: bool = False
|
|
blocked: bool = False
|
|
reason: str = Field(default="", max_length=700)
|
|
blockers: list[str] = Field(default_factory=list)
|
|
duration_ms: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
class LatencyMeasurement(BaseModel):
|
|
measured_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
trigger_entity_id: str | None = None
|
|
event_to_decision_ms: int | None = Field(default=None, ge=0)
|
|
decision_to_service_ms: int | None = Field(default=None, ge=0)
|
|
event_to_done_ms: int | None = Field(default=None, ge=0)
|
|
executed: bool = False
|
|
source: str = Field(default="manual", max_length=40)
|
|
|
|
|
|
class AdaptiveWeightUpdate(BaseModel):
|
|
entity_id: str
|
|
previous_weight: float = Field(ge=0.0, le=1.0)
|
|
new_weight: float = Field(ge=0.0, le=1.0)
|
|
reason: str = Field(max_length=300)
|
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class SafetyRule(BaseModel):
|
|
rule_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
|
|
label: str = Field(min_length=1, max_length=160)
|
|
enabled: bool = True
|
|
blocking: bool = True
|
|
reason: str = Field(default="", max_length=300)
|
|
|
|
|
|
def default_safety_rules() -> list[SafetyRule]:
|
|
return [
|
|
SafetyRule(
|
|
rule_id="activation_ready",
|
|
label="Nur nach Lernfreigabe aktiv schalten",
|
|
reason="Der Aktor muss genug eindeutiges Verhalten gelernt haben.",
|
|
),
|
|
SafetyRule(
|
|
rule_id="confidence_threshold",
|
|
label="Mindest-Sicherheit einhalten",
|
|
reason="Vorhersagen unter der Schaltschwelle bleiben im Shadow-Modus.",
|
|
),
|
|
SafetyRule(
|
|
rule_id="cooldown",
|
|
label="Sicherheits-Cooldown gegen Hin-und-her-Schalten",
|
|
reason="Gleiche Zielzustände werden nicht zu schnell wiederholt.",
|
|
),
|
|
SafetyRule(
|
|
rule_id="manual_block",
|
|
label="Manuelle Sperre respektieren",
|
|
reason="Nutzer können jeden Aktor sofort blockieren.",
|
|
),
|
|
]
|
|
|
|
|
|
class SafetyProfile(BaseModel):
|
|
stage: SafetyStage = SafetyStage.SHADOW
|
|
manual_block: bool = False
|
|
min_confidence: float = Field(default=0.82, ge=0.0, le=1.0)
|
|
min_confidence_on: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
min_confidence_off: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
cooldown_seconds: int | None = Field(default=None, ge=0)
|
|
rules: list[SafetyRule] = Field(default_factory=default_safety_rules)
|
|
updated_at: datetime | None = None
|
|
note: str | None = Field(default=None, max_length=500)
|
|
|
|
|
|
class ExecutionEvent(BaseModel):
|
|
target_state: str
|
|
executed_at: datetime
|
|
|
|
|
|
class ModelSnapshot(BaseModel):
|
|
version_id: str
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
sample_count: int = Field(default=0, ge=0)
|
|
high_confidence_sample_count: int = Field(default=0, ge=0)
|
|
average_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
incorrect_feedback_count: int = Field(default=0, ge=0)
|
|
patterns: list[BehaviorPattern] = Field(default_factory=list)
|
|
reason: str = Field(default="", max_length=500)
|
|
|
|
|
|
class AutomationConflict(BaseModel):
|
|
automation_entity_id: str = Field(pattern=r"^automation\.[a-z0-9_]+$")
|
|
severity: str = Field(default="info", max_length=20)
|
|
status: str = Field(default="open", max_length=40)
|
|
reason: str = Field(max_length=500)
|
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class AnomalyEvent(BaseModel):
|
|
anomaly_id: str = Field(pattern=r"^[a-z0-9_.-]{1,120}$")
|
|
severity: str = Field(default="info", max_length=20)
|
|
category: str = Field(max_length=40)
|
|
title: str = Field(min_length=1, max_length=160)
|
|
detail: str = Field(min_length=1, max_length=500)
|
|
detected_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
resolved: bool = False
|
|
|
|
|
|
class TimeProfile(BaseModel):
|
|
profile_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
|
|
label: str = Field(min_length=1, max_length=80)
|
|
sample_count: int = Field(default=0, ge=0)
|
|
dominant_state: str | None = None
|
|
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
|
|
|
|
class RelatedAutomation(BaseModel):
|
|
entity_id: str = Field(pattern=r"^automation\.[a-z0-9_]+$")
|
|
config_id: str = Field(min_length=1, max_length=120)
|
|
friendly_name: str = Field(min_length=1, max_length=200)
|
|
enabled: bool
|
|
|
|
|
|
class ActuatorGroup(BaseModel):
|
|
group_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
|
|
name: str = Field(min_length=1, max_length=120)
|
|
area_name: str | None = Field(default=None, max_length=120)
|
|
member_entity_ids: list[str] = Field(default_factory=list)
|
|
reason: str = Field(default="", max_length=300)
|
|
|
|
|
|
class SceneSuggestion(BaseModel):
|
|
scene_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
|
|
label: str = Field(min_length=1, max_length=120)
|
|
member_entity_ids: list[str] = Field(default_factory=list)
|
|
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
reason: str = Field(default="", max_length=500)
|
|
last_seen_at: datetime | None = None
|
|
|
|
|
|
class AgentInsight(BaseModel):
|
|
insight_id: str = Field(pattern=r"^[a-z0-9_.-]{1,120}$")
|
|
severity: str = Field(default="info", max_length=20)
|
|
title: str = Field(min_length=1, max_length=160)
|
|
detail: str = Field(min_length=1, max_length=700)
|
|
action: str | None = Field(default=None, max_length=300)
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
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)
|
|
activation_ready: bool = False
|
|
activation_reason: str = "Noch nicht genügend Verhalten für eine Freigabe gelernt."
|
|
related_automations: list[RelatedAutomation] = Field(default_factory=list)
|
|
paused_automation_entity_ids: list[str] = Field(default_factory=list)
|
|
reason: str = "Historische Aktorhandlungen werden analysiert."
|
|
safety: SafetyProfile = Field(default_factory=SafetyProfile)
|
|
decision_factors: list[DecisionFactor] = Field(default_factory=list)
|
|
knowledge: list[str] = Field(default_factory=list)
|
|
assumptions: list[str] = Field(default_factory=list)
|
|
uncertainties: list[str] = Field(default_factory=list)
|
|
safety_blockers: list[str] = Field(default_factory=list)
|
|
sample_trend: list[int] = Field(default_factory=list)
|
|
confidence_trend: list[float] = Field(default_factory=list)
|
|
correct_feedback_count: int = Field(default=0, ge=0)
|
|
incorrect_feedback_count: int = Field(default=0, ge=0)
|
|
model_snapshots: list[ModelSnapshot] = Field(default_factory=list)
|
|
active_model_version: str | None = None
|
|
adaptive_weight_updates: list[AdaptiveWeightUpdate] = Field(default_factory=list)
|
|
automation_conflicts: list[AutomationConflict] = Field(default_factory=list)
|
|
time_profiles: list[TimeProfile] = Field(default_factory=list)
|
|
anomalies: list[AnomalyEvent] = Field(default_factory=list)
|
|
decision_timeline: list[DecisionTrace] = Field(default_factory=list)
|
|
latency_measurements: list[LatencyMeasurement] = Field(default_factory=list)
|
|
feedback_log: list[FeedbackKind] = Field(default_factory=list)
|
|
dry_run_enabled: bool = False
|
|
dry_run_started_at: datetime | None = None
|
|
dry_run_sample_count: int = Field(default=0, ge=0)
|
|
dry_run_hit_count: int = Field(default=0, ge=0)
|
|
actuator_groups: list[ActuatorGroup] = Field(default_factory=list)
|
|
scene_suggestions: list[SceneSuggestion] = Field(default_factory=list)
|
|
agent_insights: list[AgentInsight] = Field(default_factory=list)
|
|
|
|
|
|
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."
|
|
|
|
|
|
class JobQueueItem(BaseModel):
|
|
job_id: str = Field(min_length=1, max_length=120)
|
|
kind: str = Field(min_length=1, max_length=40)
|
|
target: str | None = Field(default=None, max_length=160)
|
|
trigger: str = Field(default="manual", max_length=40)
|
|
status: JobStatus = JobStatus.PENDING
|
|
started_at: datetime | None = None
|
|
completed_at: datetime | None = None
|
|
duration_ms: int | None = Field(default=None, ge=0)
|
|
error: str | None = Field(default=None, max_length=500)
|
|
summary: str = Field(default="", max_length=500)
|
|
|
|
|
|
class JobQueueState(BaseModel):
|
|
jobs: list[JobQueueItem] = Field(default_factory=list)
|
|
|
|
|
|
def model_id_for_actuator(actuator_entity_id: str) -> str:
|
|
return f"actuator.{actuator_entity_id}"
|