node-red-contrib-scheduler-plus 0.0.0-stage → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,111 @@
1
- # Temporary Holding Version
1
+ # Scheduler for Node-RED
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/ltoinel/node-red-contrib-scheduler-plus/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ltoinel/node-red-contrib-scheduler-plus/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fltoinel%2Fnode-red-contrib-scheduler-plus%2Fbadges%2Fcoverage.json)](https://github.com/ltoinel/node-red-contrib-scheduler-plus/actions/workflows/ci.yml)
5
+ [![npm version](https://img.shields.io/npm/v/node-red-contrib-scheduler-plus?logo=npm)](https://www.npmjs.com/package/node-red-contrib-scheduler-plus)
6
+ [![npm downloads](https://img.shields.io/npm/dm/node-red-contrib-scheduler-plus)](https://www.npmjs.com/package/node-red-contrib-scheduler-plus)
7
+ [![Node-RED](https://img.shields.io/badge/Node--RED-%3E%3D1.0-8f0000?logo=nodered)](https://flows.nodered.org/node/node-red-contrib-scheduler-plus)
8
+ [![Node.js](https://img.shields.io/node/v/node-red-contrib-scheduler-plus?logo=nodedotjs)](https://nodejs.org)
9
+
10
+ A scheduler node for [Node-RED](https://nodered.org) offering better timing facilities than the
11
+ standard inject node: fixed times, dawn and dusk (handy for lighting), days of the week and months.
12
+
13
+ Originally written by Peter Scargill — see http://tech.scargill.net/node-red-scheduler/
14
+
15
+ ## Requirements
16
+
17
+ - Node-RED >= 1.0
18
+ - Node.js >= 14
19
+ - [suncalc](https://github.com/mourner/suncalc) ^2.1.1 (installed automatically)
20
+
21
+ ## Installation
22
+
23
+ From the Node-RED palette manager, search for **node-red-contrib-scheduler-plus**, or from your
24
+ Node-RED user directory (usually `~/.node-red`):
25
+
26
+ ```sh
27
+ npm install node-red-contrib-scheduler-plus
28
+ ```
29
+
30
+ Then restart Node-RED. The node appears in the **input** category of the palette.
31
+
32
+ > This is a maintained fork of `node-red-contrib-scheduler`, whose npm package is the original
33
+ > version and does not include the fixes below. Remove it before installing this one: both provide
34
+ > the same `scheduler` node type.
35
+
36
+ ## Configuration
37
+
38
+ | Field | Description |
39
+ |---------------|-------------|
40
+ | On Time | Start time, in 15-minute steps, or *Dawn* / *Dusk*. |
41
+ | Off Time | End time, in 15-minute steps, *Dawn* / *Dusk*, or a duration from On Time (1 to 120 minutes). |
42
+ | Dawn / Dusk Offset | Minutes added to dawn / dusk: positive for later, negative for earlier. |
43
+ | Latitude / Longitude | Location used to compute dawn and dusk. Filled from the browser geolocation if left empty. |
44
+ | Topic | `msg.topic` of the messages sent on output 1. |
45
+ | ON / OFF Msg | `msg.payload` sent on output 1 when the schedule turns on / off. |
46
+ | Days / Months | Days of the week and months on which the schedule is active. |
47
+
48
+ The schedule is active from **On Time (included) until Off Time (excluded)**, and may cross midnight
49
+ (e.g. 22:00 → 06:00). Dawn is sunrise and dusk is sunset, computed for the current day.
50
+
51
+ All times use the **local time zone of the Node-RED server**: make sure it matches the location you configure.
52
+
53
+ ## Outputs
54
+
55
+ 1. **Change** — sends `{ topic: <Topic>, payload: <ON Msg | OFF Msg> }` when the state changes,
56
+ or when the button of the node is pressed. Nothing is sent at start-up: the first evaluation only
57
+ initialises the state.
58
+ 2. **Status** — sends `{ topic: "status", payload: "1" | "0" }` every minute.
59
+
60
+ The node status shows the remaining time when on, `off` otherwise, and a red status when there is no
61
+ dawn or dusk on that day (polar day or night at high latitudes).
62
+
63
+ ## Development
64
+
65
+ ```sh
66
+ git clone https://github.com/ltoinel/node-red-contrib-scheduler-plus.git
67
+ cd scheduler
68
+ npm install # also enables the git hooks
69
+ npm run lint # ESLint
70
+ npm test # unit tests (node:test)
71
+ npm run coverage # tests + coverage report, fails under 90% of lines
72
+ ```
73
+
74
+ Git hooks (in `.githooks/`, enabled by `npm install`):
75
+
76
+ - **pre-commit** runs the lint,
77
+ - **pre-push** runs the unit tests.
78
+
79
+ ### Releasing
80
+
81
+ 1. Bump `version` in `package.json` and update the changelog.
82
+ 2. Create a GitHub release with the tag `v<version>` (e.g. `v1.1.0`).
83
+ 3. The [Release](.github/workflows/release.yml) workflow lints, tests and publishes to npm with provenance.
84
+
85
+ ## Changelog
86
+
87
+ ### 1.1.2
88
+ - Repository URLs updated to `ltoinel/node-red-contrib-scheduler-plus` (fixes npm provenance on publish).
89
+
90
+ ### 1.1.1
91
+ - The npm publishing workflow is renamed to `release.yml`.
92
+
93
+ ### 1.1.0
94
+ - Fixed wrong On Time values between 21:45 and 23:45 (offset by 10 minutes); existing flows are migrated automatically.
95
+ - Fixed a one-minute delay and negative values in the time computation.
96
+ - Fixed schedules with a duration crossing midnight.
97
+ - The button of the node sends on output 1 again.
98
+ - Fixed the help panel and the button notification.
99
+ - No crash when there is no dawn or dusk (polar regions).
100
+ - Updated to Node-RED >= 1.0 APIs and suncalc 2.x.
101
+ - New SVG icon.
102
+ - Published on npm as `node-red-contrib-scheduler-plus`, with unit tests, lint and CI.
103
+ - **Behaviour change:** Off Time is now excluded (a 07:00–08:00 slot turns off at 08:00, not 08:01).
104
+
105
+ ### 1.0.0
106
+ - Initial version by Peter Scargill.
107
+
108
+ ## License
109
+
110
+ Free to use for whatever purpose you like. If you redesign it, please keep the original author's name and link
111
+ (http://tech.scargill.net).
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="60" viewBox="0 0 40 60">
2
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round">
3
+ <!-- clock face -->
4
+ <circle cx="20" cy="30" r="10" stroke-width="3"/>
5
+ <!-- hands -->
6
+ <path d="M20 24v6l4 3" stroke-width="2.5"/>
7
+ <!-- sun rays -->
8
+ <g stroke-width="2.5">
9
+ <path d="M20 13.5v-3.5"/>
10
+ <path d="M20 46.5v3.5"/>
11
+ <path d="M3.5 30h-3"/>
12
+ <path d="M36.5 30h3"/>
13
+ <path d="M8.3 18.3l-2.5-2.5"/>
14
+ <path d="M31.7 18.3l2.5-2.5"/>
15
+ <path d="M8.3 41.7l-2.5 2.5"/>
16
+ <path d="M31.7 41.7l2.5 2.5"/>
17
+ </g>
18
+ </g>
19
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
- "name": "node-red-contrib-scheduler-plus",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "node-red-contrib-scheduler-plus",
3
+ "version": "1.1.2",
4
+ "description": "A Node Red Timer with dusk, dawn and months",
5
+ "author": "Peter Scargill <pete@scargill.org>",
6
+ "contributors": [
7
+ "Ludovic Toinel"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ltoinel/node-red-contrib-scheduler-plus.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/ltoinel/node-red-contrib-scheduler-plus/issues"
15
+ },
16
+ "homepage": "https://github.com/ltoinel/node-red-contrib-scheduler-plus#readme",
17
+ "dependencies": {
18
+ "suncalc": "^2.1.1"
19
+ },
20
+ "keywords": [
21
+ "node-red",
22
+ "scheduler",
23
+ "timer",
24
+ "dusk",
25
+ "dawn"
26
+ ],
27
+ "files": [
28
+ "scheduler.js",
29
+ "scheduler.html",
30
+ "icons/"
31
+ ],
32
+ "scripts": {
33
+ "lint": "eslint .",
34
+ "test": "node --test test/scheduler.test.js",
35
+ "coverage": "node -e \"require('fs').mkdirSync('coverage',{recursive:true})\" && node --test --experimental-test-coverage --test-coverage-include=scheduler.js --test-coverage-lines=90 --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info test/scheduler.test.js && node scripts/coverage-badge.js",
36
+ "prepare": "node scripts/install-hooks.js"
37
+ },
38
+ "engines": {
39
+ "node": ">=14"
40
+ },
41
+ "node-red": {
42
+ "version": ">=1.0.0",
43
+ "nodes": {
44
+ "scheduler": "scheduler.js"
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^10.0.1",
49
+ "eslint": "^10.14.0",
50
+ "globals": "^17.14.0"
51
+ },
52
+ "overrides": {
53
+ "minimatch": "^11.0.1"
54
+ }
55
+ }
package/scheduler.html ADDED
@@ -0,0 +1,254 @@
1
+ <!--
2
+ Mostly - apart from the Sunrise module on which this depends
3
+ this is copyright (c) Peter Scargill - but as I've had so
4
+ many ideas from others - consider it free to use for whatever
5
+ purpose you like. If you redesign it please remember to drop
6
+ my name and link in there somewhere. http://tech.scargill.net
7
+ This software puts out one of two messages on change of state
8
+ which could be sent to the MQTT node. It evaluates its schedule
9
+ once every minute.
10
+ -->
11
+
12
+ <script type="text/html" data-template-name="scheduler">
13
+ <div class="form-row">
14
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
15
+ <input type="text" id="node-input-name" placeholder="Name">
16
+ </div>
17
+
18
+ <div class="form-row">
19
+ <label for="node-input-starttime"><i class="fa fa-clock-o"></i> On Time</label>
20
+ <select id="node-input-starttime" style="width:70%"></select>
21
+ </div>
22
+
23
+ <div class="form-row">
24
+ <label for="node-input-endtime"><i class="fa fa-clock-o"></i> Off Time</label>
25
+ <select id="node-input-endtime" style="width:70%"></select>
26
+ </div>
27
+
28
+ <div class="form-row">
29
+ <label for="node-input-dawnoff"><i class="fa fa-clock-o"></i> Dawn Offset</label>
30
+ <input type="text" id="node-input-dawnoff" placeholder="0" style="width:80px"> mins
31
+ </div>
32
+
33
+ <div class="form-row">
34
+ <label for="node-input-duskoff"><i class="fa fa-clock-o"></i> Dusk Offset</label>
35
+ <input type="text" id="node-input-duskoff" placeholder="0" style="width:80px"> mins
36
+ </div>
37
+
38
+ <div class="form-row">
39
+ <label for="node-input-lat"><i class="fa fa-globe"></i> Latitude</label>
40
+ <input type="text" id="node-input-lat" placeholder="51.025">
41
+ </div>
42
+
43
+ <div class="form-row">
44
+ <label for="node-input-lon"><i class="fa fa-globe"></i> Longitude</label>
45
+ <input type="text" id="node-input-lon" placeholder="-1.4">
46
+ </div>
47
+
48
+ <div class="form-row">
49
+ <label for="node-input-outtopic"><i class="fa fa-tasks"></i> Topic</label>
50
+ <input type="text" id="node-input-outtopic" placeholder="MQTT Topic">
51
+ </div>
52
+
53
+ <div class="form-row">
54
+ <label for="node-input-outpayload1"><i class="fa fa-toggle-on"></i> ON Msg</label>
55
+ <input type="text" id="node-input-outpayload1" placeholder="MQTT Payload">
56
+ </div>
57
+
58
+ <div class="form-row">
59
+ <label for="node-input-outpayload2"><i class="fa fa-toggle-off"></i> OFF Msg</label>
60
+ <input type="text" id="node-input-outpayload2" placeholder="MQTT Payload">
61
+ </div>
62
+
63
+ <style>
64
+ .scheduler-checks span { display:inline-block; width:55px; margin-left:5px; }
65
+ .scheduler-checks input[type=checkbox] { width:auto; vertical-align:top; margin:2px 4px 0 0; }
66
+ </style>
67
+
68
+ <div class="form-row scheduler-checks">
69
+ <label><i class="fa fa-calendar"></i> Days</label>
70
+ <div style="display:inline-block; width:70%">
71
+ <span><input type="checkbox" id="node-input-mon">Mon</span>
72
+ <span><input type="checkbox" id="node-input-tue">Tue</span>
73
+ <span><input type="checkbox" id="node-input-wed">Wed</span>
74
+ <span><input type="checkbox" id="node-input-thu">Thu</span>
75
+ <span><input type="checkbox" id="node-input-fri">Fri</span>
76
+ <span><input type="checkbox" id="node-input-sat">Sat</span>
77
+ <span><input type="checkbox" id="node-input-sun">Sun</span>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="form-row scheduler-checks">
82
+ <label><i class="fa fa-calendar"></i> Months</label>
83
+ <div style="display:inline-block; width:70%">
84
+ <span><input type="checkbox" id="node-input-jan">Jan</span>
85
+ <span><input type="checkbox" id="node-input-feb">Feb</span>
86
+ <span><input type="checkbox" id="node-input-mar">Mar</span>
87
+ <span><input type="checkbox" id="node-input-apr">Apr</span>
88
+ <span><input type="checkbox" id="node-input-may">May</span>
89
+ <span><input type="checkbox" id="node-input-jun">Jun</span>
90
+ <span><input type="checkbox" id="node-input-jul">Jul</span>
91
+ <span><input type="checkbox" id="node-input-aug">Aug</span>
92
+ <span><input type="checkbox" id="node-input-sep">Sep</span>
93
+ <span><input type="checkbox" id="node-input-oct">Oct</span>
94
+ <span><input type="checkbox" id="node-input-nov">Nov</span>
95
+ <span><input type="checkbox" id="node-input-dec">Dec</span>
96
+ </div>
97
+ </div>
98
+ </script>
99
+
100
+ <script type="text/html" data-help-name="scheduler">
101
+ <p>Turns something on and off according to a schedule based on fixed times,
102
+ dawn/dusk (computed with the <code>suncalc</code> module), days of the week and months.
103
+ The schedule is evaluated every minute.</p>
104
+ <h3>Outputs</h3>
105
+ <ol class="node-ports">
106
+ <li>Change
107
+ <dl class="message-properties">
108
+ <dt>topic <span class="property-type">string</span></dt>
109
+ <dd>the configured topic.</dd>
110
+ <dt>payload <span class="property-type">string</span></dt>
111
+ <dd>the ON or OFF message. Sent only when the state changes
112
+ or when the button of the node is pressed.</dd>
113
+ </dl>
114
+ </li>
115
+ <li>Status
116
+ <dl class="message-properties">
117
+ <dt>topic <span class="property-type">string</span></dt>
118
+ <dd><code>status</code></dd>
119
+ <dt>payload <span class="property-type">string</span></dt>
120
+ <dd><code>"1"</code> when on, <code>"0"</code> when off. Sent every minute.</dd>
121
+ </dl>
122
+ </li>
123
+ </ol>
124
+ <h3>Details</h3>
125
+ <p>The node is on from <i>On Time</i> (included) until <i>Off Time</i> (excluded).
126
+ The period may cross midnight. <i>Off Time</i> can also be a duration counted from <i>On Time</i>.</p>
127
+ <p>The Dawn and Dusk offsets can be positive for minutes later or negative for minutes earlier.</p>
128
+ <p>Times are based on the local time zone of the Node-RED server.</p>
129
+ <p>Nothing is sent on output 1 at start-up: the first evaluation only initialises the state.</p>
130
+ </script>
131
+
132
+ <script type="text/javascript">
133
+ (function () {
134
+ function pad(n) { return (n < 10 ? "0" : "") + n; }
135
+
136
+ function fillTimes(select, withDurations) {
137
+ select.empty();
138
+ for (var m = 0; m < 1440; m += 15) {
139
+ select.append($("<option>").val(String(m)).text(pad(Math.floor(m / 60)) + ":" + pad(m % 60)));
140
+ }
141
+ select.append($("<option>").val("5000").text("Dawn"));
142
+ select.append($("<option>").val("6000").text("Dusk"));
143
+ if (withDurations) {
144
+ [1, 2, 5, 10, 15, 30, 60, 90, 120].forEach(function (d) {
145
+ select.append($("<option>").val(String(10000 + d)).text(d + (d === 1 ? " minute" : " minutes")));
146
+ });
147
+ }
148
+ }
149
+
150
+ // Values stored by older versions of this node for 21:45 -> 23:45 were wrong.
151
+ var LEGACY_TIMES = {
152
+ "1315": "1305", "1330": "1320", "1345": "1335", "1360": "1350", "1375": "1365",
153
+ "1390": "1380", "1405": "1395", "1420": "1410", "1435": "1425"
154
+ };
155
+ function fixLegacy(v) {
156
+ v = String(v);
157
+ return LEGACY_TIMES.hasOwnProperty(v) ? LEGACY_TIMES[v] : v;
158
+ }
159
+
160
+ function validateTime(v) {
161
+ var t = parseInt(fixLegacy(v), 10);
162
+ return (t >= 0 && t < 1440 && t % 15 === 0) || t === 5000 || t === 6000 ||
163
+ [10001, 10002, 10005, 10010, 10015, 10030, 10060, 10090, 10120].indexOf(t) !== -1;
164
+ }
165
+
166
+ RED.nodes.registerType("scheduler", {
167
+ category: "input",
168
+ color: "#6699ff",
169
+ defaults: {
170
+ name: { value: "My Scheduler" },
171
+ outtopic: { value: "" },
172
+ outpayload1: { value: "" },
173
+ outpayload2: { value: "" },
174
+ lat: { value: "", required: true, validate: RED.validators.number() },
175
+ lon: { value: "", required: true, validate: RED.validators.number() },
176
+ start: { value: "sunrise", required: true },
177
+ end: { value: "sunset", required: true },
178
+ starttime: { value: "5000", required: true, validate: validateTime },
179
+ endtime: { value: "1425", required: true, validate: validateTime },
180
+ duskoff: { value: "0", required: true, validate: RED.validators.number() },
181
+ dawnoff: { value: "0", required: true, validate: RED.validators.number() },
182
+ sun: { value: true },
183
+ mon: { value: true },
184
+ tue: { value: true },
185
+ wed: { value: true },
186
+ thu: { value: true },
187
+ fri: { value: true },
188
+ sat: { value: true },
189
+ jan: { value: true },
190
+ feb: { value: true },
191
+ mar: { value: true },
192
+ apr: { value: true },
193
+ may: { value: true },
194
+ jun: { value: true },
195
+ jul: { value: true },
196
+ aug: { value: true },
197
+ sep: { value: true },
198
+ oct: { value: true },
199
+ nov: { value: true },
200
+ dec: { value: true }
201
+ },
202
+ inputs: 0,
203
+ outputs: 2,
204
+ outputLabels: ["change", "status"],
205
+ icon: "scheduler.svg",
206
+ label: function () {
207
+ return this.name || "scheduler";
208
+ },
209
+ labelStyle: function () {
210
+ return this.name ? "node_label_italic" : "";
211
+ },
212
+ oneditprepare: function () {
213
+ var start = $("#node-input-starttime");
214
+ var end = $("#node-input-endtime");
215
+ fillTimes(start, false);
216
+ fillTimes(end, true);
217
+ start.val(fixLegacy(this.starttime || "5000"));
218
+ end.val(fixLegacy(this.endtime || "1425"));
219
+
220
+ if ($("#node-input-lat").val() === "" && $("#node-input-lon").val() === "") {
221
+ if ("geolocation" in navigator) {
222
+ navigator.geolocation.getCurrentPosition(function (position) {
223
+ $("#node-input-lat").val(Number(position.coords.latitude.toFixed(5)));
224
+ $("#node-input-lon").val(Number(position.coords.longitude.toFixed(5)));
225
+ });
226
+ }
227
+ }
228
+ },
229
+ button: {
230
+ onclick: function () {
231
+ var label = this.name || "scheduler";
232
+ $.ajax({
233
+ url: "scheduler/" + this.id,
234
+ type: "POST",
235
+ success: function () {
236
+ RED.notify("Successfully injected: " + label, "success");
237
+ },
238
+ error: function (jqXHR, textStatus) {
239
+ if (jqXHR.status == 404) {
240
+ RED.notify("<strong>Error</strong>: scheduler node not deployed", "error");
241
+ } else if (jqXHR.status == 500) {
242
+ RED.notify("<strong>Error</strong>: scheduler reset failed, see log for details.", "error");
243
+ } else if (jqXHR.status == 0) {
244
+ RED.notify("<strong>Error</strong>: no response from server", "error");
245
+ } else {
246
+ RED.notify("<strong>Error</strong>: unexpected error: (" + jqXHR.status + ") " + textStatus, "error");
247
+ }
248
+ }
249
+ });
250
+ }
251
+ }
252
+ });
253
+ })();
254
+ </script>
package/scheduler.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Mostly - apart from the Sunrise module on which this depends this is
3
+ * copyright (c) Peter Scargill - but as I've had so many ideas from others -
4
+ * consider it free to use for whatever purpose you like. If you redesign it
5
+ * please remember to drop my name and link in there somewhere.
6
+ * http://tech.scargill.net This software puts out one of two messages on change
7
+ * of state which could be sent to the MQTT node. It evaluates its schedule
8
+ * once every minute.
9
+ */
10
+
11
+ module.exports = function (RED) {
12
+ "use strict";
13
+ var SunCalc = require("suncalc");
14
+
15
+ var MINUTES_PER_DAY = 1440;
16
+ var DAWN = 5000;
17
+ var DUSK = 6000;
18
+ var DURATION_BASE = 10000;
19
+
20
+ var DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
21
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun",
22
+ "jul", "aug", "sep", "oct", "nov", "dec"];
23
+
24
+ // Earlier versions of the editor stored wrong values for the
25
+ // 21:45 -> 23:45 "On Time" entries (offset by 10 minutes).
26
+ var LEGACY_TIMES = {
27
+ 1315: 1305, 1330: 1320, 1345: 1335, 1360: 1350, 1375: 1365,
28
+ 1390: 1380, 1405: 1395, 1420: 1410, 1435: 1425
29
+ };
30
+
31
+ function wrap(minutes) {
32
+ return ((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
33
+ }
34
+
35
+ function minutesOfDay(date) {
36
+ return date.getHours() * 60 + date.getMinutes();
37
+ }
38
+
39
+ function parseTime(value) {
40
+ var t = parseInt(value, 10);
41
+ if (isNaN(t)) {
42
+ return NaN;
43
+ }
44
+ return Object.prototype.hasOwnProperty.call(LEGACY_TIMES, t) ? LEGACY_TIMES[t] : t;
45
+ }
46
+
47
+ function SchedulerNode(n) {
48
+ RED.nodes.createNode(this, n);
49
+ var node = this;
50
+
51
+ node.lat = Number(n.lat);
52
+ node.lon = Number(n.lon);
53
+ node.start = n.start || "sunrise";
54
+ node.end = n.end || "sunset";
55
+ node.startt = parseTime(n.starttime);
56
+ node.endt = parseTime(n.endtime);
57
+ node.duskOff = Number(n.duskoff) || 0;
58
+ node.dawnOff = Number(n.dawnoff) || 0;
59
+ node.outtopic = n.outtopic;
60
+ node.outPayload1 = n.outpayload1;
61
+ node.outPayload2 = n.outpayload2;
62
+ node.days = DAYS.map(function (d) { return !!n[d]; });
63
+ node.months = MONTHS.map(function (m) { return !!n[m]; });
64
+
65
+ // null = state unknown (just started): the first evaluation only
66
+ // initialises the state and does not emit on output 1.
67
+ var lastState = null;
68
+
69
+ function resolve(value, dawn, dusk) {
70
+ if (value === DAWN) { return dawn; }
71
+ if (value === DUSK) { return dusk; }
72
+ return value;
73
+ }
74
+
75
+ function evaluate(now) {
76
+ // utcOffset pins the computation to the local calendar day of the server
77
+ var times = SunCalc.getTimes(now, node.lat, node.lon, 0, -now.getTimezoneOffset());
78
+ var sunrise = times[node.start];
79
+ var sunset = times[node.end];
80
+ var dawn = NaN;
81
+ var dusk = NaN;
82
+ if (sunrise && !isNaN(sunrise.getTime())) {
83
+ dawn = wrap(minutesOfDay(sunrise) + node.dawnOff);
84
+ }
85
+ if (sunset && !isNaN(sunset.getTime())) {
86
+ dusk = wrap(minutesOfDay(sunset) + node.duskOff);
87
+ }
88
+
89
+ var startTime = resolve(node.startt, dawn, dusk);
90
+ var endTime;
91
+ if (node.endt > DURATION_BASE) {
92
+ endTime = wrap(startTime + (node.endt - DURATION_BASE));
93
+ } else {
94
+ endTime = resolve(node.endt, dawn, dusk);
95
+ }
96
+
97
+ if (isNaN(startTime) || isNaN(endTime)) {
98
+ // e.g. no sunrise/sunset at this latitude today
99
+ return { on: false, error: "no dawn/dusk today" };
100
+ }
101
+
102
+ if (!node.days[now.getDay()] || !node.months[now.getMonth()]) {
103
+ return { on: false };
104
+ }
105
+
106
+ // Active interval is [startTime, endTime), handling midnight wrap.
107
+ var today = minutesOfDay(now);
108
+ var on;
109
+ if (startTime <= endTime) {
110
+ on = today >= startTime && today < endTime;
111
+ } else {
112
+ on = today >= startTime || today < endTime;
113
+ }
114
+ return { on: on, remaining: on ? wrap(endTime - today) : 0 };
115
+ }
116
+
117
+ node.on("input", function (inmsg, send, done) {
118
+ send = send || function () { node.send.apply(node, arguments); };
119
+ var forced = !!(inmsg && inmsg.payload === "reset");
120
+ var result;
121
+ try {
122
+ result = evaluate(new Date());
123
+ } catch (err) {
124
+ if (done) { done(err); } else { node.error(err, inmsg); }
125
+ return;
126
+ }
127
+
128
+ if (result.error) {
129
+ node.status({ fill: "red", shape: "ring", text: result.error });
130
+ } else if (result.on) {
131
+ node.status({ fill: "yellow", shape: "dot", text: "on for " + result.remaining + " mins" });
132
+ } else {
133
+ node.status({ fill: "blue", shape: "dot", text: "off" });
134
+ }
135
+
136
+ var changed = lastState !== null && lastState !== result.on;
137
+ lastState = result.on;
138
+
139
+ var outmsg = null;
140
+ if (forced || changed) {
141
+ outmsg = {
142
+ topic: node.outtopic,
143
+ payload: result.on ? node.outPayload1 : node.outPayload2
144
+ };
145
+ }
146
+ var outmsg2 = { topic: "status", payload: result.on ? "1" : "0" };
147
+
148
+ send([outmsg, outmsg2]);
149
+ if (done) { done(); }
150
+ });
151
+
152
+ // wait 2 secs before starting to let things settle down - e.g. UI connect
153
+ var tock = setTimeout(function () {
154
+ tock = null;
155
+ node.receive({});
156
+ }, 2000);
157
+
158
+ var tick = setInterval(function () {
159
+ node.receive({});
160
+ }, 60000); // trigger every 60 secs
161
+
162
+ node.on("close", function () {
163
+ if (tock) {
164
+ clearTimeout(tock);
165
+ tock = null;
166
+ }
167
+ clearInterval(tick);
168
+ });
169
+ }
170
+
171
+ RED.nodes.registerType("scheduler", SchedulerNode);
172
+
173
+ RED.httpAdmin.post("/scheduler/:id", RED.auth.needsPermission("scheduler.write"), function (req, res) {
174
+ var node = RED.nodes.getNode(req.params.id);
175
+ if (node === null || node === undefined) {
176
+ res.sendStatus(404);
177
+ return;
178
+ }
179
+ try {
180
+ node.receive({ payload: "reset" });
181
+ res.sendStatus(200);
182
+ } catch (err) {
183
+ res.sendStatus(500);
184
+ node.error("Inject failed: " + err);
185
+ }
186
+ });
187
+ };