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.
@@ -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
+ &nbsp;&nbsp;<span style="color:red"
183
+ ><i class="fa fa-question-circle"></i>&nbsp;<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>&nbsp;</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>&nbsp;</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>