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, KNX AI for diagnosticsand 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