node-red-contrib-boolean-logic-ultimate 1.2.9 → 1.2.11

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 CHANGED
@@ -4,11 +4,24 @@
4
4
 
5
5
  # CHANGELOG
6
6
 
7
+ <p>
8
+ <b>Version 1.2.11</b> April 2026<br/>
9
+
10
+ - DebouncerUltimate: simplified the node to a single output, with clearer editor labels for non-technical users.<br/>
11
+ - Updated Debouncer help and README, adding a dedicated explanatory image.<br/>
12
+ </p>
13
+
14
+ <p>
15
+ <b>Version 1.2.10</b> April 2026<br/>
16
+
17
+ - NEW: DebouncerUltimate node for dedicated debounce flows with trailing/leading/both emission modes.<br/>
18
+ - Updated README, examples and tests accordingly.<br/>
19
+ </p>
20
+
7
21
  <p>
8
22
  <b>Version 1.2.9</b> April 2026<br/>
9
- - Removed the following newly introduced HA nodes because considered too complex: ForDurationUltimate, WatchdogUltimate, PriorityMuxUltimate, GroupStateUltimate.<br/>
10
- - Removed related examples, tests and documentation sections.<br/>
11
- - Kept HysteresisUltimate and aligned package registration/examples docs accordingly.<br/>
23
+
24
+ - NEW: Hysteresis node and aligned package registration/examples docs accordingly.<br/>
12
25
  </p>
13
26
 
14
27
  <p>
package/README.md CHANGED
@@ -1,17 +1,11 @@
1
1
  ![Logo](img/logo.png)
2
2
 
3
3
  [![NPM version][npm-version-image]][npm-url]
4
-
5
4
  [![NPM downloads per month][npm-downloads-month-image]][npm-url]
6
-
7
5
  [![NPM downloads total][npm-downloads-total-image]][npm-url]
8
-
9
6
  [![MIT License][license-image]][license-url]
