@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.
Files changed (87) hide show
  1. package/config.schema.json +88 -0
  2. package/dist/Accesories/ActionTriggerAccessory.d.ts.map +1 -1
  3. package/dist/Accesories/ActionTriggerAccessory.js +6 -11
  4. package/dist/Accesories/ActionTriggerAccessory.js.map +1 -1
  5. package/dist/Accesories/AirQualityAccessory.d.ts.map +1 -1
  6. package/dist/Accesories/AirQualityAccessory.js +10 -14
  7. package/dist/Accesories/AirQualityAccessory.js.map +1 -1
  8. package/dist/Accesories/ContactSensorAccessory.d.ts.map +1 -1
  9. package/dist/Accesories/ContactSensorAccessory.js +11 -15
  10. package/dist/Accesories/ContactSensorAccessory.js.map +1 -1
  11. package/dist/Accesories/DimmerAccessory.d.ts +2 -0
  12. package/dist/Accesories/DimmerAccessory.d.ts.map +1 -1
  13. package/dist/Accesories/DimmerAccessory.js +30 -26
  14. package/dist/Accesories/DimmerAccessory.js.map +1 -1
  15. package/dist/Accesories/DimmerRgbLightAccessory.d.ts.map +1 -1
  16. package/dist/Accesories/DimmerRgbLightAccessory.js +33 -43
  17. package/dist/Accesories/DimmerRgbLightAccessory.js.map +1 -1
  18. package/dist/Accesories/DoorAccessory.d.ts.map +1 -1
  19. package/dist/Accesories/DoorAccessory.js +13 -18
  20. package/dist/Accesories/DoorAccessory.js.map +1 -1
  21. package/dist/Accesories/ElectricityMeterAccessory.js +16 -24
  22. package/dist/Accesories/ElectricityMeterAccessory.js.map +1 -1
  23. package/dist/Accesories/FacadeBlindAccessory.d.ts +15 -0
  24. package/dist/Accesories/FacadeBlindAccessory.d.ts.map +1 -1
  25. package/dist/Accesories/FacadeBlindAccessory.js +235 -57
  26. package/dist/Accesories/FacadeBlindAccessory.js.map +1 -1
  27. package/dist/Accesories/GarageDoorOpenerAccesory.d.ts +2 -0
  28. package/dist/Accesories/GarageDoorOpenerAccesory.d.ts.map +1 -1
  29. package/dist/Accesories/GarageDoorOpenerAccesory.js +23 -16
  30. package/dist/Accesories/GarageDoorOpenerAccesory.js.map +1 -1
  31. package/dist/Accesories/GateAccessory.d.ts +21 -0
  32. package/dist/Accesories/GateAccessory.d.ts.map +1 -0
  33. package/dist/Accesories/GateAccessory.js +98 -0
  34. package/dist/Accesories/GateAccessory.js.map +1 -0
  35. package/dist/Accesories/GateLockAccessory.d.ts +19 -0
  36. package/dist/Accesories/GateLockAccessory.d.ts.map +1 -0
  37. package/dist/Accesories/GateLockAccessory.js +101 -0
  38. package/dist/Accesories/GateLockAccessory.js.map +1 -0
  39. package/dist/Accesories/LeakSensorAccessory.d.ts.map +1 -1
  40. package/dist/Accesories/LeakSensorAccessory.js +11 -15
  41. package/dist/Accesories/LeakSensorAccessory.js.map +1 -1
  42. package/dist/Accesories/LightBulbAccesory.d.ts +2 -0
  43. package/dist/Accesories/LightBulbAccesory.d.ts.map +1 -1
  44. package/dist/Accesories/LightBulbAccesory.js +24 -19
  45. package/dist/Accesories/LightBulbAccesory.js.map +1 -1
  46. package/dist/Accesories/PressureAccessory.d.ts.map +1 -1
  47. package/dist/Accesories/PressureAccessory.js +10 -14
  48. package/dist/Accesories/PressureAccessory.js.map +1 -1
  49. package/dist/Accesories/RGBLightBulbAccesory.d.ts.map +1 -1
  50. package/dist/Accesories/RGBLightBulbAccesory.js +21 -29
  51. package/dist/Accesories/RGBLightBulbAccesory.js.map +1 -1
  52. package/dist/Accesories/RollerShutterAccessory.d.ts +10 -0
  53. package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
  54. package/dist/Accesories/RollerShutterAccessory.js +203 -50
  55. package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
  56. package/dist/Accesories/SwitchAccessory.d.ts +2 -0
  57. package/dist/Accesories/SwitchAccessory.d.ts.map +1 -1
  58. package/dist/Accesories/SwitchAccessory.js +23 -18
  59. package/dist/Accesories/SwitchAccessory.js.map +1 -1
  60. package/dist/Accesories/TemperatureAccessory.d.ts.map +1 -1
  61. package/dist/Accesories/TemperatureAccessory.js +10 -14
  62. package/dist/Accesories/TemperatureAccessory.js.map +1 -1
  63. package/dist/Accesories/TemperatureHumidityAccessory.d.ts.map +1 -1
  64. package/dist/Accesories/TemperatureHumidityAccessory.js +18 -23
  65. package/dist/Accesories/TemperatureHumidityAccessory.js.map +1 -1
  66. package/dist/Accesories/ThermostatAccessory.d.ts.map +1 -1
  67. package/dist/Accesories/ThermostatAccessory.js +40 -48
  68. package/dist/Accesories/ThermostatAccessory.js.map +1 -1
  69. package/dist/Accesories/ValveAccessory.d.ts.map +1 -1
  70. package/dist/Accesories/ValveAccessory.js +20 -25
  71. package/dist/Accesories/ValveAccessory.js.map +1 -1
  72. package/dist/Accesories/WicketAccesory.d.ts.map +1 -1
  73. package/dist/Accesories/WicketAccesory.js +5 -8
  74. package/dist/Accesories/WicketAccesory.js.map +1 -1
  75. package/dist/Heplers/SuplaMqttClient.d.ts +7 -1
  76. package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
  77. package/dist/Heplers/SuplaMqttClient.js +148 -49
  78. package/dist/Heplers/SuplaMqttClient.js.map +1 -1
  79. package/dist/Heplers/SuplaMqttClientContext.d.ts +4 -1
  80. package/dist/Heplers/SuplaMqttClientContext.d.ts.map +1 -1
  81. package/dist/Heplers/SuplaMqttClientContext.js +4 -1
  82. package/dist/Heplers/SuplaMqttClientContext.js.map +1 -1
  83. package/dist/platform.d.ts +58 -0
  84. package/dist/platform.d.ts.map +1 -1
  85. package/dist/platform.js +523 -10
  86. package/dist/platform.js.map +1 -1
  87. 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 DoorAccessory_1 = require("./Accesories/DoorAccessory");
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
- this.setupAccessory(channel, existingAccessory);
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 === 'SuplaPlatform');
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('Failed to save channels: SuplaPlatform not found in config.');
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
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 key = channel.deviceId && channel.channelId
210
- ? `${channel.deviceId}:${channel.channelId}`
211
- : (channel.topic || channel.channelCaption);
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 DoorAccessory_1.DoorAccessory(this, accessory, channel);
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