Node-Red Subflow: Hubitat Command Check. Check Status and Re-send Command if Needed

I have a node-red subflow I wanted to share, in case others had need for it.

DESCRIPTION
The subflow should be used on the output of a hubitat command node. It will check the end device and verify that the command worked/had its intended function. If not, the subflow will attempt to re-send the command up to 3x.

NOTE - the subflow only support specific commands/attributes! Since the subflow needs to know exactly what attribute to look at when a command is done, they all have to be hard coded in! See below for some additional information on adding your own command/attribute pairs.

Currently the subflow supports the commands:

  • on, off, setLevel, lock, unlock, arrived, departed, and the custom commands from the GE/Jasco Motion Dimmer/Switch devices - Manual, Occupancy, Vacancy, setDefaultDimmerLevel, and setLightTimeout.

PREREQUISITES

  1. Hubitat Nodes for Node-RED v1.7 or later
  2. Hubitat Nodes for Node-RED setup and fully working

IMPLEMENTATION

  1. IMPORT subflow code below to node-red
  2. Edit subflow
    • Edit the hubitat "device" node and change the "server" setting to the one configured for your hub. Click Done.
    • Edit the hubitat "command" node and change the "server" setting to the one configured for your hub. Click Done.
    • Deploy changed flows/subflows
  3. Use the subflow after a hubitat command node in a flow. Similar to this:
    image

FEATURES

  1. Subflow will show Failure, Success, or the current delay in the status area.
    • Failure - device status did not match what was expected from the command.
    • Success - device status did match what was expected from the command.
    • Delay... - The subflow is waiting for the command to finish before checking the status. After the delay it will proceed to Failure or Success
  2. The delay to wait for the command to finish is adjustable as a subflow property. By default it is 1000ms. Some devices (like GE dimmers that dim down to off, for example) sometimes need longer delays before the attributes update. It can be adjust on a node by node basis.
    image
  3. Subflow automatically adjusts the delay before checking if you pass it a setLevel command that has a duration specified in the arguments.

ADDING NEW COMMANDS/ATTRIBUTES:
To add support for new commands/attributes, the code in two function nodes in the subflow needs to be updated:
image image
In the 1st one, the new command needs to be added to the "switch" statement, defining which attribute needs to be checked.

In the "Compare" function node, the "switch" statement also needs to be updated, defining what actually needs to be compared between the command and attribute.

SUBFLOW CODE

