Pomodoro Clock
Node-RED

Pomodoro Clock

Pomodoro timer for AWTRIX clock. Displays work and break steps in a custom app with a progress bar. Use the middle button to pause/resume, left/right buttons to navigate steps.
A flow by chatteleyn

Download flow

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.