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