@marcel2215/homebridge-supla-plugin 2.1.27 → 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.
Files changed (49) hide show
  1. package/README.md +43 -0
  2. package/config.schema.json +67 -71
  3. package/dist/Accesories/ActionTriggerAccessory.js.map +1 -1
  4. package/dist/Accesories/AirQualityAccessory.js +1 -2
  5. package/dist/Accesories/AirQualityAccessory.js.map +1 -1
  6. package/dist/Accesories/DimmerAccessory.js.map +1 -1
  7. package/dist/Accesories/DimmerRgbLightAccessory.js.map +1 -1
  8. package/dist/Accesories/DoorAccessory.js.map +1 -1
  9. package/dist/Accesories/ElectricityMeterAccessory.js.map +1 -1
  10. package/dist/Accesories/FacadeBlindAccessory.js +1 -1
  11. package/dist/Accesories/FacadeBlindAccessory.js.map +1 -1
  12. package/dist/Accesories/FrontGateFsm.d.ts +67 -67
  13. package/dist/Accesories/FrontGateFsm.d.ts.map +1 -1
  14. package/dist/Accesories/FrontGateFsm.js +499 -399
  15. package/dist/Accesories/FrontGateFsm.js.map +1 -1
  16. package/dist/Accesories/GarageDoorOpenerAccesory.js.map +1 -1
  17. package/dist/Accesories/GateAccessory.d.ts +6 -2
  18. package/dist/Accesories/GateAccessory.d.ts.map +1 -1
  19. package/dist/Accesories/GateAccessory.js +127 -82
  20. package/dist/Accesories/GateAccessory.js.map +1 -1
  21. package/dist/Accesories/GateLockAccessory.js.map +1 -1
  22. package/dist/Accesories/LightBulbAccesory.js.map +1 -1
  23. package/dist/Accesories/PressureAccessory.js.map +1 -1
  24. package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
  25. package/dist/Accesories/RollerShutterAccessory.js +6 -5
  26. package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
  27. package/dist/Accesories/SwitchAccessory.js.map +1 -1
  28. package/dist/Accesories/TemperatureAccessory.js.map +1 -1
  29. package/dist/Accesories/TemperatureHumidityAccessory.js.map +1 -1
  30. package/dist/Accesories/ThermostatAccessory.js +2 -2
  31. package/dist/Accesories/ThermostatAccessory.js.map +1 -1
  32. package/dist/Heplers/ColorConverters.js +4 -5
  33. package/dist/Heplers/ColorConverters.js.map +1 -1
  34. package/dist/Heplers/SuplaChannelContext.d.ts.map +1 -1
  35. package/dist/Heplers/SuplaMqttClient.d.ts +2 -3
  36. package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
  37. package/dist/Heplers/SuplaMqttClient.js +34 -28
  38. package/dist/Heplers/SuplaMqttClient.js.map +1 -1
  39. package/dist/Heplers/SuplaMqttClientContext.d.ts +5 -4
  40. package/dist/Heplers/SuplaMqttClientContext.d.ts.map +1 -1
  41. package/dist/Heplers/SuplaMqttClientContext.js +2 -1
  42. package/dist/Heplers/SuplaMqttClientContext.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/platform.d.ts +34 -34
  45. package/dist/platform.d.ts.map +1 -1
  46. package/dist/platform.js +228 -183
  47. package/dist/platform.js.map +1 -1
  48. package/package.json +9 -8
  49. package/.codex/environments/environment.toml +0 -31
package/dist/platform.js CHANGED
@@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  Object.defineProperty(exports, "__esModule", { value: true });
26
36
  exports.SuplaPlatform = void 0;
27
37
  const settings_1 = require("./settings");
@@ -55,7 +65,6 @@ const ActionTriggerAccessory_1 = require("./Accesories/ActionTriggerAccessory");
55
65
  */
