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.
- package/README.md +14 -0
- package/examples/README.md +93 -3
- package/examples/alarm-ultimate-basic.json +0 -1
- package/examples/alarm-ultimate-dashboard-controls.json +34 -4
- package/examples/alarm-ultimate-dashboard-v2.json +834 -0
- package/examples/alarm-ultimate-dashboard.json +34 -5
- package/examples/alarm-ultimate-home-assistant-alarm-panel.json +335 -0
- package/flowfuse-node-red-dashboard-1.30.2.tgz +0 -0
- package/nodes/AlarmSystemUltimate.html +332 -105
- package/nodes/AlarmSystemUltimate.js +158 -12
- 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 +112 -0
- package/test/input-adapter.spec.js +243 -0
- package/test/output-nodes.spec.js +3 -0
- package/tools/alarm-json-mapper.html +955 -167
- package/tools/alarm-panel.html +995 -139
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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>
|