Node-RED nodes for hubitat

I've had that happen too but I thought it was me overloading the maker api with too many requests..

About the empty dropdown, I don't know how it was working until now :confused:
I fix an issue this morning (I guess it's the same issue) with this commit

For the no working node, I don't have other suggestion than activate the debug mode for node-red and when it occurs, look at the log if there is something

2 Likes

I had that happen but it stopped when I turned off logging in the Maker API.

1 Like

It's back working. I reloaded webpage a couple of times and the devices came back in the list but then the flows for remotes didn't work for another 10 minutes or so. It's a mystery. I didn't have logging on in Maker API.

I did realize that thanks to getting the EcoSmart Remotes working locally thru ST last week, I had backups at almost all locations. If I couldn't use the remotes thru HE, the EcoSmart ones working thru ST and Node Red so i was able to fight off most of the WAF demotion points. I guess it's all about redundancy. :grin:

1 Like

@fblackburn

Thanks - am testing the fix later today. This has happened to me just once a while ago. Will the release with HSM nodes be out soon?

About HSM nodes, since I don't really use this application, I'm waiting the approval of @dan.t to know if the final version still solve the hsm workflow. Here's PR

1 Like

Ooh - I hope he's ok. We haven't seen @dan.t over here for a couple weeks either.

2 Likes

I am as good as anyone can be, thanks :wink:

Sorry for being MIA lately, just started a new job and during these crazy times it is sucking away my play time....

6 Likes

Are we still doing "phrasing"?
( Are We Still Doing Phrasing? ‘Archer’s 10 Best Best Recurring Jokes | Decider )

Side note... The raid card in my main server died last night. It is merely luck that I happened to move my Node-red over to my test rpi cluster yesterday morning.

Had to spin up a quick MQTT server and an haproxy container to redirect the old MQTT IP to the new server, but was back up in a few minutes.

I have a dozen or so other containers that are down, but having MQTT and node-red will tide me over until the new raid card gets here.

Yay containers!

.
If any of you peeps use any sort of JS extensions for your browser (like this one for Chrome or Brave), I've found this quick script to be super handy

Time savers rule!

// Control-S or Command-S saves current node 
document.addEventListener("keydown", function(e) {
  if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)  && e.keyCode == 83) {
    e.preventDefault();
    // See about finding the edit node tray's "Done" button
    let obj = document.querySelectorAll('.red-ui-tray.ui-draggable .red-ui-tray-toolbar button.primary');
	if (obj.length !== 0) {	// Is the edit node tray open?
		obj[0].click();
	}
	else {					// Look to click "Deploy" instead as long as it is not disabled 
		obj = document.querySelectorAll('#red-ui-header .red-ui-deploy-button:not(.disabled)');
		if (obj.length === 2) {
			obj[0].click();
		}
	}
  }
}, false);
1 Like

Just a heads up - if you look in the description for node-red-contrib-unsafe-function it says this:

nodeRedContribUnsafeFunctionAsyncSend, nodeRedContribUnsafeFunctionAsyncReceive - Set these RED settings variables in order to make the sending\receiving of messages asynchronous. The current behavior of node-red is that sending a message pauses until all the following nodes in the flow finish handling the sent message (unless they specifically do it asynchronously). For more information. ...

I think this may be outdated as Node-RED is now async by default. However just in case I added those variables to the "./.node-red/settings.js" file like this at the end between the last two curly braces...

...
...
...
    },


   // Unsafe Function Node Settings
   nodeRedContribUnsafeFunctionAsyncSend: true,
   nodeRedContribUnsafeFunctionAsyncReceive: true


}

Note: I added a comma after the second to the last curly brace (the one at the top). Again adding these vars may no longer be required... so ymmv.

