Flow Details
A Node-RED flow that implements a Pomodoro timer as a custom AWTRIX clock app, controlled entirely via MQTT. The timer cycles through configurable work and break steps, displayed as a compact step indicator on the bottom row of the clock face alongside a countdown and progress bar. The middle (select) button pauses and resumes the timer, while the left and right buttons let you skip to the previous or next step. Sound notifications are played at each step transition.
[
{
"id": "pomo-tab",
"type": "tab",
"label": "Awtrix Pomodoro",
"disabled": false,
"info": ""
},
{
"id": "bd369d53d63f924b",
"type": "group",
"z": "pomo-tab",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"36133fe5cb5f20fd",
"b10bbeea2dccf905",
"b85768a6f5067684",
"218d223e91e922d0",
"a4e4249b7d1445ef",
"6cacbec04e65d8a3",
"5347854fb985cebb",
"cc435e2a87f7705f",
"8cc2e5de1d9d4536",
"891fce0518717790",
"c3ee30bd69339174",
"0748a0fe76b63a6a",
"f9c0e370a30f2053",
"3c4aebfb42d3c2eb",
"ec2da8818fe78884",
"b9070a7afc847e27",
"f40deec908cb16a0",
"4fc2fe6bebd30a58",
"5c1d6766a0f59e79",
"df79fbade1457b55",
"c56b360c5b17d663",
"6cd8ec8057c50d7f",
"ab3fefda96548f9f",
"96e3ab1d51fbb5ca",
"6038125ceeea4fae",
"526f8805b3044b18",
"ce7f08cb739a28db",
"b1788a3fb52df1f2",
"cdecbaf49d3e604e",
"a5cfbf85a165e5f6",
"dac9640b05a6c24d",
"a201e9a6dc6b01c9",
"16b167d58df7f922",
"ebcf2646aaca8d0f",
"fdf69bd9caea5ded"
],
"x": 14,
"y": 19,
"w": 1652,
"h": 822
},
{
"id": "36133fe5cb5f20fd",
"type": "inject",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Init",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "init",
"payload": "",
"payloadType": "date",
"x": 650,
"y": 160,
"wires": [
[
"b10bbeea2dccf905"
]
]
},
{
"id": "b10bbeea2dccf905",
"type": "change",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"rules": [
{
"t": "set",
"p": "paused",
"pt": "flow",
"to": "true",
"tot": "bool"
},
{
"t": "set",
"p": "step",
"pt": "flow",
"to": "-1",
"tot": "num"
},
{
"t": "set",
"p": "icon",
"pt": "flow",
"to": "68088",
"tot": "num"
},
{
"t": "set",
"p": "prefix",
"pt": "flow",
"to": "awtrix",
"tot": "str"
},
{
"t": "set",
"p": "appname",
"pt": "flow",
"to": "pomodoro",
"tot": "str"
},
{
"t": "set",
"p": "workduration",
"pt": "flow",
"to": "25 * 60 * 1000",
"tot": "jsonata"
},
{
"t": "set",
"p": "breakduration",
"pt": "flow",
"to": "5 * 60 * 1000",
"tot": "jsonata"
},
{
"t": "set",
"p": "longbreakduration",
"pt": "flow",
"to": "20 * 60 * 1000",
"tot": "jsonata"
},
{
"t": "set",
"p": "looplength",
"pt": "flow",
"to": "4",
"tot": "num"
},
{
"t": "set",
"p": "currentapp",
"pt": "flow",
"to": "appname",
"tot": "flow"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 840,
"y": 160,
"wires": [
[
"c3ee30bd69339174"
]
]
},
{
"id": "b85768a6f5067684",
"type": "mqtt in",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "currentApp",
"topic": "awtrix/stats/currentApp",
"qos": "2",
"datatype": "auto-detect",
"broker": "pomo-mqtt-broker",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 120,
"y": 680,
"wires": [
[
"218d223e91e922d0"
]
]
},
{
"id": "218d223e91e922d0",
"type": "change",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"rules": [
{
"t": "set",
"p": "currentapp",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 320,
"y": 680,
"wires": [
[]
]
},
{
"id": "a4e4249b7d1445ef",
"type": "mqtt out",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Set Time",
"topic": "",
"qos": "0",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "pomo-mqtt-broker",
"x": 1380,
"y": 260,
"wires": []
},
{
"id": "6cacbec04e65d8a3",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "msg",
"func": "let date = new Date(flow.get(\"countdown\"))\n\nlet minutes = date.getUTCMinutes()\nlet seconds = date.getUTCSeconds()\n\nconst WIDTH = 24;\nconst START = 8;\n\nconst looplength = flow.get(\"looplength\");\nconst step = flow.get(\"step\");\nconst totalLength = looplength * 5 - 1;\n\nfunction stepPos(i) {\n return i % 2 === 0 ? (i / 2) * 5 : Math.floor(i / 2) * 5 + 3;\n}\n\nlet scrollOffset = 0;\nif (totalLength > WIDTH) {\n const curPos = stepPos(step);\n scrollOffset = curPos - Math.floor(WIDTH / 2);\n scrollOffset = Math.max(0, Math.min(scrollOffset, totalLength - WIDTH));\n}\n\nconst baseX = totalLength <= WIDTH ? Math.round(START + (WIDTH - totalLength) / 2) : START;\n\nlet draw = [];\nfor (let i = 0; i < looplength * 2; i++) {\n const relX = baseX + stepPos(i) - scrollOffset;\n\n if (i % 2 === 0) {\n const x1 = relX, x2 = relX + 1;\n if (x2 < START || x1 >= START + WIDTH)\n continue;\n draw.push({ \"dl\": [Math.max(x1, START), 7, Math.min(x2, START + WIDTH - 1), 7, step === i ? \"#FFFFFF\" : \"#6C767B\"] });\n } else {\n if (relX < START || relX >= START + WIDTH)\n continue;\n let color;\n if (i === looplength * 2 - 1)\n color = step === i ? \"#FFD600\" : \"#FFFF8D\";\n else\n color = step === i ? \"#FFFFFF\" : \"#6C767B\";\n draw.push({ \"dp\": [relX, 7, color] });\n }\n}\n\nmsg.payload = {\n \"text\": (minutes < 10 ? \"0\" + minutes : minutes) + \":\" + (seconds < 10 ? \"0\" + seconds : seconds),\n \"icon\": flow.get(\"icon\"),\n \"color\": flow.get(\"step\") % 2 == 0 ? \"#FFFFFF\" : \"#00C853\",\n \"draw\": draw\n}\n\nmsg.topic = flow.get(\"prefix\") + \"/custom/\" + flow.get(\"appname\")\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1230,
"y": 260,
"wires": [
[
"a4e4249b7d1445ef"
]
]
},
{
"id": "5347854fb985cebb",
"type": "inject",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Clock",
"props": [
{
"p": "payload"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 140,
"y": 240,
"wires": [
[
"cc435e2a87f7705f"
]
]
},
{
"id": "cc435e2a87f7705f",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check app",
"property": "app",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "appname",
"vt": "flow"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 310,
"y": 240,
"wires": [
[
"8cc2e5de1d9d4536"
]
]
},
{
"id": "8cc2e5de1d9d4536",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check paused",
"property": "paused",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "0",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 500,
"y": 240,
"wires": [
[
"891fce0518717790"
]
]
},
{
"id": "891fce0518717790",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Set countdown",
"func": "flow.set(\"countdown\", flow.get(\"countdown\") - (Date.now() - flow.get(\"time\")))\nflow.set(\"time\", Date.now())\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 700,
"y": 240,
"wires": [
[
"0748a0fe76b63a6a"
]
]
},
{
"id": "c3ee30bd69339174",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Set step",
"func": "let increment = msg.topic.endsWith(\"buttonLeft\") ? -1 : 1\n\nflow.set(\"step\", (flow.get(\"step\") + increment + flow.get(\"looplength\") * 2) % (flow.get(\"looplength\") * 2))\nflow.set(\"paused\", true)\n\nif(flow.get(\"step\") == flow.get(\"looplength\") * 2 - 1)\n flow.set(\"countdown\", flow.get(\"longbreakduration\"))\nelse if(flow.get(\"step\") % 2 == 0)\n flow.set(\"countdown\", flow.get(\"workduration\"))\nelse\n flow.set(\"countdown\", flow.get(\"breakduration\"))\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1080,
"y": 220,
"wires": [
[
"6cacbec04e65d8a3",
"4fc2fe6bebd30a58",
"fdf69bd9caea5ded"
]
]
},
{
"id": "0748a0fe76b63a6a",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"property": "countdown",
"propertyType": "flow",
"rules": [
{
"t": "lte",
"v": "0",
"vt": "num"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 880,
"y": 240,
"wires": [
[
"c3ee30bd69339174"
],
[
"6cacbec04e65d8a3"
]
]
},
{
"id": "f9c0e370a30f2053",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "msg",
"func": "msg.payload = \"RingRing:d=4,o=5,b=100:32b,32d6,32g6,32g6,32g6,8p,32b,32d6,32g6,32g6,32g6\"\n\nmsg.topic = flow.get(\"prefix\") + \"/rtttl\"\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1390,
"y": 180,
"wires": [
[
"3c4aebfb42d3c2eb"
]
]
},
{
"id": "3c4aebfb42d3c2eb",
"type": "mqtt out",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Play sound",
"topic": "",
"qos": "0",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "pomo-mqtt-broker",
"x": 1570,
"y": 180,
"wires": []
},
{
"id": "ec2da8818fe78884",
"type": "inject",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Remove",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 800,
"wires": [
[
"b9070a7afc847e27"
]
]
},
{
"id": "b9070a7afc847e27",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "msg",
"func": "msg.payload = {}\n\nmsg.topic = flow.get(\"prefix\") + \"/custom/\" + flow.get(\"appname\")\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 270,
"y": 800,
"wires": [
[
"f40deec908cb16a0"
]
]
},
{
"id": "f40deec908cb16a0",
"type": "mqtt out",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Remove app",
"topic": "",
"qos": "0",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "pomo-mqtt-broker",
"x": 430,
"y": 800,
"wires": []
},
{
"id": "4fc2fe6bebd30a58",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "msg",
"func": "msg.payload = {\n \"name\": \"pomodoro\"\n}\n\nmsg.topic = flow.get(\"prefix\") + \"/switch\"\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1230,
"y": 220,
"wires": [
[
"ebcf2646aaca8d0f"
]
]
},
{
"id": "5c1d6766a0f59e79",
"type": "mqtt out",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Switch app",
"topic": "",
"qos": "0",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "pomo-mqtt-broker",
"x": 1570,
"y": 220,
"wires": []
},
{
"id": "df79fbade1457b55",
"type": "comment",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "🍅 POMODORO CLOCK",
"info": " - Set your custom parameters here\n - Press the clock \"select button\" to pause/unpause\n - Double press the \"right/left buttons\" to chnage the current step",
"x": 170,
"y": 60,
"wires": []
},
{
"id": "c56b360c5b17d663",
"type": "comment",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Set your custom parameters here",
"info": "Here you can set your custom constants and variables to be used for the clock",
"x": 830,
"y": 120,
"wires": []
},
{
"id": "6cd8ec8057c50d7f",
"type": "mqtt in",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "buttonRight",
"topic": "awtrix/stats/buttonRight",
"qos": "2",
"datatype": "auto-detect",
"broker": "pomo-mqtt-broker",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 130,
"y": 340,
"wires": [
[
"dac9640b05a6c24d",
"a201e9a6dc6b01c9"
]
]
},
{
"id": "ab3fefda96548f9f",
"type": "mqtt in",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "buttonSelect",
"topic": "awtrix/stats/buttonSelect",
"qos": "2",
"datatype": "auto-detect",
"broker": "pomo-mqtt-broker",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 130,
"y": 540,
"wires": [
[
"6038125ceeea4fae",
"ce7f08cb739a28db"
]
]
},
{
"id": "96e3ab1d51fbb5ca",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check current app",
"property": "currentapp",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "appname",
"vt": "flow"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 510,
"y": 540,
"wires": [
[
"526f8805b3044b18"
]
]
},
{
"id": "6038125ceeea4fae",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check button",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "0",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 310,
"y": 540,
"wires": [
[
"96e3ab1d51fbb5ca"
]
]
},
{
"id": "526f8805b3044b18",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Toggle paused",
"func": "flow.set(\"time\", Date.now())\nflow.set(\"paused\", !flow.get(\"paused\"))\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 540,
"wires": [
[]
]
},
{
"id": "ce7f08cb739a28db",
"type": "change",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"rules": [
{
"t": "set",
"p": "lastbuttonstate",
"pt": "flow",
"to": "payload",
"tot": "msg"
},
{
"t": "set",
"p": "lastbutton",
"pt": "flow",
"to": "topic",
"tot": "msg"
},
{
"t": "set",
"p": "lastbuttontime",
"pt": "flow",
"to": "",
"tot": "date"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 600,
"wires": [
[]
]
},
{
"id": "b1788a3fb52df1f2",
"type": "mqtt in",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "buttonLeft",
"topic": "awtrix/stats/buttonLeft",
"qos": "2",
"datatype": "auto-detect",
"broker": "pomo-mqtt-broker",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 120,
"y": 420,
"wires": [
[
"dac9640b05a6c24d",
"a201e9a6dc6b01c9"
]
]
},
{
"id": "cdecbaf49d3e604e",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check last button time",
"property": "lastbuttontime",
"propertyType": "flow",
"rules": [
{
"t": "gte",
"v": "$millis() - 500",
"vt": "jsonata"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 540,
"y": 360,
"wires": [
[
"16b167d58df7f922"
]
]
},
{
"id": "a5cfbf85a165e5f6",
"type": "change",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"rules": [
{
"t": "set",
"p": "lastbuttontime",
"pt": "flow",
"to": "",
"tot": "date"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 550,
"y": 320,
"wires": [
[]
]
},
{
"id": "dac9640b05a6c24d",
"type": "function",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check values",
"func": "if (flow.get(\"lastbutton\") == msg.topic && flow.get(\"lastbuttonstate\") == 1 && msg.payload == 0)\n return msg\n// else\n// node.warn(flow.get(\"lastbutton\") + \"&&\"+ flow.get(\"lastbuttonstate\") + \"&&\"+ msg.payload);",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 320,
"y": 360,
"wires": [
[
"cdecbaf49d3e604e",
"a5cfbf85a165e5f6"
]
]
},
{
"id": "a201e9a6dc6b01c9",
"type": "change",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"rules": [
{
"t": "set",
"p": "lastbuttonstate",
"pt": "flow",
"to": "payload",
"tot": "msg"
},
{
"t": "set",
"p": "lastbutton",
"pt": "flow",
"to": "topic",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 400,
"wires": [
[]
]
},
{
"id": "16b167d58df7f922",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "Check current app",
"property": "currentapp",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "appname",
"vt": "flow"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 770,
"y": 360,
"wires": [
[
"c3ee30bd69339174"
]
]
},
{
"id": "ebcf2646aaca8d0f",
"type": "delay",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"pauseType": "delay",
"timeout": "500",
"timeoutUnits": "milliseconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 1390,
"y": 220,
"wires": [
[
"5c1d6766a0f59e79"
]
]
},
{
"id": "fdf69bd9caea5ded",
"type": "switch",
"z": "pomo-tab",
"g": "bd369d53d63f924b",
"name": "",
"property": "topic",
"propertyType": "msg",
"rules": [
{
"t": "empty"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 1240,
"y": 180,
"wires": [
[
"f9c0e370a30f2053"
]
]
},
{
"id": "pomo-mqtt-broker",
"type": "mqtt-broker",
"name": "Mosquitto",
"broker": "homeserver",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
}
]
-- Flow first published on April 23, 2026.