Anyone in this forum knows I can barely spell I.T. however, I have all the functionality sorted out now after only 3 years, so I'm going back to try to make things look a little better. I started with this thermostat and I think me and chatgpt did a really good job.
and here's the flow,
[{"id":"3f0bb353bfa06f22","type":"function","z":"04de3cddb4a6f215","name":"tempSet","func":"\n\nvar tempSet;\n\nif (msg.topic == \"Setpoint\"){\n tempSet = msg.payload;\n if (tempSet < 10){\n tempSet = 10;\n }\n if (tempSet > 25){\n tempSet = 25;\n }\n flow.set(\"tempSet\", tempSet);\n node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n return msg;\n};\n\n\nlet Y = msg.topic;\n\nif(Y == \"Plus\"){\n tempSet = flow.get(\"tempSet\");\n if (tempSet == 'undefined'){\n flow.set(\"tempSet\",10);\n tempSet = 10;\n }\n if (isNaN(tempSet)) {\n flow.set(\"tempSet\",10);\n tempSet = 10;\n }\n tempSet = tempSet + .5;\n if (tempSet > 25) {\n tempSet = 25;\n }\n flow.set(\"tempSet\", tempSet);\n node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n return msg;\n}\n\nif (Y == \"Minus\") {\n let tempSet = flow.get(\"tempSet\");\n if (tempSet == 'undefined') {\n flow.set(\"tempSet\", 10);\n tempSet = 10;\n }\n if (isNaN(tempSet)) {\n flow.set(\"tempSet\", 10);\n tempSet = 10;\n }\n tempSet = tempSet - .5;\n if (tempSet < 10) {\n tempSet = 10;\n }\n flow.set(\"tempSet\", tempSet);\n node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n return msg;\n}\n\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":280,"wires":[["595a26fd2a2e4546"]]},{"id":"595a26fd2a2e4546","type":"function","z":"04de3cddb4a6f215","name":"aggregate","func":"/*\nmsg.payload[0] = power\nmsg.payload[1] = fan\nmsg.payload[2] = cog\nmsg.payload[3] = flame\nmsg.payload[4] = tempAmb\nmsg.payload[5] = tempSet\n*/\nlet Sensor = \"good\";\n\nlet Sensor1 = global.get(\"Sensor1\");\nlet Sensor2 = global.get(\"Sensor2\");\ncontext.set(\"Sensor1\",Sensor1);\ncontext.set(\"Sensor2\",Sensor2);\n\nif (Sensor1 == \"failed\" && Sensor2 == \"failed\"){\n Sensor = \"failed\";\n}\n\nlet Fan = flow.get(\"Fan\");\nlet tempSet = flow.get(\"tempSet\");\n\n\nlet Cog = flow.get(\"Cog\");\nif (Cog == \"On\") {\n let W = cToF(tempSet);\n let X = W.toFixed(1);\n tempSet = X;\n}\n\nfunction cToF(num) {\n var f = ((num * 1.8) + 32);\n return f;\n}\n\nfunction fToC(num) {\n var c = ((num - 32) * (5 / 9));\n return c;\n}\n\nlet tempAmb = flow.get(\"tempAmb\");\nlet Power = flow.get(\"Power\");\nif (Sensor == \"failed\"){\n Power = \"None\";\n// flow.set(\"Power\",\"Off\");\n}\nvar Flame = \"\";\nif (global.get(\"heat_running\") == true){\n Flame = \"On\";\n}\nif (global.get(\"heat_running\") == false){\n Flame = \"Off\";\n}\n//let Flame = flow.get(\"Flame\");\nCog = flow.get(\"Cog\");\nconst payload=[];\n\nmsg = {\n payload: [Power,Fan,Cog,Flame],\n tempSet: tempSet,\n tempAmb: tempAmb,\n}\n\n\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":380,"wires":[["9c56b5c3aa936f5b"]]},{"id":"65470a8bed182c70","type":"link in","z":"04de3cddb4a6f215","name":"3 sec in","links":["637589bc3f933b91"],"x":235,"y":380,"wires":[["d67d430dec3dad02","595a26fd2a2e4546","ffdec737ec9672ee","3f0bb353bfa06f22"]]},{"id":"bab4ddb531211035","type":"function","z":"04de3cddb4a6f215","name":"Pwr","func":"\nif (msg.topic != \"Power\"){\n return null;\n}\n\nlet Power = context.get(\"Power\");\nif (Power == 'undefined'){\n context.set(\"Power\",\"On\");\n flow.set(\"Power\",\"On\")\n Power = \"On\";\n node.status({ fill: \"blue\", shape: \"dot\", text: Power });\n}\n\nif (Power == \"On\"){\n context.set(\"Power\",\"Off\");\n flow.set(\"Power\",\"Off\");\n} else {\n context.set(\"Power\", \"On\");\n flow.set(\"Power\", \"On\");\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: Power });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":380,"wires":[[]]},{"id":"d41568c07d7fba67","type":"function","z":"04de3cddb4a6f215","name":"Cog","func":"\nif (msg.topic != \"Cog\"){\n return null;\n}\n\nlet Cog = context.get(\"Cog\");\nif (Cog == 'undefined'){\n context.set(\"Cog\",\"Off\");\n flow.set(\"Cog\",\"Off\")\n Cog = \"Off\";\n node.status({ fill: \"blue\", shape: \"dot\", text: Cog });\n}\n\nif (Cog == \"On\"){\n context.set(\"Cog\",\"Off\");\n flow.set(\"Cog\",\"Off\");\n} else {\n context.set(\"Cog\", \"On\");\n flow.set(\"Cog\", \"On\");\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: Cog });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":440,"wires":[[]]},{"id":"953d89927c79927c","type":"function","z":"04de3cddb4a6f215","name":"Fan","func":"\nif (msg.topic != \"Fan\"){\n return null;\n}\n\nlet Fan = context.get(\"Fan\");\nif (Fan == 'undefined'){\n context.set(\"Fan\",\"Off\");\n flow.set(\"Fan\",\"Off\")\n Fan = \"Off\";\n node.status({ fill: \"blue\", shape: \"dot\", text: Fan });\n}\n\nif (Fan == \"On\"){\n context.set(\"Fan\",\"Off\");\n flow.set(\"Fan\",\"Off\");\n node.status({ fill: \"blue\", shape: \"dot\", text: \"Fan Off\"});\n} else {\n context.set(\"Fan\", \"On\");\n flow.set(\"Fan\", \"On\");\n node.status({ fill: \"blue\", shape: \"dot\", text: \"Fan On\" });\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":500,"wires":[[]]},{"id":"d67d430dec3dad02","type":"function","z":"04de3cddb4a6f215","name":"Thermostat logic","func":"let TempSet = Number(flow.get(\"tempSet\"));\nlet TempAmb = Number(flow.get(\"tempAmb\"));\nlet Power = flow.get(\"Power\");\nvar HeatOn; \n\nflow.set(\"Flame\", \"Off\");\nflow.set('heating_status', \"Off\");\nHeatOn = false;\n\n\nif(Power == \"On\"){\n if (TempSet <= TempAmb + 0.2) {\n HeatOn = false;\n flow.set(\"Flame\", \"Off\");\n } else {\n if (TempSet > TempAmb + 0.2) {\n HeatOn = true;\n flow.set('heating_status', HeatOn);\n flow.set(\"Flame\", \"On\");\n } \n }\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: HeatOn});\n\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":440,"wires":[[]]},{"id":"ffdec737ec9672ee","type":"function","z":"04de3cddb4a6f215","name":"heat running","func":"\nlet Heat = global.get(\"heat_running\");\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"Heat On \" + Heat});\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":260,"wires":[[]]},{"id":"9c56b5c3aa936f5b","type":"ui-template","z":"04de3cddb4a6f215","group":"546c6a08d800a5e9","page":"","ui":"","name":"good thermo","order":2,"width":"4","height":"11","head":"","format":"\n\n\n\n\n\n\n\n<template>\n <container>\n <div class=\"nexus\">\n\n <!-- HEADER -->\n <div class=\"nexus-header\">\n <div class=\"brand\">\n <span class=\"brand-icon\">◈</span>\n <div>\n <div class=\"brand-name\">Systatium</div>\n <div class=\"brand-sub\">CLIMATE CONTROL SYSTEM</div>\n </div>\n </div>\n </div>\n\n <!-- MAIN INSTRUMENT -->\n <div class=\"main-instrument\">\n\n <!-- TEMPERATURE DIAL -->\n <div\n class=\"temperature-ring\"\n :class=\"{\n adjusting: dialDragging,\n heating: flame === 'On'\n }\"\n >\n <svg\n class=\"ring-svg\"\n viewBox=\"0 0 300 300\"\n preserveAspectRatio=\"xMidYMid meet\"\n @pointerdown=\"startDial\"\n @pointermove=\"moveDial\"\n @pointerup=\"endDial\"\n @pointercancel=\"endDial\"\n @lostpointercapture=\"endDial\"\n >\n\n <!-- BACKGROUND TRACK -->\n <circle\n class=\"ring-track\"\n cx=\"150\"\n cy=\"150\"\n r=\"132\"\n stroke-dasharray=\"622 829\"\n transform=\"rotate(-135 150 150)\"\n />\n\n <!-- TEMPERATURE PROGRESS -->\n <circle\n class=\"ring-progress\"\n cx=\"150\"\n cy=\"150\"\n r=\"132\"\n stroke-dasharray=\"622 829\"\n :stroke-dashoffset=\"622 - (dialProgress * 622)\"\n transform=\"rotate(-135 150 150)\"\n />\n\n <!-- ENERGY SWEEP -->\n <circle\n class=\"energy-sweep\"\n cx=\"150\"\n cy=\"150\"\n r=\"132\"\n stroke-dasharray=\"38 791\"\n transform=\"rotate(-135 150 150)\"\n />\n\n <!-- INNER RING -->\n <circle\n class=\"ring-inner\"\n cx=\"150\"\n cy=\"150\"\n r=\"108\"\n />\n\n <!-- TEMPERATURE TICKS -->\n <g class=\"dial-ticks\">\n <line\n v-for=\"tick in dialTicks\"\n :key=\"'tick-' + tick.temp\"\n :x1=\"tick.x1\"\n :y1=\"tick.y1\"\n :x2=\"tick.x2\"\n :y2=\"tick.y2\"\n :class=\"{\n 'major-tick': tick.major,\n 'active-tick': tick.temp <= dialCurrentTemp\n }\"\n />\n\n <!-- TEMPERATURE LABELS -->\n <text\n v-for=\"tick in dialTicks.filter(t => t.major)\"\n :key=\"'label-' + tick.temp\"\n :x=\"tick.labelX\"\n :y=\"tick.labelY\"\n class=\"dial-label\"\n :class=\"{\n 'active-label': tick.temp <= dialCurrentTemp\n }\"\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n >\n {{ tick.temp }}°\n </text>\n </g>\n\n <!-- DIAL INDICATOR -->\n <circle\n class=\"dial-indicator\"\n :cx=\"150 + 132 * Math.cos(dialAngle * Math.PI / 180)\"\n :cy=\"150 + 132 * Math.sin(dialAngle * Math.PI / 180)\"\n r=\"8\"\n />\n\n </svg>\n\n <!-- CENTRAL TEMPERATURE DISPLAY -->\n <div class=\"temperature-center\">\n <div class=\"temp-caption\">CURRENT TEMP</div>\n\n <div class=\"ambient-temp\">\n {{ tempAmb }}<span class=\"degree\">°</span>\n </div>\n\n <div class=\"temp-unit\">CELSIUS</div>\n </div>\n\n <!-- DIAL INSTRUCTION -->\n <div class=\"dial-hint\">\n {{ dialDragging ? 'ADJUSTING SETPOINT' : 'DRAG TO ADJUST' }}\n </div>\n </div>\n\n <!-- TARGET TEMPERATURE -->\n <div class=\"target-display\">\n <div class=\"target-label\">\n TARGET TEMPERATURE\n </div>\n\n <div class=\"target-value\">\n {{ Number(dialCurrentTemp).toFixed(1) }}<span>°</span>\n </div>\n\n <div class=\"target-label\">\n DRAG THE OUTER RING TO CHANGE\n </div>\n </div>\n </div>\n\n <!-- BOTTOM BUTTONS -->\n <div class=\"button-container\">\n\n<v-card\n class=\"thermoValue stretch-button\"\n @click=\"Power\"\n>\n <v-icon\n size=\"36\"\n :class=\"{ 'power-icon-active': powerOn }\"\n >\n mdi-power\n </v-icon>\n</v-card>\n\n<!-- FAN -->\n<v-card\n class=\"thermoValue stretch-button\"\n :class=\"{ 'fan-active': fanOn }\"\n @click=\"Fan\"\n>\n <v-icon\n size=\"36\"\n :class=\"{ 'fan-spinning': fanOn }\"\n >\n mdi-fan\n </v-icon>\n</v-card>\n\n </div>\n\n </div>\n </container>\n</template>\n\n<script>\nexport default {\n props: {\n msg: Object,\n },\n\n data() {\n return {\n // Heating state\n flame: 'Off',\n powerOn: false,\n fanOn: false,\n\n // Temperatures\n tempAmb: '--',\n tempSet: '--',\n\n // Dial configuration\n dialMin: 10,\n dialMax: 25,\n dialStep: 0.25,\n\n // Interactive dial\n dialDragging: false,\n dialCurrentTemp: 20,\n\n // Pointer tracking\n activePointerId: null,\n }\n },\n\n computed: {\n // Dial progress from 0 to 1\n dialProgress() {\n return Math.max(\n 0,\n Math.min(\n 1,\n (this.dialCurrentTemp - this.dialMin) /\n (this.dialMax - this.dialMin),\n ),\n )\n },\n\n // Indicator angle\n dialAngle() {\n return -135 + this.dialProgress * 270\n },\n\n // Temperature markings\n dialTicks() {\n const ticks = []\n\n for (\n let temp = this.dialMin;\n temp <= this.dialMax;\n temp++\n ) {\n const progress =\n (temp - this.dialMin) /\n (this.dialMax - this.dialMin)\n\n const angle = -135 + progress * 270\n const radians = (angle * Math.PI) / 180\n const major = temp % 5 === 0\n\n ticks.push({\n temp,\n major,\n\n x1: 150 + (major ? 119 : 123) * Math.cos(radians),\n y1: 150 + (major ? 119 : 123) * Math.sin(radians),\n\n x2: 150 + 130 * Math.cos(radians),\n y2: 150 + 130 * Math.sin(radians),\n\n labelX: 150 + 138 * Math.cos(radians),\n labelY: 150 + 138 * Math.sin(radians),\n })\n }\n\n return ticks\n },\n },\n\n watch: {\n // Incoming Node-RED updates\n msg: {\n immediate: true,\n\n handler(newVal) {\n if (!newVal) return\n\n\n // Power status\nif (newVal.power !== undefined) {\n this.powerOn = newVal.power === 'On';\n}\n\n// Fan status\nif (newVal.fan !== undefined) {\n this.fanOn = newVal.fan === 'On';\n}\n\n\n // Heating status\n if (Array.isArray(newVal.payload)) {\n const [, , , flame] = newVal.payload\n\n if (flame !== undefined) {\n this.flame = flame\n }\n }\n\n // Ambient temperature\n if (newVal.tempAmb !== undefined) {\n this.tempAmb = newVal.tempAmb\n }\n\n // Set temperature\n if (newVal.tempSet !== undefined) {\n this.tempSet = newVal.tempSet\n\n if (!this.dialDragging) {\n const value = Number.parseFloat(newVal.tempSet)\n\n if (Number.isFinite(value)) {\n this.dialCurrentTemp = this.clampTemp(value)\n }\n }\n }\n },\n },\n },\n\n methods: {\n // Temperature limits\n clampTemp(value) {\n return Math.max(\n this.dialMin,\n Math.min(this.dialMax, value)\n )\n },\n\n Power() {\n this.send({\n topic: \"Power\"\n });\n\n this.powerOn = !this.powerOn;\n },\n\n\nFan() {\n this.send({\n topic: \"Fan\"\n });\n\n this.fanOn = !this.fanOn;\n},\n\n\n\n // Send setpoint to Node-RED\n sendSetpoint(value) {\n const temp = this.clampTemp(value)\n\n this.dialCurrentTemp = temp\n\n this.send({\n topic: 'Setpoint',\n payload: temp,\n })\n },\n\n // Calculate temperature from dial position\n getDialTemperature(event) {\n const rect =\n event.currentTarget.getBoundingClientRect()\n\n const x =\n event.clientX - rect.left - rect.width / 2\n\n const y =\n event.clientY - rect.top - rect.height / 2\n\n const distance = Math.sqrt(x * x + y * y)\n\n if (distance < 85) return null\n\n const angle =\n (Math.atan2(y, x) * 180) / Math.PI\n\n let relative = (angle + 135 + 360) % 360\n\n if (relative > 270) {\n relative = relative < 315 ? 270 : 0\n }\n\n let temp =\n this.dialMin +\n (relative / 270) *\n (this.dialMax - this.dialMin)\n\n temp =\n Math.round(temp / this.dialStep) *\n this.dialStep\n\n return this.clampTemp(temp)\n },\n\n // Start dial\n startDial(event) {\n if (this.dialDragging) return\n\n const temp = this.getDialTemperature(event)\n\n if (temp === null) return\n\n event.preventDefault()\n\n this.dialDragging = true\n this.activePointerId = event.pointerId\n\n event.currentTarget.setPointerCapture(\n event.pointerId\n )\n\n this.sendSetpoint(temp)\n },\n\n // Move dial\n moveDial(event) {\n if (!this.dialDragging) return\n\n if (event.pointerId !== this.activePointerId) return\n\n const temp = this.getDialTemperature(event)\n\n if (temp === null) return\n\n if (temp === this.dialCurrentTemp) return\n\n this.sendSetpoint(temp)\n },\n\n // End dial\n endDial(event) {\n if (\n this.activePointerId !== null &&\n event.pointerId !== undefined &&\n event.pointerId !== this.activePointerId\n ) {\n return\n }\n\n this.dialDragging = false\n this.activePointerId = null\n },\n },\n}\n</script>\n\n<style scoped>\n\n/* =================================\n NEXUS DESIGN\n================================= */\n\n.nexus {\n --cyan: #00d9ff;\n --amber: #ff9d32;\n\n width: 100%;\n box-sizing: border-box;\n padding: 18px;\n\n color: #d9f5ff;\n\n background: radial-gradient(\n ellipse at 50% 30%,\n #102a40 0%,\n #091725 48%,\n #050c15 100%\n );\n\n border: 1px solid #17394d;\n border-radius: 18px;\n\n font-family: Roboto, Arial, sans-serif;\n overflow: hidden;\n}\n\n/* HEADER */\n\n.nexus-header {\n display: flex;\n align-items: center;\n padding-bottom: 18px;\n border-bottom: 1px solid #18374a;\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 11px;\n}\n\n.brand-icon {\n font-size: 34px;\n color: var(--cyan);\n text-shadow: 0 0 15px var(--cyan);\n}\n\n.brand-name {\n font-size: 21px;\n font-weight: 800;\n letter-spacing: 4px;\n}\n\n.brand-sub {\n font-size: 8px;\n color: #65869a;\n letter-spacing: 2px;\n margin-top: 3px;\n}\n\n/* MAIN INSTRUMENT */\n\n.main-instrument {\n padding: 8px 0 20px;\n text-align: center;\n}\n\n.instrument-label,\n.target-label {\n font-size: 10px;\n letter-spacing: 2px;\n color: #648ba1;\n}\n\n/* CIRCULAR DIAL */\n\n.temperature-ring {\n position: relative;\n width: min(290px, 82vw);\n aspect-ratio: 1;\n margin: 8px auto 18px;\n\n user-select: none;\n -webkit-user-select: none;\n\n animation: nexusBoot 1.2s ease-out both;\n}\n\n/* SVG */\n\n.ring-svg {\n display: block;\n position: relative;\n z-index: 2;\n\n width: 100%;\n height: 100%;\n\n overflow: visible;\n touch-action: none;\n cursor: grab;\n}\n\n.ring-svg:active {\n cursor: grabbing;\n}\n\n.ring-svg circle {\n fill: none;\n}\n\n/* BACKGROUND TRACK */\n\n.ring-track {\n stroke: #18364a;\n stroke-width: 5;\n}\n\n/* TEMPERATURE PROGRESS */\n\n.ring-progress {\n stroke: var(--cyan);\n stroke-width: 5;\n stroke-linecap: round;\n\n filter:\n drop-shadow(0 0 5px var(--cyan))\n drop-shadow(0 0 12px #007da9);\n\n transition:\n stroke-dashoffset 0.25s ease,\n stroke 0.4s ease;\n}\n\n/* ENERGY SWEEP */\n\n.energy-sweep {\n stroke: var(--cyan);\n stroke-width: 7;\n stroke-linecap: round;\n\n filter:\n drop-shadow(0 0 5px var(--cyan))\n drop-shadow(0 0 12px var(--cyan));\n\n animation: energyTravel 4s linear infinite;\n pointer-events: none;\n}\n\n/* INNER RING */\n\n.ring-inner {\n stroke: #173448;\n stroke-width: 1;\n stroke-dasharray: 3 7;\n}\n\n/* TEMPERATURE TICKS */\n\n.dial-ticks line {\n stroke: #31546a;\n stroke-width: 1;\n transition: stroke 0.3s;\n}\n\n.dial-ticks .major-tick {\n stroke: #7194a8;\n stroke-width: 2;\n}\n\n.dial-ticks .active-tick {\n stroke: var(--cyan);\n filter: drop-shadow(0 0 3px var(--cyan));\n}\n\n.dial-label {\n font-size: 10px;\n font-weight: 500;\n fill: #648398;\n transition: fill 0.3s;\n}\n\n.dial-label.active-label {\n fill: #b8f5ff;\n filter: drop-shadow(0 0 4px var(--cyan));\n}\n\n/* DIAL INDICATOR */\n\n.dial-indicator {\n fill: #e8fcff;\n stroke: var(--cyan);\n stroke-width: 4;\n\n filter:\n drop-shadow(0 0 5px var(--cyan))\n drop-shadow(0 0 10px var(--cyan));\n\n transition:\n cx 0.08s,\n cy 0.08s,\n stroke 0.4s;\n}\n\n/* HEATING ACTIVE - AMBER */\n\n.temperature-ring.heating .ring-progress {\n stroke: var(--amber);\n\n filter:\n drop-shadow(0 0 5px var(--amber))\n drop-shadow(0 0 12px #ff7300);\n}\n\n.temperature-ring.heating .energy-sweep {\n stroke: var(--amber);\n\n filter:\n drop-shadow(0 0 6px var(--amber))\n drop-shadow(0 0 15px #ff7300);\n\n animation-duration: 2s;\n}\n\n.temperature-ring.heating .dial-indicator {\n stroke: var(--amber);\n\n filter:\n drop-shadow(0 0 8px var(--amber))\n drop-shadow(0 0 12px #ff7300);\n}\n\n.temperature-ring.heating .dial-ticks .active-tick {\n stroke: var(--amber);\n filter: drop-shadow(0 0 3px var(--amber));\n}\n\n.temperature-ring.heating .dial-label.active-label {\n fill: #ffc078;\n filter: drop-shadow(0 0 4px var(--amber));\n}\n\n/* DIAL ADJUSTMENT GLOW */\n\n.temperature-ring.adjusting .ring-progress {\n filter:\n drop-shadow(0 0 8px var(--cyan))\n drop-shadow(0 0 20px #00aaff);\n}\n\n.temperature-ring.heating.adjusting .ring-progress {\n filter:\n drop-shadow(0 0 8px var(--amber))\n drop-shadow(0 0 20px #ff7300);\n}\n\n/* CENTRAL TEMPERATURE */\n\n.temperature-center {\n position: absolute;\n inset: 0;\n z-index: 3;\n\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n\n pointer-events: none;\n}\n\n.temp-caption {\n font-size: 9px;\n letter-spacing: 2px;\n color: #648ba1;\n}\n\n.ambient-temp {\n font-size: clamp(55px, 14vw, 76px);\n line-height: 1.15;\n font-weight: 300;\n color: #e6faff;\n\n text-shadow: 0 0 20px #00a9db66;\n}\n\n.degree {\n font-size: 34px;\n color: var(--cyan);\n vertical-align: top;\n}\n\n.temp-unit {\n font-size: 9px;\n color: #63859a;\n letter-spacing: 3px;\n}\n\n/* DIAL HINT */\n\n.dial-hint {\n position: absolute;\n z-index: 4;\n bottom: 24px;\n width: 100%;\n\n text-align: center;\n\n font-size: 8px;\n letter-spacing: 2px;\n color: #527b91;\n\n pointer-events: none;\n}\n\n/* TARGET DISPLAY */\n\n.target-display {\n position: relative;\n z-index: 5;\n\n padding: 9px 15px;\n margin: 0 auto;\n max-width: 310px;\n\n background: #0c1d2b;\n border: 1px solid #1b4055;\n border-radius: 12px;\n\n box-shadow: inset 0 0 25px #00bfff08;\n}\n\n.target-value {\n font-size: 32px;\n font-weight: 300;\n color: var(--cyan);\n margin: 2px 0 5px;\n\n text-shadow: 0 0 12px #00bfff55;\n}\n\n.target-value span {\n font-size: 20px;\n}\n\n/* ANIMATIONS */\n\n@keyframes energyTravel {\n from {\n stroke-dashoffset: 0;\n opacity: 0.3;\n }\n\n 50% {\n opacity: 1;\n }\n\n to {\n stroke-dashoffset: -829;\n opacity: 0.3;\n }\n}\n\n@keyframes nexusBoot {\n from {\n opacity: 0;\n transform: scale(0.94);\n }\n\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n/* RESPONSIVE */\n\n@media (max-width: 350px) {\n .nexus {\n padding: 12px;\n }\n\n .brand-name {\n font-size: 17px;\n }\n}\n\n/* REDUCED MOTION */\n\n@media (prefers-reduced-motion: reduce) {\n .energy-sweep,\n .temperature-ring {\n animation: none;\n }\n\n .ring-progress,\n .dial-indicator {\n transition: none;\n }\n}\n\n/* =================================\n BOTTOM BUTTON LAYOUT\n================================= */\n\n.button-container {\n display: flex !important;\n flex-flow: row nowrap !important;\n justify-content: center;\n align-items: center;\n gap: 14px;\n\n width: 100%;\n padding: 5px 10px 5px;\n margin-top: 0;\n\n box-sizing: border-box;\n}\n\n.stretch-button {\n flex: 1 1 0;\n width: auto;\n max-width: 150px;\n min-width: 0;\n min-height: 75px;\n\n display: flex !important;\n justify-content: center;\n align-items: center;\n\n background: #3B3B3B !important;\n border: none !important;\n box-shadow: none !important;\n\n border-radius: 14px !important;\n overflow: hidden;\n\n color: #ffffff;\n}\n\n/* POWER BUTTON STATES */\n\n.stretch-button {\n background: #3B3B3B !important;\n color: #ffffff !important;\n transition:\n background 0.3s ease,\n color 0.3s ease,\n box-shadow 0.3s ease;\n}\n\n/* POWER ICON ACTIVE STATE */\n\n.power-icon-active {\n color: #7CFC00 !important;\n filter: drop-shadow(0 0 6px #7CFC0088);\n transition: color 0.3s ease, filter 0.3s ease;\n}\n\n\n/* FAN ACTIVE STATE */\n\n.fan-active {\n background: #3B3B3B !important;\n color: #00D9FF !important;\n box-shadow: 0 0 14px #00D9FF55 !important;\n}\n\n/* ROTATING FAN */\n\n.fan-spinning {\n animation: fanRotate 1s linear infinite;\n}\n\n@keyframes fanRotate {\n from {\n transform: rotate(0deg);\n }\n\n to {\n transform: rotate(360deg);\n }\n}\n\n\n\n</style>\n","noerr":0,"storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":610,"y":380,"wires":[["953d89927c79927c","3f0bb353bfa06f22","bab4ddb531211035","d41568c07d7fba67"]]},{"id":"546c6a08d800a5e9","type":"ui-group","name":"Thermo","page":"8f9ca4f2e85412d6","width":"4","height":"5","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"8f9ca4f2e85412d6","type":"ui-page","name":"Thermostat","ui":"e666d999ff129a18","path":"/Thermostat","icon":"mdi-thermostat","layout":"grid","theme":"f34d19a187ddca0b","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":"e666d999ff129a18","type":"ui-base","name":"Sprink1","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":5,"showDisconnectNotification":true,"allowInstall":true},{"id":"f34d19a187ddca0b","type":"ui-theme","name":"Main","colors":{"surface":"#000000","primary":"#000000","bgPage":"#000000","groupBg":"#000000","groupOutline":"#000000"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"acd310ce547eab86","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.36.0"}}]
The little blue dash on the ring spins around the circle continuously and the dash and circle changes to red when the furnace kicks on. A few things in the flow besides thermostat, When the whole new DB2 came along I really dreaded the change and it took a long time to get even remotely comfortable but now that I got to it there are a lot of really cool things you can do.
