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.
590 lines (544 loc) • 31.8 kB
JavaScript
// Utility function
// until node-red 3.1.0, there is a bug creating a plugin, so for backward compatibility, i must use a JS as a node.
const oOS = require("os");
const fs = require("fs");
const path = require("path");
const yaml = require('js-yaml');
const dptlib = require('knxultimate').dptlib;
const customHTTP = require('./utils/http');
const KNXClient = require('knxultimate').KNXClient;
// DATAPONT MANIPULATION HELPERS
// ####################
const sortBy = (field) => (a, b) => {
if (a[field] > b[field]) {
return 1;
} else {
return -1;
}
};
const onlyDptKeys = (kv) => {
return kv[0].startsWith("DPT");
};
const extractBaseNo = (kv) => {
return {
subtypes: kv[1].subtypes,
base: parseInt(kv[1].id.replace("DPT", "")),
};
};
const convertSubtype = (baseType) => (kv) => {
const value = `${baseType.base}.${kv[0]}`;
// let sRet = value + " " + kv[1].name + (kv[1].unit === undefined ? "" : " (" + kv[1].unit + ")");
const sRet = value + " " + kv[1].name;
return {
value,
text: sRet,
};
};
const toConcattedSubtypes = (acc, baseType) => {
const subtypes = Object.entries(baseType.subtypes).sort(sortBy(0)).map(convertSubtype(baseType));
return acc.concat(subtypes);
};
// ####################
module.exports = (RED) => {
RED.plugins.registerPlugin("commonFunctions", {
type: "foo",
onadd: function () {
RED.events.on("registry:plugin-added", function (id) {
//console.log(`my-test-plugin: plugin-added event "${id}"`)
commonFunctions();
});
}
})
function commonFunctions() {
var node = this;
// // Gather infos about all interfaces on the lan and provides a static variable utils.aDiscoveredknxGateways
// try {
// require('./utils/utils').DiscoverKNXGateways()
// } catch (error) {
// }
// 11/03/2020 Delete scene saved file, from html
RED.httpAdmin.get('/knxultimateCheckHueConnected', (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
if (serverId.hueAllResources === null || serverId.hueAllResources === undefined) {
(async function main() {
try {
await serverId.loadResourcesFromHUEBridge();
} catch (error) {
RED.log.error(`Errore RED.httpAdmin.get('/knxultimateCheckHueConnected' ${error.stack}`);
}
res.json({ ready: false });
}()).catch();
} else {
res.json({ ready: true });
}
} catch (error) {
RED.log.error(`Errore RED.httpAdmin.get('/knxultimateCheckHueConnected' ${error.stack}`);
res.json({ ready: false });
}
});
// 11/03/2020 Delete scene saved file, from html
RED.httpAdmin.get('/knxultimatescenecontrollerdelete'), (req, res) => {
// Delete the file
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
const newPath = `${serverId.userDir}/scenecontroller/SceneController_${req.query.FileName}`;
fs.unlinkSync(newPath);
} catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn(`e ${error}`); }
res.json({ status: 220 });
};
// // Find all HUE Bridges in the network
// RED.httpAdmin.get('/KNXUltimateDiscoverHueBridges'), (req, res) => {
// const url = 'https://discovery.meethue.com'; // Use HUE broker server discover process by visiting
// async function fetchData() {
// try {
// const response = await fetch(url); // Effettua la richiesta
// const dataArray = await response.json(); // Parsing dei dati JSON
// // Mostra l'array risultante
// res.json(dataArray);
// } catch (error) {
// if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Error fetching discovery.meethue.com ${error.stack}`);
// res.json("");
// }
// }
// fetchData();
// };
// Find all HUE Bridges in the network
RED.httpAdmin.get('/KNXUltimateGetHueBridgeInfo', RED.auth.needsPermission("hue-config.read"), (req, res) => {
async function fetchData() {
try {
const response = await customHTTP.getBridgeDetails(req.query.IP)
// Mostra l'array risultante
res.json(response);
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Error fetching discovery.meethue.com ${error.stack}`);
res.json({ error: error.message });
}
}
fetchData();
});
// Find all HUE Bridges in the network
RED.httpAdmin.get('/KNXUltimateGetPlainHueBridgeCredentials', RED.auth.needsPermission("hue-config.read"), (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
const username = serverId.credentials.username;
const clientkey = serverId.credentials.clientkey;
res.json({ username: username, clientkey: clientkey });
} catch (error) {
res.json({ error: error.message })
}
});
// Endpoint for connecting to HUE Bridge
RED.httpAdmin.get("/KNXUltimateRegisterToHueBridge", (req, res) => {
try {
(async () => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
// If using this code outside of the examples directory, you will want to use the line below and remove the
// const discovery = require('node-hue-api').discovery
const hueApi = require("node-hue-api").api;
const appName = "KNXUltimate";
const deviceName = "Node-Red";
// async function discoverBridge() {
// const discoveryResults = await discovery.nupnpSearch()
// if (discoveryResults.length === 0) {
// if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('Failed to resolve any Hue Bridges')
// return null
// } else {
// // Ignoring that you could have more than one Hue Bridge on a network as this is unlikely in 99.9% of users situations
// return discoveryResults[0].ipaddress
// }
// }
async function discoverAndCreateUser() {
// const ipAddress = await discoverBridge()
const ipAddress = req.query.IP;
// Create an unauthenticated instance of the Hue API so that we can create a new user
try {
const unauthenticatedApi = await hueApi.createLocal(ipAddress).connect();
let createdUser;
createdUser = await unauthenticatedApi.users.createUser(appName, deviceName);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("*******************************************************************************\n");
if (node.sysLogger !== undefined && node.sysLogger !== null) {
node.sysLogger.info(
"User has been created on the Hue Bridge. The following username can be used to\n"
+ "authenticate with the Bridge and provide full local access to the Hue Bridge.\n"
+ "YOU SHOULD TREAT THIS LIKE A PASSWORD\n",
);
}
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Hue Bridge User: ${createdUser.username}`);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Hue Bridge User Client Key: ${createdUser.clientkey}`);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("*******************************************************************************\n");
// Create a new API instance that is authenticated with the new user we created
const authenticatedApi = await hueApi.createLocal(ipAddress).connect(createdUser.username);
// Do something with the authenticated user/api
const bridgeConfig = await authenticatedApi.configuration.getConfiguration();
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Connected to Hue Bridge: ${bridgeConfig.name} :: ${bridgeConfig.ipaddress}`);
return { bridge: bridgeConfig, user: createdUser };
} catch (err) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`The Link button on the bridge was not pressed. ${err.message}`);
throw err;
// return {
// error:
// "The Link button on the bridge was not pressed or an error has occurred. " +
// err.message,
// };
}
}
async function discoverAndCreateUserInsecure() {
// const ipAddress = await discoverBridge()
const ipAddress = req.query.IP;
// Create an unauthenticated instance of the Hue API so that we can create a new user
try {
const unauthenticatedApi = await hueApi.createInsecureLocal(ipAddress).connect();
let createdUser;
createdUser = await unauthenticatedApi.users.createUser(appName, deviceName);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("*******************************************************************************\n");
if (node.sysLogger !== undefined && node.sysLogger !== null) {
node.sysLogger.info(
"User has been created on the Hue Bridge. The following username can be used to\n"
+ "authenticate with the Bridge and provide full local access to the Hue Bridge.\n"
+ "YOU SHOULD TREAT THIS LIKE A PASSWORD\n",
);
}
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Hue Bridge User: ${createdUser.username}`);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Hue Bridge User Client Key: ${createdUser.clientkey}`);
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("*******************************************************************************\n");
// Create a new API instance that is authenticated with the new user we created
const authenticatedApi = await hueApi.createInsecureLocal(ipAddress).connect(createdUser.username);
// Do something with the authenticated user/api
const bridgeConfig = await authenticatedApi.configuration.getConfiguration();
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Connected to Hue Bridge: ${bridgeConfig.name} :: ${bridgeConfig.ipaddress}`);
return { bridge: bridgeConfig, user: createdUser };
} catch (err) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`The Link button on the bridge was not pressed. ` + err.stack);
return {
error: `The Link button on the bridge was not pressed or an error has occurred.`,
};
}
}
// Invoke the discovery and create user code
try {
const jRet = await discoverAndCreateUser();
res.json(jRet);
} catch (error) {
RED.log.error(`Errore KNXUltimateRegisterToHueBridge non gestito Secure ${error.message}. Try with insecure http connection...`);
// Try with insecureClient (avoid problems with expired https certificates)
try {
const jRet = await discoverAndCreateUserInsecure();
res.json(jRet);
} catch (error) {
RED.log.error(`Errore KNXUltimateRegisterToHueBridge non gestito Insecure ${error.message}. I give up.`);
res.json({ error: error.message });
}
}
} catch (err) {
RED.log.error(`Errore KNXUltimateRegisterToHueBridge non gestito ${err.message}`);
}
})();
} catch (err) {
RED.log.error(`Errore KNXUltimateRegisterToHueBridge bsonto ${err.message}`);
res.json({ error: err.message });
}
});
// Endpoint for reading csv/esf by the other nodes
RED.httpAdmin.get("/knxUltimatecsv", RED.auth.needsPermission("knxUltimate-config.read"), (req, res) => {
try {
if (typeof req.query.nodeID !== "undefined" && req.query.nodeID !== null && req.query.nodeID !== "") {
const _node = RED.nodes.getNode(req.query.nodeID); // Retrieve node.id of the config node.
if (_node !== null) res.json(RED.nodes.getNode(_node.id).csv);
} else {
// Get the first knxultimate-config having a valid csv
try {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Requested csv maybe from visu-ultimate?");
RED.nodes.eachNode((_node) => {
if (_node.hasOwnProperty("csv") && _node.type === "knxUltimate-config" && _node.csv !== "") {
res.json(RED.nodes.getNode(_node.id).csv);
}
});
} catch (error) { }
}
} catch (error) {
}
});
// 14/08/2019 Endpoint for retrieving the ethernet interfaces
RED.httpAdmin.get("/knxUltimateETHInterfaces", (req, res) => {
const jListInterfaces = [];
try {
const oiFaces = oOS.networkInterfaces();
Object.keys(oiFaces).forEach((ifname) => {
// Interface with single IP
if (Object.keys(oiFaces[ifname]).length === 1) {
if (Object.keys(oiFaces[ifname])[0].internal === false) {
jListInterfaces.push({
name: ifname,
address: Object.keys(oiFaces[ifname])[0].address,
});
}
} else {
let sAddresses = "";
oiFaces[ifname].forEach((iface) => {
if (iface.internal === false) sAddresses += `+${iface.address}`;
});
if (sAddresses !== "") jListInterfaces.push({ name: ifname, address: sAddresses });
}
});
} catch (error) { }
res.json(jListInterfaces);
});
// Discover KNX/IP gateways on demand and return cached results
RED.httpAdmin.get("/knxUltimateDiscoverKNXGateways", RED.auth.needsPermission("knxUltimate-config.read"), async function (req, res) {
try {
const utils = require("./utils/utils");
// Always trigger discovery on request to ensure fresh data
const list = await utils.DiscoverKNXGateways();
res.json(Array.isArray(list) ? list : []);
} catch (error) {
try { RED.log.error(`KNX gateway discovery failed: ${error.message}`); } catch (e) { /* noop */ }
res.json([]);
}
});
// 12/08/2021 Endpoint for deleting the GA persistent file for the current gateway
RED.httpAdmin.get("/deletePersistGAFile", RED.auth.needsPermission("knxUltimate-config.read"), (req, res) => {
try {
if (typeof req.query.serverId !== "undefined" && req.query.serverId !== null && req.query.serverId !== "") {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
const sFile = path.join(serverId.userDir, "knxpersistvalues", `knxpersist${req.query.serverId}.json`);
fs.unlinkSync(sFile);
} catch (error) { res.json({ error: error.stack }); }
res.json({ error: "No error" });
} else {
res.json({ error: "No serverId specified" });
}
} catch (error) {
}
});
// 2025-09 List interfaces (IA) from KNX Secure keyring
RED.httpAdmin.get("/knxUltimateKeyringInterfaces", RED.auth.needsPermission("knxUltimate-config.read"), async (req, res) => {
try {
let keyringContent = (req.query.keyring || '').toString();
let password = (req.query.pwd || '').toString();
// If not provided, try to read from existing config node
if ((!keyringContent || !password) && req.query.serverId) {
const cfg = RED.nodes.getNode(req.query.serverId);
if (cfg) {
try { keyringContent = cfg.keyringFileXML || keyringContent; } catch (e) { }
try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password; } catch (e) { }
}
}
if (!keyringContent || !password) {
return res.json([]);
}
let Keyring;
try {
({ Keyring } = require('knxultimate/build/secure/keyring'));
} catch (e) {
try { RED.log.error(`KNXUltimate: cannot load Keyring module: ${e.message}`); } catch (err) { }
return res.json([]);
}
const kr = new Keyring();
try {
await kr.load(keyringContent, password);
} catch (e) {
try { RED.log.error(`KNXUltimate: keyring load error: ${e.message}`); } catch (err) { }
return res.json([]);
}
const out = [];
try {
for (const [iaStr, iface] of kr.getInterfaces()) {
out.push({ ia: iaStr, userId: iface?.userId });
}
} catch (e) { }
res.json(out);
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateKeyringInterfaces error: ${error.message}`); } catch (e) { }
res.json([]);
}
});
RED.httpAdmin.get("/knxUltimateGetHueColor", (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
// find wether the light is a light or is grouped_light
let hexColor;
const _oDevice = serverId.hueAllResources.filter((a) => a.id === req.query.id)[0];
if (_oDevice.type === "light") {
hexColor = serverId.getColorFromHueLight(req.query.id);
} else {
// grouped_light, get the first light in the group
const oLight = serverId.getFirstLightInGroup(_oDevice.id);
hexColor = serverId.getColorFromHueLight(oLight.id);
}
res.json(hexColor !== undefined ? hexColor : "Select the device first!");
} catch (error) {
res.json("Select the device first!");
}
});
// 2025-09 Secure: return list of Data Secure Group Addresses from keyring
RED.httpAdmin.get("/knxUltimateKeyringDataSecureGAs", RED.auth.needsPermission("knxUltimate-config.read"), async (req, res) => {
try {
let keyringContent = (req.query.keyring || '').toString();
let password = (req.query.pwd || '').toString();
// Try to use config node if not provided
if ((!keyringContent || !password) && req.query.serverId) {
const cfg = RED.nodes.getNode(req.query.serverId);
if (cfg) {
try { keyringContent = cfg.keyringFileXML || keyringContent; } catch (e) { }
try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password; } catch (e) { }
}
}
if (!keyringContent || !password) return res.json([]);
let Keyring;
try { ({ Keyring } = require('knxultimate/build/secure/keyring')); } catch (e) { return res.json([]); }
const kr = new Keyring();
try { await kr.load(keyringContent, password); } catch (e) { return res.json([]); }
const out = [];
try {
for (const [gaStr, g] of kr.getGroupAddresses()) {
if (g?.decryptedKey && g.decryptedKey.length > 0) out.push(gaStr);
}
} catch (e) { }
res.json(out);
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateKeyringDataSecureGAs error: ${error.message}`); } catch (e) { }
res.json([]);
}
});
RED.httpAdmin.get("/knxUltimateGetKelvinColor", (req, res) => {
try {
// find wether the light is a light or is grouped_light
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
let kelvinValue;
const _oDevice = serverId.hueAllResources.filter((a) => a.id === req.query.id)[0];
if (_oDevice.type === "light") {
kelvinValue = serverId.getKelvinFromHueLight(req.query.id);
} else {
// grouped_light, get the first light in the group
const oLight = serverId.getFirstLightInGroup(_oDevice.id);
kelvinValue = serverId.getKelvinFromHueLight(oLight.id);
}
res.json(kelvinValue !== undefined ? kelvinValue : "Select the device first!");
} catch (error) {
res.json("Select the device first!");
}
});
RED.httpAdmin.get("/knxUltimateGetLightObject", (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
if (serverId.hueAllResources === null || serverId.hueAllResources === undefined) {
throw (new Error("Resource not yet loaded"));
}
const _lightId = req.query.id;
const oLight = serverId.hueAllResources.filter((a) => a.id === _lightId)[0];
// Infer some useful info, so the HTML part can avoid to query the server
// Kelvin
try {
if (oLight.color_temperature !== undefined && oLight.color_temperature.mirek !== undefined) {
oLight.calculatedKelvin = hueColorConverter.ColorConverter.mirekToKelvin(oLight.color_temperature.mirek);
}
} catch (error) {
oLight.calculatedKelvin = undefined;
}
// HEX value from XYBri
try {
const retRGB = hueColorConverter.ColorConverter.xyBriToRgb(oLight.color.xy.x, oLight.color.xy.y, oLight.dimming.brightness);
const ret = `#${hueColorConverter.ColorConverter.rgbHex(retRGB.r, retRGB.g, retRGB.b).toString()}`;
oLight.calculatedHEXColor = ret;
} catch (error) {
oLight.calculatedHEXColor = undefined;
}
res.json(oLight);
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`KNXUltimateHue: hueEngine: knxUltimateGetLightObject: error ${error.message}.`);
res.json({});
}
});
RED.httpAdmin.get("/KNXUltimateGetResourcesHUE", (req, res) => {
try {
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
if (serverId === null) {
RED.log.warn(`Warn KNXUltimateGetResourcesHUE serverId is null`);
const jRet = [];
jRet.push({ name: 'PLEASE DEPLOY FIRST: then try again.', id: 'error' })
res.json({ devices: jRet });
return;
}
const jRet = serverId.getResources(req.query.rtype);
if (jRet !== undefined) {
res.json(jRet);
} else {
res.json({ devices: [{ name: "I'm still connecting...Try in some seconds" }] });
}
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
} catch (error) {
// RED.log.error(`Errore KNXUltimateGetResourcesHUE non gestito ${error.message}`);
res.json({ devices: error.message });
RED.log.error(`Err KNXUltimateGetResourcesHUE: ${error.message}`);
// (async () => {
// await node.initHUEConnection();
// })();
}
});
RED.httpAdmin.get("/knxUltimateDpts", (req, res) => {
try {
const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy("base"))
.reduce(toConcattedSubtypes, []);
res.json(dpts);
} catch (error) { }
});
// 15/09/2020 Supergiovane, read datapoint help usage
RED.httpAdmin.get("/knxUltimateDptsGetHelp", (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
const sDPT = req.query.dpt.split(".")[0]; // Takes only the main type
let jRet;
if (sDPT === "0") {
// Special fake datapoint, meaning "Universal Mode"
jRet = {
help: `// KNX-Ultimate set as UNIVERSAL NODE
// Example of a function that sends a message to the KNX-Ultimate
msg.destination = "0/0/1"; // Set the destination
msg.payload = false; // issues a write or response (based on the options Telegram type above) to the KNX bus
msg.event = "GroupValue_Write"; // "GroupValue_Write" or "GroupValue_Response", overrides the option Telegram type above.
msg.dpt = "1.001"; // for example "1.001", overrides the Datapoint option. (Datapoints can be sent as 9 , "9" , "9.001" or "DPT9.001")
return msg;`,
helplink: "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki",
};
res.json(jRet);
return;
}
jRet = {
help: "NO",
helplink: "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome",
};
const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys);
for (let index = 0; index < dpts.length; index++) {
if (dpts[index][0].toUpperCase() === `DPT${sDPT}`) {
jRet = {
help: dpts[index][1].basetype.hasOwnProperty("help") ? dpts[index][1].basetype.help : "NO",
helplink: dpts[index][1].basetype.hasOwnProperty("helplink")
? dpts[index][1].basetype.helplink
: "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome",
};
break;
}
}
res.json(jRet);
} catch (error) {
res.json({ error: error.stack });
}
});
// RED.httpAdmin.post("/banana", RED.auth.needsPermission("write"), (req, res) => {
// const node = RED.nodes.getNode(req.params.id);
// if (node != null) {
// try {
// if (req.body) {
// console.log(body);
// }
// } catch (err) { }
// }
// res.json(req.body);
// });
}
};