node-red-contrib-3dm-space 1.0.2 → 1.0.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/3dm-cloud-config.html +270 -83
- package/3dm-cloud-config.js +826 -123
- package/README.md +4 -4
- package/package.json +2 -2
- package/node-red-contrib-3dm-space-1.0.0.tgz +0 -0
package/3dm-cloud-config.js
CHANGED
|
@@ -1,171 +1,887 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var fs = require("fs");
|
|
5
|
+
var path = require("path");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var CLOUD_HOST = "3dm.space";
|
|
10
|
-
var CLOUD_PORT = 1883;
|
|
11
|
-
var CLOUD_URL = "mqtt://" + CLOUD_HOST + ":" + CLOUD_PORT;
|
|
7
|
+
function asBool(value) {
|
|
8
|
+
return value === true || value === "true" || value === 1 || value === "1";
|
|
9
|
+
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
function asNumber(value) {
|
|
12
|
+
var n = parseFloat(value);
|
|
13
|
+
return Number.isNaN(n) ? NaN : n;
|
|
14
|
+
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
var REPEAT_ERROR_LOG_MS = 60000;
|
|
16
|
+
function clampNumber(value, min, max, fallback) {
|
|
17
|
+
var n = parseInt(value, 10);
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return "";
|
|
19
|
+
if (Number.isNaN(n)) {
|
|
20
|
+
return fallback;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
return
|
|
23
|
+
return Math.max(min, Math.min(n, max));
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
function
|
|
32
|
-
|
|
26
|
+
function safeParseJson(value) {
|
|
27
|
+
if (typeof value !== "string") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasOwn(obj, key) {
|
|
39
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getNestedValue(obj, key) {
|
|
43
|
+
if (!obj || key === null || key === undefined || key === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
if (typeof obj !== "object") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
if (hasOwn(obj, key)) {
|
|
52
|
+
return obj[key];
|
|
53
|
+
}
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: "";
|
|
55
|
+
var current = obj;
|
|
56
|
+
var parts = String(key).split(".");
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
59
|
+
var part = parts[i];
|
|
60
|
+
|
|
61
|
+
if (!current || typeof current !== "object" || !hasOwn(current, part)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
current = current[part];
|
|
66
|
+
}
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeDir(dir) {
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(dir);
|
|
79
|
+
} catch (err2) {
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dateKey(date) {
|
|
86
|
+
return (
|
|
87
|
+
date.getFullYear() + "-" +
|
|
88
|
+
(date.getMonth() + 1) + "-" +
|
|
89
|
+
date.getDate()
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function outputNumberFromKey(outputKey, fallbackIndex, maxOutputs) {
|
|
94
|
+
maxOutputs = clampNumber(maxOutputs || 24, 1, 24, 24);
|
|
95
|
+
|
|
96
|
+
var match = String(outputKey || "").match(/output(\d+)/i);
|
|
97
|
+
|
|
98
|
+
if (match) {
|
|
99
|
+
var number = parseInt(match[1], 10);
|
|
100
|
+
|
|
101
|
+
if (number >= 1 && number <= maxOutputs) {
|
|
102
|
+
return number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
var fallbackNumber = fallbackIndex + 1;
|
|
109
|
+
|
|
110
|
+
if (fallbackNumber >= 1 && fallbackNumber <= maxOutputs) {
|
|
111
|
+
return fallbackNumber;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normaliseMode(value, fallback) {
|
|
118
|
+
if (value === null || value === undefined || value === "") {
|
|
119
|
+
return fallback;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return String(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isManualOn(mode) {
|
|
126
|
+
return String(mode).toUpperCase() === "ON";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isManualOff(mode) {
|
|
130
|
+
return String(mode).toUpperCase() === "OFF";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isAutoSchedule(mode) {
|
|
134
|
+
var text = String(mode || "").toLowerCase();
|
|
135
|
+
return text === "auto schedule" || text === "auto" || text === "schedule";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isSchedulerType(type) {
|
|
139
|
+
var text = String(type || "").toLowerCase();
|
|
140
|
+
return text === "scheduler" || text === "schedule" || text === "consched" || text === "control-schedule";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ThreeDMConfigStoreNode(config) {
|
|
144
|
+
RED.nodes.createNode(this, config);
|
|
145
|
+
|
|
146
|
+
var node = this;
|
|
147
|
+
|
|
148
|
+
node.name = config.name;
|
|
149
|
+
node.configName = String(config.configName || "").trim();
|
|
150
|
+
node.outputCount = clampNumber(config.outputs || 24, 1, 24, 24);
|
|
151
|
+
node.tickSeconds = clampNumber(config.tickSeconds || 20, 1, 60, 20);
|
|
152
|
+
node.outputMode = config.outputMode || "change";
|
|
153
|
+
node.configOutput = clampNumber(config.configOutput || 0, 0, node.outputCount, 0);
|
|
154
|
+
node.emitConfigOnUpdate = asBool(config.emitConfigOnUpdate);
|
|
155
|
+
|
|
156
|
+
var userDir = RED.settings && RED.settings.userDir
|
|
157
|
+
? RED.settings.userDir
|
|
158
|
+
: process.cwd();
|
|
159
|
+
|
|
160
|
+
var storeDir = path.join(userDir, "3dm-config-store");
|
|
161
|
+
var storeFile = path.join(storeDir, "3dm-config-store-" + node.id + ".json");
|
|
162
|
+
|
|
163
|
+
var closed = false;
|
|
164
|
+
var latestPayload = {};
|
|
165
|
+
var latestStore = {};
|
|
166
|
+
var outputStates = {};
|
|
167
|
+
var stoppedScheduleRuns = new Map();
|
|
168
|
+
var controlItemStates = new Map();
|
|
169
|
+
var lastStatusText = "";
|
|
170
|
+
var lastWarnText = "";
|
|
171
|
+
var lastWarnTime = 0;
|
|
55
172
|
|
|
56
173
|
function warnOncePerMinute(text) {
|
|
57
174
|
var now = Date.now();
|
|
58
175
|
|
|
59
|
-
if (text !==
|
|
60
|
-
|
|
61
|
-
|
|
176
|
+
if (text !== lastWarnText || (now - lastWarnTime) > 60000) {
|
|
177
|
+
lastWarnText = text;
|
|
178
|
+
lastWarnTime = now;
|
|
62
179
|
node.warn(text);
|
|
63
180
|
}
|
|
64
181
|
}
|
|
65
182
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
183
|
+
function setStatus(fill, shape, text) {
|
|
184
|
+
var shortText = String(text || "");
|
|
185
|
+
|
|
186
|
+
if (shortText.length > 90) {
|
|
187
|
+
shortText = shortText.slice(0, 87) + "...";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var key = fill + "|" + shape + "|" + shortText;
|
|
191
|
+
|
|
192
|
+
if (lastStatusText !== key) {
|
|
193
|
+
lastStatusText = key;
|
|
194
|
+
node.status({ fill: fill, shape: shape, text: shortText });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function saveToDisk() {
|
|
199
|
+
try {
|
|
200
|
+
makeDir(storeDir);
|
|
201
|
+
|
|
202
|
+
fs.writeFileSync(
|
|
203
|
+
storeFile,
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
version: 1,
|
|
206
|
+
updatedAt: new Date().toISOString(),
|
|
207
|
+
configStore: latestStore
|
|
208
|
+
}, null, 2)
|
|
209
|
+
);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
warnOncePerMinute("Failed to write 3DM config store to disk: " + (err.message || err));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function loadFromDisk() {
|
|
216
|
+
try {
|
|
217
|
+
if (!fs.existsSync(storeFile)) {
|
|
218
|
+
latestStore = {};
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var data = JSON.parse(fs.readFileSync(storeFile, "utf8"));
|
|
223
|
+
|
|
224
|
+
if (data && typeof data === "object" && data.configStore && typeof data.configStore === "object") {
|
|
225
|
+
latestStore = data.configStore;
|
|
226
|
+
} else if (data && typeof data === "object") {
|
|
227
|
+
latestStore = data;
|
|
228
|
+
} else {
|
|
229
|
+
latestStore = {};
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
latestStore = {};
|
|
233
|
+
warnOncePerMinute("Failed to load saved 3DM config store: " + (err.message || err));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shouldAcceptConfigName(configName) {
|
|
238
|
+
if (!node.configName) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return String(configName) === node.configName;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function inferBlockType(configName, block) {
|
|
246
|
+
if (block && typeof block === "object" && !Array.isArray(block) && block.type) {
|
|
247
|
+
return String(block.type);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (Array.isArray(block)) {
|
|
251
|
+
return "scheduler";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (block && typeof block === "object" && Array.isArray(block.schedule)) {
|
|
255
|
+
return "scheduler";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (String(configName || "").toLowerCase().indexOf("sched") !== -1) {
|
|
259
|
+
return "scheduler";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return "settings";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normaliseConfigBlock(configName, block) {
|
|
266
|
+
var type = inferBlockType(configName, block);
|
|
267
|
+
var data = block;
|
|
268
|
+
|
|
269
|
+
if (block && typeof block === "object" && !Array.isArray(block) && hasOwn(block, "data")) {
|
|
270
|
+
data = block.data;
|
|
271
|
+
} else if (block && typeof block === "object" && !Array.isArray(block) && Array.isArray(block.schedule)) {
|
|
272
|
+
data = block.schedule;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
name: configName,
|
|
277
|
+
type: type,
|
|
278
|
+
data: data,
|
|
279
|
+
updatedAt: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractConfigStore(msg) {
|
|
284
|
+
var payload = safeParseJson(msg.payload);
|
|
285
|
+
|
|
286
|
+
if (payload && typeof payload === "object" && hasOwn(payload, "configStore")) {
|
|
287
|
+
return payload.configStore;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (msg.key === "configStore" || msg.topic === "configStore") {
|
|
291
|
+
return payload;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (payload && typeof payload === "object" && payload.shared && hasOwn(payload.shared, "configStore")) {
|
|
295
|
+
return payload.shared.configStore;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (payload && typeof payload === "object" && payload.client && hasOwn(payload.client, "configStore")) {
|
|
299
|
+
return payload.client.configStore;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function storeConfig(configStore) {
|
|
306
|
+
var savedNames = [];
|
|
307
|
+
|
|
308
|
+
if (!configStore || typeof configStore !== "object" || Array.isArray(configStore)) {
|
|
309
|
+
return savedNames;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
Object.keys(configStore).forEach(function (configName) {
|
|
313
|
+
if (!shouldAcceptConfigName(configName)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
latestStore[configName] = normaliseConfigBlock(configName, configStore[configName]);
|
|
318
|
+
savedNames.push(configName);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (savedNames.length > 0) {
|
|
322
|
+
saveToDisk();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return savedNames;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function storedConfigValue(key) {
|
|
329
|
+
if (!key) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var cleanKey = String(key);
|
|
334
|
+
|
|
335
|
+
var direct = getNestedValue({ configStore: latestStore }, cleanKey);
|
|
336
|
+
|
|
337
|
+
if (direct !== undefined) {
|
|
338
|
+
return direct;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
direct = getNestedValue(latestStore, cleanKey);
|
|
342
|
+
|
|
343
|
+
if (direct !== undefined) {
|
|
344
|
+
return direct;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
var names = Object.keys(latestStore);
|
|
348
|
+
|
|
349
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
350
|
+
var block = latestStore[names[i]];
|
|
351
|
+
|
|
352
|
+
if (!block) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
direct = getNestedValue(block, cleanKey);
|
|
357
|
+
|
|
358
|
+
if (direct !== undefined) {
|
|
359
|
+
return direct;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
direct = getNestedValue(block.data, cleanKey);
|
|
363
|
+
|
|
364
|
+
if (direct !== undefined) {
|
|
365
|
+
return direct;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveValue(key) {
|
|
373
|
+
var value = getNestedValue(latestPayload, key);
|
|
374
|
+
|
|
375
|
+
if (value !== undefined) {
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return storedConfigValue(key);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function resolveSetpoint(item, valueKeys, fixedKeys) {
|
|
383
|
+
for (var i = 0; i < valueKeys.length; i += 1) {
|
|
384
|
+
var valueKeyName = valueKeys[i];
|
|
385
|
+
|
|
386
|
+
if (item[valueKeyName] !== undefined && item[valueKeyName] !== null && item[valueKeyName] !== "") {
|
|
387
|
+
var resolved = resolveValue(item[valueKeyName]);
|
|
388
|
+
|
|
389
|
+
if (resolved !== undefined) {
|
|
390
|
+
return resolved;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (var j = 0; j < fixedKeys.length; j += 1) {
|
|
396
|
+
var fixedKeyName = fixedKeys[j];
|
|
397
|
+
|
|
398
|
+
if (item[fixedKeyName] !== undefined && item[fixedKeyName] !== null && item[fixedKeyName] !== "") {
|
|
399
|
+
return item[fixedKeyName];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function itemMemoryKey(item, index, outputKey, configName) {
|
|
407
|
+
var explicitId = item.id || item.scheduleId || item.controlId || item.uid || item.uuid;
|
|
408
|
+
|
|
409
|
+
if (explicitId !== undefined && explicitId !== null && String(explicitId) !== "") {
|
|
410
|
+
return String(configName || "config") + "::" + String(item.controlType || "schedule") + "::" + String(explicitId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var variableName = item.scheduleVariable || item.variable || item.key || item.keyName || "";
|
|
414
|
+
|
|
415
|
+
return JSON.stringify({
|
|
416
|
+
configName: configName || "",
|
|
417
|
+
type: item.controlType || "schedule",
|
|
418
|
+
output: outputKey,
|
|
419
|
+
name: item.name || "",
|
|
420
|
+
days: item.days || [],
|
|
421
|
+
startHour: item.startHour || "",
|
|
422
|
+
startMinute: item.startMinute || "",
|
|
423
|
+
runHour: item.runHour || "",
|
|
424
|
+
runMinute: item.runMinute || "",
|
|
425
|
+
variable: variableName,
|
|
426
|
+
direction: item.direction || "",
|
|
427
|
+
lowSetpoint: item.lowSetpoint || "",
|
|
428
|
+
highSetpoint: item.highSetpoint || "",
|
|
429
|
+
scheduleHighSetpoint: item.scheduleHighSetpoint || "",
|
|
430
|
+
index: index
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function currentScheduleRun(item, now) {
|
|
435
|
+
var days = item.days || [];
|
|
436
|
+
|
|
437
|
+
if (typeof days === "string") {
|
|
438
|
+
days = days.split(",").map(function (day) {
|
|
439
|
+
return day.trim().toLowerCase();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
var startHour = parseInt(item.startHour || 0, 10);
|
|
444
|
+
var startMinute = parseInt(item.startMinute || 0, 10);
|
|
445
|
+
var runHour = parseInt(item.runHour || 0, 10);
|
|
446
|
+
var runMinute = parseInt(item.runMinute || 0, 10);
|
|
447
|
+
|
|
448
|
+
var startMinutes = startHour * 60 + startMinute;
|
|
449
|
+
var runMinutes = runHour * 60 + runMinute;
|
|
450
|
+
|
|
451
|
+
if (runMinutes <= 0) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var dayNames = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
456
|
+
var candidates = [new Date(now), new Date(now)];
|
|
457
|
+
|
|
458
|
+
candidates[1].setDate(candidates[1].getDate() - 1);
|
|
459
|
+
|
|
460
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
461
|
+
var startDay = candidates[i];
|
|
462
|
+
var dayName = dayNames[startDay.getDay()];
|
|
463
|
+
|
|
464
|
+
if (days.length > 0 && days.indexOf(dayName) === -1) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
var start = new Date(startDay);
|
|
469
|
+
start.setHours(Math.floor(startMinutes / 60), startMinutes % 60, 0, 0);
|
|
470
|
+
|
|
471
|
+
var end = new Date(start.getTime() + runMinutes * 60000);
|
|
472
|
+
|
|
473
|
+
if (now >= start && now < end) {
|
|
474
|
+
return dateKey(start) + "@" + startMinutes;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function scheduleHighStopEnabled(item) {
|
|
482
|
+
return asBool(item.highStopEnabled);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function calculateScheduleState(item, itemKey) {
|
|
486
|
+
var mode = normaliseMode(item.mode, "Auto Schedule");
|
|
487
|
+
|
|
488
|
+
if (isManualOn(mode)) {
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (isManualOff(mode)) {
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!isAutoSchedule(mode)) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var runId = currentScheduleRun(item, new Date());
|
|
501
|
+
|
|
502
|
+
if (!runId) {
|
|
503
|
+
stoppedScheduleRuns.delete(itemKey);
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!scheduleHighStopEnabled(item)) {
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (stoppedScheduleRuns.get(itemKey) === runId) {
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
var keyName = item.scheduleVariable || item.variable || item.key || item.keyName;
|
|
516
|
+
var value = asNumber(resolveValue(keyName));
|
|
517
|
+
|
|
518
|
+
var highStopValue = resolveSetpoint(
|
|
519
|
+
item,
|
|
520
|
+
["scheduleHighSetpointKey", "highStopKey", "highSetpointKey"],
|
|
521
|
+
["scheduleHighSetpoint", "highStop", "highSetpoint"]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
var highStop = asNumber(highStopValue);
|
|
525
|
+
|
|
526
|
+
if (Number.isNaN(value) || Number.isNaN(highStop)) {
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (value >= highStop) {
|
|
531
|
+
stoppedScheduleRuns.set(itemKey, runId);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function calculateControlState(item, itemKey, outputKey) {
|
|
539
|
+
var mode = normaliseMode(item.mode, "AUTO");
|
|
540
|
+
|
|
541
|
+
if (isManualOn(mode)) {
|
|
542
|
+
controlItemStates.set(itemKey, 1);
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (isManualOff(mode)) {
|
|
547
|
+
controlItemStates.set(itemKey, 0);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
var keyName = item.variable || item.key || item.keyName;
|
|
552
|
+
var value = asNumber(resolveValue(keyName));
|
|
553
|
+
|
|
554
|
+
var low = asNumber(resolveSetpoint(
|
|
555
|
+
item,
|
|
556
|
+
["lowSetpointKey", "lowKey", "startSetpointKey"],
|
|
557
|
+
["lowSetpoint", "low", "startSetpoint"]
|
|
558
|
+
));
|
|
559
|
+
|
|
560
|
+
var high = asNumber(resolveSetpoint(
|
|
561
|
+
item,
|
|
562
|
+
["highSetpointKey", "highKey", "stopSetpointKey"],
|
|
563
|
+
["highSetpoint", "high", "stopSetpoint"]
|
|
564
|
+
));
|
|
565
|
+
|
|
566
|
+
if (Number.isNaN(value) || Number.isNaN(low) || Number.isNaN(high)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
var lastState = controlItemStates.has(itemKey)
|
|
571
|
+
? controlItemStates.get(itemKey)
|
|
572
|
+
: (outputStates[outputKey] || 0);
|
|
573
|
+
|
|
574
|
+
var newState = lastState;
|
|
575
|
+
var direction = item.direction || "in";
|
|
576
|
+
|
|
577
|
+
if (direction === "in" || direction === "fill" || direction === "normal") {
|
|
578
|
+
if (value <= low) {
|
|
579
|
+
newState = 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (value >= high) {
|
|
583
|
+
newState = 0;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
if (value >= high) {
|
|
587
|
+
newState = 1;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (value <= low) {
|
|
591
|
+
newState = 0;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
controlItemStates.set(itemKey, newState);
|
|
596
|
+
return newState;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function getSchedulerItems() {
|
|
600
|
+
var items = [];
|
|
601
|
+
var configNames = Object.keys(latestStore);
|
|
602
|
+
|
|
603
|
+
configNames.forEach(function (configName) {
|
|
604
|
+
if (!shouldAcceptConfigName(configName)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
var block = latestStore[configName];
|
|
609
|
+
|
|
610
|
+
if (!block || !isSchedulerType(block.type)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
var list = [];
|
|
615
|
+
|
|
616
|
+
if (Array.isArray(block.data)) {
|
|
617
|
+
list = block.data;
|
|
618
|
+
} else if (block.data && typeof block.data === "object" && Array.isArray(block.data.schedule)) {
|
|
619
|
+
list = block.data.schedule;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
list.forEach(function (item) {
|
|
623
|
+
if (item && typeof item === "object") {
|
|
624
|
+
items.push({
|
|
625
|
+
configName: configName,
|
|
626
|
+
item: item
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
return items;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function sendConfigSnapshot(eventName) {
|
|
636
|
+
if (node.configOutput < 1 || node.configOutput > node.outputCount) {
|
|
71
637
|
return;
|
|
72
638
|
}
|
|
73
639
|
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
640
|
+
var outputs = new Array(node.outputCount).fill(null);
|
|
641
|
+
|
|
642
|
+
outputs[node.configOutput - 1] = {
|
|
643
|
+
topic: "configStore",
|
|
644
|
+
event: eventName || "configStore",
|
|
645
|
+
payload: {
|
|
646
|
+
configStore: latestStore
|
|
647
|
+
},
|
|
648
|
+
configStore: latestStore
|
|
81
649
|
};
|
|
82
650
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
651
|
+
node.send(outputs);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function runConfigStore(isOffline, forceOutput) {
|
|
655
|
+
if (closed) {
|
|
656
|
+
return;
|
|
88
657
|
}
|
|
89
658
|
|
|
90
|
-
|
|
659
|
+
var outputs = new Array(node.outputCount).fill(null);
|
|
660
|
+
var statusSummary = [];
|
|
661
|
+
var outputGroups = new Map();
|
|
662
|
+
var activeItemKeys = new Set();
|
|
663
|
+
var schedulerItems = getSchedulerItems();
|
|
664
|
+
|
|
665
|
+
schedulerItems.forEach(function (entry, index) {
|
|
666
|
+
var item = entry.item;
|
|
667
|
+
var configName = entry.configName;
|
|
668
|
+
var outputKey = item.output || ("output" + (index + 1));
|
|
669
|
+
var outputNumber = outputNumberFromKey(outputKey, index, node.outputCount);
|
|
670
|
+
|
|
671
|
+
if (outputNumber === null) {
|
|
672
|
+
statusSummary.push(outputKey + ": " + (item.name || "item") + " skipped - output disabled");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
91
675
|
|
|
92
|
-
|
|
93
|
-
node.connected = true;
|
|
94
|
-
node.lastErrorText = "";
|
|
95
|
-
node.lastErrorTime = 0;
|
|
676
|
+
var itemKey = itemMemoryKey(item, index, outputKey, configName);
|
|
96
677
|
|
|
97
|
-
|
|
678
|
+
item.output = outputKey;
|
|
679
|
+
activeItemKeys.add(itemKey);
|
|
98
680
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
681
|
+
var newState = item.controlType === "control"
|
|
682
|
+
? calculateControlState(item, itemKey, outputKey)
|
|
683
|
+
: calculateScheduleState(item, itemKey);
|
|
684
|
+
|
|
685
|
+
if (newState === null || newState === undefined) {
|
|
686
|
+
statusSummary.push(outputKey + ": " + (item.name || "item") + " skipped");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!outputGroups.has(outputKey)) {
|
|
691
|
+
outputGroups.set(outputKey, {
|
|
692
|
+
outputKey: outputKey,
|
|
693
|
+
outputNumber: outputNumber,
|
|
694
|
+
states: [],
|
|
695
|
+
names: [],
|
|
696
|
+
controlTypes: new Set()
|
|
104
697
|
});
|
|
105
|
-
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
var group = outputGroups.get(outputKey);
|
|
701
|
+
|
|
702
|
+
group.states.push(Number(newState) === 1 ? 1 : 0);
|
|
703
|
+
group.names.push(item.name || outputKey);
|
|
704
|
+
group.controlTypes.add(item.controlType || "schedule");
|
|
106
705
|
});
|
|
107
706
|
|
|
108
|
-
|
|
109
|
-
|
|
707
|
+
stoppedScheduleRuns.forEach(function (_value, key) {
|
|
708
|
+
if (!activeItemKeys.has(key)) {
|
|
709
|
+
stoppedScheduleRuns.delete(key);
|
|
710
|
+
}
|
|
110
711
|
});
|
|
111
712
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
713
|
+
controlItemStates.forEach(function (_value, key) {
|
|
714
|
+
if (!activeItemKeys.has(key)) {
|
|
715
|
+
controlItemStates.delete(key);
|
|
716
|
+
}
|
|
115
717
|
});
|
|
116
718
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
719
|
+
outputGroups.forEach(function (group) {
|
|
720
|
+
var activeRequests = group.states.filter(function (state) {
|
|
721
|
+
return state === 1;
|
|
722
|
+
}).length;
|
|
723
|
+
|
|
724
|
+
var combinedState = activeRequests > 0 ? 1 : 0;
|
|
725
|
+
var lastState = outputStates[group.outputKey];
|
|
726
|
+
|
|
727
|
+
if (node.outputMode === "always" || forceOutput || lastState !== combinedState) {
|
|
728
|
+
outputStates[group.outputKey] = combinedState;
|
|
729
|
+
|
|
730
|
+
var controlType = group.controlTypes.size === 1
|
|
731
|
+
? Array.from(group.controlTypes)[0]
|
|
732
|
+
: "combined";
|
|
733
|
+
|
|
734
|
+
outputs[group.outputNumber - 1] = {
|
|
735
|
+
payload: Boolean(combinedState),
|
|
736
|
+
topic: group.outputKey,
|
|
737
|
+
output: group.outputKey,
|
|
738
|
+
outputNumber: group.outputNumber,
|
|
739
|
+
controlType: controlType,
|
|
740
|
+
activeRequests: activeRequests,
|
|
741
|
+
totalRequests: group.states.length,
|
|
742
|
+
name: group.names.length === 1
|
|
743
|
+
? group.names[0]
|
|
744
|
+
: group.outputKey + " (" + group.names.length + " commands)"
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
statusSummary.push(
|
|
749
|
+
group.outputKey + ": " +
|
|
750
|
+
(combinedState ? "ON" : "OFF") +
|
|
751
|
+
" (" + activeRequests + "/" + group.states.length + " active)"
|
|
752
|
+
);
|
|
120
753
|
});
|
|
121
754
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
755
|
+
if (closed) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
125
758
|
|
|
126
|
-
|
|
759
|
+
if (statusSummary.length === 0) {
|
|
760
|
+
var names = Object.keys(latestStore);
|
|
127
761
|
|
|
128
|
-
if (
|
|
129
|
-
|
|
762
|
+
if (names.length === 0) {
|
|
763
|
+
setStatus("yellow", "ring", "waiting for configStore");
|
|
130
764
|
} else {
|
|
131
|
-
|
|
765
|
+
setStatus("blue", "dot", "config stored: " + names.join(", "));
|
|
132
766
|
}
|
|
133
|
-
});
|
|
134
767
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
node.send(outputs);
|
|
772
|
+
|
|
773
|
+
setStatus(
|
|
774
|
+
isOffline ? "yellow" : "blue",
|
|
775
|
+
"dot",
|
|
776
|
+
(isOffline ? "Offline: " : "") + statusSummary.join(" | ")
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function storeLatestPayload(msg) {
|
|
781
|
+
var payload = safeParseJson(msg.payload);
|
|
782
|
+
|
|
783
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
784
|
+
latestPayload = payload;
|
|
785
|
+
} else {
|
|
786
|
+
latestPayload = {};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function handleGetRequest(msg) {
|
|
791
|
+
var payload = safeParseJson(msg.payload);
|
|
139
792
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
793
|
+
if (msg.getConfigStore === true) {
|
|
794
|
+
sendConfigSnapshot("requested");
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
143
797
|
|
|
144
|
-
|
|
145
|
-
|
|
798
|
+
if (payload && typeof payload === "object") {
|
|
799
|
+
if (payload.getConfigStore === true || payload.configStoreGet === true) {
|
|
800
|
+
sendConfigSnapshot("requested");
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
loadFromDisk();
|
|
809
|
+
|
|
810
|
+
if (Object.keys(latestStore).length > 0) {
|
|
811
|
+
setStatus("blue", "dot", "loaded saved config");
|
|
812
|
+
} else {
|
|
813
|
+
setStatus("yellow", "ring", "waiting for configStore");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
var timer = setInterval(function () {
|
|
817
|
+
runConfigStore(false, false);
|
|
818
|
+
}, node.tickSeconds * 1000);
|
|
819
|
+
|
|
820
|
+
setTimeout(function () {
|
|
821
|
+
runConfigStore(false, true);
|
|
822
|
+
}, 500);
|
|
823
|
+
|
|
824
|
+
node.on("input", function (msg, send, done) {
|
|
825
|
+
send = send || function () {
|
|
826
|
+
node.send.apply(node, arguments);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
if (closed) {
|
|
830
|
+
if (done) {
|
|
831
|
+
done();
|
|
146
832
|
}
|
|
147
833
|
|
|
148
834
|
return;
|
|
149
835
|
}
|
|
150
836
|
|
|
151
|
-
|
|
837
|
+
try {
|
|
838
|
+
if (handleGetRequest(msg)) {
|
|
839
|
+
if (done) {
|
|
840
|
+
done();
|
|
841
|
+
}
|
|
152
842
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
var configStore = extractConfigStore(msg);
|
|
156
847
|
|
|
157
|
-
|
|
158
|
-
|
|
848
|
+
if (configStore) {
|
|
849
|
+
var savedNames = storeConfig(configStore);
|
|
159
850
|
|
|
160
|
-
|
|
161
|
-
|
|
851
|
+
if (savedNames.length > 0) {
|
|
852
|
+
setStatus("blue", "dot", "saved: " + savedNames.join(", "));
|
|
162
853
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
854
|
+
if (node.emitConfigOnUpdate) {
|
|
855
|
+
sendConfigSnapshot("updated");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
runConfigStore(false, true);
|
|
859
|
+
} else {
|
|
860
|
+
setStatus("yellow", "ring", "configStore ignored");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (done) {
|
|
864
|
+
done();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
storeLatestPayload(msg);
|
|
871
|
+
runConfigStore(false, false);
|
|
872
|
+
|
|
873
|
+
if (done) {
|
|
874
|
+
done();
|
|
875
|
+
}
|
|
876
|
+
} catch (err) {
|
|
877
|
+
setStatus("red", "ring", "config store error");
|
|
878
|
+
node.error("3DM Config Store error: " + (err.message || err), msg);
|
|
879
|
+
|
|
880
|
+
if (done) {
|
|
881
|
+
done(err);
|
|
882
|
+
}
|
|
167
883
|
}
|
|
168
|
-
};
|
|
884
|
+
});
|
|
169
885
|
|
|
170
886
|
node.on("close", function (removed, done) {
|
|
171
887
|
if (typeof removed === "function") {
|
|
@@ -174,24 +890,11 @@ module.exports = function (RED) {
|
|
|
174
890
|
|
|
175
891
|
done = done || function () {};
|
|
176
892
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
node.client.end(true, function () {
|
|
181
|
-
done();
|
|
182
|
-
});
|
|
183
|
-
} else {
|
|
184
|
-
done();
|
|
185
|
-
}
|
|
893
|
+
closed = true;
|
|
894
|
+
clearInterval(timer);
|
|
895
|
+
done();
|
|
186
896
|
});
|
|
187
|
-
|
|
188
|
-
node.connect();
|
|
189
897
|
}
|
|
190
898
|
|
|
191
|
-
RED.nodes.registerType("3dm-
|
|
192
|
-
credentials: {
|
|
193
|
-
username: { type: "text" },
|
|
194
|
-
password: { type: "password" }
|
|
195
|
-
}
|
|
196
|
-
});
|
|
899
|
+
RED.nodes.registerType("3dm-config-store", ThreeDMConfigStoreNode);
|
|
197
900
|
};
|