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.
Files changed (36) hide show
  1. package/README.md +31 -14
  2. package/examples/Hue Bridge.json +1 -1
  3. package/huemagic/hue-bridge-config.html +11 -4
  4. package/huemagic/hue-bridge-config.js +180 -154
  5. package/huemagic/hue-bridge.js +5 -5
  6. package/huemagic/hue-brightness.html +22 -8
  7. package/huemagic/hue-brightness.js +16 -5
  8. package/huemagic/hue-buttons.html +21 -7
  9. package/huemagic/hue-buttons.js +16 -5
  10. package/huemagic/hue-group.html +26 -12
  11. package/huemagic/hue-group.js +8 -3
  12. package/huemagic/hue-light.html +22 -8
  13. package/huemagic/hue-light.js +8 -3
  14. package/huemagic/hue-motion.html +22 -8
  15. package/huemagic/hue-motion.js +16 -5
  16. package/huemagic/hue-rules.html +20 -6
  17. package/huemagic/hue-rules.js +15 -4
  18. package/huemagic/hue-scene.html +20 -6
  19. package/huemagic/hue-scene.js +1 -1
  20. package/huemagic/hue-temperature.html +22 -8
  21. package/huemagic/hue-temperature.js +16 -5
  22. package/huemagic/locales/de/hue-bridge-config.html +8 -0
  23. package/huemagic/locales/de/hue-bridge.json +1 -1
  24. package/huemagic/locales/de/hue-group.html +1 -0
  25. package/huemagic/locales/en-US/hue-bridge-config.html +8 -0
  26. package/huemagic/locales/en-US/hue-bridge.html +1 -1
  27. package/huemagic/locales/en-US/hue-bridge.json +1 -1
  28. package/huemagic/locales/en-US/hue-buttons.html +1 -1
  29. package/huemagic/locales/en-US/hue-group.html +1 -0
  30. package/huemagic/locales/en-US/hue-light.html +1 -1
  31. package/huemagic/locales/en-US/hue-magic.html +3 -3
  32. package/huemagic/locales/en-US/hue-motion.html +1 -1
  33. package/huemagic/locales/en-US/hue-temperature.html +1 -1
  34. package/huemagic/utils/api.js +88 -94
  35. package/huemagic/utils/messages.js +134 -126
  36. 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 RESSOURCE MESSAGES
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.ressources = {};
32
- this.ressourcesInGroups = {};
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({ ip: config.bridge, key: config.key })
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.getAllRessources();
54
+ return scope.getAllResources();
47
55
  })
