@marcel2215/homebridge-supla-plugin 2.1.28 → 2.1.29

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/dist/platform.js CHANGED
@@ -74,6 +74,9 @@ class SuplaPlatform {
74
74
  this.mqttHandlers = new Map();
75
75
  this.mqttHandlerOwners = new Map();
76
76
  this.mqttWildcardHandlers = new Map();
77
+ this.mqttNoLocalTopics = new Set();
78
+ this.mqttTransportHandlers = new Map();
79
+ this.channelRegistry = new Map();
77
80
  this.ownerCleanups = new Map();
78
81
  this.mqttDesiredSubscriptions = new Set();
79
82
  this.mqttSubscriptions = new Set();
@@ -81,6 +84,7 @@ class SuplaPlatform {
81
84
  this.mqttRetryTimers = new Map();
82
85
  this.mqttRetryState = new Map();
83
86
  this.mqttRouterAttached = false;
87
+ this.mqttTransportConnected = false;
84
88
  this.log.debug('Finished initializing platform:', this.config.name);
85
89
  const configView = this.config;
86
90
  this.coveringControlMode = this.normalizeCoveringControlMode(configView.coveringControlMode);
@@ -90,9 +94,6 @@ class SuplaPlatform {
90
94
  this.coveringExecuteActionClose = (configView.coveringExecuteActionClose || 'shut').toString();
91
95
  this.coveringExecuteActionStop = (configView.coveringExecuteActionStop || 'stop').toString();
92
96
  this.coveringTravelTimeSeconds = Number(configView.coveringTravelTimeSeconds) || 0;
93
- this.gateControlMode = this.normalizeGateControlMode(configView.gateControlMode);
94
- this.gateExecuteActionOpen = (configView.gateExecuteActionOpen || 'open_close').toString();
95
- this.gateExecuteActionClose = (configView.gateExecuteActionClose || 'open_close').toString();
96
97
  this.gateExecuteActionToggle = (configView.gateExecuteActionToggle || 'open_close').toString();
97
98
  this.gateLockControlMode = this.normalizeGateLockControlMode(configView.gateLockControlMode);
98
99
  this.gateLockExecuteAction = (configView.gateLockExecuteAction || 'open').toString();
@@ -100,18 +101,15 @@ class SuplaPlatform {
100
101
  this.gateLockPulseSeconds = Number(configView.gateLockPulseSeconds) || 0;
101
102
  this.gateLockSetOnPayload = (configView.gateLockSetOnPayload ?? 'true').toString();
102
103
  this.gateLockSetOffPayload = (configView.gateLockSetOffPayload ?? 'false').toString();
103
- this.gatePartialHiMode = this.normalizeGatePartialHiMode(configView.gatePartialHiMode);
104
- this.gateReverseFollowUpDelayMs = this.normalizeGateReverseFollowUpDelayMs(configView.gateReverseFollowUpDelayMs);
105
- this.gateOpenAssumeDelayMs = this.normalizeGateOpenAssumeDelayMs(configView.gateOpenAssumeDelayMs);
106
- this.gateCommandCooldownMs = this.normalizeGateCommandCooldownMs(configView.gateCommandCooldownMs);
107
- this.gatePublishRetryDelayMs = this.normalizeGatePublishRetryDelayMs(configView.gatePublishRetryDelayMs);
108
- this.gateStrictReverseDoublePulse = this.parseBoolean(configView.gateStrictReverseDoublePulse ?? true);
109
- this.gateDebugTimeline = this.parseBoolean(configView.gateDebugTimeline ?? false);
110
- this.frontGateFullTravelMs = this.normalizeFrontGateFullTravelMs(configView.frontGateFullTravelMs ?? configView.gateOpenAssumeDelayMs);
104
+ this.frontGateFullTravelMs = this.normalizeFrontGateFullTravelMs(configView.frontGateFullTravelMs);
111
105
  this.frontGateReversePauseMs = this.normalizeFrontGateReversePauseMs(configView.frontGateReversePauseMs);
112
- this.frontGateWrongDirectionRunMs = this.normalizeFrontGateWrongDirectionRunMs(configView.frontGateWrongDirectionRunMs);
113
- this.frontGateMinimumPulseGapMs = this.normalizeFrontGateMinimumPulseGapMs(configView.frontGateMinimumPulseGapMs ?? configView.gateCommandCooldownMs);
114
- this.frontGateCloseRetryLimit = this.normalizeFrontGateCloseRetryLimit(configView.frontGateCloseRetryLimit);
106
+ this.frontGateMinimumPulseGapMs = this.normalizeFrontGateMinimumPulseGapMs(configView.frontGateMinimumPulseGapMs);
107
+ this.frontGateUnknownOpenPolicy = this.normalizeFrontGateUnknownOpenPolicy(configView.frontGateUnknownOpenPolicy);
108
+ this.frontGateUnknownClosePolicy = this.normalizeFrontGateUnknownClosePolicy(configView.frontGateUnknownClosePolicy);
109
+ this.frontGateSeekClosedMaxPulses = this.normalizeFrontGateSeekClosedMaxPulses(configView.frontGateSeekClosedMaxPulses);
110
+ this.frontGateAssumeOpenAfterTravel = this.parseBoolean(configView.frontGateAssumeOpenAfterTravel ?? false);
111
+ this.frontGateSensorFallbackToControlChannel = this.parseBoolean(configView.frontGateSensorFallbackToControlChannel ?? false);
112
+ this.mqttProtocolVersion = this.normalizeMqttProtocolVersion(configView.mqttProtocolVersion);
115
113
  this.commandQos = this.normalizeCommandQos(configView.commandQos);
116
114
  this.commandRetain = this.parseBoolean(configView.commandRetain ?? false);
117
115
  this.api.on('shutdown', () => {
@@ -126,18 +124,23 @@ class SuplaPlatform {
126
124
  this.MqttClient = new SuplaMqttClient_1.SuplaMqttClient(mqttSettings, this.log);
127
125
  this.startMqttRouter();
128
126
  this.MqttClient.client.on('connect', () => {
127
+ this.updateMqttTransportState(true);
129
128
  this.resubscribeAll(true);
130
129
  });
131
130
  this.MqttClient.client.on('close', () => {
131
+ this.updateMqttTransportState(false);
132
132
  this.clearActiveSubscriptions();
133
133
  });
134
134
  this.MqttClient.client.on('offline', () => {
135
+ this.updateMqttTransportState(false);
135
136
  this.clearActiveSubscriptions();
136
137
  });
137
138
  this.MqttClient.client.on('end', () => {
139
+ this.updateMqttTransportState(false);
138
140
  this.clearActiveSubscriptions();
139
141
  });
140
142
  if (this.MqttClient.client.connected) {
143
+ this.updateMqttTransportState(true);
141
144
  this.resubscribeAll(true);
142
145
  }
143
146
  this.discoverDevices();
@@ -166,9 +169,11 @@ class SuplaPlatform {
166
169
  }
167
170
  const normalized = this.normalizeChannelContext(cachedDevice);
168
171
  accessory.context.device = normalized;
169
- accessory.context.deviceSignature = this.getChannelSignature(normalized);
170
- const configured = this.setupAccessory(normalized, accessory);
171
- accessory.context.deviceConfigured = configured;
172
+ this.addChannelToRegistry(normalized);
173
+ // Accessory construction is intentionally deferred until didFinishLaunching.
174
+ // Homebridge restores cached accessories one at a time, so configuring a gate
175
+ // here could pair it before its separate contact-sensor accessory is known.
176
+ accessory.context.deviceConfigured = false;
172
177
  }
173
178
  /**
174
179
  * This is an example method showing how to register discovered accessories.
@@ -177,7 +182,15 @@ class SuplaPlatform {
177
182
  */
178
183
  discoverDevices(channelsOverride) {
179
184
  const rawChannels = channelsOverride ?? this.loadChannelsFromConfig();
180
- const channels = rawChannels.map(channel => this.normalizeChannelContext(channel));
185
+ let channels = rawChannels.map(channel => this.normalizeChannelContext(channel));
186
+ if (channels.length === 0 && channelsOverride === undefined) {
187
+ channels = this.accessories
188
+ .map(accessory => accessory.context.device)
189
+ .filter((channel) => Boolean(channel))
190
+ .map(channel => this.normalizeChannelContext(channel));
191
+ }
192
+ channels = this.deduplicateChannels(channels);
193
+ this.replaceChannelRegistry(channels);
181
194
  this.log.info('Channels discovered:', channels.length);
182
195
  this.log.debug(`Discovery mode: ${channelsOverride ? 'live' : 'cached'} channels`);
183
196
  const channelUuids = new Set();
@@ -279,6 +292,7 @@ class SuplaPlatform {
279
292
  return;
280
293
  }
281
294
  platformConfig.channels = JSON.stringify(channels);
295
+ this.config.channels = platformConfig.channels;
282
296
  const payload = JSON.stringify(config, null, 2);
283
297
  const tempPath = `${configPath}.tmp`;
284
298
  fs.writeFileSync(tempPath, payload);
@@ -327,14 +341,61 @@ class SuplaPlatform {
327
341
  return this.api.hap.uuid.generate(key);
328
342
  }
329
343
  getChannelSignature(channel) {
330
- return [
344
+ const signature = [
331
345
  channel.topic ?? '',
332
346
  channel.channelFunction ?? '',
333
347
  channel.channelType ?? '',
334
348
  channel.deviceId ?? '',
335
349
  channel.channelId ?? '',
350
+ ];
351
+ if (channel.channelFunction === 'CONTROLLINGTHEGATE') {
352
+ const configView = this.config;
353
+ const sensorRegistrySignature = Array.from(this.channelRegistry.values())
354
+ .filter(candidate => {
355
+ const functionName = (candidate.channelFunction ?? '').toUpperCase();
356
+ const typeName = (candidate.channelType ?? '').toUpperCase();
357
+ return functionName.startsWith('OPENINGSENSOR_') || typeName === 'BINARYSENSOR';
358
+ })
359
+ .map(candidate => [
360
+ candidate.topic,
361
+ candidate.channelFunction,
362
+ candidate.channelType,
363
+ candidate.deviceId,
364
+ candidate.channelId,
365
+ candidate.channelCaption,
366
+ ].join(':'))
367
+ .sort()
368
+ .join(';');
369
+ signature.push(configView.frontGateSensorTopic ?? '', String(configView.frontGateSensorDeviceId ?? ''), String(configView.frontGateSensorChannelId ?? ''), String(this.frontGateSensorFallbackToControlChannel), sensorRegistrySignature);
370
+ }
371
+ return signature.join('|');
372
+ }
373
+ getChannelRegistryKey(channel) {
374
+ return [
375
+ channel.deviceId ?? '',
376
+ channel.channelId ?? '',
377
+ this.normalizeTopicBase(channel.topic),
336
378
  ].join('|');
337
379
  }
380
+ addChannelToRegistry(channel) {
381
+ this.channelRegistry.set(this.getChannelRegistryKey(channel), channel);
382
+ }
383
+ replaceChannelRegistry(channels) {
384
+ this.channelRegistry.clear();
385
+ for (const channel of channels) {
386
+ this.addChannelToRegistry(channel);
387
+ }
388
+ }
389
+ deduplicateChannels(channels) {
390
+ const byKey = new Map();
391
+ for (const channel of channels) {
392
+ byKey.set(this.getChannelRegistryKey(channel), channel);
393
+ }
394
+ return Array.from(byKey.values());
395
+ }
396
+ getKnownChannels() {
397
+ return Array.from(this.channelRegistry.values());
398
+ }
338
399
  resetAccessoryServices(accessory) {
339
400
  const keepUuid = this.Service.AccessoryInformation.UUID;
340
401
  for (const service of accessory.services) {
@@ -479,15 +540,6 @@ class SuplaPlatform {
479
540
  getCoveringTravelTimeSeconds() {
480
541
  return this.coveringTravelTimeSeconds;
481
542
  }
482
- getGateControlMode() {
483
- return this.gateControlMode;
484
- }
485
- getGateExecuteActionOpen() {
486
- return this.gateExecuteActionOpen;
487
- }
488
- getGateExecuteActionClose() {
489
- return this.gateExecuteActionClose;
490
- }
491
543
  getGateExecuteActionToggle() {
492
544
  return this.gateExecuteActionToggle;
493
545
  }
@@ -509,50 +561,25 @@ class SuplaPlatform {
509
561
  getGateLockSetOffPayload() {
510
562
  return this.gateLockSetOffPayload;
511
563
  }
512
- getGatePartialHiMode() {
513
- return this.gatePartialHiMode;
514
- }
515
- getGateReverseFollowUpDelayMs() {
516
- return this.gateReverseFollowUpDelayMs;
517
- }
518
- getGateOpenAssumeDelayMs() {
519
- return this.gateOpenAssumeDelayMs;
520
- }
521
- getGateCommandCooldownMs() {
522
- return this.gateCommandCooldownMs;
523
- }
524
- getGatePublishRetryDelayMs() {
525
- return this.gatePublishRetryDelayMs;
526
- }
527
- getGateStrictReverseDoublePulse() {
528
- return this.gateStrictReverseDoublePulse;
529
- }
530
- getGateDebugTimeline() {
531
- return this.gateDebugTimeline;
532
- }
533
- getFrontGateTimings() {
564
+ getFrontGateConfig() {
534
565
  return {
535
566
  fullTravelMs: this.frontGateFullTravelMs,
536
567
  reversePauseMs: this.frontGateReversePauseMs,
537
- wrongDirectionRunMs: this.frontGateWrongDirectionRunMs,
538
568
  minimumPulseGapMs: this.frontGateMinimumPulseGapMs,
539
- closeRetryLimit: this.frontGateCloseRetryLimit,
569
+ unknownOpenPolicy: this.frontGateUnknownOpenPolicy,
570
+ unknownClosePolicy: this.frontGateUnknownClosePolicy,
571
+ seekClosedMaxPulses: this.frontGateSeekClosedMaxPulses,
572
+ assumeOpenAfterTravel: this.frontGateAssumeOpenAfterTravel,
540
573
  };
541
574
  }
575
+ shouldFallbackFrontGateSensorToControlChannel() {
576
+ return this.frontGateSensorFallbackToControlChannel;
577
+ }
578
+ isMqttNoLocalAvailable() {
579
+ return this.mqttProtocolVersion === 5;
580
+ }
542
581
  getFrontGatePulseAction() {
543
- const toggleAction = this.gateExecuteActionToggle.trim();
544
- if (toggleAction) {
545
- return toggleAction;
546
- }
547
- const openAction = this.gateExecuteActionOpen.trim();
548
- const closeAction = this.gateExecuteActionClose.trim();
549
- if (openAction && openAction === closeAction) {
550
- return openAction;
551
- }
552
- if (openAction) {
553
- return openAction;
554
- }
555
- return closeAction;
582
+ return this.gateExecuteActionToggle.trim();
556
583
  }
557
584
  normalizeCoveringControlMode(value) {
558
585
  const normalized = (value ?? 'set').toString().toLowerCase();
@@ -564,13 +591,6 @@ class SuplaPlatform {
564
591
  }
565
592
  return 'set';
566
593
  }
567
- normalizeGateControlMode(value) {
568
- const normalized = (value ?? 'execute_action').toString().toLowerCase();
569
- if (normalized === 'toggle') {
570
- return 'toggle';
571
- }
572
- return 'execute_action';
573
- }
574
594
  normalizeGateLockControlMode(value) {
575
595
  const normalized = (value ?? 'execute_action').toString().toLowerCase();
576
596
  if (normalized === 'set_on_pulse') {
@@ -578,55 +598,6 @@ class SuplaPlatform {
578
598
  }
579
599
  return 'execute_action';
580
600
  }
581
- normalizeGatePartialHiMode(value) {
582
- const normalized = (value ?? 'moving').toString().toLowerCase();
583
- if (normalized === 'open_endstop' || normalized === 'open') {
584
- return 'open_endstop';
585
- }
586
- if (normalized === 'pedestrian_endstop' || normalized === 'pedestrian') {
587
- return 'pedestrian_endstop';
588
- }
589
- if (normalized === 'ignore' || normalized === 'absent') {
590
- return 'ignore';
591
- }
592
- return 'moving';
593
- }
594
- normalizeGateReverseFollowUpDelayMs(value) {
595
- const fallbackMs = 3000;
596
- const parsed = Number(value);
597
- if (!Number.isFinite(parsed)) {
598
- return fallbackMs;
599
- }
600
- const rounded = Math.round(parsed);
601
- return Math.min(10000, Math.max(250, rounded));
602
- }
603
- normalizeGateOpenAssumeDelayMs(value) {
604
- const fallbackMs = 22000;
605
- const parsed = Number(value);
606
- if (!Number.isFinite(parsed)) {
607
- return fallbackMs;
608
- }
609
- const rounded = Math.round(parsed);
610
- return Math.min(120000, Math.max(1000, rounded));
611
- }
612
- normalizeGateCommandCooldownMs(value) {
613
- const fallbackMs = 700;
614
- const parsed = Number(value);
615
- if (!Number.isFinite(parsed)) {
616
- return fallbackMs;
617
- }
618
- const rounded = Math.round(parsed);
619
- return Math.min(15000, Math.max(3000, rounded));
620
- }
621
- normalizeGatePublishRetryDelayMs(value) {
622
- const fallbackMs = 300;
623
- const parsed = Number(value);
624
- if (!Number.isFinite(parsed)) {
625
- return fallbackMs;
626
- }
627
- const rounded = Math.round(parsed);
628
- return Math.min(5000, Math.max(0, rounded));
629
- }
630
601
  normalizeFrontGateFullTravelMs(value) {
631
602
  const fallbackMs = 25000;
632
603
  const parsed = Number(value);
@@ -645,15 +616,6 @@ class SuplaPlatform {
645
616
  const rounded = Math.round(parsed);
646
617
  return Math.min(15000, Math.max(3000, rounded));
647
618
  }
648
- normalizeFrontGateWrongDirectionRunMs(value) {
649
- const fallbackMs = 700;
650
- const parsed = Number(value);
651
- if (!Number.isFinite(parsed)) {
652
- return fallbackMs;
653
- }
654
- const rounded = Math.round(parsed);
655
- return Math.min(5000, Math.max(100, rounded));
656
- }
657
619
  normalizeFrontGateMinimumPulseGapMs(value) {
658
620
  const fallbackMs = 3000;
659
621
  const parsed = Number(value);
@@ -661,16 +623,28 @@ class SuplaPlatform {
661
623
  return fallbackMs;
662
624
  }
663
625
  const rounded = Math.round(parsed);
664
- return Math.min(10000, Math.max(0, rounded));
626
+ return Math.min(15000, Math.max(0, rounded));
627
+ }
628
+ normalizeFrontGateUnknownOpenPolicy(value) {
629
+ return value === 'accept_non_closed' ? 'accept_non_closed' : 'reject';
665
630
  }
666
- normalizeFrontGateCloseRetryLimit(value) {
667
- const fallbackValue = 0;
631
+ normalizeFrontGateUnknownClosePolicy(value) {
632
+ if (value === 'single_pulse_best_effort' || value === 'seek_closed') {
633
+ return value;
634
+ }
635
+ return 'reject';
636
+ }
637
+ normalizeFrontGateSeekClosedMaxPulses(value) {
638
+ const fallbackValue = 3;
668
639
  const parsed = Number(value);
669
640
  if (!Number.isFinite(parsed)) {
670
641
  return fallbackValue;
671
642
  }
672
643
  const rounded = Math.round(parsed);
673
- return Math.min(3, Math.max(0, rounded));
644
+ return Math.min(3, Math.max(1, rounded));
645
+ }
646
+ normalizeMqttProtocolVersion(value) {
647
+ return Number(value) === 5 ? 5 : 4;
674
648
  }
675
649
  normalizeTopicSuffix(value) {
676
650
  return value.toString().replace(/^\/+/, '');
@@ -686,7 +660,7 @@ class SuplaPlatform {
686
660
  base = base.replace(/\/set\/.+$/, '');
687
661
  return base.replace(/\/+$/, '');
688
662
  }
689
- registerMqttHandler(topic, handler, ownerId) {
663
+ registerMqttHandler(topic, handler, ownerId, options = {}) {
690
664
  if (!topic) {
691
665
  return () => undefined;
692
666
  }
@@ -701,6 +675,9 @@ class SuplaPlatform {
701
675
  ownerTopics.set(topic, ownerHandlers);
702
676
  this.mqttHandlerOwners.set(ownerId, ownerTopics);
703
677
  this.mqttDesiredSubscriptions.add(topic);
678
+ if (options.noLocal && this.mqttProtocolVersion === 5) {
679
+ this.mqttNoLocalTopics.add(topic);
680
+ }
704
681
  this.ensureSubscribed(topic, false);
705
682
  return () => {
706
683
  this.removeHandler(topic, handler);
@@ -717,6 +694,21 @@ class SuplaPlatform {
717
694
  }
718
695
  };
719
696
  }
697
+ registerMqttTransportHandler(handler, ownerId) {
698
+ const handlers = this.mqttTransportHandlers.get(ownerId) ?? new Set();
699
+ handlers.add(handler);
700
+ this.mqttTransportHandlers.set(ownerId, handlers);
701
+ handler(this.mqttTransportConnected);
702
+ const cleanup = () => {
703
+ const active = this.mqttTransportHandlers.get(ownerId);
704
+ active?.delete(handler);
705
+ if (active?.size === 0) {
706
+ this.mqttTransportHandlers.delete(ownerId);
707
+ }
708
+ };
709
+ this.registerOwnerCleanup(ownerId, cleanup);
710
+ return cleanup;
711
+ }
720
712
  registerOwnerCleanup(ownerId, cleanup) {
721
713
  const cleanups = this.ownerCleanups.get(ownerId) ?? new Set();
722
714
  cleanups.add(cleanup);
@@ -758,6 +750,8 @@ class SuplaPlatform {
758
750
  }
759
751
  this.mqttHandlers.clear();
760
752
  this.mqttWildcardHandlers.clear();
753
+ this.mqttNoLocalTopics.clear();
754
+ this.mqttTransportHandlers.clear();
761
755
  this.mqttDesiredSubscriptions.clear();
762
756
  this.mqttSubscriptions.clear();
763
757
  this.mqttPendingSubscriptions.clear();
@@ -765,6 +759,12 @@ class SuplaPlatform {
765
759
  this.ownerCleanups.clear();
766
760
  }
767
761
  publishCommand(topic, payload, callback) {
762
+ this.publishMqtt(topic, payload, { qos: this.commandQos, retain: this.commandRetain }, callback);
763
+ }
764
+ publishGateAction(topic, payload, callback) {
765
+ this.publishMqtt(topic, payload, { qos: 0, retain: false }, callback);
766
+ }
767
+ publishMqtt(topic, payload, options, callback) {
768
768
  const client = this.MqttClient?.client;
769
769
  if (!client || !client.connected) {
770
770
  const error = new Error('MQTT not connected');
@@ -774,7 +774,7 @@ class SuplaPlatform {
774
774
  }
775
775
  return;
776
776
  }
777
- client.publish(topic, payload, { qos: this.commandQos, retain: this.commandRetain }, (error) => {
777
+ client.publish(topic, payload, options, (error) => {
778
778
  if (callback) {
779
779
  callback(error);
780
780
  return;
@@ -800,6 +800,31 @@ class SuplaPlatform {
800
800
  }
801
801
  return false;
802
802
  }
803
+ parseBooleanStrict(value) {
804
+ if (typeof value === 'boolean') {
805
+ return value;
806
+ }
807
+ if (typeof value === 'number') {
808
+ if (value === 1) {
809
+ return true;
810
+ }
811
+ if (value === 0) {
812
+ return false;
813
+ }
814
+ return undefined;
815
+ }
816
+ if (typeof value !== 'string') {
817
+ return undefined;
818
+ }
819
+ const normalized = value.trim().toLowerCase();
820
+ if (['1', 'true', 'on', 'yes'].includes(normalized)) {
821
+ return true;
822
+ }
823
+ if (['0', 'false', 'off', 'no'].includes(normalized)) {
824
+ return false;
825
+ }
826
+ return undefined;
827
+ }
803
828
  runOwnerCleanup(ownerId) {
804
829
  const cleanups = this.ownerCleanups.get(ownerId);
805
830
  if (!cleanups) {
@@ -815,6 +840,22 @@ class SuplaPlatform {
815
840
  }
816
841
  this.ownerCleanups.delete(ownerId);
817
842
  }
843
+ updateMqttTransportState(connected) {
844
+ if (this.mqttTransportConnected === connected) {
845
+ return;
846
+ }
847
+ this.mqttTransportConnected = connected;
848
+ for (const handlers of this.mqttTransportHandlers.values()) {
849
+ for (const handler of handlers) {
850
+ try {
851
+ handler(connected);
852
+ }
853
+ catch (error) {
854
+ this.log.error(`MQTT transport handler failed: ${error.message}`);
855
+ }
856
+ }
857
+ }
858
+ }
818
859
  clearActiveSubscriptions() {
819
860
  if (this.mqttSubscriptions.size > 0 || this.mqttPendingSubscriptions.size > 0) {
820
861
  this.log.debug('MQTT connection lost; clearing active subscriptions.');
@@ -930,7 +971,11 @@ class SuplaPlatform {
930
971
  return;
931
972
  }
932
973
  this.mqttPendingSubscriptions.add(topic);
933
- this.MqttClient.client.subscribe(topic, (err, granted) => {
974
+ const subscribeOptions = {
975
+ qos: 0,
976
+ ...(this.mqttNoLocalTopics.has(topic) && this.mqttProtocolVersion === 5 ? { nl: true } : {}),
977
+ };
978
+ this.MqttClient.client.subscribe(topic, subscribeOptions, (err, granted) => {
934
979
  this.mqttPendingSubscriptions.delete(topic);
935
980
  if (err) {
936
981
  this.logSubscriptionIssue(topic, `MQTT subscribe failed for ${topic}: ${err.message}`);
@@ -976,6 +1021,7 @@ class SuplaPlatform {
976
1021
  }
977
1022
  removeSubscription(topic) {
978
1023
  this.mqttDesiredSubscriptions.delete(topic);
1024
+ this.mqttNoLocalTopics.delete(topic);
979
1025
  this.mqttPendingSubscriptions.delete(topic);
980
1026
  this.clearSubscriptionRetry(topic);
981
1027
  if (!this.MqttClient) {
@@ -1041,7 +1087,7 @@ class SuplaPlatform {
1041
1087
  }
1042
1088
  this.mqttRouterAttached = true;
1043
1089
  this.resubscribeAll(false);
1044
- this.MqttClient.client.on('message', (topic, message) => {
1090
+ this.MqttClient.client.on('message', (topic, message, packet) => {
1045
1091
  const dispatched = new Set();
1046
1092
  const dispatch = (handler, label) => {
1047
1093
  if (dispatched.has(handler)) {
@@ -1049,7 +1095,7 @@ class SuplaPlatform {
1049
1095
  }
1050
1096
  dispatched.add(handler);
1051
1097
  try {
1052
- handler(message, topic);
1098
+ handler(message, topic, packet);
1053
1099
  }
1054
1100
  catch (error) {
1055
1101
  this.log.error(`MQTT handler error for ${label}: ${error.message}`);