Smart Heating (alexa, google assistant compatible)
Winter is coming again, so it's time to revisit my $5 smart thermostat upgrade. I have modified the design to make it work with Google Home and Google Assistant-enabled devices.
Features:
- Complete Google Home integration
- Google Assistant integration
- Web UI with Nest alike interface
- Amazon Alexa compatible
You will need the following nodes:
- node-red-node-smooth
- node-red-contrib-alexa-home-skill -
- node-red-dashboard (for charts)
You will need **Sonoff Basics **buy here:
Settings
- defaultTemp (temperature the thermostat is set to if no temp value is provided)
- ecoL (low temp bracket responsible for displaying the leaf)
- ecoH (high temp bracket responsible for displaying the leaf)
More about me:
If you want to get the latest updates to this project you can follow me via your preferred social media:
And if you feeling like buying me a coffee or supporting me in a more continuous way:
I hope you have enjoyed the project!
[{"id":"21187818.0fa5a8","type":"subflow","name":"Heating Settings","info":"","category":"","in":[{"x":340,"y":100,"wires":[{"id":"bea13c7f.aa637"}]}],"out":[{"x":600,"y":100,"wires":[{"id":"bea13c7f.aa637","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"bea13c7f.aa637","type":"function","z":"21187818.0fa5a8","name":"Set settings","func":"var x = env.get(\"ecoL\"); \nvar y = env.get(\"ecoH\");\nvar z = env.get(\"defaultTemp\"); \n\nflow.set(\"$parent.ecoLOW\", x); \nflow.set(\"$parent.ecoHIGH\", y);\nflow.set(\"$parent.defaultTemp\", z);\n\nmsg.payload = \"Your settings has been saved: ecoL: \"+x+ \", ecoH: \"+y+ \", default TempL \"+z;\n\nreturn msg;","outputs":1,"noerr":0,"x":470,"y":100,"wires":[[]]},{"id":"951f89cd.86b0f8","type":"tab","label":"Heating Alexa + Google","disabled":false,"info":"Winter is coming again, so it's time to revisit my $5 smart thermostat upgrade. I have modified the design to make it work with Google Home and Google Assistant-enabled devices.\n\n - [**Hardware** and **Alexa** Guide](https://notenoughtech.com/home-automation/nest-your-old-thermostat-under-5/)\n - [**Google Hom**e and **Google Assistant** integration](https://notenoughtech.com/home-automation/smart-heating/): \n\n**Features**:\n\n - Complete Google Home integration\n - Google Assistant integration\n - Web UI with Nest alike interface\n - Amazon Alexa compatible \n\nYou will need the following nodes:\n\n - node-red-node-smooth\n - node-red-contrib-alexa-home-skill - \n - node-red-dashboard (for charts)\n\n\n---\n\n \n\n## You will need **Sonoff Basics **buy here:\n\n - [Aliexpress](http://s.click.aliexpress.com/e/FrAUeiak)\n - [Banggood](https://www.banggood.com/custlink/3GvG4V3PqE)\n - [Gearbest](https://www.gearbest.com/smart-socket-plug/pp_009363914208.html?lkid=64298652)\n - [AmazonUK](https://amzn.to/314LaMi)\n - [AmazonUS](https://amzn.to/3175oEV)\n - [Itead store](http://shrsl.com/1jqli)\n\n\n---\n\n# Settings\n\n - **defaultTemp** (temperature the thermostat is set to if no temp value is provided)\n - **ecoL** (low temp bracket responsible for displaying the leaf)\n - **ecoH** (high temp bracket responsible for displaying the leaf)\n - "},{"id":"8673d19b.a7ef2","type":"mqtt in","z":"951f89cd.86b0f8","name":"Read Temp DHT11","topic":"sonoff/tele/SENSOR","qos":"0","datatype":"json","broker":"eca6af44.5297b","x":250,"y":260,"wires":[["532009d8.e04728","5f346d9c.e70ff4"]]},{"id":"b29fa1ab.7b4a5","type":"ui_slider","z":"951f89cd.86b0f8","name":"Slider","label":"Target Temp","tooltip":"","group":"92170a5b.37a1d8","order":2,"width":0,"height":0,"passthru":true,"outs":"all","topic":"slider","min":"0","max":"30","step":1,"x":630,"y":460,"wires":[["6c9534b2.5995ec"]]},{"id":"5ba0f811.2fed28","type":"function","z":"951f89cd.86b0f8","name":"Process Alexa responses","func":"if (msg.command === \"GetTemperatureReadingRequest\"){\n x =flow.get('TempTarget');\n msg.extra = {\n \"temperatureReading\": {\n \"value\": x},\n \"applianceResponseTimestamp\": new Date().toISOString()};\n msg.payload = true;\n return msg;\n}\n\nif (msg.command === \"SetTargetTemperatureRequest\"){\nif (msg.payload < 10 || msg.payload > 30) {\n var range = {\n min: 10.0,\n max: 30.0\n }\n msg.payload = false;\n msg.extra = range;\n} \nelse {\n msg.extra = {\n targetTemperature: {\n value: msg.payload\n }\n };\n msg.payload = true;\n}\nreturn msg;\n}\nvar defaultTemp = flow.get(\"defaultTemp\");\n\n\n// Turn it on\nif (msg.command === \"TurnOnRequest\"){\n msg.payload = true;\n flow.set('away', false);\n flow.set('TempTarget', defaultTemp);\n return msg;\n \n}\n// Turn it off\nif (msg.command === \"TurnOffRequest\"){\n msg.payload = true;\n flow.set('away', true);\n \n return msg;\n}\n","outputs":1,"noerr":0,"x":790,"y":640,"wires":[["6b4a3598.c3f96c"]]},{"id":"1863a176.4815cf","type":"mqtt out","z":"951f89cd.86b0f8","name":"Control Sonoff","topic":"sonoff/cmnd/POWER1","qos":"0","retain":"","broker":"eca6af44.5297b","x":1300,"y":580,"wires":[]},{"id":"a75a4486.9351f8","type":"function","z":"951f89cd.86b0f8","name":"Control relay","func":"var defaultTemp = flow.get(\"defaultTemp\");\n\n//msg1 = OFF|ON\n//Mmsg2 = off|heat\n\n\n//coming frrom alexa\nif (msg.command === \"TurnOffRequest\"){\n flow.set(\"heatingState\", \"off\");\n flow.set('away', true);\n var msg1 = { payload:\"OFF\" };\n var msg2 = { payload:\"off\" };\n return [msg1, msg2];\n}\n\nif (msg.command === \"TurnOnRequest\"){\n flow.set('TempTarget', defaultTemp);\n flow.set(\"heatingState\", \"heating\");\n flow.set('away', false);\n msg1 = { payload:\"ON\" };\n msg2 = { payload:\"heat\" };\n return [msg1, msg2];\n}\n\n//coming from an update node\nif (msg.topic === \"update\"){\n var z = flow.get('heatingState');\n if(z === \"heating\"){\n msg1 = { payload: \"ON\"};\n msg2 = { payload:\"heat\" };\n return [msg1, msg2];\n }\n if(z === \"off\"){\n msg1 = { payload: \"OFF\"};\n msg2 = { payload:\"off\" };\n return [msg1, msg2];\n }\n}\n","outputs":2,"noerr":0,"x":750,"y":580,"wires":[["1863a176.4815cf"],["afae114a.380a2"]]},{"id":"532009d8.e04728","type":"smooth","z":"951f89cd.86b0f8","name":"Average temp","property":"payload.DHT11.Temperature","action":"mean","count":"15","round":"2","mult":"single","reduce":false,"x":520,"y":300,"wires":[["7472cfef.924b5"]]},{"id":"f47dce00.1b751","type":"alexa-home","z":"951f89cd.86b0f8","conf":"4ec600a6.406ae","device":"35103","acknoledge":false,"name":"Heating","topic":"Heating","x":330,"y":620,"wires":[["5ba0f811.2fed28","a75a4486.9351f8","6c9534b2.5995ec"]]},{"id":"6b4a3598.c3f96c","type":"alexa-home-resp","z":"951f89cd.86b0f8","x":1320,"y":640,"wires":[]},{"id":"705a8ee8.30b36","type":"inject","z":"951f89cd.86b0f8","name":"Refresh every 5 sec","topic":"update","payload":"upate","payloadType":"str","repeat":"5","crontab":"","once":false,"onceDelay":0.1,"x":260,"y":460,"wires":[["a75a4486.9351f8","a4a96a87.00c888","6c9534b2.5995ec"]]},{"id":"a4a96a87.00c888","type":"function","z":"951f89cd.86b0f8","name":"Update slider","func":"msg.payload = flow.get('TempTarget');\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":460,"wires":[["b29fa1ab.7b4a5"]]},{"id":"94ba62db.a830d","type":"comment","z":"951f89cd.86b0f8","name":"Measure DHT11 temp and update var","info":"Gets the Temp, averages last X readings \nand updates the variable for the next push","x":310,"y":220,"wires":[]},{"id":"1f3b265b.04c31a","type":"comment","z":"951f89cd.86b0f8","name":"Update Dial ","info":"","x":490,"y":420,"wires":[]},{"id":"db9ebea0.63e1","type":"comment","z":"951f89cd.86b0f8","name":"Control Relay","info":"","x":750,"y":720,"wires":[]},{"id":"c0e38a08.cbfd98","type":"comment","z":"951f89cd.86b0f8","name":"Alexa Responses","info":"","x":440,"y":560,"wires":[]},{"id":"f462b894.b2b1c8","type":"mqtt in","z":"951f89cd.86b0f8","name":"Gbridge GH SET","topic":"gBridge/u2491/heating/onoff","qos":"0","datatype":"auto","broker":"c4b43ba4.186ae8","x":440,"y":820,"wires":[["c8a32e2b.b1467"]]},{"id":"6c9534b2.5995ec","type":"function","z":"951f89cd.86b0f8","name":"update","func":"\nvar targetTemp = flow.get('TempTarget'); \nvar ambientTemp = flow.get('TempAmbient'); \nvar state = flow.get('heatingState'); \nvar leaf = flow.get('leaf'); \nvar hvac = flow.get('hvac'); \nvar away = flow.get('away'); \nvar heatswitch = flow.get('heatingSwitch'); \nvar ecoLOW = flow.get('ecoLOW'); \nvar ecoHIGH = flow.get('ecoHIGH'); \n\n// update the heating state\n\nif(msg.topic === \"update\"){\nif (ambientTemp < targetTemp){\n flow.set('heatingState', \"heating\");\n flow.set('heatingSwitch', \"ON\");\n \n}\n\nif (ambientTemp >= targetTemp){\n flow.set('heatingState', \"off\");\n flow.set('heatingSwitch', \"OFF\");\n}\n\n// display the leaf\nif (ambientTemp > ecoLOW && ambientTemp < ecoHIGH){\n flow.set('leaf', true);\n}\n\n//away\nif (away === true){\n flow.set('hvac', \"off\");\n flow.set('heatingSwitch', \"OFF\");\n flow.set('heatingState', \"off\");\n flow.set('TempTarget', 6);\n }\n}\n\n//Alexa\nif (msg.command === \"SetTargetTemperatureRequest\") {\n flow.set('away', false);\n flow.set('TempTarget', msg.payload);\n}\nif (msg.command === \"GetTemperatureReadingRequest\"){}\n\n//slider\nif (msg.topic === \"slider\") {\n flow.set('away', false);\n flow.set('TempTarget', msg.payload);\n msg.topic = \"setValues\";\n msg.ambient = ambientTemp;\n msg.target = msg.payload;\n msg.hvac = state;\n msg.leaf = leaf;\n msg.away = away;\n return msg;\n}\n\n\nmsg.topic = \"setValues\";\nmsg.ambient = ambientTemp;\nmsg.target = targetTemp;\nmsg.hvac = state;\nmsg.leaf = leaf;\nmsg.away = away;\n\n\nreturn msg;","outputs":1,"noerr":0,"x":730,"y":540,"wires":[["410df88d.04cae8","1f3c171e.823369","5d8914c7.0f1e0c","85e7033e.ddb46"]]},{"id":"410df88d.04cae8","type":"ui_template","z":"951f89cd.86b0f8","group":"92170a5b.37a1d8","name":"Nest 2","order":1,"width":"6","height":"6","format":"<div id=\"thermostat\"></div>\n\n<style>\n@import url(http://fonts.googleapis.com/css?family=Open+Sans:300);\n#thermostat {\n margin: 0 auto;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n.dial {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.dial.away .dial__ico__leaf {\n visibility: hidden;\n}\n.dial.away .dial__lbl--target {\n visibility: hidden;\n}\n.dial.away .dial__lbl--target--half {\n visibility: hidden;\n}\n.dial.away .dial__lbl--away {\n opacity: 1;\n}\n.dial .dial__shape {\n -webkit-transition: fill 0.5s;\n transition: fill 0.5s;\n}\n.dial__ico__leaf {\n fill: #13EB13;\n opacity: 0;\n -webkit-transition: opacity 0.5s;\n transition: opacity 0.5s;\n pointer-events: none;\n}\n.dial.has-leaf .dial__ico__leaf {\n display: block;\n opacity: 1;\n pointer-events: initial;\n}\n.dial__editableIndicator {\n fill: white;\n fill-rule: evenodd;\n opacity: 0;\n -webkit-transition: opacity 0.5s;\n transition: opacity 0.5s;\n}\n.dial--edit .dial__editableIndicator {\n opacity: 1;\n}\n.dial--state--off .dial__shape {\n fill: #3d3c3c;\n}\n.dial--state--heating .dial__shape {\n fill: #E36304;\n}\n.dial--state--cooling .dial__shape {\n fill: #007AF1;\n}\n.dial__ticks path {\n fill: rgba(255, 255, 255, 0.3);\n}\n.dial__ticks path.active {\n fill: rgba(255, 255, 255, 0.8);\n}\n.dial text {\n fill: white;\n text-anchor: middle;\n font-family: Helvetica, sans-serif;\n alignment-baseline: central;\n}\n.dial__lbl--target {\n font-size: 120px;\n font-weight: bold;\n}\n.dial__lbl--target--half {\n font-size: 40px;\n font-weight: bold;\n opacity: 0;\n -webkit-transition: opacity 0.1s;\n transition: opacity 0.1s;\n}\n.dial__lbl--target--half.shown {\n opacity: 1;\n -webkit-transition: opacity 0s;\n transition: opacity 0s;\n}\n.dial__lbl--ambient {\n font-size: 22px;\n font-weight: bold;\n}\n.dial__lbl--away {\n font-size: 72px;\n font-weight: bold;\n opacity: 0;\n pointer-events: none;\n}\n#controls {\n font-family: Open Sans;\n background-color: rgba(255, 255, 255, 0.25);\n padding: 20px;\n border-radius: 5px;\n position: absolute;\n left: 50%;\n -webkit-transform: translatex(-50%);\n transform: translatex(-50%);\n margin-top: 20px;\n}\n#controls label {\n text-align: left;\n display: block;\n}\n#controls label span {\n display: inline-block;\n width: 200px;\n text-align: right;\n font-size: 0.8em;\n text-transform: uppercase;\n}\n#controls p {\n margin: 0;\n margin-bottom: 1em;\n padding-bottom: 1em;\n border-bottom: 2px solid #ccc;\n}\n</style>\n<script>\n var thermostatDial = (function() {\n\t\n\t/*\n\t * Utility functions\n\t */\n\t\n\t// Create an element with proper SVG namespace, optionally setting its attributes and appending it to another element\n\tfunction createSVGElement(tag,attributes,appendTo) {\n\t\tvar element = document.createElementNS('http://www.w3.org/2000/svg',tag);\n\t\tattr(element,attributes);\n\t\tif (appendTo) {\n\t\t\tappendTo.appendChild(element);\n\t\t}\n\t\treturn element;\n\t}\n\t\n\t// Set attributes for an element\n\tfunction attr(element,attrs) {\n\t\tfor (var i in attrs) {\n\t\t\telement.setAttribute(i,attrs[i]);\n\t\t}\n\t}\n\t\n\t// Rotate a cartesian point about given origin by X degrees\n\tfunction rotatePoint(point, angle, origin) {\n\t\tvar radians = angle * Math.PI/180;\n\t\tvar x = point[0]-origin[0];\n\t\tvar y = point[1]-origin[1];\n\t\tvar x1 = x*Math.cos(radians) - y*Math.sin(radians) + origin[0];\n\t\tvar y1 = x*Math.sin(radians) + y*Math.cos(radians) + origin[1];\n\t\treturn [x1,y1];\n\t}\n\t\n\t// Rotate an array of cartesian points about a given origin by X degrees\n\tfunction rotatePoints(points, angle, origin) {\n\t\treturn points.map(function(point) {\n\t\t\treturn rotatePoint(point, angle, origin);\n\t\t});\n\t}\n\t\n\t// Given an array of points, return an SVG path string representing the shape they define\n\tfunction pointsToPath(points) {\n\t\treturn points.map(function(point, iPoint) {\n\t\t\treturn (iPoint>0?'L':'M') + point[0] + ' ' + point[1];\n\t\t}).join(' ')+'Z';\n\t}\n\t\n\tfunction circleToPath(cx, cy, r) {\n\t\treturn [\n\t\t\t\"M\",cx,\",\",cy,\n\t\t\t\"m\",0-r,\",\",0,\n\t\t\t\"a\",r,\",\",r,0,1,\",\",0,r*2,\",\",0,\n\t\t\t\"a\",r,\",\",r,0,1,\",\",0,0-r*2,\",\",0,\n\t\t\t\"z\"\n\t\t].join(' ').replace(/\\s,\\s/g,\",\");\n\t}\n\t\n\tfunction donutPath(cx,cy,rOuter,rInner) {\n\t\treturn circleToPath(cx,cy,rOuter) + \" \" + circleToPath(cx,cy,rInner);\n\t}\n\t\n\t// Restrict a number to a min + max range\n\tfunction restrictToRange(val,min,max) {\n\t\tif (val < min) return min;\n\t\tif (val > max) return max;\n\t\treturn val;\n\t}\n\t\n\t// Round a number to the nearest 0.5\n\tfunction roundHalf(num) {\n\t\treturn Math.round(num*2)/2;\n\t}\n\t\n\tfunction setClass(el, className, state) {\n\t\tel.classList[state ? 'add' : 'remove'](className);\n\t}\n\t\n\t/*\n\t * The \"MEAT\"\n\t */\n\n\treturn function(targetElement, options) {\n\t\tvar self = this;\n\t\t\n\t\t/*\n\t\t * Options\n\t\t */\n\t\toptions = options || {};\n\t\toptions = {\n\t\t\tdiameter: options.diameter || 400,\n\t\t\tminValue: options.minValue || 10, // Minimum value for target temperature\n\t\t\tmaxValue: options.maxValue || 30, // Maximum value for target temperature\n\t\t\tnumTicks: options.numTicks || 200, // Number of tick lines to display around the dial\n\t\t\tonSetTargetTemperature: options.onSetTargetTemperature || function() {}, // Function called when new target temperature set by the dial\n\t\t};\n\t\t\n\t\t/*\n\t\t * Properties - calculated from options in many cases\n\t\t */\n\t\tvar properties = {\n\t\t\ttickDegrees: 300, // Degrees of the dial that should be covered in tick lines\n\t\t\trangeValue: options.maxValue - options.minValue,\n\t\t\tradius: options.diameter/2,\n\t\t\tticksOuterRadius: options.diameter / 30,\n\t\t\tticksInnerRadius: options.diameter / 8,\n\t\t\thvac_states: ['off', 'heating', 'cooling'],\n\t\t\tdragLockAxisDistance: 15,\n\t\t}\n\t\tproperties.lblAmbientPosition = [properties.radius, properties.ticksOuterRadius-(properties.ticksOuterRadius-properties.ticksInnerRadius)/2]\n\t\tproperties.offsetDegrees = 180-(360-properties.tickDegrees)/2;\n\t\t\n\t\t/*\n\t\t * Object state\n\t\t */\n\t\tvar state = {\n\t\t\ttarget_temperature: options.minValue,\n\t\t\tambient_temperature: options.minValue,\n\t\t\thvac_state: properties.hvac_states[0],\n\t\t\thas_leaf: false,\n\t\t\taway: false\n\t\t};\n\t\t\n\t\t/*\n\t\t * Property getter / setters\n\t\t */\n\t\tObject.defineProperty(this,'target_temperature',{\n\t\t\tget: function() {\n\t\t\t\treturn state.target_temperature;\n\t\t\t},\n\t\t\tset: function(val) {\n\t\t\t\tstate.target_temperature = restrictTargetTemperature(+val);\n\t\t\t\trender();\n\t\t\t}\n\t\t});\n\t\tObject.defineProperty(this,'ambient_temperature',{\n\t\t\tget: function() {\n\t\t\t\treturn state.ambient_temperature;\n\t\t\t},\n\t\t\tset: function(val) {\n\t\t\t\tstate.ambient_temperature = roundHalf(+val);\n\t\t\t\trender();\n\t\t\t}\n\t\t});\n\t\tObject.defineProperty(this,'hvac_state',{\n\t\t\tget: function() {\n\t\t\t\treturn state.hvac_state;\n\t\t\t},\n\t\t\tset: function(val) {\n\t\t\t\tif (properties.hvac_states.indexOf(val)>=0) {\n\t\t\t\t\tstate.hvac_state = val;\n\t\t\t\t\trender();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tObject.defineProperty(this,'has_leaf',{\n\t\t\tget: function() {\n\t\t\t\treturn state.has_leaf;\n\t\t\t},\n\t\t\tset: function(val) {\n\t\t\t\tstate.has_leaf = !!val;\n\t\t\t\trender();\n\t\t\t}\n\t\t});\n\t\tObject.defineProperty(this,'away',{\n\t\t\tget: function() {\n\t\t\t\treturn state.away;\n\t\t\t},\n\t\t\tset: function(val) {\n\t\t\t\tstate.away = !!val;\n\t\t\t\trender();\n\t\t\t}\n\t\t});\n\t\t\n\t\t/*\n\t\t * SVG\n\t\t */\n\t\tvar svg = createSVGElement('svg',{\n\t\t\twidth: '100%', //options.diameter+'px',\n\t\t\theight: '100%', //options.diameter+'px',\n\t\t\tviewBox: '0 0 '+options.diameter+' '+options.diameter,\n\t\t\tclass: 'dial'\n\t\t},targetElement);\n\t\t// CIRCULAR DIAL\n\t\tvar circle = createSVGElement('circle',{\n\t\t\tcx: properties.radius,\n\t\t\tcy: properties.radius,\n\t\t\tr: properties.radius,\n\t\t\tclass: 'dial__shape'\n\t\t},svg);\n\t\t// EDITABLE INDICATOR\n\t\tvar editCircle = createSVGElement('path',{\n\t\t\td: donutPath(properties.radius,properties.radius,properties.radius-4,properties.radius-8),\n\t\t\tclass: 'dial__editableIndicator',\n\t\t},svg);\n\t\t\n\t\t/*\n\t\t * Ticks\n\t\t */\n\t\tvar ticks = createSVGElement('g',{\n\t\t\tclass: 'dial__ticks'\t\n\t\t},svg);\n\t\tvar tickPoints = [\n\t\t\t[properties.radius-1, properties.ticksOuterRadius],\n\t\t\t[properties.radius+1, properties.ticksOuterRadius],\n\t\t\t[properties.radius+1, properties.ticksInnerRadius],\n\t\t\t[properties.radius-1, properties.ticksInnerRadius]\n\t\t];\n\t\tvar tickPointsLarge = [\n\t\t\t[properties.radius-1.5, properties.ticksOuterRadius],\n\t\t\t[properties.radius+1.5, properties.ticksOuterRadius],\n\t\t\t[properties.radius+1.5, properties.ticksInnerRadius+20],\n\t\t\t[properties.radius-1.5, properties.ticksInnerRadius+20]\n\t\t];\n\t\tvar theta = properties.tickDegrees/options.numTicks;\n\t\tvar tickArray = [];\n\t\tfor (var iTick=0; iTick<options.numTicks; iTick++) {\n\t\t\ttickArray.push(createSVGElement('path',{d:pointsToPath(tickPoints)},ticks));\n\t\t};\n\t\t\n\t\t/*\n\t\t * Labels\n\t\t */\n\t\tvar lblTarget = createSVGElement('text',{\n\t\t\tx: properties.radius,\n\t\t\ty: properties.radius,\n\t\t\tclass: 'dial__lbl dial__lbl--target'\n\t\t},svg);\n\t\tvar lblTarget_text = document.createTextNode('');\n\t\tlblTarget.appendChild(lblTarget_text);\n\t\t//\n\t\tvar lblTargetHalf = createSVGElement('text',{\n\t\t\tx: properties.radius + properties.radius/2.5,\n\t\t\ty: properties.radius - properties.radius/8,\n\t\t\tclass: 'dial__lbl dial__lbl--target--half'\n\t\t},svg);\n\t\tvar lblTargetHalf_text = document.createTextNode('5');\n\t\tlblTargetHalf.appendChild(lblTargetHalf_text);\n\t\t//\n\t\tvar lblAmbient = createSVGElement('text',{\n\t\t\tclass: 'dial__lbl dial__lbl--ambient'\n\t\t},svg);\n\t\tvar lblAmbient_text = document.createTextNode('');\n\t\tlblAmbient.appendChild(lblAmbient_text);\n\t\t//\n\t\tvar lblAway = createSVGElement('text',{\n\t\t\tx: properties.radius,\n\t\t\ty: properties.radius,\n\t\t\tclass: 'dial__lbl dial__lbl--away'\n\t\t},svg);\n\t\tvar lblAway_text = document.createTextNode('AWAY');\n\t\tlblAway.appendChild(lblAway_text);\n\t\t//\n\t\tvar icoLeaf = createSVGElement('path',{\n\t\t\tclass: 'dial__ico__leaf'\n\t\t},svg);\n\t\t\n\t\t/*\n\t\t * LEAF\n\t\t */\n\t\tvar leafScale = properties.radius/5/100;\n\t\tvar leafDef = [\"M\", 3, 84, \"c\", 24, 17, 51, 18, 73, -6, \"C\", 100, 52, 100, 22, 100, 4, \"c\", -13, 15, -37, 9, -70, 19, \"C\", 4, 32, 0, 63, 0, 76, \"c\", 6, -7, 18, -17, 33, -23, 24, -9, 34, -9, 48, -20, -9, 10, -20, 16, -43, 24, \"C\", 22, 63, 8, 78, 3, 84, \"z\"].map(function(x) {\n\t\t\treturn isNaN(x) ? x : x*leafScale;\n\t\t}).join(' ');\n\t\tvar translate = [properties.radius-(leafScale*100*0.5),properties.radius*1.5]\n\t\tvar icoLeaf = createSVGElement('path',{\n\t\t\tclass: 'dial__ico__leaf',\n\t\t\td: leafDef,\n\t\t\ttransform: 'translate('+translate[0]+','+translate[1]+')'\n\t\t},svg);\n\t\t\t\n\t\t/*\n\t\t * RENDER\n\t\t */\n\t\tfunction render() {\n\t\t\trenderAway();\n\t\t\trenderHvacState();\n\t\t\trenderTicks();\n\t\t\trenderTargetTemperature();\n\t\t\trenderAmbientTemperature();\n\t\t\trenderLeaf();\n\t\t}\n\t\trender();\n\n\t\t/*\n\t\t * RENDER - ticks\n\t\t */\n\t\tfunction renderTicks() {\n\t\t\tvar vMin, vMax;\n\t\t\tif (self.away) {\n\t\t\t\tvMin = self.ambient_temperature;\n\t\t\t\tvMax = vMin;\n\t\t\t} else {\n\t\t\t\tvMin = Math.min(self.ambient_temperature, self.target_temperature);\n\t\t\t\tvMax = Math.max(self.ambient_temperature, self.target_temperature);\n\t\t\t}\n\t\t\tvar min = restrictToRange(Math.round((vMin-options.minValue)/properties.rangeValue * options.numTicks),0,options.numTicks-1);\n\t\t\tvar max = restrictToRange(Math.round((vMax-options.minValue)/properties.rangeValue * options.numTicks),0,options.numTicks-1);\n\t\t\t//\n\t\t\ttickArray.forEach(function(tick,iTick) {\n\t\t\t\tvar isLarge = iTick==min || iTick==max;\n\t\t\t\tvar isActive = iTick >= min && iTick <= max;\n\t\t\t\tattr(tick,{\n\t\t\t\t\td: pointsToPath(rotatePoints(isLarge ? tickPointsLarge: tickPoints,iTick*theta-properties.offsetDegrees,[properties.radius, properties.radius])),\n\t\t\t\t\tclass: isActive ? 'active' : ''\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t\n\t\t/*\n\t\t * RENDER - ambient temperature\n\t\t */\n\t\tfunction renderAmbientTemperature() {\n\t\t\tlblAmbient_text.nodeValue = Math.floor(self.ambient_temperature);\n\t\t\tif (self.ambient_temperature%1!=0) {\n\t\t\t\tlblAmbient_text.nodeValue += '⁵';\n\t\t\t}\n\t\t\tvar peggedValue = restrictToRange(self.ambient_temperature, options.minValue, options.maxValue);\n\t\t\tdegs = properties.tickDegrees * (peggedValue-options.minValue)/properties.rangeValue - properties.offsetDegrees;\n\t\t\tif (peggedValue > self.target_temperature) {\n\t\t\t\tdegs += 8;\n\t\t\t} else {\n\t\t\t\tdegs -= 8;\n\t\t\t}\n\t\t\tvar pos = rotatePoint(properties.lblAmbientPosition,degs,[properties.radius, properties.radius]);\n\t\t\tattr(lblAmbient,{\n\t\t\t\tx: pos[0],\n\t\t\t\ty: pos[1]\n\t\t\t});\n\t\t}\n\n\t\t/*\n\t\t * RENDER - target temperature\n\t\t */\n\t\tfunction renderTargetTemperature() {\n\t\t\tlblTarget_text.nodeValue = Math.floor(self.target_temperature);\n\t\t\tsetClass(lblTargetHalf,'shown',self.target_temperature%1!=0);\n\t\t}\n\t\t\n\t\t/*\n\t\t * RENDER - leaf\n\t\t */\n\t\tfunction renderLeaf() {\n\t\t\tsetClass(svg,'has-leaf',self.has_leaf);\n\t\t}\n\t\t\n\t\t/*\n\t\t * RENDER - HVAC state\n\t\t */\n\t\tfunction renderHvacState() {\n\t\t\tArray.prototype.slice.call(svg.classList).forEach(function(c) {\n\t\t\t\tif (c.match(/^dial--state--/)) {\n\t\t\t\t\tsvg.classList.remove(c);\n\t\t\t\t};\n\t\t\t});\n\t\t\tsvg.classList.add('dial--state--'+self.hvac_state);\n\t\t}\n\t\t\n\t\t/*\n\t\t * RENDER - away\n\t\t */\n\t\tfunction renderAway() {\n\t\t\tsvg.classList[self.away ? 'add' : 'remove']('away');\n\t\t}\n\t\t\n\t\t/*\n\t\t * Drag to control\n\t\t */\n\t\tvar _drag = {\n\t\t\tinProgress: false,\n\t\t\tstartPoint: null,\n\t\t\tstartTemperature: 0,\n\t\t\tlockAxis: undefined\n\t\t};\n\t\t\n\t\tfunction eventPosition(ev) {\n\t\t\tif (ev.targetTouches && ev.targetTouches.length) {\n\t\t\t\treturn [ev.targetTouches[0].clientX, ev.targetTouches[0].clientY];\n\t\t\t} else {\n\t\t\t\treturn [ev.x, ev.y];\n\t\t\t};\n\t\t}\n\t\t\n\t\tvar startDelay;\n\t\tfunction dragStart(ev) {\n\t\t\tstartDelay = setTimeout(function() {\n\t\t\t\tsetClass(svg, 'dial--edit', true);\n\t\t\t\t_drag.inProgress = true;\n\t\t\t\t_drag.startPoint = eventPosition(ev);\n\t\t\t\t_drag.startTemperature = self.target_temperature || options.minValue;\n\t\t\t\t_drag.lockAxis = undefined;\n\t\t\t},1000);\n\t\t};\n\t\t\n\t\tfunction dragEnd (ev) {\n\t\t\tclearTimeout(startDelay);\n\t\t\tsetClass(svg, 'dial--edit', false);\n\t\t\tif (!_drag.inProgress) return;\n\t\t\t_drag.inProgress = false;\n\t\t\tif (self.target_temperature != _drag.startTemperature) {\n\t\t\t\tif (typeof options.onSetTargetTemperature == 'function') {\n\t\t\t\t\toptions.onSetTargetTemperature(self.target_temperature);\n\t\t\t\t};\n\t\t\t};\n\t\t};\n\t\t\n\t\tfunction dragMove(ev) {\n\t\t\tev.preventDefault();\n\t\t\tif (!_drag.inProgress) return;\n\t\t\tvar evPos = eventPosition(ev);\n\t\t\tvar dy = _drag.startPoint[1]-evPos[1];\n\t\t\tvar dx = evPos[0] - _drag.startPoint[0];\n\t\t\tvar dxy;\n\t\t\tif (_drag.lockAxis == 'x') {\n\t\t\t\tdxy = dx;\n\t\t\t} else if (_drag.lockAxis == 'y') {\n\t\t\t\tdxy = dy;\n\t\t\t} else if (Math.abs(dy) > properties.dragLockAxisDistance) {\n\t\t\t\t_drag.lockAxis = 'y';\n\t\t\t\tdxy = dy;\n\t\t\t} else if (Math.abs(dx) > properties.dragLockAxisDistance) {\n\t\t\t\t_drag.lockAxis = 'x';\n\t\t\t\tdxy = dx;\n\t\t\t} else {\n\t\t\t\tdxy = (Math.abs(dy) > Math.abs(dx)) ? dy : dx;\n\t\t\t};\n\t\t\tvar dValue = (dxy*getSizeRatio())/(options.diameter)*properties.rangeValue;\n\t\t\tself.target_temperature = roundHalf(_drag.startTemperature+dValue);\n\t\t}\n\t\t\n\t\tsvg.addEventListener('mousedown',dragStart);\n\t\tsvg.addEventListener('touchstart',dragStart);\n\t\t\n\t\tsvg.addEventListener('mouseup',dragEnd);\n\t\tsvg.addEventListener('mouseleave',dragEnd);\n\t\tsvg.addEventListener('touchend',dragEnd);\n\t\t\n\t\tsvg.addEventListener('mousemove',dragMove);\n\t\tsvg.addEventListener('touchmove',dragMove);\n\t\t//\n\t\t\n\t\t/*\n\t\t * Helper functions\n\t\t */\n\t\tfunction restrictTargetTemperature(t) {\n\t\t\treturn restrictToRange(roundHalf(t),options.minValue,options.maxValue);\n\t\t}\n\t\t\n\t\tfunction angle(point) {\n\t\t\tvar dx = point[0] - properties.radius;\n\t\t\tvar dy = point[1] - properties.radius;\n\t\t\tvar theta = Math.atan(dx/dy) / (Math.PI/180);\n\t\t\tif (point[0]>=properties.radius && point[1] < properties.radius) {\n\t\t\t\ttheta = 90-theta - 90;\n\t\t\t} else if (point[0]>=properties.radius && point[1] >= properties.radius) {\n\t\t\t\ttheta = 90-theta + 90;\n\t\t\t} else if (point[0]<properties.radius && point[1] >= properties.radius) {\n\t\t\t\ttheta = 90-theta + 90;\n\t\t\t} else if (point[0]<properties.radius && point[1] < properties.radius) {\n\t\t\t\ttheta = 90-theta+270;\n\t\t\t}\n\t\t\treturn theta;\n\t\t};\n\t\t\n\t\tfunction getSizeRatio() {\n\t\t\treturn options.diameter / targetElement.clientWidth;\n\t\t}\n\t\t\n\t};\n})();\n\n/* ==== */\n(function(scope) {\n \n var nest = new thermostatDial(document.getElementById('thermostat'),{\n \tonSetTargetTemperature: function(v) {\n \t\tscope.send({topic: \"target_temperature\", payload: v});\n \t}\n });\n\n\n scope.$watch('msg', function(data) {\n //console.log(data.topic+\" \"+data.payload);\n if (data.topic == \"setValues\") {\n nest.ambient_temperature = data.ambient;\n nest.target_temperature = data.target;\n nest.hvac_state = data.hvac;\n nest.has_leaf = data.leaf;\n nest.away = data.away;\n \n }\n });\n})(scope);\n\n</script>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":910,"y":540,"wires":[[]]},{"id":"b6f4ca70.cd6fd8","type":"mqtt in","z":"951f89cd.86b0f8","name":"Heat State GH APP STATE","topic":"gBridge/u2491/heating/tempset-mode","qos":"0","datatype":"auto","broker":"c4b43ba4.186ae8","x":470,"y":760,"wires":[["c8a32e2b.b1467"]]},{"id":"e67674ef.77c3c8","type":"mqtt in","z":"951f89cd.86b0f8","name":"Google Home Temp set","topic":"gBridge/u2491/heating/tempset-setpoint","qos":"0","datatype":"auto","broker":"c4b43ba4.186ae8","x":260,"y":160,"wires":[["e91e5f90.4bf1c"]]},{"id":"afae114a.380a2","type":"mqtt out","z":"951f89cd.86b0f8","name":"Mode","topic":"gBridge/u2491/heating/tempset-mode/set","qos":"","retain":"","broker":"c4b43ba4.186ae8","x":1250,"y":760,"wires":[]},{"id":"77a57f14.2f93a","type":"mqtt out","z":"951f89cd.86b0f8","name":"Set Target Temp","topic":"gBridge/u2491/heating/tempset-setpoint/set","qos":"0","retain":"","broker":"c4b43ba4.186ae8","x":1100,"y":340,"wires":[]},{"id":"ca67b598.f05998","type":"comment","z":"951f89cd.86b0f8","name":"Current temperature set point (single target), in C","info":"","x":320,"y":100,"wires":[]},{"id":"798e842.b1ed07c","type":"comment","z":"951f89cd.86b0f8","name":"Float. 0-100. If available from the device.","info":"","x":1400,"y":260,"wires":[]},{"id":"d6d85a33.731138","type":"comment","z":"951f89cd.86b0f8","name":"Current observe temperature, in C","info":"","x":1380,"y":300,"wires":[]},{"id":"e91e5f90.4bf1c","type":"function","z":"951f89cd.86b0f8","name":"Update Target","func":"var temp = parseInt(msg.payload);\nflow.set(\"TempTarget\",temp);\nflow.set('away', false);\nreturn msg;","outputs":1,"noerr":0,"x":570,"y":160,"wires":[[]]},{"id":"5f346d9c.e70ff4","type":"smooth","z":"951f89cd.86b0f8","name":"Average Humidity","property":"payload.DHT11.Humidity","action":"mean","count":"15","round":"2","mult":"single","reduce":false,"x":530,"y":260,"wires":[["93cdc9f2.df3a38"]]},{"id":"1f3c171e.823369","type":"function","z":"951f89cd.86b0f8","name":"Set Decimal","func":"var target = msg.target;\nvar x = target.toFixed(1);\n\n\nmsg.payload = x;\n\nreturn msg;\n\n\n","outputs":1,"noerr":0,"x":890,"y":340,"wires":[["77a57f14.2f93a"]]},{"id":"c8a32e2b.b1467","type":"function","z":"951f89cd.86b0f8","name":"Control Relay","func":"var defaultTemp = flow.get(\"defaultTemp\"); \n\n//msg1 = OFF|ON\n//Mmsg2 = off|heat\n\nif (msg.payload === \"off\" || msg.payload === \"0\"){\n msg.payload = \"off\";\n flow.set('away', true);\n flow.set('hvac', \"off\");\n var msg1 = { payload:\"OFF\" };\n var msg2 = { payload:\"off\" };\n return [msg1, msg2];\n}\n\nif (msg.payload === \"heat\" || msg.payload === \"1\"){\n flow.set('TempTarget', defaultTemp);\n flow.set('away', false);\n flow.set('hvac', \"on\");\n var msg1 = { payload:\"ON\" };\n var msg2 = { payload:\"heat\" };\n return [msg1, msg2];\n}\n\nreturn msg;","outputs":2,"noerr":0,"x":760,"y":760,"wires":[["1863a176.4815cf"],["afae114a.380a2"]]},{"id":"c2b97b15.a32458","type":"comment","z":"951f89cd.86b0f8","name":"Google Home","info":"","x":430,"y":720,"wires":[]},{"id":"4bf9de56.812b2","type":"mqtt out","z":"951f89cd.86b0f8","name":"GH Ambient Humidity","topic":"gBridge/u2491/heating/tempset-humidity/set","qos":"0","retain":"","broker":"c4b43ba4.186ae8","x":1120,"y":260,"wires":[]},{"id":"97f4a75d.95d038","type":"mqtt out","z":"951f89cd.86b0f8","name":"Ambient Temp","topic":"gBridge/u2491/heating/tempset-ambient/set","qos":"0","retain":"false","broker":"c4b43ba4.186ae8","x":1100,"y":300,"wires":[]},{"id":"93cdc9f2.df3a38","type":"function","z":"951f89cd.86b0f8","name":"Set Decimal","func":"var humid = msg.payload.DHT11.Humidity;\nvar x = humid.toFixed(1);\nflow.set('HumidAmbient',x );\nmsg.payload = x;\nmsg.topic = \"Humidity\";\nreturn msg;","outputs":1,"noerr":0,"x":890,"y":260,"wires":[["4bf9de56.812b2","1bf1fd14.e402c3"]]},{"id":"7472cfef.924b5","type":"function","z":"951f89cd.86b0f8","name":"Set Decimal","func":"var temp = msg.payload.DHT11.Temperature;\nvar x = temp.toFixed(1);\nflow.set('TempAmbient',x );\nmsg.payload = x;\nmsg.topic = \"Temperature\";\nreturn msg;","outputs":1,"noerr":0,"x":890,"y":300,"wires":[["97f4a75d.95d038","1bf1fd14.e402c3"]]},{"id":"aea44560.dd1da8","type":"subflow:21187818.0fa5a8","z":"951f89cd.86b0f8","name":"Settings","env":[{"name":"defaultTemp","value":"21","type":"num"},{"name":"ecoL","value":"17","type":"num"},{"name":"ecoH","value":"23","type":"num"}],"x":1420,"y":120,"wires":[["c21bec04.9926c"]],"icon":"node-red/cog.svg"},{"id":"61c2da90.0b6b54","type":"inject","z":"951f89cd.86b0f8","name":"Set","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1250,"y":120,"wires":[["aea44560.dd1da8"]]},{"id":"c21bec04.9926c","type":"debug","z":"951f89cd.86b0f8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1680,"y":120,"wires":[]},{"id":"1afba9fa.0f9276","type":"comment","z":"951f89cd.86b0f8","name":"Set preferences","info":"","x":1400,"y":80,"wires":[]},{"id":"1bf1fd14.e402c3","type":"ui_chart","z":"951f89cd.86b0f8","name":"Chart","group":"1855b3f2.e3e8cc","order":3,"width":"18","height":"8","label":"Temperature and Humidity","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"90","removeOlder":"24","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#d20000","#0303ed","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1130,"y":200,"wires":[[]]},{"id":"5d8914c7.0f1e0c","type":"function","z":"951f89cd.86b0f8","name":"hysteresis ","func":"var state = msg.hvac;\n\nif(state == \"heating\"){\n var x = 5; \n}\n\nif(state === \"off\"){\n var x = 0; \n}\nmsg.payload = x;\nmsg.topic = \"Hysteresis\";\nreturn msg;\n\n\n","outputs":1,"noerr":0,"x":880,"y":380,"wires":[["1bf1fd14.e402c3"]]},{"id":"89c6edb0.4e615","type":"http in","z":"951f89cd.86b0f8","name":"Android Control","url":"thermostat/status","method":"get","upload":false,"swaggerDoc":"","x":480,"y":1100,"wires":[["3e000eee.3b4682"]]},{"id":"4215aea7.c60d1","type":"http response","z":"951f89cd.86b0f8","name":"","statusCode":"","headers":{},"x":870,"y":1100,"wires":[]},{"id":"3e000eee.3b4682","type":"function","z":"951f89cd.86b0f8","name":"Send values","func":"var targetTemp = flow.get('TempTarget'); \nvar ambientTemp = flow.get('TempAmbient'); \nvar state = flow.get('heatingState'); \nvar away = flow.get('away'); \nvar defaultTemp = flow.get(\"defaultTemp\");\n\n\n\nmsg.payload = {\n \"ambient\": ambientTemp,\n \"target\": targetTemp,\n \"hvac\": state,\n \"away\": away,\n \"defaultTemp\": defaultTemp\n}\n\n\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":1100,"wires":[["4215aea7.c60d1"]]},{"id":"d8cc5058.7d9e5","type":"comment","z":"951f89cd.86b0f8","name":"GET Status","info":"","x":470,"y":1060,"wires":[]},{"id":"1bd86905.947c57","type":"http in","z":"951f89cd.86b0f8","name":"Android Control","url":"thermostat/set","method":"post","upload":false,"swaggerDoc":"","x":480,"y":1180,"wires":[["919afbbb.e7e4e8","d44a2b9b.881e18"]]},{"id":"63a8da3d.544424","type":"http response","z":"951f89cd.86b0f8","name":"","statusCode":"","headers":{},"x":990,"y":1180,"wires":[]},{"id":"5f4d5610.70b4c8","type":"comment","z":"951f89cd.86b0f8","name":"POST update","info":"","x":470,"y":1140,"wires":[]},{"id":"919afbbb.e7e4e8","type":"debug","z":"951f89cd.86b0f8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":710,"y":1240,"wires":[]},{"id":"e607843c.8f6428","type":"http request","z":"951f89cd.86b0f8","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"https://www.googleapis.com/calendar/v3/calendars/[email protected]/events","tls":"","persist":false,"proxy":"","authType":"basic","x":420,"y":1400,"wires":[["9cc02a67.427c68","919afbbb.e7e4e8"]]},{"id":"fda6900e.6b365","type":"inject","z":"951f89cd.86b0f8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":180,"y":1360,"wires":[["e607843c.8f6428"]]},{"id":"9cc02a67.427c68","type":"http response","z":"951f89cd.86b0f8","name":"","statusCode":"","headers":{},"x":560,"y":1480,"wires":[]},{"id":"d44a2b9b.881e18","type":"function","z":"951f89cd.86b0f8","name":"Update thermostat","func":"var action = msg.payload.action;\n\nvar x,y;\n\nif(action === \"away\"){\n x = msg.payload.away\n flow.set(\"away\", x);\n}\nif(action === \"on\"){\n x = msg.payload.away;\n y = msg.payload.temp;\n flow.set(\"away\", x);\n flow.set(\"TempTarget\", y);\n}\nif(action === \"off\"){\n x = msg.payload.away;\n y = msg.payload.temp;\n flow.set(\"away\", x);\n flow.set(\"TempTarget\", y);\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":720,"y":1180,"wires":[["63a8da3d.544424"]]},{"id":"6b91546e.2ff37c","type":"function","z":"951f89cd.86b0f8","name":"Process JSON (no 1)","func":"var key = global.get('ARmi9');\nvar url = \"https://autoremotejoaomgcd.appspot.com/sendmessage\";\nvar command = \"NOT20\";\nvar body = {\n \"title\": {\n \"title\": \"This is the Perfect AutoNotification\",\n \"titleexpanded\": \"Perfect AN\"\n },\n \"text\": {\n \"text\": \"The notifications can't get any better!\",\n \"textexpanded\": \"You cannot make them easier!\"\n },\n \"icons\": {\n \"navbaricon\": \"https://raw.githubusercontent.com/google/material-design-icons/master/social/drawable-xhdpi/ic_notifications_none_black_48dp.png\",\n \"bigicon\": \"https://raw.githubusercontent.com/google/material-design-icons/master/social/drawable-xhdpi/ic_notifications_active_black_48dp.png\"\n },\n \"notificationid\": \"someID\",\n \"persistent\": true,\n \"priority\": 1,\n \"buttons\": [\n {\n \"button1\": {\n \"icon\": \"https://raw.githubusercontent.com/google/material-design-icons/master/file/2x_web/ic_file_download_black_48dp.png\",\n \"label\": \"Sounds OK\",\n \"command\": \"Download\"\n }\n }\n ]\n};\nmsg.data = body;\nvar x = JSON.stringify(body);\nvar encodedBody = encodeURIComponent(x);\n\nmsg.url = url + \"?key=\" + key + \"&message=\" +command + \"=:=\"+ encodedBody;\nreturn msg;","outputs":1,"noerr":0,"x":1460,"y":1020,"wires":[["9dcf9da8.fe334"]]},{"id":"f7c7d876.c968d8","type":"inject","z":"951f89cd.86b0f8","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1260,"y":1020,"wires":[["6b91546e.2ff37c"]]},{"id":"9dcf9da8.fe334","type":"http request","z":"951f89cd.86b0f8","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","persist":false,"authType":"","x":1680,"y":1020,"wires":[["87ecff49.42398"]]},{"id":"87ecff49.42398","type":"http response","z":"951f89cd.86b0f8","name":"","statusCode":"","headers":{},"x":1910,"y":1020,"wires":[]},{"id":"85e7033e.ddb46","type":"function","z":"951f89cd.86b0f8","name":"hysteresis ","func":"var target = flow.get(\"TempTarget\");\nvar away = flow.get(\"away\");\n\nif(away === true){\n msg.topic = \"target\";\n msg.payload = 0;\n}\nelse{\n msg.topic = \"target\";\n msg.payload = target;\n}\n\n\nreturn msg;\n\n\n\n","outputs":1,"noerr":0,"x":880,"y":420,"wires":[["5da4d070.3abdc"]]},{"id":"5da4d070.3abdc","type":"rbe","z":"951f89cd.86b0f8","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":1030,"y":420,"wires":[["1bf1fd14.e402c3"]]},{"id":"eca6af44.5297b","type":"mqtt-broker","z":"","name":"MQTT","broker":"dockerpi.local","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"92170a5b.37a1d8","type":"ui_group","z":"","name":"Heating","tab":"75cd3a25.41ce84","disp":true,"width":"6","collapse":false},{"id":"4ec600a6.406ae","type":"alexa-home-conf","z":"","username":"quintaar"},{"id":"c4b43ba4.186ae8","type":"mqtt-broker","z":"","name":"Gbridge","broker":"mqtt.gbridge.io","port":"8883","tls":"47c99f2f.8b1c1","clientid":"","usetls":true,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"1855b3f2.e3e8cc","type":"ui_group","z":"","name":"Home Heating","tab":"8d5fcc6e.77124","order":2,"disp":true,"width":"18","collapse":false},{"id":"75cd3a25.41ce84","type":"ui_tab","z":"","name":"Heating","icon":"dashboard","order":1,"disabled":false,"hidden":false},{"id":"47c99f2f.8b1c1","type":"tls-config","z":"","name":"TLS Gbridge","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"mqtt.gbridge.io","verifyservercert":true},{"id":"8d5fcc6e.77124","type":"ui_tab","z":"","name":"Heating Chart","icon":"dashboard","order":3,"disabled":false,"hidden":false}]