@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.
- package/LICENSE +674 -0
- package/README.md +62 -43
- package/api.html +22 -0
- package/api.js +176 -0
- package/examples/Basic usage.json +316 -0
- package/icons/homekit-logo.png +0 -0
- package/icons/icon-color.png +0 -0
- package/icons/icon.png +0 -0
- package/icons/icon.pxd/QuickLook/Icon.tiff +0 -0
- package/icons/icon.pxd/QuickLook/Thumbnail.tiff +0 -0
- package/icons/icon.pxd/data/15644CBE-CB2C-4054-AAB1-064FBC3F72DB +0 -0
- package/icons/icon.pxd/data/2D35D9CD-316A-4D5B-A1D9-1C09DB37FAC9 +0 -0
- package/icons/icon.pxd/data/BF7855C2-D304-4F39-84A3-141656F5E18C +0 -0
- package/icons/icon.pxd/data/selection/meta +33 -0
- package/icons/icon.pxd/data/selection/shapeSelection/meta +12 -0
- package/icons/icon.pxd/data/selection/shapeSelection/path +0 -0
- package/icons/icon.pxd/data/selectionForContentTransform/meta +33 -0
- package/icons/icon.pxd/data/selectionForContentTransform/shapeSelection/meta +12 -0
- package/icons/icon.pxd/data/selectionForContentTransform/shapeSelection/path +0 -0
- package/icons/icon.pxd/metadata.info +0 -0
- package/nodes/bridge.html +523 -0
- package/nodes/bridge.js +124 -0
- package/nodes/get.html +106 -0
- package/nodes/get.js +47 -0
- package/nodes/in.html +125 -0
- package/nodes/in.js +123 -0
- package/nodes/locales/en-US/bridge.html +23 -0
- package/nodes/locales/en-US/bridge.json +84 -0
- package/nodes/locales/en-US/get.html +19 -0
- package/nodes/locales/en-US/get.json +23 -0
- package/nodes/locales/en-US/in.html +24 -0
- package/nodes/locales/en-US/in.json +43 -0
- package/nodes/locales/en-US/out.html +24 -0
- package/nodes/locales/en-US/out.json +31 -0
- package/nodes/locales/en-US/server.html +12 -0
- package/nodes/locales/en-US/server.json +36 -0
- package/nodes/locales/sk-SK/bridge.html +23 -0
- package/nodes/locales/sk-SK/bridge.json +84 -0
- package/nodes/locales/sk-SK/get.html +18 -0
- package/nodes/locales/sk-SK/get.json +21 -0
- package/nodes/locales/sk-SK/in.html +23 -0
- package/nodes/locales/sk-SK/in.json +43 -0
- package/nodes/locales/sk-SK/out.html +23 -0
- package/nodes/locales/sk-SK/out.json +21 -0
- package/nodes/locales/sk-SK/server.html +12 -0
- package/nodes/locales/sk-SK/server.json +36 -0
- package/nodes/out.html +300 -0
- package/nodes/out.js +361 -0
- package/nodes/server.html +96 -0
- package/nodes/server.js +854 -0
- package/package.json +42 -6
- package/readme/1.png +0 -0
- package/readme/2.png +0 -0
- package/readme/3.png +0 -0
- package/readme/4.png +0 -0
- package/readme/5.png +0 -0
- package/readme/options.gif +0 -0
- package/resources/Zigbee2mqttHelper.js +470 -0
- package/resources/css/common.css +56 -0
- package/resources/css/multiple-select.css +183 -0
- package/resources/js/multiple-select.js +10 -0
- package/resources/js/multiple-select.min.js +10 -0
- package/resources/js/node-red-contrib-zigbee2mqtt-helpers.js +416 -0
- package/resources/js/zigbee2mqtt-helpers.js +416 -0
- package/resources/tokeninput/jquery.tokeninput.js +861 -0
- package/resources/tokeninput/token-input-facebook.css +134 -0
- package/resources/tokeninput/token-input-mac.css +204 -0
- package/resources/tokeninput/token-input.css +113 -0
package/nodes/server.js
ADDED
|
@@ -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
|
+
|