I am using Node-RED exclusively for home automation, specifically as an add-on to Home Assistant.
In this use case I am not so much dealing with complex contents of messages and their transformation, but rather with the routing of simple messages where each message signifies the triggering of a sensor, a switch, etc., often the routing depends on states of other switches, certain time windows or also the order in which certain messages arrived in a certain time window. For example If first the motion sensor in the driveway triggers, then soon after the sensor in the carport, I want different lights go on than when this happens in the opposite order.
For this I find myself making use of flow variables on almost every flow I create to manage that state.
And while working with flow variables I soon identified these sources of error:
- documentation (naming) of nodes not reflecting or (even worse) out of sync with what side effects it actually has
- hard to understand (or even see) the side effects when looking at the flow
Consider this:
A function node named "set time_a" and containing the code "flow.set('time_entrance', ...);". Obviously at one time in the past, in a hurry, I renamed the flow variable and forgot to update the node name. Or I update it in two places but it is also used in one more place I have totally forgotten.
So I thought by myself there must be some way to make this more transparent and less error prone. I came up with following solutions:
- dynamically parse the name of the flow variable (and other suff I normally would have wanted to hardcode) out of 'node.name', NEVER hardcode any names of flow variables in the code: Consider a function node is called "foo_state = maybe_arriving", it might contain the following code:
const name = node.name.split(' ')[0];
const value = node.name.split(' ')[2];
flow.set(name, value);
return msg;
-
confine any usage state to very few and easily identifiable nodes. Often one node is setting the state and then some time later a different message coming along through a different node is reading it, put both nodes next to each other and draw a group rectangle around it. Never access the state outside the group, instead after reading it attach it to the message, so it can be used in later nodes and switches, thereby keeping all of the flow outside the group rectangles pure.
-
do not manually assign flow variable names at all! Use the NR_GROUP_ID to generate a name for the variable inside the group, so you cannot accidentally interfere with it from outside the group, even if you wanted to. Encapsulate all flow state in groups. This has the added benefit that you can just copy paste the stateful group without needing to rename variables.
The store/recall group implements both of the above suggestions: group ID and parsing of node name:
[{"id":"5da5765352833db3","type":"group","z":"27ec49e7bab0eeaa","name":"Store / Recall","style":{"label":true},"nodes":["e164128c86ef4ffd","09c23ec1fd7d8ea6"],"x":294,"y":359,"w":172,"h":122},{"id":"e164128c86ef4ffd","type":"function","z":"27ec49e7bab0eeaa","g":"5da5765352833db3","name":"store","func":"const name = env.get('NR_GROUP_ID');\nflow.set(name, msg);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":400,"wires":[["1bae87243139671c"]]},{"id":"09c23ec1fd7d8ea6","type":"function","z":"27ec49e7bab0eeaa","g":"5da5765352833db3","name":"recall a","func":"const name = env.get('NR_GROUP_ID');\nconst recall_name = node.name.split(' ')[1];\nmsg[recall_name] = flow.get(name);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":440,"wires":[["12ca9370b832f4aa"]]}]
The "rate" node below then just uses msg.a
and msg.b
to do its calculations.
Another example for a reusable stateful group: the MonoFlop Switch that is going to be used in my new implementation of my outside motion sensor light control:
[{"id":"099d2de186307592","type":"inject","z":"3ec3298160bac448","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":140,"wires":[["f94abb65b10847fd"]]},{"id":"c62bc0d2c0d7c367","type":"debug","z":"3ec3298160bac448","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":200,"wires":[]},{"id":"3e1a547c0878192d","type":"inject","z":"3ec3298160bac448","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":220,"wires":[["767058d4b6a1b3ac"]]},{"id":"631dee1cc49cc6cd","type":"group","z":"3ec3298160bac448","name":"MonoFlop Switch","style":{"label":true},"nodes":["f94abb65b10847fd","767058d4b6a1b3ac","6afe9bdcca906fe2","267e1eba2bb3419d"],"x":334,"y":79,"w":312,"h":182},{"id":"f94abb65b10847fd","type":"trigger","z":"3ec3298160bac448","g":"631dee1cc49cc6cd","name":"","op1":"","op2":"","op1type":"pay","op2type":"payl","duration":"6","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":2,"x":420,"y":140,"wires":[["6afe9bdcca906fe2"],["267e1eba2bb3419d"]]},{"id":"767058d4b6a1b3ac","type":"switch","z":"3ec3298160bac448","g":"631dee1cc49cc6cd","name":"","property":"${NR_GROUP_ID}","propertyType":"flow","rules":[{"t":"true"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":410,"y":220,"wires":[["c62bc0d2c0d7c367"],[]]},{"id":"6afe9bdcca906fe2","type":"change","z":"3ec3298160bac448","g":"631dee1cc49cc6cd","name":"true","rules":[{"t":"set","p":"${NR_GROUP_ID}","pt":"flow","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":120,"wires":[[]]},{"id":"267e1eba2bb3419d","type":"change","z":"3ec3298160bac448","g":"631dee1cc49cc6cd","name":"false","rules":[{"t":"set","p":"${NR_GROUP_ID}","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":160,"wires":[[]]}]
I can copy paste this group multiple times and each instance will automatically have its own state encapsulated, impossible to interfere with each other.
Wish list:
- sub flows with multiple inputs
- group.get() and group.set()