node-red-contrib-boolean-logic-ultimate 1.1.26 → 1.2.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/CHANGELOG.md +13 -2
- package/README.md +108 -1
- package/boolean-logic-ultimate/PresenceSimulatorUltimate.html +119 -0
- package/boolean-logic-ultimate/PresenceSimulatorUltimate.js +247 -0
- package/boolean-logic-ultimate/RateLimiterUltimate.html +164 -0
- package/boolean-logic-ultimate/RateLimiterUltimate.js +396 -0
- package/boolean-logic-ultimate/StaircaseLightUltimate.html +165 -0
- package/boolean-logic-ultimate/StaircaseLightUltimate.js +229 -0
- package/boolean-logic-ultimate/StatusUltimate.html +5 -1
- package/boolean-logic-ultimate/StatusUltimate.js +3 -3
- package/boolean-logic-ultimate/lib/node-helpers.js +96 -0
- package/package.json +13 -1
- package/test/helpers.js +29 -0
- package/test/presence-simulator.spec.js +101 -0
- package/test/rate-limiter.spec.js +169 -0
- package/test/staircase-light.spec.js +134 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
# CHANGELOG
|
|
5
5
|
|
|
6
|
+
<p>
|
|
7
|
+
<b>Version 1.2.0</b> September 2025<br/>
|
|
8
|
+
- NEW: PresenceSimulatorUltimate to replay configurable message sequences and simulate occupancy.<br/>
|
|
9
|
+
- NEW: StaircaseLightUltimate to manage staircase lighting timers with pre-off warnings.<br/>
|
|
10
|
+
- NEW: RateLimiterUltimate node with debounce, throttle and window modes.<br/>
|
|
11
|
+
- Added shared helper for consistent status/timer handling across ultimate nodes.</br>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p>
|
|
15
|
+
<b>Version 1.1.27</b> July 2025<br/>
|
|
16
|
+
- Status node: UI correction. </br>
|
|
17
|
+
</p>
|
|
6
18
|
<p>
|
|
7
19
|
<b>Version 1.1.26</b> July 2025<br/>
|
|
8
20
|
- Interruptflow node: fixed non functional msg.play. </br>
|
|
9
21
|
</p>
|
|
10
|
-
|
|
11
22
|
<p>
|
|
12
23
|
<b>Version 1.1.25</b> January 2025<br/>
|
|
13
24
|
- BREAKING CHANGE<br/>
|
|
@@ -382,4 +393,4 @@
|
|
|
382
393
|
<p>
|
|
383
394
|
<b>Version 0.0.1</b><br/>
|
|
384
395
|
- Initial release<br/>
|
|
385
|
-
</p>
|
|
396
|
+
</p>
|
package/README.md
CHANGED
|
@@ -546,6 +546,113 @@ Please refer to [this](https://github.com/wouterbulten/kalmanjs) link, on how it
|
|
|
546
546
|
|
|
547
547
|
<br/>
|
|
548
548
|
|
|
549
|
+
# RATE LIMITER ULTIMATE
|
|
550
|
+
|
|
551
|
+
Gateway per sensori e dispositivi troppo “chiacchieroni”: limita burst e rimbalzi con modalità **Debounce**, **Throttle** e **Window**. Lo stato del nodo riporta sempre modalità corrente, contatori di messaggi inoltrati e bloccati.
|
|
552
|
+
|
|
553
|
+
### NODE CONFIGURATION
|
|
554
|
+
|
|
555
|
+
| Property | Description |
|
|
556
|
+
| -------- | ----------- |
|
|
557
|
+
| Mode | Seleziona la logica: *Debounce* (attende quiete), *Throttle* (impone un intervallo minimo), *Window* (massimo N messaggi per finestra temporale). |
|
|
558
|
+
| Wait (ms) | Ritardo di quiete per la modalità debounce. |
|
|
559
|
+
| Emit | Per debounce: scegli tra *Leading* (subito), *Trailing* (ultimo), *Both*. |
|
|
560
|
+
| Interval (ms) | Intervallo minimo tra messaggi in modalità throttle. |
|
|
561
|
+
| Emit trailing | In throttle, inoltra l’ultimo messaggio ricevuto allo scadere dell’intervallo. |
|
|
562
|
+
| Window size (ms) | Larghezza della finestra mobile in modalità window. |
|
|
563
|
+
| Max messages | Numero di messaggi ammessi nella finestra. |
|
|
564
|
+
| On limit | *Drop* scarta i messaggi extra, *Queue last* accoda l’ultimo e lo riproduce appena possibile. |
|
|
565
|
+
| Control topic | Topic dei messaggi di controllo (default `rate`). |
|
|
566
|
+
| With Input | Proprietà del messaggio da monitorare (default `msg.payload`). |
|
|
567
|
+
| Stats every (s) | Ogni quanti secondi emettere un riepilogo statistico (0 = disattivato). |
|
|
568
|
+
| Translator | Nodo translator-config opzionale per adattare le stringhe d’ingresso a true/false. |
|
|
569
|
+
|
|
570
|
+
<br/>
|
|
571
|
+
|
|
572
|
+
### OUTPUTS
|
|
573
|
+
|
|
574
|
+
- **Output 1**: messaggi inoltrati, invariati.
|
|
575
|
+
- **Output 2**: diagnostica (`{mode, reason, passed, dropped, msg, propertyValue}`) e statistiche periodiche su `controlTopic/stats`.
|
|
576
|
+
|
|
577
|
+
<br/>
|
|
578
|
+
|
|
579
|
+
### CONTROL MESSAGES (`msg.topic === controlTopic`)
|
|
580
|
+
|
|
581
|
+
- `msg.reset = true` → azzera contatori, accoda azzerata e timer cancellati.
|
|
582
|
+
- `msg.flush = true` → forza l’emissione immediata del messaggio in attesa.
|
|
583
|
+
- `msg.mode = 'debounce'|'throttle'|'window'` → cambia modalità runtime e resetta lo stato.
|
|
584
|
+
- `msg.interval`, `msg.wait`, `msg.windowSize`, `msg.maxInWindow` → aggiorna i parametri corrispondenti.
|
|
585
|
+
|
|
586
|
+
<br/>
|
|
587
|
+
|
|
588
|
+
# PRESENCE SIMULATOR ULTIMATE
|
|
589
|
+
|
|
590
|
+
The purpose of this node is to replay a programmable sequence of messages in order to simulate occupancy when you are away.
|
|
591
|
+
|
|
592
|
+
### NODE CONFIGURATION
|
|
593
|
+
|
|
594
|
+
| Property | Description |
|
|
595
|
+
| -------- | ----------- |
|
|
596
|
+
| Control topic | Topic used for runtime commands such as start/stop/reset. |
|
|
597
|
+
| Auto start | Starts the sequence automatically after deploy or restart. |
|
|
598
|
+
| Loop sequence | Repeats the sequence when it reaches the end. |
|
|
599
|
+
| Random delays | Enables a random variation of the programmed delays. |
|
|
600
|
+
| Jitter (%) | Maximum percentage of variation applied when random delays are enabled. |
|
|
601
|
+
| With Input | Message property to inspect for inline events (default `payload`). |
|
|
602
|
+
| Translator | Optional translator-config to convert incoming values. |
|
|
603
|
+
| Sequence | One JSON object per line, each containing at least `delay` (ms) plus the properties to output. |
|
|
604
|
+
|
|
605
|
+
### CONTROL MESSAGES (`msg.topic === controlTopic`)
|
|
606
|
+
|
|
607
|
+
- `msg.command = 'start'` / `msg.start = true` → begin playback.
|
|
608
|
+
- `msg.command = 'stop'` / `msg.stop = true` → halt playback.
|
|
609
|
+
- `msg.reset = true` → reset counters and start position.
|
|
610
|
+
- `msg.sequence = [...]` → load a new sequence at runtime.
|
|
611
|
+
- `msg.loop`, `msg.randomize`, `msg.jitter` → update the corresponding options.
|
|
612
|
+
|
|
613
|
+
Each event in the sequence outputs a message configured in the JSON line. When random delays are enabled, the effective delay is varied within the configured jitter.
|
|
614
|
+
|
|
615
|
+
<br/>
|
|
616
|
+
|
|
617
|
+
# STAIRCASE LIGHT ULTIMATE
|
|
618
|
+
|
|
619
|
+
The purpose of this node is to control staircase lighting with a timer, pre-off warning and optional extension on every trigger.
|
|
620
|
+
|
|
621
|
+
### NODE CONFIGURATION
|
|
622
|
+
|
|
623
|
+
| Property | Description |
|
|
624
|
+
| -------- | ----------- |
|
|
625
|
+
| Control topic | Topic that receives manual commands such as `on`, `off`, `extend`. |
|
|
626
|
+
| Duration (s) | Lighting duration for each trigger. |
|
|
627
|
+
| Warning before off | Enables emission of a pre-off warning on output 2. |
|
|
628
|
+
| Warning offset (s) | Seconds before switch-off when the warning is sent. |
|
|
629
|
+
| Restart on trigger | Restarts the timer when a new trigger arrives while active. |
|
|
630
|
+
| Allow off input | Allows a `false` from the main input to switch off immediately. |
|
|
631
|
+
| With Input | Message property evaluated as trigger (default `payload`). |
|
|
632
|
+
| Translator | Optional translator-config for true/false conversion. |
|
|
633
|
+
| On/Off payload | Values emitted on output 1 to turn the light on/off. |
|
|
634
|
+
| Warning payload | Value emitted on output 2 when the warning fires. |
|
|
635
|
+
|
|
636
|
+
### CONTROL MESSAGES (`msg.topic === controlTopic`)
|
|
637
|
+
|
|
638
|
+
- `msg.command = 'on'` / `msg.start = true` → start the timer and turn on the light.
|
|
639
|
+
- `msg.command = 'off'` / `msg.stop = true` → switch off immediately.
|
|
640
|
+
- `msg.command = 'extend'` / `msg.extend = true` → refresh the timer while keeping the light on.
|
|
641
|
+
- `msg.duration`, `msg.warningEnabled`, `msg.warningOffset` → adjust runtime settings.
|
|
642
|
+
|
|
643
|
+
Output 1 delivers the ON/OFF command. Output 2 delivers the warning and includes `msg.remaining` with the seconds left.
|
|
644
|
+
|
|
645
|
+
<br/>
|
|
646
|
+
|
|
647
|
+
# DEVELOPMENT
|
|
648
|
+
|
|
649
|
+
Per eseguire i test automatici:
|
|
650
|
+
|
|
651
|
+
1. `npm install`
|
|
652
|
+
2. `npm test`
|
|
653
|
+
|
|
654
|
+
<br/>
|
|
655
|
+
|
|
549
656
|
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
|
|
550
657
|
|
|
551
658
|
[license-url]: https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate/master/LICENSE
|
|
@@ -560,4 +667,4 @@ Please refer to [this](https://github.com/wouterbulten/kalmanjs) link, on how it
|
|
|
560
667
|
|
|
561
668
|
[youtube-image]: https://img.shields.io/badge/Visit%20me-youtube-red
|
|
562
669
|
|
|
563
|
-
[youtube-url]: https://youtube.com/playlist?list=PL9Yh1bjbLAYoRH4IyQB7EL5srHAihiKpy
|
|
670
|
+
[youtube-url]: https://youtube.com/playlist?list=PL9Yh1bjbLAYoRH4IyQB7EL5srHAihiKpy
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('PresenceSimulatorUltimate', {
|
|
3
|
+
category: 'Boolean Logic Ultimate',
|
|
4
|
+
color: '#ffe599',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
controlTopic: { value: 'presence' },
|
|
8
|
+
autoStart: { value: false },
|
|
9
|
+
autoLoop: { value: true },
|
|
10
|
+
randomize: { value: false },
|
|
11
|
+
jitter: { value: 10, validate: RED.validators.number() },
|
|
12
|
+
payloadPropName: { value: 'payload', required: false },
|
|
13
|
+
translatorConfig: { type: 'translator-config', required: false },
|
|
14
|
+
patterns: { value: '' }
|
|
15
|
+
},
|
|
16
|
+
inputs: 1,
|
|
17
|
+
outputs: 1,
|
|
18
|
+
icon: 'file-in.png',
|
|
19
|
+
label: function () {
|
|
20
|
+
return this.name || 'Presence Simulator';
|
|
21
|
+
},
|
|
22
|
+
paletteLabel: function () {
|
|
23
|
+
return 'Presence Simulator';
|
|
24
|
+
},
|
|
25
|
+
oneditprepare: function () {
|
|
26
|
+
const payloadField = $('#node-input-payloadPropName');
|
|
27
|
+
if (payloadField.val() === '') payloadField.val('payload');
|
|
28
|
+
payloadField.typedInput({ default: 'msg', types: ['msg'] });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script type="text/html" data-template-name="PresenceSimulatorUltimate">
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<b>Presence Simulator Ultimate</b>
|
|
36
|
+
<span style="color:red"><i class="fa fa-question-circle"></i> <a target="_blank" href="https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate"><u>Help online</u></a></span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
|
41
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-input-controlTopic"><i class="fa fa-tag"></i> Control topic</label>
|
|
46
|
+
<input type="text" id="node-input-controlTopic">
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-autoStart"><i class="fa fa-play"></i> Auto start</label>
|
|
51
|
+
<input type="checkbox" id="node-input-autoStart" style="width:auto; margin-top:7px;">
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<label for="node-input-autoLoop"><i class="fa fa-refresh"></i> Loop sequence</label>
|
|
56
|
+
<input type="checkbox" id="node-input-autoLoop" style="width:auto; margin-top:7px;">
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-randomize"><i class="fa fa-random"></i> Random delays</label>
|
|
61
|
+
<input type="checkbox" id="node-input-randomize" style="width:auto; margin-top:7px;">
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="form-row">
|
|
65
|
+
<label for="node-input-jitter"><i class="fa fa-percent"></i> Jitter (%)</label>
|
|
66
|
+
<input type="number" id="node-input-jitter" min="0" max="100">
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="form-row">
|
|
70
|
+
<label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
|
|
71
|
+
<input type="text" id="node-input-payloadPropName">
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-input-translatorConfig"><i class="fa fa-language"></i> Translator</label>
|
|
76
|
+
<input type="text" id="node-input-translatorConfig">
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label for="node-input-patterns"><i class="fa fa-list"></i> Sequence</label>
|
|
81
|
+
<textarea id="node-input-patterns" style="width:100%; height:150px;" placeholder='{"delay":600000,"payload":true,"topic":"light/living"}'></textarea>
|
|
82
|
+
</div>
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<script type="text/markdown" data-help-name="PresenceSimulatorUltimate">
|
|
86
|
+
<p>The purpose of this node is to replay a programmable sequence of messages in order to simulate occupancy.</p>
|
|
87
|
+
|
|
88
|
+
|Property|Description|
|
|
89
|
+
|--|--|
|
|
90
|
+
| Control topic | Topic used for runtime commands such as start/stop/reset. |
|
|
91
|
+
| Auto start | Starts the sequence automatically after deploy or restart. |
|
|
92
|
+
| Loop sequence | Repeats the sequence when the end is reached. |
|
|
93
|
+
| Random delays | Enables a random variation of the programmed delays. |
|
|
94
|
+
| Jitter (%) | Maximum percentage of variation applied when random delays are enabled. |
|
|
95
|
+
| With Input | Message property to inspect for inline events (default `payload`). |
|
|
96
|
+
| Translator | Optional translator-config to convert incoming values. |
|
|
97
|
+
| Sequence | One JSON object per line, each composed at least of `delay` (in ms) and the properties to output. |
|
|
98
|
+
|
|
99
|
+
<br/>
|
|
100
|
+
|
|
101
|
+
* Control topic commands (`msg.topic` must match the control topic)
|
|
102
|
+
|
|
103
|
+
Pass <code>msg.command = "start"</code> (or <code>msg.start = true</code>) to begin playback</br>
|
|
104
|
+
Pass <code>msg.command = "stop"</code> (or <code>msg.stop = true</code>) to halt playback</br>
|
|
105
|
+
Pass <code>msg.reset = true</code> to reset counters and start position</br>
|
|
106
|
+
Pass <code>msg.sequence = [...]</code> to load a new sequence at runtime</br>
|
|
107
|
+
Pass <code>msg.loop</code>, <code>msg.randomize</code> or <code>msg.jitter</code> to update the related options</br>
|
|
108
|
+
|
|
109
|
+
<br/>
|
|
110
|
+
|
|
111
|
+
Each event in the sequence outputs a message containing the fields defined in the JSON line. When random delays are enabled, the effective delay is varied within the configured jitter.</br>
|
|
112
|
+
|
|
113
|
+
<br/>
|
|
114
|
+
|
|
115
|
+
[SEE THE README FOR FULL HELP AND SAMPLES](https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate)</br>
|
|
116
|
+
|
|
117
|
+
[Find it useful?](https://www.paypal.me/techtoday)
|
|
118
|
+
|
|
119
|
+
</script>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
const helpers = require('./lib/node-helpers.js');
|
|
5
|
+
|
|
6
|
+
function PresenceSimulatorUltimate(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
node.config = config;
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
controlTopic = 'presence',
|
|
14
|
+
autoStart = false,
|
|
15
|
+
autoLoop = true,
|
|
16
|
+
randomize = false,
|
|
17
|
+
jitter = 0,
|
|
18
|
+
payloadPropName = 'payload',
|
|
19
|
+
translatorConfig,
|
|
20
|
+
patterns = '',
|
|
21
|
+
} = config;
|
|
22
|
+
|
|
23
|
+
const setNodeStatus = helpers.createStatus(node);
|
|
24
|
+
const timerBag = helpers.createTimerBag(node);
|
|
25
|
+
|
|
26
|
+
let eventSequence = parsePatterns(patterns || '');
|
|
27
|
+
let currentIndex = 0;
|
|
28
|
+
let running = false;
|
|
29
|
+
let scheduledTimer = null;
|
|
30
|
+
let emittedCount = 0;
|
|
31
|
+
|
|
32
|
+
function parsePatterns(text) {
|
|
33
|
+
const events = [];
|
|
34
|
+
if (!text) {
|
|
35
|
+
return events;
|
|
36
|
+
}
|
|
37
|
+
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
try {
|
|
40
|
+
const entry = JSON.parse(line);
|
|
41
|
+
if (typeof entry.delay !== 'number' || entry.delay < 0) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
events.push(entry);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// Ignore malformed lines
|
|
47
|
+
node.log(`PresenceSimulatorUltimate: unable to parse pattern line: ${line}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return events;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resetState() {
|
|
54
|
+
stopPlayback();
|
|
55
|
+
currentIndex = 0;
|
|
56
|
+
emittedCount = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function updateStatus(state) {
|
|
60
|
+
const label = state || (running ? 'running' : 'idle');
|
|
61
|
+
const colour = running ? 'green' : 'grey';
|
|
62
|
+
setNodeStatus({
|
|
63
|
+
fill: colour,
|
|
64
|
+
shape: running ? 'dot' : 'ring',
|
|
65
|
+
text: `Presence ${label} sent:${emittedCount}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scheduleNext(delay, handler) {
|
|
70
|
+
if (scheduledTimer) {
|
|
71
|
+
timerBag.clearTimeout(scheduledTimer);
|
|
72
|
+
scheduledTimer = null;
|
|
73
|
+
}
|
|
74
|
+
scheduledTimer = timerBag.setTimeout(handler, delay);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function computeDelay(baseDelay) {
|
|
78
|
+
if (!randomize) {
|
|
79
|
+
return baseDelay;
|
|
80
|
+
}
|
|
81
|
+
const jitterValue = Number(config.jitter ?? jitter) || 0;
|
|
82
|
+
if (jitterValue <= 0) {
|
|
83
|
+
return baseDelay;
|
|
84
|
+
}
|
|
85
|
+
const perc = Math.min(Math.max(jitterValue, 0), 100);
|
|
86
|
+
const delta = baseDelay * (perc / 100);
|
|
87
|
+
const min = Math.max(0, baseDelay - delta);
|
|
88
|
+
const max = baseDelay + delta;
|
|
89
|
+
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function emitEvent(event) {
|
|
93
|
+
if (!event) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const msg = {
|
|
97
|
+
topic: event.topic || config.outputTopic || node.topic || 'presence',
|
|
98
|
+
};
|
|
99
|
+
if (event.hasOwnProperty('payload')) {
|
|
100
|
+
msg.payload = event.payload;
|
|
101
|
+
}
|
|
102
|
+
if (event.hasOwnProperty('properties') && typeof event.properties === 'object') {
|
|
103
|
+
Object.assign(msg, event.properties);
|
|
104
|
+
}
|
|
105
|
+
if (event.clone && typeof event.clone === 'object') {
|
|
106
|
+
Object.assign(msg, event.clone);
|
|
107
|
+
}
|
|
108
|
+
emittedCount += 1;
|
|
109
|
+
node.send(msg);
|
|
110
|
+
updateStatus('playing');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function nextEventIndex() {
|
|
114
|
+
if (eventSequence.length === 0) {
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
if (config.autoLoop ?? autoLoop) {
|
|
118
|
+
currentIndex = (currentIndex + 1) % eventSequence.length;
|
|
119
|
+
return currentIndex;
|
|
120
|
+
}
|
|
121
|
+
currentIndex += 1;
|
|
122
|
+
if (currentIndex >= eventSequence.length) {
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
return currentIndex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function playFrom(index) {
|
|
129
|
+
if (!running || eventSequence.length === 0) {
|
|
130
|
+
stopPlayback();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const event = eventSequence[index];
|
|
134
|
+
if (!event) {
|
|
135
|
+
stopPlayback();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const delay = computeDelay(event.delay);
|
|
139
|
+
scheduleNext(delay, () => {
|
|
140
|
+
emitEvent(event);
|
|
141
|
+
const nextIndex = nextEventIndex();
|
|
142
|
+
if (nextIndex === -1) {
|
|
143
|
+
stopPlayback();
|
|
144
|
+
} else {
|
|
145
|
+
playFrom(nextIndex);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function startPlayback() {
|
|
151
|
+
if (running) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!eventSequence.length) {
|
|
155
|
+
updateStatus('no events');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
running = true;
|
|
159
|
+
updateStatus('starting');
|
|
160
|
+
currentIndex = currentIndex % eventSequence.length;
|
|
161
|
+
playFrom(currentIndex);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function stopPlayback() {
|
|
165
|
+
running = false;
|
|
166
|
+
if (scheduledTimer) {
|
|
167
|
+
timerBag.clearTimeout(scheduledTimer);
|
|
168
|
+
scheduledTimer = null;
|
|
169
|
+
}
|
|
170
|
+
updateStatus('stopped');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function setSequence(newSequence) {
|
|
174
|
+
if (Array.isArray(newSequence)) {
|
|
175
|
+
eventSequence = newSequence
|
|
176
|
+
.filter((entry) => entry && typeof entry.delay === 'number' && entry.delay >= 0)
|
|
177
|
+
.map((entry) => ({ ...entry }));
|
|
178
|
+
currentIndex = 0;
|
|
179
|
+
emittedCount = 0;
|
|
180
|
+
updateStatus('sequence loaded');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleControlMessage(msg) {
|
|
185
|
+
let consumed = false;
|
|
186
|
+
if (msg.reset === true) {
|
|
187
|
+
resetState();
|
|
188
|
+
consumed = true;
|
|
189
|
+
}
|
|
190
|
+
if (msg.start === true || msg.command === 'start') {
|
|
191
|
+
startPlayback();
|
|
192
|
+
consumed = true;
|
|
193
|
+
}
|
|
194
|
+
if (msg.stop === true || msg.command === 'stop') {
|
|
195
|
+
stopPlayback();
|
|
196
|
+
consumed = true;
|
|
197
|
+
}
|
|
198
|
+
if (msg.hasOwnProperty('sequence') && Array.isArray(msg.sequence)) {
|
|
199
|
+
setSequence(msg.sequence);
|
|
200
|
+
consumed = true;
|
|
201
|
+
}
|
|
202
|
+
if (msg.hasOwnProperty('loop')) {
|
|
203
|
+
config.autoLoop = Boolean(msg.loop);
|
|
204
|
+
consumed = true;
|
|
205
|
+
}
|
|
206
|
+
if (msg.hasOwnProperty('randomize')) {
|
|
207
|
+
config.randomize = Boolean(msg.randomize);
|
|
208
|
+
consumed = true;
|
|
209
|
+
}
|
|
210
|
+
if (msg.hasOwnProperty('jitter')) {
|
|
211
|
+
config.jitter = Number(msg.jitter);
|
|
212
|
+
consumed = true;
|
|
213
|
+
}
|
|
214
|
+
return consumed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
node.on('input', (msg) => {
|
|
218
|
+
if (msg.topic === controlTopic) {
|
|
219
|
+
if (handleControlMessage(msg)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Optional runtime addition: if message contains inline event, enqueue it
|
|
225
|
+
if (msg.hasOwnProperty('delay') && typeof msg.delay === 'number') {
|
|
226
|
+
eventSequence.push({
|
|
227
|
+
delay: msg.delay,
|
|
228
|
+
payload: msg[payloadPropName],
|
|
229
|
+
topic: msg.topic,
|
|
230
|
+
properties: msg.properties,
|
|
231
|
+
});
|
|
232
|
+
updateStatus('event added');
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
node.on('close', () => {
|
|
237
|
+
stopPlayback();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
updateStatus();
|
|
241
|
+
if (autoStart) {
|
|
242
|
+
startPlayback();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
RED.nodes.registerType('PresenceSimulatorUltimate', PresenceSimulatorUltimate);
|
|
247
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('RateLimiterUltimate', {
|
|
3
|
+
category: 'Boolean Logic Ultimate',
|
|
4
|
+
color: '#ffcc66',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
mode: { value: 'debounce' },
|
|
8
|
+
wait: { value: 500, validate: RED.validators.number() },
|
|
9
|
+
emitOn: { value: 'trailing' },
|
|
10
|
+
interval: { value: 1000, validate: RED.validators.number() },
|
|
11
|
+
trailing: { value: false },
|
|
12
|
+
windowSize: { value: 1000, validate: RED.validators.number() },
|
|
13
|
+
maxInWindow: { value: 10, validate: RED.validators.number() },
|
|
14
|
+
dropStrategy: { value: 'drop' },
|
|
15
|
+
payloadPropName: { value: 'payload', required: false },
|
|
16
|
+
translatorConfig: { type: 'translator-config', required: false },
|
|
17
|
+
controlTopic: { value: 'rate' },
|
|
18
|
+
statInterval: { value: 0, validate: RED.validators.number() }
|
|
19
|
+
},
|
|
20
|
+
inputs: 1,
|
|
21
|
+
outputs: 2,
|
|
22
|
+
outputLabels: function (index) {
|
|
23
|
+
return index === 0 ? 'Forward' : 'Diagnostics';
|
|
24
|
+
},
|
|
25
|
+
icon: 'file-in.png',
|
|
26
|
+
label: function () {
|
|
27
|
+
const mode = (this.mode || 'debounce').charAt(0).toUpperCase() + (this.mode || 'debounce').slice(1);
|
|
28
|
+
return (this.name || 'RateLimiter') + ' (' + mode + ')';
|
|
29
|
+
},
|
|
30
|
+
paletteLabel: function () {
|
|
31
|
+
return 'Rate Limiter';
|
|
32
|
+
},
|
|
33
|
+
oneditprepare: function () {
|
|
34
|
+
const payloadField = $('#node-input-payloadPropName');
|
|
35
|
+
if (payloadField.val() === '') payloadField.val('payload');
|
|
36
|
+
payloadField.typedInput({ default: 'msg', types: ['msg'] });
|
|
37
|
+
|
|
38
|
+
$('#node-input-trailing').prop('checked', this.trailing === true);
|
|
39
|
+
|
|
40
|
+
function toggleSections(mode) {
|
|
41
|
+
$('.rate-config-section').hide();
|
|
42
|
+
$('.rate-config-common').show();
|
|
43
|
+
$('.rate-config-' + mode).show();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$('#node-input-mode').on('change', function () {
|
|
47
|
+
toggleSections($(this).val());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
toggleSections($('#node-input-mode').val());
|
|
51
|
+
},
|
|
52
|
+
oneditsave: function () {
|
|
53
|
+
this.trailing = $('#node-input-trailing').is(':checked');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<script type="text/html" data-template-name="RateLimiterUltimate">
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<b>Rate Limiter Ultimate</b>
|
|
61
|
+
<span style="color:red"><i class="fa fa-question-circle"></i> <a target="_blank" href="https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate"><u>Help online</u></a></span>
|
|
62
|
+
<span style="color:red"><i class="fa fa-youtube-play"></i> <a target="_blank" href="https://youtu.be/1T1g0HCeYA8"><u>Youtube Sample</u></a></span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="form-row">
|
|
66
|
+
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
|
67
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="form-row rate-config-common">
|
|
71
|
+
<label for="node-input-mode"><i class="fa fa-sliders"></i> Mode</label>
|
|
72
|
+
<select id="node-input-mode">
|
|
73
|
+
<option value="debounce">Debounce</option>
|
|
74
|
+
<option value="throttle">Throttle</option>
|
|
75
|
+
<option value="window">Window</option>
|
|
76
|
+
</select>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="form-row rate-config-common">
|
|
80
|
+
<label for="node-input-controlTopic"><i class="fa fa-tag"></i> Control topic</label>
|
|
81
|
+
<input type="text" id="node-input-controlTopic">
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="form-row rate-config-common">
|
|
85
|
+
<label for="node-input-statInterval"><i class="fa fa-clock-o"></i> Stats every (s)</label>
|
|
86
|
+
<input type="number" min="0" id="node-input-statInterval" placeholder="0 = off">
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="form-row rate-config-common">
|
|
90
|
+
<label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
|
|
91
|
+
<input type="text" id="node-input-payloadPropName">
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="form-row rate-config-common">
|
|
95
|
+
<label for="node-input-translatorConfig"><i class="fa fa-language"></i> Translator</label>
|
|
96
|
+
<input type="text" id="node-input-translatorConfig">
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="form-row rate-config-section rate-config-debounce">
|
|
100
|
+
<label for="node-input-wait"><i class="fa fa-hourglass-o"></i> Wait (ms)</label>
|
|
101
|
+
<input type="number" id="node-input-wait" min="0">
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="form-row rate-config-section rate-config-debounce">
|
|
105
|
+
<label for="node-input-emitOn"><i class="fa fa-exchange"></i> Emit</label>
|
|
106
|
+
<select id="node-input-emitOn">
|
|
107
|
+
<option value="leading">Leading edge</option>
|
|
108
|
+
<option value="trailing">Trailing edge</option>
|
|
109
|
+
<option value="both">Both</option>
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="form-row rate-config-section rate-config-throttle">
|
|
114
|
+
<label for="node-input-interval"><i class="fa fa-hourglass-start"></i> Interval (ms)</label>
|
|
115
|
+
<input type="number" id="node-input-interval" min="0">
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="form-row rate-config-section rate-config-throttle">
|
|
119
|
+
<label for="node-input-trailing"><i class="fa fa-arrow-circle-right"></i> Emit trailing</label>
|
|
120
|
+
<input type="checkbox" id="node-input-trailing" style="width:auto; margin-top:7px;">
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="form-row rate-config-section rate-config-window">
|
|
124
|
+
<label for="node-input-windowSize"><i class="fa fa-arrows-h"></i> Window size (ms)</label>
|
|
125
|
+
<input type="number" id="node-input-windowSize" min="0">
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="form-row rate-config-section rate-config-window">
|
|
129
|
+
<label for="node-input-maxInWindow"><i class="fa fa-filter"></i> Max messages</label>
|
|
130
|
+
<input type="number" id="node-input-maxInWindow" min="1">
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="form-row rate-config-section rate-config-window">
|
|
134
|
+
<label for="node-input-dropStrategy"><i class="fa fa-random"></i> On limit</label>
|
|
135
|
+
<select id="node-input-dropStrategy">
|
|
136
|
+
<option value="drop">Drop</option>
|
|
137
|
+
<option value="queue">Queue last</option>
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<script type="text/markdown" data-help-name="RateLimiterUltimate">
|
|
143
|
+
The purpose of this node is to moderate the frequency of incoming messages.
|
|
144
|
+
|
|
145
|
+
**Modes**
|
|
146
|
+
|
|
147
|
+
- **Debounce** – waits for the line to be quiet before forwarding the final message. With *Leading* it emits the first message immediately and blocks the following ones until the delay ends; *Trailing* sends only the last message; *Both* combines both behaviours.
|
|
148
|
+
- **Throttle** – enforces a minimum interval between consecutive messages. When *Emit trailing* is enabled the most recent message is forwarded once the interval has elapsed.
|
|
149
|
+
- **Window** – limits the number of messages in a moving time window. Extra messages can be dropped or queued to play once a slot becomes available.
|
|
150
|
+
|
|
151
|
+
**Outputs**
|
|
152
|
+
|
|
153
|
+
- Output 1: messages that pass the limiter.
|
|
154
|
+
- Output 2: diagnostic events `{ payload: { mode, reason, passed, dropped, msg, propertyValue } }` and periodic statistics (`controlTopic/stats`).
|
|
155
|
+
|
|
156
|
+
**Control messages** (when `msg.topic` equals the *Control topic*):
|
|
157
|
+
|
|
158
|
+
- `msg.reset = true` – clears counters and buffers.
|
|
159
|
+
- `msg.flush = true` – forces emission of the pending message, if any.
|
|
160
|
+
- `msg.mode = 'debounce'|'throttle'|'window'` – changes mode at runtime.
|
|
161
|
+
- `msg.interval`, `msg.wait`, `msg.windowSize`, `msg.maxInWindow` – adjust the related thresholds.
|
|
162
|
+
|
|
163
|
+
The **With Input** field selects which message property to evaluate (default `msg.payload`). When configured, it can be translated through the **translator-config** node for Home Assistant compatibility.
|
|
164
|
+
</script>
|