diff --git a/index/urls.py b/index/urls.py index 26ff8ca..b44f8e0 100644 --- a/index/urls.py +++ b/index/urls.py @@ -19,5 +19,7 @@ from . import views urlpatterns = [ path('', views.view_index, name='index'), + path('fetch/stats', views.view_stats, name='fetch-stats'), + path('fetch/tank-chart', views.view_tank_chart, name='fetch-tank-chart'), # path('methods/', views.call_method, name='call_method') ] diff --git a/index/views.py b/index/views.py index 5718f4e..9e6c730 100644 --- a/index/views.py +++ b/index/views.py @@ -1,8 +1,30 @@ +import os + +from django.http import HttpResponse from django.shortcuts import render from django.db.models import Manager -# Create your views here. +# только для тестирования! +import requests + +TEST_BASE_FETCH = "https://test.wawaa.ru/dev-fetch.php" def view_index(request): return render(request, 'index.html') + + +def view_stats(request): + # только для тестирования! + res = requests.get(TEST_BASE_FETCH + "?stats", headers={'Authorization': os.getenv("TEST_AUTH")}) + response = HttpResponse(res.content) + response.headers["Content-type"] = response.headers["Content-type"] + return response + + +def view_tank_chart(request): + # только для тестирования! + res = requests.get(TEST_BASE_FETCH + "?tank_chart", headers={'Authorization': os.getenv("TEST_AUTH")}) + response = HttpResponse(res.content) + response.headers["Content-type"] = response.headers["Content-type"] + return response diff --git a/ospaz_site/settings.py b/ospaz_site/settings.py index cc2105a..838009e 100644 --- a/ospaz_site/settings.py +++ b/ospaz_site/settings.py @@ -28,7 +28,7 @@ PROJECT_ROOT = os.path.dirname(__file__) SECRET_KEY = os.getenv('DJANGO_SECRET') -ALLOWED_HOSTS = ['localhost'] +ALLOWED_HOSTS = ['localhost', '10.8.0.2'] # CSRF_TRUSTED_ORIGINS = ['https://ospaz.wawaa.ru'] # HTTPS settings https://docs.djangoproject.com/en/5.0/topics/security/ diff --git a/ospaz_site/urls.py b/ospaz_site/urls.py index 5e49318..b9717ba 100644 --- a/ospaz_site/urls.py +++ b/ospaz_site/urls.py @@ -16,10 +16,8 @@ Including another URLconf """ from django.contrib import admin from django.urls import path, include - from django.contrib.staticfiles.views import serve -import index.urls urlpatterns = [ path('', include('index.urls')), diff --git a/static/css/style.css b/static/css/style.css index 5013510..882d13f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,27 +1,26 @@ -/* TODO исправить стили, тут верхней навигации вообще нет */ - - /* ========== THEME ========== */ body { - --text-color: #111; - --brand-color: #231765; - --bkg-color-blue: #0066e3; + --text-color: #262626; + --text-color2: #3d3d3d; - --bkg-color: #fff; - --bkg-color2: #ccc; - --bkg-color3: #aaa; + --brand-bg: #EDF3FE; + --brand-text: #5488F7; + + --bg-color: #FEFEFE; + --bg-selected: #F1F1F1; } @media (prefers-color-scheme: dark) { /* defaults to dark theme */ body { --text-color: #eee; - --brand-color: #654dea; - --bkg-color-blue: #003aac; + --text-color2: #bbb; - --bkg-color: #121212; - --bkg-color2: #202020; - --bkg-color3: #353435; + --brand-bg: #393E50; + --brand-text: #5F93F3; + + --bg-color: #2d2c33; + --bg-selected: #424248; } } @@ -30,50 +29,28 @@ body { color: var(--text-color); } -body { - background: var(--bkg-color); +*, *::before, *::after { + box-sizing: border-box; } -.page-header { - text-align: center; - margin: 1em 3em; +body { + background: var(--bg-color); + margin: 0; /* браузеры зачем-то ставят свое значение */ +} + +#content { + margin: 0.2em; } /* ========== MAIN STYLES ========== */ -#header-wrapper { - display: flex; - margin: 1em; +header > h1 { + text-align: center; + background-color: var(--brand-bg); + padding: 0.5em; } -#header-wrapper * { - color: var(--brand-color); +header * { + color: var(--brand-text); } -header { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - margin: 0 auto; -} - -header > * { - margin: auto 0.5em; - text-decoration: none; - font-size: medium; -} - -header > div > * { - display: block; - margin: 0.1em 0; -} - -#logo-text { - font-weight: bolder; - font-size: xx-large; -} - -#logo-image { - width: 50px; - height: 50px; -} diff --git a/static/favicon.svg b/static/favicon.svg index f06d5de..f582e73 100644 --- a/static/favicon.svg +++ b/static/favicon.svg @@ -1,2 +1,2 @@ - favicon +favicon Layer 1 \ No newline at end of file diff --git a/static/js/index-main.js b/static/js/index-main.js new file mode 100644 index 0000000..ca12d43 --- /dev/null +++ b/static/js/index-main.js @@ -0,0 +1,160 @@ +// скрипт страницы index, на которой отображаются графики + +// интервал обновления статуса в миллисекундах +const RELOAD_STATS_INTERVAL = 10000 + +// интервал обновления графика - 1% показываемого времени (2 недели) +// const RELOAD_CHART_INTERVAL = (3600 * 1000 * 24 * 14) / 100 +// const RELOAD_CHART_INTERVAL = (3600 * 1000 * 24) / 100 +const RELOAD_CHART_INTERVAL = 30000 + +const DATETIME_FORMAT = "DD MMM YYYY HH:mm:ss" + +function unpackBits(num, desc) { + let out = "" + for (let i = 0; i < desc.length; i++) { + if (desc[i] !== null) { + if ((num & (1 << i)) !== 0) { + // бит установлен + if (out.length === 0) { + out = desc[i] + } else { + out += " + " + out += desc[i] + } + } + } + } + + return out +} + +/** + * Функция для линейной аппроксимации набора точек. Для заданных точек находит коэфицент a из уравнения y=ax+b. + * Уравнение подбирается при помощи метода наименьших квадратов. + * @param dataset набор точек в виде [[timestamp, val], [timestamp, val], ...], где timestamp - значение по X, val - значение по Y + * @returns {number}, коэфицент a + */ +function approximateWithTimestamps(dataset) { + + // для точных расчетов нужно сместить timestamp + const timestamp_offset = dataset[0][0] + + // сумма (x[i] * y[i]) + let sum_x_mul_y = 0 + for (let i = 0; i < dataset.length; i++) { + sum_x_mul_y += (dataset[i][0] - timestamp_offset) * dataset[i][1] + } + + // сумма всех x[i] + let sum_x = 0; + for (let i = 0; i < dataset.length; i++) { + sum_x += dataset[i][0] - timestamp_offset + } + + // сумма всех y[i] + let sum_y = 0; + for (let i = 0; i < dataset.length; i++) { + sum_y += dataset[i][1] + } + + // сумма всех x[i]^2 + let sum_x_mul_x = 0; + for (let i = 0; i < dataset.length; i++) { + sum_x_mul_x += Math.pow(dataset[i][0] - timestamp_offset, 2) + } + + // вычисление коэфицента a из формулы y=ax+b + // нам нужен только он + return (dataset.length * sum_x_mul_y - sum_x * sum_y) / (sum_x_mul_x - Math.pow(sum_x, 2)) +} + +async function makeRequest(url) { + let response = await fetch(url) + if (response.status !== 200) { + console.log('fetch(' + url + ') failed. Status Code: ' + response.status); + return null; + } + return await response.json() +} + +async function loadAllData() { + return await makeRequest('/fetch/all') +} + +async function loadChartData() { + return (await makeRequest('/fetch/tank-chart'))['tank_chart'] +} + +async function loadLastUpdates() { + return (await makeRequest('/fetch/stats'))['stats'] +} + +async function updateStatus() { + let dataset = await loadLastUpdates() + + // последнее обновление + document.getElementById("tank-last-update").innerHTML = moment(new Date(dataset['tank']['last_update'] * 1000)).format(DATETIME_FORMAT) + document.getElementById("pump-last-update").innerHTML = moment(new Date(dataset['pump']['last_update'] * 1000)).format(DATETIME_FORMAT) + + //

Уровень воды

+ const last_radar_values = dataset['tank']['last_radar_values'] + if (last_radar_values.length === 0) { + document.getElementById("tank-level-dir").innerHTML = "(?)"; + } else { + document.getElementById("tank-level-dir").innerHTML = approximateWithTimestamps(last_radar_values) < 0 ? '↘' : '↗'; + } + + //

Текущий уровень воды: %

+ document.getElementById("tank-level-now").innerHTML = dataset['tank']['last_percentage'] + + //

Текущее значение с радара:

+ document.getElementById("tank-raw-now").innerHTML = dataset['tank']['last_radar'] + + //

Статус:

+ const shur_status_bits = ['нужна вода', 'поплавок нижний', 'поплавок верхний', 'поплавок аварийный'] + document.getElementById("tank-status").innerHTML = unpackBits(dataset['tank']['status_reg'], shur_status_bits) + " (" + dataset['tank']['status_reg'] + ")"; + + //

Частота ПЧ: Гц

+ document.getElementById("pump-vfd-freq").innerHTML = dataset['pump']['vfd_freq'] + + //

Ток ПЧ: А

+ document.getElementById("pump-vfd-curr").innerHTML = dataset['pump']['vfd_curr'] + + //

Ошибка ПЧ:

+ document.getElementById("pump-vfd-error").innerHTML = dataset['pump']['vfd_err'] + + //

Регистр аварий:

+ document.getElementById("pump-alarms").innerHTML = dataset['pump']['alarms'] + + //

Состояние КА:

+ document.getElementById("pump-stage").innerHTML = dataset['pump']['pump_stage'] + + //

Текущий расход: м³

+ document.getElementById("pump-flow-meter").innerHTML = dataset['pump']['flow_meter'] + +} +// состояния конченого автомата +// $states = [ +// 0 => "отключен", +// 2 => "инициализация: установка задвижек в начальное состояние", +// 21 => "инициализация: закрытие задвижек 23.5 и 23.6", +// 31 => "инициализация: открытие задвижек 23.5 и 23.6", +// 99 => "авария", +// +// 100 => "ожидание сигнала на перекачку воды", +// +// 102 => "запуск: открытие задвижки 23.6", +// 110 => "запуск: ожидание сигнала от датчика уровня поз.36", +// 121 => "запуск: открытие задвижек насоса", +// 131 => "запуск: закрытие задвижки 23.6", +// 141 => "запуск: пуск ПЧ", +// +// 200 => "работает", +// +// 202 => "остановка: закрытие задвижек 23.3 и 23.4", +// 211 => "остановка: ожидание остановки ПЧ", +// 221 => "остановка: перевод запорной арматуры в исходное состояние", +// 231 => "остановка: работа компрессора", +// 235 => "остановка: сброс конденсата клапанами 25.*" +// ]; diff --git a/templates/base.html b/templates/base.html index d68a9a1..3a3ae0e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,6 +7,7 @@ {% block title %} Мониторинг резервуара {% endblock %} {% load static %} + {% block head %} {% endblock %} {% block styles %} {% endblock %} diff --git a/templates/index.html b/templates/index.html index dfe8b65..0c27a35 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,164 +1,137 @@ {% extends 'base.html' %} +{% load static %} + +{% block head %} + + + + + +{% endblock %} {% block styles %} - + .content-block { + margin: 0.5em; + } + + #canvas-wrapper { + width: 100%; + } + + .value-good { + color: green; + } + + .value-bad { + color: red; + } + {% endblock %} {% block header %} -

