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.
- package/README.md +26 -0
- package/admin/admin.js +153 -86
- package/admin/i18n/de/translations.json +16 -16
- package/admin/index_m.html +59 -90
- package/admin/tab_m.html +7 -5
- package/docs/de/readme.md +4 -1
- package/docs/en/readme.md +3 -1
- package/io-package.json +110 -54
- package/lib/binding.js +1 -1
- package/lib/commands.js +129 -97
- package/lib/developer.js +1 -1
- package/lib/devices.js +11 -7
- package/lib/exposes.js +14 -3
- package/lib/groups.js +400 -63
- package/lib/localConfig.js +16 -5
- package/lib/states.js +32 -2
- package/lib/statescontroller.js +254 -146
- package/lib/utils.js +7 -5
- package/lib/zbDeviceAvailability.js +78 -21
- package/lib/zbDeviceEvent.js +1 -1
- package/lib/zigbeecontroller.js +485 -56
- package/main.js +139 -469
- package/package.json +7 -7
package/lib/zigbeecontroller.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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')
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
|
1052
|
-
|
|
1053
|
-
|
|
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}`);
|