iobroker.zigbee 3.0.3 → 3.1.2

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.
@@ -67,7 +67,7 @@ class ZigbeeController extends EventEmitter {
67
67
  this.transmitPower = 0;
68
68
  this.herdsmanStarted = false;
69
69
  this.extensions = [
70
- new DeviceAvailabilityExt(this, {}),
70
+ new DeviceAvailabilityExt(this, {}, adapter.config),
71
71
  new DeviceConfigureExt(this, {}),
72
72
  new DeviceEventExt(this, {}),
73
73
  new DelayedActionExt(this, {}),
@@ -75,6 +75,10 @@ class ZigbeeController extends EventEmitter {
75
75
  this.herdsmanTimeoutRegexp = new RegExp(/(\d+)ms/);
76
76
  this.herdsmanLogSettings = {};
77
77
  this.debugActive = true;
78
+ this.ListDevicesAtStart = true;
79
+ this.deviceQueryActive = [];
80
+ this.storedOptions = undefined;
81
+ this.isConfigured = false;
78
82
 
79
83
  }
80
84
 
@@ -102,7 +106,7 @@ class ZigbeeController extends EventEmitter {
102
106
  }
103
107
 
104
108
  configure(options) {
105
-
109
+ this.storedOptions = options;
106
110
  if (options.transmitPower != undefined) {
107
111
  this.transmitPower = options.transmitPower;
108
112
  }
@@ -117,6 +121,8 @@ class ZigbeeController extends EventEmitter {
117
121
 
118
122
  this.powerText = powerLevels[this.transmitPower] || 'normal';
119
123
  }
124
+ this.readAtAnnounce = this.adapter.config.readAtAnnounce;
125
+ this.warnOnDeviceAnnouncement = this.adapter.config.warnOnDeviceAnnouncement;
120
126
 
121
127
  //this.info(` --> transmitPower : ${this.powerText}`);
122
128
 
@@ -156,17 +162,18 @@ class ZigbeeController extends EventEmitter {
156
162
  herdsmanSettings.transmitPower = options.transmitPower;
157
163
  }
158
164
  this.disableLed = options.disableLed;
159
- this.warnOnDeviceAnnouncement = options.warnOnDeviceAnnouncement;
160
165
  this.herdsmanLogSettings.panID = herdsmanSettings.network.panID;
161
166
  this.herdsmanLogSettings.channel = herdsmanSettings.network.channelList[0];
162
167
  this.herdsmanLogSettings.extendedPanID = utils.byteArrayToString(herdsmanSettings.network.extendedPanID);
163
168
  this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
164
169
  this.callExtensionMethod('setOptions', [{
165
- disableActivePing: options.disablePing,
170
+ disableActivePing: this.adapter.config.disablePing,
171
+ startReadDelay: this.adapter.config.readAllAtStart ? this.adapter.config.startReadDelay : 0,
166
172
  disableForcedPing: false,
167
173
  pingTimeout: 300,
168
174
  pingCount: 3
169
175
  }]);
176
+ this.isConfigured = true;
170
177
  }
171
178
 
172
179
  async stopHerdsman() {
@@ -179,11 +186,15 @@ class ZigbeeController extends EventEmitter {
179
186
  catch (error) {
180
187
  this.emit('pairing', `error stopping zigbee-herdsman: ${error && error.message ? error.message : 'no reason given'}`);
181
188
  }
189
+ this.isConfigured = false;
190
+ delete this.herdsman;
191
+
182
192
  }
183
193
 
184
194
  // Start controller
185
195
  async start() {
186
196
  try {
197
+ if (!this.isConfigured) this.configure(this.storedOptions);
187
198
  this.emit('pairing',`Starting zigbee-herdsman...`);
188
199
  this.powerText = '';
189
200
  if (this.transmitPower !== '0') {
@@ -207,7 +218,7 @@ class ZigbeeController extends EventEmitter {
207
218
  this.herdsman.on('permitJoinChanged', this.handlePermitJoinChanged.bind(this));
208
219
 
209
220
  this.info('Starting Zigbee-Herdsman');
210
- await this.herdsman.start();
221
+ const result = await this.herdsman.start();
211
222
  this.herdsmanStarted = true;
212
223
  const cv = await this.herdsman.getCoordinatorVersion();
213
224
  const MetaSt = `${cv.meta.transportrev ? cv.meta.transportrev : 'X'}-${cv.meta.product ? cv.meta.product : 'X'}.${cv.meta.majorrel ? cv.meta.majorrel : 'X'}.${cv.meta.minorrel ? cv.meta.minorrel : 'X'}.${cv.meta.maintrel ? cv.meta.maintrel : 'X'}`;
@@ -295,18 +306,20 @@ class ZigbeeController extends EventEmitter {
295
306
  (entity.mapped ? `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` : `Unsupported (model ${entity.device.modelID})`) +
296
307
  `(${entity.device.type})`);
297
308
  //this.emit('pairing',msg);
298
- this.info(msg);
309
+ if (this.ListDevicesAtStart) this.info(msg);
310
+ }
311
+
312
+ const Groups = await this.getGroups();
313
+ for (const group of Groups) {
314
+ if (this.ListDevicesAtStart) this.info(`${group.stateName} (addr ${group.id}): Group with ${group.size} members.`);
299
315
  }
300
316
 
301
317
  // Log zigbee clients on startup
302
318
  // const devices = await this.getClients();
303
- if (deviceCount > 0) {
304
- this.info(`Currently ${deviceCount} devices are joined:`);
305
- this.emit('pairing',`Currently ${deviceCount} devices are joined:`)
306
- } else {
307
- this.info(`No devices are currently joined.`);
308
- this.emit('pairing',`No devices are currently joined.`);
309
- }
319
+ const gm = Groups.length > 0 ? `and ${Groups.length} group${Groups.length > 1?'s':''} ` : '';
320
+ const m = deviceCount > 0 ? `${deviceCount} devices ${gm}are part of the network` : `No devices ${gm}`;
321
+ this.info(m);
322
+ this.emit('pairing',m)
310
323
  this.callExtensionMethod('onZigbeeStarted', []);
311
324
  }
312
325
  catch (error) {
@@ -395,6 +408,15 @@ class ZigbeeController extends EventEmitter {
395
408
  }
396
409
  }
397
410
 
411
+ getGroupIterator() {
412
+ if (this.herdsman.database) {
413
+ this.herdsman.getGroupsIterator()
414
+ }
415
+ else {
416
+ return[].values();
417
+ }
418
+ }
419
+
398
420
  async getGroups() {
399
421
  try {
400
422
  if (this.herdsman) {
@@ -495,6 +517,7 @@ class ZigbeeController extends EventEmitter {
495
517
  ieee: device.ieeeAddr,
496
518
  model: device.modelID,
497
519
  epid,
520
+ deviceNetworkAddress: nwk,
498
521
  ep: member
499
522
  });
500
523
  }
@@ -602,16 +625,19 @@ class ZigbeeController extends EventEmitter {
602
625
  return {
603
626
  type: 'group',
604
627
  mapped: group,
605
- group,
628
+ device: group,
629
+ //group,
606
630
  name: `Group ${_key.key}`,
607
631
  };
608
632
 
609
633
  }
610
- if (_key.kind === 'ieee') _key.key = await this.herdsman.getDeviceByIeeeAddr(_key.key);
611
- const device = _key.key;
634
+ //if (_key.kind === 'ieee')
635
+ const device = (_key.kind === 'ieee' ? this.herdsman.getDeviceByIeeeAddr(_key.key) : key);
612
636
  if (device) {
613
637
  const t = Date.now();
614
- const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
638
+ const mapped = await zigbeeHerdsmanConverters.findByDevice(device, false);
639
+ if (!mapped)
640
+ this.warn(`Resolve Entity did not manage to find a mapped device for ${device}`);
615
641
  const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
616
642
  let endpoint;
617
643
  if (endpoints && ep != undefined && endpoints[ep]) {
@@ -635,6 +661,9 @@ class ZigbeeController extends EventEmitter {
635
661
  name: device._ieeeAddr,
636
662
  };
637
663
  }
664
+ else {
665
+ this.warn(`resolve_entity failed for ${JSON.stringify(_key)}`);
666
+ }
638
667
  }
639
668
  catch (error)
640
669
  {
@@ -700,14 +729,18 @@ class ZigbeeController extends EventEmitter {
700
729
  }
701
730
 
702
731
  // Permit join
703
- async permitJoin(permitTime, devid, failure) {
732
+ async permitJoin(permitTime, devid) {
704
733
  try {
705
734
  this._permitJoinTime = permitTime;
706
735
  await this.herdsman.permitJoin(permitTime);
736
+ this._permitJoinDevice = (permitTime > 0 && devid ? devid : '');
707
737
  } catch (e) {
738
+ this._permitJoinTime = 0;
708
739
  this.sendError(e);
709
740
  this.error(`Failed to open the network: ${e.stack}`);
741
+ return false;
710
742
  }
743
+ return true;
711
744
  }
712
745
 
713
746
  async handlePermitJoinChanged(data)
@@ -730,6 +763,7 @@ class ZigbeeController extends EventEmitter {
730
763
  this._permitJoinInterval = null;
731
764
  // this.emit('pairing', 'Pairing time left', 0);
732
765
  this.emit('pairing', 'Closing network.',0);
766
+ this._permitJoinDevice = '';
733
767
  }
734
768
  }
735
769
  catch (error) {
@@ -774,8 +808,16 @@ class ZigbeeController extends EventEmitter {
774
808
  }
775
809
  } catch (error) {
776
810
  this.sendError(error);
777
- this.error(`Failed to remove ${error.stack}`);
778
- callback && callback(`Failed to remove ${error.stack}`);
811
+ let message = 'no reason given';
812
+ if (error && error.message) {
813
+ if (error.message.includes('AREQ - ZDO - mgmtLeaveRsp after'))
814
+ message = `No response to mgmtLeaveRequest from the device - device may be offline.`;
815
+ else
816
+ message = error.message;
817
+
818
+ }
819
+ this.warn(`Failed to remove ${deviceID ? 'device ' + deviceID : 'unspecified device'}: ${message}`);
820
+ callback && callback(`Failed to remove ${deviceID ? 'device ' + deviceID : 'unspecified device'}: ${message}`);
779
821
  }
780
822
  }
781
823
 
@@ -806,13 +848,30 @@ class ZigbeeController extends EventEmitter {
806
848
  if (this.debugActive) this.debug('handleDeviceAnnounce', message);
807
849
  const entity = await this.resolveEntity(message.device || message.ieeeAddr);
808
850
  const friendlyName = entity.name;
851
+
852
+
853
+ this.emit('pairing', `Device '${friendlyName}' announced itself`);
809
854
  if (this.adapter.stController.checkDebugDevice(friendlyName)) {
810
855
  this.emit('device_debug', {ID: Date.now(), data: {flag:'da', states:[{id: '--', value:'--', payload:message}] , IO:true} ,message:`Device '${friendlyName}' announced itself`});
811
856
  }
812
- else if (this.warnOnDeviceAnnouncement) {
813
- this.warn(`Device '${friendlyName}' announced itself`);
857
+ if (this.warnOnDeviceAnnouncement) {
858
+ this.warn(`Device '${friendlyName}' announced itself${this.readAtAnnounce ? ', trying to read its status' : ''}`);
814
859
  } else {
815
- this.info(`Device '${friendlyName}' announced itself`);
860
+ this.info(`Device '${friendlyName}' announced itself${this.readAtAnnounce ? ', trying to read its status' : ''}`);
861
+ }
862
+
863
+ if (entity.device && entity.device._modelID && entity.device.interviewState != 'SUCCESSFUL') {
864
+ this.warn(`ignoring device announcement for ${entity.device._modelID} due to interview state ${entity.device.interviewState}`);
865
+ this.emit('pairing', `device interview state is ${entity.device.interviewState}`)
866
+ return;
867
+ }
868
+
869
+ const networkOpen = this.herdsman.getPermitJoin();
870
+ if (networkOpen && entity.device && entity.device._modelID && entity.device.interviewState != 'IN_PROGRESS')
871
+ {
872
+ //entity.device.modelID = entity.device._modelID;
873
+ this.emit('new', entity);
874
+ return;
816
875
  }
817
876
 
818
877
  try {
@@ -820,20 +879,12 @@ class ZigbeeController extends EventEmitter {
820
879
  this.callExtensionMethod(
821
880
  'onZigbeeEvent',
822
881
  [{'device': message.device, 'type': 'deviceAnnounce'}, entity ? entity.mapped : null]);
882
+ this.callExtensionMethod('registerDevicePing', [message.device, entity]);
883
+ if (this.readAtAnnounce) await this.doDeviceQuery(message.device || message.ieeeAddr, Date.now(), false);
823
884
  }
824
885
  } catch (error) {
825
886
  this.sendError(error);
826
- this.error(`Failed to handleDeviceLeave ${error.stack}`);
827
- }
828
-
829
- this.emit('pairing', `Device '${friendlyName}' announced itself`);
830
- if (!this.herdsman.getPermitJoin()) {
831
- this.callExtensionMethod('registerDevicePing', [message.device, entity]);
832
- }
833
- // if has modelID so can create device
834
- if (entity.device && entity.device._modelID) {
835
- entity.device.modelID = entity.device._modelID;
836
- this.emit('new', entity);
887
+ this.error(`Failed to handleDeviceAnnounce ${error.stack}`);
837
888
  }
838
889
  }
839
890
 
@@ -864,7 +915,7 @@ class ZigbeeController extends EventEmitter {
864
915
 
865
916
  const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
866
917
  this.emit('pairing', 'Interview successful', JSON.stringify(log));
867
- entity.device.modelID = entity.device._modelID;
918
+ //entity.device.modelID = entity.device._modelID;
868
919
  this.emit('new', entity);
869
920
  // send to extensions again (for configure)
870
921
  this.callExtensionMethod(
@@ -878,7 +929,7 @@ class ZigbeeController extends EventEmitter {
878
929
  );
879
930
  const frName = {friendly_name: friendlyName, supported: false};
880
931
  this.emit('pairing', 'Interview successful', JSON.stringify(frName));
881
- entity.device.modelID = entity.device._modelID;
932
+ //entity.device.modelID = entity.device._modelID;
882
933
  this.emit('new', entity);
883
934
  }
884
935
  } else if (message.status === 'failed') {
@@ -899,6 +950,11 @@ class ZigbeeController extends EventEmitter {
899
950
 
900
951
  async handleMessage(data) {
901
952
  if (this.debugActive) this.debug(`handleMessage`, data);
953
+
954
+ const is = data.device.interviewState;
955
+ if (is != 'SUCCESSFUL' && is != 'FAILED') {
956
+ this.warn(`message ${JSON.stringify(data)} received during interview.`)
957
+ }
902
958
  const entity = await this.resolveEntity(data.device || data.ieeeAddr);
903
959
  const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
904
960
  if (this.debugActive) this.debug(
@@ -1000,7 +1056,14 @@ class ZigbeeController extends EventEmitter {
1000
1056
  }
1001
1057
  }
1002
1058
 
1059
+ processSyncStatesList(deviceId, model, syncStateList) {
1060
+ syncStateList.forEach((syncState) => {
1061
+ this.emit('acknowledge_state',deviceId, model, syncState.stateDesc, syncState.value);
1062
+ });
1063
+ }
1003
1064
 
1065
+ // publishing to the zigbee network
1066
+ // publish raw zigbee data
1004
1067
  async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
1005
1068
  const entity = await this.resolveEntity(deviceID, ep);
1006
1069
  const device = entity.device;
@@ -1024,37 +1087,403 @@ class ZigbeeController extends EventEmitter {
1024
1087
  cfg = {};
1025
1088
  }
1026
1089
 
1090
+ // try { NO Try/Catach here, this is ONLY called from the developer tab and error handling is done there
1091
+
1092
+ if (type === 'foundation') {
1093
+ cfg.disableDefaultResponse = true;
1094
+ let result;
1095
+ if (cmd === 'configReport') {
1096
+ result = await endpoint.configureReporting(cid, zclData, cfg);
1097
+ } else {
1098
+ if (cmd === 'read' && !Array.isArray(zclData))
1099
+ result = await endpoint[cmd](cid, Object.keys(zclData), cfg);
1100
+ else
1101
+ result = await endpoint[cmd](cid, zclData, cfg);
1102
+ }
1103
+ callback && callback(undefined, result);
1104
+ } else if (type === 'functionalResp') {
1105
+ cfg.disableDefaultResponse = false;
1106
+ const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
1107
+ callback && callback(undefined, result);
1108
+ } else {
1109
+ cfg.disableDefaultResponse = false;
1110
+ const result = await endpoint.command(cid, cmd, zclData, cfg);
1111
+ callback && callback(undefined, result);
1112
+ }
1113
+ /* }
1114
+ catch (error)
1115
+ {
1116
+ //this.error(`error sending ${type} ${cmd} to endpoint: ${(error && error.message ? error.message : 'no error message')} ${(error && error.stack ? error.stack : 'no call stack')}`)
1117
+ }
1118
+ */
1119
+ }
1120
+ // publish via converter
1121
+ //
1122
+ async publishFromState(deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug) {
1123
+ let isGroup = false;
1124
+ //const has_elevated_debug = this.stController.checkDebugDevice(deviceId)
1125
+
1126
+ if (has_elevated_debug)
1127
+ {
1128
+ const stateNames = stateList.map((state) => state.stateDesc.id);
1129
+ const message = `Publishing to ${deviceId} of model ${model} with ${stateNames.join(', ')}`;
1130
+ this.emit('device_debug', { ID:debugID, data: { ID: deviceId, flag: '03', IO:false }, message: message});
1131
+ }
1132
+ else
1133
+ if (this.debugActive) this.debug(`main publishFromState : ${deviceId} ${model} ${safeJsonStringify(stateList)}`);
1134
+ if (model === 'group') {
1135
+ isGroup = true;
1136
+ deviceId = parseInt(deviceId);
1137
+ }
1027
1138
  try {
1139
+ const entity = await this.resolveEntity(deviceId);
1140
+ if (this.debugActive) this.debug(`entity: ${deviceId} ${model} ${safeJsonStringify(entity)}`);
1141
+ const mappedModel = entity ? entity.mapped : undefined;
1142
+
1143
+ if (!mappedModel) {
1144
+ if (this.debugActive) this.debug(`No mapped model for ${model}`);
1145
+ if (has_elevated_debug) {
1146
+ const message=`No mapped model ${deviceId} (model ${model})`;
1147
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOMODEL' , IO:false }, message: message});
1148
+ }
1149
+ return;
1150
+ }
1028
1151
 
1029
- if (type === 'foundation') {
1030
- cfg.disableDefaultResponse = true;
1031
- let result;
1032
- if (cmd === 'configReport') {
1033
- result = await endpoint.configureReporting(cid, zclData, cfg);
1152
+ if (!mappedModel.toZigbee)
1153
+ {
1154
+ this.error(`No toZigbee in mapped model for ${model}`);
1155
+ return;
1156
+ }
1157
+
1158
+ stateList.forEach(async changedState => {
1159
+ const stateDesc = changedState.stateDesc;
1160
+ const value = changedState.value;
1161
+
1162
+ let converter = undefined;
1163
+ let msg_counter = 0;
1164
+ if (stateDesc.isOption) {
1165
+ if (has_elevated_debug) {
1166
+ const message = `No converter needed on option state for ${deviceId} of type ${model}`;
1167
+ this.emit('device_debug', { ID:debugID, data: { flag: `SUCCESS` , IO:false }, message:message});
1168
+ }
1169
+ else
1170
+ if (this.debugActive) this.debug(`No converter needed on option state for ${deviceId} of type ${model}`);
1171
+
1172
+ return;
1173
+ }
1174
+ for (const c of mappedModel.toZigbee) {
1175
+
1176
+ if (!c.hasOwnProperty('convertSet')) continue;
1177
+ if (this.debugActive) this.debug(`Type of toZigbee is '${typeof c}', Contains key ${(c.hasOwnProperty('key')?JSON.stringify(c.key):'false ')}`)
1178
+ if (!c.hasOwnProperty('key'))
1179
+ {
1180
+ if (converter === undefined)
1181
+ {
1182
+ converter = c;
1183
+ if (has_elevated_debug) {
1184
+ const message = `Setting converter to keyless converter for ${deviceId} of type ${model}`;
1185
+ this.emit('device_debug', { ID:debugID, data: { flag: `s4.${msg_counter}` , IO:false }, message:message});
1186
+ }
1187
+ else
1188
+ if (this.debugActive) this.debug(`Setting converter to keyless converter for ${deviceId} of type ${model}`);
1189
+ msg_counter++;
1190
+ }
1191
+ else
1192
+ {
1193
+ if (has_elevated_debug)
1194
+ {
1195
+ const message = `ignoring keyless converter for ${deviceId} of type ${model}`;
1196
+ this.emit('device_debug', { ID:debugID, data: { flag: `i4.${msg_counter}` , IO:false} , message:message});
1197
+ }
1198
+ else
1199
+ if (this.debugActive) this.debug(`ignoring keyless converter for ${deviceId} of type ${model}`);
1200
+ msg_counter++;
1201
+ }
1202
+ continue;
1203
+ }
1204
+ if (c.key.includes(stateDesc.prop) || c.key.includes(stateDesc.setattr) || c.key.includes(stateDesc.id))
1205
+ {
1206
+ const message = `${(converter===undefined?'Setting':'Overriding')}' converter to converter with key(s)'${JSON.stringify(c.key)}}`;
1207
+ if (has_elevated_debug) {
1208
+ this.emit('device_debugug', { ID:debugID, data: { flag: `${converter===undefined ? 's' : 'o'}4.${msg_counter}` , IO:false }, message:message});
1209
+
1210
+ }
1211
+ else
1212
+ if (this.debugActive) this.debug(message);
1213
+ converter = c;
1214
+ msg_counter++;
1215
+ }
1216
+ }
1217
+ if (converter === undefined) {
1218
+ const message = `No converter available for '${model}' with key '${stateDesc.id}' `;
1219
+ if (has_elevated_debug) {
1220
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOCONV',states:[{id:stateDesc.id, value:value, payload:'no converter'}] , IO:false }, message:message});
1221
+ }
1222
+ else {
1223
+ this.info(message);
1224
+ }
1225
+ return;
1226
+ }
1227
+
1228
+ const preparedValue = (stateDesc.setter) ? stateDesc.setter(value, options) : value;
1229
+ const preparedOptions = (stateDesc.setterOpt) ? stateDesc.setterOpt(value, options) : {};
1230
+
1231
+ let syncStateList = [];
1232
+ if (stateModel && stateModel.syncStates) {
1233
+ stateModel.syncStates.forEach(syncFunct => {
1234
+ const res = syncFunct(stateDesc, value, options);
1235
+ if (res) {
1236
+ syncStateList = syncStateList.concat(res);
1237
+ }
1238
+ });
1239
+ }
1240
+
1241
+ const epName = stateDesc.epname !== undefined ? stateDesc.epname : (stateDesc.prop || stateDesc.id);
1242
+ const key = stateDesc.setattr || stateDesc.prop || stateDesc.id;
1243
+ const message = `convert ${key}, ${safeJsonStringify(preparedValue)}, ${safeJsonStringify(preparedOptions)} for device ${deviceId} with Endpoint ${epName}`;
1244
+ if (has_elevated_debug) {
1245
+ this.emit('device_debug', { ID:debugID, data: { flag: '04', payload: {key:key, ep: stateDesc.epname, value:preparedValue, options:preparedOptions}, IO:false }, message:message});
1246
+ }
1247
+ else
1248
+ if (this.debugActive) this.debug(message);
1249
+
1250
+ let target;
1251
+ if (model === 'group') {
1252
+ target = entity.mapped;
1034
1253
  } else {
1035
- if (cmd === 'read' && !Array.isArray(zclData))
1036
- result = await endpoint[cmd](cid, Object.keys(zclData), cfg);
1254
+ target = await this.resolveEntity(deviceId, epName);
1255
+ target = target.endpoint;
1256
+ }
1257
+
1258
+ if (this.debugActive) this.debug(`target: ${safeJsonStringify(target)}`);
1259
+
1260
+ const meta = {
1261
+ endpoint_name: epName,
1262
+ options: preparedOptions,
1263
+ device: entity.device,
1264
+ mapped: model === 'group' ? [] : mappedModel,
1265
+ message: {[key]: preparedValue},
1266
+ logger: this,
1267
+ state: {},
1268
+ };
1269
+
1270
+ // new toZigbee
1271
+ if (preparedValue !== undefined && Object.keys(meta.message).filter(p => p.startsWith('state')).length > 0) {
1272
+ if (typeof preparedValue === 'number') {
1273
+ meta.message.state = preparedValue > 0 ? 'ON' : 'OFF';
1274
+ } else {
1275
+ meta.message.state = preparedValue;
1276
+ }
1277
+ }
1278
+ if (has_elevated_debug) {
1279
+ this.emit('device_debug', { ID:debugID, data: { states:[{id:stateDesc.id, value:value, payload:preparedValue, ep:stateDesc.epname}] , IO:false }});
1280
+ }
1281
+
1282
+ if (preparedOptions !== undefined) {
1283
+ if (preparedOptions.hasOwnProperty('state')) {
1284
+ meta.state = preparedOptions.state;
1285
+ }
1286
+ }
1287
+
1288
+ try {
1289
+ const result = await converter.convertSet(target, key, preparedValue, meta);
1290
+ const message = `convert result ${safeJsonStringify(result)} for device ${deviceId}`;
1291
+ this.emit('published', deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug );
1292
+ if (has_elevated_debug) {
1293
+ this.emit('device_debug', { ID:debugID, data: { flag: 'SUCCESS' , IO:false }, message:message});
1294
+ }
1037
1295
  else
1038
- result = await endpoint[cmd](cid, zclData, cfg);
1296
+ if (this.debugActive) this.debug(message);
1297
+ if (result !== undefined) {
1298
+ if (stateModel && !isGroup && !stateDesc.noack) {
1299
+ this.emit('acknowledge_state', deviceId, model, stateDesc, value );
1300
+ }
1301
+ // process sync state list
1302
+ this.processSyncStatesList(deviceId, model, syncStateList);
1303
+ }
1304
+ else {
1305
+ if (has_elevated_debug) {
1306
+ const message = `Convert does not return a result result for ${key} with ${safeJsonStringify(preparedValue)} on device ${deviceId}.`;
1307
+ this.emit('device_debug', { ID:debugID, data: { flag: '06' , IO:false }, message:message});
1308
+ }
1309
+ }
1310
+ } catch (error) {
1311
+ if (has_elevated_debug) {
1312
+ const message = `caught error ${safeJsonStringify(error)} when setting value for device ${deviceId}.`;
1313
+ this.emit('device_debug', { ID:debugID, data: { error: 'EXSET' , IO:false },message:message});
1314
+ }
1315
+ this.adapter.filterError(`Error ${error.code} on send command to ${deviceId}.` +
1316
+ ` Error: ${error.stack}`, `Send command to ${deviceId} failed with`, error);
1039
1317
  }
1040
- callback && callback(undefined, result);
1041
- } else if (type === 'functionalResp') {
1042
- cfg.disableDefaultResponse = false;
1043
- const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
1044
- callback && callback(undefined, result);
1045
- } else {
1046
- cfg.disableDefaultResponse = false;
1047
- const result = await endpoint.command(cid, cmd, zclData, cfg);
1048
- callback && callback(undefined, result);
1318
+ });
1319
+ } catch (err) {
1320
+ const message = `No entity for ${deviceId} : ${err && err.message ? err.message : 'no error message'}`;
1321
+ this.emit('device_debug', { ID:debugID, data: { error: 'EXPUB' , IO:false }, message:message});
1322
+ }
1323
+ }
1324
+
1325
+ extractEP(key, endpoints) {
1326
+ try {
1327
+ if (endpoints) for (const ep of Object.keys(endpoints)) {
1328
+ if (key.endsWith('_'+ep)) return { setattr: key.replace('_'+ep, ''), epname:ep }
1049
1329
  }
1050
1330
  }
1051
- catch (error)
1052
- {
1053
- this.log.error(`error sending ${type} ${cmd} to endpoint: ${(error && error.message ? error.message : 'no error message')} ${(error && error.stack ? error.stack : 'no call stack')}`)
1331
+ catch {
1332
+ return {};
1333
+ }
1334
+ return {};
1335
+ }
1336
+
1337
+ // publish via payload
1338
+ //
1339
+ // This function is introduced to explicitly allow user level scripts to send Commands
1340
+ // directly to the zigbee device. It utilizes the zigbee-herdsman-converters to generate
1341
+ // the exact zigbee message to be sent and can be used to set device options which are
1342
+ // not exposed as states. It serves as a wrapper function for "publishFromState" with
1343
+ // extended parameter checking
1344
+ //
1345
+ // The payload can either be a JSON object or the string representation of a JSON object
1346
+ // The following keys are supported in the object:
1347
+ // device: name of the device. For a device zigbee.0.0011223344556677 this would be 0011223344556677
1348
+ // payload: The data to send to the device as JSON object (key/Value pairs)
1349
+ // endpoint: optional: the endpoint to send the data to, if supported.
1350
+ //
1351
+ async publishPayload(payload, debugID) {
1352
+ let payloadObj = {};
1353
+ if (typeof payload === 'string') {
1354
+ try {
1355
+ payloadObj = JSON.parse(payload);
1356
+ } catch (e) {
1357
+ this.log.error(`Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`);
1358
+ return {
1359
+ success: false,
1360
+ error: `Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`
1361
+ };
1362
+ }
1363
+ } else if (typeof payload === 'object') {
1364
+ payloadObj = payload;
1365
+ } else return { success: false, error: 'illegal type of payload: ' + typeof payload};
1366
+
1367
+ if (payloadObj.hasOwnProperty('device') && payloadObj.hasOwnProperty('payload')) {
1368
+ try {
1369
+ const isDevice = !payload.device.includes('group_');
1370
+ const stateList = [];
1371
+ const devID = isDevice ? `0x${payload.device}` : parseInt(payload.device.replace('group_', ''));
1372
+
1373
+ const entity = await this.resolveEntity(devID);
1374
+ if (!entity) {
1375
+ this.log.error(`Device ${safeJsonStringify(payloadObj.device)} not found`);
1376
+ return {success: false, error: `Device ${safeJsonStringify(payloadObj.device)} not found`};
1377
+ }
1378
+ const mappedModel = entity.mapped;
1379
+ if (!mappedModel) {
1380
+ this.log.error(`No Model for Device ${safeJsonStringify(payloadObj.device)}`);
1381
+ return {success: false, error: `No Model for Device ${safeJsonStringify(payloadObj.device)}`};
1382
+ }
1383
+ if (typeof payloadObj.payload !== 'object') {
1384
+ this.log.error(`Illegal payload type for ${safeJsonStringify(payloadObj.device)}`);
1385
+ return {success: false, error: `Illegal payload type for ${safeJsonStringify(payloadObj.device)}`};
1386
+ }
1387
+ const endpoints = mappedModel && mappedModel.endpoint ? mappedModel.endpoint(entity.device) : null;
1388
+ for (const key in payloadObj.payload) {
1389
+ if (payloadObj.payload[key] != undefined) {
1390
+ const datatype = typeof payloadObj.payload[key];
1391
+ const epobj = this.extractEP(key, endpoints);
1392
+ if (payloadObj.endpoint) {
1393
+ epobj.epname = payloadObj.endpoint;
1394
+ delete epobj.setattr;
1395
+ }
1396
+ stateList.push({
1397
+ stateDesc: {
1398
+ id: key,
1399
+ prop: key,
1400
+ role: 'state',
1401
+ type: datatype,
1402
+ noack:true,
1403
+ epname: epobj.epname,
1404
+ setattr: epobj.setattr,
1405
+ },
1406
+ value: payloadObj.payload[key],
1407
+ index: 0,
1408
+ timeout: 0,
1409
+ });
1410
+ }
1411
+ }
1412
+ try {
1413
+ await this.publishFromState(`0x${payload.device}`, payload.model, payload.stateModel, stateList, payload.options, debugID);
1414
+ return {success: true};
1415
+ } catch (error) {
1416
+ this.log.error(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack} ` + `Send command to ${payload.device} failed with ` + error);
1417
+ this.adapter.filterError(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack}`, `Send command to ${payload.device} failed with`, error);
1418
+ return {success: false, error};
1419
+ }
1420
+ } catch (e) {
1421
+ return {success: false, error: e};
1422
+ }
1054
1423
  }
1055
1424
 
1425
+ return {success: false, error: `missing parameter device or payload in message ${JSON.stringify(payload)}`};
1056
1426
  }
1057
1427
 
1428
+ async doDeviceQuery(deviceId, debugID, elevated) {
1429
+ const entity = await this.resolveEntity(deviceId);
1430
+ if (this.debugActive) this.debug(`doDeviceQuery: resolveEntity for entity: ${deviceId} is ${safeJsonStringify(entity)}`);
1431
+ const mappedModel = entity ? entity.mapped : undefined;
1432
+ if (mappedModel) {
1433
+ if (elevated) {
1434
+ const message = `Device query for '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' triggered`;
1435
+ this.emit('device_debug', { ID:debugID, data: { flag: 'qs' ,states:[{id:'device_query', value:true, payload:'device_query'}], IO:false }, message:message});
1436
+ }
1437
+ else
1438
+ if (this.debugActive) this.debug(`Device query for '${entity.device.ieeeAddr}' started`);
1439
+ else this.info(`Device query for '${entity.device.ieeeAddr}' started`);
1440
+
1441
+ for (const converter of mappedModel.toZigbee) {
1442
+ if (converter.hasOwnProperty('convertGet')) {
1443
+ for (const ckey of converter.key) {
1444
+ try {
1445
+ await converter.convertGet(entity.device.endpoints[0], ckey, {device:entity.device});
1446
+ this.debug(`read state ${JSON.stringify(ckey)} of ${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID} after device query`);
1447
+ } catch (error) {
1448
+ if (elevated) {
1449
+ const message = `Failed to read state '${JSON.stringify(ckey)}'of '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' from query with '${error && error.message ? error.message : 'no error message'}`;
1450
+ this.warn(`ELEVATED OE02.1 ${message}`);
1451
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOTREAD' , IO:false }, message:message });
1452
+ }
1453
+ else
1454
+ this.debug(`failed to read state ${JSON.stringify(ckey)} of ${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID} after device query`);
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ if (elevated) {
1460
+ const message = `ELEVATED O07: Device query for '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' complete`;
1461
+ this.emit('device_debug', { ID:debugID, data: { flag: 'qe' , IO:false }, message:message});
1462
+ }
1463
+ else
1464
+ this.info(`Device query for '${entity.device.ieeeAddr}' complete`);
1465
+ }
1466
+ }
1467
+
1468
+ async deviceQuery(deviceId, debugID, elevated, callback) {
1469
+ if (this.deviceQueryActive.includes (deviceId)) {
1470
+ this.warn(`Device query for ${deviceId} is still active.`);
1471
+ return;
1472
+ }
1473
+ this.deviceQueryActive.push(deviceId);
1474
+ try {
1475
+ await this.doDeviceQuery(deviceId, debugID, elevated);
1476
+ }
1477
+ catch (e) {
1478
+ this.warn('error in doDeviceQuery')
1479
+ }
1480
+ const idx = this.deviceQueryActive.indexOf(deviceId)
1481
+ if (idx > -1)
1482
+ this.deviceQueryActive.splice(idx, 1);
1483
+ if (callback) callback(deviceId);
1484
+ }
1485
+
1486
+
1058
1487
  async addDevToGroup(devId, groupId, epid) {
1059
1488
  try {
1060
1489
  if (this.debugActive) this.debug(`called addDevToGroup with ${devId}, ${groupId}, ${epid}`);