@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/README.md +42 -0
- package/config.schema.json +67 -71
- package/dist/Accesories/FrontGateFsm.d.ts +67 -67
- package/dist/Accesories/FrontGateFsm.d.ts.map +1 -1
- package/dist/Accesories/FrontGateFsm.js +499 -399
- package/dist/Accesories/FrontGateFsm.js.map +1 -1
- package/dist/Accesories/GateAccessory.d.ts +6 -2
- package/dist/Accesories/GateAccessory.d.ts.map +1 -1
- package/dist/Accesories/GateAccessory.js +127 -80
- package/dist/Accesories/GateAccessory.js.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.js +2 -1
- package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
- package/dist/Heplers/SuplaMqttClient.d.ts +2 -2
- package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
- package/dist/Heplers/SuplaMqttClient.js +1 -0
- package/dist/Heplers/SuplaMqttClient.js.map +1 -1
- package/dist/Heplers/SuplaMqttClientContext.d.ts +2 -1
- package/dist/Heplers/SuplaMqttClientContext.d.ts.map +1 -1
- package/dist/Heplers/SuplaMqttClientContext.js +2 -1
- package/dist/Heplers/SuplaMqttClientContext.js.map +1 -1
- package/dist/platform.d.ts +34 -33
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +185 -139
- package/dist/platform.js.map +1 -1
- package/package.json +4 -3
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.
|
|
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.
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
667
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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}`);
|