56
66
  class SuplaPlatform {
57
67
  constructor(log, config, api) {
58
- var _a, _b, _c, _d, _e, _f, _g;
59
68
  this.log = log;
60
69
  this.config = config;
61
70
  this.api = api;
@@ -65,6 +74,9 @@ class SuplaPlatform {
65
74
  this.mqttHandlers = new Map();
66
75
  this.mqttHandlerOwners = new Map();
67
76
  this.mqttWildcardHandlers = new Map();
77
+ this.mqttNoLocalTopics = new Set();
78
+ this.mqttTransportHandlers = new Map();
79
+ this.channelRegistry = new Map();
68
80
  this.ownerCleanups = new Map();
69
81
  this.mqttDesiredSubscriptions = new Set();
70
82
  this.mqttSubscriptions = new Set();
@@ -72,6 +84,7 @@ class SuplaPlatform {
72
84
  this.mqttRetryTimers = new Map();
73
85
  this.mqttRetryState = new Map();
74
86
  this.mqttRouterAttached = false;
87
+ this.mqttTransportConnected = false;
75
88
  this.log.debug('Finished initializing platform:', this.config.name);
76
89
  const configView = this.config;
77
90
  this.coveringControlMode = this.normalizeCoveringControlMode(configView.coveringControlMode);
@@ -81,30 +94,24 @@ class SuplaPlatform {
81
94
  this.coveringExecuteActionClose = (configView.coveringExecuteActionClose || 'shut').toString();
82
95
  this.coveringExecuteActionStop = (configView.coveringExecuteActionStop || 'stop').toString();
83
96
  this.coveringTravelTimeSeconds = Number(configView.coveringTravelTimeSeconds) || 0;
84
- this.gateControlMode = this.normalizeGateControlMode(configView.gateControlMode);
85
- this.gateExecuteActionOpen = (configView.gateExecuteActionOpen || 'open_close').toString();
86
- this.gateExecuteActionClose = (configView.gateExecuteActionClose || 'open_close').toString();
87
97
  this.gateExecuteActionToggle = (configView.gateExecuteActionToggle || 'open_close').toString();
88
98
  this.gateLockControlMode = this.normalizeGateLockControlMode(configView.gateLockControlMode);
89
99
  this.gateLockExecuteAction = (configView.gateLockExecuteAction || 'open').toString();
90
100
  this.gateLockSetTopicSuffix = this.normalizeTopicSuffix(configView.gateLockSetTopicSuffix || 'set/on');
91
101
  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.gatePartialHiMode = this.normalizeGatePartialHiMode(configView.gatePartialHiMode);
95
- this.gateReverseFollowUpDelayMs = this.normalizeGateReverseFollowUpDelayMs(configView.gateReverseFollowUpDelayMs);
96
- this.gateOpenAssumeDelayMs = this.normalizeGateOpenAssumeDelayMs(configView.gateOpenAssumeDelayMs);
97
- this.gateCommandCooldownMs = this.normalizeGateCommandCooldownMs(configView.gateCommandCooldownMs);
98
- this.gatePublishRetryDelayMs = this.normalizeGatePublishRetryDelayMs(configView.gatePublishRetryDelayMs);
99
- this.gateStrictReverseDoublePulse = this.parseBoolean((_c = configView.gateStrictReverseDoublePulse) !== null && _c !== void 0 ? _c : true);
100
- this.gateDebugTimeline = this.parseBoolean((_d = configView.gateDebugTimeline) !== null && _d !== void 0 ? _d : false);
101
- this.frontGateFullTravelMs = this.normalizeFrontGateFullTravelMs((_e = configView.frontGateFullTravelMs) !== null && _e !== void 0 ? _e : configView.gateOpenAssumeDelayMs);
102
+ this.gateLockSetOnPayload = (configView.gateLockSetOnPayload ?? 'true').toString();
103
+ this.gateLockSetOffPayload = (configView.gateLockSetOffPayload ?? 'false').toString();
104
+ this.frontGateFullTravelMs = this.normalizeFrontGateFullTravelMs(configView.frontGateFullTravelMs);
102
105
  this.frontGateReversePauseMs = this.normalizeFrontGateReversePauseMs(configView.frontGateReversePauseMs);
103
- this.frontGateWrongDirectionRunMs = this.normalizeFrontGateWrongDirectionRunMs(configView.frontGateWrongDirectionRunMs);
104
- this.frontGateMinimumPulseGapMs = this.normalizeFrontGateMinimumPulseGapMs((_f = configView.frontGateMinimumPulseGapMs) !== null && _f !== void 0 ? _f : configView.gateCommandCooldownMs);
105
- 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);
106
113
  this.commandQos = this.normalizeCommandQos(configView.commandQos);
107
- this.commandRetain = this.parseBoolean((_g = configView.commandRetain) !== null && _g !== void 0 ? _g : false);
114
+ this.commandRetain = this.parseBoolean(configView.commandRetain ?? false);
108
115
  this.api.on('shutdown', () => {
109
116
  this.unregisterAllMqttHandlers();
110
117
  if (this.MqttClient) {
@@ -117,18 +124,23 @@ class SuplaPlatform {
117
124
  this.MqttClient = new SuplaMqttClient_1.SuplaMqttClient(mqttSettings, this.log);
118
125
  this.startMqttRouter();
119
126
  this.MqttClient.client.on('connect', () => {
127
+ this.updateMqttTransportState(true);
120
128
  this.resubscribeAll(true);
121
129
  });
122
130
  this.MqttClient.client.on('close', () => {
131
+ this.updateMqttTransportState(false);
123
132
  this.clearActiveSubscriptions();
124
133
  });
125
134
  this.MqttClient.client.on('offline', () => {
135
+ this.updateMqttTransportState(false);
126
136
  this.clearActiveSubscriptions();
127
137
  });
128
138
  this.MqttClient.client.on('end', () => {
139
+ this.updateMqttTransportState(false);
129
140
  this.clearActiveSubscriptions();
130
141
  });
131
142
  if (this.MqttClient.client.connected) {
143
+ this.updateMqttTransportState(true);
132
144
  this.resubscribeAll(true);
133
145
  }
134
146
  this.discoverDevices();
@@ -157,9 +169,11 @@ class SuplaPlatform {
157
169
  }
158
170
  const normalized = this.normalizeChannelContext(cachedDevice);
159
171
  accessory.context.device = normalized;
160
- accessory.context.deviceSignature = this.getChannelSignature(normalized);
161
- const configured = this.setupAccessory(normalized, accessory);
162
- 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;
163
177
  }
164
178
  /**
165
179
  * This is an example method showing how to register discovered accessories.
@@ -167,9 +181,16 @@ class SuplaPlatform {
167
181
  * must not be registered again to prevent "duplicate UUID" errors.
168
182
  */
169
183
  discoverDevices(channelsOverride) {
170
- var _a;
171
- const rawChannels = channelsOverride !== null && channelsOverride !== void 0 ? channelsOverride : this.loadChannelsFromConfig();
172
- const channels = rawChannels.map(channel => this.normalizeChannelContext(channel));
184
+ const rawChannels = channelsOverride ?? this.loadChannelsFromConfig();
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);
173
194
  this.log.info('Channels discovered:', channels.length);
174
195
  this.log.debug(`Discovery mode: ${channelsOverride ? 'live' : 'cached'} channels`);
175
196
  const channelUuids = new Set();
@@ -192,9 +213,9 @@ class SuplaPlatform {
192
213
  // loop over the discovered devices and register each one if it has not already been registered
193
214
  for (const channel of channels) {
194
215
  const existingByIds = findExistingByIds(channel);
195
- const uuid = (_a = existingByIds === null || existingByIds === void 0 ? void 0 : existingByIds.UUID) !== null && _a !== void 0 ? _a : this.getChannelUuid(channel);
216
+ const uuid = existingByIds?.UUID ?? this.getChannelUuid(channel);
196
217
  channelUuids.add(uuid);
197
- const existingAccessory = existingByIds !== null && existingByIds !== void 0 ? existingByIds : this.accessories.find(accessory => accessory.UUID === uuid);
218
+ const existingAccessory = existingByIds ?? this.accessories.find(accessory => accessory.UUID === uuid);
198
219
  if (existingAccessory) {
199
220
  this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
200
221
  const signature = this.getChannelSignature(channel);
@@ -262,16 +283,16 @@ class SuplaPlatform {
262
283
  return [];
263
284
  }
264
285
  persistChannels(channels) {
265
- var _a;
266
286
  try {
267
287
  const configPath = this.api.user.configPath();
268
288
  const config = JSON.parse(fs.readFileSync(configPath).toString());
269
- const platformConfig = (_a = config.platforms) === null || _a === void 0 ? void 0 : _a.find((platform) => platform.platform === settings_1.PLATFORM_NAME);
289
+ const platformConfig = config.platforms?.find((platform) => platform.platform === settings_1.PLATFORM_NAME);
270
290
  if (!platformConfig) {
271
291
  this.log.warn(`Failed to save channels: ${settings_1.PLATFORM_NAME} not found in config.`);
272
292
  return;
273
293
  }
274
294
  platformConfig.channels = JSON.stringify(channels);
295
+ this.config.channels = platformConfig.channels;
275
296
  const payload = JSON.stringify(config, null, 2);
276
297
  const tempPath = `${configPath}.tmp`;
277
298
  fs.writeFileSync(tempPath, payload);
@@ -283,9 +304,8 @@ class SuplaPlatform {
283
304
  }
284
305
  }
285
306
  normalizeChannelContext(channel) {
286
- var _a, _b, _c;
287
- const rawTopic = ((_b = (_a = channel.rawTopic) !== null && _a !== void 0 ? _a : channel.topic) !== null && _b !== void 0 ? _b : '').toString();
288
- const baseFromTopic = this.normalizeTopicBase(rawTopic || ((_c = channel.topic) !== null && _c !== void 0 ? _c : '').toString());
307
+ const rawTopic = (channel.rawTopic ?? channel.topic ?? '').toString();
308
+ const baseFromTopic = this.normalizeTopicBase(rawTopic || (channel.topic ?? '').toString());
289
309
  let deviceId = channel.deviceId;
290
310
  let channelId = channel.channelId;
291
311
  if (!deviceId || !channelId) {
@@ -321,15 +341,61 @@ class SuplaPlatform {
321
341
  return this.api.hap.uuid.generate(key);
322
342
  }
323
343
  getChannelSignature(channel) {
324
- var _a, _b, _c, _d, _e;
344
+ const signature = [
345
+ channel.topic ?? '',
346
+ channel.channelFunction ?? '',
347
+ channel.channelType ?? '',
348
+ channel.deviceId ?? '',
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) {
325
374
  return [
326
- (_a = channel.topic) !== null && _a !== void 0 ? _a : '',
327
- (_b = channel.channelFunction) !== null && _b !== void 0 ? _b : '',
328
- (_c = channel.channelType) !== null && _c !== void 0 ? _c : '',
329
- (_d = channel.deviceId) !== null && _d !== void 0 ? _d : '',
330
- (_e = channel.channelId) !== null && _e !== void 0 ? _e : '',
375
+ channel.deviceId ?? '',
376
+ channel.channelId ?? '',
377
+ this.normalizeTopicBase(channel.topic),
331
378
  ].join('|');
332
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
+ }
333
399
  resetAccessoryServices(accessory) {
334
400
  const keepUuid = this.Service.AccessoryInformation.UUID;
335
401
  for (const service of accessory.services) {
@@ -450,8 +516,7 @@ class SuplaPlatform {
450
516
  return false;
451
517
  }
452
518
  isLeakSensorChannel(channel) {
453
- var _a;
454
- const caption = ((_a = channel.channelCaption) !== null && _a !== void 0 ? _a : '').toLowerCase();
519
+ const caption = (channel.channelCaption ?? '').toLowerCase();
455
520
  return ['leak', 'flood', 'water', 'zalania'].some(term => caption.includes(term));
456
521
  }
457
522
  getCoveringControlMode() {
@@ -475,15 +540,6 @@ class SuplaPlatform {
475
540
  getCoveringTravelTimeSeconds() {
476
541
  return this.coveringTravelTimeSeconds;
477
542
  }
478
- getGateControlMode() {
479
- return this.gateControlMode;
480
- }
481
- getGateExecuteActionOpen() {
482
- return this.gateExecuteActionOpen;
483
- }
484
- getGateExecuteActionClose() {
485
- return this.gateExecuteActionClose;
486
- }
487
543
  getGateExecuteActionToggle() {
488
544
  return this.gateExecuteActionToggle;
489
545
  }
@@ -505,53 +561,28 @@ class SuplaPlatform {
505
561
  getGateLockSetOffPayload() {
506
562
  return this.gateLockSetOffPayload;
507
563
  }
508
- getGatePartialHiMode() {
509
- return this.gatePartialHiMode;
510
- }
511
- getGateReverseFollowUpDelayMs() {
512
- return this.gateReverseFollowUpDelayMs;
513
- }
514
- getGateOpenAssumeDelayMs() {
515
- return this.gateOpenAssumeDelayMs;
516
- }
517
- getGateCommandCooldownMs() {
518
- return this.gateCommandCooldownMs;
519
- }
520
- getGatePublishRetryDelayMs() {
521
- return this.gatePublishRetryDelayMs;
522
- }
523
- getGateStrictReverseDoublePulse() {
524
- return this.gateStrictReverseDoublePulse;
525
- }
526
- getGateDebugTimeline() {
527
- return this.gateDebugTimeline;
528
- }
529
- getFrontGateTimings() {
564
+ getFrontGateConfig() {
530
565
  return {
531
566
  fullTravelMs: this.frontGateFullTravelMs,
532
567
  reversePauseMs: this.frontGateReversePauseMs,
533
- wrongDirectionRunMs: this.frontGateWrongDirectionRunMs,
534
568
  minimumPulseGapMs: this.frontGateMinimumPulseGapMs,
535
- closeRetryLimit: this.frontGateCloseRetryLimit,
569
+ unknownOpenPolicy: this.frontGateUnknownOpenPolicy,
570
+ unknownClosePolicy: this.frontGateUnknownClosePolicy,
571
+ seekClosedMaxPulses: this.frontGateSeekClosedMaxPulses,
572
+ assumeOpenAfterTravel: this.frontGateAssumeOpenAfterTravel,
536
573
  };
537
574
  }
575
+ shouldFallbackFrontGateSensorToControlChannel() {
576
+ return this.frontGateSensorFallbackToControlChannel;
577
+ }
578
+ isMqttNoLocalAvailable() {
579
+ return this.mqttProtocolVersion === 5;
580
+ }
538
581
  getFrontGatePulseAction() {
539
- const toggleAction = this.gateExecuteActionToggle.trim();
540
- if (toggleAction) {
541
- return toggleAction;
542
- }
543
- const openAction = this.gateExecuteActionOpen.trim();
544
- const closeAction = this.gateExecuteActionClose.trim();
545
- if (openAction && openAction === closeAction) {
546
- return openAction;
547
- }
548
- if (openAction) {
549
- return openAction;
550
- }
551
- return closeAction;
582
+ return this.gateExecuteActionToggle.trim();
552
583
  }
553
584
  normalizeCoveringControlMode(value) {
554
- const normalized = (value !== null && value !== void 0 ? value : 'set').toString().toLowerCase();
585
+ const normalized = (value ?? 'set').toString().toLowerCase();
555
586
  if (normalized === 'execute_action') {
556
587
  return 'execute_action';
557
588
  }
@@ -560,69 +591,13 @@ class SuplaPlatform {
560
591
  }
561
592
  return 'set';
562
593
  }
563
- normalizeGateControlMode(value) {
564
- const normalized = (value !== null && value !== void 0 ? value : 'execute_action').toString().toLowerCase();
565
- if (normalized === 'toggle') {
566
- return 'toggle';
567
- }
568
- return 'execute_action';
569
- }
570
594
  normalizeGateLockControlMode(value) {
571
- const normalized = (value !== null && value !== void 0 ? value : 'execute_action').toString().toLowerCase();
595
+ const normalized = (value ?? 'execute_action').toString().toLowerCase();
572
596
  if (normalized === 'set_on_pulse') {
573
597
  return 'set_on_pulse';
574
598
  }
575
599
  return 'execute_action';
576
600
  }
577
- normalizeGatePartialHiMode(value) {
578
- const normalized = (value !== null && value !== void 0 ? value : 'moving').toString().toLowerCase();
579
- if (normalized === 'open_endstop' || normalized === 'open') {
580
- return 'open_endstop';
581
- }
582
- if (normalized === 'pedestrian_endstop' || normalized === 'pedestrian') {
583
- return 'pedestrian_endstop';
584
- }
585
- if (normalized === 'ignore' || normalized === 'absent') {
586
- return 'ignore';
587
- }
588
- return 'moving';
589
- }
590
- normalizeGateReverseFollowUpDelayMs(value) {
591
- const fallbackMs = 3000;
592
- const parsed = Number(value);
593
- if (!Number.isFinite(parsed)) {
594
- return fallbackMs;
595
- }
596
- const rounded = Math.round(parsed);
597
- return Math.min(10000, Math.max(250, rounded));
598
- }
599
- normalizeGateOpenAssumeDelayMs(value) {
600
- const fallbackMs = 22000;
601
- const parsed = Number(value);
602
- if (!Number.isFinite(parsed)) {
603
- return fallbackMs;
604
- }
605
- const rounded = Math.round(parsed);
606
- return Math.min(120000, Math.max(1000, rounded));
607
- }
608
- normalizeGateCommandCooldownMs(value) {
609
- const fallbackMs = 700;
610
- const parsed = Number(value);
611
- if (!Number.isFinite(parsed)) {
612
- return fallbackMs;
613
- }
614
- const rounded = Math.round(parsed);
615
- return Math.min(15000, Math.max(3000, rounded));
616
- }
617
- normalizeGatePublishRetryDelayMs(value) {
618
- const fallbackMs = 300;
619
- const parsed = Number(value);
620
- if (!Number.isFinite(parsed)) {
621
- return fallbackMs;
622
- }
623
- const rounded = Math.round(parsed);
624
- return Math.min(5000, Math.max(0, rounded));
625
- }
626
601
  normalizeFrontGateFullTravelMs(value) {
627
602
  const fallbackMs = 25000;
628
603
  const parsed = Number(value);
@@ -641,15 +616,6 @@ class SuplaPlatform {
641
616
  const rounded = Math.round(parsed);
642
617
  return Math.min(15000, Math.max(3000, rounded));
643
618
  }
644
- normalizeFrontGateWrongDirectionRunMs(value) {
645
- const fallbackMs = 700;
646
- const parsed = Number(value);
647
- if (!Number.isFinite(parsed)) {
648
- return fallbackMs;
649
- }
650
- const rounded = Math.round(parsed);
651
- return Math.min(5000, Math.max(100, rounded));
652
- }
653
619
  normalizeFrontGateMinimumPulseGapMs(value) {
654
620
  const fallbackMs = 3000;
655
621
  const parsed = Number(value);
@@ -657,22 +623,34 @@ class SuplaPlatform {
657
623
  return fallbackMs;
658
624
  }
659
625
  const rounded = Math.round(parsed);
660
- 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';
661
630
  }
662
- normalizeFrontGateCloseRetryLimit(value) {
663
- 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;
664
639
  const parsed = Number(value);
665
640
  if (!Number.isFinite(parsed)) {
666
641
  return fallbackValue;
667
642
  }
668
643
  const rounded = Math.round(parsed);
669
- 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;
670
648
  }
671
649
  normalizeTopicSuffix(value) {
672
650
  return value.toString().replace(/^\/+/, '');
673
651
  }
674
652
  normalizeTopicBase(topic) {
675
- let base = (topic !== null && topic !== void 0 ? topic : '').toString().trim();
653
+ let base = (topic ?? '').toString().trim();
676
654
  if (!base) {
677
655
  return '';
678
656
  }
@@ -682,31 +660,33 @@ class SuplaPlatform {
682
660
  base = base.replace(/\/set\/.+$/, '');
683
661
  return base.replace(/\/+$/, '');
684
662
  }
685
- registerMqttHandler(topic, handler, ownerId) {
686
- var _a, _b, _c;
663
+ registerMqttHandler(topic, handler, ownerId, options = {}) {
687
664
  if (!topic) {
688
665
  return () => undefined;
689
666
  }
690
667
  this.startMqttRouter();
691
668
  const handlerMap = this.isWildcardTopic(topic) ? this.mqttWildcardHandlers : this.mqttHandlers;
692
- const handlers = (_a = handlerMap.get(topic)) !== null && _a !== void 0 ? _a : new Set();
669
+ const handlers = handlerMap.get(topic) ?? new Set();
693
670
  handlers.add(handler);
694
671
  handlerMap.set(topic, handlers);
695
- const ownerTopics = (_b = this.mqttHandlerOwners.get(ownerId)) !== null && _b !== void 0 ? _b : new Map();
696
- const ownerHandlers = (_c = ownerTopics.get(topic)) !== null && _c !== void 0 ? _c : new Set();
672
+ const ownerTopics = this.mqttHandlerOwners.get(ownerId) ?? new Map();
673
+ const ownerHandlers = ownerTopics.get(topic) ?? new Set();
697
674
  ownerHandlers.add(handler);
698
675
  ownerTopics.set(topic, ownerHandlers);
699
676
  this.mqttHandlerOwners.set(ownerId, ownerTopics);
700
677
  this.mqttDesiredSubscriptions.add(topic);
678
+ if (options.noLocal && this.mqttProtocolVersion === 5) {
679
+ this.mqttNoLocalTopics.add(topic);
680
+ }
701
681
  this.ensureSubscribed(topic, false);
702
682
  return () => {
703
683
  this.removeHandler(topic, handler);
704
684
  const ownerTopics = this.mqttHandlerOwners.get(ownerId);
705
- const ownerHandlers = ownerTopics === null || ownerTopics === void 0 ? void 0 : ownerTopics.get(topic);
685
+ const ownerHandlers = ownerTopics?.get(topic);
706
686
  if (ownerHandlers) {
707
687
  ownerHandlers.delete(handler);
708
688
  if (ownerHandlers.size === 0) {
709
- ownerTopics === null || ownerTopics === void 0 ? void 0 : ownerTopics.delete(topic);
689
+ ownerTopics?.delete(topic);
710
690
  }
711
691
  }
712
692
  if (ownerTopics && ownerTopics.size === 0) {
@@ -714,9 +694,23 @@ class SuplaPlatform {
714
694
  }
715
695
  };
716
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
+ }
717
712
  registerOwnerCleanup(ownerId, cleanup) {
718
- var _a;
719
- const cleanups = (_a = this.ownerCleanups.get(ownerId)) !== null && _a !== void 0 ? _a : new Set();
713
+ const cleanups = this.ownerCleanups.get(ownerId) ?? new Set();
720
714
  cleanups.add(cleanup);
721
715
  this.ownerCleanups.set(ownerId, cleanups);
722
716
  return () => {
@@ -756,6 +750,8 @@ class SuplaPlatform {
756
750
  }
757
751
  this.mqttHandlers.clear();
758
752
  this.mqttWildcardHandlers.clear();
753
+ this.mqttNoLocalTopics.clear();
754
+ this.mqttTransportHandlers.clear();
759
755
  this.mqttDesiredSubscriptions.clear();
760
756
  this.mqttSubscriptions.clear();
761
757
  this.mqttPendingSubscriptions.clear();
@@ -763,8 +759,13 @@ class SuplaPlatform {
763
759
  this.ownerCleanups.clear();
764
760
  }
765
761
  publishCommand(topic, payload, callback) {
766
- var _a;
767
- const client = (_a = this.MqttClient) === null || _a === void 0 ? void 0 : _a.client;
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
+ const client = this.MqttClient?.client;
768
769
  if (!client || !client.connected) {
769
770
  const error = new Error('MQTT not connected');
770
771
  this.log.error(`MQTT not connected; cannot publish ${topic}`);
@@ -773,7 +774,7 @@ class SuplaPlatform {
773
774
  }
774
775
  return;
775
776
  }
776
- client.publish(topic, payload, { qos: this.commandQos, retain: this.commandRetain }, (error) => {
777
+ client.publish(topic, payload, options, (error) => {
777
778
  if (callback) {
778
779
  callback(error);
779
780
  return;
@@ -799,6 +800,31 @@ class SuplaPlatform {
799
800
  }
800
801
  return false;
801
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
+ }
802
828
  runOwnerCleanup(ownerId) {
803
829
  const cleanups = this.ownerCleanups.get(ownerId);
804
830
  if (!cleanups) {
@@ -814,6 +840,22 @@ class SuplaPlatform {
814
840
  }
815
841
  this.ownerCleanups.delete(ownerId);
816
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
+ }
817
859
  clearActiveSubscriptions() {
818
860
  if (this.mqttSubscriptions.size > 0 || this.mqttPendingSubscriptions.size > 0) {
819
861
  this.log.debug('MQTT connection lost; clearing active subscriptions.');
@@ -838,7 +880,6 @@ class SuplaPlatform {
838
880
  this.mqttRetryState.delete(topic);
839
881
  }
840
882
  scheduleSubscriptionRetry(topic, reason, hardDeny) {
841
- var _a;
842
883
  if (this.mqttRetryTimers.has(topic)) {
843
884
  return;
844
885
  }
@@ -848,7 +889,7 @@ class SuplaPlatform {
848
889
  const baseDelayMs = 2500;
849
890
  const maxDelayMs = 60000;
850
891
  const now = Date.now();
851
- const state = (_a = this.mqttRetryState.get(topic)) !== null && _a !== void 0 ? _a : {
892
+ const state = this.mqttRetryState.get(topic) ?? {
852
893
  attempt: 0,
853
894
  delayMs: baseDelayMs,
854
895
  lastLogAt: 0,
@@ -895,9 +936,8 @@ class SuplaPlatform {
895
936
  return entry.qos === 0 || entry.qos === 1 || entry.qos === 2;
896
937
  }
897
938
  logSubscriptionIssue(topic, message) {
898
- var _a;
899
939
  const now = Date.now();
900
- const state = (_a = this.mqttRetryState.get(topic)) !== null && _a !== void 0 ? _a : {
940
+ const state = this.mqttRetryState.get(topic) ?? {
901
941
  attempt: 0,
902
942
  delayMs: 2500,
903
943
  lastLogAt: 0,
@@ -931,7 +971,11 @@ class SuplaPlatform {
931
971
  return;
932
972
  }
933
973
  this.mqttPendingSubscriptions.add(topic);
934
- 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) => {
935
979
  this.mqttPendingSubscriptions.delete(topic);
936
980
  if (err) {
937
981
  this.logSubscriptionIssue(topic, `MQTT subscribe failed for ${topic}: ${err.message}`);
@@ -977,6 +1021,7 @@ class SuplaPlatform {
977
1021
  }
978
1022
  removeSubscription(topic) {
979
1023
  this.mqttDesiredSubscriptions.delete(topic);
1024
+ this.mqttNoLocalTopics.delete(topic);
980
1025
  this.mqttPendingSubscriptions.delete(topic);
981
1026
  this.clearSubscriptionRetry(topic);
982
1027
  if (!this.MqttClient) {
@@ -1042,7 +1087,7 @@ class SuplaPlatform {
1042
1087
  }
1043
1088
  this.mqttRouterAttached = true;
1044
1089
  this.resubscribeAll(false);
1045
- this.MqttClient.client.on('message', (topic, message) => {
1090
+ this.MqttClient.client.on('message', (topic, message, packet) => {
1046
1091
  const dispatched = new Set();
1047
1092
  const dispatch = (handler, label) => {
1048
1093
  if (dispatched.has(handler)) {
@@ -1050,7 +1095,7 @@ class SuplaPlatform {
1050
1095
  }
1051
1096
  dispatched.add(handler);
1052
1097
  try {
1053
- handler(message, topic);
1098
+ handler(message, topic, packet);
1054
1099
  }
1055
1100
  catch (error) {
1056
1101
  this.log.error(`MQTT handler error for ${label}: ${error.message}`);