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
JavaScript
/* 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