UNPKG

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.

521 lines (493 loc) 23.5 kB
/* eslint-disable camelcase */ /* eslint-disable no-underscore-dangle */ /* eslint-disable no-lonely-if */ /* eslint-disable no-param-reassign */ /* eslint-disable no-inner-declarations */ /* eslint-disable max-len */ const cloneDeep = require("lodash/cloneDeep"); //const classHUE = require("./utils/hueEngine").classHUE; const hueColorConverter = require("./utils/colorManipulators/hueColorConverter"); // 10/09/2024 Setup the color logger loggerSetup = (options) => { let clog = require("node-color-log").createNamedLogger(options.setPrefix); clog.setLevel(options.loglevel); clog.setDate(() => (new Date()).toLocaleString()); return clog; } module.exports = (RED) => { function hueConfig(config) { RED.nodes.createNode(this, config); const node = this; node.host = config.host; node.nodeClients = []; // Stores the registered clients node.loglevel = config.loglevel !== undefined ? config.loglevel : "error"; // loglevel doesn'e exists yet node.sysLogger = null; node.hueAllResources = undefined; node.timerHUEConfigCheckState = null; // Timer that check the connection to the hue bridge every xx seconds try { node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: "hue-config.js" }); } catch (error) { console.log(error.stack) } node.name = config.name === undefined || config.name === "" ? node.host : config.name; // Helper not to write everytime the "node.hueManager === null || node.hueManager === "undefined" || node.hueManager.HUEBridgeConnectionStatus === undefined" Object.defineProperty(node, "linkStatus", { get: function () { return node.hueManager?.HUEBridgeConnectionStatus ?? "disconnected"; } }); // Connect to Bridge and get the resources node.initHUEConnection = async () => { await node.closeConnection(); try { if (node.hueManager !== undefined) await node.hueManager.close(); } catch (error) { /* empty */ } (async () => { try { const { classHUE } = await import('./utils/hueEngine.mjs'); node.hueManager = new classHUE(node.host, node.credentials.username, node.credentials.clientkey, config.bridgeid, node.sysLogger); } catch (error) { node.sysLogger?.error(`Errore hue-config: node.initHUEConnection: ${error.message}`); throw (error) } node.hueManager.on("event", (_event) => { node.nodeClients.forEach((_oClient) => { const oClient = _oClient; try { if (oClient.handleSendHUE !== undefined) oClient.handleSendHUE(_event); } catch (error) { node.sysLogger?.error(`Errore node.hueManager.on(event): ${error.message}`); } }); }); // Connected node.hueManager.on("connected", () => { if (node.linkStatus === "disconnected") { // Start the timer to do initial read. if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead); node.timerDoInitialRead = setTimeout(() => { (async () => { try { node.sysLogger?.info(`HTTP getting resource from HUE bridge : ${node.name}`); await node.loadResourcesFromHUEBridge(); node.sysLogger?.info(`Total HUE resources count : ${node.hueAllResources.length}`); } catch (error) { node.nodeClients.forEach((_oClient) => { setTimeout(() => { _oClient.setNodeStatusHue({ fill: "red", shape: "ring", text: "HUE", payload: error.message, }); }, 200); }); } })(); }, 10000); // 17/02/2020 Do initial read of all nodes requesting initial read } }); node.hueManager.on("disconnected", () => { node.nodeClients.forEach((_oClient) => { _oClient.setNodeStatusHue({ fill: "red", shape: "ring", text: "HUE Disconnected", payload: "", }); }); }); try { await node.hueManager.Connect(); } catch (error) { } })(); }; node.startWatchdogTimer = async () => { if (node.timerHUEConfigCheckState !== null) clearTimeout(node.timerHUEConfigCheckState); node.timerHUEConfigCheckState = setTimeout(() => { (async () => { if (node.linkStatus === "disconnected") { // The hueEngine is already connected to the HUE Bridge try { await node.initHUEConnection(); } catch (error) { node.sysLogger?.error(`Errore hue-config: node.startWatchdogTimer: ${error.message}`); } } await node.startWatchdogTimer(); })(); }, 60000); }; setTimeout(() => { (async () => { await node.initHUEConnection(); node.startWatchdogTimer(); })(); }, 5000); // Functions called from the nodes ---------------------------------------------------------------- // Query the HUE Bridge to return the resources node.loadResourcesFromHUEBridge = async () => { if (node.linkStatus === "disconnected") return; // (async () => { // °°°°°° Load ALL resources try { node.hueAllResources = await node.hueManager.hueApiV2.get("/resource"); if (node.hueAllResources !== undefined) { node.hueAllRooms = node.hueAllResources.filter((a) => a.type === "room"); // Update all KNX State of the nodes with the new hue device values node.nodeClients.forEach((_node) => { if (_node.hueDevice !== undefined && node.hueAllResources !== undefined) { const oHUEDevice = node.hueAllResources.filter((a) => a.id === _node.hueDevice)[0]; if (oHUEDevice !== undefined) { // Add _Node to the clients array _node.setNodeStatusHue({ fill: "green", shape: "ring", text: "Ready", }); _node.currentHUEDevice = cloneDeep(oHUEDevice); // Copy by Value and not by ref if (_node.initializingAtStart === true) { _node.handleSendHUE(oHUEDevice); // Pass by value } } } }); } else { // The config node cannot read the resources. Signalling disconnected } } catch (error) { if (this.sysLogger !== undefined && this.sysLogger !== null) { this.sysLogger.error(`KNXUltimatehueEngine: loadResourcesFromHUEBridge: ${error.message}`); throw (error); } } //})(); }; node.getFirstLightInGroup = function getFirstLightInGroup(_groupID) { if (node.hueAllResources === undefined || node.hueAllResources === null) return; try { const group = node.hueAllResources.filter((a) => a.id === _groupID)[0]; const owner = node.hueAllResources.filter((a) => a.id === group.owner.rid)[0]; if (owner.children !== undefined) { const dev = node.hueAllResources.filter((a) => a.id === owner.children[0].rid)[0]; if (dev.type === "device" && dev.services !== undefined) { const lightID = dev.services.filter((a) => a.rtype === 'light')[0].rid; const oLight = node.hueAllResources.filter((a) => a.id === lightID)[0]; return oLight; } else if (dev.type === "light") { return dev; } } } catch (error) { } }; // Return an array of light belonging to the groupID node.getAllLightsBelongingToTheGroup = async function getAllLightsBelongingToTheGroup(_groupID, refreshResourcesFromBridge = true) { if (node.hueAllResources === undefined || node.hueAllResources === null) return; const retArr = []; let filteredResource; try { if (refreshResourcesFromBridge === true) { await node.loadResourcesFromHUEBridge(); } // filteredResource = node.hueAllResources.filter((a) => a.id === _groupID); // if (filteredResource[0].type === "grouped_light") { // filteredResource = node.hueAllResources.filter((a) => a.services); // filteredResource = filteredResource.filter((a) => a.services).filter((b) => b.type === "light"); // if (filteredResource.length > 0) { // console.log(filteredResource) // } // } node.hueAllResources.forEach((res) => { if (res.services !== undefined && res.services.length > 0) { res.services.forEach((serv) => { if (serv.rid === _groupID) { if (res.children !== undefined) { const children = res.children.filter((a) => a.rtype === "light"); for (let index = 0; index < children.length; index++) { const element = children[index]; const oLight = node.hueAllResources.filter((a) => a.id === element.rid); //if (oLight !== null && oLight !== undefined) retArr.push({ groupID: _groupID, light: oLight[0] }); if (oLight !== null && oLight !== undefined) retArr.push(oLight[0]); } } } }); } }); return retArr; } catch (error) { /* empty */ } }; // Returns the cached devices (node.hueAllResources) by type. node.getResources = function getResources(_rtype) { try { if (node.hueAllResources === undefined) return; // Returns capitalized string function capStr(s) { if (typeof s !== "string") return ""; return s.charAt(0).toUpperCase() + s.slice(1); } const retArray = []; let allResources; if (_rtype === "light" || _rtype === "grouped_light") { allResources = node.hueAllResources.filter((a) => a.type === "light" || a.type === "grouped_light"); } else { allResources = node.hueAllResources.filter((a) => a.type === _rtype); } if (allResources === null) return; for (let index = 0; index < allResources.length; index++) { const resource = allResources[index]; // Get the owner try { let resourceName = ""; let sType = ""; let sArchetype = ""; if (_rtype === "light" || _rtype === "grouped_light") { // It's a service, having a owner const owners = node.hueAllResources.filter((a) => a.id === resource.owner.rid); if (owners !== null) { for (let index = 0; index < owners.length; index++) { const owner = owners[index]; if (owner.type === "bridge_home") { resourceName += "ALL GROUPS and "; } else { resourceName += `${owner.metadata.name} and `; sArchetype += `${owner.metadata.archetype === undefined ? "" : owner.metadata.archetype} and `; // const room = node.hueAllRooms.find((child) => child.children.find((a) => a.rid === owner.id)); // sRoom += room !== undefined ? `${room.metadata.name} + ` : " + "; sType += `${capStr(owner.type)} + `; } } } sType = sType.slice(0, -" + ".length); if (sArchetype !== '') sArchetype = sArchetype.slice(0, -" and ".length); resourceName = resourceName.slice(0, -" and ".length); resourceName += sType !== "" ? ` (${sType}:${sArchetype})` : ""; retArray.push({ name: `${capStr(resource.type)}: ${resourceName}`, id: resource.id, deviceObject: resource, }); } if (_rtype === "scene") { resourceName = resource.metadata.name || "**Name Not Found**"; // Get the linked zone const zone = node.hueAllResources.find((res) => res.id === resource.group.rid); resourceName += ` - ${capStr(resource.group.rtype)}: ${zone.metadata.name}`; retArray.push({ name: `${capStr(_rtype)}: ${resourceName}`, id: resource.id, }); } if (_rtype === "button") { const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; const controlID = resource.metadata !== undefined ? resource.metadata.control_id || "" : ""; retArray.push({ name: `${capStr(_rtype)}: ${linkedDevName}, button ${controlID}`, id: resource.id, }); } if (_rtype === "motion" || _rtype === "camera_motion") { const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `${capStr(_rtype)}: ${linkedDevName}`, id: resource.id, }); } if (_rtype === "relative_rotary") { const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `Rotary: ${linkedDevName}`, id: resource.id, }); } if (_rtype === "light_level") { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)); const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `Light Level: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ""}`, id: resource.id, }); } if (_rtype === "temperature") { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)); const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `Temperature: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ""}`, id: resource.id, }); } if (_rtype === "device_power") { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)); const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `Battery: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ""}`, id: resource.id, }); } if (_rtype === "zigbee_connectivity") { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)); const linkedDevName = node.hueAllResources.find((dev) => dev.type === "device" && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || ""; retArray.push({ name: `Zigbee Connectivity: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ""}`, id: resource.id, }); // Get zigbee_connectivituy // const bridgeId = node.hueAllResources.filter((a) => a.bridge_id === config.bridgeid).owner.rid; // const zigbee_ConnectivityID = node.hueAllResources.filter((a) => a.id === bridgeId).services.filter((a) => a.rtype === "zigbee_connectivity").rid; // // connected, disconnected, connectivity_issue, unidirectional_incoming // const oZigbeeConnectivityStatus = node.hueAllResources.filter((a) => a.id === zigbee_ConnectivityID).status; //const zigbee = node.hueAllResources.filter((a) => a.services !== undefined).find((a) => a.services.rtype === "zigbee_connectivity"); //const devs = zigbee.filter((a) => a.rtype === "zigbee_connectivity"); } if (_rtype === 'contact') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Contact: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id, }); } if (_rtype === 'device_software_update') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Software status: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id, }); } } catch (error) { console.log("KNXHue-config: getResources error ", error.trace); retArray.push({ name: `${_rtype}: ERROR ${error.message}`, id: resource.id, }); } } return { devices: retArray }; } catch (error) { node.sysLogger?.error(`KNXUltimateHue: hueEngine: classHUE: getResources: error ${error.message}`); return { devices: error.message }; } }; // Get current color in HEX (used in html) node.getColorFromHueLight = (_lightId) => { try { const oLight = node.hueAllResources.filter((a) => a.id === _lightId)[0]; 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(); return ret; } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: hueEngine: getColorFromHueLight: error ${error.message}`); return {}; } }; // Get current Kelvin (used in html) node.getKelvinFromHueLight = (_lightId) => { try { const oLight = node.hueAllResources.filter((a) => a.id === _lightId)[0]; const ret = { kelvin: hueColorConverter.ColorConverter.mirekToKelvin(oLight.color_temperature.mirek), brightness: Math.round(oLight.dimming.brightness, 0) }; return JSON.stringify(ret); } catch (error) { node.sysLogger?.error(`KNXUltimateHue: hueEngine: getKelvinFromHueLight: error ${error.message}`); return {}; } }; /** * Get average color XY from a light array * @param {array} _arrayLights - Light array * @returns { x,y,mirek,brightness } - Object containing all infos */ node.getAverageColorsXYBrightnessAndTemperature = async function getAverageColorsXYBrightnessAndTemperature(_arrayLights) { let x; let y; let mirek; let brightness; let countColor = 0, countColor_Temperature = 0, countDimming = 0; _arrayLights.forEach((element) => { if (element.color !== undefined && element.color.xy !== undefined) { if (x === undefined) { x = 0; y = 0; } x += element.color.xy.x; y += element.color.xy.y; countColor += 1; } if (element.color_temperature !== undefined && element.color_temperature.mirek !== undefined) { if (mirek === undefined) mirek = 0; mirek += element.color_temperature.mirek; countColor_Temperature += 1; } if (element.dimming !== undefined && element.dimming.brightness !== undefined) { if (brightness === undefined) brightness = 0; brightness += element.dimming.brightness; countDimming += 1; } }); // Calculate and return the averages const retX = countColor === 0 ? undefined : x / countColor; const retY = countColor === 0 ? undefined : y / countColor; const retMirek = countColor_Temperature === 0 ? undefined : mirek / countColor_Temperature; const retBrightness = countDimming === 0 ? undefined : brightness / countDimming; return { x: retX, y: retY, mirek: retMirek, brightness: retBrightness }; }; // END functions called from the nodes ---------------------------------------------------------------- node.addClient = (_Node) => { // Update the node hue device, as soon as a node register itself to hue-config nodeClients if (node.nodeClients.filter((x) => x.id === _Node.id).length === 0) { node.nodeClients.push(_Node); if (node.hueAllResources !== undefined && node.hueAllResources !== null && _Node.initializingAtStart === true) { const oHUEDevice = node.hueAllResources.filter((a) => a.id === _Node.hueDevice)[0]; if (oHUEDevice !== undefined) { _Node.currentHUEDevice = cloneDeep(oHUEDevice); _Node.handleSendHUE(oHUEDevice); // Add _Node to the clients array _Node.setNodeStatusHue({ fill: "green", shape: "dot", text: "I'm new and ready.", }); } } else { // Add _Node to the clients array _Node.setNodeStatusHue({ fill: "grey", shape: "ring", text: "Waiting for connection", }); } } }; node.removeClient = (_Node) => { // Remove the client node from the clients array try { node.nodeClients = node.nodeClients.filter((x) => x.id !== _Node.id); } catch (error) { /* empty */ } }; node.closeConnection = async () => { node.hueManager?.removeAllListeners(); node.linkStatus === "disconnected"; } node.on("close", (done) => { try { node.sysLogger = null; node.nodeClients = []; node.closeConnection(); (async () => { try { await node.hueManager.close(); node.hueManager = null; delete node.hueManager; done(); } catch (error) { done(); } })(); } catch (error) { done(); } }); } RED.nodes.registerType("hue-config", hueConfig, { credentials: { username: { type: "password" }, clientkey: { type: "password" }, }, }); };