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/lib/imageController.js
CHANGED
|
@@ -2,65 +2,85 @@ const axios = require('axios').default;
|
|
|
2
2
|
const sharp = require('sharp');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Lädt Geräte-Icons von zigbee2mqtt.io herunter, cacht sie im ioBroker-Dateisystem
|
|
6
|
+
* und skaliert sie auf die konfigurierte Größe.
|
|
6
7
|
*/
|
|
7
8
|
class ImageController {
|
|
8
9
|
/**
|
|
10
|
+
* Erstellt eine neue ImageController-Instanz.
|
|
9
11
|
*
|
|
10
|
-
* @param adapter
|
|
12
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
11
13
|
*/
|
|
12
14
|
constructor(adapter) {
|
|
13
15
|
this.adapter = adapter;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
19
|
+
* Bereinigt eine Model-ID für die Verwendung in einer URL (ersetzt '/' durch '_').
|
|
17
20
|
*
|
|
18
|
-
* @param modelName
|
|
21
|
+
* @param {string} modelName Die zu bereinigende Model-ID
|
|
22
|
+
* @returns {string} Bereinigte Model-ID oder 'NA' wenn leer
|
|
19
23
|
*/
|
|
20
24
|
sanitizeModelIDForImageUrl(modelName) {
|
|
21
|
-
|
|
25
|
+
if (!modelName) { return 'NA'; }
|
|
22
26
|
// eslint-disable-next-line no-control-regex
|
|
23
|
-
return
|
|
27
|
+
return modelName.replace(/\//g, '_').replace(/\u0000/g, '');
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
31
|
+
* Bereinigt einen Z2M-Gerätenamen für die Verwendung in einer URL.
|
|
27
32
|
*
|
|
28
|
-
* @param deviceName
|
|
33
|
+
* @param {string} deviceName Der zu bereinigende Gerätename
|
|
34
|
+
* @returns {string} Bereinigter Name oder 'NA' wenn leer
|
|
29
35
|
*/
|
|
30
36
|
sanitizeZ2MDeviceName(deviceName) {
|
|
31
|
-
|
|
37
|
+
if (!deviceName) { return 'NA'; }
|
|
32
38
|
// eslint-disable-next-line no-control-regex
|
|
33
|
-
return deviceName
|
|
39
|
+
return deviceName.replace(/:|\s|\//g, '-').replace(/\u0000/g, '');
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
43
|
+
* Erstellt die zigbee2mqtt.io-Bild-URL für ein Gerät.
|
|
37
44
|
*
|
|
38
|
-
* @param device
|
|
45
|
+
* @param {object} device Das Geräteobjekt aus dem Z2M-Payload
|
|
46
|
+
* @param {string} ext Dateiendung ('jpg' oder 'png')
|
|
47
|
+
* @returns {string|undefined} Die Bild-URL oder undefined wenn kein Modell vorhanden
|
|
39
48
|
*/
|
|
40
|
-
|
|
49
|
+
_buildZ2mImageUrl(device, ext) {
|
|
41
50
|
if (device && device.definition && device.definition.model) {
|
|
42
|
-
const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}
|
|
51
|
+
const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}.${ext}`;
|
|
43
52
|
// eslint-disable-next-line no-control-regex
|
|
44
53
|
return icoString.replace(/\u0000/g, '');
|
|
45
54
|
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gibt die JPG-Bild-URL des Geräts auf zigbee2mqtt.io zurück.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} device Das Geräteobjekt
|
|
62
|
+
* @returns {string|undefined} Die JPG-URL oder undefined
|
|
63
|
+
*/
|
|
64
|
+
getZ2mDeviceImageModelJPG(device) {
|
|
65
|
+
return this._buildZ2mImageUrl(device, 'jpg');
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
/**
|
|
69
|
+
* Gibt die PNG-Bild-URL des Geräts auf zigbee2mqtt.io zurück.
|
|
49
70
|
*
|
|
50
|
-
* @param device
|
|
71
|
+
* @param {object} device Das Geräteobjekt
|
|
72
|
+
* @returns {string|undefined} Die PNG-URL oder undefined
|
|
51
73
|
*/
|
|
52
74
|
getZ2mDeviceImageModelPNG(device) {
|
|
53
|
-
|
|
54
|
-
const icoString = `https://www.zigbee2mqtt.io/images/devices/${this.sanitizeZ2MDeviceName(device.definition.model)}.png`;
|
|
55
|
-
// eslint-disable-next-line no-control-regex
|
|
56
|
-
return icoString.replace(/\u0000/g, '');
|
|
57
|
-
}
|
|
75
|
+
return this._buildZ2mImageUrl(device, 'png');
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
|
|
61
79
|
/**
|
|
80
|
+
* Gibt die SLS-Bild-URL des Geräts (alternative Quelle) zurück.
|
|
62
81
|
*
|
|
63
|
-
* @param device
|
|
82
|
+
* @param {object} device Das Geräteobjekt mit model_id-Feld
|
|
83
|
+
* @returns {string|undefined} Die Bild-URL oder undefined
|
|
64
84
|
*/
|
|
65
85
|
getSlsDeviceImage(device) {
|
|
66
86
|
if (device && device.model_id) {
|
|
@@ -68,22 +88,29 @@ class ImageController {
|
|
|
68
88
|
// eslint-disable-next-line no-control-regex
|
|
69
89
|
return icoString.replace(/\u0000/g, '');
|
|
70
90
|
}
|
|
91
|
+
return undefined;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/**
|
|
95
|
+
* Lädt das Icon eines Geräts herunter (falls noch nicht gecacht), skaliert es
|
|
96
|
+
* auf die konfigurierte Größe und gibt es als Base64-Data-URL zurück.
|
|
74
97
|
*
|
|
75
|
-
* @param device
|
|
98
|
+
* @param {object} device Das Geräteobjekt aus dem Z2M-bridge/devices-Payload
|
|
99
|
+
* @returns {Promise<string>} Base64-kodiertes Icon als Data-URL oder ''
|
|
76
100
|
*/
|
|
77
101
|
async getDeviceIcon(device) {
|
|
78
102
|
if (!this.adapter.config.useDeviceIcons) {
|
|
79
|
-
return '';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
if (!device || !device.definition || !device.definition.model) {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
83
108
|
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
109
|
+
const imageSize = this.adapter.config.deviceIconsSize;
|
|
110
|
+
const z2mModel = device.definition.model || '';
|
|
111
|
+
const z2mIconFileNameJPG = `${this.sanitizeZ2MDeviceName(z2mModel)}.jpg`;
|
|
112
|
+
const z2mIconFileNamePNG = `${this.sanitizeZ2MDeviceName(z2mModel)}.png`;
|
|
113
|
+
const slsIconFileName = `${this.sanitizeModelIDForImageUrl(device.model_id || z2mModel)}.png`;
|
|
87
114
|
|
|
88
115
|
let iconFileName = await this.getExistingIconFileName(z2mIconFileNameJPG, z2mIconFileNamePNG, slsIconFileName);
|
|
89
116
|
let iconFound = true;
|
|
@@ -93,7 +120,7 @@ return '';
|
|
|
93
120
|
this.getZ2mDeviceImageModelJPG(device),
|
|
94
121
|
this.getZ2mDeviceImageModelPNG(device),
|
|
95
122
|
this.getSlsDeviceImage(device)
|
|
96
|
-
];
|
|
123
|
+
].filter(Boolean);
|
|
97
124
|
|
|
98
125
|
for (const iconUrl of iconUrls) {
|
|
99
126
|
try {
|
|
@@ -103,47 +130,48 @@ return '';
|
|
|
103
130
|
break;
|
|
104
131
|
}
|
|
105
132
|
} catch (ex) {
|
|
106
|
-
|
|
133
|
+
this.adapter.log.debug(`Icon download failed for ${iconUrl}: ${ex && ex.message ? ex.message : ex}`);
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
111
|
-
if (!iconFound) {
|
|
138
|
+
if (!iconFound || !iconFileName) {
|
|
112
139
|
this.adapter.log.warn(`Failed to download image for device model: ${device.definition.model} - ${device.definition.description}`);
|
|
113
140
|
return '';
|
|
114
|
-
}
|
|
115
|
-
// Load image from the Meta-Store
|
|
116
|
-
const icon = await this.adapter.readFileAsync(this.adapter.namespace, iconFileName);
|
|
117
|
-
// Load Image Metadata
|
|
118
|
-
const origIconMeta = await sharp(icon.file).metadata();
|
|
119
|
-
// Check whether the image needs to be resized
|
|
120
|
-
if (
|
|
121
|
-
(origIconMeta.height && origIconMeta.height > imageSize) ||
|
|
122
|
-
(origIconMeta.width && origIconMeta.width > imageSize)
|
|
123
|
-
) {
|
|
124
|
-
// Resize image to 28x28 pixel
|
|
125
|
-
this.adapter.log.info(
|
|
126
|
-
`Resize image for device model ${device.definition.model} from: ${origIconMeta.width}x${origIconMeta.height} to ${imageSize}x${imageSize}`
|
|
127
|
-
);
|
|
128
|
-
icon.file = await sharp(icon.file)
|
|
129
|
-
.resize({
|
|
130
|
-
width: imageSize,
|
|
131
|
-
height: imageSize,
|
|
132
|
-
fit: sharp.fit.cover,
|
|
133
|
-
position: sharp.strategy.entropy,
|
|
134
|
-
})
|
|
135
|
-
.toBuffer();
|
|
136
|
-
// Replace the original image with the resize image.
|
|
137
|
-
await this.adapter.writeFileAsync(this.adapter.namespace, iconFileName, icon.file);
|
|
138
|
-
}
|
|
141
|
+
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
const icon = await this.adapter.readFileAsync(this.adapter.namespace, iconFileName);
|
|
144
|
+
if (!icon || !icon.file) {
|
|
145
|
+
this.adapter.log.warn(`Failed to read icon file: ${iconFileName}`);
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
const origIconMeta = await sharp(icon.file).metadata();
|
|
149
|
+
if (
|
|
150
|
+
(origIconMeta.height && origIconMeta.height > imageSize) ||
|
|
151
|
+
(origIconMeta.width && origIconMeta.width > imageSize)
|
|
152
|
+
) {
|
|
153
|
+
this.adapter.log.info(
|
|
154
|
+
`Resize image for device model ${device.definition.model} from: ${origIconMeta.width}x${origIconMeta.height} to ${imageSize}x${imageSize}`
|
|
155
|
+
);
|
|
156
|
+
icon.file = await sharp(icon.file)
|
|
157
|
+
.resize({
|
|
158
|
+
width: imageSize,
|
|
159
|
+
height: imageSize,
|
|
160
|
+
fit: sharp.fit.cover,
|
|
161
|
+
position: sharp.strategy.entropy,
|
|
162
|
+
})
|
|
163
|
+
.toBuffer();
|
|
164
|
+
await this.adapter.writeFileAsync(this.adapter.namespace, iconFileName, icon.file);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `data:image/png;base64,${icon.file.toString('base64')}`;
|
|
143
168
|
}
|
|
169
|
+
|
|
144
170
|
/**
|
|
171
|
+
* Extrahiert den Dateinamen (mit Erweiterung) aus einer URL.
|
|
145
172
|
*
|
|
146
|
-
* @param url
|
|
173
|
+
* @param {string} url Die vollständige URL
|
|
174
|
+
* @returns {string} Dateiname (z.B. "TRADFRI_bulb.jpg")
|
|
147
175
|
*/
|
|
148
176
|
getFileNameWithExtension(url) {
|
|
149
177
|
const path = new URL(url).pathname;
|
|
@@ -153,26 +181,30 @@ return '';
|
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
/**
|
|
184
|
+
* Lädt ein Icon von der angegebenen URL herunter und speichert es im ioBroker-Dateisystem.
|
|
156
185
|
*
|
|
157
|
-
* @param adapter
|
|
158
|
-
* @param url
|
|
159
|
-
* @param namespace
|
|
186
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
187
|
+
* @param {string} url Download-URL des Icons
|
|
188
|
+
* @param {string} namespace ioBroker-Namespace (z.B. "zigbee2mqtt.0")
|
|
189
|
+
* @returns {Promise<boolean>} true bei Erfolg, false bei Fehler
|
|
160
190
|
*/
|
|
161
191
|
async downloadIcon(adapter, url, namespace) {
|
|
162
192
|
try {
|
|
163
|
-
const res = await axios.get(url, { responseType: 'arraybuffer' });
|
|
193
|
+
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000 });
|
|
164
194
|
await adapter.writeFileAsync(namespace, this.getFileNameWithExtension(url), res.data);
|
|
165
195
|
return true;
|
|
166
196
|
} catch (ex) {
|
|
167
|
-
//adapter.log.warn(ex);
|
|
168
197
|
return false;
|
|
169
198
|
}
|
|
170
199
|
}
|
|
200
|
+
|
|
171
201
|
/**
|
|
202
|
+
* Prüft welcher lokale Icon-Dateiname bereits im ioBroker-Dateisystem existiert.
|
|
172
203
|
*
|
|
173
|
-
* @param z2mIconFileNameJPG
|
|
174
|
-
* @param z2mIconFileNamePNG
|
|
175
|
-
* @param slsIconFileName
|
|
204
|
+
* @param {string} z2mIconFileNameJPG JPG-Dateiname (Z2M-Format)
|
|
205
|
+
* @param {string} z2mIconFileNamePNG PNG-Dateiname (Z2M-Format)
|
|
206
|
+
* @param {string} slsIconFileName PNG-Dateiname (SLS-Format)
|
|
207
|
+
* @returns {Promise<string|null>} Erster gefundener Dateiname oder null
|
|
176
208
|
*/
|
|
177
209
|
async getExistingIconFileName(z2mIconFileNameJPG, z2mIconFileNamePNG, slsIconFileName) {
|
|
178
210
|
if (await this.adapter.fileExistsAsync(this.adapter.namespace, z2mIconFileNameJPG)) {
|
package/lib/messages.js
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Gibt die aktuelle Adapter-Konfiguration formatiert als Log-Block aus.
|
|
2
3
|
*
|
|
3
|
-
* @param config
|
|
4
|
-
* @param log
|
|
4
|
+
* @param {object} config Die Adapter-Konfiguration (this.config)
|
|
5
|
+
* @param {object} log Das ioBroker-Log-Objekt (this.log)
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
function adapterInfo(config, log) {
|
|
8
|
+
if (!config || !log) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
7
11
|
log.info('================================= Adapter Config =================================');
|
|
8
12
|
log.info(`|| Zigbee2MQTT Frontend Scheme: ${config.webUIScheme}`);
|
|
9
13
|
log.info(`|| Zigbee2MQTT Frontend Server: ${config.webUIServer}`);
|
|
10
14
|
log.info(`|| Zigbee2MQTT Frontend Port: ${config.webUIPort}`);
|
|
11
15
|
log.info(`|| Zigbee2MQTT Connection Type: ${config.connectionType}`);
|
|
12
|
-
if (config.connectionType
|
|
16
|
+
if (config.connectionType === 'ws') {
|
|
13
17
|
log.info(`|| Zigbee2MQTT Websocket Scheme: ${config.wsScheme}`);
|
|
14
18
|
log.info(`|| Zigbee2MQTT Websocket Server: ${config.wsServerIP}`);
|
|
15
19
|
log.info(`|| Zigbee2MQTT Websocket Port: ${config.wsServerPort}`);
|
|
16
20
|
log.info(`|| Zigbee2MQTT Websocket Auth-Token: ${config.wsTokenEnabled ? 'use' : 'unused'}`);
|
|
17
21
|
log.info(`|| Zigbee2MQTT Websocket Dummy MQTT-Server: ${config.dummyMqtt ? 'activated' : 'deactivated'}`);
|
|
18
|
-
if (config.dummyMqtt
|
|
22
|
+
if (config.dummyMqtt === true) {
|
|
19
23
|
log.info(`|| Zigbee2MQTT Dummy MQTT IP-Bind: ${config.mqttServerIPBind}`);
|
|
20
24
|
log.info(`|| Zigbee2MQTT Dummy MQTT Port: ${config.mqttServerPort}`);
|
|
21
25
|
}
|
|
22
|
-
} else if (config.connectionType
|
|
23
|
-
log.info(`|| Zigbee2MQTT
|
|
24
|
-
log.info(`|| Zigbee2MQTT
|
|
26
|
+
} else if (config.connectionType === 'exmqtt') {
|
|
27
|
+
log.info(`|| Zigbee2MQTT External MQTT Server: ${config.externalMqttServerIP}`);
|
|
28
|
+
log.info(`|| Zigbee2MQTT External MQTT Port: ${config.externalMqttServerPort}`);
|
|
25
29
|
log.info(
|
|
26
|
-
`|| Zigbee2MQTT
|
|
30
|
+
`|| Zigbee2MQTT External MQTT Credentials: ${config.externalMqttServerCredentials ? 'use' : 'unused'}`
|
|
27
31
|
);
|
|
28
|
-
} else if (config.connectionType
|
|
32
|
+
} else if (config.connectionType === 'intmqtt') {
|
|
29
33
|
log.info(`|| Zigbee2MQTT Internal MQTT IP-Bind: ${config.mqttServerIPBind}`);
|
|
30
34
|
log.info(`|| Zigbee2MQTT Internal MQTT Port: ${config.mqttServerPort}`);
|
|
31
35
|
}
|
|
@@ -47,19 +51,25 @@ async function adapterInfo(config, log) {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
54
|
+
* Gibt die Zigbee2MQTT-Bridge-Informationen formatiert als Log-Block aus.
|
|
50
55
|
*
|
|
51
|
-
* @param payload
|
|
52
|
-
* @param log
|
|
56
|
+
* @param {object} payload Der Payload des bridge/info-Topics
|
|
57
|
+
* @param {object} log Das ioBroker-Log-Objekt (this.log)
|
|
53
58
|
*/
|
|
54
|
-
|
|
59
|
+
function zigbee2mqttInfo(payload, log) {
|
|
60
|
+
if (!payload || !log) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
55
63
|
log.info('============================ Zigbee2MQTT Information =============================');
|
|
56
64
|
log.info(`|| Zigbee2MQTT Version: ${payload.version} `);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const coordType = payload.coordinator && payload.coordinator.type ? payload.coordinator.type : 'unknown';
|
|
66
|
+
const coordRev = payload.coordinator && payload.coordinator.meta ? payload.coordinator.meta.revision : 'unknown';
|
|
67
|
+
const serialPort = payload.config && payload.config.serial ? payload.config.serial.port : 'unknown';
|
|
68
|
+
const panId = payload.network ? payload.network.pan_id : 'unknown';
|
|
69
|
+
const channel = payload.network ? payload.network.channel : 'unknown';
|
|
70
|
+
const extPanId = payload.network ? payload.network.extended_pan_id : 'unknown';
|
|
71
|
+
log.info(`|| Coordinator type: ${coordType} Version: ${coordRev} Serial: ${serialPort}`);
|
|
72
|
+
log.info(`|| Network panid ${panId} channel: ${channel} ext_pan_id: ${extPanId}`);
|
|
63
73
|
log.info('==================================================================================');
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -1,65 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const core = require('@iobroker/adapter-core');
|
|
2
|
-
const Aedes = require('aedes');
|
|
3
|
-
const net = require('net');
|
|
4
|
-
let mqttServer;
|
|
4
|
+
const { Aedes } = require('aedes');
|
|
5
|
+
const net = require('node:net');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Verwaltet den internen oder Dummy-MQTT-Server (Aedes) für den Zigbee2MQTT-Adapter.
|
|
9
|
+
* Wird im intmqtt- und ws-Modus (dummyMqtt) verwendet.
|
|
8
10
|
*/
|
|
9
11
|
class MqttServerController {
|
|
10
12
|
/**
|
|
13
|
+
* Erstellt eine neue MqttServerController-Instanz.
|
|
11
14
|
*
|
|
12
|
-
* @param adapter
|
|
15
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
13
16
|
*/
|
|
14
17
|
constructor(adapter) {
|
|
15
|
-
this.adapter
|
|
18
|
+
this.adapter = adapter;
|
|
19
|
+
this.mqttServer = null;
|
|
20
|
+
this.aedesBroker = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prüft ob Port und Bind-Adresse konfiguriert sind.
|
|
25
|
+
*
|
|
26
|
+
* @returns {boolean} true wenn die Konfiguration vollständig ist, sonst false
|
|
27
|
+
*/
|
|
28
|
+
_checkConfig() {
|
|
29
|
+
if (!this.adapter.config.mqttServerPort || !this.adapter.config.mqttServerIPBind) {
|
|
30
|
+
this.adapter.log.error('MQTT server config incomplete (mqttServerPort / mqttServerIPBind missing).');
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
/**
|
|
37
|
+
* Hängt error- und close-Event-Handler an den internen net.Server.
|
|
19
38
|
*
|
|
39
|
+
* @param {string} label Bezeichnung des Servers für Log-Ausgaben (z.B. "MQTT-Server")
|
|
40
|
+
*/
|
|
41
|
+
_attachServerEvents(label) {
|
|
42
|
+
if (!this.mqttServer) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.mqttServer.on('error', (err) => {
|
|
46
|
+
this.adapter.log.error(`${label} error: ${err && err.message ? err.message : String(err)}`);
|
|
47
|
+
});
|
|
48
|
+
this.mqttServer.on('close', () => {
|
|
49
|
+
this.adapter.log.debug(`${label} closed.`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Startet den internen MQTT-Server mit nedb-Persistenz (aedes + aedes-persistence-nedb).
|
|
55
|
+
* Wartet auf den erfolgreichen listen()-Aufruf bevor die Methode zurückkehrt.
|
|
20
56
|
*/
|
|
21
57
|
async createMQTTServer() {
|
|
58
|
+
if (!this._checkConfig()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
22
61
|
try {
|
|
23
62
|
const NedbPersistence = require('aedes-persistence-nedb');
|
|
24
63
|
const db = new NedbPersistence({
|
|
25
64
|
path: `${core.getAbsoluteInstanceDataDir(this.adapter)}/mqttData`,
|
|
26
65
|
prefix: '',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
});
|
|
67
|
+
// aedes-persistence-nedb does not implement the async setup() interface
|
|
68
|
+
// required by aedes v1.x → no-op shim
|
|
69
|
+
if (!db.setup) {
|
|
70
|
+
db.setup = async () => {};
|
|
71
|
+
}
|
|
72
|
+
this.aedesBroker = await Aedes.createBroker({ persistence: db });
|
|
73
|
+
this.mqttServer = net.createServer(this.aedesBroker.handle);
|
|
74
|
+
this._attachServerEvents('MQTT-Server');
|
|
75
|
+
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
this.mqttServer.listen(
|
|
78
|
+
this.adapter.config.mqttServerPort,
|
|
79
|
+
this.adapter.config.mqttServerIPBind,
|
|
80
|
+
() => {
|
|
81
|
+
this.adapter.log.info(
|
|
82
|
+
`Starting MQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
|
|
83
|
+
);
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
33
86
|
);
|
|
87
|
+
this.mqttServer.once('error', reject);
|
|
34
88
|
});
|
|
35
89
|
} catch (err) {
|
|
36
|
-
this.adapter.log.error(err);
|
|
90
|
+
this.adapter.log.error(`createMQTTServer error: ${err && err.message ? err.message : String(err)}`);
|
|
91
|
+
// Sicherstellen dass kein halboffener Server übrig bleibt
|
|
92
|
+
this.closeServer();
|
|
37
93
|
}
|
|
38
94
|
}
|
|
39
95
|
|
|
40
96
|
/**
|
|
41
|
-
*
|
|
97
|
+
* Startet einen Dummy-MQTT-Server ohne Persistenz.
|
|
98
|
+
* Wird im WebSocket-Modus (dummyMqtt=true) benötigt damit Z2M einen MQTT-Broker findet.
|
|
42
99
|
*/
|
|
43
100
|
async createDummyMQTTServer() {
|
|
101
|
+
if (!this._checkConfig()) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
44
104
|
try {
|
|
45
|
-
|
|
46
|
-
mqttServer
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
this.aedesBroker = await Aedes.createBroker();
|
|
106
|
+
this.mqttServer = net.createServer(this.aedesBroker.handle);
|
|
107
|
+
this._attachServerEvents('DummyMQTT-Server');
|
|
108
|
+
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
this.mqttServer.listen(
|
|
111
|
+
this.adapter.config.mqttServerPort,
|
|
112
|
+
this.adapter.config.mqttServerIPBind,
|
|
113
|
+
() => {
|
|
114
|
+
this.adapter.log.info(
|
|
115
|
+
`Starting DummyMQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
|
|
116
|
+
);
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
50
119
|
);
|
|
120
|
+
this.mqttServer.once('error', reject);
|
|
51
121
|
});
|
|
52
122
|
} catch (err) {
|
|
53
|
-
this.adapter.log.error(err);
|
|
123
|
+
this.adapter.log.error(`createDummyMQTTServer error: ${err && err.message ? err.message : String(err)}`);
|
|
124
|
+
this.closeServer();
|
|
54
125
|
}
|
|
55
126
|
}
|
|
56
127
|
|
|
57
128
|
/**
|
|
58
|
-
*
|
|
129
|
+
* Schließt den MQTT-Server und den Aedes-Broker sicher.
|
|
130
|
+
* Wird beim Adapter-Stop oder nach einem Fehler beim Start aufgerufen.
|
|
59
131
|
*/
|
|
60
132
|
closeServer() {
|
|
61
|
-
if (mqttServer
|
|
62
|
-
mqttServer
|
|
133
|
+
if (this.mqttServer) {
|
|
134
|
+
const server = this.mqttServer;
|
|
135
|
+
this.mqttServer = null;
|
|
136
|
+
try {
|
|
137
|
+
server.close((err) => {
|
|
138
|
+
if (err) {
|
|
139
|
+
this.adapter.log.debug(`MQTT server close callback error: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.adapter.log.error(`closeServer mqttServer.close() error: ${err}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (this.aedesBroker) {
|
|
147
|
+
const broker = this.aedesBroker;
|
|
148
|
+
this.aedesBroker = null;
|
|
149
|
+
try {
|
|
150
|
+
broker.close((err) => {
|
|
151
|
+
if (err) {
|
|
152
|
+
this.adapter.log.debug(`Aedes broker close callback error: ${err}`);
|
|
153
|
+
} else {
|
|
154
|
+
this.adapter.log.debug('Aedes broker closed.');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
this.adapter.log.error(`closeServer aedesBroker.close() error: ${err}`);
|
|
159
|
+
}
|
|
63
160
|
}
|
|
64
161
|
}
|
|
65
162
|
}
|