iobroker.zigbee2mqtt 3.0.21 → 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 +9 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +16 -42
- 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 +67 -45
- package/lib/utils.js +1 -50
- package/lib/websocketController.js +151 -46
- package/lib/z2mController.js +38 -29
- package/main.js +390 -194
- 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
|
+
});
|
|
130
132
|
});
|
|
131
133
|
|
|
132
|
-
mqttClient.
|
|
134
|
+
this.mqttClient.on('reconnect', () => {
|
|
135
|
+
this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
|
|
136
|
+
});
|
|
133
137
|
|
|
134
|
-
mqttClient.on('
|
|
135
|
-
|
|
136
|
-
this.
|
|
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
|
+
}
|
|
137
148
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
|
|
150
|
+
this.mqttClient.on('error', (err) => {
|
|
151
|
+
this.log.error(`MQTT client error: ${err && err.message ? err.message : String(err)}`);
|
|
152
|
+
});
|
|
153
|
+
|
|
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
|
+
});
|
|
175
|
+
});
|
|
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,231 +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 'Coordinator/availability':
|
|
195
|
-
|
|
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);
|
|
196
231
|
break;
|
|
232
|
+
}
|
|
197
233
|
case 'bridge/info':
|
|
198
|
-
if (showInfo) {
|
|
199
|
-
zigbee2mqttInfo(messageObj.payload, this.log);
|
|
200
|
-
|
|
201
|
-
|
|
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;
|
|
202
240
|
}
|
|
203
241
|
break;
|
|
204
|
-
case 'bridge/state':
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
207
249
|
}
|
|
208
|
-
this.setStateChanged('info.connection',
|
|
250
|
+
this.setStateChanged('info.connection', bridgeState === 'online', true);
|
|
209
251
|
break;
|
|
252
|
+
}
|
|
210
253
|
case 'bridge/devices':
|
|
211
|
-
await deviceController.createDeviceDefinitions(messageObj.payload);
|
|
212
|
-
await deviceController.createOrUpdateDevices();
|
|
213
|
-
await deviceController.checkAndProgressDeviceRemove();
|
|
214
|
-
await statesController.subscribeWritableStates();
|
|
215
|
-
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();
|
|
216
259
|
break;
|
|
217
260
|
case 'bridge/groups':
|
|
218
|
-
await deviceController.createGroupDefinitions(messageObj.payload);
|
|
219
|
-
await deviceController.createOrUpdateDevices();
|
|
220
|
-
await statesController.subscribeWritableStates();
|
|
221
|
-
statesController.processQueue();
|
|
261
|
+
await this.deviceController.createGroupDefinitions(messageObj.payload);
|
|
262
|
+
await this.deviceController.createOrUpdateDevices();
|
|
263
|
+
await this.statesController.subscribeWritableStates();
|
|
264
|
+
await this.statesController.processQueue();
|
|
222
265
|
break;
|
|
223
266
|
case 'bridge/response/coordinator_check':
|
|
224
|
-
deviceController.processCoordinatorCheck(messageObj.payload);
|
|
267
|
+
await this.deviceController.processCoordinatorCheck(messageObj.payload);
|
|
225
268
|
break;
|
|
226
269
|
case 'bridge/logging':
|
|
227
|
-
if (this.config.proxyZ2MLogs
|
|
228
|
-
z2mController.proxyZ2MLogs(messageObj);
|
|
270
|
+
if (this.config.proxyZ2MLogs === true) {
|
|
271
|
+
await this.z2mController.proxyZ2MLogs(messageObj);
|
|
229
272
|
}
|
|
230
273
|
break;
|
|
231
274
|
case 'bridge/response/device/rename':
|
|
232
|
-
await deviceController.renameDeviceInCache(messageObj);
|
|
233
|
-
await deviceController.createOrUpdateDevices();
|
|
234
|
-
statesController.processQueue();
|
|
275
|
+
await this.deviceController.renameDeviceInCache(messageObj);
|
|
276
|
+
await this.deviceController.createOrUpdateDevices();
|
|
277
|
+
await this.statesController.processQueue();
|
|
235
278
|
break;
|
|
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
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
236
291
|
case 'bridge/config':
|
|
237
292
|
case 'bridge/health':
|
|
238
293
|
case 'bridge/definitions':
|
|
239
|
-
case 'bridge/event':
|
|
240
294
|
case 'bridge/extensions':
|
|
241
295
|
case 'bridge/response/device/configure':
|
|
242
296
|
case 'bridge/response/device/remove':
|
|
243
297
|
case 'bridge/response/device/options':
|
|
298
|
+
case 'bridge/response/device/interview':
|
|
244
299
|
case 'bridge/response/permit_join':
|
|
245
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':
|
|
246
309
|
case 'bridge/response/touchlink/scan':
|
|
247
310
|
case 'bridge/response/touchlink/identify':
|
|
248
311
|
case 'bridge/response/touchlink/factory_reset':
|
|
249
312
|
break;
|
|
250
|
-
default:
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// If MQTT is used, I have to filter the self-sent 'set' commands.
|
|
275
|
-
if (messageObj.topic.endsWith('/set')) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
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;
|
|
332
|
+
}
|
|
333
|
+
if (messageObj.topic.endsWith('/set')) {
|
|
334
|
+
break;
|
|
280
335
|
}
|
|
336
|
+
await this.statesController.processDeviceMessage(messageObj);
|
|
281
337
|
}
|
|
282
338
|
break;
|
|
339
|
+
}
|
|
283
340
|
}
|
|
284
341
|
} finally {
|
|
342
|
+
// Mutex immer freigeben
|
|
285
343
|
release();
|
|
286
344
|
}
|
|
287
345
|
}
|
|
288
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
|
+
|
|
289
456
|
async onUnload(callback) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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') {
|
|
293
477
|
try {
|
|
294
|
-
if (
|
|
295
|
-
|
|
478
|
+
if (this.websocketController) {
|
|
479
|
+
this.websocketController.closeConnection();
|
|
296
480
|
}
|
|
297
481
|
} catch (e) {
|
|
298
482
|
this.log.error(e);
|
|
299
483
|
}
|
|
300
484
|
}
|
|
301
|
-
}
|
|
302
|
-
// Internal or Dummy MQTT-Server
|
|
303
|
-
if (this.config.connectionType == 'intmqtt' || this.config.dummyMqtt == true) {
|
|
304
485
|
try {
|
|
305
|
-
if (
|
|
306
|
-
|
|
486
|
+
if (this.statesController) {
|
|
487
|
+
await this.statesController.setAllAvailableToFalse();
|
|
307
488
|
}
|
|
308
489
|
} catch (e) {
|
|
309
490
|
this.log.error(e);
|
|
310
491
|
}
|
|
311
|
-
} else if (this.config.connectionType == 'ws') {
|
|
312
|
-
// Websocket
|
|
313
492
|
try {
|
|
314
|
-
if (websocketController) {
|
|
315
|
-
websocketController.
|
|
493
|
+
if (this.websocketController) {
|
|
494
|
+
await this.websocketController.allTimerClear();
|
|
316
495
|
}
|
|
317
496
|
} catch (e) {
|
|
318
497
|
this.log.error(e);
|
|
319
498
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} catch (e) {
|
|
327
|
-
this.log.error(e);
|
|
328
|
-
}
|
|
329
|
-
// Clear all websocket timers
|
|
330
|
-
try {
|
|
331
|
-
if (websocketController) {
|
|
332
|
-
await websocketController.allTimerClear();
|
|
333
|
-
}
|
|
334
|
-
} catch (e) {
|
|
335
|
-
this.log.error(e);
|
|
336
|
-
}
|
|
337
|
-
// Clear all state timers
|
|
338
|
-
try {
|
|
339
|
-
if (statesController) {
|
|
340
|
-
await statesController.allTimerClear();
|
|
499
|
+
try {
|
|
500
|
+
if (this.statesController) {
|
|
501
|
+
await this.statesController.allTimerClear();
|
|
502
|
+
}
|
|
503
|
+
} catch (e) {
|
|
504
|
+
this.log.error(e);
|
|
341
505
|
}
|
|
342
|
-
} catch (e) {
|
|
343
|
-
this.log.error(e);
|
|
344
|
-
}
|
|
345
506
|
|
|
346
|
-
|
|
507
|
+
this.setState('info.connection', false, true);
|
|
347
508
|
|
|
348
|
-
|
|
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
|
+
}
|
|
349
519
|
}
|
|
350
520
|
|
|
351
521
|
async onStateChange(id, state) {
|
|
352
|
-
if (state && state.ack
|
|
522
|
+
if (state && state.ack === false) {
|
|
353
523
|
if (id.endsWith('info.debugmessages')) {
|
|
354
|
-
logCustomizations.debugDevices = state.val;
|
|
524
|
+
this.logCustomizations.debugDevices = String(state.val || '');
|
|
355
525
|
this.setState(id, state.val, true);
|
|
356
526
|
return;
|
|
357
527
|
}
|
|
358
528
|
if (id.endsWith('info.logfilter')) {
|
|
359
|
-
logCustomizations.logfilter = state.val.split(';').filter((x) => x);
|
|
529
|
+
this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
|
|
360
530
|
this.setState(id, state.val, true);
|
|
361
531
|
return;
|
|
362
532
|
}
|
|
363
533
|
|
|
364
|
-
|
|
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
|
+
}
|
|
365
543
|
|
|
366
544
|
if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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 }));
|
|
370
568
|
}
|
|
371
569
|
}
|
|
372
570
|
}
|
|
373
571
|
}
|
|
374
572
|
|
|
375
573
|
if (require.main !== module) {
|
|
376
|
-
// Export the constructor in compact mode
|
|
377
574
|
/**
|
|
378
575
|
* @param {Partial<core.AdapterOptions>} [options]
|
|
379
576
|
*/
|
|
380
577
|
module.exports = (options) => new Zigbee2mqtt(options);
|
|
381
578
|
} else {
|
|
382
|
-
// otherwise start the instance directly
|
|
383
579
|
new Zigbee2mqtt();
|
|
384
580
|
}
|