Hackster is hosting Hackster Holidays, Ep. 7: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Friday!Stream Hackster Holidays, Ep. 7 on Friday!
NotEnoughTech
Published © CC BY

NEST Your Old Thermostat for Under $5

Make your old thermostat smart again with this $5 device!

IntermediateFull instructions provided2 hours11,014
NEST Your Old Thermostat for Under $5

Things used in this project

Hardware components

Sonoff Basic
Itead Sonoff Basic
×1
DHT11 Temperature & Humidity Sensor (3 pins)
DHT11 Temperature & Humidity Sensor (3 pins)
×1

Software apps and online services

Node-RED on IBM Cloud
Node-RED on IBM Cloud
tasmota firmware

Story

Read more

Schematics

Connection Schematics

Here is how I wired Sonoff with the Thermostat

Code

NodeRED Nest Flow

HTML
This is the complete flow with Alexa support. Just import this through NodeRED
[{"id":"293ca76e.bb4f38","type":"ui_template","z":"3d4c38d8.c679f8","group":"dfad00fc.e2b86","name":"Nest","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 == \"ambient_temperature\") {\n            nest.ambient_temperature = data.payload;\n        } if (data.topic == \"target_temperature\") {\n            nest.target_temperature = data.payload;\n        } if (data.topic == \"hvac_state\") {\n            nest.hvac_state = data.payload;\n        } if (data.topic == \"has_leaf\") {\n            nest.has_leaf = data.payload;\n        } if (data.topic == \"away\") {\n            nest.away = data.payload;\n        }\n    });\n})(scope);\n\n</script>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":830,"y":315,"wires":[[]]},{"id":"bec0c6fd.3116b8","type":"mqtt in","z":"3d4c38d8.c679f8","name":"Read Temp DHT11","topic":"sonoff/tele/SENSOR","qos":"0","broker":"4a31cd84.72cbd4","x":150,"y":105,"wires":[["5b69274c.cdcbd8"]]},{"id":"5b69274c.cdcbd8","type":"json","z":"3d4c38d8.c679f8","name":"","property":"payload","action":"","pretty":false,"x":305,"y":105,"wires":[["155a1d57.c32543"]]},{"id":"8ed6c05f.ee25f","type":"ui_slider","z":"3d4c38d8.c679f8","name":"Slider","label":"Target Temp","group":"dfad00fc.e2b86","order":14,"width":0,"height":0,"passthru":false,"topic":"slider","min":"10","max":"30","step":1,"x":365,"y":405,"wires":[["692dc6f1.662638"]]},{"id":"7ed1b2d6.6fdc7c","type":"function","z":"3d4c38d8.c679f8","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}\n\nif (msg.command === \"TurnOnRequest\"){\n    msg.payload = true;\n    flow.set('away', false);\n    flow.set('TempTarget', 21);\n    return msg;\n    \n}\n\nif (msg.command === \"TurnOffRequest\"){\n    msg.payload = true;\n    flow.set('away', true);\n    \n    return msg;\n    \n    \n}","outputs":1,"noerr":0,"x":485,"y":615,"wires":[["f5ce638c.dc8bb"]]},{"id":"ace4aa04.d4e008","type":"function","z":"3d4c38d8.c679f8","name":"Leaf Control","func":"x = flow.get('TempAmbient');\n\nif (x > 17 && x < 23){\n     flow.set('leaf', true);\n    msg.payload = true;\n    msg.topic = \"has_leaf\";\n    return msg;\n}\n    \nelse{flow.set('leaf', false);\n    msg.payload = false;\n    msg.topic = \"has_leaf\";\n    return msg;\n}\n\n","outputs":1,"noerr":0,"x":520,"y":270,"wires":[["293ca76e.bb4f38"]]},{"id":"d4236beb.9243d8","type":"function","z":"3d4c38d8.c679f8","name":"Color ","func":"x = flow.get('TempTarget');   //target\nz = flow.get('TempAmbient');  //ambient\n\nif (z < x){\n    flow.set('heatingState', \"heating\");\n    flow.set('heatingSwitch', \"ON\");\n}\nif (z >= x){\n    flow.set('heatingState', \"off\");\n    flow.set('heatingSwitch', \"OFF\");\n}\nmsg.payload = z;\nmsg.topic = \"ambient_temperature\";\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":225,"wires":[["293ca76e.bb4f38"]]},{"id":"45fd6002.8f39b","type":"mqtt out","z":"3d4c38d8.c679f8","name":"Control Sonoff","topic":"sonoff/cmnd/POWER1","qos":"","retain":"","broker":"4a31cd84.72cbd4","x":755,"y":555,"wires":[]},{"id":"ba4bb151.fd715","type":"function","z":"3d4c38d8.c679f8","name":"Control relay","func":"if (msg.command === \"TurnOffRequest\"){\n    msg.payload = \"OFF\";\n    return msg;\n}\n\nif (msg.command === \"TurnOnRequest\"){\n    msg.payload = \"ON\";\n    flow.set('TempTarget', 21);\n    return msg;\n}\nif (msg.topic === \"update\"){\n    msg.payload = flow.get('heatingSwitch');\n}\nreturn msg;","outputs":1,"noerr":0,"x":520,"y":555,"wires":[["45fd6002.8f39b"]]},{"id":"155a1d57.c32543","type":"smooth","z":"3d4c38d8.c679f8","name":"Average temp","property":"payload.DHT11.Temperature","action":"mean","count":"15","round":"2","mult":"single","x":470,"y":105,"wires":[["79bc5ce0.0442f4"]]},{"id":"89762fd3.ef445","type":"alexa-home","z":"3d4c38d8.c679f8","conf":"df965cf5.93857","device":"35103","acknoledge":false,"name":"","topic":"Heating","x":220,"y":615,"wires":[["7ed1b2d6.6fdc7c","ba4bb151.fd715","692dc6f1.662638"]]},{"id":"f5ce638c.dc8bb","type":"alexa-home-resp","z":"3d4c38d8.c679f8","x":775,"y":615,"wires":[]},{"id":"4c13797d.961898","type":"function","z":"3d4c38d8.c679f8","name":"colour -away override","func":"x = flow.get('away');\n\nif (x === true){\n    msg.topic = \"hvac_state\";\n    msg.payload = \"off\";\n    return msg;\n}\n\nmsg.topic = \"hvac_state\";\nmsg.payload = flow.get('heatingState');\n\nreturn msg;","outputs":1,"noerr":0,"x":550,"y":315,"wires":[["293ca76e.bb4f38"]]},{"id":"aefa46a2.f36038","type":"function","z":"3d4c38d8.c679f8","name":"Away Control","func":"x = flow.get('away');\n\nif (x === true){\n    flow.set('heatingSwitch', \"OFF\");\n    flow.set('heatingState', \"off\");\n}\n\nmsg.topic = \"away\";\nmsg.payload = flow.get('away');\nreturn msg;\n\n","outputs":1,"noerr":0,"x":520,"y":360,"wires":[["293ca76e.bb4f38"]]},{"id":"692dc6f1.662638","type":"function","z":"3d4c38d8.c679f8","name":"set temp target","func":"if (msg.topic === \"update\"){\n    msg.topic = \"target_temperature\";\n    msg.payload = flow.get('TempTarget');\n    return msg;\n}\n\nif (msg.command === \"SetTargetTemperatureRequest\") {\n    flow.set('away', false);\n    msg.topic = \"target_temperature\";\n    flow.set('TempTarget', msg.payload);\n    \n}\n\nif (msg.topic === \"slider\") {\n    flow.set('away', false);\n    msg.topic = \"target_temperature\";\n    flow.set('TempTarget', msg.payload);\n}\n\nif (msg.command === \"GetTemperatureReadingRequest\"){}\n\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":405,"wires":[["293ca76e.bb4f38"]]},{"id":"f3aba7f.f852c58","type":"inject","z":"3d4c38d8.c679f8","name":"Refresh every 5 sec","topic":"update","payload":"upate","payloadType":"str","repeat":"5","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":225,"wires":[["4c13797d.961898","aefa46a2.f36038","692dc6f1.662638","ba4bb151.fd715","d4236beb.9243d8","ace4aa04.d4e008","b3981462.98d958"]]},{"id":"b3981462.98d958","type":"function","z":"3d4c38d8.c679f8","name":"Update slider","func":"msg.payload = flow.get('TempTarget');\nreturn msg;","outputs":1,"noerr":0,"x":220,"y":405,"wires":[["8ed6c05f.ee25f"]]},{"id":"53430ab1.c29204","type":"function","z":"3d4c38d8.c679f8","name":"test","func":"msg.ambinet = flow.get('TempAmbient');\nmsg.target = flow.get('TempTarget');\nmsg.leaf = flow.get('leaf');\nmsg.state = flow.get('heatingState');\nmsg.switch = flow.get('heatingSwitch');\nmsg.away = flow.get('away');\nreturn msg;","outputs":1,"noerr":0,"x":410,"y":720,"wires":[["198d2d47.f28653"]]},{"id":"198d2d47.f28653","type":"debug","z":"3d4c38d8.c679f8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":590,"y":720,"wires":[]},{"id":"fb78fd8b.0ca8a","type":"inject","z":"3d4c38d8.c679f8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":720,"wires":[["53430ab1.c29204"]]},{"id":"79bc5ce0.0442f4","type":"function","z":"3d4c38d8.c679f8","name":"Update the 'TempAmbient'","func":"flow.set('TempAmbient', msg.payload.DHT11.Temperature);\nreturn msg;","outputs":1,"noerr":0,"x":695,"y":105,"wires":[[]]},{"id":"e31f1fb1.091a2","type":"comment","z":"3d4c38d8.c679f8","name":"Measure DHT11 temp and update var","info":"Gets the Temp, averages last X readings \nand updates the variable for the next push","x":210,"y":60,"wires":[]},{"id":"12d4c524.ac0acb","type":"comment","z":"3d4c38d8.c679f8","name":"Update Dial ","info":"","x":130,"y":180,"wires":[]},{"id":"bca5ef19.3f92f","type":"comment","z":"3d4c38d8.c679f8","name":"Control Relay","info":"","x":520,"y":510,"wires":[]},{"id":"7cb30851.a329a8","type":"comment","z":"3d4c38d8.c679f8","name":"Alexa Responses","info":"","x":230,"y":570,"wires":[]},{"id":"8f8745c.fc8afb8","type":"comment","z":"3d4c38d8.c679f8","name":"Test the Flow Vars","info":"","x":240,"y":675,"wires":[]},{"id":"dfad00fc.e2b86","type":"ui_group","z":"","name":"Almost Like Nest but Cheaper","tab":"4447df43.a3ad4","order":2,"disp":true,"width":"6","collapse":false},{"id":"4a31cd84.72cbd4","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.183","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"df965cf5.93857","type":"alexa-home-conf","z":"","username":"quintaar"},{"id":"4447df43.a3ad4","type":"ui_tab","z":"","name":"Show","icon":"dashboard"}]

Credits

NotEnoughTech
10 projects • 82 followers
I'm Mat I play with gadgets, make new stuff and automate pretty much everything that requires repeating more than once!
Thanks to Dal Hundall.

Comments