feat / AEB-62 add additional DB schema for objects logic
This commit is contained in:
@@ -1,31 +1,93 @@
|
||||
from django.contrib import admin
|
||||
from .models import Multiplexor, Channel, SensorType, SignalFormat, Sensor, Metric, Alert
|
||||
from .models import *
|
||||
|
||||
@admin.register(Multiplexor)
|
||||
class MultiplexorAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'ip', 'subnet', 'gateway', 'sd_path')
|
||||
list_filter = ('name', 'ip', 'subnet', 'gateway', 'sd_path')
|
||||
search_fields = ('name', 'ip', 'subnet', 'gateway', 'sd_path')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('ip', 'subnet', 'gateway', 'sd_path')
|
||||
list_display_links = ('name',)
|
||||
|
||||
@admin.register(Channel)
|
||||
class ChannelAdmin(admin.ModelAdmin):
|
||||
list_display = ('multiplexor', 'number', 'position')
|
||||
list_filter = ('multiplexor', 'number', 'position')
|
||||
search_fields = ('multiplexor', 'number', 'position')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('number', 'position')
|
||||
list_display_links = ('multiplexor',)
|
||||
|
||||
@admin.register(SensorType)
|
||||
class SensorTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name', 'description')
|
||||
list_filter = ('code', 'name', 'description')
|
||||
search_fields = ('code', 'name', 'description')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('name', 'description')
|
||||
list_display_links = ('code',)
|
||||
|
||||
|
||||
@admin.register(Metric)
|
||||
class MetricAdmin(admin.ModelAdmin):
|
||||
list_display = ('timestamp', 'sensor', 'raw_value', 'value', 'status')
|
||||
list_filter = ('timestamp', 'sensor', 'status')
|
||||
search_fields = ('timestamp', 'sensor', 'status')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('value', 'status')
|
||||
list_display_links = ('timestamp',)
|
||||
|
||||
@admin.register(Alert)
|
||||
class AlertAdmin(admin.ModelAdmin):
|
||||
list_display = ('sensor', 'metric', 'sensor_type', 'message', 'severity', 'created_at', 'resolved')
|
||||
list_filter = ('sensor', 'metric', 'sensor_type', 'severity')
|
||||
search_fields = ('sensor', 'metric', 'sensor_type', 'severity')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('message', 'severity', 'resolved')
|
||||
list_display_links = ('sensor',)
|
||||
|
||||
@admin.register(SignalFormat)
|
||||
class SignalFormatAdmin(admin.ModelAdmin):
|
||||
list_display = ('sensor_type', 'code', 'unit', 'conversion_rule')
|
||||
list_filter = ('sensor_type', 'code', 'unit', 'conversion_rule')
|
||||
search_fields = ('sensor_type', 'code', 'unit', 'conversion_rule')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('unit', 'conversion_rule')
|
||||
list_display_links = ('sensor_type',)
|
||||
|
||||
@admin.register(Sensor)
|
||||
class SensorAdmin(admin.ModelAdmin):
|
||||
list_display = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
||||
list_display = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
||||
list_filter = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
||||
search_fields = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('signal_format', 'serial_number', 'name', 'math_formula')
|
||||
list_display_links = ('channel',)
|
||||
|
||||
@admin.register(Object)
|
||||
class ObjectAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'description', 'image', 'address', 'floors', 'area')
|
||||
list_filter = ('title', 'description', 'image', 'address', 'floors', 'area')
|
||||
search_fields = ('title', 'description', 'image', 'address', 'floors', 'area')
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('image', 'address', 'floors', 'area')
|
||||
list_display_links = ('title',)
|
||||
|
||||
@admin.register(Zone)
|
||||
class ZoneAdmin(admin.ModelAdmin):
|
||||
list_display = ('object', 'name') # Removed sensors from list_display
|
||||
list_filter = ('object', 'name') # Removed sensors from list_filter
|
||||
search_fields = ('object', 'name') # Removed sensors from search_fields
|
||||
list_per_page = 10
|
||||
list_max_show_all = 100
|
||||
list_editable = ('name',) # Changed to tuple with name only
|
||||
list_display_links = ('object',)
|
||||
3
backend/sitemanagement/constants/image_file_path.py
Normal file
3
backend/sitemanagement/constants/image_file_path.py
Normal file
@@ -0,0 +1,3 @@
|
||||
def register_object_upload_path(instance, filename):
|
||||
"""Генерирует путь: media/objects/{filename}"""
|
||||
return f"objects/{filename}"
|
||||
@@ -0,0 +1,104 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-06 10:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sitemanagement', '0004_sensor_math_formula'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='message',
|
||||
field=models.CharField(max_length=255, verbose_name='Сообщение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='metric',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alerts', to='sitemanagement.metric', verbose_name='Метрика'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='resolved',
|
||||
field=models.BooleanField(default=False, verbose_name='Статус обработки'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='sensor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alerts', to='sitemanagement.sensor', verbose_name='Датчик'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='sensor_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alerts', to='sitemanagement.sensortype', verbose_name='Тип сенсора'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='alert',
|
||||
name='severity',
|
||||
field=models.CharField(choices=[('warning', 'Warning'), ('critical', 'Critical')], default='warning', max_length=20, verbose_name='Уровень тревоги'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='metric',
|
||||
name='raw_value',
|
||||
field=models.CharField(max_length=50, verbose_name='Исходное значение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='metric',
|
||||
name='sensor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='metrics', to='sitemanagement.sensor', verbose_name='Датчик'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='metric',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='metric',
|
||||
name='value',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Преобразованное значение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sensor',
|
||||
name='math_formula',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Математическая формула'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sensor',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Название'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sensor',
|
||||
name='sensor_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sitemanagement.sensortype', verbose_name='Тип сенсора'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sensor',
|
||||
name='serial_number',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Серийный номер'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sensor',
|
||||
name='signal_format',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sitemanagement.signalformat', verbose_name='Формат сигнала'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signalformat',
|
||||
name='code',
|
||||
field=models.CharField(max_length=50, verbose_name='Код'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signalformat',
|
||||
name='conversion_rule',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Правило преобразования'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signalformat',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Единица измерения'),
|
||||
),
|
||||
]
|
||||
50
backend/sitemanagement/migrations/0006_object_zone.py
Normal file
50
backend/sitemanagement/migrations/0006_object_zone.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-06 11:28
|
||||
|
||||
import django.db.models.deletion
|
||||
import sitemanagement.constants.image_file_path
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sitemanagement', '0005_alter_alert_message_alter_alert_metric_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Object',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Название')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to=sitemanagement.constants.image_file_path.register_object_upload_path, verbose_name='Изображение')),
|
||||
('address', models.CharField(max_length=255, verbose_name='Адрес')),
|
||||
('floors', models.PositiveSmallIntegerField(verbose_name='Количество этажей')),
|
||||
('area', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Площадь')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Объект',
|
||||
'verbose_name_plural': 'Объекты',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Zone',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='zones', to='sitemanagement.object', verbose_name='Объект')),
|
||||
('sensors', models.ManyToManyField(related_name='zones', to='sitemanagement.sensor', verbose_name='Датчики')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Зона',
|
||||
'verbose_name_plural': 'Зоны',
|
||||
'ordering': ['object', 'name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.db import models
|
||||
from decimal import Decimal
|
||||
|
||||
from sitemanagement.constants.image_file_path import register_object_upload_path
|
||||
|
||||
class Multiplexor(models.Model):
|
||||
"""Устройство-мультиплексор"""
|
||||
@@ -61,9 +61,9 @@ class SensorType(models.Model):
|
||||
class SignalFormat(models.Model):
|
||||
"""Формат сигнала и правило преобразования"""
|
||||
sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE, related_name="formats")
|
||||
code = models.CharField(max_length=50) # например "4-20мА", "VW f<1600Hz", "NTC R>250Ohm"
|
||||
unit = models.CharField(max_length=20, blank=True, null=True) # °C, мкм/м, мм и т.п.
|
||||
conversion_rule = models.CharField(max_length=255, blank=True, null=True)
|
||||
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:
|
||||
@@ -79,13 +79,13 @@ class SignalFormat(models.Model):
|
||||
class Sensor(models.Model):
|
||||
"""Конкретный датчик, установленный в канале"""
|
||||
channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name="sensors")
|
||||
sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE)
|
||||
signal_format = models.ForeignKey(SignalFormat, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
serial_number = models.CharField(max_length=50, blank=True, null=True) # CL 2106009
|
||||
name = models.CharField(max_length=50, blank=True, null=True) # GA-1, HLE-1 и т.п.
|
||||
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)
|
||||
math_formula = models.CharField(null=True, blank=True, max_length=255, verbose_name="Математическая формула")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Датчик"
|
||||
@@ -99,10 +99,10 @@ class Sensor(models.Model):
|
||||
class Metric(models.Model):
|
||||
"""Значения, которые приходят из CSV"""
|
||||
timestamp = models.DateTimeField()
|
||||
sensor = models.ForeignKey(Sensor, on_delete=models.CASCADE, related_name="metrics")
|
||||
raw_value = models.CharField(max_length=50) # исходное значение из файла (например "11.964 (A)")
|
||||
value = models.FloatField(null=True, blank=True) # преобразованное значение
|
||||
status = models.CharField(max_length=20, blank=True, null=True) # No Rx, Error, NotAv и т.д.
|
||||
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)
|
||||
|
||||
@@ -121,21 +121,22 @@ class Metric(models.Model):
|
||||
|
||||
class Alert(models.Model):
|
||||
"""Тревоги по метрикам"""
|
||||
sensor = models.ForeignKey(Sensor, on_delete=models.CASCADE, related_name="alerts")
|
||||
metric = models.ForeignKey(Metric, on_delete=models.CASCADE, related_name="alerts")
|
||||
sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE, related_name="alerts")
|
||||
message = models.CharField(max_length=255)
|
||||
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"
|
||||
default="warning",
|
||||
verbose_name="Уровень тревоги"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
resolved = models.BooleanField(default=False, verbose_name="Статус обработки")
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
@@ -148,4 +149,40 @@ class Alert(models.Model):
|
||||
ordering = ["created_at", "sensor"]
|
||||
|
||||
def __str__(self):
|
||||
return f"ALERT {self.sensor} @ {self.metric.timestamp}: {self.message}"
|
||||
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="Название")
|
||||
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", "name"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.object.title} - {self.name}"
|
||||
|
||||
Reference in New Issue
Block a user