// скрипт страницы 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 } }, // Уровень воды '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 } }, // Текущий уровень воды: % '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 } }, // Статус: '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 } }, // Частота ПЧ: Гц 'pump-vfd-freq': { 'get_val': (dataset) => { return dataset['pump']['vfd_freq'] }, 'handler': (element, value) => { element.innerHTML = value return null } }, // Ток ПЧ: А 'pump-vfd-curr': { 'get_val': (dataset) => { return dataset['pump']['vfd_curr'] }, 'handler': (element, value) => { element.innerHTML = value return null } }, // Ошибка ПЧ: '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 } }, // Регистр аварий: '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 } }, // Состояние КА: '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 } } }, // Текущий расход: м³ 'pump-flow-meter': { 'get_val': (dataset) => { return dataset['pump']['flow_meter'] }, 'handler': (element, value) => { element.innerHTML = value return null } }, // Запущенный насос: '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: 'pump-moto-watch-1': { 'get_val': (dataset) => { return dataset['pump']['moto_watch_1'] }, 'handler': (element, value) => { element.innerHTML = value return null } }, // Моточасы насоса 2: 'pump-moto-watch-2': { 'get_val': (dataset) => { return dataset['pump']['moto_watch_2'] }, 'handler': (element, value) => { element.innerHTML = value return null } }, // Дистанционный режим: '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() { 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) } } } }