346 lines
12 KiB
JavaScript
Executable file
346 lines
12 KiB
JavaScript
Executable file
exports.id = 'modbus_reader';
|
|
exports.title = 'Modbus reader';
|
|
exports.version = '2.0.0';
|
|
exports.group = 'Worksys';
|
|
exports.color = '#2134B0';
|
|
exports.input = 1;
|
|
exports.output = ["red", "white", "yellow"];
|
|
exports.click = false;
|
|
exports.author = 'Rastislav Kovac';
|
|
exports.icon = 'bolt';
|
|
exports.readme = `
|
|
Modbus requests to modbus devices (electromer, twilight sensor, thermometer.
|
|
Component keeps running arround deviceConfig array in "timeoutInterval" intervals. Array items are objects with single modbus devices.
|
|
Everything is sent to dido_controller. If requests to device fail (all registers must fail to send NOK status) , we send "NOK-'device'" status to dido_controller.
|
|
This device needs to be configured in dido_controller!!! Double check if it is. In dido_controller we calculate final status and all values with status are pushed to tb.
|
|
`;
|
|
|
|
const modbus = require('jsmodbus');
|
|
const SerialPort = require('serialport');
|
|
const { timeoutInterval, deviceConfig } = require("../databases/modbus_config");
|
|
const { sendNotification } = require('./helper/notification_reporter');
|
|
|
|
const DELAY_BETWEEN_DEVICES = 10000;
|
|
|
|
const SEND_TO = {
|
|
debug: 0,
|
|
dido_controller: 1,
|
|
tb: 2
|
|
};
|
|
|
|
//to handle NOK and OK sendNotifications s
|
|
const numberOfNotResponding = {};
|
|
let tbName = null;
|
|
let mainSocket;
|
|
//number of phases inRVO
|
|
let phases;
|
|
//phases where voltage is 0 (set)
|
|
let noVoltage;
|
|
let energyToSwitchLamps;
|
|
|
|
exports.install = function(instance) {
|
|
|
|
class SocketWithClients {
|
|
|
|
constructor() {
|
|
this.stream = null;
|
|
this.socket = null;
|
|
this.clients = {};
|
|
this.allValues = {};
|
|
this.errors = 0;
|
|
this.index = 0;
|
|
this.timeoutInterval = 5000;
|
|
|
|
// we need to go always around for all devices. So we need index value, device address, as well as number of registers for single device
|
|
this.deviceAddress = null; // device address (1 - EM340 and 2 for twilight_sensor)
|
|
this.indexInDeviceConfig = 0; // first item in deviceConfig
|
|
this.lengthOfActualDeviceStream = null;
|
|
this.device = null;
|
|
|
|
// lampSwitchNotification helper variables
|
|
this.onNotificationSent = false;
|
|
this.offNotificationSent = false;
|
|
this.phases = this.buildPhases();
|
|
|
|
this.startSocket();
|
|
}
|
|
|
|
buildPhases = () => {
|
|
let a = [];
|
|
for (let i = 1; i <= phases; i++) {
|
|
a.push(`Phase_${i}_voltage`)
|
|
}
|
|
return a;
|
|
}
|
|
|
|
startSocket = () => {
|
|
|
|
let obj = this;
|
|
|
|
if (this.socket) {
|
|
this.socket.removeAllListeners();
|
|
this.socket = null;
|
|
}
|
|
|
|
this.socket = new SerialPort("/dev/ttymxc0", {
|
|
baudRate: 9600,
|
|
})
|
|
|
|
// we create a client for every deviceAddress ( = address) in list and push them into dictionary
|
|
for (let i = 0; i < deviceConfig.length; i++) {
|
|
this.clients[deviceConfig[i].deviceAddress] = new modbus.client.RTU(this.socket, deviceConfig[i].deviceAddress, 2000); // 2000 is timeout in register request, default is 5000, which is too long
|
|
}
|
|
|
|
this.socket.on('error', function(e) {
|
|
console.log('Modbus_reader: Socket connection error', e); //'ECONNREFUSED' or 'ECONNRESET' ??
|
|
});
|
|
|
|
this.socket.on('close', function() {
|
|
console.log('Modbus_reader: Socket connection closed - Waiting 10 seconds before connecting again');
|
|
setTimeout(obj.startSocket, 10000);
|
|
});
|
|
|
|
this.socket.on('open', function() {
|
|
console.log("socket connected");
|
|
obj.getActualStreamAndDevice();
|
|
obj.timeoutInterval = timeoutInterval - DELAY_BETWEEN_DEVICES; // to make sure readout always runs in timeoutinterval we substract DELAY_BETWEEN_DEVICES
|
|
})
|
|
|
|
};
|
|
|
|
getActualStreamAndDevice = () => {
|
|
const dev = deviceConfig[this.indexInDeviceConfig];
|
|
this.index = 0;
|
|
this.errors = 0;
|
|
this.stream = dev.stream;
|
|
this.lengthOfActualDeviceStream = dev.stream.length;
|
|
this.deviceAddress = dev.deviceAddress; // 1 or 2 or any number
|
|
this.device = dev.device; //em340, twilight_sensor
|
|
|
|
//if we just start to loop devices from the beginning, or there is just 1 device in config, we wait whole timeoutInterval
|
|
if (this.indexInDeviceConfig == 0 || deviceConfig.length === 1) setTimeout(this.readRegisters, this.timeoutInterval);
|
|
else setTimeout(this.readRegisters, DELAY_BETWEEN_DEVICES);
|
|
}
|
|
|
|
readRegisters = () => {
|
|
|
|
const str = this.stream[this.index];
|
|
const register = str.register;
|
|
const size = str.size;
|
|
const tbAttribute = str.tbAttribute;
|
|
|
|
let obj = this;
|
|
|
|
this.clients[this.deviceAddress].readHoldingRegisters(register, size)
|
|
.then(function(resp) {
|
|
|
|
resp = resp.response._body.valuesAsArray; //resp is array of length 1 or 2, f.e. [2360,0]
|
|
// console.log(deviceAddress, register, tbAttribute, resp);
|
|
|
|
//device is responding again after NOK status
|
|
if (numberOfNotResponding.hasOwnProperty(obj.device)) {
|
|
let message = "";
|
|
if (obj.device == "em340") {
|
|
message = "electrometer_ok";
|
|
}
|
|
else if (obj.device == "twilight_sensor") {
|
|
message = "twilight_sensor_ok";
|
|
}
|
|
message && sendNotification("modbus_reader: readRegisters", tbName, message, {}, "", SEND_TO.tb, instance);
|
|
delete numberOfNotResponding[obj.device];
|
|
}
|
|
|
|
obj.transformResponse(resp, register);
|
|
|
|
//obj.errors = 0;
|
|
obj.index++;
|
|
obj.readAnotherRegister();
|
|
|
|
}).catch(function() {
|
|
|
|
//console.log("errors pri citani modbus registra", register, obj.indexInDeviceConfig, tbName, tbAttribute);
|
|
|
|
obj.errors++;
|
|
if (obj.errors == obj.lengthOfActualDeviceStream) {
|
|
instance.send(SEND_TO.dido_controller, { status: "NOK-" + obj.device }); // NOK-em340, NOK-em111, NOK-twilight_sensor, NOK-thermometer
|
|
|
|
//todo - neposlalo notification, ked sme vypojili twilight a neposle to do tb, ale do dido ??
|
|
if (!numberOfNotResponding.hasOwnProperty(obj.device)) {
|
|
let message = "";
|
|
if (obj.device == "twilight_sensor") {
|
|
message = "twilight_sensor_nok";
|
|
}
|
|
else if (obj.device == "em340") {
|
|
message = "electrometer_nok";
|
|
}
|
|
message && sendNotification("modbus_reader: readingTimeouted", tbName, message, {}, "", SEND_TO.tb, instance);
|
|
numberOfNotResponding[obj.device] = 1;
|
|
}
|
|
|
|
obj.errors = 0;
|
|
numberOfNotResponding[obj.device] += 1;
|
|
}
|
|
|
|
// console.error(require('util').inspect(arguments, {
|
|
// depth: null
|
|
// }))
|
|
|
|
// if reading out of device's last register returns error, we send accumulated allValues to dido_controller (if allValues are not an empty object)
|
|
if (obj.index + 1 >= obj.lengthOfActualDeviceStream) {
|
|
if (!isObjectEmpty(obj.allValues)) instance.send(SEND_TO.dido_controller, { values: obj.allValues });
|
|
obj.allValues = {};
|
|
}
|
|
obj.index++;
|
|
obj.readAnotherRegister();
|
|
})
|
|
|
|
};
|
|
|
|
readAnotherRegister = () => {
|
|
if (this.index < this.lengthOfActualDeviceStream) setTimeout(this.readRegisters, 0);
|
|
else this.setNewStream();
|
|
}
|
|
|
|
transformResponse = (response, register) => {
|
|
|
|
for (let i = 0; i < this.lengthOfActualDeviceStream; i++) {
|
|
|
|
let a = this.stream[i];
|
|
if (a.register === register) {
|
|
let tbAttribute = a.tbAttribute;
|
|
let multiplier = a.multiplier;
|
|
|
|
let value = this.calculateValue(response, multiplier);
|
|
// console.log(register, tbName, tbAttribute, response, a.multiplier, value);
|
|
|
|
// if(tbName == undefined) return;
|
|
|
|
if (this.index + 1 < this.lengthOfActualDeviceStream) {
|
|
this.allValues[tbAttribute] = value;
|
|
return;
|
|
}
|
|
|
|
const values = {
|
|
...this.allValues,
|
|
[tbAttribute]: value,
|
|
};
|
|
|
|
this.checkNullVoltage(values);
|
|
this.lampSwitchNotification(values);
|
|
|
|
instance.send(SEND_TO.dido_controller, { values: values });
|
|
|
|
this.allValues = {};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
setNewStream = () => {
|
|
if (this.lengthOfActualDeviceStream == this.index) {
|
|
if (this.indexInDeviceConfig + 1 == deviceConfig.length) {
|
|
this.indexInDeviceConfig = 0;
|
|
}
|
|
else {
|
|
this.indexInDeviceConfig += 1;
|
|
}
|
|
|
|
this.getActualStreamAndDevice();
|
|
}
|
|
}
|
|
|
|
calculateValue = (response, multiplier) => {
|
|
let value = 0;
|
|
|
|
let l = response.length;
|
|
if (l === 2) {
|
|
value = (response[1] * (2 ** 16) + response[0]);
|
|
|
|
if (value >= (2 ** 31)) // ak je MSB bit nastavený, eventuálne sa dá použiť aj (value & 0x80000000), ak vieš robiť logický súčin
|
|
{
|
|
value = value - "0xFFFFFFFF" + 1;
|
|
}
|
|
}
|
|
else if (l === 1) {
|
|
value = response[0];
|
|
|
|
if (value >= (2 ** 15)) // ak je MSB bit nastavený, eventuálne sa dá použiť aj (value & 0x8000), ak vieš robiť logický súčin
|
|
{
|
|
value = value - "0xFFFF" + 1;
|
|
}
|
|
}
|
|
|
|
return Math.round(value * multiplier * 10) / 10;
|
|
}
|
|
|
|
checkNullVoltage = (values) => {
|
|
|
|
if (!(values.hasOwnProperty("Phase_1_voltage") || values.hasOwnProperty("Phase_2_voltage") || values.hasOwnProperty("Phase_3_voltage"))) return;
|
|
|
|
Object.keys(values).map(singleValue => {
|
|
if (this.phases.includes(singleValue)) {
|
|
let l = singleValue.split("_");
|
|
let phase = parseInt(l[1]);
|
|
|
|
// console.log(values[singleValue], tbName);
|
|
|
|
if (values[singleValue] == 0) {
|
|
noVoltage.add(phase);
|
|
sendNotification("modbus_reader: checkNullVoltage", tbName, "no_voltage_on_phase", { phase: phase }, "", SEND_TO.tb, instance, "voltage" + phase);
|
|
// console.log('no voltage')
|
|
}
|
|
else {
|
|
noVoltage.delete(phase);
|
|
// console.log('voltage detected')
|
|
sendNotification("modbus_reader: checkNullVoltage", tbName, "voltage_on_phase_restored", { phase: phase }, "", SEND_TO.tb, instance, "voltage" + phase);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* function sends notification to slack and to tb, if EM total_power value changes more than numberOfNodes*15. This should show, that RVO lamps has been switched on or off
|
|
*/
|
|
lampSwitchNotification = (values) => {
|
|
|
|
if (!values.hasOwnProperty("total_power")) return;
|
|
|
|
const actualTotalPower = values.total_power;
|
|
|
|
if (actualTotalPower > energyToSwitchLamps && this.onNotificationSent == false) {
|
|
sendNotification("modbus_reader: lampSwitchNotification", tbName, "lamps_have_turned_on", {}, "", SEND_TO.tb, instance);
|
|
this.onNotificationSent = true;
|
|
this.offNotificationSent = false;
|
|
}
|
|
else if (actualTotalPower <= energyToSwitchLamps && this.offNotificationSent == false) {
|
|
sendNotification("modbus_reader: lampSwitchNotification", tbName, "lamps_have_turned_off", {}, "", SEND_TO.tb, instance);
|
|
this.onNotificationSent = false;
|
|
this.offNotificationSent = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const isObjectEmpty = (objectName) => {
|
|
return Object.keys(objectName).length === 0 && objectName.constructor === Object;
|
|
}
|
|
|
|
function main() {
|
|
|
|
phases = FLOW.GLOBALS.settings.phases;
|
|
tbName = FLOW.GLOBALS.settings.rvoTbName;
|
|
noVoltage = FLOW.GLOBALS.settings.no_voltage;
|
|
energyToSwitchLamps = FLOW.GLOBALS.settings.energy_to_switch_lamps / 2.5; //half value is enought to show if lamps are turned on or off
|
|
if (deviceConfig.length) mainSocket = new SocketWithClients();
|
|
else console.log("Modbus_reader: no modbus device in configuration");
|
|
|
|
// this notification is to show, that flow (unipi) has been restarted
|
|
sendNotification("modbus_reader", tbName, "flow_restart", {}, "", SEND_TO.slack, instance);
|
|
}
|
|
|
|
instance.on("0", function(_) {
|
|
main();
|
|
})
|
|
|
|
}
|
|
|