Node-Red als Frontend

Begonnen von Master_Nick, 26 Oktober 2017, 13:07:28

Vorheriges Thema - Nächstes Thema

Master_Nick

#60
Langsam will ich nicht sagen, es hat 2 Sekunden Ladezeit, bis die Anzeige der Seite steht. Die Seiten die nur Switches haben gehen deutlich fixer. Es ist jammern auf hohem Niveau :-D und geht echt nur um die Ladezeit.

Gehostet habe ich Node-RED auf einem Pi3 und die Anzeige erfolgt auf einem anderen Pi3, PC oder Handy.
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

Shojo

Ok, ich habe da einen Odroid C2 mit NandFlash hinter der ist da einiges Performanter.
FHEM auf: Shuttle PC (x64) (Docker)
Bridge: SignalESP 433mHz, ConBee (deCONZ in Docker)
Rest: ESP8266, SONOFF, Sonos, Echo Dot, Xiaomi Vacuum (root), ESP RGBWW Wifi Led Controller, Node-RED, LEDMatrix, Pixel It

Master_Nick

 ;D Dann liegt es evtl daran.  8)
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

Shojo

#63
Hier noch mal ein Ansatz von mir.

Jeder Schalter oder Regler bekommt von mir als Topic das Fhem Device mitgegeben.
Wenn ich aber was anderes wie den State schalten möchte z.B. den HSV Wert sieht das so aus WZ.Licht.RGB.Fensterfront hvs

Hier noch die kleine Logik des Fhem Messages bauen:

var temp = msg.payload;
if (typeof temp == "string" && temp.startsWith("hsv"))
{
    temp = temp.replace(/ /g,"");
    temp = temp.replace(/%/g,"");
    temp = temp.replace(/hsv\(/g,"");
    temp = temp.replace(/\)/g,"");
    var x = temp.split(",");
    temp =  x[0] + "," + x[1] + "," + x[2];
}

msg.payload = "set " +  msg.topic + " " + temp;
return msg;


Und das Fhem Device:

defmod MQTT.NodeRed.Input MQTT_DEVICE
attr MQTT.NodeRed.Input IODev MQTTBroker
attr MQTT.NodeRed.Input stateFormat transmission-state
attr MQTT.NodeRed.Input subscribeReading_cmd {fhem("$message")} NodeRed/ToFHEM/cmd qos:1
FHEM auf: Shuttle PC (x64) (Docker)
Bridge: SignalESP 433mHz, ConBee (deCONZ in Docker)
Rest: ESP8266, SONOFF, Sonos, Echo Dot, Xiaomi Vacuum (root), ESP RGBWW Wifi Led Controller, Node-RED, LEDMatrix, Pixel It

Master_Nick

#64
Ich habe meine gesamte Thermostatgeschichte nun auf Nest Optik gezogen die Benutzbarkeit ist hier viel besser als bei einem Schieberegler.  8) ;D

Finde es richtig geil!
Habe allerdings mit den hier gelieferten Dingen nur teils was arbeiten können - die hatten beim Reload immer noch Probleme bei mir :-)
Habe das Ganze beim Ursprung des Nest Thermostats für NodeRed dann gefunden und mit Ideen von dort gelöst... krieg es schon kaum noch auf die Kette was wie wo... viel Arbeit/Lesen gewesen.
Habe dann noch einige Icons eingebunden und bin sehr zufrieden.

Auch wird hier die Temperatur verglichen und entschieden ob geheizt wird (rotes Nest Thermostat). Und das Blatt kommt bei gewissen Zieltemperaturen die man als "sparend" betrachten könnte (kann man alles selber anpassen). Ansonsten habe ich meinen VOC Sensor im Wohnzimmer noch mit eingebunden ohne Werte sondern mittels 3er Symbole: Daumen hoch grün, Daumen hoch gelb und Daumen runter rot. Auch Window Open wird angezeigt wenn der Wert auf 1 steht - gelber Text "Window" erscheint dann.

Bei mehr als einem Thermostat ist es wichtig das DIV umzubenennen und Im weiteren Code den Namen der vorher ganz oben im DIV Stand erneut zu ändern.
Ich erweitere noch um ein eingeblendetes Fenstersymbol.

