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
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