node-red-contrib-3dm-space 1.0.5 → 1.1.0
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.js +115 -825
- package/3dm-cloud-out.js +1 -1
- package/3dm-config-store.js +66 -5
- package/README.md +2246 -114
- package/package.json +1 -1
package/3dm-cloud-config.js
CHANGED
|
@@ -1,887 +1,163 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
var
|
|
5
|
-
var path = require("path");
|
|
4
|
+
var mqtt = require("mqtt");
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function asNumber(value) {
|
|
12
|
-
var n = parseFloat(value);
|
|
13
|
-
return Number.isNaN(n) ? NaN : n;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function clampNumber(value, min, max, fallback) {
|
|
17
|
-
var n = parseInt(value, 10);
|
|
18
|
-
|
|
19
|
-
if (Number.isNaN(n)) {
|
|
20
|
-
return fallback;
|
|
21
|
-
}
|
|
6
|
+
var CLOUD_HOST = "3dm.space";
|
|
7
|
+
var CLOUD_PORT = 1883;
|
|
8
|
+
var CLOUD_URL = "mqtt://" + CLOUD_HOST + ":" + CLOUD_PORT;
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
var MQTT_KEEPALIVE = 60;
|
|
11
|
+
var MQTT_CLEAN_SESSION = true;
|
|
12
|
+
var MQTT_RECONNECT_MS = 5000;
|
|
13
|
+
var MQTT_CONNECT_TIMEOUT_MS = 15000;
|
|
14
|
+
var REPEAT_ERROR_LOG_MS = 60000;
|
|
25
15
|
|
|
26
|
-
function
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
16
|
+
function cleanText(value) {
|
|
17
|
+
if (value === null || value === undefined) {
|
|
18
|
+
return "";
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
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);
|
|
21
|
+
return String(value).trim();
|
|
40
22
|
}
|
|
41
23
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (typeof obj !== "object") {
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (hasOwn(obj, key)) {
|
|
52
|
-
return obj[key];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
var current = obj;
|
|
56
|
-
var parts = String(key).split(".");
|
|
57
|
-
|
|
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
|
-
}
|
|
64
|
-
|
|
65
|
-
current = current[part];
|
|
66
|
-
}
|
|
67
|
-
|
|
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
|
-
}
|
|
24
|
+
function ThreeDMCloudConfigNode(config) {
|
|
25
|
+
RED.nodes.createNode(this, config);
|
|
92
26
|
|
|
93
|
-
|
|
94
|
-
maxOutputs = clampNumber(maxOutputs || 24, 1, 24, 24);
|
|
27
|
+
var node = this;
|
|
95
28
|
|
|
96
|
-
|
|
29
|
+
node.name = cleanText(config.name);
|
|
30
|
+
node.clientid = cleanText(config.clientid);
|
|
97
31
|
|
|
98
|
-
|
|
99
|
-
var number = parseInt(match[1], 10);
|
|
32
|
+
node.username = cleanText(config.username);
|
|
100
33
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null;
|
|
34
|
+
if (!node.username && node.credentials && node.credentials.username) {
|
|
35
|
+
node.username = cleanText(node.credentials.username);
|
|
106
36
|
}
|
|
107
37
|
|
|
108
|
-
|
|
38
|
+
node.password = node.credentials && node.credentials.password
|
|
39
|
+
? cleanText(node.credentials.password)
|
|
40
|
+
: "";
|
|
109
41
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
42
|
+
node.connected = false;
|
|
43
|
+
node.client = null;
|
|
44
|
+
node.subscriptions = {};
|
|
113
45
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
function normaliseMode(value, fallback) {
|
|
118
|
-
if (value === null || value === undefined || value === "") {
|
|
119
|
-
return fallback;
|
|
120
|
-
}
|
|
46
|
+
node.lastErrorText = "";
|
|
47
|
+
node.lastErrorTime = 0;
|
|
121
48
|
|
|
122
|
-
|
|
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;
|
|
49
|
+
node.setMaxListeners(0);
|
|
172
50
|
|
|
173
51
|
function warnOncePerMinute(text) {
|
|
174
52
|
var now = Date.now();
|
|
175
53
|
|
|
176
|
-
if (text !==
|
|
177
|
-
|
|
178
|
-
|
|
54
|
+
if (text !== node.lastErrorText || (now - node.lastErrorTime) > REPEAT_ERROR_LOG_MS) {
|
|
55
|
+
node.lastErrorText = text;
|
|
56
|
+
node.lastErrorTime = now;
|
|
179
57
|
node.warn(text);
|
|
180
58
|
}
|
|
181
59
|
}
|
|
182
60
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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) {
|
|
61
|
+
node.connect = function () {
|
|
62
|
+
if (!node.username) {
|
|
63
|
+
node.connected = false;
|
|
64
|
+
node.emit("state", false);
|
|
65
|
+
warnOncePerMinute("3DM Cloud username is missing");
|
|
637
66
|
return;
|
|
638
67
|
}
|
|
639
68
|
|
|
640
|
-
var
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
},
|
|
648
|
-
configStore: latestStore
|
|
69
|
+
var options = {
|
|
70
|
+
username: node.username,
|
|
71
|
+
password: node.password,
|
|
72
|
+
clean: MQTT_CLEAN_SESSION,
|
|
73
|
+
keepalive: MQTT_KEEPALIVE,
|
|
74
|
+
reconnectPeriod: MQTT_RECONNECT_MS,
|
|
75
|
+
connectTimeout: MQTT_CONNECT_TIMEOUT_MS
|
|
649
76
|
};
|
|
650
77
|
|
|
651
|
-
node.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
function runConfigStore(isOffline, forceOutput) {
|
|
655
|
-
if (closed) {
|
|
656
|
-
return;
|
|
78
|
+
if (node.clientid) {
|
|
79
|
+
options.clientId = node.clientid;
|
|
657
80
|
}
|
|
658
81
|
|
|
659
|
-
|
|
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
|
-
}
|
|
82
|
+
node.client = mqtt.connect(CLOUD_URL, options);
|
|
675
83
|
|
|
676
|
-
|
|
84
|
+
node.client.on("connect", function () {
|
|
85
|
+
node.connected = true;
|
|
86
|
+
node.lastErrorText = "";
|
|
87
|
+
node.lastErrorTime = 0;
|
|
677
88
|
|
|
678
|
-
|
|
679
|
-
activeItemKeys.add(itemKey);
|
|
89
|
+
node.emit("state", true);
|
|
680
90
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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()
|
|
91
|
+
Object.keys(node.subscriptions).forEach(function (topic) {
|
|
92
|
+
node.client.subscribe(topic, node.subscriptions[topic], function (err) {
|
|
93
|
+
if (err) {
|
|
94
|
+
warnOncePerMinute("3DM Cloud subscribe failed: " + (err.message || err));
|
|
95
|
+
}
|
|
697
96
|
});
|
|
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");
|
|
97
|
+
});
|
|
705
98
|
});
|
|
706
99
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
stoppedScheduleRuns.delete(key);
|
|
710
|
-
}
|
|
100
|
+
node.client.on("reconnect", function () {
|
|
101
|
+
node.emit("reconnect");
|
|
711
102
|
});
|
|
712
103
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
104
|
+
node.client.on("offline", function () {
|
|
105
|
+
node.connected = false;
|
|
106
|
+
node.emit("state", false);
|
|
717
107
|
});
|
|
718
108
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
);
|
|
109
|
+
node.client.on("close", function () {
|
|
110
|
+
node.connected = false;
|
|
111
|
+
node.emit("state", false);
|
|
753
112
|
});
|
|
754
113
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
114
|
+
node.client.on("error", function (err) {
|
|
115
|
+
node.connected = false;
|
|
116
|
+
node.emit("state", false);
|
|
758
117
|
|
|
759
|
-
|
|
760
|
-
var names = Object.keys(latestStore);
|
|
118
|
+
var text = err && err.message ? err.message : String(err);
|
|
761
119
|
|
|
762
|
-
if (
|
|
763
|
-
|
|
120
|
+
if (text.indexOf("Not authorized") !== -1 || text.indexOf("not authorized") !== -1) {
|
|
121
|
+
warnOncePerMinute("3DM Cloud login rejected. Check username, password, and Client ID.");
|
|
764
122
|
} else {
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
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);
|
|
792
|
-
|
|
793
|
-
if (msg.getConfigStore === true) {
|
|
794
|
-
sendConfigSnapshot("requested");
|
|
795
|
-
return true;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (payload && typeof payload === "object") {
|
|
799
|
-
if (payload.getConfigStore === true || payload.configStoreGet === true) {
|
|
800
|
-
sendConfigSnapshot("requested");
|
|
801
|
-
return true;
|
|
123
|
+
warnOncePerMinute("3DM Cloud connection error: " + text);
|
|
802
124
|
}
|
|
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);
|
|
125
|
+
});
|
|
819
126
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
127
|
+
node.client.on("message", function (topic, payload) {
|
|
128
|
+
node.emit("cloud-message", topic, payload);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
823
131
|
|
|
824
|
-
node.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
};
|
|
132
|
+
node.publish = function (topic, payload, options, callback) {
|
|
133
|
+
if (!node.client || !node.connected) {
|
|
134
|
+
var err = new Error("3DM Cloud is not connected");
|
|
828
135
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
done();
|
|
136
|
+
if (callback) {
|
|
137
|
+
callback(err);
|
|
832
138
|
}
|
|
833
139
|
|
|
834
140
|
return;
|
|
835
141
|
}
|
|
836
142
|
|
|
837
|
-
|
|
838
|
-
if (handleGetRequest(msg)) {
|
|
839
|
-
if (done) {
|
|
840
|
-
done();
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
var configStore = extractConfigStore(msg);
|
|
143
|
+
var data = payload;
|
|
847
144
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (savedNames.length > 0) {
|
|
852
|
-
setStatus("blue", "dot", "saved: " + savedNames.join(", "));
|
|
853
|
-
|
|
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
|
-
}
|
|
145
|
+
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
|
|
146
|
+
data = JSON.stringify(data);
|
|
147
|
+
}
|
|
869
148
|
|
|
870
|
-
|
|
871
|
-
|
|
149
|
+
node.client.publish(topic, data, options || {}, callback);
|
|
150
|
+
};
|
|
872
151
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
} catch (err) {
|
|
877
|
-
setStatus("red", "ring", "config store error");
|
|
878
|
-
node.error("3DM Config Store error: " + (err.message || err), msg);
|
|
152
|
+
node.subscribe = function (topic, options, callback) {
|
|
153
|
+
node.subscriptions[topic] = options || {};
|
|
879
154
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
155
|
+
if (node.client && node.connected) {
|
|
156
|
+
node.client.subscribe(topic, options || {}, callback);
|
|
157
|
+
} else if (callback) {
|
|
158
|
+
callback();
|
|
883
159
|
}
|
|
884
|
-
}
|
|
160
|
+
};
|
|
885
161
|
|
|
886
162
|
node.on("close", function (removed, done) {
|
|
887
163
|
if (typeof removed === "function") {
|
|
@@ -890,11 +166,25 @@ module.exports = function (RED) {
|
|
|
890
166
|
|
|
891
167
|
done = done || function () {};
|
|
892
168
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
169
|
+
node.connected = false;
|
|
170
|
+
|
|
171
|
+
if (node.client) {
|
|
172
|
+
node.client.end(true, function () {
|
|
173
|
+
node.client = null;
|
|
174
|
+
done();
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
done();
|
|
178
|
+
}
|
|
896
179
|
});
|
|
180
|
+
|
|
181
|
+
node.connect();
|
|
897
182
|
}
|
|
898
183
|
|
|
899
|
-
RED.nodes.registerType("3dm-config
|
|
184
|
+
RED.nodes.registerType("3dm-cloud-config", ThreeDMCloudConfigNode, {
|
|
185
|
+
credentials: {
|
|
186
|
+
username: { type: "text" },
|
|
187
|
+
password: { type: "password" }
|
|
188
|
+
}
|
|
189
|
+
});
|
|
900
190
|
};
|