node-red-contrib-weekly-schedule 1.0.0
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 +35 -0
- package/locales/de/weekly-schedule.json +10 -0
- package/locales/en/weekly-schedule.json +10 -0
- package/locales/es/weekly-schedule.json +10 -0
- package/package.json +46 -0
- package/weekly-schedule.html +128 -0
- package/weekly-schedule.js +107 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# node-red-contrib-weekly-schedule
|
|
2
|
+
|
|
3
|
+
A Node-RED weekly scheduler with configurable time windows per day. Outputs a boolean or custom value based on the current time.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install node-red-contrib-weekly-schedule
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Inputs
|
|
12
|
+
|
|
13
|
+
- **override** _(optional)_ `boolean`: Force ON (true) or OFF (false) regardless of schedule.
|
|
14
|
+
- **clearOverride** _(optional)_ `boolean`: Remove override and return to schedule.
|
|
15
|
+
|
|
16
|
+
## Outputs
|
|
17
|
+
|
|
18
|
+
- **Output 1 payload** `any`: ON or OFF value on every input message.
|
|
19
|
+
- **Output 2 payload** `any`: ON or OFF value only on state change (checked every 10s).
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
- **Schedule**: List of day + time window entries. Add multiple per day for multiple windows.
|
|
24
|
+
- **ON/OFF Value**: Values to output when active/inactive (default: true / false).
|
|
25
|
+
- **Timezone**: Optional timezone string (default: system timezone).
|
|
26
|
+
|
|
27
|
+
## Tips
|
|
28
|
+
|
|
29
|
+
- Midnight crossing supported: set From after To (e.g. 22:00-06:00).
|
|
30
|
+
- Trigger Output 1 by injecting a message on Node-RED start.
|
|
31
|
+
- Output 2 fires automatically every 10s when state changes.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT (c) sr.rpo
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-weekly-schedule",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Weekly scheduler with configurable time windows per day of week",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Wobi848/node-red-contrib-rpo-suite.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/Wobi848/node-red-contrib-rpo-suite/issues"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"weekly-schedule.js",
|
|
14
|
+
"weekly-schedule.html",
|
|
15
|
+
"locales",
|
|
16
|
+
"examples"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"node-red",
|
|
20
|
+
"schedule",
|
|
21
|
+
"weekly",
|
|
22
|
+
"timer",
|
|
23
|
+
"time",
|
|
24
|
+
"calendar",
|
|
25
|
+
"automation"
|
|
26
|
+
],
|
|
27
|
+
"author": "sr.rpo",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"node-red": {
|
|
30
|
+
"version": "1.0.0",
|
|
31
|
+
"nodes": {
|
|
32
|
+
"weekly-schedule": "weekly-schedule.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "mocha \"test/**/*_spec.js\" --exit"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"mocha": "^10.2.0",
|
|
43
|
+
"node-red": "^3.1.0",
|
|
44
|
+
"node-red-node-test-helper": "^0.3.6"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('weekly-schedule', {
|
|
3
|
+
category: 'rpo',
|
|
4
|
+
color: '#D4600A',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
schedule: { value: [] },
|
|
8
|
+
onValue: { value: true },
|
|
9
|
+
offValue: { value: false },
|
|
10
|
+
timezone: { value: '' }
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 2,
|
|
14
|
+
icon: 'font-awesome/fa-calendar',
|
|
15
|
+
label: function() { return this.name || 'weekly schedule'; },
|
|
16
|
+
labelStyle: function() { return this.name ? 'node_label_italic' : ''; },
|
|
17
|
+
outputLabels: ['Current state', 'On state change'],
|
|
18
|
+
oneditprepare: function() {
|
|
19
|
+
var node = this;
|
|
20
|
+
var DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
|
21
|
+
|
|
22
|
+
$('#node-input-schedule-list').editableList({
|
|
23
|
+
addItem: function(container, i, data) {
|
|
24
|
+
data = data || {};
|
|
25
|
+
container.css({ overflow: 'hidden', whiteSpace: 'nowrap' });
|
|
26
|
+
var row = $('<div style="display:flex;gap:6px;align-items:center;width:100%"></div>').appendTo(container);
|
|
27
|
+
|
|
28
|
+
var daySelect = $('<select style="flex:1"></select>');
|
|
29
|
+
for (var d = 0; d < 7; d++) {
|
|
30
|
+
var opt = $('<option></option>').val(d).text(DAYS[d]);
|
|
31
|
+
if (data.day !== undefined && parseInt(data.day) === d) opt.prop('selected', true);
|
|
32
|
+
daySelect.append(opt);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var fromInput = $('<input type="time" style="flex:1" value="' + (data.from || '06:00') + '">');
|
|
36
|
+
var toInput = $('<input type="time" style="flex:1" value="' + (data.to || '22:00') + '">');
|
|
37
|
+
|
|
38
|
+
$('<span>Day</span>').appendTo(row);
|
|
39
|
+
daySelect.appendTo(row);
|
|
40
|
+
$('<span>From</span>').appendTo(row);
|
|
41
|
+
fromInput.appendTo(row);
|
|
42
|
+
$('<span>To</span>').appendTo(row);
|
|
43
|
+
toInput.appendTo(row);
|
|
44
|
+
|
|
45
|
+
container.data('daySelect', daySelect);
|
|
46
|
+
container.data('fromInput', fromInput);
|
|
47
|
+
container.data('toInput', toInput);
|
|
48
|
+
},
|
|
49
|
+
removable: true,
|
|
50
|
+
sortable: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
var items = node.schedule || [];
|
|
54
|
+
for (var i = 0; i < items.length; i++) {
|
|
55
|
+
$('#node-input-schedule-list').editableList('addItem', items[i]);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
oneditsave: function() {
|
|
59
|
+
var node = this;
|
|
60
|
+
var items = $('#node-input-schedule-list').editableList('items');
|
|
61
|
+
node.schedule = [];
|
|
62
|
+
items.each(function() {
|
|
63
|
+
var container = $(this);
|
|
64
|
+
node.schedule.push({
|
|
65
|
+
day: parseInt(container.data('daySelect').val()),
|
|
66
|
+
from: container.data('fromInput').val(),
|
|
67
|
+
to: container.data('toInput').val()
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<script type="text/html" data-template-name="weekly-schedule">
|
|
75
|
+
<div class="form-row">
|
|
76
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
|
77
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
|
78
|
+
</div>
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label><i class="fa fa-calendar"></i> <span data-i18n="weekly-schedule.label.schedule"></span></label>
|
|
81
|
+
<ol id="node-input-schedule-list" style="margin-bottom:0"></ol>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-input-onValue"><i class="fa fa-check-circle"></i> <span data-i18n="weekly-schedule.label.onValue"></span></label>
|
|
85
|
+
<input type="text" id="node-input-onValue" style="width:70%">
|
|
86
|
+
</div>
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-input-offValue"><i class="fa fa-times-circle"></i> <span data-i18n="weekly-schedule.label.offValue"></span></label>
|
|
89
|
+
<input type="text" id="node-input-offValue" style="width:70%">
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-input-timezone"><i class="fa fa-globe"></i> <span data-i18n="weekly-schedule.label.timezone"></span></label>
|
|
93
|
+
<input type="text" id="node-input-timezone" style="width:70%" placeholder="(system default)">
|
|
94
|
+
</div>
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<script type="text/html" data-help-name="weekly-schedule">
|
|
98
|
+
<p>Weekly scheduler with configurable time windows per day. Outputs <code>true</code>/<code>false</code> (or custom values) based on current time.</p>
|
|
99
|
+
|
|
100
|
+
<h3>Outputs</h3>
|
|
101
|
+
<ol>
|
|
102
|
+
<li><b>Current state</b> — fires on every input message with the current active/inactive state.</li>
|
|
103
|
+
<li><b>State change</b> — fires only when the state changes (on→off or off→on), checked every 10 seconds.</li>
|
|
104
|
+
</ol>
|
|
105
|
+
|
|
106
|
+
<h3>Input</h3>
|
|
107
|
+
<dl class="message-properties">
|
|
108
|
+
<dt class="optional">override <span class="property-type">boolean</span></dt>
|
|
109
|
+
<dd>Force ON (<code>true</code>) or OFF (<code>false</code>) regardless of schedule.</dd>
|
|
110
|
+
<dt class="optional">clearOverride <span class="property-type">boolean</span></dt>
|
|
111
|
+
<dd>Set to <code>true</code> to remove override and return to schedule.</dd>
|
|
112
|
+
</dl>
|
|
113
|
+
|
|
114
|
+
<h3>Output</h3>
|
|
115
|
+
<dl class="message-properties">
|
|
116
|
+
<dt>payload <span class="property-type">any</span></dt>
|
|
117
|
+
<dd>Configured ON or OFF value.</dd>
|
|
118
|
+
<dt>schedule <span class="property-type">object</span></dt>
|
|
119
|
+
<dd><code>{ active, day, time, override }</code></dd>
|
|
120
|
+
</dl>
|
|
121
|
+
|
|
122
|
+
<h3>Tips</h3>
|
|
123
|
+
<ul>
|
|
124
|
+
<li>Midnight crossing is supported: set "From" after "To" (e.g. 22:00–06:00).</li>
|
|
125
|
+
<li>Multiple windows per day: add multiple entries with the same day.</li>
|
|
126
|
+
<li>Trigger Output 1 by injecting any message (e.g. on Node-RED start).</li>
|
|
127
|
+
</ul>
|
|
128
|
+
</script>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function WeeklyScheduleNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
|
|
6
|
+
node.schedule = config.schedule || [];
|
|
7
|
+
node.onValue = config.onValue !== undefined ? config.onValue : true;
|
|
8
|
+
node.offValue = config.offValue !== undefined ? config.offValue : false;
|
|
9
|
+
node.timezone = config.timezone || '';
|
|
10
|
+
|
|
11
|
+
node.override = null; // null = no override, true = forced on, false = forced off
|
|
12
|
+
node.lastState = null;
|
|
13
|
+
|
|
14
|
+
updateStatus(node);
|
|
15
|
+
|
|
16
|
+
// Periodic check every 10 seconds for Output 2 (state change)
|
|
17
|
+
node.timer = setInterval(function() {
|
|
18
|
+
var active = isActive(node);
|
|
19
|
+
var current = (node.override !== null) ? node.override : active;
|
|
20
|
+
|
|
21
|
+
if (current !== node.lastState) {
|
|
22
|
+
node.lastState = current;
|
|
23
|
+
var changeMsg = buildMsg(node, active, current);
|
|
24
|
+
updateStatus(node);
|
|
25
|
+
node.send([null, changeMsg]);
|
|
26
|
+
}
|
|
27
|
+
}, 10000);
|
|
28
|
+
|
|
29
|
+
node.on('input', function(msg) {
|
|
30
|
+
if (msg.override !== undefined) {
|
|
31
|
+
node.override = (msg.override === true || msg.override === 'true') ? true : false;
|
|
32
|
+
}
|
|
33
|
+
if (msg.clearOverride === true) {
|
|
34
|
+
node.override = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var active = isActive(node);
|
|
38
|
+
var current = (node.override !== null) ? node.override : active;
|
|
39
|
+
|
|
40
|
+
if (current !== node.lastState) {
|
|
41
|
+
node.lastState = current;
|
|
42
|
+
node.send([buildMsg(node, active, current), buildMsg(node, active, current)]);
|
|
43
|
+
} else {
|
|
44
|
+
node.send([buildMsg(node, active, current), null]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateStatus(node);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
node.on('close', function() {
|
|
51
|
+
clearInterval(node.timer);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function isActive(n) {
|
|
55
|
+
var now = new Date();
|
|
56
|
+
var day = now.getDay(); // 0=Sun, 6=Sat
|
|
57
|
+
var hhmm = pad(now.getHours()) + ':' + pad(now.getMinutes());
|
|
58
|
+
|
|
59
|
+
for (var i = 0; i < n.schedule.length; i++) {
|
|
60
|
+
var entry = n.schedule[i];
|
|
61
|
+
if (parseInt(entry.day, 10) !== day) continue;
|
|
62
|
+
var from = entry.from || '00:00';
|
|
63
|
+
var to = entry.to || '24:00';
|
|
64
|
+
if (to === '24:00') to = '23:59';
|
|
65
|
+
|
|
66
|
+
if (from <= to) {
|
|
67
|
+
if (hhmm >= from && hhmm <= to) return true;
|
|
68
|
+
} else {
|
|
69
|
+
// midnight crossing
|
|
70
|
+
if (hhmm >= from || hhmm <= to) return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildMsg(n, active, current) {
|
|
77
|
+
var now = new Date();
|
|
78
|
+
var hhmm = pad(now.getHours()) + ':' + pad(now.getMinutes());
|
|
79
|
+
var payload = current ? n.onValue : n.offValue;
|
|
80
|
+
return {
|
|
81
|
+
payload: payload,
|
|
82
|
+
schedule: {
|
|
83
|
+
active: active,
|
|
84
|
+
day: now.getDay(),
|
|
85
|
+
time: hhmm,
|
|
86
|
+
override: n.override !== null ? n.override : false
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateStatus(n) {
|
|
92
|
+
var active = isActive(n);
|
|
93
|
+
var current = (n.override !== null) ? n.override : active;
|
|
94
|
+
if (n.override !== null) {
|
|
95
|
+
n.status({ fill: 'yellow', shape: 'dot', text: 'OVERRIDE ' + (n.override ? 'ON' : 'OFF') });
|
|
96
|
+
} else if (current) {
|
|
97
|
+
n.status({ fill: 'green', shape: 'dot', text: 'ACTIVE' });
|
|
98
|
+
} else {
|
|
99
|
+
n.status({ fill: 'grey', shape: 'dot', text: 'INACTIVE' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pad(n) { return n < 10 ? '0' + n : String(n); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
RED.nodes.registerType('weekly-schedule', WeeklyScheduleNode);
|
|
107
|
+
};
|