node-red-contrib-huemagic 2.8.2 → 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 (165) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +806 -729
  3. package/examples/Hue Bridge.json +189 -35
  4. package/examples/Hue Brightness.json +124 -14
  5. package/examples/Hue Buttons.json +88 -0
  6. package/examples/Hue Group.json +575 -63
  7. package/examples/Hue Light.json +805 -58
  8. package/examples/Hue Magic.json +69 -24
  9. package/examples/Hue Motion.json +110 -42
  10. package/examples/Hue Rules.json +105 -37
  11. package/examples/Hue Scene.json +145 -28
  12. package/examples/Hue Temperature.json +124 -14
  13. package/huemagic/animations/001-sunny.json +22 -22
  14. package/huemagic/animations/002-cloudy.json +46 -46
  15. package/huemagic/animations/003-nightsky.json +20 -20
  16. package/huemagic/animations/004-sunrise.json +54 -54
  17. package/huemagic/animations/005-sunset.json +46 -46
  18. package/huemagic/animations/006-fireplace.json +74 -74
  19. package/huemagic/animations/007-spring.json +31 -31
  20. package/huemagic/animations/008-autmn.json +23 -23
  21. package/huemagic/animations/009-winter.json +23 -23
  22. package/huemagic/animations/010-dirtyfog.json +23 -23
  23. package/huemagic/animations/011-lgbt.json +49 -50
  24. package/huemagic/animations/012-rasta.json +28 -29
  25. package/huemagic/animations/013-strobo.json +30 -31
  26. package/huemagic/animations/014-randomrainbow.json +30 -31
  27. package/huemagic/animations/015-sos.json +158 -159
  28. package/huemagic/animations/016-milkyway.json +83 -0
  29. package/huemagic/animations/017-beach.json +40 -0
  30. package/huemagic/animations/018-forest.json +43 -0
  31. package/huemagic/animations/019-christmas.json +30 -0
  32. package/huemagic/animations/020-superhero.json +56 -0
  33. package/huemagic/animations/021-neoncity.json +52 -0
  34. package/huemagic/animations/previews/001-sunny.gif +0 -0
  35. package/huemagic/animations/previews/002-cloudy.gif +0 -0
  36. package/huemagic/animations/previews/003-nightsky.gif +0 -0
  37. package/huemagic/animations/previews/004-sunrise.gif +0 -0
  38. package/huemagic/animations/previews/005-sunset.gif +0 -0
  39. package/huemagic/animations/previews/006-fireplace.gif +0 -0
  40. package/huemagic/animations/previews/007-spring.gif +0 -0
  41. package/huemagic/animations/previews/008-autmn.gif +0 -0
  42. package/huemagic/animations/previews/009-winter.gif +0 -0
  43. package/huemagic/animations/previews/010-dirtyfog.gif +0 -0
  44. package/huemagic/animations/previews/011-lgbt.gif +0 -0
  45. package/huemagic/animations/previews/012-rasta.gif +0 -0
  46. package/huemagic/animations/previews/013-strobo.gif +0 -0
  47. package/huemagic/animations/previews/014-randomrainbow.gif +0 -0
  48. package/huemagic/animations/previews/015-sos.gif +0 -0
  49. package/huemagic/animations/previews/016-milkyway.gif +0 -0
  50. package/huemagic/animations/previews/017-beach.gif +0 -0
  51. package/huemagic/animations/previews/018-forest.gif +0 -0
  52. package/huemagic/animations/previews/019-christmas.gif +0 -0
  53. package/huemagic/animations/previews/020-superhero.gif +0 -0
  54. package/huemagic/animations/previews/021-neoncity.gif +0 -0
  55. package/huemagic/hue-bridge-config.html +105 -61
  56. package/huemagic/hue-bridge-config.js +629 -319
  57. package/huemagic/hue-bridge.html +41 -21
  58. package/huemagic/hue-bridge.js +191 -398
  59. package/huemagic/hue-brightness.html +130 -49
  60. package/huemagic/hue-brightness.js +131 -35
  61. package/huemagic/hue-buttons.html +197 -0
  62. package/huemagic/hue-buttons.js +152 -0
  63. package/huemagic/hue-group.html +137 -60
  64. package/huemagic/hue-group.js +394 -367
  65. package/huemagic/hue-light.html +133 -59
  66. package/huemagic/hue-light.js +551 -336
  67. package/huemagic/hue-magic.html +91 -237
  68. package/huemagic/hue-magic.js +57 -27
  69. package/huemagic/hue-motion.html +128 -50
  70. package/huemagic/hue-motion.js +116 -87
  71. package/huemagic/hue-rules.html +121 -50
  72. package/huemagic/hue-rules.js +84 -64
  73. package/huemagic/hue-scene.html +96 -115
  74. package/huemagic/hue-scene.js +65 -96
  75. package/huemagic/hue-temperature.html +130 -49
  76. package/huemagic/hue-temperature.js +136 -32
  77. package/huemagic/icons/hue-bridge.png +0 -0
  78. package/huemagic/icons/hue-bridge.svg +3 -0
  79. package/huemagic/icons/hue-brightness.png +0 -0
  80. package/huemagic/icons/hue-brightness.svg +11 -0
  81. package/huemagic/icons/hue-buttons.png +0 -0
  82. package/huemagic/icons/hue-buttons.svg +4 -0
  83. package/huemagic/icons/hue-group.png +0 -0
  84. package/huemagic/icons/hue-group.svg +12 -0
  85. package/huemagic/icons/hue-light.png +0 -0
  86. package/huemagic/icons/hue-light.svg +3 -0
  87. package/huemagic/icons/hue-magic.png +0 -0
  88. package/huemagic/icons/hue-magic.svg +3 -0
  89. package/huemagic/icons/hue-motion.png +0 -0
  90. package/huemagic/icons/hue-motion.svg +9 -0
  91. package/huemagic/icons/hue-rules.png +0 -0
  92. package/huemagic/icons/hue-rules.svg +4 -0
  93. package/huemagic/icons/hue-scene.png +0 -0
  94. package/huemagic/icons/hue-scene.svg +5 -0
  95. package/huemagic/icons/hue-temperature.png +0 -0
  96. package/huemagic/icons/hue-temperature.svg +7 -0
  97. package/huemagic/locales/de/hue-bridge-config.html +17 -12
  98. package/huemagic/locales/de/hue-bridge-config.json +7 -5
  99. package/huemagic/locales/de/hue-bridge.html +64 -145
  100. package/huemagic/locales/de/hue-bridge.json +6 -17
  101. package/huemagic/locales/de/hue-brightness.html +52 -38
  102. package/huemagic/locales/de/hue-brightness.json +7 -2
  103. package/huemagic/locales/de/hue-buttons.html +58 -0
  104. package/huemagic/locales/de/hue-buttons.json +28 -0
  105. package/huemagic/locales/de/hue-group.html +46 -122
  106. package/huemagic/locales/de/hue-group.json +4 -2
  107. package/huemagic/locales/de/hue-light.html +68 -116
  108. package/huemagic/locales/de/hue-light.json +5 -2
  109. package/huemagic/locales/de/hue-magic.html +65 -57
  110. package/huemagic/locales/de/hue-magic.json +2 -1
  111. package/huemagic/locales/de/hue-motion.html +48 -44
  112. package/huemagic/locales/de/hue-motion.json +6 -3
  113. package/huemagic/locales/de/hue-rules.html +68 -73
  114. package/huemagic/locales/de/hue-rules.json +5 -2
  115. package/huemagic/locales/de/hue-scene.html +23 -32
  116. package/huemagic/locales/de/hue-scene.json +5 -2
  117. package/huemagic/locales/de/hue-temperature.html +53 -36
  118. package/huemagic/locales/de/hue-temperature.json +7 -2
  119. package/huemagic/locales/en-US/hue-bridge-config.html +17 -12
  120. package/huemagic/locales/en-US/hue-bridge-config.json +5 -3
  121. package/huemagic/locales/en-US/hue-bridge.html +64 -145
  122. package/huemagic/locales/en-US/hue-bridge.json +5 -16
  123. package/huemagic/locales/en-US/hue-brightness.html +52 -38
  124. package/huemagic/locales/en-US/hue-brightness.json +10 -3
  125. package/huemagic/locales/en-US/hue-buttons.html +58 -0
  126. package/huemagic/locales/en-US/hue-buttons.json +34 -0
  127. package/huemagic/locales/en-US/hue-group.html +46 -122
  128. package/huemagic/locales/en-US/hue-group.json +7 -3
  129. package/huemagic/locales/en-US/hue-light.html +68 -116
  130. package/huemagic/locales/en-US/hue-light.json +8 -3
  131. package/huemagic/locales/en-US/hue-magic.html +65 -57
  132. package/huemagic/locales/en-US/hue-magic.json +3 -1
  133. package/huemagic/locales/en-US/hue-motion.html +48 -44
  134. package/huemagic/locales/en-US/hue-motion.json +9 -4
  135. package/huemagic/locales/en-US/hue-rules.html +68 -73
  136. package/huemagic/locales/en-US/hue-rules.json +5 -2
  137. package/huemagic/locales/en-US/hue-scene.html +23 -32
  138. package/huemagic/locales/en-US/hue-scene.json +5 -2
  139. package/huemagic/locales/en-US/hue-temperature.html +53 -36
  140. package/huemagic/locales/en-US/hue-temperature.json +10 -3
  141. package/huemagic/utils/api.js +290 -0
  142. package/huemagic/utils/color.js +382 -0
  143. package/{utils → huemagic/utils}/merge.js +9 -6
  144. package/huemagic/utils/messages.js +447 -0
  145. package/package.json +12 -12
  146. package/examples/Hue Switch.json +0 -40
  147. package/examples/Hue Tap.json +0 -40
  148. package/huemagic/assets/anime.js +0 -8
  149. package/huemagic/hue-switch.html +0 -119
  150. package/huemagic/hue-switch.js +0 -113
  151. package/huemagic/hue-tap.html +0 -119
  152. package/huemagic/hue-tap.js +0 -74
  153. package/huemagic/icons/hue-button.png +0 -0
  154. package/huemagic/icons/hue-switch.png +0 -0
  155. package/huemagic/icons/hue-tap.png +0 -0
  156. package/huemagic/locales/de/hue-switch.html +0 -50
  157. package/huemagic/locales/de/hue-switch.json +0 -31
  158. package/huemagic/locales/de/hue-tap.html +0 -41
  159. package/huemagic/locales/de/hue-tap.json +0 -23
  160. package/huemagic/locales/en-US/hue-switch.html +0 -50
  161. package/huemagic/locales/en-US/hue-switch.json +0 -31
  162. package/huemagic/locales/en-US/hue-tap.html +0 -41
  163. package/huemagic/locales/en-US/hue-tap.json +0 -23
  164. package/utils/messages.js +0 -571
  165. 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();
