node-red-contrib-alarm-ultimate 0.1.0
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/LICENSE +22 -0
- package/README.md +73 -0
- package/examples/README.md +21 -0
- package/examples/alarm-ultimate-basic.json +636 -0
- package/examples/alarm-ultimate-dashboard.json +99 -0
- package/nodes/AlarmSystemUltimate.html +697 -0
- package/nodes/AlarmSystemUltimate.js +1418 -0
- package/nodes/AlarmUltimateSiren.html +94 -0
- package/nodes/AlarmUltimateSiren.js +83 -0
- package/nodes/AlarmUltimateState.html +95 -0
- package/nodes/AlarmUltimateState.js +87 -0
- package/nodes/AlarmUltimateZone.html +130 -0
- package/nodes/AlarmUltimateZone.js +91 -0
- package/nodes/lib/alarm-registry.js +15 -0
- package/nodes/lib/node-helpers.js +96 -0
- package/nodes/utils.js +95 -0
- package/package.json +33 -0
- package/test/alarm-system.spec.js +470 -0
- package/test/helpers.js +28 -0
- package/test/output-nodes.spec.js +155 -0
- package/tools/alarm-json-mapper.html +596 -0
- package/tools/alarm-panel.html +728 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils.js');
|
|
4
|
+
|
|
5
|
+
function createStatus(node) {
|
|
6
|
+
return function setNodeStatus({ fill, shape, text }) {
|
|
7
|
+
const dDate = new Date();
|
|
8
|
+
node.status({
|
|
9
|
+
fill,
|
|
10
|
+
shape,
|
|
11
|
+
text: `${text} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveInput(msg, propertyPath, translatorConfigId, RED) {
|
|
17
|
+
const propName = propertyPath || 'payload';
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = utils.fetchFromObject(msg, propName);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return { value: undefined, boolean: undefined };
|
|
23
|
+
}
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
return { value: undefined, boolean: undefined };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const translatorConfig = translatorConfigId
|
|
29
|
+
? RED.nodes.getNode(translatorConfigId)
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
value,
|
|
34
|
+
boolean: utils.ToBoolean(value, translatorConfig),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTimerBag(node) {
|
|
39
|
+
const timers = new Set();
|
|
40
|
+
const intervals = new Set();
|
|
41
|
+
|
|
42
|
+
function trackTimeout(handle) {
|
|
43
|
+
timers.add(handle);
|
|
44
|
+
return handle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function trackInterval(handle) {
|
|
48
|
+
intervals.add(handle);
|
|
49
|
+
return handle;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clearAll() {
|
|
53
|
+
for (const handle of timers) {
|
|
54
|
+
clearTimeout(handle);
|
|
55
|
+
}
|
|
56
|
+
timers.clear();
|
|
57
|
+
for (const handle of intervals) {
|
|
58
|
+
clearInterval(handle);
|
|
59
|
+
}
|
|
60
|
+
intervals.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
node.on('close', (_removed, done) => {
|
|
64
|
+
clearAll();
|
|
65
|
+
done();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
setTimeout(fn, timeout) {
|
|
70
|
+
const handle = setTimeout(() => {
|
|
71
|
+
timers.delete(handle);
|
|
72
|
+
fn();
|
|
73
|
+
}, timeout);
|
|
74
|
+
return trackTimeout(handle);
|
|
75
|
+
},
|
|
76
|
+
setInterval(fn, interval) {
|
|
77
|
+
const handle = setInterval(fn, interval);
|
|
78
|
+
return trackInterval(handle);
|
|
79
|
+
},
|
|
80
|
+
clearTimeout(handle) {
|
|
81
|
+
clearTimeout(handle);
|
|
82
|
+
timers.delete(handle);
|
|
83
|
+
},
|
|
84
|
+
clearInterval(handle) {
|
|
85
|
+
clearInterval(handle);
|
|
86
|
+
intervals.delete(handle);
|
|
87
|
+
},
|
|
88
|
+
clearAll,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
createStatus,
|
|
94
|
+
resolveInput,
|
|
95
|
+
createTimerBag,
|
|
96
|
+
};
|
package/nodes/utils.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module.exports.ToBoolean = function ToBoolean(value, _configTranslationNode) {
|
|
2
|
+
let res = false;
|
|
3
|
+
let decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/;
|
|
4
|
+
|
|
5
|
+
if (typeof value === "boolean") {
|
|
6
|
+
return value;
|
|
7
|
+
} else if (typeof value === "string" || typeof value === "number") {
|
|
8
|
+
if (typeof value === "number") value = value.toString(); // We work with strings
|
|
9
|
+
try {
|
|
10
|
+
let translationTable = [];
|
|
11
|
+
value = value.toLowerCase();
|
|
12
|
+
if (_configTranslationNode === null) {
|
|
13
|
+
translationTable = DEFAULTTRANSLATIONINPUT.split("\n");
|
|
14
|
+
} else {
|
|
15
|
+
translationTable = _configTranslationNode.commandText.split("\n");
|
|
16
|
+
}
|
|
17
|
+
for (let index = 0; index < translationTable.length; index++) {
|
|
18
|
+
// HA Style evaluation in the format "{{value>=0}}"
|
|
19
|
+
let inputPayloadToBeTranslated = translationTable[index].toLowerCase().split(":")[0];
|
|
20
|
+
if (inputPayloadToBeTranslated.indexOf("{{") > -1 && inputPayloadToBeTranslated.indexOf("}}") > -1) {
|
|
21
|
+
// Eval content of the brackets {{value<=0}}, HA style
|
|
22
|
+
inputPayloadToBeTranslated = inputPayloadToBeTranslated.replace("{{", "").replace("}}", "").replace("value", value); // Set the word value to real value
|
|
23
|
+
if (eval(inputPayloadToBeTranslated)) {
|
|
24
|
+
return translationTable[index].split(":")[1] === "true"
|
|
25
|
+
? true
|
|
26
|
+
: false;
|
|
27
|
+
} // Eval the operation
|
|
28
|
+
} else if (
|
|
29
|
+
// Normal string value
|
|
30
|
+
value === inputPayloadToBeTranslated
|
|
31
|
+
) {
|
|
32
|
+
return translationTable[index].split(":")[1] === "true"
|
|
33
|
+
? true
|
|
34
|
+
: false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log("Alarm-Ultimate:utils:toBoolean: " + error.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// else if (typeof value === "number") {
|
|
42
|
+
// // Is it formated as a decimal number?
|
|
43
|
+
// if (decimal.test(value)) {
|
|
44
|
+
// res = parseFloat(value) != 0;
|
|
45
|
+
// } else {
|
|
46
|
+
// res = value.toLowerCase() === "true";
|
|
47
|
+
// }
|
|
48
|
+
// return res;
|
|
49
|
+
// }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports.ToAny = function ToAny(value, _configTranslationNode) {
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
let translationTable = [];
|
|
56
|
+
if (_configTranslationNode === null) {
|
|
57
|
+
// Don't do translation, because the default translation input may contin unwanted translations
|
|
58
|
+
return value;
|
|
59
|
+
} else {
|
|
60
|
+
translationTable = _configTranslationNode.commandText.split("\n");
|
|
61
|
+
}
|
|
62
|
+
for (let index = 0; index < translationTable.length; index++) {
|
|
63
|
+
let inputPayloadToBeTranslated = translationTable[index].split(":")[0];
|
|
64
|
+
//let outputBoolean = Boolean(translationTable[index].split(":")[1]);
|
|
65
|
+
if (
|
|
66
|
+
String(value).toLowerCase() === inputPayloadToBeTranslated.toLowerCase() &&
|
|
67
|
+
inputPayloadToBeTranslated.toLowerCase() !== ""
|
|
68
|
+
) {
|
|
69
|
+
return translationTable[index].split(":")[1];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.log("Alarm-Ultimate:utils:toAny: " + error.message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports.fetchFromObject = function fetchFromObject(
|
|
80
|
+
_msg,
|
|
81
|
+
_payloadPropName
|
|
82
|
+
) {
|
|
83
|
+
// The output cannot be an oblect. In case, return undefined.
|
|
84
|
+
var _index = _payloadPropName.indexOf(".");
|
|
85
|
+
if (_index > -1) {
|
|
86
|
+
return fetchFromObject(
|
|
87
|
+
_msg[_payloadPropName.substring(0, _index)],
|
|
88
|
+
_payloadPropName.substr(_index + 1)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (typeof _msg[_payloadPropName] === "object") return undefined;
|
|
92
|
+
return _msg[_payloadPropName];
|
|
93
|
+
};
|
|
94
|
+
const DEFAULTTRANSLATIONINPUT =
|
|
95
|
+
"on:true\noff:false\nactive:true\ninactive:false\nopen:true\nclosed:false\nclose:false\n1:true\n0:false\ntrue:true\nfalse:false\nhome:true\nnot_home:false\nnormal:false\nviolated:true";
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-alarm-ultimate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Alarm System node for Node-RED.",
|
|
5
|
+
"author": "Supergiovane (https://github.com/Supergiovane)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"node-red",
|
|
9
|
+
"alarm",
|
|
10
|
+
"security"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/Supergiovane/node-red-contrib-alarm-ultimate"
|
|
15
|
+
},
|
|
16
|
+
"node-red": {
|
|
17
|
+
"nodes": {
|
|
18
|
+
"AlarmSystemUltimate": "nodes/AlarmSystemUltimate.js",
|
|
19
|
+
"AlarmUltimateState": "nodes/AlarmUltimateState.js",
|
|
20
|
+
"AlarmUltimateZone": "nodes/AlarmUltimateZone.js",
|
|
21
|
+
"AlarmUltimateSiren": "nodes/AlarmUltimateSiren.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"chai": "^4.3.10",
|
|
26
|
+
"mocha": "^10.4.0",
|
|
27
|
+
"node-red": "^3.1.0",
|
|
28
|
+
"node-red-node-test-helper": "^0.3.5"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "mocha test/**/*.spec.js"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const { helper } = require('./helpers');
|
|
5
|
+
|
|
6
|
+
const alarmNode = require('../nodes/AlarmSystemUltimate.js');
|
|
7
|
+
|
|
8
|
+
const ALARM_OUTPUT_COUNT = 9;
|
|
9
|
+
|
|
10
|
+
function loadAlarm(flow, credentials) {
|
|
11
|
+
const normalizedFlow = flow.map((node, index) => {
|
|
12
|
+
if (
|
|
13
|
+
node &&
|
|
14
|
+
node.type &&
|
|
15
|
+
node.type !== 'tab' &&
|
|
16
|
+
node.type !== 'subflow' &&
|
|
17
|
+
node.type !== 'group' &&
|
|
18
|
+
node.z &&
|
|
19
|
+
!(Object.prototype.hasOwnProperty.call(node, 'x') && Object.prototype.hasOwnProperty.call(node, 'y'))
|
|
20
|
+
) {
|
|
21
|
+
const adjusted = { ...node, x: 100 + index * 10, y: 100 + index * 10 };
|
|
22
|
+
if (adjusted.type === 'AlarmSystemUltimate' && Array.isArray(adjusted.wires)) {
|
|
23
|
+
const nextWires = adjusted.wires.map((wire) => (Array.isArray(wire) ? wire : []));
|
|
24
|
+
while (nextWires.length < ALARM_OUTPUT_COUNT) {
|
|
25
|
+
nextWires.push([]);
|
|
26
|
+
}
|
|
27
|
+
adjusted.wires = nextWires;
|
|
28
|
+
}
|
|
29
|
+
return adjusted;
|
|
30
|
+
}
|
|
31
|
+
return node;
|
|
32
|
+
});
|
|
33
|
+
return helper.load(alarmNode, normalizedFlow, credentials || {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('AlarmSystemUltimate node', function () {
|
|
37
|
+
this.timeout(5000);
|
|
38
|
+
|
|
39
|
+
before(function (done) {
|
|
40
|
+
helper.startServer(done);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
after(function (done) {
|
|
44
|
+
helper.stopServer(done);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(function () {
|
|
48
|
+
return helper.unload();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('triggers alarm after entry delay', function (done) {
|
|
52
|
+
const flowId = 'alarm1';
|
|
53
|
+
const flow = [
|
|
54
|
+
{ id: flowId, type: 'tab', label: 'alarm1' },
|
|
55
|
+
{
|
|
56
|
+
id: 'alarm',
|
|
57
|
+
type: 'AlarmSystemUltimate',
|
|
58
|
+
z: flowId,
|
|
59
|
+
controlTopic: 'alarm',
|
|
60
|
+
exitDelaySeconds: 0.05,
|
|
61
|
+
entryDelaySeconds: 0.05,
|
|
62
|
+
sirenDurationSeconds: 0.05,
|
|
63
|
+
sirenLatchUntilDisarm: false,
|
|
64
|
+
requireCodeForDisarm: false,
|
|
65
|
+
blockArmOnViolations: true,
|
|
66
|
+
zones: '{"id":"front","name":"Front","topic":"sensor/frontdoor","type":"perimeter","entry":true}',
|
|
67
|
+
wires: [['events'], ['siren']],
|
|
68
|
+
},
|
|
69
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
70
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
loadAlarm(flow).then(() => {
|
|
74
|
+
const alarm = helper.getNode('alarm');
|
|
75
|
+
const events = helper.getNode('events');
|
|
76
|
+
const siren = helper.getNode('siren');
|
|
77
|
+
|
|
78
|
+
const received = { entry: false, alarm: false, sirenOn: false };
|
|
79
|
+
|
|
80
|
+
function maybeDone() {
|
|
81
|
+
if (received.entry && received.alarm && received.sirenOn) {
|
|
82
|
+
done();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
events.on('input', (msg) => {
|
|
87
|
+
try {
|
|
88
|
+
if (msg.event === 'entry_delay') {
|
|
89
|
+
received.entry = true;
|
|
90
|
+
}
|
|
91
|
+
if (msg.event === 'alarm') {
|
|
92
|
+
received.alarm = true;
|
|
93
|
+
}
|
|
94
|
+
maybeDone();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
done(err);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
siren.on('input', (msg) => {
|
|
101
|
+
try {
|
|
102
|
+
if (msg.event === 'siren_on') {
|
|
103
|
+
received.sirenOn = true;
|
|
104
|
+
}
|
|
105
|
+
maybeDone();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
done(err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
alarm.receive({ topic: 'alarm', command: 'arm' });
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: 'open' });
|
|
114
|
+
}, 80);
|
|
115
|
+
}).catch(done);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('disarms during entry delay and prevents alarm', function (done) {
|
|
119
|
+
const flowId = 'alarm2';
|
|
120
|
+
const flow = [
|
|
121
|
+
{ id: flowId, type: 'tab', label: 'alarm2' },
|
|
122
|
+
{
|
|
123
|
+
id: 'alarm',
|
|
124
|
+
type: 'AlarmSystemUltimate',
|
|
125
|
+
z: flowId,
|
|
126
|
+
controlTopic: 'alarm',
|
|
127
|
+
exitDelaySeconds: 0.05,
|
|
128
|
+
entryDelaySeconds: 0.1,
|
|
129
|
+
sirenDurationSeconds: 0.2,
|
|
130
|
+
requireCodeForDisarm: false,
|
|
131
|
+
zones: '{"id":"front","topic":"sensor/frontdoor","type":"perimeter","entry":true}',
|
|
132
|
+
wires: [['events'], ['siren']],
|
|
133
|
+
},
|
|
134
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
135
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
loadAlarm(flow).then(() => {
|
|
139
|
+
const alarm = helper.getNode('alarm');
|
|
140
|
+
const events = helper.getNode('events');
|
|
141
|
+
const siren = helper.getNode('siren');
|
|
142
|
+
|
|
143
|
+
const seenEvents = [];
|
|
144
|
+
const sirenOn = [];
|
|
145
|
+
|
|
146
|
+
events.on('input', (msg) => {
|
|
147
|
+
seenEvents.push(msg.event);
|
|
148
|
+
if (msg.event === 'entry_delay') {
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
alarm.receive({ topic: 'alarm', command: 'disarm' });
|
|
151
|
+
}, 20);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
siren.on('input', (msg) => {
|
|
156
|
+
if (msg.event === 'siren_on') {
|
|
157
|
+
sirenOn.push(msg);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
alarm.receive({ topic: 'alarm', command: 'arm' });
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: 'open' });
|
|
164
|
+
}, 80);
|
|
165
|
+
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
try {
|
|
168
|
+
expect(seenEvents).to.include('entry_delay');
|
|
169
|
+
expect(seenEvents).to.include('disarmed');
|
|
170
|
+
expect(seenEvents).to.not.include('alarm');
|
|
171
|
+
expect(sirenOn.length).to.equal(0);
|
|
172
|
+
done();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
done(err);
|
|
175
|
+
}
|
|
176
|
+
}, 300);
|
|
177
|
+
}).catch(done);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('bypasses a zone and ignores its trigger', function (done) {
|
|
181
|
+
const flowId = 'alarm3';
|
|
182
|
+
const flow = [
|
|
183
|
+
{ id: flowId, type: 'tab', label: 'alarm3' },
|
|
184
|
+
{
|
|
185
|
+
id: 'alarm',
|
|
186
|
+
type: 'AlarmSystemUltimate',
|
|
187
|
+
z: flowId,
|
|
188
|
+
controlTopic: 'alarm',
|
|
189
|
+
exitDelaySeconds: 0.05,
|
|
190
|
+
entryDelaySeconds: 0.05,
|
|
191
|
+
sirenDurationSeconds: 0.2,
|
|
192
|
+
requireCodeForDisarm: false,
|
|
193
|
+
zones: '{"id":"front","topic":"sensor/frontdoor","type":"perimeter","entry":false,"bypassable":true}',
|
|
194
|
+
wires: [['events'], ['siren']],
|
|
195
|
+
},
|
|
196
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
197
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
loadAlarm(flow).then(() => {
|
|
201
|
+
const alarm = helper.getNode('alarm');
|
|
202
|
+
const events = helper.getNode('events');
|
|
203
|
+
const siren = helper.getNode('siren');
|
|
204
|
+
|
|
205
|
+
const seenEvents = [];
|
|
206
|
+
let sirenOn = false;
|
|
207
|
+
|
|
208
|
+
events.on('input', (msg) => {
|
|
209
|
+
seenEvents.push(msg.event);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
siren.on('input', (msg) => {
|
|
213
|
+
if (msg.event === 'siren_on') {
|
|
214
|
+
sirenOn = true;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
alarm.receive({ topic: 'alarm', command: 'bypass', zone: 'front' });
|
|
219
|
+
alarm.receive({ topic: 'alarm', command: 'arm' });
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: 'open' });
|
|
222
|
+
}, 80);
|
|
223
|
+
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
try {
|
|
226
|
+
expect(seenEvents).to.include('bypassed');
|
|
227
|
+
expect(seenEvents).to.not.include('alarm');
|
|
228
|
+
expect(sirenOn).to.equal(false);
|
|
229
|
+
done();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
done(err);
|
|
232
|
+
}
|
|
233
|
+
}, 250);
|
|
234
|
+
}).catch(done);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('accepts zones as a JSON array (formatted)', function (done) {
|
|
238
|
+
const flowId = 'alarm4';
|
|
239
|
+
const flow = [
|
|
240
|
+
{ id: flowId, type: 'tab', label: 'alarm4' },
|
|
241
|
+
{
|
|
242
|
+
id: 'alarm',
|
|
243
|
+
type: 'AlarmSystemUltimate',
|
|
244
|
+
z: flowId,
|
|
245
|
+
controlTopic: 'alarm',
|
|
246
|
+
exitDelaySeconds: 0.05,
|
|
247
|
+
entryDelaySeconds: 0.05,
|
|
248
|
+
sirenDurationSeconds: 0.05,
|
|
249
|
+
sirenLatchUntilDisarm: false,
|
|
250
|
+
requireCodeForDisarm: false,
|
|
251
|
+
blockArmOnViolations: true,
|
|
252
|
+
zones: JSON.stringify(
|
|
253
|
+
[
|
|
254
|
+
{
|
|
255
|
+
id: 'front',
|
|
256
|
+
name: 'Front',
|
|
257
|
+
topic: 'sensor/frontdoor',
|
|
258
|
+
type: 'perimeter',
|
|
259
|
+
entry: true,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
null,
|
|
263
|
+
2
|
|
264
|
+
),
|
|
265
|
+
wires: [['events'], ['siren']],
|
|
266
|
+
},
|
|
267
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
268
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
loadAlarm(flow).then(() => {
|
|
272
|
+
const alarm = helper.getNode('alarm');
|
|
273
|
+
const events = helper.getNode('events');
|
|
274
|
+
const siren = helper.getNode('siren');
|
|
275
|
+
|
|
276
|
+
const received = { entry: false, alarm: false, sirenOn: false };
|
|
277
|
+
|
|
278
|
+
function maybeDone() {
|
|
279
|
+
if (received.entry && received.alarm && received.sirenOn) {
|
|
280
|
+
done();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
events.on('input', (msg) => {
|
|
285
|
+
try {
|
|
286
|
+
if (msg.event === 'entry_delay') {
|
|
287
|
+
received.entry = true;
|
|
288
|
+
}
|
|
289
|
+
if (msg.event === 'alarm') {
|
|
290
|
+
received.alarm = true;
|
|
291
|
+
}
|
|
292
|
+
maybeDone();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
done(err);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
siren.on('input', (msg) => {
|
|
299
|
+
try {
|
|
300
|
+
if (msg.event === 'siren_on') {
|
|
301
|
+
received.sirenOn = true;
|
|
302
|
+
}
|
|
303
|
+
maybeDone();
|
|
304
|
+
} catch (err) {
|
|
305
|
+
done(err);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
alarm.receive({ topic: 'alarm', command: 'arm' });
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: 'open' });
|
|
312
|
+
}, 80);
|
|
313
|
+
}).catch(done);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('routes alarm events to the "Alarm Triggered" output', function (done) {
|
|
317
|
+
const flowId = 'alarm5';
|
|
318
|
+
const flow = [
|
|
319
|
+
{ id: flowId, type: 'tab', label: 'alarm5' },
|
|
320
|
+
{
|
|
321
|
+
id: 'alarm',
|
|
322
|
+
type: 'AlarmSystemUltimate',
|
|
323
|
+
z: flowId,
|
|
324
|
+
controlTopic: 'alarm',
|
|
325
|
+
exitDelaySeconds: 0,
|
|
326
|
+
entryDelaySeconds: 0,
|
|
327
|
+
sirenDurationSeconds: 0,
|
|
328
|
+
requireCodeForDisarm: false,
|
|
329
|
+
zones: '{"id":"front","topic":"sensor/frontdoor","type":"perimeter","entry":false}',
|
|
330
|
+
wires: [['events'], ['siren'], ['alarmTriggered']],
|
|
331
|
+
},
|
|
332
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
333
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
334
|
+
{ id: 'alarmTriggered', type: 'helper', z: flowId },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
loadAlarm(flow)
|
|
338
|
+
.then(() => {
|
|
339
|
+
const alarm = helper.getNode('alarm');
|
|
340
|
+
const alarmTriggered = helper.getNode('alarmTriggered');
|
|
341
|
+
|
|
342
|
+
alarmTriggered.on('input', (msg) => {
|
|
343
|
+
try {
|
|
344
|
+
expect(msg.event).to.equal('alarm');
|
|
345
|
+
done();
|
|
346
|
+
} catch (err) {
|
|
347
|
+
done(err);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
alarm.receive({ topic: 'alarm', command: 'arm' });
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: true });
|
|
354
|
+
}, 20);
|
|
355
|
+
})
|
|
356
|
+
.catch(done);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('emits "Any Zone Open" boolean output', function (done) {
|
|
360
|
+
const flowId = 'alarm6';
|
|
361
|
+
const flow = [
|
|
362
|
+
{ id: flowId, type: 'tab', label: 'alarm6' },
|
|
363
|
+
{
|
|
364
|
+
id: 'alarm',
|
|
365
|
+
type: 'AlarmSystemUltimate',
|
|
366
|
+
z: flowId,
|
|
367
|
+
controlTopic: 'alarm',
|
|
368
|
+
exitDelaySeconds: 0,
|
|
369
|
+
entryDelaySeconds: 0,
|
|
370
|
+
sirenDurationSeconds: 0,
|
|
371
|
+
requireCodeForDisarm: false,
|
|
372
|
+
zones: '{"id":"front","topic":"sensor/frontdoor","type":"perimeter","entry":false}',
|
|
373
|
+
wires: [['events'], ['siren'], [], [], [], [], ['anyZoneOpen']],
|
|
374
|
+
},
|
|
375
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
376
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
377
|
+
{ id: 'anyZoneOpen', type: 'helper', z: flowId },
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
loadAlarm(flow)
|
|
381
|
+
.then(() => {
|
|
382
|
+
const alarm = helper.getNode('alarm');
|
|
383
|
+
const anyZoneOpen = helper.getNode('anyZoneOpen');
|
|
384
|
+
|
|
385
|
+
let hasSeenTrue = false;
|
|
386
|
+
|
|
387
|
+
anyZoneOpen.on('input', (msg) => {
|
|
388
|
+
try {
|
|
389
|
+
if (msg.payload === true) {
|
|
390
|
+
hasSeenTrue = true;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (hasSeenTrue && msg.payload === false) {
|
|
394
|
+
done();
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
done(err);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
setTimeout(() => {
|
|
402
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: true });
|
|
403
|
+
}, 30);
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: false });
|
|
406
|
+
}, 80);
|
|
407
|
+
})
|
|
408
|
+
.catch(done);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('lists open zones on request topic', function (done) {
|
|
412
|
+
const flowId = 'alarm7';
|
|
413
|
+
const flow = [
|
|
414
|
+
{ id: flowId, type: 'tab', label: 'alarm7' },
|
|
415
|
+
{
|
|
416
|
+
id: 'alarm',
|
|
417
|
+
type: 'AlarmSystemUltimate',
|
|
418
|
+
z: flowId,
|
|
419
|
+
controlTopic: 'alarm',
|
|
420
|
+
exitDelaySeconds: 0,
|
|
421
|
+
entryDelaySeconds: 0,
|
|
422
|
+
sirenDurationSeconds: 0,
|
|
423
|
+
requireCodeForDisarm: false,
|
|
424
|
+
openZonesRequestTopic: 'alarm/listOpenZones',
|
|
425
|
+
openZonesRequestIntervalSeconds: 0,
|
|
426
|
+
zones: JSON.stringify(
|
|
427
|
+
[
|
|
428
|
+
{ id: 'front', topic: 'sensor/frontdoor', type: 'perimeter', entry: false },
|
|
429
|
+
{ id: 'back', topic: 'sensor/backdoor', type: 'perimeter', entry: false },
|
|
430
|
+
],
|
|
431
|
+
null,
|
|
432
|
+
2
|
|
433
|
+
),
|
|
434
|
+
wires: [['events'], ['siren'], [], [], [], [], [], [], ['openZones']],
|
|
435
|
+
},
|
|
436
|
+
{ id: 'events', type: 'helper', z: flowId },
|
|
437
|
+
{ id: 'siren', type: 'helper', z: flowId },
|
|
438
|
+
{ id: 'openZones', type: 'helper', z: flowId },
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
loadAlarm(flow)
|
|
442
|
+
.then(() => {
|
|
443
|
+
const alarm = helper.getNode('alarm');
|
|
444
|
+
const openZones = helper.getNode('openZones');
|
|
445
|
+
|
|
446
|
+
const receivedIds = new Set();
|
|
447
|
+
|
|
448
|
+
openZones.on('input', (msg) => {
|
|
449
|
+
try {
|
|
450
|
+
expect(msg.event).to.equal('open_zone');
|
|
451
|
+
expect(msg).to.have.nested.property('payload.zone.id');
|
|
452
|
+
receivedIds.add(msg.payload.zone.id);
|
|
453
|
+
if (receivedIds.has('front') && receivedIds.has('back')) {
|
|
454
|
+
done();
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
done(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
alarm.receive({ topic: 'sensor/frontdoor', payload: true });
|
|
462
|
+
alarm.receive({ topic: 'sensor/backdoor', payload: true });
|
|
463
|
+
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
alarm.receive({ topic: 'alarm/listOpenZones' });
|
|
466
|
+
}, 30);
|
|
467
|
+
})
|
|
468
|
+
.catch(done);
|
|
469
|
+
});
|
|
470
|
+
});
|