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
HTML
<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>