I have received a lot of help along the way in my HE+NR journey and typically try to build an entire system and ecosystem rather than a just solve my immediate problem. As a result, I have learned a great deal in NR in support of my long-term vision. In particular, I am planning to use MQTT as a bridge between different systems (homebridge for my initial Homekit bluetooth and WIFI accessories, HE for all Z-wave and most Zigbee, and Zigbee2 MQTT for my IKEA accessories that do such weird things that HE doesn't support them). I am also building NR to InfluxDB data path to be able to chart, graph and analyze all of variables in my home automation.

With that in mind, I wanted to share a few function nodes that I had to build to get HE data into the right format for Influx insertion. I have tried to document the code inside the function nodes to be able to use for my own reference. It is possible that someone with more Javascript skills will find fault with how I did what I did, but I am happy enough with them to share here. I am considering at some point making them into real NR nodes, but I still have many other parts to work on.

Format for InfluxDB

  • This one takes the information from the output format of the Hubitat Device nodes and turns it into an array of arrays of measurements and tags that the InfluxDB consumes via the InfluxDB nodes (node-red-contrib-influxdb).
Pasteable Node

[{"id":"5ef9bc5b.f32634","type":"function","z":"ae851807.40c0e8","name":"Format for InfluxDB","func":"/* This function takes input from a hubitat device node and data stored in config node which holds details about the device\n * and creates an properly formated object to be sent to InfluxDB node which will in turn write to the InfluxDB\n * \n * \n */\n//gather the name of the device which had a reading that triggered the flow to be included as a tag (index) sent to InfluxDB\n//note: this is the hubitat device ID\nvar deviceID = msg.payload.deviceId;\n\n//gather the name of the measurement that was sent by the device to be included as a tag (index) for invalid measurements and as the fieldname for valid measurements sent to InfluxDB\nvar typeOfMeasurement = msg.payload.name;\n\n//gather the actual measurement value that was sent by the device to be included as the data for the field (when the measurement is valid) sent to InfluxDB\nvar theData = msg.payload.value;\n\n//initialize the measurements object\nvar measurementsObj = {};\n\n//initialize the tags (indices) object\nvar tagsObj = {};\n\n//create the object that will later be used to combine the measurementsObj and tagsObj\nvar combinedObj;\n\n//create the boolean that will be used to hold the evaluation of whether the measurement is valid or not\nvar validMeasurement;\n\n//create the booleans that will be used to hold whether the reason a measurement is invalid is because it is a non-standard type or\n// if the type of the measurement and the type passed from the device differ or\n// if the value (data) is null\nvar nonStandardType = false;\nvar measurementTypeMismatch = false;\nvar nullData = false;\n\n//create a boolean to keep track of if there are any tags (indices) that should be sent to the InfluxDB \n// it starts false but if any tag is valid then will become true. See below for what to send to InfluxDB based on valid measures and tags\nvar validTag = false;\n\n\n\nif ((typeof theData == msg.payload.dataType.toLowerCase()) || (msg.payload.dataType == 'ENUM') || (theData === null)) {\n if (typeof theData == 'number') {\n node.status({ fill: "red", shape: "ring", text: "matched number" });\n\n if (!theData.isNaN) {\n validMeasurement = true;\n }\n } else if (typeof theData == 'string') {\n node.status({ fill: "red", shape: "ring", text: "matched string" });\n if (theData.length >= 0) {\n validMeasurement = true;\n }\n } else if (msg.payload.dataType == 'ENUM') { //This is checking to make sure the value matches one of the values in the ENUM from the device \n node.status({ fill: "red", shape: "ring", text: msg.payload.dataType + " matched ENUM" });\n\n msg.payload.values.forEach(element => {\n if (flow.get(element) == theData) {\n validMeasurement = true;\n }\n })\n } else if (theData === null) {\n node.status({ fill: "red", shape: "ring", text: "matched null data" });\n nullData = true;\n } else {\n node.status({ fill: "red", shape: "ring", text: "matched nonstandardtype" });\n nonStandardType = true;\n }\n} else {\n node.status({ fill: "red", shape: "ring", text: "measurement type mismatch" });\n measurementTypeMismatch = true;\n}\n\n\n\n\n\n//scanning all of the tags from the config node and creating a tag for that item if the entry on the config node has info\ncommonTags = ["House", "Floor", "Room", "Location", "DeviceType", "Manufacturer", "AccessProtocol", "PhysicalVirtualType", "PhysicalDigitalRegular"]\ncommonTags.forEach(element => {\n if (flow.get(element).length > 0) {\n tagsObj[element] = flow.get(element)\n validTag = true\n }\n});\n\n//Based on whether or not the measurement is "valid" it either adds the device id to the tags or it goes to the else portion\nif (validMeasurement) {\n measurementsObj[typeOfMeasurement] = theData;\n if (deviceID.length > 0) {\n tagsObj["DeviceID"] = deviceID;\n validTag = true;\n }\n if (validTag) {\n combinedObj = [measurementsObj, tagsObj]\n } else {\n //skip tags if none would be valid\n combinedObj = [measurementsObj]\n }\n} else {\n\n //if the measeurement was not "valid", then it puts in a Generic measurement of true and tag it as either Non-Standard Type of Measurement Type Mismatch\n if (nonStandardType) {\n measurementsObj["Generic"] = true\n tagsObj["ReasonGeneric"] = "Non-Standard Type"\n tagsObj["Measure"] = typeOfMeasurement\n }\n if (deviceID.length > 0) {\n tagsObj["DeviceID"] = deviceID\n validTag = true\n }\n if (measurementTypeMismatch) {\n measurementsObj["Generic"] = true\n tagsObj["ReasonGeneric"] = "Measurement Type Mismatch"\n tagsObj["Measure"] = typeOfMeasurement\n }\n if (deviceID.length > 0) {\n tagsObj["DeviceID"] = deviceID\n validTag = true\n }\n combinedObj = [measurementsObj, tagsObj]\n}\n\n//put the object into the payload\nmsg.payload = combinedObj;\n\n//build the status to be shown on the status of the subnode\nmsg.status = typeOfMeasurement + ": " + theData;\n\n//send the created combinedObj downstream (to the InfluxDB node), but only if the measurement is not null\nif (!nullData) {\n return msg;\n};\n","outputs":1,"noerr":0,"x":890,"y":40,"wires":[["985ccc13.04932","190b5fc0.438e"]]}]

JSON Preprocessor

  • This one I built to handle the few cases where the data coming from the device node is in JSON. This splits that into a separate outputs as if each one was a separate event
  • It also deals with some of those labelled JSON are not actually JSON (a current example is the playlist from echo speaks, though I hear it is in the queue to be fixed) and with "valid" JSON like "bob" which is not a key:value pair.
Pasteable Node

[{"id":"e0cf241e.5c4588","type":"function","z":"ae851807.40c0e8","name":"JSON Preprocessor","func":"/*\n * Main function node code \n * \n /\n//Check if there is any data. If not, stop processing. At the time this was created, the trackData sent from a Hubitat Elevation device node was always null\nif (msg.payload.value === null) {\n return null;\n}\n//make downstream troubleshooting easier by removing the deprecated currentValue if it exists\ndelete msg.payload.currentValue;\n//send the msg to the recursive function\nSplitJSON(msg)\n//return nothing as any valid messages are send via node.send in the recursive function\nreturn;\n\n/\n *The recursive function that takes in a msg and if it is JSON, sends out multiple msgs each with the msg.dataType STRING, the JSON key concatenated\n * to the msg.name and the JSON value moved to the msg.value.\n * \n * This is a recursive function so if the JSON value is more JSON, then it will decompose it as well.\n /\nfunction SplitJSON(latestMsg) {\n //Only evaluate objects labelled as JSON Object\n if (latestMsg.payload.dataType == 'JSON_OBJECT') {\n //make sure the messages value is valid JSON and contains a colon (:)\n //The check for a colon is that a string like "example" is "valid" JSON, but this function assumes the JSON is made up of key:value pairs\n if ((IsValidJSONString(latestMsg.payload.value)) && (latestMsg.payload.value.includes(":"))) {\n //turn the message value inot an array of key:value pairs\n var valueJSON = JSON.parse(latestMsg.payload.value);\n //set aside the original name to become the leading part of the concatenation\n var name = msg.payload.name;\n // Loop through the arry of ke:value pairs\n Object.keys(valueJSON).forEach(function (key) {\n //concatenate the original name, a colon and the key from the pair\n latestMsg.payload.name = name + ":" + key;\n //move the value from the pair to payload.value\n latestMsg.payload.value = valueJSON[key];\n //recursively call this function in case the value is itself JSON\n SplitJSON(latestMsg)\n })\n } else {\n //Since the dataType was labelled as JSON_OBJECT even though it was not made up of key:value pairs, then changed the type to STRING and sent it along\n //This is encountered both if the initial input is mislabelled, but more often from the recursive call to this function\n latestMsg.payload.dataType = "STRING"\n node.send(latestMsg);\n }\n } else {\n //This node is only for JSON splitting so if the object type is something else, then it just passes along the input\n node.send(latestMsg);\n }\n}\n\n/\n * This function was found on the internet to check if an input is valid JSON\n * There seems to be a belief that there should be some more elegant method that trying parse and the catching the error,\n * but no one seems to have demonstrated what that more elegant way would be\n */\nfunction IsValidJSONString(str) {\n try {\n JSON.parse(str);\n } catch (e) {\n return false;\n }\n return true;\n}","outputs":1,"noerr":0,"x":680,"y":40,"wires":[["5ef9bc5b.f32634"]]}]

I have given a little thought to combining them into a single node, but as with all of NR, there seems no end to what can be refactored.

Finally, I also used a self assigned task to learn about NR. That task was to build a flow to monitor the connectivity of my internet connection using pings as well as monitoring if my Lennox thermostat stays on the WIFI network. Part of the flow involves recording how long an outage lasts. It is easy enough to get the duration in milliseconds, but I wanted to get that into a more human readable format. So just to share here is the function node that does this just in case anyone is interested:

Duration Translator

Pasteable Node

[{"id":"2ea97499.5a805c","type":"function","z":"76e3605b.0e6e7","name":"Translate Duration Function","func":"/* \n * Create Variables\n * \n /\n var milliseconds = msg.payload;\n var years, weeks, days, hours, minutes, seconds, millis;\n //creating 2 string versions of each of the durations that can be in the middle of a 1:01:01.001 duration \n //minstr is the always 2 character one\n //lminstr is the version that can be 1 or more characers that will be used when that part is the portion of the duration\n var minstr, secstr, millistr, lminstr, lsecstr, lmillistr;\n var whichcomponents, readableduration;\n\n/\n * Calculate the numbers for the subcomponents\n * \n /\n //by getting the remainder when dividing by 1000\n //it leaves just the fraction of milliseconds\n millis = milliseconds % 1000;\n //the floor function drops the decimal portion\n //returning the non fractional seconds\n seconds = Math.floor(milliseconds / 1000);\n //the floor function drops the decimal portion\n //returning the non fractional minutes\n minutes = Math.floor(seconds / 60);\n //Once the minutes have been calculated, then\n //the seconds is replaced with those seconds not\n //accounted for by the minutes\n seconds = seconds % 60;\n //The same sequence as used for minutes/seconds\n //then used to calulate minutes, hours, dayss, weeks, years\n\n hours = Math.floor(minutes / 60);\n minutes = minutes % 60;\n days = Math.floor(hours / 24);\n hours = hours % 24;\n weeks = Math.floor(days / 7)\n days = days % 7;\n years = Math.floor(days / 365.25)\n weeks = weeks % 52;\n\n/\n * Put the subcomponents into msg payloads for easy access by later nodes\n *\n /\n msg.years = years\n msg.weeks = weeks\n msg.days = days\n msg.hours = hours\n msg.minutes = minutes\n msg.seconds = seconds\n msg.millis = millis\n \n\n/\n * create the string verssion for the subcomponents\n /\n //milliseconds\n lmillistr = millis.toString();\n //put padding on the milliseconds so that 1 ms shows as .001 and not .1 when concatenated after a decimal\n millistr = millis.toString().padStart(3, "0");\n //seconds\n lsecstr = seconds.toString();\n //put padding on the seconds so that 1 s shows as :01.000 and not :1.000 when concatenated after a dividing :\n secstr = lsecstr.padStart(2, "0");\n //minutes\n lminstr = minutes.toString();\n //put padding on the minutes so that 1 m shows as :01:00.000 and not :1:00.000 when concatenated after a dividing :\n minstr = lminstr.padStart(2, "0");\n\n/\n * Build output based on largest significant populated unit\n * The general rule is to include in the output the largest unit (years, weeks, days, etc) followed by the next 2\n * smaller units if the happen to be populated\n */\n readableduration =""\n//years populated\n if (years !== 0) { \n if (days !== 0) {\n if (days === 1) {\n readableduration = " ".concat(days.toString(), " Day", readableduration)\n } else {\n readableduration = " ".concat(days.toString(), " Days", readableduration)\n }\n }\n if (weeks !== 0) {\n if (weeks === 1) {\n readableduration = " ".concat(weeks.toString(), " Week", readableduration)\n } else {\n readableduration = " ".concat(weeks.toString(), " Weeks", readableduration)\n }\n }\n if (years === 1) {\n readableduration = years.toString().concat(" Year", readableduration)\n } else {\n readableduration = years.toString().concat(" Years", readableduration)\n }\n//weeks populated\n } else if (weeks !== 0) {\n if (hours !== 0) {\n if (hours === 1) {\n readableduration = " ".concat(hours.toString(), " Hour", readableduration)\n } else {\n readableduration = " ".concat(hours.toString(), " Hours", readableduration)\n }\n }\n if (days !== 0) {\n if (weeks === 1) {\n readableduration = " ".concat(days.toString(), " Day", readableduration)\n } else {\n readableduration = " ".concat(days.toString(), " Days", readableduration)\n }\n }\n if (weeks === 1) {\n readableduration = weeks.toString().concat(" Week", readableduration)\n } else {\n readableduration = weeks.toString().concat(" Weeks", readableduration)\n }\n//days populated\n } else if (days !== 0) {\n if ((minutes !== 0) && (hours !== 0)) { //If both are non-zero then 1:01 format. If only 1 then either 2 hours or 1 minute\n if (minutes !== 0) {\n readableduration = ":".concat(minstr, readableduration)\n }\n if (hours !== 0) {\n readableduration = " ".concat(hours.toString(), readableduration)\n } else {\n readableduration = " 0".concat(readableduration)\n }\n } else {\n if (minutes === 1) {\n readableduration = " 1 Minute".concat(readableduration)\n } else if (minutes > 1) {\n readableduration = " ".concat(lminstr," Minutes", readableduration)\n } else if (hours === 1) {\n readableduration = " 1 Hour".concat(readableduration)\n } else {\n if (hours >1) {\n readableduration = " ".concat(hours.toString()," Hours", readableduration)\n }\n }\n }\n if (days === 1) {\n readableduration = days.toString().concat(" Day", readableduration)\n } else {\n readableduration = days.toString().concat(" Days", readableduration)\n }\n//hours populated\n } else if (hours !== 0) {\n if ((minutes !== 0) || (seconds !== 0)) { //End result h Hours or h:mm:ss\n readableduration = hours.toString().concat(":", minstr, ":", secstr, readableduration)\n } else {\n if (hours === 1) {\n readableduration = "1 Hour"\n } else {\n readableduration = hours.toString().concat(" Hours", readableduration)\n }\n }\n//minutes populated\n } else if (minutes !== 0) { //End result m Minutes or mm:ss.MMM\n if (seconds !== 0 || millis !== 0) { //End result h Hours or h:mm:ss\n readableduration = lminstr.concat(":", secstr, ".", millistr, readableduration)\n } else {\n if (minutes === 1) {\n readableduration = "1 Minute".concat( readableduration)\n } else {\n readableduration = lminstr.concat(" Minutes", readableduration)\n }\n }\n//seconds populated\n } else if (seconds !== 0) {\n if (millis !== 0) { //End result s Seconds or s.MMM\n readableduration = lsecstr.concat(".", millistr, " Seconds", readableduration)\n } else {\n if (seconds === 1) {\n readableduration = "1 Second".concat( readableduration)\n } else {\n readableduration = lsecstr.concat(" Seconds", readableduration)\n }\n }\n//milliseconds populated\n } else if (millis !== 0) {\n if (millis !== 1) {\n readableduration = lmillistr.concat(" Milliseconds", readableduration)\n } else {\n readableduration = "1 Millisecond".concat( readableduration)\n }\n } else {\n readableduration = "0"\n }\n msg.years = years\n msg.weeks = weeks\n msg.days = days\n msg.hours = hours\n msg.minutes = minutes\n msg.seconds = seconds\n msg.millis = millis\n msg.payload = readableduration\n return msg;","outputs":1,"noerr":0,"x":500,"y":1080,"wires":[["82c9edda.537d7"]]}]

5 Likes

Really appreciate these, however, this board doesnt like "code style" in "collapsed/hidden format", and your flows above are not importable.

Could you also show use case? What does it look like on a dashboard? Or wherever?

Does anyone have any thoughts on how to properly handle groups of lights using NR Nodes + HE?

Ideally you'd want a group that can turn the lights on/off (maybe color change) but also react if all the lights in the group were turned off or on individually.. Shifting the burden to HE is also a possibility but the group options like "Use group device to indicate.." tends to force all lights in the group on (or off) depending upon the selection if a single light is turned on (or off).

This is potentially trickier than it seems or maybe I'm just over thinking things.. I already have a complicated solution that mostly works (still hashing it out) but was wondering how other people are doing it.

90% of my bulbs are Hue bulbs, I ended up ditching all of Hubitat's relationship to my Hue devices (built in app, accessories) and use node-red-contrib-huemagic to have NR deal directly with them. Hue groups and scenes work great. (especially when using the iOS app iConnectHue to put some things together that the stock Hue app has trouble with)

I do have a few Sengled bulbs in the house, so those for now are just different branches in logic to deal with.

1 Like

After switching completely to NR, I haven't had a single slowdown on either of my HEs. To be fair, when I switched completely to NR, I also removed several stranded and ghost z-wave devices. But I suspect that most of my slowdowns could be attributed to database gridlock caused by overly complex rules with lots of conditionals.

2 Likes

Ditto, except I didn't have any ghosts that needed to be removed.

I've never been happier with the reliability and speed of my Hubitat hubs than I am now. i have no need to reboot them periodically anymore either - before I had to do it every few days.

2 Likes

I using this flow to use my own file to speak from Sono's speaker so that if the net is down it will still work local and this is what I have the true node is a boolean.

image

so I'm trying to use an change to change the payload to a boolean?

image

but cannot get it to work? I have change the payload several times added value etc still no love? any suggestions thanks. Also when I used the inject node it works but not when I open the sensor?

I'm not certain I understand what you're trying to do. Exactly what do you want the Sonos speaker to say. And when - only when the door opens, or only when the door closes, or both?