iobroker.zigbee2mqtt 3.0.20 → 3.1.1
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 +12 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +42 -55
- package/lib/check.js +19 -11
- package/lib/deviceController.js +167 -133
- package/lib/exposes.js +1350 -1315
- package/lib/imageController.js +49 -47
- package/lib/messages.js +14 -6
- package/lib/mqttServerController.js +113 -22
- package/lib/states.js +12 -5
- package/lib/statesController.js +68 -43
- package/lib/utils.js +1 -50
- package/lib/websocketController.js +151 -46
- package/lib/z2mController.js +38 -29
- package/main.js +398 -207
- package/package.json +13 -12
package/main.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/*
|
|
3
|
-
* Created with @iobroker/create-adapter v2.2.1
|
|
4
|
-
*/
|
|
5
2
|
|
|
6
3
|
// The adapter-core module gives you access to the core ioBroker functions
|
|
7
4
|
// you need to create an adapter
|
|
@@ -18,134 +15,174 @@ const StatesController = require('./lib/statesController').StatesController;
|
|
|
18
15
|
const WebsocketController = require('./lib/websocketController').WebsocketController;
|
|
19
16
|
const MqttServerController = require('./lib/mqttServerController').MqttServerController;
|
|
20
17
|
|
|
21
|
-
let mqttClient;
|
|
22
|
-
|
|
23
|
-
let deviceCache = [];
|
|
24
|
-
|
|
25
|
-
let groupCache = [];
|
|
26
|
-
const createCache = {};
|
|
27
|
-
const logCustomizations = { debugDevices: '', logfilter: [] };
|
|
28
|
-
let showInfo = true;
|
|
29
|
-
let statesController;
|
|
30
|
-
let deviceController;
|
|
31
|
-
let z2mController;
|
|
32
|
-
let websocketController;
|
|
33
|
-
let mqttServerController;
|
|
34
|
-
|
|
35
|
-
let messageParseMutex = Promise.resolve();
|
|
36
|
-
|
|
37
18
|
class Zigbee2mqtt extends core.Adapter {
|
|
38
19
|
constructor(options) {
|
|
39
20
|
super({
|
|
40
21
|
...options,
|
|
41
22
|
name: 'zigbee2mqtt',
|
|
42
23
|
});
|
|
24
|
+
|
|
25
|
+
// Instance-level state (kein Modul-globaler Zustand)
|
|
26
|
+
this.mqttClient = null;
|
|
27
|
+
this.deviceCache = [];
|
|
28
|
+
this.groupCache = [];
|
|
29
|
+
this.createCache = {};
|
|
30
|
+
this.logCustomizations = { debugDevices: '', logfilter: [] };
|
|
31
|
+
this.showInfo = true;
|
|
32
|
+
this.statesController = null;
|
|
33
|
+
this.deviceController = null;
|
|
34
|
+
this.z2mController = null;
|
|
35
|
+
this.websocketController = null;
|
|
36
|
+
this.mqttServerController = null;
|
|
37
|
+
this.messageParseMutex = Promise.resolve();
|
|
38
|
+
|
|
43
39
|
this.on('ready', this.onReady.bind(this));
|
|
44
40
|
this.on('stateChange', this.onStateChange.bind(this));
|
|
41
|
+
this.on('message', this.onMessage.bind(this));
|
|
45
42
|
this.on('unload', this.onUnload.bind(this));
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
async onReady() {
|
|
49
|
-
statesController = new StatesController(this, deviceCache, groupCache, logCustomizations, createCache);
|
|
50
|
-
deviceController = new DeviceController(
|
|
46
|
+
this.statesController = new StatesController(this, this.deviceCache, this.groupCache, this.logCustomizations, this.createCache);
|
|
47
|
+
this.deviceController = new DeviceController(
|
|
51
48
|
this,
|
|
52
|
-
deviceCache,
|
|
53
|
-
groupCache,
|
|
49
|
+
this.deviceCache,
|
|
50
|
+
this.groupCache,
|
|
54
51
|
this.config,
|
|
55
|
-
logCustomizations,
|
|
56
|
-
createCache
|
|
52
|
+
this.logCustomizations,
|
|
53
|
+
this.createCache
|
|
57
54
|
);
|
|
58
|
-
z2mController = new Z2mController(this, deviceCache, groupCache, logCustomizations);
|
|
55
|
+
this.z2mController = new Z2mController(this, this.deviceCache, this.groupCache, this.logCustomizations);
|
|
59
56
|
|
|
60
|
-
//
|
|
61
|
-
adapterInfo(this.config, this.log);
|
|
57
|
+
// Fix 2: adapterInfo ist async → awaiten
|
|
58
|
+
await adapterInfo(this.config, this.log);
|
|
62
59
|
|
|
63
60
|
this.setState('info.connection', false, true);
|
|
64
61
|
|
|
65
62
|
const debugDevicesState = await this.getStateAsync('info.debugmessages');
|
|
66
63
|
if (debugDevicesState && debugDevicesState.val) {
|
|
67
|
-
logCustomizations.debugDevices = String(debugDevicesState.val);
|
|
64
|
+
this.logCustomizations.debugDevices = String(debugDevicesState.val);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
const logfilterState = await this.getStateAsync('info.logfilter');
|
|
71
|
-
if (logfilterState && logfilterState.val) {
|
|
72
|
-
logCustomizations.logfilter = String(logfilterState.val)
|
|
68
|
+
if (logfilterState && logfilterState.val) {
|
|
69
|
+
this.logCustomizations.logfilter = String(logfilterState.val)
|
|
73
70
|
.split(';')
|
|
74
|
-
.filter((x) => x);
|
|
71
|
+
.filter((x) => x);
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
if (this.config.coordinatorCheck
|
|
74
|
+
if (this.config.coordinatorCheck === true) {
|
|
78
75
|
try {
|
|
79
|
-
schedule.scheduleJob('coordinatorCheck', this.config.coordinatorCheckCron, () =>
|
|
80
|
-
this.onStateChange('manual_trigger._.info.coordinator_check', { ack: false })
|
|
81
|
-
|
|
76
|
+
schedule.scheduleJob('coordinatorCheck', this.config.coordinatorCheckCron, () => {
|
|
77
|
+
this.onStateChange('manual_trigger._.info.coordinator_check', { ack: false, val: null })
|
|
78
|
+
.catch((e) => this.log.error(`coordinatorCheck trigger error: ${e}`));
|
|
79
|
+
});
|
|
82
80
|
} catch (e) {
|
|
83
81
|
this.log.error(e);
|
|
84
82
|
}
|
|
85
83
|
}
|
|
84
|
+
|
|
86
85
|
// MQTT
|
|
87
86
|
if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
|
|
88
87
|
// External MQTT-Server
|
|
89
|
-
if (this.config.connectionType
|
|
90
|
-
if (this.config.externalMqttServerIP
|
|
88
|
+
if (this.config.connectionType === 'exmqtt') {
|
|
89
|
+
if (!this.config.externalMqttServerIP) {
|
|
91
90
|
this.log.warn('Please configure the External MQTT-Server connection!');
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
// MQTT connection settings
|
|
96
94
|
const mqttClientOptions = {
|
|
97
95
|
clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
|
|
98
96
|
clean: true,
|
|
99
97
|
reconnectPeriod: 500,
|
|
100
98
|
};
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
if (this.config.externalMqttServerCredentials == true) {
|
|
100
|
+
if (this.config.externalMqttServerCredentials === true) {
|
|
104
101
|
mqttClientOptions.username = this.config.externalMqttServerUsername;
|
|
105
102
|
mqttClientOptions.password = this.config.externalMqttServerPassword;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
mqttClient = mqtt.connect(
|
|
105
|
+
this.mqttClient = mqtt.connect(
|
|
110
106
|
`mqtt://${this.config.externalMqttServerIP}:${this.config.externalMqttServerPort}`,
|
|
111
107
|
mqttClientOptions
|
|
112
108
|
);
|
|
113
109
|
} else {
|
|
114
|
-
|
|
115
|
-
mqttServerController = new MqttServerController(this);
|
|
116
|
-
await mqttServerController.createMQTTServer();
|
|
110
|
+
// Internal MQTT-Server
|
|
111
|
+
this.mqttServerController = new MqttServerController(this);
|
|
112
|
+
await this.mqttServerController.createMQTTServer();
|
|
117
113
|
await this.delay(1500);
|
|
118
|
-
mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
|
|
114
|
+
this.mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
|
|
119
115
|
clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
|
|
120
116
|
clean: true,
|
|
121
117
|
reconnectPeriod: 500,
|
|
122
118
|
});
|
|
123
119
|
}
|
|
124
120
|
|
|
125
|
-
// MQTT Client
|
|
126
|
-
mqttClient.on('connect', () => {
|
|
121
|
+
// MQTT Client Events
|
|
122
|
+
this.mqttClient.on('connect', () => {
|
|
127
123
|
this.log.info(
|
|
128
|
-
`Connect to Zigbee2MQTT over ${this.config.connectionType
|
|
124
|
+
`Connect to Zigbee2MQTT over ${this.config.connectionType === 'exmqtt' ? 'external mqtt' : 'internal mqtt'} connection.`
|
|
129
125
|
);
|
|
126
|
+
this.setStateChanged('info.connection', true, true);
|
|
127
|
+
this.mqttClient.subscribe(`${this.config.baseTopic}/#`, (err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
this.log.error(`MQTT subscribe error: ${err && err.message ? err.message : String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.mqttClient.on('reconnect', () => {
|
|
135
|
+
this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.mqttClient.on('offline', async () => {
|
|
139
|
+
this.log.warn('MQTT client offline – connection to Zigbee2MQTT lost.');
|
|
140
|
+
this.setStateChanged('info.connection', false, true);
|
|
141
|
+
try {
|
|
142
|
+
if (this.statesController) {
|
|
143
|
+
await this.statesController.setAllAvailableToFalse();
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
this.log.error(`MQTT offline setAllAvailableToFalse error: ${e}`);
|
|
147
|
+
}
|
|
130
148
|
});
|
|
131
149
|
|
|
132
|
-
mqttClient.
|
|
150
|
+
this.mqttClient.on('error', (err) => {
|
|
151
|
+
this.log.error(`MQTT client error: ${err && err.message ? err.message : String(err)}`);
|
|
152
|
+
});
|
|
133
153
|
|
|
134
|
-
mqttClient.on('message', (topic, payload) => {
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
this.mqttClient.on('message', (topic, payload) => {
|
|
155
|
+
// baseTopic aus dem Topic entfernen – Guard falls kein '/' vorhanden
|
|
156
|
+
const sepIdx = topic.indexOf('/');
|
|
157
|
+
if (sepIdx === -1) {
|
|
158
|
+
this.log.debug(`MQTT message with unexpected topic format: ${topic}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const payloadStr = payload.toString();
|
|
162
|
+
let parsedPayload;
|
|
163
|
+
try {
|
|
164
|
+
parsedPayload = payloadStr === '' ? null : JSON.parse(payloadStr);
|
|
165
|
+
} catch {
|
|
166
|
+
parsedPayload = payloadStr;
|
|
167
|
+
}
|
|
168
|
+
const messageObj = {
|
|
169
|
+
payload: parsedPayload,
|
|
170
|
+
topic: topic.slice(sepIdx + 1),
|
|
171
|
+
};
|
|
172
|
+
this.messageParse(messageObj).catch((err) => {
|
|
173
|
+
this.log.error(`messageParse error: ${err}`);
|
|
174
|
+
});
|
|
137
175
|
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (this.config.wsServerIP
|
|
176
|
+
} else if (this.config.connectionType === 'ws') {
|
|
177
|
+
// Websocket
|
|
178
|
+
if (!this.config.wsServerIP) {
|
|
141
179
|
this.log.warn('Please configure the Websoket connection!');
|
|
142
180
|
return;
|
|
143
181
|
}
|
|
144
182
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
mqttServerController
|
|
148
|
-
await mqttServerController.createDummyMQTTServer();
|
|
183
|
+
if (this.config.dummyMqtt === true) {
|
|
184
|
+
this.mqttServerController = new MqttServerController(this);
|
|
185
|
+
await this.mqttServerController.createDummyMQTTServer();
|
|
149
186
|
await this.delay(1500);
|
|
150
187
|
}
|
|
151
188
|
|
|
@@ -154,236 +191,390 @@ class Zigbee2mqtt extends core.Adapter {
|
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
startWebsocket() {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (wsClient) {
|
|
161
|
-
wsClient.on('open', () => {
|
|
162
|
-
this.log.info('Connect to Zigbee2MQTT over websocket connection.');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
wsClient.on('message', (message) => {
|
|
166
|
-
this.messageParse(message);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
wsClient.on('close', async () => {
|
|
170
|
-
this.setStateChanged('info.connection', false, true);
|
|
171
|
-
await statesController.setAllAvailableToFalse();
|
|
172
|
-
deviceCache = [];
|
|
173
|
-
groupCache = [];
|
|
174
|
-
});
|
|
194
|
+
// Controller nur einmal erstellen – bei Reconnect wird derselbe wiederverwendet
|
|
195
|
+
if (!this.websocketController) {
|
|
196
|
+
this.websocketController = new WebsocketController(this);
|
|
175
197
|
}
|
|
198
|
+
this.websocketController.initWsClient();
|
|
176
199
|
}
|
|
177
200
|
|
|
178
|
-
async messageParse(
|
|
179
|
-
// Mutex
|
|
180
|
-
|
|
201
|
+
async messageParse(messageObj) {
|
|
202
|
+
// Mutex: serialisiert alle messageParse-Aufrufe
|
|
203
|
+
// Wichtig: lock muss IMMER wieder freigegeben werden, auch bei early return / Fehler.
|
|
204
|
+
let release = () => {};
|
|
181
205
|
const lock = new Promise((resolve) => (release = resolve));
|
|
182
|
-
const prev = messageParseMutex;
|
|
183
|
-
messageParseMutex = lock;
|
|
184
|
-
|
|
206
|
+
const prev = this.messageParseMutex;
|
|
207
|
+
this.messageParseMutex = lock;
|
|
208
|
+
|
|
209
|
+
// Wenn prev rejected hat, wollen wir trotzdem weiterarbeiten (sonst blockiert alles)
|
|
185
210
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
await prev;
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
if (!messageObj || typeof messageObj !== 'object') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!this.statesController || !this.deviceController || !this.z2mController) {
|
|
221
|
+
this.log.debug('messageParse: controllers not yet initialized, dropping message.');
|
|
188
222
|
return;
|
|
189
223
|
}
|
|
190
|
-
|
|
191
|
-
const messageObj = JSON.parse(message);
|
|
192
224
|
|
|
193
225
|
switch (messageObj.topic) {
|
|
194
|
-
case '
|
|
226
|
+
case 'Coordinator/availability': {
|
|
227
|
+
const coordState = typeof messageObj.payload === 'object' && messageObj.payload !== null
|
|
228
|
+
? messageObj.payload.state
|
|
229
|
+
: messageObj.payload;
|
|
230
|
+
this.setStateChanged('info.coordinator_status', coordState, true);
|
|
195
231
|
break;
|
|
232
|
+
}
|
|
196
233
|
case 'bridge/info':
|
|
197
|
-
if (showInfo) {
|
|
198
|
-
zigbee2mqttInfo(messageObj.payload, this.log);
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
if (this.showInfo && messageObj.payload) {
|
|
235
|
+
await zigbee2mqttInfo(messageObj.payload, this.log);
|
|
236
|
+
if (messageObj.payload.config && messageObj.payload.version) {
|
|
237
|
+
checkConfig(messageObj.payload.config, this.log, messageObj.payload.version);
|
|
238
|
+
}
|
|
239
|
+
this.showInfo = false;
|
|
201
240
|
}
|
|
202
241
|
break;
|
|
203
|
-
case 'bridge/state':
|
|
204
|
-
|
|
205
|
-
|
|
242
|
+
case 'bridge/state': {
|
|
243
|
+
const bridgeState = typeof messageObj.payload === 'object' && messageObj.payload !== null
|
|
244
|
+
? messageObj.payload.state
|
|
245
|
+
: messageObj.payload;
|
|
246
|
+
if (bridgeState !== 'online') {
|
|
247
|
+
await this.statesController.setAllAvailableToFalse();
|
|
248
|
+
this.showInfo = true;
|
|
206
249
|
}
|
|
207
|
-
this.setStateChanged('info.connection',
|
|
250
|
+
this.setStateChanged('info.connection', bridgeState === 'online', true);
|
|
208
251
|
break;
|
|
252
|
+
}
|
|
209
253
|
case 'bridge/devices':
|
|
210
|
-
await deviceController.createDeviceDefinitions(messageObj.payload);
|
|
211
|
-
await deviceController.createOrUpdateDevices();
|
|
212
|
-
await deviceController.checkAndProgressDeviceRemove();
|
|
213
|
-
await statesController.subscribeWritableStates();
|
|
214
|
-
statesController.processQueue();
|
|
254
|
+
await this.deviceController.createDeviceDefinitions(messageObj.payload);
|
|
255
|
+
await this.deviceController.createOrUpdateDevices();
|
|
256
|
+
await this.deviceController.checkAndProgressDeviceRemove();
|
|
257
|
+
await this.statesController.subscribeWritableStates();
|
|
258
|
+
await this.statesController.processQueue();
|
|
215
259
|
break;
|
|
216
260
|
case 'bridge/groups':
|
|
217
|
-
await deviceController.createGroupDefinitions(messageObj.payload);
|
|
218
|
-
await deviceController.createOrUpdateDevices();
|
|
219
|
-
await statesController.subscribeWritableStates();
|
|
220
|
-
statesController.processQueue();
|
|
221
|
-
break;
|
|
222
|
-
case 'bridge/event':
|
|
261
|
+
await this.deviceController.createGroupDefinitions(messageObj.payload);
|
|
262
|
+
await this.deviceController.createOrUpdateDevices();
|
|
263
|
+
await this.statesController.subscribeWritableStates();
|
|
264
|
+
await this.statesController.processQueue();
|
|
223
265
|
break;
|
|
224
266
|
case 'bridge/response/coordinator_check':
|
|
225
|
-
deviceController.processCoordinatorCheck(messageObj.payload);
|
|
226
|
-
break;
|
|
227
|
-
case 'bridge/response/device/remove':
|
|
228
|
-
break;
|
|
229
|
-
case 'bridge/response/device/options':
|
|
230
|
-
break;
|
|
231
|
-
case 'bridge/response/permit_join':
|
|
232
|
-
break;
|
|
233
|
-
case 'bridge/extensions':
|
|
267
|
+
await this.deviceController.processCoordinatorCheck(messageObj.payload);
|
|
234
268
|
break;
|
|
235
269
|
case 'bridge/logging':
|
|
236
|
-
if (this.config.proxyZ2MLogs
|
|
237
|
-
z2mController.proxyZ2MLogs(messageObj);
|
|
270
|
+
if (this.config.proxyZ2MLogs === true) {
|
|
271
|
+
await this.z2mController.proxyZ2MLogs(messageObj);
|
|
238
272
|
}
|
|
239
273
|
break;
|
|
240
|
-
case 'bridge/response/device/configure':
|
|
241
|
-
break;
|
|
242
274
|
case 'bridge/response/device/rename':
|
|
243
|
-
await deviceController.renameDeviceInCache(messageObj);
|
|
244
|
-
await deviceController.createOrUpdateDevices();
|
|
245
|
-
statesController.processQueue();
|
|
275
|
+
await this.deviceController.renameDeviceInCache(messageObj);
|
|
276
|
+
await this.deviceController.createOrUpdateDevices();
|
|
277
|
+
await this.statesController.processQueue();
|
|
246
278
|
break;
|
|
247
|
-
case 'bridge/
|
|
279
|
+
case 'bridge/event': {
|
|
280
|
+
const evType = messageObj.payload && messageObj.payload.type;
|
|
281
|
+
const evData = messageObj.payload && messageObj.payload.data;
|
|
282
|
+
if (evType === 'device_announce' && evData && evData.friendly_name) {
|
|
283
|
+
const newMessage = { payload: { available: true }, topic: evData.friendly_name };
|
|
284
|
+
await this.statesController.processDeviceMessage(newMessage);
|
|
285
|
+
} else if (evType === 'device_leave' && evData && evData.friendly_name) {
|
|
286
|
+
const newMessage = { payload: { available: false }, topic: evData.friendly_name };
|
|
287
|
+
await this.statesController.processDeviceMessage(newMessage);
|
|
288
|
+
}
|
|
248
289
|
break;
|
|
290
|
+
}
|
|
291
|
+
case 'bridge/config':
|
|
292
|
+
case 'bridge/health':
|
|
293
|
+
case 'bridge/definitions':
|
|
294
|
+
case 'bridge/extensions':
|
|
295
|
+
case 'bridge/response/device/configure':
|
|
296
|
+
case 'bridge/response/device/remove':
|
|
297
|
+
case 'bridge/response/device/options':
|
|
298
|
+
case 'bridge/response/device/interview':
|
|
299
|
+
case 'bridge/response/permit_join':
|
|
300
|
+
case 'bridge/response/networkmap':
|
|
301
|
+
case 'bridge/response/options':
|
|
302
|
+
case 'bridge/response/restart':
|
|
303
|
+
case 'bridge/response/backup':
|
|
304
|
+
case 'bridge/response/install_code/add':
|
|
305
|
+
case 'bridge/response/group/add':
|
|
306
|
+
case 'bridge/response/group/remove':
|
|
307
|
+
case 'bridge/response/group/members/add':
|
|
308
|
+
case 'bridge/response/group/members/remove':
|
|
249
309
|
case 'bridge/response/touchlink/scan':
|
|
250
|
-
break;
|
|
251
310
|
case 'bridge/response/touchlink/identify':
|
|
252
|
-
break;
|
|
253
311
|
case 'bridge/response/touchlink/factory_reset':
|
|
254
312
|
break;
|
|
255
|
-
default:
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
} else {
|
|
275
|
-
// With the MQTT output type attribute_and_json, primitive payloads arrive here that must be discarded.
|
|
276
|
-
if (utils.isObject(messageObj.payload) == false) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
// If MQTT is used, I have to filter the self-sent 'set' commands.
|
|
280
|
-
if (messageObj.topic.endsWith('/set')) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
statesController.processDeviceMessage(messageObj);
|
|
313
|
+
default: {
|
|
314
|
+
if (!messageObj.topic || typeof messageObj.topic !== 'string') {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
if (messageObj.topic.endsWith('/availability')) {
|
|
318
|
+
if (messageObj.payload === null || messageObj.payload === undefined) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
const availState = typeof messageObj.payload === 'object'
|
|
322
|
+
? messageObj.payload.state
|
|
323
|
+
: messageObj.payload;
|
|
324
|
+
const newMessage = {
|
|
325
|
+
payload: { available: availState === 'online' },
|
|
326
|
+
topic: messageObj.topic.replace('/availability', ''),
|
|
327
|
+
};
|
|
328
|
+
await this.statesController.processDeviceMessage(newMessage);
|
|
329
|
+
} else {
|
|
330
|
+
if (!utils.isObject(messageObj.payload)) {
|
|
331
|
+
break;
|
|
285
332
|
}
|
|
333
|
+
if (messageObj.topic.endsWith('/set')) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
await this.statesController.processDeviceMessage(messageObj);
|
|
286
337
|
}
|
|
287
338
|
break;
|
|
339
|
+
}
|
|
288
340
|
}
|
|
289
341
|
} finally {
|
|
342
|
+
// Mutex immer freigeben
|
|
290
343
|
release();
|
|
291
344
|
}
|
|
292
345
|
}
|
|
293
346
|
|
|
347
|
+
async onMessage(obj) {
|
|
348
|
+
if (!obj || !obj.command) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (obj.command === 'deleteOldStates') {
|
|
353
|
+
const deletedList = [];
|
|
354
|
+
const errorList = [];
|
|
355
|
+
try {
|
|
356
|
+
// Guard: Caches müssen gefüllt sein – sonst würden ALLE States gelöscht
|
|
357
|
+
if (this.deviceCache.length === 0 && this.groupCache.length === 0) {
|
|
358
|
+
const warn = 'deleteOldStates: device list is empty – adapter not connected to Zigbee2MQTT yet. Aborting.';
|
|
359
|
+
this.log.warn(warn);
|
|
360
|
+
if (obj.callback) {
|
|
361
|
+
this.sendTo(obj.from, obj.command, { error: warn }, obj.callback);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Nur echte Geräte (kein group_*) – Gruppen werden beim Löschen komplett ausgelassen
|
|
367
|
+
const knownStateIDs = new Set();
|
|
368
|
+
for (const device of this.deviceCache) {
|
|
369
|
+
if (!device || !device.ieee_address || !Array.isArray(device.states)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const base = `${this.namespace}.${device.ieee_address}`;
|
|
373
|
+
for (const state of device.states) {
|
|
374
|
+
if (state && state.id) {
|
|
375
|
+
knownStateIDs.add(`${base}.${state.id}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// additional-Channel und Pflicht-States immer erlauben
|
|
379
|
+
knownStateIDs.add(`${base}.additional`);
|
|
380
|
+
knownStateIDs.add(`${base}.available`);
|
|
381
|
+
knownStateIDs.add(`${base}.last_seen`);
|
|
382
|
+
knownStateIDs.add(`${base}.send_payload`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Alle vorhandenen Adapter-Objekte laden
|
|
386
|
+
const allObjects = await this.getAdapterObjectsAsync();
|
|
387
|
+
for (const id of Object.keys(allObjects)) {
|
|
388
|
+
const iobObj = allObjects[id];
|
|
389
|
+
|
|
390
|
+
// Nur States löschen – keine channels, devices, folders
|
|
391
|
+
if (!iobObj || iobObj.type !== 'state') {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// info.* States niemals löschen
|
|
396
|
+
if (id.includes('.info.')) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Gruppen-States (group_*) niemals löschen
|
|
401
|
+
// Format: adapter.instance.ieee_address.stateid → parts[2] ist ieee_address
|
|
402
|
+
const parts = id.split('.');
|
|
403
|
+
if (parts.length >= 3 && parts[2].startsWith('group_')) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// additional.* Sub-States erlauben
|
|
408
|
+
if (parts.length >= 5) {
|
|
409
|
+
const additionalBase = parts.slice(0, 4).join('.');
|
|
410
|
+
if (knownStateIDs.has(additionalBase)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Wenn nicht im bekannten Set → löschen
|
|
416
|
+
if (!knownStateIDs.has(id)) {
|
|
417
|
+
try {
|
|
418
|
+
await this.delObjectAsync(id);
|
|
419
|
+
deletedList.push(id);
|
|
420
|
+
this.log.debug(`deleteOldStates: deleted ${id}`);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
errorList.push(id);
|
|
423
|
+
this.log.warn(`deleteOldStates: could not delete ${id}: ${e}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Liste aller gelöschten States als Warning ausgeben
|
|
429
|
+
if (deletedList.length > 0) {
|
|
430
|
+
this.log.warn(`deleteOldStates: deleted ${deletedList.length} state(s):`);
|
|
431
|
+
for (const deletedId of deletedList) {
|
|
432
|
+
this.log.warn(` - ${deletedId}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (errorList.length > 0) {
|
|
436
|
+
this.log.warn(`deleteOldStates: failed to delete ${errorList.length} state(s):`);
|
|
437
|
+
for (const errId of errorList) {
|
|
438
|
+
this.log.warn(` - ${errId}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const msg = `Deleted ${deletedList.length} old state(s)${errorList.length > 0 ? `, ${errorList.length} error(s)` : ''}.`;
|
|
443
|
+
this.log.info(`deleteOldStates: ${msg}`);
|
|
444
|
+
if (obj.callback) {
|
|
445
|
+
this.sendTo(obj.from, obj.command, { result: msg }, obj.callback);
|
|
446
|
+
}
|
|
447
|
+
} catch (e) {
|
|
448
|
+
this.log.error(`deleteOldStates error: ${e}`);
|
|
449
|
+
if (obj.callback) {
|
|
450
|
+
this.sendTo(obj.from, obj.command, { error: String(e) }, obj.callback);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
294
456
|
async onUnload(callback) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
457
|
+
try {
|
|
458
|
+
if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
|
|
459
|
+
if (this.mqttClient && !this.mqttClient.disconnected) {
|
|
460
|
+
try {
|
|
461
|
+
this.mqttClient.removeAllListeners();
|
|
462
|
+
this.mqttClient.end(true);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
this.log.error(e);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (this.config.connectionType === 'intmqtt' || this.config.dummyMqtt === true) {
|
|
469
|
+
try {
|
|
470
|
+
if (this.mqttServerController) {
|
|
471
|
+
this.mqttServerController.closeServer();
|
|
472
|
+
}
|
|
473
|
+
} catch (e) {
|
|
474
|
+
this.log.error(e);
|
|
475
|
+
}
|
|
476
|
+
} else if (this.config.connectionType === 'ws') {
|
|
298
477
|
try {
|
|
299
|
-
if (
|
|
300
|
-
|
|
478
|
+
if (this.websocketController) {
|
|
479
|
+
this.websocketController.closeConnection();
|
|
301
480
|
}
|
|
302
481
|
} catch (e) {
|
|
303
482
|
this.log.error(e);
|
|
304
483
|
}
|
|
305
484
|
}
|
|
306
|
-
}
|
|
307
|
-
// Internal or Dummy MQTT-Server
|
|
308
|
-
if (this.config.connectionType == 'intmqtt' || this.config.dummyMqtt == true) {
|
|
309
485
|
try {
|
|
310
|
-
if (
|
|
311
|
-
|
|
486
|
+
if (this.statesController) {
|
|
487
|
+
await this.statesController.setAllAvailableToFalse();
|
|
312
488
|
}
|
|
313
489
|
} catch (e) {
|
|
314
490
|
this.log.error(e);
|
|
315
491
|
}
|
|
316
|
-
} else if (this.config.connectionType == 'ws') {
|
|
317
|
-
// Websocket
|
|
318
492
|
try {
|
|
319
|
-
if (websocketController) {
|
|
320
|
-
websocketController.
|
|
493
|
+
if (this.websocketController) {
|
|
494
|
+
await this.websocketController.allTimerClear();
|
|
321
495
|
}
|
|
322
496
|
} catch (e) {
|
|
323
497
|
this.log.error(e);
|
|
324
498
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
} catch (e) {
|
|
332
|
-
this.log.error(e);
|
|
333
|
-
}
|
|
334
|
-
// Clear all websocket timers
|
|
335
|
-
try {
|
|
336
|
-
if (websocketController) {
|
|
337
|
-
await websocketController.allTimerClear();
|
|
338
|
-
}
|
|
339
|
-
} catch (e) {
|
|
340
|
-
this.log.error(e);
|
|
341
|
-
}
|
|
342
|
-
// Clear all state timers
|
|
343
|
-
try {
|
|
344
|
-
if (statesController) {
|
|
345
|
-
await statesController.allTimerClear();
|
|
499
|
+
try {
|
|
500
|
+
if (this.statesController) {
|
|
501
|
+
await this.statesController.allTimerClear();
|
|
502
|
+
}
|
|
503
|
+
} catch (e) {
|
|
504
|
+
this.log.error(e);
|
|
346
505
|
}
|
|
347
|
-
} catch (e) {
|
|
348
|
-
this.log.error(e);
|
|
349
|
-
}
|
|
350
506
|
|
|
351
|
-
|
|
507
|
+
this.setState('info.connection', false, true);
|
|
352
508
|
|
|
353
|
-
|
|
509
|
+
// Schedule-Job beenden
|
|
510
|
+
const job = schedule.scheduledJobs['coordinatorCheck'];
|
|
511
|
+
if (job) {
|
|
512
|
+
job.cancel();
|
|
513
|
+
}
|
|
514
|
+
} finally {
|
|
515
|
+
// callback() wird IMMER aufgerufen – auch wenn oben etwas wirft
|
|
516
|
+
// Ohne das hängt der Adapter-Stop dauerhaft
|
|
517
|
+
callback();
|
|
518
|
+
}
|
|
354
519
|
}
|
|
355
520
|
|
|
356
521
|
async onStateChange(id, state) {
|
|
357
|
-
if (state && state.ack
|
|
522
|
+
if (state && state.ack === false) {
|
|
358
523
|
if (id.endsWith('info.debugmessages')) {
|
|
359
|
-
logCustomizations.debugDevices = state.val;
|
|
524
|
+
this.logCustomizations.debugDevices = String(state.val || '');
|
|
360
525
|
this.setState(id, state.val, true);
|
|
361
526
|
return;
|
|
362
527
|
}
|
|
363
528
|
if (id.endsWith('info.logfilter')) {
|
|
364
|
-
logCustomizations.logfilter = state.val.split(';').filter((x) => x);
|
|
529
|
+
this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
|
|
365
530
|
this.setState(id, state.val, true);
|
|
366
531
|
return;
|
|
367
532
|
}
|
|
368
533
|
|
|
369
|
-
|
|
534
|
+
if (!this.z2mController) {
|
|
535
|
+
this.log.debug(`onStateChange: z2mController not yet initialized, dropping state change for ${id}.`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const message = await this.z2mController.createZ2MMessage(id, state);
|
|
540
|
+
if (!message || !message.topic) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
370
543
|
|
|
371
544
|
if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
545
|
+
if (!this.mqttClient || this.mqttClient.disconnected) {
|
|
546
|
+
this.log.warn(`Cannot publish state, MQTT client not connected. (${id})`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
this.mqttClient.publish(
|
|
551
|
+
`${this.config.baseTopic}/${message.topic}`,
|
|
552
|
+
JSON.stringify(message.payload),
|
|
553
|
+
(err) => {
|
|
554
|
+
if (err) {
|
|
555
|
+
this.log.error(`MQTT publish error for ${id}: ${err && err.message ? err.message : String(err)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
this.log.error(`MQTT publish exception for ${id}: ${e}`);
|
|
561
|
+
}
|
|
562
|
+
} else if (this.config.connectionType === 'ws') {
|
|
563
|
+
if (!this.websocketController) {
|
|
564
|
+
this.log.warn(`Cannot send state, WebSocket not initialized. (${id})`);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.websocketController.send(JSON.stringify({ topic: message.topic, payload: message.payload }));
|
|
375
568
|
}
|
|
376
569
|
}
|
|
377
570
|
}
|
|
378
571
|
}
|
|
379
572
|
|
|
380
573
|
if (require.main !== module) {
|
|
381
|
-
// Export the constructor in compact mode
|
|
382
574
|
/**
|
|
383
575
|
* @param {Partial<core.AdapterOptions>} [options]
|
|
384
576
|
*/
|
|
385
577
|
module.exports = (options) => new Zigbee2mqtt(options);
|
|
386
578
|
} else {
|
|
387
|
-
// otherwise start the instance directly
|
|
388
579
|
new Zigbee2mqtt();
|
|
389
580
|
}
|