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.
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
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".
Heating mode:
Cooling mode:
Off:
Led Rign
Ther ring arround the quandrant change colorthe 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
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 temperaturetarget_temperature
actual setted teperaturemode
actual modeswitch_state
swtitch state (heating
if heating in on -cooling
if ac is on -off
if is off)away
if away is enabled (treu
/false
)
Customizzation
You can easily customize the color of the ring, labels and icons by changing only the values inoptions
and parameters
.
For the icons you have to use the unicode codes of the glyph (by default I use fontawesome)
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:
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}]