node-red-contrib-huemagic 2.8.6 → 4.0.2

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 (170) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +807 -766
  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 -320
  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 -368
  65. package/huemagic/hue-light.html +133 -59
  66. package/huemagic/hue-light.js +551 -337
  67. package/huemagic/hue-magic.html +91 -240
  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-button.json → hue-buttons.json} +8 -8
  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-button.json → hue-buttons.json} +11 -5
  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 +11 -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-button.html +0 -119
  150. package/huemagic/hue-button.js +0 -94
  151. package/huemagic/hue-switch.html +0 -119
  152. package/huemagic/hue-switch.js +0 -113
  153. package/huemagic/hue-tap.html +0 -119
  154. package/huemagic/hue-tap.js +0 -74
  155. package/huemagic/icons/hue-button.png +0 -0
  156. package/huemagic/icons/hue-switch.png +0 -0
  157. package/huemagic/icons/hue-tap.png +0 -0
  158. package/huemagic/locales/de/hue-button.html +0 -50
  159. package/huemagic/locales/de/hue-switch.html +0 -50
  160. package/huemagic/locales/de/hue-switch.json +0 -31
  161. package/huemagic/locales/de/hue-tap.html +0 -41
  162. package/huemagic/locales/de/hue-tap.json +0 -23
  163. package/huemagic/locales/en-US/hue-button.html +0 -50
  164. package/huemagic/locales/en-US/hue-switch.html +0 -50
  165. package/huemagic/locales/en-US/hue-switch.json +0 -31
  166. package/huemagic/locales/en-US/hue-tap.html +0 -41
  167. package/huemagic/locales/en-US/hue-tap.json +0 -23
  168. package/utils/color.js +0 -9
  169. package/utils/messages.js +0 -658
  170. 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,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
706
 
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;
707
+ // CONVERT TO ARRAY
708
+ targetRules = Object.values(targetRules);
352
709
 
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)
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
+ };