Looking for a countdown clock

I'm looking for a digital countdown clock that displays Hours, Minutes and Seconds. I will need to be able to preset the countdown time for up to 12 Hours. I've been able to get the Digital Clock to count down using the epoch timestamp and multiplying by -1 but I haven't yet figured out how to preset a time like say 6:00:00 and have it countdown to 00:00:00 so I can shutdown a system after the preset time. What's confusing me is how to calculate what number I need to enter for a preset time and then get it to actually countdown. I've tried setting the countdown increment to 1000 but I can't get it to work.

Thanks in advance.

Which digital clock? Do you have a link to a node package that provides the digital clock - sorry for asking the obvious if everyone else knows which clock is meant!

The digital clock is smart enough to know to go backwards if value is negative? For me the series of values you are sending to the clock would be (each second and epoch is second not millisecond):

epoch * -1
(epoch+1) * -1
(epoch+2) * -1
...

I assume you send something to the clock at a specific time interval (e.g. 1 second) and you send the clock a value that it should be displaying.

Also epoch here represents the timestamp when you begin sending messages.

Or do you store the original epoch value, i.e. first epoch value and subtract from that so that the series becomes:

epoch * -1
(epoch-1) * -1
(epoch-2) * -1
...

In this second case, won't it be a matter of sending 6 * 60 * 60 as initial value and the subtracting one from that value until the final value is zero?

How/where are you displaying this? In a browser? If so, which tool are you using to display it - Dashboard-2 or UIBUILDER? The answers will likely change the advice given.

My apologies for not supplying that info. I'm working with the node-red-contrib-ui-digital-clock node. Now this is likely not meant to work in countdown mode and I'm quite brutal at code writing so that's why I'm trying to leverage this node.

Currently I'm using Dashboard2 but I'm open to anything.

Let me see if I can knock something together as a fun excercise.

Here you go. :smiley:

Doesn't yet have control from Node-RED but that is easy to add.

I'm afraid that you will need to install node-red-contrib-uibuilder and then restart node-red before using this flow.

