// скрипт страницы index, на которой отображаются графики
// интервал обновления статуса в миллисекундах
const RELOAD_STATS_INTERVAL = 10000
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) {
const timestamp_offset = dataset[0][0]
const x = dataset.map((element) => element[0] - timestamp_offset);
const y = dataset.map((element) => element[1]);
const sumX = x.reduce((prev, curr) => prev + curr, 0);
const avgX = sumX / x.length;
const xDifferencesToAverage = x.map((value) => avgX - value);
const xDifferencesToAverageSquared = xDifferencesToAverage.map(
(value) => value ** 2
);
const SSxx = xDifferencesToAverageSquared.reduce(
(prev, curr) => prev + curr,
0
);
const sumY = y.reduce((prev, curr) => prev + curr, 0);
const avgY = sumY / y.length;
const yDifferencesToAverage = y.map((value) => avgY - value);
const xAndYDifferencesMultiplied = xDifferencesToAverage.map(
(curr, index) => curr * yDifferencesToAverage[index]
);
const SSxy = xAndYDifferencesMultiplied.reduce(
(prev, curr) => prev + curr,
0
);
// const slope = SSxy / SSxx;
// const intercept = avgY - slope * avgX;
// return (x) => intercept + slope * x;
return SSxy / SSxx
}
async function makeRequest(url) {
let response = await fetch(url)
if (response.status === 403) {
// http Forbidden, исправляется перезагрузкой страницы и просмотром окошка "Требуется авторизация"
window.location.reload()
return {}
} else if (response.status !== 200) {
console.log('fetch(' + url + ') failed. Status Code: ' + response.status);
return null;
}
return await response.json()
}
async function loadChartData() {
let chartTime = localStorage.getItem("settings-chart-time")
return (await makeRequest('/fetch/tank-chart?days=' + chartTime))['tank_chart']
}
async function loadLastUpdates() {
return (await makeRequest('/fetch/stats'))['stats']
}
/**
* Функция, устанавливающая классы CSS 'value-good' и 'value-bad'.
* @param element
* @param good
* @private
*/
function _setIndicatorClass(element, good) {
if (good) {
element.classList.remove("value-bad")
element.classList.add("value-good")
} else {
element.classList.remove("value-good")
element.classList.add("value-bad")
}
}
const updateFunctions = {
// последнее обновление резервуара
'tank-last-update': (element, dataset) => {
const now = Date.now() / 1000
element.innerHTML = moment(new Date(dataset['tank']['last_update'] * 1000)).format(DATETIME_FORMAT)
// для резервуара нормально, если обновление было меньше получаса назад
_setIndicatorClass(element, now - dataset['tank']['last_update'] < 1800)
},
// последнее обновление насосной
'pump-last-update': (element, dataset) => {
const now = Date.now() / 1000
element.innerHTML = moment(new Date(dataset['pump']['last_update'] * 1000)).format(DATETIME_FORMAT)
// для насосной можно допустить последнее обновление 10 минут назад
_setIndicatorClass(element, now - dataset['pump']['last_update'] < 600)
},
// Уровень воды
'tank-level-dir': (element, dataset) => {
const last_radar_values = dataset['tank']['last_radar_values']
if (last_radar_values.length < 3) {
element.innerHTML = "(?)"
} else {
let ap = approximateWithTimestamps(last_radar_values)
if (Math.abs(ap) < 0.02) {
element.innerHTML = '→'
} else {
element.innerHTML = ap < 0 ? '↘' : '↗'
}
}
},
// Текущий уровень воды: %
'tank-level-now': (element, dataset) => {
element.innerHTML = dataset['tank']['last_percentage']
// тут все хорошо если влезаем в установленные рамки +-2% (69-80%)
_setIndicatorClass(element, 67 <= dataset['tank']['last_percentage'] <= 82)
},
// Текущее значение с радара
'tank-raw-now': (element, dataset) => {
element.innerHTML = dataset['tank']['last_radar']
},
// Статус:
'tank-status': (element, dataset) => {
const shur_status_bits = ['нужна вода', 'поплавок нижний', 'поплавок верхний', 'поплавок аварийный']
element.innerHTML = unpackBits(dataset['tank']['status_reg'], shur_status_bits) + " (" + dataset['tank']['status_reg'] + ")"
// тут все хорошо, пока нет аварийного поплавка
_setIndicatorClass(element, (dataset['tank']['status_reg'] & 0x8) === 0)
},
// Частота ПЧ: Гц
'pump-vfd-freq': (element, dataset) => {
element.innerHTML = dataset['pump']['vfd_freq']
},
// Ток ПЧ: А
'pump-vfd-curr': (element, dataset) => {
document.getElementById("pump-vfd-curr").innerHTML = dataset['pump']['vfd_curr']
},
// Ошибка ПЧ:
'pump-vfd-error': (element, dataset) => {
const vfdErrorsDescription = {
5: "превышение напряжения",
8: "пониженное напряжение",
11: "обрыв фазы питания",
12: "отказ выходной цепи",
15: "перегрев ПЧ",
17: "замыкание двигателя на землю",
19: "двигатель без нагрузки",
34: "перегрузка по току"
}
if (dataset['pump']['vfd_err'] in vfdErrorsDescription) {
element.innerHTML = dataset['pump']['vfd_err'] + " (" + vfdErrorsDescription[dataset['pump']['vfd_err']] + ")"
} else {
element.innerHTML = dataset['pump']['vfd_err']
}
_setIndicatorClass(element, dataset['pump']['vfd_err'] === 0)
},
// Регистр аварий:
'pump-alarms': (element, dataset) => {
const pumpAlarmRegister = [
"общая авария",
"реле контроля фаз",
"насос",
"ошибка ПЧ",
"датчик потока",
"датчик уровня воды",
"датчик наличия воды",
"задвижки"
]
if (dataset['pump']['alarms'] === 0) {
element.innerHTML = "ok"
} else {
element.innerHTML = unpackBits(dataset['pump']['alarms'], pumpAlarmRegister) + " (" + dataset['pump']['alarms'] + ")"
}
_setIndicatorClass(element, dataset['pump']['alarms'] === 0)
},
// Состояние КА:
'pump-stage': (element, dataset) => {
const pumpStageDescription = {
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.*"
}
if (dataset['pump']['pump_stage'] in pumpStageDescription) {
element.innerHTML = dataset['pump']['pump_stage'] + " (" + pumpStageDescription[dataset['pump']['pump_stage']] + ")"
} else {
element.innerHTML = dataset['pump']['pump_stage']
}
},
// Текущий расход: м³
'pump-flow-meter': (element, dataset) => {
element.innerHTML = dataset['pump']['flow_meter']
},
// 'id': (element, dataset) => {},
}
async function updateStatus() {
let dataset = await loadLastUpdates()
for (let id in updateFunctions) {
let element = document.getElementById(id)
if (element !== null) {
updateFunctions[id](element, dataset)
}
}
}