113
- }
180
+ if(!config.disableupdates)
181
+ {
182
+ scope.log("Keeping nodes up-to-date…");
114
183
 
184
+ // REFRESH STATES (SSE)
185
+ this.refreshStatesSSE();
186
+ }
187
+ }
115
188
 
116
- // DETERMINE UPDATES
117
- this.getUpdates = function(mode, content)
189
+ // GET UPDATED STATES (SSE)
190
+ this.refreshStatesSSE = function()
118
191
  {
119
- var updates = [];
120
- for (var i = content.length - 1; i >= 0; i--)
192
+ scope.log("Subscribing to bridge events…");
193
+ API.subscribe(function(updates)
121
194
  {
122
- let device = content[i];
123
- var updated = false;
124
- var uniqueStatus = "";
195
+ const currentDateTime = dayjs().format();
125
196
 
126
- if(mode == "light")
197
+ for(let ressource of updates)
127
198
  {
128
- 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;
201
+
202
+ let previousState = false;
129
203
 
130
- if(device.id in this.lights)
204
+ // HAS OWNER?
205
+ if(ressource["owner"])
131
206
  {
132
- if(this.lights[device.id] != uniqueStatus)
207
+ let targetId = ressource["owner"]["rid"];
208
+
209
+ if(scope.ressources[targetId])
133
210
  {
134
- this.lights[device.id] = uniqueStatus;
135
- 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
+ }
136
222
  }
137
223
  }
138
- else
224
+ else if(scope.ressources[id])
139
225
  {
140
- this.lights[device.id] = uniqueStatus;
141
- updated = true;
226
+ // GET PREVIOUS STATE
227
+ previousState = scope.ressources[id];
142
228
  }
143
- }
144
- else if(mode == "sensor")
145
- {
146
- uniqueStatus = device.state.lastUpdated;
147
- 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)
148
238
  {
149
- if(this.sensors[device.id] != uniqueStatus)
239
+ if(ressource["owner"])
150
240
  {
151
- this.sensors[device.id] = uniqueStatus;
152
- 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);
153
256
  }
154
257
  }
155
- else
156
- {
157
- this.sensors[device.id] = uniqueStatus;
158
- updated = true;
159
- }
160
258
  }
