@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.
- package/README.md +43 -0
- package/config.schema.json +67 -71
- package/dist/Accesories/ActionTriggerAccessory.js.map +1 -1
- package/dist/Accesories/AirQualityAccessory.js +1 -2
- package/dist/Accesories/AirQualityAccessory.js.map +1 -1
- package/dist/Accesories/DimmerAccessory.js.map +1 -1
- package/dist/Accesories/DimmerRgbLightAccessory.js.map +1 -1
- package/dist/Accesories/DoorAccessory.js.map +1 -1
- package/dist/Accesories/ElectricityMeterAccessory.js.map +1 -1
- package/dist/Accesories/FacadeBlindAccessory.js +1 -1
- package/dist/Accesories/FacadeBlindAccessory.js.map +1 -1
- 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/GarageDoorOpenerAccesory.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 -82
- package/dist/Accesories/GateAccessory.js.map +1 -1
- package/dist/Accesories/GateLockAccessory.js.map +1 -1
- package/dist/Accesories/LightBulbAccesory.js.map +1 -1
- package/dist/Accesories/PressureAccessory.js.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.d.ts.map +1 -1
- package/dist/Accesories/RollerShutterAccessory.js +6 -5
- package/dist/Accesories/RollerShutterAccessory.js.map +1 -1
- package/dist/Accesories/SwitchAccessory.js.map +1 -1
- package/dist/Accesories/TemperatureAccessory.js.map +1 -1
- package/dist/Accesories/TemperatureHumidityAccessory.js.map +1 -1
- package/dist/Accesories/ThermostatAccessory.js +2 -2
- package/dist/Accesories/ThermostatAccessory.js.map +1 -1
- package/dist/Heplers/ColorConverters.js +4 -5
- package/dist/Heplers/ColorConverters.js.map +1 -1
- package/dist/Heplers/SuplaChannelContext.d.ts.map +1 -1
- package/dist/Heplers/SuplaMqttClient.d.ts +2 -3
- package/dist/Heplers/SuplaMqttClient.d.ts.map +1 -1
- package/dist/Heplers/SuplaMqttClient.js +34 -28
- package/dist/Heplers/SuplaMqttClient.js.map +1 -1
- package/dist/Heplers/SuplaMqttClientContext.d.ts +5 -4
- 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/index.d.ts.map +1 -1
- package/dist/platform.d.ts +34 -34
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +228 -183
- package/dist/platform.js.map +1 -1
- package/package.json +9 -8
- 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 (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 = (
|
|
93
|
-
this.gateLockSetOffPayload = (
|
|
94
|
-
this.
|
|
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.
|
|
104
|
-
this.
|
|
105
|
-
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);
|
|
106
113
|
this.commandQos = this.normalizeCommandQos(configView.commandQos);
|
|
107
|
-
this.commandRetain = this.parseBoolean(
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 =
|
|
216
|
+
const uuid = existingByIds?.UUID ?? this.getChannelUuid(channel);
|
|
196
217
|
channelUuids.add(uuid);
|
|
197
|
-
const existingAccessory = existingByIds
|
|
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 =
|
|
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
|
-
|
|
287
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
663
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
669
|
+
const handlers = handlerMap.get(topic) ?? new Set();
|
|
693
670
|
handlers.add(handler);
|
|
694
671
|
handlerMap.set(topic, handlers);
|
|
695
|
-
const ownerTopics =
|
|
696
|
-
const ownerHandlers =
|
|
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
|
|
685
|
+
const ownerHandlers = ownerTopics?.get(topic);
|
|
706
686
|
if (ownerHandlers) {
|
|
707
687
|
ownerHandlers.delete(handler);
|
|
708
688
|
if (ownerHandlers.size === 0) {
|
|
709
|
-
ownerTopics
|
|
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
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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}`);
|