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, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.

840 lines (805 loc) 90.9 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 config.updateLocalStateFromKNXWrite = config.updateLocalStateFromKNXWrite === true || config.updateLocalStateFromKNXWrite === 'true' // Starting from v 4.1.31 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 = '' } } const formatTs = (date) => { const d = date instanceof Date ? date : new Date(date) const provider = node.serverKNX if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d) return `${d.getDate()}, ${d.toLocaleTimeString()}` } node.syncCurrentHUEDeviceFromKNXState = function syncCurrentHUEDeviceFromKNXState (_state) { // Starting from v 4.1.31 if (config.updateLocalStateFromKNXWrite !== true) return // Starting from v 4.1.31 if (_state === undefined || _state === null || typeof _state !== 'object') return // Starting from v 4.1.31 if (node.currentHUEDevice === undefined || node.currentHUEDevice === null) return // Starting from v 4.1.31 if (_state.on !== undefined && typeof _state.on.on === 'boolean') { // Starting from v 4.1.31 if (node.currentHUEDevice.on === undefined || node.currentHUEDevice.on === null) node.currentHUEDevice.on = {} // Starting from v 4.1.31 node.currentHUEDevice.on.on = _state.on.on // Starting from v 4.1.31 } // Starting from v 4.1.31 if (_state.dimming !== undefined && _state.dimming !== null && _state.dimming.brightness !== undefined) { // Starting from v 4.1.31 if (node.currentHUEDevice.dimming === undefined || node.currentHUEDevice.dimming === null) node.currentHUEDevice.dimming = {} // Starting from v 4.1.31 node.currentHUEDevice.dimming.brightness = _state.dimming.brightness // Starting from v 4.1.31 } // Starting from v 4.1.31 if (_state.color !== undefined && _state.color !== null && _state.color.xy !== undefined) { // Starting from v 4.1.31 if (node.currentHUEDevice.color === undefined || node.currentHUEDevice.color === null) node.currentHUEDevice.color = {} // Starting from v 4.1.31 node.currentHUEDevice.color.xy = cloneDeep(_state.color.xy) // Starting from v 4.1.31 } // Starting from v 4.1.31 if (_state.color_temperature !== undefined && _state.color_temperature !== null && _state.color_temperature.mirek !== undefined) { // Starting from v 4.1.31 if (node.currentHUEDevice.color_temperature === undefined || node.currentHUEDevice.color_temperature === null) node.currentHUEDevice.color_temperature = {} // Starting from v 4.1.31 node.currentHUEDevice.color_temperature.mirek = _state.color_temperature.mirek // Starting from v 4.1.31 } // Starting from v 4.1.31 } // Starting from v 4.1.31 // 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} (${formatTs(dDate)})` 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} (${formatTs(dDate)})` node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') }) } catch (error) { } } node.writeHueState = function writeHueState (_state) { const defaultOperation = node.isGrouped_light === false ? 'setLight' : 'setGroupedLight' const isGroupedLightOff = node.isGrouped_light === true && node.currentHUEDevice?.on?.on === false const stateKeys = _state && typeof _state === 'object' ? Object.keys(_state) : [] const presetKeys = ['dimming', 'color', 'color_temperature', 'gradient'] const actionableKeys = ['dimming', ...presetKeys] const hasActionablePayload = stateKeys.some((key) => actionableKeys.includes(key)) const mustPresetGroupedLightChildren = isGroupedLightOff && stateKeys.some((key) => presetKeys.includes(key)) if (!mustPresetGroupedLightChildren) { node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation) return } (async () => { try { const groupLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false) RED.log.debug(`knxUltimateHueLight: preset grouped_light children before group on. Group=${node.hueDevice} lights=${Array.isArray(groupLights) ? groupLights.length : 0}`) let hasWrittenAtLeastOneLight = false for (let index = 0; index < groupLights.length; index++) { const light = groupLights[index] if (!light?.id) continue const lightState = {} let hasActionableStateForLight = false if (_state.dimming !== undefined && light.dimming !== undefined) { lightState.dimming = cloneDeep(_state.dimming) hasActionableStateForLight = true } if (_state.color !== undefined && light.color !== undefined) { lightState.color = cloneDeep(_state.color) hasActionableStateForLight = true } if (_state.color_temperature !== undefined && light.color_temperature !== undefined) { lightState.color_temperature = cloneDeep(_state.color_temperature) hasActionableStateForLight = true } if (_state.gradient !== undefined && light.gradient !== undefined) { lightState.gradient = cloneDeep(_state.gradient) hasActionableStateForLight = true } if (!hasActionableStateForLight) continue node.serverHue.hueManager.writeHueQueueAdd(light.id, lightState, 'setLight') hasWrittenAtLeastOneLight = true } if (!hasWrittenAtLeastOneLight || !hasActionablePayload) { node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation) return } if (_state?.on?.on !== true) return const groupedLightState = { on: cloneDeep(_state.on) } if (_state.dynamics !== undefined) groupedLightState.dynamics = cloneDeep(_state.dynamics) RED.log.debug(`knxUltimateHueLight: turning on grouped_light after children preset. Group=${node.hueDevice}`) node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, groupedLightState, defaultOperation) } catch (error) { RED.log.debug(`knxUltimateHueLight: node.writeHueState fallback to grouped_light write: ${error.message}`) node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, _state, defaultOperation) } })() } node.deleteHueStateQueue = function deleteHueStateQueue () { node.serverHue.hueManager.deleteHueQueue(node.hueDevice) if (node.isGrouped_light !== true) return; (async () => { try { const groupLights = await node.serverHue.getAllLightsBelongingToTheGroup(node.hueDevice, false) for (let index = 0; index < groupLights.length; index++) { const light = groupLights[index] if (!light?.id) continue node.serverHue.hueManager.deleteHueQueue(light.id) } } catch (error) { RED.log.debug(`knxUltimateHueLight: node.deleteHueStateQueue: ${error.message}`) } })() } 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.writeHueState(state) 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 } } : { 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.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31 node.writeHueState(state) 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.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31 node.writeHueState(state) 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.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31 node.writeHueState(state) 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)) if (typeof msg.payload !== 'number' || Number.isNaN(msg.payload)) throw new Error('Invalid KNX brightness payload') state = { dimming: { brightness: msg.payload } } if (msg.payload === 0) { state.on = { on: false } } else if (node.currentHUEDevice !== undefined && node.currentHUEDevice.on?.on === true) { state.on = { on: true } } node.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31 node.writeHueState(state) 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.syncCurrentHUEDeviceFromKNXState(state) // Starting from v 4.1.31 node.writeHueState(state) 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.writeHueState(state) }, 1500) } else { if (node.timerBlink !== undefined) clearInterval(node.timerBlink) node.writeHueState({ on: { on: false } }) } 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.writeHueState({ on: { on: true } }) 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.writeHueState(state) } catch (error) { } }, 10000) } else { if (node.timerColorCycle !== undefined) clearInterval(node.timerColorCycle) node.writeHueState({ on: { on: false } }) } 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} (${formatTs(new Date())})` }) 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) { const zeroBrightnessWhenOff = (config.updateKNXBrightnessStatusOnHUEOnOff === undefined || config.updateKNXBrightnessStatusOnHUEOnOff === 'onhueoff') 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: // Hue keeps the last brightness even when the light is OFF. If the user enabled "brightness status -> 0 on HUE off", // keep returning 0 while OFF to avoid the KNX status jumping back to the last/start value after some time/read-requests. if (zeroBrightnessWhenOff === true && node.currentHUEDevice?.on?.on === false) { ret = 0 } else { 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} (${formatTs(new Date())})` }) 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.deleteHueStateQueue() // 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.writeHueState(hueTelegram) 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.writeHueState(hueTelegram) 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.timerStepDimTunableW