"""Equipos del proyecto y modelos catalogados en la biblioteca.""" from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, field_validator from vmssailor.core.coords import ShipCoord from vmssailor.core.enums import ( AlarmPriority, EquipmentCategory, SignalType, SystemId, UnitSI, ) class Sensor(BaseModel): """Sensor típico que pertenece a un EquipmentModel. Esta es la "plantilla" del sensor declarada en la biblioteca curada. Cuando un proyecto instancia un Equipment, los sensores se traducen a `Tag`s concretos con su binding físico. """ model_config = ConfigDict(extra="forbid") id: str = Field(..., min_length=1, max_length=64, description="Snake_case, ej: 'oil_press'.") name: str = Field(..., min_length=1, max_length=128, description="Nombre humano.") unit_si: UnitSI = Field(default=UnitSI.NONE) range_normal_min: float | None = Field( default=None, description="Mínimo del rango normal en unidad SI." ) range_normal_max: float | None = Field( default=None, description="Máximo del rango normal en unidad SI." ) alarm_low_value: float | None = None alarm_low_priority: AlarmPriority | None = None alarm_high_value: float | None = None alarm_high_priority: AlarmPriority | None = None default_signal_type: SignalType | None = Field( default=None, description="Tipo de señal sugerido para conectar a la tarjeta.", ) description: str = Field(default="", max_length=512) @field_validator("range_normal_max") @classmethod def _max_above_min(cls, v: float | None, info) -> float | None: rmin = info.data.get("range_normal_min") if v is not None and rmin is not None and v <= rmin: raise ValueError( "range_normal_max debe ser > range_normal_min " f"(min={rmin}, max={v})." ) return v class EquipmentSpec(BaseModel): """Especificaciones técnicas declarativas del modelo. Mapa flexible para que cada `EquipmentCategory` pueda guardar sus parámetros característicos (potencia para motores, capacidad para tanques, etc.) sin proliferar clases hijas. """ model_config = ConfigDict(extra="allow") power_kw: float | None = Field(default=None, ge=0.0, description="Potencia nominal [kW].") rpm_nominal: float | None = Field(default=None, ge=0.0, description="RPM nominales.") weight_kg: float | None = Field(default=None, ge=0.0, description="Peso [kg].") length_m: float | None = Field(default=None, ge=0.0, description="Longitud [m].") width_m: float | None = Field(default=None, ge=0.0, description="Ancho [m].") height_m: float | None = Field(default=None, ge=0.0, description="Altura [m].") voltage_v: float | None = Field(default=None, ge=0.0, description="Tensión nominal [V].") current_a: float | None = Field(default=None, ge=0.0, description="Corriente nominal [A].") fuel_consumption_lph: float | None = Field( default=None, ge=0.0, description="Consumo a régimen [L/h]." ) capacity_l: float | None = Field(default=None, ge=0.0, description="Capacidad [L].") class EquipmentModel(BaseModel): """Modelo de equipo de la biblioteca curada. Ejemplos: "MTU 12V 2000 M96", "Volvo D13 900hp", "Northern Lights M65C13". Cada modelo declara qué sensores típicamente trae, lo cual permite al wizard del Studio proponer tags automáticamente cuando el integrador selecciona ese modelo. """ model_config = ConfigDict(extra="forbid") id: str = Field(..., min_length=1, max_length=128) manufacturer: str = Field(..., min_length=1, max_length=128) model_name: str = Field(..., min_length=1, max_length=128) category: EquipmentCategory typical_systems: list[SystemId] = Field( default_factory=list, description="Sistemas en los que típicamente aparece este modelo.", ) specs: EquipmentSpec = Field(default_factory=EquipmentSpec) default_sensors: list[Sensor] = Field(default_factory=list) description: str = Field(default="", max_length=2048) data_source: str = Field( default="seed_estimate", description=( "'seed_estimate' | 'manufacturer_datasheet' | 'field_measured' | 'user_input'. " "Ver docs/seed_data_notes.md." ), ) @field_validator("default_sensors") @classmethod def _sensors_unique_ids(cls, v: list[Sensor]) -> list[Sensor]: ids = [s.id for s in v] if len(ids) != len(set(ids)): raise ValueError("Sensor IDs deben ser únicos dentro de un EquipmentModel.") return v class Equipment(BaseModel): """Instancia concreta de un equipo dentro de un proyecto. Por ejemplo: el "Motor principal babor" del Sunseeker 76 del Sr. Pérez es una instancia `Equipment` con `tag_prefix='ME_PORT'` que referencia al `EquipmentModel` 'mtu_12v_2000_m96'. """ model_config = ConfigDict(extra="forbid") id: str = Field(..., min_length=1, max_length=128, description="UUID o snake_case único.") model_ref: str = Field( ..., min_length=1, max_length=128, description="`EquipmentModel.id` referenciado.", ) tag_prefix: str = Field( ..., min_length=1, max_length=32, pattern=r"^[A-Z][A-Z0-9_]*$", description="Prefijo de tags del equipo, MAYÚSCULAS: ME_PORT, GEN_1, BILGE_PUMP_FWD.", ) display_name: str = Field(..., min_length=1, max_length=128) location: ShipCoord deck_id: str | None = Field(default=None, description="ID de la Deck donde está montado.") system_id: SystemId = Field(..., description="Sistema al que pertenece este equipo.") description: str = Field(default="", max_length=1024) installed: bool = Field( default=True, description="Si False, el equipo está planeado pero no instalado todavía.", )