node-red-contrib-ramp-thermostat
Version:
A Node-RED node that emulates a programmable thermostat.
321 lines (302 loc) • 15 kB
HTML
<script type="text/x-red" data-template-name="ramp-thermostat">
<div class="form-row">
<label for="node-input-profile"><i class="fa fa-line-chart"></i> Profile</label>
<input type="text" id="node-input-profile">
</div>
<div class="form-row">
<label for="node-input-hysteresisplus"><i class="fa fa-arrows-v"></i> Hysteresis [+]</label>
<input type="text" id="node-input-hysteresisplus" style="width:50px">
</div>
<div class="form-row">
<label for="node-input-hysteresisminus"><i class="fa fa-arrows-v"></i> Hysteresis [-]</label>
<input type="text" id="node-input-hysteresisminus" style="width:50px">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips"><span>The Hysteresis is used to prevent osciliation. The [+] value is added to the target and the [-] (absolute) value is subtracted from the target. Within this neutral zone no action accurs.</span>
</div>
</script>
<script type="text/x-red" data-help-name="ramp-thermostat">
<p>A node that emulates a thermostat.</p>
<p>The ramp-thermostat controls an actuator depending on the current input temperature and the target temperature.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload
<span class="property-type">number</span>
</dt>
<dd> current temperature</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setCurrent (or undefined)</dd>
</dl>
<dl class="message-properties">
<dt>payload
<span class="property-type">number</span>
</dt>
<dd> target temperature</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setTarget</dd>
</dl>
<dl class="message-properties">
<dt>payload
<span class="property-type">number</span>
</dt>
<dd> hysteresis [+]</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setHysteresisPlus</dd>
</dl>
<dl class="message-properties">
<dt>payload
<span class="property-type">number</span>
</dt>
<dd> hysteresis [-]</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setHysteresisMinus</dd>
</dl>
<dl class="message-properties">
<dt>payload
<span class="property-type">string</span>
</dt>
<dd> profile name</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setProfile</dd>
</dl>
<dl class="message-properties">
<dt>payload
<span class="property-type">object</span>
</dt>
<dd> JSON e.g. {"name":"myGreatProfile","points":{"00:00":16.0,"08:00":20.0,
"20:00":20.0,"23:59":16.0}}</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> setProfile</dd>
</dl>
<h3>Outputs</h3>
<ol class="node-ports">
<li>State
<dl class="message-properties">
<dt>payload <span class="property-type">boolean</span></dt>
<dd>the state of the thermostat.</dd>
</dl>
</li>
<li>Current Temperature
<dl class="message-properties">
<dt>payload <span class="property-type">number</span></dt>
<dd>the current temperature.</dd>
</dl>
</li>
<li>Target Temperature
<dl class="message-properties">
<dt>payload <span class="property-type">number</span></dt>
<dd>the actual target temperature.</dd>
</dl>
</li>
</ol>
<h3>Details</h3>
<p>The target temperature is defined by a profile that
provides the value depending on the current time <code>00:00-23:59</code>.
The profile consists of several points whose connections build a sequence of lines.
The switching moment can be optimized by defining a gradient line like a <code>ramp</code>.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('ramp-thermostat',{
category: 'smart home',
color: '#E9967A',
defaults: {
name: {value:""},
profile: {value:"",type:"profile"},
hysteresisplus: {value:0, validate:RED.validators.number()},
hysteresisminus: {value:0, validate:RED.validators.number()}
},
inputs:1,
outputs:3,
outputLabels: ["state","current temperature","target temperature"],
icon: "trigger.png",
label: function() {
return this.name||"ramp-thermostat";
},
paletteLabel: 'thermostat'
});
</script>
<script type="text/x-red" data-template-name="profile">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name">
</div>
<div>
<label style="float:left; margin-bottom:10px"><i class="fa fa-dot-circle-o"></i> Points:</label>
<label style="float:left;margin-left:45px;"> Time (hh:mm) Temp (°)</label>
</div>
<div class="form-row">
<label for="node-config-input-time1"><i class="fa fa-dot-circle-o"></i> #1</label>
<input class="input-append-left" type="time" id="node-config-input-time1" style="width:120px">
<input type="number" id="node-config-input-temp1" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time2"><i class="fa fa-dot-circle-o"></i> #2</label>
<input class="input-append-left" type="time" id="node-config-input-time2" style="width:120px">
<input type="number" id="node-config-input-temp2" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time3"><i class="fa fa-dot-circle-o"></i> #3</label>
<input class="input-append-left" type="time" id="node-config-input-time3" style="width:120px">
<input type="number" id="node-config-input-temp3" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time4"><i class="fa fa-dot-circle-o"></i> #4</label>
<input class="input-append-left" type="time" id="node-config-input-time4" style="width:120px">
<input type="number" id="node-config-input-temp4" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time5"><i class="fa fa-dot-circle-o"></i> #5</label>
<input class="input-append-left" type="time" id="node-config-input-time5" style="width:120px">
<input type="number" id="node-config-input-temp5" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time6"><i class="fa fa-dot-circle-o"></i> #6</label>
<input class="input-append-left" type="time" id="node-config-input-time6" style="width:120px">
<input type="number" id="node-config-input-temp6" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time7"><i class="fa fa-dot-circle-o"></i> #7</label>
<input class="input-append-left" type="time" id="node-config-input-time7" style="width:120px">
<input type="number" id="node-config-input-temp7" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time8"><i class="fa fa-dot-circle-o"></i> #8</label>
<input class="input-append-left" type="time" id="node-config-input-time8" style="width:120px">
<input type="number" id="node-config-input-temp8" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time9"><i class="fa fa-dot-circle-o"></i> #9</label>
<input class="input-append-left" type="time" id="node-config-input-time9" style="width:120px">
<input type="number" id="node-config-input-temp9" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-row">
<label for="node-config-input-time10"><i class="fa fa-dot-circle-o"></i> #10</label>
<input class="input-append-left" type="time" id="node-config-input-time10" style="width:120px">
<input type="number" id="node-config-input-temp10" style="margin-left:45px; width:100px" step=".1">
</div>
<div class="form-tips"><span>A Profile has at least 2 Points and must start at 00:00 and end at 23:59.</span>
</div>
</script>
<script type="text/javascript">
var time_re = new RegExp(/(^$)|(^(?:[01]\d|2[0-4]):[0-5]\d$)/);
var temp_re = new RegExp(/^[+-]?\d*\.?\d+$|^$/);
function getValue(node, property) {
var field = $('#node-config-input-' + property)[0];
// When the config screen is open, we need to get the values from the screen.
// When the config screen is closed, we need to get the values from 'this' Javascript object.
if (field) {
return field.value;
}
else {
return node[property];
}
}
function validateSetPoints(property, value) {
// The index is after the first 4 characters ('time' or 'temp')
var index = parseInt(property.substring(4));
if (value) {
if (property.startsWith("time")) {
// Invalid time: when time pattern invalid
if (!time_re.test(value)) {
return false;
}
var parts = value.split(':');
var minutes = 60 * parseInt(parts[0]) + parseInt(parts[1]);
var first = true;
var last = true;
// Search backwards (from the current pair to pair 1) for non-empty times
for (var i = index - 1; i > 0; i--) {
if (getValue(this, 'time' + i)) {
first = false;
parts = getValue(this, 'time' + i).split(':');
var previousMinutes = 60 * parseInt(parts[0]) + parseInt(parts[1]);
// Invalid time: when current time is before the previous non-empty time
if (minutes < previousMinutes) {
return false;
}
break;
}
}
// Invalid time: when first non-empty time pattern not equals 00:00
if (first && value != "00:00") {
return false;
}
// Search forward (from the current pair to pair 10) for non-empty times
for (var j = index + 1; j <= 10; j++) {
if (getValue(this, 'time' + j)) {
last = false;
break;
}
}
if (last && value != "23:59" && value != "24:00") {
return false;
}
}
else {
// Invalid temperature: when temperature pattern invalid
if (!temp_re.test(value)) {
return false;
}
}
}
else {
if (property.startsWith("time")) {
// Invalid time: when time empty but temperature not empty
if (getValue(this, 'temp' + index)) {
return false;
}
}
else {
// Invalid temperature: when temperature empty but time not empty
if (getValue(this, 'time' + index)) {
return false;
}
}
}
return true;
}
RED.nodes.registerType('profile',{
category: 'config',
defaults: {
name: {value:'', required:true},
time1: {value:'', validate:function(v) { return validateSetPoints.call(this,"time1" ,v) }},
temp1: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp1" ,v) }},
time2: {value:'', validate:function(v) { return validateSetPoints.call(this,"time2" ,v) }},
temp2: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp2" ,v) }},
time3: {value:'', validate:function(v) { return validateSetPoints.call(this,"time3" ,v) }},
temp3: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp3" ,v) }},
time4: {value:'', validate:function(v) { return validateSetPoints.call(this,"time4" ,v) }},
temp4: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp4" ,v) }},
time5: {value:'', validate:function(v) { return validateSetPoints.call(this,"time5" ,v) }},
temp5: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp5" ,v) }},
time6: {value:'', validate:function(v) { return validateSetPoints.call(this,"time6" ,v) }},
temp6: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp6" ,v) }},
time7: {value:'', validate:function(v) { return validateSetPoints.call(this,"time7" ,v) }},
temp7: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp7" ,v) }},
time8: {value:'', validate:function(v) { return validateSetPoints.call(this,"time8" ,v) }},
temp8: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp8" ,v) }},
time9: {value:'', validate:function(v) { return validateSetPoints.call(this,"time9" ,v) }},
temp9: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp9" ,v) }},
time10: {value:'', validate:function(v) { return validateSetPoints.call(this,"time10",v) }},
temp10: {value:'', validate:function(v) { return validateSetPoints.call(this,"temp10",v) }},
},
label: function() {
return this.name;
},
oneditprepare: function() {
var timei;
for (var i = 1; i <= 10; i++) {
timei = "time"+i;
if (this[timei] === "24:00") {
// Since 24:00 is not supported by the 'time' input field, we will convert it to 23:59 into
// both the Javascript object and into the 'time' input field on the screen.
this[timei] = "23:59";
$("#node-config-input-time"+i).val("23:59");
}
}
}
});
</script>