node-red-contrib-alarm-ultimate 0.1.0 → 0.1.2

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.
Files changed (35) hide show
  1. package/README.md +87 -13
  2. package/docs/images/alarm-panel-mock.svg +114 -0
  3. package/docs/images/banner.svg +63 -0
  4. package/docs/images/flow-overview.svg +85 -0
  5. package/examples/README.md +32 -11
  6. package/examples/alarm-ultimate-basic.json +0 -1
  7. package/examples/alarm-ultimate-dashboard-controls.json +575 -0
  8. package/examples/alarm-ultimate-dashboard-v2.json +762 -0
  9. package/examples/alarm-ultimate-dashboard.json +3 -3
  10. package/flowfuse-node-red-dashboard-1.30.2.tgz +0 -0
  11. package/nodes/AlarmSystemUltimate.html +174 -85
  12. package/nodes/AlarmSystemUltimate.js +39 -8
  13. package/nodes/AlarmUltimateInputAdapter.html +304 -0
  14. package/nodes/AlarmUltimateInputAdapter.js +188 -0
  15. package/nodes/AlarmUltimateSiren.html +3 -3
  16. package/nodes/AlarmUltimateSiren.js +6 -2
  17. package/nodes/AlarmUltimateState.html +3 -3
  18. package/nodes/AlarmUltimateState.js +6 -2
  19. package/nodes/AlarmUltimateZone.html +11 -6
  20. package/nodes/AlarmUltimateZone.js +27 -6
  21. package/nodes/icons/alarm-ultimate-siren.svg +6 -0
  22. package/nodes/icons/alarm-ultimate-state.svg +5 -0
  23. package/nodes/icons/alarm-ultimate-zone.svg +5 -0
  24. package/nodes/icons/alarm-ultimate.svg +6 -0
  25. package/nodes/presets/input-adapter/ax-pro-hikvision-ultimate.js +34 -0
  26. package/nodes/presets/input-adapter/boolean-from-payload.js +10 -0
  27. package/nodes/presets/input-adapter/ha-on-off.js +24 -0
  28. package/nodes/presets/input-adapter/knx-ultimate.js +29 -0
  29. package/nodes/presets/input-adapter/passthrough.js +7 -0
  30. package/package.json +5 -4
  31. package/test/alarm-system.spec.js +51 -0
  32. package/test/input-adapter.spec.js +243 -0
  33. package/test/output-nodes.spec.js +3 -0
  34. package/tools/alarm-json-mapper.html +1882 -460
  35. package/tools/alarm-panel.html +630 -131
@@ -18,10 +18,13 @@ module.exports = function (RED) {
18
18
 
19
19
  let lastOpen = null;
20
20
 
21
- function buildTopic(controlTopic) {
21
+ function buildTopic(controlTopic, zoneTopic) {
22
22
  if (configuredTopic) return configuredTopic;
23
23
  const base = typeof controlTopic === 'string' && controlTopic.trim().length > 0 ? controlTopic.trim() : 'alarm';
24
- const z = zoneId || 'zone';
24
+ const z =
25
+ typeof zoneTopic === 'string' && zoneTopic.trim().length > 0
26
+ ? zoneTopic.trim()
27
+ : zoneId || 'zone';
25
28
  return `${base}/zone/${z}`;
26
29
  }
27
30
 
@@ -30,8 +33,18 @@ module.exports = function (RED) {
30
33
  if (lastOpen === open && reason !== 'init') return;
31
34
  lastOpen = open;
32
35
 
36
+ const zoneTopic =
37
+ evt && evt.zone ? evt.zone.topic || evt.zone.topicPattern || null : null;
38
+ const statusLabel = zoneTopic || zoneId || 'zone';
39
+
40
+ setNodeStatus({
41
+ fill: open ? 'red' : 'green',
42
+ shape: 'dot',
43
+ text: `${statusLabel}: ${open ? 'open' : 'closed'}`,
44
+ });
45
+
33
46
  const msg = {
34
- topic: buildTopic(evt && evt.controlTopic),
47
+ topic: buildTopic(evt && evt.controlTopic, zoneTopic),
35
48
  payload: open,
36
49
  alarmId: evt ? evt.alarmId : alarmId,
37
50
  zone: evt && evt.zone ? evt.zone : { id: zoneId || null },
@@ -59,8 +72,17 @@ module.exports = function (RED) {
59
72
  setNodeStatus({ fill: 'red', shape: 'ring', text: `Unknown zone (${zoneId})` });
60
73
  return;
61
74
  }
62
- setNodeStatus({ fill: 'green', shape: 'dot', text: `Connected (${zoneId}: ${selected.open ? 'open' : 'closed'})` });
63
- emitZone(Boolean(selected.open), { alarmId, controlTopic: ui.controlTopic, zone: { id: selected.id, name: selected.name, type: selected.type } }, reason);
75
+ emitZone(Boolean(selected.open), {
76
+ alarmId,
77
+ controlTopic: ui.controlTopic,
78
+ zone: {
79
+ id: selected.id,
80
+ name: selected.name,
81
+ type: selected.type,
82
+ topic: selected.topic || null,
83
+ topicPattern: selected.topicPattern || null,
84
+ },
85
+ }, reason);
64
86
  }
65
87
 
66
88
  function onZoneState(evt) {
@@ -88,4 +110,3 @@ module.exports = function (RED) {
88
110
 
89
111
  RED.nodes.registerType('AlarmUltimateZone', AlarmUltimateZone);
90
112
  };
91
-
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true">
2
+ <path fill="#fff" d="M20 7c-6.1 0-11 4.9-11 11v3.1l-2.3 5c-.9 2 .5 4.3 2.7 4.3h21.2c2.2 0 3.6-2.3 2.7-4.3l-2.3-5V18c0-6.1-4.9-11-11-11z"/>
3
+ <path fill="#fff" opacity="0.55" d="M20 10.2c-4.3 0-7.8 3.5-7.8 7.8v4.1l-.7 1.5h17l-.7-1.5V18c0-4.3-3.5-7.8-7.8-7.8z"/>
4
+ <path fill="#fff" d="M14.8 31.2c.3 2.7 2.5 4.8 5.2 4.8s4.9-2.1 5.2-4.8H14.8z"/>
5
+ <path fill="#fff" opacity="0.75" d="M6.2 16.8c.6-4.2 3.4-7.7 7.3-9.3l.6 1.5c-3.3 1.4-5.7 4.3-6.2 7.9l-1.7-.1zm27.6 0-1.7.1c-.5-3.6-2.9-6.5-6.2-7.9l.6-1.5c3.9 1.6 6.7 5.1 7.3 9.3z"/>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true">
2
+ <path fill="#fff" d="M20 4 32 9v10c0 8.2-5.2 14.7-12 17-6.8-2.3-12-8.8-12-17V9l12-5z"/>
3
+ <path fill="#fff" opacity="0.55" d="M20 9.5 12.5 13v6.5c0 6 3.6 10.6 7.5 12.4 3.9-1.8 7.5-6.4 7.5-12.4V13L20 9.5z"/>
4
+ <path fill="#fff" d="M13.6 22.6h4.2l1.4-5.2 2.4 9 1.1-3.8h3.7v1.8h-2.3l-2.4 8.2-2.4-8.9-1.2 4.7h-5.2v-1.8z"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true">
2
+ <path fill="#fff" d="M20 6a14 14 0 1 1 0 28 14 14 0 0 1 0-28zm0 3.2a10.8 10.8 0 1 0 0 21.6 10.8 10.8 0 0 0 0-21.6z"/>
3
+ <path fill="#fff" d="M20 14a6 6 0 1 1 0 12 6 6 0 0 1 0-12zm0 3.2a2.8 2.8 0 1 0 0 5.6 2.8 2.8 0 0 0 0-5.6z"/>
4
+ <path fill="#fff" opacity="0.75" d="M20 10.2h1.6v3.2H20zM20 26.6h1.6v3.2H20zM26.6 19.2h3.2v1.6h-3.2zM10.2 19.2h3.2v1.6h-3.2z"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true">
2
+ <path fill="#fff" d="M20 4 32 9v10c0 8.2-5.2 14.7-12 17-6.8-2.3-12-8.8-12-17V9l12-5z"/>
3
+ <path fill="#fff" opacity="0.55" d="M20 9.5 12.5 13v6.5c0 6 3.6 10.6 7.5 12.4 3.9-1.8 7.5-6.4 7.5-12.4V13L20 9.5z"/>
4
+ <path fill="#fff" d="M20 13.2 15.2 17v7.6h9.6V17L20 13.2zm-3.2 11.2V18.1L20 15.6l3.2 2.5v6.3h-6.4z"/>
5
+ <path fill="#fff" d="M19.2 19.6h1.6v4.8h-1.6z"/>
6
+ </svg>
@@ -0,0 +1,34 @@
1
+ module.exports = {
2
+ id: "axpro_hikvision_ultimate",
3
+ name: "AX Pro (Hikvision Ultimate)",
4
+ description:
5
+ "Maps payload.zoneUpdate from Hikvision-Ultimate AX Pro nodes to {topic,payload} for Alarm zones.",
6
+ code: `
7
+ if (!msg || typeof msg !== "object") return;
8
+ const zone = msg.payload && msg.payload.zoneUpdate ? msg.payload.zoneUpdate : null;
9
+ if (!zone || typeof zone !== "object") return;
10
+
11
+ const rawTopic =
12
+ typeof zone.name === "string" && zone.name.trim()
13
+ ? zone.name.trim()
14
+ : zone.id !== undefined && zone.id !== null
15
+ ? String(zone.id)
16
+ : "";
17
+ if (!rawTopic) return;
18
+
19
+ let open;
20
+ if (typeof zone.magnetOpenStatus === "boolean") {
21
+ open = zone.magnetOpenStatus;
22
+ } else if (typeof zone.alarm === "boolean") {
23
+ open = zone.alarm;
24
+ } else if (typeof zone.sensorStatus === "string") {
25
+ const v = zone.sensorStatus.trim().toLowerCase();
26
+ open = v !== "normal" && v !== "closed" && v !== "ok";
27
+ } else {
28
+ return;
29
+ }
30
+
31
+ return { topic: rawTopic, payload: open, zoneUpdate: zone };
32
+ `.trim(),
33
+ };
34
+
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ id: "boolean_from_payload",
3
+ name: "Boolean from payload",
4
+ description: "Copies msg.topic and converts msg.payload to boolean.",
5
+ code: `
6
+ if (typeof msg !== "object" || msg === null) return;
7
+ return { topic: msg.topic, payload: !!msg.payload };
8
+ `.trim(),
9
+ };
10
+
@@ -0,0 +1,24 @@
1
+ module.exports = {
2
+ id: "home_assistant_on_off",
3
+ name: "Home Assistant on/off",
4
+ description: 'Converts msg.payload "on"/"off" (or boolean) to boolean payload.',
5
+ code: `
6
+ if (!msg || typeof msg !== "object") return;
7
+ const topic = msg.topic;
8
+ const value = msg.payload;
9
+ let b;
10
+ if (typeof value === "boolean") b = value;
11
+ else if (typeof value === "string") {
12
+ const v = value.trim().toLowerCase();
13
+ if (v === "on" || v === "open" || v === "true" || v === "1") b = true;
14
+ else if (v === "off" || v === "closed" || v === "false" || v === "0") b = false;
15
+ else return;
16
+ } else if (typeof value === "number") {
17
+ b = value !== 0;
18
+ } else {
19
+ return;
20
+ }
21
+ return { topic, payload: b };
22
+ `.trim(),
23
+ };
24
+
@@ -0,0 +1,29 @@
1
+ module.exports = {
2
+ id: "knx_ultimate",
3
+ name: "KNX Ultimate",
4
+ description: "Uses knx.destination (fallback msg.topic) and converts payload to boolean.",
5
+ code: `
6
+ if (!msg || typeof msg !== "object") return;
7
+ const topic =
8
+ msg.knx && typeof msg.knx.destination === "string" && msg.knx.destination.trim()
9
+ ? msg.knx.destination.trim()
10
+ : msg.topic;
11
+ if (typeof topic !== "string" || !topic.trim()) return;
12
+
13
+ const value = msg.payload;
14
+ let b;
15
+ if (typeof value === "boolean") b = value;
16
+ else if (typeof value === "number") b = value !== 0;
17
+ else if (typeof value === "string") {
18
+ const v = value.trim().toLowerCase();
19
+ if (v === "1" || v === "true" || v === "on" || v === "open") b = true;
20
+ else if (v === "0" || v === "false" || v === "off" || v === "closed") b = false;
21
+ else return;
22
+ } else {
23
+ return;
24
+ }
25
+
26
+ return { topic, payload: b };
27
+ `.trim(),
28
+ };
29
+
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ id: "passthrough",
3
+ name: "Passthrough",
4
+ description: "Emit the incoming msg unchanged.",
5
+ code: `return msg;`,
6
+ };
7
+
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "node-red-contrib-alarm-ultimate",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Alarm System node for Node-RED.",
5
- "author": "Supergiovane (https://github.com/Supergiovane)",
5
+ "author": "MAssimo Saccani (https://github.com/Supergiovane)",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "node-red",
@@ -11,11 +11,12 @@
11
11
  ],
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/Supergiovane/node-red-contrib-alarm-ultimate"
14
+ "url": "https://github.com/Supergiovane/node-red-contrib-alarm-ultimate.git"
15
15
  },
16
16
  "node-red": {
17
17
  "nodes": {
18
18
  "AlarmSystemUltimate": "nodes/AlarmSystemUltimate.js",
19
+ "AlarmUltimateInputAdapter": "nodes/AlarmUltimateInputAdapter.js",
19
20
  "AlarmUltimateState": "nodes/AlarmUltimateState.js",
20
21
  "AlarmUltimateZone": "nodes/AlarmUltimateZone.js",
21
22
  "AlarmUltimateSiren": "nodes/AlarmUltimateSiren.js"
@@ -30,4 +31,4 @@
30
31
  "scripts": {
31
32
  "test": "mocha test/**/*.spec.js"
32
33
  }
33
- }
34
+ }
@@ -467,4 +467,55 @@ describe('AlarmSystemUltimate node', function () {
467
467
  })
468
468
  .catch(done);
469
469
  });
470
+
471
+ it('emits zone open/close events while disarmed', function (done) {
472
+ const flowId = 'alarm-zone-events';
473
+ const flow = [
474
+ { id: flowId, type: 'tab', label: 'alarm-zone-events' },
475
+ {
476
+ id: 'alarm',
477
+ type: 'AlarmSystemUltimate',
478
+ z: flowId,
479
+ controlTopic: 'alarm',
480
+ requireCodeForDisarm: false,
481
+ zones: '{"id":"front","name":"Front","topic":"sensor/frontdoor","type":"perimeter","entry":false}',
482
+ wires: [[], [], [], [], ['zoneEvents']],
483
+ },
484
+ { id: 'zoneEvents', type: 'helper', z: flowId },
485
+ ];
486
+
487
+ loadAlarm(flow)
488
+ .then(() => {
489
+ const alarm = helper.getNode('alarm');
490
+ const zoneEvents = helper.getNode('zoneEvents');
491
+
492
+ const seen = [];
493
+ zoneEvents.on('input', (msg) => {
494
+ seen.push(msg);
495
+ });
496
+
497
+ // Default mode is disarmed. We should still see zone_open/zone_close.
498
+ alarm.receive({ topic: 'sensor/frontdoor', payload: true });
499
+ setTimeout(() => {
500
+ alarm.receive({ topic: 'sensor/frontdoor', payload: false });
501
+ }, 30);
502
+
503
+ setTimeout(() => {
504
+ try {
505
+ const events = seen.map((m) => m.event).filter(Boolean);
506
+ expect(events).to.include('zone_open');
507
+ expect(events).to.include('zone_close');
508
+ const openEvt = seen.find((m) => m && m.event === 'zone_open');
509
+ expect(openEvt).to.be.an('object');
510
+ expect(openEvt.payload).to.be.an('object');
511
+ expect(openEvt.payload.zone).to.be.an('object');
512
+ expect(openEvt.payload.zone.topic).to.equal('sensor/frontdoor');
513
+ done();
514
+ } catch (err) {
515
+ done(err);
516
+ }
517
+ }, 120);
518
+ })
519
+ .catch(done);
520
+ });
470
521
  });
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { helper } = require('./helpers');
5
+
6
+ const adapterNode = require('../nodes/AlarmUltimateInputAdapter.js');
7
+
8
+ function loadAdapter(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(adapterNode, normalizedFlow, credentials || {});
24
+ }
25
+
26
+ describe('AlarmUltimateInputAdapter 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('applies built-in preset (Home Assistant on/off)', function (done) {
42
+ const flowId = 'adapter1';
43
+ const flow = [
44
+ { id: flowId, type: 'tab', label: 'adapter1' },
45
+ {
46
+ id: 'adapter',
47
+ type: 'AlarmUltimateInputAdapter',
48
+ z: flowId,
49
+ presetSource: 'builtin',
50
+ presetId: 'home_assistant_on_off',
51
+ wires: [['out']],
52
+ },
53
+ { id: 'out', type: 'helper', z: flowId },
54
+ ];
55
+
56
+ loadAdapter(flow)
57
+ .then(() => {
58
+ const adapter = helper.getNode('adapter');
59
+ const out = helper.getNode('out');
60
+
61
+ out.on('input', (msg) => {
62
+ try {
63
+ expect(msg).to.be.an('object');
64
+ expect(msg.topic).to.equal('sensor/frontdoor');
65
+ expect(msg.payload).to.equal(true);
66
+ done();
67
+ } catch (err) {
68
+ done(err);
69
+ }
70
+ });
71
+
72
+ adapter.receive({ topic: 'sensor/frontdoor', payload: 'on' });
73
+ })
74
+ .catch(done);
75
+ });
76
+
77
+ it('applies built-in preset (KNX Ultimate)', function (done) {
78
+ const flowId = 'adapter3';
79
+ const flow = [
80
+ { id: flowId, type: 'tab', label: 'adapter3' },
81
+ {
82
+ id: 'adapter',
83
+ type: 'AlarmUltimateInputAdapter',
84
+ z: flowId,
85
+ presetSource: 'builtin',
86
+ presetId: 'knx_ultimate',
87
+ wires: [['out']],
88
+ },
89
+ { id: 'out', type: 'helper', z: flowId },
90
+ ];
91
+
92
+ loadAdapter(flow)
93
+ .then(() => {
94
+ const adapter = helper.getNode('adapter');
95
+ const out = helper.getNode('out');
96
+
97
+ out.on('input', (msg) => {
98
+ try {
99
+ expect(msg).to.be.an('object');
100
+ expect(msg.topic).to.equal('0/1/2');
101
+ expect(msg.payload).to.equal(false);
102
+ done();
103
+ } catch (err) {
104
+ done(err);
105
+ }
106
+ });
107
+
108
+ adapter.receive({
109
+ topic: '0/1/2',
110
+ payload: false,
111
+ previouspayload: true,
112
+ payloadmeasureunit: '%',
113
+ payloadsubtypevalue: 'Start',
114
+ devicename: 'Dinning table lamp',
115
+ gainfo: {
116
+ maingroupname: 'Light actuators',
117
+ middlegroupname: 'First flow lights',
118
+ ganame: 'Table Light',
119
+ maingroupnumber: '1',
120
+ middlegroupnumber: '1',
121
+ ganumber: '0',
122
+ },
123
+ knx: {
124
+ event: 'GroupValue_Write',
125
+ dpt: '1.001',
126
+ dptdesc: 'Humidity',
127
+ source: '15.15.22',
128
+ destination: '0/1/2',
129
+ rawValue: { 0: '0x0' },
130
+ },
131
+ });
132
+ })
133
+ .catch(done);
134
+ });
135
+
136
+ it('applies built-in preset (AX Pro from Hikvision-Ultimate)', function (done) {
137
+ const flowId = 'adapter4';
138
+ const flow = [
139
+ { id: flowId, type: 'tab', label: 'adapter4' },
140
+ {
141
+ id: 'adapter',
142
+ type: 'AlarmUltimateInputAdapter',
143
+ z: flowId,
144
+ presetSource: 'builtin',
145
+ presetId: 'axpro_hikvision_ultimate',
146
+ wires: [['out']],
147
+ },
148
+ { id: 'out', type: 'helper', z: flowId },
149
+ ];
150
+
151
+ loadAdapter(flow)
152
+ .then(() => {
153
+ const adapter = helper.getNode('adapter');
154
+ const out = helper.getNode('out');
155
+
156
+ out.on('input', (msg) => {
157
+ try {
158
+ expect(msg).to.be.an('object');
159
+ expect(msg.topic).to.equal('Cancello#2/7/12');
160
+ expect(msg.payload).to.equal(false);
161
+ expect(msg.zoneUpdate).to.be.an('object');
162
+ expect(msg.zoneUpdate.id).to.equal(9);
163
+ done();
164
+ } catch (err) {
165
+ done(err);
166
+ }
167
+ });
168
+
169
+ adapter.receive({
170
+ payload: {
171
+ zoneUpdate: {
172
+ id: 9,
173
+ name: 'Cancello#2/7/12',
174
+ status: 'online',
175
+ sensorStatus: 'normal',
176
+ magnetOpenStatus: false,
177
+ tamperEvident: false,
178
+ shielded: false,
179
+ bypassed: false,
180
+ armed: false,
181
+ isArming: false,
182
+ alarm: false,
183
+ subSystemNo: 5,
184
+ linkageSubSystem: [5],
185
+ detectorType: 'magneticContact',
186
+ stayAway: false,
187
+ zoneType: 'Instant',
188
+ accessModuleType: 'localTransmitter',
189
+ moduleChannel: 9,
190
+ zoneAttrib: 'wired',
191
+ deviceNo: 21,
192
+ abnormalOrNot: false,
193
+ },
194
+ },
195
+ _msgid: '5f34ea7333772aeb',
196
+ });
197
+ })
198
+ .catch(done);
199
+ });
200
+
201
+ it('applies user preset stored in node config', function (done) {
202
+ const flowId = 'adapter2';
203
+ const userCode = [
204
+ 'if (!msg || typeof msg !== "object") return;',
205
+ 'const topic = msg.payload && msg.payload.topic ? msg.payload.topic : msg.topic;',
206
+ 'const open = msg.payload && msg.payload.state === "open";',
207
+ 'return { topic, payload: open };',
208
+ ].join('\n');
209
+
210
+ const flow = [
211
+ { id: flowId, type: 'tab', label: 'adapter2' },
212
+ {
213
+ id: 'adapter',
214
+ type: 'AlarmUltimateInputAdapter',
215
+ z: flowId,
216
+ presetSource: 'user',
217
+ presetId: 'custom',
218
+ userCode,
219
+ wires: [['out']],
220
+ },
221
+ { id: 'out', type: 'helper', z: flowId },
222
+ ];
223
+
224
+ loadAdapter(flow)
225
+ .then(() => {
226
+ const adapter = helper.getNode('adapter');
227
+ const out = helper.getNode('out');
228
+
229
+ out.on('input', (msg) => {
230
+ try {
231
+ expect(msg.topic).to.equal('sensor/door');
232
+ expect(msg.payload).to.equal(true);
233
+ done();
234
+ } catch (err) {
235
+ done(err);
236
+ }
237
+ });
238
+
239
+ adapter.receive({ payload: { topic: 'sensor/door', state: 'open' } });
240
+ })
241
+ .catch(done);
242
+ });
243
+ });
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { expect } = require('chai');
3
4
  const { helper } = require('./helpers');
4
5
 
5
6
  const alarmNode = require('../nodes/AlarmSystemUltimate.js');
@@ -119,9 +120,11 @@ describe('Alarm Ultimate output-only nodes', function () {
119
120
  try {
120
121
  if (String(msg.reason || '').startsWith('init') && msg.payload === false) {
121
122
  seen.initialZoneClosed = true;
123
+ expect(msg.topic).to.equal('alarm/zone/sensor/frontdoor');
122
124
  }
123
125
  if (msg.payload === true && msg.zone && msg.zone.id === 'front') {
124
126
  seen.zoneOpen = true;
127
+ expect(msg.topic).to.equal('alarm/zone/sensor/frontdoor');
125
128
  }
126
129
  maybeDone();
127
130
  } catch (err) {