Hubitat to Node-Red Hue, Saturation, and Level combination

Not a full project, but in case someone looks here instead of the Hubitat forum, I wanted to share a flow.

I had discussed a challenge with separating input from Hubitat into Node-Red. The situation is that when a color change to a generic RGBW bulb is done in the Hubitat interfaces, it comes into Node Red as 3 separate messages (Hue, Saturation, Level). For my purposes, I wanted to combine that into a single HSL array. The problem is that only those readings that change are sent (thus it is possible to get H, S, L, HS, HL, etc. Additionally if the brightness is adjusted, the a change of L is sent. So not only did I want to combine the HSL readings, I also wanted to identify the brightness change case and process it in a different way. between a case where the color (hue and/or saturation and optionally level) is being changed and where only the brightness (level) is being changed.

I have now built a solution to this challenge that I want to share here. I have tested it, but it is possible that there is some corner case that I did not reach in my design and testing. If you choose to use it, feel free to reach out if you run into a problem and I will try to help or update the flow to address your problem.

[{"id":"d4f615bad7f8c4d9","type":"group","z":"5c9e82eadde3cbc4","name":"Flow that takes inputs from Hue, Saturation and Level and figures out if it is a color change or only a brightness (Level) change","style":{"stroke":"#000000","fill":"#e3f3d3","label":true,"color":"#000000"},"nodes":["5e85427461b9099f","386250a07242f362","bd9646b4dcad07d6","a195a6014dce2014","097a5670fbf70301","d26243490fe07b6c"],"x":158,"y":2733,"w":1048,"h":254},{"id":"5e85427461b9099f","type":"rbe","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"Filter out duplicate\\n attribute readings","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload.attributes","topi":"topic","x":730,"y":2860,"wires":[["bd9646b4dcad07d6"]]},{"id":"386250a07242f362","type":"http request","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"Get Full Details\\n from Maker interface\\n with http request node","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://192.168.86.116:80/apps/api/103/devices/139/setLevel/52?access_token=11111111-1111-1111-111-5b74f038850f","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":2860,"wires":[["5e85427461b9099f"]]},{"id":"bd9646b4dcad07d6","type":"function","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"0026 Decide if it is\\n HSL or Brightness","func":"//0026 Decide if it is HSL or Brightness\n/* Very important note.\n    For the original creators need, the HSL output sends the hue and saturation that comes from the Hubitat, but ignores the Level.\n    This is because when you use the Hubitat color picker, then you will almost inevitably change the level.\n    The level however is also the brightness of the bulb.\n    So, I use the previously set brightness (level) so the color of the light changes, but the brightness doesn't change.\n    If you want to use the level sent from Hubitat when coming from the color picker, then you want to alter the line:\n        updateHistory(\"VirtualDevices\", devID, [\"Hue\", \"Saturation\", \"Level\"], [newHue, newSaturation, oldLevel])\n    to\n        updateHistory(\"VirtualDevices\", devID, [\"Hue\", \"Saturation\", \"Level\"], [newHue, newSaturation, newLevel])\n*/\n//standard debugging capability start\nlet showDebug = false;\nlet showDebug2 = false;\nlet showDebug3 = false;\nshowDebug ? node.warn(\"DesiredDebugOutput\") : null;\n//standard debugging capability end\n\nshowDebug2 ? node.warn(\"-------------\") : null;\nlet devID = msg.payload.id;                                                 //get hubitat device number which is the key within the global variable\nlet pastValues = RED.util.cloneMessage(global.get(\"VirtualDevices\") || {}); //get a new object (not reference) that is either empty or global variable\nif (!(pastValues.hasOwnProperty(devID))) {                                  //check if the device is already in the global variable\n    let newMeasurement =                                                    //placeholder structure if the device needs to be added to the global variable\n    {\n        \"Latest\": {\n            \"Value\": null,\n            \"Time\": null\n        },\n        \"Last\": {\n            \"Value\": null,\n            \"Time\": null\n        },\n        \"Last Different\": {\n            \"Value\": null,\n            \"Time\": null\n        }\n    };\n    let nullObject = {                                                      //creates the object with the device id as the key and the empty entries object as the value\n        [devID]: {\n            \"Hue\": RED.util.cloneMessage(global.get(newMeasurement)),       //IMPORTANT NOTE: \n            //When I originally did this, I did not clone the object. As a result when it was\n            //saved to the global varible, the Hue, Saturation and Level keys all had the same value because\n            //it was a shared reference to the same object.\n            //This made debugging the flow very hard because I kept thinking the code below was updating all\n            //3 for some bug in the update code when it was 1 object being displayed under 3 different keys\n            //Lesson learned for the creator who isn't as smart about Javascript as he thought he was\n            \"Saturation\": RED.util.cloneMessage(global.get(newMeasurement)),\n            \"Level\": RED.util.cloneMessage(global.get(newMeasurement))\n        }\n    };\n    Object.assign(pastValues, nullObject);                                  //adds the formated but empty entry to the global variable\n    global.set(\"VirtualDevices\", pastValues);                               //writes the global variable\n    showDebug ? node.warn(\"New Blank put in place because global either doesn't exist or doesn't have the device yet\") : null;\n}\nshowDebug ? node.warn(msg.payload) : null;\nlet oldHue = pastValues[devID].Hue.Latest.Value                             //get the previous hue value saved in the global\nlet oldSaturation = pastValues[devID].Saturation.Latest.Value               //get the previous saturation value saved in the global\nlet oldLevel = pastValues[devID].Level.Latest.Value                         //get the previous level value saved in the global\nlet newHue = msg.payload.attributes[6].currentValue                         //get the new hue value saved in the global\nlet newSaturation = msg.payload.attributes[2].currentValue                  //get the new saturation value saved in the global\nlet newLevel = msg.payload.attributes[9].currentValue                       //get the new level value saved in the global\nlet newMsg = {}\nshowDebug3 ? node.warn(oldHue) : null;\nshowDebug3 ? node.warn(newHue) : null;\nshowDebug3 ? node.warn(oldSaturation) : null;\nshowDebug3 ? node.warn(newSaturation) : null;\nif ((oldHue == newHue) && (oldSaturation == newSaturation)) {               //go into the then section if neither hue nor saturation changed (i.e. it is really a brightness change)\n    if (!(pastValues[devID].Level.Latest == newLevel)) {   //check if this is a new brightness value\n        newMsg.Brightness = newLevel;                                       //put the level into the msg object that will be returned\n        updateHistory(\"VirtualDevices\", devID, [\"Level\"], [newLevel]);      //call the common function to upadate the global variable with the new level\n        return [newMsg, null];                                              //return the msg on output 1 for use as a brightness change\n    };\n} else {\n    newMsg.HSL = [newHue, newSaturation, oldLevel];                         //build the HSL output from the new hue, new saturation and the pre-existing level\n    //See important note on why the pre-existing level\n    updateHistory(\"VirtualDevices\",                                         //call the common function to upadate the global variable with the new hue,saturation, and level triplet\n        devID,\n        [\"Hue\", \"Saturation\", \"Level\"],\n        [newHue, newSaturation, oldLevel]);\n    return [null, newMsg];                                                  //return the msg on output 2 for use as a HSL color value\n};\nreturn;\n\n/**\n* @param {string} deviceType\n* @param {string} deviceKey\n* @param {array} valueKeys\n* @param {array} updates\n*/\nfunction updateHistory(deviceType, deviceKey, valueKeys, updates) {\n    showDebug ? node.warn(valueKeys) : null;\n    showDebug ? node.warn(updates) : null;\n    if (valueKeys.length != updates.length) { return };                     //do not process if the number of keys and values don't match\n    let pastValues = RED.util.cloneMessage(global.get(deviceType) || {});   //get a new object (not reference) that is either empty or global variable\n    let newMeasurement =                                                    //placeholder structure if the device needs to be added to the global variable\n    {\n        \"Latest\": {\n            \"Value\": null,\n            \"Time\": null\n        },\n        \"Last\": {\n            \"Value\": null,\n            \"Time\": null\n        },\n        \"Last Different\": {\n            \"Value\": null,\n            \"Time\": null\n        }\n    };\n    var d = new Date();                                                     //get a date to be used to label when the updated values where recorded\n    let measurement;                                                        //create variables to be used inside of the for loop\n    let value;\n    for (let index = 0; index < updates.length; index++) {                  //loop through the keys and values performing the same logic on each one\n        measurement = valueKeys[index]                                      //store the key for this time through the look into a variable for easier reference\n        value = updates[index]\n        showDebug ? node.warn(measurement + \" \" + value) : null;\n        if (pastValues[deviceKey].hasOwnProperty(measurement)) {            //check if the measurement is already a key in the object attached to the device Id\n            showDebug ? node.warn(\"if (pastValues[deviceKey].hasOwnProperty(measurement)) {  The measurement exists\") : null;\n            pastValues[deviceKey][measurement].Last = RED.util.cloneMessage(pastValues[deviceKey][measurement].Latest); //create a copy of the previous entry and put into the Last part of the object\n            pastValues[deviceKey][measurement].Latest.Time = d;                                                         //Store the date for the latest time entry in the global variable\n            if (!(pastValues[deviceKey][measurement].Latest.Value == value)) {                                          //check if the value is different from the last update\n                showDebug2 ? node.warn(\"new value for the \" + measurement) : null;\n                showDebug2 ? node.warn(deviceKey) : null;\n                showDebug3 ? node.warn(measurement) : null;\n                showDebug3 ? node.warn(\"~124     before the update of Latest value\") : null;\n                showDebug3 ? node.warn(pastValues[deviceKey][measurement]) : null;\n                pastValues[deviceKey][measurement][\"Last Different\"] =                                                  //since they did no match, create a copy of the previous entry and put into the Last Different part of the object\n                    RED.util.cloneMessage(pastValues[deviceKey][measurement].Latest);\n                showDebug2 ? node.warn(\"~128 \" + measurement + \" \" + value) : null;\n                showDebug2 ? node.warn(pastValues[deviceKey][measurement]) : null;\n                pastValues[deviceKey][measurement].Latest.Value = value;                                                //add the passed value to the Latest part of the object\n                showDebug2 ? node.warn(pastValues[deviceKey][measurement]) : null;\n            }\n        } else {                                                                                                        //process for when the passed measurement doesn't yet exist\n            showDebug2 ? node.warn(\"if (pastValues[deviceKey].hasOwnProperty(measurement)) {} Else\") : null;\n            pastValues[deviceKey][measurement] = RED.util.cloneMessage(newMeasurement);                                 //add the measurement as a key with the empty structure\n            pastValues[deviceKey][measurement].Latest.Time = d;                                                         //add the date to the Latest part of the object\n            pastValues[deviceKey][measurement].Latest.Value = value;                                                    //add the passed value to the Latest part of the object\n        };\n        showDebug2 ? node.warn(\"About to Loop\") : null;\n        showDebug2 ? node.warn(pastValues) : null;\n    };                                                                                                                  //end of the for loop\n    showDebug2 ? node.warn(\"~141 about to show the object to be added to\") : null;\n    showDebug2 ? node.warn(pastValues) : null;\n    global.set(\"VirtualDevices\", pastValues);                                                                           //write the updates to the global variable\n    return\n};","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":930,"y":2860,"wires":[["097a5670fbf70301"],["a195a6014dce2014"]],"info":"# Very important note.\r\n    For the original creators need, the HSL output sends the hue and saturation that comes from the Hubitat, but ignores the Level.\r\n    This is because when you use the Hubitat color picker, then you will almost inevitably change the level.\r\n    The level however is also the brightness of the bulb.\r\n    So, I use the previously set brightness (level) so the color of the light changes, but the brightness doesn't change.\r\n    If you want to use the level sent from Hubitat when coming from the color picker, then you want to alter the line:\r\n        updateHistory(\"VirtualDevices\", devID, [\"Hue\", \"Saturation\", \"Level\"], [newHue, newSaturation, oldLevel])    \r\n    to\r\n        updateHistory(\"VirtualDevices\", devID, [\"Hue\", \"Saturation\", \"Level\"], [newHue, newSaturation, newLevel])\r\n\r\n"},{"id":"a195a6014dce2014","type":"debug","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"HSL","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1110,"y":2900,"wires":[]},{"id":"097a5670fbf70301","type":"debug","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"B","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1110,"y":2820,"wires":[]},{"id":"d26243490fe07b6c","type":"group","z":"5c9e82eadde3cbc4","g":"d4f615bad7f8c4d9","name":"Incoming inputs from Hubitat","style":{"fill":"#ffffff","label":true,"color":"#000000"},"nodes":["435e6f24f59ba2f0","7df790dc571302b3","9b375b45b3e33ace"],"x":184,"y":2759,"w":192,"h":202},{"id":"435e6f24f59ba2f0","type":"hubitat device","z":"5c9e82eadde3cbc4","g":"d26243490fe07b6c","deviceLabel":"Mike's High Desk Lamp","name":"Hue","server":"4452f29.dcf160c","deviceId":"139","attribute":"hue","sendEvent":true,"x":300,"y":2800,"wires":[["386250a07242f362"]]},{"id":"7df790dc571302b3","type":"hubitat device","z":"5c9e82eadde3cbc4","g":"d26243490fe07b6c","deviceLabel":"Mike's High Desk Lamp","name":"Saturation","server":"4452f29.dcf160c","deviceId":"139","attribute":"saturation","sendEvent":true,"x":280,"y":2860,"wires":[["386250a07242f362"]]},{"id":"9b375b45b3e33ace","type":"hubitat device","z":"5c9e82eadde3cbc4","g":"d26243490fe07b6c","deviceLabel":"Mike's High Desk Lamp","name":"Level","server":"4452f29.dcf160c","deviceId":"139","attribute":"level","sendEvent":true,"x":300,"y":2920,"wires":[["386250a07242f362"]]},{"id":"4452f29.dcf160c","type":"hubitat config","name":"116 to NTPI1F","usetls":false,"host":"192.168.86.116","port":"80","appId":"103","nodeRedServer":"http://ntpi1f:1880","webhookPath":"/hubitat/116toNTPI1F","autoRefresh":true,"useWebsocket":false,"colorEnabled":true,"color":"#a942e0"}]

To use this, change the Hubitat nodes to pull from your hub. Use the Hubitat interface to change the color (color picker, hue or saturation). You will see the format of the output on the HSL debug node. msg.HSL is an array with following format: [hue, saturation, level] Then change just the level and see the format of the output on the B debug node. You will see the format of the output is msg.Brightness = level.

It is worth knowing that it stores info in a global variable name VirtualDevices. That name can be changed by editing the function node. Also, if you want the output under a different key (say msg.payload), that alteration can be made in function node as well.

I hope this helps anyone else that runs into the challenge I had. If you want more detail it is at: https://community.hubitat.com/t/node-red-nodes-for-hubitat/34386/4988

Do also see that the access token to your maker instance (along with the Hubitat address) are in the HTTP node. You should alter them to match your setup if you want to get it to work. Sorry I missed that on first posting.

Very important note.
If you ignore this portion of the post, the output may look like it has an error for the Level portion of the HSL array.

For my need, the HSL output sends the hue and saturation that comes from the Hubitat, but ignores the Level. This is because when you use the Hubitat color picker, then you will almost inevitably change the level. The level, however, is also the brightness of the bulb.

So, I use the previously set brightness (level) so the color of the light changes, but the brightness doesn't change. If you want to use the level sent from Hubitat when coming from the color picker, then you want to alter the line in the function node:

updateHistory("VirtualDevices", devID, ["Hue", "Saturation", "Level"], [newHue, newSaturation, oldLevel])
to
updateHistory("VirtualDevices", devID, ["Hue", "Saturation", "Level"], [newHue, newSaturation, newLevel])