UNPKG

@frangoteam/fuxa

Version:

Web-based Process Visualization (SCADA/HMI/Dashboard) software

493 lines (462 loc) 17.4 kB
/** * 'http': webapi wrapper to manage REST API request */ 'use strict'; const axios = require('axios'); const utils = require('../../utils'); const deviceUtils = require('../device-utils'); function HTTPclient(_data, _logger, _events, _runtime) { var runtime = _runtime; var data = _data; // Current webapi data var logger = _logger; // Logger var working = false; // Working flag to manage overloading polling and connection var working = false; // Working flag to manage overloading polling and connection var connected = false; // Connected flag var events = _events; // Events to commit change to runtime var lastStatus = ''; // Last webapi status var varsValue = []; // Signale to send to frontend { id, type, value } var requestItemsMap = {}; // Map of request (JSON, CSV, XML, ...) {key: item path, value: tag} var overloading = 0; // Overloading counter to mange the break connection var lastTimestampRequest; // Last Timestamp request var lastTimestampValue; // Last Timestamp of asked values var newItemsCount; // Count of new items, between load and received var apiProperty = { getTags: null, postTags: null, format: 'JSON', ownFlag: true }; /** * Connect the client by make a request */ this.connect = function () { return new Promise(function (resolve, reject) { if (_checkConnection()) { try { if (_checkWorking(true)) { logger.info(`'${data.name}' try to connect ${apiProperty.getTags}`, true); _clearVarsValue(); _emitStatus('connect-ok'); resolve(); connected = true; lastTimestampRequest = new Date().getTime(); _checkWorking(false); } else { reject(); } } catch (err) { logger.error(`'${data.name}' try to connect error! ${err}`); _checkWorking(false); connected = false; _emitStatus('connect-error'); _clearVarsValue(); reject(); } } else { logger.error(`'${data.name}' missing connection data!`); connected = false; _emitStatus('connect-failed'); _clearVarsValue(); reject(); } }); } /** * Disconnect the client * Emit connection status to clients, clear all Tags values */ this.disconnect = function () { return new Promise(function (resolve, reject) { _checkWorking(false); logger.info(`'${data.name}' disconnected!`, true); connected = false; _emitStatus('connect-off'); _clearVarsValue(); resolve(); }); } /** * Read values in polling mode * Update the tags values list, save in DAQ if value changed or in interval and emit values to clients */ this.polling = async function () { if (_checkWorking(true)) { // check connection status let dt = new Date().getTime(); if ((lastTimestampRequest + (data.polling * 3)) < dt) { _emitStatus('connect-error'); _checkWorking(false); } try { const result = await _readRequest(); if (result) { let varsValueChanged = await _updateVarsValue(result); lastTimestampValue = new Date().getTime(); _emitValues(varsValue); if (this.addDaq && !utils.isEmptyObject(varsValueChanged)) { this.addDaq(varsValueChanged, data.name, data.id); } if (lastStatus !== 'connect-ok') { _emitStatus('connect-ok'); } } _checkWorking(false); } catch (reason){ logger.error(`'${data.name}' _readRequest error! ${reason}`); _checkWorking(false); }; } else { _emitStatus('connect-busy'); } } /** * Return if http request is working * is disconnected if the last request result is older as 3 polling interval */ this.isConnected = function () { return connected; } /** * Set the callback to set value to DAQ */ this.bindAddDaq = function (fnc) { this.addDaq = fnc; // Add the DAQ value to db history } this.addDaq = null; // Callback to add the DAQ value to db history /** * Return the timestamp of last read tag operation on polling * @returns */ this.lastReadTimestamp = () => { return lastTimestampValue; } /** * Load Tags to read by polling */ this.load = function (_data) { varsValue = []; data = JSON.parse(JSON.stringify(_data)); try { requestItemsMap = {}; var count = Object.keys(data.tags).length; for (var id in data.tags) { const address = data.tags[id].address || data.tags[id].id; if (!requestItemsMap[address]) { requestItemsMap[address] = [data.tags[id]]; } else { requestItemsMap[address].push(data.tags[id]); } } logger.info(`'${data.name}' data loaded (${count})`, true); } catch (err) { logger.error(`'${data.name}' load error! ${err}`); } } /** * Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> } */ this.getValue = function (id) { if (varsValue[id]) { return {id: id, value: varsValue[id].value, ts: lastTimestampValue }; } return null; } /** * Return Tags values array { id: <name>, value: <value>, type: <type> } */ this.getValues = function () { return varsValue; } /** * Set the Tag value, not used */ this.setValue = async function (tagId, value) { if (apiProperty.ownFlag && data.tags[tagId]) { if (apiProperty.postTags) { value = _parseValue(data.tags[tagId].type, value); data.tags[tagId].value = await deviceUtils.tagRawCalculator(value, data.tags[tagId], runtime); axios.post(apiProperty.getTags, [{id: tagId, value: data.tags[tagId].value}]).then(res => { lastTimestampRequest = new Date().getTime(); logger.info(`setValue '${data.tags[tagId].name}' to ${value})`, true, true); }).catch(err => { logger.error(`setValue '${data.tags[tagId].name}' error! ${err}`); }); } else { logger.error(`postTags undefined (setValue)`, true); } return true; } else { logger.error(`setValue not supported!`, true); } return false; } /** * Return connection status 'connect-off', 'connect-ok', 'connect-error' */ this.getStatus = function () { return lastStatus; } /** * Return Tag property */ this.getTagProperty = function (id) { if (data.tags[id]) { return { id: id, name: data.tags[id].name, type: data.tags[id].type, format: data.tags[id].format }; } else { return null; } } /** * Return Tags property */ this.getTagsProperty = function () { return new Promise(function (resolve, reject) { try { resolve({ tags: Object.values(requestItemsMap), newTagsCount: newItemsCount }); } catch (err) { reject(err); } }); } /** * Return the Daq settings of Tag * @returns */ this.getTagDaqSettings = (tagId) => { return data.tags[tagId] ? data.tags[tagId].daq : null; } /** * Set Daq settings of Tag * @returns */ this.setTagDaqSettings = (tagId, settings) => { if (data.tags[tagId]) { utils.mergeObjectsValues(data.tags[tagId].daq, settings); } } var _checkConnection = function () { if (data.property.address) { apiProperty.getTags = data.property.address; apiProperty.ownFlag = false; } else { apiProperty.getTags = data.property.getTags; apiProperty.postTags = data.property.postTags; } return apiProperty.getTags || apiProperty.postTags; } var _readRequest = function () { return new Promise(function (resolve, reject) { if (apiProperty.getTags) { axios.get(apiProperty.getTags).then(res => { lastTimestampRequest = new Date().getTime(); resolve(res.data); }).catch(err => { reject(err); }); } else { reject(); } }); } /** * Clear the Tags values by setting to null * Emit to clients */ var _clearVarsValue = function () { for (var id in varsValue) { varsValue[id].value = null; } _emitValues(varsValue); } /** * Update the Tags values read * For WebAPI NotOwn: first convert the request data to a flat struct * @param {*} reqdata */ var _updateVarsValue = async (reqdata) => { const timestamp = new Date().getTime(); var changed = {}; if (apiProperty.ownFlag) { var newItems = 0; for (var i = 0; i < reqdata.length; i++) { const id = reqdata[i].id; if (id) { if (!data.tags[id]) { newItems++; } else { reqdata[i].daq = data.tags[id].daq; } requestItemsMap[id] = [reqdata[i]]; reqdata[i].changed = varsValue[id] && reqdata[i].value !== varsValue[id].value; if (!utils.isNullOrUndefined(reqdata[i].value)) { reqdata[i].value = await deviceUtils.tagValueCompose(reqdata[i].value, varsValue[id] ? varsValue[id].value : null, data.tags[id], runtime); reqdata[i].timestamp = timestamp; if (this.addDaq && deviceUtils.tagDaqToSave(reqdata[i], timestamp)) { changed[id] = reqdata[i]; } } reqdata[i].changed = false; varsValue[id] = reqdata[i]; } } newItemsCount = newItems; return changed; } else { var someval = false; var result = {}; var items = dataToFlat(reqdata, apiProperty); for (var key in items) { if (requestItemsMap[key]) { for (var index in requestItemsMap[key]) { var tag = requestItemsMap[key][index]; if (tag) { someval = true; result[tag.id] = { id: tag.id, value: (tag.memaddress) ? items[tag.memaddress] : items[key], type: items[key]?.type, daq: tag.daq, tagref: tag }; } } } } if (someval) { for (var id in result) { result[id].changed = varsValue[id] && result[id].value !== varsValue[id].value; if (!utils.isNullOrUndefined(result[id].value)) { result[id].value = await deviceUtils.tagValueCompose(result[id].value, varsValue[id] ? varsValue[id].value : null, result[id].tagref, runtime); result[id].timestamp = timestamp; if (this.addDaq && deviceUtils.tagDaqToSave(result[id], timestamp)) { changed[id] = result[id]; } } result[id].changed = false; varsValue[id] = result[id]; } return changed; } } return null; } /** * Emit the webapi connection status * @param {*} status */ var _emitStatus = function (status) { lastStatus = status; events.emit('device-status:changed', { id: data.id, status: status }); } /** * Emit the webapi Tags values array { id: <name>, value: <value>, type: <type> } * @param {*} values */ var _emitValues = function (values) { events.emit('device-value:changed', { id: data.id, values: values }); } /** * Used to manage the async connection and polling automation (that not overloading) * @param {*} check */ var _checkWorking = function (check) { if (check && working) { overloading++; logger.warn(`'${data.name}' working (connection || polling) overload! ${overloading}`); // !The driver don't give the break connection if (overloading >= 3) { connected = false; // disconnect(); } else { return false; } } working = check; overloading = 0; return true; } /** * Cheack and parse the value return converted value * @param {*} type as string * @param {*} value as string * return converted value */ var _parseValue = function (type, value) { if (type === 'number') { return parseFloat(value); } else if (type === 'boolean') { return Boolean(value); } else if (type === 'string') { return value; } else { let val = parseFloat(value); if (Number.isNaN(val)) { // maybe boolean val = Number(value); // maybe string if (Number.isNaN(val)) { val = value; } } else { val = parseFloat(val.toFixed(5)); } return val; } } } function dataToFlat(data, property) { var parseTree = function(nodes, id, parent) { let result = {}; let nodeId = id; if (parent) { nodeId = parent + ':' + nodeId; } if (Array.isArray(nodes)) { let idx = 0; for(var key in nodes) { let tres = parseTree(nodes[key], '[' + idx++ + ']', nodeId); Object.keys(tres).forEach( key => { result[key] = tres[key]; }); } } else if (nodes && typeof nodes === 'object') { for(var key in nodes) { let tres = parseTree(nodes[key], key, nodeId); Object.keys(tres).forEach( key => { result[key] = tres[key]; }); } } else { result[nodeId] = nodes; } return result; } if (property.format === 'CSV') { } else if (property.format === 'JSON') { return parseTree(data); } return data; } /** * Return the result of http request */ function getRequestResult(property) { return new Promise(function (resolve, reject) { try { if (property.method === 'GET') { axios.get(property.address).then(res => { resolve(res.data); }).catch(err => { reject(err); }); } else { reject('getrequestresult-error: method is missing!'); } } catch (err) { reject('getrequestresult-error: ' + err); } }); } module.exports = { init: function (settings) { // deviceCloseTimeout = settings.deviceCloseTimeout || 15000; }, create: function (data, logger, events, runtime) { return new HTTPclient(data, logger, events, runtime); }, getRequestResult: getRequestResult }