10
-
11
7
  [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
12
-
13
8
  [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
14
-
15
9
  [![youtube][youtube-image]][youtube-url]
16
10
 
17
11
  A set of Node-RED enhanced boolean logic and utility nodes, with persistent values after reboot. Compatible also with Homeassistant values.
@@ -662,7 +656,7 @@ Gateway per sensori e dispositivi troppo “chiacchieroni”: limita burst e rim
662
656
  | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
663
657
  | Mode | Seleziona la logica: _Debounce_ (attende quiete), _Throttle_ (impone un intervallo minimo), _Window_ (massimo N messaggi per finestra temporale). |
664
658
  | Wait (ms) | Ritardo di quiete per la modalità debounce. |
665
- | Emit | Per debounce: scegli tra _Leading_ (subito), _Trailing_ (ultimo), _Both_. |
659
+ | Emit | Per debounce: scegli se inviare subito il primo messaggio, solo l’ultimo dopo la pausa, oppure entrambi. |
666
660
  | Interval (ms) | Intervallo minimo tra messaggi in modalità throttle. |
667
661
  | Emit trailing | In throttle, inoltra l’ultimo messaggio ricevuto allo scadere dell’intervallo. |
668
662
  | Window size (ms) | Larghezza della finestra mobile in modalità window. |
@@ -691,6 +685,37 @@ Gateway per sensori e dispositivi troppo “chiacchieroni”: limita burst e rim
691
685
 
692
686
  <br/>
693
687
 
688
+ # DEBOUNCER ULTIMATE
689
+
690
+ Nodo dedicato al solo debounce: utile quando vuoi filtrare rimbalzi o burst rapidi senza portarti dietro le altre modalità del `RateLimiterUltimate`. Può inoltrare il **primo** messaggio, l’**ultimo** oppure **entrambi**, dopo un intervallo di quiete configurabile.
691
+
692
+ <img src='img/debouncer.png' width='80%'>
693
+
694
+ Nell'immagine si vede il comportamento tipico: a sinistra arrivano molti messaggi ravvicinati, il nodo aspetta una breve pausa, e a destra lascia passare solo il messaggio utile.
695
+
696
+ Example flow: [`examples/DebouncerUltimate.json`](examples/DebouncerUltimate.json)
697
+
698
+ ### NODE CONFIGURATION
699
+
700
+ | Property | Description |
701
+ | ------------- | --------------------------------------------------------------------------------------------- |
702
+ | Wait (ms) | Tempo di quiete richiesto prima di chiudere la finestra di debounce. |
703
+ | Emit | Scegli se inviare subito il primo messaggio, solo l’ultimo dopo la pausa, oppure entrambi. |
704
+ | Control topic | Topic dei messaggi di controllo (default `debouncer`). |
705
+
706
+ ### OUTPUT
707
+
708
+ - **Output 1**: messaggi inoltrati dopo il debounce.
709
+
710
+ ### CONTROL MESSAGES (`msg.topic === controlTopic`)
711
+
712
+ - `msg.reset = true` &rarr; cancella timer e messaggio pendente.
713
+ - `msg.flush = true` &rarr; inoltra subito l’ultimo messaggio in attesa.
714
+ - `msg.wait` &rarr; aggiorna il ritardo di debounce a runtime.
715
+ - `msg.emitOn = 'leading'|'trailing'|'both'` &rarr; cambia la modalità di emissione.
716
+
717
+ <br/>
718
+
694
719
  # PRESENCE SIMULATOR ULTIMATE
695
720
 
696
721
  The purpose of this node is to replay a programmable sequence of messages in order to simulate occupancy when you are away.
@@ -758,16 +783,16 @@ Example flow: [`examples/HysteresisUltimate.json`](examples/HysteresisUltimate.j
758
783
 
759
784
  ### NODE CONFIGURATION
760
785
 
761
- | Property | Description |
762
- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
763
- | Control topic | Topic that receives runtime commands such as threshold updates and reset. |
764
- | Mode | `high` = ON above threshold, OFF below. `low` = ON below threshold, OFF above. |
765
- | ON/OFF threshold | Hysteresis limits. |
766
- | Initial state | Startup output state. |
767
- | Emit only on change | If enabled, output 1 emits only on state transitions. |
768
- | With Input | Message property evaluated as numeric value (default `payload`). |
769
- | Translator | Optional translator-config. |
770
- | On/Off payload | Typed payloads sent on output 1. |
786
+ | Property | Description |
787
+ | ------------------- | ------------------------------------------------------------------------------ |
788
+ | Control topic | Topic that receives runtime commands such as threshold updates and reset. |
789
+ | Mode | `high` = ON above threshold, OFF below. `low` = ON below threshold, OFF above. |
790
+ | ON/OFF threshold | Hysteresis limits. |
791
+ | Initial state | Startup output state. |
792
+ | Emit only on change | If enabled, output 1 emits only on state transitions. |
793
+ | With Input | Message property evaluated as numeric value (default `payload`). |
794
+ | Translator | Optional translator-config. |
795
+ | On/Off payload | Typed payloads sent on output 1. |
771
796
 
772
797
  ### CONTROL MESSAGES (`msg.topic === controlTopic`)
773
798
 
@@ -0,0 +1,76 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('DebouncerUltimate', {
3
+ category: 'Boolean Logic Ultimate',
4
+ color: '#ff8080',
5
+ defaults: {
6
+ name: { value: '' },
7
+ wait: { value: 500, validate: RED.validators.number() },
8
+ emitOn: { value: 'trailing' },
9
+ controlTopic: { value: 'debouncer' }
10
+ },
11
+ inputs: 1,
12
+ outputs: 1,
13
+ outputLabels: ['Forward'],
14
+ icon: 'file-in.png',
15
+ label: function () {
16
+ return this.name || 'Debouncer';
17
+ },
18
+ paletteLabel: function () {
19
+ return 'Debouncer';
20
+ }
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="DebouncerUltimate">
25
+ <div class="form-row">
26
+ <b>Debouncer Ultimate</b>
27
+ &nbsp;&nbsp;<span style="color:red"><i class="fa fa-youtube-play"></i>&nbsp;<a target="_blank" href="https://youtu.be/t45kaMQzm5Q"><u>Youtube Video</u></a></span>
28
+ </div>
29
+
30
+ <div class="form-row">
31
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
32
+ <input type="text" id="node-input-name" placeholder="Name">
33
+ </div>
34
+
35
+ <div class="form-row">
36
+ <label for="node-input-wait"><i class="fa fa-hourglass-o"></i> Wait (ms)</label>
37
+ <input type="number" id="node-input-wait" min="0">
38
+ </div>
39
+
40
+ <div class="form-row">
41
+ <label for="node-input-emitOn"><i class="fa fa-exchange"></i> Emit</label>
42
+ <select id="node-input-emitOn">
43
+ <option value="leading">Send first message immediately</option>
44
+ <option value="trailing">Send only the last message after the pause</option>
45
+ <option value="both">Send first immediately and last after the pause</option>
46
+ </select>
47
+ </div>
48
+
49
+ <div class="form-row">
50
+ <label for="node-input-controlTopic"><i class="fa fa-tag"></i> Control topic</label>
51
+ <input type="text" id="node-input-controlTopic">
52
+ </div>
53
+ </script>
54
+
55
+ <script type="text/markdown" data-help-name="DebouncerUltimate">
56
+ <p>Dedicated debounce node to suppress rapid bursts and forward only the first, last or both messages of a quiet-time window.</p>
57
+
58
+ <p><img src="/resources/node-red-contrib-boolean-logic-ultimate/img/debouncer.png" alt="Debouncer example" style="max-width:100%; border-radius:6px;" /></p>
59
+
60
+ |Property|Description|
61
+ |--|--|
62
+ | Wait (ms) | Quiet time before the debounce window closes. |
63
+ | Emit | Choose whether to send the first message immediately, only the last one after the pause, or both. |
64
+ | Control topic | Topic used for runtime commands (default `debouncer`). |
65
+
66
+ Output:
67
+
68
+ - Output 1 forwards the debounced message.
69
+
70
+ Control topic messages:
71
+
72
+ - `msg.reset = true` clears the timer and pending message.
73
+ - `msg.flush = true` emits the pending message immediately.
74
+ - `msg.wait` updates the debounce time.
75
+ - `msg.emitOn = 'leading'|'trailing'|'both'` changes the emission mode.
76
+ </script>
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (RED) {
4
+ function DebouncerUltimate(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+ const REDUtil = RED.util;
8
+ const helpers = require('./lib/node-helpers.js');
9
+
10
+ const setNodeStatus = helpers.createStatus(node);
11
+ const timerBag = helpers.createTimerBag(node);
12
+
13
+ const controlTopic = config.controlTopic || 'debouncer';
14
+
15
+ let wait = Number(config.wait);
16
+ if (!Number.isFinite(wait) || wait < 0) {
17
+ wait = 500;
18
+ }
19
+
20
+ let emitOn = normalizeEmitOn(config.emitOn);
21
+ let passedCount = 0;
22
+ let droppedCount = 0;
23
+ let currentState = 'idle';
24
+ let debounceTimer = null;
25
+ let leadingSent = false;
26
+ let pendingMessage = null;
27
+
28
+ function normalizeEmitOn(value) {
29
+ const normalized = String(value || 'trailing').toLowerCase();
30
+ return ['leading', 'trailing', 'both'].includes(normalized)
31
+ ? normalized
32
+ : 'trailing';
33
+ }
34
+
35
+ function clone(msg) {
36
+ return REDUtil.cloneMessage ? REDUtil.cloneMessage(msg) : JSON.parse(JSON.stringify(msg));
37
+ }
38
+
39
+ function sendStatus() {
40
+ const colour = currentState === 'idle' ? 'green' : 'yellow';
41
+ const modeLabel = {
42
+ leading: 'L',
43
+ trailing: 'T',
44
+ both: 'B',
45
+ }[emitOn] || 'T';
46
+
47
+ setNodeStatus({
48
+ fill: colour,
49
+ shape: 'dot',
50
+ text: `DB|${modeLabel}|${currentState} pass:${passedCount} drop:${droppedCount}`,
51
+ });
52
+ }
53
+
54
+ function emitForward(msg, nextState = 'idle') {
55
+ passedCount += 1;
56
+ currentState = nextState;
57
+ node.send(msg);
58
+ sendStatus();
59
+ }
60
+
61
+ function emitDrop() {
62
+ droppedCount += 1;
63
+ currentState = 'waiting';
64
+ sendStatus();
65
+ }
66
+
67
+ function clearDebounce() {
68
+ if (debounceTimer) {
69
+ timerBag.clearTimeout(debounceTimer);
70
+ debounceTimer = null;
71
+ }
72
+ pendingMessage = null;
73
+ leadingSent = false;
74
+ }
75
+
76
+ function resetDebounce() {
77
+ clearDebounce();
78
+ currentState = 'idle';
79
+ sendStatus();
80
+ }
81
+
82
+ function flushPending() {
83
+ if (!pendingMessage) {
84
+ currentState = 'idle';
85
+ sendStatus();
86
+ return false;
87
+ }
88
+
89
+ const toSend = pendingMessage;
90
+ if (debounceTimer) {
91
+ timerBag.clearTimeout(debounceTimer);
92
+ debounceTimer = null;
93
+ }
94
+ pendingMessage = null;
95
+ leadingSent = false;
96
+ emitForward(toSend, 'idle');
97
+ return true;
98
+ }
99
+
100
+ function handleControlMessage(msg) {
101
+ let consumed = false;
102
+
103
+ if (Object.prototype.hasOwnProperty.call(msg, 'wait')) {
104
+ const nextWait = Number(msg.wait);
105
+ if (Number.isFinite(nextWait) && nextWait >= 0) {
106
+ wait = nextWait;
107
+ consumed = true;
108
+ }
109
+ }
110
+
111
+ if (typeof msg.emitOn === 'string') {
112
+ emitOn = normalizeEmitOn(msg.emitOn);
113
+ consumed = true;
114
+ }
115
+
116
+ if (msg.reset === true) {
117
+ resetDebounce();
118
+ consumed = true;
119
+ }
120
+
121
+ if (msg.flush === true) {
122
+ flushPending();
123
+ consumed = true;
124
+ }
125
+
126
+ if (msg.status === true) {
127
+ consumed = true;
128
+ sendStatus();
129
+ }
130
+
131
+ if (consumed) {
132
+ sendStatus();
133
+ }
134
+
135
+ return consumed;
136
+ }
137
+
138
+ function handleDebounce(msg) {
139
+ const cloned = clone(msg);
140
+
141
+ if (debounceTimer) {
142
+ timerBag.clearTimeout(debounceTimer);
143
+ debounceTimer = null;
144
+ }
145
+
146
+ pendingMessage = cloned;
147
+ currentState = 'waiting';
148
+
149
+ const shouldEmitLeading =
150
+ (emitOn === 'leading' || emitOn === 'both') && !leadingSent;
151
+
152
+ if (shouldEmitLeading) {
153
+ emitForward(cloned, 'waiting');
154
+ leadingSent = true;
155
+ }
156
+
157
+ if (emitOn === 'leading' && !shouldEmitLeading) {
158
+ emitDrop();
159
+ }
160
+
161
+ debounceTimer = timerBag.setTimeout(() => {
162
+ debounceTimer = null;
163
+ leadingSent = false;
164
+ const shouldEmitTrailing = emitOn === 'trailing' || emitOn === 'both';
165
+
166
+ if (shouldEmitTrailing && pendingMessage) {
167
+ const toSend = pendingMessage;
168
+ pendingMessage = null;
169
+ emitForward(toSend, 'idle');
170
+ } else {
171
+ pendingMessage = null;
172
+ currentState = 'idle';
173
+ sendStatus();
174
+ }
175
+ }, wait);
176
+ }
177
+
178
+ node.on('input', function (msg) {
179
+ if (msg.topic === controlTopic && handleControlMessage(msg)) {
180
+ return;
181
+ }
182
+
183
+ handleDebounce(msg);
184
+ });
185
+
186
+ sendStatus();
187
+ }
188
+
189
+ RED.nodes.registerType('DebouncerUltimate', DebouncerUltimate);
190
+ };
@@ -104,9 +104,9 @@
104
104
  <div class="form-row rate-config-section rate-config-debounce">
105
105
  <label for="node-input-emitOn"><i class="fa fa-exchange"></i> Emit</label>
106
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>
107
+ <option value="leading">Send first message immediately</option>
108
+ <option value="trailing">Send only the last message after the pause</option>
109
+ <option value="both">Send first immediately and last after the pause</option>
110
110
  </select>
111
111
  </div>
112
112
 
@@ -116,7 +116,7 @@
116
116
  </div>
117
117
 
118
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>
119
+ <label for="node-input-trailing"><i class="fa fa-arrow-circle-right"></i> Also send the last queued message</label>
120
120
  <input type="checkbox" id="node-input-trailing" style="width:auto; margin-top:7px;">
121
121
  </div>
122
122
 
@@ -144,8 +144,8 @@ The purpose of this node is to moderate the frequency of incoming messages.
144
144
 
145
145
  **Modes**
146
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.
147
+ - **Debounce** – waits for the line to be quiet before forwarding a message. You can choose to send the first message immediately, only the last message after the pause, or both.
148
+ - **Throttle** – enforces a minimum interval between consecutive messages. When *Also send the last queued message* is enabled the most recent message is forwarded once the interval has elapsed.
149
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
150
 
151
151
  **Outputs**
@@ -161,4 +161,4 @@ The purpose of this node is to moderate the frequency of incoming messages.
161
161
  - `msg.interval`, `msg.wait`, `msg.windowSize`, `msg.maxInWindow` – adjust the related thresholds.
162
162
 
163
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>
164
+ </script>
@@ -0,0 +1,168 @@
1
+ [
2
+ {
3
+ "id": "db_tab_1",
4
+ "type": "tab",
5
+ "label": "DebouncerUltimate - burst filter",
6
+ "disabled": false,
7
+ "info": "Esempio: filtra burst ravvicinati con il nodo DebouncerUltimate."
8
+ },
9
+ {
10
+ "id": "db_cmt_1",
11
+ "type": "comment",
12
+ "z": "db_tab_1",
13
+ "name": "Burst → Debouncer → Forward",
14
+ "info": "Clicca BURST per generare messaggi ravvicinati. Cambia la modalità con LEADING/TRAILING/BOTH oppure usa FLUSH/RESET via topic \"debouncer\".",
15
+ "x": 300,
16
+ "y": 60,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "db_inj_burst",
21
+ "type": "inject",
22
+ "z": "db_tab_1",
23
+ "name": "BURST (10 msgs)",
24
+ "props": [
25
+ { "p": "count", "v": "10", "vt": "num" },
26
+ { "p": "interval", "v": "50", "vt": "num" }
27
+ ],
28
+ "repeat": "",
29
+ "crontab": "",
30
+ "once": false,
31
+ "onceDelay": 0.1,
32
+ "x": 150,
33
+ "y": 140,
34
+ "wires": [["db_fn_burst"]]
35
+ },
36
+ {
37
+ "id": "db_fn_burst",
38
+ "type": "function",
39
+ "z": "db_tab_1",
40
+ "name": "Burst generator",
41
+ "func": "const count = Number(msg.count || 10);\nconst interval = Number(msg.interval || 50);\nlet i = 0;\n\nconst sendOne = () => {\n node.send({ topic: 'data', payload: i, index: i, ts: Date.now() });\n i += 1;\n if (i < count) {\n setTimeout(sendOne, interval);\n }\n};\n\nsendOne();\nreturn null;",
42
+ "outputs": 1,
43
+ "noerr": 0,
44
+ "initialize": "",
45
+ "finalize": "",
46
+ "libs": [],
47
+ "x": 360,
48
+ "y": 140,
49
+ "wires": [["db_node_1"]]
50
+ },
51
+ {
52
+ "id": "db_inj_leading",
53
+ "type": "inject",
54
+ "z": "db_tab_1",
55
+ "name": "LEADING (400ms)",
56
+ "props": [
57
+ { "p": "topic", "v": "debouncer", "vt": "str" },
58
+ { "p": "emitOn", "v": "leading", "vt": "str" },
59
+ { "p": "wait", "v": "400", "vt": "num" }
60
+ ],
61
+ "repeat": "",
62
+ "crontab": "",
63
+ "once": false,
64
+ "onceDelay": 0.1,
65
+ "x": 160,
66
+ "y": 220,
67
+ "wires": [["db_node_1"]]
68
+ },
69
+ {
70
+ "id": "db_inj_trailing",
71
+ "type": "inject",
72
+ "z": "db_tab_1",
73
+ "name": "TRAILING (400ms)",
74
+ "props": [
75
+ { "p": "topic", "v": "debouncer", "vt": "str" },
76
+ { "p": "emitOn", "v": "trailing", "vt": "str" },
77
+ { "p": "wait", "v": "400", "vt": "num" }
78
+ ],
79
+ "repeat": "",
80
+ "crontab": "",
81
+ "once": false,
82
+ "onceDelay": 0.1,
83
+ "x": 170,
84
+ "y": 260,
85
+ "wires": [["db_node_1"]]
86
+ },
87
+ {
88
+ "id": "db_inj_both",
89
+ "type": "inject",
90
+ "z": "db_tab_1",
91
+ "name": "BOTH (400ms)",
92
+ "props": [
93
+ { "p": "topic", "v": "debouncer", "vt": "str" },
94
+ { "p": "emitOn", "v": "both", "vt": "str" },
95
+ { "p": "wait", "v": "400", "vt": "num" }
96
+ ],
97
+ "repeat": "",
98
+ "crontab": "",
99
+ "once": false,
100
+ "onceDelay": 0.1,
101
+ "x": 150,
102
+ "y": 300,
103
+ "wires": [["db_node_1"]]
104
+ },
105
+ {
106
+ "id": "db_inj_flush",
107
+ "type": "inject",
108
+ "z": "db_tab_1",
109
+ "name": "FLUSH",
110
+ "props": [
111
+ { "p": "topic", "v": "debouncer", "vt": "str" },
112
+ { "p": "flush", "v": "true", "vt": "bool" }
113
+ ],
114
+ "repeat": "",
115
+ "crontab": "",
116
+ "once": false,
117
+ "onceDelay": 0.1,
118
+ "x": 120,
119
+ "y": 340,
120
+ "wires": [["db_node_1"]]
121
+ },
122
+ {
123
+ "id": "db_inj_reset",
124
+ "type": "inject",
125
+ "z": "db_tab_1",
126
+ "name": "RESET",
127
+ "props": [
128
+ { "p": "topic", "v": "debouncer", "vt": "str" },
129
+ { "p": "reset", "v": "true", "vt": "bool" }
130
+ ],
131
+ "repeat": "",
132
+ "crontab": "",
133
+ "once": false,
134
+ "onceDelay": 0.1,
135
+ "x": 120,
136
+ "y": 380,
137
+ "wires": [["db_node_1"]]
138
+ },
139
+ {
140
+ "id": "db_node_1",
141
+ "type": "DebouncerUltimate",
142
+ "z": "db_tab_1",
143
+ "name": "Debouncer",
144
+ "wait": 400,
145
+ "emitOn": "trailing",
146
+ "controlTopic": "debouncer",
147
+ "x": 590,
148
+ "y": 240,
149
+ "wires": [["db_dbg_fwd"]]
150
+ },
151
+ {
152
+ "id": "db_dbg_fwd",
153
+ "type": "debug",
154
+ "z": "db_tab_1",
155
+ "name": "Forward",
156
+ "active": true,
157
+ "tosidebar": true,
158
+ "console": false,
159
+ "tostatus": false,
160
+ "complete": "true",
161
+ "targetType": "full",
162
+ "statusVal": "",
163
+ "statusType": "auto",
164
+ "x": 780,
165
+ "y": 220,
166
+ "wires": []
167
+ }
168
+ ]
@@ -11,6 +11,7 @@ How to import:
11
11
  - `BlinkerUltimate.json` — BlinkerUltimate
12
12
  - `BooleanLogicUltimate.json` — BooleanLogicUltimate
13
13
  - `Comparator.json` — Comparator
14
+ - `DebouncerUltimate.json` — DebouncerUltimate
14
15
  - `FilterUltimate.json` — FilterUltimate
15
16
  - `HysteresisUltimate.json` — HysteresisUltimate
16
17
  - `ImpulseUltimate.json` — ImpulseUltimate
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-boolean-logic-ultimate",
3
- "version": "1.2.9",
4
- "description": "A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.",
3
+ "version": "1.2.11",
4
+ "description": "A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, debouncer, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.",
5
5
  "author": "Supergiovane (https://github.com/Supergiovane)",
6
6
  "dependencies": {
7
7
  "fs": "0.0.1-security",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "node-red": {
30
30
  "nodes": {
31
+ "DebouncerUltimate": "boolean-logic-ultimate/DebouncerUltimate.js",
31
32
  "BooleanLogicUltimate": "boolean-logic-ultimate/BooleanLogicUltimate.js",
32
33
  "InvertUltimate": "boolean-logic-ultimate/InvertUltimate.js",
33
34
  "FilterUltimate": "boolean-logic-ultimate/FilterUltimate.js",
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { helper, loadDebouncer } = require('./helpers');
5
+
6
+ describe('DebouncerUltimate node', function () {
7
+ this.timeout(5000);
8
+
9
+ before(function (done) {
10
+ helper.startServer(done);
11
+ });
12
+
13
+ after(function (done) {
14
+ helper.stopServer(done);
15
+ });
16
+
17
+ afterEach(function () {
18
+ return helper.unload();
19
+ });
20
+
21
+ it('trailing debounce emits only the last message after quiet time', function (done) {
22
+ const flowId = 'flow1';
23
+ const flow = [
24
+ { id: flowId, type: 'tab', label: 'flow1' },
25
+ {
26
+ id: 'debouncer',
27
+ type: 'DebouncerUltimate',
28
+ z: flowId,
29
+ wait: 40,
30
+ emitOn: 'trailing',
31
+ controlTopic: 'debouncer',
32
+ wires: [['out']],
33
+ },
34
+ { id: 'in', type: 'helper', z: flowId, wires: [['debouncer']] },
35
+ { id: 'out', type: 'helper', z: flowId },
36
+ ];
37
+
38
+ loadDebouncer(flow).then(() => {
39
+ const debouncer = helper.getNode('debouncer');
40
+ const out = helper.getNode('out');
41
+ const seen = [];
42
+
43
+ out.on('input', (msg) => {
44
+ seen.push(msg.payload);
45
+ });
46
+
47
+ debouncer.receive({ topic: 'sensor', payload: 'first' });
48
+ setTimeout(() => {
49
+ debouncer.receive({ topic: 'sensor', payload: 'second' });
50
+ }, 15);
51
+
52
+ setTimeout(() => {
53
+ try {
54
+ expect(seen).to.deep.equal(['second']);
55
+ done();
56
+ } catch (error) {
57
+ done(error);
58
+ }
59
+ }, 120);
60
+ }).catch(done);
61
+ });
62
+
63
+ it('both mode emits first immediately and last after quiet time', function (done) {
64
+ const flowId = 'flow2';
65
+ const flow = [
66
+ { id: flowId, type: 'tab', label: 'flow2' },
67
+ {
68
+ id: 'debouncer',
69
+ type: 'DebouncerUltimate',
70
+ z: flowId,
71
+ wait: 50,
72
+ emitOn: 'both',
73
+ controlTopic: 'debouncer',
74
+ wires: [['out']],
75
+ },
76
+ { id: 'in', type: 'helper', z: flowId, wires: [['debouncer']] },
77
+ { id: 'out', type: 'helper', z: flowId },
78
+ ];
79
+
80
+ loadDebouncer(flow).then(() => {
81
+ const debouncer = helper.getNode('debouncer');
82
+ const out = helper.getNode('out');
83
+ const seen = [];
84
+
85
+ out.on('input', (msg) => {
86
+ seen.push(msg.payload);
87
+ });
88
+
89
+ debouncer.receive({ topic: 'sensor', payload: 'first' });
90
+ setTimeout(() => {
91
+ debouncer.receive({ topic: 'sensor', payload: 'second' });
92
+ }, 15);
93
+
94
+ setTimeout(() => {
95
+ try {
96
+ expect(seen).to.deep.equal(['first', 'second']);
97
+ done();
98
+ } catch (error) {
99
+ done(error);
100
+ }
101
+ }, 140);
102
+ }).catch(done);
103
+ });
104
+
105
+ it('leading mode emits only the first message during the debounce window', function (done) {
106
+ const flowId = 'flow3';
107
+ const flow = [
108
+ { id: flowId, type: 'tab', label: 'flow3' },
109
+ {
110
+ id: 'debouncer',
111
+ type: 'DebouncerUltimate',
112
+ z: flowId,
113
+ wait: 60,
114
+ emitOn: 'leading',
115
+ controlTopic: 'debouncer',
116
+ wires: [['out']],
117
+ },
118
+ { id: 'in', type: 'helper', z: flowId, wires: [['debouncer']] },
119
+ { id: 'out', type: 'helper', z: flowId },
120
+ ];
121
+
122
+ loadDebouncer(flow).then(() => {
123
+ const debouncer = helper.getNode('debouncer');
124
+ const out = helper.getNode('out');
125
+ const seen = [];
126
+
127
+ out.on('input', (msg) => {
128
+ seen.push(msg.payload);
129
+ });
130
+
131
+ debouncer.receive({ topic: 'sensor', payload: 'first' });
132
+ setTimeout(() => {
133
+ debouncer.receive({ topic: 'sensor', payload: 'second' });
134
+ }, 15);
135
+
136
+ setTimeout(() => {
137
+ try {
138
+ expect(seen).to.deep.equal(['first']);
139
+ done();
140
+ } catch (error) {
141
+ done(error);
142
+ }
143
+ }, 140);
144
+ }).catch(done);
145
+ });
146
+ });
package/test/helpers.js CHANGED
@@ -6,6 +6,7 @@ const helper = require('node-red-node-test-helper');
6
6
  helper.init(require.resolve('node-red')); // initialise with Node-RED runtime
7
7
 
8
8
  const nodes = {
9
+ DebouncerUltimate: require(path.join('..', 'boolean-logic-ultimate', 'DebouncerUltimate.js')),
9
10
  RateLimiterUltimate: require(path.join('..', 'boolean-logic-ultimate', 'RateLimiterUltimate.js')),
10
11
  PresenceSimulatorUltimate: require(path.join('..', 'boolean-logic-ultimate', 'PresenceSimulatorUltimate.js')),
11
12
  RailwaySwitchUltimate: require(path.join('..', 'boolean-logic-ultimate', 'RailwaySwitchUltimate.js')),
@@ -33,6 +34,10 @@ function loadRateLimiter(flow, credentials = {}) {
33
34
  return loadNode(nodes.RateLimiterUltimate, flow, credentials);
34
35
  }
35
36
 
37
+ function loadDebouncer(flow, credentials = {}) {
38
+ return loadNode(nodes.DebouncerUltimate, flow, credentials);
39
+ }
40
+
36
41
  function loadPresence(flow, credentials = {}) {
37
42
  return loadNode(nodes.PresenceSimulatorUltimate, flow, credentials);
38
43
  }
@@ -43,6 +48,7 @@ function loadRailwaySwitch(flow, credentials = {}) {
43
48
 
44
49
  module.exports = {
45
50
  helper,
51
+ loadDebouncer,
46
52
  loadRateLimiter,
47
53
  loadPresence,
48
54
  loadRailwaySwitch,