ospaz-site/static/js/index-main.js

398 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// скрипт страницы 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() {
try {
const chartTime = localStorage.getItem("settings-chart-time")
const res = await makeRequest('/fetch/tank-chart?days=' + chartTime)
document.getElementById('chart-update-error').hidden = true
return res['tank_chart']
} catch (e) {
document.getElementById('chart-update-error').hidden = false
return undefined
}
}
async function loadLastUpdates() {
try {
const res = await makeRequest('/fetch/stats')
document.getElementById('stats-update-error').hidden = true
return res['stats']
} catch (e) {
document.getElementById('stats-update-error').hidden = false
return undefined
}
}
/**
* Функция, устанавливающая классы 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 < 600
}
},
// последнее обновление насосной
'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 & 0x18) === 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: "ожидание сигнала на перекачку воды",
101: "запуск: продувка",
102: "запуск: сброс конденсата",
105: "запуск: открытие задвижки 23.6",
110: "запуск: ожидание сигнала от датчика уровня поз.36",
121: "запуск: открытие задвижек насоса",
131: "запуск: закрытие задвижки 23.6",
141: "запуск: пуск ПЧ",
200: "работает",
202: "остановка: закрытие задвижек 23.3 и 23.4",
211: "остановка: ожидание остановки ПЧ",
221: "остановка: перевод запорной арматуры в исходное состояние",
}
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
}
},
'pump-accumulated-flow': {
'get_val': (dataset) => { return dataset['hart']['accumulated_flow'] },
'handler': (element, value) => {
element.innerHTML = value
return null
}
},
// 'id': {
// 'get_val': (dataset) => { return dataset['...']['...'] },
// 'handler': (element, value) => {
// element.innerHTML = value
// return null
// }
// },
}
async function updateStatus() {
const dataset = await loadLastUpdates()
if (dataset === undefined) {
return
}
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)
}
}
}
}