iobroker.sonoff 3.2.1 → 4.0.0
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/LICENSE +1 -1
- package/README.md +95 -149
- package/admin/i18n/de.json +89 -0
- package/admin/i18n/en.json +89 -0
- package/admin/i18n/es.json +89 -0
- package/admin/i18n/fr.json +89 -0
- package/admin/i18n/it.json +89 -0
- package/admin/i18n/nl.json +89 -0
- package/admin/i18n/pl.json +89 -0
- package/admin/i18n/pt.json +89 -0
- package/admin/i18n/ru.json +89 -0
- package/admin/i18n/uk.json +89 -0
- package/admin/i18n/zh-cn.json +89 -0
- package/admin/jsonConfig.json +394 -125
- package/build/lib/bridge.js +512 -0
- package/build/lib/bridge.js.map +1 -0
- package/build/lib/checkStates.js +68 -0
- package/build/lib/checkStates.js.map +1 -0
- package/build/lib/datapoints.js +430 -0
- package/build/lib/datapoints.js.map +1 -0
- package/build/lib/mqttBase.js +1280 -0
- package/build/lib/mqttBase.js.map +1 -0
- package/build/lib/server.js +485 -0
- package/build/lib/server.js.map +1 -0
- package/build/main.js +103 -0
- package/build/main.js.map +1 -0
- package/io-package.json +64 -43
- package/package.json +23 -19
- package/admin/i18n/de/translations.json +0 -54
- package/admin/i18n/en/translations.json +0 -54
- package/admin/i18n/es/translations.json +0 -54
- package/admin/i18n/fr/translations.json +0 -54
- package/admin/i18n/it/translations.json +0 -54
- package/admin/i18n/nl/translations.json +0 -54
- package/admin/i18n/pl/translations.json +0 -54
- package/admin/i18n/pt/translations.json +0 -54
- package/admin/i18n/ru/translations.json +0 -54
- package/admin/i18n/uk/translations.json +0 -54
- package/admin/i18n/zh-cn/translations.json +0 -54
- package/admin/index.html +0 -177
- package/admin/index_m.html +0 -218
- package/admin/words.js +0 -63
- package/lib/datapoints.js +0 -216
- package/lib/server.js +0 -1810
- package/main.js +0 -99
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_fs_1 = require("node:fs");
|
|
40
|
+
const mqtt_1 = __importDefault(require("mqtt"));
|
|
41
|
+
const mqttBase_1 = __importStar(require("./mqttBase"));
|
|
42
|
+
/**
|
|
43
|
+
* Where the name of a device comes from. A name from a less reliable source never overwrites
|
|
44
|
+
* a name from a better one, so devices are not renamed back and forth with every message.
|
|
45
|
+
*/
|
|
46
|
+
const NAME_PRIORITY = {
|
|
47
|
+
/** Fallback: the topic itself */
|
|
48
|
+
topic: 0,
|
|
49
|
+
/** "Hostname" from tele/.../STATE, tele/.../INFO2 or stat/.../STATUS5 */
|
|
50
|
+
hostname: 1,
|
|
51
|
+
/** "MqttClient" from stat/.../STATUS6 - the same name as with the built-in broker */
|
|
52
|
+
mqttClient: 2,
|
|
53
|
+
};
|
|
54
|
+
const TASMOTA_PREFIXES = ['tele', 'stat', 'cmnd'];
|
|
55
|
+
// "tele/#" and "stat/#" are the standard full topics, "+/tele/+" and "+/stat/+" the device-first ones
|
|
56
|
+
const DEFAULT_TOPICS = ['tele/#', 'stat/#', '+/tele/+', '+/stat/+'];
|
|
57
|
+
const NAME_TIMEOUT_MS = 30_000;
|
|
58
|
+
const MAX_PENDING_MESSAGES = 100;
|
|
59
|
+
/**
|
|
60
|
+
* MQTT bridge: connects as a client to an existing (external) MQTT broker
|
|
61
|
+
* instead of starting an own broker. All message processing is shared with the
|
|
62
|
+
* server implementation via MQTTBase.
|
|
63
|
+
*/
|
|
64
|
+
class MQTTBridge extends mqttBase_1.default {
|
|
65
|
+
mqttClient = null;
|
|
66
|
+
deviceNames = {};
|
|
67
|
+
pendingMessages = {};
|
|
68
|
+
pendingTimers = {};
|
|
69
|
+
tasmotaTopics = new Set();
|
|
70
|
+
topicFormats = {};
|
|
71
|
+
reportedConflicts = new Set();
|
|
72
|
+
/** All messages are processed strictly one after another */
|
|
73
|
+
queue = Promise.resolve();
|
|
74
|
+
constructor(adapter) {
|
|
75
|
+
super(adapter);
|
|
76
|
+
if (!this.config.externalBrokerUrl) {
|
|
77
|
+
this.adapter.log.error('External broker URL is not configured');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.connect();
|
|
81
|
+
}
|
|
82
|
+
async destroy() {
|
|
83
|
+
for (const topicPrefix in this.pendingTimers) {
|
|
84
|
+
clearTimeout(this.pendingTimers[topicPrefix]);
|
|
85
|
+
}
|
|
86
|
+
this.pendingTimers = {};
|
|
87
|
+
this.pendingMessages = {};
|
|
88
|
+
// wait till the currently processed message is done
|
|
89
|
+
await this.queue.catch(() => { });
|
|
90
|
+
if (this.mqttClient) {
|
|
91
|
+
for (const id in this.clients) {
|
|
92
|
+
await this.adapter.setForeignStateAsync(`${this.adapter.namespace}.${this.clients[id].iobId}.alive`, false, true);
|
|
93
|
+
}
|
|
94
|
+
await this.adapter.setStateAsync('info.connection', '', true);
|
|
95
|
+
await new Promise(resolve => this.mqttClient.end(false, {}, () => resolve()));
|
|
96
|
+
this.mqttClient = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** In bridge mode info.connection shows the URL of the external broker, not the devices */
|
|
100
|
+
async updateConnectionState() {
|
|
101
|
+
// nothing to do, the state is set when the connection to the broker is established or lost
|
|
102
|
+
}
|
|
103
|
+
sendState2Client(client, topic, state, qos) {
|
|
104
|
+
const brokerTopic = this.toBrokerTopic(topic);
|
|
105
|
+
this.adapter.log.debug(`Send to external broker "${client.id}": ${brokerTopic} = ${state}`);
|
|
106
|
+
this.mqttClient?.publish(brokerTopic, state === null ? 'null' : (state?.toString() ?? ''), { qos });
|
|
107
|
+
}
|
|
108
|
+
normalizeUrl(url) {
|
|
109
|
+
if (!url.includes('://')) {
|
|
110
|
+
return `mqtt://${url}`;
|
|
111
|
+
}
|
|
112
|
+
return url;
|
|
113
|
+
}
|
|
114
|
+
/** Topics to subscribe to. Can be configured, e.g. to support other full topics or OpenBeken devices */
|
|
115
|
+
getSubscriptions() {
|
|
116
|
+
const topics = (this.config.externalBrokerTopics || '')
|
|
117
|
+
.split(/[,;\s]+/)
|
|
118
|
+
.map(topic => topic.trim())
|
|
119
|
+
.filter(topic => topic);
|
|
120
|
+
return topics.length ? topics : DEFAULT_TOPICS;
|
|
121
|
+
}
|
|
122
|
+
/** Read an optional certificate file for encrypted connections */
|
|
123
|
+
readCertificate(path, name) {
|
|
124
|
+
if (!path) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return (0, node_fs_1.readFileSync)(path);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
this.adapter.log.error(`Cannot read ${name} "${path}": ${err.message}`);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
getConnectOptions() {
|
|
136
|
+
const options = {
|
|
137
|
+
username: this.config.externalBrokerUser || undefined,
|
|
138
|
+
password: this.config.externalBrokerPassword || undefined,
|
|
139
|
+
clientId: this.config.externalBrokerClientId || `iobroker_sonoff_${this.adapter.namespace}`,
|
|
140
|
+
keepalive: parseInt(this.config.externalBrokerKeepalive, 10) || 60,
|
|
141
|
+
clean: this.config.externalBrokerCleanSession !== false,
|
|
142
|
+
reconnectPeriod: 5000,
|
|
143
|
+
};
|
|
144
|
+
// Certificates are only used for encrypted connections: mqtts://, ssl://, tls://, wss://
|
|
145
|
+
if (this.normalizeUrl(this.config.externalBrokerUrl).includes('s://')) {
|
|
146
|
+
options.rejectUnauthorized = this.config.externalBrokerRejectUnauthorized !== false;
|
|
147
|
+
options.ca = this.readCertificate(this.config.externalBrokerCaPath, 'CA certificate');
|
|
148
|
+
options.cert = this.readCertificate(this.config.externalBrokerCertPath, 'client certificate');
|
|
149
|
+
options.key = this.readCertificate(this.config.externalBrokerKeyPath, 'client key');
|
|
150
|
+
}
|
|
151
|
+
return options;
|
|
152
|
+
}
|
|
153
|
+
connect() {
|
|
154
|
+
const url = this.normalizeUrl(this.config.externalBrokerUrl);
|
|
155
|
+
const subscriptions = this.getSubscriptions();
|
|
156
|
+
this.adapter.log.info(`Connecting to external MQTT broker: ${url}`);
|
|
157
|
+
this.mqttClient = mqtt_1.default.connect(url, this.getConnectOptions());
|
|
158
|
+
this.mqttClient.on('connect', () => {
|
|
159
|
+
this.adapter.log.info(`Connected to external MQTT broker ${url}`);
|
|
160
|
+
this.adapter
|
|
161
|
+
.setStateAsync('info.connection', url, true)
|
|
162
|
+
.catch(err => this.adapter.log.error(`Cannot set connection state: ${err}`));
|
|
163
|
+
this.mqttClient.subscribe(subscriptions, { qos: this.config.defaultQoS }, err => {
|
|
164
|
+
if (err) {
|
|
165
|
+
this.adapter.log.error(`Cannot subscribe to external broker: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.adapter.log.info(`Subscribed to ${subscriptions.join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
this.updateClients().catch(err => this.adapter.log.error(`Cannot update clients: ${err.message}`));
|
|
172
|
+
});
|
|
173
|
+
this.mqttClient.on('message', (topic, payload, packet) => {
|
|
174
|
+
// Devices are created and renamed asynchronously, so the messages must not overlap
|
|
175
|
+
this.queue = this.queue
|
|
176
|
+
.then(() => this.handleExternalMessage(topic, payload, packet))
|
|
177
|
+
.catch(err => this.adapter.log.error(`Cannot handle external message: ${err.message}`));
|
|
178
|
+
});
|
|
179
|
+
this.mqttClient.on('error', err => this.adapter.log.error(`External MQTT broker error: ${err.message}`));
|
|
180
|
+
this.mqttClient.on('close', () => {
|
|
181
|
+
this.adapter.log.debug('External MQTT broker connection closed');
|
|
182
|
+
this.adapter
|
|
183
|
+
.setStateAsync('info.connection', '', true)
|
|
184
|
+
.catch(err => this.adapter.log.error(`Cannot set connection state: ${err}`));
|
|
185
|
+
});
|
|
186
|
+
this.mqttClient.on('reconnect', () => {
|
|
187
|
+
this.adapter.log.debug('Reconnecting to external MQTT broker...');
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Determine which device a topic belongs to and how the full topic is built:
|
|
192
|
+
* - tele/device/STATE => device, standard
|
|
193
|
+
* - tele/house/floor/dev/STATE => house/floor/dev, standard (nested full topics)
|
|
194
|
+
* - device/tele/STATE => device, device-first (full topic %topic%/%prefix%/)
|
|
195
|
+
* - gateway/tele/device/STATE => device, standard with the prefix "gateway"
|
|
196
|
+
* - device/led_enableAll/get => device, no tasmota structure (OpenBeken)
|
|
197
|
+
*/
|
|
198
|
+
analyzeTopic(topic) {
|
|
199
|
+
const parts = topic.split('/');
|
|
200
|
+
const index = parts.findIndex(part => TASMOTA_PREFIXES.includes(part));
|
|
201
|
+
const command = parts[parts.length - 1];
|
|
202
|
+
// the prefix is the first part: tele/device/STATE
|
|
203
|
+
if (index === 0) {
|
|
204
|
+
return {
|
|
205
|
+
device: parts.length > 2 ? parts.slice(1, -1).join('/') : '',
|
|
206
|
+
structure: 'standard',
|
|
207
|
+
prefix: '',
|
|
208
|
+
standardTopic: topic,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// the prefix stands directly before the command: device/tele/STATE
|
|
212
|
+
if (index > 0 && index === parts.length - 2) {
|
|
213
|
+
const device = parts.slice(0, index).join('/');
|
|
214
|
+
return {
|
|
215
|
+
device,
|
|
216
|
+
structure: 'device-first',
|
|
217
|
+
prefix: '',
|
|
218
|
+
standardTopic: `${parts[index]}/${device}/${command}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// something stands in front of the full topic: gateway/tele/device/STATE
|
|
222
|
+
if (index > 0) {
|
|
223
|
+
const device = parts.slice(index + 1, -1).join('/');
|
|
224
|
+
return {
|
|
225
|
+
device,
|
|
226
|
+
structure: 'standard',
|
|
227
|
+
prefix: parts.slice(0, index).join('/'),
|
|
228
|
+
standardTopic: `${parts[index]}/${device}/${command}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return { device: parts[0] || '', standardTopic: topic };
|
|
232
|
+
}
|
|
233
|
+
/** Convert a standard topic (cmnd/device/POWER) into the structure and prefix the device uses */
|
|
234
|
+
toBrokerTopic(topic) {
|
|
235
|
+
const parts = topic.split('/');
|
|
236
|
+
if (parts.length < 3 || !TASMOTA_PREFIXES.includes(parts[0])) {
|
|
237
|
+
return topic;
|
|
238
|
+
}
|
|
239
|
+
const device = parts.slice(1, -1).join('/');
|
|
240
|
+
const format = this.topicFormats[device];
|
|
241
|
+
if (!format) {
|
|
242
|
+
return topic;
|
|
243
|
+
}
|
|
244
|
+
const command = parts[parts.length - 1];
|
|
245
|
+
const fullTopic = format.structure === 'device-first'
|
|
246
|
+
? `${device}/${parts[0]}/${command}`
|
|
247
|
+
: `${parts[0]}/${device}/${command}`;
|
|
248
|
+
return format.prefix ? `${format.prefix}/${fullTopic}` : fullTopic;
|
|
249
|
+
}
|
|
250
|
+
publishCommand(device, command, payload) {
|
|
251
|
+
this.mqttClient?.publish(this.toBrokerTopic(`cmnd/${device}/${command}`), payload);
|
|
252
|
+
}
|
|
253
|
+
toPacket(topic, payload, packet) {
|
|
254
|
+
return {
|
|
255
|
+
topic,
|
|
256
|
+
payload,
|
|
257
|
+
qos: packet.qos,
|
|
258
|
+
retain: packet.retain,
|
|
259
|
+
messageId: 0,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Find the name of the device in the payload of a message */
|
|
263
|
+
detectDeviceName(payload) {
|
|
264
|
+
const text = payload.toString('utf8');
|
|
265
|
+
if (!text.startsWith('{')) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
let data;
|
|
269
|
+
try {
|
|
270
|
+
data = JSON.parse(text);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
// stat/device/STATUS6 = {"StatusMQT":{"MqttClient":"DVES_123456", ...}}
|
|
276
|
+
const mqttClient = data.StatusMQT?.MqttClient;
|
|
277
|
+
if (typeof mqttClient === 'string' && mqttClient) {
|
|
278
|
+
return { name: mqttClient, priority: NAME_PRIORITY.mqttClient };
|
|
279
|
+
}
|
|
280
|
+
// stat/device/STATUS5 = {"StatusNET":{"Hostname":"tasmota-1234", ...}}
|
|
281
|
+
const netHostname = data.StatusNET?.Hostname;
|
|
282
|
+
if (typeof netHostname === 'string' && netHostname) {
|
|
283
|
+
return { name: netHostname, priority: NAME_PRIORITY.hostname };
|
|
284
|
+
}
|
|
285
|
+
// tele/device/STATE or tele/device/INFO2 = {..., "Hostname":"tasmota-1234", ...}
|
|
286
|
+
if (typeof data.Hostname === 'string' && data.Hostname) {
|
|
287
|
+
return { name: data.Hostname, priority: NAME_PRIORITY.hostname };
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
async createClientForDevice(topicPrefix, deviceName) {
|
|
292
|
+
const client = this.createPseudoClient(deviceName, topicPrefix);
|
|
293
|
+
this.clients[client.id] = client;
|
|
294
|
+
this.mappingClients[client.iobId] = client.id;
|
|
295
|
+
this.createClient(client, () => { });
|
|
296
|
+
await this.updateClients();
|
|
297
|
+
this.adapter.log.debug(`Created device "${deviceName}" for topic "${topicPrefix}"`);
|
|
298
|
+
// The external broker keeps running while the adapter restarts, so the devices do not
|
|
299
|
+
// repeat their boot messages. Ask them for the information which is normally sent in INFO2/INFO3
|
|
300
|
+
if (this.tasmotaTopics.has(topicPrefix)) {
|
|
301
|
+
this.publishCommand(topicPrefix, 'Status', '5');
|
|
302
|
+
this.publishCommand(topicPrefix, 'Status', '2');
|
|
303
|
+
}
|
|
304
|
+
return client;
|
|
305
|
+
}
|
|
306
|
+
async renameDevice(oldName, newName, topicPrefix, priority) {
|
|
307
|
+
if (oldName === newName) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (this.clients[newName]) {
|
|
311
|
+
const conflict = `${topicPrefix}=>${newName}`;
|
|
312
|
+
if (!this.reportedConflicts.has(conflict)) {
|
|
313
|
+
this.reportedConflicts.add(conflict);
|
|
314
|
+
this.adapter.log.warn(`Cannot rename device "${oldName}" (topic: "${topicPrefix}") to "${newName}": another device with this name already exists`);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.adapter.log.info(`Renaming device "${oldName}" => "${newName}" (topic: "${topicPrefix}")`);
|
|
319
|
+
const oldIobId = oldName.replace(mqttBase_1.FORBIDDEN_CHARS, '_');
|
|
320
|
+
const newIobId = newName.replace(mqttBase_1.FORBIDDEN_CHARS, '_');
|
|
321
|
+
const oldPrefix = `${this.adapter.namespace}.${oldIobId}`;
|
|
322
|
+
const newPrefix = `${this.adapter.namespace}.${newIobId}`;
|
|
323
|
+
// Create new client and update routing maps synchronously BEFORE any awaits.
|
|
324
|
+
// This ensures incoming messages during the async object migration are routed
|
|
325
|
+
// to the new client name immediately, not the old one.
|
|
326
|
+
const newClient = this.createPseudoClient(newName, topicPrefix);
|
|
327
|
+
// Preserve existing topic mappings so commands work immediately after rename
|
|
328
|
+
if (this.clients[oldName]?._map) {
|
|
329
|
+
newClient._map = { ...this.clients[oldName]._map };
|
|
330
|
+
}
|
|
331
|
+
this.createClient(newClient, () => { });
|
|
332
|
+
delete this.clients[oldName];
|
|
333
|
+
delete this.mappingClients[oldIobId];
|
|
334
|
+
this.clients[newName] = newClient;
|
|
335
|
+
this.mappingClients[newIobId] = newName;
|
|
336
|
+
this.deviceNames[topicPrefix] = { name: newName, priority };
|
|
337
|
+
const objects = await this.adapter.getForeignObjectsAsync(`${oldPrefix}.*`);
|
|
338
|
+
for (const [oldId, obj] of Object.entries(objects)) {
|
|
339
|
+
const suffix = oldId.slice(oldPrefix.length);
|
|
340
|
+
const newId = `${newPrefix}${suffix}`;
|
|
341
|
+
const { _id, ...objWithoutId } = obj;
|
|
342
|
+
void _id;
|
|
343
|
+
await this.adapter.setForeignObjectAsync(newId, objWithoutId);
|
|
344
|
+
if (obj.type === 'state') {
|
|
345
|
+
const state = await this.adapter.getForeignStateAsync(oldId);
|
|
346
|
+
if (state) {
|
|
347
|
+
await this.adapter.setForeignStateAsync(newId, state.val, state.ack);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await this.adapter.delForeignObjectAsync(oldId);
|
|
351
|
+
}
|
|
352
|
+
await this.adapter.delForeignObjectAsync(oldPrefix);
|
|
353
|
+
await this.updateClients();
|
|
354
|
+
}
|
|
355
|
+
/** Assign the detected name to a topic: create the device or rename an existing one */
|
|
356
|
+
async applyDeviceName(topicPrefix, name, priority) {
|
|
357
|
+
const current = this.deviceNames[topicPrefix];
|
|
358
|
+
if (current) {
|
|
359
|
+
if (current.name === name) {
|
|
360
|
+
if (priority > current.priority) {
|
|
361
|
+
current.priority = priority;
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (priority < current.priority) {
|
|
366
|
+
// do not rename the device because of a less reliable source
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
await this.renameDevice(current.name, name, topicPrefix, priority);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (this.pendingTimers[topicPrefix]) {
|
|
373
|
+
clearTimeout(this.pendingTimers[topicPrefix]);
|
|
374
|
+
delete this.pendingTimers[topicPrefix];
|
|
375
|
+
}
|
|
376
|
+
const topicIobId = topicPrefix.replace(mqttBase_1.FORBIDDEN_CHARS, '_');
|
|
377
|
+
if (topicIobId !== name.replace(mqttBase_1.FORBIDDEN_CHARS, '_')) {
|
|
378
|
+
const existingObj = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${topicIobId}`);
|
|
379
|
+
if (existingObj) {
|
|
380
|
+
// The objects were created with the topic as fallback name => migrate them
|
|
381
|
+
this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };
|
|
382
|
+
await this.renameDevice(topicPrefix, name, topicPrefix, priority);
|
|
383
|
+
// if the renaming was not possible, the device must exist with its old name
|
|
384
|
+
await this.ensureClient(topicPrefix);
|
|
385
|
+
await this.processPending(topicPrefix);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
this.deviceNames[topicPrefix] = { name, priority };
|
|
390
|
+
await this.createClientForDevice(topicPrefix, name);
|
|
391
|
+
await this.processPending(topicPrefix);
|
|
392
|
+
}
|
|
393
|
+
/** Make sure that a client exists for the current name of a topic */
|
|
394
|
+
async ensureClient(topicPrefix) {
|
|
395
|
+
const device = this.deviceNames[topicPrefix];
|
|
396
|
+
if (device && !this.clients[device.name]) {
|
|
397
|
+
await this.createClientForDevice(topicPrefix, device.name);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Process the messages which arrived before the name of the device was known */
|
|
401
|
+
async processPending(topicPrefix) {
|
|
402
|
+
const pending = this.pendingMessages[topicPrefix];
|
|
403
|
+
delete this.pendingMessages[topicPrefix];
|
|
404
|
+
const device = this.deviceNames[topicPrefix];
|
|
405
|
+
const client = device ? this.clients[device.name] : null;
|
|
406
|
+
if (!pending?.length || !client) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
for (const msg of pending) {
|
|
410
|
+
await this.receivedTopic(this.toPacket(msg.topic, msg.payload, msg.packet), client).catch(err => this.adapter.log.error(`Error processing buffered message: ${err.message}`));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async fallbackToTopicPrefix(topicPrefix) {
|
|
414
|
+
delete this.pendingTimers[topicPrefix];
|
|
415
|
+
if (this.deviceNames[topicPrefix]) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.adapter.log.debug(`No device name received for "${topicPrefix}" within ${NAME_TIMEOUT_MS / 1000}s, using the topic as device name`);
|
|
419
|
+
this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };
|
|
420
|
+
await this.createClientForDevice(topicPrefix, topicPrefix);
|
|
421
|
+
await this.processPending(topicPrefix);
|
|
422
|
+
}
|
|
423
|
+
/** Store a message till the name of the device is known */
|
|
424
|
+
bufferMessage(topicPrefix, topic, payload, packet) {
|
|
425
|
+
if (!this.pendingMessages[topicPrefix]) {
|
|
426
|
+
this.pendingMessages[topicPrefix] = [];
|
|
427
|
+
// ask the device how its MQTT client is called
|
|
428
|
+
if (this.tasmotaTopics.has(topicPrefix)) {
|
|
429
|
+
this.publishCommand(topicPrefix, 'Status', '6');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const pending = this.pendingMessages[topicPrefix];
|
|
433
|
+
if (pending.length >= MAX_PENDING_MESSAGES) {
|
|
434
|
+
pending.shift();
|
|
435
|
+
}
|
|
436
|
+
pending.push({ topic, payload, packet });
|
|
437
|
+
this.pendingTimers[topicPrefix] ||= setTimeout(() => {
|
|
438
|
+
this.queue = this.queue
|
|
439
|
+
.then(() => this.fallbackToTopicPrefix(topicPrefix))
|
|
440
|
+
.catch(err => this.adapter.log.error(`Cannot create device "${topicPrefix}": ${err.message}`));
|
|
441
|
+
}, NAME_TIMEOUT_MS);
|
|
442
|
+
}
|
|
443
|
+
async handleExternalMessage(topic, payload, packet) {
|
|
444
|
+
const info = this.analyzeTopic(topic);
|
|
445
|
+
const topicPrefix = info.device;
|
|
446
|
+
if (!topicPrefix) {
|
|
447
|
+
this.adapter.log.debug(`Ignore message with unexpected topic: ${topic}`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (info.structure) {
|
|
451
|
+
this.tasmotaTopics.add(topicPrefix);
|
|
452
|
+
this.topicFormats[topicPrefix] = { structure: info.structure, prefix: info.prefix || '' };
|
|
453
|
+
}
|
|
454
|
+
// process the message like it would come with the standard full topic
|
|
455
|
+
topic = info.standardTopic;
|
|
456
|
+
// The name of the device can be part of this message
|
|
457
|
+
const detected = this.detectDeviceName(payload);
|
|
458
|
+
if (detected) {
|
|
459
|
+
await this.applyDeviceName(topicPrefix, detected.name, detected.priority);
|
|
460
|
+
}
|
|
461
|
+
else if (!this.deviceNames[topicPrefix] && !this.tasmotaTopics.has(topicPrefix)) {
|
|
462
|
+
// Devices which do not use the tasmota topics (e.g. OpenBeken) cannot be asked
|
|
463
|
+
// for their name, so the topic is used immediately
|
|
464
|
+
this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };
|
|
465
|
+
await this.createClientForDevice(topicPrefix, topicPrefix);
|
|
466
|
+
await this.processPending(topicPrefix);
|
|
467
|
+
}
|
|
468
|
+
const device = this.deviceNames[topicPrefix];
|
|
469
|
+
if (device) {
|
|
470
|
+
const client = this.clients[device.name];
|
|
471
|
+
if (client) {
|
|
472
|
+
await this.receivedTopic(this.toPacket(topic, payload, packet), client);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.bufferMessage(topicPrefix, topic, payload, packet);
|
|
477
|
+
}
|
|
478
|
+
createPseudoClient(id, fallBackName) {
|
|
479
|
+
return {
|
|
480
|
+
__secret: `${Date.now()}_${Math.round(Math.random() * 10000)}`,
|
|
481
|
+
id,
|
|
482
|
+
iobId: id.replace(mqttBase_1.FORBIDDEN_CHARS, '_'),
|
|
483
|
+
cleanSession: true,
|
|
484
|
+
_messages: [],
|
|
485
|
+
_subsID: {},
|
|
486
|
+
_subs: {},
|
|
487
|
+
_resendonStart: null,
|
|
488
|
+
_map: {},
|
|
489
|
+
_fallBackName: fallBackName,
|
|
490
|
+
publish: (packet) => {
|
|
491
|
+
const pkts = Array.isArray(packet) ? packet : [packet];
|
|
492
|
+
pkts.forEach(p => this.mqttClient?.publish(this.toBrokerTopic(p.topic), p.payload, {
|
|
493
|
+
qos: p.qos || 0,
|
|
494
|
+
retain: p.retain || false,
|
|
495
|
+
}));
|
|
496
|
+
},
|
|
497
|
+
connack: () => { },
|
|
498
|
+
puback: () => { },
|
|
499
|
+
pubrec: () => { },
|
|
500
|
+
pubrel: () => { },
|
|
501
|
+
pubcomp: () => { },
|
|
502
|
+
unsuback: () => { },
|
|
503
|
+
suback: () => { },
|
|
504
|
+
pingresp: () => { },
|
|
505
|
+
destroy: () => { },
|
|
506
|
+
on: () => { },
|
|
507
|
+
stream: { remoteAddress: 'external-broker', remotePort: 0 },
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
exports.default = MQTTBridge;
|
|
512
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../../src/lib/bridge.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport mqtt from 'mqtt';\nimport MQTTBase, { FORBIDDEN_CHARS, type MQTTClient, type MQTTPacket } from './mqttBase';\n\ninterface PublishPacket {\n qos: 0 | 1 | 2;\n retain: boolean;\n}\n\n/**\n * How the tasmota full topic is built:\n * - standard: %prefix%/%topic%/ => tele/device/STATE\n * - device-first: %topic%/%prefix%/ => device/tele/STATE\n * Both can have a fix prefix in front of it: gateway/%prefix%/%topic%/ => gateway/tele/device/STATE\n */\ntype TopicStructure = 'standard' | 'device-first';\n\n/** How the topics of one device look like */\ninterface TopicFormat {\n structure: TopicStructure;\n /** Fix prefix in front of the full topic, e.g. for multiple gateways on one broker */\n prefix: string;\n}\n\ninterface TopicInfo extends Partial<TopicFormat> {\n /** The device part of the topic, empty if the topic cannot be assigned to a device */\n device: string;\n /** undefined for topics without tasmota prefix, e.g. OpenBeken */\n structure?: TopicStructure;\n /** The topic in the standard structure without prefix, so it is processed like with the built-in broker */\n standardTopic: string;\n}\n\ninterface PendingMessage {\n topic: string;\n payload: Buffer;\n packet: PublishPacket;\n}\n\ninterface DeviceName {\n name: string;\n priority: number;\n}\n\n/**\n * Where the name of a device comes from. A name from a less reliable source never overwrites\n * a name from a better one, so devices are not renamed back and forth with every message.\n */\nconst NAME_PRIORITY = {\n /** Fallback: the topic itself */\n topic: 0,\n /** \"Hostname\" from tele/.../STATE, tele/.../INFO2 or stat/.../STATUS5 */\n hostname: 1,\n /** \"MqttClient\" from stat/.../STATUS6 - the same name as with the built-in broker */\n mqttClient: 2,\n};\n\nconst TASMOTA_PREFIXES = ['tele', 'stat', 'cmnd'];\n// \"tele/#\" and \"stat/#\" are the standard full topics, \"+/tele/+\" and \"+/stat/+\" the device-first ones\nconst DEFAULT_TOPICS = ['tele/#', 'stat/#', '+/tele/+', '+/stat/+'];\nconst NAME_TIMEOUT_MS = 30_000;\nconst MAX_PENDING_MESSAGES = 100;\n\n/**\n * MQTT bridge: connects as a client to an existing (external) MQTT broker\n * instead of starting an own broker. All message processing is shared with the\n * server implementation via MQTTBase.\n */\nexport default class MQTTBridge extends MQTTBase {\n private mqttClient: ReturnType<typeof mqtt.connect> | null = null;\n private deviceNames: Record<string, DeviceName> = {};\n private pendingMessages: Record<string, PendingMessage[]> = {};\n private pendingTimers: Record<string, ReturnType<typeof setTimeout>> = {};\n private tasmotaTopics: Set<string> = new Set();\n private topicFormats: Record<string, TopicFormat> = {};\n private reportedConflicts: Set<string> = new Set();\n /** All messages are processed strictly one after another */\n private queue: Promise<void> = Promise.resolve();\n\n constructor(adapter: ioBroker.Adapter) {\n super(adapter);\n\n if (!this.config.externalBrokerUrl) {\n this.adapter.log.error('External broker URL is not configured');\n return;\n }\n\n this.connect();\n }\n\n async destroy(): Promise<void> {\n for (const topicPrefix in this.pendingTimers) {\n clearTimeout(this.pendingTimers[topicPrefix]);\n }\n this.pendingTimers = {};\n this.pendingMessages = {};\n\n // wait till the currently processed message is done\n await this.queue.catch(() => {});\n\n if (this.mqttClient) {\n for (const id in this.clients) {\n await this.adapter.setForeignStateAsync(\n `${this.adapter.namespace}.${this.clients[id].iobId}.alive`,\n false,\n true,\n );\n }\n await this.adapter.setStateAsync('info.connection', '', true);\n await new Promise<void>(resolve => this.mqttClient!.end(false, {}, () => resolve()));\n this.mqttClient = null;\n }\n }\n\n /** In bridge mode info.connection shows the URL of the external broker, not the devices */\n protected async updateConnectionState(): Promise<void> {\n // nothing to do, the state is set when the connection to the broker is established or lost\n }\n\n protected sendState2Client(client: MQTTClient, topic: string, state: ioBroker.StateValue, qos: 0 | 1 | 2): void {\n const brokerTopic = this.toBrokerTopic(topic);\n this.adapter.log.debug(`Send to external broker \"${client.id}\": ${brokerTopic} = ${state}`);\n this.mqttClient?.publish(brokerTopic, state === null ? 'null' : (state?.toString() ?? ''), { qos });\n }\n\n private normalizeUrl(url: string): string {\n if (!url.includes('://')) {\n return `mqtt://${url}`;\n }\n return url;\n }\n\n /** Topics to subscribe to. Can be configured, e.g. to support other full topics or OpenBeken devices */\n private getSubscriptions(): string[] {\n const topics = (this.config.externalBrokerTopics || '')\n .split(/[,;\\s]+/)\n .map(topic => topic.trim())\n .filter(topic => topic);\n\n return topics.length ? topics : DEFAULT_TOPICS;\n }\n\n /** Read an optional certificate file for encrypted connections */\n private readCertificate(path: string, name: string): Buffer | undefined {\n if (!path) {\n return undefined;\n }\n try {\n return readFileSync(path);\n } catch (err: unknown) {\n this.adapter.log.error(`Cannot read ${name} \"${path}\": ${(err as Error).message}`);\n return undefined;\n }\n }\n\n private getConnectOptions(): mqtt.IClientOptions {\n const options: mqtt.IClientOptions = {\n username: this.config.externalBrokerUser || undefined,\n password: this.config.externalBrokerPassword || undefined,\n clientId: this.config.externalBrokerClientId || `iobroker_sonoff_${this.adapter.namespace}`,\n keepalive: parseInt(this.config.externalBrokerKeepalive as unknown as string, 10) || 60,\n clean: this.config.externalBrokerCleanSession !== false,\n reconnectPeriod: 5000,\n };\n\n // Certificates are only used for encrypted connections: mqtts://, ssl://, tls://, wss://\n if (this.normalizeUrl(this.config.externalBrokerUrl).includes('s://')) {\n options.rejectUnauthorized = this.config.externalBrokerRejectUnauthorized !== false;\n options.ca = this.readCertificate(this.config.externalBrokerCaPath, 'CA certificate');\n options.cert = this.readCertificate(this.config.externalBrokerCertPath, 'client certificate');\n options.key = this.readCertificate(this.config.externalBrokerKeyPath, 'client key');\n }\n\n return options;\n }\n\n private connect(): void {\n const url = this.normalizeUrl(this.config.externalBrokerUrl);\n const subscriptions = this.getSubscriptions();\n this.adapter.log.info(`Connecting to external MQTT broker: ${url}`);\n this.mqttClient = mqtt.connect(url, this.getConnectOptions());\n\n this.mqttClient.on('connect', () => {\n this.adapter.log.info(`Connected to external MQTT broker ${url}`);\n this.adapter\n .setStateAsync('info.connection', url, true)\n .catch(err => this.adapter.log.error(`Cannot set connection state: ${err}`));\n this.mqttClient!.subscribe(subscriptions, { qos: this.config.defaultQoS }, err => {\n if (err) {\n this.adapter.log.error(`Cannot subscribe to external broker: ${err.message}`);\n } else {\n this.adapter.log.info(`Subscribed to ${subscriptions.join(', ')}`);\n }\n });\n this.updateClients().catch(err => this.adapter.log.error(`Cannot update clients: ${err.message}`));\n });\n\n this.mqttClient.on('message', (topic: string, payload: Buffer, packet: PublishPacket): void => {\n // Devices are created and renamed asynchronously, so the messages must not overlap\n this.queue = this.queue\n .then(() => this.handleExternalMessage(topic, payload, packet))\n .catch(err => this.adapter.log.error(`Cannot handle external message: ${err.message}`));\n });\n\n this.mqttClient.on('error', err => this.adapter.log.error(`External MQTT broker error: ${err.message}`));\n\n this.mqttClient.on('close', () => {\n this.adapter.log.debug('External MQTT broker connection closed');\n this.adapter\n .setStateAsync('info.connection', '', true)\n .catch(err => this.adapter.log.error(`Cannot set connection state: ${err}`));\n });\n\n this.mqttClient.on('reconnect', () => {\n this.adapter.log.debug('Reconnecting to external MQTT broker...');\n });\n }\n\n /**\n * Determine which device a topic belongs to and how the full topic is built:\n * - tele/device/STATE => device, standard\n * - tele/house/floor/dev/STATE => house/floor/dev, standard (nested full topics)\n * - device/tele/STATE => device, device-first (full topic %topic%/%prefix%/)\n * - gateway/tele/device/STATE => device, standard with the prefix \"gateway\"\n * - device/led_enableAll/get => device, no tasmota structure (OpenBeken)\n */\n private analyzeTopic(topic: string): TopicInfo {\n const parts = topic.split('/');\n const index = parts.findIndex(part => TASMOTA_PREFIXES.includes(part));\n const command = parts[parts.length - 1];\n\n // the prefix is the first part: tele/device/STATE\n if (index === 0) {\n return {\n device: parts.length > 2 ? parts.slice(1, -1).join('/') : '',\n structure: 'standard',\n prefix: '',\n standardTopic: topic,\n };\n }\n\n // the prefix stands directly before the command: device/tele/STATE\n if (index > 0 && index === parts.length - 2) {\n const device = parts.slice(0, index).join('/');\n return {\n device,\n structure: 'device-first',\n prefix: '',\n standardTopic: `${parts[index]}/${device}/${command}`,\n };\n }\n\n // something stands in front of the full topic: gateway/tele/device/STATE\n if (index > 0) {\n const device = parts.slice(index + 1, -1).join('/');\n return {\n device,\n structure: 'standard',\n prefix: parts.slice(0, index).join('/'),\n standardTopic: `${parts[index]}/${device}/${command}`,\n };\n }\n\n return { device: parts[0] || '', standardTopic: topic };\n }\n\n /** Convert a standard topic (cmnd/device/POWER) into the structure and prefix the device uses */\n private toBrokerTopic(topic: string): string {\n const parts = topic.split('/');\n if (parts.length < 3 || !TASMOTA_PREFIXES.includes(parts[0])) {\n return topic;\n }\n const device = parts.slice(1, -1).join('/');\n const format = this.topicFormats[device];\n if (!format) {\n return topic;\n }\n\n const command = parts[parts.length - 1];\n const fullTopic =\n format.structure === 'device-first'\n ? `${device}/${parts[0]}/${command}`\n : `${parts[0]}/${device}/${command}`;\n\n return format.prefix ? `${format.prefix}/${fullTopic}` : fullTopic;\n }\n\n private publishCommand(device: string, command: string, payload: string): void {\n this.mqttClient?.publish(this.toBrokerTopic(`cmnd/${device}/${command}`), payload);\n }\n\n private toPacket(topic: string, payload: Buffer, packet: PublishPacket): MQTTPacket {\n return {\n topic,\n payload,\n qos: packet.qos,\n retain: packet.retain,\n messageId: 0,\n };\n }\n\n /** Find the name of the device in the payload of a message */\n private detectDeviceName(payload: Buffer): DeviceName | null {\n const text = payload.toString('utf8');\n if (!text.startsWith('{')) {\n return null;\n }\n let data: Record<string, any>;\n try {\n data = JSON.parse(text);\n } catch {\n return null;\n }\n\n // stat/device/STATUS6 = {\"StatusMQT\":{\"MqttClient\":\"DVES_123456\", ...}}\n const mqttClient = data.StatusMQT?.MqttClient;\n if (typeof mqttClient === 'string' && mqttClient) {\n return { name: mqttClient, priority: NAME_PRIORITY.mqttClient };\n }\n\n // stat/device/STATUS5 = {\"StatusNET\":{\"Hostname\":\"tasmota-1234\", ...}}\n const netHostname = data.StatusNET?.Hostname;\n if (typeof netHostname === 'string' && netHostname) {\n return { name: netHostname, priority: NAME_PRIORITY.hostname };\n }\n\n // tele/device/STATE or tele/device/INFO2 = {..., \"Hostname\":\"tasmota-1234\", ...}\n if (typeof data.Hostname === 'string' && data.Hostname) {\n return { name: data.Hostname, priority: NAME_PRIORITY.hostname };\n }\n\n return null;\n }\n\n private async createClientForDevice(topicPrefix: string, deviceName: string): Promise<MQTTClient> {\n const client = this.createPseudoClient(deviceName, topicPrefix);\n this.clients[client.id] = client;\n this.mappingClients[client.iobId] = client.id;\n this.createClient(client, () => {});\n await this.updateClients();\n this.adapter.log.debug(`Created device \"${deviceName}\" for topic \"${topicPrefix}\"`);\n\n // The external broker keeps running while the adapter restarts, so the devices do not\n // repeat their boot messages. Ask them for the information which is normally sent in INFO2/INFO3\n if (this.tasmotaTopics.has(topicPrefix)) {\n this.publishCommand(topicPrefix, 'Status', '5');\n this.publishCommand(topicPrefix, 'Status', '2');\n }\n return client;\n }\n\n private async renameDevice(oldName: string, newName: string, topicPrefix: string, priority: number): Promise<void> {\n if (oldName === newName) {\n return;\n }\n\n if (this.clients[newName]) {\n const conflict = `${topicPrefix}=>${newName}`;\n if (!this.reportedConflicts.has(conflict)) {\n this.reportedConflicts.add(conflict);\n this.adapter.log.warn(\n `Cannot rename device \"${oldName}\" (topic: \"${topicPrefix}\") to \"${newName}\": another device with this name already exists`,\n );\n }\n return;\n }\n\n this.adapter.log.info(`Renaming device \"${oldName}\" => \"${newName}\" (topic: \"${topicPrefix}\")`);\n\n const oldIobId = oldName.replace(FORBIDDEN_CHARS, '_');\n const newIobId = newName.replace(FORBIDDEN_CHARS, '_');\n const oldPrefix = `${this.adapter.namespace}.${oldIobId}`;\n const newPrefix = `${this.adapter.namespace}.${newIobId}`;\n\n // Create new client and update routing maps synchronously BEFORE any awaits.\n // This ensures incoming messages during the async object migration are routed\n // to the new client name immediately, not the old one.\n const newClient = this.createPseudoClient(newName, topicPrefix);\n // Preserve existing topic mappings so commands work immediately after rename\n if (this.clients[oldName]?._map) {\n newClient._map = { ...this.clients[oldName]._map };\n }\n this.createClient(newClient, () => {});\n delete this.clients[oldName];\n delete this.mappingClients[oldIobId];\n this.clients[newName] = newClient;\n this.mappingClients[newIobId] = newName;\n this.deviceNames[topicPrefix] = { name: newName, priority };\n\n const objects = await this.adapter.getForeignObjectsAsync(`${oldPrefix}.*`);\n for (const [oldId, obj] of Object.entries(objects)) {\n const suffix = oldId.slice(oldPrefix.length);\n const newId = `${newPrefix}${suffix}`;\n const { _id, ...objWithoutId } = obj;\n void _id;\n await this.adapter.setForeignObjectAsync(newId, objWithoutId);\n if (obj.type === 'state') {\n const state = await this.adapter.getForeignStateAsync(oldId);\n if (state) {\n await this.adapter.setForeignStateAsync(newId, state.val, state.ack);\n }\n }\n await this.adapter.delForeignObjectAsync(oldId);\n }\n\n await this.adapter.delForeignObjectAsync(oldPrefix);\n\n await this.updateClients();\n }\n\n /** Assign the detected name to a topic: create the device or rename an existing one */\n private async applyDeviceName(topicPrefix: string, name: string, priority: number): Promise<void> {\n const current = this.deviceNames[topicPrefix];\n\n if (current) {\n if (current.name === name) {\n if (priority > current.priority) {\n current.priority = priority;\n }\n return;\n }\n if (priority < current.priority) {\n // do not rename the device because of a less reliable source\n return;\n }\n await this.renameDevice(current.name, name, topicPrefix, priority);\n return;\n }\n\n if (this.pendingTimers[topicPrefix]) {\n clearTimeout(this.pendingTimers[topicPrefix]);\n delete this.pendingTimers[topicPrefix];\n }\n\n const topicIobId = topicPrefix.replace(FORBIDDEN_CHARS, '_');\n if (topicIobId !== name.replace(FORBIDDEN_CHARS, '_')) {\n const existingObj = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${topicIobId}`);\n if (existingObj) {\n // The objects were created with the topic as fallback name => migrate them\n this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };\n await this.renameDevice(topicPrefix, name, topicPrefix, priority);\n // if the renaming was not possible, the device must exist with its old name\n await this.ensureClient(topicPrefix);\n await this.processPending(topicPrefix);\n return;\n }\n }\n\n this.deviceNames[topicPrefix] = { name, priority };\n await this.createClientForDevice(topicPrefix, name);\n await this.processPending(topicPrefix);\n }\n\n /** Make sure that a client exists for the current name of a topic */\n private async ensureClient(topicPrefix: string): Promise<void> {\n const device = this.deviceNames[topicPrefix];\n if (device && !this.clients[device.name]) {\n await this.createClientForDevice(topicPrefix, device.name);\n }\n }\n\n /** Process the messages which arrived before the name of the device was known */\n private async processPending(topicPrefix: string): Promise<void> {\n const pending = this.pendingMessages[topicPrefix];\n delete this.pendingMessages[topicPrefix];\n\n const device = this.deviceNames[topicPrefix];\n const client = device ? this.clients[device.name] : null;\n if (!pending?.length || !client) {\n return;\n }\n\n for (const msg of pending) {\n await this.receivedTopic(this.toPacket(msg.topic, msg.payload, msg.packet), client).catch(err =>\n this.adapter.log.error(`Error processing buffered message: ${err.message}`),\n );\n }\n }\n\n private async fallbackToTopicPrefix(topicPrefix: string): Promise<void> {\n delete this.pendingTimers[topicPrefix];\n if (this.deviceNames[topicPrefix]) {\n return;\n }\n this.adapter.log.debug(\n `No device name received for \"${topicPrefix}\" within ${NAME_TIMEOUT_MS / 1000}s, using the topic as device name`,\n );\n this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };\n await this.createClientForDevice(topicPrefix, topicPrefix);\n await this.processPending(topicPrefix);\n }\n\n /** Store a message till the name of the device is known */\n private bufferMessage(topicPrefix: string, topic: string, payload: Buffer, packet: PublishPacket): void {\n if (!this.pendingMessages[topicPrefix]) {\n this.pendingMessages[topicPrefix] = [];\n // ask the device how its MQTT client is called\n if (this.tasmotaTopics.has(topicPrefix)) {\n this.publishCommand(topicPrefix, 'Status', '6');\n }\n }\n\n const pending = this.pendingMessages[topicPrefix];\n if (pending.length >= MAX_PENDING_MESSAGES) {\n pending.shift();\n }\n pending.push({ topic, payload, packet });\n\n this.pendingTimers[topicPrefix] ||= setTimeout(() => {\n this.queue = this.queue\n .then(() => this.fallbackToTopicPrefix(topicPrefix))\n .catch(err => this.adapter.log.error(`Cannot create device \"${topicPrefix}\": ${err.message}`));\n }, NAME_TIMEOUT_MS);\n }\n\n private async handleExternalMessage(topic: string, payload: Buffer, packet: PublishPacket): Promise<void> {\n const info = this.analyzeTopic(topic);\n const topicPrefix = info.device;\n if (!topicPrefix) {\n this.adapter.log.debug(`Ignore message with unexpected topic: ${topic}`);\n return;\n }\n\n if (info.structure) {\n this.tasmotaTopics.add(topicPrefix);\n this.topicFormats[topicPrefix] = { structure: info.structure, prefix: info.prefix || '' };\n }\n\n // process the message like it would come with the standard full topic\n topic = info.standardTopic;\n\n // The name of the device can be part of this message\n const detected = this.detectDeviceName(payload);\n if (detected) {\n await this.applyDeviceName(topicPrefix, detected.name, detected.priority);\n } else if (!this.deviceNames[topicPrefix] && !this.tasmotaTopics.has(topicPrefix)) {\n // Devices which do not use the tasmota topics (e.g. OpenBeken) cannot be asked\n // for their name, so the topic is used immediately\n this.deviceNames[topicPrefix] = { name: topicPrefix, priority: NAME_PRIORITY.topic };\n await this.createClientForDevice(topicPrefix, topicPrefix);\n await this.processPending(topicPrefix);\n }\n\n const device = this.deviceNames[topicPrefix];\n if (device) {\n const client = this.clients[device.name];\n if (client) {\n await this.receivedTopic(this.toPacket(topic, payload, packet), client);\n }\n return;\n }\n\n this.bufferMessage(topicPrefix, topic, payload, packet);\n }\n\n private createPseudoClient(id: string, fallBackName: string): MQTTClient {\n return {\n __secret: `${Date.now()}_${Math.round(Math.random() * 10000)}`,\n id,\n iobId: id.replace(FORBIDDEN_CHARS, '_'),\n cleanSession: true,\n _messages: [],\n _subsID: {},\n _subs: {},\n _resendonStart: null,\n _map: {},\n _fallBackName: fallBackName,\n publish: (packet: MQTTPacket | MQTTPacket[]) => {\n const pkts = Array.isArray(packet) ? packet : [packet];\n pkts.forEach(p =>\n this.mqttClient?.publish(this.toBrokerTopic(p.topic), p.payload, {\n qos: p.qos || 0,\n retain: p.retain || false,\n }),\n );\n },\n connack: () => {},\n puback: () => {},\n pubrec: () => {},\n pubrel: () => {},\n pubcomp: () => {},\n unsuback: () => {},\n suback: () => {},\n pingresp: () => {},\n destroy: () => {},\n on: () => {},\n stream: { remoteAddress: 'external-broker', remotePort: 0 },\n };\n }\n}\n"]}
|