48
- .then(function(allRessources)
56
+ .then(function(allResources)
49
57
  {
50
- scope.log("Process ressources…");
51
- return API.processRessources(allRessources);
58
+ scope.log("Processing bridge resources…");
59
+ return API.processResources(allResources);
52
60
  })
53
- .then(function(allRessources)
61
+ .then(function(allResources)
54
62
  {
55
- // SAVE CURRENT RESSOURCES
56
- scope.ressources = allRessources;
63
+ // SAVE CURRENT RESOURCES
64
+ scope.resources = allResources;
57
65
 
58
66
  // EMIT INITIAL STATES -> NODES
59
- scope.log("Initial emit of ressource states…");
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(replaceRessources = false)
88
+ this.getBridgeInformation = function(replaceResources = false)
81
89
  {
82
90
  return new Promise(function(resolve, reject)
83
91
  {
84
- API.request({ ressource: "/config", version: 1 })
92
+ API.request({ config: config, resource: "/config", version: 1 })
85
93
  .then(function(bridgeInformation)
86
94
  {
87
- // PREPARE TO MATCH V2 RESSOURCES
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 RESSOURCE?
93
- if(replaceRessources === true)
100
+ // ALSO REPLACE CURRENT RESOURCE?
101
+ if(replaceResources === true)
94
102
  {
95
- scope.ressources[bridgeInformation.id] = bridgeInformation;
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 RESSOURCES + RULES
109
- this.getAllRessources = function()
116
+ // GET ALL RESOURCES + RULES
117
+ this.getAllResources = function()
110
118
  {
111
119
  return new Promise(function(resolve, reject)
112
120
  {
113
- var allRessources = [];
121
+ var allResources = [];
114
122
 
115
123
  // GET BRIDGE INFORMATION
116
124
  scope.getBridgeInformation()
117
125
  .then(function(bridgeInformation)
118
126
  {
119
- // PUSH TO RESSOURCES
120
- allRessources.push(bridgeInformation);
127
+ // PUSH TO RESOURCES
128
+ allResources.push(bridgeInformation);
121
129
 
122
- // CONTINUE WITH ALL RESSOURCES
123
- return API.request({ ressource: "all" });
130
+ // CONTINUE WITH ALL RESOURCES
131
+ return API.request({ config: config, resource: "all" });
124
132
  })
125
- .then(function(v2Ressources)
133
+ .then(function(v2Resources)
126
134
  {
127
- // MERGE RESSOURCES
128
- allRessources = allRessources.concat(v2Ressources);
135
+ // MERGE RESOURCES
136
+ allResources = allResources.concat(v2Resources);
129
137
 
130
138
  // GET RULES (LEGACY API)
131
- return API.request({ ressource: "/rules", version: 1 });
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
- allRessources.push(rule);
157
+ allResources.push(rule);
150
158
  }
151
159
 
152
- resolve(allRessources);
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(ressources = false)
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, ressource] of Object.entries(scope.ressources))
175
+ for (const [id, resource] of Object.entries(scope.resources))
168
176
  {
169
- scope.pushUpdatedState(ressource, ressource.type, true);
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 ressource of updates)
205
+ for(let resource of updates)
198
206
  {
199
- let id = ressource.id;
200
- let type = ressource.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(ressource["owner"])
213
+ if(resource["owner"])
206
214
  {
207
- let targetId = ressource["owner"]["rid"];
215
+ let targetId = resource["owner"]["rid"];
208
216
 
209
- if(scope.ressources[targetId])
217
+ if(scope.resources[targetId])
210
218
  {
211
219
  // GET PREVIOUS STATE
212
- previousState = scope.ressources[targetId]["services"][type][id];
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.ressources[targetId]["services"]["button"]))
225
+ for (const [oneButtonID, oneButton] of Object.entries(scope.resources[targetId]["services"]["button"]))
218
226
  {
219
- delete scope.ressources[targetId]["services"]["button"][oneButtonID]["button"];
227
+ delete scope.resources[targetId]["services"]["button"][oneButtonID]["button"];
220
228
  }
221
229
  }
222
230
  }
223
231
  }
224
- else if(scope.ressources[id])
232
+ else if(scope.resources[id])
225
233
  {
226
234
  // GET PREVIOUS STATE
227
- previousState = scope.ressources[id];
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, ressource);
235
- const updatedRessources = diff(previousState, mergedState);
242
+ const mergedState = merge.deep(previousState, resource);
243
+ const updatedResources = diff(previousState, mergedState);
236
244
 
237
- if(Object.values(updatedRessources).length > 0)
245
+ if(Object.values(updatedResources).length > 0)
238
246
  {
239
- if(ressource["owner"])
247
+ if(resource["owner"])
240
248
  {
241
- let targetId = ressource["owner"]["rid"];
249
+ let targetId = resource["owner"]["rid"];
242
250
 
243
- scope.ressources[targetId]["services"][type][id] = mergedState;
244
- scope.ressources[targetId]["updated"] = currentDateTime;
251
+ scope.resources[targetId]["services"][type][id] = mergedState;
252
+ scope.resources[targetId]["updated"] = currentDateTime;
245
253
 
246
254
  // PUSH STATE
247
- scope.pushUpdatedState(scope.ressources[targetId], ressource.type);
255
+ scope.pushUpdatedState(scope.resources[targetId], resource.type);
248
256
  }
249
257
  else
250
258
  {
251
- scope.ressources[id] = mergedState;
252
- scope.ressources[id]["updated"] = currentDateTime;
259
+ scope.resources[id] = mergedState;
260
+ scope.resources[id]["updated"] = currentDateTime;
253
261
 
254
262
  // PUSH STATE
255
- scope.pushUpdatedState(scope.ressources[id], ressource.type);
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(ressource, updatedType, suppressMessage = false)
271
+ this.pushUpdatedState = function(resource, updatedType, suppressMessage = false)
264
272
  {
265
- const msg = { id: ressource.id, type: ressource.type, updatedType: updatedType, services: ressource["services"] ? Object.keys(ressource["services"]) : [], suppressMessage: suppressMessage };
266
- this.events.emit(ressource.id, msg);
267
- this.events.emit("globalRessourceUpdates", msg);
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
- // RESSOURCE CONTAINS SERVICES? -> SERVICE IN GROUP? -> EMIT CHANGES TO GROUPS ALSO
270
- if(this.ressources["_groupsOf"][ressource.id])
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.ressources["_groupsOf"][ressource.id].length - 1; g >= 0; g--)
280
+ for (var g = this.resources["_groupsOf"][resource.id].length - 1; g >= 0; g--)
273
281
  {
274
- const groupID = this.ressources["_groupsOf"][ressource.id][g];
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("globalRessourceUpdates", groupMessage);
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 RESSOURCE (FROM NODES)
291
+ // GET RESOURCE (FROM NODES)
284
292
  this.get = function(type, id = false, options = {})
285
293
  {
286
- // GET SPECIFIC RESSOURCE
294
+ // GET SPECIFIC RESOURCE
287
295
  if(id)
288
296
  {
289
- // RESSOURCE EXISTS? -> PROCEED
290
- if(scope.ressources[id])
297
+ // RESOURCE EXISTS? -> PROCEED
298
+ if(scope.resources[id])
291
299
  {
292
300
  // RESOLVE LINKS
293
- const targetRessource = scope.ressources[id];
294
- const lastState = scope.lastStates[type+targetRessource.id] ? Object.assign({}, scope.lastStates[type+targetRessource.id]) : false;
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(targetRessource, options);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, { ressources: scope.ressources, ...options});
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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(targetRessource, options);
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+targetRessource.id] = Object.assign({}, currentState);
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 RESSOURCES BY TYPE
402
- let allFilteredRessources = {};
409
+ // FILTER RESOURCES BY TYPE
410
+ let allFilteredResources = {};
403
411
 
404
- for (const [rootID, ressource] of Object.entries(scope.ressources))
412
+ for (const [rootID, resource] of Object.entries(scope.resources))
405
413
  {
406
- const isGroup = (ressource["type"] == "room" || ressource["type"] == "zone" || ressource["type"] == "bridge_home");
414
+ const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
407
415
 
408
416
  // NORMAL DEVICES
409
- if(!isGroup && ressource["services"] && ressource["services"][type])
417
+ if(!isGroup && resource["services"] && resource["services"][type])
410
418
  {
411
- for (const [serviceID, targetDevice] of Object.entries(ressource["services"][type]))
419
+ for (const [serviceID, targetDevice] of Object.entries(resource["services"][type]))
412
420
  {
413
- allFilteredRessources[rootID] = scope.get(type, rootID);
421
+ allFilteredResources[rootID] = scope.get(type, rootID);
414
422
  }
415
423
  }
416
- // GROUPED RESSOURCES
424
+ // GROUPED RESOURCES
417
425
  else if(isGroup && type === "group")
418
426
  {
419
- allFilteredRessources[rootID] = scope.get(type, rootID);
427
+ allFilteredResources[rootID] = scope.get(type, rootID);
420
428
  }
421
429
  }
422
430
 
423
- return Object.values(allFilteredRessources);
431
+ return Object.values(allFilteredResources);
424
432
  }
425
433
  }
426
434
 
427
- // PATCH RESSOURCE (FROM NODES)
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
- // GET SERVICE ID
433
- if(version !== 1 && scope.ressources[id] && scope.ressources[id]["services"] && scope.ressources[id]["services"][type])
440
+ if(!scope.patchQueue) { return false; }
441
+ scope.patchQueue.push({ type: type, id: id, patch: patch, version: version }, function (error, response)
434
442
  {
435
- const targetRessource = Object.values(scope.ressources[id]["services"][type])[0];
436
- id = targetRessource.id;
437
- }
438
-
439
- // ACTION!
440
- API.request({ method: "PUT", ressource: (version === 2) ? (type+"/"+id) : id, data: patch, version: version })
441
- .then(function(response) {
442
- resolve(response);
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({ ressource: "/rules/" + id, version: 1 })
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 RESSOURCES
473
- scope.ressources[rule["id"]] = rule;
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("globalRessourceUpdates", function(info)
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 RESSOURCE MODE
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.log("Checking for Hue Bridge firmware updates…");
567
+ if(scope.firmwareUpdateTimeout !== null) { clearTimeout(scope.firmwareUpdateTimeout); };
543
568
  API.request({
569
+ config: config,
544
570
  method: "PUT",
545
- ressource: "/config",
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 * 5);
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 3H
589
+ // NO UPDATES AVAILABLE // TRY AGAIN IN 12H
564
590
  if(scope.nodeActive == true)
565
591
  {
566
- setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 180);
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({ ip: req.query.ip })
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": "huemagic_" + Math.floor((Math.random() * 100) + 1)
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 RESSOURCES
682
- RED.httpAdmin.get('/hue/ressources', function(req, res, next)
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.init({ ip: req.query.bridge, key: req.query.key })
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 RESSOURCES
746
+ // GET ALL OTHER RESOURCES
718
747
  else
719
748
  {
720
- API.init({ ip: req.query.bridge, key: req.query.key })
721
- .then(function(bridge) {
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.processRessources(allRessources);
752
+ return API.processResources(allResources);
727
753
  })
728
- .then(function(processedRessources)
754
+ .then(function(processedResources)
729
755
  {
730
756
  let targetDevices = {};
731
757
 
732
- for (const [id, ressource] of Object.entries(processedRessources))
758
+ for (const [id, resource] of Object.entries(processedResources))
733
759
  {
734
- const isGroup = (ressource["type"] == "room" || ressource["type"] == "zone" || ressource["type"] == "bridge_home");
760
+ const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
735
761
 
736
762
  // NORMAL DEVICES
737
- if(!isGroup && ressource["services"] && ressource["services"][targetType])
763
+ if(!isGroup && resource["services"] && resource["services"][targetType])
738
764
  {
739
- for (const [deviceID, targetDevice] of Object.entries(ressource["services"][targetType]))
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 = ressource.metadata ? ressource.metadata.name : false;
744
- oneDevice.model = ressource.product_data ? ressource.product_data.product_name : false;
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) RESSOURCES
775
+ // GROUPED (LIGHT) RESOURCES
750
776
  else if(isGroup && targetType === "group")
751
777
  {
752
- if(ressource["services"] && ressource["services"]["grouped_light"])
778
+ if(resource["services"] && resource["services"]["grouped_light"])
753
779
  {
754
780
  var oneDevice = {};
755
781
  oneDevice.id = id;
756
- oneDevice.name = ressource.metadata ? ressource.metadata.name : false;
757
- oneDevice.model = ressource["type"];
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" && ressource["type"] == "scene")
789
+ else if(targetType === "scene" && resource["type"] == "scene")
764
790
  {
765
791
  var oneDevice = {};
766
792
  oneDevice.id = id;
767
- oneDevice.name = ressource.metadata ? ressource.metadata.name : false;
768
- oneDevice.group = processedRessources[ressource["group"]["rid"]].metadata.name;
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
  }