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