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.
276 lines (258 loc) • 12.3 kB
JavaScript
module.exports = function (RED) {
const dptlib = require('knxultimate').dptlib;
function knxUltimateHueButton(config) {
RED.nodes.createNode(this, config);
const node = this;
node.serverKNX = RED.nodes.getNode(config.server) || undefined;
node.serverHue = RED.nodes.getNode(config.serverHue) || undefined;
node.topic = node.name;
node.name = config.name === undefined ? 'Hue' : config.name;
node.dpt = '';
node.notifyreadrequest = false;
node.notifyreadrequestalsorespondtobus = 'false';
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = '';
node.notifyresponse = false;
node.notifywrite = true;
node.initialread = true;
node.listenallga = true; // Don't remove
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.currentPayload = ''; // Current value for the RBE input and for the .previouspayload msg
node.passthrough = 'no';
node.formatmultiplyvalue = 1;
node.formatnegativevalue = 'leave';
node.formatdecimalsvalue = 2;
node.short_releaseValue = false;
node.isTimerDimStopRunning = false;
node.hueDevice = config.hueDevice;
node.initializingAtStart = false;
// When toggle status is disabled, uses these values
node.switchSend = config.switchSend === undefined ? 'true' : config.switchSend;
node.switchSend = node.switchSend === 'true'; // The typedvalue in the html returns a string, so i convert it to bool
node.dimSend = config.dimSend === undefined ? 'up' : config.dimSend;
if (node.dimSend === 'up') node.dimSend = { decr_incr: 1, data: 3 };
if (node.dimSend === 'down') node.dimSend = { decr_incr: 0, data: 3 };
if (node.dimSend === 'stop') node.dimSend = { decr_incr: 0, data: 0 };
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);
};
const safeSendToKNX = (telegram, context = 'write') => {
try {
if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
const now = new Date();
updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${now.getDate()}, ${now.toLocaleTimeString()})` });
return;
}
node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id });
} catch (error) {
updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` });
}
};
// Used to call the status update from the config node.
node.setNodeStatus = ({
fill, shape, text, payload,
}) => {
try {
if (payload === undefined) payload = '';
const dDate = new Date();
payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
updateStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
} catch (error) { }
};
// Used to call the status update from the HUE config node.
node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
try {
if (payload === undefined) payload = '';
const dDate = new Date();
payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
} catch (error) { }
};
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = (msg) => {
try {
switch (msg.knx.destination) {
case config.GAshort_releaseStatus:
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptshort_release));
node.short_releaseValue = msg.payload;
node.setNodeStatusHue({
fill: 'green', shape: 'dot', text: 'KNX->HUE Short Release Status', payload: msg.payload,
});
break;
case config.GArepeatStatus:
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptrepeat));
node.toggleGArepeat = msg.payload.decr_incr === 1;
node.setNodeStatusHue({
fill: 'green', shape: 'dot', text: 'KNX->HUE Repeat Status', payload: msg.payload,
});
break;
default:
break;
}
} catch (error) {
node.setNodeStatusHue({
fill: 'red', shape: 'dot', text: `KNX->HUE error ${error.message}`, payload: '',
});
}
};
node.handleSendHUE = (_event) => {
try {
if (_event.id === config.hueDevice) {
const buttonEvent = _event?.button?.button_report?.event || _event?.button?.last_event;
if (!_event.hasOwnProperty('button') || buttonEvent === undefined) return;
const knxMsgPayload = {};
let flowMsgPayload = true;
// Handling events with toggles
// KNX Dimming reminder tips
// { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
// { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
switch (buttonEvent) {
case 'initial_press':
if (node.initial_pressValue === undefined) node.initial_pressValue = false;
node.initial_pressValue = config.toggleValues ? !node.initial_pressValue : node.switchSend;
flowMsgPayload = node.initial_pressValue;
break;
case 'long_release':
flowMsgPayload = node.long_pressValue;
// if the dimmer was running, send the STOP telegram to the KNX bus wires, using the GArepeat Group address and dpt.
if (node.isTimerDimStopRunning) {
knxMsgPayload.topic = config.GArepeat;
knxMsgPayload.dpt = config.dptrepeat;
node.stopDIM(knxMsgPayload);
}
break;
case 'double_short_release':
if (node.double_short_releaseValue === undefined) node.double_short_releaseValue = false;
node.double_short_releaseValue = config.toggleValues ? !node.double_short_releaseValue : node.switchSend;
flowMsgPayload = node.double_short_releaseValue;
break;
case 'long_press':
if (node.long_pressValue === undefined) node.long_pressValue = false;
node.long_pressValue = config.toggleValues ? !node.long_pressValue : node.dimSend;
flowMsgPayload = node.long_pressValue;
break;
case 'short_release':
node.short_releaseValue = config.toggleValues ? !node.short_releaseValue : node.switchSend;
flowMsgPayload = node.short_releaseValue;
if (config.GAshort_release !== undefined && config.GAshort_release !== '') {
knxMsgPayload.topic = config.GAshort_release;
knxMsgPayload.dpt = config.dptshort_release;
knxMsgPayload.payload = node.short_releaseValue;
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write',
}, 'write');
}
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
node.setNodeStatusHue({
fill: 'blue', shape: 'dot', text: `HUE->KNX ${buttonEvent}`, payload: knxMsgPayload.payload,
});
}
}
break;
case 'repeat':
flowMsgPayload = node.long_pressValue;
if (config.GArepeat !== undefined && config.GArepeat !== '') {
if (node.isTimerDimStopRunning === false) {
// Set KNX Dim up/down start
knxMsgPayload.topic = config.GArepeat;
knxMsgPayload.dpt = config.dptrepeat;
if (typeof (node.long_pressValue) === 'object') {
knxMsgPayload.payload = node.long_pressValue; // Send fixed value when toggleValues is false
} else {
knxMsgPayload.payload = node.long_pressValue ? { decr_incr: 0, data: 3 } : { decr_incr: 1, data: 3 }; // If the light is turned on, the initial DIM direction must be down, otherwise, up
}
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write',
}, 'write');
}
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
node.setNodeStatusHue({
fill: 'blue', shape: 'dot', text: 'HUE->KNX START DIM', payload: '',
});
}
}
node.startDimStopper(knxMsgPayload);
}
break;
default:
break;
}
// Setup the output msg
const flowMsg = {};
flowMsg.name = node.name;
flowMsg.event = buttonEvent;
if (_event.button?.button_report?.updated) flowMsg.updated = _event.button.button_report.updated;
flowMsg.rawEvent = _event;
flowMsg.payload = flowMsgPayload;
node.send(flowMsg);
if (node.serverKNX === undefined) node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: '', payload: flowMsg.event })
}
} catch (error) {
node.setNodeStatusHue({
fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message}`, payload: '',
});
}
};
// Timer to stop the dimming sequence
node.startDimStopper = function (knxMsgPayload) {
if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop);
node.isTimerDimStopRunning = true;
node.timerDimStop = setTimeout(() => {
node.stopDIM(knxMsgPayload);
}, 2000);
};
node.stopDIM = function (knxMsgPayload) {
// KNX Stop DIM
if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop);
node.isTimerDimStopRunning = false;
knxMsgPayload.payload = { decr_incr: 0, data: 0 }; // Payload for the output msg
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write',
}, 'write');
node.setNodeStatusHue({
fill: 'grey', shape: 'ring', text: 'HUE->KNX STOP DIM', payload: knxMsgPayload.payload,
});
}
};
// On each deploy, unsubscribe+resubscribe
if (node.serverKNX) {
node.serverKNX.removeClient(node);
node.serverKNX.addClient(node);
}
if (node.serverHue) {
node.serverHue.removeClient(node);
node.serverHue.addClient(node);
}
node.on('input', (msg) => {
});
node.on('close', (done) => {
if (node.serverKNX) {
node.serverKNX.removeClient(node);
}
if (node.serverHue) {
node.serverHue.removeClient(node);
}
done();
});
}
RED.nodes.registerType('knxUltimateHueButton', knxUltimateHueButton);
};