node-red-contrib-huemagic 4.1.0 → 4.2.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 CHANGED
@@ -796,11 +796,21 @@ If the status of the node has changed via a certain command, the entire command
796
796
 
797
797
  # Changelog
798
798
 
799
- ### v4.1.0 (latest)
799
+ ### v4.2.0 (latest)
800
+
801
+ * Commands are now re-executed up to three times if they fail due to a bridge timeout
802
+ * The "image" option on the "Hue Light" node will now set the corresponding gradient colors on supported resources
803
+ * Better handling of broken connections to the bridge ([#309](https://github.com/Foddy/node-red-contrib-huemagic/pull/309)) (thx)
804
+ * Fixed an error with the "Hue Scenes" node on newer bridge firmwares ([#335](https://github.com/Foddy/node-red-contrib-huemagic/issues/335)) ([#339](https://github.com/Foddy/node-red-contrib-huemagic/pull/339)) (thx)
805
+ * Fixed an uncaught exception on newer bridge firmwares ([#302](https://github.com/Foddy/node-red-contrib-huemagic/issues/302)) ([#309](https://github.com/Foddy/node-red-contrib-huemagic/pull/309)) (thx)
806
+ * Updated dependencies to the latest versions
807
+ * Fixed some typos here and there
808
+
809
+ ### v4.1.0
800
810
 
801
811
  * New queue worker throttles the number of parallel requests to the bridge to avoid 503 API limit errors (can be configured in the Bridge configuration)
802
- * Resources are now alphabetically sorted in the node´s configuration inetrface ([#282](https://github.com/Foddy/node-red-contrib-huemagic/pull/282)) (thx)
803
- * "Hue Bridghtness" node was optimized to output more accurate "dark" and "dayLight" values
812
+ * Resources are now alphabetically sorted in the node´s configuration interface ([#282](https://github.com/Foddy/node-red-contrib-huemagic/pull/282)) (thx)
813
+ * "Hue Brightness" node was optimized to output more accurate "dark" and "dayLight" values
804
814
  * Several optimizations in the documentation of some nodes
805
815
 
806
816
  ### v4.0.5
@@ -51,8 +51,8 @@
51
51
  name: { value:"Hue Bridge", required: true },
52
52
  bridge: { value:"", required: true },
53
53
  key: { value:"", required: true },
54
- worker: { value: 10, required: true, validate:function(v) {
55
- return (!isNaN(v) && v > 0)
54
+ worker: { value: 10, required: true, validate: function(v) {
55
+ return (!isNaN(v) && v > 0);
56
56
  }},
57
57
  autoupdates: { value: true },
58
58
  disableupdates: { value: false }
@@ -34,6 +34,7 @@ module.exports = function(RED)
34
34
  this.lastStates = {};
35
35
  this.events = new events.EventEmitter();
36
36
  this.patchQueue = null;
37
+ this.timerWatchDog = null; // 31/01/2022 Supergiovane: watchdog for the connection with the bridge
37
38
 
38
39
  // RESOURCE ID PATTERN
39
40
  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;
@@ -44,9 +45,35 @@ module.exports = function(RED)
44
45
  // CREATE NODE
45
46
  RED.nodes.createNode(scope, config);
46
47
 
48
+ this.startWatchdog = function() {
49
+ // 31/01/2022 Supergiovane: Check wether the bridge is connected.
50
+ if (this.timerWatchDog !== null) clearTimeout(this.timerWatchDog);
51
+ this.timerWatchDog = setTimeout(() => {
52
+ try {
53
+ API.request({ config: config, resource: "/config", version: 1 })
54
+ .then(function(bridgeInformation)
55
+ {
56
+ // The bridge is still connected
57
+ scope.startWatchdog();
58
+ })
59
+ .catch(function(error)
60
+ {
61
+ // Error connecting to the bridge
62
+ scope.log("Error requesting info from the bridge. Reconnect in some secs. " + error.message);
63
+ scope.start();
64
+ });
65
+ } catch (error) {
66
+ scope.log("Lost connection with the bridge. Reconnect in some secs. " + error.message);
67
+ scope.start();
68
+ }
69
+
70
+ }, 10000);
71
+ }
47
72
  // INITIALIZE
48
73
  this.start = function()
49
74
  {
75
+ this.startWatchdog(); // 31/01/2022 Supergiovane: starts the watchdog.
76
+
50
77
  scope.log("Initializing the bridge ("+config.bridge+")…");
51
78
  API.init({ config: config })
52
79
  .then(function(bridge) {
@@ -303,96 +330,129 @@ module.exports = function(RED)
303
330
 
304
331
  if(type == "bridge")
305
332
  {
306
- const message = new HueBridgeMessage(targetResource, options);
307
-
308
- // GET CURRENT STATE MESSAGE
309
- let currentState = message.msg;
310
- return currentState;
333
+ try {
334
+ const message = new HueBridgeMessage(targetResource, options);
335
+
336
+ // GET CURRENT STATE MESSAGE
337
+ let currentState = message.msg;
338
+ return currentState;
339
+ } catch (error) {
340
+ return false;
341
+ }
342
+
311
343
  }
312
344
  else if(type == "light")
313
345
  {
314
- const message = new HueLightMessage(targetResource, options);
315
-
316
- // GET & SAVE LAST STATE AND DIFFERENCES
317
- let currentState = message.msg;
318
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
319
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
320
- currentState.lastState = lastState;
321
-
322
- return currentState;
346
+ try {
347
+ const message = new HueLightMessage(targetResource, options);
348
+
349
+ // GET & SAVE LAST STATE AND DIFFERENCES
350
+ let currentState = message.msg;
351
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
352
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
353
+ currentState.lastState = lastState;
354
+
355
+ return currentState;
356
+ } catch (error) {
357
+ return false;
358
+ }
323
359
  }
324
360
  else if(type == "group")
325
361
  {
326
- // GET MESSAGE
327
- const message = new HueGroupMessage(targetResource, { resources: scope.resources, ...options});
328
-
329
- // GET & SAVE LAST STATE AND DIFFERENCES
330
- let currentState = message.msg;
331
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
332
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
333
- currentState.lastState = lastState;
334
-
335
- return currentState;
362
+ try {
363
+ // GET MESSAGE
364
+ const message = new HueGroupMessage(targetResource, { resources: scope.resources, ...options});
365
+
366
+ // GET & SAVE LAST STATE AND DIFFERENCES
367
+ let currentState = message.msg;
368
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
369
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
370
+ currentState.lastState = lastState;
371
+
372
+ return currentState;
373
+ } catch (error) {
374
+ return false;
375
+ }
336
376
  }
337
377
  else if(type == "button")
338
378
  {
339
- const message = new HueButtonsMessage(targetResource, options);
340
-
341
- // GET & SAVE LAST STATE AND DIFFERENCES
342
- let currentState = message.msg;
343
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
344
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
345
- currentState.lastState = lastState;
346
-
347
- return currentState;
379
+ try {
380
+ const message = new HueButtonsMessage(targetResource, options);
381
+
382
+ // GET & SAVE LAST STATE AND DIFFERENCES
383
+ let currentState = message.msg;
384
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
385
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
386
+ currentState.lastState = lastState;
387
+
388
+ return currentState;
389
+ } catch (error) {
390
+ return false;
391
+ }
348
392
  }
349
393
  else if(type == "motion")
350
394
  {
351
- const message = new HueMotionMessage(targetResource, options);
352
-
353
- // GET & SAVE LAST STATE AND DIFFERENCES
354
- let currentState = message.msg;
355
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
356
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
357
- currentState.lastState = lastState;
358
-
359
- return currentState;
395
+ try {
396
+ const message = new HueMotionMessage(targetResource, options);
397
+
398
+ // GET & SAVE LAST STATE AND DIFFERENCES
399
+ let currentState = message.msg;
400
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
401
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
402
+ currentState.lastState = lastState;
403
+
404
+ return currentState;
405
+ } catch (error) {
406
+ return false;
407
+ }
360
408
  }
361
409
  else if(type == "temperature")
362
410
  {
363
- const message = new HueTemperatureMessage(targetResource, options);
364
-
365
- // GET & SAVE LAST STATE AND DIFFERENCES
366
- let currentState = message.msg;
367
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
368
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
369
- currentState.lastState = lastState;
370
-
371
- return currentState;
411
+ try {
412
+ const message = new HueTemperatureMessage(targetResource, options);
413
+
414
+ // GET & SAVE LAST STATE AND DIFFERENCES
415
+ let currentState = message.msg;
416
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
417
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
418
+ currentState.lastState = lastState;
419
+
420
+ return currentState;
421
+ } catch (error) {
422
+ return false;
423
+ }
372
424
  }
373
425
  else if(type == "light_level")
374
426
  {
375
- const message = new HueBrightnessMessage(targetResource, options);
376
-
377
- // GET & SAVE LAST STATE AND DIFFERENCES
378
- let currentState = message.msg;
379
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
380
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
381
- currentState.lastState = lastState;
382
-
383
- return currentState;
427
+ try {
428
+ const message = new HueBrightnessMessage(targetResource, options);
429
+
430
+ // GET & SAVE LAST STATE AND DIFFERENCES
431
+ let currentState = message.msg;
432
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
433
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
434
+ currentState.lastState = lastState;
435
+
436
+ return currentState;
437
+ } catch (error) {
438
+ return false;
439
+ }
384
440
  }
385
441
  else if(type == "rule")
386
442
  {
387
- const message = new HueRulesMessage(targetResource, options);
388
-
389
- // GET & SAVE LAST STATE AND DIFFERENCES
390
- let currentState = message.msg;
391
- scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
392
- currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
393
- currentState.lastState = lastState;
394
-
395
- return currentState;
443
+ try {
444
+ const message = new HueRulesMessage(targetResource, options);
445
+
446
+ // GET & SAVE LAST STATE AND DIFFERENCES
447
+ let currentState = message.msg;
448
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
449
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
450
+ currentState.lastState = lastState;
451
+
452
+ return currentState;
453
+ } catch (error) {
454
+ return false;
455
+ }
396
456
  }
397
457
  else
398
458
  {
@@ -598,7 +658,7 @@ module.exports = function(RED)
598
658
  //
599
659
  // START THE MAGIC
600
660
  this.start();
601
-
661
+
602
662
  //
603
663
  // CLOSE NODE / REMOVE EVENT LISTENER
604
664
  this.on('close', function()
@@ -8,6 +8,7 @@ module.exports = function(RED)
8
8
 
9
9
  const scope = this;
10
10
  const bridge = RED.nodes.getNode(config.bridge);
11
+ const async = require('async');
11
12
 
12
13
  // SAVE LAST COMMAND
13
14
  this.lastCommand = null;
@@ -116,7 +117,7 @@ module.exports = function(RED)
116
117
  let currentState = bridge.get("light_level", tempSensorID);
117
118
  if(!currentState)
118
119
  {
119
- scope.error("The sensor in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid..");
120
+ scope.error("The sensor in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid.");
120
121
  return false;
121
122
  }
122
123
 
@@ -157,9 +158,30 @@ module.exports = function(RED)
157
158
  }
158
159
 
159
160
  // PATCH!
160
- bridge.patch("light_level", tempSensorID, patchObject)
161
- .then(function() { if(done) { done(); }})
162
- .catch(function(errors) { scope.error(errors); });
161
+ async.retry({
162
+ times: 3,
163
+ errorFilter: function(err) {
164
+ return (err.status == 503);
165
+ },
166
+ interval: function(retryCount) { return retryCount*2000; }
167
+ },
168
+ function(callback, results)
169
+ {
170
+ bridge.patch("light_level", tempSensorID, patchObject)
171
+ .then(function() { callback(null, true); })
172
+ .catch(function(errors) { callback(errors, null); });
173
+ },
174
+ function(errors, success)
175
+ {
176
+ if(errors)
177
+ {
178
+ scope.error(errors);
179
+ }
180
+ else if(done)
181
+ {
182
+ done();
183
+ }
184
+ });
163
185
  }
164
186
  else
165
187
  {
@@ -134,7 +134,7 @@ module.exports = function(RED)
134
134
  let currentState = bridge.get("button", tempSensorID);
135
135
  if(!currentState)
136
136
  {
137
- scope.error("The button/switch in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid..");
137
+ scope.error("The button/switch in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid.");
138
138
  return false;
139
139
  }
140
140
 
@@ -8,6 +8,7 @@ module.exports = function(RED)
8
8
 
9
9
  const scope = this;
10
10
  const bridge = RED.nodes.getNode(config.bridge);
11
+ const async = require('async');
11
12
 
12
13
  // EXPORT CONFIG
13
14
  this.exportedConfig = config;
@@ -112,7 +113,7 @@ module.exports = function(RED)
112
113
  let currentState = bridge.get("group", tempGroupID, { colornames: config.colornamer ? true : false });
113
114
  if(!currentState)
114
115
  {
115
- scope.error("The group in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid..");
116
+ scope.error("The group in not yet available. Please wait until HueMagic has established a connection with the bridge or check whether the resource ID in the configuration is valid.");
116
117
  return false;
117
118
  }
118
119
 
@@ -158,18 +159,41 @@ module.exports = function(RED)
158
159
  "bri": msg.payload.brightness ? Math.round((254/100)*msg.payload.brightness) : 254
159
160
  };
160
161
 
161
- bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
162
- .then(function(status) {
163
- // RESET COLORLOOP ANIMATION AFTER X SECONDS
164
- setTimeout(function()
165
- {
166
- bridge.patch("group", currentState.info.idV1 + "/action", { "effect": "none" }, 1)
167
- .then(function() { if(done) { done(); }});
168
- }, parseInt(msg.payload.colorloop) * 1000);
169
- })
170
- .catch(function(errors) {
171
- scope.error(errors);
172
- scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"});
162
+ // PATCH!
163
+ async.retry({
164
+ times: 3,
165
+ errorFilter: function(err) {
166
+ return (err.status == 503);
167
+ },
168
+ interval: function(retryCount) { return retryCount*2000; }
169
+ },
170
+ function(callback, results)
171
+ {
172
+ bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
173
+ .then(function(status) {
174
+ // RESET COLORLOOP ANIMATION AFTER X SECONDS
175
+ setTimeout(function()
176
+ {
177
+ bridge.patch("group", currentState.info.idV1 + "/action", { "effect": "none" }, 1)
178
+ .then(function() { if(done) { done(); }});
179
+ }, parseInt(msg.payload.colorloop) * 1000);
180
+ callback(null, true);
181
+ })
182
+ .catch(function(errors) {
183
+ callback(errors, null);
184
+ });
185
+ },
186
+ function(errors, success)
187
+ {
188
+ if(errors)
189
+ {
190
+ scope.error(errors);
191
+ scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"});
192
+ }
193
+ else if(done)
194
+ {
195
+ done();
196
+ }
173
197
  });
174
198
 
175
199
  return false;
@@ -240,42 +264,65 @@ module.exports = function(RED)
240
264
  // SET ALERT EFFECT
241
265
  patchObject["alert"] = "lselect";
242
266
 
243
- // 1. TURN ON THE LIGHT BULB
244
- bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
245
- .then(function(status) {
246
- setTimeout(function()
267
+ // PATCH!
268
+ async.retry({
269
+ times: 3,
270
+ errorFilter: function(err) {
271
+ return (err.status == 503);
272
+ },
273
+ interval: function(retryCount) { return retryCount*2000; }
274
+ },
275
+ function(callback, results)
276
+ {
277
+ // 1. TURN ON THE LIGHT BULB
278
+ bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
279
+ .then(function(status)
247
280
  {
248
- const tempPreviousState = scope.context().get('groupPreviousState');
249
- var tempPreviousStatePatch = {};
250
-
251
- tempPreviousStatePatch.dimming = { brightness: tempPreviousState.payload.brightness };
252
- if(tempPreviousState.payload.xyColor)
253
- {
254
- tempPreviousStatePatch.xy = [tempPreviousState.payload.xyColor.x, tempPreviousState.payload.xyColor.y];
255
- }
256
- else if(tempPreviousState.payload.colorTemp)
281
+ setTimeout(function()
257
282
  {
258
- tempPreviousStatePatch.ct = tempPreviousState.payload.colorTemp;
259
- }
283
+ const tempPreviousState = scope.context().get('groupPreviousState');
284
+ var tempPreviousStatePatch = {};
260
285
 
261
- bridge.patch("group", currentState.info.idV1 + "/action", tempPreviousStatePatch, 1)
262
- .then(function(status)
263
- {
264
- return bridge.patch("group", currentState.info.idV1 + "/action", { on: false }, 1);
265
- })
266
- .then(function(status) {
267
- if(done) { done(); }
268
- if(tempPreviousState.payload.on === true)
286
+ tempPreviousStatePatch.dimming = { brightness: tempPreviousState.payload.brightness };
287
+ if(tempPreviousState.payload.xyColor)
269
288
  {
270
- bridge.patch("group", currentState.info.idV1, { on: true }, 1);
289
+ tempPreviousStatePatch.xy = [tempPreviousState.payload.xyColor.x, tempPreviousState.payload.xyColor.y];
271
290
  }
272
- });
273
- }, parseInt(msg.payload.alert) * 1000);
274
- })
275
- .catch(function(errors) {
276
- scope.error(errors);
277
- scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"});
278
- if(done) { done(error); }
291
+ else if(tempPreviousState.payload.colorTemp)
292
+ {
293
+ tempPreviousStatePatch.ct = tempPreviousState.payload.colorTemp;
294
+ }
295
+
296
+ bridge.patch("group", currentState.info.idV1 + "/action", tempPreviousStatePatch, 1)
297
+ .then(function(status)
298
+ {
299
+ return bridge.patch("group", currentState.info.idV1 + "/action", { on: false }, 1);
300
+ })
301
+ .then(function(status) {
302
+ if(tempPreviousState.payload.on === true)
303
+ {
304
+ bridge.patch("group", currentState.info.idV1, { on: true }, 1);
305
+ }
306
+ });
307
+ }, parseInt(msg.payload.alert) * 1000);
308
+
309
+ callback(null, true);
310
+ })
311
+ .catch(function(errors) {
312
+ callback(errors, null);
313
+ });
314
+ },
315
+ function(errors, success)
316
+ {
317
+ if(errors)
318
+ {
319
+ scope.error(errors);
320
+ scope.status({fill: "red", shape: "ring", text: "hue-group.node.error-input"});
321
+ }
322
+ else if(done)
323
+ {
324
+ done();
325
+ }
279
326
  });
280
327
  }
281
328
  // ANIMATION STARTED?
@@ -300,19 +347,44 @@ module.exports = function(RED)
300
347
  tempPreviousStatePatch.ct = tempPreviousState.payload.colorTemp;
301
348
  }
302
349
 
303
- bridge.patch("light", currentState.info.lightIds[l], tempPreviousStatePatch).
304
- then(function(status)
350
+ // PATCH!
351
+ async.retry({
352
+ times: 3,
353
+ errorFilter: function(err) {
354
+ return (err.status == 503);
355
+ },
356
+ interval: function(retryCount) { return retryCount*2000; }
357
+ },
358
+ function(callback, results)
305
359
  {
306
- if(tempPreviousState.payload.on === false)
360
+ bridge.patch("light", currentState.info.lightIds[l], tempPreviousStatePatch).
361
+ then(function(status)
307
362
  {
308
- bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } })
363
+ if(tempPreviousState.payload.on === false)
364
+ {
365
+ bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } })
366
+ .then(function() { callback(null, true); });
367
+ }
368
+ else
369
+ {
370
+ bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } })
371
+ .then(function(status) {
372
+ callback(null, true);
373
+ return bridge.patch("light", currentState.info.lightIds[l], { on: { on: true } });
374
+ });
375
+ }
376
+ })
377
+ .catch(function(errors) { callback(errors, null); });
378
+ },
379
+ function(errors, success)
380
+ {
381
+ if(errors)
382
+ {
383
+ scope.error(errors);
309
384
  }
310
- else
385
+ else if(done)
311
386
  {
312
- bridge.patch("light", currentState.info.lightIds[l], { on: { on: false } })
313
- .then(function(status) {
314
- return bridge.patch("light", currentState.info.lightIds[l], { on: { on: true } })
315
- });
387
+ done();
316
388
  }
317
389
  });
318
390
  }
@@ -516,9 +588,30 @@ module.exports = function(RED)
516
588
  }
517
589
 
518
590
  // PATCH!
519
- bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
520
- .then(function() { if(done) { done(); }})
521
- .catch(function(errors) { scope.error(errors); });
591
+ async.retry({
592
+ times: 3,
593
+ errorFilter: function(err) {
594
+ return (err.status == 503);
595
+ },
596
+ interval: function(retryCount) { return retryCount*2000; }
597
+ },
598
+ function(callback, results)
599
+ {
600
+ bridge.patch("group", currentState.info.idV1 + "/action", patchObject, 1)
601
+ .then(function() { callback(null, true); })
602
+ .catch(function(errors) { callback(errors, null); });
603
+ },
604
+ function(errors, success)
605
+ {
606
+ if(errors)
607
+ {
608
+ scope.error(errors);
609
+ }
610
+ else if(done)
611
+ {
612
+ done();
613
+ }
614
+ });
522
615
  }
523
616
  else
524
617
  {