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, 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": "",
        "category": "",
        "in": [
            {
                "x": 60,
                "y": 100,
                "wires": [
                    {
                        "id": "ecc481b9.7d3d1"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 900,
                "y": 160,
                "wires": [
                    {
                        "id": "28dba094.bdd1",
                        "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": "d28ae4c6.ecf228",
        "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    // 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,
        "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,
        "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}\n\nmsg.itMatches = itMatches;\n\nif (itMatches) {\n    return [msg, null];    \n} else {\n    return [null, msg];\n}\n",
        "outputs": 2,
        "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": [
            []
        ]
    },
    {
        "id": "9bde0df0.121cc",
        "type": "hubitat command",
        "z": "d7dbc436.02cd78",
        "deviceLabel": "",
        "name": "",
        "server": "d28ae4c6.ecf228",
        "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": [
            []
        ]
    }
]
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.

Download the Hubitat app