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 +110 -2
- package/icons/scheduler.svg +19 -0
- package/package.json +54 -5
- package/scheduler.html +254 -0
- package/scheduler.js +187 -0
package/README.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Scheduler for Node-RED
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/ltoinel/node-red-contrib-scheduler-plus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/ltoinel/node-red-contrib-scheduler-plus/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/node-red-contrib-scheduler-plus)
|
|
6
|
+
[](https://www.npmjs.com/package/node-red-contrib-scheduler-plus)
|
|
7
|
+
[](https://flows.nodered.org/node/node-red-contrib-scheduler-plus)
|
|
8
|
+
[](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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
};
|