from django.db import models from decimal import Decimal from sitemanagement.constants.image_file_path import register_object_upload_path class Multiplexor(models.Model): """Устройство-мультиплексор""" name = models.CharField(max_length=50, unique=True) ip = models.GenericIPAddressField(null=True, blank=True) subnet = models.GenericIPAddressField(null=True, blank=True) gateway = models.GenericIPAddressField(null=True, blank=True) sd_path = models.CharField(max_length=255, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Мультиплексор" verbose_name_plural = "Мультиплексоры" ordering = ["name"] def __str__(self): return self.name class Channel(models.Model): """Физический канал мультиплексора""" multiplexor = models.ForeignKey(Multiplexor, on_delete=models.CASCADE, related_name="channels") number = models.PositiveSmallIntegerField() # CH-1 ... CH-14 # для "полканалов" можно использовать дробное значение (1, 1.5, 2, ...) position = models.DecimalField(max_digits=4, decimal_places=1, default=Decimal("1.0")) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ("multiplexor", "number", "position") verbose_name = "Канал" verbose_name_plural = "Каналы" ordering = ["multiplexor", "number", "position"] def __str__(self): return f"{self.multiplexor.name} - CH{self.number} ({self.position})" class SensorType(models.Model): """Тип датчика: GA, PE, GLE""" code = models.CharField(max_length=10, unique=True) # GA, PE, GLE name = models.CharField(max_length=100) description = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # пороговые значения для всех сенсоров этого типа min_value = models.DecimalField(max_digits=12, decimal_places=4, null=True, blank=True) max_value = models.DecimalField(max_digits=12, decimal_places=4, null=True, blank=True) class Meta: verbose_name = "Тип сенсора" verbose_name_plural = "Типы сенсоров" ordering = ["name"] def __str__(self): return self.code class SignalFormat(models.Model): """Формат сигнала и правило преобразования""" sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE, related_name="formats") code = models.CharField(max_length=50, verbose_name="Код") # например "4-20мА", "VW f<1600Hz", "NTC R>250Ohm" unit = models.CharField(max_length=20, blank=True, null=True, verbose_name="Единица измерения") # °C, мкм/м, мм и т.п. conversion_rule = models.CharField(max_length=255, blank=True, null=True, verbose_name="Правило преобразования") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ("sensor_type", "code") verbose_name = "Формат сигнала" verbose_name_plural = "Форматы сигналов" ordering = ["sensor_type", "code"] def __str__(self): return f"{self.sensor_type.code} - {self.code}" class Sensor(models.Model): """Конкретный датчик, установленный в канале""" channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name="sensors") sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE, verbose_name="Тип сенсора") signal_format = models.ForeignKey(SignalFormat, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Формат сигнала") serial_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Серийный номер") # CL 2106009 name = models.CharField(max_length=50, blank=True, null=True, verbose_name="Название") # GA-1, HLE-1 и т.п. created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) math_formula = models.CharField(null=True, blank=True, max_length=255, verbose_name="Математическая формула") class Meta: verbose_name = "Датчик" verbose_name_plural = "Датчики" ordering = ["channel", "sensor_type", "serial_number"] def __str__(self): return f"{self.name or self.sensor_type.code} ({self.serial_number})" class Metric(models.Model): """Значения, которые приходят из CSV""" timestamp = models.DateTimeField() sensor = models.ForeignKey(Sensor, on_delete=models.CASCADE, related_name="metrics", verbose_name="Датчик") raw_value = models.CharField(max_length=50, verbose_name="Исходное значение") # исходное значение из файла (например "11.964 (A)") value = models.FloatField(null=True, blank=True, verbose_name="Преобразованное значение") # преобразованное значение status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус") # No Rx, Error, NotAv и т.д. created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Метрика" verbose_name_plural = "Метрики" ordering = ["timestamp", "sensor"] indexes = [ models.Index(fields=["timestamp"]), models.Index(fields=["sensor"]), ] def __str__(self): return f"{self.timestamp} {self.sensor} = {self.value} {self.sensor.signal_format.unit if self.sensor.signal_format else ''}" class Alert(models.Model): """Тревоги по метрикам""" sensor = models.ForeignKey(Sensor, on_delete=models.CASCADE, related_name="alerts", verbose_name="Датчик") metric = models.ForeignKey(Metric, on_delete=models.CASCADE, related_name="alerts", verbose_name="Метрика") sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE, related_name="alerts", verbose_name="Тип сенсора") message = models.CharField(max_length=255, verbose_name="Сообщение") severity = models.CharField( max_length=20, choices=[ ("warning", "Warning"), ("critical", "Critical"), ], default="warning", verbose_name="Уровень тревоги" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) resolved = models.BooleanField(default=False, verbose_name="Статус обработки") class Meta: indexes = [ models.Index(fields=["created_at"]), models.Index(fields=["sensor"]), models.Index(fields=["sensor_type"]), ] verbose_name = "Тревога" verbose_name_plural = "Тревоги" ordering = ["created_at", "sensor"] def __str__(self): return f"ALERT {self.sensor} @ {self.metric.timestamp}: {self.message}" class Object(models.Model): """Объект""" title = models.CharField(max_length=255, verbose_name="Название") description = models.TextField(blank=True, null=True, verbose_name="Описание") image = models.ImageField(upload_to=register_object_upload_path, null=True, blank=True, verbose_name="Изображение") address = models.CharField(max_length=255, verbose_name="Адрес") floors = models.PositiveSmallIntegerField(verbose_name="Количество этажей") area = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Площадь") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Объект" verbose_name_plural = "Объекты" ordering = ["title"] def __str__(self): return self.title class Zone(models.Model): """Зона""" object = models.ForeignKey(Object, on_delete=models.CASCADE, related_name="zones", verbose_name="Объект") name = models.CharField(max_length=255, verbose_name="Название") floor = models.PositiveSmallIntegerField(verbose_name="Этаж") image_path = models.CharField(max_length=255, verbose_name="Путь к изображению", blank=True, default="test_image.png", help_text="Например 'test_image_2.png'") model_path = models.CharField(max_length=255, verbose_name="Путь к 3D модели", blank=True, null=True, help_text="Например '/static-models/AerBIM-Monitor_ASM-HTViewer_Expo2017Astana_20250908_L_+76190.glb'") order = models.PositiveSmallIntegerField(default=0, verbose_name="Порядок") sensors = models.ManyToManyField(Sensor, related_name="zones", verbose_name="Датчики") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Зона" verbose_name_plural = "Зоны" ordering = ["object", "floor", "order", "name"] # сортировка по объекту, этажу, порядку, названию def clean(self): from django.core.exceptions import ValidationError # проверяем что номер этажа не превышает количество этажей в здании if self.floor > self.object.floors: raise ValidationError({ 'floor': f'Номер этажа не может быть больше количества этажей в здании ({self.object.floors})' }) def __str__(self): return f"{self.object.title} - Этаж {self.floor} - {self.name}"