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.
@@ -11,7 +11,6 @@ const DeviceAvailabilityExt = require('./zbDeviceAvailability');
11
11
  const DeviceConfigureExt = require('./zbDeviceConfigure');
12
12
  const DeviceEventExt = require('./zbDeviceEvent');
13
13
  const DelayedActionExt = require('./zbDelayedAction');
14
- const Groups = require('./groups');
15
14
  const utils = require('./utils');
16
15
 
17
16
  const { access, constants } =require('node:fs/promises');
@@ -67,7 +66,7 @@ class ZigbeeController extends EventEmitter {
67
66
  this.transmitPower = 0;
68
67
  this.herdsmanStarted = false;
69
68
  this.extensions = [
70
- new DeviceAvailabilityExt(this, {}),
69
+ new DeviceAvailabilityExt(this, {}, adapter.config),
71
70
  new DeviceConfigureExt(this, {}),
72
71
  new DeviceEventExt(this, {}),
73
72
  new DelayedActionExt(this, {}),
@@ -75,6 +74,10 @@ class ZigbeeController extends EventEmitter {
75
74
  this.herdsmanTimeoutRegexp = new RegExp(/(\d+)ms/);
76
75
  this.herdsmanLogSettings = {};
77
76
  this.debugActive = true;
77
+ this.ListDevicesAtStart = adapter.config.listDevicesAtStart;
78
+ this.deviceQueryActive = [];
79
+ this.storedOptions = undefined;
80
+ this.isConfigured = false;
78
81
 
79
82
  }
80
83
 
@@ -102,7 +105,7 @@ class ZigbeeController extends EventEmitter {
102
105
  }
103
106
 
104
107
  configure(options) {
105
-
108
+ this.storedOptions = options;
106
109
  if (options.transmitPower != undefined) {
107
110
  this.transmitPower = options.transmitPower;
108
111
  }
@@ -117,6 +120,8 @@ class ZigbeeController extends EventEmitter {
117
120
 
118
121
  this.powerText = powerLevels[this.transmitPower] || 'normal';
119
122
  }
123
+ this.readAtAnnounce = this.adapter.config.readAtAnnounce;
124
+ this.warnOnDeviceAnnouncement = this.adapter.config.warnOnDeviceAnnouncement;
120
125
 
121
126
  //this.info(` --> transmitPower : ${this.powerText}`);
122
127
 
@@ -156,17 +161,18 @@ class ZigbeeController extends EventEmitter {
156
161
  herdsmanSettings.transmitPower = options.transmitPower;
157
162
  }
158
163
  this.disableLed = options.disableLed;
159
- this.warnOnDeviceAnnouncement = options.warnOnDeviceAnnouncement;
160
164
  this.herdsmanLogSettings.panID = herdsmanSettings.network.panID;
161
165
  this.herdsmanLogSettings.channel = herdsmanSettings.network.channelList[0];
162
166
  this.herdsmanLogSettings.extendedPanID = utils.byteArrayToString(herdsmanSettings.network.extendedPanID);
163
167
  this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
164
168
  this.callExtensionMethod('setOptions', [{
165
- disableActivePing: options.disablePing,
169
+ pingCluster: this.adapter.config.pingCluster,
170
+ startReadDelay: this.adapter.config.readAllAtStart ? this.adapter.config.startReadDelay : 0,
166
171
  disableForcedPing: false,
167
172
  pingTimeout: 300,
168
173
  pingCount: 3
169
174
  }]);
175
+ this.isConfigured = true;
170
176
  }
171
177
 
172
178
  async stopHerdsman() {
@@ -179,11 +185,15 @@ class ZigbeeController extends EventEmitter {
179
185
  catch (error) {
180
186
  this.emit('pairing', `error stopping zigbee-herdsman: ${error && error.message ? error.message : 'no reason given'}`);
181
187
  }
188
+ this.isConfigured = false;
189
+ delete this.herdsman;
190
+
182
191
  }
183
192
 
184
193
  // Start controller
185
194
  async start() {
186
195
  try {
196
+ if (!this.isConfigured) this.configure(this.storedOptions);
187
197
  this.emit('pairing',`Starting zigbee-herdsman...`);
188
198
  this.powerText = '';
189
199
  if (this.transmitPower !== '0') {
@@ -207,7 +217,7 @@ class ZigbeeController extends EventEmitter {
207
217
  this.herdsman.on('permitJoinChanged', this.handlePermitJoinChanged.bind(this));
208
218
 
209
219
  this.info('Starting Zigbee-Herdsman');
210
- await this.herdsman.start();
220
+ const result = await this.herdsman.start();
211
221
  this.herdsmanStarted = true;
212
222
  const cv = await this.herdsman.getCoordinatorVersion();
213
223
  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'}`;
@@ -272,8 +282,7 @@ class ZigbeeController extends EventEmitter {
272
282
  // get the model description for the known devices
273
283
  const entity = await this.resolveEntity(device);
274
284
  if (!entity) {
275
- this.warn('failed to resolve Entity for ' + device.ieeeAddr);
276
- //this.emit('pairing','failed to resolve Entity for ' + device.ieeeAddr)
285
+ this.debug('failed to resolve Entity for ' + device.ieeeAddr);
277
286
  continue;
278
287
  }
279
288
  //await this.adapter.stController.AddModelFromHerdsman(device, entity.mapped.model);
@@ -294,19 +303,20 @@ class ZigbeeController extends EventEmitter {
294
303
  ` (addr ${entity.device.networkAddress}): ` +
295
304
  (entity.mapped ? `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` : `Unsupported (model ${entity.device.modelID})`) +
296
305
  `(${entity.device.type})`);
297
- //this.emit('pairing',msg);
298
- this.info(msg);
306
+ if (this.ListDevicesAtStart) this.info(msg);
307
+ }
308
+
309
+ const Groups = await this.getGroups();
310
+ for (const group of Groups) {
311
+ if (this.ListDevicesAtStart) this.info(`${group.stateName} (addr ${group.id}): Group with ${group.size} members.`);
299
312
  }
300
313
 
301
314
  // Log zigbee clients on startup
302
315
  // 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
- }
316
+ const gm = Groups.length > 0 ? `and ${Groups.length} group${Groups.length > 1?'s':''} ` : '';
317
+ const m = deviceCount > 0 ? `${deviceCount} devices ${gm}are part of the network` : `No devices ${gm}`;
318
+ this.info(m);
319
+ this.emit('pairing',m)
310
320
  this.callExtensionMethod('onZigbeeStarted', []);
311
321
  }
312
322
  catch (error) {
@@ -395,6 +405,15 @@ class ZigbeeController extends EventEmitter {
395
405
  }
396
406
  }
397
407
 
408
+ getGroupIterator() {
409
+ if (this.herdsman.database) {
410
+ this.herdsman.getGroupsIterator()
411
+ }
412
+ else {
413
+ return[].values();
414
+ }
415
+ }
416
+
398
417
  async getGroups() {
399
418
  try {
400
419
  if (this.herdsman) {
@@ -495,6 +514,7 @@ class ZigbeeController extends EventEmitter {
495
514
  ieee: device.ieeeAddr,
496
515
  model: device.modelID,
497
516
  epid,
517
+ deviceNetworkAddress: nwk,
498
518
  ep: member
499
519
  });
500
520
  }
@@ -513,9 +533,10 @@ class ZigbeeController extends EventEmitter {
513
533
  return members;
514
534
  }
515
535
 
516
- getDevice(key) {
536
+ async getDevice(key) {
517
537
  try {
518
- return this.herdsman.getDeviceByIeeeAddr(key);
538
+ const dev = await this.herdsman.getDeviceByIeeeAddr(key);
539
+ return dev;
519
540
  }
520
541
  catch (error) {
521
542
  this.error(`getDeviceByIeeeAddr: ${(error && error.message ? error.message : 'no error message')}`);
@@ -549,7 +570,9 @@ class ZigbeeController extends EventEmitter {
549
570
  key: key,
550
571
  message: 'success'
551
572
  }
552
- if (typeof key === 'object') return rv;
573
+ if (typeof key === 'object') {
574
+ return rv;
575
+ }
553
576
  if (typeof key === 'number') {
554
577
  rv.kind = 'group';
555
578
  return rv;
@@ -579,8 +602,11 @@ class ZigbeeController extends EventEmitter {
579
602
  return rv;
580
603
  }
581
604
 
605
+ async getGroup(id) {
606
+ return await this.herdsman.getGroupByID(id);
607
+ }
608
+
582
609
  async resolveEntity(key, ep) {
583
- // this.warn('resolve entity with key of tyoe ' + typeof (key));
584
610
  try {
585
611
  const _key = await this.analyzeKey(key);
586
612
  if (_key.message !== 'success') return undefined;
@@ -592,6 +618,7 @@ class ZigbeeController extends EventEmitter {
592
618
  device: coordinator,
593
619
  endpoint: coordinator.getEndpoint(1),
594
620
  name: 'Coordinator',
621
+ options:{}
595
622
  };
596
623
  }
597
624
  if (_key.kind === 'group') {
@@ -602,16 +629,39 @@ class ZigbeeController extends EventEmitter {
602
629
  return {
603
630
  type: 'group',
604
631
  mapped: group,
605
- group,
632
+ device: group,
633
+ endpoint: group,
634
+ //group,
606
635
  name: `Group ${_key.key}`,
636
+ options: {},
607
637
  };
608
638
 
609
639
  }
610
- if (_key.kind === 'ieee') _key.key = await this.herdsman.getDeviceByIeeeAddr(_key.key);
611
- const device = _key.key;
640
+ //if (_key.kind === 'ieee')
641
+ const device = (_key.kind === 'ieee' ? this.herdsman.getDeviceByIeeeAddr(_key.key) : key);
642
+ if (device && device.model === 'group') {
643
+ return {
644
+ type: 'group',
645
+ mapped: device,
646
+ device,
647
+ endpoint: device,
648
+ name: `Group ${device.groupID}`,
649
+ options:{}
650
+ }
651
+ }
612
652
  if (device) {
613
653
  const t = Date.now();
614
- const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
654
+ const mapped = await zigbeeHerdsmanConverters.findByDevice(device, false);
655
+ if (!mapped) {
656
+ if (device.type === 'Coordinator')
657
+ return {
658
+ type: 'device',
659
+ device: device,
660
+ endpoint: device.getEndpoint(1),
661
+ name: 'Coordinator',
662
+ };
663
+ this.warn(`Resolve Entity did not manage to find a mapped device for ${device.ieeeAddr} of type ${device.modelID}`);
664
+ }
615
665
  const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
616
666
  let endpoint;
617
667
  if (endpoints && ep != undefined && endpoints[ep]) {
@@ -626,6 +676,7 @@ class ZigbeeController extends EventEmitter {
626
676
  endpoint = device.endpoints[0];
627
677
  }
628
678
  }
679
+ const options = this.adapter.stController.localConfig.getOptions(device.ieeeAddr, mapped.model);
629
680
  return {
630
681
  type: 'device',
631
682
  device,
@@ -633,8 +684,12 @@ class ZigbeeController extends EventEmitter {
633
684
  endpoint,
634
685
  endpoints: device.endpoints,
635
686
  name: device._ieeeAddr,
687
+ options:options
636
688
  };
637
689
  }
690
+ else {
691
+ this.debug(`resolve_entity failed for ${JSON.stringify(_key)}`);
692
+ }
638
693
  }
639
694
  catch (error)
640
695
  {
@@ -676,10 +731,13 @@ class ZigbeeController extends EventEmitter {
676
731
  }
677
732
 
678
733
  try {
679
- if (this.HerdsmanStarted) await this.permitJoin(0);
680
- await this.herdsman.stop();
681
- this.HerdsmanStarted = false;
682
- this.info('zigbecontroller stopped successfully');
734
+ if (this.HerdsmanStarted) {
735
+ await this.permitJoin(0);
736
+ await this.herdsman.stop();
737
+ this.HerdsmanStarted = false;
738
+ this.info('zigbecontroller stopped successfully');
739
+ }
740
+ this.info('zigbecontroller stopped successfully - ZH was not running');
683
741
  } catch (error) {
684
742
  this.sendError(error);
685
743
  if (this.herdsmanStarted) {
@@ -700,14 +758,18 @@ class ZigbeeController extends EventEmitter {
700
758
  }
701
759
 
702
760
  // Permit join
703
- async permitJoin(permitTime, devid, failure) {
761
+ async permitJoin(permitTime, devid) {
704
762
  try {
705
763
  this._permitJoinTime = permitTime;
706
764
  await this.herdsman.permitJoin(permitTime);
765
+ this._permitJoinDevice = (permitTime > 0 && devid ? devid : '');
707
766
  } catch (e) {
767
+ this._permitJoinTime = 0;
708
768
  this.sendError(e);
709
769
  this.error(`Failed to open the network: ${e.stack}`);
770
+ return false;
710
771
  }
772
+ return true;
711
773
  }
712
774
 
713
775
  async handlePermitJoinChanged(data)
@@ -730,6 +792,7 @@ class ZigbeeController extends EventEmitter {
730
792
  this._permitJoinInterval = null;
731
793
  // this.emit('pairing', 'Pairing time left', 0);
732
794
  this.emit('pairing', 'Closing network.',0);
795
+ this._permitJoinDevice = '';
733
796
  }
734
797
  }
735
798
  catch (error) {
@@ -774,8 +837,16 @@ class ZigbeeController extends EventEmitter {
774
837
  }
775
838
  } catch (error) {
776
839
  this.sendError(error);
777
- this.error(`Failed to remove ${error.stack}`);
778
- callback && callback(`Failed to remove ${error.stack}`);
840
+ let message = 'no reason given';
841
+ if (error && error.message) {
842
+ if (error.message.includes('AREQ - ZDO - mgmtLeaveRsp after'))
843
+ message = `No response to mgmtLeaveRequest from the device - device may be offline.`;
844
+ else
845
+ message = error.message;
846
+
847
+ }
848
+ this.warn(`Failed to remove ${deviceID ? 'device ' + deviceID : 'unspecified device'}: ${message}`);
849
+ callback && callback(`Failed to remove ${deviceID ? 'device ' + deviceID : 'unspecified device'}: ${message}`);
779
850
  }
780
851
  }
781
852
 
@@ -806,41 +877,52 @@ class ZigbeeController extends EventEmitter {
806
877
  if (this.debugActive) this.debug('handleDeviceAnnounce', message);
807
878
  const entity = await this.resolveEntity(message.device || message.ieeeAddr);
808
879
  const friendlyName = entity.name;
880
+
881
+
882
+ this.emit('pairing', `Device '${friendlyName}' announced itself`);
809
883
  if (this.adapter.stController.checkDebugDevice(friendlyName)) {
810
884
  this.emit('device_debug', {ID: Date.now(), data: {flag:'da', states:[{id: '--', value:'--', payload:message}] , IO:true} ,message:`Device '${friendlyName}' announced itself`});
811
885
  }
812
- else if (this.warnOnDeviceAnnouncement) {
813
- this.warn(`Device '${friendlyName}' announced itself`);
886
+ if (this.warnOnDeviceAnnouncement) {
887
+ this.warn(`Device '${friendlyName}' announced itself${this.readAtAnnounce ? ', trying to read its status' : ''}`);
814
888
  } else {
815
- this.info(`Device '${friendlyName}' announced itself`);
889
+ this.info(`Device '${friendlyName}' announced itself${this.readAtAnnounce ? ', trying to read its status' : ''}`);
890
+ }
891
+
892
+ if (entity.device && entity.device.modelID && entity.device.interviewState != 'SUCCESSFUL') {
893
+ this.info(`ignoring device announcement for ${entity.device.modelID} due to interview state ${entity.device.interviewState}`);
894
+ this.emit('pairing', `device interview state is ${entity.device.interviewState}`)
895
+ return;
816
896
  }
817
897
 
898
+ const networkOpen = this.herdsman.getPermitJoin();
899
+ /*
900
+ if (networkOpen && entity.device && entity.device.modelID && entity.device.interviewState != 'IN_PROGRESS')
901
+ {
902
+ //entity.device.modelID = entity.device._modelID;
903
+ //this.emit('new', entity);
904
+ return;
905
+ }
906
+ */
818
907
  try {
819
908
  if (entity && entity.mapped) {
820
909
  this.callExtensionMethod(
821
910
  'onZigbeeEvent',
822
- [{'device': message.device, 'type': 'deviceAnnounce'}, entity ? entity.mapped : null]);
911
+ [{'device': message.device, 'type': 'deviceAnnounce', options: entity.options || {}}, entity ? entity.mapped : null]);
912
+ this.callExtensionMethod('registerDevicePing', [message.device, entity]);
913
+ if (this.readAtAnnounce) await this.doDeviceQuery(message.device || message.ieeeAddr, Date.now(), false);
823
914
  }
824
915
  } catch (error) {
825
916
  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);
917
+ this.error(`Failed to handleDeviceAnnounce ${error.stack}`);
837
918
  }
838
919
  }
839
920
 
840
921
  async handleDeviceJoined(message) {
841
922
  if (this.debugActive) this.debug('handleDeviceJoined', message);
842
923
  //const entity = await this.resolveEntity(message.device || message.ieeeAddr);
843
- //this.emit('new', entity);
924
+ // this.emit('new', entity);
925
+ //if (entity && entity.mapped) this.callExtensionMethod([message, entity.mapped]);
844
926
  }
845
927
 
846
928
  async handleDeviceInterview(message) {
@@ -864,13 +946,13 @@ class ZigbeeController extends EventEmitter {
864
946
 
865
947
  const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
866
948
  this.emit('pairing', 'Interview successful', JSON.stringify(log));
867
- entity.device.modelID = entity.device._modelID;
868
- this.emit('new', entity);
869
- // send to extensions again (for configure)
949
+ //entity.device.modelID = entity.device._modelID;
870
950
  this.callExtensionMethod(
871
951
  'onZigbeeEvent',
872
- [message, entity ? entity.mapped : null],
952
+ [{...message,type:'deviceInterview', options: entity.options || {}}, entity.mapped],
873
953
  );
954
+ this.emit('new', entity);
955
+ // send to extensions again (for configure)
874
956
  } else {
875
957
  if (this.debugActive) this.debug(
876
958
  `Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
@@ -878,15 +960,23 @@ class ZigbeeController extends EventEmitter {
878
960
  );
879
961
  const frName = {friendly_name: friendlyName, supported: false};
880
962
  this.emit('pairing', 'Interview successful', JSON.stringify(frName));
881
- entity.device.modelID = entity.device._modelID;
963
+ //entity.device.modelID = entity.device._modelID;
882
964
  this.emit('new', entity);
883
965
  }
884
966
  } else if (message.status === 'failed') {
885
967
  this.error(`Failed to interview '${friendlyName}', device has not successfully been paired. Try again !!!!!!!!!! `);
886
968
  //this.error(`Failed to interview '${friendlyName}', device has not successfully been paired. Try again !!!!!!!!!! ${message.error}`);
887
969
  this.emit('pairing', 'Interview failed', friendlyName);
970
+ this.callExtensionMethod(
971
+ 'onZigbeeEvent',
972
+ [message, entity ? entity.mapped : null],
973
+ );
888
974
  } else {
889
975
  if (message.status === 'started') {
976
+ this.callExtensionMethod(
977
+ 'onZigbeeEvent',
978
+ [message, entity ? entity.mapped : null],
979
+ );
890
980
  this.info(`Starting interview of '${friendlyName}'`);
891
981
  this.emit('pairing', 'Interview started', friendlyName);
892
982
  }
@@ -899,6 +989,11 @@ class ZigbeeController extends EventEmitter {
899
989
 
900
990
  async handleMessage(data) {
901
991
  if (this.debugActive) this.debug(`handleMessage`, data);
992
+
993
+ const is = data.device.interviewState;
994
+ if (is != 'SUCCESSFUL' && is != 'FAILED') {
995
+ this.info(`message ${JSON.stringify(data)} received during interview.`)
996
+ }
902
997
  const entity = await this.resolveEntity(data.device || data.ieeeAddr);
903
998
  const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
904
999
  if (this.debugActive) this.debug(
@@ -907,11 +1002,10 @@ class ZigbeeController extends EventEmitter {
907
1002
  (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
908
1003
  );
909
1004
  this.event(data.type, entity, data);
910
-
911
1005
  // Call extensions
912
1006
  this.callExtensionMethod(
913
1007
  'onZigbeeEvent',
914
- [data, entity ? entity.mapped : null],
1008
+ [{...data, options:entity.options || {}}, entity ? entity.mapped : null],
915
1009
  );
916
1010
  }
917
1011
 
@@ -1000,7 +1094,14 @@ class ZigbeeController extends EventEmitter {
1000
1094
  }
1001
1095
  }
1002
1096
 
1097
+ processSyncStatesList(deviceId, model, syncStateList) {
1098
+ syncStateList.forEach((syncState) => {
1099
+ this.emit('acknowledge_state',deviceId, model, syncState.stateDesc, syncState.value);
1100
+ });
1101
+ }
1003
1102
 
1103
+ // publishing to the zigbee network
1104
+ // publish raw zigbee data
1004
1105
  async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
1005
1106
  const entity = await this.resolveEntity(deviceID, ep);
1006
1107
  const device = entity.device;
@@ -1024,37 +1125,413 @@ class ZigbeeController extends EventEmitter {
1024
1125
  cfg = {};
1025
1126
  }
1026
1127
 
1128
+ // try { NO Try/Catach here, this is ONLY called from the developer tab and error handling is done there
1129
+
1130
+ if (type === 'foundation') {
1131
+ cfg.disableDefaultResponse = true;
1132
+ let result;
1133
+ if (cmd === 'configReport') {
1134
+ result = await endpoint.configureReporting(cid, zclData, cfg);
1135
+ } else {
1136
+ if (cmd === 'read' && !Array.isArray(zclData))
1137
+ result = await endpoint[cmd](cid, Object.keys(zclData), cfg);
1138
+ else
1139
+ result = await endpoint[cmd](cid, zclData, cfg);
1140
+ }
1141
+ callback && callback(undefined, result);
1142
+ } else if (type === 'functionalResp') {
1143
+ cfg.disableDefaultResponse = false;
1144
+ const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
1145
+ callback && callback(undefined, result);
1146
+ } else {
1147
+ cfg.disableDefaultResponse = false;
1148
+ const result = await endpoint.command(cid, cmd, zclData, cfg);
1149
+ callback && callback(undefined, result);
1150
+ }
1151
+ /* }
1152
+ catch (error)
1153
+ {
1154
+ //this.error(`error sending ${type} ${cmd} to endpoint: ${(error && error.message ? error.message : 'no error message')} ${(error && error.stack ? error.stack : 'no call stack')}`)
1155
+ }
1156
+ */
1157
+ }
1158
+ // publish via converter
1159
+ //
1160
+ async publishFromState(deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug) {
1161
+ let isGroup = false;
1162
+ //const has_elevated_debug = this.stController.checkDebugDevice(deviceId)
1163
+
1164
+ if (has_elevated_debug)
1165
+ {
1166
+ const stateNames = stateList.map((state) => state.stateDesc.id);
1167
+ const message = `Publishing to ${deviceId} of model ${model} with ${stateNames.join(', ')}`;
1168
+ this.emit('device_debug', { ID:debugID, data: { ID: deviceId, flag: '03', IO:false }, message: message});
1169
+ }
1170
+ else
1171
+ if (this.debugActive) this.debug(`main publishFromState : ${deviceId} ${model} ${safeJsonStringify(stateList)}`);
1172
+ if (model === 'group') {
1173
+ isGroup = true;
1174
+ deviceId = parseInt(deviceId);
1175
+ }
1027
1176
  try {
1177
+ const entity = await this.resolveEntity(deviceId);
1178
+ if (this.debugActive) this.debug(`entity: ${deviceId} ${model} ${safeJsonStringify(entity)}`);
1179
+ const mappedModel = entity ? entity.mapped : undefined;
1180
+
1181
+ if (!mappedModel) {
1182
+ if (this.debugActive) this.debug(`No mapped model for ${model}`);
1183
+ if (has_elevated_debug) {
1184
+ const message=`No mapped model ${deviceId} (model ${model})`;
1185
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOMODEL' , IO:false }, message: message});
1186
+ }
1187
+ return;
1188
+ }
1028
1189
 
1029
- if (type === 'foundation') {
1030
- cfg.disableDefaultResponse = true;
1031
- let result;
1032
- if (cmd === 'configReport') {
1033
- result = await endpoint.configureReporting(cid, zclData, cfg);
1190
+ if (!mappedModel.toZigbee)
1191
+ {
1192
+ this.error(`No toZigbee in mapped model for ${model}`);
1193
+ return;
1194
+ }
1195
+
1196
+ stateList.forEach(async changedState => {
1197
+ const stateDesc = changedState.stateDesc;
1198
+ const value = changedState.value;
1199
+
1200
+ let converter = undefined;
1201
+ let msg_counter = 0;
1202
+ if (stateDesc.isOption) {
1203
+ if (has_elevated_debug) {
1204
+ const message = `No converter needed on option state for ${deviceId} of type ${model}`;
1205
+ this.emit('device_debug', { ID:debugID, data: { flag: `SUCCESS` , IO:false }, message:message});
1206
+ }
1207
+ else
1208
+ if (this.debugActive) this.debug(`No converter needed on option state for ${deviceId} of type ${model}`);
1209
+
1210
+ return;
1211
+ }
1212
+ for (const c of mappedModel.toZigbee) {
1213
+
1214
+ if (!c.hasOwnProperty('convertSet')) continue;
1215
+ if (this.debugActive) this.debug(`Type of toZigbee is '${typeof c}', Contains key ${(c.hasOwnProperty('key')?JSON.stringify(c.key):'false ')}`)
1216
+ if (!c.hasOwnProperty('key'))
1217
+ {
1218
+ if (converter === undefined)
1219
+ {
1220
+ converter = c;
1221
+ if (has_elevated_debug) {
1222
+ const message = `Setting converter to keyless converter for ${deviceId} of type ${model}`;
1223
+ this.emit('device_debug', { ID:debugID, data: { flag: `s4.${msg_counter}` , IO:false }, message:message});
1224
+ }
1225
+ else
1226
+ if (this.debugActive) this.debug(`Setting converter to keyless converter for ${deviceId} of type ${model}`);
1227
+ msg_counter++;
1228
+ }
1229
+ else
1230
+ {
1231
+ if (has_elevated_debug)
1232
+ {
1233
+ const message = `ignoring keyless converter for ${deviceId} of type ${model}`;
1234
+ this.emit('device_debug', { ID:debugID, data: { flag: `i4.${msg_counter}` , IO:false} , message:message});
1235
+ }
1236
+ else
1237
+ if (this.debugActive) this.debug(`ignoring keyless converter for ${deviceId} of type ${model}`);
1238
+ msg_counter++;
1239
+ }
1240
+ continue;
1241
+ }
1242
+ if (c.key.includes(stateDesc.prop) || c.key.includes(stateDesc.setattr) || c.key.includes(stateDesc.id))
1243
+ {
1244
+ const message = `${(converter===undefined?'Setting':'Overriding')}' converter to converter with key(s)'${JSON.stringify(c.key)}}`;
1245
+ if (has_elevated_debug) {
1246
+ this.emit('device_debugug', { ID:debugID, data: { flag: `${converter===undefined ? 's' : 'o'}4.${msg_counter}` , IO:false }, message:message});
1247
+
1248
+ }
1249
+ else
1250
+ if (this.debugActive) this.debug(message);
1251
+ converter = c;
1252
+ msg_counter++;
1253
+ }
1254
+ }
1255
+ if (converter === undefined) {
1256
+ const message = `No converter available for '${model}' with key '${stateDesc.id}' `;
1257
+ if (has_elevated_debug) {
1258
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOCONV',states:[{id:stateDesc.id, value:value, payload:'no converter'}] , IO:false }, message:message});
1259
+ }
1260
+ else {
1261
+ this.info(message);
1262
+ }
1263
+ return;
1264
+ }
1265
+
1266
+ const preparedValue = (stateDesc.setter) ? stateDesc.setter(value, options) : value;
1267
+ const preparedOptions = (stateDesc.setterOpt) ? stateDesc.setterOpt(value, options) : {};
1268
+
1269
+ let syncStateList = [];
1270
+ if (stateModel && stateModel.syncStates) {
1271
+ stateModel.syncStates.forEach(syncFunct => {
1272
+ const res = syncFunct(stateDesc, value, options);
1273
+ if (res) {
1274
+ syncStateList = syncStateList.concat(res);
1275
+ }
1276
+ });
1277
+ }
1278
+
1279
+ const epName = stateDesc.epname !== undefined ? stateDesc.epname : (stateDesc.prop || stateDesc.id);
1280
+ const key = stateDesc.setattr || stateDesc.prop || stateDesc.id;
1281
+ const message = `convert ${key} with value ${safeJsonStringify(preparedValue)} and options ${safeJsonStringify(preparedOptions)} for device ${deviceId} with Endpoint ${epName}`;
1282
+ if (has_elevated_debug) {
1283
+ this.emit('device_debug', { ID:debugID, data: { flag: '04', payload: {key:key, ep: stateDesc.epname, value:preparedValue, options:preparedOptions}, IO:false }, message:message});
1284
+ }
1285
+ else
1286
+ if (this.debugActive) this.debug(message);
1287
+
1288
+ let target;
1289
+ if (model === 'group') {
1290
+ target = entity.mapped;
1034
1291
  } else {
1035
- if (cmd === 'read' && !Array.isArray(zclData))
1036
- result = await endpoint[cmd](cid, Object.keys(zclData), cfg);
1292
+ target = await this.resolveEntity(deviceId, epName);
1293
+ target = target.endpoint;
1294
+ }
1295
+
1296
+ if (this.debugActive) this.debug(`target: ${safeJsonStringify(target)}`);
1297
+
1298
+ const meta = {
1299
+ endpoint_name: epName,
1300
+ options: preparedOptions,
1301
+ device: entity.device,
1302
+ mapped: model === 'group' ? [] : mappedModel,
1303
+ message: {[key]: preparedValue},
1304
+ logger: this,
1305
+ state: {},
1306
+ };
1307
+
1308
+ // new toZigbee
1309
+ if (preparedValue !== undefined && Object.keys(meta.message).filter(p => p.startsWith('state')).length > 0) {
1310
+ if (typeof preparedValue === 'number') {
1311
+ meta.message.state = preparedValue > 0 ? 'ON' : 'OFF';
1312
+ } else {
1313
+ meta.message.state = preparedValue;
1314
+ }
1315
+ }
1316
+ if (has_elevated_debug) {
1317
+ this.emit('device_debug', { ID:debugID, data: { states:[{id:stateDesc.id, value:value, payload:preparedValue, ep:stateDesc.epname}] , IO:false }});
1318
+ }
1319
+
1320
+ if (preparedOptions !== undefined) {
1321
+ if (preparedOptions.hasOwnProperty('state')) {
1322
+ meta.state = preparedOptions.state;
1323
+ }
1324
+ }
1325
+
1326
+ try {
1327
+ const result = await converter.convertSet(target, key, preparedValue, meta);
1328
+ const message = `convert result ${safeJsonStringify(result)} for device ${deviceId}`;
1329
+ if (isGroup)
1330
+ this.emit('published', deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug );
1331
+ if (has_elevated_debug) {
1332
+ this.emit('device_debug', { ID:debugID, data: { flag: 'SUCCESS' , IO:false }, message:message});
1333
+ }
1037
1334
  else
1038
- result = await endpoint[cmd](cid, zclData, cfg);
1335
+ if (this.debugActive) this.debug(message);
1336
+ if (result !== undefined) {
1337
+ if (stateModel && !isGroup && !stateDesc.noack) {
1338
+ this.emit('acknowledge_state', deviceId, model, stateDesc, value );
1339
+ }
1340
+ // process sync state list
1341
+ this.processSyncStatesList(deviceId, model, syncStateList);
1342
+ }
1343
+ else {
1344
+ if (has_elevated_debug) {
1345
+ const message = `Convert does not return a result result for ${key} with ${safeJsonStringify(preparedValue)} on device ${deviceId}.`;
1346
+ this.emit('device_debug', { ID:debugID, data: { flag: '06' , IO:false }, message:message});
1347
+ }
1348
+ }
1349
+ } catch (error) {
1350
+ if (has_elevated_debug) {
1351
+ const message = `caught error ${error && error.message ? error.message : 'no reason given'} when setting value for device ${deviceId}.`;
1352
+ this.emit('device_debug', { ID:debugID, data: { error: 'EXSET' , IO:false },message:message});
1353
+ }
1354
+ this.adapter.filterError(`Error ${error.code} on send command to ${deviceId}.` +
1355
+ ` Error: ${error.stack}`, `Send command to ${deviceId} failed with`, error);
1039
1356
  }
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);
1357
+ });
1358
+ } catch (err) {
1359
+ const message = `No entity for ${deviceId} : ${err && err.message ? err.message : 'no error message'}`;
1360
+ this.emit('device_debug', { ID:debugID, data: { error: 'EXPUB' , IO:false }, message:message});
1361
+ }
1362
+ }
1363
+
1364
+ extractEP(key, endpoints) {
1365
+ try {
1366
+ if (endpoints) for (const ep of Object.keys(endpoints)) {
1367
+ if (key.endsWith('_'+ep)) return { setattr: key.replace('_'+ep, ''), epname:ep }
1049
1368
  }
1050
1369
  }
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')}`)
1370
+ catch {
1371
+ return {};
1372
+ }
1373
+ return {};
1374
+ }
1375
+
1376
+ // publish via payload
1377
+ //
1378
+ // This function is introduced to explicitly allow user level scripts to send Commands
1379
+ // directly to the zigbee device. It utilizes the zigbee-herdsman-converters to generate
1380
+ // the exact zigbee message to be sent and can be used to set device options which are
1381
+ // not exposed as states. It serves as a wrapper function for "publishFromState" with
1382
+ // extended parameter checking
1383
+ //
1384
+ // The payload can either be a JSON object or the string representation of a JSON object
1385
+ // The following keys are supported in the object:
1386
+ // device: name of the device. For a device zigbee.0.0011223344556677 this would be 0011223344556677
1387
+ // payload: The data to send to the device as JSON object (key/Value pairs)
1388
+ // endpoint: optional: the endpoint to send the data to, if supported.
1389
+ //
1390
+ async publishPayload(payload, debugID, has_elevated_debug) {
1391
+ let payloadObj = {};
1392
+ if (typeof payload === 'string') {
1393
+ try {
1394
+ payloadObj = JSON.parse(payload);
1395
+ } catch (e) {
1396
+ this.log.error(`Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`);
1397
+ return {
1398
+ success: false,
1399
+ error: `Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`
1400
+ };
1401
+ }
1402
+ } else if (typeof payload === 'object') {
1403
+ payloadObj = payload;
1404
+ } else return { success: false, error: 'illegal type of payload: ' + typeof payload};
1405
+
1406
+ if (payloadObj.hasOwnProperty('device') && payloadObj.hasOwnProperty('payload')) {
1407
+ try {
1408
+ const isDevice = !payload.device.includes('group_');
1409
+ const stateList = [];
1410
+ const devID = isDevice ? `0x${payload.device}` : parseInt(payload.device.replace('group_', ''));
1411
+
1412
+ const entity = await this.resolveEntity(devID);
1413
+ if (!entity) {
1414
+ this.log.error(`Device ${safeJsonStringify(payloadObj.device)} not found`);
1415
+ return {success: false, error: `Device ${safeJsonStringify(payloadObj.device)} not found`};
1416
+ }
1417
+ const mappedModel = entity.mapped;
1418
+ if (!mappedModel) {
1419
+ this.log.error(`No Model for Device ${safeJsonStringify(payloadObj.device)}`);
1420
+ return {success: false, error: `No Model for Device ${safeJsonStringify(payloadObj.device)}`};
1421
+ }
1422
+ if (typeof payloadObj.payload !== 'object') {
1423
+ this.log.error(`Illegal payload type for ${safeJsonStringify(payloadObj.device)}`);
1424
+ return {success: false, error: `Illegal payload type for ${safeJsonStringify(payloadObj.device)}`};
1425
+ }
1426
+ const endpoints = mappedModel && mappedModel.endpoint ? mappedModel.endpoint(entity.device) : null;
1427
+ for (const key in payloadObj.payload) {
1428
+ if (payloadObj.payload[key] != undefined) {
1429
+ const datatype = typeof payloadObj.payload[key];
1430
+ const epobj = this.extractEP(key, endpoints);
1431
+ if (payloadObj.endpoint) {
1432
+ epobj.epname = payloadObj.endpoint;
1433
+ delete epobj.setattr;
1434
+ }
1435
+ stateList.push({
1436
+ stateDesc: {
1437
+ id: key,
1438
+ prop: key,
1439
+ role: 'state',
1440
+ type: datatype,
1441
+ noack:true,
1442
+ epname: epobj.epname,
1443
+ setattr: epobj.setattr,
1444
+ },
1445
+ value: payloadObj.payload[key],
1446
+ index: 0,
1447
+ timeout: 0,
1448
+ });
1449
+ }
1450
+ }
1451
+ try {
1452
+ await this.publishFromState(`0x${payload.device}`, payload.model, payload.stateModel, stateList, payload.options, debugID, has_elevated_debug);
1453
+ return {success: true};
1454
+ } catch (error) {
1455
+ this.log.error(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack} ` + `Send command to ${payload.device} failed with ` + error);
1456
+ this.adapter.filterError(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack}`, `Send command to ${payload.device} failed with`, error);
1457
+ return {success: false, error};
1458
+ }
1459
+ } catch (e) {
1460
+ return {success: false, error: e};
1461
+ }
1054
1462
  }
1055
1463
 
1464
+ return {success: false, error: `missing parameter device or payload in message ${JSON.stringify(payload)}`};
1056
1465
  }
1057
1466
 
1467
+ async doDeviceQuery(deviceId, debugID, elevated) {
1468
+ const entity = await this.resolveEntity(deviceId);
1469
+ if (this.debugActive) this.debug(`doDeviceQuery: resolveEntity for entity: ${deviceId} is ${safeJsonStringify(entity)}`);
1470
+ const mappedModel = entity ? entity.mapped : undefined;
1471
+ if (mappedModel) {
1472
+ const epmap = mappedModel.endpoint ? mappedModel.endpoint() : [];
1473
+ if (elevated) {
1474
+ const message = `Device query for '${entity.device.ieeeAddr}' triggered`;
1475
+ this.emit('device_debug', { ID:debugID, data: { flag: 'qs' ,states:[{id:'device_query', value:true, payload:'device_query'}], IO:false }, message:message});
1476
+ }
1477
+ else
1478
+ if (this.debugActive) this.debug(`Device query for '${entity.device.ieeeAddr}' started`);
1479
+ else this.info(`Device query for '${entity.device.ieeeAddr}' started`);
1480
+
1481
+ for (const converter of mappedModel.toZigbee) {
1482
+ if (converter.hasOwnProperty('convertGet')) {
1483
+ const sources = [];
1484
+ if (converter.endpoints && epmap) {
1485
+ for (const epname of converter.endpoints) {
1486
+ const source = entity.device.endpoints.find((id) => id.ID == epmap[epname]);
1487
+ if (source) sources.push(source);
1488
+ }
1489
+ }
1490
+ if (sources.length == 0) sources.push(entity.device.endpoints[0]);
1491
+ for (const source of sources) {
1492
+ try {
1493
+ await converter.convertGet(source, '', {device:entity.device});
1494
+ this.debug(`read for state${converter.key.length ? '' : 's'} '${converter.key.join(',')}' of '${entity.device.ieeeAddr}/${source.ID}' after device query`);
1495
+ } catch (error) {
1496
+ if (elevated) {
1497
+ const message = `Failed to read for state${converter.key.length ? '' : 's'} '${converter.key.join(',')}' of '${source.ID}' from query with '${error && error.message ? error.message : 'no error message'}`;
1498
+ this.warn(`ELEVATED OE02.1 ${message}`);
1499
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOTREAD' , IO:false }, message:message });
1500
+ }
1501
+ else
1502
+ this.debug(`failed to read for state${converter.key.length ? '' : 's'} '${converter.key.join(',')}' of '${source.ID}'after device query`);
1503
+ }
1504
+ }
1505
+ }
1506
+ }
1507
+ if (elevated) {
1508
+ const message = `ELEVATED O07: Device query for '${entity.device.ieeeAddr}}' complete`;
1509
+ this.emit('device_debug', { ID:debugID, data: { flag: 'qe' , IO:false }, message:message});
1510
+ }
1511
+ else
1512
+ this.info(`Device query for '${entity.device.ieeeAddr}' complete`);
1513
+ }
1514
+ }
1515
+
1516
+ async deviceQuery(deviceId, debugID, elevated, callback) {
1517
+ if (this.deviceQueryActive.includes (deviceId)) {
1518
+ this.info(`Device query for ${deviceId} is still active.`);
1519
+ return;
1520
+ }
1521
+ this.deviceQueryActive.push(deviceId);
1522
+ try {
1523
+ await this.doDeviceQuery(deviceId, debugID, elevated);
1524
+ }
1525
+ catch (e) {
1526
+ this.warn('error in doDeviceQuery')
1527
+ }
1528
+ const idx = this.deviceQueryActive.indexOf(deviceId)
1529
+ if (idx > -1)
1530
+ this.deviceQueryActive.splice(idx, 1);
1531
+ if (callback) callback(deviceId);
1532
+ }
1533
+
1534
+
1058
1535
  async addDevToGroup(devId, groupId, epid) {
1059
1536
  try {
1060
1537
  if (this.debugActive) this.debug(`called addDevToGroup with ${devId}, ${groupId}, ${epid}`);