CODE
[
    {
        "id": "d7dbc436.02cd78",
        "type": "subflow",
        "name": "Hubitat Command Check",
        "info": "### Inputs\r\nThis node must be placed immediately after a Hubitat Command node, as it uses values from the response of the command node.\r\n\r\n### Outputs\r\nThe output message is sanitized of Hubitat specific values, so that commands can be daisy chained with no fear of unintended interaction from the previous Hubitat commands.",
        "category": "",
        "in": [
            {
                "x": 60,
                "y": 100,
                "wires": [
                    {
                        "id": "ecc481b9.7d3d1"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 940,
                "y": 180,
                "wires": [
                    {
                        "id": "410f04c8906919e7",
                        "port": 0
                    }
                ]
            }
        ],
        "env": [
            {
                "name": "delayInMs",
                "type": "num",
                "value": "1000"
            }
        ],
        "meta": {},
        "color": "#DDAA99",
        "status": {
            "x": 1220,
            "y": 260,
            "wires": [
                {
                    "id": "28dba094.bdd1",
                    "port": 0
                },
                {
                    "id": "198288c1.729167",
                    "port": 0
                },
                {
                    "id": "31773f7a.abb72",
                    "port": 0
                },
                {
                    "id": "1f33a4d2.e91c0b",
                    "port": 0
                }
            ]
        }
    },
    {
        "id": "f9c75c9c.41425",
        "type": "do-return",
        "z": "d7dbc436.02cd78",
        "name": "",
        "mode": "done",
        "x": 1870,
        "y": 80,
        "wires": []
    },
    {
        "id": "2e641855.b3ccd8",
        "type": "hubitat device",
        "z": "d7dbc436.02cd78",
        "deviceLabel": "",
        "name": "",
        "server": "4f6ecd8f2eafd25d",
        "deviceId": "",
        "attribute": "",
        "sendEvent": false,
        "x": 1370,
        "y": 80,
        "wires": [
            [
                "3b6f2d01.7301e2"
            ]
        ]
    },
    {
        "id": "162dd42f.0b3e0c",
        "type": "function",
        "z": "d7dbc436.02cd78",
        "name": "Create device node params",
        "func": "msg.deviceId = msg.response.id;\n\nswitch(msg.requestCommand){\n    // switch\n    case \"on\":\n    case \"off\":\n        msg.attribute = \"switch\";\n        break;\n    // level\n    case \"setLevel\":\n        if (msg.requestArguments.split(',')[0] == 0) {\n            msg.attribute = \"switch\";\n        } else\n        {\n            msg.attribute = \"level\";\n        }\n        break;\n    // lock\n    case \"lock\":\n    case \"unlock\":\n        msg.attribute = \"lock\";\n        break;\n    // presence\n    case \"arrived\":\n    case \"departed\":\n        msg.attribute = \"presence\";\n        break;\n    // Jasco Motion Dimmer/Switch Specific Commands   \n    case \"Manual\":\n    case \"Occupancy\":\n    case \"Vacancy\":\n        msg.attribute = \"operatingMode\";\n        break;\n    case \"setDefaultDimmerLevel\":\n        msg.attribute = \"defaultDimmerLevel\";\n        break;\n    case \"setLightTimeout\":\n        msg.attribute = \"lightTimeout\";\n        break;\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1180,
        "y": 80,
        "wires": [
            [
                "2e641855.b3ccd8"
            ]
        ]
    },
    {
        "id": "e560d1ba.6ca2f",
        "type": "delay",
        "z": "d7dbc436.02cd78",
        "name": "",
        "pauseType": "delayv",
        "timeout": "500",
        "timeoutUnits": "milliseconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "outputs": 1,
        "x": 960,
        "y": 80,
        "wires": [
            [
                "162dd42f.0b3e0c"
            ]
        ]
    },
    {
        "id": "96b990cc.531ff",
        "type": "function",
        "z": "d7dbc436.02cd78",
        "name": "Calculate Delay",
        "func": "msg.delay = env.get(\"delayInMs\");\n\nif (msg.requestCommand==\"setLevel\") {\n   if (msg.requestArguments.split(',')[1]) {\n       msg.delay = msg.delay + (msg.requestArguments.split(',')[1] * 1000);\n   }\n}\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 780,
        "y": 80,
        "wires": [
            [
                "e560d1ba.6ca2f",
                "198288c1.729167"
            ]
        ]
    },
    {
        "id": "ecc481b9.7d3d1",
        "type": "switch",
        "z": "d7dbc436.02cd78",
        "name": "Has command?",
        "property": "requestCommand",
        "propertyType": "msg",
        "rules": [
            {
                "t": "nnull"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 200,
        "y": 100,
        "wires": [
            [
                "b0f7cbc8.06f378"
            ],
            [
                "31773f7a.abb72"
            ]
        ]
    },
    {
        "id": "b0f7cbc8.06f378",
        "type": "switch",
        "z": "d7dbc436.02cd78",
        "name": "Has response.id?",
        "property": "response.id",
        "propertyType": "msg",
        "rules": [
            {
                "t": "nnull"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 410,
        "y": 80,
        "wires": [
            [
                "de092a82.b50988"
            ],
            [
                "1f33a4d2.e91c0b"
            ]
        ]
    },
    {
        "id": "3b6f2d01.7301e2",
        "type": "function",
        "z": "d7dbc436.02cd78",
        "name": "Compare",
        "func": "var itMatches = false;\n\nswitch(msg.requestCommand){\n    // Any item you can compare command and attribute value directly\n    case \"on\":\n    case \"off\":\n    case \"Manual\":\n    case \"Vacancy\":\n        if (msg.requestCommand == msg.payload.value) {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n        }\n        break;\n    // Any item you can compare arguments and attribute value directly \n    case \"setDefaultDimmerLevel\":\n    case \"setLightTimeout\":\n        if (msg.requestArguments == msg.payload.value) {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n            msg.arguments = msg.requestArguments; \n        }\n        break;\n    //\n    // Special cases below here\n    //\n    // Level has to take into account multiple arguments like duration\n    case \"setLevel\":\n        if (msg.requestArguments.split(',')[0] == 0) {\n            if (msg.payload.value == \"off\") {\n                itMatches = true;\n            } else {\n                itMatches = false;\n                msg.command = msg.requestCommand;\n                msg.arguments = msg.requestArguments; \n            }\n        } else {\n            if (msg.payload.value == msg.requestArguments.split(',')[0]) {\n                itMatches = true;\n            } else {\n                itMatches = false;\n                msg.command = msg.requestCommand;\n                msg.arguments = msg.requestArguments; \n            }\n        }\n        break;\n    // Presence has to be massaged\n    case \"arrived\":\n        if (msg.payload.value == \"present\") {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n        }\n        break;\n    case \"departed\":\n        if (msg.payload.value == \"not present\") {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n        }\n        break;\n    // Jasco Motion dimmer/switch driver added the wording (default) to Occupancy. Oops.    \n    case \"Occupancy\":\n        if ((msg.requestCommand + \" (default)\") == msg.payload.value) {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n        }\n        break;\n    // Locks\n    case \"lock\":\n        if (msg.payload.value == \"locked\") {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n            msg.arguments = msg.requestArguments;\n        }\n        break;\n    case \"unlock\":\n        if (msg.payload.value == \"unlocked\") {\n            itMatches = true;\n        } else {\n            itMatches = false;\n            msg.command = msg.requestCommand;\n            msg.arguments = msg.requestArguments;\n        }\n        break;\n}\n\nmsg.itMatches = itMatches;\n\nif (itMatches) {\n    return [msg, null];    \n} else {\n    return [null, msg];\n}\n",
        "outputs": 2,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1520,
        "y": 80,
        "wires": [
            [
                "43a3afe6.34d54"
            ],
            [
                "9bde0df0.121cc"
            ]
        ]
    },
    {
        "id": "43a3afe6.34d54",
        "type": "do-return",
        "z": "d7dbc436.02cd78",
        "name": "",
        "mode": "abort",
        "x": 1870,
        "y": 20,
        "wires": []
    },
    {
        "id": "28dba094.bdd1",
        "type": "change",
        "z": "d7dbc436.02cd78",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "itMatches ? \"Success\" : \"Failure\"",
                "tot": "jsonata"
            },
            {
                "t": "delete",
                "p": "command",
                "pt": "msg"
            },
            {
                "t": "delete",
                "p": "arguments",
                "pt": "msg"
            },
            {
                "t": "delete",
                "p": "deviceId",
                "pt": "msg"
            },
            {
                "t": "delete",
                "p": "attribute",
                "pt": "msg"
            },
            {
                "t": "delete",
                "p": "itMatches",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 780,
        "y": 120,
        "wires": [
            [
                "410f04c8906919e7"
            ]
        ]
    },
    {
        "id": "9bde0df0.121cc",
        "type": "hubitat command",
        "z": "d7dbc436.02cd78",
        "deviceLabel": "",
        "name": "",
        "server": "4f6ecd8f2eafd25d",
        "deviceId": "",
        "command": "",
        "commandArgs": "",
        "x": 1740,
        "y": 80,
        "wires": [
            [
                "f9c75c9c.41425"
            ]
        ]
    },
    {
        "id": "de092a82.b50988",
        "type": "do",
        "z": "d7dbc436.02cd78",
        "name": "",
        "tasks": [
            "Task 1",
            "Task 2",
            "Task 3"
        ],
        "outputs": 4,
        "x": 610,
        "y": 80,
        "wires": [
            [
                "96b990cc.531ff"
            ],
            [
                "96b990cc.531ff"
            ],
            [
                "96b990cc.531ff"
            ],
            [
                "28dba094.bdd1"
            ]
        ]
    },
    {
        "id": "198288c1.729167",
        "type": "change",
        "z": "d7dbc436.02cd78",
        "name": "Update status",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "(delay / 1000) & \"s delay...\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1020,
        "y": 120,
        "wires": [
            []
        ]
    },
    {
        "id": "31773f7a.abb72",
        "type": "change",
        "z": "d7dbc436.02cd78",
        "name": "Update status",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "Failed command check",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 660,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "1f33a4d2.e91c0b",
        "type": "change",
        "z": "d7dbc436.02cd78",
        "name": "Update status",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "Failed response.id check",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 660,
        "y": 220,
        "wires": [
            []
        ]
    },
    {
        "id": "ddf24629e572aea4",
        "type": "comment",
        "z": "d7dbc436.02cd78",
        "name": "Documentation",
        "info": "DESCRIPTION\nThe subflow should be used on the output of a hubitat command node. It will check the end device and verify that the command worked/had its intended function. If not, the subflow will attempt to re-send the command up to 3x.\n\nNOTE - the subflow only support specific commands/attributes! Since the subflow needs to know exactly what attribute to look at when a command is done, they all have to be hard coded in! See below for some additional information on adding your own command/attribute pairs.\n\nCurrently the subflow supports the commands:\non, off, setLevel, arrived, departed, lock, unlock, and the custom commands from the GE/Jasco Motion Dimmer/Switch devices - Manual, Occupancy, Vacancy, setDefaultDimmerLevel, and setLightTimeout.\n\n\nFEATURES\nSubflow will show Failure, Success, or the current delay in the status area.\nFailure - device status did not match what was expected from the command.\nSuccess - device status did match what was expected from the command.\nDelay... - The subflow is waiting for the command to finish before checking the status. After the delay it will proceed to Failure or Success\n\nThe delay to wait for the command to finish is adjustable as a subflow property. By default it is 1000ms. Some devices (like GE dimmers that dim down to off, for example) sometimes need longer delays before the attributes update. It can be adjust on a node by node basis.\n\nThe subflow automatically adjusts the delay before checking if you pass it a setLevel command that has a duration specified in the arguments.\n\nADDING NEW COMMANDS/ATTRIBUTES:\nTo add support for new commands/attributes, the code in two function nodes in the subflow needs to be updated: \"Create device node params\" and \"Compare\"\n\nIn the \"Create device node params\" function node, the new command needs to be added to the switch statement, defining which attribute needs to be checked.\n\nIn the \"Compare\" function node, the switch statement also needs to be updated defining what actually needs to be compared between the command and attribute.",
        "x": 420,
        "y": 320,
        "wires": []
    },
    {
        "id": "410f04c8906919e7",
        "type": "delay",
        "z": "d7dbc436.02cd78",
        "name": "",
        "pauseType": "delay",
        "timeout": "50",
        "timeoutUnits": "milliseconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 830,
        "y": 180,
        "wires": [
            []
        ]
    }
]
11 Likes

That you for this. I added this to the water shutoff valves, which gives us peace of mind. Now I wish to add this to the door lock. I added
//lock
case "locked":
case "unlocked":
msg.attribute = "lock";
break
to the node parameters at the top and
case "locked":
case "unlocked":
to the compare node. I get a failure output, so I am obviously doing something wrong.

Directions?

How long does it take before the lock/,unlocked status event comes in on the device after locking/unlocking? May need to increase the delay time before checking on the subflow properties (not in the code).

I had the timing up to 2000. I see the lock engaging, and when it stops, the timing stats. I just tried 3000 ms. Here is the failure

I'll take a look. I don't have any schlage locks, just kwikset, but I'll see if mine does the same thing or not.

Thanks!

What about using the custom app Reliable Locks vs doing this in nodered? Seems on-device functionality would be a must for something like locks.

Is the command "locked" and "unlocked" (what you show in your code changes) or "lock" and "unlock"?

On my kwikset the commands are "lock" and "unlock", so thought I would ask.

Adding this to the subflow, plus a delay of 3000, worked perfectly on my kwikset locks:
1st function node:

    // Locks
    case "lock":
    case "unlock":
        msg.attribute = "lock";
        break;

Second function node:

    // Locks
    case "lock":
        if (msg.payload.value == "locked") {
            itMatches = true;
        } else {
            itMatches = false;
            msg.command = msg.requestCommand;
        }
        break;
    case "unlock":
        if (msg.payload.value == "unlocked") {
            itMatches = true;
        } else {
            itMatches = false;
            msg.command = msg.requestCommand;
        }
        break;

The problem is if the lock/unlock command comes from node-red, you still want some way to make 100% sure that the lock/unlock command made it from node-red to Hubitat. If it didn't Reliable Locks would never trigger in the 1st place.

Could always do BOTH though... The command check on the node-red side to make sure the command got to hubitat, and Reliable Locks on the hubitat side to ensure the lock operated.

1 Like

Thank you. I did not have the second function set properly. I assumed I could just add the "lock" and "unlock" in with the switch parameters. This with give Mrs. Mrmike peace of mind at night.

1 Like

Well thanks for asking about locks. I had meant to add that before publishing this in the 1st place, but forgot.

I'm looking to use this for setting the LED color bar on some Inovelli Red Series switches. Do I need a separate instance of this subflow for each device or does it pick up the device from the message coming from the HE device?

Not sure what you mean by separate instance of the subflow.

1 subflow, and a subflow node deployed wherever you want the command checking done. So 1 for each device/command to want to check.

That said, I don't think the code posted above supports color commands so it would need to be added to the subflow as well.

Yeah, I can add the color commands (assuming I can get them working in the first place). What I meant is there's a device node in the subflow... How does it get set to the correct device?

You shouldn't need to modify anything else in the subflow.

it gets its deviceid # from the incoming msg from the upstream command node you should have right before the command check node.

Other than adding new command capabilities in the 2 function nodes, the subflow node should just be deployable as-is.

I do have to change the delay on the deployed subflow node properties depending on how fast the device updates status in Hubitat though.

Ah, gotcha. Another question... the setColor() command updates 3 attributes. Does this just pull one attribute to compare, or can it compare any number of attributes?

Per MakerAPI instance used in NR, correct? Because that’s how I have it. Is there a way to eliminate that?

Just 1. It would take major work to do multiple attributes.

Correct, thanks for the clarifier!

1 Like

Okay, gotcha. I really only care about the "hue" attribute anyway. I think I'm gonna have to do some work to get it to parse the command params anyway since they're getting passed as a JSON string.

Yeah, you might just need to write a new checker for that. this one has a lot of baggage to make it "generic" that you wouldn't need for something specific like color checking.

1 Like