@frangoteam/fuxa
Version:
Web-based Process Visualization (SCADA/HMI/Dashboard) software
656 lines (612 loc) • 24.3 kB
JavaScript
/*
* Alarms manager: check ... and save
*/
'use strict';
const alarmstorage = require('./alarmstorage');
var ALARMS_CHECK_STATUS_INTERVAL = 1000;
var TimeMultiplier = 1000; //1000 = rates are in seconds - alpaslanske
function AlarmsManager(_runtime) {
var runtime = _runtime;
var devices = runtime.devices; // Devices to ask variable value
var events = runtime.events; // Events to commit change to runtime
var settings = runtime.settings; // Settings
var logger = runtime.logger; // Logger
var alarmsCheckStatus = null; // TimerInterval to check Alarms status
var alarmsLoading = false; // Flag to check if loading
var working = false; // Working flag to manage overloading of check alarms status
var alarms = {}; // Alarms matrix, grupped by variable to check, [variable][...AlarmSubProperty + permission]
var alarmsProperty = {}; // Alarms property list, key = alarm name + ^~^ + type
var status = AlarmsStatusEnum.INIT; // Current status (StateMachine)
var clearAlarms = false; // Flag to clear current alarms from DB
var actionsProperty = {}; // Actions property list, key = alarm name + ^~^ + type
/**
* Start TimerInterval to check Alarms
*/
this.start = function () {
return new Promise(function (resolve, reject) {
logger.info('alarms check start', true);
alarmsCheckStatus = setInterval(function () {
_checkStatus();
}, ALARMS_CHECK_STATUS_INTERVAL);
});
}
/**
* Stop StateMachine, break TimerInterval (_checkStatus)
*/
this.stop = function () {
return new Promise(function (resolve, reject) {
logger.info('alarms.stop-checkstatus!', true);
if (alarmsCheckStatus) {
clearInterval(alarmsCheckStatus);
alarmsCheckStatus = null;
status = AlarmsStatusEnum.INIT;
working = false;
}
resolve();
});
}
this.reset = function () {
this.clear();
status = AlarmsStatusEnum.LOAD;
}
this.clear = function () {
clearAlarms = true;
}
/**
* Return the alarms status (active/passive alarms count), { highhigh: <count>, high: <count>, low: <count>, info: <count> }
*/
this.getAlarmsStatus = function () {
return new Promise(function (resolve, reject) {
alarmstorage.getAlarms().then(function (alrs) {
var result = { highhigh: 0, high: 0, low: 0, info: 0, actions: [] };
if (alrs) {
Object.values(alrs).forEach(alr => {
result[alr.type]++;
if (alr.type === AlarmsTypes.ACTION && !alr.offtime) {
var action = actionsProperty[alr.nametype];
if (action.subproperty) {
if (action.subproperty.type === ActionsTypes.POPUP || action.subproperty.type === ActionsTypes.SET_VIEW) {
result.actions.push({ type: action.subproperty.type, params: action.subproperty.actparam })
}
}
}
});
}
resolve(result);
}).catch(function (err) {
reject(err);
});
});
}
/**
* Return the current active alarms values
*/
this.getAlarmsValues = function (query, groups) {
var result = [];
Object.keys(alarms).forEach(alrkey => {
alarms[alrkey].forEach(alr => {
if (alr.status && alr.type !== AlarmsTypes.ACTION) {
var alritem = { name: alr.getId(), type: alr.type, ontime: alr.ontime, offtime: alr.offtime, acktime: alr.acktime,
status: alr.status, text: alr.subproperty.text, group: alr.subproperty.group,
bkcolor: alr.subproperty.bkcolor, color: alr.subproperty.color, toack: alr.isToAck() };
var toshow = true;
var canack = true;
if (alr.tagproperty && alr.tagproperty.permission) {
var mask = (alr.tagproperty.permission >> 8);
toshow = (mask) ? mask & groups : 1;
mask = (alr.tagproperty.permission & 255);
canack = (mask) ? mask & groups : 1;
}
if (toshow) {
if (!canack) {
alritem.toack = 0;
}
result.push(alritem);
}
}
});
});
return result;
}
this.getAlarmsString = function (type) {
var result = '';
Object.keys(alarms).forEach(alrkey => {
alarms[alrkey].forEach(alr => {
if (alr.status && alr.type === type && alr.ontime) {
var ontime = new Date(alr.ontime);
result += `${ontime.toLocaleString()} - ${alr.type} - ${alr.subproperty.text || ''} - ${alr.status} - ${alr.subproperty.group || ''}\n`;
}
});
});
return result;
}
/**
* Return the alarms history
*/
this.getAlarmsHistory = function (query, groups) {
return new Promise(function (resolve, reject) {
var history = [];
alarmstorage.getAlarmsHistory(query.from, query.to).then(result => {
for (var i = 0; i < result.length; i++) {
var alr = new AlarmHistory(result[i].nametype);
alr.status = result[i].status;
alr.text = result[i].text;
alr.ontime = result[i].ontime;
alr.offtime = result[i].offtime;
alr.acktime = result[i].acktime;
alr.userack = result[i].userack;
alr.group = result[i].grp;
if (alr.ontime) {
var toshow = true;
if (alarmsProperty[alr.name] && alarmsProperty[alr.name].property) {
var mask = (alarmsProperty[alr.name].property.permission >> 8);
var toshow = (mask) ? mask & groups : 1;
}
if (toshow) {
history.push(alr);
}
}
// add action or defined colors
if (alr.type === AlarmsTypes.ACTION) {
alr.text = `${alr.name}`;
alr.group = `Actions`;
} else if (alarmsProperty[alr.name] && alarmsProperty[alr.name][alr.type]) {
alr.bkcolor = alarmsProperty[alr.name][alr.type].bkcolor;
alr.color = alarmsProperty[alr.name][alr.type].color;
}
}
resolve(history);
}).catch(function (err) {
logger.error('alarms.load-current.failed: ' + err);
reject(err);
});
});
}
/**
* Set Ack to alarm
* @param {*} alarmname
* @returns
*/
this.setAlarmAck = function (alarmname, userId, groups) {
return new Promise(function (resolve, reject) {
var changed = [];
var authError = false;
Object.keys(alarms).forEach(alrkey => {
alarms[alrkey].forEach(alr => {
if (alr.getId() === alarmname) {
var mask = ((alr.tagproperty.permission >> 8) & 255);
var canack = (mask) ? mask & groups : 1;
if (canack) {
alr.setAck(userId);
changed.push(alr);
} else {
authError = true;
}
}
});
});
if (authError) {
reject({code: 401, error:"unauthorized_error", message: "Unauthorized!"});
} else {
if (changed.length) {
alarmstorage.setAlarms(changed).then(function (result) {
resolve(true);
}).catch(function (err) {
reject(err);
});
} else {
resolve(false);
}
}
});
}
this.clearAlarms = function (all) {
return new Promise(function (resolve, reject) {
alarmstorage.clearAlarms(all).then((result) => {
resolve(true);
}).catch(function (err) {
reject(err);
});
});
}
/**
* Check the Alarms state machine
*/
var _checkStatus = function () {
if (status === AlarmsStatusEnum.INIT) {
if (_checkWorking(true)) {
_init().then(function () {
status = AlarmsStatusEnum.LOAD;
_checkWorking(false);
}).catch(function (err) {
// devices.woking = null;
_checkWorking(false);
});
}
} else if (status === AlarmsStatusEnum.LOAD) {
if (_checkWorking(true)) {
_loadProperty().then(function () {
_loadAlarms().then(function () {
status = AlarmsStatusEnum.IDLE;
_emitAlarmsChanged();
_checkWorking(false);
}).catch(function (err) {
_checkWorking(false);
});
}).catch(function (err) {
_checkWorking(false);
});
}
} else if (status === AlarmsStatusEnum.IDLE) {
if (_checkWorking(true)) {
_checkAlarms().then(function (changed) {
if (changed) {
_emitAlarmsChanged(true);
}
_checkWorking(false);
}).catch(function (err) {
_checkWorking(false);
});
}
}
}
/**
* Check Alarms status
*/
var _checkAlarms = function () {
return new Promise(function (resolve, reject) {
var time = new Date().getTime();
var changed = [];
Object.keys(alarms).forEach(alrkey => {
var groupalarms = alarms[alrkey];
var tag = devices.getDeviceValue(alarms[alrkey]['variableSource'], alrkey);
if (tag !== null) {
groupalarms.forEach(alr => {
var value = _checkBitmask(alr, tag.value);
if (alr.check(time, tag.ts, value)) {
changed.push(alr);
}
});
}
});
if (changed.length) {
_checkActions(changed);
alarmstorage.setAlarms(changed).then(function (result) {
changed.forEach(alr => {
if (alr.toremove) {
alr.init();
}
});
resolve(true);
}).catch(function (err) {
reject(err);
});
} else {
resolve(false);
}
});
}
var _checkBitmask = function(alarm, value) {
if (alarm.tagproperty.bitmask) {
return (value & alarm.tagproperty.bitmask) ? 1 : 0;
}
return Number(value);
}
/**
* Init Alarm database
*/
var _init = function () {
return new Promise(function (resolve, reject) {
alarmstorage.init(settings, logger).then(result => {
logger.info('alarms.alarmstorage-init-successful!', true);
resolve();
}).catch(function (err) {
logger.error('project.prjstorage.failed-to-init: ' + err);
reject(err);
});
});
}
/**
* Load Alarms property in local for check
*/
var _loadProperty = function () {
return new Promise(function (resolve, reject) {
alarms = {};
alarmsProperty = {};
runtime.project.getAlarms().then(function (result) {
var alarmsFound = 0;
if (result) {
result.forEach(alr => {
if (alr.property && alr.property.variableId) {
if (!alarms[alr.property.variableId]) {
alarms[alr.property.variableId] = [];
var deviceId = devices.getDeviceIdFromTag(alr.property.variableId);
if (deviceId) {
// help for a fast get value
alarms[alr.property.variableId]['variableSource'] = deviceId;
}
}
if (_isAlarmEnabled(alr.highhigh)) {
var alarm = new Alarm(alr.name, AlarmsTypes.HIGH_HIGH, alr.highhigh, alr.property);
alarms[alr.property.variableId].push(alarm);
alarmsFound++;
}
if (_isAlarmEnabled(alr.high)) {
var alarm = new Alarm(alr.name, AlarmsTypes.HIGH, alr.high, alr.property);
alarms[alr.property.variableId].push(alarm);
alarmsFound++;
}
if (_isAlarmEnabled(alr.low)) {
var alarm = new Alarm(alr.name, AlarmsTypes.LOW, alr.low, alr.property);
alarms[alr.property.variableId].push(alarm);
alarmsFound++;
}
if (_isAlarmEnabled(alr.info)) {
var alarm = new Alarm(alr.name, AlarmsTypes.INFO, alr.info, alr.property);
alarms[alr.property.variableId].push(alarm);
alarmsFound++;
}
if (_isAlarmActionsEnabled(alr.actions)) {
for (var i = 0; i < alr.actions.values.length; i++) {
if (_isActionsValid(alr.actions.values[i])) {
var alarm = new Alarm(`${alr.name} - ${i}`, AlarmsTypes.ACTION, alr.actions.values[i], alr.property);
alarms[alr.property.variableId].push(alarm);
alarmsFound++;
actionsProperty[alarm.getId()] = alarm;
}
}
}
alarmsProperty[alr.name] = alr;
}
});
}
resolve();
}).catch(function (err) {
reject(err);
});
});
}
/**
* Load current Alarms and merge with loaded property
*/
var _loadAlarms = function () {
return new Promise(function (resolve, reject) {
if (clearAlarms) {
alarmstorage.clearAlarms().then(result => {
resolve();
clearAlarms = false;
}).catch(function (err) {
logger.error('alarms.clear-current.failed: ' + err);
reject(err);
});
} else {
alarmstorage.getAlarms().then(result => {
Object.keys(alarms).forEach(alrkey => {
var groupalarms = alarms[alrkey];
groupalarms.forEach(alr => {
var alrid = alr.getId();
var curalr = result.find(ca => ca.nametype === alrid);
if (curalr) {
alr.status = curalr.status;
alr.ontime = curalr.ontime;
alr.offtime = curalr.offtime;
alr.acktime = curalr.acktime;
}
});
});
resolve();
}).catch(function (err) {
logger.error('alarms.load-current.failed: ' + err);
reject(err);
});
}
});
}
var _checkActions = function (alarms) {
for (var i = 0; i < alarms.length; i++) {
if (alarms[i].type === AlarmsTypes.ACTION && alarms[i].subproperty && !alarms[i].offtime) {
if (alarms[i].subproperty.type === ActionsTypes.SET_VALUE) {
var deviceId = devices.getDeviceIdFromTag(alarms[i].subproperty.variableId);
if (deviceId) {
devices.setDeviceValue(deviceId, alarms[i].subproperty.variableId, alarms[i].subproperty.actparam);
} else {
logger.error(`alarms.action.deviceId not found: ${alarms[i].name}`);
}
}
}
}
}
var _checkWorking = function (check) {
if (check && working) {
logger.warn('alarms working (check) overload!');
return false;
}
working = check;
return true;
}
var _isAlarmEnabled = function (alarm) {
if (alarm && alarm.enabled && alarm.checkdelay > 0 && alarm.min && alarm.max && alarm.timedelay) {
return true;
}
return false;
}
var _isAlarmActionsEnabled = function (alarm) {
if (alarm && alarm.enabled && alarm.values && alarm.values.length > 0) {
return true;
}
return false;
}
var _isActionsValid = function (action) {
if (action && action.checkdelay > 0 && action.min && action.max && action.timedelay) {
return true;
}
return false;
}
var _emitAlarmsChanged = function () {
events.emit('alarms-status:changed');
}
var _formatDateTime = function (dt) {
var dt = new Date(dt);
return dt.toLocaleDateString() + '-' + dt.toLocaleTimeString();
}
}
module.exports = {
create: function (runtime) {
return new AlarmsManager(runtime);
}
}
/**
* State of StateMachine
*/
var AlarmsStatusEnum = {
INIT: 'init',
LOAD: 'load',
IDLE: 'idle',
}
function Alarm(name, type, subprop, tagprop) {
this.name = name;
this.type = type;
this.subproperty = subprop;
this.tagproperty = tagprop;
this.ontime = 0;
this.offtime = 0;
this.acktime = 0;
this.status = AlarmStatusEnum.VOID;
this.lastcheck = 0;
this.toremove = false;
this.userack;
this.getId = function () {
return this.name + '^~^' + this.type;
}
this.check = function (time, dt, value) {
if (this.lastcheck + (this.subproperty.checkdelay * TimeMultiplier) > time) {
return false;
}
this.lastcheck = time;
this.toremove = false;
var onrange = (value >= this.subproperty.min && value <= this.subproperty.max);
switch(this.status) {
case AlarmStatusEnum.VOID:
// check to activate
if (!onrange) {
return false;
} else if (!this.ontime) {
this.ontime = dt;
return false;
}
if (this.ontime + (this.subproperty.timedelay * TimeMultiplier) < time) {
this.status = AlarmStatusEnum.ON;
return true;
}
case AlarmStatusEnum.ON:
// check to deactivate
if (!onrange) {
this.status = AlarmStatusEnum.OFF;
if (this.offtime == 0) {
this.offtime = time;
}
// remove if float or already acknowledged
if (this.subproperty.ackmode === AlarmAckModeEnum.float || this.acktime) {
this.toRemove();
}
return true;
}
if (this.acktime) {
this.status = AlarmStatusEnum.ACK;
return true;
}
return false;
case AlarmStatusEnum.OFF:
// check to reactivate
if (onrange) {
this.status = AlarmStatusEnum.ON;
this.acktime = 0;
this.offtime = 0;
this.ontime = time;
this.userack = '';
return true;
}
// remove if acknowledged
if (this.acktime || this.type === AlarmsTypes.ACTION) {
this.toRemove();
return true;
}
return false;
case AlarmStatusEnum.ACK:
// remove if deactivate
if (!onrange) {
if (this.offtime == 0) {
this.offtime = time;
}
this.status = AlarmStatusEnum.ON;
return true;
}
return false;
}
}
this.init = function () {
this.toremove = false;
this.ontime = 0;
this.offtime = 0;
this.acktime = 0;
this.status = AlarmStatusEnum.VOID;
this.lastcheck = 0;
this.userack = '';
}
this.toRemove = function () {
this.toremove = true;
}
this.setAck = function (user) {
if (!this.acktime) {
this.acktime = new Date().getTime();
this.lastcheck = 0;
this.userack = user;
}
}
this.isToAck = function () {
if (this.subproperty.ackmode === AlarmAckModeEnum.float) {
return -1;
}
if (this.subproperty.ackmode === AlarmAckModeEnum.ackpassive && this.status === AlarmStatusEnum.ON) {
return 0;
}
return 1;
}
}
function AlarmHistory(id) {
this.name;
this.type;
this.laststatus;
this.alarmtext;
this.ontime = 0;
this.offtime = 0;
this.acktime = 0;
this.userack;
var ids = id.split('^~^');
this.name = ids[0];
this.type = ids[1];
}
var AlarmStatusEnum = {
VOID: '',
ON: 'N',
OFF: 'NF',
ACK: 'NA'
}
var AlarmAckModeEnum = {
float: 'float',
ackactive: 'ackactive',
ackpassive: 'ackpassive'
}
const AlarmsTypes = {
HIGH_HIGH: 'highhigh',
HIGH: 'high',
LOW: 'low',
INFO: 'info',
ACTION: 'action'
}
const ActionsTypes = {
POPUP: 'popup',
SET_VALUE: 'setValue',
SET_VIEW: 'setView',
SEND_MSG: 'sendMsg'
}