UNPKG

smart-nodes

Version:

Controls light, shutters and more. Includes common used logic and statistic nodes to control your home.

316 lines (272 loc) 11.9 kB
<script type="text/javascript"> (function () { RED.nodes.registerType("smart_heating-curve", { category: "Smart Nodes", paletteLabel: "Heating Curve", color: "#3FADB5", defaults: { name: { value: "" }, room_setpoint: { value: 20 }, slope: { value: 1.3 }, offset: { value: 0 }, flow_max: { value: 75 }, flow_min: { value: 20 }, config_change_date: { value: "" }, }, inputs: 1, outputs: 1, icon: "font-awesome/fa-line-chart", label: function () { return this.name || "Heating Curve"; }, oneditprepare: function () { let node = this; $("#node-input-room_setpoint") .css("max-width", "4rem") .spinner({ min: 5, max: 100, step: 0.5, change: function (event, ui) { var value = parseFloat(this.value); value = isNaN(value) ? 0 : value; value = Math.max(value, parseFloat($(this).attr("aria-valuemin"))); value = Math.min(value, parseFloat($(this).attr("aria-valuemax"))); if (value !== this.value) $(this).spinner("value", value); drawCanvas(); }, }); $("#node-input-slope") .css("max-width", "4rem") .spinner({ min: 0.05, max: 4, step: 0.05, change: function (event, ui) { var value = parseFloat(this.value); value = isNaN(value) ? 0 : value; value = Math.max(value, parseFloat($(this).attr("aria-valuemin"))); value = Math.min(value, parseFloat($(this).attr("aria-valuemax"))); if (value !== this.value) $(this).spinner("value", value); drawCanvas(); }, }); $("#node-input-offset").css("max-width", "4rem") .spinner({ min: -20, max: 20, step: 0.5, change: function (event, ui) { var value = parseFloat(this.value); value = isNaN(value) ? 0 : value; value = Math.max(value, parseFloat($(this).attr("aria-valuemin"))); value = Math.min(value, parseFloat($(this).attr("aria-valuemax"))); if (value !== this.value) $(this).spinner("value", value); drawCanvas(); }, }); $("#node-input-flow_max") .css("max-width", "4rem") .spinner({ min: 5, max: 100, change: function (event, ui) { var value = parseFloat(this.value); value = isNaN(value) ? 0 : value; value = Math.max(value, parseFloat($(this).attr("aria-valuemin"))); value = Math.min(value, parseFloat($(this).attr("aria-valuemax"))); if (value !== this.value) $(this).spinner("value", value); drawCanvas(); }, }); $("#node-input-flow_min") .css("max-width", "4rem") .spinner({ min: 5, max: 100, change: function (event, ui) { var value = parseFloat(this.value); value = isNaN(value) ? 0 : value; value = Math.max(value, parseFloat($(this).attr("aria-valuemin"))); value = Math.min(value, parseFloat($(this).attr("aria-valuemax"))); if (value !== this.value) $(this).spinner("value", value); drawCanvas(); }, }); drawCanvas(); }, oneditsave: function () { let node = this; node.config_change_date = (new Date()).toISOString(); }, }); let drawCanvas = () => { const canvas = $("#heating-curve-diagram")[0]; const ctx = canvas.getContext("2d"); ctx.reset(); drawAxis(canvas, ctx); drawGrid(canvas, ctx); drawCurve(canvas, ctx, "#808080", 0.5, 0); drawCurve(canvas, ctx, "#808080", 1, 0); drawCurve(canvas, ctx, "#808080", 1.5, 0); drawCurve(canvas, ctx, "#808080", 2, 0); drawCurve(canvas, ctx, "#808080", 2.5, 0); drawCurve(canvas, ctx, "#808080", 3, 0); drawCurve(canvas, ctx, "#808080", 3.5, 0); drawCurve(canvas, ctx, "#808080", 4, 0); drawCurve(canvas, ctx, "#0000FF", parseFloat($("#node-input-slope").val()), parseFloat($("#node-input-offset").val()), true); } let mapPoint = (out, flow) => { const canvas = $("#heating-curve-diagram")[0]; return [scale(out, -20, 25, canvas.width, 0), scale(flow, 10, 100, canvas.height, 0)]; } let scale = (number, inMin, inMax, outMin, outMax) => { return (number - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; } let drawAxis = (canvas, ctx) => { const arrow_length = 10; ctx.strokeStyle = "#000000"; ctx.beginPath(); // draw axis ctx.moveTo(...mapPoint(20, 100)); ctx.lineTo(...mapPoint(20, 10)); ctx.moveTo(...mapPoint(25, 20)); ctx.lineTo(...mapPoint(-20, 20)); // draw arrow up let point = mapPoint(20, 100); ctx.moveTo(...point); ctx.lineTo(point[0] - arrow_length / 2, point[1] + arrow_length); ctx.moveTo(...point); ctx.lineTo(point[0] + arrow_length / 2, point[1] + arrow_length); // draw arrow right point = mapPoint(-20, 20); ctx.moveTo(...point); ctx.lineTo(point[0] - arrow_length, point[1] - arrow_length / 2); ctx.moveTo(...point); ctx.lineTo(point[0] - arrow_length, point[1] + arrow_length / 2); ctx.stroke(); } let drawGrid = (canvas, ctx) => { ctx.fillStyle = "#000000"; ctx.font = "15px Arial"; ctx.strokeStyle = "#CCCCCC"; ctx.beginPath(); // horizontal lines ctx.textAlign = "right"; ctx.textBaseline = "middle"; for (let i = 30; i <= 90; i += 10) { let [x, y] = mapPoint(20, i); ctx.moveTo(x - 15, y); ctx.lineTo(canvas.width - 15, y); ctx.fillText(i.toString(), 25, y); } // vertical lines ctx.textAlign = "center"; for (let i = -15; i <= 15; i += 5) { let [x, y] = mapPoint(i, 20); ctx.moveTo(x, 15); ctx.lineTo(x, y + 15); ctx.fillText(i.toString(), x, canvas.height - 10); } ctx.stroke(); } let drawCurve = (canvas, ctx, color, slope, offset, useMinMax = false) => { const room_setpoint = parseFloat($("#node-input-room_setpoint").val()); const flow_min = parseFloat($("#node-input-flow_min").val()); const flow_max = parseFloat($("#node-input-flow_max").val()); ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = "12px Arial"; ctx.strokeStyle = color; ctx.beginPath(); let first = true; let labels = []; for (let out = 20; out >= -20; out -= 1) { let dar = out - room_setpoint; let flow = room_setpoint + offset - slope * dar * (1.4347 + 0.021 * dar + 247.9 * Math.pow(10, -6) * Math.pow(dar, 2)); if (useMinMax) flow = Math.min(Math.max(flow, flow_min), flow_max); let point = mapPoint(out, flow); if (first) ctx.moveTo(...point); else ctx.lineTo(...point); first = false; if (point[0] >= canvas.width) { let x = canvas.width - 20; let y = Math.max(point[1], 20); labels.push({ x, y, text: slope.toString() }); break; } if (point[1] <= 0) { let x = Math.min(point[0] - 20, canvas.width - 20); let y = 10; labels.push({ x, y, text: slope.toString() }); break; } } ctx.stroke(); ctx.beginPath(); for (let label of labels) { ctx.fillStyle = "#FFFFFF"; ctx.rect(label.x - 10.5, label.y - 8.5, 21, 16); ctx.fillRect(label.x - 10, label.y - 8, 20, 15); ctx.fillStyle = "#000000"; if (useMinMax) ctx.fillStyle = color; ctx.fillText(label.text, label.x, label.y); } ctx.stroke(); } })(); </script> <script type="text/html" data-template-name="smart_heating-curve"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="heating.ui.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]heating.ui.name" /> </div> <div class="form-row"> <label for="node-input-room_setpoint"><i class="fa fa-thermometer-three-quarters"></i> <span data-i18n="heating.ui.room_setpoint"></span></label> <input id="node-input-room_setpoint" value="0" /> °C [15 - 25] </div> <div class="form-row"> <label for="node-input-slope"><i class="fa fa-expand"></i> <span data-i18n="heating.ui.slope"></span></label> <input id="node-input-slope" value="0" /> [0,05 - 4] </div> <div class="form-row"> <label for="node-input-offset"><i class="fa fa-arrows-v"></i> <span data-i18n="heating.ui.offset"></span></label> <input id="node-input-offset" value="0" /> </div> <div class="form-row"> <label for="node-input-flow_min"><i class="fa fa-arrow-down"></i> <span data-i18n="heating.ui.min_setpoint"></span></label> <input id="node-input-flow_min" value="0" /> </div> <div class="form-row"> <label for="node-input-flow_max"><i class="fa fa-arrow-up"></i> <span data-i18n="heating.ui.max_setpoint"></span></label> <input id="node-input-flow_max" value="0" /> </div> <div class="form-row"> <canvas id="heating-curve-diagram" width="450" height="300"></canvas> </div> </script>