node-red-contrib-huemagic 4.0.2 → 4.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/README.md +31 -14
- package/examples/Hue Bridge.json +1 -1
- package/huemagic/hue-bridge-config.html +11 -4
- package/huemagic/hue-bridge-config.js +180 -154
- package/huemagic/hue-bridge.js +5 -5
- package/huemagic/hue-brightness.html +22 -8
- package/huemagic/hue-brightness.js +16 -5
- package/huemagic/hue-buttons.html +21 -7
- package/huemagic/hue-buttons.js +16 -5
- package/huemagic/hue-group.html +26 -12
- package/huemagic/hue-group.js +8 -3
- package/huemagic/hue-light.html +22 -8
- package/huemagic/hue-light.js +8 -3
- package/huemagic/hue-motion.html +22 -8
- package/huemagic/hue-motion.js +16 -5
- package/huemagic/hue-rules.html +20 -6
- package/huemagic/hue-rules.js +15 -4
- package/huemagic/hue-scene.html +20 -6
- package/huemagic/hue-scene.js +1 -1
- package/huemagic/hue-temperature.html +22 -8
- package/huemagic/hue-temperature.js +16 -5
- package/huemagic/locales/de/hue-bridge-config.html +8 -0
- package/huemagic/locales/de/hue-bridge.json +1 -1
- package/huemagic/locales/de/hue-group.html +1 -0
- package/huemagic/locales/en-US/hue-bridge-config.html +8 -0
- package/huemagic/locales/en-US/hue-bridge.html +1 -1
- package/huemagic/locales/en-US/hue-bridge.json +1 -1
- package/huemagic/locales/en-US/hue-buttons.html +1 -1
- package/huemagic/locales/en-US/hue-group.html +1 -0
- package/huemagic/locales/en-US/hue-light.html +1 -1
- package/huemagic/locales/en-US/hue-magic.html +3 -3
- package/huemagic/locales/en-US/hue-motion.html +1 -1
- package/huemagic/locales/en-US/hue-temperature.html +1 -1
- package/huemagic/utils/api.js +88 -94
- package/huemagic/utils/messages.js +134 -126
- package/package.json +2 -1
|
@@ -9,8 +9,9 @@ module.exports = function(RED)
|
|
|
9
9
|
const diff = require("deep-object-diff").diff;
|
|
10
10
|
const axios = require('axios');
|
|
11
11
|
const https = require('https');
|
|
12
|
+
const fastq = require('fastq');
|
|
12
13
|
|
|
13
|
-
// READABLE
|
|
14
|
+
// READABLE RESOURCE MESSAGES
|
|
14
15
|
const { HueBridgeMessage,
|
|
15
16
|
HueLightMessage,
|
|
16
17
|
HueGroupMessage,
|
|
@@ -28,10 +29,17 @@ module.exports = function(RED)
|
|
|
28
29
|
// STATES
|
|
29
30
|
this.nodeActive = true;
|
|
30
31
|
this.config = config;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
32
|
+
this.resources = {};
|
|
33
|
+
this.resourcesInGroups = {};
|
|
33
34
|
this.lastStates = {};
|
|
34
35
|
this.events = new events.EventEmitter();
|
|
36
|
+
this.patchQueue = null;
|
|
37
|
+
|
|
38
|
+
// RESOURCE ID PATTERN
|
|
39
|
+
this.validResourceID = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
|
|
40
|
+
|
|
41
|
+
// FIRMWARE UPDATE TIMEOUT
|
|
42
|
+
this.firmwareUpdateTimeout = null;
|
|
35
43
|
|
|
36
44
|
// CREATE NODE
|
|
37
45
|
RED.nodes.createNode(scope, config);
|
|
@@ -39,24 +47,24 @@ module.exports = function(RED)
|
|
|
39
47
|
// INITIALIZE
|
|
40
48
|
this.start = function()
|
|
41
49
|
{
|
|
42
|
-
scope.log("Initializing the bridge…");
|
|
43
|
-
API.init({
|
|
50
|
+
scope.log("Initializing the bridge ("+config.bridge+")…");
|
|
51
|
+
API.init({ config: config })
|
|
44
52
|
.then(function(bridge) {
|
|
45
53
|
scope.log("Connected to bridge");
|
|
46
|
-
return scope.
|
|
54
|
+
return scope.getAllResources();
|
|
47
55
|
})
|
|
48
|
-
.then(function(
|
|
56
|
+
.then(function(allResources)
|
|
49
57
|
{
|
|
50
|
-
scope.log("
|
|
51
|
-
return API.
|
|
58
|
+
scope.log("Processing bridge resources…");
|
|
59
|
+
return API.processResources(allResources);
|
|
52
60
|
})
|
|
53
|
-
.then(function(
|
|
61
|
+
.then(function(allResources)
|
|
54
62
|
{
|
|
55
|
-
// SAVE CURRENT
|
|
56
|
-
scope.
|
|
63
|
+
// SAVE CURRENT RESOURCES
|
|
64
|
+
scope.resources = allResources;
|
|
57
65
|
|
|
58
66
|
// EMIT INITIAL STATES -> NODES
|
|
59
|
-
scope.log("Initial emit of
|
|
67
|
+
scope.log("Initial emit of resource states…");
|
|
60
68
|
return scope.emitInitialStates();
|
|
61
69
|
})
|
|
62
70
|
.then(function(emitted)
|
|
@@ -77,22 +85,22 @@ module.exports = function(RED)
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
// FETCH BRIDGE INFORMATION
|
|
80
|
-
this.getBridgeInformation = function(
|
|
88
|
+
this.getBridgeInformation = function(replaceResources = false)
|
|
81
89
|
{
|
|
82
90
|
return new Promise(function(resolve, reject)
|
|
83
91
|
{
|
|
84
|
-
API.request({
|
|
92
|
+
API.request({ config: config, resource: "/config", version: 1 })
|
|
85
93
|
.then(function(bridgeInformation)
|
|
86
94
|
{
|
|
87
|
-
// PREPARE TO MATCH V2
|
|
95
|
+
// PREPARE TO MATCH V2 RESOURCES
|
|
88
96
|
bridgeInformation.id = "bridge";
|
|
89
97
|
bridgeInformation.id_v1 = "/config";
|
|
90
98
|
bridgeInformation.updated = dayjs().format();
|
|
91
99
|
|
|
92
|
-
// ALSO REPLACE CURRENT
|
|
93
|
-
if(
|
|
100
|
+
// ALSO REPLACE CURRENT RESOURCE?
|
|
101
|
+
if(replaceResources === true)
|
|
94
102
|
{
|
|
95
|
-
scope.
|
|
103
|
+
scope.resources[bridgeInformation.id] = bridgeInformation;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
// GIVE BACK
|
|
@@ -105,30 +113,30 @@ module.exports = function(RED)
|
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
// GET ALL
|
|
109
|
-
this.
|
|
116
|
+
// GET ALL RESOURCES + RULES
|
|
117
|
+
this.getAllResources = function()
|
|
110
118
|
{
|
|
111
119
|
return new Promise(function(resolve, reject)
|
|
112
120
|
{
|
|
113
|
-
var
|
|
121
|
+
var allResources = [];
|
|
114
122
|
|
|
115
123
|
// GET BRIDGE INFORMATION
|
|
116
124
|
scope.getBridgeInformation()
|
|
117
125
|
.then(function(bridgeInformation)
|
|
118
126
|
{
|
|
119
|
-
// PUSH TO
|
|
120
|
-
|
|
127
|
+
// PUSH TO RESOURCES
|
|
128
|
+
allResources.push(bridgeInformation);
|
|
121
129
|
|
|
122
|
-
// CONTINUE WITH ALL
|
|
123
|
-
return API.request({
|
|
130
|
+
// CONTINUE WITH ALL RESOURCES
|
|
131
|
+
return API.request({ config: config, resource: "all" });
|
|
124
132
|
})
|
|
125
|
-
.then(function(
|
|
133
|
+
.then(function(v2Resources)
|
|
126
134
|
{
|
|
127
|
-
// MERGE
|
|
128
|
-
|
|
135
|
+
// MERGE RESOURCES
|
|
136
|
+
allResources = allResources.concat(v2Resources);
|
|
129
137
|
|
|
130
138
|
// GET RULES (LEGACY API)
|
|
131
|
-
return API.request({
|
|
139
|
+
return API.request({ config: config, resource: "/rules", version: 1 });
|
|
132
140
|
})
|
|
133
141
|
.then(function(rules)
|
|
134
142
|
{
|
|
@@ -146,17 +154,17 @@ module.exports = function(RED)
|
|
|
146
154
|
rule["type"] = "rule";
|
|
147
155
|
|
|
148
156
|
// PUSH RULES
|
|
149
|
-
|
|
157
|
+
allResources.push(rule);
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
resolve(
|
|
160
|
+
resolve(allResources);
|
|
153
161
|
})
|
|
154
162
|
.catch(function(error) { reject(error); });
|
|
155
163
|
});
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
// EMIT INITIAL STATES -> NODES
|
|
159
|
-
this.emitInitialStates = function(
|
|
167
|
+
this.emitInitialStates = function(resources = false)
|
|
160
168
|
{
|
|
161
169
|
return new Promise(function(resolve, reject)
|
|
162
170
|
{
|
|
@@ -164,9 +172,9 @@ module.exports = function(RED)
|
|
|
164
172
|
setTimeout(function()
|
|
165
173
|
{
|
|
166
174
|
// PUSH ALL STATES
|
|
167
|
-
for (const [id,
|
|
175
|
+
for (const [id, resource] of Object.entries(scope.resources))
|
|
168
176
|
{
|
|
169
|
-
scope.pushUpdatedState(
|
|
177
|
+
scope.pushUpdatedState(resource, resource.type, true);
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
resolve(true);
|
|
@@ -190,69 +198,69 @@ module.exports = function(RED)
|
|
|
190
198
|
this.refreshStatesSSE = function()
|
|
191
199
|
{
|
|
192
200
|
scope.log("Subscribing to bridge events…");
|
|
193
|
-
API.subscribe(function(updates)
|
|
201
|
+
API.subscribe(config, function(updates)
|
|
194
202
|
{
|
|
195
203
|
const currentDateTime = dayjs().format();
|
|
196
204
|
|
|
197
|
-
for(let
|
|
205
|
+
for(let resource of updates)
|
|
198
206
|
{
|
|
199
|
-
let id =
|
|
200
|
-
let type =
|
|
207
|
+
let id = resource.id;
|
|
208
|
+
let type = resource.type;
|
|
201
209
|
|
|
202
210
|
let previousState = false;
|
|
203
211
|
|
|
204
212
|
// HAS OWNER?
|
|
205
|
-
if(
|
|
213
|
+
if(resource["owner"])
|
|
206
214
|
{
|
|
207
|
-
let targetId =
|
|
215
|
+
let targetId = resource["owner"]["rid"];
|
|
208
216
|
|
|
209
|
-
if(scope.
|
|
217
|
+
if(scope.resources[targetId])
|
|
210
218
|
{
|
|
211
219
|
// GET PREVIOUS STATE
|
|
212
|
-
previousState = scope.
|
|
220
|
+
previousState = scope.resources[targetId]["services"][type][id];
|
|
213
221
|
|
|
214
222
|
// IS BUTTON? -> REMOVE PREVIOUS STATES
|
|
215
223
|
if(type === "button")
|
|
216
224
|
{
|
|
217
|
-
for (const [oneButtonID, oneButton] of Object.entries(scope.
|
|
225
|
+
for (const [oneButtonID, oneButton] of Object.entries(scope.resources[targetId]["services"]["button"]))
|
|
218
226
|
{
|
|
219
|
-
delete scope.
|
|
227
|
+
delete scope.resources[targetId]["services"]["button"][oneButtonID]["button"];
|
|
220
228
|
}
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
|
-
else if(scope.
|
|
232
|
+
else if(scope.resources[id])
|
|
225
233
|
{
|
|
226
234
|
// GET PREVIOUS STATE
|
|
227
|
-
previousState = scope.
|
|
235
|
+
previousState = scope.resources[id];
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
// NO PREVIOUS STATE?
|
|
231
239
|
if(!previousState) { return false; }
|
|
232
240
|
|
|
233
241
|
// CHECK DIFFERENCES
|
|
234
|
-
const mergedState = merge.deep(previousState,
|
|
235
|
-
const
|
|
242
|
+
const mergedState = merge.deep(previousState, resource);
|
|
243
|
+
const updatedResources = diff(previousState, mergedState);
|
|
236
244
|
|
|
237
|
-
if(Object.values(
|
|
245
|
+
if(Object.values(updatedResources).length > 0)
|
|
238
246
|
{
|
|
239
|
-
if(
|
|
247
|
+
if(resource["owner"])
|
|
240
248
|
{
|
|
241
|
-
let targetId =
|
|
249
|
+
let targetId = resource["owner"]["rid"];
|
|
242
250
|
|
|
243
|
-
scope.
|
|
244
|
-
scope.
|
|
251
|
+
scope.resources[targetId]["services"][type][id] = mergedState;
|
|
252
|
+
scope.resources[targetId]["updated"] = currentDateTime;
|
|
245
253
|
|
|
246
254
|
// PUSH STATE
|
|
247
|
-
scope.pushUpdatedState(scope.
|
|
255
|
+
scope.pushUpdatedState(scope.resources[targetId], resource.type);
|
|
248
256
|
}
|
|
249
257
|
else
|
|
250
258
|
{
|
|
251
|
-
scope.
|
|
252
|
-
scope.
|
|
259
|
+
scope.resources[id] = mergedState;
|
|
260
|
+
scope.resources[id]["updated"] = currentDateTime;
|
|
253
261
|
|
|
254
262
|
// PUSH STATE
|
|
255
|
-
scope.pushUpdatedState(scope.
|
|
263
|
+
scope.pushUpdatedState(scope.resources[id], resource.type);
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
266
|
}
|
|
@@ -260,42 +268,42 @@ module.exports = function(RED)
|
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
// PUSH UPDATED STATE
|
|
263
|
-
this.pushUpdatedState = function(
|
|
271
|
+
this.pushUpdatedState = function(resource, updatedType, suppressMessage = false)
|
|
264
272
|
{
|
|
265
|
-
const msg = { id:
|
|
266
|
-
this.events.emit(
|
|
267
|
-
this.events.emit("
|
|
273
|
+
const msg = { id: resource.id, type: resource.type, updatedType: updatedType, services: resource["services"] ? Object.keys(resource["services"]) : [], suppressMessage: suppressMessage };
|
|
274
|
+
this.events.emit(config.id + "_" + resource.id, msg);
|
|
275
|
+
this.events.emit(config.id + "_" + "globalResourceUpdates", msg);
|
|
268
276
|
|
|
269
|
-
//
|
|
270
|
-
if(this.
|
|
277
|
+
// RESOURCE CONTAINS SERVICES? -> SERVICE IN GROUP? -> EMIT CHANGES TO GROUPS ALSO
|
|
278
|
+
if(this.resources["_groupsOf"][resource.id])
|
|
271
279
|
{
|
|
272
|
-
for (var g = this.
|
|
280
|
+
for (var g = this.resources["_groupsOf"][resource.id].length - 1; g >= 0; g--)
|
|
273
281
|
{
|
|
274
|
-
const groupID = this.
|
|
282
|
+
const groupID = this.resources["_groupsOf"][resource.id][g];
|
|
275
283
|
const groupMessage = { id: groupID, type: "group", updatedType: updatedType, services: [], suppressMessage: suppressMessage };
|
|
276
284
|
|
|
277
|
-
this.events.emit(groupID, groupMessage);
|
|
278
|
-
this.events.emit("
|
|
285
|
+
this.events.emit(config.id + "_" + groupID, groupMessage);
|
|
286
|
+
this.events.emit(config.id + "_" + "globalResourceUpdates", groupMessage);
|
|
279
287
|
}
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
283
|
-
// GET
|
|
291
|
+
// GET RESOURCE (FROM NODES)
|
|
284
292
|
this.get = function(type, id = false, options = {})
|
|
285
293
|
{
|
|
286
|
-
// GET SPECIFIC
|
|
294
|
+
// GET SPECIFIC RESOURCE
|
|
287
295
|
if(id)
|
|
288
296
|
{
|
|
289
|
-
//
|
|
290
|
-
if(scope.
|
|
297
|
+
// RESOURCE EXISTS? -> PROCEED
|
|
298
|
+
if(scope.resources[id])
|
|
291
299
|
{
|
|
292
300
|
// RESOLVE LINKS
|
|
293
|
-
const
|
|
294
|
-
const lastState = scope.lastStates[type+
|
|
301
|
+
const targetResource = scope.resources[id];
|
|
302
|
+
const lastState = scope.lastStates[type+targetResource.id] ? Object.assign({}, scope.lastStates[type+targetResource.id]) : false;
|
|
295
303
|
|
|
296
304
|
if(type == "bridge")
|
|
297
305
|
{
|
|
298
|
-
const message = new HueBridgeMessage(
|
|
306
|
+
const message = new HueBridgeMessage(targetResource, options);
|
|
299
307
|
|
|
300
308
|
// GET CURRENT STATE MESSAGE
|
|
301
309
|
let currentState = message.msg;
|
|
@@ -303,11 +311,11 @@ module.exports = function(RED)
|
|
|
303
311
|
}
|
|
304
312
|
else if(type == "light")
|
|
305
313
|
{
|
|
306
|
-
const message = new HueLightMessage(
|
|
314
|
+
const message = new HueLightMessage(targetResource, options);
|
|
307
315
|
|
|
308
316
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
309
317
|
let currentState = message.msg;
|
|
310
|
-
scope.lastStates[type+
|
|
318
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
311
319
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
312
320
|
currentState.lastState = lastState;
|
|
313
321
|
|
|
@@ -316,11 +324,11 @@ module.exports = function(RED)
|
|
|
316
324
|
else if(type == "group")
|
|
317
325
|
{
|
|
318
326
|
// GET MESSAGE
|
|
319
|
-
const message = new HueGroupMessage(
|
|
327
|
+
const message = new HueGroupMessage(targetResource, { resources: scope.resources, ...options});
|
|
320
328
|
|
|
321
329
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
322
330
|
let currentState = message.msg;
|
|
323
|
-
scope.lastStates[type+
|
|
331
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
324
332
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
325
333
|
currentState.lastState = lastState;
|
|
326
334
|
|
|
@@ -328,11 +336,11 @@ module.exports = function(RED)
|
|
|
328
336
|
}
|
|
329
337
|
else if(type == "button")
|
|
330
338
|
{
|
|
331
|
-
const message = new HueButtonsMessage(
|
|
339
|
+
const message = new HueButtonsMessage(targetResource, options);
|
|
332
340
|
|
|
333
341
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
334
342
|
let currentState = message.msg;
|
|
335
|
-
scope.lastStates[type+
|
|
343
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
336
344
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
337
345
|
currentState.lastState = lastState;
|
|
338
346
|
|
|
@@ -340,11 +348,11 @@ module.exports = function(RED)
|
|
|
340
348
|
}
|
|
341
349
|
else if(type == "motion")
|
|
342
350
|
{
|
|
343
|
-
const message = new HueMotionMessage(
|
|
351
|
+
const message = new HueMotionMessage(targetResource, options);
|
|
344
352
|
|
|
345
353
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
346
354
|
let currentState = message.msg;
|
|
347
|
-
scope.lastStates[type+
|
|
355
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
348
356
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
349
357
|
currentState.lastState = lastState;
|
|
350
358
|
|
|
@@ -352,11 +360,11 @@ module.exports = function(RED)
|
|
|
352
360
|
}
|
|
353
361
|
else if(type == "temperature")
|
|
354
362
|
{
|
|
355
|
-
const message = new HueTemperatureMessage(
|
|
363
|
+
const message = new HueTemperatureMessage(targetResource, options);
|
|
356
364
|
|
|
357
365
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
358
366
|
let currentState = message.msg;
|
|
359
|
-
scope.lastStates[type+
|
|
367
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
360
368
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
361
369
|
currentState.lastState = lastState;
|
|
362
370
|
|
|
@@ -364,11 +372,11 @@ module.exports = function(RED)
|
|
|
364
372
|
}
|
|
365
373
|
else if(type == "light_level")
|
|
366
374
|
{
|
|
367
|
-
const message = new HueBrightnessMessage(
|
|
375
|
+
const message = new HueBrightnessMessage(targetResource, options);
|
|
368
376
|
|
|
369
377
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
370
378
|
let currentState = message.msg;
|
|
371
|
-
scope.lastStates[type+
|
|
379
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
372
380
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
373
381
|
currentState.lastState = lastState;
|
|
374
382
|
|
|
@@ -376,11 +384,11 @@ module.exports = function(RED)
|
|
|
376
384
|
}
|
|
377
385
|
else if(type == "rule")
|
|
378
386
|
{
|
|
379
|
-
const message = new HueRulesMessage(
|
|
387
|
+
const message = new HueRulesMessage(targetResource, options);
|
|
380
388
|
|
|
381
389
|
// GET & SAVE LAST STATE AND DIFFERENCES
|
|
382
390
|
let currentState = message.msg;
|
|
383
|
-
scope.lastStates[type+
|
|
391
|
+
scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
|
|
384
392
|
currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
|
|
385
393
|
currentState.lastState = lastState;
|
|
386
394
|
|
|
@@ -398,61 +406,78 @@ module.exports = function(RED)
|
|
|
398
406
|
}
|
|
399
407
|
else
|
|
400
408
|
{
|
|
401
|
-
// FILTER
|
|
402
|
-
let
|
|
409
|
+
// FILTER RESOURCES BY TYPE
|
|
410
|
+
let allFilteredResources = {};
|
|
403
411
|
|
|
404
|
-
for (const [rootID,
|
|
412
|
+
for (const [rootID, resource] of Object.entries(scope.resources))
|
|
405
413
|
{
|
|
406
|
-
const isGroup = (
|
|
414
|
+
const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
|
|
407
415
|
|
|
408
416
|
// NORMAL DEVICES
|
|
409
|
-
if(!isGroup &&
|
|
417
|
+
if(!isGroup && resource["services"] && resource["services"][type])
|
|
410
418
|
{
|
|
411
|
-
for (const [serviceID, targetDevice] of Object.entries(
|
|
419
|
+
for (const [serviceID, targetDevice] of Object.entries(resource["services"][type]))
|
|
412
420
|
{
|
|
413
|
-
|
|
421
|
+
allFilteredResources[rootID] = scope.get(type, rootID);
|
|
414
422
|
}
|
|
415
423
|
}
|
|
416
|
-
// GROUPED
|
|
424
|
+
// GROUPED RESOURCES
|
|
417
425
|
else if(isGroup && type === "group")
|
|
418
426
|
{
|
|
419
|
-
|
|
427
|
+
allFilteredResources[rootID] = scope.get(type, rootID);
|
|
420
428
|
}
|
|
421
429
|
}
|
|
422
430
|
|
|
423
|
-
return Object.values(
|
|
431
|
+
return Object.values(allFilteredResources);
|
|
424
432
|
}
|
|
425
433
|
}
|
|
426
434
|
|
|
427
|
-
// PATCH
|
|
435
|
+
// PATCH RESOURCE (FROM NODES)
|
|
428
436
|
this.patch = function(type, id, patch, version = 2)
|
|
429
437
|
{
|
|
430
438
|
return new Promise(function(resolve, reject)
|
|
431
439
|
{
|
|
432
|
-
|
|
433
|
-
|
|
440
|
+
if(!scope.patchQueue) { return false; }
|
|
441
|
+
scope.patchQueue.push({ type: type, id: id, patch: patch, version: version }, function (error, response)
|
|
434
442
|
{
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
})
|
|
444
|
-
.catch(function(error) {
|
|
445
|
-
reject(error);
|
|
443
|
+
if(error)
|
|
444
|
+
{
|
|
445
|
+
reject(error);
|
|
446
|
+
}
|
|
447
|
+
else
|
|
448
|
+
{
|
|
449
|
+
resolve(response);
|
|
450
|
+
}
|
|
446
451
|
});
|
|
447
452
|
});
|
|
448
453
|
}
|
|
449
454
|
|
|
455
|
+
// PATCH RESOURCE (WORKER) / 7 PROCESSES IN PARALLEL
|
|
456
|
+
this.patchQueue = fastq(function({ type, id, patch, version }, callback)
|
|
457
|
+
{
|
|
458
|
+
// GET SERVICE ID
|
|
459
|
+
if(version !== 1 && scope.resources[id] && scope.resources[id]["services"] && scope.resources[id]["services"][type])
|
|
460
|
+
{
|
|
461
|
+
const targetResource = Object.values(scope.resources[id]["services"][type])[0];
|
|
462
|
+
id = targetResource.id;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ACTION!
|
|
466
|
+
API.request({ config: config, method: "PUT", resource: (version === 2) ? (type+"/"+id) : id, data: patch, version: version })
|
|
467
|
+
.then(function(response) {
|
|
468
|
+
callback(null, response);
|
|
469
|
+
})
|
|
470
|
+
.catch(function(error) {
|
|
471
|
+
callback(error, null);
|
|
472
|
+
});
|
|
473
|
+
}, config.worker ? parseInt(config.worker) : 10);
|
|
474
|
+
|
|
450
475
|
// RE-FETCH RULE (RECEIVES NO UPDATES VIA SSE)
|
|
451
476
|
this.refetchRule = function(id)
|
|
452
477
|
{
|
|
453
478
|
return new Promise(function(resolve, reject)
|
|
454
479
|
{
|
|
455
|
-
API.request({
|
|
480
|
+
API.request({ config: config, resource: "/rules/" + id, version: 1 })
|
|
456
481
|
.then(function(rule)
|
|
457
482
|
{
|
|
458
483
|
// "RENAME" OWNER
|
|
@@ -469,8 +494,8 @@ module.exports = function(RED)
|
|
|
469
494
|
// UPDATED TIME
|
|
470
495
|
rule["updated"] = dayjs().format();
|
|
471
496
|
|
|
472
|
-
// ADD BACK TO
|
|
473
|
-
scope.
|
|
497
|
+
// ADD BACK TO RESOURCES
|
|
498
|
+
scope.resources[rule["id"]] = rule;
|
|
474
499
|
|
|
475
500
|
// PUSH UPDATED STATE
|
|
476
501
|
scope.pushUpdatedState(rule, "rule");
|
|
@@ -505,7 +530,7 @@ module.exports = function(RED)
|
|
|
505
530
|
if(!id)
|
|
506
531
|
{
|
|
507
532
|
// UNIVERSAL MODE
|
|
508
|
-
this.events.on("
|
|
533
|
+
this.events.on(config.id + "_" + "globalResourceUpdates", function(info)
|
|
509
534
|
{
|
|
510
535
|
if(type === "bridge")
|
|
511
536
|
{
|
|
@@ -523,8 +548,8 @@ module.exports = function(RED)
|
|
|
523
548
|
}
|
|
524
549
|
else
|
|
525
550
|
{
|
|
526
|
-
// SPECIFIC
|
|
527
|
-
this.events.on(id, function(info)
|
|
551
|
+
// SPECIFIC RESOURCE MODE
|
|
552
|
+
this.events.on(config.id + "_" + id, function(info)
|
|
528
553
|
{
|
|
529
554
|
if(type === "bridge" || messageWhitelist[type].includes(info.updatedType))
|
|
530
555
|
{
|
|
@@ -539,10 +564,11 @@ module.exports = function(RED)
|
|
|
539
564
|
{
|
|
540
565
|
if((config.autoupdates && config.autoupdates == true) || typeof config.autoupdates == 'undefined')
|
|
541
566
|
{
|
|
542
|
-
scope.
|
|
567
|
+
if(scope.firmwareUpdateTimeout !== null) { clearTimeout(scope.firmwareUpdateTimeout); };
|
|
543
568
|
API.request({
|
|
569
|
+
config: config,
|
|
544
570
|
method: "PUT",
|
|
545
|
-
|
|
571
|
+
resource: "/config",
|
|
546
572
|
version: 1,
|
|
547
573
|
data: {
|
|
548
574
|
swupdate2: {
|
|
@@ -555,15 +581,15 @@ module.exports = function(RED)
|
|
|
555
581
|
{
|
|
556
582
|
if(scope.nodeActive == true)
|
|
557
583
|
{
|
|
558
|
-
setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 *
|
|
584
|
+
scope.firmwareUpdateTimeout = setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 720);
|
|
559
585
|
}
|
|
560
586
|
})
|
|
561
587
|
.catch(function(error)
|
|
562
588
|
{
|
|
563
|
-
// NO UPDATES AVAILABLE // TRY AGAIN IN
|
|
589
|
+
// NO UPDATES AVAILABLE // TRY AGAIN IN 12H
|
|
564
590
|
if(scope.nodeActive == true)
|
|
565
591
|
{
|
|
566
|
-
setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 *
|
|
592
|
+
scope.firmwareUpdateTimeout = setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 720);
|
|
567
593
|
}
|
|
568
594
|
});
|
|
569
595
|
}
|
|
@@ -581,10 +607,16 @@ module.exports = function(RED)
|
|
|
581
607
|
|
|
582
608
|
// UNSUBSCRIBE FROM BRIDGE EVENTS
|
|
583
609
|
scope.log("Unsubscribing from bridge events…");
|
|
584
|
-
API.unsubscribe();
|
|
610
|
+
API.unsubscribe(config);
|
|
585
611
|
|
|
586
612
|
// UNSUBSCRIBE FROM "READY" EVENTS
|
|
587
613
|
scope.events.removeAllListeners();
|
|
614
|
+
|
|
615
|
+
// REMOVE FIRMWARE UPDATE TIMEOUT
|
|
616
|
+
if(scope.firmwareUpdateTimeout !== null) { clearTimeout(scope.firmwareUpdateTimeout); }
|
|
617
|
+
|
|
618
|
+
// KILL QUEUE
|
|
619
|
+
scope.patchQueue.kill();
|
|
588
620
|
});
|
|
589
621
|
}
|
|
590
622
|
|
|
@@ -628,7 +660,7 @@ module.exports = function(RED)
|
|
|
628
660
|
}
|
|
629
661
|
else
|
|
630
662
|
{
|
|
631
|
-
API.init({
|
|
663
|
+
API.init({ config: { bridge: req.query.ip, key: "huemagic" } })
|
|
632
664
|
.then(function(bridge) {
|
|
633
665
|
res.end(bridge.name);
|
|
634
666
|
})
|
|
@@ -656,7 +688,7 @@ module.exports = function(RED)
|
|
|
656
688
|
"Content-Type": "application/json; charset=utf-8"
|
|
657
689
|
},
|
|
658
690
|
"data": {
|
|
659
|
-
"devicetype": "
|
|
691
|
+
"devicetype": "HueMagic for Node-RED (" + Math.floor((Math.random() * 100) + 1) + ")"
|
|
660
692
|
}
|
|
661
693
|
})
|
|
662
694
|
.then(function(response)
|
|
@@ -678,18 +710,15 @@ module.exports = function(RED)
|
|
|
678
710
|
});
|
|
679
711
|
|
|
680
712
|
//
|
|
681
|
-
// DISCOVER
|
|
682
|
-
RED.httpAdmin.get('/hue/
|
|
713
|
+
// DISCOVER RESOURCES
|
|
714
|
+
RED.httpAdmin.get('/hue/resources', function(req, res, next)
|
|
683
715
|
{
|
|
684
716
|
const targetType = req.query.type;
|
|
685
717
|
|
|
686
718
|
// GET ALL RULES
|
|
687
719
|
if(targetType == "rule")
|
|
688
720
|
{
|
|
689
|
-
API.
|
|
690
|
-
.then(function(bridge) {
|
|
691
|
-
return API.request({ ressource: "/rules", version: 1 });
|
|
692
|
-
})
|
|
721
|
+
API.request({ config: { bridge: req.query.bridge, key: req.query.key }, resource: "/rules", version: 1 })
|
|
693
722
|
.then(function(rules)
|
|
694
723
|
{
|
|
695
724
|
let targetRules = {};
|
|
@@ -714,58 +743,55 @@ module.exports = function(RED)
|
|
|
714
743
|
res.status(500).send(JSON.stringify(error));
|
|
715
744
|
});
|
|
716
745
|
}
|
|
717
|
-
// GET ALL OTHER
|
|
746
|
+
// GET ALL OTHER RESOURCES
|
|
718
747
|
else
|
|
719
748
|
{
|
|
720
|
-
API.
|
|
721
|
-
.then(function(
|
|
722
|
-
return API.request({ ressource: "all" });
|
|
723
|
-
})
|
|
724
|
-
.then(function(allRessources)
|
|
749
|
+
API.request({ config: { bridge: req.query.bridge, key: req.query.key }, resource: "all" })
|
|
750
|
+
.then(function(allResources)
|
|
725
751
|
{
|
|
726
|
-
return API.
|
|
752
|
+
return API.processResources(allResources);
|
|
727
753
|
})
|
|
728
|
-
.then(function(
|
|
754
|
+
.then(function(processedResources)
|
|
729
755
|
{
|
|
730
756
|
let targetDevices = {};
|
|
731
757
|
|
|
732
|
-
for (const [id,
|
|
758
|
+
for (const [id, resource] of Object.entries(processedResources))
|
|
733
759
|
{
|
|
734
|
-
const isGroup = (
|
|
760
|
+
const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
|
|
735
761
|
|
|
736
762
|
// NORMAL DEVICES
|
|
737
|
-
if(!isGroup &&
|
|
763
|
+
if(!isGroup && resource["services"] && resource["services"][targetType])
|
|
738
764
|
{
|
|
739
|
-
for (const [deviceID, targetDevice] of Object.entries(
|
|
765
|
+
for (const [deviceID, targetDevice] of Object.entries(resource["services"][targetType]))
|
|
740
766
|
{
|
|
741
767
|
var oneDevice = {};
|
|
742
768
|
oneDevice.id = id;
|
|
743
|
-
oneDevice.name =
|
|
744
|
-
oneDevice.model =
|
|
769
|
+
oneDevice.name = resource.metadata ? resource.metadata.name : false;
|
|
770
|
+
oneDevice.model = resource.product_data ? resource.product_data.product_name : false;
|
|
745
771
|
|
|
746
772
|
targetDevices[id] = oneDevice;
|
|
747
773
|
}
|
|
748
774
|
}
|
|
749
|
-
// GROUPED (LIGHT)
|
|
775
|
+
// GROUPED (LIGHT) RESOURCES
|
|
750
776
|
else if(isGroup && targetType === "group")
|
|
751
777
|
{
|
|
752
|
-
if(
|
|
778
|
+
if(resource["services"] && resource["services"]["grouped_light"])
|
|
753
779
|
{
|
|
754
780
|
var oneDevice = {};
|
|
755
781
|
oneDevice.id = id;
|
|
756
|
-
oneDevice.name =
|
|
757
|
-
oneDevice.model =
|
|
782
|
+
oneDevice.name = resource.metadata ? resource.metadata.name : false;
|
|
783
|
+
oneDevice.model = resource["type"];
|
|
758
784
|
|
|
759
785
|
targetDevices[id] = oneDevice;
|
|
760
786
|
}
|
|
761
787
|
}
|
|
762
788
|
// SCENES
|
|
763
|
-
else if(targetType === "scene" &&
|
|
789
|
+
else if(targetType === "scene" && resource["type"] == "scene")
|
|
764
790
|
{
|
|
765
791
|
var oneDevice = {};
|
|
766
792
|
oneDevice.id = id;
|
|
767
|
-
oneDevice.name =
|
|
768
|
-
oneDevice.group =
|
|
793
|
+
oneDevice.name = resource.metadata ? resource.metadata.name : false;
|
|
794
|
+
oneDevice.group = processedResources[resource["group"]["rid"]].metadata.name;
|
|
769
795
|
|
|
770
796
|
targetDevices[id] = oneDevice;
|
|
771
797
|
}
|