Skip to content

Instantly share code, notes, and snippets.

@igrowing
Last active April 12, 2023 01:40
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save igrowing/79130bba78def09495e0533c89f0b28f to your computer and use it in GitHub Desktop.
Save igrowing/79130bba78def09495e0533c89f0b28f to your computer and use it in GitHub Desktop.
Timer with User Interface control/feedback

The timer is based on Schedex node and applicable for following purposes:

  • Automated adjustable watering/lighting/pumping.
  • Timer-controlled watering/lighting/pumping.

Dark User Interface example Light User Interface example

Features:

  • programmable automated timer (Schedex ) which can be:
  • Enabled or disabled from UI;
  • Or overridden with switch;
  • Active time can be adjusted from UI;
  • Or enable load for timeout defined in UI with countdown feedback;
  • logging, including persistent graphs;
  • Control each circuit separately;
  • Show the status of each circuit on dashboard;
  • Keep/restore important settings and status.

Easy to add:

  • MQTT posting.
  • Local IO control.
  • Active time correction by actual measurements (for instance, adding PID loop for watering correction).

The flow

The valve/switch control script is here.

To multiply control circuits:

  • 1 - Set your schedule in Schedex Timer.
  • 2 - Set persist storage to store and restore last command.
  • 3 - Tune/replace Exec node with your actuator.
  • 4 - Rename UI switch.
  • 5 - In parse_timeout function set the same name to flow.get() as in UI_to_flow_context flow.set().
  • 6 - Connect output of switch2timer function to Schedex timer of each circuit.

It is possible to set separated systems: one for watering, one for pumping and one for lighting. Edit the flow.set/get variable name in UI_to_flow_context and in parse_timeout functions.

Credits: Thank you people helped in the Node-RED forum