Code für NodeRed für das Wohnzimmer Thermostat:
[{"id":"53e6b42a.aabbe4","type":"function","z":"15bc8d4b.90af3b","name":"Konvertieren der Temperatur","func":"msg.payload = parseFloat(msg.payload);\nmsg.topic = 'sensor_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":810,"y":300,"wires":[["730bb5a.9f6bfcc","c0dc329a.4f59b"]]},{"id":"730bb5a.9f6bfcc","type":"debug","z":"15bc8d4b.90af3b","name":"Temp Sensor","active":false,"console":"false","complete":"payload","x":1130,"y":300,"wires":[]},{"id":"63e04c4d.bb5f6c","type":"ui_template","z":"15bc8d4b.90af3b","group":"d2bea201.d68888","name":"Nest","order":5,"width":"6","height":"6","format":"<div id=\"thermostat-WZ\"></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-WZ'),{\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":1030,"y":540,"wires":[["228ed0cb.d3b908"]]},{"id":"773154d5.ab0824","type":"function","z":"15bc8d4b.90af3b","name":"ambient_temperature","func":"msg.topic = \"ambient_temperature\";\nglobal.set(\"wz-ambient\",msg.payload);\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":777.6190490722656,"y":388.33333587646484,"wires":[["63e04c4d.bb5f6c"]]},{"id":"1ae634e.7e416cb","type":"function","z":"15bc8d4b.90af3b","name":"hvac_state","func":"global.set(\"wz-state\",msg.payload);\n\nmsg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":1670,"y":340,"wires":[["63e04c4d.bb5f6c"]]},{"id":"228ed0cb.d3b908","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature","func":"if (msg.topic == \"target_temperature\") {\nglobal.set(\"wz-target\",msg.payload);\nreturn msg;\n}","outputs":1,"noerr":0,"x":1210,"y":540,"wires":[["c0dc329a.4f59b","2ab0e1e3.ac211e"]]},{"id":"c0dc329a.4f59b","type":"function","z":"15bc8d4b.90af3b","name":"Temperaturen Vergleichen","func":"context.target = context.target || 0.0;\ncontext.sensor = context.sensor || 0.0;\n\nif (msg.topic === 'sensor_temperature') {\n  context.sensor = msg.payload;\n} else if (msg.topic === 'target_temperature') {\n  context.target = msg.payload;\n} \n\nif (context.target >= context.sensor) {\n  return {payload: 1};\n} else {\n  return {payload: 0};\n}\nnode.status({text:msg.payload});","outputs":1,"noerr":0,"x":1460,"y":500,"wires":[["7dc8f30f.3d7064"]]},{"id":"7dc8f30f.3d7064","type":"function","z":"15bc8d4b.90af3b","name":"Farbstatus Nest","func":"msg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":1700,"y":500,"wires":[["74d1ff15.059858"]]},{"id":"74d1ff15.059858","type":"switch","z":"15bc8d4b.90af3b","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"eq","v":"1","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":1310,"y":360,"wires":[["3d83fd16.3def3a"],["42df8b17.773ac4"]]},{"id":"3d83fd16.3def3a","type":"template","z":"15bc8d4b.90af3b","name":"Heizen OFF","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"off","output":"str","x":1490,"y":300,"wires":[["1ae634e.7e416cb"]]},{"id":"42df8b17.773ac4","type":"template","z":"15bc8d4b.90af3b","name":"Heizen ON","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"heating","output":"str","x":1490,"y":380,"wires":[["1ae634e.7e416cb"]]},{"id":"bcc9c2f.902a54","type":"delay","z":"15bc8d4b.90af3b","name":"","pauseType":"delay","timeout":"50","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":540,"y":540,"wires":[["c6c28969.9dbff8","bf2d08a0.229458","c02295c7.804d98","dc76cf88.de8868"]]},{"id":"c6c28969.9dbff8","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-target\");\nmsg.topic = 'target_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":440,"wires":[["63e04c4d.bb5f6c"]]},{"id":"ae55f012.dbc9e","type":"ui_ui_control","z":"15bc8d4b.90af3b","name":"ui change","x":360,"y":540,"wires":[["bcc9c2f.902a54"]]},{"id":"c02295c7.804d98","type":"function","z":"15bc8d4b.90af3b","name":"global color-state","func":"msg.payload = global.get(\"wz-state\");\nmsg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":560,"wires":[["63e04c4d.bb5f6c"]]},{"id":"8872a148.d992","type":"function","z":"15bc8d4b.90af3b","name":"Leaf","func":"minleaf = 15;\nmaxleaf = 22;\ntemperature = msg.payload;\nmsg.payload=false;\nif (temperature >= minleaf){\n    if (temperature <= maxleaf){\n        msg.payload = true;\n    }\n}\nmsg.topic = \"has_leaf\";\nglobal.set(\"wz-leaf\",msg.payload);\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":810,"y":720,"wires":[["63e04c4d.bb5f6c"]]},{"id":"4737da1d.f282fc","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/desiredTemperature","qos":"2","broker":"34eb4531.5459e2","x":350,"y":720,"wires":[["137bccd8.b66d93","8872a148.d992","19428fcb.0c576"]],"outputLabels":["target_temperature"]},{"id":"b09e0a1f.5122c8","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/temperature","qos":"2","broker":"34eb4531.5459e2","x":360,"y":300,"wires":[["773154d5.ab0824","53e6b42a.aabbe4"]],"outputLabels":["ambient_temperature"]},{"id":"137bccd8.b66d93","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature","func":"msg.topic = 'target_temperature';\nglobal.set(\"wz-target\",msg.payload);\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":680,"wires":[["63e04c4d.bb5f6c","c0dc329a.4f59b"]]},{"id":"7f9ed5e0.953424","type":"mqtt out","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/desiredTemperature/set","qos":"2","retain":"false","broker":"34eb4531.5459e2","x":2040,"y":620,"wires":[]},{"id":"bf2d08a0.229458","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-ambient\");\nmsg.topic = 'ambient_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":500,"wires":[["63e04c4d.bb5f6c"]]},{"id":"dc76cf88.de8868","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-leaf\");\nmsg.topic = 'has_leaf';\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":620,"wires":[["63e04c4d.bb5f6c"]]},{"id":"a84aee83.b857b","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/humidity","qos":"2","broker":"34eb4531.5459e2","x":350,"y":120,"wires":[["6ea3b9b3.85c2e"]],"outputLabels":["humidity"]},{"id":"b04d3a0d.8c6ba8","type":"trigger","z":"15bc8d4b.90af3b","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"4","extend":true,"units":"s","reset":"","bytopic":"all","name":"","x":1680,"y":620,"wires":[["7f9ed5e0.953424"]]},{"id":"8b86a571.ef3fb","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/voc","qos":"2","broker":"34eb4531.5459e2","x":330,"y":180,"wires":[["4b4c2777.fcd59"]]},{"id":"4b4c2777.fcd59","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\nbad = 'Air: <div class=\"fa fa-thumbs-down fa-2x nr-dashboard-error\"></div>';\nok = 'Air: <div class=\"fa fa-thumbs-up fa-2x nr-dashboard-warning\"></div>';\ngood = 'Air: <div class=\"fa fa-thumbs-up fa-2x nr-dashboard-ok\"></div>';\nif (msg.payload > 1750) { \n    msg.payload = (bad);\n    msg.topic = 'air';\n}\nif (msg.payload <= 1750 && msg.payload > 750) {\n    msg.payload = (ok);\n    msg.topic = 'air';\n}\nif (msg.payload <= 750) {\n    msg.payload = (good);\n    msg.topic = 'air';\n}\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":180,"wires":[["44ad4257.3b5dec"]]},{"id":"45291976.3021f","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/windowOpen","qos":"2","broker":"34eb4531.5459e2","x":360,"y":240,"wires":[["3708d80.98e8aa8"]]},{"id":"3708d80.98e8aa8","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\ntext = '<div class=\"nr-dashboard-warning\">Window</div>';\nif (msg.payload == \"1\") {\n    msg.payload = (text);\n    msg.topic = 'window';\n}\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":240,"wires":[["44ad4257.3b5dec"]]},{"id":"f6640250.4d957","type":"ui_text","z":"15bc8d4b.90af3b","group":"d2bea201.d68888","order":1,"width":"0","height":"0","name":"Temperatur/Luftfeuchtigkeit","label":"{{msg.payload.humidity.payload}}","format":"{{msg.payload.air.payload}} {{msg.payload.window.payload}}","layout":"row-spread","x":1040,"y":180,"wires":[]},{"id":"6ea3b9b3.85c2e","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\ntext1 = '<div class=\"fa fa-tint fa-2x nr-dashboard-dim\"></div> ';\ntext2 = ' %';\nmsg.payload = (text1 + msg.payload + text2);\nmsg.topic = 'humidity';\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":120,"wires":[["44ad4257.3b5dec"]]},{"id":"44ad4257.3b5dec","type":"join","z":"15bc8d4b.90af3b","name":"","mode":"custom","build":"object","property":"","propertyType":"full","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":830,"y":180,"wires":[["f6640250.4d957"]]},{"id":"2ab0e1e3.ac211e","type":"function","z":"15bc8d4b.90af3b","name":"Nur bei Abweichung senden","func":"if (msg.topic === 'target_temperature_old')\n{\n  oldvalue = msg.payload;\n} \nelse if (msg.topic === 'target_temperature')\n{\n  newvalue = msg.payload;\n} \n\nif (oldvalue != newvalue) {\n  oldvalue = newvalue;\n  return {payload: newvalue};\n}","outputs":1,"noerr":0,"x":1460,"y":620,"wires":[["b04d3a0d.8c6ba8"]]},{"id":"19428fcb.0c576","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature_old","func":"msg.topic = 'target_temperature_old';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":760,"wires":[["2ab0e1e3.ac211e"]]},{"id":"d2bea201.d68888","type":"ui_group","z":"","name":"Wohnzimmer","tab":"7a08a4e0.9f9cf4","disp":true,"width":"6","collapse":false},{"id":"34eb4531.5459e2","type":"mqtt-broker","z":"","name":"","broker":"192.168.0.8","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"willTopic":"homeland/haushalt/steuerung/nodered/$online","willQos":"0","willPayload":"false","birthTopic":"","birthQos":"0","birthPayload":""},{"id":"7a08a4e0.9f9cf4","type":"ui_tab","z":"","name":"Klima","icon":"dashboard"}]
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

Master_Nick

#65
So ich habe noch weiter getweakt.

Wenn ihr Fragen oder Code braucht einfach Bescheid geben :-)
Sonst belasse ich es erst mal beim komplexesten dem Wohnzimmerthermostat.
Generell habe ich alle angepasst um den Minimal Wert und den Maximalwert zu ändern (statt 10° 4° beim Balkon -20° bis 50°).
Und Missbrauche die nun der Optik wegen auch für normales Thermo/Hygrometer. Auch habe ich das symbolisieren von Heizen von Zieltemp >= Sensortemp auf nur > geändert.

Ansonsten mal ein Bild von meinem Touch Display - befeuert von einem Raspi3.

[{"id":"53e6b42a.aabbe4","type":"function","z":"15bc8d4b.90af3b","name":"Konvertieren der Temperatur","func":"msg.payload = parseFloat(msg.payload);\nmsg.topic = 'sensor_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":810,"y":300,"wires":[["730bb5a.9f6bfcc","c0dc329a.4f59b"]]},{"id":"730bb5a.9f6bfcc","type":"debug","z":"15bc8d4b.90af3b","name":"Temp Sensor","active":false,"console":"false","complete":"payload","x":1130,"y":300,"wires":[]},{"id":"63e04c4d.bb5f6c","type":"ui_template","z":"15bc8d4b.90af3b","group":"d2bea201.d68888","name":"Nest","order":5,"width":"6","height":"6","format":"<div id=\"thermostat-WZ\"></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 || 4, // 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-WZ'),{\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":1030,"y":540,"wires":[["228ed0cb.d3b908"]]},{"id":"773154d5.ab0824","type":"function","z":"15bc8d4b.90af3b","name":"ambient_temperature","func":"msg.topic = \"ambient_temperature\";\nglobal.set(\"wz-ambient\",msg.payload);\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":777.6190490722656,"y":388.33333587646484,"wires":[["63e04c4d.bb5f6c"]]},{"id":"1ae634e.7e416cb","type":"function","z":"15bc8d4b.90af3b","name":"hvac_state","func":"global.set(\"wz-state\",msg.payload);\n\nmsg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":1670,"y":340,"wires":[["63e04c4d.bb5f6c"]]},{"id":"228ed0cb.d3b908","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature","func":"if (msg.topic == \"target_temperature\") {\nglobal.set(\"wz-target\",msg.payload);\nreturn msg;\n}","outputs":1,"noerr":0,"x":1210,"y":540,"wires":[["c0dc329a.4f59b","2ab0e1e3.ac211e"]]},{"id":"c0dc329a.4f59b","type":"function","z":"15bc8d4b.90af3b","name":"Temperaturen Vergleichen","func":"context.target = context.target || 0.0;\ncontext.sensor = context.sensor || 0.0;\n\nif (msg.topic === 'sensor_temperature') {\n  context.sensor = msg.payload;\n} else if (msg.topic === 'target_temperature') {\n  context.target = msg.payload;\n} \n\nif (context.target > context.sensor) {\n  return {payload: 1};\n} else {\n  return {payload: 0};\n}\nnode.status({text:msg.payload});","outputs":1,"noerr":0,"x":1460,"y":500,"wires":[["7dc8f30f.3d7064"]]},{"id":"7dc8f30f.3d7064","type":"function","z":"15bc8d4b.90af3b","name":"Farbstatus Nest","func":"msg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":1700,"y":500,"wires":[["74d1ff15.059858"]]},{"id":"74d1ff15.059858","type":"switch","z":"15bc8d4b.90af3b","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"eq","v":"1","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":1310,"y":360,"wires":[["3d83fd16.3def3a"],["42df8b17.773ac4"]]},{"id":"3d83fd16.3def3a","type":"template","z":"15bc8d4b.90af3b","name":"Heizen OFF","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"off","output":"str","x":1490,"y":300,"wires":[["1ae634e.7e416cb"]]},{"id":"42df8b17.773ac4","type":"template","z":"15bc8d4b.90af3b","name":"Heizen ON","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"heating","output":"str","x":1490,"y":380,"wires":[["1ae634e.7e416cb"]]},{"id":"bcc9c2f.902a54","type":"delay","z":"15bc8d4b.90af3b","name":"","pauseType":"delay","timeout":"50","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":540,"y":540,"wires":[["c6c28969.9dbff8","bf2d08a0.229458","c02295c7.804d98","dc76cf88.de8868"]]},{"id":"c6c28969.9dbff8","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-target\");\nmsg.topic = 'target_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":440,"wires":[["63e04c4d.bb5f6c"]]},{"id":"ae55f012.dbc9e","type":"ui_ui_control","z":"15bc8d4b.90af3b","name":"ui change","x":360,"y":540,"wires":[["bcc9c2f.902a54"]]},{"id":"c02295c7.804d98","type":"function","z":"15bc8d4b.90af3b","name":"global color-state","func":"msg.payload = global.get(\"wz-state\");\nmsg.topic = \"hvac_state\";\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":560,"wires":[["63e04c4d.bb5f6c"]]},{"id":"8872a148.d992","type":"function","z":"15bc8d4b.90af3b","name":"Leaf","func":"minleaf = 15;\nmaxleaf = 22;\ntemperature = msg.payload;\nmsg.payload=false;\nif (temperature >= minleaf){\n    if (temperature <= maxleaf){\n        msg.payload = true;\n    }\n}\nmsg.topic = \"has_leaf\";\nglobal.set(\"wz-leaf\",msg.payload);\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":810,"y":720,"wires":[["63e04c4d.bb5f6c"]]},{"id":"4737da1d.f282fc","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/desiredTemperature","qos":"2","broker":"34eb4531.5459e2","x":350,"y":720,"wires":[["137bccd8.b66d93","8872a148.d992","19428fcb.0c576"]],"outputLabels":["target_temperature"]},{"id":"b09e0a1f.5122c8","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/temperature","qos":"2","broker":"34eb4531.5459e2","x":360,"y":300,"wires":[["773154d5.ab0824","53e6b42a.aabbe4"]],"outputLabels":["ambient_temperature"]},{"id":"137bccd8.b66d93","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature","func":"msg.topic = 'target_temperature';\nglobal.set(\"wz-target\",msg.payload);\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":680,"wires":[["63e04c4d.bb5f6c","c0dc329a.4f59b"]]},{"id":"7f9ed5e0.953424","type":"mqtt out","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/desiredTemperature/set","qos":"2","retain":"false","broker":"34eb4531.5459e2","x":2040,"y":620,"wires":[]},{"id":"bf2d08a0.229458","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-ambient\");\nmsg.topic = 'ambient_temperature';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":500,"wires":[["63e04c4d.bb5f6c"]]},{"id":"dc76cf88.de8868","type":"function","z":"15bc8d4b.90af3b","name":"global target-temp","func":"msg.payload = global.get(\"wz-leaf\");\nmsg.topic = 'has_leaf';\nnode.status({text:msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":620,"wires":[["63e04c4d.bb5f6c"]]},{"id":"a84aee83.b857b","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/humidity","qos":"2","broker":"34eb4531.5459e2","x":350,"y":120,"wires":[["6ea3b9b3.85c2e"]],"outputLabels":["humidity"]},{"id":"b04d3a0d.8c6ba8","type":"trigger","z":"15bc8d4b.90af3b","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"4","extend":true,"units":"s","reset":"","bytopic":"all","name":"","x":1680,"y":620,"wires":[["7f9ed5e0.953424"]]},{"id":"8b86a571.ef3fb","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/voc","qos":"2","broker":"34eb4531.5459e2","x":330,"y":180,"wires":[["4b4c2777.fcd59"]]},{"id":"4b4c2777.fcd59","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\nbad = 'Air: <div class=\"fa fa-thumbs-down fa-2x nr-dashboard-error\"></div>';\nok = 'Air: <div class=\"fa fa-thumbs-up fa-2x nr-dashboard-warning\"></div>';\ngood = 'Air: <div class=\"fa fa-thumbs-up fa-2x nr-dashboard-ok\"></div>';\nif (msg.payload > 1750) { \n    msg.payload = (bad);\n    msg.topic = 'air';\n}\nif (msg.payload <= 1750 && msg.payload > 750) {\n    msg.payload = (ok);\n    msg.topic = 'air';\n}\nif (msg.payload <= 750) {\n    msg.payload = (good);\n    msg.topic = 'air';\n}\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":180,"wires":[["44ad4257.3b5dec"]]},{"id":"45291976.3021f","type":"mqtt in","z":"15bc8d4b.90af3b","name":"","topic":"homeland/haushalt/heizung/Wohnzimmer_Thermostat/windowOpen","qos":"2","broker":"34eb4531.5459e2","x":360,"y":240,"wires":[["3708d80.98e8aa8"]]},{"id":"3708d80.98e8aa8","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\ntext = '<div class=\"nr-dashboard-warning\">Window</div>';\nif (msg.payload == \"1\") {\n    msg.payload = (text);\n    msg.topic = 'window';\n}\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":240,"wires":[["44ad4257.3b5dec"]]},{"id":"f6640250.4d957","type":"ui_text","z":"15bc8d4b.90af3b","group":"d2bea201.d68888","order":1,"width":"0","height":"0","name":"Temperatur/Luftfeuchtigkeit","label":"{{msg.payload.humidity.payload}}","format":"{{msg.payload.air.payload}} {{msg.payload.window.payload}}","layout":"row-spread","x":1040,"y":180,"wires":[]},{"id":"6ea3b9b3.85c2e","type":"function","z":"15bc8d4b.90af3b","name":"","func":"node.status({text:msg.payload});\ntext1 = '<div class=\"fa fa-tint fa-2x nr-dashboard-dim\"></div> ';\ntext2 = ' %';\nmsg.payload = (text1 + msg.payload + text2);\nmsg.topic = 'humidity';\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":120,"wires":[["44ad4257.3b5dec"]]},{"id":"44ad4257.3b5dec","type":"join","z":"15bc8d4b.90af3b","name":"","mode":"custom","build":"object","property":"","propertyType":"full","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":830,"y":180,"wires":[["f6640250.4d957"]]},{"id":"2ab0e1e3.ac211e","type":"function","z":"15bc8d4b.90af3b","name":"Nur bei Abweichung senden","func":"if (msg.topic === 'target_temperature_old')\n{\n  oldvalue = msg.payload;\n} \nelse if (msg.topic === 'target_temperature')\n{\n  newvalue = msg.payload;\n} \n\nif (oldvalue != newvalue) {\n  oldvalue = newvalue;\n  return {payload: newvalue};\n}","outputs":1,"noerr":0,"x":1460,"y":620,"wires":[["b04d3a0d.8c6ba8"]]},{"id":"19428fcb.0c576","type":"function","z":"15bc8d4b.90af3b","name":"target_temperature_old","func":"msg.topic = 'target_temperature_old';\nnode.status({text:msg.payload + \"°C\"});\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":760,"wires":[["2ab0e1e3.ac211e"]]},{"id":"d2bea201.d68888","type":"ui_group","z":"","name":"Wohnzimmer","tab":"7a08a4e0.9f9cf4","order":1,"disp":true,"width":"7","collapse":false},{"id":"34eb4531.5459e2","type":"mqtt-broker","z":"","name":"","broker":"192.168.0.8","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"willTopic":"homeland/haushalt/steuerung/nodered/$online","willQos":"0","willPayload":"false","birthTopic":"","birthQos":"0","birthPayload":""},{"id":"7a08a4e0.9f9cf4","type":"ui_tab","z":"","name":"Klima","icon":"dashboard","order":1}]
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

SamNitro

Hat jemand eine schöne Lösung einen WeekdayTimer zu realisieren, wie beim TabletUI?
https://forum.fhem.de/index.php/topic,48106.msg397622.html#msg397622
(Intel-Nuc Proxmox) (Homematic) (EnOcean) (CUL868) (CUL433) (Zigbee2MQTT) (ESP8266) (Echo) (DUOFERN)

Master_Nick

 ;) Alos ich nicht, da ich dafür aber auch irgendwie keinerlei Verwendung habe.
Jede tiefere Schaltung die nicht an aus ist frühstücke ich in FHEM ab.
Machbar sind diverse Logiken aber generell auch in Node-Red.
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

SamNitro

Ja ein paar Lösungen habe ich auch schon aber leider nicht so komfortabel wie der WeekdayTimer.

node-red-contrib-schedex:
Schöne Funktion mit sunrise sunset.

node-red-contrib-simple-weekly-scheduler:
Einfache Schaltungen.

Aber beide haben eine kleines Problem die wollen immer einen aus und an Befehl haben. Wenn man nur eins haben will muss man filtern.
(Intel-Nuc Proxmox) (Homematic) (EnOcean) (CUL868) (CUL433) (Zigbee2MQTT) (ESP8266) (Echo) (DUOFERN)

Master_Nick

Das kannste dir doch bestimmt mit einem Switch auf JS Basis umbauen.
Oder was meinst du mit AN/AUS Befehl?
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)

SamNitro

#70
Die fertigen Nodes sind so ausgelegt das man eine einschaltzeit und eine ausschaltzeit programmieren muss.

Wenn ich jetzt aber z.B. einen Rollladen nur um 09:00 Uhr hochfahren möchte muss ich die andere Zeit filtern.

Schön wäre hier ein Node wo man pro Position die man hinzufügt nur eine schalt Befehl hat.

Als Eingabe dann
-Zeit
-On/Off oder Position
-ggf. noch der Wochentag


Mobil unterwegs!
(Intel-Nuc Proxmox) (Homematic) (EnOcean) (CUL868) (CUL433) (Zigbee2MQTT) (ESP8266) (Echo) (DUOFERN)

Absolute Beginner

Lange nicht mehr hier gewesen. Ich möchte kurz über meine Erfahrungen berichten. Mit Node-RED bin ich nach wie vor begeistert unterwegs. Die Möglichkeiten der Bedienoberfläche sind gut, und ich habe diese auf meine Bedürfnisse für ein Smartphone optimiert (siehe Screenshot). Dabei geht es mir um eine Ein-Seiten-Darstellung aller relevanten Informationen (Licht, Rollos, Heizung, Anwesenheit, Temperaturen und Wetter. Solange alles 'im grünen Bereich' ist, sehe ich dies auf einem Blick - sonst tauchen andere Farben auf. So schön und praktisch Slider auch sind - ich habe sie gegen Pull-down-Menüs ausgetauscht, weil sie beim Wischen ständig ungewollt Aktionen ausführten. Auf weiteren Seiten sind Anrufliste, Systemkontrollen und Ansichten von Videokameras.

Angefangen habe ich mit der Brückenfunktion FHEM-MQTT-NodeRED. Inzwischen habe ich mehr und mehr direkt in NodeRED umgesetzt (8266-basierte WLAN-Produkte, Intertechno, Anrufliste, Wettervorhersage, IKEA Tradfri und Alexa-Sprachbedienung), sodass heute nur noch HomeMatic und SOMFY (über CUL) und Viessmann-Heizung (über USB) mittels FHEM/MQTT laufen.

Ich würde mich natürlich freuen, wenn hier mehr Aktivität zum Thema zu finden ist. Gerne beantworte ich Fragen zu meiner Umsetzung.
Raspberry Pi 3 - CUL868 - Jessie - FHEM5.8 - MQTT - Node-RED
HM-TC-IT-WM-W-EU, HM-LC-BI1PBU-FM, HM-Sec-SCo, HM-WDS30-0T2-SM, SOMFY, Echo, ESP, SonOff

SamNitro

Hier mal eine kleine Übersicht von mir. Alles noch ziemlich am Anfang.
(Intel-Nuc Proxmox) (Homematic) (EnOcean) (CUL868) (CUL433) (Zigbee2MQTT) (ESP8266) (Echo) (DUOFERN)

oetti77

#73
Hallo zusammen,

ich habe mich mal an euren Beispielen orientiert, und habe aktuell eine HUE als Switch im Dashboard zum Testen.
Den State der HUE habe ich per Funktion auseinander genommen, da dieser nicht nur "on","off", sondern auch "dimXX%" sein kann, und dann per String an den Switch übergeben - funktioniert soweit.

Was mir nicht ganz klar ist, wie die Änderung im Dashboard wieder zurück zum FHEM kommt. Nehmt ihr das gleiche Topic, wie für den Input? Was muss im FHEM noch gemacht werden? Reicht da ein Subscribe auf das Topic?

Fragen über Fragen - ich hoffe ihr könnt mir helfen :-)

Danke
Chris

Erledigt! Habe mir nochmal genau Shojo's Flow angeschaut, da ist mir das Prinzipp bewusst geworden  ;D
FHEM 5.8 (CentOS 7 auf ESXi 6.5), HM-CC-RT-DN, HM-Sec-Sc, HM-WDS40-TO, HM-LC-SW1-FM, HM-LC-Bl1PBU-FM, Sonos, Alexa, Nest Protect 2, Tradfri

Module: HUE, Lightify, ECOTOUCH, TelegramBot, Sonos, Alexa, Pushover, Enigma2

Master_Nick

Jeder Device hat ein Topic auf dem er lauscht und danach schaltet und ein Topic auf dem er Rückmeldung gibt.

Beispiel fürs schalten:  wohnung/elektrik/sonoff/sonoff-s20-3/relay/set

Beispiel fürs Rückmelden:  wohnung/elektrik/sonoff/sonoff-s20-3/relay/

Beim ersten würde FHEM ein "set true" auf das topic "wohnung/elektrik/sonoff/sonoff-s20-3/relay/set"  abliefern (publish) beim 2. bekommt FHEM die Rückmeldung über den tatsächlichen Zustand (subscribe auf wohnung/elektrik/sonoff/sonoff-s20-3/relay/).
Konvertierungen von Strings und Co. sind mit NodeRed kein problem. Zur not einfach mal Google anhauen oder deinen Flow mal hier posten.
Rancher K8s Cluster mit nanoCUL (a-culfw) | IObroker | IT(V1&V3), IT-PIR, THGR122NX |Co² | alexa-fhem | WOL | NFC | Harmony UltimateHub | Anwesenheitserkennnung | Roomba | 10" Touch mit Node-Red | SonOff S20 | SonOff Touch | SonOff Dual | Rolladen | Und ganz viel anderes tolles Gerödel.... ;-)