"""Generación de IDs deterministas para el modelo de datos. Reglas: - IDs de Tag son **deterministas**: `{equipment.tag_prefix}.{sensor.id_upper}` para que un mismo proyecto produzca siempre el mismo grafo de tags. - IDs aleatorios de proyecto/equipo usan UUID4 si el integrador no provee uno. - IDs de Alarm (instancia activa) son timestamps + tag_id en hash corto. """ from __future__ import annotations import re import uuid _TAG_PREFIX_RE = re.compile(r"^[A-Z][A-Z0-9_]*$") _SENSOR_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") def make_tag_id(equipment_tag_prefix: str, sensor_id: str) -> str: """Compone un Tag.id determinista: 'ME_PORT' + 'oil_press' → 'ME_PORT.OIL_PRESS'. Reglas: - `equipment_tag_prefix` debe ser MAYÚSCULAS y empezar con letra. - `sensor_id` debe ser snake_case minúsculas. - Resultado: prefix + "." + sensor.upper(). """ if not _TAG_PREFIX_RE.match(equipment_tag_prefix): raise ValueError( f"equipment_tag_prefix '{equipment_tag_prefix}' debe ser MAYÚSCULAS " "[A-Z][A-Z0-9_]*." ) if not _SENSOR_ID_RE.match(sensor_id): raise ValueError( f"sensor_id '{sensor_id}' debe ser snake_case [a-z][a-z0-9_]*." ) return f"{equipment_tag_prefix}.{sensor_id.upper()}" def make_alarm_config_id(tag_id: str, level_name: str) -> str: """ID determinista para un AlarmConfig: 'ME_PORT.OIL_PRESS.LOW'.""" if not level_name.replace("_", "").isalnum(): raise ValueError(f"level_name '{level_name}' inválido.") return f"{tag_id}.{level_name.upper()}" def make_alarm_instance_id(tag_id: str, alarm_config_id: str, timestamp_unix: float) -> str: """ID determinista para una INSTANCIA activa de alarma. Garantiza unicidad por timestamp y por config. """ short = hex(int(timestamp_unix * 1000))[2:][-10:] return f"alm.{alarm_config_id}.{short}" def new_uuid() -> str: """UUID4 para IDs de proyecto, equipos, cartas sin nombre semántico.""" return str(uuid.uuid4()) def make_project_id(customer_slug: str, vessel_id: str) -> str: """Compone un Project.id estable a partir de cliente + buque.""" customer = re.sub(r"[^a-z0-9]+", "_", customer_slug.lower()).strip("_") vessel = re.sub(r"[^a-z0-9_-]+", "_", vessel_id.lower()).strip("_") if not customer or not vessel: raise ValueError("customer_slug y vessel_id no pueden quedar vacíos tras normalizar.") return f"{customer}__{vessel}"