node-red-contrib-alarm-ultimate 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/examples/README.md +13 -0
- package/examples/alarm-ultimate-basic.json +0 -1
- package/examples/alarm-ultimate-dashboard-controls.json +3 -2
- package/examples/alarm-ultimate-dashboard-v2.json +762 -0
- package/examples/alarm-ultimate-dashboard.json +3 -3
- package/flowfuse-node-red-dashboard-1.30.2.tgz +0 -0
- package/nodes/AlarmSystemUltimate.html +171 -82
- package/nodes/AlarmSystemUltimate.js +39 -8
- 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 +51 -0
- package/test/input-adapter.spec.js +243 -0
- package/test/output-nodes.spec.js +3 -0
- package/tools/alarm-json-mapper.html +934 -165
- package/tools/alarm-panel.html +630 -131
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"name": "Home Alarm",
|
|
43
43
|
"controlTopic": "alarm",
|
|
44
44
|
"payloadPropName": "payload",
|
|
45
|
-
"translatorConfig": "",
|
|
46
45
|
"persistState": true,
|
|
47
46
|
"requireCodeForArm": false,
|
|
48
47
|
"requireCodeForDisarm": false,
|
|
@@ -93,7 +92,8 @@
|
|
|
93
92
|
"fwdInMessages": false,
|
|
94
93
|
"resendOnRefresh": true,
|
|
95
94
|
"templateScope": "local",
|
|
96
|
-
"className": ""
|
|
95
|
+
"className": "",
|
|
96
|
+
"x": 520,
|
|
97
|
+
"y": 160
|
|
97
98
|
}
|
|
98
99
|
]
|
|
99
|
-
|
|
Binary file
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
controlTopic: { value: "alarm" },
|
|
8
8
|
payloadPropName: { value: "payload", required: false },
|
|
9
|
-
translatorConfig: { type: "translator-config", required: false },
|
|
10
9
|
persistState: { value: true },
|
|
11
10
|
requireCodeForArm: { value: false },
|
|
12
11
|
requireCodeForDisarm: { value: true },
|
|
@@ -58,6 +57,12 @@
|
|
|
58
57
|
},
|
|
59
58
|
oneditprepare: function () {
|
|
60
59
|
const nodeId = this.id;
|
|
60
|
+
const origin = window.location.origin;
|
|
61
|
+
const openerOrigin = origin;
|
|
62
|
+
const bc =
|
|
63
|
+
typeof BroadcastChannel === "function"
|
|
64
|
+
? new BroadcastChannel("alarm-ultimate-zones")
|
|
65
|
+
: null;
|
|
61
66
|
const payloadField = $("#node-input-payloadPropName");
|
|
62
67
|
if (payloadField.val() === "") payloadField.val("payload");
|
|
63
68
|
payloadField.typedInput({ default: "msg", types: ["msg"] });
|
|
@@ -81,7 +86,7 @@
|
|
|
81
86
|
);
|
|
82
87
|
|
|
83
88
|
const zonesField = $("#node-input-zones");
|
|
84
|
-
|
|
89
|
+
const zonesSummary = $("#node-input-zones-summary");
|
|
85
90
|
|
|
86
91
|
function parseZonesText(text) {
|
|
87
92
|
const raw = String(text || "").trim();
|
|
@@ -116,42 +121,23 @@
|
|
|
116
121
|
return zones;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
evt.preventDefault();
|
|
124
|
+
function updateZonesSummary() {
|
|
121
125
|
try {
|
|
122
126
|
const zones = parseZonesText(zonesField.val());
|
|
123
|
-
|
|
127
|
+
zonesSummary.text(zones.length ? `${zones.length} zones configured` : "No zones configured");
|
|
124
128
|
} catch (err) {
|
|
125
|
-
|
|
129
|
+
zonesSummary.text("Zones: invalid JSON");
|
|
126
130
|
}
|
|
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
|
-
});
|
|
131
|
+
}
|
|
144
132
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
);
|
|
154
|
-
});
|
|
133
|
+
function openZonesManager() {
|
|
134
|
+
const httpAdminRoot = (RED.settings && RED.settings.httpAdminRoot) || "/";
|
|
135
|
+
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
136
|
+
const currentName = String($("#node-input-name").val() || "").trim();
|
|
137
|
+
const namePart = currentName ? `&name=${encodeURIComponent(currentName)}` : "";
|
|
138
|
+
const idPart = nodeId ? `?id=${encodeURIComponent(nodeId)}${namePart}` : "";
|
|
139
|
+
window.open(`${root}alarm-ultimate/alarm-json-mapper${idPart}`, "_blank");
|
|
140
|
+
}
|
|
155
141
|
|
|
156
142
|
$("#node-input-zones-panel").on("click", function (evt) {
|
|
157
143
|
evt.preventDefault();
|
|
@@ -164,8 +150,99 @@
|
|
|
164
150
|
"noopener,noreferrer",
|
|
165
151
|
);
|
|
166
152
|
});
|
|
153
|
+
|
|
154
|
+
$("#node-input-zones-manage").on("click", function (evt) {
|
|
155
|
+
evt.preventDefault();
|
|
156
|
+
openZonesManager();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const messageListener = (evt) => {
|
|
160
|
+
if (!evt || evt.origin !== openerOrigin) return;
|
|
161
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : null;
|
|
162
|
+
if (!data || typeof data.type !== "string") return;
|
|
163
|
+
if (data.nodeId && data.nodeId !== nodeId) return;
|
|
164
|
+
|
|
165
|
+
if (data.type === "alarm-ultimate-request-zones") {
|
|
166
|
+
let zonesJson = "";
|
|
167
|
+
try {
|
|
168
|
+
const zones = parseZonesText(zonesField.val());
|
|
169
|
+
zonesJson = zones.length ? JSON.stringify(zones, null, 2) : "";
|
|
170
|
+
} catch (_err) {
|
|
171
|
+
zonesJson = "";
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
evt.source.postMessage(
|
|
175
|
+
{
|
|
176
|
+
type: "alarm-ultimate-zones",
|
|
177
|
+
nodeId,
|
|
178
|
+
nodeName: String($("#node-input-name").val() || "").trim() || String(this.name || ""),
|
|
179
|
+
zonesJson,
|
|
180
|
+
},
|
|
181
|
+
openerOrigin,
|
|
182
|
+
);
|
|
183
|
+
} catch (_err) {
|
|
184
|
+
// ignore
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (bc) {
|
|
188
|
+
bc.postMessage({
|
|
189
|
+
type: "alarm-ultimate-zones",
|
|
190
|
+
nodeId,
|
|
191
|
+
nodeName: String($("#node-input-name").val() || "").trim() || String(this.name || ""),
|
|
192
|
+
zonesJson,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
} catch (_err) {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (data.type === "alarm-ultimate-zones" && typeof data.zonesJson === "string") {
|
|
202
|
+
zonesField.val(String(data.zonesJson || ""));
|
|
203
|
+
try {
|
|
204
|
+
zonesField.trigger("change");
|
|
205
|
+
} catch (_err) {
|
|
206
|
+
// ignore
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
if (RED && RED.nodes && typeof RED.nodes.dirty === "function") {
|
|
210
|
+
RED.nodes.dirty(true);
|
|
211
|
+
}
|
|
212
|
+
} catch (_err) {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
updateZonesSummary();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
window.addEventListener("message", messageListener);
|
|
220
|
+
if (bc) {
|
|
221
|
+
bc.addEventListener("message", (ev) => {
|
|
222
|
+
const data = ev && ev.data && typeof ev.data === "object" ? ev.data : null;
|
|
223
|
+
if (!data || typeof data.type !== "string") return;
|
|
224
|
+
if (data.nodeId && data.nodeId !== nodeId) return;
|
|
225
|
+
// Reuse the same handler surface by calling it with a minimal shape.
|
|
226
|
+
messageListener({ origin: openerOrigin, data, source: null });
|
|
227
|
+
});
|
|
228
|
+
this._alarmUltimateZonesBroadcast = bc;
|
|
229
|
+
}
|
|
230
|
+
this._alarmUltimateZonesMessageListener = messageListener;
|
|
231
|
+
updateZonesSummary();
|
|
167
232
|
},
|
|
168
233
|
oneditsave: function () {
|
|
234
|
+
if (this._alarmUltimateZonesMessageListener) {
|
|
235
|
+
window.removeEventListener("message", this._alarmUltimateZonesMessageListener);
|
|
236
|
+
this._alarmUltimateZonesMessageListener = null;
|
|
237
|
+
}
|
|
238
|
+
if (this._alarmUltimateZonesBroadcast) {
|
|
239
|
+
try {
|
|
240
|
+
this._alarmUltimateZonesBroadcast.close();
|
|
241
|
+
} catch (_err) {
|
|
242
|
+
// ignore
|
|
243
|
+
}
|
|
244
|
+
this._alarmUltimateZonesBroadcast = null;
|
|
245
|
+
}
|
|
169
246
|
this.sirenOnPayloadType = $("#node-input-sirenOnPayload").typedInput(
|
|
170
247
|
"type",
|
|
171
248
|
);
|
|
@@ -173,10 +250,55 @@
|
|
|
173
250
|
"type",
|
|
174
251
|
);
|
|
175
252
|
},
|
|
253
|
+
oneditcancel: function () {
|
|
254
|
+
if (this._alarmUltimateZonesMessageListener) {
|
|
255
|
+
window.removeEventListener("message", this._alarmUltimateZonesMessageListener);
|
|
256
|
+
this._alarmUltimateZonesMessageListener = null;
|
|
257
|
+
}
|
|
258
|
+
if (this._alarmUltimateZonesBroadcast) {
|
|
259
|
+
try {
|
|
260
|
+
this._alarmUltimateZonesBroadcast.close();
|
|
261
|
+
} catch (_err) {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
this._alarmUltimateZonesBroadcast = null;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
176
267
|
});
|
|
177
268
|
</script>
|
|
178
269
|
|
|
179
270
|
<script type="text/html" data-template-name="AlarmSystemUltimate">
|
|
271
|
+
<style>
|
|
272
|
+
.alarm-ultimate-editor .red-ui-button.au-btn {
|
|
273
|
+
border-radius: 8px;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
border-width: 1px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.alarm-ultimate-editor .red-ui-button.au-btn i {
|
|
279
|
+
margin-right: 4px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.alarm-ultimate-editor .red-ui-button.au-btn-primary {
|
|
283
|
+
background: #2563eb !important;
|
|
284
|
+
border-color: #1d4ed8 !important;
|
|
285
|
+
color: #fff !important;
|
|
286
|
+
}
|
|
287
|
+
.alarm-ultimate-editor .red-ui-button.au-btn-primary:hover {
|
|
288
|
+
background: #1d4ed8 !important;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.alarm-ultimate-editor .red-ui-button.au-btn-success {
|
|
292
|
+
background: #16a34a !important;
|
|
293
|
+
border-color: #15803d !important;
|
|
294
|
+
color: #fff !important;
|
|
295
|
+
}
|
|
296
|
+
.alarm-ultimate-editor .red-ui-button.au-btn-success:hover {
|
|
297
|
+
background: #15803d !important;
|
|
298
|
+
}
|
|
299
|
+
</style>
|
|
300
|
+
|
|
301
|
+
<div class="alarm-ultimate-editor">
|
|
180
302
|
<div class="form-row">
|
|
181
303
|
<b>Alarm System Ultimate (BETA)</b>
|
|
182
304
|
<span style="color:red"
|
|
@@ -190,7 +312,7 @@
|
|
|
190
312
|
|
|
191
313
|
<div class="form-row">
|
|
192
314
|
<label>WEB PAGE</label>
|
|
193
|
-
<button type="button" class="red-ui-button" id="node-input-zones-panel">
|
|
315
|
+
<button type="button" class="red-ui-button au-btn au-btn-primary" id="node-input-zones-panel">
|
|
194
316
|
<i class="fa fa-keyboard-o"></i> Panel
|
|
195
317
|
</button>
|
|
196
318
|
</div>
|
|
@@ -217,14 +339,6 @@
|
|
|
217
339
|
<input type="text" id="node-input-payloadPropName" />
|
|
218
340
|
</div>
|
|
219
341
|
|
|
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
342
|
<div class="form-row">
|
|
229
343
|
<label for="node-input-persistState"
|
|
230
344
|
><i class="fa fa-database"></i> Persist state</label
|
|
@@ -434,42 +548,16 @@
|
|
|
434
548
|
</div>
|
|
435
549
|
|
|
436
550
|
<div class="form-row">
|
|
437
|
-
<label
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
>
|
|
441
|
-
<
|
|
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>
|
|
551
|
+
<label><i class="fa fa-th-large"></i> Zones</label>
|
|
552
|
+
<button type="button" class="red-ui-button au-btn au-btn-success" id="node-input-zones-manage">
|
|
553
|
+
<i class="fa fa-list"></i> Manage zones
|
|
554
|
+
</button>
|
|
555
|
+
<span id="node-input-zones-summary" style="margin-left:8px; color:#777;"></span>
|
|
448
556
|
</div>
|
|
449
557
|
|
|
450
|
-
<
|
|
451
|
-
|
|
452
|
-
|
|
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>
|
|
558
|
+
<textarea id="node-input-zones" style="display:none;"></textarea>
|
|
559
|
+
</div>
|
|
560
|
+
</script>
|
|
473
561
|
|
|
474
562
|
<script type="text/markdown" data-help-name="AlarmSystemUltimate">
|
|
475
563
|
<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>
|
|
@@ -477,7 +565,7 @@
|
|
|
477
565
|
The node has 2 inputs "in one":
|
|
478
566
|
|
|
479
567
|
- **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`)
|
|
568
|
+
- **Sensor messages**: any other message, matched to a zone by `msg.topic` and converted to boolean using **With Input** (default `payload`).
|
|
481
569
|
|
|
482
570
|
<br/>
|
|
483
571
|
|
|
@@ -485,7 +573,6 @@
|
|
|
485
573
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
486
574
|
| Control topic | Topic that receives runtime commands (arm/disarm/status/bypass/siren/panic/reset). |
|
|
487
575
|
| 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
576
|
| Persist state | Persists arming state, bypass list and event log across restarts. |
|
|
490
577
|
| 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
578
|
| Require code for disarm | Requires a valid PIN for disarming commands (via `msg.code` or `msg.pin`). If **Code** is empty, commands are allowed. |
|
|
@@ -503,13 +590,13 @@
|
|
|
503
590
|
| Latch siren until disarm | Forces siren to remain on until disarm (ignores duration). |
|
|
504
591
|
| Emit restore events | Emits `zone_restore` when a zone returns to false. |
|
|
505
592
|
| Event log size | Max stored log entries in node context (0 disables log). |
|
|
506
|
-
| Zones | Zone definitions.
|
|
593
|
+
| Zones | Zone definitions. Use **Manage zones** to edit with the web tool (advanced JSON is available there). |
|
|
507
594
|
|
|
508
595
|
<br/>
|
|
509
596
|
|
|
510
597
|
### Web tools
|
|
511
598
|
|
|
512
|
-
- **Zones
|
|
599
|
+
- **Zones Manager**: available at `/alarm-ultimate/alarm-json-mapper` (or via the **Manage zones** button).
|
|
513
600
|
- **Alarm Panel**: available at `/alarm-ultimate/alarm-panel` (or via the **Panel** button).
|
|
514
601
|
- The Alarm Panel supports `?id=<alarmNodeId>` and an embed mode for Dashboard: `?embed=1&id=<alarmNodeId>`.
|
|
515
602
|
|
|
@@ -544,7 +631,7 @@
|
|
|
544
631
|
|
|
545
632
|
**Output 5 (Zone Activity)**
|
|
546
633
|
|
|
547
|
-
- Same message as Output 1, only for zone events (`bypassed`, `unbypassed`, `chime`, `zone_ignored_exit`, `zone_bypassed_trigger`, `zone_restore`).
|
|
634
|
+
- Same message as Output 1, only for zone events (`zone_open`, `zone_close`, `bypassed`, `unbypassed`, `chime`, `zone_ignored_exit`, `zone_bypassed_trigger`, `zone_restore`).
|
|
548
635
|
|
|
549
636
|
**Output 6 (Errors/Denied)**
|
|
550
637
|
|
|
@@ -582,9 +669,11 @@
|
|
|
582
669
|
|
|
583
670
|
Zones / alarm:
|
|
584
671
|
|
|
585
|
-
- `entry_delay` → `{ zone:{id,name}, seconds }`
|
|
672
|
+
- `entry_delay` → `{ zone:{id,name,type,topic}, seconds }`
|
|
586
673
|
- `alarm` → `{ kind, zone, silent }` where `kind` can be `instant|entry_timeout|panic|duress|fire|tamper|24h|...`
|
|
587
674
|
- `chime` → `{ zone:{id,name,type} }` (while disarmed)
|
|
675
|
+
- `zone_open` → `{ zone:{id,name,type,topic}, open:true, bypassed }` (always)
|
|
676
|
+
- `zone_close` → `{ zone:{id,name,type,topic}, open:false, bypassed }` (always)
|
|
588
677
|
- `zone_ignored_exit` → `{ zone:{id,name,type} }` (triggered during exit delay and not allowed)
|
|
589
678
|
- `zone_bypassed_trigger` → `{ zone:{id,name,type} }`
|
|
590
679
|
- `zone_restore` → `{ zone:{id,name,type} }` (when enabled)
|
|
@@ -88,7 +88,8 @@ module.exports = function (RED) {
|
|
|
88
88
|
try {
|
|
89
89
|
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
90
90
|
api.command(body);
|
|
91
|
-
|
|
91
|
+
const snapshot = api.getState && typeof api.getState === 'function' ? api.getState() : null;
|
|
92
|
+
res.json({ ok: true, result: snapshot });
|
|
92
93
|
} catch (err) {
|
|
93
94
|
res.status(500).json({ ok: false, error: err.message });
|
|
94
95
|
}
|
|
@@ -255,6 +256,8 @@ module.exports = function (RED) {
|
|
|
255
256
|
'bypassed',
|
|
256
257
|
'unbypassed',
|
|
257
258
|
'chime',
|
|
259
|
+
'zone_open',
|
|
260
|
+
'zone_close',
|
|
258
261
|
'zone_ignored_exit',
|
|
259
262
|
'zone_bypassed_trigger',
|
|
260
263
|
'zone_restore',
|
|
@@ -550,6 +553,22 @@ module.exports = function (RED) {
|
|
|
550
553
|
let shape = 'ring';
|
|
551
554
|
let text = 'DISARMED';
|
|
552
555
|
|
|
556
|
+
// When idle/disarmed, show recent arming errors to make the reason visible in the editor status.
|
|
557
|
+
if (!state.alarmActive && !state.entry && !state.arming && state.mode === 'disarmed') {
|
|
558
|
+
const last = Array.isArray(state.log) && state.log.length ? state.log[state.log.length - 1] : null;
|
|
559
|
+
const evt = last && typeof last.event === 'string' ? last.event : '';
|
|
560
|
+
if (evt === 'arm_blocked') {
|
|
561
|
+
const violations = Array.isArray(last.violations) ? last.violations.length : 0;
|
|
562
|
+
fill = 'yellow';
|
|
563
|
+
shape = 'ring';
|
|
564
|
+
text = `ARM BLOCKED${violations ? ` (${violations})` : ''}`;
|
|
565
|
+
} else if (evt === 'denied' && last && String(last.action || '') === 'arm') {
|
|
566
|
+
fill = 'red';
|
|
567
|
+
shape = 'ring';
|
|
568
|
+
text = 'ARM DENIED';
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
553
572
|
if (state.alarmActive) {
|
|
554
573
|
fill = 'red';
|
|
555
574
|
shape = 'dot';
|
|
@@ -1107,7 +1126,7 @@ module.exports = function (RED) {
|
|
|
1107
1126
|
}
|
|
1108
1127
|
const until = now() + delay;
|
|
1109
1128
|
state.entry = { zoneId: zone.id, until };
|
|
1110
|
-
emitEvent('entry_delay', { zone:
|
|
1129
|
+
emitEvent('entry_delay', { zone: buildZoneSummary(zone), seconds: remainingSeconds(until) }, baseMsg);
|
|
1111
1130
|
startStatusInterval();
|
|
1112
1131
|
clearEntryTimer();
|
|
1113
1132
|
entryTimer = timerBag.setTimeout(() => {
|
|
@@ -1177,7 +1196,7 @@ module.exports = function (RED) {
|
|
|
1177
1196
|
}
|
|
1178
1197
|
state.bypass[id] = Boolean(enabled);
|
|
1179
1198
|
persist();
|
|
1180
|
-
emitEvent(enabled ? 'bypassed' : 'unbypassed', { zone:
|
|
1199
|
+
emitEvent(enabled ? 'bypassed' : 'unbypassed', { zone: buildZoneSummary(zone) }, baseMsg);
|
|
1181
1200
|
}
|
|
1182
1201
|
|
|
1183
1202
|
function handleControlMessage(msg) {
|
|
@@ -1278,7 +1297,7 @@ module.exports = function (RED) {
|
|
|
1278
1297
|
if (!zone) {
|
|
1279
1298
|
return;
|
|
1280
1299
|
}
|
|
1281
|
-
const resolved = helpers.resolveInput(msg, payloadPropName,
|
|
1300
|
+
const resolved = helpers.resolveInput(msg, payloadPropName, null, RED);
|
|
1282
1301
|
const value = resolved.boolean;
|
|
1283
1302
|
if (value === undefined) {
|
|
1284
1303
|
return;
|
|
@@ -1290,8 +1309,20 @@ module.exports = function (RED) {
|
|
|
1290
1309
|
zoneMeta.lastChangeAt = now();
|
|
1291
1310
|
state.zoneState[zone.id] = zoneMeta;
|
|
1292
1311
|
|
|
1312
|
+
if (changed) {
|
|
1313
|
+
emitEvent(
|
|
1314
|
+
value === true ? 'zone_open' : 'zone_close',
|
|
1315
|
+
{
|
|
1316
|
+
zone: buildZoneSummary(zone),
|
|
1317
|
+
open: value === true,
|
|
1318
|
+
bypassed: state.bypass[zone.id] === true,
|
|
1319
|
+
},
|
|
1320
|
+
msg
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1293
1324
|
if (changed && emitRestoreEvents && value === false) {
|
|
1294
|
-
emitEvent('zone_restore', { zone:
|
|
1325
|
+
emitEvent('zone_restore', { zone: buildZoneSummary(zone) }, msg);
|
|
1295
1326
|
}
|
|
1296
1327
|
|
|
1297
1328
|
if (changed) {
|
|
@@ -1317,7 +1348,7 @@ module.exports = function (RED) {
|
|
|
1317
1348
|
}
|
|
1318
1349
|
|
|
1319
1350
|
if (state.bypass[zone.id] === true && zone.bypassable !== false) {
|
|
1320
|
-
emitEvent('zone_bypassed_trigger', { zone:
|
|
1351
|
+
emitEvent('zone_bypassed_trigger', { zone: buildZoneSummary(zone) }, msg);
|
|
1321
1352
|
return;
|
|
1322
1353
|
}
|
|
1323
1354
|
|
|
@@ -1335,13 +1366,13 @@ module.exports = function (RED) {
|
|
|
1335
1366
|
}
|
|
1336
1367
|
|
|
1337
1368
|
if (state.arming && !zone.instantDuringExit) {
|
|
1338
|
-
emitEvent('zone_ignored_exit', { zone:
|
|
1369
|
+
emitEvent('zone_ignored_exit', { zone: buildZoneSummary(zone) }, msg);
|
|
1339
1370
|
return;
|
|
1340
1371
|
}
|
|
1341
1372
|
|
|
1342
1373
|
if (state.mode === 'disarmed') {
|
|
1343
1374
|
if (zone.chime) {
|
|
1344
|
-
emitEvent('chime', { zone:
|
|
1375
|
+
emitEvent('chime', { zone: buildZoneSummary(zone) }, msg);
|
|
1345
1376
|
}
|
|
1346
1377
|
return;
|
|
1347
1378
|
}
|