nodes/heater/heater-controller.html
<script type="text/html text/x-red" data-template-name="ui_heater_controller">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="heater_controller.label.name"></span></label>
<input type="text" id="node-input-name">
</div>
<div class="form-row" id="template-row-group">
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="heater_controller.label.group"></span></label>
<input type="text" id="node-input-group">
</div>
<div class="form-row" id="template-row-size">
<label><i class="fa fa-object-group"></i> <span data-i18n="heater_controller.label.size"></span></label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="heater_controller.label.topic"></span></label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-title"><i class="fa fa-tasks"></i> <span data-i18n="heater_controller.label.title"></span></label>
<input type="text" id="node-input-title">
</div>
<div class="form-row">
<label for="node-input-displayMode"><i class="fa fa-thermometer-empty"></i> <span data-i18n="heater_controller.label.displayMode"></span></label>
<select id="node-input-displayMode">
<option value="buttons" id="btns" selected="selected" data-i18n="heater_controller.label.dmButtons"></option>
<option value="slider" id="slide" data-i18n="heater_controller.label.dmSlider"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-thermometer-empty"></i> <span data-i18n="heater_controller.label.unit"></span></label>
<select id="node-input-unit">
<option value="C" id="cUnit" selected="selected" data-i18n="heater_controller.label.celsiusUnit"></option>
<option value="F" id="fUnit" data-i18n="heater_controller.label.fahrenheitUnit"></option>
<option value="K" id="kUnit" data-i18n="heater_controller.label.kelvinUnit"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-threshold"><i class="fa fa-arrows-v"></i> <span data-i18n="heater_controller.label.hysteresis"></label>
<input type="number" min="-100" max="100" step="0.1" id="node-input-threshold" data-i18n="[placeholder]heater_controller.placeholder.value" dir="">
</div>
<div class="form-row">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="heater_controller.label.logTitle"> </span></label>
</div>
<div class="ident">
<div class="form-row">
<label for="node-input-logLength"><i class="fa fa-file-text-o" aria-hidden="true"></i> <span data-i18n="heater_controller.label.logLength"></span></label>
<input type="number" min="0" max="100" id="node-input-logLength">
</div>
</div>
<div id="slider-settings">
<div class="form-row">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="heater_controller.label.slider"> </span></label>
</div>
<div class="ident" >
<div class="form-row">
<label for="node-input-sliderMinValue"><i class="fa fa-step-backward" aria-hidden="true"></i> <span data-i18n="heater_controller.label.min"></span></label>
<input type="number" step="1" id="node-input-sliderMinValue" data-i18n="[placeholder]heater_controller.placeholder.value">
</div>
<div class="form-row">
<label for="node-input-sliderMaxValue"><i class="fa fa-step-forward" aria-hidden="true"></i> <span data-i18n="heater_controller.label.max"></span></label>
<input type="number" step="1" id="node-input-sliderMaxValue" data-i18n="[placeholder]heater_controller.placeholder.value">
</div>
<div class="form-row">
<label for="node-input-sliderStep"> <span data-i18n="heater_controller.label.step"></span></label>
<input type="number" min="0.1" step="0.1" id="node-input-sliderStep" data-i18n="[placeholder]heater_controller.placeholder.value">
</div>
</div>
</div>
<div class="form-row">
<label id="node-input-calendarEditorLabel" style="width:95%; border-bottom: 1px solid #eee;">
<i class="fa fa-calendar"></i>
<span data-i18n="heater_controller.label.calendar"></span>
<button id="node-input-json-format" class="editor-button editor-button-small" style="float: right"><span data-i18n="editor:jsonEditor.format"></span></button>
</label>
</div>
<div class="ident">
<div class="form-row">
<input type="hidden" id="node-input-calendar">
</div>
<div class="form-row node-text-editor-row">
<div style="height:650px; min-height:100px" class="node-text-editor" id="node-input-calendarEditor" ></div>
</div>
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('ui_heater_controller', {
category: 'dashboard',
color: 'rgb( 63, 173, 181)',
defaults: {
name: { value: 'heater' },
group: {
type: 'ui_group',
required: true,
validate: function (v) {
return v && RED.nodes.node(v) && RED.nodes.node(v).type === 'ui_group';
}
},
unit: {
value: 'C',
required: true,
validate: function (value) {
return ['C', 'F', 'K'].indexOf(value) >= 0;
}
},
displayMode: {
value: 'buttons',
required: true,
validate: function (value) {
return ['buttons', 'slider'].indexOf(value) >= 0;
}
},
order: { value: 0 },
width: {
value: 6,
required: true,
validate: function (v) {
var valid = true;
var width = v || 0;
var currentGroup = $('#node-input-group').val() || this.group;
var groupNode = RED.nodes.node(currentGroup);
valid = !groupNode || +width <= +groupNode.width;
$('#node-input-size').toggleClass('input-error', !valid);
return valid;
}
},
height: { value: 4 },
topic: { value: '' },
title: { value: 'Heater' },
logLength: {
value: 0,
required: true,
validate: function(v){
return RED.validators.number()(v) && v >= 0 && v <= 100;
}
},
sliderMinValue: {
value: 10,
required: true,
validate: function (v) {
var max = $('#node-input-sliderMaxValue').val() || this.sliderMaxValue;
return RED.validators.number(v)(v) && v > 0 && max > v;
}
},
sliderMaxValue: {
value: 35,
required: true,
validate: function (v) {
var min = $('#node-input-sliderMinValue').val() || this.sliderMinValue;
return RED.validators.number(v)(v) && v > 0 && min < v;
}
},
sliderStep: {
value: 0.5,
required: true,
validate: function (v) {
return RED.validators.number(v)(v) && v > 0;
}
},
threshold: {
value: 0.5,
validate: function (v) {
return RED.validators.number(v)(v) && v >= -100 && v <= 100;
}
},
calendar: {
required: true,
validate: function (v) {
var valid = true;
var calendar = {};
if (typeof v === 'object') {
calendar = v;
} else if (typeof v === 'string') {
try {
calendar = JSON.parse(v);
} catch (e) {
valid = false;
console.log('Invalid JSON in Heater calendar ', e);
}
} else {
valid = false;
console.log('Invalid JSON in Heater calendar ', e);
}
var wd = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
].sort();
if (valid && wd.length != Object.keys(calendar).sort().length) {
valid = false
}
for (var i = 0; valid && i < wd.length; i++) {
if (Object.keys(calendar[wd[i]]).length <= 0) {
valid = false
}
}
$('#node-input-calendarEditorLabel').toggleClass('input-error', !valid);
$('#node-input-calendarEditor').toggleClass('input-error', !valid);
return valid;
},
value: JSON.stringify(
{
Monday: {
'00:00': 19,
'06:20': 22,
'08:00': 19,
'16:40': 22,
'23:59': 19
},
Tuesday: {
'00:00': 19,
'06:20': 22,
'08:00': 19,
'16:40': 22,
'23:59': 19
},
Wednesday: {
'00:00': 19,
'06:20': 22,
'08:00': 19,
'16:40': 22,
'23:59': 19
},
Thursday: {
'00:00': 19,
'06:20': 22,
'08:00': 19,
'16:40': 22,
'23:59': 19
},
Friday: {
'00:00': 19,
'06:20': 23,
'08:00': 19,
'16:40': 22,
'23:59': 19
},
Saturday: {
'00:00': 19,
'08:00': 20,
'20:00': 22,
'23:59': 19
},
Sunday: {
'00:00': 19,
'08:00': 20,
'20:00': 22,
'23:59': 19
}
},
null,
4
)
}
},
inputs: 1,
outputs: 2,
align: 'left',
icon: 'heater.png',
paletteLabel: 'heater',
label: function () {
return this.name || 'heater'
},
oneditprepare: function () {
var node = this;
$('#btns').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.dmButtons'));
$('#slide').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.dmSlider'));
$('#node-input-topic').val(this.topic);
$('#cUnit').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.celsiusUnit'));
$('#fUnit').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.fahrenheitUnit'));
$('#kUnit').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.kelvinUnit'));
$('#daysType').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.daysType'));
$('#changesType').text(RED._('node-red-contrib-heater_controller/ui_heater_controller:heater_controller.label.changesType'));
$('#red-ui-editor-node-label-form-input-0').val('currentTemp | userConfig | setCalendar | config | status');
$('#red-ui-editor-node-label-form-output-0').val('Heater status');
$('#red-ui-editor-node-label-form-output-1').val('Info');
ace.edit($('#node-info-input-info-editor>.ace_editor')[0]).setValue('Description');
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
});
this.editor = RED.editor.createEditor({
id: 'node-input-calendarEditor',
mode: 'ace/mode/json',
value: $('#node-input-calendar').val()
});
var editor = this.editor;
$('#node-input-json-format').on('click', function (e) {
e.preventDefault();
try {
editor
.getSession()
.setValue(
JSON.stringify(
JSON.parse(editor.getSession().getValue()),
null,
4
)
)
} catch (e) {
console.log('Invalid JSON in Heater calendar ');
}
});
this.editor.focus();
},
oneditsave: function () {
var calendarVal = this.editor.getValue();
this.editor.destroy();
delete this.editor;
this.topic = $('#node-input-topic').val();
this.logLength = Number.parseInt($('#node-input-logLength').val());
this.sliderMaxValue = Number.parseFloat($('#node-input-sliderMaxValue').val());
this.sliderMinValue = Number.parseFloat($('#node-input-sliderMinValue').val());
this.sliderStep = Number.parseFloat($('#node-input-sliderStep').val());
this.threshold = Number.parseFloat($('#node-input-threshold').val());
$('#node-input-calendar').val(calendarVal);
},
oneditcancel: function () {
this.editor.destroy();
delete this.editor;
},
oneditresize: function (size) {
this.editor.resize();
}
})
</script>
<script type="text/html text/x-red" data-help-name="ui_heater_controller">
<p>A dashboard ui interface node for controlling a heater;</p>
<h3>Properties</h3>
<dl class="message-properties">
<dt>Topic <span class="property-type">string</span></dt>
<dd>Topic sent with all messages; this cannot be taken from input since it expects a fixed topic</dd>
</dl>
<dl class="message-properties">
<dt>Title <span class="property-type">string</span></dt>
<dd>An optional title that will appear in front view to identify the heater.</dd>
</dl>
<dl class="message-properties">
<dt>Unit <span class="property-type">Celsius/Fahrenheit/Kelvin</span></dt>
<dd>Display unit</dd>
</dl>
<dl class="message-properties">
<dt>Min <span class="property-type">integer</span></dt>
<dd>Minimum value selectable using slider</dd>
</dl>
<dl class="message-properties">
<dt>Max <span class="property-type">boolean</span></dt>
<dd>Maximum vale selectable using slider</dd>
</dl>
<dl class="message-properties">
<dt>Step <span class="property-type">integer</span></dt>
<dd>Step value selectable from slider</dd>
</dl>
<dl class="message-properties">
<dt>Threshold<span class="property-type">float</span></dt>
<dd>Hysteresis/threshold limit. This value is added or subtracted from the target temperature to determinate the point of setting heater on or off</dd>
</dl>
<dl class="message-properties">
<dt>Calendar<span class="property-type">json</span></dt>
<dd>The calendar which will be apply in automatic mode. Needs to be a valid JSON with float values for temperature.<br>
Is important to cover the entire interval of 24/7, otherwise will keep the temperature until next since or next week day</dd>
<dd>For example:</dd>
<pre>
{
"Monday" : {
"00:00" : 19,
"06:20" : 22,
"08:00" : 19,
"16:40" : 22,
"23:59" : 19
},
"Tuesday" : {
"00:00" : 19,
"06:20" : 22,
"08:00" : 19,
"16:40" : 22,
"23:59" : 19
}
}
</pre>
</dl>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>currentTemp</dt>
<dd>
This controller accepts one main input which has to have topic as "currentTemp" and payload needs to be a float
<p>The heater status is recalculated when this message received, or when the user is changing the target temperature.</p>
<p style="color: #AD1625;">The entire control is not functional until this message is received</p>
<pre>{
"topic" : "currentTemp",
"payload" : "22.5"
}</pre>
</dd>
</dl>
<dl class="message-properties">
<dt>userConfig</dt>
<dd>To set user configuration value other than through the UI (eg. Voice command), the optional topic <b>userConfig</b> can be used.
<ul>
<li><b>isLocked</b> - will enable/disable the lock on the dashboard;</li>
<li><b>userTargetValue</b> - will set a user target temperature on the dashboard, and will set `isUserCustom` as true if is not set in the message; </li>
<li><b>isUserCustom</b> - will set a user target temperature on the dashboard according to current target or to current `userTargetValue`, if is received on false without `userTargetValue` then the will change only the slider;</li>
</ul>
<pre>{
"topic" : "userConfig",
"payload" : {"isLocked": true, "userTargetValue": 25.5, "isUserCustom": false}
}</pre>
</dd>
</dl>
<h3>Output</h3>
<p>We have two outputs:
<ol class='node-ports'>
<li>Heater status: Emitted each time when the heater status has been recalculated; </li>
<li>Info: Emitted when receives status, log or config topics; </li>
</ol>
</p>
<dd>Heater status syntax:</dd>
<pre>{
"topic" : <topic set in properties>,
"payload": [on|off]
}</pre>
<dd>Computation status example:</dd>
<pre>
{
"currentTemp":25,
"targetValue":20,
"currentSchedule":{
temp: 19,
day: "Wednesday",
time: "00:00"
},
"nextSchedule":{
temp: 22,
day: "Wednesday",
time: "06:20"
},
"currentHeaterStatus":"off",
"userTargetValue":20,
"isUserCustom":true
}
</pre>
<dl class="message-properties">
<dt>currentTemp<span class="property-type">float</span></dt>
<dd>The last current temperature received</dd>
</dl>
<dl class="message-properties">
<dt>targetValue<span class="property-type">float</span></dt>
<dd>Target temperature displayed in front-end. Could be user custom value, if is changed by the user, or calendar current temperature value set by calendar</dd>
</dl>
<dl class="message-properties">
<dt>currentSchedule<span class="property-type">float</span></dt>
<dd>Current calendar schedule. The value which would be set if the controller is set on calendar and some additional information like day of the week and time</dd>
</dl>
<dl class="message-properties">
<dt>nextSchedule<span class="property-type">float</span></dt>
<dd>Next calendar schedule. The value which will be set if the controller is set on calendar and some additional information when this will happen, like week day and time</dd>
</dl>
<dl class="message-properties">
<dt>currentHeaterStatus<span class="property-type">string (on|off)</span></dt>
<dd>Calculated the heater status based on the difference between target value and current temperature</dd>
</dl>
<dl class="message-properties">
<dt>userTargetValue<span class="property-type">float</span></dt>
<dd>The current target temperature set by user. This value will be the target until the next schedule if the isLocked is false.</dd>
</dl>
<dl class="message-properties">
<dt>isUserCustom<span class="property-type">boolean</span></dt>
<dd>True if current target temperature is set by the user</dd>
</dl>
<dl class="message-properties">
<dt>isLocked<span class="property-type">boolean</span></dt>
<dd>True if current target temperature is locked; if is true, the calendar value will not become targetValue, if is false and the user has set a custom value that value will be available until next schedule change</dd>
</dl>
</script>