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

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,38 @@
4
4
 
5
5
  # CHANGELOG
6
6
 
7
+ <p>
8
+ <b>Version 1.2.13</b> May 2026<br/>
9
+
10
+ - BooleanLogicUltimate: renamed the editor field label from "Evaluate" to "Input".<br/>
11
+ - Updated BooleanLogic help and README terminology to match the new label.<br/>
12
+ </p>
13
+
14
+ <p>
15
+ <b>Version 1.2.12</b> May 2026<br/>
16
+
17
+ - Renamed editor label from "With Input" to "Input" for clearer wording across affected nodes.<br/>
18
+ - Updated the related node help sections and README terminology accordingly.<br/>
19
+ </p>
20
+
21
+ <p>
22
+ <b>Version 1.2.11</b> April 2026<br/>
23
+
24
+ - DebouncerUltimate: simplified the node to a single output, with clearer editor labels for non-technical users.<br/>
25
+ - Updated Debouncer help and README, adding a dedicated explanatory image.<br/>
26
+ </p>
27
+
28
+ <p>
29
+ <b>Version 1.2.10</b> April 2026<br/>
30
+
31
+ - NEW: DebouncerUltimate node for dedicated debounce flows with trailing/leading/both emission modes.<br/>
32
+ - Updated README, examples and tests accordingly.<br/>
33
+ </p>
34
+
7
35
  <p>
8
36
  <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/>
37
+
38
+ - NEW: Hysteresis node and aligned package registration/examples docs accordingly.<br/>
12
39
  </p>
13
40
 
14
41
  <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.
@@ -105,7 +99,7 @@ The node can convert arbitrary input values to true/false. It supports Homeassis
105
99
  | Property | Description |
106
100
  | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
101
  | Inputs count | Set the number of different topics to be evaluated. The node will output a message to the flow, after this number of _different_ topics arrives. _Remember: each input topic must be different. For example, if you set this field to 3, the node expects 3 different topics._ |
108
- | Evaluate | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
102
+ | Input | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
109
103
  | Filter output | **Output both 'true' and 'false'** results: Standard behaviour, the node will output <b>true</b> and <b>false</b> whenever it receives an input and calculate the boolean logics as output. **Output only 'true'** results: whenever the node receives an input, it outputs a payload <b>true</b> only if the result of the logic is true. <b>False</b> results are filtered out. |
110
104
  | Trigger mode | **All topics**: standard behaviour, the node will evaluate each input topic and ouputs the values. At each input change, it will output a msg on the flow. **Single topic + eval other inputs**: the node evaluates all the input topics, but only whenever it receives a msg input with the **specified topic**, it outputs a msg to the flow. |
