node-red-contrib-huemagic 3.0.0 → 4.0.3

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 (138) hide show
  1. package/README.md +764 -603
  2. package/examples/Hue Bridge.json +189 -35
  3. package/examples/Hue Brightness.json +124 -14
  4. package/examples/Hue Buttons.json +88 -0
  5. package/examples/Hue Group.json +575 -63
  6. package/examples/Hue Light.json +805 -58
  7. package/examples/Hue Magic.json +69 -24
  8. package/examples/Hue Motion.json +110 -42
  9. package/examples/Hue Rules.json +105 -37
  10. package/examples/Hue Scene.json +145 -28
  11. package/examples/Hue Temperature.json +124 -14
  12. package/huemagic/animations/019-christmas.json +30 -0
  13. package/huemagic/animations/020-superhero.json +56 -0
  14. package/huemagic/animations/021-neoncity.json +52 -0
  15. package/huemagic/animations/previews/006-fireplace.gif +0 -0
  16. package/huemagic/animations/previews/013-strobo.gif +0 -0
  17. package/huemagic/animations/previews/014-randomrainbow.gif +0 -0
  18. package/huemagic/animations/previews/016-milkyway.gif +0 -0
  19. package/huemagic/animations/previews/018-forest.gif +0 -0
  20. package/huemagic/animations/previews/019-christmas.gif +0 -0
  21. package/huemagic/animations/previews/020-superhero.gif +0 -0
  22. package/huemagic/animations/previews/021-neoncity.gif +0 -0
  23. package/huemagic/hue-bridge-config.html +105 -61
  24. package/huemagic/hue-bridge-config.js +631 -319
  25. package/huemagic/hue-bridge.html +41 -21
  26. package/huemagic/hue-bridge.js +190 -396
  27. package/huemagic/hue-brightness.html +127 -59
  28. package/huemagic/hue-brightness.js +111 -55
  29. package/huemagic/hue-buttons.html +197 -0
  30. package/huemagic/hue-buttons.js +158 -0
  31. package/huemagic/hue-group.html +137 -66
  32. package/huemagic/hue-group.js +387 -403
  33. package/huemagic/hue-light.html +132 -64
  34. package/huemagic/hue-light.js +544 -378
  35. package/huemagic/hue-magic.html +36 -20
  36. package/huemagic/hue-magic.js +49 -27
  37. package/huemagic/hue-motion.html +127 -59
  38. package/huemagic/hue-motion.js +105 -101
  39. package/huemagic/hue-rules.html +121 -50
  40. package/huemagic/hue-rules.js +89 -63
  41. package/huemagic/hue-scene.html +95 -114
  42. package/huemagic/hue-scene.js +64 -95
  43. package/huemagic/hue-temperature.html +127 -59
  44. package/huemagic/hue-temperature.js +120 -56
  45. package/huemagic/icons/hue-bridge.png +0 -0
  46. package/huemagic/icons/hue-bridge.svg +3 -0
  47. package/huemagic/icons/hue-brightness.png +0 -0
  48. package/huemagic/icons/hue-brightness.svg +11 -0
  49. package/huemagic/icons/hue-buttons.png +0 -0
  50. package/huemagic/icons/hue-buttons.svg +4 -0
  51. package/huemagic/icons/hue-group.png +0 -0
  52. package/huemagic/icons/hue-group.svg +12 -0
  53. package/huemagic/icons/hue-light.png +0 -0
  54. package/huemagic/icons/hue-light.svg +3 -0
  55. package/huemagic/icons/hue-magic.png +0 -0
  56. package/huemagic/icons/hue-magic.svg +3 -0
  57. package/huemagic/icons/hue-motion.png +0 -0
  58. package/huemagic/icons/hue-motion.svg +9 -0
  59. package/huemagic/icons/hue-rules.png +0 -0
  60. package/huemagic/icons/hue-rules.svg +4 -0
  61. package/huemagic/icons/hue-scene.png +0 -0
  62. package/huemagic/icons/hue-scene.svg +5 -0
  63. package/huemagic/icons/hue-temperature.png +0 -0
  64. package/huemagic/icons/hue-temperature.svg +7 -0
  65. package/huemagic/locales/de/hue-bridge-config.html +17 -12
  66. package/huemagic/locales/de/hue-bridge-config.json +7 -5
  67. package/huemagic/locales/de/hue-bridge.html +64 -145
  68. package/huemagic/locales/de/hue-bridge.json +6 -17
  69. package/huemagic/locales/de/hue-brightness.html +50 -50
  70. package/huemagic/locales/de/hue-brightness.json +6 -5
  71. package/huemagic/locales/de/hue-buttons.html +58 -0
  72. package/huemagic/locales/de/{hue-button.json → hue-buttons.json} +7 -11
  73. package/huemagic/locales/de/hue-group.html +46 -122
  74. package/huemagic/locales/de/hue-group.json +5 -5
  75. package/huemagic/locales/de/hue-light.html +68 -116
  76. package/huemagic/locales/de/hue-light.json +5 -5
  77. package/huemagic/locales/de/hue-magic.html +65 -57
  78. package/huemagic/locales/de/hue-magic.json +2 -1
  79. package/huemagic/locales/de/hue-motion.html +47 -50
  80. package/huemagic/locales/de/hue-motion.json +5 -5
  81. package/huemagic/locales/de/hue-rules.html +68 -73
  82. package/huemagic/locales/de/hue-rules.json +5 -2
  83. package/huemagic/locales/de/hue-scene.html +23 -32
  84. package/huemagic/locales/de/hue-scene.json +5 -2
  85. package/huemagic/locales/de/hue-temperature.html +51 -48
  86. package/huemagic/locales/de/hue-temperature.json +6 -5
  87. package/huemagic/locales/en-US/hue-bridge-config.html +17 -12
  88. package/huemagic/locales/en-US/hue-bridge-config.json +5 -3
  89. package/huemagic/locales/en-US/hue-bridge.html +64 -145
  90. package/huemagic/locales/en-US/hue-bridge.json +5 -16
  91. package/huemagic/locales/en-US/hue-brightness.html +50 -50
  92. package/huemagic/locales/en-US/hue-brightness.json +5 -2
  93. package/huemagic/locales/en-US/hue-buttons.html +58 -0
  94. package/huemagic/locales/en-US/{hue-button.json → hue-buttons.json} +6 -4
  95. package/huemagic/locales/en-US/hue-group.html +46 -122
  96. package/huemagic/locales/en-US/hue-group.json +4 -2
  97. package/huemagic/locales/en-US/hue-light.html +68 -116
  98. package/huemagic/locales/en-US/hue-light.json +4 -2
  99. package/huemagic/locales/en-US/hue-magic.html +65 -57
  100. package/huemagic/locales/en-US/hue-magic.json +3 -1
  101. package/huemagic/locales/en-US/hue-motion.html +47 -50
  102. package/huemagic/locales/en-US/hue-motion.json +4 -2
  103. package/huemagic/locales/en-US/hue-rules.html +68 -73
  104. package/huemagic/locales/en-US/hue-rules.json +5 -2
  105. package/huemagic/locales/en-US/hue-scene.html +23 -32
  106. package/huemagic/locales/en-US/hue-scene.json +5 -2
  107. package/huemagic/locales/en-US/hue-temperature.html +51 -48
  108. package/huemagic/locales/en-US/hue-temperature.json +5 -2
  109. package/huemagic/utils/api.js +290 -0
  110. package/huemagic/utils/color.js +382 -0
  111. package/{utils → huemagic/utils}/merge.js +4 -6
  112. package/huemagic/utils/messages.js +447 -0
  113. package/package.json +9 -15
  114. package/examples/Hue Button.json +0 -40
  115. package/examples/Hue Switch.json +0 -40
  116. package/examples/Hue Tap.json +0 -40
  117. package/huemagic/hue-button.html +0 -132
  118. package/huemagic/hue-button.js +0 -143
  119. package/huemagic/hue-switch.html +0 -132
  120. package/huemagic/hue-switch.js +0 -163
  121. package/huemagic/hue-tap.html +0 -132
  122. package/huemagic/hue-tap.js +0 -123
  123. package/huemagic/icons/hue-button.png +0 -0
  124. package/huemagic/icons/hue-switch.png +0 -0
  125. package/huemagic/icons/hue-tap.png +0 -0
  126. package/huemagic/locales/de/hue-button.html +0 -64
  127. package/huemagic/locales/de/hue-switch.html +0 -64
  128. package/huemagic/locales/de/hue-switch.json +0 -35
  129. package/huemagic/locales/de/hue-tap.html +0 -55
  130. package/huemagic/locales/de/hue-tap.json +0 -27
  131. package/huemagic/locales/en-US/hue-button.html +0 -64
  132. package/huemagic/locales/en-US/hue-switch.html +0 -64
  133. package/huemagic/locales/en-US/hue-switch.json +0 -35
  134. package/huemagic/locales/en-US/hue-tap.html +0 -55
  135. package/huemagic/locales/en-US/hue-tap.json +0 -27
  136. package/utils/color.js +0 -9
  137. package/utils/messages.js +0 -658
  138. package/utils/rgb.js +0 -188
