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