node-red-contrib-alarm-ultimate 0.1.1 → 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.
@@ -32,6 +32,19 @@ function writeJsonFileAtomicSync(filePath, data) {
32
32
  module.exports = function (RED) {
33
33
  const helpers = require('./lib/node-helpers.js');
34
34
 
35
+ function parseJsonObject(value) {
36
+ const raw = String(value || '').trim();
37
+ if (!raw) {
38
+ return null;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
43
+ } catch (_err) {
44
+ return null;
45
+ }
46
+ }
47
+
35
48
  if (RED && RED.httpAdmin && typeof RED.httpAdmin.get === 'function') {
36
49
  const needsRead =
37
50
  RED.auth && typeof RED.auth.needsPermission === 'function'
@@ -79,6 +92,27 @@ module.exports = function (RED) {
79
92
  res.json(api.getState());
80
93
  });
81
94
 
95
+ RED.httpAdmin.get('/alarm-ultimate/alarm/:id/log', needsRead, (req, res) => {
96
+ const api = alarmInstances.get(req.params.id);
97
+ if (!api) {
98
+ res.sendStatus(404);
99
+ return;
100
+ }
101
+ const getLog = api.getLog && typeof api.getLog === 'function' ? api.getLog : null;
102
+ if (!getLog) {
103
+ res.status(501).json({ ok: false, error: 'log_not_supported' });
104
+ return;
105
+ }
106
+ const since = Number(req.query && req.query.since);
107
+ const limit = Number(req.query && req.query.limit);
108
+ res.json(
109
+ getLog({
110
+ since: Number.isFinite(since) ? since : null,
111
+ limit: Number.isFinite(limit) ? limit : null,
112
+ })
113
+ );
114
+ });
115
+
82
116
  RED.httpAdmin.post('/alarm-ultimate/alarm/:id/command', needsWrite, (req, res) => {
83
117
  const api = alarmInstances.get(req.params.id);
84
118
  if (!api) {
@@ -88,7 +122,8 @@ module.exports = function (RED) {
88
122
  try {
89
123
  const body = req.body && typeof req.body === 'object' ? req.body : {};
90
124
  api.command(body);
91
- res.json({ ok: true });
125
+ const snapshot = api.getState && typeof api.getState === 'function' ? api.getState() : null;
126
+ res.json({ ok: true, result: snapshot });
92
127
  } catch (err) {
93
128
  res.status(500).json({ ok: false, error: err.message });
94
129
  }
@@ -105,6 +140,7 @@ module.exports = function (RED) {
105
140
 
106
141
  const controlTopic = config.controlTopic || 'alarm';
107
142
  const payloadPropName = config.payloadPropName || 'payload';
143
+ const syncTargetsConfig = parseJsonObject(config.syncTargets) || {};
108
144
 
109
145
  const requireCodeForArm = config.requireCodeForArm === true;
110
146
  const requireCodeForDisarm = config.requireCodeForDisarm !== false;
@@ -255,6 +291,8 @@ module.exports = function (RED) {
255
291
  'bypassed',
256
292
  'unbypassed',
257
293
  'chime',
294
+ 'zone_open',
295
+ 'zone_close',
258
296
  'zone_ignored_exit',
259
297
  'zone_bypassed_trigger',
260
298
  'zone_restore',
@@ -550,6 +588,22 @@ module.exports = function (RED) {
550
588
  let shape = 'ring';
551
589
  let text = 'DISARMED';
552
590
 
591
+ // When idle/disarmed, show recent arming errors to make the reason visible in the editor status.
592
+ if (!state.alarmActive && !state.entry && !state.arming && state.mode === 'disarmed') {
593
+ const last = Array.isArray(state.log) && state.log.length ? state.log[state.log.length - 1] : null;
594
+ const evt = last && typeof last.event === 'string' ? last.event : '';
595
+ if (evt === 'arm_blocked') {
596
+ const violations = Array.isArray(last.violations) ? last.violations.length : 0;
597
+ fill = 'yellow';
598
+ shape = 'ring';
599
+ text = `ARM BLOCKED${violations ? ` (${violations})` : ''}`;
600
+ } else if (evt === 'denied' && last && String(last.action || '') === 'arm') {
601
+ fill = 'red';
602
+ shape = 'ring';
603
+ text = 'ARM DENIED';
604
+ }
605
+ }
606
+
553
607
  if (state.alarmActive) {
554
608
  fill = 'red';
555
609
  shape = 'dot';
@@ -678,6 +732,27 @@ module.exports = function (RED) {
678
732
  };
679
733
  }
680
734
 
735
+ function getLogSnapshot(opts) {
736
+ const options = opts && typeof opts === 'object' ? opts : {};
737
+ const since = Number.isFinite(Number(options.since)) ? Number(options.since) : null;
738
+ const limit = Number.isFinite(Number(options.limit)) ? clampInt(options.limit, 200, 0, 500) : null;
739
+
740
+ const all = Array.isArray(state.log) ? state.log : [];
741
+ const filtered = since ? all.filter((e) => (e && Number(e.ts)) > since) : all;
742
+ const out = limit === null ? filtered : limit === 0 ? [] : filtered.slice(-limit);
743
+
744
+ return {
745
+ id: node.id,
746
+ name: node.name || '',
747
+ now: now(),
748
+ total: all.length,
749
+ returned: out.length,
750
+ since,
751
+ limit,
752
+ log: out.map((e) => ({ ...(e || {}) })),
753
+ };
754
+ }
755
+
681
756
  function buildZoneSummary(zone) {
682
757
  return {
683
758
  id: zone ? zone.id : null,
@@ -1016,6 +1091,7 @@ module.exports = function (RED) {
1016
1091
  state.mode = 'disarmed';
1017
1092
  persist();
1018
1093
  emitEvent('disarmed', { reason, duress: Boolean(duress) }, baseMsg);
1094
+ return true;
1019
1095
  }
1020
1096
 
1021
1097
  function violatedZonesForArm() {
@@ -1038,13 +1114,13 @@ module.exports = function (RED) {
1038
1114
  function arm(baseMsg, reason) {
1039
1115
  if (state.mode === 'armed' && !state.arming) {
1040
1116
  emitEvent('already_armed', { target: 'armed' }, baseMsg);
1041
- return;
1117
+ return true;
1042
1118
  }
1043
1119
 
1044
1120
  const violations = blockArmOnViolations ? violatedZonesForArm() : [];
1045
1121
  if (blockArmOnViolations && violations.length > 0) {
1046
1122
  emitEvent('arm_blocked', { target: 'armed', violations }, baseMsg);
1047
- return;
1123
+ return false;
1048
1124
  }
1049
1125
 
1050
1126
  stopOpenZonesDuringArming();
@@ -1065,7 +1141,7 @@ module.exports = function (RED) {
1065
1141
  stopOpenZonesDuringArming();
1066
1142
  persist();
1067
1143
  emitEvent('armed', { reason }, baseMsg);
1068
- return;
1144
+ return true;
1069
1145
  }
1070
1146
 
1071
1147
  const until = now() + exitDelayMs;
@@ -1094,6 +1170,55 @@ module.exports = function (RED) {
1094
1170
  persist();
1095
1171
  emitEvent('armed', { reason }, baseMsg);
1096
1172
  }, exitDelayMs);
1173
+ return true;
1174
+ }
1175
+
1176
+ function isSyncedControlMessage(msg) {
1177
+ return Boolean(msg && typeof msg === 'object' && msg._alarmUltimateSync);
1178
+ }
1179
+
1180
+ function normalizeSyncAction(value) {
1181
+ const v = String(value || '').toLowerCase().trim();
1182
+ if (v === 'arm') return 'arm';
1183
+ if (v === 'disarm') return 'disarm';
1184
+ return 'leave';
1185
+ }
1186
+
1187
+ function syncOtherAlarms(trigger, baseMsg) {
1188
+ if (!syncTargetsConfig || typeof syncTargetsConfig !== 'object') {
1189
+ return;
1190
+ }
1191
+ if (isSyncedControlMessage(baseMsg)) {
1192
+ return;
1193
+ }
1194
+ const when = trigger === 'disarm' ? 'onDisarm' : 'onArm';
1195
+ const entries = Object.entries(syncTargetsConfig);
1196
+ if (!entries.length) {
1197
+ return;
1198
+ }
1199
+ for (const [targetId, rule] of entries) {
1200
+ if (!targetId || targetId === node.id) continue;
1201
+ const action = normalizeSyncAction(rule && typeof rule === 'object' ? rule[when] : '');
1202
+ if (action === 'leave') continue;
1203
+ const api = alarmInstances.get(targetId);
1204
+ if (!api || typeof api.command !== 'function') continue;
1205
+
1206
+ const payload = {
1207
+ command: action,
1208
+ _alarmUltimateSync: {
1209
+ origin: node.id,
1210
+ trigger,
1211
+ },
1212
+ };
1213
+ if (typeof baseMsg.code === 'string') payload.code = baseMsg.code;
1214
+ if (typeof baseMsg.pin === 'string') payload.pin = baseMsg.pin;
1215
+
1216
+ try {
1217
+ api.command(payload);
1218
+ } catch (_err) {
1219
+ // ignore
1220
+ }
1221
+ }
1097
1222
  }
1098
1223
 
1099
1224
  function startEntryDelay(zone, baseMsg) {
@@ -1107,7 +1232,7 @@ module.exports = function (RED) {
1107
1232
  }
1108
1233
  const until = now() + delay;
1109
1234
  state.entry = { zoneId: zone.id, until };
1110
- emitEvent('entry_delay', { zone: { id: zone.id, name: zone.name }, seconds: remainingSeconds(until) }, baseMsg);
1235
+ emitEvent('entry_delay', { zone: buildZoneSummary(zone), seconds: remainingSeconds(until) }, baseMsg);
1111
1236
  startStatusInterval();
1112
1237
  clearEntryTimer();
1113
1238
  entryTimer = timerBag.setTimeout(() => {
@@ -1177,7 +1302,7 @@ module.exports = function (RED) {
1177
1302
  }
1178
1303
  state.bypass[id] = Boolean(enabled);
1179
1304
  persist();
1180
- emitEvent(enabled ? 'bypassed' : 'unbypassed', { zone: { id: zone.id, name: zone.name } }, baseMsg);
1305
+ emitEvent(enabled ? 'bypassed' : 'unbypassed', { zone: buildZoneSummary(zone) }, baseMsg);
1181
1306
  }
1182
1307
 
1183
1308
  function handleControlMessage(msg) {
@@ -1241,9 +1366,11 @@ module.exports = function (RED) {
1241
1366
  if (validation.duress) {
1242
1367
  triggerAlarm('duress', null, msg, true);
1243
1368
  disarm(msg, 'duress', true);
1369
+ syncOtherAlarms('disarm', msg);
1244
1370
  return true;
1245
1371
  }
1246
1372
  disarm(msg, 'manual', false);
1373
+ syncOtherAlarms('disarm', msg);
1247
1374
  return true;
1248
1375
  }
1249
1376
 
@@ -1266,7 +1393,10 @@ module.exports = function (RED) {
1266
1393
  if (validation.duress) {
1267
1394
  triggerAlarm('duress', null, msg, true);
1268
1395
  }
1269
- arm(msg, 'manual');
1396
+ const accepted = arm(msg, 'manual');
1397
+ if (accepted) {
1398
+ syncOtherAlarms('arm', msg);
1399
+ }
1270
1400
  return true;
1271
1401
  }
1272
1402
 
@@ -1278,7 +1408,7 @@ module.exports = function (RED) {
1278
1408
  if (!zone) {
1279
1409
  return;
1280
1410
  }
1281
- const resolved = helpers.resolveInput(msg, payloadPropName, config.translatorConfig, RED);
1411
+ const resolved = helpers.resolveInput(msg, payloadPropName, null, RED);
1282
1412
  const value = resolved.boolean;
1283
1413
  if (value === undefined) {
1284
1414
  return;
@@ -1290,8 +1420,20 @@ module.exports = function (RED) {
1290
1420
  zoneMeta.lastChangeAt = now();
1291
1421
  state.zoneState[zone.id] = zoneMeta;
1292
1422
 
1423
+ if (changed) {
1424
+ emitEvent(
1425
+ value === true ? 'zone_open' : 'zone_close',
1426
+ {
1427
+ zone: buildZoneSummary(zone),
1428
+ open: value === true,
1429
+ bypassed: state.bypass[zone.id] === true,
1430
+ },
1431
+ msg
1432
+ );
1433
+ }
1434
+
1293
1435
  if (changed && emitRestoreEvents && value === false) {
1294
- emitEvent('zone_restore', { zone: { id: zone.id, name: zone.name, type: zone.type } }, msg);
1436
+ emitEvent('zone_restore', { zone: buildZoneSummary(zone) }, msg);
1295
1437
  }
1296
1438
 
1297
1439
  if (changed) {
@@ -1317,7 +1459,7 @@ module.exports = function (RED) {
1317
1459
  }
1318
1460
 
1319
1461
  if (state.bypass[zone.id] === true && zone.bypassable !== false) {
1320
- emitEvent('zone_bypassed_trigger', { zone: { id: zone.id, name: zone.name, type: zone.type } }, msg);
1462
+ emitEvent('zone_bypassed_trigger', { zone: buildZoneSummary(zone) }, msg);
1321
1463
  return;
1322
1464
  }
1323
1465
 
@@ -1335,13 +1477,13 @@ module.exports = function (RED) {
1335
1477
  }
1336
1478
 
1337
1479
  if (state.arming && !zone.instantDuringExit) {
1338
- emitEvent('zone_ignored_exit', { zone: { id: zone.id, name: zone.name, type: zone.type } }, msg);
1480
+ emitEvent('zone_ignored_exit', { zone: buildZoneSummary(zone) }, msg);
1339
1481
  return;
1340
1482
  }
1341
1483
 
1342
1484
  if (state.mode === 'disarmed') {
1343
1485
  if (zone.chime) {
1344
- emitEvent('chime', { zone: { id: zone.id, name: zone.name, type: zone.type } }, msg);
1486
+ emitEvent('chime', { zone: buildZoneSummary(zone) }, msg);
1345
1487
  }
1346
1488
  return;
1347
1489
  }
@@ -1374,6 +1516,7 @@ module.exports = function (RED) {
1374
1516
  name: node.name || '',
1375
1517
  controlTopic,
1376
1518
  getState: getUiState,
1519
+ getLog: getLogSnapshot,
1377
1520
  command(body) {
1378
1521
  const payload = body && typeof body === 'object' ? body : {};
1379
1522
  const msg = { topic: controlTopic };
@@ -1402,6 +1545,9 @@ module.exports = function (RED) {
1402
1545
  if (typeof payload.zone === 'string') {
1403
1546
  msg.zone = payload.zone;
1404
1547
  }
1548
+ if (payload._alarmUltimateSync && typeof payload._alarmUltimateSync === 'object') {
1549
+ msg._alarmUltimateSync = payload._alarmUltimateSync;
1550
+ }
1405
1551
 
1406
1552
  node.receive(msg);
1407
1553
  },
@@ -0,0 +1,304 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("AlarmUltimateInputAdapter", {
3
+ category: "Alarm Ultimate",
4
+ color: "#A8DADC",
5
+ defaults: {
6
+ name: { value: "" },
7
+ presetSource: { value: "builtin" },
8
+ presetId: { value: "passthrough" },
9
+ userCode: { value: "return msg;", required: false },
10
+ },
11
+ inputs: 1,
12
+ outputs: 1,
13
+ icon: "alarm-ultimate.svg",
14
+ label: function () {
15
+ return this.name || "Input Adapter";
16
+ },
17
+ paletteLabel: function () {
18
+ return "Input Adapter";
19
+ },
20
+ oneditprepare: function () {
21
+ const self = this;
22
+ const els = {
23
+ presetSource: $("#node-input-presetSource"),
24
+ presetId: $("#node-input-presetId"),
25
+ builtinPreset: $("#node-input-builtinPreset"),
26
+ builtinInfo: $("#node-input-builtinInfo"),
27
+ userCode: $("#node-input-userCode"),
28
+ };
29
+
30
+ let builtinPresets = [];
31
+ let editor = null;
32
+ let userCodeCache = String(self.userCode || "return msg;");
33
+ let builtinIdCache = String(self.presetId || "passthrough");
34
+
35
+ function refreshBuiltinSelect() {
36
+ els.builtinPreset.empty();
37
+ builtinPresets.forEach((p) => {
38
+ els.builtinPreset.append(
39
+ $("<option></option>").attr("value", p.id).text(p.name),
40
+ );
41
+ });
42
+ }
43
+
44
+ function builtinExists(id) {
45
+ const target = String(id || "").trim();
46
+ if (!target) return false;
47
+ return builtinPresets.some((p) => p && p.id === target);
48
+ }
49
+
50
+ function setMode(mode) {
51
+ const m = mode === "user" ? "user" : "builtin";
52
+ els.presetSource.val(m);
53
+ $("#builtin-row").toggle(m === "builtin");
54
+ }
55
+
56
+ function setBuiltinInfo(presetId) {
57
+ const p = builtinPresets.find((x) => x.id === presetId);
58
+ if (!p) {
59
+ els.builtinInfo.text("");
60
+ return;
61
+ }
62
+ const desc = p.description ? ` — ${p.description}` : "";
63
+ els.builtinInfo.text(`${p.name}${desc}`);
64
+ }
65
+
66
+ function getSelectedBuiltinPreset() {
67
+ const id = String(els.builtinPreset.val() || "").trim();
68
+ return builtinPresets.find((p) => p && p.id === id) || null;
69
+ }
70
+
71
+ function applyBuiltinSelection(desiredId) {
72
+ let id = String(desiredId || "").trim();
73
+ if (!builtinExists(id)) {
74
+ id = builtinExists(builtinIdCache) ? builtinIdCache : "passthrough";
75
+ }
76
+ if (!builtinExists(id) && builtinPresets.length > 0) {
77
+ id = String(builtinPresets[0].id || "").trim();
78
+ }
79
+ if (builtinExists(id)) {
80
+ els.builtinPreset.val(id);
81
+ builtinIdCache = id;
82
+ return id;
83
+ }
84
+ return "";
85
+ }
86
+
87
+ function updateEditor() {
88
+ const src = els.presetSource.val() === "user" ? "user" : "builtin";
89
+ if (src === "builtin") {
90
+ const preset = getSelectedBuiltinPreset();
91
+ editor.setReadOnly(true);
92
+ editor.setValue(String((preset && preset.code) || ""), -1);
93
+ return;
94
+ }
95
+ editor.setReadOnly(false);
96
+ editor.setValue(String(userCodeCache || "return msg;"), -1);
97
+ }
98
+
99
+ editor = RED.editor.createEditor({
100
+ id: "node-input-code-editor",
101
+ mode: "ace/mode/javascript",
102
+ value: userCodeCache,
103
+ });
104
+ self._inputAdapterEditor = editor;
105
+
106
+ function loadBuiltins() {
107
+ const httpAdminRoot =
108
+ (RED.settings && RED.settings.httpAdminRoot) || "/";
109
+ const root = httpAdminRoot.endsWith("/")
110
+ ? httpAdminRoot
111
+ : `${httpAdminRoot}/`;
112
+ $.getJSON(`${root}alarm-ultimate/input-adapter/presets`)
113
+ .done((data) => {
114
+ builtinPresets = Array.isArray(data && data.presets)
115
+ ? data.presets
116
+ : [];
117
+ refreshBuiltinSelect();
118
+ const configured = String(els.presetId.val() || "").trim();
119
+ const selected = applyBuiltinSelection(configured || builtinIdCache);
120
+ if (els.presetSource.val() !== "user" && selected) {
121
+ els.presetId.val(selected);
122
+ setBuiltinInfo(selected);
123
+ }
124
+ updateEditor();
125
+ })
126
+ .fail(() => {
127
+ builtinPresets = [
128
+ {
129
+ id: "passthrough",
130
+ name: "Passthrough",
131
+ description: "",
132
+ code: "return msg;",
133
+ },
134
+ ];
135
+ refreshBuiltinSelect();
136
+ const configured = String(els.presetId.val() || "").trim();
137
+ const selected = applyBuiltinSelection(configured || builtinIdCache);
138
+ if (selected) setBuiltinInfo(selected);
139
+ updateEditor();
140
+ });
141
+ }
142
+
143
+ // Initialize.
144
+ loadBuiltins();
145
+ setMode(els.presetSource.val());
146
+
147
+ // Seed selects from stored presetId.
148
+ const initialSource =
149
+ els.presetSource.val() === "user" ? "user" : "builtin";
150
+ const initialId = String(els.presetId.val() || "");
151
+ if (initialSource === "builtin") {
152
+ builtinIdCache = String(initialId || builtinIdCache || "passthrough");
153
+ }
154
+ if (!els.userCode.val()) {
155
+ els.userCode.val(userCodeCache);
156
+ } else {
157
+ userCodeCache = String(els.userCode.val() || "return msg;");
158
+ }
159
+ updateEditor();
160
+
161
+ // Events.
162
+ els.presetSource.on("change", () => {
163
+ // keep user draft before switching away
164
+ if (editor && editor.getReadOnly && editor.getReadOnly() === false) {
165
+ userCodeCache = String(editor.getValue() || "");
166
+ els.userCode.val(userCodeCache);
167
+ }
168
+ const m = els.presetSource.val() === "user" ? "user" : "builtin";
169
+ setMode(m);
170
+ if (m === "builtin") {
171
+ const selected = applyBuiltinSelection(els.builtinPreset.val() || builtinIdCache);
172
+ if (selected) {
173
+ els.presetId.val(selected);
174
+ setBuiltinInfo(selected);
175
+ }
176
+ } else {
177
+ builtinIdCache = String(els.builtinPreset.val() || builtinIdCache);
178
+ els.presetId.val("custom");
179
+ }
180
+ updateEditor();
181
+ });
182
+
183
+ els.builtinPreset.on("change", () => {
184
+ const id = String(els.builtinPreset.val() || "");
185
+ builtinIdCache = id;
186
+ els.presetId.val(id);
187
+ setBuiltinInfo(id);
188
+ updateEditor();
189
+ });
190
+
191
+ editor.getSession().on("change", () => {
192
+ const src = els.presetSource.val() === "user" ? "user" : "builtin";
193
+ if (src !== "user") return;
194
+ userCodeCache = String(editor.getValue() || "");
195
+ els.userCode.val(userCodeCache);
196
+ });
197
+ },
198
+ oneditsave: function () {
199
+ // Keep presetId aligned with current selection.
200
+ const src =
201
+ $("#node-input-presetSource").val() === "user" ? "user" : "builtin";
202
+ if (src === "builtin") {
203
+ $("#node-input-presetId").val(
204
+ String($("#node-input-builtinPreset").val() || ""),
205
+ );
206
+ } else {
207
+ $("#node-input-presetId").val("custom");
208
+ }
209
+ if (this._inputAdapterEditor && src === "user") {
210
+ $("#node-input-userCode").val(
211
+ String(this._inputAdapterEditor.getValue() || ""),
212
+ );
213
+ }
214
+ if (this._inputAdapterEditor) {
215
+ try {
216
+ this._inputAdapterEditor.destroy();
217
+ } catch (_err) {
218
+ // ignore
219
+ }
220
+ this._inputAdapterEditor = null;
221
+ }
222
+ },
223
+ oneditcancel: function () {
224
+ if (this._inputAdapterEditor) {
225
+ try {
226
+ this._inputAdapterEditor.destroy();
227
+ } catch (_err) {
228
+ // ignore
229
+ }
230
+ this._inputAdapterEditor = null;
231
+ }
232
+ },
233
+ });
234
+ </script>
235
+
236
+ <script type="text/html" data-template-name="AlarmUltimateInputAdapter">
237
+ <div class="form-row">
238
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
239
+ <input type="text" id="node-input-name" placeholder="Name" />
240
+ </div>
241
+
242
+ <div class="form-row">
243
+ <label for="node-input-presetSource"
244
+ ><i class="fa fa-random"></i> Preset source</label
245
+ >
246
+ <select id="node-input-presetSource" style="width: 70%;">
247
+ <option value="builtin">Built-in</option>
248
+ <option value="user">User</option>
249
+ </select>
250
+ </div>
251
+
252
+ <br />
253
+
254
+ <div class="form-row" id="builtin-row">
255
+ <label for="node-input-builtinPreset"
256
+ ><i class="fa fa-cube"></i> Built-in preset</label
257
+ >
258
+ <select id="node-input-builtinPreset" style="width: 70%;"></select>
259
+ <br />
260
+ <br />
261
+ <div class="form-tips" id="node-input-builtinInfo"></div>
262
+ </div>
263
+
264
+ <div class="form-row">
265
+ <label><i class="fa fa-code"></i> Code</label>
266
+ </div>
267
+
268
+ <div class="form-row">
269
+ <div style="width:100%;">
270
+ <div
271
+ style="height: 320px; width: 100%;"
272
+ class="node-text-editor"
273
+ id="node-input-code-editor"
274
+ ></div>
275
+ <br />
276
+ <div class="form-tips">
277
+ Built-in presets are read-only. Switch to <b>User</b> to edit your
278
+ custom code. Return a msg, an array of msgs, or nothing to drop.
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <input type="hidden" id="node-input-userCode" />
284
+ <input type="hidden" id="node-input-presetId" />
285
+ <br />
286
+ <br />
287
+ <br />
288
+ <br />
289
+ </script>
290
+
291
+ <script type="text/markdown" data-help-name="AlarmUltimateInputAdapter">
292
+ Translates incoming messages into the format expected by **Alarm System Ultimate** zones.
293
+
294
+ Configure a preset (built-in or user-defined). A preset is JavaScript code executed as a function body:
295
+
296
+ - Input: `msg`
297
+ - Return: a message object, an array of messages, or `null/undefined` to drop.
298
+
299
+ Example:
300
+
301
+ ```js
302
+ return { topic: msg.topic, payload: !!msg.payload };
303
+ ```
304
+ </script>