368 lines
14 KiB
JavaScript
368 lines
14 KiB
JavaScript
// скрипт страницы index, на которой отображаются графики
|
||
|
||
// интервал обновления статуса в миллисекундах
|
||
const RELOAD_STATS_INTERVAL = 10000
|
||
|
||
const DATETIME_FORMAT = "DD MMM YYYY HH:mm:ss"
|
||
|
||
const UNDEFINED_FIELD_TEXT = '(?)'
|
||
|
||
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 === null) {
|
||
element.classList.remove("value-good")
|
||
element.classList.remove("value-bad")
|
||
} else if (good !== undefined) {
|
||
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': {
|
||
'get_val': (dataset) => { return dataset['tank']['last_update'] },
|
||
'handler': (element, value) => {
|
||
const now = Date.now() / 1000
|
||
element.innerHTML = moment(new Date(value * 1000)).format(DATETIME_FORMAT)
|
||
// для резервуара нормально, если обновление было меньше получаса назад
|
||
return now - value < 1800
|
||
}
|
||
},
|
||
|
||
// последнее обновление насосной
|
||
'pump-last-update': {
|
||
'get_val': (dataset) => { return dataset['pump']['last_update'] },
|
||
'handler': (element, value) => {
|
||
const now = Date.now() / 1000
|
||
element.innerHTML = moment(new Date(value * 1000)).format(DATETIME_FORMAT)
|
||
// для насосной можно допустить последнее обновление 10 минут назад
|
||
return now - value < 600
|
||
}
|
||
},
|
||
|
||
// Уровень воды <span id="tank-level-dir"></span>
|
||
'tank-level-dir': {
|
||
'get_val': (dataset) => { return dataset['tank']['last_radar_values'] },
|
||
'handler': (element, value) => {
|
||
if (value.length < 3) {
|
||
element.innerHTML = UNDEFINED_FIELD_TEXT
|
||
} else {
|
||
let ap = approximateWithTimestamps(value)
|
||
if (Math.abs(ap) < 0.02) {
|
||
element.innerHTML = '→'
|
||
} else {
|
||
element.innerHTML = ap < 0 ? '↘' : '↗'
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Текущий уровень воды: <span id="tank-level-now"></span>%
|
||
'tank-level-now': {
|
||
'get_val': (dataset) => { return dataset['tank']['level'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
// тут все хорошо если влезаем в установленные рамки +-2% (69-80%)
|
||
return 67 <= value <= 82
|
||
}
|
||
},
|
||
|
||
// Текущее значение с радара
|
||
'tank-raw-now': {
|
||
'get_val': (dataset) => { return dataset['tank']['radar'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Статус: <span id="tank-status"></span>
|
||
'tank-status': {
|
||
'get_val': (dataset) => { return dataset['tank']['status'] },
|
||
'handler': (element, value) => {
|
||
const shur_status_bits = ['нужна вода', 'поплавок нижний', 'поплавок верхний', 'поплавок аварийный']
|
||
element.innerHTML = unpackBits(value, shur_status_bits) + " (" + value + ")"
|
||
// тут все хорошо, пока нет аварийного поплавка
|
||
return (value & 0x8) === 0
|
||
}
|
||
},
|
||
|
||
// Частота ПЧ: <span id="pump-vfd-freq"></span>Гц
|
||
'pump-vfd-freq': {
|
||
'get_val': (dataset) => { return dataset['pump']['vfd_freq'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Ток ПЧ: <span id="pump-vfd-curr"></span>А
|
||
'pump-vfd-curr': {
|
||
'get_val': (dataset) => { return dataset['pump']['vfd_curr'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Ошибка ПЧ: <span id="pump-vfd-error"></span>
|
||
'pump-vfd-error': {
|
||
'get_val': (dataset) => { return dataset['pump']['vfd_err'] },
|
||
'handler': (element, value) => {
|
||
const vfdErrorsDescription = {
|
||
5: "превышение напряжения",
|
||
8: "пониженное напряжение",
|
||
11: "обрыв фазы питания",
|
||
12: "отказ выходной цепи",
|
||
15: "перегрев ПЧ",
|
||
17: "замыкание двигателя на землю",
|
||
19: "двигатель без нагрузки",
|
||
34: "перегрузка по току"
|
||
}
|
||
|
||
if (value in vfdErrorsDescription) {
|
||
element.innerHTML = value + " (" + vfdErrorsDescription[value] + ")"
|
||
} else {
|
||
element.innerHTML = value
|
||
}
|
||
return value === 0
|
||
}
|
||
},
|
||
|
||
// Регистр аварий: <span id="pump-alarms"></span>
|
||
'pump-alarms': {
|
||
'get_val': (dataset) => { return dataset['pump']['alarms'] },
|
||
'handler': (element, value) => {
|
||
const pumpAlarmRegister = [
|
||
"общая авария",
|
||
"реле контроля фаз",
|
||
"насос",
|
||
"ошибка ПЧ",
|
||
"датчик потока",
|
||
"датчик уровня воды",
|
||
"датчик наличия воды",
|
||
"задвижки"
|
||
]
|
||
|
||
if (value === 0) {
|
||
element.innerHTML = "ok"
|
||
} else {
|
||
element.innerHTML = unpackBits(value, pumpAlarmRegister) + " (" + value + ")"
|
||
}
|
||
return value === 0
|
||
}
|
||
},
|
||
|
||
// Состояние КА: <span id="pump-stage"></span>
|
||
'pump-stage': {
|
||
'get_val': (dataset) => { return dataset['pump']['pump_stage'] },
|
||
'handler': (element, value) => {
|
||
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 (value in pumpStageDescription) {
|
||
element.innerHTML = value + " (" + pumpStageDescription[value] + ")"
|
||
} else {
|
||
element.innerHTML = value
|
||
}
|
||
|
||
if (value === 99) {
|
||
return false
|
||
} else if (value === 200) {
|
||
return true
|
||
} else {
|
||
return null
|
||
}
|
||
}
|
||
},
|
||
|
||
// Текущий расход: <span id="pump-flow-meter"></span>м³
|
||
'pump-flow-meter': {
|
||
'get_val': (dataset) => { return dataset['pump']['flow_meter'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Запущенный насос: <span id="pump-running"></span>
|
||
'pump-running': {
|
||
'get_val': (dataset) => { return dataset['pump']['pump_running'] },
|
||
'handler': (element, value) => {
|
||
if (value <= -1) {
|
||
element.innerHTML = '-'
|
||
} else {
|
||
element.innerHTML = value
|
||
}
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Моточасы насоса 1: <span id="pump-moto-watch-1"></span>
|
||
'pump-moto-watch-1': {
|
||
'get_val': (dataset) => { return dataset['pump']['moto_watch_1'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Моточасы насоса 2: <span id="pump-moto-watch-2"></span>
|
||
'pump-moto-watch-2': {
|
||
'get_val': (dataset) => { return dataset['pump']['moto_watch_2'] },
|
||
'handler': (element, value) => {
|
||
element.innerHTML = value
|
||
return null
|
||
}
|
||
},
|
||
|
||
// Дистанционный режим: <span id="pump-remote-mode"></span>
|
||
'pump-remote-mode': {
|
||
'get_val': (dataset) => { return dataset['pump']['half_auto_control'] },
|
||
'handler': (element, value) => {
|
||
if ((value & (1 << 2)) === 0) {
|
||
element.innerHTML = 'не используется'
|
||
} else {
|
||
element.innerHTML = 'насос ' + (((value & 1) === 0) ? '1 ' : '2 ') + (((value & (1 << 1)) !== 0) ? 'включен' : 'выключен')
|
||
}
|
||
return null
|
||
}
|
||
},
|
||
|
||
// 'id': {
|
||
// 'get_val': (dataset) => { return dataset['...']['...'] },
|
||
// 'handler': (element, value) => {
|
||
// element.innerHTML = value
|
||
// return null
|
||
// }
|
||
// },
|
||
}
|
||
|
||
async function updateStatus() {
|
||
let dataset = await loadLastUpdates()
|
||
|
||
for (let id in updateFunctions) {
|
||
let element = document.getElementById(id)
|
||
if (element !== null) {
|
||
try {
|
||
const value = updateFunctions[id]['get_val'](dataset)
|
||
if (value === undefined || value === null) {
|
||
element.innerHTML = UNDEFINED_FIELD_TEXT
|
||
_setIndicatorClass(element, false)
|
||
} else {
|
||
_setIndicatorClass(element, updateFunctions[id]['handler'](element, value))
|
||
}
|
||
} catch (e) {
|
||
console.log(e)
|
||
}
|
||
}
|
||
}
|
||
}
|