Integrate Beca Modbus thermostat with Nest like UI
I am integrating a Beca Modbus Thermostat with Node-Red using the Nest like dashboard control. This flow shows how you can read and write the registers to create a 2 way communication between Node-Red and the modbus thermostat. In this flow, I am also using a Nest like UI widget as the temperature display.
You can see a video on this flow here: https://youtu.be/z2HaGW_dKdU
[{"id":"1e740be5.c8eab4","type":"ui_template","z":"a91a4693.aec638","group":"fc782db1.a0407","name":"Living Room","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":true,"fwdInMessages":false,"templateScope":"local","x":690,"y":100,"wires":[["e95132a2.1f66c","9e38c59e.061878"]]},{"id":"82c7a4d2.1d8998","type":"function","z":"a91a4693.aec638","name":"ambient_temperature","func":"msg.topic = \"ambient_temperature\";\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":100,"wires":[["1e740be5.c8eab4"]]},{"id":"ffa902ad.3d839","type":"function","z":"a91a4693.aec638","name":"target_temperature","func":"msg.topic = \"target_temperature\";\nreturn msg;","outputs":1,"noerr":0,"x":450,"y":140,"wires":[["1e740be5.c8eab4"]]},{"id":"c9ec6d8.c313d9","type":"function","z":"a91a4693.aec638","name":"hvac_state","func":"msg.topic = \"hvac_state\";\nreturn msg;","outputs":1,"noerr":0,"x":430,"y":180,"wires":[["1e740be5.c8eab4"]]},{"id":"34778b8f.25d234","type":"function","z":"a91a4693.aec638","name":"has_leaf","func":"msg.topic = \"has_leaf\";\nreturn msg;","outputs":1,"noerr":0,"x":420,"y":220,"wires":[["1e740be5.c8eab4"]]},{"id":"345f31fc.a645ae","type":"function","z":"a91a4693.aec638","name":"away","func":"msg.topic = \"away\";\nreturn msg;","outputs":1,"noerr":0,"x":410,"y":260,"wires":[["1e740be5.c8eab4"]]},{"id":"85d1d6fc.abd858","type":"inject","z":"a91a4693.aec638","name":"","topic":"has_leaf","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":true,"x":150,"y":300,"wires":[["34778b8f.25d234"]]},{"id":"c99a1af2.faa4f8","type":"inject","z":"a91a4693.aec638","name":"","topic":"has_leaf","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"x":150,"y":340,"wires":[["34778b8f.25d234"]]},{"id":"6243084.7eb40f8","type":"inject","z":"a91a4693.aec638","name":"","topic":"away","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"x":140,"y":380,"wires":[["345f31fc.a645ae"]]},{"id":"d7aa0f47.ab0d","type":"inject","z":"a91a4693.aec638","name":"","topic":"away","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"x":140,"y":420,"wires":[["345f31fc.a645ae"]]},{"id":"ffbf8da0.89546","type":"inject","z":"a91a4693.aec638","name":"","topic":"hvac_state","payload":"off","payloadType":"str","repeat":"","crontab":"","once":false,"x":150,"y":180,"wires":[["c9ec6d8.c313d9"]]},{"id":"6501277e.e6a368","type":"inject","z":"a91a4693.aec638","name":"","topic":"hvac_state","payload":"heating","payloadType":"str","repeat":"","crontab":"","once":false,"x":170,"y":220,"wires":[["c9ec6d8.c313d9"]]},{"id":"c31c6cc3.a8965","type":"inject","z":"a91a4693.aec638","name":"","topic":"hvac_state","payload":"cooling","payloadType":"str","repeat":"","crontab":"","once":false,"x":170,"y":260,"wires":[["c9ec6d8.c313d9"]]},{"id":"848f2dd3.6809d","type":"inject","z":"a91a4693.aec638","name":"","topic":"ambient_temperature","payload":"23","payloadType":"num","repeat":"","crontab":"","once":true,"onceDelay":"","x":190,"y":100,"wires":[["82c7a4d2.1d8998"]]},{"id":"f02e99a2.0849b8","type":"inject","z":"a91a4693.aec638","name":"","topic":"target_temperature","payload":"20","payloadType":"num","repeat":"","crontab":"","once":true,"x":180,"y":140,"wires":[["ffa902ad.3d839"]]},{"id":"a192231f.31605","type":"debug","z":"a91a4693.aec638","name":"","active":false,"console":"false","complete":"false","x":330,"y":720,"wires":[]},{"id":"539766e6.ad21d8","type":"comment","z":"a91a4693.aec638","name":"Beca Thermostat","info":"Master read from the Beca thermostat and update\nthe values on the dashboard","x":120,"y":660,"wires":[]},{"id":"28fc9002.29091","type":"modbus-read","z":"a91a4693.aec638","name":"Beca","topic":"","showStatusActivities":true,"logIOActivities":false,"showErrors":true,"unitid":"5","dataType":"HoldingRegister","adr":"0","quantity":"10","rate":"1","rateUnit":"s","delayOnStart":false,"startDelayTime":"","server":"84f6b45b.6287c8","useIOFile":false,"ioFile":"","useIOForPayload":false,"x":130,"y":760,"wires":[["a192231f.31605","19b53864.7c8258"],[]]},{"id":"94e0cc38.18a54","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Ambient temperature","format":"{{msg.payload.ambient}} °C","layout":"row-spread","x":626.1666870117188,"y":794.5833740234375,"wires":[]},{"id":"19b53864.7c8258","type":"function","z":"a91a4693.aec638","name":"Convert data","func":"var dL = [\"\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"];\nvar dS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nvar beca = {};\n\nbeca.ambient = msg.payload[8]/10;\nbeca.target = msg.payload[3]/10;\nbeca.power = msg.payload[0];\nbeca.power_text = msg.payload[0] ? \"On\": \"Off\";\nbeca.fan = msg.payload[1];\nif (beca.fan===0) { beca.fan_text = \"Auto\"; }\nif (beca.fan===1) { beca.fan_text = \"High\"; }\nif (beca.fan===2) { beca.fan_text = \"Medium\"; }\nif (beca.fan===3) { beca.fan_text = \"Low\"; }\nbeca.mode = msg.payload[2];\nif (beca.mode===0) { beca.mode_text = \"Cooling\"; }\nif (beca.mode===1) { beca.mode_text = \"Heating\"; }\nif (beca.mode===2) { beca.mode_text = \"Ventilation\"; }\nbeca.lock = msg.payload[4];\nbeca.lock_text = msg.payload[4] ? \"Locked\": \"Unlocked\";\nbeca.hour = msg.payload[6];\nbeca.minute = msg.payload[5];\nbeca.weekday = msg.payload[7];\nbeca.weekday_text = dL[msg.payload[7]];\nbeca.valve = msg.payload[9];\nbeca.valve_text = msg.payload[9] ? \"Open\": \"Closed\";\nmsg.payload = beca;\nreturn msg;","outputs":1,"noerr":0,"x":336.16668701171875,"y":774.5833740234375,"wires":[["94e0cc38.18a54","2ea3e7b5.404578","6b23359a.488d9c","547af1e3.21441","8c1e1451.2475f8","ecb801a0.83315","3413de87.c89062","59a3f418.6e9dec","1d39c7b4.5b1d88","8f0bfa43.5aab68","8831c57b.d4b348","bc2ea663.593388","b8de2220.c8b83","c49a5518.a96ad8","37e12418.937bfc","bb00e71d.f2fed8","db29277.32725d8"]]},{"id":"2ea3e7b5.404578","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Target temperature","format":"{{msg.payload.target}} °C","layout":"row-spread","x":616.1666870117188,"y":834.5833740234375,"wires":[]},{"id":"6b23359a.488d9c","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Power","format":"{{msg.payload.power_text}}","layout":"row-spread","x":576.1666870117188,"y":874.5833740234375,"wires":[]},{"id":"547af1e3.21441","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Fan speed","format":"{{msg.payload.fan_text}}","layout":"row-spread","x":596.1666870117188,"y":914.5833740234375,"wires":[]},{"id":"8c1e1451.2475f8","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Mode","format":"{{msg.payload.mode_text}}","layout":"row-spread","x":576.1666870117188,"y":954.5833740234375,"wires":[]},{"id":"ecb801a0.83315","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Time","format":"{{msg.payload.hour}} : {{msg.payload.minute}}","layout":"row-spread","x":576.1666870117188,"y":994.5833740234375,"wires":[]},{"id":"3413de87.c89062","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Weekday","format":"{{msg.payload.weekday_text}}","layout":"row-spread","x":586.1666870117188,"y":1034.5833740234375,"wires":[]},{"id":"2318559.8b630aa","type":"inject","z":"a91a4693.aec638","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":1280,"wires":[["3d90694a.7caaf6","8895534b.34d4d","78884654.81d848"]]},{"id":"67886a7c.5f4024","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Hour update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"6","quantity":"1","server":"84f6b45b.6287c8","x":610,"y":1340,"wires":[[],[]]},{"id":"59a3f418.6e9dec","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Valve","format":"{{msg.payload.valve_text}}","layout":"row-spread","x":576.1666870117188,"y":1074.5833740234375,"wires":[]},{"id":"49ae8dd5.fa49f4","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Minute update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"5","quantity":"1","server":"84f6b45b.6287c8","x":620,"y":1400,"wires":[[],[]]},{"id":"2f61dd84.786c82","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Weekday update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"7","quantity":"1","server":"84f6b45b.6287c8","x":620,"y":1460,"wires":[[],[]]},{"id":"3d90694a.7caaf6","type":"function","z":"a91a4693.aec638","name":"Current Hour","func":"var now = new Date();\nmsg.payload = now.getHours();\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":1340,"wires":[["67886a7c.5f4024"]]},{"id":"8895534b.34d4d","type":"function","z":"a91a4693.aec638","name":"Current Minute","func":"var now = new Date();\nmsg.payload = now.getMinutes();\nreturn msg;","outputs":1,"noerr":0,"x":360,"y":1400,"wires":[["49ae8dd5.fa49f4"]]},{"id":"78884654.81d848","type":"function","z":"a91a4693.aec638","name":"Current Weekday","func":"var now = new Date();\nmsg.payload = now.getDay();\nif (msg.payload===0) { msg.payload = 7; }\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":1460,"wires":[["2f61dd84.786c82"]]},{"id":"1d39c7b4.5b1d88","type":"function","z":"a91a4693.aec638","name":"Ambient Temperature","func":"var newmsg = {};\n\nnewmsg.topic = \"ambient_temperature\";\nnewmsg.payload = msg.payload.ambient;\nreturn newmsg;","outputs":1,"noerr":0,"x":580,"y":480,"wires":[["1e740be5.c8eab4"]]},{"id":"8f0bfa43.5aab68","type":"function","z":"a91a4693.aec638","name":"Target Temperature","func":"var newmsg = {};\n\nnewmsg.topic = \"target_temperature\";\nnewmsg.payload = msg.payload.target;\n\n// This only lets the message through to the nest if it is changed. \n// This is to prevent the nest to update the temperature when it could also be edited manually\nif (context.get(\"target\")!==msg.payload.target) {\n context.set(\"target\",msg.payload.target);\n return newmsg;\n}","outputs":1,"noerr":0,"x":570,"y":520,"wires":[["1e740be5.c8eab4","e1d6ba23.e0a3b8"]]},{"id":"8831c57b.d4b348","type":"function","z":"a91a4693.aec638","name":"State","func":"var newmsg = {};\n\nnewmsg.topic = \"hvac_state\";\nif (msg.payload.power===1) {\n if (msg.payload.mode===0) { newmsg.payload = \"cooling\"; }\n if (msg.payload.mode===1) { newmsg.payload = \"heating\"; }\n if (msg.payload.mode===2) { newmsg.payload = \"heating\"; }\n} else {\n newmsg.payload = \"off\";\n}\nreturn newmsg;","outputs":1,"noerr":0,"x":570,"y":560,"wires":[["1e740be5.c8eab4"]]},{"id":"bc2ea663.593388","type":"function","z":"a91a4693.aec638","name":"Leaf","func":"var newmsg = {};\n\nnewmsg.topic = \"has_leaf\";\nif (msg.payload.mode===2) { \n newmsg.payload = true; \n} else {\n newmsg.payload = false;\n}\nreturn newmsg;","outputs":1,"noerr":0,"x":570,"y":600,"wires":[["1e740be5.c8eab4"]]},{"id":"e1d6ba23.e0a3b8","type":"ui_slider","z":"a91a4693.aec638","name":"","label":"Target Temp","group":"fc782db1.a0407","order":0,"width":0,"height":0,"passthru":false,"topic":"","min":"10","max":"40","step":"0.5","x":770,"y":240,"wires":[["f274421.62dabc","9e38c59e.061878"]]},{"id":"dfaacbe1.c3e348","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Target Temp update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"3","quantity":"1","server":"84f6b45b.6287c8","x":1210,"y":240,"wires":[[],[]]},{"id":"f274421.62dabc","type":"function","z":"a91a4693.aec638","name":"Target Temperature","func":"var newmsg = {};\n\nnewmsg.topic = \"target_temperature\";\nnewmsg.payload = msg.payload;\nreturn newmsg;","outputs":1,"noerr":0,"x":1010,"y":180,"wires":[["1e740be5.c8eab4"]]},{"id":"9e38c59e.061878","type":"function","z":"a91a4693.aec638","name":"Convert data","func":"msg.payload = msg.payload * 10;\nreturn msg;","outputs":1,"noerr":0,"x":990,"y":240,"wires":[["dfaacbe1.c3e348"]]},{"id":"b5448b46.5c1078","type":"ui_switch","z":"a91a4693.aec638","name":"","label":"Power","group":"e4263281.60a98","order":0,"width":0,"height":0,"passthru":false,"decouple":"false","topic":"","style":"","onvalue":"1","onvalueType":"num","onicon":"","oncolor":"","offvalue":"0","offvalueType":"num","officon":"","offcolor":"","x":910,"y":880,"wires":[["af250622.841428"]]},{"id":"af250622.841428","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Power update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"0","quantity":"1","server":"84f6b45b.6287c8","x":1090,"y":880,"wires":[[],[]]},{"id":"b8de2220.c8b83","type":"function","z":"a91a4693.aec638","name":"Convert","func":"\n// This only lets the message through if it is changed. \n// This is to prevent the UI to reset the state when it could also be edited manually\nif (context.get(\"target\")!==msg.payload.power) {\n context.set(\"target\",msg.payload.power);\n msg.payload = msg.payload.power;\nreturn msg;\n}","outputs":1,"noerr":0,"x":760,"y":880,"wires":[["b5448b46.5c1078"]]},{"id":"3277127.7bc3cee","type":"ui_dropdown","z":"a91a4693.aec638","name":"","label":"Fan speed","place":"Select option","group":"e4263281.60a98","order":0,"width":0,"height":0,"passthru":false,"options":[{"label":"Auto","value":0,"type":"num"},{"label":"High","value":"1","type":"str"},{"label":"Medium","value":"2","type":"str"},{"label":"Low","value":"3","type":"str"}],"payload":"","topic":"","x":930,"y":940,"wires":[["dfc575e1.245dd8"]]},{"id":"dfc575e1.245dd8","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Fan speed update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"1","quantity":"1","server":"84f6b45b.6287c8","x":1170,"y":940,"wires":[[],[]]},{"id":"c49a5518.a96ad8","type":"function","z":"a91a4693.aec638","name":"Convert","func":"// This only lets the message through if it is changed. \n// This is to prevent the UI to reset the state when it could also be edited manually\nif (context.get(\"target\")!==msg.payload.fan) {\n context.set(\"target\",msg.payload.fan);\n msg.payload = msg.payload.fan;\n return msg;\n}","outputs":1,"noerr":0,"x":760,"y":940,"wires":[["3277127.7bc3cee"]]},{"id":"70fe4930.ddad18","type":"ui_dropdown","z":"a91a4693.aec638","name":"","label":"Mode","place":"Select option","group":"e4263281.60a98","order":0,"width":0,"height":0,"passthru":false,"options":[{"label":"Cooling","value":0,"type":"num"},{"label":"Heating","value":"1","type":"str"},{"label":"Ventillation","value":"2","type":"str"}],"payload":"","topic":"","x":910,"y":1000,"wires":[["82bcb7e9.fdbc18"]]},{"id":"82bcb7e9.fdbc18","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Fan speed update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"2","quantity":"1","server":"84f6b45b.6287c8","x":1170,"y":1000,"wires":[[],[]]},{"id":"37e12418.937bfc","type":"function","z":"a91a4693.aec638","name":"Convert","func":"// This only lets the message through if it is changed. \n// This is to prevent the UI to reset the state when it could also be edited manually\nif (context.get(\"target\")!==msg.payload.mode) {\n context.set(\"target\",msg.payload.mode);\n msg.payload = msg.payload.mode;\n return msg;\n}","outputs":1,"noerr":0,"x":760,"y":1000,"wires":[["70fe4930.ddad18"]]},{"id":"12e0a82.d29fc58","type":"ui_switch","z":"a91a4693.aec638","name":"","label":"Lock","group":"e4263281.60a98","order":0,"width":0,"height":0,"passthru":false,"decouple":"false","topic":"","style":"","onvalue":"1","onvalueType":"num","onicon":"","oncolor":"","offvalue":"0","offvalueType":"num","officon":"","offcolor":"","x":910,"y":1140,"wires":[["fe7df5ad.21e878"]]},{"id":"fe7df5ad.21e878","type":"modbus-write","z":"a91a4693.aec638","name":"Beca Power update","showStatusActivities":false,"showErrors":false,"unitid":"5","dataType":"HoldingRegister","adr":"4","quantity":"1","server":"84f6b45b.6287c8","x":1090,"y":1140,"wires":[[],[]]},{"id":"bb00e71d.f2fed8","type":"ui_text","z":"a91a4693.aec638","group":"9caf9527.dfe288","order":0,"width":0,"height":0,"name":"","label":"Screen lock","format":"{{msg.payload.lock_text}}","layout":"row-spread","x":590,"y":1120,"wires":[]},{"id":"db29277.32725d8","type":"function","z":"a91a4693.aec638","name":"Convert","func":"// This only lets the message through if it is changed. \n// This is to prevent the UI to reset the state when it could also be edited manually\nif (context.get(\"target\")!==msg.payload.lock) {\n context.set(\"target\",msg.payload.lock);\n msg.payload = msg.payload.lock;\n return msg;\n}","outputs":1,"noerr":0,"x":760,"y":1140,"wires":[["12e0a82.d29fc58"]]},{"id":"97546c0f.209eb","type":"comment","z":"a91a4693.aec638","name":"Testing the Nest features","info":"This inject nodes are left here from the example code","x":150,"y":40,"wires":[]},{"id":"64631e09.1b705","type":"comment","z":"a91a4693.aec638","name":"Setting the Target Temperature","info":"This slides sets the target temperature and updates \nthe Nest display and the modbus device as well","x":850,"y":300,"wires":[]},{"id":"4ba88e34.bc845","type":"comment","z":"a91a4693.aec638","name":"Nest update","info":"These functions generate the messages so the Nest\ncontrol shows the same temperatures as on the\nmodbus device.\n\nI needed these function nodes, because the \nNest control does not seem to accept messages\nthat have other attributes besides topic and \na simple numberic / boolean payload.","x":770,"y":540,"wires":[]},{"id":"d98143d0.2eeb","type":"comment","z":"a91a4693.aec638","name":"Beca control function","info":"These allow to control the Beca thermostat \nbesides the target temperature","x":930,"y":820,"wires":[]},{"id":"4fd69561.e0a2ec","type":"comment","z":"a91a4693.aec638","name":"Beca time update","info":"This flow updates the hour, minute and \nweekday on the modbus device. The device does\nnot have a real time clock, therefore the time\nresets to 00:00 when powered up.","x":112.5,"y":1217.5,"wires":[]},{"id":"e95132a2.1f66c","type":"debug","z":"a91a4693.aec638","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":930,"y":100,"wires":[]},{"id":"fc782db1.a0407","type":"ui_group","z":"","name":"Living Room","tab":"d13b4678.d56ed8","order":2,"disp":true,"width":"6","collapse":false},{"id":"84f6b45b.6287c8","type":"modbus-client","z":"","name":"2nd Serial 9600_8_N_1","clienttype":"simpleser","bufferCommands":true,"stateLogEnabled":false,"tcpHost":"127.0.0.1","tcpPort":"502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB2","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","unit_id":"","commandDelay":"1","clientTimeout":"1000","reconnectTimeout":"2000"},{"id":"9caf9527.dfe288","type":"ui_group","z":"","name":"Beca","tab":"d13b4678.d56ed8","disp":true,"width":"6","collapse":false},{"id":"e4263281.60a98","type":"ui_group","z":"","name":"Beca control","tab":"d13b4678.d56ed8","disp":true,"width":"6","collapse":false},{"id":"d13b4678.d56ed8","type":"ui_tab","z":"","name":"Thermostat","icon":"fa-thermometer-empty"}]