UNPKG

node-red-contrib-heater-controller

Version:
491 lines (479 loc) 22.5 kB
<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" : &lt;topic set in properties&gt;, "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>