from datetime import datetime from typing import Optional from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, TimestampMixin DEVICE_CATEGORIES = ( "main_server", # caps-cs — основной сервер объекта "vm", # виртуалки: БД, Asterisk, RTSP, recognition "router", # MikroTik, IRZ и прочие роутеры "embedded", # пром-ПК стоек (Raspberry Pi) "camera", # камеры (role: plate / face / overview) "io_board", # модули ввода/вывода (Овен МК210-302) "bank_terminal", # банковские терминалы (SberPilot, PAX IM20) "cash_register", # ККТ (PayOnline-01-ФА) "other", # всё остальное: RFID, табло, реле, IP-телефоны ) # Используется только для camera (остальным выставляется "other" или NULL) DEVICE_ROLES = ("plate", "face", "overview", "other") # Физическая локация устройств не привязанных к стойке DEVICE_LOCATIONS = ("server_room", "street") DEVICE_STATUSES = ("online", "offline", "unknown") DEVICE_SOURCES = ("auto_sync", "manual", "csv_import", "sheets_sync", "auto_ssh") class Device(Base, TimestampMixin): __tablename__ = "devices" id: Mapped[int] = mapped_column(Integer, primary_key=True) object_id: Mapped[int] = mapped_column( Integer, ForeignKey("objects.id", ondelete="RESTRICT"), nullable=False, index=True, ) ip: Mapped[str] = mapped_column(String(45), nullable=False) hostname: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) mac: Mapped[Optional[str]] = mapped_column(String(17), nullable=True) # Classification category: Mapped[str] = mapped_column(String(20), nullable=False, default="other") role: Mapped[str] = mapped_column(String(20), nullable=False, default="other") vendor: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) model: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) firmware_version: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # Source tracking source: Mapped[str] = mapped_column(String(20), nullable=False, default="manual") external_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # SSH overrides (NULL = use object defaults) ssh_user_override: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) ssh_pass_override_enc: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) ssh_port_override: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Device-specific metadata (Mikrotik API port, RTSP URL, etc.) device_meta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) # Capabilities populated by drivers capabilities: Mapped[list[str]] = mapped_column( ARRAY(String), nullable=False, default=list ) # Monitoring monitor_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) last_seen: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) last_ping_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="unknown") rack_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("racks.id", ondelete="SET NULL"), nullable=True, index=True, ) # Для устройств не в стойке: "server_room" | "street" | None # rack_id и location взаимоисключают друг друга location: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, index=True) # Параметры подключения для операционной вебки (Вариант В) # camera: { auth_type, username, password, frame_url, stream_url } # bank_terminal: { port, timeout_on_sale } # cash_register: { port, tax, taxation } # embedded: { config_path } # io_board: { port, protocol } connection_params: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) object: Mapped["Object"] = relationship("Object", back_populates="devices") rack: Mapped[Optional["Rack"]] = relationship("Rack", back_populates="devices") config_snapshots: Mapped[list["ConfigSnapshot"]] = relationship( "ConfigSnapshot", back_populates="device", cascade="all, delete-orphan" )