обновление бизнес логики
This commit is contained in:
@@ -44,8 +44,11 @@ class SensorView(APIView):
|
||||
)])})
|
||||
@handle_exceptions
|
||||
def get(self, request):
|
||||
"""Получение всех датчиков"""
|
||||
"""Получение всех датчиков или датчиков конкретной зоны"""
|
||||
try:
|
||||
# Получаем опциональный параметр zone_id из query string
|
||||
zone_id = request.query_params.get('zone_id', None)
|
||||
|
||||
sensors = Sensor.objects.select_related(
|
||||
'sensor_type',
|
||||
'signal_format'
|
||||
@@ -53,7 +56,14 @@ class SensorView(APIView):
|
||||
'zones',
|
||||
'zones__object',
|
||||
'alerts'
|
||||
).all()
|
||||
)
|
||||
|
||||
# Фильтруем по зоне если zone_id передан
|
||||
if zone_id:
|
||||
sensors = sensors.filter(zones__id=zone_id)
|
||||
print(f"[SensorView] Filtering by zone_id: {zone_id}")
|
||||
|
||||
sensors = sensors.all()
|
||||
|
||||
total_count = sensors.count()
|
||||
print(f"[SensorView] Total sensors in DB: {total_count}")
|
||||
|
||||
81
backend/api/account/views/sensors_views — копия 2.py
Normal file
81
backend/api/account/views/sensors_views — копия 2.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample
|
||||
|
||||
from api.account.serializers.sensor_serializers import DetectorsResponseSerializer
|
||||
from sitemanagement.models import Sensor
|
||||
|
||||
from api.utils.decorators import handle_exceptions
|
||||
|
||||
@extend_schema(tags=['Датчики'])
|
||||
class SensorView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = DetectorsResponseSerializer
|
||||
pagination_class = None # Отключаем пагинацию для получения всех датчиков
|
||||
|
||||
@extend_schema(
|
||||
summary="Получение всех датчиков",
|
||||
description="Получение всех датчиков в формате детекторов",
|
||||
responses={200: OpenApiResponse(response=DetectorsResponseSerializer, description="Датчики успешно получены",
|
||||
examples=[OpenApiExample(
|
||||
'Успешный ответ',
|
||||
value={
|
||||
|
||||
"id": 1,
|
||||
"type": "fire_detector",
|
||||
"name": "Датчик 1",
|
||||
"object": "Объект 1",
|
||||
"status": "warning",
|
||||
"zone": "Зона 1",
|
||||
"serial_number": "GA-123",
|
||||
"floor": 1,
|
||||
"notifications": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "warning",
|
||||
"timestamp": "2024-01-15T14:30:00Z",
|
||||
"acknowledged": False,
|
||||
"priority": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
)])})
|
||||
@handle_exceptions
|
||||
def get(self, request):
|
||||
"""Получение всех датчиков"""
|
||||
try:
|
||||
sensors = Sensor.objects.select_related(
|
||||
'sensor_type',
|
||||
'signal_format'
|
||||
).prefetch_related(
|
||||
'zones',
|
||||
'zones__object',
|
||||
'alerts'
|
||||
).all()
|
||||
|
||||
total_count = sensors.count()
|
||||
print(f"[SensorView] Total sensors in DB: {total_count}")
|
||||
|
||||
# Проверяем уникальность serial_number
|
||||
serial_numbers = [s.serial_number for s in sensors if s.serial_number]
|
||||
unique_serials = set(serial_numbers)
|
||||
print(f"[SensorView] Unique serial_numbers: {len(unique_serials)} out of {len(serial_numbers)}")
|
||||
|
||||
if len(serial_numbers) != len(unique_serials):
|
||||
from collections import Counter
|
||||
duplicates = {k: v for k, v in Counter(serial_numbers).items() if v > 1}
|
||||
print(f"[SensorView] WARNING: Found duplicate serial_numbers: {duplicates}")
|
||||
|
||||
serializer = DetectorsResponseSerializer(sensors)
|
||||
detectors_dict = serializer.data.get('detectors', {})
|
||||
|
||||
print(f"[SensorView] Serialized detectors count: {len(detectors_dict)}")
|
||||
print(f"[SensorView] Sample detector_ids: {list(detectors_dict.keys())[:5]}")
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Sensor.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Датчики не найдены"},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-03 21:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sitemanagement', '0010_alter_zone_image_path_alter_zone_model_path'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sensortype',
|
||||
name='code',
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -40,7 +40,7 @@ class Channel(models.Model):
|
||||
|
||||
class SensorType(models.Model):
|
||||
"""Тип датчика: GA, PE, GLE"""
|
||||
code = models.CharField(max_length=10, unique=True) # GA, PE, GLE
|
||||
code = models.CharField(max_length=255, 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)
|
||||
|
||||
200
backend/sitemanagement/models — копия.py
Normal file
200
backend/sitemanagement/models — копия.py
Normal file
@@ -0,0 +1,200 @@
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user