@evilbunny/node-red-contrib-zigbee2mqtt 0.0.0 → 3.2.8

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 (68) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +62 -43
  3. package/api.html +22 -0
  4. package/api.js +176 -0
  5. package/examples/Basic usage.json +316 -0
  6. package/icons/homekit-logo.png +0 -0
  7. package/icons/icon-color.png +0 -0
  8. package/icons/icon.png +0 -0
  9. package/icons/icon.pxd/QuickLook/Icon.tiff +0 -0
  10. package/icons/icon.pxd/QuickLook/Thumbnail.tiff +0 -0
  11. package/icons/icon.pxd/data/15644CBE-CB2C-4054-AAB1-064FBC3F72DB +0 -0
  12. package/icons/icon.pxd/data/2D35D9CD-316A-4D5B-A1D9-1C09DB37FAC9 +0 -0
  13. package/icons/icon.pxd/data/BF7855C2-D304-4F39-84A3-141656F5E18C +0 -0
  14. package/icons/icon.pxd/data/selection/meta +33 -0
  15. package/icons/icon.pxd/data/selection/shapeSelection/meta +12 -0
  16. package/icons/icon.pxd/data/selection/shapeSelection/path +0 -0
  17. package/icons/icon.pxd/data/selectionForContentTransform/meta +33 -0
  18. package/icons/icon.pxd/data/selectionForContentTransform/shapeSelection/meta +12 -0
  19. package/icons/icon.pxd/data/selectionForContentTransform/shapeSelection/path +0 -0
  20. package/icons/icon.pxd/metadata.info +0 -0
  21. package/nodes/bridge.html +523 -0
  22. package/nodes/bridge.js +124 -0
  23. package/nodes/get.html +106 -0
  24. package/nodes/get.js +47 -0
  25. package/nodes/in.html +125 -0
  26. package/nodes/in.js +123 -0
  27. package/nodes/locales/en-US/bridge.html +23 -0
  28. package/nodes/locales/en-US/bridge.json +84 -0
  29. package/nodes/locales/en-US/get.html +19 -0
  30. package/nodes/locales/en-US/get.json +23 -0
  31. package/nodes/locales/en-US/in.html +24 -0
  32. package/nodes/locales/en-US/in.json +43 -0
  33. package/nodes/locales/en-US/out.html +24 -0
  34. package/nodes/locales/en-US/out.json +31 -0
  35. package/nodes/locales/en-US/server.html +12 -0
  36. package/nodes/locales/en-US/server.json +36 -0
  37. package/nodes/locales/sk-SK/bridge.html +23 -0
  38. package/nodes/locales/sk-SK/bridge.json +84 -0
  39. package/nodes/locales/sk-SK/get.html +18 -0
  40. package/nodes/locales/sk-SK/get.json +21 -0
  41. package/nodes/locales/sk-SK/in.html +23 -0
  42. package/nodes/locales/sk-SK/in.json +43 -0
  43. package/nodes/locales/sk-SK/out.html +23 -0
  44. package/nodes/locales/sk-SK/out.json +21 -0
  45. package/nodes/locales/sk-SK/server.html +12 -0
  46. package/nodes/locales/sk-SK/server.json +36 -0
  47. package/nodes/out.html +300 -0
  48. package/nodes/out.js +361 -0
  49. package/nodes/server.html +96 -0
  50. package/nodes/server.js +854 -0
  51. package/package.json +42 -6
  52. package/readme/1.png +0 -0
  53. package/readme/2.png +0 -0
  54. package/readme/3.png +0 -0
  55. package/readme/4.png +0 -0
  56. package/readme/5.png +0 -0
  57. package/readme/options.gif +0 -0
  58. package/resources/Zigbee2mqttHelper.js +470 -0
  59. package/resources/css/common.css +56 -0
  60. package/resources/css/multiple-select.css +183 -0
  61. package/resources/js/multiple-select.js +10 -0
  62. package/resources/js/multiple-select.min.js +10 -0
  63. package/resources/js/node-red-contrib-zigbee2mqtt-helpers.js +416 -0
  64. package/resources/js/zigbee2mqtt-helpers.js +416 -0
  65. package/resources/tokeninput/jquery.tokeninput.js +861 -0
  66. package/resources/tokeninput/token-input-facebook.css +134 -0
  67. package/resources/tokeninput/token-input-mac.css +204 -0
  68. package/resources/tokeninput/token-input.css +113 -0
