node-red-contrib-boolean-logic-ultimate 1.2.8 → 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.
@@ -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,
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { helper } = require('./helpers');
5
+
6
+ const hysteresisNode = require('../boolean-logic-ultimate/HysteresisUltimate.js');
7
+
8
+ function loadHysteresis(flow, credentials) {
9
+ const normalizedFlow = flow.map((node, index) => {
10
+ if (
11
+ node &&
12
+ node.type &&
13
+ node.type !== 'tab' &&
14
+ node.type !== 'subflow' &&
15
+ node.type !== 'group' &&
16
+ node.z &&
17
+ !(Object.prototype.hasOwnProperty.call(node, 'x') && Object.prototype.hasOwnProperty.call(node, 'y'))
18
+ ) {
19
+ return { ...node, x: 100 + index * 10, y: 100 + index * 10 };
20
+ }
21
+ return node;
22
+ });
23
+ return helper.load(hysteresisNode, normalizedFlow, credentials || {});
24
+ }
25
+
26
+ describe('HysteresisUltimate node', function () {
27
+ this.timeout(5000);
28
+
29
+ before(function (done) {
30
+ helper.startServer(done);
31
+ });
32
+
33
+ after(function (done) {
34
+ helper.stopServer(done);
35
+ });
36
+
37
+ afterEach(function () {
38
+ return helper.unload();
39
+ });
40
+
41
+ it('emits on state transitions only', function (done) {
42
+ const flowId = 'hys1';
43
+ const flow = [
44
+ { id: flowId, type: 'tab', label: 'hys1' },
45
+ {
46
+ id: 'hys',
47
+ type: 'HysteresisUltimate',
48
+ z: flowId,
49
+ mode: 'high',
50
+ onThreshold: 70,
51
+ offThreshold: 60,
52
+ emitOnlyOnChange: true,
53
+ onPayload: true,
54
+ onPayloadType: 'bool',
55
+ offPayload: false,
56
+ offPayloadType: 'bool',
57
+ wires: [['out'], ['diag']],
58
+ },
59
+ { id: 'in', type: 'helper', z: flowId, wires: [['hys']] },
60
+ { id: 'out', type: 'helper', z: flowId },
61
+ { id: 'diag', type: 'helper', z: flowId },
62
+ ];
63
+
64
+ loadHysteresis(flow).then(() => {
65
+ const hys = helper.getNode('hys');
66
+ const out = helper.getNode('out');
67
+ const results = [];
68
+
69
+ out.on('input', (msg) => {
70
+ results.push(msg.payload);
71
+ if (results.length === 2) {
72
+ try {
73
+ expect(results).to.deep.equal([true, false]);
74
+ done();
75
+ } catch (error) {
76
+ done(error);
77
+ }
78
+ }
79
+ });
80
+
81
+ hys.receive({ topic: 'sensor', payload: 65 });
82
+ hys.receive({ topic: 'sensor', payload: 72 });
83
+ hys.receive({ topic: 'sensor', payload: 68 });
84
+ hys.receive({ topic: 'sensor', payload: 58 });
85
+ }).catch(done);
86
+ });
87
+ });
@@ -1,286 +0,0 @@
1
- [
2
- {
3
- "id": "al_tab_1",
4
- "type": "tab",
5
- "label": "AlarmSystemUltimate - demo base",
6
- "disabled": false,
7
- "info": "Esempio per il nodo ALARM (Alarm System Ultimate - BETA): armo/disarmo, bypass zone, trigger sensori, eventi e sirena."
8
- },
9
- {
10
- "id": "al_cmt_1",
11
- "type": "comment",
12
- "z": "al_tab_1",
13
- "name": "Topic di controllo = alarm | Topic sirena = siren",
14
- "info": "Comandi principali (topic=alarm): arm_away/arm_home/arm_night, disarm, status, bypass/unbypass (con msg.zone). I sensori entrano con msg.topic uguale a quello configurato nelle zone e payload booleano true/false.",
15
- "x": 260,
16
- "y": 60,
17
- "wires": []
18
- },
19
- {
20
- "id": "al_inj_arm_away",
21
- "type": "inject",
22
- "z": "al_tab_1",
23
- "name": "ARM AWAY (code=1234)",
24
- "props": [
25
- { "p": "topic", "v": "alarm", "vt": "str" },
26
- { "p": "command", "v": "arm_away", "vt": "str" },
27
- { "p": "code", "v": "1234", "vt": "str" }
28
- ],
29
- "repeat": "",
30
- "crontab": "",
31
- "once": false,
32
- "onceDelay": 0.1,
33
- "x": 160,
34
- "y": 140,
35
- "wires": [["al_alarm_1"]]
36
- },
37
- {
38
- "id": "al_inj_arm_home",
39
- "type": "inject",
40
- "z": "al_tab_1",
41
- "name": "ARM HOME (code=1234)",
42
- "props": [
43
- { "p": "topic", "v": "alarm", "vt": "str" },
44
- { "p": "command", "v": "arm_home", "vt": "str" },
45
- { "p": "code", "v": "1234", "vt": "str" }
46
- ],
47
- "repeat": "",
48
- "crontab": "",
49
- "once": false,
50
- "onceDelay": 0.1,
51
- "x": 160,
52
- "y": 180,
53
- "wires": [["al_alarm_1"]]
54
- },
55
- {
56
- "id": "al_inj_disarm",
57
- "type": "inject",
58
- "z": "al_tab_1",
59
- "name": "DISARM (code=1234)",
60
- "props": [
61
- { "p": "topic", "v": "alarm", "vt": "str" },
62
- { "p": "command", "v": "disarm", "vt": "str" },
63
- { "p": "code", "v": "1234", "vt": "str" }
64
- ],
65
- "repeat": "",
66
- "crontab": "",
67
- "once": false,
68
- "onceDelay": 0.1,
69
- "x": 160,
70
- "y": 220,
71
- "wires": [["al_alarm_1"]]
72
- },
73
- {
74
- "id": "al_inj_status",
75
- "type": "inject",
76
- "z": "al_tab_1",
77
- "name": "STATUS",
78
- "props": [
79
- { "p": "topic", "v": "alarm", "vt": "str" },
80
- { "p": "command", "v": "status", "vt": "str" }
81
- ],
82
- "repeat": "",
83
- "crontab": "",
84
- "once": false,
85
- "onceDelay": 0.1,
86
- "x": 110,
87
- "y": 260,
88
- "wires": [["al_alarm_1"]]
89
- },
90
- {
91
- "id": "al_inj_bypass_front",
92
- "type": "inject",
93
- "z": "al_tab_1",
94
- "name": "BYPASS front_door",
95
- "props": [
96
- { "p": "topic", "v": "alarm", "vt": "str" },
97
- { "p": "command", "v": "bypass", "vt": "str" },
98
- { "p": "zone", "v": "front_door", "vt": "str" }
99
- ],
100
- "repeat": "",
101
- "crontab": "",
102
- "once": false,
103
- "onceDelay": 0.1,
104
- "x": 140,
105
- "y": 320,
106
- "wires": [["al_alarm_1"]]
107
- },
108
- {
109
- "id": "al_inj_unbypass_front",
110
- "type": "inject",
111
- "z": "al_tab_1",
112
- "name": "UNBYPASS front_door",
113
- "props": [
114
- { "p": "topic", "v": "alarm", "vt": "str" },
115
- { "p": "command", "v": "unbypass", "vt": "str" },
116
- { "p": "zone", "v": "front_door", "vt": "str" }
117
- ],
118
- "repeat": "",
119
- "crontab": "",
120
- "once": false,
121
- "onceDelay": 0.1,
122
- "x": 150,
123
- "y": 360,
124
- "wires": [["al_alarm_1"]]
125
- },
126
- {
127
- "id": "al_cmt_2",
128
- "type": "comment",
129
- "z": "al_tab_1",
130
- "name": "Sensori (topic = zona) | payload true = trigger | payload false = restore",
131
- "info": "La zona front_door è con entry delay e chime quando disarmata. pir_living è attiva solo in away. smoke è fire 24/7 (sempre attiva).",
132
- "x": 320,
133
- "y": 420,
134
- "wires": []
135
- },
136
- {
137
- "id": "al_inj_front_open",
138
- "type": "inject",
139
- "z": "al_tab_1",
140
- "name": "Front door OPEN (true)",
141
- "props": [
142
- { "p": "topic", "v": "sensor/frontdoor", "vt": "str" },
143
- { "p": "payload", "v": "true", "vt": "bool" }
144
- ],
145
- "repeat": "",
146
- "crontab": "",
147
- "once": false,
148
- "onceDelay": 0.1,
149
- "x": 170,
150
- "y": 500,
151
- "wires": [["al_alarm_1"]]
152
- },
153
- {
154
- "id": "al_inj_front_close",
155
- "type": "inject",
156
- "z": "al_tab_1",
157
- "name": "Front door CLOSE (false)",
158
- "props": [
159
- { "p": "topic", "v": "sensor/frontdoor", "vt": "str" },
160
- { "p": "payload", "v": "false", "vt": "bool" }
161
- ],
162
- "repeat": "",
163
- "crontab": "",
164
- "once": false,
165
- "onceDelay": 0.1,
166
- "x": 180,
167
- "y": 540,
168
- "wires": [["al_alarm_1"]]
169
- },
170
- {
171
- "id": "al_inj_pir",
172
- "type": "inject",
173
- "z": "al_tab_1",
174
- "name": "PIR living (true)",
175
- "props": [
176
- { "p": "topic", "v": "sensor/living_pir", "vt": "str" },
177
- { "p": "payload", "v": "true", "vt": "bool" }
178
- ],
179
- "repeat": "",
180
- "crontab": "",
181
- "once": false,
182
- "onceDelay": 0.1,
183
- "x": 150,
184
- "y": 600,
185
- "wires": [["al_alarm_1"]]
186
- },
187
- {
188
- "id": "al_inj_smoke",
189
- "type": "inject",
190
- "z": "al_tab_1",
191
- "name": "SMOKE (true)",
192
- "props": [
193
- { "p": "topic", "v": "sensor/smoke", "vt": "str" },
194
- { "p": "payload", "v": "true", "vt": "bool" }
195
- ],
196
- "repeat": "",
197
- "crontab": "",
198
- "once": false,
199
- "onceDelay": 0.1,
200
- "x": 130,
201
- "y": 640,
202
- "wires": [["al_alarm_1"]]
203
- },
204
- {
205
- "id": "al_alarm_1",
206
- "type": "AlarmSystemUltimate",
207
- "z": "al_tab_1",
208
- "name": "ALARM",
209
- "controlTopic": "alarm",
210
- "payloadPropName": "payload",
211
- "translatorConfig": "",
212
- "persistState": true,
213
- "requireCodeForArm": true,
214
- "requireCodeForDisarm": true,
215
- "armCode": "1234",
216
- "duressCode": "9999",
217
- "blockArmOnViolations": true,
218
- "exitDelaySeconds": 10,
219
- "entryDelaySeconds": 5,
220
- "sirenDurationSeconds": 15,
221
- "sirenLatchUntilDisarm": false,
222
- "sirenTopic": "siren",
223
- "sirenOnPayload": true,
224
- "sirenOnPayloadType": "bool",
225
- "sirenOffPayload": false,
226
- "sirenOffPayloadType": "bool",
227
- "emitRestoreEvents": true,
228
- "maxLogEntries": 50,
229
- "zones": "{\"id\":\"front_door\",\"name\":\"Porta ingresso\",\"topic\":\"sensor/frontdoor\",\"type\":\"perimeter\",\"entry\":true,\"modes\":[\"away\",\"home\",\"night\"],\"bypassable\":true,\"chime\":true}\n{\"id\":\"pir_living\",\"name\":\"PIR soggiorno\",\"topic\":\"sensor/living_pir\",\"type\":\"motion\",\"modes\":[\"away\"],\"entry\":false,\"bypassable\":true,\"cooldownSeconds\":5}\n{\"id\":\"smoke\",\"name\":\"Fumo\",\"topic\":\"sensor/smoke\",\"type\":\"fire\",\"modes\":[],\"entry\":false,\"bypassable\":false}",
230
- "x": 450,
231
- "y": 280,
232
- "wires": [["al_dbg_evt"], ["al_dbg_siren"]]
233
- },
234
- {
235
- "id": "al_dbg_evt",
236
- "type": "debug",
237
- "z": "al_tab_1",
238
- "name": "Events (output 1)",
239
- "active": true,
240
- "tosidebar": true,
241
- "console": false,
242
- "tostatus": false,
243
- "complete": "true",
244
- "targetType": "full",
245
- "statusVal": "",
246
- "statusType": "auto",
247
- "x": 680,
248
- "y": 260,
249
- "wires": []
250
- },
251
- {
252
- "id": "al_dbg_siren",
253
- "type": "debug",
254
- "z": "al_tab_1",
255
- "name": "Siren (output 2)",
256
- "active": true,
257
- "tosidebar": true,
258
- "console": false,
259
- "tostatus": false,
260
- "complete": "true",
261
- "targetType": "full",
262
- "statusVal": "",
263
- "statusType": "auto",
264
- "x": 680,
265
- "y": 300,
266
- "wires": []
267
- },
268
- {
269
- "id": "al_inj_disarm_duress",
270
- "type": "inject",
271
- "z": "al_tab_1",
272
- "name": "DISARM (DURESS 9999)",
273
- "props": [
274
- { "p": "topic", "v": "alarm", "vt": "str" },
275
- { "p": "command", "v": "disarm", "vt": "str" },
276
- { "p": "code", "v": "9999", "vt": "str" }
277
- ],
278
- "repeat": "",
279
- "crontab": "",
280
- "once": false,
281
- "onceDelay": 0.1,
282
- "x": 160,
283
- "y": 400,
284
- "wires": [["al_alarm_1"]]
285
- }
286
- ]