node-red-contrib-huemagic 4.0.3 → 4.0.4
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 +6 -2
- package/huemagic/hue-bridge-config.js +31 -28
- package/huemagic/utils/api.js +45 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,6 @@ HueMagic provides several input and output nodes for Node-RED and is the most in
|
|
|
12
12
|
* Output and input of multiple color code definitions *(HEX, RGB & human readable color names)*
|
|
13
13
|
* Automatic color temperature & brightness setting based on the current time
|
|
14
14
|
* Event-based status messages for all devices connected to the Hue Bridge
|
|
15
|
-
* Virtual pressing of the Hue Bridge button (Link Button)
|
|
16
15
|
* Pairing of new devices without app enforcement (TouchLink)
|
|
17
16
|
* Automatic firmware updates to the Hue Bridge and connected devices
|
|
18
17
|
* Activating / deactivating of sensors & rules on the Hue Bridge
|
|
@@ -796,7 +795,12 @@ If the status of the node has changed via a certain command, the entire command
|
|
|
796
795
|
|
|
797
796
|
# Changelog
|
|
798
797
|
|
|
799
|
-
### v4.0.
|
|
798
|
+
### v4.0.4 (latest)
|
|
799
|
+
|
|
800
|
+
* Fixed an issue with the bridge config node checking for updates too frequently ([#246](https://github.com/Foddy/node-red-contrib-huemagic/issues/246#issuecomment-1009376442))
|
|
801
|
+
* Fixed an issue with multiple bridges configured
|
|
802
|
+
|
|
803
|
+
### v4.0.3
|
|
800
804
|
|
|
801
805
|
> **Attention!** HueMagic v4+ has been almost completely rewritten under the hood and requires at least the (square-shaped) Philips Hue Bridge firmware 1948086000+ from November 1st, 2021 ([Upgrade instructions](https://www.lighting.philips.com/content/B2C/en_US/microsites/meethue/marketing-catalog/huewireless_ca/support/security-advisory/general/where-and-how-can-i-update-my-hue-system-with-the-latest-software.html)) and Node-RED v1+ ([Upgrade instructions](https://nodered.org/docs/getting-started/local#upgrading-node-red)). If you are upgrading from a previous HueMagic version to the v4, you will have to reconfigure (not completely rebuild) all nodes by clicking them and selecting the appropriate device from the list. This also applies to nodes / functions that are operated in universal mode, as the numeric identifiers of the latest Philips Hue API version have been replaced in UUIDs. The nodes "Hue Switch", "Hue Button" & "Hue Tap" have been replaced in v4 by the universal and uniform node "Hue Buttons", which works with all button / switch devices that are connected to the Hue Bridge (please note here also the new API in the documentation). The request and return objects of the individual nodes are largely compatible with older HueMagic versions - with the exception of the nodes "Hue Bridge", "Hue Buttons", "Hue Scene" & "Hue Group". These need to be adjusted in the v4. Make sure that you meet the minimum technical requirements and have a quiet minute for the migration before upgrading to the v4.
|
|
802
806
|
|
|
@@ -36,21 +36,24 @@ module.exports = function(RED)
|
|
|
36
36
|
// RESOURCE ID PATTERN
|
|
37
37
|
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;
|
|
38
38
|
|
|
39
|
+
// FIRMWARE UPDATE TIMEOUT
|
|
40
|
+
this.firmwareUpdateTimeout = null;
|
|
41
|
+
|
|
39
42
|
// CREATE NODE
|
|
40
43
|
RED.nodes.createNode(scope, config);
|
|
41
44
|
|
|
42
45
|
// INITIALIZE
|
|
43
46
|
this.start = function()
|
|
44
47
|
{
|
|
45
|
-
scope.log("Initializing the bridge…");
|
|
46
|
-
API.init({
|
|
48
|
+
scope.log("Initializing the bridge ("+config.bridge+")…");
|
|
49
|
+
API.init({ config: config })
|
|
47
50
|
.then(function(bridge) {
|
|
48
51
|
scope.log("Connected to bridge");
|
|
49
52
|
return scope.getAllResources();
|
|
50
53
|
})
|
|
51
54
|
.then(function(allResources)
|
|
52
55
|
{
|
|
53
|
-
scope.log("
|
|
56
|
+
scope.log("Processing bridge resources…");
|
|
54
57
|
return API.processResources(allResources);
|
|
55
58
|
})
|
|
56
59
|
.then(function(allResources)
|
|
@@ -84,7 +87,7 @@ module.exports = function(RED)
|
|
|
84
87
|
{
|
|
85
88
|
return new Promise(function(resolve, reject)
|
|
86
89
|
{
|
|
87
|
-
API.request({ resource: "/config", version: 1 })
|
|
90
|
+
API.request({ config: config, resource: "/config", version: 1 })
|
|
88
91
|
.then(function(bridgeInformation)
|
|
89
92
|
{
|
|
90
93
|
// PREPARE TO MATCH V2 RESOURCES
|
|
@@ -123,7 +126,7 @@ module.exports = function(RED)
|
|
|
123
126
|
allResources.push(bridgeInformation);
|
|
124
127
|
|
|
125
128
|
// CONTINUE WITH ALL RESOURCES
|
|
126
|
-
return API.request({ resource: "all" });
|
|
129
|
+
return API.request({ config: config, resource: "all" });
|
|
127
130
|
})
|
|
128
131
|
.then(function(v2Resources)
|
|
129
132
|
{
|
|
@@ -131,7 +134,7 @@ module.exports = function(RED)
|
|
|
131
134
|
allResources = allResources.concat(v2Resources);
|
|
132
135
|
|
|
133
136
|
// GET RULES (LEGACY API)
|
|
134
|
-
return API.request({ resource: "/rules", version: 1 });
|
|
137
|
+
return API.request({ config: config, resource: "/rules", version: 1 });
|
|
135
138
|
})
|
|
136
139
|
.then(function(rules)
|
|
137
140
|
{
|
|
@@ -193,7 +196,7 @@ module.exports = function(RED)
|
|
|
193
196
|
this.refreshStatesSSE = function()
|
|
194
197
|
{
|
|
195
198
|
scope.log("Subscribing to bridge events…");
|
|
196
|
-
API.subscribe(function(updates)
|
|
199
|
+
API.subscribe(config, function(updates)
|
|
197
200
|
{
|
|
198
201
|
const currentDateTime = dayjs().format();
|
|
199
202
|
|
|
@@ -266,8 +269,8 @@ module.exports = function(RED)
|
|
|
266
269
|
this.pushUpdatedState = function(resource, updatedType, suppressMessage = false)
|
|
267
270
|
{
|
|
268
271
|
const msg = { id: resource.id, type: resource.type, updatedType: updatedType, services: resource["services"] ? Object.keys(resource["services"]) : [], suppressMessage: suppressMessage };
|
|
269
|
-
this.events.emit(resource.id, msg);
|
|
270
|
-
this.events.emit("globalResourceUpdates", msg);
|
|
272
|
+
this.events.emit(config.id + "_" + resource.id, msg);
|
|
273
|
+
this.events.emit(config.id + "_" + "globalResourceUpdates", msg);
|
|
271
274
|
|
|
272
275
|
// RESOURCE CONTAINS SERVICES? -> SERVICE IN GROUP? -> EMIT CHANGES TO GROUPS ALSO
|
|
273
276
|
if(this.resources["_groupsOf"][resource.id])
|
|
@@ -277,8 +280,8 @@ module.exports = function(RED)
|
|
|
277
280
|
const groupID = this.resources["_groupsOf"][resource.id][g];
|
|
278
281
|
const groupMessage = { id: groupID, type: "group", updatedType: updatedType, services: [], suppressMessage: suppressMessage };
|
|
279
282
|
|
|
280
|
-
this.events.emit(groupID, groupMessage);
|
|
281
|
-
this.events.emit("globalResourceUpdates", groupMessage);
|
|
283
|
+
this.events.emit(config.id + "_" + groupID, groupMessage);
|
|
284
|
+
this.events.emit(config.id + "_" + "globalResourceUpdates", groupMessage);
|
|
282
285
|
}
|
|
283
286
|
}
|
|
284
287
|
}
|
|
@@ -440,7 +443,7 @@ module.exports = function(RED)
|
|
|
440
443
|
}
|
|
441
444
|
|
|
442
445
|
// ACTION!
|
|
443
|
-
API.request({ method: "PUT", resource: (version === 2) ? (type+"/"+id) : id, data: patch, version: version })
|
|
446
|
+
API.request({ config: config, method: "PUT", resource: (version === 2) ? (type+"/"+id) : id, data: patch, version: version })
|
|
444
447
|
.then(function(response) {
|
|
445
448
|
resolve(response);
|
|
446
449
|
})
|
|
@@ -455,7 +458,7 @@ module.exports = function(RED)
|
|
|
455
458
|
{
|
|
456
459
|
return new Promise(function(resolve, reject)
|
|
457
460
|
{
|
|
458
|
-
API.request({ resource: "/rules/" + id, version: 1 })
|
|
461
|
+
API.request({ config: config, resource: "/rules/" + id, version: 1 })
|
|
459
462
|
.then(function(rule)
|
|
460
463
|
{
|
|
461
464
|
// "RENAME" OWNER
|
|
@@ -508,7 +511,7 @@ module.exports = function(RED)
|
|
|
508
511
|
if(!id)
|
|
509
512
|
{
|
|
510
513
|
// UNIVERSAL MODE
|
|
511
|
-
this.events.on("globalResourceUpdates", function(info)
|
|
514
|
+
this.events.on(config.id + "_" + "globalResourceUpdates", function(info)
|
|
512
515
|
{
|
|
513
516
|
if(type === "bridge")
|
|
514
517
|
{
|
|
@@ -527,7 +530,7 @@ module.exports = function(RED)
|
|
|
527
530
|
else
|
|
528
531
|
{
|
|
529
532
|
// SPECIFIC RESOURCE MODE
|
|
530
|
-
this.events.on(id, function(info)
|
|
533
|
+
this.events.on(config.id + "_" + id, function(info)
|
|
531
534
|
{
|
|
532
535
|
if(type === "bridge" || messageWhitelist[type].includes(info.updatedType))
|
|
533
536
|
{
|
|
@@ -542,8 +545,11 @@ module.exports = function(RED)
|
|
|
542
545
|
{
|
|
543
546
|
if((config.autoupdates && config.autoupdates == true) || typeof config.autoupdates == 'undefined')
|
|
544
547
|
{
|
|
548
|
+
if(scope.firmwareUpdateTimeout !== null) { clearTimeout(scope.firmwareUpdateTimeout); };
|
|
549
|
+
|
|
545
550
|
scope.log("Checking for Hue Bridge firmware updates…");
|
|
546
551
|
API.request({
|
|
552
|
+
config: config,
|
|
547
553
|
method: "PUT",
|
|
548
554
|
resource: "/config",
|
|
549
555
|
version: 1,
|
|
@@ -558,7 +564,7 @@ module.exports = function(RED)
|
|
|
558
564
|
{
|
|
559
565
|
if(scope.nodeActive == true)
|
|
560
566
|
{
|
|
561
|
-
setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 *
|
|
567
|
+
scope.firmwareUpdateTimeout = setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 180);
|
|
562
568
|
}
|
|
563
569
|
})
|
|
564
570
|
.catch(function(error)
|
|
@@ -566,7 +572,7 @@ module.exports = function(RED)
|
|
|
566
572
|
// NO UPDATES AVAILABLE // TRY AGAIN IN 3H
|
|
567
573
|
if(scope.nodeActive == true)
|
|
568
574
|
{
|
|
569
|
-
setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 180);
|
|
575
|
+
scope.firmwareUpdateTimeout = setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 180);
|
|
570
576
|
}
|
|
571
577
|
});
|
|
572
578
|
}
|
|
@@ -584,10 +590,13 @@ module.exports = function(RED)
|
|
|
584
590
|
|
|
585
591
|
// UNSUBSCRIBE FROM BRIDGE EVENTS
|
|
586
592
|
scope.log("Unsubscribing from bridge events…");
|
|
587
|
-
API.unsubscribe();
|
|
593
|
+
API.unsubscribe(config);
|
|
588
594
|
|
|
589
595
|
// UNSUBSCRIBE FROM "READY" EVENTS
|
|
590
596
|
scope.events.removeAllListeners();
|
|
597
|
+
|
|
598
|
+
// REMOVE FIRMWARE UPDATE TIMEOUT
|
|
599
|
+
if(scope.firmwareUpdateTimeout !== null) { clearTimeout(scope.firmwareUpdateTimeout); }
|
|
591
600
|
});
|
|
592
601
|
}
|
|
593
602
|
|
|
@@ -631,7 +640,7 @@ module.exports = function(RED)
|
|
|
631
640
|
}
|
|
632
641
|
else
|
|
633
642
|
{
|
|
634
|
-
API.init({
|
|
643
|
+
API.init({ config: { bridge: req.query.ip, key: "huemagic" } })
|
|
635
644
|
.then(function(bridge) {
|
|
636
645
|
res.end(bridge.name);
|
|
637
646
|
})
|
|
@@ -659,7 +668,7 @@ module.exports = function(RED)
|
|
|
659
668
|
"Content-Type": "application/json; charset=utf-8"
|
|
660
669
|
},
|
|
661
670
|
"data": {
|
|
662
|
-
"devicetype": "
|
|
671
|
+
"devicetype": "HueMagic for Node-RED (" + Math.floor((Math.random() * 100) + 1) + ")"
|
|
663
672
|
}
|
|
664
673
|
})
|
|
665
674
|
.then(function(response)
|
|
@@ -689,10 +698,7 @@ module.exports = function(RED)
|
|
|
689
698
|
// GET ALL RULES
|
|
690
699
|
if(targetType == "rule")
|
|
691
700
|
{
|
|
692
|
-
API.
|
|
693
|
-
.then(function(bridge) {
|
|
694
|
-
return API.request({ resource: "/rules", version: 1 });
|
|
695
|
-
})
|
|
701
|
+
API.request({ config: { bridge: req.query.bridge, key: req.query.key }, resource: "/rules", version: 1 })
|
|
696
702
|
.then(function(rules)
|
|
697
703
|
{
|
|
698
704
|
let targetRules = {};
|
|
@@ -720,10 +726,7 @@ module.exports = function(RED)
|
|
|
720
726
|
// GET ALL OTHER RESOURCES
|
|
721
727
|
else
|
|
722
728
|
{
|
|
723
|
-
API.
|
|
724
|
-
.then(function(bridge) {
|
|
725
|
-
return API.request({ resource: "all" });
|
|
726
|
-
})
|
|
729
|
+
API.request({ config: { bridge: req.query.bridge, key: req.query.key }, resource: "all" })
|
|
727
730
|
.then(function(allResources)
|
|
728
731
|
{
|
|
729
732
|
return API.processResources(allResources);
|
package/huemagic/utils/api.js
CHANGED
|
@@ -5,68 +5,62 @@ const EventSource = require('eventsource');
|
|
|
5
5
|
|
|
6
6
|
function API()
|
|
7
7
|
{
|
|
8
|
-
//
|
|
9
|
-
this.
|
|
10
|
-
this.ipAddress = null;
|
|
11
|
-
this.accessKey = null;
|
|
12
|
-
this.events = null;
|
|
8
|
+
// EVENTS
|
|
9
|
+
this.events = {};
|
|
13
10
|
|
|
14
11
|
//
|
|
15
12
|
// INITIALIZE
|
|
16
|
-
this.init = function({
|
|
13
|
+
this.init = function({ config = null })
|
|
17
14
|
{
|
|
18
15
|
const scope = this;
|
|
19
16
|
|
|
20
|
-
// FORCE RELOAD?
|
|
21
|
-
const forceReload = (this.ipAddress != ip.toString().trim());
|
|
22
|
-
|
|
23
|
-
// STORE ACCESS INFORMATION
|
|
24
|
-
this.ipAddress = ip.toString().trim();
|
|
25
|
-
this.accessKey = key.toString().trim();
|
|
26
|
-
|
|
27
17
|
// GET BRIDGE
|
|
28
18
|
return new Promise(function(resolve, reject)
|
|
29
19
|
{
|
|
30
|
-
if(
|
|
20
|
+
if(!config)
|
|
31
21
|
{
|
|
32
|
-
|
|
22
|
+
reject("Bridge is not configured!");
|
|
23
|
+
return false;
|
|
33
24
|
}
|
|
34
|
-
|
|
25
|
+
|
|
26
|
+
// GET BRIDGE INFORMATION
|
|
27
|
+
axios({
|
|
28
|
+
"method": "GET",
|
|
29
|
+
"url": "https://" + config.bridge + "/api/config",
|
|
30
|
+
"headers": { "Content-Type": "application/json; charset=utf-8" },
|
|
31
|
+
"httpsAgent": new https.Agent({ rejectUnauthorized: false }),
|
|
32
|
+
})
|
|
33
|
+
.then(function(response)
|
|
35
34
|
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
43
|
-
.then(function(response)
|
|
44
|
-
{
|
|
45
|
-
scope.bridge = response.data;
|
|
46
|
-
resolve(scope.bridge);
|
|
47
|
-
})
|
|
48
|
-
.catch(function(error)
|
|
49
|
-
{
|
|
50
|
-
reject(error);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
35
|
+
resolve(response.data);
|
|
36
|
+
})
|
|
37
|
+
.catch(function(error)
|
|
38
|
+
{
|
|
39
|
+
reject(error);
|
|
40
|
+
});
|
|
53
41
|
});
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
//
|
|
57
45
|
// MAKE A REQUEST
|
|
58
|
-
this.request = function({ method = 'GET', resource = null, data = null, version = 2 })
|
|
46
|
+
this.request = function({ config = null, method = 'GET', resource = null, data = null, version = 2 })
|
|
59
47
|
{
|
|
60
48
|
const scope = this;
|
|
61
49
|
return new Promise(function(resolve, reject)
|
|
62
50
|
{
|
|
51
|
+
if(!config)
|
|
52
|
+
{
|
|
53
|
+
reject("Bridge is not configured!");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
63
57
|
// BUILD REQUEST OBJECT
|
|
64
58
|
var request = {
|
|
65
59
|
"method": method,
|
|
66
|
-
"url": "https://" +
|
|
60
|
+
"url": "https://" + config.bridge,
|
|
67
61
|
"headers": {
|
|
68
62
|
"Content-Type": "application/json; charset=utf-8",
|
|
69
|
-
"hue-application-key":
|
|
63
|
+
"hue-application-key": config.key
|
|
70
64
|
},
|
|
71
65
|
"httpsAgent": new https.Agent({ rejectUnauthorized: false }), // Node is somehow not able to parse the official Philips Hue PEM
|
|
72
66
|
};
|
|
@@ -81,7 +75,7 @@ function API()
|
|
|
81
75
|
}
|
|
82
76
|
else if(version === 1)
|
|
83
77
|
{
|
|
84
|
-
request['url'] += "/api/" +
|
|
78
|
+
request['url'] += "/api/" + config.key + resource;
|
|
85
79
|
}
|
|
86
80
|
}
|
|
87
81
|
|
|
@@ -119,23 +113,23 @@ function API()
|
|
|
119
113
|
|
|
120
114
|
//
|
|
121
115
|
// SUBSCRIBE TO BRIDGE EVENTS
|
|
122
|
-
this.subscribe = function(callback)
|
|
116
|
+
this.subscribe = function(config, callback)
|
|
123
117
|
{
|
|
124
118
|
const scope = this;
|
|
125
119
|
return new Promise(function(resolve, reject)
|
|
126
120
|
{
|
|
127
|
-
if(scope.events
|
|
121
|
+
if(!scope.events[config.id])
|
|
128
122
|
{
|
|
129
|
-
var sseURL = "https://" +
|
|
123
|
+
var sseURL = "https://" + config.bridge + "/eventstream/clip/v2";
|
|
130
124
|
|
|
131
125
|
// INITIALIZE EVENT SOURCE
|
|
132
|
-
scope.events = new EventSource(sseURL, {
|
|
133
|
-
headers: { 'hue-application-key':
|
|
126
|
+
scope.events[config.id] = new EventSource(sseURL, {
|
|
127
|
+
headers: { 'hue-application-key': config.key },
|
|
134
128
|
https: { rejectUnauthorized: false },
|
|
135
129
|
});
|
|
136
130
|
|
|
137
131
|
// PIPE MESSAGE TO TARGET FUNCTION
|
|
138
|
-
scope.events.onmessage = function(event)
|
|
132
|
+
scope.events[config.id].onmessage = function(event)
|
|
139
133
|
{
|
|
140
134
|
if(event && event.type === 'message' && event.data)
|
|
141
135
|
{
|
|
@@ -152,33 +146,33 @@ function API()
|
|
|
152
146
|
};
|
|
153
147
|
|
|
154
148
|
// CONNECTED?
|
|
155
|
-
scope.events.onopen = function()
|
|
149
|
+
scope.events[config.id].onopen = function()
|
|
156
150
|
{
|
|
157
151
|
resolve(true);
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
// ERROR? -> RETRY?
|
|
161
|
-
scope.events.onerror = function(error)
|
|
155
|
+
scope.events[config.id].onerror = function(error)
|
|
162
156
|
{
|
|
163
157
|
console.log("HueMagic:", "Connection to bridge lost. Trying to reconnect again in 30 seconds…", err);
|
|
164
|
-
setTimeout(function(){ scope.subscribe(callback); }, 30000);
|
|
158
|
+
setTimeout(function(){ scope.subscribe(config, callback); }, 30000);
|
|
165
159
|
resolve(true);
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
else
|
|
169
163
|
{
|
|
170
|
-
scope.unsubscribe();
|
|
171
|
-
scope.subscribe(callback);
|
|
164
|
+
scope.unsubscribe(config);
|
|
165
|
+
scope.subscribe(config, callback);
|
|
172
166
|
}
|
|
173
167
|
});
|
|
174
168
|
}
|
|
175
169
|
|
|
176
170
|
//
|
|
177
171
|
// UNSUBSCRIBE
|
|
178
|
-
this.unsubscribe = function()
|
|
172
|
+
this.unsubscribe = function(config)
|
|
179
173
|
{
|
|
180
|
-
if(this.events !== null) { this.events.close(); }
|
|
181
|
-
this.events = null;
|
|
174
|
+
if(this.events[config.id] !== null) { this.events[config.id].close(); }
|
|
175
|
+
this.events[config.id] = null;
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-huemagic",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"description": "Philips Hue node to control bridges, lights, groups, scenes, rules, taps, switches, buttons, motion sensors, temperature sensors and Lux sensors using Node-RED.",
|
|
5
5
|
"author": "foddy",
|
|
6
6
|
"homepage": "https://github.com/Foddy/node-red-contrib-huemagic",
|