iobroker.zigbee 3.0.5 → 3.1.4

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/lib/utils.js CHANGED
@@ -88,11 +88,13 @@ function decimalToHex(decimal, padding) {
88
88
  }
89
89
 
90
90
  function getZbId(adapterDevId) {
91
- const idx = adapterDevId.indexOf('group');
92
- if (idx > 0) {
93
- return adapterDevId.substr(idx + 6);
91
+ const pieces = adapterDevId.split('.');
92
+ const piece = pieces.length > 2 ? pieces[2] : adapterDevId;
93
+ const idx = piece.indexOf('group_');
94
+ if (idx > -1) {
95
+ return Number(piece.substr(idx + 6));
94
96
  }
95
- return `0x${adapterDevId.split('.')[2]}`;
97
+ return `0x${piece}`;
96
98
  }
97
99
 
98
100
  function getAdId(adapter, id) {
@@ -197,4 +199,4 @@ exports.getDeviceIcon = getDeviceIcon;
197
199
  exports.getEntityInfo = getEntityInfo;
198
200
  exports.getNetAddress = getNetAddress;
199
201
  exports.byteArrayToString = byteArrayToString;
200
- exports.reverseByteString = reverseByteString;
202
+ exports.reverseByteString = reverseByteString;
@@ -45,10 +45,10 @@ class DelayedAction extends BaseExtension {
45
45
 
46
46
  onZigbeeEvent(data) {
47
47
  try {
48
- const device = data.device;
49
- // if (this.shouldAction(device, mappedDevice)) {
50
- this.doActions(device);
51
- // }
48
+ if (data && data.device) {
49
+ const device = data.device;
50
+ this.doActions(device);
51
+ }
52
52
  } catch (error) {
53
53
  this.sendError(error);
54
54
  this.error(
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const BaseExtension = require('./zbBaseExtension');
4
- const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
5
4
  const utils = require('./utils');
6
5
 
7
6
  // Some EndDevices should be pinged
@@ -19,20 +18,23 @@ const Hours25 = 1000 * 60 * 60 * 25;
19
18
  const MinAvailabilityTimeout = 300; // ping every 5 minutes with few devices
20
19
  const MaxAvailabilityTimeout = 1800; // ping every 30 minutes with many devices;
21
20
  const AverageTimeBetweenPings = 45; // on average, plan for 30 seconds between pings.
21
+ const pingClusters = {
22
+ c0a0: {id:'genBasic', attribute:'zclVersion'},
23
+ c0a5: {id:'genBasic', attribute:'modelId'},
24
+ c25a0: {id:'genOta', attribute:'upgradeServerId'},
25
+ };
22
26
 
23
27
  /**
24
28
  * This extensions pings devices to check if they are online.
25
29
  */
26
30
  class DeviceAvailability extends BaseExtension {
27
- constructor(zigbee, options) {
31
+ constructor(zigbee, options, config) {
28
32
  super(zigbee, options);
29
33
  this.availability_timeout = 300; // wait 5 min for live check
30
34
  this.timers = {};
31
35
  this.ping_counters = {};
32
36
  this.max_ping = 3;
33
37
  this.state = {};
34
- this.active_ping = true;
35
- this.forced_ping = true;
36
38
  this.forcedNonPingable = {};
37
39
  this.number_of_registered_devices = 0;
38
40
  // force publish availability for new devices
@@ -44,28 +46,34 @@ class DeviceAvailability extends BaseExtension {
44
46
  this.startDevicePingQueue = []; // simple fifo array for starting device pings
45
47
  this.startDevicePingTimeout = null; // handle for the timeout which empties the queue
46
48
  this.startDevicePingDelay = 500; // 200 ms delay between starting the ping timeout
49
+ this.startReadDelay = 0;
47
50
  this.name = 'DeviceAvailability';
48
51
  this.elevate_debug = false;
49
52
  this.isStarted = false;
53
+ this.active_ping = config.pingCluster != 'off';
54
+ this.max_ping = 5;
55
+ this.availability_timeout = Math.max(60, typeof config.pingTimeout == 'number' ? config.pingTimeout : 300);
56
+ this.startReadDelay = config.readAllAtStart ? Math.max(500, Math.min(10000, config.startReadDelay * 1000)) : 0;
57
+ this.debugDevices = [];
58
+ this.pingCluster = pingClusters[config.pingCluster] ? pingClusters[config.pingCluster] : {};
50
59
  }
51
60
 
52
- setOptions(options) {
53
- if (typeof options !== 'object') {
54
- return false;
55
- }
56
- if (options.disableActivePing) {
57
- this.active_ping = false;
58
- }
59
- if (options.disableForcedPing) {
60
- this.forced_ping = false;
61
- }
62
- if (typeof options.pingTimeout === 'number') {
63
- this.availability_timeout = Math.min(60, options.pingTimeout);
64
- }
65
- if (typeof options.pingCount === 'number') {
66
- this.max_ping = Math.min(2, options.pingCount);
61
+ checkDebugDevice(dev) {
62
+ if (typeof dev != 'string' || dev == '') return false;
63
+ if (this.debugDevices === undefined) return false;
64
+ else
65
+ {
66
+ for (const addressPart of this.debugDevices) {
67
+ if (typeof dev === 'string' && dev.includes(addressPart)) {
68
+ return true;
69
+ }
70
+ }
67
71
  }
68
- return true;
72
+ return false;
73
+ }
74
+
75
+ setLocalVariable(name, value) {
76
+ this[name] = value;
69
77
  }
70
78
 
71
79
  isPingable(device) {
@@ -89,7 +97,6 @@ class DeviceAvailability extends BaseExtension {
89
97
  if (!this.isStarted) return;
90
98
  this.debug(`register device Ping for ${JSON.stringify(device.ieeeAddr)}`);
91
99
  this.forcedNonPingable[device.ieeeAddr] = false;
92
- // this.warn(`Called registerDevicePing for '${device}' of '${entity}'`);
93
100
  if (!this.isPingable(device)) {
94
101
  return;
95
102
  }
@@ -119,7 +126,6 @@ class DeviceAvailability extends BaseExtension {
119
126
  }
120
127
 
121
128
  async startDevicePing() {
122
- // this.warn(JSON.stringify(this));
123
129
  if (!this.isStarted) return;
124
130
  this.startDevicePingTimeout = null;
125
131
  const item = this.startDevicePingQueue.shift();
@@ -132,61 +138,104 @@ class DeviceAvailability extends BaseExtension {
132
138
  }
133
139
  }
134
140
 
141
+ async startNotPingable(device) {
142
+ this.publishAvailability(device, true);
143
+ this.timers[device.ieeeAddr] = setInterval(() =>
144
+ this.handleIntervalNotPingable(device), utils.secondsToMilliseconds(this.availability_timeout));
145
+ }
146
+
135
147
  async onZigbeeStarted() {
136
148
  // As some devices are not checked for availability (e.g. battery powered devices)
137
149
  // we mark these device as online by default.
138
150
  // NOTE: The start of active pings for pingable devices is done separately,
139
151
  // triggered by the 'new device' event to ensure that they are handled
140
152
  // identically on reconnect, disconnect and new pair (as)
141
- const clients = await this.zigbee.getClients();
153
+ const clients = await this.zigbee.getClientIterator();
154
+ const readables = [];
155
+
142
156
  this.isStarted = true;
143
- // this.warn('onZigbeeStarted called');
157
+ this.debug('onZigbeeStarted called');
158
+
144
159
  for (const device of clients) {
145
- if (this.isPingable(device)) {
160
+ if (this.isPingable(device) && this.active_ping) {
161
+ readables.push(device);
146
162
  // this.setTimerPingable(device);
147
163
  } else {
148
- // this.warn(`Setting '${device.ieeeAddr}' as available - battery driven`);
164
+ this.debug(`Setting '${device.ieeeAddr}' as available - battery driven or no active availability check`);
149
165
  this.publishAvailability(device, true);
150
166
  this.timers[device.ieeeAddr] = setInterval(() =>
151
167
  this.handleIntervalNotPingable(device), utils.secondsToMilliseconds(this.availability_timeout));
152
168
  }
153
169
  }
170
+ if (this.startReadDelay > 0 && readables.length > 0) {
171
+ this.info(`Triggering device_query on ${readables.length} devices in ${this.startReadDelay / 1000} seconds.`)
172
+ setTimeout(() => {
173
+ readables.forEach(device => this.zigbee.doDeviceQuery(device, Date().now, false));
174
+ }, this.startReadDelay)
175
+ }
154
176
  }
155
177
 
156
178
  async handleIntervalPingable(device, entity) {
157
179
  if (!this.isStarted) return;
180
+ if (!this.active_ping) {
181
+ return await this.handleIntervalnonPingable();
182
+ }
183
+ const has_elevated_debug = this.checkDebugDevice(device.ieeeAddr)
184
+
158
185
  const ieeeAddr = device.ieeeAddr;
159
186
  const resolvedEntity = entity ? entity : await this.zigbee.resolveEntity(ieeeAddr);
160
187
  if (!resolvedEntity) {
161
- this.warn(`Stop pinging '${ieeeAddr}' ${device.modelID}, device is not known anymore`);
188
+ const msg = `Stop pinging '${ieeeAddr}' ${device.modelID}, device is not known anymore`
189
+ if (has_elevated_debug) this.warn(`ELEVADED: ${msg}`);
190
+ else this.info(msg);
162
191
  return;
163
192
  }
164
193
  if (this.isPingable(device)) {
194
+ const pt = Date.now();
165
195
  let pingCount = this.ping_counters[device.ieeeAddr];
166
196
  if (pingCount === undefined) {
167
197
  this.ping_counters[device.ieeeAddr] = {failed: 0, reported: 0};
168
198
  pingCount = {failed: 0, reported: 0};
169
199
  }
170
200
  try {
171
- //this.warn(`Pinging '${ieeeAddr}' (${device.modelID})`)
172
- await device.ping();
201
+ if (!this.pingCluster || !this.pingCluster.hasOwnProperty('id')) {
202
+ this.debug(`Pinging '${ieeeAddr}' (${device.modelID}) via ZH Ping`)
203
+ await device.ping();
204
+ }
205
+ else {
206
+ const zclData = {};
207
+ zclData[this.pingCluster.attribute] = {};
208
+ this.debug(`Pinging '${ieeeAddr}' (${device.modelID}) via ZCL Read with ${this.pingCluster.id}:${this.pingCluster.attribute}`)
209
+ await this.zigbee.publish(device, this.pingCluster.id, 'read', zclData, null, undefined, 'foundation');
210
+ }
173
211
  this.publishAvailability(device, true);
174
- //this.warn(`Successfully pinged ${ieeeAddr} (${device.modelID})`);
212
+ if (has_elevated_debug) this.warn(`ELEVATED : Successfully pinged ${ieeeAddr} (${device.modelID}) in ${Date.now()-pt} ms`);
175
213
  this.setTimerPingable(device, 1);
176
214
  this.ping_counters[device.ieeeAddr].failed = 0;
177
215
  } catch (error) {
216
+ if (error && error.message && error.message.includes('UNSUPPORTED_ATTRIBUTE')) {
217
+ // this error is acceptable, as it is raised off an answer of the device.
218
+ this.publishAvailability(device, true);
219
+ if (has_elevated_debug) this.warn(`ELEVATED : Successfully pinged ${ieeeAddr} (${device.modelID}) in ${Date.now()-pt} ms`);
220
+ this.setTimerPingable(device, 1);
221
+ this.ping_counters[device.ieeeAddr].failed = 0;
222
+ return;
223
+ }
224
+ if (has_elevated_debug) this.warn(`ELEVATED : Failed to ping ${ieeeAddr} (${device.modelID}) after ${Date.now()-pt} ms${error && error.message ? ' - '+error.message : ''}`);
178
225
  this.publishAvailability(device, false);
179
226
  if (pingCount.failed++ <= this.max_ping) {
227
+ const msg = `Failed to ping ${ieeeAddr} ${device.modelID} for ${JSON.stringify(pingCount)} attempts`
180
228
  if (pingCount.failed < 2 && pingCount.reported < this.max_ping) {
181
- this.info(`Failed to ping ${ieeeAddr} ${device.modelID}`);
229
+ if (has_elevated_debug) this.warn(`ELEVATED: ${msg}`); else this.info(msg);
182
230
  pingCount.reported++;
183
231
  } else {
184
- this.debug(`Failed to ping ${ieeeAddr} ${device.modelID} on ${pingCount} consecutive attempts`);
232
+ this.debug(msg);
185
233
  }
186
234
  this.setTimerPingable(device, pingCount.failed);
187
235
  this.ping_counters[device.ieeeAddr] = pingCount;
188
236
  } else {
189
- this.info(`Stopping to ping ${ieeeAddr} ${device.modelID} after ${pingCount.failed} ping attempts`);
237
+ const msg = `Stopping to ping ${ieeeAddr} ${device.modelID} after ${pingCount.failed} ping attempts`;
238
+ if (has_elevated_debug) this.warn(`ELEVATED ${msg}`); else this.info(msg);
190
239
  }
191
240
  }
192
241
  }
@@ -200,11 +249,15 @@ class DeviceAvailability extends BaseExtension {
200
249
  }
201
250
 
202
251
  const ago = Date.now() - entity.device.lastSeen;
203
- this.debug(`Non-pingable device ${entity.device.ieeeAddr} ${entity.device.modelID} was last seen '${ago / 1000}' seconds ago.`);
204
252
 
205
253
  if (ago > Hours25) {
206
254
  this.publishAvailability(entity.device, false);
255
+ const msg = `Non-pingable device ${entity.device.ieeeAddr} ${entity.device.modelID} was last seen over 25 hrs ago - setting it offline.`
256
+ if (this.checkDebugDevice(device.ieeeAddr)) this.warn(`ELEVATED: ${msg}`); else this.debug(msg);
257
+ return;
207
258
  }
259
+ const msg = `Non-pingable device ${entity.device.ieeeAddr} ${entity.device.modelID} was last seen '${ago / 1000}' seconds ago.`
260
+ if (this.checkDebugDevice(device.ieeeAddr)) this.warn(`ELEVATED: ${msg}`); else this.debug(msg);
208
261
  }
209
262
 
210
263
  setTimerPingable(device, factor) {
@@ -236,7 +289,7 @@ class DeviceAvailability extends BaseExtension {
236
289
  for (const key of toZigbeeCandidates) {
237
290
  const converter = entity.mapped.toZigbee.find((tz) => tz.key.includes(key));
238
291
  if (converter && !used.includes(converter)) {
239
- await converter.convertGet(device.endpoints[0], key, {});
292
+ await converter.convertGet(device.endpoints[0], key, {device:entity.device});
240
293
  used.push(converter);
241
294
  }
242
295
  }
@@ -266,7 +319,7 @@ class DeviceAvailability extends BaseExtension {
266
319
  }
267
320
  }
268
321
 
269
- onZigbeeEvent(data) {
322
+ async onZigbeeEvent(data) {
270
323
  const device = data.device;
271
324
  if (!device || this.forcedNonPingable[device.ieeeAddr]) {
272
325
  return;
@@ -287,16 +340,19 @@ class DeviceAvailability extends BaseExtension {
287
340
 
288
341
  const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
289
342
  if (online && data.type === 'deviceAnnounce' && !utils.isIkeaTradfriDevice(device)) {
290
- /**
291
- * In case the device is powered off AND on within the availability timeout,
292
- * zigbee2qmtt does not detect the device as offline (device is still marked online).
293
- * When a device is turned on again the state could be out of sync.
294
- * https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168
295
- * endDeviceAnnce is typically send when a device comes online.
296
- *
297
- * This isn't needed for TRADFRI devices as they already send the state themself.
298
- */
299
- this.onReconnect(device);
343
+ const entity = await this.zigbee.resolveEntity(device);
344
+ if (entity && entity.mapped) {
345
+ /**
346
+ * In case the device is powered off AND on within the availability timeout,
347
+ * zigbee2qmtt does not detect the device as offline (device is still marked online).
348
+ * When a device is turned on again the state could be out of sync.
349
+ * https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168
350
+ * endDeviceAnnce is typically send when a device comes online.
351
+ *
352
+ * This isn't needed for TRADFRI devices as they already send the state themself.
353
+ */
354
+ this.onReconnect(device);
355
+ }
300
356
  }
301
357
  }
302
358
  }
@@ -90,6 +90,7 @@ class DeviceConfigure extends BaseExtension {
90
90
  }
91
91
 
92
92
  onZigbeeEvent(data, mappedDevice) {
93
+ if (!mappedDevice || !data.device) return;
93
94
  try {
94
95
  const device = data.device;
95
96
  const com = this.configureOnMessageAttempts[device.ieeeAddr];
@@ -100,7 +101,13 @@ class DeviceConfigure extends BaseExtension {
100
101
  this.info('Configure on Message for ' + device.ieeeAddr);
101
102
  this.doConfigure(device, mappedDevice);
102
103
  }
104
+ return;
105
+ }
106
+ // check if the event is a 'deviceInterview successful' Event
107
+ if (data.type == 'deviceJoined' || data.type == 'deviceInterview' && data.status == 'successful') {
108
+ this.PushDeviceToQueue(device, mappedDevice);
103
109
  }
110
+
104
111
  } catch (error) {
105
112
  this.sendError(error);
106
113
  this.error(`Failed to DeviceConfigure.onZigbeeEvent (${error && error.message ? error.message : 'no error message'})`);
@@ -2,7 +2,6 @@
2
2
 
3
3
  const BaseExtension = require('./zbBaseExtension');
4
4
  const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
5
-
6
5
  class DeviceEvent extends BaseExtension {
7
6
  constructor(zigbee, options) {
8
7
  super(zigbee, options);
@@ -10,8 +9,10 @@ class DeviceEvent extends BaseExtension {
10
9
  }
11
10
 
12
11
  async onZigbeeStarted() {
13
- for (const device of await this.zigbee.getClients()) {
14
- await this.callOnEvent(device, 'start', {});
12
+ for (const device of await this.zigbee.getClientIterator()) {
13
+ const entity = await this.zigbee.resolveEntity(device);
14
+
15
+ await this.callOnEvent(device, 'start', {device, options:entity.options || {}});
15
16
  }
16
17
  }
17
18
 
@@ -21,7 +22,7 @@ class DeviceEvent extends BaseExtension {
21
22
  }
22
23
 
23
24
  async onZigbeeEvent(data, mappedDevice) {
24
- if (data.device) {
25
+ if (data && data.device && data.type) {
25
26
  this.callOnEvent(data.device, data.type, data, mappedDevice);
26
27
  }
27
28
  }
@@ -29,7 +30,7 @@ class DeviceEvent extends BaseExtension {
29
30
  async stop() {
30
31
  if (this.zigbee.getClients() > 0) {
31
32
  for (const device of await this.zigbee.getClients()) {
32
- await this.callOnEvent(device, 'stop', {});
33
+ await this.callOnEvent(device, 'stop', {ieeeAddr:device.ieeeAddr});
33
34
  }
34
35
  }
35
36
  }
@@ -38,9 +39,41 @@ class DeviceEvent extends BaseExtension {
38
39
  if (!mappedDevice) {
39
40
  mappedDevice = await zigbeeHerdsmanConverters.findByDevice(device);
40
41
  }
42
+ const baseData = {device, deviceExposeChanged: function() { return; }, options: data.options || {}, state: data.state || {}}
43
+ const eventData = {
44
+ type,
45
+ }
41
46
 
42
- if (mappedDevice && mappedDevice.onEvent) {
43
- mappedDevice.onEvent(type, data, device,mappedDevice.options,'{}');
47
+ switch (type) {
48
+ case 'start':
49
+ case 'deviceNetworkAddressChanged':
50
+ case 'deviceAnnounce':
51
+ case `deviceJoined`:
52
+ {
53
+ eventData.data = baseData;
54
+ break;
55
+ }
56
+ case 'stop':
57
+ eventData.data = { ieeeAddr:device.ieeeAddr };
58
+ break;
59
+ case 'deviceInterview':
60
+ eventData.data = {...baseData,status: data.status};
61
+ break;
62
+ case 'deviceOptionsChanged':
63
+ // NOTE: This does not currently work. OptionsChange is not yet defined.
64
+ eventData.data = {...baseData, from:data.from || {}, to:data.to || {}};
65
+ break;
66
+ }
67
+
68
+
69
+ if (mappedDevice && mappedDevice.onEvent && eventData.data) {
70
+ this.warn(`calling onEvent for ${eventData.type} on ${device.ieeeAddr}`);
71
+ try {
72
+ mappedDevice.onEvent(eventData);
73
+ }
74
+ catch (error) {
75
+ this.warn(`Error in onEvent: ${error && error.message ? error.message : 'no message'}`);
76
+ }
44
77
  }
45
78
  }
46
79
  }