node-red-contrib-knx-ultimate
Version:
Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control and ETS group address importer. Easy to use and highly configurable.
266 lines (242 loc) • 12.1 kB
JavaScript
const loggerClass = require('./utils/sysLogger')
module.exports = function (RED) {
const dptlib = require('knxultimate').dptlib;
const fs = require("fs");
const path = require("path");
// msg is:
// // Build final input message object
// return {
// topic: _outputtopic
// , payload: jsValue
// , devicename: (typeof _devicename !== 'undefined') ? _devicename : ""
// , payloadmeasureunit: sPayloadmeasureunit
// , payloadsubtypevalue: sPayloadsubtypevalue
// , knx:
// {
// event: _event
// , dpt: sInputDpt
// //, details: dpt
// , dptdesc: sDptdesc
// , source: _srcGA
// , destination: _destGA
// , rawValue: _Rawvalue
// }
// };
// The node.exposedGAs is and array of:
// {
// address,
// dpt,
// payload
// }
function knxUltimateAutoResponder(config) {
RED.nodes.createNode(this, config)
const node = this
node.serverKNX = RED.nodes.getNode(config.server)
node.topic = node.name
node.name = config.name === undefined ? 'Auto responder' : config.name
node.outputtopic = node.name
node.dpt = ''
node.notifyreadrequest = true
node.notifyresponse = true
node.notifywrite = true
node.initialread = false
node.listenallga = true
node.outputtype = 'write'
node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
node.exposedGAs = [];
node.commandText = []; // Raw list Respond To
node.timerSaveExposedGAs = null;
const pushStatus = (status) => {
if (!status) return;
const provider = node.serverKNX;
if (provider && typeof provider.applyStatusUpdate === 'function') {
provider.applyStatusUpdate(node, status);
} else {
node.status(status);
}
};
const updateStatus = (status) => {
if (!status) return;
pushStatus(status);
};
if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
try {
const baseLogLevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error';
node.sysLogger = new loggerClass({ loglevel: baseLogLevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
} catch (error) { console.log(error.stack) }
// Used to call the status update from the config node.
node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
// try {
// if (node.serverKNX === null) { node.status({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
// GA = GA === undefined ? '' : GA
// payload = payload === undefined ? '' : payload
// payload = typeof payload === 'object' ? JSON.stringify(payload) : payload
// const dDate = new Date()
// node.status({ fill, shape, text: GA + ' ' + payload + ' ' + text + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' })
// } catch (error) {
// }
}
node.saveExposedGAs = async () => {
const sFile = path.join(node.serverKNX.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json");
try {
if (node.exposedGAs.length > 0) {
fs.writeFileSync(sFile, JSON.stringify(node.exposedGAs));
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("knxUltimateAutoResponder: wrote peristent values to the file " + sFile);
}
} catch (err) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimateAutoResponder: unable to write peristent values to the file " + sFile + " " + err.message);
}
}
node.loadExposedGAs = () => {
const sFile = path.join(node.serverKNX.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json");
try {
node.exposedGAs = JSON.parse(fs.readFileSync(sFile, "utf8"));
} catch (err) {
node.exposedGAs = [];
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn("knxUltimateAutoResponder: unable to read peristent file " + sFile + " " + err.message);
}
}
// Load persistent file
try {
node.loadExposedGAs()
// Set all saved GAs to disabled. Will be enabled later (directive's list)
node.exposedGAs.forEach(element => {
element.enabled = false;
})
if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
node.sysLogger?.info("Started timerSaveExposedGAs with array lenght ", node.exposedGAs?.length);
node.timerSaveExposedGAs = setInterval(async () => {
await node.saveExposedGAs();
}, 5000);
} catch (error) {
}
// Add the ETS CSV file list to exposedGAs
if (node.serverKNX.csv === undefined || node.serverKNX.csv === '' || node.serverKNX.csv.length === 0) {
updateStatus({ fill: 'grey', shape: 'ring', text: 'No ETS file imported', payload: '', dpt: '', devicename: '' });
//return;
} else {
node.serverKNX.csv.forEach(element => {
const curGa = node.exposedGAs.find(a => a.address === element.ga);
if (curGa === undefined) {
node.exposedGAs.push({ address: element.ga, dpt: element.dpt, default: undefined, payload: undefined, enabled: false }); // "enabled" will be used to filter only the node.commandText directiver
} else {
curGa.enabled = false;
}
})
updateStatus({ fill: 'green', shape: 'ring', text: 'ETS file loaded', payload: '', dpt: '', devicename: '' });
}
// Fill the filter list
try {
node.commandText = JSON.parse(config.commandText);
} catch (error) {
updateStatus({ fill: 'red', shape: 'dot', text: 'JSON error: ' + error.message, payload: '', dpt: '', devicename: '' });
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: node.commandText = JSON.parse(config.commandText) ${error.stack}`);
return;
}
// Decode the commandText list be exploding the format 2/2/..
node.commandText.forEach(element => {
if (element.ga !== undefined && element.default !== undefined) {
let defaultVal = element.default;
let dpt = element.dpt;
if (element.ga.includes('..')) {
const start = Number(element.ga.substring(element.ga.lastIndexOf("/") + 1, element.ga.indexOf("..")));
const end = Number(element.ga.substring(element.ga.indexOf("..") + 2));
const twoLevel = element.ga.substring(0, element.ga.lastIndexOf("/") + 1);
for (let index = start; index < end; index++) {
const decAdd = twoLevel + index;
// Add also to the exposedGAs list, if not already present
let curGa = node.exposedGAs.find(a => a.address === decAdd);
if (curGa === undefined) {
node.exposedGAs.push({ address: decAdd, dpt: dpt, default: defaultVal, payload: undefined, enabled: true });
} else {
if (dpt !== undefined) curGa.dpt = dpt; // Take the Datapoint from the commandText directive, replacing from ETS CSV file, if exists.
curGa.enabled = true;
}
}
} else {
let curGa = node.exposedGAs.find(a => a.address === element.ga);
if (curGa === undefined) {
node.exposedGAs.push({ address: element.ga, dpt: dpt, default: defaultVal, payload: undefined, enabled: true });
} else {
if (dpt !== undefined) curGa.dpt = dpt; // Take the Datapoint from the commandText directive, replacing from ETS CSV file, if exists.
curGa.enabled = true;
}
}
// Delete all not wanted GAs, that aren't in the node.commandText directive list.
node.exposedGAs = node.exposedGAs.filter(a => (a.enabled !== undefined && a.enabled === true));
updateStatus({ fill: 'green', shape: 'ring', text: 'JSON parsed: ' + node.commandText.length + " directive(s).", payload: '', dpt: '', devicename: '' });
} else {
// Error
updateStatus({ fill: 'red', shape: 'dot', text: 'JSON error: ga or default keys not set. Abort.', payload: '', dpt: '', devicename: '' });
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: node.commandText.forEach(element.. JSON error: ga or default keys not set. Abort.`);
return;
}
});
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = msg => {
if (msg.knx !== undefined && msg.knx.event !== undefined && msg.knx.event !== "GroupValue_Read") {
// Save the value
try {
var oGa = node.exposedGAs.find(ga => ga.address === msg.knx.destination);
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: var oGa = node.exposedGAs.find(ga => ga.address === msg.knx.destination) ${error.stack}`);
}
if (oGa !== undefined) {
let decodedPayload;
try {
// Don't care about the decoded payload, because knxUltimate-config could pass a TryToFindDatapoint from raw data
// Take only RAW data and decode it with the dpt specified by the commandText directive
decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt));
} catch (error) {
updateStatus({ fill: 'red', shape: 'dot', text: 'const decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt)); ' + error.message, payload: '', dpt: '', devicename: '' });
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: const decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt)); ${error.stack}`);
}
oGa.payload = decodedPayload
}
} else {
try {
let retVal;
let oFoundGA = node.exposedGAs.find(ga => ga.address === msg.knx.destination);
if (oFoundGA === undefined) return;
if (oFoundGA.payload === undefined) {
retVal = oFoundGA.default;
} else {
retVal = oFoundGA.payload;
}
if (retVal !== undefined) {
const dDate = new Date()
if (oFoundGA.address !== undefined && oFoundGA.dpt !== undefined && retVal !== undefined) {
node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: oFoundGA.address, payload: retVal, dpt: oFoundGA.dpt, outputtype: 'response', nodecallerid: node.id });
updateStatus({ fill: 'blue', shape: 'dot', text: 'Respond ' + oFoundGA.address + ' => ' + retVal + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' })
} else {
updateStatus({ fill: 'yellow', shape: 'ring', text: 'Issue responding ' + oFoundGA.address + ' => ' + retVal + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' })
}
}
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: after bFound ${error.stack}`);
}
}
}
node.on('input', function (msg) {
})
node.on('close', function (done) {
try {
if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
node.saveExposedGAs();
} catch (error) {
}
node.exposedGAs = [];
if (node.serverKNX) {
node.serverKNX.removeClient(node)
}
done()
})
// On each deploy, unsubscribe+resubscribe
if (node.serverKNX) {
node.serverKNX.removeClient(node)
node.serverKNX.addClient(node)
}
}
RED.nodes.registerType('knxUltimateAutoResponder', knxUltimateAutoResponder)
}