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