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.

807 lines (776 loc) 85.1 kB
/* eslint-disable no-param-reassign */ /* eslint-disable camelcase */ /* eslint-disable max-len */ /* eslint-disable no-lonely-if */ const cloneDeep = require("lodash/cloneDeep"); const dptlib = require('knxultimate').dptlib; const hueColorConverter = require("./utils/colorManipulators/hueColorConverter"); module.exports = function (RED) { function knxUltimateHueLight(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; // Convert for backward compatibility if (config.nameLightKelvinDIM === undefined) { config.nameLightKelvinDIM = config.nameLightHSV; config.GALightKelvinDIM = config.GALightHSV; config.dptLightKelvinDIM = config.dptLightHSV; config.nameLightKelvinPercentage = config.nameLightHSVPercentage; config.GALightKelvinPercentage = config.GALightHSVPercentage; config.dptLightKelvinPercentage = config.dptLightHSVPercentage; config.nameLightKelvinPercentageState = config.nameLightHSVState; config.GALightKelvinPercentageState = config.GALightHSVState; config.dptLightKelvinPercentageState = config.dptLightHSVState; } node.topic = node.name; node.name = config.name === undefined ? "Hue" : config.name; node.outputtopic = node.name; node.dpt = ""; node.notifyreadrequest = true; 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.currentHUEDevice = undefined; // At start, this value is filled by a call to HUE api. It stores a value representing the current light status. node.HUEDeviceWhileDaytime = null;// This retains the HUE device status while daytime, to be restored after nighttime elapsed. node.HUELightsBelongingToGroupWhileDaytime = null; // Array contains all light belonging to the grouped_light (if grouped_light is selected) node.DayTime = true; node.isGrouped_light = config.hueDevice.split("#")[1] === "grouped_light"; node.hueDevice = config.hueDevice.split("#")[0]; node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === "yes"); config.specifySwitchOnBrightness = (config.specifySwitchOnBrightness === undefined || config.specifySwitchOnBrightness === '') ? "temperature" : config.specifySwitchOnBrightness; config.specifySwitchOnBrightnessNightTime = (config.specifySwitchOnBrightnessNightTime === undefined || config.specifySwitchOnBrightnessNightTime === '') ? "no" : config.specifySwitchOnBrightnessNightTime; config.colorAtSwitchOnDayTime = (config.colorAtSwitchOnDayTime === '' || config.colorAtSwitchOnDayTime === undefined) ? '{ "kelvin":3000, "brightness":100 }' : config.colorAtSwitchOnDayTime; config.colorAtSwitchOnNightTime = (config.colorAtSwitchOnNightTime === '' || config.colorAtSwitchOnNightTime === undefined) ? '{ "kelvin":2700, "brightness":20 }' : config.colorAtSwitchOnNightTime; config.colorAtSwitchOnDayTime = config.colorAtSwitchOnDayTime.replace("geen", "green"); config.colorAtSwitchOnNightTime = config.colorAtSwitchOnNightTime.replace("geen", "green"); config.dimSpeed = (config.dimSpeed === undefined || config.dimSpeed === '') ? 5000 : Number(config.dimSpeed); config.HSVDimSpeed = (config.HSVDimSpeed === undefined || config.HSVDimSpeed === '') ? 5000 : Number(config.HSVDimSpeed); config.invertDimTunableWhiteDirection = config.invertDimTunableWhiteDirection !== undefined; config.restoreDayMode = config.restoreDayMode === undefined ? "no" : config.restoreDayMode; // no or setDayByFastSwitchLightSingle or setDayByFastSwitchLightALL node.timerCheckForFastLightSwitch = null; config.invertDayNight = config.invertDayNight === undefined ? false : config.invertDayNight; node.HSVObject = null; //{ h, s, v };// Store the current light calculated HSV // Transform HEX in RGB and stringified json in json oblects. if (config.colorAtSwitchOnDayTime.indexOf("#") !== -1) { // Transform to rgb. try { config.colorAtSwitchOnDayTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnDayTime.replace("#", "")); } catch (error) { config.colorAtSwitchOnDayTime = { kelvin: 3000, brightness: 100 }; } } else { try { config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnDayTime); } catch (error) { RED.log.error(`knxUltimateHueLight: config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnDayTime): ${error.message} : ${error.stack || ""} `); config.colorAtSwitchOnDayTime = ""; } } // Same thing, but with night color if (config.colorAtSwitchOnNightTime.indexOf("#") !== -1) { // Transform to rgb. try { config.colorAtSwitchOnNightTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnNightTime.replace("#", "")); } catch (error) { config.colorAtSwitchOnNightTime = { kelvin: 2700, brightness: 20 }; } } else { try { config.colorAtSwitchOnNightTime = JSON.parse(config.colorAtSwitchOnNightTime); } catch (error) { RED.log.error(`knxUltimateHueLight: config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnNightTime): ${error.message} : ${error.stack || ""} `); config.colorAtSwitchOnNightTime = ""; } } // Used to call the status update from the config node. node.setNodeStatus = ({ fill, shape, text, payload, }) => { try { if (node.currentHUEDevice?.on?.on === true) { fill = "blue"; shape = "dot" } else { fill = "blue"; shape = "ring" }; 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()})`; node.status({ 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 (node.currentHUEDevice?.on?.on === true) { fill = "blue"; shape = "dot" } else { fill = "blue"; shape = "ring" }; 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()})`; node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') }); } catch (error) { } }; function getRandomIntInclusive(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive } // This function is called by the hue-config.js node.handleSend = (msg) => { if (node.currentHUEDevice === undefined && node.serverHue.linkStatus === "connected") { node.setNodeStatusHue({ fill: "yellow", shape: "ring", text: "Initializing. Please wait.", payload: "", }); return; } if (msg.knx.event !== "GroupValue_Read" && node.currentHUEDevice !== undefined) { let state = {}; try { switch (msg.knx.destination) { case config.GALightSwitch: msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightSwitch)); // 15/05/2024 Supergiovane: check the Override to Day option // config.restoreDayMode can be: no or setDayByFastSwitchLightSingle or setDayByFastSwitchLightALL // ---------------------------------------------------------- if (config.restoreDayMode === "setDayByFastSwitchLightSingle" || config.restoreDayMode === "setDayByFastSwitchLightALL") { if (node.DayTime === false) { if (msg.payload === true) { if (node.timerCheckForFastLightSwitch === null) { node.timerCheckForFastLightSwitch = setTimeout(() => { node.DayTime = false; RED.log.debug("knxUltimateHueLight: node.timerCheckForFastLightSwitch: set daytime to false after node.timerCheckForFastLightSwitch elapsed"); node.timerCheckForFastLightSwitch = null; }, 10000); // 10 seconds } else { if (config.restoreDayMode === "setDayByFastSwitchLightALL") { // Turn off the Day/Night group address if (config.GADaylightSensor !== undefined && config.GADaylightSensor !== "") { if (node.timerCheckForFastLightSwitch !== null) { clearTimeout(node.timerCheckForFastLightSwitch); node.timerCheckForFastLightSwitch = null; } RED.log.debug(`knxUltimateHueLight: node.timerCheckForFastLightSwitch: set daytime the group address ${config.GADaylightSensor}`); node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: config.GADaylightSensor, payload: config.invertDayNight === false, dpt: config.dptDaylightSensor, outputtype: "write", nodecallerid: node.id, }); } } node.DayTime = true; RED.log.debug("knxUltimateHueLight: node.timerCheckForFastLightSwitch: set daytime to true"); } } } } // ---------------------------------------------------------- if (msg.payload === true) { // From HUE Api core concepts: // If you try and control multiple conflicting parameters at once e.g. {"color": {"xy": {"x":0.5,"y":0.5}}, "color_temperature": {"mirek": 250}} // the lights can only physically do one, for this we apply the rule that xy beats ct. Simple. // color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum if ((node.DayTime === true && config.specifySwitchOnBrightness === "no") && ((node.isGrouped_light === false && node.HUEDeviceWhileDaytime !== null) || (node.isGrouped_light === true && node.HUELightsBelongingToGroupWhileDaytime !== null))) { if (node.isGrouped_light === false && node.HUEDeviceWhileDaytime !== null) { // The DayNight has switched into day, so restore the previous light status state = { on: { on: true }, dimming: node.HUEDeviceWhileDaytime.dimming, color: node.HUEDeviceWhileDaytime.color, color_temperature: node.HUEDeviceWhileDaytime.color_temperature }; if (node.HUEDeviceWhileDaytime.color_temperature !== undefined && node.HUEDeviceWhileDaytime.color_temperature.mirek === null) delete state.color_temperature; // Otherwise the lamp will not turn on due to an error. color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: "Restore light status", }); node.HUEDeviceWhileDaytime = null; // Nullize the object. } else if (node.isGrouped_light === true && node.HUELightsBelongingToGroupWhileDaytime !== null) { // The DayNight has switched into day, so restore the previous light state, belonging to the group let bAtLeastOneIsOn = false; for (let index = 0; index < node.HUELightsBelongingToGroupWhileDaytime.length; index++) { // Ensure, at least 1 lamp was on, otherwise turn all lamps on const element = node.HUELightsBelongingToGroupWhileDaytime[index].light[0]; if (element.on.on === true) { bAtLeastOneIsOn = true; break; } } for (let index = 0; index < node.HUELightsBelongingToGroupWhileDaytime.length; index++) { const element = node.HUELightsBelongingToGroupWhileDaytime[index].light[0]; if (bAtLeastOneIsOn === true) { state = { on: element.on, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature }; } else { // Failsafe all on state = { on: { on: true }, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature }; } if (element.color_temperature !== undefined && element.color_temperature.mirek === null) delete state.color_temperature; // Otherwise the lamp will not turn on due to an error. color_temperature.mirek: color temperature in mirek is null when the light color is not in the ct spectrum node.serverHue.hueManager.writeHueQueueAdd(element.id, state, "setLight"); } node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: "Resuming all group's light", }); node.HUELightsBelongingToGroupWhileDaytime = null; // Nullize the object. return; } } else { let colorChoosen; let temperatureChoosen; let brightnessChoosen; // The light must support the temperature (in this case, colorAtSwitchOnNightTime is an object {kelvin:xx, brightness:yy}) if (node.currentHUEDevice.color_temperature !== undefined) { if (node.DayTime === true && config.specifySwitchOnBrightness === "temperature") { temperatureChoosen = config.colorAtSwitchOnDayTime.kelvin; } else if (node.DayTime === false && config.enableDayNightLighting === "temperature") { temperatureChoosen = config.colorAtSwitchOnNightTime.kelvin; } } if (node.currentHUEDevice.dimming !== undefined) { // Check wether the user selected specific brightness at switch on (in this case, colorAtSwitchOnNightTime is an object {kelvin:xx, brightness:yy}) if (node.DayTime === true && config.specifySwitchOnBrightness === "temperature") { brightnessChoosen = config.colorAtSwitchOnDayTime.brightness; } else if (node.DayTime === false && config.enableDayNightLighting === "temperature") { brightnessChoosen = config.colorAtSwitchOnNightTime.brightness; } } if (node.currentHUEDevice.color !== undefined) { // Check wether the user selected specific color at switch on (in this case, colorAtSwitchOnDayTime is a text with HTML web color) if (node.DayTime === true && config.specifySwitchOnBrightness === "yes") { colorChoosen = config.colorAtSwitchOnDayTime; } else if (node.DayTime === false && config.enableDayNightLighting === "yes") { colorChoosen = config.colorAtSwitchOnNightTime; } } // Create the HUE command if (colorChoosen !== undefined) { // Now we have a jColorChoosen. Proceed illuminating the light let gamut = null; if (node.currentHUEDevice.color.gamut !== undefined) { gamut = node.currentHUEDevice.color.gamut; } const dretXY = hueColorConverter.ColorConverter.calculateXYFromRGB(colorChoosen.red, colorChoosen.green, colorChoosen.blue, gamut); const dbright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(colorChoosen.red, colorChoosen.green, colorChoosen.blue); node.currentHUEDevice.dimming.brightness = Math.round(dbright, 0); if (node.currentHUEDevice.color !== undefined) node.currentHUEDevice.color.xy = dretXY; // 26/03/2024 node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness); state = dbright > 0 ? { on: { on: true }, dimming: { brightness: dbright }, color: { xy: dretXY } } : { on: { on: false } }; // state = { on: { on: true }, dimming: { brightness: dbright }, color: { xy: dretXY } }; } else if (temperatureChoosen !== undefined) { // Kelvin const mirek = hueColorConverter.ColorConverter.kelvinToMirek(temperatureChoosen); node.currentHUEDevice.color_temperature.mirek = mirek; node.currentHUEDevice.dimming.brightness = brightnessChoosen; node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness); // Kelvin temp state = brightnessChoosen > 0 ? { on: { on: true }, dimming: { brightness: brightnessChoosen }, color_temperature: { mirek: mirek } } : { on: { on: false } }; // state = { on: { on: true }, dimming: { brightness: brightnessChoosen }, color_temperature: { mirek: mirek } }; } else if (brightnessChoosen !== undefined) { state = brightnessChoosen > 0 ? { on: { on: true }, dimming: { brightness: brightnessChoosen } } : { on: { on: false } }; // state = { on: { on: true }, dimming: { brightness: brightnessChoosen } }; } else { state = { on: { on: true } }; } } } else { // Stop color cycle if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle); // Stop Blinking if (node.timerBlink !== undefined) clearInterval(node.timerBlink); state = { on: { on: false } }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: state, }); break; case config.GALightDIM: // { 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. msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM)); node.hueDimming(msg.payload.decr_incr, msg.payload.data, config.dimSpeed); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: JSON.stringify(msg.payload), }); break; case config.GALightHSV_H_DIM: // { 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. msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM)); node.hueDimmingHSV_H(msg.payload.decr_incr, msg.payload.data, config.HSVDimSpeed); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: JSON.stringify(msg.payload), }); break; case config.GALightHSV_S_DIM: // { 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. msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightDIM)); node.hueDimmingHSV_S(msg.payload.decr_incr, msg.payload.data, config.HSVDimSpeed); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: JSON.stringify(msg.payload), }); break; case config.GALightKelvin: let retMirek; let kelvinValue = 0; msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvin)); if (config.dptLightKelvin === "7.600") { if (msg.payload > 6535) msg.payload = 6535; if (msg.payload < 2000) msg.payload = 2000; kelvinValue = msg.payload;//hueColorConverter.ColorConverter.scale(msg.payload, [0, 65535], [2000, 6535]); retMirek = hueColorConverter.ColorConverter.kelvinToMirek(kelvinValue); } else if (config.dptLightKelvin === "9.002") { // Relative temperature in Kelvin. Use HUE scale. if (msg.payload > 6535) msg.payload = 6535; if (msg.payload < 2000) msg.payload = 2000; retMirek = hueColorConverter.ColorConverter.kelvinToMirek(msg.payload); } state = { color_temperature: { mirek: retMirek } }; node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: kelvinValue, }); break; case config.GADaylightSensor: node.DayTime = Boolean(dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptDaylightSensor))); if (config.invertDayNight === true) node.DayTime = !node.DayTime; if (config.specifySwitchOnBrightness === "no") { // This retains the HUE device status while daytime, to be restored after nighttime elapsed. https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/298 if (node.DayTime === false) { if (node.isGrouped_light === false) node.HUEDeviceWhileDaytime = cloneDeep(node.currentHUEDevice); // DayTime has switched to false: save the currentHUEDevice into the HUEDeviceWhileDaytime if (node.isGrouped_light === true) { (async () => { try { const retLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false); node.HUELightsBelongingToGroupWhileDaytime = cloneDeep(retLights); // DayTime has switched to false: save the lights belonging to the group into the HUELightsBelongingToGroupWhileDaytime array } catch (error) { /* empty */ } })(); } } } else { node.HUEDeviceWhileDaytime = null; node.HUELightsBelongingToGroupWhileDaytime = null; } node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE Daytime", payload: node.DayTime, }); break; case config.GALightKelvinDIM: if (config.dptLightKelvinDIM === "3.007") { // MDT smartbutton will dim the color temperature // { 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. msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvinDIM)); node.hueDimmingTunableWhite(msg.payload.decr_incr, msg.payload.data, 5000); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: JSON.stringify(msg.payload), }); } break; case config.GALightKelvinPercentage: if (config.dptLightKelvinPercentage === "5.001") { // 0-100% tunable white msg.payload = 100 - dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvinPercentage)); // msg.payload = msg.payload <= 0 ? 1 : msg.payload const retMirek = hueColorConverter.ColorConverter.scale(msg.payload, [0, 100], [153, 500]); msg.payload = retMirek; state = { color_temperature: { mirek: msg.payload } }; node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: state, }); } break; case config.GALightBrightness: msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightBrightness)); state = { dimming: { brightness: msg.payload } }; if (node.currentHUEDevice === undefined) { // Grouped light state.on = { on: msg.payload > 0 }; } else { // Light if (node.currentHUEDevice.on.on === false && msg.payload > 0) state.on = { on: true }; if (node.currentHUEDevice.on.on === true && msg.payload === 0) state.on = { on: false }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: state, }); break; case config.GALightColor: msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightColor)); let gamut = null; if ( node.currentHUEDevice !== undefined && node.currentHUEDevice.color !== undefined && node.currentHUEDevice.color.gamut !== undefined ) { gamut = node.currentHUEDevice.color.gamut; } const retXY = hueColorConverter.ColorConverter.calculateXYFromRGB(msg.payload.red, msg.payload.green, msg.payload.blue, gamut); const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(msg.payload.red, msg.payload.green, msg.payload.blue); state = { dimming: { brightness: bright }, color: { xy: retXY } }; if (node.currentHUEDevice === undefined) { // Grouped light state.on = { on: bright > 0 }; } else { // Light if (node.currentHUEDevice.on.on === false && bright > 0) state.on = { on: true }; if (node.currentHUEDevice.on.on === true && bright === 0) state = { on: { on: false }, dimming: { brightness: bright } }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: state, }); break; case config.GALightBlink: const gaVal = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightBlink)); if (gaVal) { node.timerBlink = setInterval(() => { if (node.blinkValue === undefined) node.blinkValue = true; node.blinkValue = !node.blinkValue; msg.payload = node.blinkValue; // state = msg.payload === true ? { on: { on: true } } : { on: { on: false } } state = msg.payload === true ? { on: { on: true }, dimming: { brightness: 100 }, dynamics: { duration: 0 } } : { on: { on: false }, dimming: { brightness: 0 }, dynamics: { duration: 0 } }; node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); }, 1500); } else { if (node.timerBlink !== undefined) clearInterval(node.timerBlink); node.serverHue.hueManager.writeHueQueueAdd( node.hueDevice, { on: { on: false } }, node.isGrouped_light === false ? "setLight" : "setGroupedLight", ); } node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: gaVal, }); break; case config.GALightColorCycle: { if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle); const gaValColorCycle = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightColorCycle)); if (gaValColorCycle === true) { node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, { on: { on: true } }, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); node.timerColorCycle = setInterval(() => { try { const red = getRandomIntInclusive(0, 255); const green = getRandomIntInclusive(0, 255); const blue = getRandomIntInclusive(0, 255); let gamut = null; if ( node.currentHUEDevice !== undefined && node.currentHUEDevice.color !== undefined && node.currentHUEDevice.color.gamut !== undefined ) { gamut = node.currentHUEDevice.color.gamut; } const retXY = hueColorConverter.ColorConverter.calculateXYFromRGB(red, green, blue, gamut); const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(red, green, blue); state = bright > 0 ? { on: { on: true }, dimming: { brightness: bright }, color: { xy: retXY } } : { on: { on: false } }; node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); } catch (error) { } }, 10000); } else { if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle); node.serverHue.hueManager.writeHueQueueAdd( node.hueDevice, { on: { on: false } }, node.isGrouped_light === false ? "setLight" : "setGroupedLight", ); } node.setNodeStatusHue({ fill: "green", shape: "dot", text: "KNX->HUE", payload: gaValColorCycle, }); } break; default: break; } } catch (error) { node.status({ fill: "red", shape: "dot", text: `KNX->HUE errorRead ${error.message} (${new Date().getDate()}, ${new Date().toLocaleTimeString()})`, }); RED.log.error(`knxUltimateHueLight: node.handleSend: if (msg.knx.event !== "GroupValue_Read"): ${error.message} : ${error.stack || ""} `); } } // I must respond to query requests (read request) sent from the KNX BUS try { if (msg.knx.event === "GroupValue_Read" && node.currentHUEDevice !== undefined) { let ret; switch (msg.knx.destination) { case config.GALightState: ret = node.currentHUEDevice.on.on; if (ret !== undefined) node.updateKNXLightState(ret, "response"); break; case config.GALightColorState: ret = node.currentHUEDevice.color.xy; if (ret !== undefined) node.updateKNXLightColorState(node.currentHUEDevice.color, "response"); break; case config.GALightKelvinPercentageState: // The kelvin level belongs to the group defice, so i don't need to get the first light in the collection (if the device is a grouped_light) ret = node.currentHUEDevice.color_temperature.mirek; if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, "response"); // if (node.isGrouped_light === false) { // ret = node.currentHUEDevice.color_temperature.mirek; // if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, "response"); // } else { // (async () => { // try { // // Find the first light in the collection, having the color_temperature capabilities // const devices = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false); // for (let index = 0; index < devices.length; index++) { // const element = devices[index]; // if (element.light[0].color_temperature !== undefined) { // ret = element.light[0].color_temperature.mirek; // break; // } // } // if (ret !== undefined) node.updateKNXLightKelvinPercentageState(ret, "response"); // } catch (error) { /* empty */ } // })(); // } break; case config.GALightBrightnessState: ret = node.currentHUEDevice.dimming.brightness; if (ret !== undefined) node.updateKNXBrightnessState(ret, "response"); break; case config.GALightKelvinState: // The kelvin level belongs to the group defice, so i don't need to get the first light in the collection (if the device is a grouped_light) ret = node.currentHUEDevice.color_temperature.mirek; if (ret !== undefined) node.updateKNXLightKelvinState(ret, "response"); // if (node.isGrouped_light === false) { // ret = node.currentHUEDevice.color_temperature.mirek; // if (ret !== undefined) node.updateKNXLightKelvinState(ret, "response"); // } else { // (async () => { // try { // // Find the first light in the collection, having the color_temperature capabilities // const devices = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false); // for (let index = 0; index < devices.length; index++) { // const element = devices[index]; // if (element.light[0].color_temperature !== undefined) { // ret = element.light[0].color_temperature.mirek; // break; // } // } // if (ret !== undefined) node.updateKNXLightKelvinState(ret, "response"); // } catch (error) { /* empty */ } // })(); // } break; default: break; } } } catch (error) { node.status({ fill: "red", shape: "dot", text: `KNX->HUE error :-( ${error.message} (${new Date().getDate()}, ${new Date().toLocaleTimeString()})`, }); RED.log.error(`knxUltimateHueLight: node.handleSend: if (msg.knx.event === "GroupValue_Read" && node.currentHUEDevice !== undefined): ${error.message} : ${error.stack || ""} `); } }; // Start dimming // *********************************************************** node.hueDimming = function hueDimming(_KNXaction, _KNXbrightness_Direction, _dimSpeedInMillisecs = undefined) { // 31/10/2023 after many attempts to use dimming_delta function of the HueApeV2, loosing days of my life, without a decent success, will use the standard dimming calculations // i decide to go to the "step brightness" way. try { let hueTelegram = {}; let numStep = 10; // Steps from 0 to 100 by 10 const extendedConf = {}; if (_KNXbrightness_Direction === 0 && _KNXaction === 0) { // STOP DIM if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim); node.brightnessStep = null; node.serverHue.hueManager.deleteHueQueue(node.hueDevice); // Clear dimming queue. return; } // 26/03/2024 set the extended configuration as well, because the light was off (so i've been unable to set it up elsewhere) if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) { // if (node.currentHUEDevice.color !== undefined) extendedConf.color = { xy: node.currentHUEDevice.color.xy }; // DO NOT ADD THE COLOR, BECAUSE THE COLOR HAS ALSO THE BRIGHTNESS, SO ALL THE DATA NEEDED TO BE SWITCHED ON CORRECLY if (node.currentHUEDevice.color_temperature !== undefined) extendedConf.color_temperature = { mirek: node.currentHUEDevice.color_temperature.mirek }; } // If i'm dimming up while the light is off, start the dim with the initial brightness set to zero if (_KNXbrightness_Direction > 0 && _KNXaction === 1 && node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) { node.brightnessStep = null; node.currentHUEDevice.dimming.brightness = 0; } // Set the actual brightness to start with if (node.brightnessStep === null || node.brightnessStep === undefined) node.brightnessStep = node.currentHUEDevice.dimming.brightness !== undefined ? node.currentHUEDevice.dimming.brightness : 50; node.brightnessStep = Math.ceil(Number(node.brightnessStep)); // We have also minDimLevelLight and maxDimLevelLight to take care of. let minDimLevelLight; if (config.minDimLevelLight === undefined) { minDimLevelLight = 10; } else if (config.minDimLevelLight === "useHueLightLevel") { minDimLevelLight = node.currentHUEDevice.dimming.min_dim_level === undefined ? 10 : node.currentHUEDevice.dimming.min_dim_level; } else { minDimLevelLight = Number(config.minDimLevelLight); } const maxDimLevelLight = config.maxDimLevelLight === undefined ? 100 : Number(config.maxDimLevelLight); // Set the speed _dimSpeedInMillisecs /= numStep; numStep = Math.round((maxDimLevelLight - minDimLevelLight) / numStep, 0); if (_KNXbrightness_Direction > 0 && _KNXaction === 1) { // DIM UP if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim); node.timerStepDim = setInterval(() => { node.updateKNXBrightnessState(node.brightnessStep); // Unnecessary, but necessary to set the KNX Status in real time. node.brightnessStep += numStep; if (node.brightnessStep > maxDimLevelLight) node.brightnessStep = maxDimLevelLight; hueTelegram = { dimming: { brightness: node.brightnessStep }, dynamics: { duration: _dimSpeedInMillisecs + 500 } }; // + is to avoid ladder effect // Switch on the light if off if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) { hueTelegram.on = { on: true }; Object.assign(hueTelegram, extendedConf); // 26/03/2024 add extended conf } //console.log(hueTelegram) node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, hueTelegram, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); if (node.brightnessStep >= maxDimLevelLight) clearInterval(node.timerStepDim); }, _dimSpeedInMillisecs); } if (_KNXbrightness_Direction > 0 && _KNXaction === 0) { if (node.currentHUEDevice.on.on === false) return; // Don't dim down, if the light is already off. // DIM DOWN if (node.timerStepDim !== undefined) clearInterval(node.timerStepDim); node.timerStepDim = setInterval(() => { node.updateKNXBrightnessState(node.brightnessStep); // Unnecessary, but necessary to set the KNX Status in real time. node.brightnessStep -= numStep; if (node.brightnessStep < minDimLevelLight) node.brightnessStep = minDimLevelLight; hueTelegram = { dimming: { brightness: node.brightnessStep }, dynamics: { duration: _dimSpeedInMillisecs + 500 } };// + 100 is to avoid ladder effect // Switch off the light if on if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStep === 0) { hueTelegram.on = { on: false }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, hueTelegram, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); if (node.brightnessStep <= minDimLevelLight) clearInterval(node.timerStepDim); }, _dimSpeedInMillisecs); } } catch (error) { } }; // *********************************************************** // Start dimming tunable white // mirek: required(integer minimum: 153, maximum: 500) // *********************************************************** node.hueDimmingTunableWhite = function hueDimmingTunableWhite(_KNXaction, _KNXbrightness_DirectionTunableWhite, _dimSpeedInMillisecsTunableWhite = undefined) { // 23/23/2023 after many attempts to use dimming_delta function of the HueApeV2, loosing days of my life, without a decent success, will use the standard dimming calculations // i decide to go to the "step brightness" way. try { if (_KNXbrightness_DirectionTunableWhite === 0 && _KNXaction === 0) { // STOP DIM if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite); node.brightnessStepTunableWhite = null; node.serverHue.hueManager.deleteHueQueue(node.hueDevice); // Clear dimming queue. return; } let numStepTunableWhite = 10; // Steps from 153 to 500 if (config.invertDimTunableWhiteDirection === true) { if (_KNXaction === 1) { _KNXaction = 0; } else { _KNXaction = 1; } } // Set the actual brightness to start with if (node.brightnessStepTunableWhite === null || node.brightnessStepTunableWhite === undefined) node.brightnessStepTunableWhite = node.currentHUEDevice.color_temperature.mirek || 372; node.brightnessStepTunableWhite = Math.ceil(Number(node.brightnessStepTunableWhite)); // Set the speed _dimSpeedInMillisecsTunableWhite = Math.ceil(_dimSpeedInMillisecsTunableWhite / numStepTunableWhite); const minDimLevelLightTunableWhite = 153; const maxDimLevelLightTunableWhite = 500; //numStepTunableWhite = hueColorConverter.ColorConverter.scale(numStepTunableWhite, [0, 100], [minDimLevelLightTunableWhite, maxDimLevelLightTunableWhite]); //numStepTunableWhite = hueColorConverter.ColorConverter.scale(numStepTunableWhite, [node.brightnessStepTunableWhite, maxDimLevelLightTunableWhite], [node.brightnessStepTunableWhite, maxDimLevelLightTunableWhite]); numStepTunableWhite = Math.round((maxDimLevelLightTunableWhite - minDimLevelLightTunableWhite) / numStepTunableWhite, 0); if (_KNXbrightness_DirectionTunableWhite > 0 && _KNXaction === 1) { // DIM UP if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite); node.timerStepDimTunableWhite = setInterval(() => { node.updateKNXLightKelvinPercentageState(node.brightnessStepTunableWhite); // Unnecessary, but necessary to set the KNX Status in real time. node.brightnessStepTunableWhite += numStepTunableWhite; // *2 to speed up the things if (node.brightnessStepTunableWhite > maxDimLevelLightTunableWhite) node.brightnessStepTunableWhite = maxDimLevelLightTunableWhite; const hueTelegram = { color_temperature: { mirek: node.brightnessStepTunableWhite }, dynamics: { duration: _dimSpeedInMillisecsTunableWhite } }; // Switch on the light if off if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false) { hueTelegram.on = { on: true }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, hueTelegram, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); if (node.brightnessStepTunableWhite >= maxDimLevelLightTunableWhite) clearInterval(node.timerStepDimTunableWhite); }, _dimSpeedInMillisecsTunableWhite); } if (_KNXbrightness_DirectionTunableWhite > 0 && _KNXaction === 0) { if (node.currentHUEDevice.on.on === false) return; // Don't dim down, if the light is already off. // DIM DOWN if (node.timerStepDimTunableWhite !== undefined) clearInterval(node.timerStepDimTunableWhite); node.timerStepDimTunableWhite = setInterval(() => { node.updateKNXLightKelvinPercentageState(node.brightnessStepTunableWhite); // Unnecessary, but necessary to set the KNX Status in real time. node.brightnessStepTunableWhite -= numStepTunableWhite; // *2 to speed up the things if (node.brightnessStepTunableWhite < minDimLevelLightTunableWhite) node.brightnessStepTunableWhite = minDimLevelLightTunableWhite; const hueTelegram = { color_temperature: { mirek: node.brightnessStepTunableWhite }, dynamics: { duration: _dimSpeedInMillisecsTunableWhite } }; // Switch off the light if on if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true && node.brightnessStepTunableWhite === 0) { hueTelegram.on = { on: false }; } node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, hueTelegram, node.isGrouped_light === false ? "setLight" : "setGroupedLight"); if (node.brightnessStepTunableWhite <= minDimLevelLightTunableWhite) clearInterval(node.timerStepDimTunableWhite); }, _dimSpeedInMillisecsTunableWhite); } } catch (error) { } }; // *********************************************************** /** * Starts dimming / stop dimming of the HUE * @param {number} _KNXaction 1 for start, 0 for stop * @param {number} _KNXbrightness_DirectionHSV_H 1 for up, 0 for down * @param {number} _dimSpeedInMillisecsHSV Speed time in milliseconds * @returns {} */ node.hueDimmingHSV_H = function hueDimmingHSV_H(_KNXaction, _KNXbrightness_DirectionHSV_H, _dimSpeedInMillisecsHSV = undefined) { // After many attempts to use dimming_delta function of the HueApiV2, loosing days of my life, without a decent success, will use the standard dimming calculations // i decide to go to the "step brightness" way. try { if (_KNXbrightness_DirectionHSV_H === 0 && _KNXaction === 0) { // STOP DIM if (node.timerStepDimHSV_H !== undefined) clearInterval(node.timerStepDimHSV_H); node.serverHue.hueManager.deleteHueQueue(node.hueDevice); // Clear dimming queue. return; } let xyBrightnessToHsv; if (node.currentHUEDevice.color !== undefined && node.currentHUEDevice.color.xy !== undefined) { // Get the XY + brightness from the lamp and transform it into HSV // xyBrightnessToHsv = hueColorConverter.ColorConverter.xyBrig