@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.
Files changed (84) hide show
  1. package/config.schema.json +26 -0
  2. package/dist/Accesories/ActionTriggerAccessory.d.ts.map +1 -1
  3. package/dist/Accesories/ActionTriggerAccessory.js +2 -2
  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 +2 -2
  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 +6 -6
  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 +17 -7
  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 +11 -11
  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 +4 -4
  20. package/dist/Accesories/DoorAccessory.js.map +1 -1
  21. package/dist/Accesories/ElectricityMeterAccessory.d.ts.map +1 -1
  22. package/dist/Accesories/ElectricityMeterAccessory.js +2 -2
  23. package/dist/Accesories/ElectricityMeterAccessory.js.map +1 -1
  24. package/dist/Accesories/FacadeBlindAccessory.d.ts +15 -0
  25. package/dist/Accesories/FacadeBlindAccessory.d.ts.map +1 -1
  26. package/dist/Accesories/FacadeBlindAccessory.js +229 -46
  27. package/dist/Accesories/FacadeBlindAccessory.js.map +1 -1
  28. package/dist/Accesories/GarageDoorOpenerAccesory.d.ts +2 -0
  29. package/dist/Accesories/GarageDoorOpenerAccesory.d.ts.map +1 -1
  30. package/dist/Accesories/GarageDoorOpenerAccesory.js +15 -5
  31. package/dist/Accesories/GarageDoorOpenerAccesory.js.map +1 -1
  32. package/dist/Accesories/GateAccessory.d.ts +19 -3
  33. package/dist/Accesories/GateAccessory.d.ts.map +1 -1
  34. package/dist/Accesories/GateAccessory.js +177 -28
  35. package/dist/Accesories/GateAccessory.js.map +1 -1
  36. package/dist/Accesories/GateLockAccessory.d.ts +11 -0
  37. package/dist/Accesories/GateLockAccessory.d.ts.map +1 -1
  38. package/dist/Accesories/GateLockAccessory.js +132 -19
  39. package/dist/Accesories/GateLockAccessory.js.map +1 -1
  40. package/dist/Accesories/LeakSensorAccessory.d.ts.map +1 -1
  41. package/dist/Accesories/LeakSensorAccessory.js +2 -2
  42. package/dist/Accesories/LeakSensorAccessory.js.map +1 -1
  43. package/dist/Accesories/LightBulbAccesory.d.ts +2 -0
  44. package/dist/Accesories/LightBulbAccesory.d.ts.map +1 -1
  45. package/dist/Accesories/LightBulbAccesory.js +15 -5
  46. package/dist/Accesories/LightBulbAccesory.js.map +1 -1
  47. package/dist/Accesories/PressureAccessory.d.ts.map +1 -1
  48. package/dist/Accesories/PressureAccessory.js +2 -2
  49. package/dist/Accesories/PressureAccessory.js.map +1 -1
  50. package/dist/Accesories/RGBLightBulbAccesory.d.ts.map +1 -1
  51. package/dist/Accesories/RGBLightBulbAccesory.js +8 -8
  52. package/dist/Accesories/RGBLightBulbAccesory.js.map +1 -1
  53. package/dist/Accesories/RollerShutterAccessory.d.ts +10 -0
  54. package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
  55. package/dist/Accesories/RollerShutterAccessory.js +187 -26
  56. package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
  57. package/dist/Accesories/SwitchAccessory.d.ts +2 -0
  58. package/dist/Accesories/SwitchAccessory.d.ts.map +1 -1
  59. package/dist/Accesories/SwitchAccessory.js +15 -5
  60. package/dist/Accesories/SwitchAccessory.js.map +1 -1
  61. package/dist/Accesories/TemperatureAccessory.d.ts.map +1 -1
  62. package/dist/Accesories/TemperatureAccessory.js +2 -2
  63. package/dist/Accesories/TemperatureAccessory.js.map +1 -1
  64. package/dist/Accesories/TemperatureHumidityAccessory.d.ts.map +1 -1
  65. package/dist/Accesories/TemperatureHumidityAccessory.js +3 -3
  66. package/dist/Accesories/TemperatureHumidityAccessory.js.map +1 -1
  67. package/dist/Accesories/ThermostatAccessory.d.ts.map +1 -1
  68. package/dist/Accesories/ThermostatAccessory.js +10 -10
  69. package/dist/Accesories/ThermostatAccessory.js.map +1 -1
  70. package/dist/Accesories/ValveAccessory.d.ts.map +1 -1
  71. package/dist/Accesories/ValveAccessory.js +4 -4
  72. package/dist/Accesories/ValveAccessory.js.map +1 -1
  73. package/dist/Accesories/WicketAccesory.d.ts.map +1 -1
  74. package/dist/Accesories/WicketAccesory.js +2 -2
  75. package/dist/Accesories/WicketAccesory.js.map +1 -1
  76. package/dist/Heplers/SuplaMqttClient.d.ts +4 -1
  77. package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
  78. package/dist/Heplers/SuplaMqttClient.js +74 -39
  79. package/dist/Heplers/SuplaMqttClient.js.map +1 -1
  80. package/dist/platform.d.ts +33 -1
  81. package/dist/platform.d.ts.map +1 -1
  82. package/dist/platform.js +433 -37
  83. package/dist/platform.js.map +1 -1
  84. 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
- this.setupAccessory(channel, existingAccessory);
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 === 'SuplaPlatform');
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('Failed to save channels: SuplaPlatform not found in config.');
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
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 key = channel.deviceId && channel.channelId
226
- ? `${channel.deviceId}:${channel.channelId}`
227
- : (channel.topic || channel.channelCaption);
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 handlers = (_a = this.mqttHandlers.get(topic)) !== null && _a !== void 0 ? _a : new Set();
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
- this.mqttHandlers.set(topic, handlers);
431
- if (!this.mqttSubscriptions.has(topic)) {
432
- this.MqttClient.client.subscribe(topic, (err) => {
433
- if (err) {
434
- this.log.error(`MQTT subscribe failed for ${topic}: ${err.message}`);
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
- this.mqttSubscriptions.add(topic);
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.mqttHandlers.get(topic);
548
+ const active = this.ownerCleanups.get(ownerId);
441
549
  if (!active) {
442
550
  return;
443
551
  }
444
- active.delete(handler);
552
+ active.delete(cleanup);
445
553
  if (active.size === 0) {
446
- this.mqttHandlers.delete(topic);
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 handlers = this.mqttHandlers.get(topic);
481
- if (!handlers) {
482
- return;
483
- }
484
- for (const handler of handlers) {
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 ${topic}: ${error.message}`);
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
  });