[
{
"id": "ee00a260.25be4",
"type": "tab",
"label": "2share",
"disabled": false,
"info": ""
},
{
"id": "bb9aa72f.f142b8",
"type": "debug",
"z": "ee00a260.25be4",
"name": "watering",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"x": 770,
"y": 300,
"wires": []
},
{
"id": "3c42dbd2.1e05b4",
"type": "ui_switch",
"z": "ee00a260.25be4",
"name": "",
"label": "Water Lawn",
"group": "97ade2e4.08d3a",
"order": 2,
"width": "5",
"height": "1",
"passthru": false,
"decouple": "true",
"topic": "shm/garden/water_lawn/state",
"style": "",
"onvalue": "on",
"onvalueType": "str",
"onicon": "local_florist",
"oncolor": "green",
"offvalue": "off",
"offvalueType": "str",
"officon": "local_florist",
"offcolor": "grey",
"x": 130,
"y": 520,
"wires": [
[
"8a84b5dd.ae4e58"
]
]
},
{
"id": "6441aba8.d2b1b4",
"type": "ui_numeric",
"z": "ee00a260.25be4",
"name": "",
"label": "Adjustment, %",
"group": "97ade2e4.08d3a",
"order": 3,
"width": 0,
"height": 0,
"passthru": true,
"topic": "adjustment",
"format": "{{value}}",
"min": 0,
"max": "300",
"step": "10",
"x": 260,
"y": 180,
"wires": [
[
"11bb8145.de1bcf",
"3e2ee852.8125c8"
]
]
},
{
"id": "6fa30bc9.05bbe4",
"type": "ui_switch",
"z": "ee00a260.25be4",
"name": "enable_water",
"label": "Enable Watering",
"group": "97ade2e4.08d3a",
"order": 1,
"width": 0,
"height": 0,
"passthru": true,
"decouple": "false",
"topic": "shm/garden/water/enable",
"style": "",
"onvalue": "on",
"onvalueType": "str",
"onicon": "",
"oncolor": "",
"offvalue": "off",
"offvalueType": "str",
"officon": "",
"offcolor": "",
"x": 120,
"y": 280,
"wires": [
[
"488908dd.205d88"
]
]
},
{
"id": "f31149a4.c48a68",
"type": "ui_text",
"z": "ee00a260.25be4",
"group": "97ade2e4.08d3a",
"order": 4,
"width": "1",
"height": "1",
"name": "counter",
"label": "",
"format": "{{msg.payload}}",
"layout": "row-right",
"x": 360,
"y": 560,
"wires": []
},
{
"id": "c3fda280.7177b",
"type": "trigger",
"z": "ee00a260.25be4",
"op1": "",
"op2": "",
"op1type": "nul",
"op2type": "pay",
"duration": "1",
"extend": false,
"units": "min",
"reset": "",
"bytopic": "all",
"name": "",
"x": 140,
"y": 640,
"wires": [
[
"8a84b5dd.ae4e58"
]
]
},
{
"id": "fd6e73a0.acea9",
"type": "inject",
"z": "ee00a260.25be4",
"name": "water_sch_on",
"topic": "shm/garden/water/enable",
"payload": "on",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"x": 100,
"y": 240,
"wires": [
[
"6fa30bc9.05bbe4"
]
]
},
{
"id": "72ae558b.45668c",
"type": "exec",
"z": "ee00a260.25be4",
"command": "actuators/valve.py 1 ",
"addpay": true,
"append": "",
"useSpawn": "false",
"timer": "",
"oldrc": false,
"name": "",
"x": 760,
"y": 520,
"wires": [
[
"bb9aa72f.f142b8"
],
[
"697816e4.cf9db8"
],
[]
]
},
{
"id": "bc7c106c.5fdf3",
"type": "inject",
"z": "ee00a260.25be4",
"name": "init_off",
"topic": "",
"payload": "off",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.05",
"x": 370,
"y": 320,
"wires": [
[
"72ae558b.45668c"
]
]
},
{
"id": "cfe6eb07.872248",
"type": "persist out",
"z": "ee00a260.25be4",
"name": "watering_cor",
"storageNode": "2c5b61a9.137b0e",
"x": 90,
"y": 180,
"wires": [
[
"6441aba8.d2b1b4"
]
]
},
{
"id": "11bb8145.de1bcf",
"type": "persist in",
"z": "ee00a260.25be4",
"name": "watering_cor",
"storageNode": "2c5b61a9.137b0e",
"x": 450,
"y": 220,
"wires": []
},
{
"id": "3e2ee852.8125c8",
"type": "trigger",
"z": "ee00a260.25be4",
"op1": "",
"op2": "",
"op1type": "nul",
"op2type": "payl",
"duration": "2",
"extend": true,
"units": "min",
"reset": "",
"bytopic": "all",
"name": "delay 2m",
"x": 440,
"y": 180,
"wires": [
[
"3bfb0b7c.5ef504"
]
]
},
{
"id": "d3f2a255.22a5c",
"type": "comment",
"z": "ee00a260.25be4",
"name": "____________ Watering system description ____________",
"info": "3 circuits: Lawn, Tree, Stone.\nControls:\n- Enable/disable scheduling.\n- Adjust irrigation time in %.\n- Control each circuit separately with timer or manually.\n\nShow the status of each circuit on dashboard.\nShow watering log in dashboard.\nKeep/restore watering adjustment and watering chart.\n\n100% time and days per circuit:\nLawn - 50 min - Mon, Wed, Fri.\nTrees - 70 min - Tue, Sat.\nStone - 60 min - Thu, Sun.\n\nTODO:\n1 - Revise flow.set() variable name in UI_to_flow_context.\n2 - Adapt number of outputs in calc_off_offset function to your needs.\n",
"x": 450,
"y": 40,
"wires": []
},
{
"id": "488908dd.205d88",
"type": "change",
"z": "ee00a260.25be4",
"name": "switch2timer",
"rules": [
{
"t": "change",
"p": "payload",
"pt": "msg",
"from": "off",
"fromt": "str",
"to": "suspended true",
"tot": "str"
},
{
"t": "change",
"p": "payload",
"pt": "msg",
"from": "on",
"fromt": "str",
"to": "suspended false",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 150,
"y": 320,
"wires": [
[
"4ff10be1.cd3d94"
]
]
},
{
"id": "bf519293.47cdb",
"type": "ui_slider",
"z": "ee00a260.25be4",
"name": "timeout",
"label": "Stop in {{value}} min",
"group": "97ade2e4.08d3a",
"order": 5,
"width": 0,
"height": 0,
"passthru": true,
"topic": "/shm/garden/timeout",
"min": 0,
"max": "90",
"step": 1,
"x": 340,
"y": 140,
"wires": [
[
"cb50e867.521618"
]
]
},
{
"id": "cb50e867.521618",
"type": "function",
"z": "ee00a260.25be4",
"name": "UI_to_flow_context",
"func": "flow.set(\"get_water_timeout\", msg.payload);\nreturn null;",
"outputs": 1,
"noerr": 0,
"x": 630,
"y": 140,
"wires": [
[]
]
},
{
"id": "a63bdebb.14d08",
"type": "inject",
"z": "ee00a260.25be4",
"name": "",
"topic": "",
"payload": "10",
"payloadType": "num",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"x": 110,
"y": 140,
"wires": [
[
"bf519293.47cdb"
]
]
},
{
"id": "e32111f3.5f18c",
"type": "trigger",
"z": "ee00a260.25be4",
"op1": "",
"op2": "",
"op1type": "nul",
"op2type": "pay",
"duration": "2",
"extend": false,
"units": "s",
"reset": "",
"bytopic": "all",
"name": "2sec delay",
"x": 550,
"y": 560,
"wires": [
[
"2363bd12.6c9232",
"72ae558b.45668c",
"3c42dbd2.1e05b4",
"b902888f.45f258"
]
]
},
{
"id": "4ff10be1.cd3d94",
"type": "schedex",
"z": "ee00a260.25be4",
"name": "LawnTimer",
"suspended": false,
"lat": "32",
"lon": "34",
"ontime": "goldenHourEnd",
"ontopic": "shm/garden/water_lawn/state",
"onpayload": "on",
"onoffset": "-20",
"onrandomoffset": 0,
"offtime": "goldenHourEnd",
"offtopic": "shm/garden/water_lawn/state",
"offpayload": "off",
"offoffset": "30",
"offrandomoffset": false,
"mon": true,
"tue": false,
"wed": true,
"thu": false,
"fri": true,
"sat": false,
"sun": false,
"x": 360,
"y": 600,
"wires": [
[
"e32111f3.5f18c"
]
]
},
{
"id": "8a84b5dd.ae4e58",
"type": "function",
"z": "ee00a260.25be4",
"name": "parse_timeout",
"func": "// Outputs:\n// Counter_to_display, GPIO control via BigTimer, Counter_to_process\n\n// This beautiful expression doesn't work, gets default 10 always.\n// var delay = typeof(context.get('get_water_timeout') == undefined) ? 10 : flow.get('get_water_timeout');\n// This ugly construction works. :(\ntry{\n var delay = flow.get('get_water_timeout');\n} catch (er) {\n var delay = 10;\n}\n\nvar state = context.get('state') || 'ignore';\nvar topic = context.get('topic') || msg.topic;\ncontext.set('topic', topic);\nnode.status({fill:\"yellow\",shape:\"ring\",text: delay+','+state+','+msg.payload});\n\n// Block loop of messages when off\nif ('off' == msg.payload && 'off' == state) return [null, null, null];\nnode.status({fill:\"yellow\",shape:\"dot\",text: delay+','+state+','+msg.payload});\n\n// Keep msg for next round\nif ('on' == msg.payload) {\n // Don't count/display if fully manual\n var m1 = (delay > 0) ? {payload: delay} : null; \n // Set delay, start countdown, on load\n context.set('state', 'on');\n node.status({fill:\"green\", shape:\"dot\", text: delay+','+context.get('state')+','+msg.payload});\n return [m1, msg, m1]; \n} else if (('off' == msg.payload) || (parseInt(msg.payload) <= 1)) {\n // off load, reset clear counter\n context.set('state', 'off');\n node.status({fill:\"red\",shape:\"dot\",text: delay+','+context.get('state')+','+msg.payload});\n return [{payload: ' '}, {payload: 'off'}, null];\n} else if (parseInt(msg.payload) > 1 && context.get('state') == 'on') {\n // decrement countdown\n var m1 = {payload: parseInt(msg.payload)-1};\n node.status({fill:\"blue\",shape:\"dot\",text: delay+'>'+m1.payload+','+state+','+msg.payload});\n return [m1, null, m1];\n}\n\n// Unexpected state: look for a bug in reached here.\nnode.status({fill:\"grey\",shape:\"ring\",text: delay+','+state+','+msg.payload+'???'});\nreturn [{payload: ' '}, {payload: 'off'}, null];\n",
"outputs": 3,
"noerr": 0,
"x": 130,
"y": 580,
"wires": [
[
"f31149a4.c48a68"
],
[
"4ff10be1.cd3d94",
"3c42dbd2.1e05b4"
],
[
"c3fda280.7177b"
]
]
},
{
"id": "298bc26f.bd320e",
"type": "persist out",
"z": "ee00a260.25be4",
"name": "cmd_garden_lawn",
"storageNode": "fe86bf2f.158b3",
"x": 390,
"y": 520,
"wires": [
[
"e32111f3.5f18c"
]
]
},
{
"id": "2363bd12.6c9232",
"type": "persist in",
"z": "ee00a260.25be4",
"name": "cmd_garden_lawn",
"storageNode": "fe86bf2f.158b3",
"x": 750,
"y": 560,
"wires": []
},
{
"id": "3bfb0b7c.5ef504",
"type": "function",
"z": "ee00a260.25be4",
"name": "calc_off_offset",
"func": "/*\nPer each watering circuit in dictionary:\nCalculate new offtime from input payload.\n*/\n\nconst MAP = [ // Circuit: total time\n 50, // 'LawnTimer':\n 70, // 'TreeTimer': \n 60, // 'StoneTimer': \n];\nvar res = [];\n\nfor (const value of MAP) {\n res.push({payload: 'offoffset ' + Math.round(value * parseInt(msg.payload) / 100 - 20)});\n}\n\nreturn res;",
"outputs": 3,
"noerr": 0,
"x": 620,
"y": 180,
"wires": [
[
"4ff10be1.cd3d94"
],
[],
[]
]
},
{
"id": "7dd7b92d.8645b8",
"type": "comment",
"z": "ee00a260.25be4",
"name": "__________ Circuit specific nodes: duplicate these per circuit __________",
"info": "TO DO:\n1 - Set your schedule in Schedex Timer.\n2 - Set persist storage to store and restore last command.\n3 - Tune/replace Exec node with your actuator.\n4 - Rename UI switch.\n5 - In parse_timeout function set the same name to flow.get() as in UI_to_flow_context flow.set().\n6 - Connect output of switch2timer function to Schedex timer of each circuit.\n",
"x": 460,
"y": 480,
"wires": []
},
{
"id": "fe5728ae.eb12f8",
"type": "comment",
"z": "ee00a260.25be4",
"name": "Connect init_off to each actuator in your scheme",
"info": "",
"x": 420,
"y": 280,
"wires": []
},
{
"id": "60e2ab39.9b5004",
"type": "comment",
"z": "ee00a260.25be4",
"name": "___________________ Common part for all circuits ___________________",
"info": "",
"x": 450,
"y": 100,
"wires": []
},
{
"id": "71a6bee0.2a10d",
"type": "comment",
"z": "ee00a260.25be4",
"name": "Logging",
"info": "",
"x": 790,
"y": 220,
"wires": []
},
{
"id": "697816e4.cf9db8",
"type": "mqtt out",
"z": "ee00a260.25be4",
"name": "",
"topic": "",
"qos": "",
"retain": "",
"broker": "af8eb750.bd7d38",
"x": 770,
"y": 340,
"wires": []
},
{
"id": "db907094.b56ba",
"type": "ui_chart",
"z": "ee00a260.25be4",
"name": "",
"group": "97ade2e4.08d3a",
"order": 6,
"width": "6",
"height": "3",
"label": "Watering Log",
"chartType": "line",
"legend": "true",
"xformat": "dd HH:mm",
"interpolate": "step",
"nodata": "No data",
"dot": false,
"ymin": "0",
"ymax": "1",
"removeOlder": 1,
"removeOlderPoints": "",
"removeOlderUnit": "604800",
"cutout": 0,
"useOneColor": false,
"colors": [
"#1f77b4",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"useOldStyle": false,
"x": 550,
"y": 400,
"wires": [
[
"a50ff488.45b088"
],
[]
]
},
{
"id": "b902888f.45f258",
"type": "function",
"z": "ee00a260.25be4",
"name": "msg2digit",
"func": "var old = msg.payload;\nnode.status({text: old + '???'});\nif (msg.payload == null) return;\n\nif (msg.payload === true || \n msg.payload.toLowerCase() == 'open' || \n msg.payload.toLowerCase() == 'on' ||\n parseInt(msg.payload) > 0) { msg.payload = 1 }\n else if (msg.payload === false || \n msg.payload.toLowerCase() == 'close' || \n msg.payload.toLowerCase() == 'off' ||\n parseInt(msg.payload) <= 0) { msg.payload = 0 }\n else return;\n\nnode.status({text: old + \">>\" + msg.payload});\n\ntry{\n // Get 2nd word from the end of topic\n msg.topic = msg.topic.split('/').slice(-2)[0].trim();\n} catch (er) {\n node.status({text: 'Topic error'});\n return null;\n}\nnode.status({text: msg.topic + '>>' + old + \",\" + msg.payload});\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 240,
"y": 400,
"wires": [
[
"db907094.b56ba"
]
]
},
{
"id": "a50ff488.45b088",
"type": "persist in",
"z": "ee00a260.25be4",
"name": "watering",
"storageNode": "5a252273.7c18ac",
"x": 700,
"y": 400,
"wires": []
},
{
"id": "e27e688e.50e1c8",
"type": "persist out",
"z": "ee00a260.25be4",
"name": "watering",
"storageNode": "5a252273.7c18ac",
"x": 400,
"y": 420,
"wires": [
[
"db907094.b56ba"
]
]
},
{
"id": "c42a5fb9.33de2",
"type": "comment",
"z": "ee00a260.25be4",
"name": "Connect here outputs from all actuators",
"info": "",
"x": 790,
"y": 260,
"wires": []
},
{
"id": "35e88818.b547e8",
"type": "comment",
"z": "ee00a260.25be4",
"name": "Log visualization",
"info": "",
"x": 400,
"y": 380,
"wires": []
},
{
"id": "97ade2e4.08d3a",
"type": "ui_group",
"z": "",
"name": "Garden: Watering",
"tab": "11bccf83.c6223",
"order": 1,
"disp": true,
"width": "6",
"collapse": false
},
{
"id": "2c5b61a9.137b0e",
"type": "persist-store",
"z": "",
"filename": "watering_cor.json",
"interval": "3600"
},
{
"id": "fe86bf2f.158b3",
"type": "persist-store",
"z": "",
"filename": "cmd_garden_lawn.json",
"interval": "600"
},
{
"id": "af8eb750.bd7d38",
"type": "mqtt-broker",
"z": "",
"name": "",
"broker": "localhost",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": true,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
},
{
"id": "5a252273.7c18ac",
"type": "persist-store",
"z": "",
"filename": "watering.json",
"interval": "3600"
},
{
"id": "11bccf83.c6223",
"type": "ui_tab",
"z": "",
"name": "2share",
"icon": "home",
"order": 1
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment