feat / AEB-62 add additional DB schema for objects logic

This commit is contained in:
Timofey
2025-10-06 14:55:46 +03:00
parent a822ea587c
commit 5a0bb27c95
11 changed files with 426 additions and 31 deletions

View File

@@ -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',)

View File

@@ -0,0 +1,3 @@
def register_object_upload_path(instance, filename):
"""Генерирует путь: media/objects/{filename}"""
return f"objects/{filename}"

View File

@@ -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='Единица измерения'),
),
]

View 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'],
},
),
]

View File

@@ -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}"