Ghost Thermostat

Thermostat Dashboard Widget

This widget is based on the automatikas / Dashboard Nest thermostat.

With this widget you can display and control your heating/cooling via Node-red dashboard.

immagine

It's fully responsive and can be setted by touch

UPDATE: add multi thermostats on the same tab support

General Usage

You can set target temperature by pressing on down left zone of the quadrant (where is the label "SET") then widget show you another panel where you can change target temperature. Once set, just touch the top of the dial to return to the main panel

immagine immagine

in the same way, by pressing the bottom right, you can choose the mode, at this moment there are three different types of mode: "heating", "cooling" and "off".

immagine

Heating mode:

immagine

Cooling mode:

immagine

Off:

immagine

Led Rign

Ther ring arround the quandrant change color

the ring around the dial changes color as conditions turn the thermostat on or off. By default the colors are: gray for when the thermostat is off, orange for when the heating is on and blue for when the air conditioning is on. Obviously the LED will light up when the room temperature is lower than the target one for heating and vice versa for air conditioning

immagine immagine

Input and Output

you can push in input:

  • ambient_temperature Your temperature readings numeric payload.
  • target_temperature [optional] your thermostat setpoint numeric payload.
  • mode [optional] string (heating/cooling/off) payload.
  • away [optional] boolean (true/false) payload.

Input Example:

msg.topic = 'ambient_temperature';
var data = {
    'ambient_temperature':msg.payload.ambient,
    'target_temperature':msg.payload.target
}
msg.payload = data;
return msg;

you can got msg output every time switch state or target_temperature (this so you can store in globa variable) change:

  • ambient_temperature actual ambient temperature
  • target_temperature actual setted teperature
  • modeactual mode
  • switch_state swtitch state (heating if heating in on - cooling if ac is on - off if is off)
  • awayif away is enabled (treu/false)

Customizzation

You can easily customize the color of the ring, labels and icons by changing only the values in options and parameters. For the icons you have to use the unicode codes of the glyph (by default I use fontawesome)

immagine

Flow Example

In this example we take the ambient temperature from the DHT22 sensor (connected to the Rasperry PI) and, through our widget, we control the relays that switch the heating or air conditioning on or off:

immagine

ambient_temperature node:

msg.topic = 'ambient_temperature';  
var data = {
    'ambient_temperature':msg.payload || 20
}
msg.payload = data; 
return msg;

Flow

