From 8a7b15f09344d1544d60d4f5bdbffa7aee39a45d Mon Sep 17 00:00:00 2001 From: VladislavOstapov Date: Mon, 22 Jan 2024 20:17:06 +0300 Subject: [PATCH] =?UTF-8?q?=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=BD=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=8E=D1=89=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D0=BD=D0=B3=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D1=83=D0=B0=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env-template | 12 ++- logs_service/admin.py | 5 ++ logs_service/apps.py | 54 +++++++++++-- logs_service/models.py | 1 + logs_service/services.py | 164 +++++++++++++++++++++++++++++++++++++-- ospaz_site/settings.py | 3 +- static/js/index-main.js | 2 +- users/models.py | 12 ++- 8 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 logs_service/admin.py diff --git a/.env-template b/.env-template index 36d8ce0..b43b8a6 100644 --- a/.env-template +++ b/.env-template @@ -13,4 +13,14 @@ DB_PORT=5432 DJANGO_SECRET="django-secure" # включение/отключение сервисов ModBus -ENABLE_MB_SERVICES=1 +ENABLE_MB_TANK_SERVICE=1 +MB_TANK_ADDRESS="10.8.105.3" +MB_TANK_PORT=504 +MB_TANK_SCAN_RATE_MS=60000 + +ENABLE_MB_PUMP_SERVICE=1 +MB_PUMP_ADDRESS="10.8.105.2" +MB_PUMP_PORT=503 +MB_PUMP_SCAN_RATE_MS=5000 + +LOGGER_SAVE_DAYS=60 diff --git a/logs_service/admin.py b/logs_service/admin.py new file mode 100644 index 0000000..7dffb9d --- /dev/null +++ b/logs_service/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import MbPumpRecord, MbTankRecord + +admin.site.register(MbPumpRecord) +admin.site.register(MbTankRecord) diff --git a/logs_service/apps.py b/logs_service/apps.py index 9d5165c..d22e6e0 100644 --- a/logs_service/apps.py +++ b/logs_service/apps.py @@ -1,14 +1,58 @@ +import os from django.apps import AppConfig -from .services import MbTankService, MbPumpService +from ospaz_site.settings import DEBUG +_is_ready = False class LogsServiceConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'logs_service' - mb_tank_service = MbTankService('10.105.0.3') - mb_pump_service = MbPumpService('10.105.0.2') + mb_tank_service = None + mb_pump_service = None + + mb_tank_service_enable = True + mb_pump_service_enable = True def ready(self): - LogsServiceConfig.mb_tank_service.start() - LogsServiceConfig.mb_pump_service.start() + # защита от двойного запуска + if os.environ.get('RUN_MAIN', None) == 'true': + return + + from .services import MbTankService, MbPumpService + # включение/выключение сервисов + mb_tank_service_enable = os.getenv('ENABLE_MB_TANK_SERVICE', '1') + LogsServiceConfig.mb_tank_service_enable = int(mb_tank_service_enable) + + mb_pump_service_enable = os.getenv('ENABLE_MB_PUMP_SERVICE', '1') + LogsServiceConfig.mb_pump_service_enable = int(mb_pump_service_enable) + + save_days = int(os.getenv('LOGGER_SAVE_DAYS')) + + # создание сервисов + if LogsServiceConfig.mb_tank_service_enable: + LogsServiceConfig.mb_tank_service = MbTankService(ip_addr=os.getenv('MB_TANK_ADDRESS'), + port=int(os.getenv('MB_TANK_PORT')), + scan_rate=int(os.getenv('MB_TANK_SCAN_RATE_MS')), + save_days=save_days) + + if LogsServiceConfig.mb_pump_service_enable: + LogsServiceConfig.mb_pump_service = MbPumpService(ip_addr=os.getenv('MB_PUMP_ADDRESS'), + port=int(os.getenv('MB_PUMP_PORT')), + scan_rate=int(os.getenv('MB_PUMP_SCAN_RATE_MS')), + save_days=save_days) + + # запуск сервисов + if LogsServiceConfig.mb_tank_service_enable: + if DEBUG: + print('Creating service MbTankService...') + LogsServiceConfig.mb_tank_service.start() + + if LogsServiceConfig.mb_pump_service_enable: + if DEBUG: + print('Creating service MbPumpService...') + LogsServiceConfig.mb_pump_service.start() + + return True + + diff --git a/logs_service/models.py b/logs_service/models.py index 4a6d6a9..a3fece4 100644 --- a/logs_service/models.py +++ b/logs_service/models.py @@ -7,6 +7,7 @@ class MbTankRecord(models.Model): dt = models.DateTimeField(verbose_name="Время записи", default=timezone.now) radar_raw = models.PositiveIntegerField(verbose_name="Уровень воды по радару радара") level = models.PositiveSmallIntegerField(verbose_name="Уровень воды в % (пересчитан ПЛК)") + status = models.PositiveSmallIntegerField(verbose_name="Регистр статуса") def __str__(self): return f"({self.id}) {self.dt}: radar={self.radar_raw}, level={self.level}" diff --git a/logs_service/services.py b/logs_service/services.py index ad8e4c0..6866046 100644 --- a/logs_service/services.py +++ b/logs_service/services.py @@ -1,21 +1,173 @@ from .models import * +from ospaz_site.settings import DEBUG from threading import Thread +import time +from datetime import datetime, timedelta +from pyModbusTCP.client import ModbusClient class MbService(Thread): - def __init__(self, ip_addr): + def __init__(self, ip_addr, port, scan_rate, poll_time_ms=60000, save_days=60): super().__init__() - self._ip_addr = ip_addr + self._poll_time_ms = poll_time_ms + self._scan_rate = scan_rate + + self.mb = ModbusClient(host=ip_addr, port=port, debug=DEBUG, auto_open=True) + + def _init_state(self): + """ + Функция вызывается один раз при создании сервиса, в ней нужно загрузить содержимое регистров из базы данных. + """ + pass + + def _push_current_state(self): + """ + Функция вызывается в момент, когда требуется сохранить состояние в базу данных + """ + pass + + def _load_current_state(self): + """ + Функция должна совершить операцию чтения регистров modbus + """ + pass + + def _check_need_save(self): + """ + Функция должна вернуть результат: нужно ли сохранять текущее состояние в базу данных + """ + return True + + def run(self): + # if self.log_type == 'on-change': + # pass + # else: + print(f"[MbService:{self.__class__.name}] service started!") + self._init_state() + + last_query = datetime.now() + scan_rate = timedelta(milliseconds=self._scan_rate) + + while True: + try: + self._load_current_state() + if self._check_need_save(): + self._push_current_state() + except Exception as ex: + print(f"[MbService:{self.__class__.name}] Exception: {ex}") + + # вычислим время до следующего опроса и подождем + # время следующего опроса + need_time = last_query + scan_rate + curr_time = datetime.now() + if need_time > curr_time: + delta = need_time - curr_time + time.sleep(delta.seconds + (delta.microseconds / 1000000)) + last_query = need_time + else: + last_query = datetime.now() def to_json(self): return {} -class MbTankService(MbService): +class MbClearHistoryService(Thread): + def __init__(self, model, save_days): + super().__init__() + self.model = model + self._days = save_days + def run(self): - pass + while True: + self.model.objects.filter(dt__lt=(timezone.now() - timezone.timedelta(days=60))).delete() + time.sleep(10) + + +class MbTankService(MbService): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.registers = {} + # { + # 'last_update': None, # последнее обновление, unix time + # 'level': None, # текущее показание, пересчитанное в % + # 'radar': None, # текущее показание с радара + # 'status': None, # статусный регистр + # 'last_radar_values': [] # последние показания за 15 минут + # } + + self._curr_state = None + # { + # "level": int, + # "status": int, + # "radar": int + # } + + # для фильтрации уровня, чтобы его в бд писать + self.level_filter = [] + + def _update_last_radar_values(self): + res = [[item.dt.timestamp(), item.radar_raw] + for item in MbTankRecord.objects.filter(dt__gt=(datetime.now() - timedelta(minutes=15))).order_by('-dt')] + self.registers['last_radar_values'] = res + + def _init_state(self): + self.last_save = MbTankRecord.objects.order_by('-dt').first() + print(f"Initializing MbTankService: last_save={self.last_save}") + if self.last_save is not None: + self.registers['last_update'] = self.last_save.dt + self.registers['level'] = self.last_save.level + self.registers['radar'] = self.last_save.radar_raw + self.registers['status'] = self.last_save.status + self._update_last_radar_values() + + def _push_current_state(self): + record = MbTankRecord(radar_raw=self._curr_state['radar'], + level=self._curr_state['level'], + status=self._curr_state['status']) + record.save() + + def _load_current_state(self): + # D0: level_percentage + # D5: status_reg + # D1: radar_high_reg + # D2: radar_low_reg + values = self.mb.read_holding_registers(0, 6) + self._curr_state = { + "level": values[0], + "status": values[5], + "radar": (values[1] << 16) | values[2] + } + self.registers['last_update'] = datetime.now() + + def _check_need_save(self): + return True + + def to_json(self): + return self.registers class MbPumpService(MbService): - def run(self): - pass + _config = { + "modbus": { + "host": "10.8.105.2", + "port": 503, + "log-type": "on-change" + }, + "registers": [ + ["D0", "v231_v236"], + ["D1", "v236_v29"], + ["D15", "ps_39"], + ["D16", "flow_meter"], + ["D28", "pump_stage"], + ["D30", "half_auto_control"], + ["D31", "watch_1"], + ["D32", "watch_2"], + + ["D33", "st_valves"], + ["D35", "alarms"], + + ["D25", "vfd_freq"], + ["D26", "vfd_current"], + ["D27", "vfd_error"] + ] + } diff --git a/ospaz_site/settings.py b/ospaz_site/settings.py index 5efa1ac..086be32 100644 --- a/ospaz_site/settings.py +++ b/ospaz_site/settings.py @@ -51,7 +51,8 @@ INSTALLED_APPS = [ # custom apps 'users.apps.UsersConfig', - 'index.apps.IndexConfig' + 'index.apps.IndexConfig', + 'logs_service' ] MIDDLEWARE = [ diff --git a/static/js/index-main.js b/static/js/index-main.js index 9816ccb..7093ba1 100644 --- a/static/js/index-main.js +++ b/static/js/index-main.js @@ -113,7 +113,7 @@ const updateFunctions = { const now = Date.now() / 1000 element.innerHTML = moment(new Date(dataset['pump']['last_update'] * 1000)).format(DATETIME_FORMAT) // для насосной можно допустить последнее обновление 10 минут назад - _setIndicatorClass(element, now - dataset['tank']['last_update'] < 600) + _setIndicatorClass(element, now - dataset['pump']['last_update'] < 600) }, // Уровень воды diff --git a/users/models.py b/users/models.py index 125e2c9..99e088c 100644 --- a/users/models.py +++ b/users/models.py @@ -49,7 +49,17 @@ class User(AbstractBaseUser): 'users.add_user': 1, 'users.change_user': 1, 'users.delete_user': 1, - 'users.view_user': 1 + 'users.view_user': 1, + + 'logs_service.add_mbpumprecord': 1, + 'logs_service.change_mbpumprecord': 1, + 'logs_service.delete_mbpumprecord': 1, + 'logs_service.view_mbpumprecord': 1, + + 'logs_service.add_mbtankrecord': 1, + 'logs_service.change_mbtankrecord': 1, + 'logs_service.delete_mbtankrecord': 1, + 'logs_service.view_mbtankrecord': 0, } if perm in permissions: if permissions[perm] <= secure_level: