node-red-contrib-alarm-ultimate 0.1.1 → 0.1.3
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/README.md +14 -0
- package/examples/README.md +93 -3
- package/examples/alarm-ultimate-basic.json +0 -1
- package/examples/alarm-ultimate-dashboard-controls.json +34 -4
- package/examples/alarm-ultimate-dashboard-v2.json +834 -0
- package/examples/alarm-ultimate-dashboard.json +34 -5
- package/examples/alarm-ultimate-home-assistant-alarm-panel.json +335 -0
- package/flowfuse-node-red-dashboard-1.30.2.tgz +0 -0
- package/nodes/AlarmSystemUltimate.html +332 -105
- package/nodes/AlarmSystemUltimate.js +158 -12
- package/nodes/AlarmUltimateInputAdapter.html +304 -0
- package/nodes/AlarmUltimateInputAdapter.js +188 -0
- package/nodes/AlarmUltimateZone.html +2 -2
- package/nodes/AlarmUltimateZone.js +6 -3
- package/nodes/presets/input-adapter/ax-pro-hikvision-ultimate.js +34 -0
- package/nodes/presets/input-adapter/boolean-from-payload.js +10 -0
- package/nodes/presets/input-adapter/ha-on-off.js +24 -0
- package/nodes/presets/input-adapter/knx-ultimate.js +29 -0
- package/nodes/presets/input-adapter/passthrough.js +7 -0
- package/package.json +4 -3
- package/test/alarm-system.spec.js +112 -0
- package/test/input-adapter.spec.js +243 -0
- package/test/output-nodes.spec.js +3 -0
- package/tools/alarm-json-mapper.html +955 -167
- package/tools/alarm-panel.html +995 -139
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const vm = require("vm");
|
|
6
|
+
|
|
7
|
+
function safeReadDir(dirPath) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.readdirSync(dirPath);
|
|
10
|
+
} catch (_err) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadBuiltinPresets() {
|
|
16
|
+
const presetsDir = path.join(__dirname, "presets", "input-adapter");
|
|
17
|
+
const files = safeReadDir(presetsDir)
|
|
18
|
+
.filter((f) => f.endsWith(".js"))
|
|
19
|
+
.sort((a, b) => a.localeCompare(b));
|
|
20
|
+
|
|
21
|
+
const presets = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const fullPath = path.join(presetsDir, file);
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
26
|
+
const mod = require(fullPath);
|
|
27
|
+
const preset = mod && typeof mod === "object" ? mod : null;
|
|
28
|
+
if (!preset) continue;
|
|
29
|
+
if (typeof preset.id !== "string" || preset.id.trim().length === 0) continue;
|
|
30
|
+
if (typeof preset.name !== "string" || preset.name.trim().length === 0) continue;
|
|
31
|
+
if (typeof preset.code !== "string" || preset.code.trim().length === 0) continue;
|
|
32
|
+
presets.push({
|
|
33
|
+
id: preset.id.trim(),
|
|
34
|
+
name: preset.name.trim(),
|
|
35
|
+
description: typeof preset.description === "string" ? preset.description.trim() : "",
|
|
36
|
+
code: preset.code,
|
|
37
|
+
});
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
// ignore broken preset files
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return presets;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildContextApi(node) {
|
|
46
|
+
const nodeCtx = node.context();
|
|
47
|
+
const flowCtx = node.context().flow;
|
|
48
|
+
const globalCtx = node.context().global;
|
|
49
|
+
|
|
50
|
+
function wrap(ctx) {
|
|
51
|
+
return {
|
|
52
|
+
get: (key) => ctx.get(key),
|
|
53
|
+
set: (key, value) => ctx.set(key, value),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
node: wrap(nodeCtx),
|
|
59
|
+
flow: wrap(flowCtx),
|
|
60
|
+
global: wrap(globalCtx),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = function (RED) {
|
|
65
|
+
const builtins = loadBuiltinPresets();
|
|
66
|
+
const builtinById = new Map(builtins.map((p) => [p.id, p]));
|
|
67
|
+
|
|
68
|
+
if (RED && RED.httpAdmin && typeof RED.httpAdmin.get === "function") {
|
|
69
|
+
const needsRead =
|
|
70
|
+
RED.auth && typeof RED.auth.needsPermission === "function"
|
|
71
|
+
? RED.auth.needsPermission("AlarmUltimateInputAdapter.read")
|
|
72
|
+
: (req, res, next) => next();
|
|
73
|
+
|
|
74
|
+
RED.httpAdmin.get("/alarm-ultimate/input-adapter/presets", needsRead, (_req, res) => {
|
|
75
|
+
res.json({ presets: builtins });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function AlarmUltimateInputAdapter(config) {
|
|
80
|
+
RED.nodes.createNode(this, config);
|
|
81
|
+
const node = this;
|
|
82
|
+
const REDUtil = RED.util;
|
|
83
|
+
|
|
84
|
+
const presetSource = config.presetSource === "user" ? "user" : "builtin";
|
|
85
|
+
const presetId =
|
|
86
|
+
typeof config.presetId === "string" && config.presetId.trim().length > 0
|
|
87
|
+
? config.presetId.trim()
|
|
88
|
+
: "passthrough";
|
|
89
|
+
const userCode = typeof config.userCode === "string" ? config.userCode : "";
|
|
90
|
+
|
|
91
|
+
const preset =
|
|
92
|
+
presetSource === "user"
|
|
93
|
+
? {
|
|
94
|
+
id: "custom",
|
|
95
|
+
name: "Custom",
|
|
96
|
+
code: userCode,
|
|
97
|
+
}
|
|
98
|
+
: builtinById.get(presetId);
|
|
99
|
+
const sandbox = {
|
|
100
|
+
msg: null,
|
|
101
|
+
context: buildContextApi(node),
|
|
102
|
+
log: (...args) => node.log(args.map(String).join(" ")),
|
|
103
|
+
warn: (...args) => node.warn(args.map(String).join(" ")),
|
|
104
|
+
error: (...args) => node.error(args.map(String).join(" ")),
|
|
105
|
+
fn: null,
|
|
106
|
+
result: undefined,
|
|
107
|
+
};
|
|
108
|
+
const vmContext = vm.createContext(sandbox);
|
|
109
|
+
|
|
110
|
+
function compile(code) {
|
|
111
|
+
const body = String(code || "").trim();
|
|
112
|
+
if (!body) return null;
|
|
113
|
+
const fnScript = new vm.Script(
|
|
114
|
+
`fn = (function (msg, context, log, warn, error) { "use strict";\n${body}\n});`,
|
|
115
|
+
);
|
|
116
|
+
fnScript.runInContext(vmContext, { timeout: 250 });
|
|
117
|
+
const callScript = new vm.Script(
|
|
118
|
+
`result = fn(msg, context, log, warn, error);`,
|
|
119
|
+
);
|
|
120
|
+
return { callScript };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let compiled = null;
|
|
124
|
+
try {
|
|
125
|
+
if (!preset || typeof preset.code !== "string" || preset.code.trim().length === 0) {
|
|
126
|
+
node.status({
|
|
127
|
+
fill: "red",
|
|
128
|
+
shape: "ring",
|
|
129
|
+
text: presetSource === "user" ? "missing user code" : "preset not found",
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
compiled = compile(preset.code);
|
|
133
|
+
node.status({
|
|
134
|
+
fill: "green",
|
|
135
|
+
shape: "dot",
|
|
136
|
+
text: presetSource === "user" ? "preset: custom" : `preset: ${preset.name}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
node.status({ fill: "red", shape: "dot", text: "invalid preset" });
|
|
141
|
+
node.error(err);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
node.on("input", (msg, send, done) => {
|
|
145
|
+
const doSend = send || ((m) => node.send(m));
|
|
146
|
+
if (!compiled) {
|
|
147
|
+
if (done) done();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
sandbox.msg = msg ? REDUtil.cloneMessage(msg) : {};
|
|
153
|
+
sandbox.result = undefined;
|
|
154
|
+
compiled.callScript.runInContext(vmContext, { timeout: 100 });
|
|
155
|
+
const out = sandbox.result;
|
|
156
|
+
|
|
157
|
+
if (out === undefined || out === null) {
|
|
158
|
+
if (done) done();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(out)) {
|
|
163
|
+
out.filter(Boolean).forEach((m, idx) => {
|
|
164
|
+
if (idx === 0) doSend(m);
|
|
165
|
+
else doSend(REDUtil.cloneMessage(m));
|
|
166
|
+
});
|
|
167
|
+
if (done) done();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof out === "object") {
|
|
172
|
+
doSend(out);
|
|
173
|
+
if (done) done();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
doSend({ ...(msg || {}), payload: out });
|
|
178
|
+
if (done) done();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
node.status({ fill: "red", shape: "dot", text: "transform error" });
|
|
181
|
+
node.error(err, msg);
|
|
182
|
+
if (done) done(err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
RED.nodes.registerType("AlarmUltimateInputAdapter", AlarmUltimateInputAdapter);
|
|
188
|
+
};
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
|
|
113
113
|
<div class="form-row">
|
|
114
114
|
<label for="node-input-topic"><i class="fa fa-tag"></i> Topic</label>
|
|
115
|
-
<input type="text" id="node-input-topic" placeholder="(default: <controlTopic>/zone/<
|
|
115
|
+
<input type="text" id="node-input-topic" placeholder="(default: <controlTopic>/zone/<zoneTopic>)" />
|
|
116
116
|
</div>
|
|
117
117
|
|
|
118
118
|
<div class="form-row">
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
Emits the state of a single configured zone from a selected **Alarm System Ultimate** node.
|
|
126
126
|
|
|
127
127
|
- `msg.payload`: `true` when the zone is open/active, otherwise `false`
|
|
128
|
-
- `msg.zone`: `{ id, name?, type? }`
|
|
128
|
+
- `msg.zone`: `{ id, name?, type?, topic? }`
|
|
129
129
|
- `msg.bypassed`: `true|false`
|
|
130
130
|
|
|
131
131
|
Notes:
|
|
@@ -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 =
|
|
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
|
|
|
@@ -41,7 +44,7 @@ module.exports = function (RED) {
|
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
const msg = {
|
|
44
|
-
topic: buildTopic(evt && evt.controlTopic),
|
|
47
|
+
topic: buildTopic(evt && evt.controlTopic, zoneTopic),
|
|
45
48
|
payload: open,
|
|
46
49
|
alarmId: evt ? evt.alarmId : alarmId,
|
|
47
50
|
zone: evt && evt.zone ? evt.zone : { id: zoneId || null },
|
|
@@ -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
|
+
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-alarm-ultimate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Alarm System node for Node-RED.",
|
|
5
|
-
"author": "
|
|
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"
|
|
@@ -467,4 +467,116 @@ 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
|
+
});
|
|
521
|
+
|
|
522
|
+
it('syncs arm/disarm to other Alarm nodes', function (done) {
|
|
523
|
+
const flowId = 'alarm-sync';
|
|
524
|
+
const flow = [
|
|
525
|
+
{ id: flowId, type: 'tab', label: 'alarm-sync' },
|
|
526
|
+
{
|
|
527
|
+
id: 'alarmA',
|
|
528
|
+
type: 'AlarmSystemUltimate',
|
|
529
|
+
z: flowId,
|
|
530
|
+
name: 'Alarm A',
|
|
531
|
+
controlTopic: 'alarmA',
|
|
532
|
+
exitDelaySeconds: 0,
|
|
533
|
+
requireCodeForDisarm: false,
|
|
534
|
+
syncTargets: JSON.stringify({
|
|
535
|
+
alarmB: { onArm: 'arm', onDisarm: 'disarm' },
|
|
536
|
+
}),
|
|
537
|
+
wires: [['aEvents']],
|
|
538
|
+
},
|
|
539
|
+
{ id: 'aEvents', type: 'helper', z: flowId },
|
|
540
|
+
{
|
|
541
|
+
id: 'alarmB',
|
|
542
|
+
type: 'AlarmSystemUltimate',
|
|
543
|
+
z: flowId,
|
|
544
|
+
name: 'Alarm B',
|
|
545
|
+
controlTopic: 'alarmB',
|
|
546
|
+
exitDelaySeconds: 0,
|
|
547
|
+
requireCodeForDisarm: false,
|
|
548
|
+
wires: [['bEvents']],
|
|
549
|
+
},
|
|
550
|
+
{ id: 'bEvents', type: 'helper', z: flowId },
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
loadAlarm(flow)
|
|
554
|
+
.then(() => {
|
|
555
|
+
const alarmA = helper.getNode('alarmA');
|
|
556
|
+
const bEvents = helper.getNode('bEvents');
|
|
557
|
+
|
|
558
|
+
const seenB = [];
|
|
559
|
+
bEvents.on('input', (msg) => {
|
|
560
|
+
if (msg && typeof msg.event === 'string') {
|
|
561
|
+
seenB.push(msg.event);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
alarmA.receive({ topic: 'alarmA', command: 'arm' });
|
|
566
|
+
setTimeout(() => {
|
|
567
|
+
alarmA.receive({ topic: 'alarmA', command: 'disarm' });
|
|
568
|
+
}, 60);
|
|
569
|
+
|
|
570
|
+
setTimeout(() => {
|
|
571
|
+
try {
|
|
572
|
+
expect(seenB).to.include('armed');
|
|
573
|
+
expect(seenB).to.include('disarmed');
|
|
574
|
+
done();
|
|
575
|
+
} catch (err) {
|
|
576
|
+
done(err);
|
|
577
|
+
}
|
|
578
|
+
}, 250);
|
|
579
|
+
})
|
|
580
|
+
.catch(done);
|
|
581
|
+
});
|
|
470
582
|
});
|