[{"id":"ab8fb1c28921f1dd","type":"ui_template","z":"d6c548e700f619c9","group":"9c90a075584ede2b","name":"GhostThermostat","order":2,"width":6,"height":6,"format":"<style>\n    @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');\n    \n    svg {\n        transition: all .6s cubic-bezier(0.175, 0.885, 0.32, 1.2);\n    }\n\n    stop {\n        transition: all .5s;\n    }\n    \n\t{{'#GhostThermostat' + $id}} .led {\n    \t-webkit-transition: all 0.5s;\n    \ttransition: all 0.5s;\n    \tfill: url({{'#GhostThermostat' + $id + 'ledColor'}});\n    }\n    \n    {{'#GhostThermostat' + $id}} .fa-text {\n       font-family: FontAwesome !important; \n    }\n    {{'#GhostThermostat' + $id}} .dial {\n        -webkit-user-select: none;\n        -moz-user-select: none;\n        -ms-user-select: none;\n        user-select: none;\n    }\n    {{'#GhostThermostat' + $id}} .qGradient {\n       fill : url({{'#GhostThermostat' + $id + 'qGradient'}});\n    }\n    {{'#GhostThermostat' + $id}} .qGradientT {\n        fill : url({{'#GhostThermostat' + $id + 'qGradientT'}});\n    }\n    {{'#GhostThermostat' + $id}} .eGradient {\n        fill : url({{'#GhostThermostat' + $id + 'eGradient'}});\n    }\n    {{'#GhostThermostat' + $id}} .lbl {\n        font-family: 'Roboto', sans-serif;\n        text-anchor: middle;\n        fill : #ffffff;\n        clip-path: url({{'#GhostThermostat' + $id + 'qClip'}});\n    }\n    {{'#GhostThermostat' + $id}} .lblDial {\n        fill: #dddddd;\n    }\n    \n    {{'#GhostThermostat' + $id}} .lblAmbient {\n        font-weight: 400;\n        clip-path: url({{'#GhostThermostat' + $id + 'qClip'}});\n    }\n    \n    {{'#GhostThermostat' + $id}} .lblAmbient tspan {\n        font-weight: 400;\n    }\n    \n    {{'#GhostThermostat' + $id}} .lblTarget {\n        font-weight: 400;\n        fill: orange;\n    }\n    \n    {{'#GhostThermostat' + $id}} .lblTarget tspan {\n        font-weight: 400;\n        fill: orange;\n        clip-path: url({{'#GhostThermostat' + $id + 'qClip'}});\n    }    \n    \n    {{'#GhostThermostat' + $id}} .nodisplay {\n        display: none !important;\n    }\n    \n    {{'#GhostThermostat' + $id}} .icon {\n        font-family: FontAwesome !important;\n    }\n    \n    {{'#GhostThermostat' + $id}} .animate {\n        transition: all 0.5s;\n    }\n\n</style>\n<div id=\"{{'GhostThermostat' + $id}}\"></div> \n<script>\nvar mousedownID = -1;\nvar ghostThermostatDial = (function() {\n    console.log(\"START\");\n\n    function createSVGElement(tag, attributes, appendTo) {\n        var element = document.createElementNS('http://www.w3.org/2000/svg', tag);\n        attr(element, attributes);\n        if (appendTo) {\n            appendTo.appendChild(element);\n        }\n        return element;\n    }\n\n    function attr(element, attrs) {\n        for (var i in attrs) {\n            element.setAttribute(i, attrs[i]);\n        }\n    }\n\n    function setClass(el, className, state) {\n        el.classList[state ? 'add' : 'remove'](className);\n    }\n\n    return function(targetElement, options) {\n        console.log(\"RET FUN\");\n        var self = this;\n\n        /*\n         * Options\n         */\n        options = options || {};\n        options = {\n            diameter: options.diameter || 400,\n            mintemp: options.mintemp || 10, // Minimum value for target temperature\n            maxtemp: options.maxtemp || 30, // Maximum value for target temperature\n            ledColors: {\n                'off': 'rgb(143,141,141)',\n                'heating': 'rgb(255,128,0)',\n                'cooling': 'rgb(81,170,214)'\n            }, //Led Ring Colors\n            labels: {\n                ambient: \"AMBIENT\",\n                set: \"SET\",\n                mode: \"MODE\",\n                minus: \"-\",\n                plus: \"+\",\n                left: \"<\",\n                right: \">\"\n            },\n            onChangeState: options.onChangeState || function() {} // Function called when  switch state change\n        };\n\n        /*\n         * Properties\n         */\n        var properties = {\n            radius: options.diameter / 2,\n            modes: [{\n                    label: \"heating\",\n                    icon: \"\\uf06d\",\n                    color: \"orange\"\n                }, {\n                    label: 'cooling',\n                    icon: \"\\uf2dc\",\n                    color: \"rgb(81,170,214)\"\n                }, {\n                    label: \"off\",\n                    icon: \"\\uf011\",\n                    color: \"rgb(230,0,0)\"\n                }\n                /*, {\n\t\t\t\tlabel: 'away',\n\t\t\t\ticon: \"\\uf1ce\",\n\t\t\t\tcolor: \"gray\"\n\t\t\t} */\n            ],\n            modeNames: [\"heating\", \"cooling\", \"off\"],\n            swtitchStates: [\"heating\", \"cooling\", \"off\"]\n        };\n\n        /*\n         * Object state\n         */\n        var state = {\n            target_temperature: options.mintemp,\n            ambient_temperature: options.maxtemp,\n            mode: properties.modes.indexOf(properties.modes[0]),\n            switch_state: 'off',\n            away: false\n        };\n\n        /*\n         * Property getter / setters\n         */\n        Object.defineProperty(this, 'target_temperature', {\n            get: function() {\n                return state.target_temperature;\n            },\n            set: function(val) {\n                state.target_temperature = rangedTemperature(+val);\n                //render()\n            }\n        });\n\n        Object.defineProperty(this, 'ambient_temperature', {\n            get: function() {\n                return state.ambient_temperature;\n            },\n            set: function(val) {\n                state.ambient_temperature = +val;\n                render();\n            }\n        });\n\n        Object.defineProperty(this, 'mode_name', {\n            get: function() {\n                return properties.modeNames[state.mode];\n            },\n            set: function(val) {\n                if (properties.modeNames.indexOf(val) >= 0) {\n                    state.mode = properties.modeNames.indexOf(val);\n                    //render();\n                }\n            }\n        });\n\n        Object.defineProperty(this, 'switch_state', {\n            get: function() {\n                return state.switch_state;\n            },\n            set: function(val) {\n                if (properties.swtitchStates.indexOf(val) >= 0) {\n                    state.switch_state = val;\n                    //render();\n                }\n            }\n        });\n\n\n        function str2bool(strvalue) {\n            return (strvalue && typeof strvalue == 'string') ? (strvalue.toLowerCase() == 'true') : (strvalue == true);\n        }\n\n        Object.defineProperty(this, 'away', {\n            get: function() {\n                return state.away;\n            },\n            set: function(val) {\n                state.away = !!str2bool(val);\n                //render();\n            }\n        });\n\n\n        /*\n         * SVG\n         */\n        var svg = createSVGElement('svg', {\n            width: '100%', //options.diameter+'px',\n            height: '100%', //options.diameter+'px',\n            viewBox: '0 0 ' + options.diameter + ' ' + options.diameter,\n            class: 'dial'\n        }, targetElement);\n\n        // DEFS \n        var defs = createSVGElement('defs', null, svg);\n\n        var qgradient = createSVGElement('linearGradient', {\n            'id': targetElement.getAttribute('id') + 'qGradient',\n            gradientTransform: 'rotate(65)'\n        }, defs);\n        var stop = createSVGElement('stop', {\n            'offset': '50%',\n            'stop-color': 'rgb(86,89,94)'\n        }, qgradient);\n        var stop = createSVGElement('stop', {\n            'offset': '65%',\n            'stop-color': 'rgb(30,30,30)'\n        }, qgradient);\n\n        var qGradientT = createSVGElement('linearGradient', {\n            'id': targetElement.getAttribute('id') + 'qGradientT',\n            gradientTransform: 'rotate(65)'\n        }, defs);\n        var stop = createSVGElement('stop', {\n            'offset': '55%',\n            'stop-color': '#3b3e43',\n            'stop-opacity': '1'\n        }, qGradientT);\n        var stop = createSVGElement('stop', {\n            'offset': '90%',\n            'stop-color': 'rgb(0,0,0)',\n            'stop-opacity': '1'\n        }, qGradientT);\n\n        var clipPath = createSVGElement('clipPath', {\n            'id': targetElement.getAttribute('id') + 'qClip',\n        }, defs);\n        var circle = createSVGElement('circle', {\n            cx: properties.radius,\n            cy: properties.radius,\n            r: properties.radius - 25\n        }, clipPath);\n\n\n        var ledRingGradient = createSVGElement('radialGradient', {\n            'id': targetElement.getAttribute('id') + 'ledColor',\n            'cx': \"50%\",\n            'cy': \"50%\",\n            'r': \"95%\",\n            'fx': \"50%\",\n            'fy': \"50%\"\n        }, defs);\n        var ledRingGradientColorIn = createSVGElement('stop', {\n            'offset': '45%',\n            'stop-color': 'rgb(255,0,130)',\n            'stop-opacity': '1'\n        }, ledRingGradient);\n        var ledRingGradientColorOut = createSVGElement('stop', {\n            'offset': '65%',\n            'stop-color': 'rgb(0,0,0)',\n            'stop-opacity': '1'\n        }, ledRingGradient);\n\n        var egradient = createSVGElement('linearGradient', {\n            'id': targetElement.getAttribute('id') + 'eGradient',\n            gradientTransform: 'rotate(55)'\n        }, defs);\n        var stop = createSVGElement('stop', {\n            'offset': '55%',\n            'stop-color': '#888888',\n            'stop-opacity': '1'\n        }, egradient);\n        var stop = createSVGElement('stop', {\n            'offset': '95%',\n            'stop-color': '#333333',\n            'stop-opacity': '1'\n        }, egradient);\n\n        // DIAL\n        var circle = createSVGElement('circle', {\n            cx: properties.radius,\n            cy: properties.radius,\n            r: properties.radius,\n            class: 'eGradient'\n        }, svg);\n        var ledRing = createSVGElement('circle', {\n            cx: properties.radius,\n            cy: properties.radius,\n            r: properties.radius - 3,\n            'stroke': 'black',\n            'stroke-width': '1',\n            class: 'led'\n        }, svg);\n        var circle = createSVGElement('circle', {\n            cx: properties.radius,\n            cy: properties.radius,\n            r: properties.radius - 20,\n            class: 'qGradient'\n        }, svg);\n        var circle = createSVGElement('circle', {\n            cx: properties.radius,\n            cy: properties.radius,\n            r: properties.radius - 25,\n            class: 'qGradient'\n        }, svg);\n        var lblMain = createSVGElement('text', {\n            x: properties.radius,\n            y: 70,\n            class: 'lbl lblDial'\n        }, svg);\n        var lblMainText = document.createTextNode(options.labels.ambient);\n        lblMain.appendChild(lblMainText);\n\n        var lblAmbient = createSVGElement('text', {\n            x: properties.radius,\n            y: 210,\n            'font-size': '160',\n            class: 'lbl lblAmbient'\n        }, svg);\n        var lblAmbientText = document.createTextNode('21');\n        lblAmbient.appendChild(lblAmbientText);\n        var lblAmbientDec = createSVGElement('tspan', {\n            'font-size': '60',\n        }, lblAmbient);\n        var lblAmbientDecText = document.createTextNode('.5');\n        lblAmbientDec.appendChild(lblAmbientDecText);\n\n        var line = createSVGElement('line', {\n            x1: 55,\n            y1: properties.radius + 35,\n            x2: options.diameter - 55,\n            y2: properties.radius + 35,\n            'stroke': '#DDDDDD',\n            'stroke-width': '1',\n            'opacity': '0.8'\n        }, svg);\n\n        var lblLeft = createSVGElement('text', {\n            x: 125,\n            y: properties.radius + 75,\n            class: 'lbl lblDial'\n        }, svg);\n        var lblLeftText = document.createTextNode(options.labels.set);\n        lblLeft.appendChild(lblLeftText);\n\n        var lblTarget = createSVGElement('text', {\n            x: 125,\n            y: properties.radius + 115,\n            'font-size': '35',\n            class: 'lbl lblTarget',\n            'id': targetElement.getAttribute('id') + 'lblTarget'\n        }, svg);\n        var lblTargetText = document.createTextNode('20');\n        lblTarget.appendChild(lblTargetText);\n\n        var lblTargetDec = createSVGElement('tspan', {\n            'font-size': '20',\n        }, lblTarget);\n\n        var lblTargetDecText = document.createTextNode('.5');\n        lblTargetDec.appendChild(lblTargetDecText);\n\n        var lblRight = createSVGElement('text', {\n            x: options.diameter - 125,\n            y: properties.radius + 75,\n            class: 'lbl lblDial'\n        }, svg);\n        var lblRightText = document.createTextNode(options.labels.mode);\n        lblRight.appendChild(lblRightText);\n\n        var lblMode = createSVGElement('text', {\n            x: options.diameter - 125,\n            y: properties.radius + 115,\n            'font-size': '35',\n            class: 'lbl lblTarget icon',\n            'id' : targetElement.getAttribute('id') + 'lblMode'\n        }, svg);\n        var lblModeText = document.createTextNode(properties.modes[0].icon);\n        lblMode.appendChild(lblModeText);\n\n        var btnSet = createSVGElement('g', {\n            transform: 'translate(200,200)'\n        }, svg);\n        var btnLeft = createSVGElement('path', {\n            d: 'M0,40 L0,175   A175,175 0 0,1 -175,40    z',\n            fill: 'blue',\n            opacity: '0',\n            'id': targetElement.getAttribute('id') + 'btnLeft'\n        }, btnSet);\n        var btnRight = createSVGElement('path', {\n            d: 'M0,40 L175,40   A175,175 0 0,1    0,175  z',\n            fill: 'red',\n            opacity: '0',\n            'id': targetElement.getAttribute('id') + 'btnRight'\n        }, btnSet);\n\n\n\n        btnLeft.onclick = function() {\n            setTargetClick();\n        };\n\n        btnRight.onclick = function() {\n            setModeClick();\n        };\n\n        var targetPanel = false;\n        var modePanel = false;\n\n        var lblAmbientAttributes = {\n            x: lblAmbient.getAttribute('x'),\n            y: lblAmbient.getAttribute('y'),\n            size: lblAmbient.getAttribute('font-size')\n        };\n\n        var lblAmbientDecAttributes = {\n            x: lblAmbientDec.getAttribute('x'),\n            y: lblAmbientDec.getAttribute('y'),\n            size: lblAmbientDec.getAttribute('font-size')\n        };\n\n        var lblTargetAttributes = {\n            x: lblTarget.getAttribute('x'),\n            y: lblTarget.getAttribute('y'),\n            size: lblTarget.getAttribute('font-size')\n        };\n\n        var lblTargetDecAttributes = {\n            x: lblTargetDec.getAttribute('x'),\n            y: lblTargetDec.getAttribute('y'),\n            size: lblTargetDec.getAttribute('font-size')\n        };\n\n        var lblModeAttributes = {\n            x: lblMode.getAttribute('x'),\n            y: lblMode.getAttribute('y'),\n            size: lblMode.getAttribute('font-size')\n        };\n\n        var lblRightAttributes = {\n            x: lblRight.getAttribute('x'),\n            y: lblRight.getAttribute('y'),\n            size: lblRight.getAttribute('font-size')\n        };\n\n        var lblLeftAttributes = {\n            x: lblLeft.getAttribute('x'),\n            y: lblLeft.getAttribute('y'),\n            size: lblLeft.getAttribute('font-size')\n        };\n\n        render();\n\n        function setAmbientTemperature(ambientTemp) {\n            var splitValues = separateDecValue(ambientTemp);\n            lblAmbientText.textContent = splitValues.int;\n            lblAmbientDecText.textContent = splitValues.dec;\n        };\n\n\n        function calcTargetTemperature(operation) {\n            let currentTemp = Number(parseFloat(lblTargetText.textContent + lblTargetDecText.textContent)).toFixed(1);\n            let targetTemp = (operation == '-' ? Number(Number(currentTemp) - 0.5).toFixed(1) : Number(Number(currentTemp) + 0.5).toFixed(1));\n            targetTemp = rangedTemperature(targetTemp);\n            setTargetTemperature(targetTemp);\n            chkSwitchState();\n        };\n\n        function setTargetTemperature(targetTemp) {\n            var splitValues = separateDecValue(targetTemp);\n            lblTargetText.textContent = splitValues.int;\n            lblTargetDecText.textContent = splitValues.dec;\n            if (state.target_temperature != targetTemp) {\n                state.target_temperature = targetTemp\n                sendMsg();\n            };\n        };\n\n        function separateDecValue(floatFalue) {\n            var int = Math.floor(floatFalue);\n            var dec = Math.floor(((floatFalue % 1) * 10)) > 0 ? (\".\" + Math.floor(((floatFalue % 1) * 10))) : \"\";\n            return {\n                int,\n                dec\n            };\n        };\n\n        function rangedTemperature(temperature) {\n            temperature = temperature < options.mintemp ? options.maxtemp : temperature;\n            temperature = temperature > options.maxtemp ? options.mintemp : temperature;\n            return temperature;\n        };\n\n        function chkSwitchState() {\n            console.log(\"chkSwitchState\");\n            var switchState = state.switch_state;\n            switch (state.mode) {\n                case 0:\n                    switchState = state.ambient_temperature < state.target_temperature ? 'heating' : 'off';\n                    break;\n                case 1:\n                    switchState = state.ambient_temperature > state.target_temperature ? 'cooling' : 'off';\n                    break;\n                default:\n                    switchState = 'off';\n            };\n\n            ledRingGradientColorIn.setAttribute('stop-color', options.ledColors[state.switch_state]);\n\n            if (state.switch_state != switchState) {\n                state.switch_state = switchState;\n                sendMsg();\n            };\n        };\n\n\n        function resetButton() {\n            btnLeft.onmousedown = \"\";\n            btnLeft.onmouseup = \"\";\n            btnLeft.onclick = function() {\n                setTargetClick();\n            };\n            btnRight.onmousedown = \"\";\n            btnRight.onmouseup = \"\";\n            btnRight.onclick = function() {\n                setModeClick();\n            };\n        };\n\n        function switchMainView(element, originalAttributes, mainLabel, leftLabel, rightLabel, panelState) {\n            setClass(lblAmbient, \"nodisplay\", panelState);\n            setClass(lblMain, \"animate\", panelState);\n            setClass(lblLeft, \"animate\", panelState);\n            setClass(lblRight, \"animate\", panelState);\n            setClass(element, \"animate\", panelState);\n\n            lblMainText.textContent = panelState ? mainLabel : options.labels.ambient;\n            lblLeftText.textContent = panelState ? leftLabel : options.labels.set;\n\n            lblLeft.setAttribute('y', panelState ? Number(lblLeftAttributes.y) + 40 : lblLeftAttributes.y);\n            lblLeft.setAttribute('font-size', panelState ? \"3.5em\" : \"1em\");\n\n            lblRightText.textContent = panelState ? rightLabel : options.labels.mode;\n            lblRight.setAttribute('y', panelState ? Number(lblRightAttributes.y) + 40 : lblRightAttributes.y);\n            lblRight.setAttribute('font-size', panelState ? \"3.5em\" : \"1em\");\n\n            element.setAttribute('x', panelState ? lblAmbientAttributes.x : originalAttributes.x);\n            element.setAttribute('x', panelState ? lblAmbientAttributes.x : originalAttributes.x);\n            element.setAttribute('y', panelState ? lblAmbientAttributes.y : originalAttributes.y);\n            element.setAttribute('font-size', panelState ? lblAmbientAttributes.size : originalAttributes.size);\n\n        };\n\n\n        function setTargetClick() {\n\n            targetPanel = targetPanel ? false : true;\n            setClass(lblMode, \"nodisplay\", targetPanel);\n            switchMainView(lblTarget, lblTargetAttributes, options.labels.set, options.labels.minus, options.labels.plus, targetPanel);\n\n            lblTargetDec.setAttribute('font-size', targetPanel ? lblAmbientDecAttributes.size : lblTargetDecAttributes.size);\n\n            if (targetPanel) {\n                btnLeft.onclick = \"\";\n                btnRight.onclick = \"\";\n\n                btnLeft.onmousedown = function() {\n                    calcTargetTemperature(\"-\");\n                    if (mousedownID == -1) { //Prevent multimple loops!\n                        mousedownID = setInterval(calcTargetTemperature, 500, '-');\n                    }\n                };\n                btnLeft.onmouseup = function() {\n                    if (mousedownID != -1) { //Only stop if exists\n                        clearInterval(mousedownID);\n                        mousedownID = -1;\n                    }\n                };\n\n                btnRight.onmousedown = function() {\n                    calcTargetTemperature(\"+\");\n                    if (mousedownID == -1) { //Prevent multimple loops!\n                        mousedownID = setInterval(calcTargetTemperature, 500, '+');\n                    }\n                };\n                btnRight.onmouseup = function() {\n                    if (mousedownID != -1) { //Only stop if exists\n                        clearInterval(mousedownID);\n                        mousedownID = -1;\n                    }\n                };\n\n                lblTarget.onclick = function() {\n                    setTargetClick();\n                };\n            } else {\n                resetButton()\n            }\n        };\n\n        function setModeClick() {\n\n            modePanel = modePanel ? false : true;\n            setClass(lblTarget, \"nodisplay\", modePanel);\n            switchMainView(lblMode, lblModeAttributes, options.labels.mode, options.labels.left, options.labels.right, modePanel);\n\n            if (modePanel) {\n\n                btnLeft.onclick = function() {\n                    mode = state.mode;\n                    mode = --mode < 0 ? properties.modes.length - 1 : mode;\n                    console.log(\"MODE :\" + mode);\n                    setModeName(properties.modeNames[mode]);\n                    chkSwitchState();\n                    sendMsg();\n                };\n\n                btnRight.onclick = function() {\n                    mode = state.mode;\n                    mode = ++mode > properties.modes.length - 1 ? 0 : mode;\n                    console.log(\"MODE :\" + mode);\n                    setModeName(properties.modeNames[mode]);\n                    chkSwitchState();\n                    sendMsg();\n                };\n//                document.getElementById(targetElement.getAttribute('id') + \"lblMode\").onclick = function() {\n                lblMode.onclick = function() {\n                    setModeClick();\n                };\n            } else {\n                resetButton()\n            }\n        };\n\n        function setModeName(modeName) {\n            lblMode.textContent = properties.modes[properties.modeNames.indexOf(modeName)].icon;\n            lblMode.style.fill = properties.modes[properties.modeNames.indexOf(modeName)].color;\n            state.mode = properties.modeNames.indexOf(modeName);\n        };\n\n        function sendMsg() {\n            if (typeof options.onChangeState == 'function') {\n                options.onChangeState(state.switch_state);\n            }\n        };\n\n        function render() {\n            console.log(\"RENDER\");\n            setAmbientTemperature(self.ambient_temperature);\n            setTargetTemperature(self.target_temperature);\n            setModeName(self.mode_name);\n            chkSwitchState();\n        };\n\n    };\n})();\n\nvar initializing = true;\n(function(scope) {\n    console.log(\"scope.id = GhostThermostat\" + scope.$id);\n    $(function() {\n        var ghostThermostat = new ghostThermostatDial(document.getElementById('GhostThermostat' + scope.$id), {\n            onChangeState: function() {\n                var p = {\n                    \"ambient_temperature\": ghostThermostat.ambient_temperature,\n                    \"target_temperature\": ghostThermostat.target_temperature,\n                    \"mode\": ghostThermostat.mode_name,\n                    \"switch_state\": ghostThermostat.switch_state,\n                    \"away\": ghostThermostat.away\n                };\n                scope.send({\n                    topic: \"changed_state\",\n                    payload: p\n                });\n            }\n        });\n\n\n        scope.$watch('msg', function(data) {\n            if (initializing) {\n                initializing = false;\n            } else {\n                ghostThermostat.ambient_temperature = data.payload.ambient_temperature || ghostThermostat.ambient_temperature;\n                ghostThermostat.target_temperature = data.payload.target_temperature || ghostThermostat.target_temperature;\n                ghostThermostat.mode_name = data.payload.mode || ghostThermostat.mode_name;\n                ghostThermostat.switch_state = data.payload.switch_state || ghostThermostat.switch_state;\n                ghostThermostat.away = data.payload.away || ghostThermostat.away;\n            }\n        });\n    });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","className":"","x":1030,"y":420,"wires":[["9854a9e5fd2ac2b8","2d54542cf9c837f1"]],"icon":"font-awesome/fa-tachometer"},{"id":"9c90a075584ede2b","type":"ui_group","name":"Thermostat","tab":"f394bc89e321d6f1","order":1,"disp":false,"width":"10","collapse":false,"className":""},{"id":"f394bc89e321d6f1","type":"ui_tab","name":"Thermostat","icon":"dashboard","disabled":false,"hidden":true}]

Flow Info

Created 2 years, 11 months ago
Updated 2 years, 9 months ago
Rating: 4.285714285714286 7

Actions

Rate:

Node Types

Other
  • ui_group (x1)
  • ui_tab (x1)
  • ui_template (x1)

Tags

  • thermostat
  • flow
  • temperature
  • ui
  • template
  • dashboard
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option