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,697 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("AlarmSystemUltimate", {
|
|
3
|
+
category: "Alarm Ultimate",
|
|
4
|
+
color: "#ff8080",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
controlTopic: { value: "alarm" },
|
|
8
|
+
payloadPropName: { value: "payload", required: false },
|
|
9
|
+
translatorConfig: { type: "translator-config", required: false },
|
|
10
|
+
persistState: { value: true },
|
|
11
|
+
requireCodeForArm: { value: false },
|
|
12
|
+
requireCodeForDisarm: { value: true },
|
|
13
|
+
armCode: { value: "" },
|
|
14
|
+
duressCode: { value: "" },
|
|
15
|
+
blockArmOnViolations: { value: true },
|
|
16
|
+
exitDelaySeconds: { value: 30, validate: RED.validators.number() },
|
|
17
|
+
entryDelaySeconds: { value: 30, validate: RED.validators.number() },
|
|
18
|
+
emitOpenZonesDuringArming: { value: false },
|
|
19
|
+
openZonesArmingIntervalSeconds: {
|
|
20
|
+
value: 1,
|
|
21
|
+
validate: RED.validators.number(),
|
|
22
|
+
},
|
|
23
|
+
openZonesRequestTopic: { value: "alarm/listOpenZones" },
|
|
24
|
+
openZonesRequestIntervalSeconds: {
|
|
25
|
+
value: 0,
|
|
26
|
+
validate: RED.validators.number(),
|
|
27
|
+
},
|
|
28
|
+
sirenDurationSeconds: { value: 180, validate: RED.validators.number() },
|
|
29
|
+
sirenLatchUntilDisarm: { value: false },
|
|
30
|
+
sirenTopic: { value: "siren" },
|
|
31
|
+
sirenOnPayload: { value: true },
|
|
32
|
+
sirenOnPayloadType: { value: "bool" },
|
|
33
|
+
sirenOffPayload: { value: false },
|
|
34
|
+
sirenOffPayloadType: { value: "bool" },
|
|
35
|
+
emitRestoreEvents: { value: false },
|
|
36
|
+
maxLogEntries: { value: 50, validate: RED.validators.number() },
|
|
37
|
+
zones: { value: "" },
|
|
38
|
+
},
|
|
39
|
+
inputs: 1,
|
|
40
|
+
outputs: 9,
|
|
41
|
+
outputLabels: [
|
|
42
|
+
"All Events",
|
|
43
|
+
"Siren",
|
|
44
|
+
"Alarm Triggered",
|
|
45
|
+
"Arm/Disarm Updates",
|
|
46
|
+
"Zone Activity",
|
|
47
|
+
"Errors/Denied",
|
|
48
|
+
"Any Zone Open",
|
|
49
|
+
"Open Zones (Arming)",
|
|
50
|
+
"Open Zones (On Request)",
|
|
51
|
+
],
|
|
52
|
+
icon: "alert.png",
|
|
53
|
+
label: function () {
|
|
54
|
+
return this.name || "Alarm System (BETA)";
|
|
55
|
+
},
|
|
56
|
+
paletteLabel: function () {
|
|
57
|
+
return "Alarm System (BETA)";
|
|
58
|
+
},
|
|
59
|
+
oneditprepare: function () {
|
|
60
|
+
const nodeId = this.id;
|
|
61
|
+
const payloadField = $("#node-input-payloadPropName");
|
|
62
|
+
if (payloadField.val() === "") payloadField.val("payload");
|
|
63
|
+
payloadField.typedInput({ default: "msg", types: ["msg"] });
|
|
64
|
+
|
|
65
|
+
$("#node-input-sirenOnPayload").typedInput({
|
|
66
|
+
default: this.sirenOnPayloadType || "bool",
|
|
67
|
+
types: ["str", "num", "bool", "json", "date"],
|
|
68
|
+
});
|
|
69
|
+
$("#node-input-sirenOnPayload").typedInput(
|
|
70
|
+
"type",
|
|
71
|
+
this.sirenOnPayloadType || "bool",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
$("#node-input-sirenOffPayload").typedInput({
|
|
75
|
+
default: this.sirenOffPayloadType || "bool",
|
|
76
|
+
types: ["str", "num", "bool", "json", "date"],
|
|
77
|
+
});
|
|
78
|
+
$("#node-input-sirenOffPayload").typedInput(
|
|
79
|
+
"type",
|
|
80
|
+
this.sirenOffPayloadType || "bool",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const zonesField = $("#node-input-zones");
|
|
84
|
+
zonesField.css("font-family", "monospace");
|
|
85
|
+
|
|
86
|
+
function parseZonesText(text) {
|
|
87
|
+
const raw = String(text || "").trim();
|
|
88
|
+
if (!raw) return [];
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (Array.isArray(parsed)) {
|
|
93
|
+
return parsed.filter((z) => z && typeof z === "object");
|
|
94
|
+
}
|
|
95
|
+
if (parsed && typeof parsed === "object") {
|
|
96
|
+
return [parsed];
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// fallthrough to JSON-per-line
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const zones = [];
|
|
103
|
+
const lines = raw.split("\n");
|
|
104
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
105
|
+
const line = String(lines[index] || "").trim();
|
|
106
|
+
if (!line) continue;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(line);
|
|
109
|
+
if (parsed && typeof parsed === "object") {
|
|
110
|
+
zones.push(parsed);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw new Error(`Invalid JSON at line ${index + 1}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return zones;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$("#node-input-zones-format").on("click", function (evt) {
|
|
120
|
+
evt.preventDefault();
|
|
121
|
+
try {
|
|
122
|
+
const zones = parseZonesText(zonesField.val());
|
|
123
|
+
zonesField.val(zones.length ? JSON.stringify(zones, null, 2) : "");
|
|
124
|
+
} catch (err) {
|
|
125
|
+
RED.notify(`Zones: ${err.message}`, "error");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
$("#node-input-zones-legacy").on("click", function (evt) {
|
|
130
|
+
evt.preventDefault();
|
|
131
|
+
try {
|
|
132
|
+
const zones = parseZonesText(zonesField.val());
|
|
133
|
+
const legacy = zones
|
|
134
|
+
.map((zone) => JSON.stringify(zone))
|
|
135
|
+
.filter(
|
|
136
|
+
(line) => line !== undefined && line !== null && line !== "",
|
|
137
|
+
)
|
|
138
|
+
.join("\n");
|
|
139
|
+
zonesField.val(legacy);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
RED.notify(`Zones: ${err.message}`, "error");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
$("#node-input-zones-mapper").on("click", function (evt) {
|
|
146
|
+
evt.preventDefault();
|
|
147
|
+
const httpAdminRoot = (RED.settings && RED.settings.httpAdminRoot) || "/";
|
|
148
|
+
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
149
|
+
window.open(
|
|
150
|
+
`${root}alarm-ultimate/alarm-json-mapper`,
|
|
151
|
+
"_blank",
|
|
152
|
+
"noopener,noreferrer",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
$("#node-input-zones-panel").on("click", function (evt) {
|
|
157
|
+
evt.preventDefault();
|
|
158
|
+
const httpAdminRoot = (RED.settings && RED.settings.httpAdminRoot) || "/";
|
|
159
|
+
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
160
|
+
const idPart = nodeId ? `?id=${encodeURIComponent(nodeId)}` : "";
|
|
161
|
+
window.open(
|
|
162
|
+
`${root}alarm-ultimate/alarm-panel${idPart}`,
|
|
163
|
+
"_blank",
|
|
164
|
+
"noopener,noreferrer",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
oneditsave: function () {
|
|
169
|
+
this.sirenOnPayloadType = $("#node-input-sirenOnPayload").typedInput(
|
|
170
|
+
"type",
|
|
171
|
+
);
|
|
172
|
+
this.sirenOffPayloadType = $("#node-input-sirenOffPayload").typedInput(
|
|
173
|
+
"type",
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<script type="text/html" data-template-name="AlarmSystemUltimate">
|
|
180
|
+
<div class="form-row">
|
|
181
|
+
<b>Alarm System Ultimate (BETA)</b>
|
|
182
|
+
<span style="color:red"
|
|
183
|
+
><i class="fa fa-question-circle"></i> <a
|
|
184
|
+
target="_blank"
|
|
185
|
+
href="https://github.com/Supergiovane/node-red-contrib-alarm-ultimate"
|
|
186
|
+
><u>Help online</u></a
|
|
187
|
+
></span
|
|
188
|
+
>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="form-row">
|
|
192
|
+
<label> </label>
|
|
193
|
+
<button type="button" class="red-ui-button" id="node-input-zones-panel">
|
|
194
|
+
<i class="fa fa-keyboard-o"></i> Panel
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="form-row">
|
|
199
|
+
<label for="node-input-name"
|
|
200
|
+
><i class="icon-tag"></i> Name</label
|
|
201
|
+
>
|
|
202
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="form-row">
|
|
206
|
+
<label for="node-input-controlTopic"
|
|
207
|
+
><i class="fa fa-tag"></i> Control topic</label
|
|
208
|
+
>
|
|
209
|
+
<input type="text" id="node-input-controlTopic" />
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="form-row">
|
|
213
|
+
<label
|
|
214
|
+
for="node-input-payloadPropName"
|
|
215
|
+
><i class="fa fa-ellipsis-h"></i> With Input</label
|
|
216
|
+
>
|
|
217
|
+
<input type="text" id="node-input-payloadPropName" />
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div class="form-row">
|
|
221
|
+
<label
|
|
222
|
+
for="node-input-translatorConfig"
|
|
223
|
+
><i class="fa fa-language"></i> Translator</label
|
|
224
|
+
>
|
|
225
|
+
<input type="text" id="node-input-translatorConfig" />
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div class="form-row">
|
|
229
|
+
<label for="node-input-persistState"
|
|
230
|
+
><i class="fa fa-database"></i> Persist state</label
|
|
231
|
+
>
|
|
232
|
+
<input
|
|
233
|
+
type="checkbox"
|
|
234
|
+
id="node-input-persistState"
|
|
235
|
+
style="width:auto; margin-top:7px;"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<hr />
|
|
240
|
+
|
|
241
|
+
<div class="form-row">
|
|
242
|
+
<label
|
|
243
|
+
for="node-input-requireCodeForArm"
|
|
244
|
+
><i class="fa fa-lock"></i> Require code for arm</label
|
|
245
|
+
>
|
|
246
|
+
<input
|
|
247
|
+
type="checkbox"
|
|
248
|
+
id="node-input-requireCodeForArm"
|
|
249
|
+
style="width:auto; margin-top:7px;"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="form-row">
|
|
254
|
+
<label
|
|
255
|
+
for="node-input-requireCodeForDisarm"
|
|
256
|
+
><i class="fa fa-unlock"></i> Require code for disarm</label
|
|
257
|
+
>
|
|
258
|
+
<input
|
|
259
|
+
type="checkbox"
|
|
260
|
+
id="node-input-requireCodeForDisarm"
|
|
261
|
+
style="width:auto; margin-top:7px;"
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="form-row">
|
|
266
|
+
<label for="node-input-armCode"
|
|
267
|
+
><i class="fa fa-key"></i> Code</label
|
|
268
|
+
>
|
|
269
|
+
<input type="password" id="node-input-armCode" placeholder="(optional)" />
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="form-row">
|
|
273
|
+
<label for="node-input-duressCode"
|
|
274
|
+
><i class="fa fa-user-secret"></i> Duress code</label
|
|
275
|
+
>
|
|
276
|
+
<input
|
|
277
|
+
type="password"
|
|
278
|
+
id="node-input-duressCode"
|
|
279
|
+
placeholder="(optional)"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div class="form-row">
|
|
284
|
+
<label
|
|
285
|
+
for="node-input-blockArmOnViolations"
|
|
286
|
+
><i class="fa fa-shield"></i> Block arm on violations</label
|
|
287
|
+
>
|
|
288
|
+
<input
|
|
289
|
+
type="checkbox"
|
|
290
|
+
id="node-input-blockArmOnViolations"
|
|
291
|
+
style="width:auto; margin-top:7px;"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<hr />
|
|
296
|
+
|
|
297
|
+
<div class="form-row">
|
|
298
|
+
<label
|
|
299
|
+
for="node-input-exitDelaySeconds"
|
|
300
|
+
><i class="fa fa-sign-out"></i> Exit delay (s)</label
|
|
301
|
+
>
|
|
302
|
+
<input type="number" id="node-input-exitDelaySeconds" min="0" />
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div class="form-row">
|
|
306
|
+
<label
|
|
307
|
+
for="node-input-entryDelaySeconds"
|
|
308
|
+
><i class="fa fa-sign-in"></i> Entry delay (s)</label
|
|
309
|
+
>
|
|
310
|
+
<input type="number" id="node-input-entryDelaySeconds" min="0" />
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<hr />
|
|
314
|
+
|
|
315
|
+
<div class="form-row">
|
|
316
|
+
<label for="node-input-sirenTopic"
|
|
317
|
+
><i class="fa fa-bullhorn"></i> Siren topic</label
|
|
318
|
+
>
|
|
319
|
+
<input type="text" id="node-input-sirenTopic" placeholder="siren" />
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="form-row">
|
|
323
|
+
<label
|
|
324
|
+
for="node-input-sirenDurationSeconds"
|
|
325
|
+
><i class="fa fa-clock-o"></i> Siren duration (s)</label
|
|
326
|
+
>
|
|
327
|
+
<input type="number" id="node-input-sirenDurationSeconds" min="0" />
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="form-row">
|
|
331
|
+
<label
|
|
332
|
+
for="node-input-sirenLatchUntilDisarm"
|
|
333
|
+
><i class="fa fa-link"></i> Latch siren until disarm</label
|
|
334
|
+
>
|
|
335
|
+
<input
|
|
336
|
+
type="checkbox"
|
|
337
|
+
id="node-input-sirenLatchUntilDisarm"
|
|
338
|
+
style="width:auto; margin-top:7px;"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div class="form-row">
|
|
343
|
+
<label
|
|
344
|
+
for="node-input-sirenOnPayload"
|
|
345
|
+
><i class="fa fa-play"></i> Siren ON payload</label
|
|
346
|
+
>
|
|
347
|
+
<input type="text" id="node-input-sirenOnPayload" />
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div class="form-row">
|
|
351
|
+
<label
|
|
352
|
+
for="node-input-sirenOffPayload"
|
|
353
|
+
><i class="fa fa-stop"></i> Siren OFF payload</label
|
|
354
|
+
>
|
|
355
|
+
<input type="text" id="node-input-sirenOffPayload" />
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<hr />
|
|
359
|
+
|
|
360
|
+
<div class="form-row">
|
|
361
|
+
<label
|
|
362
|
+
for="node-input-emitRestoreEvents"
|
|
363
|
+
><i class="fa fa-undo"></i> Emit restore events</label
|
|
364
|
+
>
|
|
365
|
+
<input
|
|
366
|
+
type="checkbox"
|
|
367
|
+
id="node-input-emitRestoreEvents"
|
|
368
|
+
style="width:auto; margin-top:7px;"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div class="form-row">
|
|
373
|
+
<label
|
|
374
|
+
for="node-input-maxLogEntries"
|
|
375
|
+
><i class="fa fa-list"></i> Event log size</label
|
|
376
|
+
>
|
|
377
|
+
<input type="number" id="node-input-maxLogEntries" min="0" max="500" />
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<hr />
|
|
381
|
+
|
|
382
|
+
<div class="form-row">
|
|
383
|
+
<b>Open zones listing</b>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="form-row">
|
|
387
|
+
<label
|
|
388
|
+
for="node-input-emitOpenZonesDuringArming"
|
|
389
|
+
><i class="fa fa-list"></i> Emit open zones while arming</label
|
|
390
|
+
>
|
|
391
|
+
<input
|
|
392
|
+
type="checkbox"
|
|
393
|
+
id="node-input-emitOpenZonesDuringArming"
|
|
394
|
+
style="width:auto; margin-top:7px;"
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div class="form-row">
|
|
399
|
+
<label
|
|
400
|
+
for="node-input-openZonesArmingIntervalSeconds"
|
|
401
|
+
><i class="fa fa-clock-o"></i> Arming list interval (s)</label
|
|
402
|
+
>
|
|
403
|
+
<input
|
|
404
|
+
type="number"
|
|
405
|
+
id="node-input-openZonesArmingIntervalSeconds"
|
|
406
|
+
min="0"
|
|
407
|
+
step="0.1"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div class="form-row">
|
|
412
|
+
<label
|
|
413
|
+
for="node-input-openZonesRequestTopic"
|
|
414
|
+
><i class="fa fa-list"></i> Open zones request topic</label
|
|
415
|
+
>
|
|
416
|
+
<input
|
|
417
|
+
type="text"
|
|
418
|
+
id="node-input-openZonesRequestTopic"
|
|
419
|
+
placeholder="alarm/listOpenZones"
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div class="form-row">
|
|
424
|
+
<label
|
|
425
|
+
for="node-input-openZonesRequestIntervalSeconds"
|
|
426
|
+
><i class="fa fa-clock-o"></i> Request list interval (s)</label
|
|
427
|
+
>
|
|
428
|
+
<input
|
|
429
|
+
type="number"
|
|
430
|
+
id="node-input-openZonesRequestIntervalSeconds"
|
|
431
|
+
min="0"
|
|
432
|
+
step="0.1"
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<div class="form-row">
|
|
437
|
+
<label for="node-input-zones"
|
|
438
|
+
><i class="fa fa-th-large"></i> Zones (JSON per line or JSON
|
|
439
|
+
array)</label
|
|
440
|
+
>
|
|
441
|
+
<textarea
|
|
442
|
+
id="node-input-zones"
|
|
443
|
+
spellcheck="false"
|
|
444
|
+
wrap="off"
|
|
445
|
+
style="width:100%; height:220px; font-family:monospace;"
|
|
446
|
+
placeholder='{"id":"front_door","name":"Front door","topic":"sensor/frontdoor","type":"perimeter","entry":true,"bypassable":true,"chime":true}\n{"id":"pir_living","name":"Living PIR","topic":"sensor/living_pir","type":"motion","entry":false,"bypassable":true,"cooldownSeconds":10}'
|
|
447
|
+
></textarea>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div class="form-row">
|
|
451
|
+
<label> </label>
|
|
452
|
+
<button type="button" class="red-ui-button" id="node-input-zones-format">
|
|
453
|
+
<i class="fa fa-indent"></i> Format
|
|
454
|
+
</button>
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
class="red-ui-button"
|
|
458
|
+
id="node-input-zones-legacy"
|
|
459
|
+
style="margin-left:5px;"
|
|
460
|
+
>
|
|
461
|
+
<i class="fa fa-list"></i> Legacy
|
|
462
|
+
</button>
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
465
|
+
class="red-ui-button"
|
|
466
|
+
id="node-input-zones-mapper"
|
|
467
|
+
style="margin-left:5px;"
|
|
468
|
+
>
|
|
469
|
+
<i class="fa fa-external-link"></i> Mapper
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
</script>
|
|
473
|
+
|
|
474
|
+
<script type="text/markdown" data-help-name="AlarmSystemUltimate">
|
|
475
|
+
<p><b>BETA</b> – Alarm control panel node with a single armed/disarmed state, zones, entry/exit delays, bypass, chime, tamper/fire/24h zones, siren control, status and event log.</p>
|
|
476
|
+
|
|
477
|
+
The node has 2 inputs "in one":
|
|
478
|
+
|
|
479
|
+
- **Control messages**: when `msg.topic` equals **Control topic**.
|
|
480
|
+
- **Sensor messages**: any other message, matched to a zone by `msg.topic` and converted to boolean using **With Input** (default `payload`) and the optional **Translator**.
|
|
481
|
+
|
|
482
|
+
<br/>
|
|
483
|
+
|
|
484
|
+
| Property | Description |
|
|
485
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
486
|
+
| Control topic | Topic that receives runtime commands (arm/disarm/status/bypass/siren/panic/reset). |
|
|
487
|
+
| With Input | Message property evaluated as sensor value (default `payload`). |
|
|
488
|
+
| Translator | Optional translator-config for true/false conversion (useful for Home Assistant strings). |
|
|
489
|
+
| Persist state | Persists arming state, bypass list and event log across restarts. |
|
|
490
|
+
| Require code for arm | Requires a valid PIN for arming commands (via `msg.code` or `msg.pin`). If **Code** is empty, commands are allowed. |
|
|
491
|
+
| Require code for disarm | Requires a valid PIN for disarming commands (via `msg.code` or `msg.pin`). If **Code** is empty, commands are allowed. |
|
|
492
|
+
| Code | PIN required to arm/disarm when enabled. |
|
|
493
|
+
| Duress code | If provided and `msg.code` matches, the node raises a silent duress alarm while still executing the command. |
|
|
494
|
+
| Block arm on violations | Prevents arming if any active (true) zone would be armed (excluding bypassed zones). Checked both at arm start and after exit delay. |
|
|
495
|
+
| Exit/Entry delay (s) | Global exit/entry delays (each zone can override entry delay). |
|
|
496
|
+
| Emit open zones while arming | Emits open zones one-by-one on the dedicated output while arming (during exit delay). |
|
|
497
|
+
| Arming list interval (s) | Interval between each open zone emitted while arming. |
|
|
498
|
+
| Open zones request topic | When a message arrives with `msg.topic` equal to this topic, the node lists open zones even while disarmed. |
|
|
499
|
+
| Request list interval (s) | Interval between each open zone emitted on request (0 = send immediately). |
|
|
500
|
+
| Siren topic | Topic used on output 2 to turn the siren on/off (default: `controlTopic + "/siren"`). |
|
|
501
|
+
| Siren ON/OFF payload | Values emitted on output 2 for siren on/off (typed). |
|
|
502
|
+
| Siren duration (s) | Auto stop duration (`0` = latch until disarm). |
|
|
503
|
+
| Latch siren until disarm | Forces siren to remain on until disarm (ignores duration). |
|
|
504
|
+
| Emit restore events | Emits `zone_restore` when a zone returns to false. |
|
|
505
|
+
| Event log size | Max stored log entries in node context (0 disables log). |
|
|
506
|
+
| Zones | Zone definitions. Supports legacy "one JSON object per line" or a JSON array (use **Format** / **Legacy** buttons). |
|
|
507
|
+
|
|
508
|
+
<br/>
|
|
509
|
+
|
|
510
|
+
### Web tools
|
|
511
|
+
|
|
512
|
+
- **Zones JSON Mapper**: available at `/alarm-ultimate/alarm-json-mapper` (or via the **Mapper** button).
|
|
513
|
+
- **Alarm Panel**: available at `/alarm-ultimate/alarm-panel` (or via the **Panel** button).
|
|
514
|
+
- The Alarm Panel supports `?id=<alarmNodeId>` and an embed mode for Dashboard: `?embed=1&id=<alarmNodeId>`.
|
|
515
|
+
|
|
516
|
+
<br/>
|
|
517
|
+
|
|
518
|
+
## Outputs
|
|
519
|
+
|
|
520
|
+
**Output 1 (All Events)**
|
|
521
|
+
|
|
522
|
+
- `msg.topic = controlTopic + "/event"`
|
|
523
|
+
- `msg.event` (string) and `msg.payload` (object)
|
|
524
|
+
|
|
525
|
+
`msg.payload` always contains at least:
|
|
526
|
+
|
|
527
|
+
- `event`: same as `msg.event`
|
|
528
|
+
- `mode`: `disarmed|armed`
|
|
529
|
+
|
|
530
|
+
**Output 2 (Siren)**
|
|
531
|
+
|
|
532
|
+
- `msg.topic = sirenTopic`
|
|
533
|
+
- `msg.payload`: configured siren ON/OFF payload (typed)
|
|
534
|
+
- `msg.event`: `siren_on` / `siren_off`
|
|
535
|
+
- `msg.reason`: why it changed (`manual`, `panic`, `instant`, `entry_timeout`, `timeout`, `disarm`, ...)
|
|
536
|
+
|
|
537
|
+
**Output 3 (Alarm Triggered)**
|
|
538
|
+
|
|
539
|
+
- Same message as Output 1, only when `msg.event === "alarm"`.
|
|
540
|
+
|
|
541
|
+
**Output 4 (Arm/Disarm Updates)**
|
|
542
|
+
|
|
543
|
+
- Same message as Output 1, only for arming/state events (`arming`, `armed`, `disarmed`, `entry_delay`, `arm_blocked`, `already_armed`, `status`, `reset`, `siren_on`, `siren_off`).
|
|
544
|
+
|
|
545
|
+
**Output 5 (Zone Activity)**
|
|
546
|
+
|
|
547
|
+
- Same message as Output 1, only for zone events (`bypassed`, `unbypassed`, `chime`, `zone_ignored_exit`, `zone_bypassed_trigger`, `zone_restore`).
|
|
548
|
+
|
|
549
|
+
**Output 6 (Errors/Denied)**
|
|
550
|
+
|
|
551
|
+
- Same message as Output 1, only for `error` and `denied`.
|
|
552
|
+
|
|
553
|
+
**Output 7 (Any Zone Open)**
|
|
554
|
+
|
|
555
|
+
- `msg.topic = controlTopic + "/anyZoneOpen"`
|
|
556
|
+
- `msg.payload = true|false`
|
|
557
|
+
- `msg.openZonesCount` and `msg.openZones` (array)
|
|
558
|
+
|
|
559
|
+
**Outputs 8..9 (Open Zones Listing)**
|
|
560
|
+
|
|
561
|
+
- Output 8: while arming (optional)
|
|
562
|
+
- Output 9: on request topic / command
|
|
563
|
+
- `msg.topic = controlTopic + "/openZone"`
|
|
564
|
+
- `msg.event = "open_zone"`
|
|
565
|
+
- `msg.payload = { context, position, total, zone }`
|
|
566
|
+
|
|
567
|
+
<br/>
|
|
568
|
+
|
|
569
|
+
## Events (`msg.event`)
|
|
570
|
+
|
|
571
|
+
Arming / state:
|
|
572
|
+
|
|
573
|
+
- `arming` → `{ target, seconds, reason }`
|
|
574
|
+
- `armed` → `{ reason }`
|
|
575
|
+
- `disarmed` → `{ reason, duress }`
|
|
576
|
+
- `already_armed` → `{ target }`
|
|
577
|
+
- `arm_blocked` → `{ target, violations:[{id,name,type}] }`
|
|
578
|
+
- `status` → `{ state: { mode, arming, entry, alarmActive, silentAlarmActive, sirenActive, alarmZone, bypassedZones, log } }`
|
|
579
|
+
- `reset` → `{}`
|
|
580
|
+
- `denied` → `{ action: "arm"|"disarm", target? }`
|
|
581
|
+
- `error` → `{ error: "missing_zone"|"unknown_zone"|"zone_not_bypassable", zone? }`
|
|
582
|
+
|
|
583
|
+
Zones / alarm:
|
|
584
|
+
|
|
585
|
+
- `entry_delay` → `{ zone:{id,name}, seconds }`
|
|
586
|
+
- `alarm` → `{ kind, zone, silent }` where `kind` can be `instant|entry_timeout|panic|duress|fire|tamper|24h|...`
|
|
587
|
+
- `chime` → `{ zone:{id,name,type} }` (while disarmed)
|
|
588
|
+
- `zone_ignored_exit` → `{ zone:{id,name,type} }` (triggered during exit delay and not allowed)
|
|
589
|
+
- `zone_bypassed_trigger` → `{ zone:{id,name,type} }`
|
|
590
|
+
- `zone_restore` → `{ zone:{id,name,type} }` (when enabled)
|
|
591
|
+
- `siren_on` → `{ reason }` (manual `siren_on` command)
|
|
592
|
+
- `siren_off` → `{ reason }` (manual stop, disarm, timeout, arm, ...)
|
|
593
|
+
- `open_zone` → `{ context, position, total, zone }` (open zones listing outputs)
|
|
594
|
+
|
|
595
|
+
<br/>
|
|
596
|
+
|
|
597
|
+
## Control messages (`msg.topic === controlTopic`)
|
|
598
|
+
|
|
599
|
+
Arm:
|
|
600
|
+
|
|
601
|
+
- `msg.command = "arm"`
|
|
602
|
+
- or `msg.arm = "armed"`
|
|
603
|
+
- or `msg.mode = "armed"`
|
|
604
|
+
- legacy commands `arm_away` / `arm_home` / `arm_night` are accepted and map to `arm`
|
|
605
|
+
|
|
606
|
+
Disarm:
|
|
607
|
+
|
|
608
|
+
- `msg.command = "disarm"` or `msg.disarm = true`
|
|
609
|
+
|
|
610
|
+
Status snapshot:
|
|
611
|
+
|
|
612
|
+
- `msg.command = "status"` or `msg.status = true`
|
|
613
|
+
|
|
614
|
+
List open zones (one message per zone on Output 9):
|
|
615
|
+
|
|
616
|
+
- `msg.command = "list_open_zones"` or `msg.listOpenZones = true`
|
|
617
|
+
- or send a message with `msg.topic = <Open zones request topic>` (works even while disarmed)
|
|
618
|
+
|
|
619
|
+
Bypass / unbypass (zone id):
|
|
620
|
+
|
|
621
|
+
- `msg.command = "bypass"` / `msg.command = "unbypass"`
|
|
622
|
+
- or `msg.bypass = true` / `msg.unbypass = true`
|
|
623
|
+
- pass the zone id in `msg.zone` (alias: `msg.zoneId`, `msg.zoneName` - still expects the zone `id`)
|
|
624
|
+
|
|
625
|
+
Siren:
|
|
626
|
+
|
|
627
|
+
- `msg.command = "siren_on"|"siren_off"`
|
|
628
|
+
|
|
629
|
+
Panic:
|
|
630
|
+
|
|
631
|
+
- `msg.command = "panic"` (audible)
|
|
632
|
+
- `msg.command = "panic_silent"` (silent)
|
|
633
|
+
|
|
634
|
+
Reset:
|
|
635
|
+
|
|
636
|
+
- `msg.command = "reset"` or `msg.reset = true` (clears state, bypass list, log and timers)
|
|
637
|
+
|
|
638
|
+
Codes:
|
|
639
|
+
|
|
640
|
+
- pass `msg.code` (or `msg.pin`) when code checks are enabled
|
|
641
|
+
- if `msg.code` equals **Duress code**, the node emits a silent duress alarm while still arming/disarming
|
|
642
|
+
|
|
643
|
+
<br/>
|
|
644
|
+
|
|
645
|
+
## Zones
|
|
646
|
+
|
|
647
|
+
Zones can be configured as:
|
|
648
|
+
|
|
649
|
+
- **Legacy**: one JSON object per line
|
|
650
|
+
- **Formatted**: a JSON array of objects
|
|
651
|
+
|
|
652
|
+
Minimal fields:
|
|
653
|
+
|
|
654
|
+
- `topic` (exact topic, or a prefix ending with `*`) OR `topicPattern` (regex string)
|
|
655
|
+
- `topicPattern` is evaluated as a JavaScript `RegExp` and tested against `msg.topic`
|
|
656
|
+
- `id` is optional (auto-generated if missing)
|
|
657
|
+
|
|
658
|
+
Common fields:
|
|
659
|
+
|
|
660
|
+
- `id`: unique identifier used for bypass (`msg.zone`)
|
|
661
|
+
- `name`: label used in events
|
|
662
|
+
- `type`: `perimeter` (default), `motion`, `tamper`, `fire`, `24h`
|
|
663
|
+
- `entry`: `true` to use entry delay, otherwise triggers instantly
|
|
664
|
+
- `entryDelaySeconds`: overrides the global entry delay
|
|
665
|
+
- `bypassable`: `true|false` (default `true`)
|
|
666
|
+
- `chime`: `true` to emit `chime` event while disarmed
|
|
667
|
+
- `cooldownSeconds`: minimum time between triggers
|
|
668
|
+
- `instantDuringExit`: if true, the zone can trigger during exit delay
|
|
669
|
+
|
|
670
|
+
Notes:
|
|
671
|
+
|
|
672
|
+
- `tamper`, `fire` and `24h` zones are considered **always active** (they can alarm even while disarmed).
|
|
673
|
+
- If a zone is bypassed, triggers generate `zone_bypassed_trigger` and do not alarm.
|
|
674
|
+
|
|
675
|
+
Example (formatted JSON array):
|
|
676
|
+
|
|
677
|
+
```json
|
|
678
|
+
[
|
|
679
|
+
{
|
|
680
|
+
"id": "front_door",
|
|
681
|
+
"name": "Front door",
|
|
682
|
+
"topic": "house/door/front",
|
|
683
|
+
"type": "perimeter",
|
|
684
|
+
"entry": true,
|
|
685
|
+
"entryDelaySeconds": 30,
|
|
686
|
+
"bypassable": true,
|
|
687
|
+
"chime": true
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
<br/>
|
|
693
|
+
|
|
694
|
+
[SEE THE README FOR FULL HELP AND SAMPLES](https://github.com/Supergiovane/node-red-contrib-alarm-ultimate)</br>
|
|
695
|
+
|
|
696
|
+
[Find it useful?](https://www.paypal.me/techtoday)
|
|
697
|
+
</script>
|