[{"id":"fa0240a25588cf67","type":"group","z":"b2f18a716bd20f99","name":"Countdown timer demo","style":{"label":true},"nodes":["e51ba7ef3e333151","a86aea213a59fe7d","2d170e4ebc0b7f1f","fa56099adb3acf7e","3dc0031905a11e96","7fc5dc8d632aca16","b0661e7aef09d16f","24be6cd926d1deb0"],"x":88,"y":1459,"w":648,"h":328},{"id":"e51ba7ef3e333151","type":"debug","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"debug 7","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":655,"y":1500,"wires":[],"l":false},{"id":"a86aea213a59fe7d","type":"uibuilder","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","topic":"","url":"becker","instancePath":"","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"7.7.0","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/D:/src\\uibRoot/becker/?windowId=_blank","x":300,"y":1540,"wires":[["2d170e4ebc0b7f1f"],[]]},{"id":"2d170e4ebc0b7f1f","type":"switch","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"query","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":450,"y":1520,"wires":[["e51ba7ef3e333151","3dc0031905a11e96"],["fa56099adb3acf7e"]]},{"id":"fa56099adb3acf7e","type":"debug","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"debug 33","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":565,"y":1560,"wires":[],"l":false},{"id":"3dc0031905a11e96","type":"link out","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"link out 62","mode":"link","links":["7fc5dc8d632aca16"],"x":695,"y":1540,"wires":[]},{"id":"7fc5dc8d632aca16","type":"link in","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"link in 19","links":["3dc0031905a11e96"],"x":135,"y":1540,"wires":[["b0661e7aef09d16f"]]},{"id":"b0661e7aef09d16f","type":"change","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"RESPONSE","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":195,"y":1540,"wires":[["a86aea213a59fe7d"]],"l":false},{"id":"24be6cd926d1deb0","type":"group","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"RUN ONCE: Initialise Front-end Code - Run after the uibuilder node has been deployed. \\n REMEMBER to change the uib node name before use \\n Sets up FE code, reloads connected clients. \\n ","style":{"label":true,"stroke":"#a4a4a4","fill-opacity":"0.33","color":"#000000","fill":"#ffffff"},"nodes":["54afea71bcf96374","c1d30ee72e72cbb0","c92d83bda675832c","5bcce172317b7bfd","34ff3e8e97de6fd5","5763fbb206b384ed"],"x":114,"y":1591,"w":554,"h":170},{"id":"54afea71bcf96374","type":"inject","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setup all FE files","x":175,"y":1680,"wires":[["34ff3e8e97de6fd5","5763fbb206b384ed"]],"l":false},{"id":"c1d30ee72e72cbb0","type":"template","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<!doctype html>\n<html lang=\"en\"><head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"icon\" href=\"../uibuilder/images/uib-world.svg\" type=\"image/svg+xml\">\n\n    <title>Countdown Timer - Node-RED UIBUILDER</title>\n    <meta name=\"description\" content=\"Node-RED UIBUILDER - Countdown Timer\">\n\n    <!-- Your own CSS (defaults to loading uibuilders css)-->\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n    <style>\n        #timer {\n            font-size: 5rem;\n            font-weight: bold;\n            font-variant-numeric: tabular-nums;\n            letter-spacing: 4px;\n        }\n\n        .labels {\n            display: flex;\n            justify-content: center;\n            gap: 2.5rem;\n            margin-top: -10px;\n            color: #888;\n            text-transform: uppercase;\n            font-size: 0.8rem;\n        }\n    </style>\n\n    <script type=\"module\" async src=\"./index.js\"></script>\n</head><body>\n    <h1 class=\"with-subtitle\">Countdown Timer</h1>\n    <div role=\"doc-subtitle\">Using UIBUILDER for Node-RED</div>\n    \n    <!-- '#more' is used as a parent for dynamic HTML content in examples\n         Also, send {topic:\"more\", payload:\"Hello from <b>Node-RED</b>\"} to auto-display the payload -->\n    <div id=\"more\" uib-topic=\"more\"></div>\n\n    <article class='container'>\n        <h2>COUNTDOWN</h2>\n        <div id='timer'>00:00:00</div>\n        <div class='labels'>\n            <span>Hours</span>\n            <span>Mins</span>\n            <span>Secs</span>\n        </div>\n    </article>\n\n</body></html>\n","output":"str","x":380,"y":1680,"wires":[["c92d83bda675832c"]]},{"id":"c92d83bda675832c","type":"uib-save","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","url":"becker","uibId":"a86aea213a59fe7d","folder":"src","fname":"","createFolder":false,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":550,"y":1680,"wires":[]},{"id":"5bcce172317b7bfd","type":"template","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.js","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Give VS Code IntelliSense for uibuilder\n/// <reference path=\"../types/uibuilder.d.ts\" />\n\n// @ts-ignore\nimport { uibuilder } from '../uibuilder/uibuilder.esm.min.js'\n\n// Set the initial countdown time in seconds (Max 43200 for 12 hours)\nlet timeLeft = 3600 // 1 hour\n\nconst timerDisplay = document.getElementById('timer')\n\nconst updateTimer = () => {\n    if (timeLeft <= 0) {\n        clearInterval(countdownInterval)\n        timerDisplay.textContent = '00:00:00'\n        timerDisplay.style.color = '#ff4b2b'\n        return\n    }\n\n    const hours = Math.floor(timeLeft / 3600)\n    const minutes = Math.floor((timeLeft % 3600) / 60)\n    const seconds = timeLeft % 60\n\n    // Format numbers to always show two digits\n    const displayHours = String(hours).padStart(2, '0')\n    const displayMinutes = String(minutes).padStart(2, '0')\n    const displaySeconds = String(seconds).padStart(2, '0')\n\n    timerDisplay.textContent = `${displayHours}:${displayMinutes}:${displaySeconds}`\n    \n    timeLeft--\n}\n\n// Run the timer immediately on load, then every second\nupdateTimer()\nconst countdownInterval = setInterval(updateTimer, 1000)\n","output":"str","x":390,"y":1720,"wires":[["c92d83bda675832c"]]},{"id":"34ff3e8e97de6fd5","type":"change","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":265,"y":1680,"wires":[["c1d30ee72e72cbb0"]],"l":false},{"id":"5763fbb206b384ed","type":"change","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":265,"y":1720,"wires":[["5bcce172317b7bfd"]],"l":false},{"id":"db5d34e28a4ae831","type":"global-config","env":[],"modules":{"node-red-contrib-uibuilder":"7.7.0"}}]

Oh, and you can add this into the index.js to be able to update the timer from Node-RED:

uibuilder.onTopic('setTime', (msg) => {
    const newTime = parseInt(msg.payload, 10)
    if (!isNaN(newTime) && newTime >= 0 && newTime <= 43200) {
        timeLeft = newTime
        updateTimer() // Update immediately when time is set
    } else {
        console.warn('Invalid time received. Please send a number between 0 and 43200.')
    }
})

And add this to the end of the updateTimer function to get an alert in Node-RED when the timer is expiring:

    if (timeLeft <= 1000) {
        uibuilder.send({ topic: 'timeout', payload: timeLeft })
    }

Having a keep look at the code, it does seem to take a millisecond value:

  The time to be displayed is transferred as timestamp (milliseconds since 01.01.1970) in
  <code>msg.payload</code>. The local time is displayed in the dashboard.

So generating a storing a value in the context of the node could work:

[{"id":"da0f9a3098c5a96b","type":"inject","z":"b06b5fcc3ecc1bac","name":"","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":585.0000228881836,"y":395.7142915725708,"wires":[["6cb2380794d52e8e"]]},{"id":"6cb2380794d52e8e","type":"function","z":"b06b5fcc3ecc1bac","name":"function 1","func":"if ( context.get(\"timer\")) {\n    let v = context.get(\"timer\") \n    msg.payload = v - 1000\n    context.set(\"timer\", msg.payload)\n} else {\n    msg.payload = 60 * 6 * 60 * 1000\n    context.set(\"timer\", msg.payload)\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750.0000190734863,"y":538.5714836120605,"wires":[["3446616efbce26e5"]]},{"id":"3446616efbce26e5","type":"ui_digital_clock","z":"b06b5fcc3ecc1bac","name":"","group":"a49a51e326c4bbe1","order":2,"width":0,"height":0,"x":967.8572082519531,"y":610.0000400543213,"wires":[]},{"id":"a49a51e326c4bbe1","type":"ui_group","name":"Cachemap","tab":"2e11dca0bed4eaa2","order":1,"disp":true,"width":"12","collapse":false,"className":""},{"id":"2e11dca0bed4eaa2","type":"ui_tab","name":"GeoCache","icon":"dashboard","order":9,"disabled":false,"hidden":false}]

try that, it does the epoch - 1000 each second and starts with 6 * 60 * 60 * 1000 <--- that might be wrong though.

So this works but I can't seem to figure out the formula to use to calculate the times. If I use 39600 I get 5 Hours but if I use 43200 I get 6 hours. 39600/5/60/60 = 2.2 but 43200/6/60/60 = 2 and so I'm not sure why the dividend changes.

flows.json (6.8 KB)

The timestamp is in JavaScript Date milliseconds. 1000 = 1 second, 60,000 = 1 minute, 3,600,000=10006060= 1 hour. So 12 hours would be 43,200,000.

For dashboard 2 (no extra contrib nodes - just 1 ui-template). Its not perfect but it should give you a head start.



[
    {
        "id": "5deb7ffd584f9748",
        "type": "ui-template",
        "z": "648fda2a9b3e2104",
        "group": "group1",
        "page": "",
        "ui": "",
        "name": "Countdown Timer",
        "order": 1,
        "width": 6,
        "height": 3,
        "head": "",
        "format": "<template>\n  <v-card class=\"pa-2\" elevation=\"2\" style=\"background-color: #000; color: #0f0; font-family: 'DS-Digital', monospace;\">\n    <v-row align=\"center\" justify=\"space-between\">\n      <v-col cols=\"8\" class=\"d-flex justify-center\">\n        <div :class=\"['countdown', { 'alert': alert }]\">\n          {{ displayTime }}\n        </div>\n      </v-col>\n      <v-col cols=\"4\" class=\"d-flex justify-end\">\n        <v-btn icon @click=\"showSettings = true\">\n          <v-icon>mdi-cog</v-icon>\n        </v-btn>\n        <v-btn icon @click=\"startTimer\">\n          <v-icon>mdi-play</v-icon>\n        </v-btn>\n        <v-btn icon @click=\"stopTimer\">\n          <v-icon>mdi-stop</v-icon>\n        </v-btn>\n        <v-btn icon @click=\"resetTimer\">\n          <v-icon>mdi-replay</v-icon>\n        </v-btn>\n      </v-col>\n    </v-row>\n\n    <v-dialog v-model=\"showSettings\" max-width=\"400\">\n      <v-card>\n        <v-card-title>Set Duration</v-card-title>\n        <v-card-text>\n          <v-text-field\n            label=\"Duration (HH:mm:ss)\"\n            v-model=\"inputTime\"\n            :rules=\"[timeRule]\"\n            maxlength=\"8\"\n            counter\n          ></v-text-field>\n        </v-card-text>\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn text @click=\"showSettings = false\">Cancel</v-btn>\n          <v-btn text @click=\"applySettings\" :disabled=\"!validTime\">Apply</v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </v-card>\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      duration: 0, // in seconds\n      remaining: 0, // in seconds\n      timer: null,\n      running: false,\n      alert: false,\n      showSettings: false,\n      inputTime: '00:00:00',\n    }\n  },\n  computed: {\n    displayTime() {\n      const absRemaining = Math.abs(this.remaining)\n      const h = Math.floor(absRemaining / 3600).toString().padStart(2, '0')\n      const m = Math.floor((absRemaining % 3600) / 60).toString().padStart(2, '0')\n      const s = (absRemaining % 60).toString().padStart(2, '0')\n      const sign = this.remaining < 0 ? '-' : ''\n      return `${sign}${h}:${m}:${s}`\n    },\n    validTime() {\n      return /^([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)$/.test(this.inputTime)\n    }\n  },\n  watch: {\n    inputTime(val) {\n      if (this.validTime) {\n        this.$refs.inputField?.setError(false)\n      }\n    }\n  },\n  methods: {\n    startTimer() {\n      if (!this.running) {\n        this.send({topic: 'start', payload: this.remaining})\n        this.running = true\n        this.timer = setInterval(() => {\n          this.remaining--\n          if (this.remaining === 0) {\n            this.send({topic: 'complete', payload: 0})\n          } else if (this.remaining < 0) {\n            this.alert = true\n          }\n        }, 1000)\n      }\n    },\n    stopTimer() {\n      const report = this.running\n      this.running = false\n      clearInterval(this.timer)\n      if (report) {\n        this.send({topic: 'stop', payload: this.remaining})\n      }\n      this.timer = null\n    },\n    resetTimer() {\n      this.stopTimer()\n      this.remaining = this.duration\n      this.send({topic: 'reset', payload: this.remaining})\n      this.alert = false\n    },\n    applySettings() {\n      const parts = this.inputTime.split(':')\n      this.duration = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2])\n      this.remaining = this.duration\n      this.send({topic: 'set', payload: this.remaining})\n      this.alert = false\n      this.showSettings = false\n      this.stopTimer()\n    },\n    timeRule(value) {\n      return this.validTime || 'Invalid time format'\n    },\n    handleInput(msg) {\n      switch (msg.topic) {\n        case 'start':\n          this.startTimer()\n          break\n        case 'pause':\n          this.stopTimer()\n          break\n        case 'stop':\n          this.resetTimer()\n          break\n        case 'reset':\n          this.resetTimer()\n          break\n        case 'set':\n          if (typeof msg.payload === 'number') {\n            this.duration = Math.floor(msg.payload / 1000)\n            this.remaining = this.duration\n            this.alert = false\n            this.stopTimer()\n            this.send({topic: 'set', payload: this.remaining})\n          }\n          break\n      }\n    }\n  },\n  mounted() {\n    this.$watch('msg', (msg) => {\n      if (msg) {\n        this.handleInput(msg)\n      }\n    })\n  },\n  beforeUnmount() {\n    this.stopTimer()\n  }\n}\n</script>\n\n<style scoped>\n@import url('https://fonts.googleapis.com/css2?family=DS-Digital&display=swap');\n.countdown {\n  font-size: 3rem;\n  letter-spacing: 0.2rem;\n  user-select: none;\n  transition: color 0.5s ease;\n}\n.countdown.alert {\n  color: red;\n  animation: blink 1s infinite;\n}\n@keyframes blink {\n  0%, 100% { opacity: 1; }\n  50% { opacity: 0; }\n}\n</style>",
        "storeOutMessages": true,
        "passthru": false,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 340,
        "y": 200,
        "wires": [
            [
                "5c498c5cc67e64aa"
            ]
        ]
    },
    {
        "id": "18ca3e6f93f4591c",
        "type": "inject",
        "z": "648fda2a9b3e2104",
        "name": "Start",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "start",
        "payload": "",
        "payloadType": "date",
        "x": 100,
        "y": 100,
        "wires": [
            [
                "5deb7ffd584f9748"
            ]
        ]
    },
    {
        "id": "2ba5093f59a96efe",
        "type": "inject",
        "z": "648fda2a9b3e2104",
        "name": "Pause",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "pause",
        "payload": "",
        "payloadType": "date",
        "x": 100,
        "y": 140,
        "wires": [
            [
                "5deb7ffd584f9748"
            ]
        ]
    },
    {
        "id": "02654f4556a65d54",
        "type": "inject",
        "z": "648fda2a9b3e2104",
        "name": "Stop",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "stop",
        "payload": "",
        "payloadType": "date",
        "x": 100,
        "y": 180,
        "wires": [
            [
                "5deb7ffd584f9748"
            ]
        ]
    },
    {
        "id": "8c4a3e6a4d78e328",
        "type": "inject",
        "z": "648fda2a9b3e2104",
        "name": "Reset",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "reset",
        "payload": "",
        "payloadType": "date",
        "x": 100,
        "y": 220,
        "wires": [
            [
                "5deb7ffd584f9748"
            ]
        ]
    },
    {
        "id": "ac77491e25f02c68",
        "type": "inject",
        "z": "648fda2a9b3e2104",
        "name": "Set (1 min)",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "set",
        "payload": "60000",
        "payloadType": "num",
        "x": 110,
        "y": 260,
        "wires": [
            [
                "5deb7ffd584f9748"
            ]
        ]
    },
    {
        "id": "5c498c5cc67e64aa",
        "type": "debug",
        "z": "648fda2a9b3e2104",
        "name": "debug 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 550,
        "y": 200,
        "wires": []
    },
    {
        "id": "group1",
        "type": "ui-group",
        "name": "Countdown Group",
        "page": "page1",
        "width": 6,
        "height": 3,
        "order": 1,
        "showTitle": true,
        "className": "",
        "visible": "true",
        "disabled": "false",
        "groupType": "default"
    },
    {
        "id": "page1",
        "type": "ui-page",
        "name": "Countdown Page",
        "ui": "f4aaf8d032f42383",
        "path": "/countdown",
        "icon": "home",
        "layout": "grid",
        "theme": "theme1",
        "breakpoints": [
            {
                "name": "Default",
                "px": "0",
                "cols": "3"
            },
            {
                "name": "Tablet",
                "px": "576",
                "cols": "6"
            },
            {
                "name": "Small Desktop",
                "px": "768",
                "cols": "9"
            },
            {
                "name": "Desktop",
                "px": "1024",
                "cols": "12"
            }
        ],
        "order": 1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "f4aaf8d032f42383",
        "type": "ui-base",
        "name": "Dashboard",
        "path": "/dashboard",
        "appIcon": "",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "headerContent": "page",
        "navigationStyle": "default",
        "titleBarStyle": "default",
        "showReconnectNotification": true,
        "notificationDisplayTime": 1,
        "showDisconnectNotification": true,
        "allowInstall": false
    },
    {
        "id": "theme1",
        "type": "ui-theme",
        "name": "Countdown Theme",
        "colors": {
            "surface": "#ffffff",
            "primary": "#0094CE",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        },
        "sizes": {
            "density": "default",
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    },
    {
        "id": "ba56b23e0a8890cc",
        "type": "global-config",
        "env": [],
        "modules": {
            "@flowfuse/node-red-dashboard": "1.30.2"
        }
    }
]

Yes, that makes sense however this is not the result I'm getting with the Digital-Clock Node. 39600000 gives me 5:00:00 hours and 43200000 results in 6:00:00 hours. Zero gives me 18:00:00 Hours and 18000000 gives me 23:00:00 Hours so I think maybe 18:00:00 is the common number here and I will need to include that in a formula to calculate my preset times.

Oops! No, got that wrong!

An hour is 3600 of course! 5 hours would be 5*3600 = 18000

My if statement in the onTopic callback also slightly wrong, it should be:

if (!isNaN(newTime) && newTime >= 0 && newTime <= Number.MAX_SAFE_INTEGER) {

Argh! Fools rush in!

The input is in seconds of course, not milliseconds:

// Give VS Code IntelliSense for uibuilder
/// <reference path="../types/uibuilder.d.ts" />

// @ts-ignore
import { uibuilder } from '../uibuilder/uibuilder.esm.min.js'

// Set the initial countdown time in seconds (Max 43200 for 12 hours)
let timeLeft = 3600 // 1 hour

uibuilder.onTopic('setTime', (msg) => {
    const newTime = parseInt(msg.payload, 10)
    console.log('Received new time:', msg.payload, newTime)
    if (!isNaN(newTime) && newTime >= 0 && newTime <= Number.MAX_SAFE_INTEGER) {
        timeLeft = newTime
        updateTimer() // Update immediately when time is set
    } else {
        console.warn('Invalid time received. Please send a number between 0 and 43200.')
    }
})

const timerDisplay = document.getElementById('timer')

const updateTimer = () => {
    if (timeLeft <= 0) {
        clearInterval(countdownInterval)
        timerDisplay.textContent = '00:00:00'
        timerDisplay.style.color = '#ff4b2b'
        return
    }

    const hours = Math.floor(timeLeft / 3600)
    const minutes = Math.floor((timeLeft % 3600) / 60)
    const seconds = timeLeft % 60

    // Format numbers to always show two digits
    const displayHours = String(hours).padStart(2, '0')
    const displayMinutes = String(minutes).padStart(2, '0')
    const displaySeconds = String(seconds).padStart(2, '0')

    timerDisplay.textContent = `${displayHours}:${displayMinutes}:${displaySeconds}`
    
    timeLeft--

    if (timeLeft <= 1) {
        uibuilder.send({ topic: 'timeout', payload: timeLeft })
    }
}

// Run the timer immediately on load, then every second
updateTimer()
const countdownInterval = setInterval(updateTimer, 1000)

I think I may have a solution with the Digital Clock node but time will tell. To everyone who's contributed to this thread I very much thank you. These forums are great especially since I'm so crappy at any kind of programming. Unfortunately that means that there is very little I can contribute and yet I still get help from these communities.

Thanks again.