161
- 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])
162
291
  {
163
- uniqueStatus = ((device.on) ? "1" : "0") + device.brightness + device.hue + device.saturation + device.colorTemp + ((device.anyOn) ? "1" : "0") + ((device.allOn) ? "1" : "0");
164
- 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")
165
297
  {
166
- if(this.groups[device.id] != uniqueStatus)
167
- {
168
- this.groups[device.id] = uniqueStatus;
169
- updated = true;
170
- }
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;
171
388
  }
172
389
  else
173
390
  {
174
- this.groups[device.id] = uniqueStatus;
175
- updated = true;
391
+ return false;
176
392
  }
177
393
  }
178
- 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))
179
405
  {
180
- let rule = device;
181
- uniqueStatus = rule.lastTriggered + rule.status;
406
+ const isGroup = (ressource["type"] == "room" || ressource["type"] == "zone" || ressource["type"] == "bridge_home");
182
407
 
183
- if(rule.id in this.rules)
408
+ // NORMAL DEVICES
409
+ if(!isGroup && ressource["services"] && ressource["services"][type])
184
410
  {
185
- if(this.rules[rule.id] != uniqueStatus)
411
+ for (const [serviceID, targetDevice] of Object.entries(ressource["services"][type]))
186
412
  {
187
- this.rules[rule.id] = uniqueStatus;
188
- updated = true;
413
+ allFilteredRessources[rootID] = scope.get(type, rootID);
189
414
  }
190
415
  }
191
- else
416
+ // GROUPED RESSOURCES
417
+ else if(isGroup && type === "group")
192
418
  {
193
- this.rules[rule.id] = uniqueStatus;
194
- updated = true;
419
+ allFilteredRessources[rootID] = scope.get(type, rootID);
195
420
  }
196
421
  }
