"""Instancia activa de alarma (estado runtime, no configuración). La **configuración** de una alarma vive en `AlarmConfig` (ver `tag.py`). La **instancia** de una alarma — qué se disparó, cuándo, quién acuso recibo — vive aquí. Estas instancias son las que persiste el Runtime en su tabla de alarmas y las que la API WebSocket transmite en mensajes `alarm_event`. """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, model_validator from vmssailor.core.enums import AlarmPriority, AlarmState class Alarm(BaseModel): """Una alarma activa o histórica en el Runtime.""" model_config = ConfigDict(extra="forbid") id: str = Field(..., min_length=1, max_length=128, description="UUID o ID determinista.") tag_id: str = Field(..., min_length=1, max_length=128, description="Tag que disparó la alarma.") alarm_config_id: str = Field( ..., min_length=1, max_length=128, description="ID del AlarmConfig que se evaluó como verdadero.", ) priority: AlarmPriority state: AlarmState timestamp_active: datetime = Field( ..., description="Cuándo se disparó (entró en ACTIVE)." ) timestamp_ack: datetime | None = Field( default=None, description="Cuándo el operador hizo ack. None si nunca." ) timestamp_cleared: datetime | None = Field( default=None, description="Cuándo la condición se resolvió. None si sigue presente.", ) acknowledged_by: str | None = Field( default=None, max_length=128, description="Usuario que hizo ack. None si nunca.", ) message: str = Field(default="", max_length=512) value_at_trigger: float | None = Field( default=None, description="Valor del tag al momento del disparo (snapshot para logbook).", ) @model_validator(mode="after") def _state_timestamps_consistency(self) -> Alarm: if self.state == AlarmState.ACK and self.timestamp_ack is None: raise ValueError( "Alarma en estado ACK requiere timestamp_ack." ) if self.state == AlarmState.CLEARED and self.timestamp_cleared is None: raise ValueError( "Alarma en estado CLEARED requiere timestamp_cleared." ) if ( self.timestamp_ack is not None and self.timestamp_ack < self.timestamp_active ): raise ValueError("timestamp_ack debe ser ≥ timestamp_active.") if ( self.timestamp_cleared is not None and self.timestamp_cleared < self.timestamp_active ): raise ValueError("timestamp_cleared debe ser ≥ timestamp_active.") if ( self.acknowledged_by is not None and self.timestamp_ack is None ): raise ValueError( "acknowledged_by sin timestamp_ack es inconsistente." ) return self