Submission for examination. Subflow button control

This is a subflow I wrote for use with button nodes.

It has undergone a bit or recent work.

Probably not perfect, but I did some testing on it and it NOW seems to work better than it did.

Scenario

You have a button (GUI) and want to prevent it being accidentally bumped.
So it requires a double press to get the output to toggle.
It supports single output too.
I'll get to that shortly.

Example flow / subflow.
NO FOREIGN nodes used.

[{"id":"ce274e69.c6f778","type":"subflow","name":"Button toggle with enable","info":"Update 2024 06 08\n======\n\nEdited so you can set the start mode.\nA/B\nHUGE rewrite to get code working correctly.\n\nsend \"RESET\" as `msg.payload` and the `startOn`\nsetting will be reloaded.\nsend \"RELOAD\" as `msg.payload` and the state of\nthe node will be resent to the `button` node.\n(NO output sent)\n\nUpdate 2023 09 03\n------\n\nNow fixed.\n`trigger` node not set to accept `msg.delay`\n\n\nUpdate 2022 02 21\n------\n\nTo initialise the node send a message of `Z` into the input.\n\nIf you want a specific ouptut:\n`A` sets the mode to what the `a` named messages indicate.\n\n`B` sets the mode to what the `b` named messages indicate.\n\nThe output from the `button` node must be `X` to toggle the state, and the button must NOT pass the input onto the output!\n\nYou MUST set the `environment` variables listed below or it won't work.\n(Example)\n```\nmode: (0/1) (see below)\nStartOn:  either A or B (the two conditions)\ndisabledColour: brown\ndisabledTXT:  something\ncolourA: lime\ncolourB: green\ntxtA: Log\ntxtB: Stop\ntxtclrA: black\ntxtclrB: black\npayloadA: GO\npayloadB: STOP\ntopicSET: CONTROL\ndelay: 800\n```\n```\nmode - toggle mode or single mode (0/1).\nThis means in toggle mode you get 2 output messages.\nSingle mode you only get one output message.\n```\n```\ndelay - (optional)  millisecond delay for time button is active.\n```\n```\ndisabledColour - the colour the button is when disabled.\n```\n```\ndisabledTXT - This is used when the `mode` is set to `1`.  The text is displayed by default.\n```\n```\ncolourA - the background colour if condition `A` is selected.  (Temporary)\n```\n```\ncolourB - the background colour if condition `B` is selected.  (Temporary)\n```\n```\ntxtA - The text to display if condition `A` is selected.\n```\n```\ntxtB - The text to display if condition `B` is selected.\n```\n```\ntxtclrA - the colour of the text if condition `A` is selected.\n```\n```\ntxtclrB - the colour of the text if condtion `B` is selected.\n```\n```\npayloadA - the text sent to the output if condtion `A` is selected.\n```\n```\npayloadB - the text sent to the output if condition `B` is selected.\n```\n```\nThese payloads can then be used to set context values or used to control (external node) `gate`\n```\n```\ntopicSET - the message topic which is sent to the `gate`.\n(this is needed if/when used to control the afore mentioned `gate` nodes.)\n```\n\nThe *temporary* meaning is that the button will show that colour for a few seconds\nthen it will turn to the `disabled` colour.\n\nThe active time is `3` seconds (adjustable via the `delay` value) to press/toggle the button once initially pressed.\nNow: if you keep pressing the button, it will *wait* until you finish pressing it.\n\nThe background colours (and text colours) apply to the `button` node (or other) that is pressed.\nThat node must be configured to allow inputs of `{{msg.txt}}`, `{{msg.colour}}` and `{{mdg.fontclr}}`.\n\n****\nFoot note:\n`msg.payload = \"0/1\"` `msg.toic = \"D\"`\nToggles debug mode.","category":"","in":[{"x":190,"y":260,"wires":[{"id":"c43c865d57f36958"},{"id":"e8e0b354f6fd8938"}]}],"out":[{"x":930,"y":220,"wires":[{"id":"c43c865d57f36958","port":0}]},{"x":950,"y":300,"wires":[{"id":"c43c865d57f36958","port":1}]}],"env":[{"name":"delay","type":"num","value":"","ui":{"type":"input","opts":{"types":["num"]}}},{"name":"StartOn","type":"str","value":"","ui":{"label":{"en-US":"StartCond"},"type":"input","opts":{"types":["str"]}}},{"name":"mode","type":"num","value":""},{"name":"disabledColour","type":"str","value":""},{"name":"disabledTXT","type":"str","value":""},{"name":"colourA","type":"str","value":""},{"name":"colourB","type":"str","value":""},{"name":"txtA","type":"str","value":""},{"name":"txtB","type":"str","value":""},{"name":"txtclrA","type":"str","value":""},{"name":"txtclrB","type":"str","value":""},{"name":"payloadA","type":"str","value":""},{"name":"payloadB","type":"str","value":""},{"name":"topicSET","type":"str","value":""}],"meta":{},"color":"#3FADB5","outputLabels":["To the `GATE` node","To the `BUTTON` node"],"icon":"node-red-dashboard/ui_switch.png","status":{"x":860,"y":380,"wires":[{"id":"4d8b7aa6.6af80c","port":0}]}},{"id":"4d8b7aa6.6af80c","type":"status","z":"ce274e69.c6f778","name":"","scope":["c43c865d57f36958"],"x":590,"y":380,"wires":[[]]},{"id":"c43c865d57f36958","type":"function","z":"ce274e69.c6f778","name":"Push Button","func":"//  ----    Assign variable names to ENV variables.\n//  all future use of variables is by their names below.\n//  These are the userdefined variables from ENV\nconst INPUT = msg.payload;\n//var mode = env.get(\"mode\");\nvar mode = parseInt(env.get(\"mode\"));\nconst Disabledcolour = env.get(\"disabledColour\");\nconst disabledTXT = env.get(\"disabledTXT\");\nconst colourA = env.get(\"colourA\");\nconst colourB = env.get(\"colourB\");\nconst txtA = env.get(\"txtA\");\nconst txtB = env.get(\"txtB\");\nconst txtclrA = env.get(\"txtclrA\");\nconst txtclrB = env.get(\"txtclrB\");\nconst payloadA = env.get(\"payloadA\");\nconst payloadB = env.get(\"payloadB\");\nlet topic = env.get(\"topicSET\");\nif (topic === undefined)\n    topic = \"\";\n\n//  These are other variables\nlet state = context.get(\"STATE\")||0;\nlet enabled = context.get(\"ENABLED\")||0;\nconst msg1 = {};\n\n\n//--------------------------------------------\n\n\nif (msg.topic === \"D\")\n{\n    context.set(\"DEBUG\",msg.payload);\n    //  Show env \n    node.warn(\"Disabled colour \" + Disabledcolour);\n    node.warn(\"Disabled text \" + disabledTXT);\n    node.warn(\"colourA \" + colourA);\n    node.warn(\"colourB \" + colourB); \n    node.warn(\"txtA \" + txtA);\n    node.warn(\"txtB \" + txtB);\n    node.warn(\"txtclrA \" + txtclrA);\n    node.warn(\"txtclrB \" + txtclrB);\n    node.warn(\"payloadA \" + payloadA);\n    node.warn(\"payloadB \" + payloadB);\n    node.warn(\"topic \" + topic);\n    return;\n}\n\nlet debug = context.get(\"DEBUG\") || 0;\n\nif (debug === 1)\n{\n    node.warn(\"INPUT \" + INPUT);\n    node.warn(\"Disabled colour \" + Disabledcolour);\n    node.warn(\"Disabled text \" + disabledTXT);\n    node.warn(\"colourA \" + colourA);\n    node.warn(\"colourB \" + colourB); \n    node.warn(\"txtA \" + txtA);\n    node.warn(\"txtB \" + txtB);\n    node.warn(\"txtclrA \" + txtclrA);\n    node.warn(\"txtclrB \" + txtclrB);\n    node.warn(\"payloadA \" + payloadA);\n    node.warn(\"payloadB \" + payloadB);\n    node.warn(\"topic \" + topic);\n}\n//--------------------------------------------\n\n//  Reload desired state\nif (INPUT == \"RESET\")\n{\n    // load startOn value.\n    msg.payload = env.get(\"StartOn\")\n}\n\nif (INPUT == \"RELOAD\")\n{\n    //  force redisplay for `button` node.\n    if (state == 0)\n    {\n        //\n        msg1.txt = txtA;\n        msg1.colour = Disabledcolour;\n        msg1.fontclr = txtclrA;\n        node.status({ fill: \"yellow\", text: \"A\" });\n    } else\n    if (state == 1)\n    {\n        //\n        msg1.txt = txtB;\n        msg1.colour = Disabledcolour;\n        msg1.fontclr = txtclrB;\n        node.status({ fill: \"red\", text: \"B\" });\n\n    }\n    return [null,msg1];\n}\n\n//  =========================================\n//  Set condition if the message is either A or B\n\nif (msg.payload === \"A\")\n{\n    msg.payload = payloadA;\n    msg.topic = topic;\n\n    msg1.txt = txtA;\n    msg1.colour = Disabledcolour;\n    msg1.fontclr = txtclrA;\n    node.status({ fill: \"yellow\", text: \"A\" });\n    \n    context.set(\"STATE\",0);\n\n    return [msg,msg1];\n}\n\nif (msg.payload === \"B\")\n{\n    msg.payload = payloadB;\n    msg.topic = topic;\n\n    msg1.txt = txtB;\n    msg1.colour = Disabledcolour;\n    msg1.fontclr = txtclrB;\n    node.status({ fill: \"red\", text: \"B\" });\n    \n    context.set(\"STATE\",1);\n\n    return [msg,msg1];\n}\n///////////////////////////////////////////////////////////////////////////////\n///////////////////////////////////////////////////////////////////////////////\n///////////////////////////////////////////////////////////////////////////////\n//      Main routine below.\n//-------------------------------------\n//      Now on to the real stuff.\nif (msg.payload === \"X\")\n{\n    //\n    //  Button pressed.\n    //\n    node.status({fill: \"green\",text: \"Pressed\"});\n    if (enabled === undefined)\n    {\n        node.status({text:\"Button_Toggle_Enable_Context_Not_Set\"});\n        node.warn(\"Button toggle enable context not set\");\n        return [null,null];\n    }\n    if (enabled === 0)\n    {\n        //\n        node.status({ fill: \"blue\", text: \"enabled\" });\n        context.set(\"ENABLED\", 1);\n        if (state === 0)\n        {\n            //\n            //  Set things for state 0\n            //\n            msg1.colour = colourA;\n            msg1.txt = txtA;\n            msg1.fontclr = txtclrA;\n            //node.status({ fill: \"yellow\", text: \"A\" });\n        }\n        else \n        if (state === 1)\n        {\n            //\n            //  Set things for state 1\n            //\n            msg1.colour = colourB;\n            msg1.txt = txtB;\n            msg1.fontclr = txtclrB;\n            //node.status({ fill: \"red\", text: \"B\" });\n        }\n        return [null,msg1];\n    }\n    //  To here `enabled` === 0.  Now set to 1.\n    //-------------------------------------\n    //      ENABLED from here down.\n    if (enabled === 1)\n    {\n\n        state = (state + 1)% 2;\n        context.set(\"STATE\",state);\n\n        if (mode === 0)\n        {\n            //\n            //  Condition A\n            //\n            node.status({ fill: \"yellow\", text: \"A\" });\n            msg.payload = payloadA;\n            msg1.colour = colourA;\n            msg1.txt = txtA;\n            msg1.fontclr = txtclrA;\n        }   //  END FOR MODE === 0\n        //-------------------------------------\n        //  Code here for MODE === 1\n        if (mode === 1)\n        {\n            if (state === 0)\n            {\n                //\n                //  Condition A\n                //\n                node.status({fill: \"yellow\",text: \"A\"});\n                msg.payload = payloadA;\n                msg1.colour = colourA;\n                msg1.txt = txtA;\n                msg1.fontclr = txtclrA;\n            } else\n            if (state === 1)\n            {\n                //\n                //  Condition B\n                //\n                node.status({fill: \"red\",text: \"B\"});\n                msg.payload = payloadB;\n                msg1.colour = colourB;\n                msg1.txt = txtB;\n                msg1.fontclr = txtclrB;\n            }\n        }   //  END FOR MODE === 1\n    }\n    //-------------------------------------\n    msg.topic = topic;\n    return [msg,msg1];\n}\n//-------------------------------------\n\n//-------------------------------------\nif (msg.payload === \"Z\")\n{\n    //\n    //      Timed out\n    ////node.warn(\"Timed out\");\n    //\n    //-------------------------------------\n    context.set(\"ENABLED\",0);\n    if (mode === 0) {\n        //\n        //  Condition A\n        //\n        node.status({ text: \"\" });\n        context.set(\"STATE\", 0);\n        msg1.colour = Disabledcolour;\n        msg1.txt = disabledTXT;\n    }\n    //-------------------------------------\n\n    //-------------------------------------\n    if (mode === 1)\n    {\n        //node.status({text: \"\"});\n        if (payloadA === undefined)\n        {\n            //  This needs work.   Throw error?\n            return [null,null];\n        }\n        if (state === 0)\n        {\n            //\n            //  Set things for state 0\n            //\n            msg.payload = payloadA;\n            msg.topic = topic;\n            msg1.colour = Disabledcolour;\n            msg1.txt = txtA;\n            msg1.fontclr = txtclrA;\n            node.status({ fill: \"yellow\", text: \"A\" });\n        }\n        else if (state === 1)\n        {\n            //\n            //  Set things for state 1\n            //\n            msg.payload = payloadB;\n            msg.topic = topic;\n            msg1.colour = Disabledcolour;\n            msg1.txt = txtB;\n            msg1.fontclr = txtclrB;\n            node.status({ fill: \"red\", text: \"B\" });\n        }\n    }\n    //-------------------------------------\n    return [null,msg1];\n}\n//-------------------------------------\n","outputs":2,"timeout":"","noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\ncontext.set(\"ABGC\", env.get(\"colourA\"));\ncontext.set(\"BBGC\", env.get(\"colourB\"));\n//\n//  Disabled button background colour.\n//\ncontext.set(\"DISABLEDCLR\",env.get(\"disabledColour\"));\n//\n//  Now do text.\n//\ncontext.set(\"Atxt\", env.get(\"txtA\"));\ncontext.set(\"Btxt\", env.get(\"txtB\"));\n//\n//  Font colours.\n//\ncontext.set(\"AFC\",env.get(\"txtclrA\"));\ncontext.set(\"BFC\",env.get(\"txtclrB\"));\n//\n//  Payloads.\n//\ncontext.set(\"PayloadA\", env.get(\"payloadA\"));\ncontext.set(\"PayloadB\", env.get(\"payloadB\"));\n\n","finalize":"","libs":[],"x":600,"y":260,"wires":[[],[]],"outputLabels":["To gate","To button"]},{"id":"ce88ca05dfbfc987","type":"trigger","z":"ce274e69.c6f778","name":"","op1":"","op2":"Z","op1type":"nul","op2type":"str","duration":"3","extend":true,"overrideDelay":true,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":425,"y":370,"wires":[["c43c865d57f36958"]],"l":false},{"id":"e8e0b354f6fd8938","type":"switch","z":"ce274e69.c6f778","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"X","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":305,"y":370,"wires":[["f3f9aaf11f719403"]],"l":false},{"id":"f3f9aaf11f719403","type":"function","z":"ce274e69.c6f778","name":"set msg.delay","func":"msg.delay = env.get(\"delay\") || 1000;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":365,"y":370,"wires":[["ce88ca05dfbfc987"]],"l":false},{"id":"c906d5b6e5da1f26","type":"inject","z":"ce274e69.c6f778","name":"Selected","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"StartOn","payloadType":"env","x":350,"y":170,"wires":[["c43c865d57f36958","f4758814ab1ca9b8"]]},{"id":"f4758814ab1ca9b8","type":"switch","z":"ce274e69.c6f778","name":"","property":"mode","propertyType":"env","rules":[{"t":"eq","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":305,"y":330,"wires":[["f3f9aaf11f719403"]],"l":false},{"id":"4ace159cbaefa773","type":"subflow:ce274e69.c6f778","z":"60d0e3e966d484ae","name":"","env":[{"name":"delay","value":"800","type":"num"},{"name":"StartOn","value":"B","type":"str"},{"name":"mode","value":"1","type":"num"},{"name":"disabledColour","value":"brown","type":"str"},{"name":"disabledTXT","value":"disabled","type":"str"},{"name":"colourA","value":"lime","type":"str"},{"name":"colourB","value":"green","type":"str"},{"name":"txtA","value":"A","type":"str"},{"name":"txtB","value":"B","type":"str"},{"name":"txtclrA","value":"black","type":"str"},{"name":"txtclrB","value":"black","type":"str"},{"name":"payloadA","value":"outputA","type":"str"},{"name":"payloadB","value":"outputB","type":"str"},{"name":"topicSET","value":"topic","type":"str"}],"x":760,"y":1670,"wires":[["110c99add66b6502","f67d99ab2b255de8"],["9d38f301937d0ece","de365ece51f1b832"]]},{"id":"de365ece51f1b832","type":"ui_button","z":"60d0e3e966d484ae","name":"","group":"f149abdd741ca58e","order":4,"width":"4","height":"1","passthru":false,"label":"{{msg.txt}}","tooltip":"","color":"{{msg.fontclr}}","bgcolor":"{{msg.colour}}","className":"","icon":"","payload":"X","payloadType":"str","topic":"topic","topicType":"msg","x":750,"y":1740,"wires":[["4ace159cbaefa773"]]},{"id":"f67d99ab2b255de8","type":"ui_text","z":"60d0e3e966d484ae","group":"f149abdd741ca58e","order":5,"width":"4","height":"1","name":"","label":"","format":"{{msg.payload}}","layout":"row-spread","className":"","x":1000,"y":1580,"wires":[]},{"id":"f149abdd741ca58e","type":"ui_group","name":"Demo","tab":"1caa8458.b17814","order":1,"disp":true,"width":"12","collapse":false,"className":"test"},{"id":"1caa8458.b17814","type":"ui_tab","name":"Demo","icon":"dashboard","order":10,"disabled":false,"hidden":false}]

All names are structures with A and B to identify the two possible messages.

delay is the maximum time between button presses.
startCond selects if output A or B is the active one.
mode determines if it is single send or toggle output.
disabledColour is the colour of the button when it is disabled. (Inactive - may be better term)
disabledTXT is the text displayed on the button when disabled.
colourA the colour of the button when in state A.
txtA is the text displayed when in state A.
payloadA is what is sent to the next node in state A.
(and likewise for the B modes)
topicSET is the topic of the message.
(I use gate nodes and this is handy when controlling those kind of nodes.)

Oh, the mode.
0 = toggle mode. Message A and B are sent on alternate double clicks (invocations)
1 = single mode. When you double press the button, message A is sent.
Pressing the button repeatedly either toggles the message sent or repeatedly sends A message.

Check the node's documentation and let me know if there is anything missing.

There are other things but they are covered in the documentation page.
I included them because I also do a lot of dashboard redesign and sometimes the button nodes is wiped.

Hope someone can find it useful.