node-red-contrib-light-scheduler
Version:
Light Scheduler is a node-red node that provides a weekly schedule, and is mainly focused on controlling light in home automation scenarios.
385 lines (348 loc) • 15.1 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('light-scheduler', {
category: 'function',
color:"#F8B1FF",
defaults: {
settings: {value: "", type: "light-scheduler-settings"},
events: {value:"[]"},
topic: {value:""},
name: {value:""},
onPayload: {value:"ON", validate: RED.validators.typedInput("onPayloadType")},
onPayloadType: {value:"str"},
offPayload: {value:"OFF", validate: RED.validators.typedInput("offPayloadType")},
offPayloadType: {value:"str"},
onlyWhenDark: {value: true},
scheduleRndMax: {value: 0},
sunElevationThreshold: {value: 6, required: true, validate: RED.validators.number()},
sunShowElevationInStatus: {value: false},
outputfreq: {value: "output.statechange.startup"}
},
inputs: 1,
outputs: 1,
icon: 'cal.png',
paletteLabel: 'Light Scheduler',
label: function(){
return this.name?this.name:'Light Scheduler'
},
inputLabels: ['Control Input'],
outputLabels: ['Schedule Output'],
oneditprepare: function() {
var node = this;
node.fromMoment = function(m) {
m = moment(m);
return {
dow: m.day(),
mod: m.hours() * 60 + m.minutes(),
};
};
node.toMoment = function(o) {
var day = o.dow == 0 ? 7 : o.dow;
var m = moment('2018-01-0' + day + ' 00:00:00');
m.hours(Math.floor(o.mod / 60));
m.minutes(o.mod % 60);
return m;
};
node.nextId = (function() {
var id = 0;
return function() {
return id++;
};
})();
var setup = function(node) {
$("#node-input-duskdawn").val(node.onlyWhenDark ? 'duskdawn.goldenhour' : 'duskdawn.none');
$("#node-input-outputfreq").val(node.outputfreq ? node.outputfreq : 'output.statechange.startup');
node.configuredEvents = [];
JSON.parse(node.events).forEach(function(e) {
if(!(e.start != undefined && e.start.dow != undefined && e.start.mod != undefined))
return;
if(!(e.end != undefined && e.end.dow != undefined && e.end.mod != undefined))
return;
// Workaround for issue #64, for events that cross over to the next day.
// This should be solved by creating a new event the next day.
// https://github.com/niklaswall/node-red-contrib-light-scheduler/issues/64
if(e.start.mod > e.end.mod)
e.end.mod = 0;
eventData = {
id: node.nextId(),
title: '',
start: node.toMoment(e.start),
end: node.toMoment(e.end),
stick: true,
};
if(eventData.start.isAfter(eventData.end)) {
// start is a sunday (which is moved from date 0 to 7)
eventData.end.add(7, 'days');
}
node.configuredEvents.push(eventData);
});
$('#calendar').fullCalendar({
timezone: false,
defaultDate: moment('2018-01-01 00:00:00'),
header: false,
footer: false,
firstDay: 1,
allDaySlot: false,
defaultView: 'agendaWeek',
timeFormat: 'H:mm',
slotLabelFormat: 'H:mm',
columnFormat: 'ddd',
duration: '00:05:00',
snapDuration: '00:05:00',
displayEventTime: false,
selectOverlap: false,
eventOverlap: false,
selectable: true,
editable: true,
events: node.configuredEvents,
select: function(start, end) {
// Remove timezone
start = node.toMoment(node.fromMoment(start));
end = node.toMoment(node.fromMoment(end));
var eventData = {
id: node.nextId(),
title: '',
start: start,
end: end,
stick: true,
};
$('#calendar').fullCalendar('renderEvent', eventData, true);
$('#calendar').fullCalendar('unselect');
node.configuredEvents.push(eventData);
},
selectAllow: function(selectInfo) {
var start = moment(selectInfo.start);
var end = moment(selectInfo.end);
if(start.isSame(end, 'day')) {
return true;
}
if(start.add(1, 'day').startOf('day').isSame(end, 'minute')) {
return true; // Next day but midnight
}
return false;
},
eventClick: function(event, jsEvent, view) {
$('#calendar').fullCalendar('removeEvents', event._id);
node.configuredEvents = node.configuredEvents.filter(function(e) {
return event.id != e.id;
});
},
eventResize: function(event, delta, revertFunc) {
// Prevent resize outside of the current day
start = moment(event.start);
end = moment(event.end);
if(!start.isSame(end, 'day'))
if(!start.add(1, 'day').startOf('day').isSame(end, 'minute'))
revertFunc();
node.configuredEvents.forEach(function(e) {
if(event.id == e.id) {
e.end = moment(e.end).add(delta);
}
});
},
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
// Prevent drag outside of the current day
start = moment(event.start);
end = moment(event.end);
if(!start.isSame(end, 'day'))
if(!start.add(1, 'day').startOf('day').isSame(end, 'minute'))
revertFunc();
node.configuredEvents.forEach(function(e) {
if(event.id == e.id) {
e.start = moment(e.start).add(delta);
e.end = moment(e.end).add(delta);
}
});
},
});
if(node.onPayloadType == null) {
node.onPayloadType = "str";
}
$("#node-input-onPayloadType").val(node.onPayloadType);
$("#node-input-onPayload").typedInput({
default: 'str',
typeField: $("#node-input-onPayloadType"),
types:['str','num','bool','json']
});
if(node.offPayloadType == null) {
node.offPayloadType = "str";
}
$("#node-input-offPayloadType").val(node.offPayloadType);
$("#node-input-offPayload").typedInput({
default: 'str',
typeField: $("#node-input-offPayloadType"),
types:['str','num','bool','json']
});
}
$.getScript('light-scheduler/js/moment.min.js')
.done(function(data, textStatus, jqxhr) {
$.getScript('light-scheduler/js/fullcalendar.min.js')
.done(function(data, textStatus, jqxhr){
setup(node);
})
.fail(function(jqxhr, settings, exception ){
console.log("failed to load fullcalendar.min.js");
console.log(exception);
console.log(exception.stack);
});
});
},
oneditsave: function() {
var node = this;
// Transform and store data
var events = node.configuredEvents.map(function(e) {
return {
start: node.fromMoment(e.start),
end: node.fromMoment(e.end),
};
});
node.events = JSON.stringify(events);
var duskdawn = $("#node-input-duskdawn").val();
node.onlyWhenDark = (duskdawn == 'duskdawn.goldenhour');
delete window.calendar;
},
oneditresize: function() {
}
});
</script>
<script type="text/x-red" data-template-name="light-scheduler">
<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"></input>
</div>
<div class="form-row">
<label for="node-input-settings"><i class="fa fa-globe"></i> Globals</label>
<input type="text" id="node-input-settings"></input>
</div>
<div class="form-row">
<link rel='stylesheet' href='light-scheduler/js/fullcalendar.min.css' />
<style type="text/css">
.wc-business-hours {
font-size: 1.0em;
}
</style>
<div id="calendar" style="width: 100%; border: 1px solid grey"></div>
<div class="form-row">
<p style="padding-top: 10px"><i class="fa fa-info-circle"></i> Note: Marked schedule = "On Payload"</p>
</div>
<div class="form-row">
<label for="node-input-scheduleRndMax"><i class="fa fa-tasks"></i> Randomness (in minutes)</label>
<input type="text" id="node-input-scheduleRndMax" placeholder="0"></input>
</div>
</div>
<div style="padding-top: 15px">
<h5>Output</h5>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic"></input>
</div>
<div class="form-row">
<label for="node-input-onPayload"><i class="fa fa-envelope"></i> On Payload</label>
<input type="text" id="node-input-onPayload" placeholder="ON"></input>
<input type="hidden" id="node-input-onPayloadType"></input>
</div>
<div class="form-row">
<label for="node-input-offPayload"><i class="fa fa-envelope"></i> Off Payload</label>
<input type="text" id="node-input-offPayload" placeholder="OFF"></input>
<input type="hidden" id="node-input-offPayloadType"></input>
</div>
<div class="form-row">
<label for="node-input-outputfreq"><i class="fa fa-play"></i> Output </label>
<select id="node-input-outputfreq">
<option value="output.statechange">when state changes</option>
<option value="output.statechange.startup">when state changes + startup</option>
<option value="output.minutely">minutely</option>
</select>
</div>
</div>
<div style="padding-top: 15px">
<h5>Dusk / Dawn</h5>
<div class="form-row">
<label for="node-input-duskdawn"><i class="fa fa-moon-o"></i> Dusk / Dawn</label>
<select id="node-input-duskdawn">
<option value="duskdawn.goldenhour">Only when dark</option>
<option value="duskdawn.none">Schedule only</option>
</select>
</div>
<div class="form-row">
<label for="node-input-sunElevationThreshold"><i class="fa fa-sun-o"></i> Threshold </label>
<select id="node-input-sunElevationThreshold">
<option value="10">+10°</option>
<option value="9">+9°</option>
<option value="8">+8°</option>
<option value="7">+7°</option>
<option value="6">+6° - Golden Hour</option>
<option value="5">+5°</option>
<option value="4">+4°</option>
<option value="3">+3°</option>
<option value="2">+2°</option>
<option value="1">+1°</option>
<option value="0">+0° - Sunset / Sunrise</option>
<option value="-1">-1°</option>
<option value="-2">-2°</option>
<option value="-3">-3°</option>
<option value="-4">-4°</option>
<option value="-5">-5°</option>
<option value="-6">-6° - Civil dawn</option>
<option value="-7">-7°</option>
<option value="-8">-8°</option>
<option value="-9">-9°</option>
<option value="-10">-10°</option>
<option value="-11">-11°</option>
<option value="-12">-12° - Nautical dawn</option>
<option value="-13">-13°</option>
<option value="-14">-14°</option>
<option value="-15">-15°</option>
<option value="-16">-16°</option>
<option value="-17">-17°</option>
<option value="-18">-18° - Astronomical dawn / Nigh</option>
</select>
<div class="form-row">
<label> </label>
<input type="checkbox" id="node-input-sunShowElevationInStatus" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-sunShowElevationInStatus" style="width: 70%;">Show sun elevation in status.</label>
</div>
</div>
</div>
</script>
<script type="text/x-red" data-help-name="light-scheduler">
<p>A node to control light based on a schedule and the sun position.</p>
<p>The marked area in the weekly schedule correlates to the "On Payload" and the non marked area to the "Off Payload". The Dusk / Dawn settings overrides the schedule so that the sun position will force the schedule "Off" during daylight.</p>
<p>For more information on planned features and changes please <a href="https://github.com/niklaswall/node-red-contrib-light-scheduler#planned-features--changes">read here</a>.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">string</span></dt>
<dd>
<p>The <code>msg.payload</code> will be used to set the override-mode of the light scheduler.</p>
<p>Valid overrides:
<ul>
<li>auto - Removes the override and act according to configuration.</li>
<li>stop - Postpone all output.</li>
<li>on / 1 - Forces output to ON state.</li>
<li>off / 0 - Forces output to OFF state.</li>
<li>light-only - Ignores the schedule and turns ON if it is considered dark.</li>
<li>schedule-only - Ignores the light level and turns ON according to schedule.</li>
<li>trigger - Force output according to the ongoing settings.</li>
</ul>
<i>Please Note:</i> The override is runtime only and will revert to auto during deploy / restart.
</p>
</dd>
</dl>
<h3>Output</h3>
<dl class="message-properties">
<dt>payload</dt>
<dd>The payload will be set according to the settings and will output either the ON or OFF payload depending on the set parameters.</dd>
</dl>
The output setting can be either of:
<ul>
<li>"when state changes" - Will only output a <code>msg</code> when the state changes. The state is evaluated on a minutely basis, so it can take up to a minute after deploy of a new configuration before the output it triggered.</li>
<li>"when state changes + startup" - Will only output a <code>msg</code> when the state changes and directly after a deploy and a restart of node-red.</li>
<li>"minutely" - Will only output a <code>msg</code> on a minutely basis even if the state have not changed.</li>
</ul>
<h3>Dusk / Dawn</h3>
The Dusk / Dawn setting can be either of:
<ul>
<li>"Only when dark" - Require it "to be dark" and that the schedule allows it, to go "ON".</li>
<li>"Schedule only" - Only control the output based on the schedule.</li>
</ul>
<p>The <i>Threshold</i> adjusts the sun angle that should be used for determining if is is dark or not. A higher value will make the output turn on when it's brighter and a lower when it's darker. 0° corresponds to the time when the sun is at the horizon.</p>
</script>