An OR latch for your Node-RED projects
Do one thing when a condition becomes true. Do another thing when all conditions become false.
Have you ever encountered the need to do a thing when one of your inputs "goes high", and then do something else when all of your inputs have "gone low"?
If so, here is a generic solution for Node-RED. The following program is a subflow you can copy and import to your Node-RED workbench:
[
{
"id": "939193159b60ae0d",
"type": "subflow",
"name": "OR latch",
"info": "Sends a \"signal high\" message on output #1 the first time any\nincoming message's `condition_key` evaluates to true, and sends\na \"signal low\" message the last time that all of the incoming\nmessages have evaluated to false.\n\nMessages are classified by their `classifier_key` (default `topic`)\nand their true/false value is evaluated from their `condition_key`\n(default `payload`). The classifier key and condition key are\nconfigurable through node properties.\n\nFor the following message sequence, this is what the latch does:\n\n1. `msg.topic: \"a\", msg.payload: true}` -> emit output #1\n2. `msg.topic: \"b\", msg.payload: true}` -> silence\n3. `msg.topic: \"a\", msg.payload: false}` -> silence\n4. `msg.topic: \"b\", msg.payload: false}` -> emit output #2\n5. `msg.topic: \"b\", msg.payload: false}` -> silence\n\n(1) emits output because the latch went from no classified message's\n`condition_key` being true to at least one classified message\nbeing true. (4) emits output because all of the known classified\nmessages have changed to evaluating to false.\n\nMessages without a classifier or whose value is undefined are\nignored. With default classifier and condition keys, the following\nmessages exemplify what would get ignored:\n\n* `{topic2: \"a\", payload: true}` (classifier absent)\n* `{topic: \"a\", mode: true}` (condition absent)\n\nIf a message with an attribute named after the `reset_key` variable\nis received, all memory of past messages is cleared. No message will\nbe forwarded in that case.",
"category": "",
"in": [
{
"x": 60,
"y": 100,
"wires": [
{
"id": "9e698e07e2c75c21"
}
]
}
],
"out": [
{
"x": 450,
"y": 60,
"wires": [
{
"id": "9e698e07e2c75c21",
"port": 0
}
]
},
{
"x": 430,
"y": 140,
"wires": [
{
"id": "9e698e07e2c75c21",
"port": 1
}
]
}
],
"env": [
{
"name": "classifier_key",
"type": "str",
"value": "topic"
},
{
"name": "condition_key",
"type": "str",
"value": "payload"
},
{
"name": "reset_key",
"type": "str",
"value": "reset"
}
],
"meta": {},
"color": "#DDAA99",
"outputLabels": [
"at least one",
"none"
],
"status": {
"x": 400,
"y": 220,
"wires": [
{
"id": "076a4807936cbb67",
"port": 0
}
]
}
},
{
"id": "9e698e07e2c75c21",
"type": "function",
"z": "939193159b60ae0d",
"name": "State latch",
"func": "var classifier_key = env.get(\"classifier_key\");\nvar condition_key = env.get(\"condition_key\");\nvar reset_key = env.get(\"reset_key\");\n\nif (msg[reset_key] !== undefined) {\n context.set(\"objects\", {});\n node.status(\"Latch reset\");\n return;\n}\n\nvar objects = context.get(\"objects\") || {};\ncontext.set(\"objects\", objects);\n\nvar objects_on_before = Object.fromEntries(\n Object.entries(objects).filter(\n ([k, v]) => v == true\n )\n);\n\nvar classifier = msg[classifier_key];\nif (classifier === null || classifier === undefined) {\n return\n}\nvar value = msg[condition_key];\nif (value === null || value === undefined) {\n return\n}\nvalue = Boolean(value);\nobjects[classifier] = value;\n\nvar objects_on_after = Object.fromEntries(\n Object.entries(objects).filter(\n ([k, v]) => v == true\n )\n);\n\nvar status = \"\";\n\nif (Object.keys(objects_on_after).length) {\n status = \"active: \" + Object.keys(objects_on_after).join(\", \");\n} else {\n status = \"none active\";\n}\n\nif (Object.keys(objects_on_before).length == 0 && Object.keys(objects_on_after).length > 0) {\n node.status({\"fill\": \"green\", \"text\": status});\n return [msg, null];\n} else if (Object.keys(objects_on_before).length > 0 && Object.keys(objects_on_after).length == 0) {\n node.status({ \"fill\": \"green\", \"text\": status});\n return [null, msg];\n}\n\nnode.status({\"fill\": \"yellow\", \"text\": status})",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 210,
"y": 100,
"wires": [
[],
[]
],
"outputLabels": [
"one on",
"all off"
]
},
{
"id": "076a4807936cbb67",
"type": "status",
"z": "939193159b60ae0d",
"name": "",
"scope": null,
"x": 200,
"y": 220,
"wires": [
[]
]
}
]
This is how it looks like, internally:
The subflow has one input and two outputs. The input expects messages that will be classified according a to a key (default topic
), and which will be evaluated according to a value (default payload
). The logic is fairly simple:
- The first time a message is received whose value evaluates to
true
, the associated value is classified according to the classifier key, and the first output gets the message. - Then, all messages are blocked, until every message seen has been classified to evaluate to
false
. At this point, the message is sent through the second output.
How to use this latch in practical terms
To give a more concrete example: let's say you have two cars, represented as nodes in Node-RED, that emit their on/off state (true / false) in the message payload, and their names in the message topic. We'll do a demo flow using this subflow, with inject
nodes pretending to be said cars, all attached to this latch:
Here is what the flow depicted above would do:
- The first time you hop in your car and you turn the ignition key, the first output of the latch will receive a message.
- Your daughter now gets the second car, and starts it. No message is sent past the latch.
- You come back home, and now you park the car — then switch it off. No message is sent.
- Finally, your daughter arrives from her errand and parks the car. At this point, the second output of the latch gets a message.
In graphical sequence:
Implementation notes
- Messages without an attribute matching the classifier key or the condition key will simply be ignored.
- Any message with an attribute matching the reset key (by default,
reset
) will cause the latch to forget all known classified messages and values, essentially returning the latch to its "zero state". No reset message will be forwarded.