node-red-contrib-knx-ultimate
Version:
Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable.
297 lines (261 loc) • 13.9 kB
JavaScript
module.exports = function (RED) {
function knxUltimateAlerter(config) {
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
const Address = require('./../KNXEngine/protocol/KNXAddress')
const KnxConstants = require("./../KNXEngine/protocol/KNXConstants");
RED.nodes.createNode(this, config)
var node = this;
node.server = RED.nodes.getNode(config.server)
node.name = config.name || "KNX Alerter";
node.listenallga = true; // Dont' remove this.
node.notifyreadrequest = false;
node.notifyresponse = true;
node.notifywrite = true; // Dont' remove this.
node.initialread = false
node.outputtype = "write"
node.outputRBE = "false"
node.inputRBE = "false"
node.rules = config.rules || [{}];
node.isalertnode = true; // Signal to config node, that this is a node scene controller
node.userDir = path.join(RED.settings.userDir, "knxultimatestorage"); // 09/03/2020 Storage of ttsultimate (otherwise, at each upgrade to a newer version, the node path is wiped out and recreated, loosing all custom files)
node.alertedDevices = [];
node.curIndexAlertedDevice = 0;
node.timerSend = null;
node.whentostart = config.whentostart === undefined ? "ifnewalert" : config.whentostart;
node.timerinterval = (config.timerinterval === undefined || config.timerinterval == "") ? "2" : config.timerinterval;
if (config.initialreadGAInRules === undefined) {
node.initialread = true;
} else {
node.initialread = config.initialreadGAInRules === "0" ? false: true;
}
try {
node.sysLogger = require("./utils/sysLogger.js").get({ loglevel: node.server.loglevel || "error" }); // 08/04/2021 new logger to adhere to the loglevel selected in the config-window
} catch (error) {
node.sysLogger = "error";
}
// Used to call the status update from the config node.
node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
if (node.server === null) return;
// Log only service statuses, not the GA values
if (dpt !== undefined) return;
if (dpt !== "") return;
var dDate = new Date();
// 30/08/2019 Display only the things selected in the config
GA = (typeof GA == "undefined" || GA == "") ? "" : "(" + GA + ") ";
devicename = devicename || "";
dpt = (typeof dpt == "undefined" || dpt == "") ? "" : " DPT" + dpt;
node.status({ fill: fill, shape: shape, text: GA + payload + ((node.listenallga && node.server.statusDisplayDeviceNameWhenALL) === true ? " " + devicename : "") + (node.server.statusDisplayDataPoint === true ? dpt : "") + (node.server.statusDisplayLastUpdate === true ? " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" : "") + " " + text });
}
// Used to call the status update from the config node.
node.setLocalStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
var dDate = new Date();
// 30/08/2019 Display only the things selected in the config
GA = (typeof GA == "undefined" || GA == "") ? "" : "(" + GA + ") ";
devicename = devicename || "";
dpt = (typeof dpt == "undefined" || dpt == "") ? "" : " DPT" + dpt;
try {
node.status({ fill: fill, shape: shape, text: GA + payload + ((node.listenallga && node.server.statusDisplayDeviceNameWhenALL) === true ? " " + devicename : "") + (node.server.statusDisplayDataPoint === true ? dpt : "") + (node.server.statusDisplayLastUpdate === true ? " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" : "") + " " + text });
} catch (error) {
}
}
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = msg => {
try {
if (!msg.knx.dpt.startsWith("1.")) return;
} catch (error) {
return;
}
let bFound = false; // 24/04/2021 true if the cycle below found a match, otherwise false
// Update the node.rules with the values taken from the file, if any, otherwise leave the default value
for (var i = 0; i < node.rules.length; i++) {
// rule is { topic: rowRuleTopic, devicename: rowRuleDeviceName, longdevicename: rowRuleLongDeviceName}
var rule = node.rules[i];
if (msg.topic === rule.topic) {
if (msg.payload == true) {
bFound = true;
// Add the device to the array of alertedDevices
let oTrovato = node.alertedDevices.find(a => a.topic === rule.topic);
if (oTrovato === undefined) {
node.alertedDevices.unshift({ topic: rule.topic, devicename: rule.devicename, longdevicename: rule.longdevicename }); // Add to the begin of array
if (node.whentostart === "ifnewalert") node.send([null, null, node.getThirdPinMSG()]);
}
node.setLocalStatus({ fill: "red", shape: "dot", text: "Alert", payload: "", GA: msg.topic, dpt: "", devicename: rule.devicename });
} else {
// Remove the device from the array
node.alertedDevices = node.alertedDevices.filter(a => a.topic !== msg.topic);
node.setLocalStatus({ fill: "green", shape: "dot", text: "Restore", payload: "", GA: msg.topic, dpt: "", devicename: rule.devicename });
}
}
}
// If there's some device to alert, stop current timer and restart
// This allow the last alerted device to be outputted immediately
if (bFound && node.whentostart === "ifnewalert" && node.alertedDevices.length > 0) {
clearTimeout(node.timerSend);
// Send directly the second and third message PIN
node.send([null, node.getSecondPinMSG(), null]);
node.curIndexAlertedDevice = 0; // Restart form the beginning
node.startTimer();
}
};
// Get the msg to be outputted on second PIN
node.getSecondPinMSG = () => {
if (node.alertedDevices.length > 0) {
let msg = {};
let sRet = "";
let sRetLong = "";
let sTopic = "";
node.alertedDevices.forEach(function (item) {
sTopic += item.topic + ", ";
if (item.devicename !== undefined && item.devicename !== "") sRet += item.devicename + ", ";
if (item.longdevicename !== undefined && item.longdevicename !== "") sRetLong += item.longdevicename + ", ";
});
sTopic = sTopic.slice(0, -2);
if (sRet.length > 2) sRet = sRet.slice(0, -2);
if (sRetLong.length > 2) sRetLong = sRetLong.slice(0, -2);
msg.topic = sTopic;
msg.devicename = sRet;
msg.longdevicename = sRetLong;
msg.count = node.alertedDevices.length;
msg.payload = true;
return msg;
}
}
// Get the msg to be outputted on third PIN
node.getThirdPinMSG = () => {
if (node.alertedDevices.length > 0) {
let msg = {};
let sRet = "";
let sRetLong = "";
let sTopic = "";
let item = node.alertedDevices[0]; // Pick the last alerted device
sTopic = item.topic;
if (item.devicename !== undefined && item.devicename !== "") sRet = item.devicename;
if (item.longdevicename !== undefined && item.longdevicename !== "") sRetLong = item.longdevicename;
msg.topic = sTopic;
msg.devicename = sRet;
msg.longdevicename = sRetLong;
msg.count = node.alertedDevices.length;
msg.payload = true;
return msg;
}
}
// 24/04/2021 perform a read on all GA in the rule list. Called both from node.on("input") and knxUltimate-config
node.initialReadAllDevicesInRules = () => {
if (node.server) {
let grpaddr = "";
for (var i = 0; i < node.rules.length; i++) {
// rule is { topic: rowRuleTopic, devicename: rowRuleDeviceName, longdevicename: rowRuleLongDeviceName}
var rule = node.rules[i];
// READ: Send a Read request to the bus
grpaddr = rule.topic;
try {
// Check if it's a group address
let ret = Address.KNXAddress.createFromString(grpaddr, Address.KNXAddress.TYPE_GROUP);
node.setLocalStatus({ fill: "grey", shape: "dot", text: "Read", payload: "", GA: grpaddr, dpt: "", devicename: rule.devicename });
node.server.writeQueueAdd({ grpaddr: grpaddr, payload: "", dpt: "", outputtype: "read", nodecallerid: node.id });
} catch (error) {
node.setLocalStatus({ fill: "grey", shape: "dot", text: "Not a KNX GA " + error.message, payload: "", GA: grpaddr, dpt: "", devicename: rule.devicename });
}
}
} else {
node.setLocalStatus({ fill: "red", shape: "ring", text: "No gateway selected. Unable to read from KNX bus", payload: "", GA: "", dpt: "", devicename: "" });
}
}
node.on("input", function (msg) {
if (typeof msg === "undefined") return;
if (msg.hasOwnProperty("start")) {
clearTimeout(node.timerSend);
node.curIndexAlertedDevice = 0; // Restart form the beginning
if (node.alertedDevices.length > 0) {
node.send([null, node.getSecondPinMSG(), node.getThirdPinMSG()]);
node.startTimer();
} else {
// Nothing more to output
node.sendNoMoreDevices();
}
return;
}
// 24/04/2021 if payload is read or the output type is set to "read", do a read
if ((msg.hasOwnProperty('readstatus') && msg.readstatus === true)) {
node.initialReadAllDevicesInRules();
return;
}
if (msg.topic === undefined) {
node.setLocalStatus({ fill: "grey", shape: "dot", text: "ERROR: You must provide a msg.topic", payload: "", GA: "", dpt: "", devicename: "" });
return;
}
if (msg.payload === undefined) {
node.setLocalStatus({ fill: "grey", shape: "dot", text: "ERROR: You must provide payload (true/false)", payload: "", GA: "", dpt: "", devicename: "" });
return;
}
msg.knx = { dpt: "1.001" };
node.handleSend(msg);
})
node.on("close", function (done) {
clearTimeout(node.timerSend);
if (node.server) {
node.server.removeClient(node)
}
done();
})
node.handleTimer = () => {
if (node.alertedDevices.length > 0) {
let count = node.alertedDevices.length;
if (node.curIndexAlertedDevice > count - 1) {
node.curIndexAlertedDevice = 0;
if (node.whentostart === "manualstart") {
node.curIndexAlertedDevice = 0; // Restart form the beginning
return;
}
}
// Create output message
try {
let curDev = node.alertedDevices[node.curIndexAlertedDevice]; // is { topic: rule.topic, devicename: rule.devicename }
let msg = {};
msg.topic = curDev.topic;
msg.count = count;
msg.devicename = curDev.devicename;
msg.longdevicename = curDev.longdevicename;
msg.payload = true;
node.send([msg, null, null]);
} catch (error) {
}
node.curIndexAlertedDevice += 1;
// Restart timer
node.startTimer();
} else {
// Nothing more to output
node.sendNoMoreDevices();
}
}
// Start timer
node.startTimer = () => {
clearTimeout(node.timerSend);
node.timerSend = setTimeout(() => {
node.handleTimer();
}, node.timerinterval * 1000);
}
// As soon as there no more devices..
node.sendNoMoreDevices = () => {
let msg = {};
msg.topic = "";
msg.count = 0;
msg.devicename = "";
msg.longdevicename = "";
msg.payload = false;
node.send([msg, msg, msg]);
}
// Init
node.sendNoMoreDevices();
// On each deploy, unsubscribe+resubscribe
if (node.server) {
node.server.removeClient(node);
if (node.topic !== "" || node.topicSave !== "") {
node.server.addClient(node);
}
}
}
RED.nodes.registerType("knxUltimateAlerter", knxUltimateAlerter)
}