@marcel2215/homebridge-supla-plugin 2.1.9 → 2.1.11
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/config.schema.json +26 -0
- package/dist/Accesories/ActionTriggerAccessory.d.ts.map +1 -1
- package/dist/Accesories/ActionTriggerAccessory.js +2 -2
- package/dist/Accesories/ActionTriggerAccessory.js.map +1 -1
- package/dist/Accesories/AirQualityAccessory.d.ts.map +1 -1
- package/dist/Accesories/AirQualityAccessory.js +2 -2
- package/dist/Accesories/AirQualityAccessory.js.map +1 -1
- package/dist/Accesories/ContactSensorAccessory.d.ts.map +1 -1
- package/dist/Accesories/ContactSensorAccessory.js +6 -6
- package/dist/Accesories/ContactSensorAccessory.js.map +1 -1
- package/dist/Accesories/DimmerAccessory.d.ts +2 -0
- package/dist/Accesories/DimmerAccessory.d.ts.map +1 -1
- package/dist/Accesories/DimmerAccessory.js +17 -7
- package/dist/Accesories/DimmerAccessory.js.map +1 -1
- package/dist/Accesories/DimmerRgbLightAccessory.d.ts.map +1 -1
- package/dist/Accesories/DimmerRgbLightAccessory.js +11 -11
- package/dist/Accesories/DimmerRgbLightAccessory.js.map +1 -1
- package/dist/Accesories/DoorAccessory.d.ts.map +1 -1
- package/dist/Accesories/DoorAccessory.js +4 -4
- package/dist/Accesories/DoorAccessory.js.map +1 -1
- package/dist/Accesories/ElectricityMeterAccessory.d.ts.map +1 -1
- package/dist/Accesories/ElectricityMeterAccessory.js +2 -2
- package/dist/Accesories/ElectricityMeterAccessory.js.map +1 -1
- package/dist/Accesories/FacadeBlindAccessory.d.ts +15 -0
- package/dist/Accesories/FacadeBlindAccessory.d.ts.map +1 -1
- package/dist/Accesories/FacadeBlindAccessory.js +229 -46
- package/dist/Accesories/FacadeBlindAccessory.js.map +1 -1
- package/dist/Accesories/GarageDoorOpenerAccesory.d.ts +2 -0
- package/dist/Accesories/GarageDoorOpenerAccesory.d.ts.map +1 -1
- package/dist/Accesories/GarageDoorOpenerAccesory.js +15 -5
- package/dist/Accesories/GarageDoorOpenerAccesory.js.map +1 -1
- package/dist/Accesories/GateAccessory.d.ts +19 -3
- package/dist/Accesories/GateAccessory.d.ts.map +1 -1
- package/dist/Accesories/GateAccessory.js +177 -28
- package/dist/Accesories/GateAccessory.js.map +1 -1
- package/dist/Accesories/GateLockAccessory.d.ts +11 -0
- package/dist/Accesories/GateLockAccessory.d.ts.map +1 -1
- package/dist/Accesories/GateLockAccessory.js +132 -19
- package/dist/Accesories/GateLockAccessory.js.map +1 -1
- package/dist/Accesories/LeakSensorAccessory.d.ts.map +1 -1
- package/dist/Accesories/LeakSensorAccessory.js +2 -2
- package/dist/Accesories/LeakSensorAccessory.js.map +1 -1
- package/dist/Accesories/LightBulbAccesory.d.ts +2 -0
- package/dist/Accesories/LightBulbAccesory.d.ts.map +1 -1
- package/dist/Accesories/LightBulbAccesory.js +15 -5
- package/dist/Accesories/LightBulbAccesory.js.map +1 -1
- package/dist/Accesories/PressureAccessory.d.ts.map +1 -1
- package/dist/Accesories/PressureAccessory.js +2 -2
- package/dist/Accesories/PressureAccessory.js.map +1 -1
- package/dist/Accesories/RGBLightBulbAccesory.d.ts.map +1 -1
- package/dist/Accesories/RGBLightBulbAccesory.js +8 -8
- package/dist/Accesories/RGBLightBulbAccesory.js.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.d.ts +10 -0
- package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.js +187 -26
- package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
- package/dist/Accesories/SwitchAccessory.d.ts +2 -0
- package/dist/Accesories/SwitchAccessory.d.ts.map +1 -1
- package/dist/Accesories/SwitchAccessory.js +15 -5
- package/dist/Accesories/SwitchAccessory.js.map +1 -1
- package/dist/Accesories/TemperatureAccessory.d.ts.map +1 -1
- package/dist/Accesories/TemperatureAccessory.js +2 -2
- package/dist/Accesories/TemperatureAccessory.js.map +1 -1
- package/dist/Accesories/TemperatureHumidityAccessory.d.ts.map +1 -1
- package/dist/Accesories/TemperatureHumidityAccessory.js +3 -3
- package/dist/Accesories/TemperatureHumidityAccessory.js.map +1 -1
- package/dist/Accesories/ThermostatAccessory.d.ts.map +1 -1
- package/dist/Accesories/ThermostatAccessory.js +10 -10
- package/dist/Accesories/ThermostatAccessory.js.map +1 -1
- package/dist/Accesories/ValveAccessory.d.ts.map +1 -1
- package/dist/Accesories/ValveAccessory.js +4 -4
- package/dist/Accesories/ValveAccessory.js.map +1 -1
- package/dist/Accesories/WicketAccesory.d.ts.map +1 -1
- package/dist/Accesories/WicketAccesory.js +2 -2
- package/dist/Accesories/WicketAccesory.js.map +1 -1
- package/dist/Heplers/SuplaMqttClient.d.ts +4 -1
- package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
- package/dist/Heplers/SuplaMqttClient.js +74 -39
- package/dist/Heplers/SuplaMqttClient.js.map +1 -1
- package/dist/platform.d.ts +33 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +433 -37
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
package/dist/platform.js
CHANGED
|
@@ -55,7 +55,7 @@ const ActionTriggerAccessory_1 = require("./Accesories/ActionTriggerAccessory");
|
|
|
55
55
|
*/
|
|
56
56
|
class SuplaPlatform {
|
|
57
57
|
constructor(log, config, api) {
|
|
58
|
-
var _a, _b;
|
|
58
|
+
var _a, _b, _c;
|
|
59
59
|
this.log = log;
|
|
60
60
|
this.config = config;
|
|
61
61
|
this.api = api;
|
|
@@ -63,7 +63,14 @@ class SuplaPlatform {
|
|
|
63
63
|
this.Characteristic = this.api.hap.Characteristic;
|
|
64
64
|
this.accessories = [];
|
|
65
65
|
this.mqttHandlers = new Map();
|
|
66
|
+
this.mqttHandlerOwners = new Map();
|
|
67
|
+
this.mqttWildcardHandlers = new Map();
|
|
68
|
+
this.ownerCleanups = new Map();
|
|
69
|
+
this.mqttDesiredSubscriptions = new Set();
|
|
66
70
|
this.mqttSubscriptions = new Set();
|
|
71
|
+
this.mqttPendingSubscriptions = new Set();
|
|
72
|
+
this.mqttRetryTimers = new Map();
|
|
73
|
+
this.mqttRetryState = new Map();
|
|
67
74
|
this.mqttRouterAttached = false;
|
|
68
75
|
this.log.debug('Finished initializing platform:', this.config.name);
|
|
69
76
|
const configView = this.config;
|
|
@@ -84,13 +91,37 @@ class SuplaPlatform {
|
|
|
84
91
|
this.gateLockPulseSeconds = Number(configView.gateLockPulseSeconds) || 0;
|
|
85
92
|
this.gateLockSetOnPayload = ((_a = configView.gateLockSetOnPayload) !== null && _a !== void 0 ? _a : 'true').toString();
|
|
86
93
|
this.gateLockSetOffPayload = ((_b = configView.gateLockSetOffPayload) !== null && _b !== void 0 ? _b : 'false').toString();
|
|
94
|
+
this.gatePartialHiMode = this.normalizeGatePartialHiMode(configView.gatePartialHiMode);
|
|
95
|
+
this.commandQos = this.normalizeCommandQos(configView.commandQos);
|
|
96
|
+
this.commandRetain = this.parseBoolean((_c = configView.commandRetain) !== null && _c !== void 0 ? _c : false);
|
|
97
|
+
this.api.on('shutdown', () => {
|
|
98
|
+
this.unregisterAllMqttHandlers();
|
|
99
|
+
if (this.MqttClient) {
|
|
100
|
+
this.MqttClient.client.end(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
87
103
|
this.api.on('didFinishLaunching', () => {
|
|
88
104
|
log.debug('Executed didFinishLaunching callback');
|
|
89
105
|
const mqttSettings = this.config;
|
|
90
106
|
this.MqttClient = new SuplaMqttClient_1.SuplaMqttClient(mqttSettings, this.log);
|
|
91
107
|
this.startMqttRouter();
|
|
108
|
+
this.MqttClient.client.on('connect', () => {
|
|
109
|
+
this.resubscribeAll(true);
|
|
110
|
+
});
|
|
111
|
+
this.MqttClient.client.on('close', () => {
|
|
112
|
+
this.clearActiveSubscriptions();
|
|
113
|
+
});
|
|
114
|
+
this.MqttClient.client.on('offline', () => {
|
|
115
|
+
this.clearActiveSubscriptions();
|
|
116
|
+
});
|
|
117
|
+
this.MqttClient.client.on('end', () => {
|
|
118
|
+
this.clearActiveSubscriptions();
|
|
119
|
+
});
|
|
120
|
+
if (this.MqttClient.client.connected) {
|
|
121
|
+
this.resubscribeAll(true);
|
|
122
|
+
}
|
|
92
123
|
this.discoverDevices();
|
|
93
|
-
this.MqttClient.discoverChannelsAsync().then((channels) => {
|
|
124
|
+
this.MqttClient.discoverChannelsAsync((topic, handler) => (this.registerMqttHandler(topic, handler, 'discovery'))).then((channels) => {
|
|
94
125
|
this.persistChannels(channels);
|
|
95
126
|
this.discoverDevices(channels);
|
|
96
127
|
this.log.info('Channels discovered and saved to config file');
|
|
@@ -126,10 +157,21 @@ class SuplaPlatform {
|
|
|
126
157
|
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
|
|
127
158
|
if (existingAccessory) {
|
|
128
159
|
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
|
|
160
|
+
const signature = this.getChannelSignature(channel);
|
|
161
|
+
const previousSignature = existingAccessory.context.deviceSignature;
|
|
162
|
+
const wasConfigured = existingAccessory.context.deviceConfigured === true;
|
|
129
163
|
existingAccessory.context.device = channel;
|
|
164
|
+
existingAccessory.context.deviceSignature = signature;
|
|
130
165
|
this.log.debug(`Restoring channel ${channel.channelCaption} (${channel.deviceId}/${channel.channelId}) ` +
|
|
131
166
|
`function=${channel.channelFunction} type=${channel.channelType}`);
|
|
132
|
-
|
|
167
|
+
if (previousSignature !== signature || !wasConfigured) {
|
|
168
|
+
if (previousSignature && previousSignature !== signature) {
|
|
169
|
+
this.resetAccessoryServices(existingAccessory);
|
|
170
|
+
}
|
|
171
|
+
const configured = this.setupAccessory(channel, existingAccessory);
|
|
172
|
+
existingAccessory.context.deviceConfigured = configured;
|
|
173
|
+
}
|
|
174
|
+
this.api.updatePlatformAccessories([existingAccessory]);
|
|
133
175
|
continue;
|
|
134
176
|
}
|
|
135
177
|
this.log.info('Adding new accessory:', channel.channelCaption);
|
|
@@ -137,15 +179,19 @@ class SuplaPlatform {
|
|
|
137
179
|
`function=${channel.channelFunction} type=${channel.channelType}`);
|
|
138
180
|
const accessory = new this.api.platformAccessory(channel.channelCaption, uuid);
|
|
139
181
|
accessory.context.device = channel;
|
|
182
|
+
accessory.context.deviceSignature = this.getChannelSignature(channel);
|
|
140
183
|
if (this.setupAccessory(channel, accessory)) {
|
|
184
|
+
accessory.context.deviceConfigured = true;
|
|
141
185
|
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
|
|
142
186
|
this.accessories.push(accessory);
|
|
187
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
143
188
|
}
|
|
144
189
|
}
|
|
145
190
|
if (shouldPrune) {
|
|
146
191
|
const accessoriesToRemove = this.accessories.filter(accessory => !channelUuids.has(accessory.UUID));
|
|
147
192
|
for (const accessory of accessoriesToRemove) {
|
|
148
193
|
this.log.info('Removing existing accessory from cache:', accessory.displayName);
|
|
194
|
+
this.unregisterMqttHandlers(accessory.UUID);
|
|
149
195
|
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
|
|
150
196
|
const index = this.accessories.indexOf(accessory);
|
|
151
197
|
if (index !== -1) {
|
|
@@ -180,13 +226,16 @@ class SuplaPlatform {
|
|
|
180
226
|
try {
|
|
181
227
|
const configPath = this.api.user.configPath();
|
|
182
228
|
const config = JSON.parse(fs.readFileSync(configPath).toString());
|
|
183
|
-
const platformConfig = (_a = config.platforms) === null || _a === void 0 ? void 0 : _a.find((platform) => platform.platform ===
|
|
229
|
+
const platformConfig = (_a = config.platforms) === null || _a === void 0 ? void 0 : _a.find((platform) => platform.platform === settings_1.PLATFORM_NAME);
|
|
184
230
|
if (!platformConfig) {
|
|
185
|
-
this.log.warn(
|
|
231
|
+
this.log.warn(`Failed to save channels: ${settings_1.PLATFORM_NAME} not found in config.`);
|
|
186
232
|
return;
|
|
187
233
|
}
|
|
188
234
|
platformConfig.channels = JSON.stringify(channels);
|
|
189
|
-
|
|
235
|
+
const payload = JSON.stringify(config, null, 2);
|
|
236
|
+
const tempPath = `${configPath}.tmp`;
|
|
237
|
+
fs.writeFileSync(tempPath, payload);
|
|
238
|
+
fs.renameSync(tempPath, configPath);
|
|
190
239
|
this.log.debug(`Saved ${channels.length} channels to config.`);
|
|
191
240
|
}
|
|
192
241
|
catch (error) {
|
|
@@ -222,12 +271,35 @@ class SuplaPlatform {
|
|
|
222
271
|
return new SuplaChannelContext_1.SuplaChannelContext(baseTopic, channelType, channelFunction, caption, deviceId, channelId);
|
|
223
272
|
}
|
|
224
273
|
getChannelUuid(channel) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
274
|
+
const deviceId = channel.deviceId;
|
|
275
|
+
const channelId = channel.channelId;
|
|
276
|
+
const hasIds = deviceId && channelId && deviceId !== 'unknown' && channelId !== 'unknown';
|
|
277
|
+
const key = hasIds
|
|
278
|
+
? `${deviceId}:${channelId}`
|
|
279
|
+
: (channel.topic || channel.channelCaption || `${deviceId}:${channelId}`);
|
|
228
280
|
return this.api.hap.uuid.generate(key);
|
|
229
281
|
}
|
|
282
|
+
getChannelSignature(channel) {
|
|
283
|
+
var _a, _b, _c, _d, _e;
|
|
284
|
+
return [
|
|
285
|
+
(_a = channel.topic) !== null && _a !== void 0 ? _a : '',
|
|
286
|
+
(_b = channel.channelFunction) !== null && _b !== void 0 ? _b : '',
|
|
287
|
+
(_c = channel.channelType) !== null && _c !== void 0 ? _c : '',
|
|
288
|
+
(_d = channel.deviceId) !== null && _d !== void 0 ? _d : '',
|
|
289
|
+
(_e = channel.channelId) !== null && _e !== void 0 ? _e : '',
|
|
290
|
+
].join('|');
|
|
291
|
+
}
|
|
292
|
+
resetAccessoryServices(accessory) {
|
|
293
|
+
const keepUuid = this.Service.AccessoryInformation.UUID;
|
|
294
|
+
for (const service of accessory.services) {
|
|
295
|
+
if (service.UUID === keepUuid) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
accessory.removeService(service);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
230
301
|
setupAccessory(channel, accessory) {
|
|
302
|
+
this.unregisterMqttHandlers(accessory.UUID);
|
|
231
303
|
this.log.debug(`Mapping channel ${channel.channelCaption} (${channel.deviceId}/${channel.channelId}) ` +
|
|
232
304
|
`function=${channel.channelFunction} type=${channel.channelType}`);
|
|
233
305
|
switch (channel.channelFunction) {
|
|
@@ -392,6 +464,9 @@ class SuplaPlatform {
|
|
|
392
464
|
getGateLockSetOffPayload() {
|
|
393
465
|
return this.gateLockSetOffPayload;
|
|
394
466
|
}
|
|
467
|
+
getGatePartialHiMode() {
|
|
468
|
+
return this.gatePartialHiMode;
|
|
469
|
+
}
|
|
395
470
|
normalizeCoveringControlMode(value) {
|
|
396
471
|
const normalized = (value !== null && value !== void 0 ? value : 'set').toString().toLowerCase();
|
|
397
472
|
if (normalized === 'execute_action') {
|
|
@@ -416,45 +491,113 @@ class SuplaPlatform {
|
|
|
416
491
|
}
|
|
417
492
|
return 'execute_action';
|
|
418
493
|
}
|
|
494
|
+
normalizeGatePartialHiMode(value) {
|
|
495
|
+
const normalized = (value !== null && value !== void 0 ? value : 'moving').toString().toLowerCase();
|
|
496
|
+
if (normalized === 'open_endstop' || normalized === 'open') {
|
|
497
|
+
return 'open_endstop';
|
|
498
|
+
}
|
|
499
|
+
if (normalized === 'pedestrian_endstop' || normalized === 'pedestrian') {
|
|
500
|
+
return 'pedestrian_endstop';
|
|
501
|
+
}
|
|
502
|
+
if (normalized === 'ignore' || normalized === 'absent') {
|
|
503
|
+
return 'ignore';
|
|
504
|
+
}
|
|
505
|
+
return 'moving';
|
|
506
|
+
}
|
|
419
507
|
normalizeTopicSuffix(value) {
|
|
420
508
|
return value.toString().replace(/^\/+/, '');
|
|
421
509
|
}
|
|
422
|
-
registerMqttHandler(topic, handler) {
|
|
423
|
-
var _a;
|
|
510
|
+
registerMqttHandler(topic, handler, ownerId) {
|
|
511
|
+
var _a, _b, _c;
|
|
424
512
|
if (!topic) {
|
|
425
513
|
return () => undefined;
|
|
426
514
|
}
|
|
427
515
|
this.startMqttRouter();
|
|
428
|
-
const
|
|
516
|
+
const handlerMap = this.isWildcardTopic(topic) ? this.mqttWildcardHandlers : this.mqttHandlers;
|
|
517
|
+
const handlers = (_a = handlerMap.get(topic)) !== null && _a !== void 0 ? _a : new Set();
|
|
429
518
|
handlers.add(handler);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
519
|
+
handlerMap.set(topic, handlers);
|
|
520
|
+
const ownerTopics = (_b = this.mqttHandlerOwners.get(ownerId)) !== null && _b !== void 0 ? _b : new Map();
|
|
521
|
+
const ownerHandlers = (_c = ownerTopics.get(topic)) !== null && _c !== void 0 ? _c : new Set();
|
|
522
|
+
ownerHandlers.add(handler);
|
|
523
|
+
ownerTopics.set(topic, ownerHandlers);
|
|
524
|
+
this.mqttHandlerOwners.set(ownerId, ownerTopics);
|
|
525
|
+
this.mqttDesiredSubscriptions.add(topic);
|
|
526
|
+
this.ensureSubscribed(topic, false);
|
|
527
|
+
return () => {
|
|
528
|
+
this.removeHandler(topic, handler);
|
|
529
|
+
const ownerTopics = this.mqttHandlerOwners.get(ownerId);
|
|
530
|
+
const ownerHandlers = ownerTopics === null || ownerTopics === void 0 ? void 0 : ownerTopics.get(topic);
|
|
531
|
+
if (ownerHandlers) {
|
|
532
|
+
ownerHandlers.delete(handler);
|
|
533
|
+
if (ownerHandlers.size === 0) {
|
|
534
|
+
ownerTopics === null || ownerTopics === void 0 ? void 0 : ownerTopics.delete(topic);
|
|
435
535
|
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
536
|
+
}
|
|
537
|
+
if (ownerTopics && ownerTopics.size === 0) {
|
|
538
|
+
this.mqttHandlerOwners.delete(ownerId);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
registerOwnerCleanup(ownerId, cleanup) {
|
|
543
|
+
var _a;
|
|
544
|
+
const cleanups = (_a = this.ownerCleanups.get(ownerId)) !== null && _a !== void 0 ? _a : new Set();
|
|
545
|
+
cleanups.add(cleanup);
|
|
546
|
+
this.ownerCleanups.set(ownerId, cleanups);
|
|
439
547
|
return () => {
|
|
440
|
-
const active = this.
|
|
548
|
+
const active = this.ownerCleanups.get(ownerId);
|
|
441
549
|
if (!active) {
|
|
442
550
|
return;
|
|
443
551
|
}
|
|
444
|
-
active.delete(
|
|
552
|
+
active.delete(cleanup);
|
|
445
553
|
if (active.size === 0) {
|
|
446
|
-
this.
|
|
447
|
-
if (this.mqttSubscriptions.has(topic)) {
|
|
448
|
-
this.MqttClient.client.unsubscribe(topic, (err) => {
|
|
449
|
-
if (err) {
|
|
450
|
-
this.log.error(`MQTT unsubscribe failed for ${topic}: ${err.message}`);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
this.mqttSubscriptions.delete(topic);
|
|
454
|
-
}
|
|
554
|
+
this.ownerCleanups.delete(ownerId);
|
|
455
555
|
}
|
|
456
556
|
};
|
|
457
557
|
}
|
|
558
|
+
unregisterMqttHandlers(ownerId) {
|
|
559
|
+
this.runOwnerCleanup(ownerId);
|
|
560
|
+
const ownerTopics = this.mqttHandlerOwners.get(ownerId);
|
|
561
|
+
if (!ownerTopics) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
for (const [topic, handlers] of ownerTopics) {
|
|
565
|
+
for (const handler of handlers) {
|
|
566
|
+
this.removeHandler(topic, handler);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
this.mqttHandlerOwners.delete(ownerId);
|
|
570
|
+
}
|
|
571
|
+
unregisterAllMqttHandlers() {
|
|
572
|
+
const ownerIds = new Set([
|
|
573
|
+
...this.mqttHandlerOwners.keys(),
|
|
574
|
+
...this.ownerCleanups.keys(),
|
|
575
|
+
]);
|
|
576
|
+
for (const ownerId of Array.from(ownerIds)) {
|
|
577
|
+
this.unregisterMqttHandlers(ownerId);
|
|
578
|
+
}
|
|
579
|
+
for (const topic of Array.from(this.mqttDesiredSubscriptions)) {
|
|
580
|
+
this.removeSubscription(topic);
|
|
581
|
+
}
|
|
582
|
+
this.mqttHandlers.clear();
|
|
583
|
+
this.mqttWildcardHandlers.clear();
|
|
584
|
+
this.mqttDesiredSubscriptions.clear();
|
|
585
|
+
this.mqttSubscriptions.clear();
|
|
586
|
+
this.mqttPendingSubscriptions.clear();
|
|
587
|
+
this.clearAllSubscriptionRetries();
|
|
588
|
+
this.ownerCleanups.clear();
|
|
589
|
+
}
|
|
590
|
+
publishCommand(topic, payload, callback) {
|
|
591
|
+
this.MqttClient.client.publish(topic, payload, { qos: this.commandQos, retain: this.commandRetain }, (error) => {
|
|
592
|
+
if (callback) {
|
|
593
|
+
callback(error);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (error) {
|
|
597
|
+
this.log.error(`Publish failed for ${topic}: ${error.message}`);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
458
601
|
parseBoolean(value) {
|
|
459
602
|
if (typeof value === 'boolean') {
|
|
460
603
|
return value;
|
|
@@ -471,22 +614,275 @@ class SuplaPlatform {
|
|
|
471
614
|
}
|
|
472
615
|
return false;
|
|
473
616
|
}
|
|
617
|
+
runOwnerCleanup(ownerId) {
|
|
618
|
+
const cleanups = this.ownerCleanups.get(ownerId);
|
|
619
|
+
if (!cleanups) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
for (const cleanup of cleanups) {
|
|
623
|
+
try {
|
|
624
|
+
cleanup();
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
this.log.error(`Owner cleanup error for ${ownerId}: ${error.message}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
this.ownerCleanups.delete(ownerId);
|
|
631
|
+
}
|
|
632
|
+
clearActiveSubscriptions() {
|
|
633
|
+
if (this.mqttSubscriptions.size > 0 || this.mqttPendingSubscriptions.size > 0) {
|
|
634
|
+
this.log.debug('MQTT connection lost; clearing active subscriptions.');
|
|
635
|
+
}
|
|
636
|
+
this.mqttSubscriptions.clear();
|
|
637
|
+
this.mqttPendingSubscriptions.clear();
|
|
638
|
+
this.clearAllSubscriptionRetries();
|
|
639
|
+
}
|
|
640
|
+
clearAllSubscriptionRetries() {
|
|
641
|
+
for (const timer of this.mqttRetryTimers.values()) {
|
|
642
|
+
clearTimeout(timer);
|
|
643
|
+
}
|
|
644
|
+
this.mqttRetryTimers.clear();
|
|
645
|
+
this.mqttRetryState.clear();
|
|
646
|
+
}
|
|
647
|
+
clearSubscriptionRetry(topic) {
|
|
648
|
+
const timer = this.mqttRetryTimers.get(topic);
|
|
649
|
+
if (timer) {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
this.mqttRetryTimers.delete(topic);
|
|
652
|
+
}
|
|
653
|
+
this.mqttRetryState.delete(topic);
|
|
654
|
+
}
|
|
655
|
+
scheduleSubscriptionRetry(topic, reason, hardDeny) {
|
|
656
|
+
var _a;
|
|
657
|
+
if (this.mqttRetryTimers.has(topic)) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!this.mqttDesiredSubscriptions.has(topic)) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const baseDelayMs = 2500;
|
|
664
|
+
const maxDelayMs = 60000;
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
const state = (_a = this.mqttRetryState.get(topic)) !== null && _a !== void 0 ? _a : {
|
|
667
|
+
attempt: 0,
|
|
668
|
+
delayMs: baseDelayMs,
|
|
669
|
+
lastLogAt: 0,
|
|
670
|
+
hardDenyCount: 0,
|
|
671
|
+
blockedUntil: 0,
|
|
672
|
+
};
|
|
673
|
+
if (hardDeny) {
|
|
674
|
+
state.hardDenyCount += 1;
|
|
675
|
+
if (state.hardDenyCount >= 3) {
|
|
676
|
+
state.blockedUntil = Math.max(state.blockedUntil, now + 5 * 60 * 1000);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const delayMs = state.delayMs;
|
|
680
|
+
const blockedDelayMs = state.blockedUntil > now ? state.blockedUntil - now : 0;
|
|
681
|
+
let scheduledDelayMs = delayMs;
|
|
682
|
+
if (blockedDelayMs > 0) {
|
|
683
|
+
scheduledDelayMs = blockedDelayMs;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
const jitter = Math.round(delayMs * 0.2 * (Math.random() * 2 - 1));
|
|
687
|
+
scheduledDelayMs = Math.max(baseDelayMs, Math.min(maxDelayMs, delayMs + jitter));
|
|
688
|
+
state.delayMs = Math.min(maxDelayMs, Math.max(baseDelayMs, delayMs * 2));
|
|
689
|
+
state.attempt += 1;
|
|
690
|
+
}
|
|
691
|
+
this.mqttRetryState.set(topic, state);
|
|
692
|
+
this.log.debug(`Retrying MQTT subscribe for ${topic} in ${scheduledDelayMs}ms (${reason}).`);
|
|
693
|
+
const timer = setTimeout(() => {
|
|
694
|
+
this.mqttRetryTimers.delete(topic);
|
|
695
|
+
if (!this.mqttDesiredSubscriptions.has(topic)) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
this.ensureSubscribed(topic, true);
|
|
699
|
+
}, scheduledDelayMs);
|
|
700
|
+
this.mqttRetryTimers.set(topic, timer);
|
|
701
|
+
}
|
|
702
|
+
isSubscriptionGranted(topic, granted) {
|
|
703
|
+
if (!Array.isArray(granted) || granted.length === 0) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
const entry = granted.find((item) => item.topic === topic);
|
|
707
|
+
if (!entry) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
return entry.qos === 0 || entry.qos === 1 || entry.qos === 2;
|
|
711
|
+
}
|
|
712
|
+
logSubscriptionIssue(topic, message) {
|
|
713
|
+
var _a;
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
const state = (_a = this.mqttRetryState.get(topic)) !== null && _a !== void 0 ? _a : {
|
|
716
|
+
attempt: 0,
|
|
717
|
+
delayMs: 2500,
|
|
718
|
+
lastLogAt: 0,
|
|
719
|
+
hardDenyCount: 0,
|
|
720
|
+
blockedUntil: 0,
|
|
721
|
+
};
|
|
722
|
+
const shouldLog = state.lastLogAt === 0 || now - state.lastLogAt > 60000;
|
|
723
|
+
if (shouldLog) {
|
|
724
|
+
this.log.error(message);
|
|
725
|
+
state.lastLogAt = now;
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
this.log.debug(message);
|
|
729
|
+
}
|
|
730
|
+
this.mqttRetryState.set(topic, state);
|
|
731
|
+
}
|
|
732
|
+
ensureSubscribed(topic, force) {
|
|
733
|
+
if (!this.MqttClient) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (!this.MqttClient.client.connected) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!this.mqttDesiredSubscriptions.has(topic)) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (!force && (this.mqttSubscriptions.has(topic) || this.mqttPendingSubscriptions.has(topic))) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (this.mqttPendingSubscriptions.has(topic)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
this.mqttPendingSubscriptions.add(topic);
|
|
749
|
+
this.MqttClient.client.subscribe(topic, (err, granted) => {
|
|
750
|
+
this.mqttPendingSubscriptions.delete(topic);
|
|
751
|
+
if (err) {
|
|
752
|
+
this.logSubscriptionIssue(topic, `MQTT subscribe failed for ${topic}: ${err.message}`);
|
|
753
|
+
this.mqttSubscriptions.delete(topic);
|
|
754
|
+
this.scheduleSubscriptionRetry(topic, 'error', false);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!this.isSubscriptionGranted(topic, granted)) {
|
|
758
|
+
this.logSubscriptionIssue(topic, `MQTT subscription denied for ${topic}.`);
|
|
759
|
+
this.mqttSubscriptions.delete(topic);
|
|
760
|
+
this.scheduleSubscriptionRetry(topic, 'denied', true);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!this.mqttDesiredSubscriptions.has(topic)) {
|
|
764
|
+
this.mqttSubscriptions.delete(topic);
|
|
765
|
+
this.clearSubscriptionRetry(topic);
|
|
766
|
+
if (this.MqttClient.client.connected) {
|
|
767
|
+
this.MqttClient.client.unsubscribe(topic, (unsubscribeErr) => {
|
|
768
|
+
if (unsubscribeErr) {
|
|
769
|
+
this.log.error(`MQTT unsubscribe failed for ${topic}: ${unsubscribeErr.message}`);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.clearSubscriptionRetry(topic);
|
|
776
|
+
this.mqttSubscriptions.add(topic);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
resubscribeAll(force) {
|
|
780
|
+
if (!this.MqttClient) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (force) {
|
|
784
|
+
this.mqttPendingSubscriptions.clear();
|
|
785
|
+
}
|
|
786
|
+
for (const topic of this.mqttDesiredSubscriptions) {
|
|
787
|
+
this.ensureSubscribed(topic, force);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
removeSubscription(topic) {
|
|
791
|
+
this.mqttDesiredSubscriptions.delete(topic);
|
|
792
|
+
this.mqttPendingSubscriptions.delete(topic);
|
|
793
|
+
this.clearSubscriptionRetry(topic);
|
|
794
|
+
if (!this.MqttClient) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (this.mqttSubscriptions.has(topic)) {
|
|
798
|
+
this.MqttClient.client.unsubscribe(topic, (err) => {
|
|
799
|
+
if (err) {
|
|
800
|
+
this.log.error(`MQTT unsubscribe failed for ${topic}: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
this.mqttSubscriptions.delete(topic);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
removeHandler(topic, handler) {
|
|
807
|
+
const handlerMap = this.isWildcardTopic(topic) ? this.mqttWildcardHandlers : this.mqttHandlers;
|
|
808
|
+
const active = handlerMap.get(topic);
|
|
809
|
+
if (!active) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
active.delete(handler);
|
|
813
|
+
if (active.size === 0) {
|
|
814
|
+
handlerMap.delete(topic);
|
|
815
|
+
this.removeSubscription(topic);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
isWildcardTopic(topic) {
|
|
819
|
+
return topic.includes('+') || topic.includes('#');
|
|
820
|
+
}
|
|
821
|
+
topicMatchesFilter(topic, filter) {
|
|
822
|
+
if (filter === topic) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
const filterParts = filter.split('/');
|
|
826
|
+
const topicParts = topic.split('/');
|
|
827
|
+
for (let i = 0; i < filterParts.length; i += 1) {
|
|
828
|
+
const filterPart = filterParts[i];
|
|
829
|
+
if (filterPart === '#') {
|
|
830
|
+
return i === filterParts.length - 1;
|
|
831
|
+
}
|
|
832
|
+
if (i >= topicParts.length) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
if (filterPart === '+') {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (filterPart !== topicParts[i]) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return filterParts.length === topicParts.length;
|
|
843
|
+
}
|
|
844
|
+
normalizeCommandQos(value) {
|
|
845
|
+
const parsed = Number(value);
|
|
846
|
+
if (parsed === 1 || parsed === 2) {
|
|
847
|
+
return parsed;
|
|
848
|
+
}
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
474
851
|
startMqttRouter() {
|
|
475
852
|
if (this.mqttRouterAttached || !this.MqttClient) {
|
|
476
853
|
return;
|
|
477
854
|
}
|
|
478
855
|
this.mqttRouterAttached = true;
|
|
856
|
+
this.resubscribeAll(false);
|
|
479
857
|
this.MqttClient.client.on('message', (topic, message) => {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
858
|
+
const dispatched = new Set();
|
|
859
|
+
const dispatch = (handler, label) => {
|
|
860
|
+
if (dispatched.has(handler)) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
dispatched.add(handler);
|
|
485
864
|
try {
|
|
486
865
|
handler(message, topic);
|
|
487
866
|
}
|
|
488
867
|
catch (error) {
|
|
489
|
-
this.log.error(`MQTT handler error for ${
|
|
868
|
+
this.log.error(`MQTT handler error for ${label}: ${error.message}`);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const exactHandlers = this.mqttHandlers.get(topic);
|
|
872
|
+
if (exactHandlers) {
|
|
873
|
+
for (const handler of exactHandlers) {
|
|
874
|
+
dispatch(handler, topic);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (this.mqttWildcardHandlers.size === 0) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
for (const [filter, handlers] of this.mqttWildcardHandlers) {
|
|
881
|
+
if (!this.topicMatchesFilter(topic, filter)) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
for (const handler of handlers) {
|
|
885
|
+
dispatch(handler, filter);
|
|
490
886
|
}
|
|
491
887
|
}
|
|
492
888
|
});
|