iobroker.zigbee 3.1.2 → 3.1.5
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/README.md +19 -1
- package/admin/admin.js +1020 -729
- package/admin/index_m.html +55 -155
- package/admin/tab_m.html +161 -242
- package/io-package.json +42 -39
- package/lib/DeviceDebug.js +24 -2
- package/lib/binding.js +7 -7
- package/lib/commands.js +319 -255
- package/lib/developer.js +1 -1
- package/lib/devices.js +2 -2
- package/lib/exclude.js +1 -1
- package/lib/exposes.js +54 -24
- package/lib/groups.js +26 -28
- package/lib/localConfig.js +8 -8
- package/lib/networkmap.js +10 -2
- package/lib/statescontroller.js +135 -91
- package/lib/zbDelayedAction.js +4 -4
- package/lib/zbDeviceAvailability.js +32 -33
- package/lib/zbDeviceConfigure.js +7 -0
- package/lib/zbDeviceEvent.js +38 -6
- package/lib/zigbeecontroller.js +98 -46
- package/main.js +43 -57
- package/package.json +6 -9
- package/lib/tools.js +0 -55
package/lib/statescontroller.js
CHANGED
|
@@ -5,19 +5,11 @@ const { EventEmitter } = require('events');
|
|
|
5
5
|
const statesMapping = require('./devices');
|
|
6
6
|
const { getAdId, getZbId } = require('./utils');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
-
const axios = require('axios');
|
|
9
8
|
const localConfig = require('./localConfig');
|
|
10
|
-
//const { deviceAddCustomCluster } = require('zigbee-herdsman-converters/lib/modernExtend');
|
|
11
|
-
//const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
|
|
12
|
-
//const { runInThisContext } = require('vm');
|
|
13
|
-
//const { time } = require('console');
|
|
14
|
-
const { exec } = require('child_process');
|
|
15
|
-
const { tmpdir } = require('os');
|
|
16
9
|
const path = require('path');
|
|
17
|
-
const
|
|
10
|
+
const axios = require('axios');
|
|
18
11
|
const zigbeeHerdsmanConvertersUtils = require('zigbee-herdsman-converters/lib/utils');
|
|
19
12
|
|
|
20
|
-
|
|
21
13
|
class StatesController extends EventEmitter {
|
|
22
14
|
constructor(adapter) {
|
|
23
15
|
super();
|
|
@@ -84,7 +76,6 @@ class StatesController extends EventEmitter {
|
|
|
84
76
|
async AddModelFromHerdsman(device, model) {
|
|
85
77
|
const namespace = `${this.adapter.name}.admin`;
|
|
86
78
|
|
|
87
|
-
// this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
|
|
88
79
|
if (this.localConfig.getOverrideWithTargetAndKey(model, 'legacy', true)) {
|
|
89
80
|
this.debug('Applying legacy definition for ' + model);
|
|
90
81
|
await this.addLegacyDevice(model);
|
|
@@ -213,7 +204,7 @@ class StatesController extends EventEmitter {
|
|
|
213
204
|
async toggleDeviceDebug(id) {
|
|
214
205
|
const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
|
|
215
206
|
if (!arr) {
|
|
216
|
-
this.warn(`unable to toggle debug for device ${id}: there was no
|
|
207
|
+
this.warn(`unable to toggle debug for device ${id}: there was no matc (${JSON.stringify(arr)}) `);
|
|
217
208
|
return this.debugDevices;
|
|
218
209
|
}
|
|
219
210
|
if (arr[1] === undefined) {
|
|
@@ -278,10 +269,9 @@ class StatesController extends EventEmitter {
|
|
|
278
269
|
this.emit('device_debug', { ID:debugId, data: { ID: deviceId, flag:'01' }, message:message});
|
|
279
270
|
} else
|
|
280
271
|
if (this.debugActive) this.debug(`User stateChange ${id} ${JSON.stringify(state)}`);
|
|
281
|
-
// const stateKey = id.split('.')[3];
|
|
282
272
|
const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(id);
|
|
283
273
|
if (arr[1] === undefined) {
|
|
284
|
-
|
|
274
|
+
this.debug(`unable to extract id from state ${id}`);
|
|
285
275
|
return;
|
|
286
276
|
}
|
|
287
277
|
const stateKey = arr[1];
|
|
@@ -295,16 +285,7 @@ class StatesController extends EventEmitter {
|
|
|
295
285
|
if (this.debugActive) this.debug('State Change detected on deactivated Device - ignored');
|
|
296
286
|
return;
|
|
297
287
|
}
|
|
298
|
-
|
|
299
|
-
/*
|
|
300
|
-
if (model === 'group' && typeof deviceId == 'number' && Boolean(deviceId)) {
|
|
301
|
-
const options = this.localConfig.getOptions(`group_${deviceId}`);
|
|
302
|
-
options.isActive == (obj.common !== null);
|
|
303
|
-
this.publishFromState(deviceId, model, stateKey, state, options, debugId);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
*/
|
|
307
|
-
// handle send_payload here
|
|
288
|
+
|
|
308
289
|
if (model && model.id === 'device_query') {
|
|
309
290
|
if (this.query_device_block.indexOf(deviceId) > -1 && !state.source.includes('.admin.')) {
|
|
310
291
|
this.info(`Device query for '${deviceId}' blocked - device query timeout has not elapsed yet.`);
|
|
@@ -477,10 +458,10 @@ class StatesController extends EventEmitter {
|
|
|
477
458
|
if (stateDesc.id === 'send_payload') {
|
|
478
459
|
try {
|
|
479
460
|
const json_value = JSON.parse(value);
|
|
480
|
-
const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel};
|
|
461
|
+
const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel, acknowledge:true};
|
|
481
462
|
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { flag: '04' ,payload:value ,states:[{id:stateDesc.id, value:json_value, payload:'none'}], IO:false }});
|
|
482
463
|
|
|
483
|
-
this.emit('send_payload', payload, debugID);
|
|
464
|
+
this.emit('send_payload', payload, debugID, has_elevated_debug);
|
|
484
465
|
} catch (error) {
|
|
485
466
|
const message = `send_payload: ${value} does not parse as JSON Object : ${error.message}`;
|
|
486
467
|
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { error: 'EXSEND' ,states:[{id:stateDesc.id, value:value, payload:error.message}], IO:false }, message:message});
|
|
@@ -571,8 +552,8 @@ class StatesController extends EventEmitter {
|
|
|
571
552
|
}
|
|
572
553
|
|
|
573
554
|
|
|
574
|
-
setDeviceActivated(id,
|
|
575
|
-
this.adapter.extendObject(id, {common: {deactivated:
|
|
555
|
+
setDeviceActivated(id, inActive) {
|
|
556
|
+
this.adapter.extendObject(id, {common: {deactivated: inActive, color:inActive ? '#888888' : null, statusStates: inActive ? null : {onlineId:`${id}.available`} }});
|
|
576
557
|
}
|
|
577
558
|
|
|
578
559
|
storeDeviceName(id, name) {
|
|
@@ -804,13 +785,10 @@ class StatesController extends EventEmitter {
|
|
|
804
785
|
async applyLegacyDevices() {
|
|
805
786
|
const legacyModels = await this.localConfig.getLegacyModels();
|
|
806
787
|
const modelarr1 = [];
|
|
807
|
-
//this.warn('devices are' + modelarr1.join(','));
|
|
808
788
|
statesMapping.devices.forEach(item => modelarr1.push(item.models));
|
|
809
|
-
//this.warn('legacy models are ' + JSON.stringify(legacyModels));
|
|
810
789
|
statesMapping.setLegacyDevices(legacyModels);
|
|
811
790
|
const modelarr2 = [];
|
|
812
791
|
statesMapping.devices.forEach(item => modelarr2.push(item.models));
|
|
813
|
-
//this.warn('devices are' + modelarr2.join(','));
|
|
814
792
|
}
|
|
815
793
|
|
|
816
794
|
async addLegacyDevice(model) {
|
|
@@ -856,7 +834,6 @@ class StatesController extends EventEmitter {
|
|
|
856
834
|
} else {
|
|
857
835
|
const model_modif = model.replace(/\//g, '-');
|
|
858
836
|
const pathToAdminIcon = `img/${model_modif}.png`;
|
|
859
|
-
// await this.fetchIcon(icon, '')
|
|
860
837
|
|
|
861
838
|
|
|
862
839
|
if (icon.startsWith('http')) {
|
|
@@ -869,31 +846,82 @@ class StatesController extends EventEmitter {
|
|
|
869
846
|
}
|
|
870
847
|
}
|
|
871
848
|
|
|
872
|
-
this.adapter.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
native: {id: dev_id}
|
|
884
|
-
}, () => {
|
|
885
|
-
// update type and icon
|
|
849
|
+
const obj = await this.adapter.getObjectAsync(id);
|
|
850
|
+
|
|
851
|
+
const myCommon = {
|
|
852
|
+
name: __dev_name,
|
|
853
|
+
type: model,
|
|
854
|
+
icon,
|
|
855
|
+
modelIcon: modelIcon,
|
|
856
|
+
color: (obj && obj.common && obj.common.deactivated) ? `#888888` : null,
|
|
857
|
+
statusStates: (obj && obj.common && obj.common.deactivated) ? null : {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
858
|
+
}
|
|
859
|
+
if (obj) {
|
|
886
860
|
this.adapter.extendObject(id, {
|
|
887
|
-
common:
|
|
888
|
-
name: __dev_name,
|
|
889
|
-
type: model,
|
|
890
|
-
icon,
|
|
891
|
-
modelIcon: modelIcon,
|
|
892
|
-
color: null,
|
|
893
|
-
statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
894
|
-
}
|
|
861
|
+
common: myCommon
|
|
895
862
|
}, callback);
|
|
896
|
-
}
|
|
863
|
+
} else {
|
|
864
|
+
this.adapter.setObjectNotExists(id, {
|
|
865
|
+
type: 'device',
|
|
866
|
+
// actually this is an error, so device.common has no attribute type. It must be in native part
|
|
867
|
+
common: myCommon,
|
|
868
|
+
native: {id: dev_id}
|
|
869
|
+
}, callback);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async streamToBufferFetch(readableStream) {
|
|
874
|
+
const reader = readableStream.getReader();
|
|
875
|
+
const chunks = [];
|
|
876
|
+
let done, value;
|
|
877
|
+
try {
|
|
878
|
+
while (true) {
|
|
879
|
+
const result = await reader.read();
|
|
880
|
+
done = result.done;
|
|
881
|
+
value = result.value;
|
|
882
|
+
if (done) break;
|
|
883
|
+
if (value) chunks.push(Buffer.from(value));
|
|
884
|
+
}
|
|
885
|
+
return Buffer.concat(chunks);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
this.error(`error getting buffer from stream: ${err && err.message ? err.message : 'no reason given'}`);
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async fetchIcon(url, image_path) {
|
|
893
|
+
const namespace = `${this.adapter.name}.admin`;
|
|
894
|
+
try {
|
|
895
|
+
return new Promise((resolve, reject) => {
|
|
896
|
+
fetch(url)
|
|
897
|
+
.then(async response => {
|
|
898
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
899
|
+
const data = await this.streamToBufferFetch(response.body);
|
|
900
|
+
this.adapter.writeFile(namespace, image_path, data, (err) => {
|
|
901
|
+
if (err) {
|
|
902
|
+
this.error(`error writing ${image_path} to admin: ${err.message ? err.message : 'no message given'}`);
|
|
903
|
+
reject(err);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
this.info(`downloaded ${url} to ${image_path}.`);
|
|
907
|
+
resolve();
|
|
908
|
+
});
|
|
909
|
+
})
|
|
910
|
+
.catch(err => {
|
|
911
|
+
this.warn(`error downloading icon ${err && err.message ? err.message : 'no message given'}`);
|
|
912
|
+
reject(err);
|
|
913
|
+
})
|
|
914
|
+
.finally(() => {
|
|
915
|
+
const idx = this.ImagesToDownload.indexOf(url);
|
|
916
|
+
if (idx > -1) {
|
|
917
|
+
this.ImagesToDownload.splice(idx, 1);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
this.warn(`error fetching ${url} : ${error && error.message ? error.message : 'no reason given'}`)
|
|
924
|
+
}
|
|
897
925
|
}
|
|
898
926
|
|
|
899
927
|
async streamToBuffer(readableStream) {
|
|
@@ -921,12 +949,6 @@ class StatesController extends EventEmitter {
|
|
|
921
949
|
});
|
|
922
950
|
}
|
|
923
951
|
|
|
924
|
-
async fetchIcon(url, image_path) {
|
|
925
|
-
const response = await fetch(url);
|
|
926
|
-
const data = await this.streamToBuffer(response.body);
|
|
927
|
-
fs.writeFileSync('/opt/iobroker/iobroker-data/zigbee_0/test.png', response.data);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
952
|
async downloadIcon(url, image_path) {
|
|
931
953
|
try {
|
|
932
954
|
const namespace = `${this.adapter.name}.admin`;
|
|
@@ -971,7 +993,6 @@ class StatesController extends EventEmitter {
|
|
|
971
993
|
}
|
|
972
994
|
});
|
|
973
995
|
}
|
|
974
|
-
|
|
975
996
|
CleanupRequired(set) {
|
|
976
997
|
try {
|
|
977
998
|
if (typeof set === 'boolean') this.cleanupRequired = set;
|
|
@@ -1060,8 +1081,10 @@ class StatesController extends EventEmitter {
|
|
|
1060
1081
|
const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
|
|
1061
1082
|
|
|
1062
1083
|
const message = `message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`;
|
|
1063
|
-
if (has_elevated_debug)
|
|
1064
|
-
|
|
1084
|
+
if (has_elevated_debug)
|
|
1085
|
+
this.emit('device_debug', { ID:debugId, data: { deviceID: devId, flag:'03', IO:true }, message:message});
|
|
1086
|
+
else
|
|
1087
|
+
if (this.debugActive) this.debug(message);
|
|
1065
1088
|
if (!devStates) {
|
|
1066
1089
|
const message = `no device states for device ${devId} type '${model}'`;
|
|
1067
1090
|
if (has_elevated_debug)this.emit('device_debug', { ID:debugId, data: { error:'NOSTATE',states:[{ id:'--', value:'--', payload:payload}], IO:true }, message:message});
|
|
@@ -1092,7 +1115,7 @@ class StatesController extends EventEmitter {
|
|
|
1092
1115
|
let stateID = statedesc.id;
|
|
1093
1116
|
|
|
1094
1117
|
const message = `value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`;
|
|
1095
|
-
if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'
|
|
1118
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'04', IO:true }, message});
|
|
1096
1119
|
else if (this.debugActive) this.debug(message);
|
|
1097
1120
|
|
|
1098
1121
|
const common = {
|
|
@@ -1179,35 +1202,57 @@ class StatesController extends EventEmitter {
|
|
|
1179
1202
|
}
|
|
1180
1203
|
}
|
|
1181
1204
|
|
|
1182
|
-
async processConverters(converters, devId, model, mappedModel, message, meta, debugId) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1205
|
+
async processConverters(converters, devId, model, mappedModel, message, meta, debugId, has_elevated_debug) {
|
|
1206
|
+
let cnt = 0;
|
|
1207
|
+
const publish = (payload, dID) => {
|
|
1208
|
+
if (typeof payload === 'object' && Object.keys(payload).length > 0) {
|
|
1209
|
+
this.publishToState(devId, model, payload,dID);
|
|
1210
|
+
}
|
|
1211
|
+
else if (has_elevated_debug)
|
|
1212
|
+
this.emit('device_debug', {ID:debugId,data: { error:`NOVAL`, IO:true }, message:` payload ${JSON.stringify(payload)} is empty`})
|
|
1213
|
+
};
|
|
1214
|
+
const options = await new Promise((resolve, reject) => {
|
|
1215
|
+
this.collectOptions(devId, model, false, (options) => {
|
|
1216
|
+
resolve(options);
|
|
1194
1217
|
});
|
|
1218
|
+
});
|
|
1195
1219
|
|
|
1196
|
-
|
|
1220
|
+
const chain = [];
|
|
1221
|
+
for (const converter of converters) {
|
|
1222
|
+
const idx = cnt++;
|
|
1223
|
+
chain.push(new Promise((resolve) => {
|
|
1224
|
+
if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}a`, IO:true }, message:`converter ${cnt} : Cluster ${converter.cluster}`})
|
|
1197
1225
|
const payloadConv = converter.convert(mappedModel, message, publish, options, meta);
|
|
1226
|
+
const metapost = meta ? {
|
|
1227
|
+
deviceIEEE: meta.device ? meta.device.ieeeAddr : 'no device',
|
|
1228
|
+
deviceModelId: meta.device ? meta.device.ModelId : 'no device',
|
|
1229
|
+
logger: meta.logger ? (meta.logger.constructor ? meta.logger.constructor.name : 'not a class') : 'undefined',
|
|
1230
|
+
state : meta.state
|
|
1231
|
+
} : 'undefined';
|
|
1232
|
+
if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${idx}b`, IO:true }, message:` data: ${safeJsonStringify(message.data)} options: ${safeJsonStringify(options)} meta:${safeJsonStringify(metapost)} result:${safeJsonStringify(payloadConv)}`})
|
|
1198
1233
|
if (typeof payloadConv === 'object') {
|
|
1199
1234
|
resolve(payloadConv);
|
|
1200
1235
|
}
|
|
1201
|
-
|
|
1236
|
+
else resolve({});
|
|
1237
|
+
}));
|
|
1238
|
+
}
|
|
1239
|
+
const candidates = await Promise.all(chain);
|
|
1240
|
+
const payload = {};
|
|
1202
1241
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1242
|
+
for (const candidate of candidates) {
|
|
1243
|
+
for (const key in candidate)
|
|
1244
|
+
payload[key] = candidate[key];
|
|
1245
|
+
}
|
|
1206
1246
|
|
|
1207
|
-
|
|
1247
|
+
if (Object.keys(payload).length > 0 && Object.keys(options).length > 0) {
|
|
1248
|
+
const premsg = `candidates: ${JSON.stringify(candidates)} => payload ${JSON.stringify(payload)}`
|
|
1249
|
+
this.postProcessConvertedFromZigbeeMessage(mappedModel, payload, options, null);
|
|
1250
|
+
if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}d`, IO:true }, message:`${premsg} => processed payload : ${JSON.stringify(payload)}`})
|
|
1208
1251
|
}
|
|
1209
|
-
|
|
1252
|
+
else if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}c`, IO:true }, message:`candidates: ${JSON.stringify(candidates)} => payload ${JSON.stringify(payload)}`})
|
|
1210
1253
|
|
|
1254
|
+
publish(payload, debugId);
|
|
1255
|
+
}
|
|
1211
1256
|
|
|
1212
1257
|
async onZigbeeEvent(type, entity, message) {
|
|
1213
1258
|
if (this.debugActive) this.debug(`Type ${type} device ${safeJsonStringify(entity)} incoming event: ${safeJsonStringify(message)}`);
|
|
@@ -1293,9 +1338,6 @@ class StatesController extends EventEmitter {
|
|
|
1293
1338
|
}
|
|
1294
1339
|
}
|
|
1295
1340
|
|
|
1296
|
-
// publish raw event to "from_zigbee"
|
|
1297
|
-
// some cleanup
|
|
1298
|
-
|
|
1299
1341
|
this.publishToState(devId, model, {msg_from_zigbee: safeJsonStringify(msgForState)}, -1);
|
|
1300
1342
|
|
|
1301
1343
|
if (!entity.mapped) {
|
|
@@ -1311,6 +1353,11 @@ class StatesController extends EventEmitter {
|
|
|
1311
1353
|
Array.isArray(c.type) ? c.type.includes('attributeReport') : c.type === 'attributeReport'));
|
|
1312
1354
|
}
|
|
1313
1355
|
|
|
1356
|
+
if (has_elevated_debug) {
|
|
1357
|
+
const message = `${converters.length} converter${converters.length > 1 ? 's' : ''} available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`
|
|
1358
|
+
this.emit('device_debug', { ID:debugId, data: { flag:'02', IO:true }, message:message})
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1314
1361
|
if (!converters.length) {
|
|
1315
1362
|
if (type !== 'readResponse' && type !== 'commandQueryNextImageRequest') {
|
|
1316
1363
|
const message = `No converter available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`;
|
|
@@ -1322,7 +1369,7 @@ class StatesController extends EventEmitter {
|
|
|
1322
1369
|
|
|
1323
1370
|
meta.state = { state: '' }; // for tuya
|
|
1324
1371
|
|
|
1325
|
-
this.processConverters(converters, devId, model, mappedModel, message, meta, debugId)
|
|
1372
|
+
this.processConverters(converters, devId, model, mappedModel, message, meta, debugId, has_elevated_debug)
|
|
1326
1373
|
.catch((error) => {
|
|
1327
1374
|
// 'Error: Expected one of: 0, 1, got: 'undefined''
|
|
1328
1375
|
if (cluster !== '64529') {
|
|
@@ -1336,9 +1383,6 @@ class StatesController extends EventEmitter {
|
|
|
1336
1383
|
async stop() {
|
|
1337
1384
|
this.localConfig.retainData();
|
|
1338
1385
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
1386
|
}
|
|
1343
1387
|
|
|
1344
|
-
module.exports = StatesController;
|
|
1388
|
+
module.exports = StatesController;
|
package/lib/zbDelayedAction.js
CHANGED
|
@@ -45,10 +45,10 @@ class DelayedAction extends BaseExtension {
|
|
|
45
45
|
|
|
46
46
|
onZigbeeEvent(data) {
|
|
47
47
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,6 +18,11 @@ 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.
|
|
@@ -31,8 +35,6 @@ class DeviceAvailability extends BaseExtension {
|
|
|
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
|
|
@@ -48,34 +50,14 @@ class DeviceAvailability extends BaseExtension {
|
|
|
48
50
|
this.name = 'DeviceAvailability';
|
|
49
51
|
this.elevate_debug = false;
|
|
50
52
|
this.isStarted = false;
|
|
51
|
-
this.active_ping =
|
|
52
|
-
this.forced_ping = false;
|
|
53
|
+
this.active_ping = config.pingCluster != 'off';
|
|
53
54
|
this.max_ping = 5;
|
|
54
55
|
this.availability_timeout = Math.max(60, typeof config.pingTimeout == 'number' ? config.pingTimeout : 300);
|
|
55
56
|
this.startReadDelay = config.readAllAtStart ? Math.max(500, Math.min(10000, config.startReadDelay * 1000)) : 0;
|
|
56
57
|
this.debugDevices = [];
|
|
58
|
+
this.pingCluster = pingClusters[config.pingCluster] ? pingClusters[config.pingCluster] : {};
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/* setOptions(options) {
|
|
60
|
-
if (typeof options !== 'object') {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
if (options.disableActivePing) {
|
|
64
|
-
this.active_ping = false;
|
|
65
|
-
}
|
|
66
|
-
if (options.disableForcedPing) {
|
|
67
|
-
this.forced_ping = false;
|
|
68
|
-
}
|
|
69
|
-
if (typeof options.pingTimeout === 'number') {
|
|
70
|
-
this.availability_timeout = Math.min(60, options.pingTimeout);
|
|
71
|
-
}
|
|
72
|
-
if (typeof options.pingCount === 'number') {
|
|
73
|
-
this.max_ping = Math.min(2, options.pingCount);
|
|
74
|
-
}
|
|
75
|
-
this.startReadDelay = (typeof options.startReadDelay === 'number') ? Math.max(500, Math.min(10000, options.startReadDelay * 1000)) : 0;
|
|
76
|
-
this.warn(`DA: setting options to ${JSON.stringify(options)} - Read Delay is ${this.startReadDelay}`)
|
|
77
|
-
return true;
|
|
78
|
-
} */
|
|
79
61
|
checkDebugDevice(dev) {
|
|
80
62
|
if (typeof dev != 'string' || dev == '') return false;
|
|
81
63
|
if (this.debugDevices === undefined) return false;
|
|
@@ -115,7 +97,6 @@ class DeviceAvailability extends BaseExtension {
|
|
|
115
97
|
if (!this.isStarted) return;
|
|
116
98
|
this.debug(`register device Ping for ${JSON.stringify(device.ieeeAddr)}`);
|
|
117
99
|
this.forcedNonPingable[device.ieeeAddr] = false;
|
|
118
|
-
// this.warn(`Called registerDevicePing for '${device}' of '${entity}'`);
|
|
119
100
|
if (!this.isPingable(device)) {
|
|
120
101
|
return;
|
|
121
102
|
}
|
|
@@ -145,7 +126,6 @@ class DeviceAvailability extends BaseExtension {
|
|
|
145
126
|
}
|
|
146
127
|
|
|
147
128
|
async startDevicePing() {
|
|
148
|
-
// this.warn(JSON.stringify(this));
|
|
149
129
|
if (!this.isStarted) return;
|
|
150
130
|
this.startDevicePingTimeout = null;
|
|
151
131
|
const item = this.startDevicePingQueue.shift();
|
|
@@ -174,21 +154,21 @@ class DeviceAvailability extends BaseExtension {
|
|
|
174
154
|
const readables = [];
|
|
175
155
|
|
|
176
156
|
this.isStarted = true;
|
|
177
|
-
|
|
157
|
+
this.debug('onZigbeeStarted called');
|
|
178
158
|
|
|
179
159
|
for (const device of clients) {
|
|
180
|
-
if (this.isPingable(device)) {
|
|
160
|
+
if (this.isPingable(device) && this.active_ping) {
|
|
181
161
|
readables.push(device);
|
|
182
162
|
// this.setTimerPingable(device);
|
|
183
163
|
} else {
|
|
184
|
-
|
|
164
|
+
this.debug(`Setting '${device.ieeeAddr}' as available - battery driven or no active availability check`);
|
|
185
165
|
this.publishAvailability(device, true);
|
|
186
166
|
this.timers[device.ieeeAddr] = setInterval(() =>
|
|
187
167
|
this.handleIntervalNotPingable(device), utils.secondsToMilliseconds(this.availability_timeout));
|
|
188
168
|
}
|
|
189
169
|
}
|
|
190
170
|
if (this.startReadDelay > 0 && readables.length > 0) {
|
|
191
|
-
this.
|
|
171
|
+
this.info(`Triggering device_query on ${readables.length} devices in ${this.startReadDelay / 1000} seconds.`)
|
|
192
172
|
setTimeout(() => {
|
|
193
173
|
readables.forEach(device => this.zigbee.doDeviceQuery(device, Date().now, false));
|
|
194
174
|
}, this.startReadDelay)
|
|
@@ -197,6 +177,9 @@ class DeviceAvailability extends BaseExtension {
|
|
|
197
177
|
|
|
198
178
|
async handleIntervalPingable(device, entity) {
|
|
199
179
|
if (!this.isStarted) return;
|
|
180
|
+
if (!this.active_ping) {
|
|
181
|
+
return await this.handleIntervalnonPingable();
|
|
182
|
+
}
|
|
200
183
|
const has_elevated_debug = this.checkDebugDevice(device.ieeeAddr)
|
|
201
184
|
|
|
202
185
|
const ieeeAddr = device.ieeeAddr;
|
|
@@ -215,13 +198,29 @@ class DeviceAvailability extends BaseExtension {
|
|
|
215
198
|
pingCount = {failed: 0, reported: 0};
|
|
216
199
|
}
|
|
217
200
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
220
211
|
this.publishAvailability(device, true);
|
|
221
212
|
if (has_elevated_debug) this.warn(`ELEVATED : Successfully pinged ${ieeeAddr} (${device.modelID}) in ${Date.now()-pt} ms`);
|
|
222
213
|
this.setTimerPingable(device, 1);
|
|
223
214
|
this.ping_counters[device.ieeeAddr].failed = 0;
|
|
224
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
|
+
}
|
|
225
224
|
if (has_elevated_debug) this.warn(`ELEVATED : Failed to ping ${ieeeAddr} (${device.modelID}) after ${Date.now()-pt} ms${error && error.message ? ' - '+error.message : ''}`);
|
|
226
225
|
this.publishAvailability(device, false);
|
|
227
226
|
if (pingCount.failed++ <= this.max_ping) {
|
package/lib/zbDeviceConfigure.js
CHANGED
|
@@ -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'})`);
|
package/lib/zbDeviceEvent.js
CHANGED
|
@@ -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.
|
|
14
|
-
await this.
|
|
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
|
|
|
@@ -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,40 @@ 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
|
-
|
|
43
|
-
|
|
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
|
+
try {
|
|
71
|
+
mappedDevice.onEvent(eventData);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.warn(`Error in onEvent: ${error && error.message ? error.message : 'no message'}`);
|
|
75
|
+
}
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
}
|