node-red-contrib-alarm-ultimate 0.1.0 → 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 +87 -13
- package/docs/images/alarm-panel-mock.svg +114 -0
- package/docs/images/banner.svg +63 -0
- package/docs/images/flow-overview.svg +85 -0
- package/examples/README.md +32 -11
- package/examples/alarm-ultimate-basic.json +0 -1
- package/examples/alarm-ultimate-dashboard-controls.json +575 -0
- 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 +174 -85
- package/nodes/AlarmSystemUltimate.js +39 -8
- package/nodes/AlarmUltimateInputAdapter.html +304 -0
- package/nodes/AlarmUltimateInputAdapter.js +188 -0
- package/nodes/AlarmUltimateSiren.html +3 -3
- package/nodes/AlarmUltimateSiren.js +6 -2
- package/nodes/AlarmUltimateState.html +3 -3
- package/nodes/AlarmUltimateState.js +6 -2
- package/nodes/AlarmUltimateZone.html +11 -6
- package/nodes/AlarmUltimateZone.js +27 -6
- package/nodes/icons/alarm-ultimate-siren.svg +6 -0
- package/nodes/icons/alarm-ultimate-state.svg +5 -0
- package/nodes/icons/alarm-ultimate-zone.svg +5 -0
- package/nodes/icons/alarm-ultimate.svg +6 -0
- 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 +5 -4
- 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 +1882 -460
- package/tools/alarm-panel.html +630 -131
|
@@ -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>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const vm = require("vm");
|
|
6
|
+
|
|
7
|
+
function safeReadDir(dirPath) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.readdirSync(dirPath);
|
|
10
|
+
} catch (_err) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadBuiltinPresets() {
|
|
16
|
+
const presetsDir = path.join(__dirname, "presets", "input-adapter");
|
|
17
|
+
const files = safeReadDir(presetsDir)
|
|
18
|
+
.filter((f) => f.endsWith(".js"))
|
|
19
|
+
.sort((a, b) => a.localeCompare(b));
|
|
20
|
+
|
|
21
|
+
const presets = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const fullPath = path.join(presetsDir, file);
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
26
|
+
const mod = require(fullPath);
|
|
27
|
+
const preset = mod && typeof mod === "object" ? mod : null;
|
|
28
|
+
if (!preset) continue;
|
|
29
|
+
if (typeof preset.id !== "string" || preset.id.trim().length === 0) continue;
|
|
30
|
+
if (typeof preset.name !== "string" || preset.name.trim().length === 0) continue;
|
|
31
|
+
if (typeof preset.code !== "string" || preset.code.trim().length === 0) continue;
|
|
32
|
+
presets.push({
|
|
33
|
+
id: preset.id.trim(),
|
|
34
|
+
name: preset.name.trim(),
|
|
35
|
+
description: typeof preset.description === "string" ? preset.description.trim() : "",
|
|
36
|
+
code: preset.code,
|
|
37
|
+
});
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
// ignore broken preset files
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return presets;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildContextApi(node) {
|
|
46
|
+
const nodeCtx = node.context();
|
|
47
|
+
const flowCtx = node.context().flow;
|
|
48
|
+
const globalCtx = node.context().global;
|
|
49
|
+
|
|
50
|
+
function wrap(ctx) {
|
|
51
|
+
return {
|
|
52
|
+
get: (key) => ctx.get(key),
|
|
53
|
+
set: (key, value) => ctx.set(key, value),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
node: wrap(nodeCtx),
|
|
59
|
+
flow: wrap(flowCtx),
|
|
60
|
+
global: wrap(globalCtx),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = function (RED) {
|
|
65
|
+
const builtins = loadBuiltinPresets();
|
|
66
|
+
const builtinById = new Map(builtins.map((p) => [p.id, p]));
|
|
67
|
+
|
|
68
|
+
if (RED && RED.httpAdmin && typeof RED.httpAdmin.get === "function") {
|
|
69
|
+
const needsRead =
|
|
70
|
+
RED.auth && typeof RED.auth.needsPermission === "function"
|
|
71
|
+
? RED.auth.needsPermission("AlarmUltimateInputAdapter.read")
|
|
72
|
+
: (req, res, next) => next();
|
|
73
|
+
|
|
74
|
+
RED.httpAdmin.get("/alarm-ultimate/input-adapter/presets", needsRead, (_req, res) => {
|
|
75
|
+
res.json({ presets: builtins });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function AlarmUltimateInputAdapter(config) {
|
|
80
|
+
RED.nodes.createNode(this, config);
|
|
81
|
+
const node = this;
|
|
82
|
+
const REDUtil = RED.util;
|
|
83
|
+
|
|
84
|
+
const presetSource = config.presetSource === "user" ? "user" : "builtin";
|
|
85
|
+
const presetId =
|
|
86
|
+
typeof config.presetId === "string" && config.presetId.trim().length > 0
|
|
87
|
+
? config.presetId.trim()
|
|
88
|
+
: "passthrough";
|
|
89
|
+
const userCode = typeof config.userCode === "string" ? config.userCode : "";
|
|
90
|
+
|
|
91
|
+
const preset =
|
|
92
|
+
presetSource === "user"
|
|
93
|
+
? {
|
|
94
|
+
id: "custom",
|
|
95
|
+
name: "Custom",
|
|
96
|
+
code: userCode,
|
|
97
|
+
}
|
|
98
|
+
: builtinById.get(presetId);
|
|
99
|
+
const sandbox = {
|
|
100
|
+
msg: null,
|
|
101
|
+
context: buildContextApi(node),
|
|
102
|
+
log: (...args) => node.log(args.map(String).join(" ")),
|
|
103
|
+
warn: (...args) => node.warn(args.map(String).join(" ")),
|
|
104
|
+
error: (...args) => node.error(args.map(String).join(" ")),
|
|
105
|
+
fn: null,
|
|
106
|
+
result: undefined,
|
|
107
|
+
};
|
|
108
|
+
const vmContext = vm.createContext(sandbox);
|
|
109
|
+
|
|
110
|
+
function compile(code) {
|
|
111
|
+
const body = String(code || "").trim();
|
|
112
|
+
if (!body) return null;
|
|
113
|
+
const fnScript = new vm.Script(
|
|
114
|
+
`fn = (function (msg, context, log, warn, error) { "use strict";\n${body}\n});`,
|
|
115
|
+
);
|
|
116
|
+
fnScript.runInContext(vmContext, { timeout: 250 });
|
|
117
|
+
const callScript = new vm.Script(
|
|
118
|
+
`result = fn(msg, context, log, warn, error);`,
|
|
119
|
+
);
|
|
120
|
+
return { callScript };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let compiled = null;
|
|
124
|
+
try {
|
|
125
|
+
if (!preset || typeof preset.code !== "string" || preset.code.trim().length === 0) {
|
|
126
|
+
node.status({
|
|
127
|
+
fill: "red",
|
|
128
|
+
shape: "ring",
|
|
129
|
+
text: presetSource === "user" ? "missing user code" : "preset not found",
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
compiled = compile(preset.code);
|
|
133
|
+
node.status({
|
|
134
|
+
fill: "green",
|
|
135
|
+
shape: "dot",
|
|
136
|
+
text: presetSource === "user" ? "preset: custom" : `preset: ${preset.name}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
node.status({ fill: "red", shape: "dot", text: "invalid preset" });
|
|
141
|
+
node.error(err);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
node.on("input", (msg, send, done) => {
|
|
145
|
+
const doSend = send || ((m) => node.send(m));
|
|
146
|
+
if (!compiled) {
|
|
147
|
+
if (done) done();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
sandbox.msg = msg ? REDUtil.cloneMessage(msg) : {};
|
|
153
|
+
sandbox.result = undefined;
|
|
154
|
+
compiled.callScript.runInContext(vmContext, { timeout: 100 });
|
|
155
|
+
const out = sandbox.result;
|
|
156
|
+
|
|
157
|
+
if (out === undefined || out === null) {
|
|
158
|
+
if (done) done();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(out)) {
|
|
163
|
+
out.filter(Boolean).forEach((m, idx) => {
|
|
164
|
+
if (idx === 0) doSend(m);
|
|
165
|
+
else doSend(REDUtil.cloneMessage(m));
|
|
166
|
+
});
|
|
167
|
+
if (done) done();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof out === "object") {
|
|
172
|
+
doSend(out);
|
|
173
|
+
if (done) done();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
doSend({ ...(msg || {}), payload: out });
|
|
178
|
+
if (done) done();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
node.status({ fill: "red", shape: "dot", text: "transform error" });
|
|
181
|
+
node.error(err, msg);
|
|
182
|
+
if (done) done(err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
RED.nodes.registerType("AlarmUltimateInputAdapter", AlarmUltimateInputAdapter);
|
|
188
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType("AlarmUltimateSiren", {
|
|
3
3
|
category: "Alarm Ultimate",
|
|
4
|
-
color: "#
|
|
4
|
+
color: "#CFEFF0",
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
alarmId: { value: "", required: true },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
inputs: 0,
|
|
12
12
|
outputs: 1,
|
|
13
|
-
icon: "
|
|
13
|
+
icon: "alarm-ultimate-siren.svg",
|
|
14
14
|
label: function () {
|
|
15
15
|
return this.name || "Alarm Siren";
|
|
16
16
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
|
|
56
56
|
<script type="text/html" data-template-name="AlarmUltimateSiren">
|
|
57
57
|
<div class="form-row">
|
|
58
|
-
<label
|
|
58
|
+
<label>WEB PAGE</label>
|
|
59
59
|
<button type="button" class="red-ui-button" id="node-input-alarm-panel">
|
|
60
60
|
<i class="fa fa-keyboard-o"></i> Panel
|
|
61
61
|
</button>
|
|
@@ -28,6 +28,12 @@ module.exports = function (RED) {
|
|
|
28
28
|
if (lastActive === active && reason !== 'init') return;
|
|
29
29
|
lastActive = active;
|
|
30
30
|
|
|
31
|
+
setNodeStatus({
|
|
32
|
+
fill: active ? 'red' : 'green',
|
|
33
|
+
shape: 'dot',
|
|
34
|
+
text: active ? 'Siren on' : 'Siren off',
|
|
35
|
+
});
|
|
36
|
+
|
|
31
37
|
const msg = {
|
|
32
38
|
topic: buildTopic(evt && evt.controlTopic),
|
|
33
39
|
payload: active,
|
|
@@ -52,7 +58,6 @@ module.exports = function (RED) {
|
|
|
52
58
|
const ui = api.getState && typeof api.getState === 'function' ? api.getState() : null;
|
|
53
59
|
const state = ui && ui.state ? ui.state : null;
|
|
54
60
|
const active = state ? Boolean(state.sirenActive) : null;
|
|
55
|
-
setNodeStatus({ fill: 'green', shape: 'dot', text: `Connected (${active ? 'on' : 'off'})` });
|
|
56
61
|
emitSiren(active, { alarmId, controlTopic: ui.controlTopic, name: ui.name }, reason);
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -80,4 +85,3 @@ module.exports = function (RED) {
|
|
|
80
85
|
|
|
81
86
|
RED.nodes.registerType('AlarmUltimateSiren', AlarmUltimateSiren);
|
|
82
87
|
};
|
|
83
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType("AlarmUltimateState", {
|
|
3
3
|
category: "Alarm Ultimate",
|
|
4
|
-
color: "#
|
|
4
|
+
color: "#CFEFF0",
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
alarmId: { value: "", required: true },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
inputs: 0,
|
|
12
12
|
outputs: 1,
|
|
13
|
-
icon: "
|
|
13
|
+
icon: "alarm-ultimate-state.svg",
|
|
14
14
|
label: function () {
|
|
15
15
|
return this.name || "Alarm State";
|
|
16
16
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
|
|
56
56
|
<script type="text/html" data-template-name="AlarmUltimateState">
|
|
57
57
|
<div class="form-row">
|
|
58
|
-
<label
|
|
58
|
+
<label>WEB PAGE</label>
|
|
59
59
|
<button type="button" class="red-ui-button" id="node-input-alarm-panel">
|
|
60
60
|
<i class="fa fa-keyboard-o"></i> Panel
|
|
61
61
|
</button>
|
|
@@ -32,6 +32,12 @@ module.exports = function (RED) {
|
|
|
32
32
|
}
|
|
33
33
|
lastMode = mode;
|
|
34
34
|
|
|
35
|
+
setNodeStatus({
|
|
36
|
+
fill: mode === 'disarmed' ? 'green' : 'red',
|
|
37
|
+
shape: 'dot',
|
|
38
|
+
text: mode === 'disarmed' ? 'Disarmed' : 'Armed',
|
|
39
|
+
});
|
|
40
|
+
|
|
35
41
|
const msg = {
|
|
36
42
|
topic: buildTopic(api && api.controlTopic),
|
|
37
43
|
payload: mode,
|
|
@@ -54,7 +60,6 @@ module.exports = function (RED) {
|
|
|
54
60
|
}
|
|
55
61
|
const state = api.getState && typeof api.getState === 'function' ? api.getState() : null;
|
|
56
62
|
const mode = state && state.state ? state.state.mode : null;
|
|
57
|
-
setNodeStatus({ fill: 'green', shape: 'dot', text: `Connected (${mode || 'unknown'})` });
|
|
58
63
|
emitMode(mode, api, reason);
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -84,4 +89,3 @@ module.exports = function (RED) {
|
|
|
84
89
|
|
|
85
90
|
RED.nodes.registerType('AlarmUltimateState', AlarmUltimateState);
|
|
86
91
|
};
|
|
87
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType("AlarmUltimateZone", {
|
|
3
3
|
category: "Alarm Ultimate",
|
|
4
|
-
color: "#
|
|
4
|
+
color: "#CFEFF0",
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
alarmId: { value: "", required: true },
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
inputs: 0,
|
|
13
13
|
outputs: 1,
|
|
14
|
-
icon: "
|
|
14
|
+
icon: "alarm-ultimate-zone.svg",
|
|
15
15
|
label: function () {
|
|
16
16
|
return this.name || "Alarm Zone";
|
|
17
17
|
},
|
|
@@ -65,7 +65,12 @@
|
|
|
65
65
|
zoneSelect.empty();
|
|
66
66
|
zoneSelect.append($("<option></option>").attr("value", "").text("-- select --"));
|
|
67
67
|
zones.forEach((z) => {
|
|
68
|
-
const
|
|
68
|
+
const topic = z.topic || z.topicPattern || "";
|
|
69
|
+
const label = z.name
|
|
70
|
+
? topic
|
|
71
|
+
? `${z.name} (${topic})`
|
|
72
|
+
: z.name
|
|
73
|
+
: topic || z.id;
|
|
69
74
|
zoneSelect.append($("<option></option>").attr("value", z.id).text(label));
|
|
70
75
|
});
|
|
71
76
|
if (this.zoneId) zoneSelect.val(this.zoneId);
|
|
@@ -84,7 +89,7 @@
|
|
|
84
89
|
|
|
85
90
|
<script type="text/html" data-template-name="AlarmUltimateZone">
|
|
86
91
|
<div class="form-row">
|
|
87
|
-
<label
|
|
92
|
+
<label>WEB PAGE</label>
|
|
88
93
|
<button type="button" class="red-ui-button" id="node-input-alarm-panel">
|
|
89
94
|
<i class="fa fa-keyboard-o"></i> Panel
|
|
90
95
|
</button>
|
|
@@ -107,7 +112,7 @@
|
|
|
107
112
|
|
|
108
113
|
<div class="form-row">
|
|
109
114
|
<label for="node-input-topic"><i class="fa fa-tag"></i> Topic</label>
|
|
110
|
-
<input type="text" id="node-input-topic" placeholder="(default: <controlTopic>/zone/<
|
|
115
|
+
<input type="text" id="node-input-topic" placeholder="(default: <controlTopic>/zone/<zoneTopic>)" />
|
|
111
116
|
</div>
|
|
112
117
|
|
|
113
118
|
<div class="form-row">
|
|
@@ -120,7 +125,7 @@
|
|
|
120
125
|
Emits the state of a single configured zone from a selected **Alarm System Ultimate** node.
|
|
121
126
|
|
|
122
127
|
- `msg.payload`: `true` when the zone is open/active, otherwise `false`
|
|
123
|
-
- `msg.zone`: `{ id, name?, type? }`
|
|
128
|
+
- `msg.zone`: `{ id, name?, type?, topic? }`
|
|
124
129
|
- `msg.bypassed`: `true|false`
|
|
125
130
|
|
|
126
131
|
Notes:
|