197
422
 
198
- // IS UPDATED?
199
- 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])
200
434
  {
201
- updates.push(device);
435
+ const targetRessource = Object.values(scope.ressources[id]["services"][type])[0];
436
+ id = targetRessource.id;
202
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;
203
492
  }
204
493
 
205
- // RETURN UPDATES
206
- 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
+ }
207
535
  }
208
536
 
209
- // EMIT UPDATES
210
- this.emitUpdates = function(device, updates)
537
+ // AUTO UPDATES?
538
+ this.autoUpdateFirmware = function()
211
539
  {
212
- for(let deviceUpdate of updates)
540
+ if((config.autoupdates && config.autoupdates == true) || typeof config.autoupdates == 'undefined')
213
541
  {
214
- let eventName = device + deviceUpdate.id;
215
- scope.events.emit(eventName, 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,199 +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;
699
+ var oneDevice = {};
700
+ oneDevice.id = id;
701
+ oneDevice.name = rule.name;
702
+ oneDevice.model = false;
332
703
 
333
- allSensorDevices.push(oneSensor);
704
+ targetRules[id] = oneDevice;
334
705
  }
335
- }
336
-
337
- res.end(JSON.stringify(allSensorDevices));
338
- })
339
- .catch(error => {
340
- res.send(500).send(error.stack);
341
- });
342
- });
343
706
 