@@ -2,226 +2,592 @@ module.exports = function(RED)
2
2
  {
3
3
  "use strict";
4
4
 
5
+ const API = require('./utils/api');
6
+ const merge = require('./utils/merge');
7
+ const events = require('events');
8
+ const dayjs = require('dayjs');
9
+ const diff = require("deep-object-diff").diff;
10
+ const axios = require('axios');
11
+ const https = require('https');
12
+
13
+ // READABLE RESOURCE MESSAGES
14
+ const { HueBridgeMessage,
15
+ HueLightMessage,
16
+ HueGroupMessage,
17
+ HueMotionMessage,
18
+ HueTemperatureMessage,
19
+ HueBrightnessMessage,
20
+ HueButtonsMessage,
21
+ HueRulesMessage
22
+ } = require('./utils/messages');
23
+
5
24
  function HueBridge(config)
6
25
  {
7
- var scope = this;
8
- var events = require('events');
9
- let huejay = require('huejay');
26
+ const scope = this;
10
27
 
11
- // INTERVAL AND EVENT EMITTER
28
+ // STATES
12
29
  this.nodeActive = true;
13
- this.lights = [];
14
- this.sensors = [];
15
- this.groups = [];
16
- this.rules = [];
30
+ this.config = config;
31
+ this.resources = {};
32
+ this.resourcesInGroups = {};
33
+ this.lastStates = {};
17
34
  this.events = new events.EventEmitter();
18
35
 
36
+ // RESOURCE ID PATTERN
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
+
19
39
  // CREATE NODE
20
40
  RED.nodes.createNode(scope, config);
21
41
 
22
- // DEFINE CLIENT
23
- let ipHostArray = (config.bridge).toString().trim().split(":");
24
- this.client = new huejay.Client({
25
- host: ipHostArray[0],
26
- port: (ipHostArray > 1) ? parseInt(ipHostArray[1]) : 80,
27
- username: config.key
28
- });
29
-
30
- // DELAY
31
- this.delay = function(ms)
42
+ // INITIALIZE
43
+ this.start = function()
32
44
  {
33
- return new Promise(resolve => setTimeout(resolve, ms));
34
- }
35
-
36
- // RECHECK DEVICES
37
- this.recheckAll = function()
38
- {
39
- scope.client.sensors.getAll().then(sensors => {
40
- let sensorUpdates = scope.getUpdates("sensor", sensors);
41
- scope.emitUpdates("sensor", sensorUpdates);
42
-
43
- return true;
45
+ scope.log("Initializing the bridge…");
46
+ API.init({ ip: config.bridge, key: config.key })
47
+ .then(function(bridge) {
48
+ scope.log("Connected to bridge");
49
+ return scope.getAllResources();
44
50
  })
45
- scope.delay(500).then(() => { return "next"; })
46
- scope.client.lights.getAll().then(lights => {
47
- let lightUpdates = scope.getUpdates("light", lights);
48
- scope.emitUpdates("light", lightUpdates);
49
-
50
- return true;
51
+ .then(function(allResources)
52
+ {
53
+ scope.log("Process resources…");
54
+ return API.processResources(allResources);
51
55
  })
52
- scope.delay(500).then(() => { return "next"; })
53
- scope.client.groups.getAll().then(groups => {
54
- let groupUpdates = scope.getUpdates("group", groups);
55
- scope.emitUpdates("group", groupUpdates);
56
+ .then(function(allResources)
57
+ {
58
+ // SAVE CURRENT RESOURCES
59
+ scope.resources = allResources;
56
60
 
57
- return true;
61
+ // EMIT INITIAL STATES -> NODES
62
+ scope.log("Initial emit of resource states…");
63
+ return scope.emitInitialStates();
58
64
  })
59
- scope.delay(500).then(() => { return "next"; })
60
- scope.client.rules.getAll().then(rules => {
61
- let ruleUpdates = scope.getUpdates("rule", rules);
62
- scope.emitUpdates("rule", ruleUpdates);
65
+ .then(function(emitted)
66
+ {
67
+ // START REFRESHING STATES
68
+ scope.keepUpdated();
63
69
 
70
+ // START LOOKING FOR FIRMWARE-UPDATES
71
+ scope.autoUpdateFirmware();
64
72
  return true;
65
73
  })
66
- .then(proceed => {
67
- // RECHECK
68
- if(scope.nodeActive == true && proceed == true) { setTimeout(function(){ scope.recheckAll(); }, config.interval); }
69
- return true;
70
- })
71
- .catch(error => {
72
- scope.debug(error.stack);
74
+ .catch(function(error)
75
+ {
76
+ // RETRY AFTER 30 SECONDS
77
+ scope.log(error);
78
+ if(scope.nodeActive == true) { setTimeout(function(){ scope.start(); }, 30000); }
79
+ });
80
+ }
73
81
 
74
- if(scope.nodeActive == true)
82
+ // FETCH BRIDGE INFORMATION
83
+ this.getBridgeInformation = function(replaceResources = false)
84
+ {
85
+ return new Promise(function(resolve, reject)
86
+ {
87
+ API.request({ resource: "/config", version: 1 })
88
+ .then(function(bridgeInformation)
75
89
  {
76
- setTimeout(function(){ scope.recheckAll(); }, 5000);
77
- }
90
+ // PREPARE TO MATCH V2 RESOURCES
91
+ bridgeInformation.id = "bridge";
92
+ bridgeInformation.id_v1 = "/config";
93
+ bridgeInformation.updated = dayjs().format();
94
+
95
+ // ALSO REPLACE CURRENT RESOURCE?
96
+ if(replaceResources === true)
97
+ {
98
+ scope.resources[bridgeInformation.id] = bridgeInformation;
99
+ }
100
+
101
+ // GIVE BACK
102
+ resolve(bridgeInformation);
103
+ })
104
+ .catch(function(error)
105
+ {
106
+ reject(error);
107
+ });
78
108
  });
79
109
  }
80
110
 
81
- // RECHECK ONLY SENSORS
82
- this.recheckSensors = function()
111
+ // GET ALL RESOURCES + RULES
112
+ this.getAllResources = function()
83
113
  {
84
- scope.client.sensors.getAll().then(sensors => {
85
- let sensorUpdates = scope.getUpdates("sensor", sensors);
86
- scope.emitUpdates("sensor", sensorUpdates);
114
+ return new Promise(function(resolve, reject)
115
+ {
116
+ var allResources = [];
87
117
 
88
- return true;
89
- })
90
- .then(proceed => {
91
- // RECHECK
92
- if(scope.nodeActive == true && proceed == true) { setTimeout(function(){ scope.recheckSensors(); }, config.interval); }
93
- return true;
94
- })
95
- .catch(error => {
96
- scope.debug(error.stack);
118
+ // GET BRIDGE INFORMATION
119
+ scope.getBridgeInformation()
120
+ .then(function(bridgeInformation)
121
+ {
122
+ // PUSH TO RESOURCES
123
+ allResources.push(bridgeInformation);
97
124
 
98
- if(scope.nodeActive == true)
125
+ // CONTINUE WITH ALL RESOURCES
126
+ return API.request({ resource: "all" });
127
+ })
128
+ .then(function(v2Resources)
99
129
  {
100
- setTimeout(function(){ scope.recheckSensors(); }, 5000);
101
- }
130
+ // MERGE RESOURCES
131
+ allResources = allResources.concat(v2Resources);
132
+
133
+ // GET RULES (LEGACY API)
134
+ return API.request({ resource: "/rules", version: 1 });
135
+ })
136
+ .then(function(rules)
137
+ {
138
+ for (var [id, rule] of Object.entries(rules))
139
+ {
140
+ // "RENAME" OWNER
141
+ rule["_owner"] = rule["owner"];
142
+ delete rule["owner"];
143
+
144
+ // ADD RULE ID(S)
145
+ rule["id"] = "rule_" + id;
146
+ rule["id_v1"] = "/rules/" + id;
147
+
148
+ // ADD RULE TYPE
149
+ rule["type"] = "rule";
150
+
151
+ // PUSH RULES
152
+ allResources.push(rule);
153
+ }
154
+
155
+ resolve(allResources);
156
+ })
157
+ .catch(function(error) { reject(error); });
102
158
  });
103
159
  }
104
160
 
105
- // START FIRST CHECK
106
- if(!config.disableupdates)
161
+ // EMIT INITIAL STATES -> NODES
162
+ this.emitInitialStates = function(resources = false)
107
163
  {
108
- this.recheckAll();
164
+ return new Promise(function(resolve, reject)
165
+ {
166
+ // PUSH STATES
167
+ setTimeout(function()
168
+ {
169
+ // PUSH ALL STATES
170
+ for (const [id, resource] of Object.entries(scope.resources))
171
+ {
172
+ scope.pushUpdatedState(resource, resource.type, true);
173
+ }
174
+
175
+ resolve(true);
176
+ }, 500);
177
+ });
109
178
  }
110
- else
179
+
180
+ // KEEEP STATES UP-TO-DATE
181
+ this.keepUpdated = function()
111
182
  {
112
- this.recheckSensors();
183
+ if(!config.disableupdates)
184
+ {
185
+ scope.log("Keeping nodes up-to-date…");
186
+
187
+ // REFRESH STATES (SSE)
188
+ this.refreshStatesSSE();
189
+ }
113
190
  }
114
191
 
115
- // DETERMINE UPDATES
116
- this.getUpdates = function(mode, content)
192
+ // GET UPDATED STATES (SSE)
193
+ this.refreshStatesSSE = function()
117
194
  {
118
- var updates = [];
119
- for (var i = content.length - 1; i >= 0; i--)
195
+ scope.log("Subscribing to bridge events…");
196
+ API.subscribe(function(updates)
120
197
  {
121
- let device = content[i];
122
- var updated = false;
123
- var uniqueStatus = "";
198
+ const currentDateTime = dayjs().format();
124
199
 
125
- if(mode == "light")
200
+ for(let resource of updates)
126
201
  {
127
- uniqueStatus = ((device.on) ? "1" : "0") + device.brightness + device.hue + device.saturation + device.colorTemp + device.reachable;
202
+ let id = resource.id;
203
+ let type = resource.type;
204
+
205
+ let previousState = false;
128
206
 
129
- if(device.id in this.lights)
207
+ // HAS OWNER?
208
+ if(resource["owner"])
130
209
  {
131
- if(this.lights[device.id] != uniqueStatus)
210
+ let targetId = resource["owner"]["rid"];
211
+
212
+ if(scope.resources[targetId])
132
213
  {
133
- this.lights[device.id] = uniqueStatus;
134
- updated = true;
214
+ // GET PREVIOUS STATE
215
+ previousState = scope.resources[targetId]["services"][type][id];
216
+
217
+ // IS BUTTON? -> REMOVE PREVIOUS STATES
218
+ if(type === "button")
219
+ {
220
+ for (const [oneButtonID, oneButton] of Object.entries(scope.resources[targetId]["services"]["button"]))
221
+ {
222
+ delete scope.resources[targetId]["services"]["button"][oneButtonID]["button"];
223
+ }
224
+ }
135
225
  }
136
226
  }
137
- else
227
+ else if(scope.resources[id])
138
228
  {
139
- this.lights[device.id] = uniqueStatus;
140
- updated = true;
229
+ // GET PREVIOUS STATE
230
+ previousState = scope.resources[id];
141
231
  }
142
- }
143
- else if(mode == "sensor")
144
- {
145
- uniqueStatus = device.state.lastUpdated;
146
- if(device.id in this.sensors)
232
+
233
+ // NO PREVIOUS STATE?
234
+ if(!previousState) { return false; }
235
+
236
+ // CHECK DIFFERENCES
237
+ const mergedState = merge.deep(previousState, resource);
238
+ const updatedResources = diff(previousState, mergedState);
239
+
240
+ if(Object.values(updatedResources).length > 0)
147
241
  {
148
- if(this.sensors[device.id] != uniqueStatus)
242
+ if(resource["owner"])
149
243
  {
150
- this.sensors[device.id] = uniqueStatus;
151
- updated = true;
244
+ let targetId = resource["owner"]["rid"];
245
+
246
+ scope.resources[targetId]["services"][type][id] = mergedState;
247
+ scope.resources[targetId]["updated"] = currentDateTime;
248
+
249
+ // PUSH STATE
250
+ scope.pushUpdatedState(scope.resources[targetId], resource.type);
251
+ }
252
+ else
253
+ {
254
+ scope.resources[id] = mergedState;
255
+ scope.resources[id]["updated"] = currentDateTime;
256
+
257
+ // PUSH STATE
258
+ scope.pushUpdatedState(scope.resources[id], resource.type);
152
259
  }
153
260
  }
154
- else
155
- {
156
- this.sensors[device.id] = uniqueStatus;
157
- updated = true;
158
- }
159
261
  }
160
- else if(mode == "group")
262
+ });
263
+ }
264
+
265
+ // PUSH UPDATED STATE
266
+ this.pushUpdatedState = function(resource, updatedType, suppressMessage = false)
267
+ {
268
+ 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);
271
+
272
+ // RESOURCE CONTAINS SERVICES? -> SERVICE IN GROUP? -> EMIT CHANGES TO GROUPS ALSO
273
+ if(this.resources["_groupsOf"][resource.id])
274
+ {
275
+ for (var g = this.resources["_groupsOf"][resource.id].length - 1; g >= 0; g--)
276
+ {
277
+ const groupID = this.resources["_groupsOf"][resource.id][g];
278
+ const groupMessage = { id: groupID, type: "group", updatedType: updatedType, services: [], suppressMessage: suppressMessage };
279
+
280
+ this.events.emit(groupID, groupMessage);
281
+ this.events.emit("globalResourceUpdates", groupMessage);
282
+ }
283
+ }
284
+ }
285
+
286
+ // GET RESOURCE (FROM NODES)
287
+ this.get = function(type, id = false, options = {})
288
+ {
289
+ // GET SPECIFIC RESOURCE
290
+ if(id)
291
+ {
292
+ // RESOURCE EXISTS? -> PROCEED
293
+ if(scope.resources[id])
161
294
  {
162
- uniqueStatus = ((device.on) ? "1" : "0") + device.brightness + device.hue + device.saturation + device.colorTemp + ((device.anyOn) ? "1" : "0") + ((device.allOn) ? "1" : "0");
163
- if(device.id in this.groups)
295
+ // RESOLVE LINKS
296
+ const targetResource = scope.resources[id];
297
+ const lastState = scope.lastStates[type+targetResource.id] ? Object.assign({}, scope.lastStates[type+targetResource.id]) : false;
298
+
299
+ if(type == "bridge")
164
300
  {
165
- if(this.groups[device.id] != uniqueStatus)
166
- {
167
- this.groups[device.id] = uniqueStatus;
168
- updated = true;
169
- }
301
+ const message = new HueBridgeMessage(targetResource, options);
302
+
303
+ // GET CURRENT STATE MESSAGE
304
+ let currentState = message.msg;
305
+ return currentState;
306
+ }
307
+ else if(type == "light")
308
+ {
309
+ const message = new HueLightMessage(targetResource, options);
310
+
311
+ // GET & SAVE LAST STATE AND DIFFERENCES
312
+ let currentState = message.msg;
313
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
314
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
315
+ currentState.lastState = lastState;
316
+
317
+ return currentState;
318
+ }
319
+ else if(type == "group")
320
+ {
321
+ // GET MESSAGE
322
+ const message = new HueGroupMessage(targetResource, { resources: scope.resources, ...options});
323
+
324
+ // GET & SAVE LAST STATE AND DIFFERENCES
325
+ let currentState = message.msg;
326
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
327
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
328
+ currentState.lastState = lastState;
329
+
330
+ return currentState;
331
+ }
332
+ else if(type == "button")
333
+ {
334
+ const message = new HueButtonsMessage(targetResource, options);
335
+
336
+ // GET & SAVE LAST STATE AND DIFFERENCES
337
+ let currentState = message.msg;
338
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
339
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
340
+ currentState.lastState = lastState;
341
+
342
+ return currentState;
343
+ }
344
+ else if(type == "motion")
345
+ {
346
+ const message = new HueMotionMessage(targetResource, options);
347
+
348
+ // GET & SAVE LAST STATE AND DIFFERENCES
349
+ let currentState = message.msg;
350
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
351
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
352
+ currentState.lastState = lastState;
353
+
354
+ return currentState;
355
+ }
356
+ else if(type == "temperature")
357
+ {
358
+ const message = new HueTemperatureMessage(targetResource, options);
359
+
360
+ // GET & SAVE LAST STATE AND DIFFERENCES
361
+ let currentState = message.msg;
362
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
363
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
364
+ currentState.lastState = lastState;
365
+
366
+ return currentState;
367
+ }
368
+ else if(type == "light_level")
369
+ {
370
+ const message = new HueBrightnessMessage(targetResource, options);
371
+
372
+ // GET & SAVE LAST STATE AND DIFFERENCES
373
+ let currentState = message.msg;
374
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
375
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
376
+ currentState.lastState = lastState;
377
+
378
+ return currentState;
379
+ }
380
+ else if(type == "rule")
381
+ {
382
+ const message = new HueRulesMessage(targetResource, options);
383
+
384
+ // GET & SAVE LAST STATE AND DIFFERENCES
385
+ let currentState = message.msg;
386
+ scope.lastStates[type+targetResource.id] = Object.assign({}, currentState);
387
+ currentState.updated = (lastState === false) ? {} : diff(lastState, currentState);
388
+ currentState.lastState = lastState;
389
+
390
+ return currentState;
170
391
  }
171
392
  else
172
393
  {
173
- this.groups[device.id] = uniqueStatus;
174
- updated = true;
394
+ return false;
175
395
  }
176
396
  }
177
- else if(mode == "rule")
397
+ else
398
+ {
399
+ return false;
400
+ }
401
+ }
402
+ else
403
+ {
404
+ // FILTER RESOURCES BY TYPE
405
+ let allFilteredResources = {};
406
+
407
+ for (const [rootID, resource] of Object.entries(scope.resources))
178
408
  {
179
- let rule = device;
180
- uniqueStatus = rule.lastTriggered + rule.status;
409
+ const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
181
410
 
182
- if(rule.id in this.rules)
411
+ // NORMAL DEVICES
412
+ if(!isGroup && resource["services"] && resource["services"][type])
183
413
  {
184
- if(this.rules[rule.id] != uniqueStatus)
414
+ for (const [serviceID, targetDevice] of Object.entries(resource["services"][type]))
185
415
  {
186
- this.rules[rule.id] = uniqueStatus;
187
- updated = true;
416
+ allFilteredResources[rootID] = scope.get(type, rootID);
188
417
  }
189
418
  }
190
- else
419
+ // GROUPED RESOURCES
420
+ else if(isGroup && type === "group")
191
421
  {
192
- this.rules[rule.id] = uniqueStatus;
193
- updated = true;
422
+ allFilteredResources[rootID] = scope.get(type, rootID);
194
423
  }
195
424
  }
196
425
 
197
- // IS UPDATED?
198
- if(updated == true)
426
+ return Object.values(allFilteredResources);
427
+ }
428
+ }
429
+
430
+ // PATCH RESOURCE (FROM NODES)
431
+ this.patch = function(type, id, patch, version = 2)
432
+ {
433
+ return new Promise(function(resolve, reject)
434
+ {
435
+ // GET SERVICE ID
436
+ if(version !== 1 && scope.resources[id] && scope.resources[id]["services"] && scope.resources[id]["services"][type])
199
437
  {
200
- updates.push(device);
438
+ const targetResource = Object.values(scope.resources[id]["services"][type])[0];
439
+ id = targetResource.id;
201
440
  }
441
+
442
+ // ACTION!
443
+ API.request({ method: "PUT", resource: (version === 2) ? (type+"/"+id) : id, data: patch, version: version })
444
+ .then(function(response) {
445
+ resolve(response);
446
+ })
447
+ .catch(function(error) {
448
+ reject(error);
449
+ });
450
+ });
451
+ }
452
+
453
+ // RE-FETCH RULE (RECEIVES NO UPDATES VIA SSE)
454
+ this.refetchRule = function(id)
455
+ {
456
+ return new Promise(function(resolve, reject)
457
+ {
458
+ API.request({ resource: "/rules/" + id, version: 1 })
459
+ .then(function(rule)
460
+ {
461
+ // "RENAME" OWNER
462
+ rule["_owner"] = rule["owner"];
463
+ delete rule["owner"];
464
+
465
+ // ADD RULE ID(S)
466
+ rule["id"] = "rule_" + id;
467
+ rule["id_v1"] = "/rules/" + id;
468
+
469
+ // ADD RULE TYPE
470
+ rule["type"] = "rule";
471
+
472
+ // UPDATED TIME
473
+ rule["updated"] = dayjs().format();
474
+
475
+ // ADD BACK TO RESOURCES
476
+ scope.resources[rule["id"]] = rule;
477
+
478
+ // PUSH UPDATED STATE
479
+ scope.pushUpdatedState(rule, "rule");
480
+ resolve(resolve);
481
+ })
482
+ .catch(function(error) {
483
+ reject(error);
484
+ });
485
+ });
486
+ }
487
+
488
+ // SUBSCRIBE (FROM NODES)
489
+ this.subscribe = function(type, id = null, callback = null)
490
+ {
491
+ // IS RULE?
492
+ if(type == "rule" && !!id)
493
+ {
494
+ id = "rule_" + id;
202
495
  }
203
496
 
204
- // RETURN UPDATES
205
- return updates;
497
+ // PUSH WHITELIST
498
+ const messageWhitelist = {
499
+ "light": ["light", "zigbee_connectivity", "zgp_connectivity", "device"],
500
+ "motion": ["motion", "zigbee_connectivity", "zgp_connectivity", "device_power", "device"],
501
+ "temperature": ["temperature", "zigbee_connectivity", "zgp_connectivity", "device_power", "device"],
502
+ "light_level": ["light_level", "zigbee_connectivity", "zgp_connectivity", "device_power", "device"],
503
+ "button": ["button", "zigbee_connectivity", "zgp_connectivity", "device_power", "device"],
504
+ "group": ["group", "light", "grouped_light"],
505
+ "rule": ["rule"]
506
+ };
507
+
508
+ if(!id)
509
+ {
510
+ // UNIVERSAL MODE
511
+ this.events.on("globalResourceUpdates", function(info)
512
+ {
513
+ if(type === "bridge")
514
+ {
515
+ callback(info);
516
+ }
517
+ else if(info.services.includes(type) && messageWhitelist[type].includes(info.updatedType))
518
+ {
519
+ callback(info);
520
+ }
521
+ else if(type == "rule" && messageWhitelist[type].includes(info.updatedType))
522
+ {
523
+ callback(info);
524
+ }
525
+ });
526
+ }
527
+ else
528
+ {
529
+ // SPECIFIC RESOURCE MODE
530
+ this.events.on(id, function(info)
531
+ {
532
+ if(type === "bridge" || messageWhitelist[type].includes(info.updatedType))
533
+ {
534
+ callback(info);
535
+ }
536
+ });
537
+ }
206
538
  }
207
539
 
208
- // EMIT UPDATES
209
- this.emitUpdates = function(device, updates)
540
+ // AUTO UPDATES?
541
+ this.autoUpdateFirmware = function()
210
542
  {
211
- for(let deviceUpdate of updates)
543
+ if((config.autoupdates && config.autoupdates == true) || typeof config.autoupdates == 'undefined')
212
544
  {
213
- let eventName = device + deviceUpdate.id;
214
- scope.events.emit(eventName, deviceUpdate);
215
- scope.events.emit(device, deviceUpdate);
216
- scope.events.emit("globalDeviceUpdates", {type: device, payload: deviceUpdate});
545
+ scope.log("Checking for Hue Bridge firmware updates…");
546
+ API.request({
547
+ method: "PUT",
548
+ resource: "/config",
549
+ version: 1,
550
+ data: {
551
+ swupdate2: {
552
+ checkforupdate: true,
553
+ install: true
554
+ }
555
+ }
556
+ })
557
+ .then(function(status)
558
+ {
559
+ if(scope.nodeActive == true)
560
+ {
561
+ setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 5);
562
+ }
563
+ })
564
+ .catch(function(error)
565
+ {
566
+ // NO UPDATES AVAILABLE // TRY AGAIN IN 3H
567
+ if(scope.nodeActive == true)
568
+ {
569
+ setTimeout(function(){ scope.autoUpdateFirmware(); }, 60000 * 180);
570
+ }
571
+ });
217
572
  }
218
573
  }
219
574
 
575
+ //
576
+ // START THE MAGIC
577
+ this.start();
578
+
220
579
  //
221
580
  // CLOSE NODE / REMOVE EVENT LISTENER
222
581
  this.on('close', function()
223
582
  {
224
583
  scope.nodeActive = false;
584
+
585
+ // UNSUBSCRIBE FROM BRIDGE EVENTS
586
+ scope.log("Unsubscribing from bridge events…");
587
+ API.unsubscribe();
588
+
589
+ // UNSUBSCRIBE FROM "READY" EVENTS
590
+ scope.events.removeAllListeners();
225
591
  });
226
592
  }
227
593
 
@@ -229,16 +595,29 @@ module.exports = function(RED)
229
595
 
230
596
  //
231
597
  // DISCOVER HUE BRIDGES ON LOCAL NETWORK
232
- RED.httpAdmin.get('/hue/bridges', function(req, res, next)
598
+ RED.httpAdmin.get('/hue/bridges', async function(req, res, next)
233
599
  {
234
- let huejay = require('huejay');
600
+ axios({
601
+ "method": "GET",
602
+ "url": "https://discovery.meethue.com",
603
+ "headers": {
604
+ "Content-Type": "application/json; charset=utf-8"
605
+ },
606
+ })
607
+ .then(function(response)
608
+ {
609
+ // PREPARE BRIDGES OUTPUT
610
+ var bridges = {};
611
+ for (var i = response.data.length - 1; i >= 0; i--)
612
+ {
613
+ var ipAddress = response.data[i].internalipaddress;
614
+ bridges[ipAddress] = { ip: ipAddress, name: ipAddress };
615
+ }
235
616
 
236
- huejay.discover()
237
- .then(bridges => {
238
- res.end(JSON.stringify(bridges));
617
+ res.end(JSON.stringify(Object.values(bridges)));
239
618
  })
240
- .catch(error => {
241
- res.send(500).send(error.message);
619
+ .catch(function(error) {
620
+ res.send(error);
242
621
  });
243
622
  });
244
623
 
@@ -252,17 +631,12 @@ module.exports = function(RED)
252
631
  }
253
632
  else
254
633
  {
255
- var huejay = require('huejay');
256
-
257
- var bridgeIP = (req.query.ip).toString();
258
- var client = new huejay.Client({host: bridgeIP, username: 'default'});
259
-
260
- client.bridge.get()
261
- .then(bridge => {
634
+ API.init({ ip: req.query.ip })
635
+ .then(function(bridge) {
262
636
  res.end(bridge.name);
263
637
  })
264
- .catch(error => {
265
- res.send(500).send(error.message);
638
+ .catch(function(error) {
639
+ res.send(error);
266
640
  });
267
641
  }
268
642
  });
@@ -277,200 +651,138 @@ module.exports = function(RED)
277
651
  }
278
652
  else
279
653
  {
280
- var request = require('request');
281
- let bridgeIP = (req.query.ip).toString();
282
-
283
- request.post('http://'+bridgeIP+'/api', {body: JSON.stringify({"devicetype": "nodered_" + Math.floor((Math.random() * 100) + 1)}) }, function(err,httpResponse,body)
654
+ axios({
655
+ "method": "POST",
656
+ "url": "http://"+req.query.ip+"/api",
657
+ "httpsAgent": new https.Agent({ rejectUnauthorized: false }),
658
+ "headers": {
659
+ "Content-Type": "application/json; charset=utf-8"
660
+ },
661
+ "data": {
662
+ "devicetype": "huemagic_" + Math.floor((Math.random() * 100) + 1)
663
+ }
664
+ })
665
+ .then(function(response)
284
666
  {
285
- if(err)
667
+ var bridge = response.data;
668
+ if(bridge[0].error)
286
669
  {
287
670
  rescope.end("error");
288
671
  }
289
672
  else
290
673
  {
291
- var bridge = JSON.parse(body);
292
-
293
- if(bridge[0].error)
294
- {
295
- rescope.end("error");
296
- }
297
- else
298
- {
299
- rescope.end(JSON.stringify(bridge));
300
- }
674
+ rescope.end(JSON.stringify(bridge));
301
675
  }
676
+ })
677
+ .catch(function(error) {
678
+ rescope.status(500).send(error);
302
679
  });
303
680
  }
304
681
  });
305
682
 
306
683
  //
307
- // DISCOVER SENSORS
308
- RED.httpAdmin.get('/hue/sensors', function(req, res, next)
684
+ // DISCOVER RESOURCES
685
+ RED.httpAdmin.get('/hue/resources', function(req, res, next)
309
686
  {
310
- let huejay = require('huejay');
311
- var bridge = (req.query.bridge).toString();
312
- var username = req.query.key;
313
- var type = req.query.type;
314
-
315
- let client = new huejay.Client({
316
- host: bridge,
317
- username: username
318
- });
319
-
320
- client.sensors.getAll()
321
- .then(sensors => {
322
- var allSensorDevices = [];
687
+ const targetType = req.query.type;
323
688
 
324
- for (let sensor of sensors)
689
+ // GET ALL RULES
690
+ if(targetType == "rule")
691
+ {
692
+ API.init({ ip: req.query.bridge, key: req.query.key })
693
+ .then(function(bridge) {
694
+ return API.request({ resource: "/rules", version: 1 });
695
+ })
696
+ .then(function(rules)
325
697
  {
326
- if(sensor.type == type)
698
+ let targetRules = {};
699
+
700
+ for (var [id, rule] of Object.entries(rules))
327
701
  {
328
- var oneSensor = {};
329
- oneSensor.id = sensor.id;
330
- oneSensor.name = sensor.name;
331
- oneSensor.model = sensor.model.name;
332
- oneSensor.model_id = sensor.attributes.attributes.modelid;
702
+ var oneDevice = {};
703
+ oneDevice.id = id;
704
+ oneDevice.name = rule.name;
705
+ oneDevice.model = false;
333
706
 
334
- allSensorDevices.push(oneSensor);
707
+ targetRules[id] = oneDevice;
335
708
  }
336
- }
337
-
338
- res.end(JSON.stringify(allSensorDevices));
339
- })
340
- .catch(error => {
341
- res.send(500).send(error.stack);
342
- });
343
- });
344
709
 
345
- //
346
- // DISCOVER LIGHTS
347
- RED.httpAdmin.get('/hue/lights', function(req, res, next)
348
- {
349
- let huejay = require('huejay');
350
- var bridge = (req.query.bridge).toString();
351
- var username = req.query.key;
710
+ // CONVERT TO ARRAY
711
+ targetRules = Object.values(targetRules);
352
712
 
353
- let client = new huejay.Client({
354
- host: bridge,
355
- username: username
356
- });
357
-
358
- client.lights.getAll()
359
- .then(lights => {
360
- var allLights = [];
361
-
362
- for (let light of lights)
363
- {
364
- var oneLightBulb = {};
365
- oneLightBulb.id = light.id;
366
- oneLightBulb.name = light.name;
367
-
368
- allLights.push(oneLightBulb);
369
- }
370
-
371
- res.end(JSON.stringify(allLights));
372
- })
373
- .catch(error => {
374
- res.send(500).send(error.stack);
375
- });
376
- });
377
-
378
- //
379
- // DISCOVER GROUPS
380
- RED.httpAdmin.get('/hue/groups', function(req, res, next)
381
- {
382
- let huejay = require('huejay');
383
- var bridge = (req.query.bridge).toString();
384
- var username = req.query.key;
385
-
386
- let client = new huejay.Client({
387
- host: bridge,
388
- username: username
389
- });
390
-
391
- client.groups.getAll()
392
- .then(groups => {
393
- var allGroups = [];
394
-
395
- for (let group of groups)
713
+ // GIVE BACK
714
+ res.end(JSON.stringify(targetRules));
715
+ })
716
+ .catch(function(error) {
717
+ res.status(500).send(JSON.stringify(error));
718
+ });
719
+ }
720
+ // GET ALL OTHER RESOURCES
721
+ else
722
+ {
723
+ API.init({ ip: req.query.bridge, key: req.query.key })
724
+ .then(function(bridge) {
725
+ return API.request({ resource: "all" });
726
+ })
727
+ .then(function(allResources)
396
728
  {
397
- var oneGroup = {};
398
- oneGroup.id = group.id;
399
- oneGroup.name = group.name;
400
-
401
- allGroups.push(oneGroup);
402
- }
403
-
404
- res.end(JSON.stringify(allGroups));
405
- })
406
- .catch(error => {
407
- res.send(500).send(error.stack);
408
- });
409
- });
410
-
411
- //
412
- // DISCOVER SCENES
413
- RED.httpAdmin.get('/hue/scenes', function(req, res, next)
414
- {
415
- let huejay = require('huejay');
416
- var bridge = (req.query.bridge).toString();
417
- var username = req.query.key;
418
-
419
- let client = new huejay.Client({
420
- host: bridge,
421
- username: username
422
- });
423
-
424
- client.scenes.getAll()
425
- .then(scenes => {
426
- var allScenes = [];
427
-
428
- for (let scene of scenes)
729
+ return API.processResources(allResources);
730
+ })
731
+ .then(function(processedResources)
429
732
  {
430
- var oneScene = {};
431
- oneScene.id = scene.id;
432
- oneScene.name = scene.name;
433
-
434
- allScenes.push(oneScene);
435
- }
733
+ let targetDevices = {};
436
734
 
437
- res.end(JSON.stringify(allScenes));
438
- })
439
- .catch(error => {
440
- res.send(500).send(error.stack);
441
- });
442
- });
735
+ for (const [id, resource] of Object.entries(processedResources))
736
+ {
737
+ const isGroup = (resource["type"] == "room" || resource["type"] == "zone" || resource["type"] == "bridge_home");
443
738
 
444
- //
445
- // DISCOVER RULES
446
- RED.httpAdmin.get('/hue/rules', function(req, res, next)
447
- {
448
- let huejay = require('huejay');
449
- var bridge = (req.query.bridge).toString();
450
- var username = req.query.key;
739
+ // NORMAL DEVICES
740
+ if(!isGroup && resource["services"] && resource["services"][targetType])
741
+ {
742
+ for (const [deviceID, targetDevice] of Object.entries(resource["services"][targetType]))
743
+ {
744
+ var oneDevice = {};
745
+ oneDevice.id = id;
746
+ oneDevice.name = resource.metadata ? resource.metadata.name : false;
747
+ oneDevice.model = resource.product_data ? resource.product_data.product_name : false;
451
748
 
452
- let client = new huejay.Client({
453
- host: bridge,
454
- username: username
455
- });
749
+ targetDevices[id] = oneDevice;
750
+ }
751
+ }
752
+ // GROUPED (LIGHT) RESOURCES
753
+ else if(isGroup && targetType === "group")
754
+ {
755
+ if(resource["services"] && resource["services"]["grouped_light"])
756
+ {
757
+ var oneDevice = {};
758
+ oneDevice.id = id;
759
+ oneDevice.name = resource.metadata ? resource.metadata.name : false;
760
+ oneDevice.model = resource["type"];
456
761
 
457
- client.rules.getAll()
458
- .then(rules => {
459
- var allRules = [];
762
+ targetDevices[id] = oneDevice;
763
+ }
764
+ }
765
+ // SCENES
766
+ else if(targetType === "scene" && resource["type"] == "scene")
767
+ {
768
+ var oneDevice = {};
769
+ oneDevice.id = id;
770
+ oneDevice.name = resource.metadata ? resource.metadata.name : false;
771
+ oneDevice.group = processedResources[resource["group"]["rid"]].metadata.name;
460
772
 
461
- for (let rule of rules)
462
- {
463
- var oneRule = {};
464
- oneRule.id = rule.id;
465
- oneRule.name = rule.name;
773
+ targetDevices[id] = oneDevice;
774
+ }
775
+ }
466
776
 
467
- allRules.push(oneRule);
468
- }
777
+ // CONVERT TO ARRAY
778
+ targetDevices = Object.values(targetDevices);
469
779
 
470
- res.end(JSON.stringify(allRules));
471
- })
472
- .catch(error => {
473
- res.send(500).send(error.stack);
474
- });
780
+ // GIVE BACK
781
+ res.end(JSON.stringify(targetDevices));
782
+ })
783
+ .catch(function(error) {
784
+ res.status(500).send(JSON.stringify(error));
785
+ });
786
+ }
475
787
  });
476
788
  };