node-red-contrib-alarm-ultimate 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,335 @@
1
+ [
2
+ {
3
+ "id": "b3b0c2c1a9e7d001",
4
+ "type": "tab",
5
+ "label": "Alarm Ultimate - Home Assistant (Alarm Panel)",
6
+ "disabled": false,
7
+ "info": ""
8
+ },
9
+ {
10
+ "id": "8b2d0e6a71a54e10",
11
+ "type": "comment",
12
+ "z": "b3b0c2c1a9e7d001",
13
+ "name": "Home Assistant integration (no MQTT)",
14
+ "info": "Requires `node-red-contrib-home-assistant-websocket`.\n\nThis example is designed for Node-RED running as the Home Assistant Add-on.\n\nWhat it does:\n- Takes Home Assistant `binary_sensor.*` state changes (\"on\"/\"off\") and feeds Alarm zones.\n- Receives arm/disarm commands from a Home Assistant Template Alarm Control Panel via an HA event.\n- Mirrors Alarm events back into Home Assistant by updating `input_select.alarm_ultimate_state`.\n\nYou must create in Home Assistant:\n1) An input_select named `input_select.alarm_ultimate_state` with options:\n - disarmed\n - arming\n - armed_away\n - armed_home\n - pending\n - triggered\n2) A Template Alarm Control Panel (example YAML):\n\ntemplate:\n - alarm_control_panel:\n - name: \"Alarm Ultimate\"\n unique_id: alarm_ultimate\n state: \"{{ states('input_select.alarm_ultimate_state') }}\"\n code_format: no_code\n arm_away:\n - event: alarm_ultimate_command\n event_data:\n action: arm_away\n arm_home:\n - event: alarm_ultimate_command\n event_data:\n action: arm_home\n disarm:\n - event: alarm_ultimate_command\n event_data:\n action: disarm\n trigger:\n - event: alarm_ultimate_command\n event_data:\n action: trigger\n\nThen add the standard HA \"Alarm panel\" card and select this entity.\n\nEdit these entities in this flow:\n- binary_sensor.front_door_contact\n- binary_sensor.living_room_motion\n\nAnd edit the Alarm zones topic to match those entity_ids (already set in this example).\n",
15
+ "x": 330,
16
+ "y": 60,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "a6c9d8f2b7f24a10",
21
+ "type": "server",
22
+ "name": "Home Assistant",
23
+ "addon": true,
24
+ "rejectUnauthorizedCerts": true,
25
+ "ha_boolean": "y|yes|true|on|home|open",
26
+ "connectionDelay": true,
27
+ "cacheJson": true,
28
+ "heartbeat": false,
29
+ "heartbeatInterval": "30",
30
+ "areaSelector": "id",
31
+ "deviceSelector": "id",
32
+ "entitySelector": "id",
33
+ "statusSeparator": "at: ",
34
+ "statusYear": "hidden",
35
+ "statusMonth": "short",
36
+ "statusDay": "numeric",
37
+ "statusHourCycle": "default",
38
+ "statusTimeFormat": "h:m",
39
+ "enableGlobalContextStore": false,
40
+ "version": 5
41
+ },
42
+ {
43
+ "id": "d0c1b2a3f4e50617",
44
+ "type": "AlarmSystemUltimate",
45
+ "z": "b3b0c2c1a9e7d001",
46
+ "name": "Home Alarm",
47
+ "controlTopic": "alarm",
48
+ "payloadPropName": "payload",
49
+ "persistState": true,
50
+ "requireCodeForArm": false,
51
+ "requireCodeForDisarm": false,
52
+ "armCode": "",
53
+ "duressCode": "",
54
+ "blockArmOnViolations": true,
55
+ "exitDelaySeconds": 10,
56
+ "entryDelaySeconds": 30,
57
+ "emitOpenZonesDuringArming": false,
58
+ "openZonesArmingIntervalSeconds": 1,
59
+ "openZonesRequestTopic": "alarm/listOpenZones",
60
+ "openZonesRequestIntervalSeconds": 0,
61
+ "sirenDurationSeconds": 180,
62
+ "sirenLatchUntilDisarm": false,
63
+ "sirenTopic": "alarm/siren",
64
+ "sirenOnPayload": true,
65
+ "sirenOnPayloadType": "bool",
66
+ "sirenOffPayload": false,
67
+ "sirenOffPayloadType": "bool",
68
+ "emitRestoreEvents": false,
69
+ "maxLogEntries": 200,
70
+ "zones": "[\n {\n \"id\": \"front_door\",\n \"name\": \"Front door\",\n \"topic\": \"binary_sensor.front_door_contact\",\n \"type\": \"perimeter\",\n \"entry\": true,\n \"bypassable\": true,\n \"chime\": true\n },\n {\n \"id\": \"living_pir\",\n \"name\": \"Living room motion\",\n \"topic\": \"binary_sensor.living_room_motion\",\n \"type\": \"motion\",\n \"entry\": false,\n \"bypassable\": true,\n \"cooldownSeconds\": 10\n }\n]",
71
+ "x": 760,
72
+ "y": 240,
73
+ "wires": [
74
+ [
75
+ "e3e4f5060718293a",
76
+ "3f50b6a7c1c8b3d10"
77
+ ],
78
+ [
79
+ "3f50b6a7c1c8b3d11"
80
+ ],
81
+ [],
82
+ [],
83
+ [],
84
+ [
85
+ "3f50b6a7c1c8b3d12"
86
+ ],
87
+ [],
88
+ [],
89
+ []
90
+ ]
91
+ },
92
+ {
93
+ "id": "c44ac2b6af1a1e2a",
94
+ "type": "AlarmUltimateInputAdapter",
95
+ "z": "b3b0c2c1a9e7d001",
96
+ "name": "HA sensors on/off → boolean",
97
+ "presetSource": "builtin",
98
+ "presetId": "home_assistant_on_off",
99
+ "userCode": "return msg;",
100
+ "x": 500,
101
+ "y": 240,
102
+ "wires": [
103
+ [
104
+ "d0c1b2a3f4e50617"
105
+ ]
106
+ ]
107
+ },
108
+ {
109
+ "id": "c2b3a4d5e6f70819",
110
+ "type": "server-state-changed",
111
+ "z": "b3b0c2c1a9e7d001",
112
+ "name": "HA binary_sensor.* → Alarm zones",
113
+ "server": "a6c9d8f2b7f24a10",
114
+ "version": 6,
115
+ "outputs": 1,
116
+ "exposeAsEntityConfig": "",
117
+ "entities": {
118
+ "entity": [
119
+ "binary_sensor.front_door_contact",
120
+ "binary_sensor.living_room_motion"
121
+ ],
122
+ "substring": [],
123
+ "regex": []
124
+ },
125
+ "outputInitially": true,
126
+ "stateType": "str",
127
+ "ifState": "",
128
+ "ifStateType": "str",
129
+ "ifStateOperator": "is",
130
+ "outputOnlyOnStateChange": true,
131
+ "for": "0",
132
+ "forType": "num",
133
+ "forUnits": "seconds",
134
+ "ignorePrevStateNull": false,
135
+ "ignorePrevStateUnknown": false,
136
+ "ignorePrevStateUnavailable": false,
137
+ "ignoreCurrentStateUnknown": false,
138
+ "ignoreCurrentStateUnavailable": false,
139
+ "outputProperties": [
140
+ {
141
+ "property": "topic",
142
+ "propertyType": "msg",
143
+ "value": "triggerId",
144
+ "valueType": "eventData"
145
+ },
146
+ {
147
+ "property": "payload",
148
+ "propertyType": "msg",
149
+ "value": "new_state.state",
150
+ "valueType": "eventData"
151
+ }
152
+ ],
153
+ "x": 220,
154
+ "y": 240,
155
+ "wires": [
156
+ [
157
+ "c44ac2b6af1a1e2a"
158
+ ]
159
+ ]
160
+ },
161
+ {
162
+ "id": "e1f2a3b4c5d60718",
163
+ "type": "server-events",
164
+ "z": "b3b0c2c1a9e7d001",
165
+ "name": "HA event: alarm_ultimate_command",
166
+ "server": "a6c9d8f2b7f24a10",
167
+ "version": 3,
168
+ "exposeAsEntityConfig": "",
169
+ "eventType": "alarm_ultimate_command",
170
+ "eventData": "",
171
+ "waitForRunning": true,
172
+ "outputProperties": [
173
+ {
174
+ "property": "payload",
175
+ "propertyType": "msg",
176
+ "value": "",
177
+ "valueType": "eventData"
178
+ }
179
+ ],
180
+ "x": 230,
181
+ "y": 380,
182
+ "wires": [
183
+ [
184
+ "f2a3b4c5d6e70819"
185
+ ]
186
+ ]
187
+ },
188
+ {
189
+ "id": "f2a3b4c5d6e70819",
190
+ "type": "function",
191
+ "z": "b3b0c2c1a9e7d001",
192
+ "name": "HA command → Alarm control msg",
193
+ "func": "const root = msg && typeof msg === \"object\" ? msg : {};\nconst eventData = root.payload && typeof root.payload === \"object\" ? root.payload : {};\nconst ev = eventData.event && typeof eventData.event === \"object\" ? eventData.event : {};\n\nconst action = String(ev.action || ev.command || \"\").trim().toLowerCase();\nconst mode = String(ev.mode || \"\").trim().toLowerCase();\nconst code = typeof ev.code === \"string\" ? ev.code.trim() : \"\";\n\nif (!action) return null;\n\nconst out = { topic: \"alarm\" };\n\nif (action === \"disarm\") {\n out.command = \"disarm\";\n} else if (action === \"trigger\" || action === \"panic\") {\n out.command = \"panic\";\n} else if (action.startsWith(\"arm\")) {\n out.command = \"arm\";\n} else {\n return null;\n}\n\nif (code) out.code = code;\nif (mode) {\n // AlarmSystemUltimate maps legacy modes to \"armed\".\n out.mode = mode;\n}\n\n// Remember last requested arm mode to map HA state on \"armed\" events.\nif (out.command === \"arm\") {\n const map = {\n arm_home: \"armed_home\",\n arm_away: \"armed_away\",\n arm_night: \"armed_home\",\n arm: \"armed_away\",\n };\n flow.set(\"alarmUltimate:lastArmMode\", map[action] || \"armed_away\");\n}\n\nreturn out;",
194
+ "outputs": 1,
195
+ "noerr": 0,
196
+ "initialize": "",
197
+ "finalize": "",
198
+ "libs": [],
199
+ "x": 520,
200
+ "y": 380,
201
+ "wires": [
202
+ [
203
+ "d0c1b2a3f4e50617"
204
+ ]
205
+ ]
206
+ },
207
+ {
208
+ "id": "e3e4f5060718293a",
209
+ "type": "function",
210
+ "z": "b3b0c2c1a9e7d001",
211
+ "name": "Alarm events → HA alarm state",
212
+ "func": "const evt = msg && typeof msg.event === \"string\" ? msg.event : \"\";\nlet haState = null;\n\nif (evt === \"disarmed\" || evt === \"reset\") {\n haState = \"disarmed\";\n} else if (evt === \"arming\") {\n haState = \"arming\";\n} else if (evt === \"armed\") {\n haState = flow.get(\"alarmUltimate:lastArmMode\") || \"armed_away\";\n} else if (evt === \"entry_delay\") {\n haState = \"pending\";\n} else if (evt === \"alarm\") {\n haState = \"triggered\";\n}\n\nif (!haState) return null;\n\nmsg.haState = haState;\nreturn msg;",
213
+ "outputs": 1,
214
+ "noerr": 0,
215
+ "initialize": "",
216
+ "finalize": "",
217
+ "libs": [],
218
+ "x": 520,
219
+ "y": 500,
220
+ "wires": [
221
+ [
222
+ "a1b2c3d4e5f60718"
223
+ ]
224
+ ]
225
+ },
226
+ {
227
+ "id": "a1b2c3d4e5f60718",
228
+ "type": "api-call-service",
229
+ "z": "b3b0c2c1a9e7d001",
230
+ "name": "Set input_select.alarm_ultimate_state",
231
+ "server": "a6c9d8f2b7f24a10",
232
+ "version": 5,
233
+ "debugenabled": false,
234
+ "domain": "input_select",
235
+ "service": "select_option",
236
+ "areaId": [],
237
+ "deviceId": [],
238
+ "entityId": [
239
+ "input_select.alarm_ultimate_state"
240
+ ],
241
+ "data": "{\"option\":haState}",
242
+ "dataType": "jsonata",
243
+ "mergeContext": "",
244
+ "mustacheAltTags": false,
245
+ "outputProperties": [],
246
+ "queue": "none",
247
+ "x": 830,
248
+ "y": 500,
249
+ "wires": [
250
+ []
251
+ ]
252
+ },
253
+ {
254
+ "id": "b0a1c2d3e4f50607",
255
+ "type": "inject",
256
+ "z": "b3b0c2c1a9e7d001",
257
+ "name": "Sync on deploy (status)",
258
+ "props": [
259
+ {
260
+ "p": "topic",
261
+ "v": "alarm",
262
+ "vt": "str"
263
+ },
264
+ {
265
+ "p": "command",
266
+ "v": "status",
267
+ "vt": "str"
268
+ }
269
+ ],
270
+ "repeat": "",
271
+ "crontab": "",
272
+ "once": true,
273
+ "onceDelay": 0.5,
274
+ "topic": "",
275
+ "x": 260,
276
+ "y": 500,
277
+ "wires": [
278
+ [
279
+ "d0c1b2a3f4e50617"
280
+ ]
281
+ ]
282
+ },
283
+ {
284
+ "id": "3f50b6a7c1c8b3d10",
285
+ "type": "debug",
286
+ "z": "b3b0c2c1a9e7d001",
287
+ "name": "All events",
288
+ "active": true,
289
+ "tosidebar": true,
290
+ "console": false,
291
+ "tostatus": false,
292
+ "complete": "true",
293
+ "targetType": "full",
294
+ "statusVal": "",
295
+ "statusType": "auto",
296
+ "x": 1040,
297
+ "y": 200,
298
+ "wires": []
299
+ },
300
+ {
301
+ "id": "3f50b6a7c1c8b3d11",
302
+ "type": "debug",
303
+ "z": "b3b0c2c1a9e7d001",
304
+ "name": "Siren output",
305
+ "active": true,
306
+ "tosidebar": true,
307
+ "console": false,
308
+ "tostatus": false,
309
+ "complete": "true",
310
+ "targetType": "full",
311
+ "statusVal": "",
312
+ "statusType": "auto",
313
+ "x": 1050,
314
+ "y": 240,
315
+ "wires": []
316
+ },
317
+ {
318
+ "id": "3f50b6a7c1c8b3d12",
319
+ "type": "debug",
320
+ "z": "b3b0c2c1a9e7d001",
321
+ "name": "Errors / denied",
322
+ "active": true,
323
+ "tosidebar": true,
324
+ "console": false,
325
+ "tostatus": false,
326
+ "complete": "true",
327
+ "targetType": "full",
328
+ "statusVal": "",
329
+ "statusType": "auto",
330
+ "x": 1060,
331
+ "y": 280,
332
+ "wires": []
333
+ }
334
+ ]
335
+
@@ -2,15 +2,16 @@
2
2
  RED.nodes.registerType("AlarmSystemUltimate", {
3
3
  category: "Alarm Ultimate",
4
4
  color: "#A8DADC",
5
- defaults: {
6
- name: { value: "" },
7
- controlTopic: { value: "alarm" },
8
- payloadPropName: { value: "payload", required: false },
9
- persistState: { value: true },
10
- requireCodeForArm: { value: false },
11
- requireCodeForDisarm: { value: true },
12
- armCode: { value: "" },
13
- duressCode: { value: "" },
5
+ defaults: {
6
+ name: { value: "" },
7
+ controlTopic: { value: "alarm" },
8
+ payloadPropName: { value: "payload", required: false },
9
+ persistState: { value: true },
10
+ syncTargets: { value: "" },
11
+ requireCodeForArm: { value: false },
12
+ requireCodeForDisarm: { value: true },
13
+ armCode: { value: "" },
14
+ duressCode: { value: "" },
14
15
  blockArmOnViolations: { value: true },
15
16
  exitDelaySeconds: { value: 30, validate: RED.validators.number() },
16
17
  entryDelaySeconds: { value: 30, validate: RED.validators.number() },
@@ -55,8 +56,8 @@
55
56
  paletteLabel: function () {
56
57
  return "Alarm System (BETA)";
57
58
  },
58
- oneditprepare: function () {
59
- const nodeId = this.id;
59
+ oneditprepare: function () {
60
+ const nodeId = this.id;
60
61
  const origin = window.location.origin;
61
62
  const openerOrigin = origin;
62
63
  const bc =
@@ -85,10 +86,12 @@
85
86
  this.sirenOffPayloadType || "bool",
86
87
  );
87
88
 
88
- const zonesField = $("#node-input-zones");
89
- const zonesSummary = $("#node-input-zones-summary");
89
+ const zonesField = $("#node-input-zones");
90
+ const zonesSummary = $("#node-input-zones-summary");
91
+ const syncTargetsField = $("#node-input-syncTargets");
92
+ const syncTargetsUi = $("#node-input-syncTargets-ui");
90
93
 
91
- function parseZonesText(text) {
94
+ function parseZonesText(text) {
92
95
  const raw = String(text || "").trim();
93
96
  if (!raw) return [];
94
97
 
@@ -119,9 +122,131 @@
119
122
  }
120
123
  }
121
124
  return zones;
122
- }
125
+ }
126
+
127
+ function parseSyncTargets(text) {
128
+ const raw = String(text || "").trim();
129
+ if (!raw) return {};
130
+ try {
131
+ const parsed = JSON.parse(raw);
132
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
133
+ } catch (_err) {
134
+ return {};
135
+ }
136
+ }
137
+
138
+ function normalizeSyncAction(value) {
139
+ const v = String(value || "").trim().toLowerCase();
140
+ if (v === "arm") return "arm";
141
+ if (v === "disarm") return "disarm";
142
+ return "leave";
143
+ }
123
144
 
124
- function updateZonesSummary() {
145
+ function syncActionLabel(value) {
146
+ const v = normalizeSyncAction(value);
147
+ if (v === "arm") return "Arm";
148
+ if (v === "disarm") return "Disarm";
149
+ return "Leave as is";
150
+ }
151
+
152
+ function escapeHtml(text) {
153
+ return String(text || "")
154
+ .replace(/&/g, "&")
155
+ .replace(/</g, "&lt;")
156
+ .replace(/>/g, "&gt;")
157
+ .replace(/"/g, "&quot;")
158
+ .replace(/'/g, "&#039;");
159
+ }
160
+
161
+ function buildSyncTargetsUi() {
162
+ if (!syncTargetsUi || !syncTargetsUi.length) return;
163
+ try {
164
+ const saved = parseSyncTargets(syncTargetsField.val());
165
+
166
+ // Collect other Alarm nodes in the workspace.
167
+ const other = [];
168
+ try {
169
+ if (RED && RED.nodes && typeof RED.nodes.eachNode === "function") {
170
+ RED.nodes.eachNode((n) => {
171
+ if (!n) return;
172
+ const type = String(n.type || "");
173
+ if (type !== "AlarmSystemUltimate") return;
174
+ if (n.id === nodeId) return;
175
+ other.push({ id: n.id, name: String(n.name || "").trim() });
176
+ });
177
+ }
178
+ } catch (_err) {}
179
+
180
+ other.sort((a, b) => {
181
+ const an = (a.name || a.id).toLowerCase();
182
+ const bn = (b.name || b.id).toLowerCase();
183
+ return an.localeCompare(bn);
184
+ });
185
+
186
+ if (other.length === 0) {
187
+ syncTargetsUi.html('<span style="color:#777;">No other Alarm nodes found.</span>');
188
+ return;
189
+ }
190
+
191
+ const table = $('<table style="width:100%; border-collapse:collapse;"></table>');
192
+ table.append(
193
+ '<thead><tr>' +
194
+ '<th style="text-align:left; padding:4px 6px;">Alarm</th>' +
195
+ '<th style="text-align:left; padding:4px 6px; width:160px;">On ARM</th>' +
196
+ '<th style="text-align:left; padding:4px 6px; width:160px;">On DISARM</th>' +
197
+ '</tr></thead>',
198
+ );
199
+
200
+ const tbody = $("<tbody></tbody>");
201
+ other.forEach((n) => {
202
+ const rule = saved[n.id] && typeof saved[n.id] === "object" ? saved[n.id] : {};
203
+ const onArm = normalizeSyncAction(rule.onArm);
204
+ const onDisarm = normalizeSyncAction(rule.onDisarm);
205
+
206
+ const armSel = $('<select style="width:100%;"></select>');
207
+ const disarmSel = $('<select style="width:100%;"></select>');
208
+ ["arm", "leave", "disarm"].forEach((v) => {
209
+ armSel.append(`<option value="${v}">${syncActionLabel(v)}</option>`);
210
+ disarmSel.append(`<option value="${v}">${syncActionLabel(v)}</option>`);
211
+ });
212
+ armSel.val(onArm);
213
+ disarmSel.val(onDisarm);
214
+
215
+ const tr = $("<tr></tr>");
216
+ const label = n.name ? `${n.name}` : "(unnamed Alarm)";
217
+ tr.append(
218
+ `<td style="padding:4px 6px;">${escapeHtml(label)}</td>`,
219
+ );
220
+ const tdArm = $('<td style="padding:4px 6px;"></td>').append(armSel);
221
+ const tdDisarm = $('<td style="padding:4px 6px;"></td>').append(disarmSel);
222
+ tr.append(tdArm).append(tdDisarm);
223
+ tbody.append(tr);
224
+
225
+ function persist() {
226
+ const next = parseSyncTargets(syncTargetsField.val());
227
+ next[n.id] = {
228
+ onArm: String(armSel.val() || "leave"),
229
+ onDisarm: String(disarmSel.val() || "leave"),
230
+ };
231
+ syncTargetsField.val(JSON.stringify(next));
232
+ try {
233
+ syncTargetsField.trigger("change");
234
+ } catch (_err) {}
235
+ }
236
+
237
+ armSel.on("change", persist);
238
+ disarmSel.on("change", persist);
239
+ });
240
+ table.append(tbody);
241
+ syncTargetsUi.empty().append(table);
242
+ } catch (err) {
243
+ syncTargetsUi.html(
244
+ `<span style="color:#c00;">Unable to render sync list. Check browser console. (${escapeHtml(err && err.message ? err.message : err)})</span>`,
245
+ );
246
+ }
247
+ }
248
+
249
+ function updateZonesSummary() {
125
250
  try {
126
251
  const zones = parseZonesText(zonesField.val());
127
252
  zonesSummary.text(zones.length ? `${zones.length} zones configured` : "No zones configured");
@@ -216,7 +341,7 @@
216
341
  }
217
342
  };
218
343
 
219
- window.addEventListener("message", messageListener);
344
+ window.addEventListener("message", messageListener);
220
345
  if (bc) {
221
346
  bc.addEventListener("message", (ev) => {
222
347
  const data = ev && ev.data && typeof ev.data === "object" ? ev.data : null;
@@ -227,9 +352,10 @@
227
352
  });
228
353
  this._alarmUltimateZonesBroadcast = bc;
229
354
  }
230
- this._alarmUltimateZonesMessageListener = messageListener;
231
- updateZonesSummary();
232
- },
355
+ this._alarmUltimateZonesMessageListener = messageListener;
356
+ updateZonesSummary();
357
+ setTimeout(() => buildSyncTargetsUi(), 0);
358
+ },
233
359
  oneditsave: function () {
234
360
  if (this._alarmUltimateZonesMessageListener) {
235
361
  window.removeEventListener("message", this._alarmUltimateZonesMessageListener);
@@ -317,10 +443,21 @@
317
443
  </button>
318
444
  </div>
319
445
 
320
- <div class="form-row">
321
- <label for="node-input-name"
322
- ><i class="icon-tag"></i> Name</label
323
- >
446
+ <div class="form-row">
447
+ <label><i class="fa fa-random"></i> Sync other alarms</label>
448
+ <div style="width: 70%;">
449
+ <div class="form-tips" style="margin: 0 0 6px 0;">
450
+ When this Alarm is armed/disarmed, optionally arm/disarm other Alarm nodes in the workspace (codes are forwarded if present).
451
+ </div>
452
+ <div id="node-input-syncTargets-ui"></div>
453
+ <input type="hidden" id="node-input-syncTargets" />
454
+ </div>
455
+ </div>
456
+
457
+ <div class="form-row">
458
+ <label for="node-input-name"
459
+ ><i class="icon-tag"></i> Name</label
460
+ >
324
461
  <input type="text" id="node-input-name" placeholder="Name" />
325
462
  </div>
326
463
 
@@ -587,10 +724,11 @@
587
724
  | Siren topic | Topic used on output 2 to turn the siren on/off (default: `controlTopic + "/siren"`). |
588
725
  | Siren ON/OFF payload | Values emitted on output 2 for siren on/off (typed). |
589
726
  | Siren duration (s) | Auto stop duration (`0` = latch until disarm). |
590
- | Latch siren until disarm | Forces siren to remain on until disarm (ignores duration). |
591
- | Emit restore events | Emits `zone_restore` when a zone returns to false. |
592
- | Event log size | Max stored log entries in node context (0 disables log). |
593
- | Zones | Zone definitions. Use **Manage zones** to edit with the web tool (advanced JSON is available there). |
727
+ | Latch siren until disarm | Forces siren to remain on until disarm (ignores duration). |
728
+ | Emit restore events | Emits `zone_restore` when a zone returns to false. |
729
+ | Event log size | Max stored log entries in node context (0 disables log). |
730
+ | Sync other alarms | Optional: when this node is armed/disarmed, it can arm/disarm other Alarm nodes. The original `msg.code`/`msg.pin` are forwarded. |
731
+ | Zones | Zone definitions. Use **Manage zones** to edit with the web tool (advanced JSON is available there). |
594
732
 
595
733
  <br/>
596
734