@@ -0,0 +1,854 @@
1
+ const Zigbee2mqttHelper = require('../resources/Zigbee2mqttHelper.js');
2
+ var mqtt = require('mqtt');
3
+ var Viz = require('@viz-js/viz');
4
+ var vizInstancePromise = Viz.instance();
5
+
6
+ module.exports = function(RED) {
7
+ class ServerNode {
8
+ constructor(n) {
9
+ RED.nodes.createNode(this, n);
10
+ var node = this;
11
+ node.config = n;
12
+ node.connection = false;
13
+ node.items = undefined;
14
+ node.groups = undefined;
15
+ node.devices = undefined;
16
+ node.devices_values = {};
17
+ node.avaialability = {};
18
+ node.bridge_info = null;
19
+ node.bridge_state = null;
20
+ node.map = null;
21
+ node.on('close', () => this.onClose());
22
+ node.setMaxListeners(0);
23
+
24
+ //mqtt
25
+ node.mqtt = node.connectMQTT();
26
+ node.mqtt.on('connect', () => this.onMQTTConnect());
27
+ node.mqtt.on('message', (topic, message) => this.onMQTTMessage(topic, message));
28
+
29
+ node.mqtt.on('close', () => this.onMQTTClose());
30
+ node.mqtt.on('end', () => this.onMQTTEnd());
31
+ node.mqtt.on('reconnect', () => this.onMQTTReconnect());
32
+ node.mqtt.on('offline', () => this.onMQTTOffline());
33
+ node.mqtt.on('disconnect', (error) => this.onMQTTDisconnect(error));
34
+ node.mqtt.on('error', (error) => this.onMQTTError(error));
35
+
36
+ // console.log(node.config._users);
37
+ }
38
+
39
+ connectMQTT(clientId = null) {
40
+ var node = this;
41
+ var options = {
42
+ port: node.config.mqtt_port || 1883,
43
+ username: node.config.mqtt_username || null,
44
+ password: node.config.mqtt_password || null,
45
+ clientId: 'NodeRed-' + node.id + '-' + (clientId ? clientId : (Math.random() + 1).toString(36).substring(7)),
46
+ };
47
+
48
+ let baseUrl = 'mqtt://';
49
+
50
+ var tlsNode = RED.nodes.getNode(node.config.tls);
51
+ if (node.config.usetls && tlsNode) {
52
+ tlsNode.addTLSOptions(options);
53
+ baseUrl = 'mqtts://';
54
+ }
55
+
56
+ return mqtt.connect(baseUrl + node.config.host, options);
57
+ }
58
+
59
+ subscribeMQTT() {
60
+ var node = this;
61
+ node.mqtt.subscribe(node.getTopic('/#'), {'qos':parseInt(node.config.mqtt_qos||0)}, function(err) {
62
+ if (err) {
63
+ node.warn('MQTT Error: Subscribe to "' + node.getTopic('/#'));
64
+ node.emit('onConnectError', err);
65
+ } else {
66
+ node.log('MQTT Subscribed to: "' + node.getTopic('/#'));
67
+ }
68
+ });
69
+ }
70
+
71
+ unsubscribeMQTT() {
72
+ var node = this;
73
+ node.log('MQTT Unsubscribe from mqtt topic: ' + node.getTopic('/#'));
74
+ node.mqtt.unsubscribe(node.getTopic('/#'), function(err) {});
75
+ node.devices_values = {};
76
+ }
77
+
78
+ getDevices(callback, withGroups = false) {
79
+ if (typeof (callback) !== 'function') {
80
+ return
81
+ }
82
+ let node = this;
83
+ if (node.devices && (!withGroups || node.groups)) {
84
+ node.log('Using cached devices')
85
+ callback(withGroups ? [node.devices, node.groups] : node.devices);
86
+ } else {
87
+ node.log('Waiting for device list')
88
+ let timeout = null
89
+ let checkAvailability = null
90
+ new Promise(function(resolve) {
91
+ timeout = setTimeout(function() {
92
+ resolve()
93
+ }, 60_000);
94
+ checkAvailability = function() {
95
+ if (node.devices && (!withGroups || node.groups)) {
96
+ resolve()
97
+ }
98
+ }
99
+ node.on('onMQTTMessageBridge', checkAvailability)
100
+ }).then(function() {
101
+ clearTimeout(timeout)
102
+ node.removeListener('onMQTTMessageBridge', checkAvailability);
103
+ if (node.devices && (!withGroups || node.groups)) {
104
+ callback(withGroups ? [node.devices, node.groups] : node.devices);
105
+ } else {
106
+ node.error('Error: getDevices timeout')
107
+ callback(null)
108
+ }
109
+ })
110
+ }
111
+ }
112
+
113
+
114
+ _getItemByKey(key, varName) {
115
+ var node = this;
116
+ var result = null;
117
+ for (var i in node[varName]) {
118
+ if (key === node.getTopic('/' + node[varName][i]['friendly_name'])
119
+ || key === node.getTopic('/' + node[varName][i]['ieee_address'])
120
+ || key === node[varName][i]['friendly_name']
121
+ || key === node[varName][i]['ieee_address']
122
+ || ('id' in node[varName][i] && parseInt(key) === parseInt(node[varName][i]['id']))
123
+ || key === Zigbee2mqttHelper.generateSelector(node.getTopic('/' + node[varName][i]['friendly_name']))
124
+ ) {
125
+ result = node[varName][i];
126
+ result['current_values'] = null;
127
+ result['homekit'] = null;
128
+ result['format'] = null;
129
+
130
+ let topic = node.getTopic('/' + (node[varName][i]['friendly_name'] ? node[varName][i]['friendly_name'] : node[varName][i]['ieee_address']));
131
+ if (topic in node.devices_values) {
132
+ result['current_values'] = node.devices_values[topic];
133
+ result['homekit'] = Zigbee2mqttHelper.payload2homekit(node.devices_values[topic]);
134
+ result['format'] = Zigbee2mqttHelper.formatPayload(node.devices_values[topic], node[varName][i]);
135
+ }
136
+ break;
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+
142
+ getDeviceOrGroupByKey(key) {
143
+ let device = this.getDeviceByKey(key);
144
+ if (device) {
145
+ return device;
146
+ }
147
+ let group = this.getGroupByKey(key);
148
+ if (group) {
149
+ return group;
150
+ }
151
+
152
+ return null;
153
+ }
154
+
155
+ getDeviceByKey(key) {
156
+ return this._getItemByKey(key, 'devices');
157
+ }
158
+
159
+ getGroupByKey(key) {
160
+ return this._getItemByKey(key, 'groups');
161
+ }
162
+
163
+ getDeviceAvailabilityColor(topic) {
164
+ let color = 'blue';
165
+ if (topic in this.avaialability) {
166
+ color = this.avaialability[topic]?'green':'red';
167
+ }
168
+ return color;
169
+ }
170
+
171
+ getBaseTopic() {
172
+ return this.config.base_topic;
173
+ }
174
+
175
+ getTopic(path) {
176
+ return this.getBaseTopic() + path;
177
+ }
178
+
179
+ restart() {
180
+ let node = this;
181
+ node.mqtt.publish(node.getTopic('/bridge/request/restart'));
182
+ node.log('Restarting zigbee2mqtt...');
183
+ }
184
+
185
+ setLogLevel(val) {
186
+ let node = this;
187
+ if (['info', 'debug', 'warning', 'error'].indexOf(val) < 0) val = 'info';
188
+ let payload = {
189
+ 'options': {
190
+ 'advanced': {
191
+ 'log_level': val,
192
+ },
193
+ },
194
+ };
195
+ node.mqtt.publish(node.getTopic('/bridge/request/options'), JSON.stringify(payload));
196
+ node.log('Log Level was set to: ' + val);
197
+ }
198
+
199
+ setPermitJoin(val) {
200
+ let node = this;
201
+ let payload = {
202
+ 'options': {
203
+ 'permit_join': val,
204
+ },
205
+ };
206
+ node.mqtt.publish(node.getTopic('/bridge/request/options'), JSON.stringify(payload));
207
+ node.log('Permit Join was set to: ' + val);
208
+ }
209
+
210
+ renameDevice(ieee_address, newName) {
211
+ let node = this;
212
+
213
+ let device = node.getDeviceByKey(ieee_address);
214
+ if (!device) {
215
+ return {'error': true, 'description': 'no such device'};
216
+ }
217
+
218
+ if (!newName.length) {
219
+ return {'error': true, 'description': 'can not be empty'};
220
+ }
221
+
222
+ let payload = {
223
+ 'from': device.friendly_name, 'to': newName,
224
+ };
225
+
226
+ node.mqtt.publish(node.getTopic('/bridge/request/device/rename'), JSON.stringify(payload));
227
+ node.log('Rename device ' + ieee_address + ' to ' + newName);
228
+
229
+ return {'success': true, 'description': 'command sent'};
230
+ }
231
+
232
+ removeDevice(ieee_address) {
233
+ let node = this;
234
+
235
+ let device = node.getDeviceByKey(ieee_address);
236
+ if (!device) {
237
+ return {'error': true, 'description': 'no such device'};
238
+ }
239
+ let payload = {
240
+ 'id': ieee_address,
241
+ 'force': true
242
+ };
243
+
244
+ node.mqtt.publish(node.getTopic('/bridge/device/remove'), JSON.stringify(payload));
245
+ node.log('Remove device: ' + device.friendly_name);
246
+
247
+ return {'success': true, 'description': 'command sent'};
248
+ }
249
+
250
+ setDeviceOptions(ieee_address, options) {
251
+ let node = this;
252
+ let device = node.getDeviceByKey(ieee_address);
253
+ if (!device) {
254
+ return {'error': true, 'description': 'no such device'};
255
+ }
256
+
257
+ let payload = {
258
+ 'id': ieee_address,
259
+ 'options': options
260
+ };
261
+
262
+ node.mqtt.publish(node.getTopic('/bridge/request/device/options'), JSON.stringify(payload));
263
+ node.log('Set device options for "'+device.friendly_name+'" : '+JSON.stringify(payload));
264
+
265
+ return {"success":true,"description":"command sent"};
266
+ }
267
+
268
+ renameGroup(id, newName) {
269
+ let node = this;
270
+
271
+ let group = node.getGroupByKey(id);
272
+ if (!group) {
273
+ return {'error': true, 'description': 'no such group'};
274
+ }
275
+
276
+ if (!newName.length) {
277
+ return {'error': true, 'description': 'can not be empty'};
278
+ }
279
+
280
+ let payload = {
281
+ 'from': group.friendly_name, 'to': newName,
282
+ };
283
+ node.mqtt.publish(node.getTopic('/bridge/request/group/rename'), JSON.stringify(payload));
284
+ node.log('Rename group ' + id + ' to ' + newName);
285
+
286
+ return {'success': true, 'description': 'command sent'};
287
+ }
288
+
289
+ removeGroup(id) {
290
+ let node = this;
291
+
292
+ let group = node.getGroupByKey(id);
293
+ if (!group) {
294
+ return {'error': true, 'description': 'no such group'};
295
+ }
296
+
297
+ let payload = {
298
+ 'id': id,
299
+ };
300
+ node.mqtt.publish(node.getTopic('/bridge/request/group/remove'), JSON.stringify(payload));
301
+ node.log('Remove group: ' + group.friendly_name);
302
+
303
+ return {'success': true, 'description': 'command sent'};
304
+ }
305
+
306
+ addGroup(name) {
307
+ let node = this;
308
+ let payload = {
309
+ 'friendly_name': name,
310
+ };
311
+ node.mqtt.publish(node.getTopic('/bridge/request/group/add'), JSON.stringify(payload));
312
+ node.log('Add group: ' + name);
313
+
314
+ return {'success': true, 'description': 'command sent'};
315
+ }
316
+
317
+ removeDeviceFromGroup(deviceId, groupId) {
318
+ let node = this;
319
+
320
+ let device = node.getDeviceByKey(deviceId);
321
+ if (!device) {
322
+ device = {'friendly_name': deviceId};
323
+ }
324
+
325
+ let group = node.getGroupByKey(groupId);
326
+ if (!group) {
327
+ return {'error': true, 'description': 'no such group'};
328
+ }
329
+ let payload = {
330
+ 'group': groupId, 'device': deviceId,
331
+ };
332
+ node.mqtt.publish(node.getTopic('/bridge/request/group/members/remove'), JSON.stringify(payload));
333
+ node.log('Removing device: ' + device.friendly_name + ' from group: ' + group.friendly_name);
334
+
335
+ return {'success': true, 'description': 'command sent'};
336
+ }
337
+
338
+ addDeviceToGroup(deviceId, groupId) {
339
+ let node = this;
340
+
341
+ let device = node.getDeviceByKey(deviceId);
342
+ if (!device) {
343
+ return {'error': true, 'description': 'no such device'};
344
+ }
345
+
346
+ let group = node.getGroupByKey(groupId);
347
+ if (!group) {
348
+ return {'error': true, 'description': 'no such group'};
349
+ }
350
+ let payload = {
351
+ 'group': groupId,
352
+ 'device': deviceId,
353
+ };
354
+ node.mqtt.publish(node.getTopic('/bridge/request/group/members/add'), JSON.stringify(payload));
355
+ node.log('Adding device: ' + device.friendly_name + ' to group: ' + group.friendly_name);
356
+
357
+ return {'success': true, 'description': 'command sent'};
358
+ }
359
+
360
+ refreshMap(wait = false, engine = null) {
361
+ var node = this;
362
+
363
+ return new Promise(function(resolve, reject) {
364
+ if (wait) {
365
+ var timeout = null;
366
+ var timeout_ms = 60000 * 5;
367
+
368
+ var client = node.connectMQTT('tmp');
369
+ client.on('connect', function() {
370
+
371
+ //end function after timeout, if now response
372
+ timeout = setTimeout(function() {
373
+ client.end(true);
374
+ }, timeout_ms);
375
+ client.subscribe(node.getTopic('/bridge/response/networkmap'), function(err) {
376
+ if (!err) {
377
+ client.publish(node.getTopic('/bridge/request/networkmap'), JSON.stringify({'type': 'graphviz', 'routes': false}));
378
+
379
+ node.log('Refreshing map and waiting...');
380
+ } else {
381
+ RED.log.error('zigbee2mqtt: error code #0023: ' + err);
382
+ client.end(true);
383
+ reject({'success': false, 'description': 'zigbee2mqtt: error code #0023'});
384
+ }
385
+ });
386
+ });
387
+
388
+ client.on('error', function(error) {
389
+ RED.log.error('zigbee2mqtt: error code #0024: ' + error);
390
+ client.end(true);
391
+ reject({'success': false, 'description': 'zigbee2mqtt: error code #0024'});
392
+ });
393
+
394
+ client.on('end', function(error, s) {
395
+ clearTimeout(timeout);
396
+ });
397
+
398
+ client.on('message', function(topic, message) {
399
+ if (node.getTopic('/bridge/response/networkmap') === topic) {
400
+
401
+ var messageString = message.toString();
402
+ node.graphviz(JSON.parse(messageString).data.value, engine).then(function(data) {
403
+ resolve({'success': true, 'svg': node.map});
404
+ }).catch(error => {
405
+ reject({'success': false, 'description': 'graphviz failed'});
406
+ });
407
+ client.end(true);
408
+ }
409
+ });
410
+ } else {
411
+ node.mqtt.publish(node.getTopic('/bridge/request/networkmap'), JSON.stringify({'type': 'graphviz', 'routes': false}));
412
+ node.log('Refreshing map...');
413
+
414
+ resolve({'success': true, 'svg': node.map});
415
+ }
416
+ });
417
+ }
418
+
419
+ async graphviz(payload, engine = null) {
420
+ var node = this;
421
+ var options = {
422
+ format: 'svg', engine: engine ? engine : 'circo',
423
+ };
424
+ var viz = await vizInstancePromise;
425
+ return node.map = await viz.renderString(payload, options);
426
+ }
427
+
428
+ nodeSend(node, opts) {
429
+ if (node.config.enableMultiple) {
430
+ this.nodeSendMultiple(node, opts)
431
+ } else {
432
+ this.nodeSendSingle(node, opts)
433
+ }
434
+ }
435
+
436
+ nodeSendSingle(node, opts) {
437
+ clearTimeout(node.cleanTimer);
438
+
439
+ opts = Object.assign({
440
+ 'node_send':true,
441
+ 'key':node.config.device_id,
442
+ 'msg': {},
443
+ 'filter': false //skip the same payload, send only changes
444
+ }, opts);
445
+
446
+ let msg = opts.msg;
447
+ let payload = null;
448
+ let payload_all = null;
449
+ let text = RED._("@evilbunny/node-red-contrib-zigbee2mqtt/server:status.received");
450
+ let item = this.getDeviceOrGroupByKey(opts.key);
451
+
452
+ if (item) {
453
+ payload_all = item.current_values;
454
+ if (payload_all == null) {
455
+ node.warn('You need to turn on the "retain" option for the device in Zigbee2MQTT to be able to read it before a state change.')
456
+ }
457
+ } else {
458
+ node.status({
459
+ fill: "red",
460
+ shape: "dot",
461
+ text: "@evilbunny/node-red-contrib-zigbee2mqtt/server:status.no_device"
462
+ });
463
+ return;
464
+ }
465
+
466
+ let useProperty = null;
467
+ if (node.config.state && node.config.state !== '0') {
468
+ if (item.homekit && node.config.state.split("homekit_").join('') in item.homekit) {
469
+ payload = item.homekit[node.config.state.split("homekit_").join('')];
470
+ useProperty = node.config.state.split("homekit_").join('');
471
+ } else if (payload_all && node.config.state in payload_all) {
472
+ payload = text = payload_all[node.config.state];
473
+ useProperty = node.config.state;
474
+ } else {
475
+ //state was not found in payload (button case)
476
+ //payload: { last_seen: '2022-07-27T15:25:22+03:00', linkquality: 36 }
477
+ //payload: { action: 'single', last_seen: '2022-07-27T15:25:22+03:00', linkquality: 36 }
478
+ return;
479
+ }
480
+ } else {
481
+ payload = payload_all;
482
+ }
483
+
484
+
485
+ //add unit
486
+ if (useProperty) {
487
+ try {
488
+ for (let ind in item.definition.exposes) {
489
+ if ('features' in item.definition.exposes) { //why did they add it... what a crap!?
490
+ for (let featureInd in item.definition.exposes.features) {
491
+ if (item.definition.exposes[ind]['features'][featureInd]['property'] == useProperty && 'unit' in item.definition.exposes[ind]['features'][featureInd]) {
492
+ text += ' ' + item.definition.exposes[ind]['unit'];
493
+ }
494
+ }
495
+ } else {
496
+ if (item.definition.exposes[ind]['property'] == useProperty && 'unit' in item.definition.exposes[ind]) {
497
+ text += ' ' + item.definition.exposes[ind]['unit'];
498
+ }
499
+ }
500
+ }
501
+ } catch (e) {}
502
+ }
503
+
504
+ if ('firstMsg' in node && node.firstMsg) {
505
+ node.firstMsg = false;
506
+
507
+ if (opts.node_send && 'outputAtStartup' in node.config && !node.config.outputAtStartup) {
508
+ // console.log('Skipped first value');
509
+ node.last_value = payload;
510
+ return;
511
+ }
512
+ }
513
+
514
+ if (opts.filter) {
515
+ if (opts.node_send && JSON.stringify(node.last_value) === JSON.stringify(payload)) {
516
+ // console.log('Filtered the same value');
517
+ return;
518
+ }
519
+ }
520
+
521
+ if (item && "power_source" in item && 'Battery' === item.power_source && payload_all && "battery" in payload_all && parseInt(payload_all.battery) > 0) {
522
+ text += ' ⚡' + payload_all.battery + '%';
523
+ }
524
+
525
+ msg.topic = this.getTopic('/'+item.friendly_name);
526
+ if ("payload" in msg) {
527
+ msg.payload_in = msg.payload;
528
+ }
529
+ if ('last_value' in node) {
530
+ msg.changed = {
531
+ 'old': node.last_value,
532
+ 'new': payload//,
533
+ // 'diff': Zigbee2mqttHelper.objectsDiff(node.last_value, payload)
534
+ };
535
+ }
536
+ msg.payload = payload;
537
+ msg.payload_raw = payload_all;
538
+ msg.homekit = item.homekit;
539
+ msg.format = item.format;
540
+ msg.selector = Zigbee2mqttHelper.generateSelector(msg.topic);
541
+ msg.item = item;
542
+ if (opts.node_send) {
543
+ // console.log('SEND:');
544
+ // console.log(payload);
545
+ node.send(msg);
546
+ node.last_value = payload;
547
+ }
548
+
549
+ let time = Zigbee2mqttHelper.statusUpdatedAt();
550
+ let fill = this.getDeviceAvailabilityColor(msg.topic);
551
+ let status = {
552
+ fill: fill,
553
+ shape: 'dot',
554
+ text: text
555
+ };
556
+ node.setSuccessfulStatus(status);
557
+
558
+ node.cleanTimer = setTimeout(() => {
559
+ status.text += ' ' + time;
560
+ status.shape = 'ring';
561
+ node.setSuccessfulStatus(status);
562
+ }, 3000);
563
+ }
564
+
565
+ nodeSendMultiple(node, opts) {
566
+ let that = this;
567
+ clearTimeout(node.cleanTimer);
568
+
569
+ opts = Object.assign({
570
+ 'node_send':true,
571
+ 'key':node.config.device_id,
572
+ 'msg': {},
573
+ 'changed': null,
574
+ 'filter': false //skip the same payload, send only changes
575
+ }, opts);
576
+
577
+ let msg = opts.msg;
578
+ let payload = {};
579
+ let math = [];
580
+ let text = RED._("@evilbunny/node-red-contrib-zigbee2mqtt/server:status.received");
581
+
582
+ for (let index in node.config.device_id) {
583
+ let item = that.getDeviceOrGroupByKey(node.config.device_id[index]);
584
+ if (item) {
585
+ let itemData = {};
586
+ itemData.item = item;
587
+ itemData.topic = this.getTopic('/' + item.friendly_name);
588
+ itemData.selector = Zigbee2mqttHelper.generateSelector(itemData.topic);
589
+ itemData.homekit = item.homekit;
590
+ itemData.payload = item.current_values;
591
+ itemData.format = item.format;
592
+
593
+ payload[node.config.device_id[index]] = itemData;
594
+ math.push(itemData.payload);
595
+ }
596
+ }
597
+
598
+ msg.payload_in = ("payload" in msg)?msg.payload:null;
599
+ msg.payload = payload;
600
+ msg.math = Zigbee2mqttHelper.formatMath(math);
601
+ if (opts.changed !== null) {
602
+ msg.changed = opts.changed;
603
+ }
604
+
605
+ if (!Object.keys(msg.payload).length) {
606
+ node.status({
607
+ fill: "red",
608
+ shape: "dot",
609
+ text: "@evilbunny/node-red-contrib-zigbee2mqtt/server:status.no_device"
610
+ });
611
+ return;
612
+ }
613
+
614
+ if ('firstMsg' in node && node.firstMsg) {
615
+ node.firstMsg = false;
616
+
617
+ if (opts.node_send && 'outputAtStartup' in node.config && !node.config.outputAtStartup) {
618
+ // console.log('Skipped first value');
619
+ node.last_value = payload;
620
+ return;
621
+ }
622
+ }
623
+ //
624
+ // if (opts.filter) {
625
+ // if (opts.node_send && JSON.stringify(node.last_value) === JSON.stringify(payload)) {
626
+ // // console.log('Filtered the same value');
627
+ // return;
628
+ // }
629
+ // }
630
+
631
+
632
+ // if ('last_value' in node) {
633
+ // msg.changed = {
634
+ // 'old': node.last_value,
635
+ // 'new': payload//,
636
+ // // 'diff': Zigbee2mqttHelper.objectsDiff(node.last_value, payload)
637
+ // };
638
+ // }
639
+
640
+ if (opts.node_send) {
641
+ // console.log('SEND:');
642
+ // console.log(payload);
643
+ node.send(msg);
644
+ node.last_value = payload;
645
+ }
646
+
647
+ let time = Zigbee2mqttHelper.statusUpdatedAt();
648
+ let status = {
649
+ fill: 'blue',
650
+ shape: 'dot',
651
+ text: text
652
+ };
653
+ node.setSuccessfulStatus(status);
654
+
655
+ node.cleanTimer = setTimeout(() => {
656
+ status.text += ' ' + time;
657
+ status.shape = 'ring';
658
+ node.setSuccessfulStatus(status);
659
+ }, 3000);
660
+ }
661
+
662
+
663
+ onMQTTConnect() {
664
+ var node = this;
665
+ node.connection = true;
666
+ node.log('MQTT Connected');
667
+ node.emit('onMQTTConnect');
668
+ node.subscribeMQTT();
669
+
670
+ }
671
+
672
+ onMQTTDisconnect(error) {
673
+ var node = this;
674
+ // node.connection = true;
675
+ node.log('MQTT Disconnected');
676
+ console.log(error);
677
+
678
+ }
679
+
680
+ onMQTTError(error) {
681
+ var node = this;
682
+ // node.connection = true;
683
+ node.log('MQTT Error');
684
+ console.log(error);
685
+
686
+ }
687
+
688
+ onMQTTOffline() {
689
+ let node = this;
690
+ // node.connection = true;
691
+ node.warn('MQTT Offline');
692
+ }
693
+
694
+ onMQTTEnd() {
695
+ var node = this;
696
+ // node.connection = true;
697
+ node.log('MQTT End');
698
+ // console.log();
699
+
700
+ }
701
+
702
+ onMQTTReconnect() {
703
+ var node = this;
704
+ // node.connection = true;
705
+ node.log('MQTT Reconnect');
706
+ // console.log();
707
+
708
+ }
709
+
710
+ onMQTTClose() {
711
+ var node = this;
712
+ // node.connection = true;
713
+ node.log('MQTT Close');
714
+ // console.log(node.connection);
715
+
716
+ }
717
+
718
+ onMQTTMessage(topic, message) {
719
+ var node = this;
720
+ var messageString = message.toString();
721
+ // console.log(topic);
722
+ // console.log(messageString);
723
+ //bridge
724
+ if (topic.includes('/bridge/')) {
725
+ if (node.getTopic('/bridge/devices') === topic) {
726
+ if (Zigbee2mqttHelper.isJson(messageString)) {
727
+ node.devices = JSON.parse(messageString);
728
+ for (let ind in node.devices) {
729
+ let topic = node.getTopic('/' + (node.devices[ind]['friendly_name'] ? node.devices[ind]['friendly_name'] : node.devices[ind]['ieee_address']));
730
+ if (topic in node.devices_values) {
731
+ // getDeviceOrGroupByKey will add up-to-date information from node.device_values
732
+ } else {
733
+ // force get data
734
+ // definition.exposes[].access has to be 0b1xx to support get
735
+ // special devices, like "Coordinator", don't have a definition
736
+ // Zigbee2MQTT seems to answer with the full state, not just the ones marked with gettable (but if we just send an empty/dummy payload, there won't be an answer)
737
+ if (node.devices[ind].definition) {
738
+ let getPayload = {}
739
+ let isEmpty = true
740
+ for (let exp of node.devices[ind].definition.exposes) {
741
+ if (exp.access && (exp.access & 0b100)) {
742
+ getPayload[exp.name] = ""
743
+ isEmpty = false
744
+ }
745
+ }
746
+ if (!isEmpty) {
747
+ node.mqtt.publish(topic + '/get', JSON.stringify(getPayload))
748
+ }
749
+ }
750
+ }
751
+ }
752
+ }
753
+ } else if (node.getTopic('/bridge/groups') === topic) {
754
+ if (Zigbee2mqttHelper.isJson(messageString)) {
755
+ node.groups = JSON.parse(messageString);
756
+ }
757
+ } else if (node.getTopic('/bridge/state') === topic) {
758
+ let availabilityStatus = false;
759
+ if (Zigbee2mqttHelper.isJson(messageString)) {
760
+ let availabilityStatusObject = JSON.parse(messageString);
761
+ availabilityStatus = 'state' in availabilityStatusObject && availabilityStatusObject.state === 'online';
762
+ } else {
763
+ availabilityStatus = messageString === 'online';
764
+ }
765
+ node.emit('onMQTTBridgeState', {
766
+ topic: topic,
767
+ payload: availabilityStatus,
768
+ });
769
+ if (node.bridge_state !== null || !availabilityStatus) {
770
+ node.warn(`Bridge ${availabilityStatus ? 'online' : 'offline'}`)
771
+ }
772
+ node.bridge_state = availabilityStatus
773
+ } else if (node.getTopic('/bridge/info') === topic) {
774
+ try {
775
+ node.bridge_info = JSON.parse(messageString);
776
+ } catch (error) {
777
+ node.warn("Failed to parse Bridge info JSON:", error);
778
+ node.bridge_info = null;
779
+ }
780
+ }
781
+
782
+ node.emit('onMQTTMessageBridge', {
783
+ topic: topic,
784
+ payload: messageString,
785
+ });
786
+
787
+ } else {
788
+ //ignore set topics
789
+ if (topic.substring(topic.length - 4, topic.length) === '/set') {
790
+ return;
791
+ }
792
+
793
+ //availability
794
+ if (topic.substring(topic.length - 13, topic.length) === '/availability') {
795
+
796
+ let availabilityStatus = null;
797
+ if (Zigbee2mqttHelper.isJson(messageString)) {
798
+ let availabilityStatusObject = JSON.parse(messageString);
799
+ availabilityStatus = 'state' in availabilityStatusObject && availabilityStatusObject.state === 'online';
800
+
801
+ } else {
802
+ availabilityStatus = messageString === 'online';
803
+ }
804
+
805
+ node.avaialability[topic.split('/availability').join('')] = availabilityStatus;
806
+
807
+ node.emit('onMQTTAvailability', {
808
+ topic: topic,
809
+ payload: availabilityStatus,
810
+ item: node.getDeviceOrGroupByKey(topic.split('/availability').join(''))
811
+ });
812
+ return;
813
+ }
814
+
815
+ let payload = null;
816
+ if (Zigbee2mqttHelper.isJson(messageString)) {
817
+ payload = {};
818
+ Object.assign(payload, JSON.parse(messageString)); //clone object for payload output
819
+ } else {
820
+ payload = messageString;
821
+ }
822
+
823
+ // console.log('==========MQTT START')
824
+ // console.log(topic);
825
+ // console.log(payload_json);
826
+ // console.log('==========MQTT END')
827
+ node.devices_values[topic] = payload;
828
+ // node.devices_values[topic] = {
829
+ // 'old':topic in node.devices_values?node.devices_values[topic].new:null,
830
+ // 'new':payload,
831
+ // 'timestamp': new Date().getTime()
832
+ // };
833
+ node.emit('onMQTTMessage', {
834
+ topic: topic,
835
+ payload: payload,
836
+ item: node.getDeviceOrGroupByKey(topic)
837
+ });
838
+
839
+ }
840
+ }
841
+
842
+ onClose() {
843
+ var node = this;
844
+ node.unsubscribeMQTT();
845
+ node.mqtt.end();
846
+ node.connection = false;
847
+ node.emit('onClose');
848
+ node.log('MQTT connection closed');
849
+ }
850
+ }
851
+
852
+ RED.nodes.registerType('zigbee2mqtt-eb-server', ServerNode, {});
853
+ };
854
+