111
105
  | If input states are undefined | Every time you create a node or modify the node, all inputs are set to undefined. This means that the node will wait the arrive of all topics (for example 3 topics, if you've selected 3 topics in the option), before it can output a payload. This can be a problem if your logic must be operative as soon as you deploy the flow. To overcome this problem, you can "initialize" all the undefined inputs with True or False. **Leave undefined**: Standard behaviour, the node will wait all the "undefined" topics to arrive, then starts a flow with the result. **True or False**: The node is immediately operative, by force the initialization of the "undefined" inputs with "true" or "false". |
@@ -137,7 +131,7 @@ The interrupt flows is able to stop the input messages to exiting the node.
137
131
  | Property | Description |
138
132
  | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
139
133
  | Trigger by topic | Whenever the node receives a payload = false from this topic,it stops output messages to the flow. As soon it receives payload = true from this topic, the output messages start to flow out again. The node will output the current stored message plus an added property "isReplay = true", as soon as it receives a **_msg.play = true_** from this topic. The node will clear the current stored message, as soon as it receives a **_msg.reset = true_** from this topic. |
140
- | With Input | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
134
+ | Input | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
141
135
  | Then | This property, allow you to auto toggle the selected start state (pass or block) after a timer has elapsed. You can choose from some pre-defined delays. If you have, for example, an Homekit-Bridged nodeset with a thermostat node or security system node in your flow, once node-red restarts, these homekit nodes output a default message to the flow. Just put an InterruptFlow node with a "block at start" behaviour and a toggle delay enabled behind homekit nodes, to temporary stop the chained nodes to receive the unwanted startup message. |
142
136
 
143
137
  <br/>
@@ -477,7 +471,7 @@ The railway switcher, redirect the incoming messages to one ot the avaiable outp
477
471
  | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
478
472
  | Switcher topic | Whenever the node receives a payload from this **topic**, it redirects the input messages to a choosen output PIN. |
479
473
  | Output pins | Number of output pins (outputs) to show. Default is 5, range is 1..10. |
480
- | With Input | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
474
+ | Input | It's the msg property to be evaluated. _By default, it is "payload", but you can also specify other properties, for example "payload.value"_ |
481
475
  | Translator | Translates the incoming <code>payload</code> value. This allows the compatibility with, for example, **HomeAssistant** nodes. |
482
476
 
483
477
  ### Inputs
@@ -496,7 +490,7 @@ Take the example where you choosen such properties:
496
490
 
497
491
  **Switcher topic**: "switcher"
498
492
 
499
- **With Input**: "payload"
493
+ **Input**: "payload"
500
494
 
501
495
  this JSON input message redirects all input messages to the first PIN
502
496
 
@@ -662,14 +656,14 @@ 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. |
669
663
  | Max messages | Numero di messaggi ammessi nella finestra. |
670
664
  | On limit | _Drop_ scarta i messaggi extra, _Queue last_ accoda l’ultimo e lo riproduce appena possibile. |
671
665
  | Control topic | Topic dei messaggi di controllo (default `rate`). |
672
- | With Input | Proprietà del messaggio da monitorare (default `msg.payload`). |
666
+ | Input | Proprietà del messaggio da monitorare (default `msg.payload`). |
673
667
  | Stats every (s) | Ogni quanti secondi emettere un riepilogo statistico (0 = disattivato). |
674
668
  | Translator | Nodo translator-config opzionale per adattare le stringhe d’ingresso a true/false. |
675
669
 
@@ -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.
@@ -704,7 +729,7 @@ The purpose of this node is to replay a programmable sequence of messages in ord
704
729
  | Loop sequence | Repeats the sequence when it reaches the end. |
705
730
  | Random delays | Enables a random variation of the programmed delays. |
706
731
  | Jitter (%) | Maximum percentage of variation applied when random delays are enabled. |
707
- | With Input | Message property to inspect for inline events (default `payload`). |
732
+ | Input | Message property to inspect for inline events (default `payload`). |
708
733
  | Translator | Optional translator-config to convert incoming values. |
709
734
  | Sequence | One JSON object per line, each containing at least `delay` (ms) plus the properties to output. |
710
735
 
@@ -734,7 +759,7 @@ The purpose of this node is to control staircase lighting with a timer, pre-off
734
759
  | Warning offset (s) | Seconds before switch-off when the warning is sent. |
735
760
  | Restart on trigger | Restarts the timer when a new trigger arrives while active. |
736
761
  | Allow off input | Allows a `false` from the main input to switch off immediately. |
737
- | With Input | Message property evaluated as trigger (default `payload`). |
762
+ | Input | Message property evaluated as trigger (default `payload`). |
738
763
  | Translator | Optional translator-config for true/false conversion. |
739
764
  | On/Off payload | Values emitted on output 1 to turn the light on/off. |
740
765
  | Warning payload | Value emitted on output 2 when the warning fires. |
@@ -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
+ | 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
 
@@ -142,7 +142,7 @@
142
142
  <input style="width:100px" type="text" id="node-input-inputCount" placeholder="Inputs count, for example: 2">
143
143
  </div>
144
144
  <div class="form-row">
145
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Evaluate</label>
145
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
146
146
  <input type="text" id="node-input-payloadPropName">
147
147
  </div>
148
148
  <div class="form-row">
@@ -228,7 +228,7 @@
228
228
  |Property|Description|
229
229
  |--|--|
230
230
  | Inputs count | Set the number of different topics to be evaluated. The node will output a message to the flow, after this number of *different* topics arrives. *Remember: each input topic must be different. For example, if you set this field to 3, the node expects 3 different topics.* |
231
- | Evaluate | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
231
+ | Input | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
232
232
 
233
233
  <br/>
234
234
 
@@ -259,4 +259,4 @@
259
259
  [Find it useful?](https://www.paypal.me/techtoday)
260
260
  <br/>
261
261
 
262
- </script>
262
+ </script>
@@ -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
+ };
@@ -96,7 +96,7 @@
96
96
  </div>
97
97
 
98
98
  <div class="form-row">
99
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
99
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
100
100
  <input type="text" id="node-input-payloadPropName">
101
101
  </div>
102
102
 
@@ -124,7 +124,7 @@
124
124
  | Mode | `high`: ON above threshold, OFF below. `low`: ON below threshold, OFF above. |
125
125
  | ON/OFF threshold | The two hysteresis limits. |
126
126
  | Emit only on change | Emits output only when state changes. |
127
- | With Input | Message property to evaluate (default `payload`). |
127
+ | Input | Message property to evaluate (default `payload`). |
128
128
  | Translator | Optional translator-config. |
129
129
  | On/Off payload | Values sent on output 1. |
130
130
 
@@ -56,7 +56,7 @@
56
56
  <input type="text" id="node-input-triggertopic" placeholder="Name">
57
57
  </div>
58
58
  <div class="form-row">
59
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
59
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
60
60
  <input type="text" id="node-input-payloadPropName">
61
61
  </div>
62
62
  <div><i>Whenever the node receives a msg with false from this topic,<br/> it stops output messages to the flow.<br/>As soon it receives a message with true from this topic,<br/>the output messages start to flow out again.
@@ -105,7 +105,7 @@
105
105
  |Property|Description|
106
106
  |--|--|
107
107
  | Trigger by topic | Whenever the node receives a payload = false from this topic,it stops output messages to the flow. As soon it receives payload = true from this topic, the output messages start to flow out again. The node will output the current stored message plus an added property "isReplay = true", as soon as it receives a ***msg.play = true*** from this topic. The node will clear the current stored message, as soon as it receives a ***msg.reset = true*** from this topic. |
108
- | With Input | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
108
+ | Input | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
109
109
  | Then | This property, allow you to auto toggle the selected start state (pass or block) after a timer has elapsed. You can choose from some pre-defined delays. If you have, for example, an Homekit-Bridged nodeset with a thermostat node or security system node in your flow, once node-red restarts, these homekit nodes output a default message to the flow. Just put an InterruptFlow node with a "block at start" behaviour and a toggle delay enabled behind homekit nodes, to temporary stop the chained nodes to receive the unwanted startup message.|
110
110
  | Translator Input | Translates the incoming <code>payload</code> value, to true/false. This allows the compatibility with, for example, **HomeAssistant** nodes. |
111
111
 
@@ -68,7 +68,7 @@
68
68
  </div>
69
69
 
70
70
  <div class="form-row">
71
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
71
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
72
72
  <input type="text" id="node-input-payloadPropName">
73
73
  </div>
74
74
 
@@ -93,7 +93,7 @@
93
93
  | Loop sequence | Repeats the sequence when the end is reached. |
94
94
  | Random delays | Enables a random variation of the programmed delays. |
95
95
  | Jitter (%) | Maximum percentage of variation applied when random delays are enabled. |
96
- | With Input | Message property to inspect for inline events (default `payload`). |
96
+ | Input | Message property to inspect for inline events (default `payload`). |
97
97
  | Translator | Optional translator-config to convert incoming values. |
98
98
  | Sequence | One JSON object per line, each composed at least of `delay` (in ms) and the properties to output. |
99
99
 
@@ -141,7 +141,7 @@
141
141
  </select>
142
142
  </div>
143
143
  <div class="form-row">
144
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
144
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
145
145
  <input type="text" id="node-input-payloadPropName">
146
146
  </div>
147
147
  <div class="form-row">
@@ -163,7 +163,7 @@
163
163
  |--|--|
164
164
  | Switcher topic | Whenever the node receives a payload from this **topic**, it redirects the input messages to a choosen output PIN. |
165
165
  | Output pins | Number of output pins to display (1..10). |
166
- | With Input | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
166
+ | Input | It's the msg property to be evaluated. *By default, it is "payload", but you can also specify other properties, for example "payload.value"* |
167
167
  | Translator | Translates the incoming <code>payload</code> value. This allows the compatibility with, for example, **HomeAssistant** nodes. |
168
168
 
169
169
  ### Inputs
@@ -178,7 +178,7 @@ Once an output PIN has been choosen, all messages passing through the node will
178
178
 
179
179
  Take the example where you choosen such properties:
180
180
  **Switcher topic**: "switcher"
181
- **With Input**: "payload"
181
+ **Input**: "payload"
182
182
  this JSON input message redirects all input messages to the first PIN
183
183
 
184
184
  ```json
@@ -87,7 +87,7 @@
87
87
  </div>
88
88
 
89
89
  <div class="form-row rate-config-common">
90
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
90
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
91
91
  <input type="text" id="node-input-payloadPropName">
92
92
  </div>
93
93
 
@@ -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**
@@ -160,5 +160,5 @@ The purpose of this node is to moderate the frequency of incoming messages.
160
160
  - `msg.mode = 'debounce'|'throttle'|'window'` – changes mode at runtime.
161
161
  - `msg.interval`, `msg.wait`, `msg.windowSize`, `msg.maxInWindow` – adjust the related thresholds.
162
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>
163
+ The **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>
@@ -103,7 +103,7 @@
103
103
  </div>
104
104
 
105
105
  <div class="form-row">
106
- <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
106
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
107
107
  <input type="text" id="node-input-payloadPropName">
108
108
  </div>
109
109
 
@@ -139,7 +139,7 @@
139
139
  | Warning offset (s) | Seconds before switch-off when the warning is sent. |
140
140
  | Restart on trigger | Restarts the timer every time a new trigger arrives while active. |
141
141
  | Allow off input | Allows a `false` from the main input to switch off immediately. |
142
- | With Input | Message property to evaluate as trigger (default `payload`). |
142
+ | Input | Message property to evaluate as trigger (default `payload`). |
143
143
  | Translator | Optional translator-config for true/false conversion. |
144
144
  | On/Off payload | Values emitted on output 1 to switch the light. |
145
145
  | Warning payload | Value emitted on output 2 when the warning fires. |
@@ -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.13",
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,