Мониторинг водозаборного узла

+

Мониторинг водозаборного узла

{% if user.is_superuser %} Админка {% endif %} {% endblock %} {% block content %} - - - {% for method in api_methods %} -
-
- {{ method.name }} - -
-

Описание

-

- {{ method.doc | safe }} -

- -

Параметры

- {% if method.params %} -
- - - - - - - - {% for param in method.params %} - - - - - - - {% endfor %} -
НазваниеТипОписаниеОбязательный
{{ param.name }}{{ param.type }}{{ param.description | safe }}{{ param.required }}
- {% else %} -

- Этот метод не принимает параметров. -

- {% endif %} - -

Результат

-

- {{ method.returns | safe }} -

-

- Ссылка на метод (без параметров): {{ method.name }} -

-
- Конструктор -
-
-
-

Параметры

-
-
- - {% if method.params %} - {% for param in method.params %} -
- - -
- {% endfor %} - {% else %} -
-

- Этот метод не принимает параметров. -

-
- {% endif %} - -
- -
-
-
-

Результат

-
-

-                            
-
-
-
- -
+
+
+
+

Состояние насосной станции

+

Последнее обновление:

+

Частота ПЧ: Гц

+

Ток ПЧ: А

+

Ошибка ПЧ:

+

Регистр аварий:

+

Состояние КА:

+

Текущий расход: м³

- {% endfor %} - -
- Перейти в админку. -
- Текущий токен:
Сбросить +
+

Состояние резервуара

+

Последнее обновление:

+

Текущий уровень воды: %

+

Текущее значение с радара:

+

Уровень воды

+

Статус:

+
+

Уровень воды в резервуаре, %

+
+
+
- + } + }); + + document.addEventListener("DOMContentLoaded", (event) => { + // запуск обновления статистики + updateStatus().then(() => { + setInterval(() => { + updateStatus().then() + }, RELOAD_STATS_INTERVAL) + }) + + function updateChart(data) { + chart_dataset.data = data.map((e) => { + return {x: new Date(e[0] * 1000), y: e[1]} + }) + chart.update(); + } + + loadChartData().then((data) => { + updateChart(data) + + setInterval(() => { + loadChartData().then((d) => { + updateChart(d) + }) + }, RELOAD_CHART_INTERVAL) + }) + }); + {% endblock %}