344
- //
345
- // DISCOVER LIGHTS
346
- RED.httpAdmin.get('/hue/lights', function(req, res, next)
347
- {
348
- let huejay = require('huejay');
349
- var bridge = (req.query.bridge).toString();
350
- var username = req.query.key;
707
+ // CONVERT TO ARRAY
708
+ targetRules = Object.values(targetRules);
351
709
 
352
- let client = new huejay.Client({
353
- host: bridge,
354
- username: username
355
- });
356
-
357
- client.lights.getAll()
358
- .then(lights => {
359
- var allLights = [];
360
-
361
- for (let light of lights)
362
- {
363
- var oneLightBulb = {};
364
- oneLightBulb.id = light.id;
365
- oneLightBulb.name = light.name;
366
-
367
- allLights.push(oneLightBulb);
368
- }
369
-
370
- res.end(JSON.stringify(allLights));
371
- })
372
- .catch(error => {
373
- res.send(500).send(error.stack);
374
- });
375
- });
376
-
377
- //
378
- // DISCOVER GROUPS
379
- RED.httpAdmin.get('/hue/groups', function(req, res, next)
380
- {
381
- let huejay = require('huejay');
382
- var bridge = (req.query.bridge).toString();
383
- var username = req.query.key;
384
-
385
- let client = new huejay.Client({
386
- host: bridge,
387
- username: username
388
- });
389
-
390
- client.groups.getAll()
391
- .then(groups => {
392
- var allGroups = [];
393
-
394
- 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)
395
725
  {
396
- var oneGroup = {};
397
- oneGroup.id = group.id;
398
- oneGroup.name = group.name;
399
-
400
- allGroups.push(oneGroup);
401
- }
402
-
403
- res.end(JSON.stringify(allGroups));
404
- })
405
- .catch(error => {
406
- res.send(500).send(error.stack);
407
- });
408
- });
409
-
410
- //
411
- // DISCOVER SCENES
412
- RED.httpAdmin.get('/hue/scenes', function(req, res, next)
413
- {
414
- let huejay = require('huejay');
415
- var bridge = (req.query.bridge).toString();
416
- var username = req.query.key;
417
-
418
- let client = new huejay.Client({
419
- host: bridge,
420
- username: username
421
- });
422
-
423
- client.scenes.getAll()
424
- .then(scenes => {
425
- var allScenes = [];
426
-
427
- for (let scene of scenes)
726
+ return API.processRessources(allRessources);
727
+ })
728
+ .then(function(processedRessources)
428
729
  {
429
- var oneScene = {};
430
- oneScene.id = scene.id;
431
- oneScene.name = scene.name;
432
-
433
- allScenes.push(oneScene);
434
- }
730
+ let targetDevices = {};
435
731
 
436
- res.end(JSON.stringify(allScenes));
437
- })
438
- .catch(error => {
439
- res.send(500).send(error.stack);
440
- });
441
- });
732
+ for (const [id, ressource] of Object.entries(processedRessources))
733
+ {
734
+ const isGroup = (ressource["type"] == "room" || ressource["type"] == "zone" || ressource["type"] == "bridge_home");
442
735
 
443
- //
444
- // DISCOVER RULES
445
- RED.httpAdmin.get('/hue/rules', function(req, res, next)
446
- {
447
- let huejay = require('huejay');
448
- var bridge = (req.query.bridge).toString();
449
- 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;
450
745
 
451
- let client = new huejay.Client({
452
- host: bridge,
453
- username: username
454
- });
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"];
455
758
 
456
- client.rules.getAll()
457
- .then(rules => {
458
- 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;
459
769
 
460
- for (let rule of rules)
461
- {
462
- var oneRule = {};
463
- oneRule.id = rule.id;
464
- oneRule.name = rule.name;
770
+ targetDevices[id] = oneDevice;
771
+ }
772
+ }
465
773
 
466
- allRules.push(oneRule);
467
- }
774
+ // CONVERT TO ARRAY
775
+ targetDevices = Object.values(targetDevices);
468
776
 
469
- res.end(JSON.stringify(allRules));
470
- })
471
- .catch(error => {
472
- res.send(500).send(error.stack);
473
- });
777
+ // GIVE BACK
778
+ res.end(JSON.stringify(targetDevices));
779
+ })
780
+ .catch(function(error) {
781
+ res.status(500).send(JSON.stringify(error));
782
+ });
783
+ }
474
784
  });
475
- };
785
+ };