iobroker.zigbee 1.8.17 → 1.8.19
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 +21 -21
- package/README.md +419 -412
- package/admin/adapter-settings.js +244 -244
- package/admin/admin.js +2981 -2981
- package/admin/i18n/de/translations.json +108 -108
- package/admin/i18n/en/translations.json +108 -108
- package/admin/i18n/es/translations.json +102 -102
- package/admin/i18n/fr/translations.json +108 -108
- package/admin/i18n/it/translations.json +102 -102
- package/admin/i18n/nl/translations.json +108 -108
- package/admin/i18n/pl/translations.json +108 -108
- package/admin/i18n/pt/translations.json +102 -102
- package/admin/i18n/ru/translations.json +108 -108
- package/admin/i18n/uk/translations.json +108 -108
- package/admin/i18n/zh-cn/translations.json +102 -102
- package/admin/img/philips_hue_lom001.png +0 -0
- package/admin/index.html +159 -159
- package/admin/index_m.html +1356 -1356
- package/admin/moment.min.js +1 -1
- package/admin/shuffle.min.js +2 -2
- package/admin/tab_m.html +1009 -1009
- package/admin/vis-network.min.css +1 -1
- package/admin/vis-network.min.js +27 -27
- package/admin/words.js +110 -110
- package/docs/de/basedocu.md +19 -19
- package/docs/de/readme.md +126 -126
- package/docs/en/readme.md +128 -128
- package/docs/flashing_via_arduino_(en).md +110 -110
- package/docs/ru/readme.md +28 -28
- package/docs/tutorial/groups-1.png +0 -0
- package/docs/tutorial/groups-2.png +0 -0
- package/docs/tutorial/tab-dev-1.png +0 -0
- package/io-package.json +27 -18
- package/lib/backup.js +171 -171
- package/lib/binding.js +319 -319
- package/lib/colors.js +465 -465
- package/lib/commands.js +534 -534
- package/lib/developer.js +145 -145
- package/lib/devices.js +3135 -3135
- package/lib/exclude.js +162 -162
- package/lib/exposes.js +913 -913
- package/lib/groups.js +345 -345
- package/lib/json.js +59 -59
- package/lib/networkmap.js +55 -55
- package/lib/ota.js +198 -198
- package/lib/rgb.js +297 -297
- package/lib/seriallist.js +48 -48
- package/lib/states.js +6420 -6420
- package/lib/statescontroller.js +672 -672
- package/lib/tools.js +54 -54
- package/lib/utils.js +163 -163
- package/lib/zbBaseExtension.js +36 -36
- package/lib/zbDelayedAction.js +144 -144
- package/lib/zbDeviceAvailability.js +319 -319
- package/lib/zbDeviceConfigure.js +147 -147
- package/lib/zbDeviceEvent.js +48 -48
- package/lib/zigbeecontroller.js +989 -989
- package/main.js +62 -37
- package/package.json +6 -4
- package/support/docgen.js +93 -93
package/lib/zigbeecontroller.js
CHANGED
|
@@ -1,989 +1,989 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const pathLib = require('path');
|
|
4
|
-
const ZigbeeHerdsman = require('zigbee-herdsman');
|
|
5
|
-
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
6
|
-
const EventEmitter = require('events').EventEmitter;
|
|
7
|
-
const safeJsonStringify = require('./json');
|
|
8
|
-
const DeviceAvailabilityExt = require('./zbDeviceAvailability');
|
|
9
|
-
const DeviceConfigureExt = require('./zbDeviceConfigure');
|
|
10
|
-
const DeviceEventExt = require('./zbDeviceEvent');
|
|
11
|
-
const DelayedActionExt = require('./zbDelayedAction');
|
|
12
|
-
const utils = require('./utils');
|
|
13
|
-
const groupConverters = [
|
|
14
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
|
15
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
|
|
16
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
|
|
17
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
|
|
18
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
|
|
19
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
|
|
20
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
|
|
21
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
|
|
22
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
|
|
23
|
-
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_startup
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
function isFunction(functionToCheck) {
|
|
27
|
-
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
class ZigbeeController extends EventEmitter {
|
|
31
|
-
/*
|
|
32
|
-
events:
|
|
33
|
-
|
|
34
|
-
log - log (level, msg, data)
|
|
35
|
-
event - preparsed device events (type, dev, msg, data)
|
|
36
|
-
new - new device connected to network (id, msg)
|
|
37
|
-
leave - device leave the network (id, msg)
|
|
38
|
-
join - join countdown (counter)
|
|
39
|
-
ready - connection successfull ()
|
|
40
|
-
*/
|
|
41
|
-
constructor(adapter) {
|
|
42
|
-
super();
|
|
43
|
-
this.adapter = adapter;
|
|
44
|
-
this._permitJoinTime = 0;
|
|
45
|
-
this.herdsmanStarted = false;
|
|
46
|
-
this.extensions = [
|
|
47
|
-
new DeviceAvailabilityExt(this, {}),
|
|
48
|
-
new DeviceConfigureExt(this, {}),
|
|
49
|
-
new DeviceEventExt(this, {}),
|
|
50
|
-
new DelayedActionExt(this, {}),
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
configure(options) {
|
|
55
|
-
const herdsmanSettings = {
|
|
56
|
-
network: {
|
|
57
|
-
panID: options.net.panId,
|
|
58
|
-
extendedPanID: options.net.extPanId,
|
|
59
|
-
channelList: options.net.channelList,
|
|
60
|
-
networkKey: options.net.precfgkey,
|
|
61
|
-
},
|
|
62
|
-
databasePath: pathLib.join(options.dbDir, options.dbPath),
|
|
63
|
-
backupPath: pathLib.join(options.dbDir, options.backupPath),
|
|
64
|
-
serialPort: {
|
|
65
|
-
baudRate: options.sp.baudRate,
|
|
66
|
-
rtscts: options.sp.rtscts,
|
|
67
|
-
path: options.sp.port,
|
|
68
|
-
adapter: options.sp.adapter,
|
|
69
|
-
},
|
|
70
|
-
adapter: {
|
|
71
|
-
forceStartWithInconsistentAdapterConfiguration: options.startWithInconsistent
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
// https://github.com/ioBroker/ioBroker.zigbee/issues/668
|
|
75
|
-
if (!options.extPanIdFix) {
|
|
76
|
-
delete herdsmanSettings.network.extendedPanID;
|
|
77
|
-
herdsmanSettings.network.extenedPanID = options.net.extPanId;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (options.transmitPower == undefined) {
|
|
81
|
-
this.transmitPower = 0;
|
|
82
|
-
} else {
|
|
83
|
-
this.transmitPower = options.transmitPower;
|
|
84
|
-
}
|
|
85
|
-
this.disableLed = options.disableLed;
|
|
86
|
-
|
|
87
|
-
this.debug(`Using zigbee-herdsman with settings: ${JSON.stringify(herdsmanSettings)}`);
|
|
88
|
-
this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
|
|
89
|
-
this.callExtensionMethod('setOptions', [{
|
|
90
|
-
disableActivePing: options.disablePing,
|
|
91
|
-
disableForcedPing: false,
|
|
92
|
-
pingTimeout: 300,
|
|
93
|
-
pingCount: 3
|
|
94
|
-
}]);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Start controller
|
|
98
|
-
async start() {
|
|
99
|
-
try {
|
|
100
|
-
//this.debug(`Using zigbee-herdsman with settings2: ${JSON.stringify(this)}`);
|
|
101
|
-
this.debug(`Starting zigbee-herdsman...`);
|
|
102
|
-
|
|
103
|
-
// install event handlers before start
|
|
104
|
-
this.herdsman.on('adapterDisconnected', this.handleDisconnected.bind(this));
|
|
105
|
-
this.herdsman.on('deviceAnnounce', this.handleDeviceAnnounce.bind(this));
|
|
106
|
-
this.herdsman.on('deviceInterview', this.handleDeviceInterview.bind(this));
|
|
107
|
-
this.herdsman.on('deviceJoined', this.handleDeviceJoined.bind(this));
|
|
108
|
-
this.herdsman.on('deviceLeave', this.handleDeviceLeave.bind(this));
|
|
109
|
-
this.herdsman.on('message', this.handleMessage.bind(this));
|
|
110
|
-
|
|
111
|
-
await this.herdsman.start();
|
|
112
|
-
|
|
113
|
-
this.debug('zigbee-herdsman started');
|
|
114
|
-
this.herdsmanStarted = true;
|
|
115
|
-
this.info(`Coordinator firmware version: ${JSON.stringify(await this.herdsman.getCoordinatorVersion())}`);
|
|
116
|
-
|
|
117
|
-
// debug info from herdsman getNetworkParameters
|
|
118
|
-
const debNetworkParam = JSON.parse(JSON.stringify(await this.herdsman.getNetworkParameters()));
|
|
119
|
-
const extendedPanIDDebug = typeof debNetworkParam.extendedPanID === 'string' ? debNetworkParam.extendedPanID.replace('0x', '') : debNetworkParam.extendedPanID;
|
|
120
|
-
|
|
121
|
-
let extPanIDDebug = '';
|
|
122
|
-
for (let i = extendedPanIDDebug.length - 1; i >= 0; i--) {
|
|
123
|
-
extPanIDDebug += extendedPanIDDebug[i - 1];
|
|
124
|
-
extPanIDDebug += extendedPanIDDebug[i];
|
|
125
|
-
i--;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
this.debug(`Zigbee network parameters: panID=${debNetworkParam.panID} channel=${debNetworkParam.channel} extendedPanID=${extPanIDDebug}`);
|
|
129
|
-
} catch (e) {
|
|
130
|
-
this.sendError(e);
|
|
131
|
-
this.error(`Starting zigbee-herdsman problem : ${JSON.stringify(e.message)}`);
|
|
132
|
-
throw 'Error herdsman start';
|
|
133
|
-
}
|
|
134
|
-
// Check if we have to turn off the LED
|
|
135
|
-
try {
|
|
136
|
-
if (this.disableLed) {
|
|
137
|
-
this.info('Disable LED');
|
|
138
|
-
await this.herdsman.setLED(false);
|
|
139
|
-
} else {
|
|
140
|
-
await this.herdsman.setLED(true);
|
|
141
|
-
}
|
|
142
|
-
} catch (e) {
|
|
143
|
-
this.info('Unable to disable LED, unsupported function.');
|
|
144
|
-
this.sendError(e);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// only for CC1352P and CC26X2R1 transmit power
|
|
148
|
-
let powerText = 'normal';
|
|
149
|
-
|
|
150
|
-
if (this.transmitPower != '0') {
|
|
151
|
-
switch (this.transmitPower) {
|
|
152
|
-
case '-22':
|
|
153
|
-
powerText = 'low';
|
|
154
|
-
break;
|
|
155
|
-
case '19':
|
|
156
|
-
powerText = 'high';
|
|
157
|
-
break;
|
|
158
|
-
case '20':
|
|
159
|
-
powerText = 'high+';
|
|
160
|
-
break;
|
|
161
|
-
default:
|
|
162
|
-
powerText = 'normal';
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.info(` --> transmitPower : ${powerText}`);
|
|
168
|
-
try {
|
|
169
|
-
await this.herdsman.setTransmitPower(this.transmitPower);
|
|
170
|
-
} catch (e) {
|
|
171
|
-
this.sendError(e);
|
|
172
|
-
this.info('Unable to set transmit power, unsupported function.');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Call extensions
|
|
176
|
-
this.callExtensionMethod('onZigbeeStarted', []);
|
|
177
|
-
|
|
178
|
-
// Log zigbee clients on startup
|
|
179
|
-
const devices = await this.getClients();
|
|
180
|
-
if (devices.length > 0) {
|
|
181
|
-
this.info(`Currently ${devices.length} devices are joined:`);
|
|
182
|
-
} else {
|
|
183
|
-
this.info(`Currently no devices.`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const device of devices) {
|
|
187
|
-
const entity = await this.resolveEntity(device);
|
|
188
|
-
this.adapter.getObject(device.ieeeAddr.substr(2), (err, obj) => {
|
|
189
|
-
if (obj && obj.common && obj.common.deactivated) {
|
|
190
|
-
this.callExtensionMethod('deregisterDevicePing', [device, entity]);
|
|
191
|
-
} else {
|
|
192
|
-
this.callExtensionMethod('registerDevicePing', [device, entity]);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
// ensure that objects for all found clients are present
|
|
196
|
-
|
|
197
|
-
if (entity.mapped) {
|
|
198
|
-
this.emit('new', entity);
|
|
199
|
-
}
|
|
200
|
-
this.info(
|
|
201
|
-
(entity.device.ieeeAddr) +
|
|
202
|
-
` (addr ${entity.device.networkAddress}): ` +
|
|
203
|
-
(entity.mapped ?
|
|
204
|
-
`${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
|
|
205
|
-
`Not supported (model ${entity.device.modelID})`) +
|
|
206
|
-
`(${entity.device.type})`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.emit('ready');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
info(message, data) {
|
|
214
|
-
this.emit('log', 'info', message, data);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
error(message, data) {
|
|
218
|
-
this.emit('log', 'error', message, data);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
debug(message, data) {
|
|
222
|
-
this.emit('log', 'debug', message, data);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
warn(message, data) {
|
|
226
|
-
this.emit('log', 'warn', message, data);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
event(type, dev, message, data) {
|
|
230
|
-
this.emit('event', type, dev, message, data);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
sendError(error, message) {
|
|
234
|
-
this.adapter.sendError(error, message);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
callExtensionMethod(method, parameters) {
|
|
238
|
-
const result = [];
|
|
239
|
-
for (const extension of this.extensions) {
|
|
240
|
-
if (extension[method]) {
|
|
241
|
-
try {
|
|
242
|
-
if (parameters !== undefined) {
|
|
243
|
-
result.push(extension[method](...parameters));
|
|
244
|
-
} else {
|
|
245
|
-
result.push(extension[method]());
|
|
246
|
-
}
|
|
247
|
-
} catch (error) {
|
|
248
|
-
this.sendError(error);
|
|
249
|
-
this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return Promise.all(result);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async getClients(all) {
|
|
257
|
-
if (this.herdsman.database) {
|
|
258
|
-
const devices = await this.herdsman.getDevices();
|
|
259
|
-
if (all) {
|
|
260
|
-
return devices;
|
|
261
|
-
} else {
|
|
262
|
-
return devices.filter(device => device.type !== 'Coordinator');
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
return [];
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async getGroups() {
|
|
270
|
-
try {
|
|
271
|
-
if (this.herdsman) {
|
|
272
|
-
return await this.herdsman.getGroups();
|
|
273
|
-
} else {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
} catch (error) {
|
|
277
|
-
this.sendError(error);
|
|
278
|
-
this.error(JSON.stringify(error));
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async removeGroupById(id) {
|
|
284
|
-
const group = await this.getGroupByID(id);
|
|
285
|
-
try {
|
|
286
|
-
group && group.removeFromDatabase();
|
|
287
|
-
} catch (error) {
|
|
288
|
-
this.sendError(error);
|
|
289
|
-
this.error(`error in removeGroupById: ${error}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async getGroupByID(id) {
|
|
294
|
-
try {
|
|
295
|
-
return this.herdsman.getGroupByID(id);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
this.sendError(error);
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async verifyGroupExists(id) {
|
|
303
|
-
const nid = typeof id === 'number' ? id : parseInt(id);
|
|
304
|
-
let group = await this.herdsman.getGroupByID(nid);
|
|
305
|
-
if (!group) {
|
|
306
|
-
group = await this.herdsman.createGroup(nid);
|
|
307
|
-
group.toZigbee = groupConverters;
|
|
308
|
-
group.model = 'group';
|
|
309
|
-
this.debug(`verifyGroupExists: created group ${nid}`);
|
|
310
|
-
} else {
|
|
311
|
-
this.debug(`verifyGroupExists: group ${nid} exists`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async addPairingCode(code) {
|
|
316
|
-
this.debug(`calling addPairingCode with ${code}`);
|
|
317
|
-
if (code) {
|
|
318
|
-
await this.herdsman.addInstallCode(code)
|
|
319
|
-
this.info(`added code ${code} for pairing`)
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async getGroupMembersFromController(id) {
|
|
326
|
-
const members = [];
|
|
327
|
-
try {
|
|
328
|
-
const group = await this.getGroupByID(id);
|
|
329
|
-
if (group) {
|
|
330
|
-
const groupMembers = group.members;
|
|
331
|
-
for (const member of groupMembers) {
|
|
332
|
-
const epid = member.ID ? member.ID : -1;
|
|
333
|
-
const nwk = member.deviceNetworkAddress;
|
|
334
|
-
const device = this.getDeviceByNetworkAddress(nwk);
|
|
335
|
-
if (device && device.ieeeAddr) {
|
|
336
|
-
members.push({
|
|
337
|
-
ieee: device.ieeeAddr,
|
|
338
|
-
model: device.modelID,
|
|
339
|
-
epid,
|
|
340
|
-
ep: member
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
} catch (error) {
|
|
348
|
-
this.sendError(error);
|
|
349
|
-
if (error) {
|
|
350
|
-
this.error(`getGroupMembersFromController: error is ${JSON.stringify(error)} ${JSON.stringify(new Error().stack)}`);
|
|
351
|
-
} else {
|
|
352
|
-
this.error('unidentified error in getGroupMembersFromController');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return members;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
getDevice(key) {
|
|
359
|
-
return this.herdsman.getDeviceByIeeeAddr(key);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
getDevicesByType(type) {
|
|
363
|
-
return this.herdsman.getDevicesByType(type);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
getDeviceByNetworkAddress(networkAddress) {
|
|
367
|
-
return this.herdsman.getDeviceByNetworkAddress(networkAddress);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async resolveEntity(key, ep) {
|
|
371
|
-
// assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
|
|
372
|
-
|
|
373
|
-
if (typeof key === 'string') {
|
|
374
|
-
if (key === 'coordinator') {
|
|
375
|
-
const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
|
|
376
|
-
return {
|
|
377
|
-
type: 'device',
|
|
378
|
-
device: coordinator,
|
|
379
|
-
endpoint: coordinator.getEndpoint(1),
|
|
380
|
-
name: 'Coordinator',
|
|
381
|
-
};
|
|
382
|
-
} else {
|
|
383
|
-
const device = await this.herdsman.getDeviceByIeeeAddr(key);
|
|
384
|
-
if (device) {
|
|
385
|
-
const mapped = zigbeeHerdsmanConverters.findByDevice(device);
|
|
386
|
-
const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
|
|
387
|
-
let endpoint;
|
|
388
|
-
if (endpoints && ep != undefined && endpoints[ep]) {
|
|
389
|
-
endpoint = device.getEndpoint(endpoints[ep]);
|
|
390
|
-
} else if (endpoints && endpoints['default']) {
|
|
391
|
-
endpoint = device.getEndpoint(endpoints['default']);
|
|
392
|
-
} else {
|
|
393
|
-
const epNum = parseInt(ep);
|
|
394
|
-
if (!isNaN(epNum)) {
|
|
395
|
-
endpoint = device.getEndpoint(epNum);
|
|
396
|
-
} else {
|
|
397
|
-
endpoint = device.endpoints[0];
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return {
|
|
401
|
-
type: 'device',
|
|
402
|
-
device,
|
|
403
|
-
mapped,
|
|
404
|
-
endpoint,
|
|
405
|
-
endpoints: device.endpoints,
|
|
406
|
-
name: key,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
} else if (typeof key === 'number') {
|
|
411
|
-
let group = await this.herdsman.getGroupByID(key);
|
|
412
|
-
if (!group) group = await this.herdsman.createGroup(key);
|
|
413
|
-
group.toZigbee = groupConverters;
|
|
414
|
-
group.model = 'group';
|
|
415
|
-
return {
|
|
416
|
-
type: 'group',
|
|
417
|
-
mapped: group,
|
|
418
|
-
group,
|
|
419
|
-
name: `Group ${key}`,
|
|
420
|
-
};
|
|
421
|
-
} else {
|
|
422
|
-
return {
|
|
423
|
-
type: 'device',
|
|
424
|
-
device: key,
|
|
425
|
-
mapped: zigbeeHerdsmanConverters.findByDevice(key),
|
|
426
|
-
name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
async incMsgHandler(message) {
|
|
432
|
-
this.debug('incoming msg', message);
|
|
433
|
-
const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
|
|
434
|
-
if (!device) {
|
|
435
|
-
this.debug('Message without device!');
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
// We can't handle devices without modelId.
|
|
439
|
-
if (!device.modelId) {
|
|
440
|
-
this.debug('Message without modelId!');
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
this.event('msg', device.ieeeAddr, message, {
|
|
444
|
-
modelId: device.modelId
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Stop controller
|
|
449
|
-
async stop() {
|
|
450
|
-
// Call extensions
|
|
451
|
-
await this.callExtensionMethod('stop', []);
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
await this.permitJoin(0);
|
|
455
|
-
await this.herdsman.stop();
|
|
456
|
-
} catch (error) {
|
|
457
|
-
this.sendError(error);
|
|
458
|
-
if (this.herdsmanStarted) {
|
|
459
|
-
this.error(`Failed to stop zigbee (${error.stack})`);
|
|
460
|
-
} else {
|
|
461
|
-
this.warn(`Failed to stop zigbee during startup`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async handleDisconnected() {
|
|
467
|
-
this.herdsmanStarted = false;
|
|
468
|
-
this.emit('disconnect');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
connected() {
|
|
472
|
-
return this.herdsmanStarted;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Permit join
|
|
476
|
-
async permitJoin(permitTime, devid, failure) {
|
|
477
|
-
let permitDev;
|
|
478
|
-
if (isFunction(devid) && !isFunction(failure)) {
|
|
479
|
-
failure = devid;
|
|
480
|
-
} else {
|
|
481
|
-
permitDev = this.getDevice(devid);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (permitTime) {
|
|
485
|
-
this.info('Zigbee: allowing new devices to join.');
|
|
486
|
-
} else {
|
|
487
|
-
this.info('Zigbee: disabling joining new devices.');
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
try {
|
|
491
|
-
if (permitTime && !this.herdsman.getPermitJoin()) {
|
|
492
|
-
clearInterval(this._permitJoinInterval);
|
|
493
|
-
this._permitJoinTime = permitTime;
|
|
494
|
-
await this.herdsman.permitJoin(true, permitDev);
|
|
495
|
-
this._permitJoinInterval = setInterval(async () => {
|
|
496
|
-
this.emit('pairing', 'Pairing time left', this._permitJoinTime);
|
|
497
|
-
if (this._permitJoinTime === 0) {
|
|
498
|
-
this.info('Zigbee: stop joining');
|
|
499
|
-
clearInterval(this._permitJoinInterval);
|
|
500
|
-
await this.herdsman.permitJoin(false);
|
|
501
|
-
}
|
|
502
|
-
this._permitJoinTime -= 1;
|
|
503
|
-
}, 1000);
|
|
504
|
-
} else if (this.herdsman.getPermitJoin()) {
|
|
505
|
-
if (permitTime) {
|
|
506
|
-
this.info('Joining already permitted');
|
|
507
|
-
} else {
|
|
508
|
-
clearInterval(this._permitJoinInterval);
|
|
509
|
-
await this.herdsman.permitJoin(false, permitDev);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
} catch (e) {
|
|
513
|
-
this.sendError(e);
|
|
514
|
-
this.error(`Failed to open the network: ${e.stack}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Remove device
|
|
519
|
-
async remove(deviceID, force, callback) {
|
|
520
|
-
try {
|
|
521
|
-
const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
|
|
522
|
-
if (device) {
|
|
523
|
-
try {
|
|
524
|
-
await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
|
|
525
|
-
} catch (error) {
|
|
526
|
-
this.sendError(error);
|
|
527
|
-
if (error)
|
|
528
|
-
this.debug(`Failed to remove device ${error.stack}`);
|
|
529
|
-
// skip error if force
|
|
530
|
-
if (!force) {
|
|
531
|
-
throw error;
|
|
532
|
-
} else {
|
|
533
|
-
this.debug(`Force remove`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
try {
|
|
538
|
-
await device.removeFromDatabase();
|
|
539
|
-
} catch (error) {
|
|
540
|
-
this.sendError(error);
|
|
541
|
-
// skip error
|
|
542
|
-
if (error)
|
|
543
|
-
this.debug(`Failed to remove from DB ${error.stack}`);
|
|
544
|
-
}
|
|
545
|
-
this.debug('Remove successful.');
|
|
546
|
-
callback && callback();
|
|
547
|
-
this.callExtensionMethod(
|
|
548
|
-
'onDeviceRemove',
|
|
549
|
-
[device],
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
} catch (error) {
|
|
553
|
-
this.sendError(error);
|
|
554
|
-
this.error(`Failed to remove ${error.stack}`);
|
|
555
|
-
callback && callback(`Failed to remove ${error.stack}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Zigbee events
|
|
560
|
-
async handleDeviceLeave(message) {
|
|
561
|
-
try {
|
|
562
|
-
this.debug('handleDeviceLeave', message);
|
|
563
|
-
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
564
|
-
const friendlyName = entity ? entity.name : message.ieeeAddr;
|
|
565
|
-
this.debug(`Device '${friendlyName}' left the network`);
|
|
566
|
-
this.emit('leave', message.ieeeAddr);
|
|
567
|
-
// Call extensions
|
|
568
|
-
this.callExtensionMethod(
|
|
569
|
-
'onDeviceLeave',
|
|
570
|
-
[message, entity],
|
|
571
|
-
);
|
|
572
|
-
} catch (error) {
|
|
573
|
-
this.sendError(error);
|
|
574
|
-
this.error(`Failed to handleDeviceLeave ${error.stack}`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async handleDeviceAnnounce(message) {
|
|
579
|
-
this.debug('handleDeviceAnnounce', message);
|
|
580
|
-
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
581
|
-
const friendlyName = entity.name;
|
|
582
|
-
this.warn(`Device '${friendlyName}' announced itself`);
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
if (entity && entity.mapped) {
|
|
586
|
-
this.callExtensionMethod(
|
|
587
|
-
'onZigbeeEvent',
|
|
588
|
-
[{'device': message.device, 'type': 'deviceAnnounce'}, entity ? entity.mapped : null]);
|
|
589
|
-
}
|
|
590
|
-
} catch (error) {
|
|
591
|
-
this.sendError(error);
|
|
592
|
-
this.error(`Failed to handleDeviceLeave ${error.stack}`);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
this.emit('pairing', `Device '${friendlyName}' announced itself`);
|
|
596
|
-
if (!this.herdsman.getPermitJoin()) {
|
|
597
|
-
this.callExtensionMethod('registerDevicePing', [message.device, entity]);
|
|
598
|
-
}
|
|
599
|
-
// if has modelID so can create device
|
|
600
|
-
if (entity.device && entity.device._modelID) {
|
|
601
|
-
entity.device.modelID = entity.device._modelID;
|
|
602
|
-
this.emit('new', entity);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async handleDeviceJoined(message) {
|
|
607
|
-
this.debug('handleDeviceJoined', message);
|
|
608
|
-
//const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
609
|
-
//this.emit('new', entity);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
async handleDeviceInterview(message) {
|
|
613
|
-
this.debug('handleDeviceInterview', message);
|
|
614
|
-
// safeguard: We do not allow to start an interview if the network is not opened
|
|
615
|
-
if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
|
|
616
|
-
this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
620
|
-
const friendlyName = entity.name;
|
|
621
|
-
if (message.status === 'successful') {
|
|
622
|
-
this.info(`Successfully interviewed '${friendlyName}', device has successfully been paired`);
|
|
623
|
-
|
|
624
|
-
if (entity.mapped) {
|
|
625
|
-
const {vendor, description, model} = entity.mapped;
|
|
626
|
-
this.info(
|
|
627
|
-
`Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
|
|
631
|
-
this.emit('pairing', 'Interview successful', JSON.stringify(log));
|
|
632
|
-
entity.device.modelID = entity.device._modelID;
|
|
633
|
-
this.emit('new', entity);
|
|
634
|
-
// send to extensions again (for configure)
|
|
635
|
-
this.callExtensionMethod(
|
|
636
|
-
'onZigbeeEvent',
|
|
637
|
-
[message, entity ? entity.mapped : null],
|
|
638
|
-
);
|
|
639
|
-
} else {
|
|
640
|
-
this.debug(
|
|
641
|
-
`Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
|
|
642
|
-
`please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
|
|
643
|
-
);
|
|
644
|
-
this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
|
|
645
|
-
entity.device.modelID = entity.device._modelID;
|
|
646
|
-
this.emit('new', entity);
|
|
647
|
-
}
|
|
648
|
-
} else if (message.status === 'failed') {
|
|
649
|
-
this.error(`Failed to interview '${friendlyName}', device has not successfully been paired. ${message.error}`);
|
|
650
|
-
this.emit('pairing', 'Interview failed', friendlyName);
|
|
651
|
-
} else {
|
|
652
|
-
if (message.status === 'started') {
|
|
653
|
-
this.info(`Starting interview of '${friendlyName}'`);
|
|
654
|
-
this.emit('pairing', 'Interview started', friendlyName);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
async handleMessage(data) {
|
|
660
|
-
this.debug(`handleMessage`, data);
|
|
661
|
-
const entity = await this.resolveEntity(data.device || data.ieeeAddr);
|
|
662
|
-
const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
|
|
663
|
-
this.debug(
|
|
664
|
-
`Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
|
|
665
|
-
`, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
|
666
|
-
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
|
|
667
|
-
);
|
|
668
|
-
this.event(data.type, entity, data);
|
|
669
|
-
|
|
670
|
-
// Call extensions
|
|
671
|
-
this.callExtensionMethod(
|
|
672
|
-
'onZigbeeEvent',
|
|
673
|
-
[data, entity ? entity.mapped : null],
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
async getMap(callback) {
|
|
678
|
-
try {
|
|
679
|
-
const devices = this.herdsman.getDevices(true);
|
|
680
|
-
const lqis = [];
|
|
681
|
-
const routing = [];
|
|
682
|
-
|
|
683
|
-
for (const device of devices.filter((d) => d.type !== 'EndDevice')) {
|
|
684
|
-
const resolved = await this.resolveEntity(device);
|
|
685
|
-
let result;
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
result = await device.lqi();
|
|
689
|
-
} catch (error) {
|
|
690
|
-
this.sendError(error);
|
|
691
|
-
error && this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
|
|
692
|
-
|
|
693
|
-
lqis.push({
|
|
694
|
-
parent: 'undefined',
|
|
695
|
-
networkAddress: 0,
|
|
696
|
-
ieeeAddr: device.ieeeAddr,
|
|
697
|
-
lqi: 'undefined',
|
|
698
|
-
relationship: 0,
|
|
699
|
-
depth: 0,
|
|
700
|
-
status: 'offline',
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (result !== undefined) {
|
|
705
|
-
for (const dev of result.neighbors) {
|
|
706
|
-
if (dev !== undefined && dev.ieeeAddr !== '0xffffffffffffffff') {
|
|
707
|
-
lqis.push({
|
|
708
|
-
parent: resolved.device.ieeeAddr,
|
|
709
|
-
networkAddress: dev.networkAddress,
|
|
710
|
-
ieeeAddr: dev.ieeeAddr,
|
|
711
|
-
lqi: dev.linkquality,
|
|
712
|
-
relationship: dev.relationship,
|
|
713
|
-
depth: dev.depth,
|
|
714
|
-
status: dev.linkquality > 0 ? 'online' : 'offline',
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
this.debug(`LQI succeeded for '${resolved.name}'`);
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
result = await device.routingTable();
|
|
724
|
-
} catch (error) {
|
|
725
|
-
this.sendError(error);
|
|
726
|
-
if (error) {
|
|
727
|
-
this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
|
|
732
|
-
if (result !== undefined) {
|
|
733
|
-
if (result.table !== undefined) {
|
|
734
|
-
for (const dev of result.table) {
|
|
735
|
-
routing.push({
|
|
736
|
-
source: resolved.device.ieeeAddr,
|
|
737
|
-
destination: dev.destinationAddress,
|
|
738
|
-
nextHop: dev.nextHop,
|
|
739
|
-
status: dev.status,
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
this.debug(`Routing table succeeded for '${resolved.name}'`);
|
|
745
|
-
}
|
|
746
|
-
this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
|
|
747
|
-
|
|
748
|
-
callback && callback({lqis, routing});
|
|
749
|
-
} catch (error) {
|
|
750
|
-
this.sendError(error);
|
|
751
|
-
this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
|
|
756
|
-
const entity = await this.resolveEntity(deviceID, ep);
|
|
757
|
-
const device = entity.device;
|
|
758
|
-
const endpoint = entity.endpoint;
|
|
759
|
-
if (!device) {
|
|
760
|
-
this.error(
|
|
761
|
-
`Zigbee cannot publish message to device because '${deviceID}' is not known`
|
|
762
|
-
);
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
if (!endpoint) {
|
|
766
|
-
this.error(
|
|
767
|
-
`Zigbee cannot publish message to endpoint because '${ep}' is not known`
|
|
768
|
-
);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
|
|
773
|
-
|
|
774
|
-
if (cfg == null) {
|
|
775
|
-
cfg = {};
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (type === 'foundation') {
|
|
779
|
-
cfg.disableDefaultResponse = true;
|
|
780
|
-
if (cmd === 'read' && !Array.isArray(zclData)) {
|
|
781
|
-
// needs to be iterable (string[] | number [])
|
|
782
|
-
zclData[Symbol.iterator] = function* () {
|
|
783
|
-
let k;
|
|
784
|
-
for (k in this) {
|
|
785
|
-
yield k;
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
let result;
|
|
790
|
-
if (cmd === 'configReport') {
|
|
791
|
-
result = await endpoint.configureReporting(cid, zclData, cfg);
|
|
792
|
-
} else {
|
|
793
|
-
result = await endpoint[cmd](cid, zclData, cfg);
|
|
794
|
-
}
|
|
795
|
-
callback && callback(undefined, result);
|
|
796
|
-
} else if (type === 'functionalResp') {
|
|
797
|
-
cfg.disableDefaultResponse = false;
|
|
798
|
-
const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
|
|
799
|
-
callback && callback(undefined, result);
|
|
800
|
-
} else {
|
|
801
|
-
cfg.disableDefaultResponse = false;
|
|
802
|
-
const result = await endpoint.command(cid, cmd, zclData, cfg);
|
|
803
|
-
callback && callback(undefined, result);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
async addDevToGroup(devId, groupId, epid) {
|
|
808
|
-
try {
|
|
809
|
-
this.debug(`called addDevToGroup with ${devId}, ${groupId}, ${epid}`)
|
|
810
|
-
const entity = await this.resolveEntity(devId);
|
|
811
|
-
const group = await this.resolveEntity(groupId);
|
|
812
|
-
this.debug(`addDevFromGroup - entity: ${utils.getEntityInfo(entity)}`);
|
|
813
|
-
// generate group debug info and display it
|
|
814
|
-
const members = await this.getGroupMembersFromController(groupId)
|
|
815
|
-
let memberIDs = []
|
|
816
|
-
for (let member of members) {
|
|
817
|
-
memberIDs.push(member.ieee)
|
|
818
|
-
}
|
|
819
|
-
this.debug(`addDevToGroup ${groupId} with ${memberIDs.length} members ${safeJsonStringify(memberIDs)}`);
|
|
820
|
-
if (epid != undefined) {
|
|
821
|
-
for (const ep of entity.endpoints) {
|
|
822
|
-
this.debug(`checking ep ${ep.ID} of ${devId} (${epid})`)
|
|
823
|
-
if (ep.ID == epid) {
|
|
824
|
-
if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)) {
|
|
825
|
-
this.debug(`adding endpoint ${ep.ID} (${epid}) to group ${groupId}`);
|
|
826
|
-
await (ep.addToGroup(group.mapped));
|
|
827
|
-
}
|
|
828
|
-
else this.error(`cluster genGroups not supported for endpoint ${epid} of ${devId}`)
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
} else {
|
|
832
|
-
if (entity.endpoint.inputClusters.includes(4)) {
|
|
833
|
-
this.info(`adding endpoint ${entity.endpoint.ID} of ${devId} to group`);
|
|
834
|
-
await entity.endpoint.addToGroup(group.mapped);
|
|
835
|
-
} else {
|
|
836
|
-
let added = false;
|
|
837
|
-
for (const ep of entity.endpoints) {
|
|
838
|
-
if (ep.inputClusters.includes(4)) {
|
|
839
|
-
await ep.addToGroup(group.mapped);
|
|
840
|
-
added = true;
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
if (!added) {
|
|
845
|
-
throw ('cluster genGroups not supported');
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
} catch (error) {
|
|
850
|
-
this.sendError(error);
|
|
851
|
-
this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
|
|
852
|
-
return {error: `Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}`};
|
|
853
|
-
}
|
|
854
|
-
return {};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
async removeDevFromGroup(devId, groupId, epid) {
|
|
858
|
-
this.debug(`removeDevFromGroup with ${devId}, ${groupId}, ${epid}`)
|
|
859
|
-
let entity;
|
|
860
|
-
try {
|
|
861
|
-
entity = await this.resolveEntity(devId);
|
|
862
|
-
const group = await this.resolveEntity(groupId);
|
|
863
|
-
|
|
864
|
-
const members = await this.getGroupMembersFromController(groupId)
|
|
865
|
-
let memberIDs = []
|
|
866
|
-
for (let member of members) {
|
|
867
|
-
memberIDs.push(member.ieee)
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
this.debug(`removeDevFromGroup - entity: ${utils.getEntityInfo(entity)}`);
|
|
871
|
-
this.debug(`removeDevFromGroup ${groupId} with ${memberIDs.length} members ${safeJsonStringify(memberIDs)}`);
|
|
872
|
-
|
|
873
|
-
if (epid != undefined) {
|
|
874
|
-
for (const ep of entity.endpoints) {
|
|
875
|
-
this.debug(`checking ep ${ep.ID} of ${devId} (${epid})`)
|
|
876
|
-
if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4))) {
|
|
877
|
-
await ep.removeFromGroup(group.mapped);
|
|
878
|
-
this.info(`removing endpoint ${ep.ID} of ${devId} from group ${groupId}`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
} else {
|
|
882
|
-
await entity.endpoint.removeFromGroup(group.mapped);
|
|
883
|
-
this.info(`removing endpoint ${entity.endpoint.ID} of ${devId} from group ${groupId}`);
|
|
884
|
-
}
|
|
885
|
-
} catch (error) {
|
|
886
|
-
this.sendError(error);
|
|
887
|
-
this.error(`Exception when trying remove ${devId} (ep ${epid ? epid : (entity ? entity.endpoint.ID : '')}) from group ${devId}`, error);
|
|
888
|
-
return {error: `Failed to remove dev ${devId} (ep ${epid ? epid : (entity ? entity.endpoint.ID : '')}) from group ${devId}`};
|
|
889
|
-
}
|
|
890
|
-
return {};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
async removeDevFromAllGroups(devId) {
|
|
894
|
-
try {
|
|
895
|
-
const entity = await this.resolveEntity(devId);
|
|
896
|
-
this.debug(`entity: ${safeJsonStringify(entity)}`);
|
|
897
|
-
for (const ep of entity.endpoints) {
|
|
898
|
-
if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)) {
|
|
899
|
-
await ep.removefromAllGroups();
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
} catch (error) {
|
|
903
|
-
this.sendError(error);
|
|
904
|
-
this.error(`Exception when trying remove ${devId} from all groups`, error);
|
|
905
|
-
return {error: `Failed to remove dev ${devId} from all groups: ${error}`};
|
|
906
|
-
}
|
|
907
|
-
return {};
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
bind(ep, cluster, target, callback) {
|
|
911
|
-
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
|
912
|
-
target = !target ? this.getCoordinator() : target;
|
|
913
|
-
|
|
914
|
-
this.debug(`Binding ${log}`);
|
|
915
|
-
ep.bind(cluster, target, error => {
|
|
916
|
-
if (error) {
|
|
917
|
-
this.sendError(error);
|
|
918
|
-
this.error(`Failed to bind ${log} - (${error})`);
|
|
919
|
-
} else {
|
|
920
|
-
this.debug(`Successfully bound ${log}`);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
callback(error);
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
unbind(ep, cluster, target, callback) {
|
|
928
|
-
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
|
929
|
-
target = !target ? this.getCoordinator() : target;
|
|
930
|
-
|
|
931
|
-
this.debug(`Unbinding ${log}`);
|
|
932
|
-
ep.unbind(cluster, target, (error) => {
|
|
933
|
-
if (error) {
|
|
934
|
-
this.error(`Failed to unbind ${log} - (${error})`);
|
|
935
|
-
} else {
|
|
936
|
-
this.debug(`Successfully unbound ${log}`);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
callback(error);
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
reset(mode, callback) {
|
|
944
|
-
try {
|
|
945
|
-
this.herdsman.reset(mode);
|
|
946
|
-
callback && callback();
|
|
947
|
-
} catch (error) {
|
|
948
|
-
this.sendError(error);
|
|
949
|
-
this.error(`Failed to reset ${error.stack}`);
|
|
950
|
-
callback && callback(error);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async touchlinkReset(permitTime) {
|
|
955
|
-
try {
|
|
956
|
-
await this.herdsman.touchlinkFactoryResetFirst();
|
|
957
|
-
this.permitJoin(permitTime);
|
|
958
|
-
} catch (error) {
|
|
959
|
-
this.sendError(error);
|
|
960
|
-
this.error(`Failed to touchlinkReset ${error.stack}`);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
async getChannelsEnergy() {
|
|
965
|
-
const payload = {
|
|
966
|
-
dstaddr: 0x0,
|
|
967
|
-
dstaddrmode: 0x02,
|
|
968
|
-
channelmask: 0x07FFF800,
|
|
969
|
-
scanduration: 0x5,
|
|
970
|
-
scancount: 1,
|
|
971
|
-
nwkmanageraddr: 0x0000
|
|
972
|
-
};
|
|
973
|
-
const energyScan = this.herdsman.adapter.znp.waitFor(
|
|
974
|
-
2, // unpi_1.Constants.Type.AREQ,
|
|
975
|
-
5, // Subsystem.ZDO,
|
|
976
|
-
'mgmtNwkUpdateNotify'
|
|
977
|
-
);
|
|
978
|
-
await this.herdsman.adapter.znp.request(
|
|
979
|
-
0x5, // Subsystem.ZDO
|
|
980
|
-
'mgmtNwkUpdateReq',
|
|
981
|
-
payload,
|
|
982
|
-
energyScan.ID
|
|
983
|
-
);
|
|
984
|
-
const result = await energyScan.start().promise;
|
|
985
|
-
return result.payload;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
module.exports = ZigbeeController;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pathLib = require('path');
|
|
4
|
+
const ZigbeeHerdsman = require('zigbee-herdsman');
|
|
5
|
+
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
6
|
+
const EventEmitter = require('events').EventEmitter;
|
|
7
|
+
const safeJsonStringify = require('./json');
|
|
8
|
+
const DeviceAvailabilityExt = require('./zbDeviceAvailability');
|
|
9
|
+
const DeviceConfigureExt = require('./zbDeviceConfigure');
|
|
10
|
+
const DeviceEventExt = require('./zbDeviceEvent');
|
|
11
|
+
const DelayedActionExt = require('./zbDelayedAction');
|
|
12
|
+
const utils = require('./utils');
|
|
13
|
+
const groupConverters = [
|
|
14
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
|
15
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
|
|
16
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
|
|
17
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
|
|
18
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
|
|
19
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
|
|
20
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
|
|
21
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
|
|
22
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
|
|
23
|
+
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_startup
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function isFunction(functionToCheck) {
|
|
27
|
+
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ZigbeeController extends EventEmitter {
|
|
31
|
+
/*
|
|
32
|
+
events:
|
|
33
|
+
|
|
34
|
+
log - log (level, msg, data)
|
|
35
|
+
event - preparsed device events (type, dev, msg, data)
|
|
36
|
+
new - new device connected to network (id, msg)
|
|
37
|
+
leave - device leave the network (id, msg)
|
|
38
|
+
join - join countdown (counter)
|
|
39
|
+
ready - connection successfull ()
|
|
40
|
+
*/
|
|
41
|
+
constructor(adapter) {
|
|
42
|
+
super();
|
|
43
|
+
this.adapter = adapter;
|
|
44
|
+
this._permitJoinTime = 0;
|
|
45
|
+
this.herdsmanStarted = false;
|
|
46
|
+
this.extensions = [
|
|
47
|
+
new DeviceAvailabilityExt(this, {}),
|
|
48
|
+
new DeviceConfigureExt(this, {}),
|
|
49
|
+
new DeviceEventExt(this, {}),
|
|
50
|
+
new DelayedActionExt(this, {}),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
configure(options) {
|
|
55
|
+
const herdsmanSettings = {
|
|
56
|
+
network: {
|
|
57
|
+
panID: options.net.panId,
|
|
58
|
+
extendedPanID: options.net.extPanId,
|
|
59
|
+
channelList: options.net.channelList,
|
|
60
|
+
networkKey: options.net.precfgkey,
|
|
61
|
+
},
|
|
62
|
+
databasePath: pathLib.join(options.dbDir, options.dbPath),
|
|
63
|
+
backupPath: pathLib.join(options.dbDir, options.backupPath),
|
|
64
|
+
serialPort: {
|
|
65
|
+
baudRate: options.sp.baudRate,
|
|
66
|
+
rtscts: options.sp.rtscts,
|
|
67
|
+
path: options.sp.port,
|
|
68
|
+
adapter: options.sp.adapter,
|
|
69
|
+
},
|
|
70
|
+
adapter: {
|
|
71
|
+
forceStartWithInconsistentAdapterConfiguration: options.startWithInconsistent
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// https://github.com/ioBroker/ioBroker.zigbee/issues/668
|
|
75
|
+
if (!options.extPanIdFix) {
|
|
76
|
+
delete herdsmanSettings.network.extendedPanID;
|
|
77
|
+
herdsmanSettings.network.extenedPanID = options.net.extPanId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.transmitPower == undefined) {
|
|
81
|
+
this.transmitPower = 0;
|
|
82
|
+
} else {
|
|
83
|
+
this.transmitPower = options.transmitPower;
|
|
84
|
+
}
|
|
85
|
+
this.disableLed = options.disableLed;
|
|
86
|
+
|
|
87
|
+
this.debug(`Using zigbee-herdsman with settings: ${JSON.stringify(herdsmanSettings)}`);
|
|
88
|
+
this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
|
|
89
|
+
this.callExtensionMethod('setOptions', [{
|
|
90
|
+
disableActivePing: options.disablePing,
|
|
91
|
+
disableForcedPing: false,
|
|
92
|
+
pingTimeout: 300,
|
|
93
|
+
pingCount: 3
|
|
94
|
+
}]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Start controller
|
|
98
|
+
async start() {
|
|
99
|
+
try {
|
|
100
|
+
//this.debug(`Using zigbee-herdsman with settings2: ${JSON.stringify(this)}`);
|
|
101
|
+
this.debug(`Starting zigbee-herdsman...`);
|
|
102
|
+
|
|
103
|
+
// install event handlers before start
|
|
104
|
+
this.herdsman.on('adapterDisconnected', this.handleDisconnected.bind(this));
|
|
105
|
+
this.herdsman.on('deviceAnnounce', this.handleDeviceAnnounce.bind(this));
|
|
106
|
+
this.herdsman.on('deviceInterview', this.handleDeviceInterview.bind(this));
|
|
107
|
+
this.herdsman.on('deviceJoined', this.handleDeviceJoined.bind(this));
|
|
108
|
+
this.herdsman.on('deviceLeave', this.handleDeviceLeave.bind(this));
|
|
109
|
+
this.herdsman.on('message', this.handleMessage.bind(this));
|
|
110
|
+
|
|
111
|
+
await this.herdsman.start();
|
|
112
|
+
|
|
113
|
+
this.debug('zigbee-herdsman started');
|
|
114
|
+
this.herdsmanStarted = true;
|
|
115
|
+
this.info(`Coordinator firmware version: ${JSON.stringify(await this.herdsman.getCoordinatorVersion())}`);
|
|
116
|
+
|
|
117
|
+
// debug info from herdsman getNetworkParameters
|
|
118
|
+
const debNetworkParam = JSON.parse(JSON.stringify(await this.herdsman.getNetworkParameters()));
|
|
119
|
+
const extendedPanIDDebug = typeof debNetworkParam.extendedPanID === 'string' ? debNetworkParam.extendedPanID.replace('0x', '') : debNetworkParam.extendedPanID;
|
|
120
|
+
|
|
121
|
+
let extPanIDDebug = '';
|
|
122
|
+
for (let i = extendedPanIDDebug.length - 1; i >= 0; i--) {
|
|
123
|
+
extPanIDDebug += extendedPanIDDebug[i - 1];
|
|
124
|
+
extPanIDDebug += extendedPanIDDebug[i];
|
|
125
|
+
i--;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.debug(`Zigbee network parameters: panID=${debNetworkParam.panID} channel=${debNetworkParam.channel} extendedPanID=${extPanIDDebug}`);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.sendError(e);
|
|
131
|
+
this.error(`Starting zigbee-herdsman problem : ${JSON.stringify(e.message)}`);
|
|
132
|
+
throw 'Error herdsman start';
|
|
133
|
+
}
|
|
134
|
+
// Check if we have to turn off the LED
|
|
135
|
+
try {
|
|
136
|
+
if (this.disableLed) {
|
|
137
|
+
this.info('Disable LED');
|
|
138
|
+
await this.herdsman.setLED(false);
|
|
139
|
+
} else {
|
|
140
|
+
await this.herdsman.setLED(true);
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
this.info('Unable to disable LED, unsupported function.');
|
|
144
|
+
this.sendError(e);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// only for CC1352P and CC26X2R1 transmit power
|
|
148
|
+
let powerText = 'normal';
|
|
149
|
+
|
|
150
|
+
if (this.transmitPower != '0') {
|
|
151
|
+
switch (this.transmitPower) {
|
|
152
|
+
case '-22':
|
|
153
|
+
powerText = 'low';
|
|
154
|
+
break;
|
|
155
|
+
case '19':
|
|
156
|
+
powerText = 'high';
|
|
157
|
+
break;
|
|
158
|
+
case '20':
|
|
159
|
+
powerText = 'high+';
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
powerText = 'normal';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
this.info(` --> transmitPower : ${powerText}`);
|
|
168
|
+
try {
|
|
169
|
+
await this.herdsman.setTransmitPower(this.transmitPower);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
this.sendError(e);
|
|
172
|
+
this.info('Unable to set transmit power, unsupported function.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Call extensions
|
|
176
|
+
this.callExtensionMethod('onZigbeeStarted', []);
|
|
177
|
+
|
|
178
|
+
// Log zigbee clients on startup
|
|
179
|
+
const devices = await this.getClients();
|
|
180
|
+
if (devices.length > 0) {
|
|
181
|
+
this.info(`Currently ${devices.length} devices are joined:`);
|
|
182
|
+
} else {
|
|
183
|
+
this.info(`Currently no devices.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const device of devices) {
|
|
187
|
+
const entity = await this.resolveEntity(device);
|
|
188
|
+
this.adapter.getObject(device.ieeeAddr.substr(2), (err, obj) => {
|
|
189
|
+
if (obj && obj.common && obj.common.deactivated) {
|
|
190
|
+
this.callExtensionMethod('deregisterDevicePing', [device, entity]);
|
|
191
|
+
} else {
|
|
192
|
+
this.callExtensionMethod('registerDevicePing', [device, entity]);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// ensure that objects for all found clients are present
|
|
196
|
+
|
|
197
|
+
if (entity.mapped) {
|
|
198
|
+
this.emit('new', entity);
|
|
199
|
+
}
|
|
200
|
+
this.info(
|
|
201
|
+
(entity.device.ieeeAddr) +
|
|
202
|
+
` (addr ${entity.device.networkAddress}): ` +
|
|
203
|
+
(entity.mapped ?
|
|
204
|
+
`${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
|
|
205
|
+
`Not supported (model ${entity.device.modelID})`) +
|
|
206
|
+
`(${entity.device.type})`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.emit('ready');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
info(message, data) {
|
|
214
|
+
this.emit('log', 'info', message, data);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
error(message, data) {
|
|
218
|
+
this.emit('log', 'error', message, data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
debug(message, data) {
|
|
222
|
+
this.emit('log', 'debug', message, data);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
warn(message, data) {
|
|
226
|
+
this.emit('log', 'warn', message, data);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
event(type, dev, message, data) {
|
|
230
|
+
this.emit('event', type, dev, message, data);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
sendError(error, message) {
|
|
234
|
+
this.adapter.sendError(error, message);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
callExtensionMethod(method, parameters) {
|
|
238
|
+
const result = [];
|
|
239
|
+
for (const extension of this.extensions) {
|
|
240
|
+
if (extension[method]) {
|
|
241
|
+
try {
|
|
242
|
+
if (parameters !== undefined) {
|
|
243
|
+
result.push(extension[method](...parameters));
|
|
244
|
+
} else {
|
|
245
|
+
result.push(extension[method]());
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
this.sendError(error);
|
|
249
|
+
this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return Promise.all(result);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async getClients(all) {
|
|
257
|
+
if (this.herdsman.database) {
|
|
258
|
+
const devices = await this.herdsman.getDevices();
|
|
259
|
+
if (all) {
|
|
260
|
+
return devices;
|
|
261
|
+
} else {
|
|
262
|
+
return devices.filter(device => device.type !== 'Coordinator');
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async getGroups() {
|
|
270
|
+
try {
|
|
271
|
+
if (this.herdsman) {
|
|
272
|
+
return await this.herdsman.getGroups();
|
|
273
|
+
} else {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
this.sendError(error);
|
|
278
|
+
this.error(JSON.stringify(error));
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async removeGroupById(id) {
|
|
284
|
+
const group = await this.getGroupByID(id);
|
|
285
|
+
try {
|
|
286
|
+
group && group.removeFromDatabase();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
this.sendError(error);
|
|
289
|
+
this.error(`error in removeGroupById: ${error}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async getGroupByID(id) {
|
|
294
|
+
try {
|
|
295
|
+
return this.herdsman.getGroupByID(id);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.sendError(error);
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async verifyGroupExists(id) {
|
|
303
|
+
const nid = typeof id === 'number' ? id : parseInt(id);
|
|
304
|
+
let group = await this.herdsman.getGroupByID(nid);
|
|
305
|
+
if (!group) {
|
|
306
|
+
group = await this.herdsman.createGroup(nid);
|
|
307
|
+
group.toZigbee = groupConverters;
|
|
308
|
+
group.model = 'group';
|
|
309
|
+
this.debug(`verifyGroupExists: created group ${nid}`);
|
|
310
|
+
} else {
|
|
311
|
+
this.debug(`verifyGroupExists: group ${nid} exists`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async addPairingCode(code) {
|
|
316
|
+
this.debug(`calling addPairingCode with ${code}`);
|
|
317
|
+
if (code) {
|
|
318
|
+
await this.herdsman.addInstallCode(code)
|
|
319
|
+
this.info(`added code ${code} for pairing`)
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async getGroupMembersFromController(id) {
|
|
326
|
+
const members = [];
|
|
327
|
+
try {
|
|
328
|
+
const group = await this.getGroupByID(id);
|
|
329
|
+
if (group) {
|
|
330
|
+
const groupMembers = group.members;
|
|
331
|
+
for (const member of groupMembers) {
|
|
332
|
+
const epid = member.ID ? member.ID : -1;
|
|
333
|
+
const nwk = member.deviceNetworkAddress;
|
|
334
|
+
const device = this.getDeviceByNetworkAddress(nwk);
|
|
335
|
+
if (device && device.ieeeAddr) {
|
|
336
|
+
members.push({
|
|
337
|
+
ieee: device.ieeeAddr,
|
|
338
|
+
model: device.modelID,
|
|
339
|
+
epid,
|
|
340
|
+
ep: member
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
this.sendError(error);
|
|
349
|
+
if (error) {
|
|
350
|
+
this.error(`getGroupMembersFromController: error is ${JSON.stringify(error)} ${JSON.stringify(new Error().stack)}`);
|
|
351
|
+
} else {
|
|
352
|
+
this.error('unidentified error in getGroupMembersFromController');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return members;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getDevice(key) {
|
|
359
|
+
return this.herdsman.getDeviceByIeeeAddr(key);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getDevicesByType(type) {
|
|
363
|
+
return this.herdsman.getDevicesByType(type);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
getDeviceByNetworkAddress(networkAddress) {
|
|
367
|
+
return this.herdsman.getDeviceByNetworkAddress(networkAddress);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async resolveEntity(key, ep) {
|
|
371
|
+
// assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
|
|
372
|
+
|
|
373
|
+
if (typeof key === 'string') {
|
|
374
|
+
if (key === 'coordinator') {
|
|
375
|
+
const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
|
|
376
|
+
return {
|
|
377
|
+
type: 'device',
|
|
378
|
+
device: coordinator,
|
|
379
|
+
endpoint: coordinator.getEndpoint(1),
|
|
380
|
+
name: 'Coordinator',
|
|
381
|
+
};
|
|
382
|
+
} else {
|
|
383
|
+
const device = await this.herdsman.getDeviceByIeeeAddr(key);
|
|
384
|
+
if (device) {
|
|
385
|
+
const mapped = zigbeeHerdsmanConverters.findByDevice(device);
|
|
386
|
+
const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
|
|
387
|
+
let endpoint;
|
|
388
|
+
if (endpoints && ep != undefined && endpoints[ep]) {
|
|
389
|
+
endpoint = device.getEndpoint(endpoints[ep]);
|
|
390
|
+
} else if (endpoints && endpoints['default']) {
|
|
391
|
+
endpoint = device.getEndpoint(endpoints['default']);
|
|
392
|
+
} else {
|
|
393
|
+
const epNum = parseInt(ep);
|
|
394
|
+
if (!isNaN(epNum)) {
|
|
395
|
+
endpoint = device.getEndpoint(epNum);
|
|
396
|
+
} else {
|
|
397
|
+
endpoint = device.endpoints[0];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
type: 'device',
|
|
402
|
+
device,
|
|
403
|
+
mapped,
|
|
404
|
+
endpoint,
|
|
405
|
+
endpoints: device.endpoints,
|
|
406
|
+
name: key,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} else if (typeof key === 'number') {
|
|
411
|
+
let group = await this.herdsman.getGroupByID(key);
|
|
412
|
+
if (!group) group = await this.herdsman.createGroup(key);
|
|
413
|
+
group.toZigbee = groupConverters;
|
|
414
|
+
group.model = 'group';
|
|
415
|
+
return {
|
|
416
|
+
type: 'group',
|
|
417
|
+
mapped: group,
|
|
418
|
+
group,
|
|
419
|
+
name: `Group ${key}`,
|
|
420
|
+
};
|
|
421
|
+
} else {
|
|
422
|
+
return {
|
|
423
|
+
type: 'device',
|
|
424
|
+
device: key,
|
|
425
|
+
mapped: zigbeeHerdsmanConverters.findByDevice(key),
|
|
426
|
+
name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async incMsgHandler(message) {
|
|
432
|
+
this.debug('incoming msg', message);
|
|
433
|
+
const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
|
|
434
|
+
if (!device) {
|
|
435
|
+
this.debug('Message without device!');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// We can't handle devices without modelId.
|
|
439
|
+
if (!device.modelId) {
|
|
440
|
+
this.debug('Message without modelId!');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this.event('msg', device.ieeeAddr, message, {
|
|
444
|
+
modelId: device.modelId
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Stop controller
|
|
449
|
+
async stop() {
|
|
450
|
+
// Call extensions
|
|
451
|
+
await this.callExtensionMethod('stop', []);
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
await this.permitJoin(0);
|
|
455
|
+
await this.herdsman.stop();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
this.sendError(error);
|
|
458
|
+
if (this.herdsmanStarted) {
|
|
459
|
+
this.error(`Failed to stop zigbee (${error.stack})`);
|
|
460
|
+
} else {
|
|
461
|
+
this.warn(`Failed to stop zigbee during startup`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async handleDisconnected() {
|
|
467
|
+
this.herdsmanStarted = false;
|
|
468
|
+
this.emit('disconnect');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
connected() {
|
|
472
|
+
return this.herdsmanStarted;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Permit join
|
|
476
|
+
async permitJoin(permitTime, devid, failure) {
|
|
477
|
+
let permitDev;
|
|
478
|
+
if (isFunction(devid) && !isFunction(failure)) {
|
|
479
|
+
failure = devid;
|
|
480
|
+
} else {
|
|
481
|
+
permitDev = this.getDevice(devid);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (permitTime) {
|
|
485
|
+
this.info('Zigbee: allowing new devices to join.');
|
|
486
|
+
} else {
|
|
487
|
+
this.info('Zigbee: disabling joining new devices.');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
if (permitTime && !this.herdsman.getPermitJoin()) {
|
|
492
|
+
clearInterval(this._permitJoinInterval);
|
|
493
|
+
this._permitJoinTime = permitTime;
|
|
494
|
+
await this.herdsman.permitJoin(true, permitDev);
|
|
495
|
+
this._permitJoinInterval = setInterval(async () => {
|
|
496
|
+
this.emit('pairing', 'Pairing time left', this._permitJoinTime);
|
|
497
|
+
if (this._permitJoinTime === 0) {
|
|
498
|
+
this.info('Zigbee: stop joining');
|
|
499
|
+
clearInterval(this._permitJoinInterval);
|
|
500
|
+
await this.herdsman.permitJoin(false);
|
|
501
|
+
}
|
|
502
|
+
this._permitJoinTime -= 1;
|
|
503
|
+
}, 1000);
|
|
504
|
+
} else if (this.herdsman.getPermitJoin()) {
|
|
505
|
+
if (permitTime) {
|
|
506
|
+
this.info('Joining already permitted');
|
|
507
|
+
} else {
|
|
508
|
+
clearInterval(this._permitJoinInterval);
|
|
509
|
+
await this.herdsman.permitJoin(false, permitDev);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch (e) {
|
|
513
|
+
this.sendError(e);
|
|
514
|
+
this.error(`Failed to open the network: ${e.stack}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Remove device
|
|
519
|
+
async remove(deviceID, force, callback) {
|
|
520
|
+
try {
|
|
521
|
+
const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
|
|
522
|
+
if (device) {
|
|
523
|
+
try {
|
|
524
|
+
await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
this.sendError(error);
|
|
527
|
+
if (error)
|
|
528
|
+
this.debug(`Failed to remove device ${error.stack}`);
|
|
529
|
+
// skip error if force
|
|
530
|
+
if (!force) {
|
|
531
|
+
throw error;
|
|
532
|
+
} else {
|
|
533
|
+
this.debug(`Force remove`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await device.removeFromDatabase();
|
|
539
|
+
} catch (error) {
|
|
540
|
+
this.sendError(error);
|
|
541
|
+
// skip error
|
|
542
|
+
if (error)
|
|
543
|
+
this.debug(`Failed to remove from DB ${error.stack}`);
|
|
544
|
+
}
|
|
545
|
+
this.debug('Remove successful.');
|
|
546
|
+
callback && callback();
|
|
547
|
+
this.callExtensionMethod(
|
|
548
|
+
'onDeviceRemove',
|
|
549
|
+
[device],
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
this.sendError(error);
|
|
554
|
+
this.error(`Failed to remove ${error.stack}`);
|
|
555
|
+
callback && callback(`Failed to remove ${error.stack}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Zigbee events
|
|
560
|
+
async handleDeviceLeave(message) {
|
|
561
|
+
try {
|
|
562
|
+
this.debug('handleDeviceLeave', message);
|
|
563
|
+
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
564
|
+
const friendlyName = entity ? entity.name : message.ieeeAddr;
|
|
565
|
+
this.debug(`Device '${friendlyName}' left the network`);
|
|
566
|
+
this.emit('leave', message.ieeeAddr);
|
|
567
|
+
// Call extensions
|
|
568
|
+
this.callExtensionMethod(
|
|
569
|
+
'onDeviceLeave',
|
|
570
|
+
[message, entity],
|
|
571
|
+
);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
this.sendError(error);
|
|
574
|
+
this.error(`Failed to handleDeviceLeave ${error.stack}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async handleDeviceAnnounce(message) {
|
|
579
|
+
this.debug('handleDeviceAnnounce', message);
|
|
580
|
+
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
581
|
+
const friendlyName = entity.name;
|
|
582
|
+
this.warn(`Device '${friendlyName}' announced itself`);
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
if (entity && entity.mapped) {
|
|
586
|
+
this.callExtensionMethod(
|
|
587
|
+
'onZigbeeEvent',
|
|
588
|
+
[{'device': message.device, 'type': 'deviceAnnounce'}, entity ? entity.mapped : null]);
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
this.sendError(error);
|
|
592
|
+
this.error(`Failed to handleDeviceLeave ${error.stack}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
this.emit('pairing', `Device '${friendlyName}' announced itself`);
|
|
596
|
+
if (!this.herdsman.getPermitJoin()) {
|
|
597
|
+
this.callExtensionMethod('registerDevicePing', [message.device, entity]);
|
|
598
|
+
}
|
|
599
|
+
// if has modelID so can create device
|
|
600
|
+
if (entity.device && entity.device._modelID) {
|
|
601
|
+
entity.device.modelID = entity.device._modelID;
|
|
602
|
+
this.emit('new', entity);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async handleDeviceJoined(message) {
|
|
607
|
+
this.debug('handleDeviceJoined', message);
|
|
608
|
+
//const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
609
|
+
//this.emit('new', entity);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async handleDeviceInterview(message) {
|
|
613
|
+
this.debug('handleDeviceInterview', message);
|
|
614
|
+
// safeguard: We do not allow to start an interview if the network is not opened
|
|
615
|
+
if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
|
|
616
|
+
this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const entity = await this.resolveEntity(message.device || message.ieeeAddr);
|
|
620
|
+
const friendlyName = entity.name;
|
|
621
|
+
if (message.status === 'successful') {
|
|
622
|
+
this.info(`Successfully interviewed '${friendlyName}', device has successfully been paired`);
|
|
623
|
+
|
|
624
|
+
if (entity.mapped) {
|
|
625
|
+
const {vendor, description, model} = entity.mapped;
|
|
626
|
+
this.info(
|
|
627
|
+
`Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
|
|
631
|
+
this.emit('pairing', 'Interview successful', JSON.stringify(log));
|
|
632
|
+
entity.device.modelID = entity.device._modelID;
|
|
633
|
+
this.emit('new', entity);
|
|
634
|
+
// send to extensions again (for configure)
|
|
635
|
+
this.callExtensionMethod(
|
|
636
|
+
'onZigbeeEvent',
|
|
637
|
+
[message, entity ? entity.mapped : null],
|
|
638
|
+
);
|
|
639
|
+
} else {
|
|
640
|
+
this.debug(
|
|
641
|
+
`Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
|
|
642
|
+
`please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
|
|
643
|
+
);
|
|
644
|
+
this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
|
|
645
|
+
entity.device.modelID = entity.device._modelID;
|
|
646
|
+
this.emit('new', entity);
|
|
647
|
+
}
|
|
648
|
+
} else if (message.status === 'failed') {
|
|
649
|
+
this.error(`Failed to interview '${friendlyName}', device has not successfully been paired. ${message.error}`);
|
|
650
|
+
this.emit('pairing', 'Interview failed', friendlyName);
|
|
651
|
+
} else {
|
|
652
|
+
if (message.status === 'started') {
|
|
653
|
+
this.info(`Starting interview of '${friendlyName}'`);
|
|
654
|
+
this.emit('pairing', 'Interview started', friendlyName);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async handleMessage(data) {
|
|
660
|
+
this.debug(`handleMessage`, data);
|
|
661
|
+
const entity = await this.resolveEntity(data.device || data.ieeeAddr);
|
|
662
|
+
const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
|
|
663
|
+
this.debug(
|
|
664
|
+
`Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
|
|
665
|
+
`, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
|
666
|
+
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
|
|
667
|
+
);
|
|
668
|
+
this.event(data.type, entity, data);
|
|
669
|
+
|
|
670
|
+
// Call extensions
|
|
671
|
+
this.callExtensionMethod(
|
|
672
|
+
'onZigbeeEvent',
|
|
673
|
+
[data, entity ? entity.mapped : null],
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async getMap(callback) {
|
|
678
|
+
try {
|
|
679
|
+
const devices = this.herdsman.getDevices(true);
|
|
680
|
+
const lqis = [];
|
|
681
|
+
const routing = [];
|
|
682
|
+
|
|
683
|
+
for (const device of devices.filter((d) => d.type !== 'EndDevice')) {
|
|
684
|
+
const resolved = await this.resolveEntity(device);
|
|
685
|
+
let result;
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
result = await device.lqi();
|
|
689
|
+
} catch (error) {
|
|
690
|
+
this.sendError(error);
|
|
691
|
+
error && this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
|
|
692
|
+
|
|
693
|
+
lqis.push({
|
|
694
|
+
parent: 'undefined',
|
|
695
|
+
networkAddress: 0,
|
|
696
|
+
ieeeAddr: device.ieeeAddr,
|
|
697
|
+
lqi: 'undefined',
|
|
698
|
+
relationship: 0,
|
|
699
|
+
depth: 0,
|
|
700
|
+
status: 'offline',
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (result !== undefined) {
|
|
705
|
+
for (const dev of result.neighbors) {
|
|
706
|
+
if (dev !== undefined && dev.ieeeAddr !== '0xffffffffffffffff') {
|
|
707
|
+
lqis.push({
|
|
708
|
+
parent: resolved.device.ieeeAddr,
|
|
709
|
+
networkAddress: dev.networkAddress,
|
|
710
|
+
ieeeAddr: dev.ieeeAddr,
|
|
711
|
+
lqi: dev.linkquality,
|
|
712
|
+
relationship: dev.relationship,
|
|
713
|
+
depth: dev.depth,
|
|
714
|
+
status: dev.linkquality > 0 ? 'online' : 'offline',
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this.debug(`LQI succeeded for '${resolved.name}'`);
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
result = await device.routingTable();
|
|
724
|
+
} catch (error) {
|
|
725
|
+
this.sendError(error);
|
|
726
|
+
if (error) {
|
|
727
|
+
this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
|
|
732
|
+
if (result !== undefined) {
|
|
733
|
+
if (result.table !== undefined) {
|
|
734
|
+
for (const dev of result.table) {
|
|
735
|
+
routing.push({
|
|
736
|
+
source: resolved.device.ieeeAddr,
|
|
737
|
+
destination: dev.destinationAddress,
|
|
738
|
+
nextHop: dev.nextHop,
|
|
739
|
+
status: dev.status,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this.debug(`Routing table succeeded for '${resolved.name}'`);
|
|
745
|
+
}
|
|
746
|
+
this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
|
|
747
|
+
|
|
748
|
+
callback && callback({lqis, routing});
|
|
749
|
+
} catch (error) {
|
|
750
|
+
this.sendError(error);
|
|
751
|
+
this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
|
|
756
|
+
const entity = await this.resolveEntity(deviceID, ep);
|
|
757
|
+
const device = entity.device;
|
|
758
|
+
const endpoint = entity.endpoint;
|
|
759
|
+
if (!device) {
|
|
760
|
+
this.error(
|
|
761
|
+
`Zigbee cannot publish message to device because '${deviceID}' is not known`
|
|
762
|
+
);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (!endpoint) {
|
|
766
|
+
this.error(
|
|
767
|
+
`Zigbee cannot publish message to endpoint because '${ep}' is not known`
|
|
768
|
+
);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
|
|
773
|
+
|
|
774
|
+
if (cfg == null) {
|
|
775
|
+
cfg = {};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (type === 'foundation') {
|
|
779
|
+
cfg.disableDefaultResponse = true;
|
|
780
|
+
if (cmd === 'read' && !Array.isArray(zclData)) {
|
|
781
|
+
// needs to be iterable (string[] | number [])
|
|
782
|
+
zclData[Symbol.iterator] = function* () {
|
|
783
|
+
let k;
|
|
784
|
+
for (k in this) {
|
|
785
|
+
yield k;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
let result;
|
|
790
|
+
if (cmd === 'configReport') {
|
|
791
|
+
result = await endpoint.configureReporting(cid, zclData, cfg);
|
|
792
|
+
} else {
|
|
793
|
+
result = await endpoint[cmd](cid, zclData, cfg);
|
|
794
|
+
}
|
|
795
|
+
callback && callback(undefined, result);
|
|
796
|
+
} else if (type === 'functionalResp') {
|
|
797
|
+
cfg.disableDefaultResponse = false;
|
|
798
|
+
const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
|
|
799
|
+
callback && callback(undefined, result);
|
|
800
|
+
} else {
|
|
801
|
+
cfg.disableDefaultResponse = false;
|
|
802
|
+
const result = await endpoint.command(cid, cmd, zclData, cfg);
|
|
803
|
+
callback && callback(undefined, result);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async addDevToGroup(devId, groupId, epid) {
|
|
808
|
+
try {
|
|
809
|
+
this.debug(`called addDevToGroup with ${devId}, ${groupId}, ${epid}`)
|
|
810
|
+
const entity = await this.resolveEntity(devId);
|
|
811
|
+
const group = await this.resolveEntity(groupId);
|
|
812
|
+
this.debug(`addDevFromGroup - entity: ${utils.getEntityInfo(entity)}`);
|
|
813
|
+
// generate group debug info and display it
|
|
814
|
+
const members = await this.getGroupMembersFromController(groupId)
|
|
815
|
+
let memberIDs = []
|
|
816
|
+
for (let member of members) {
|
|
817
|
+
memberIDs.push(member.ieee)
|
|
818
|
+
}
|
|
819
|
+
this.debug(`addDevToGroup ${groupId} with ${memberIDs.length} members ${safeJsonStringify(memberIDs)}`);
|
|
820
|
+
if (epid != undefined) {
|
|
821
|
+
for (const ep of entity.endpoints) {
|
|
822
|
+
this.debug(`checking ep ${ep.ID} of ${devId} (${epid})`)
|
|
823
|
+
if (ep.ID == epid) {
|
|
824
|
+
if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)) {
|
|
825
|
+
this.debug(`adding endpoint ${ep.ID} (${epid}) to group ${groupId}`);
|
|
826
|
+
await (ep.addToGroup(group.mapped));
|
|
827
|
+
}
|
|
828
|
+
else this.error(`cluster genGroups not supported for endpoint ${epid} of ${devId}`)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
if (entity.endpoint.inputClusters.includes(4)) {
|
|
833
|
+
this.info(`adding endpoint ${entity.endpoint.ID} of ${devId} to group`);
|
|
834
|
+
await entity.endpoint.addToGroup(group.mapped);
|
|
835
|
+
} else {
|
|
836
|
+
let added = false;
|
|
837
|
+
for (const ep of entity.endpoints) {
|
|
838
|
+
if (ep.inputClusters.includes(4)) {
|
|
839
|
+
await ep.addToGroup(group.mapped);
|
|
840
|
+
added = true;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (!added) {
|
|
845
|
+
throw ('cluster genGroups not supported');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch (error) {
|
|
850
|
+
this.sendError(error);
|
|
851
|
+
this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
|
|
852
|
+
return {error: `Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}`};
|
|
853
|
+
}
|
|
854
|
+
return {};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async removeDevFromGroup(devId, groupId, epid) {
|
|
858
|
+
this.debug(`removeDevFromGroup with ${devId}, ${groupId}, ${epid}`)
|
|
859
|
+
let entity;
|
|
860
|
+
try {
|
|
861
|
+
entity = await this.resolveEntity(devId);
|
|
862
|
+
const group = await this.resolveEntity(groupId);
|
|
863
|
+
|
|
864
|
+
const members = await this.getGroupMembersFromController(groupId)
|
|
865
|
+
let memberIDs = []
|
|
866
|
+
for (let member of members) {
|
|
867
|
+
memberIDs.push(member.ieee)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
this.debug(`removeDevFromGroup - entity: ${utils.getEntityInfo(entity)}`);
|
|
871
|
+
this.debug(`removeDevFromGroup ${groupId} with ${memberIDs.length} members ${safeJsonStringify(memberIDs)}`);
|
|
872
|
+
|
|
873
|
+
if (epid != undefined) {
|
|
874
|
+
for (const ep of entity.endpoints) {
|
|
875
|
+
this.debug(`checking ep ${ep.ID} of ${devId} (${epid})`)
|
|
876
|
+
if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4))) {
|
|
877
|
+
await ep.removeFromGroup(group.mapped);
|
|
878
|
+
this.info(`removing endpoint ${ep.ID} of ${devId} from group ${groupId}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
await entity.endpoint.removeFromGroup(group.mapped);
|
|
883
|
+
this.info(`removing endpoint ${entity.endpoint.ID} of ${devId} from group ${groupId}`);
|
|
884
|
+
}
|
|
885
|
+
} catch (error) {
|
|
886
|
+
this.sendError(error);
|
|
887
|
+
this.error(`Exception when trying remove ${devId} (ep ${epid ? epid : (entity ? entity.endpoint.ID : '')}) from group ${devId}`, error);
|
|
888
|
+
return {error: `Failed to remove dev ${devId} (ep ${epid ? epid : (entity ? entity.endpoint.ID : '')}) from group ${devId}`};
|
|
889
|
+
}
|
|
890
|
+
return {};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async removeDevFromAllGroups(devId) {
|
|
894
|
+
try {
|
|
895
|
+
const entity = await this.resolveEntity(devId);
|
|
896
|
+
this.debug(`entity: ${safeJsonStringify(entity)}`);
|
|
897
|
+
for (const ep of entity.endpoints) {
|
|
898
|
+
if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)) {
|
|
899
|
+
await ep.removefromAllGroups();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
this.sendError(error);
|
|
904
|
+
this.error(`Exception when trying remove ${devId} from all groups`, error);
|
|
905
|
+
return {error: `Failed to remove dev ${devId} from all groups: ${error}`};
|
|
906
|
+
}
|
|
907
|
+
return {};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
bind(ep, cluster, target, callback) {
|
|
911
|
+
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
|
912
|
+
target = !target ? this.getCoordinator() : target;
|
|
913
|
+
|
|
914
|
+
this.debug(`Binding ${log}`);
|
|
915
|
+
ep.bind(cluster, target, error => {
|
|
916
|
+
if (error) {
|
|
917
|
+
this.sendError(error);
|
|
918
|
+
this.error(`Failed to bind ${log} - (${error})`);
|
|
919
|
+
} else {
|
|
920
|
+
this.debug(`Successfully bound ${log}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
callback(error);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
unbind(ep, cluster, target, callback) {
|
|
928
|
+
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
|
929
|
+
target = !target ? this.getCoordinator() : target;
|
|
930
|
+
|
|
931
|
+
this.debug(`Unbinding ${log}`);
|
|
932
|
+
ep.unbind(cluster, target, (error) => {
|
|
933
|
+
if (error) {
|
|
934
|
+
this.error(`Failed to unbind ${log} - (${error})`);
|
|
935
|
+
} else {
|
|
936
|
+
this.debug(`Successfully unbound ${log}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
callback(error);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
reset(mode, callback) {
|
|
944
|
+
try {
|
|
945
|
+
this.herdsman.reset(mode);
|
|
946
|
+
callback && callback();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.sendError(error);
|
|
949
|
+
this.error(`Failed to reset ${error.stack}`);
|
|
950
|
+
callback && callback(error);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async touchlinkReset(permitTime) {
|
|
955
|
+
try {
|
|
956
|
+
await this.herdsman.touchlinkFactoryResetFirst();
|
|
957
|
+
this.permitJoin(permitTime);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
this.sendError(error);
|
|
960
|
+
this.error(`Failed to touchlinkReset ${error.stack}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async getChannelsEnergy() {
|
|
965
|
+
const payload = {
|
|
966
|
+
dstaddr: 0x0,
|
|
967
|
+
dstaddrmode: 0x02,
|
|
968
|
+
channelmask: 0x07FFF800,
|
|
969
|
+
scanduration: 0x5,
|
|
970
|
+
scancount: 1,
|
|
971
|
+
nwkmanageraddr: 0x0000
|
|
972
|
+
};
|
|
973
|
+
const energyScan = this.herdsman.adapter.znp.waitFor(
|
|
974
|
+
2, // unpi_1.Constants.Type.AREQ,
|
|
975
|
+
5, // Subsystem.ZDO,
|
|
976
|
+
'mgmtNwkUpdateNotify'
|
|
977
|
+
);
|
|
978
|
+
await this.herdsman.adapter.znp.request(
|
|
979
|
+
0x5, // Subsystem.ZDO
|
|
980
|
+
'mgmtNwkUpdateReq',
|
|
981
|
+
payload,
|
|
982
|
+
energyScan.ID
|
|
983
|
+
);
|
|
984
|
+
const result = await energyScan.start().promise;
|
|
985
|
+
return result.payload;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
module.exports = ZigbeeController;
|