@camstack/addon-provider-velux 0.2.153 → 0.2.154
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/CHANGELOG.md +6 -0
- package/dist/addon.js +154 -8
- package/dist/addon.mjs +154 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.154] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The gateway connection test now names the step that timed out (TCP, secure login, version, close) within 17 s and frees the session, instead of hanging until the wizard gives up; an empty gateway scan is logged.
|
|
15
|
+
|
|
10
16
|
## [0.2.153] - 2026-11-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -33,6 +33,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
//#endregion
|
|
34
34
|
let node_tls = require("node:tls");
|
|
35
35
|
let timers_promises = require("timers/promises");
|
|
36
|
+
let node_net = require("node:net");
|
|
36
37
|
//#region ../types/dist/event-category-CaURIwQA.mjs
|
|
37
38
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
38
39
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -66414,6 +66415,60 @@ onColorChanged: { data: object({
|
|
|
66414
66415
|
*/
|
|
66415
66416
|
history: "none"
|
|
66416
66417
|
};
|
|
66418
|
+
/**
|
|
66419
|
+
* `connection-test` — pre-creation validation of an integration's settings,
|
|
66420
|
+
* system-scoped collection (one provider per integration addon).
|
|
66421
|
+
*
|
|
66422
|
+
* ── Why this cap exists ─────────────────────────────────────────────────────
|
|
66423
|
+
*
|
|
66424
|
+
* Before it, `integrations.testConnection` had exactly two behaviours: a broker
|
|
66425
|
+
* branch (`settings.brokerId` → `broker.testConnection`) and a DEFAULT branch
|
|
66426
|
+
* that probed `settings.main_stream_url ?? settings.url` with ffprobe. Every
|
|
66427
|
+
* account-based integration — Dreo, Dreame, Tuya, Petkit, Wyze — fell into that
|
|
66428
|
+
* default and was told `{"success":false,"error":"No stream URL provided"}`.
|
|
66429
|
+
* That answer neither validated nor refused anything, so:
|
|
66430
|
+
*
|
|
66431
|
+
* - the Test button had NEVER worked for an account integration, and
|
|
66432
|
+
* - `integrations.create` had nothing to gate on, so an integration with a
|
|
66433
|
+
* wrong password was created happily and failed later, silently, at
|
|
66434
|
+
* reconcile time.
|
|
66435
|
+
*
|
|
66436
|
+
* The structural gap was that at CREATE time the integration does not exist
|
|
66437
|
+
* yet: there is no `integrationId`, no `brokerId`, no live client — and every
|
|
66438
|
+
* existing provider surface (`device-provider`, `device-adoption`) is keyed by
|
|
66439
|
+
* one of those. There was no way to ask "are these settings valid?" before the
|
|
66440
|
+
* row existed.
|
|
66441
|
+
*
|
|
66442
|
+
* ── The contract ────────────────────────────────────────────────────────────
|
|
66443
|
+
*
|
|
66444
|
+
* The provider knows how to validate its OWN settings. The framework does not
|
|
66445
|
+
* guess from the shape of the settings — a stream-URL probe is one provider's
|
|
66446
|
+
* implementation of this cap, not the universal fallback. A provider that does
|
|
66447
|
+
* not register this cap is a KNOWN "cannot validate", never a silent pass.
|
|
66448
|
+
*
|
|
66449
|
+
* `testSettings` takes the candidate settings blob exactly as the wizard's
|
|
66450
|
+
* config step collected it (the same blob that would be handed to
|
|
66451
|
+
* `integrations.create`), and MUST NOT persist anything, mint an integration,
|
|
66452
|
+
* or mutate live state. It opens a throwaway session, asks, and closes it.
|
|
66453
|
+
*
|
|
66454
|
+
* ── The three outcomes are NOT interchangeable ──────────────────────────────
|
|
66455
|
+
*
|
|
66456
|
+
* This is the whole point of the discriminated union: a `null` from a timeout
|
|
66457
|
+
* must never look like a `null` from a refusal.
|
|
66458
|
+
*
|
|
66459
|
+
* - `validated` — the remote ACCEPTED these credentials. Observed.
|
|
66460
|
+
* - `rejected` — the remote REFUSED these credentials. Observed.
|
|
66461
|
+
* This is the ONLY outcome that blocks creation.
|
|
66462
|
+
* - `inconclusive` — the check could not complete (DNS, timeout, 5xx, an
|
|
66463
|
+
* unexpected shape). NOTHING was observed about the
|
|
66464
|
+
* credentials. Never rendered, or counted, as a failure.
|
|
66465
|
+
*
|
|
66466
|
+
* A provider that cannot tell a refusal from a transport fault must return
|
|
66467
|
+
* `inconclusive`. Guessing `rejected` would block creation on a flaky network;
|
|
66468
|
+
* guessing `validated` would wave a wrong password through.
|
|
66469
|
+
*/
|
|
66470
|
+
/** Ceiling on how long a pre-creation probe may hold the wizard. */
|
|
66471
|
+
var CONNECTION_TEST_TIMEOUT_MS = 2e4;
|
|
66417
66472
|
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
66418
66473
|
object({
|
|
66419
66474
|
outcome: literal("validated"),
|
|
@@ -103431,8 +103486,69 @@ var VeluxClientRegistry = class {
|
|
|
103431
103486
|
};
|
|
103432
103487
|
//#endregion
|
|
103433
103488
|
//#region src/klf-probe.ts
|
|
103434
|
-
/**
|
|
103435
|
-
var
|
|
103489
|
+
/** The gateway's TLS API port. */
|
|
103490
|
+
var KLF_PORT = 51200;
|
|
103491
|
+
/**
|
|
103492
|
+
* Defaults add up, worst case, to 3 + 2 x 5 + 2 + 2 = 17 s, under the
|
|
103493
|
+
* `CONNECTION_TEST_TIMEOUT_MS` ceiling of the wizard, so a stuck gateway is named
|
|
103494
|
+
* here instead of the wizard giving up unnamed.
|
|
103495
|
+
*/
|
|
103496
|
+
var DEFAULT_KLF_PROBE_TIMEOUTS_MS = {
|
|
103497
|
+
tcp: 3e3,
|
|
103498
|
+
login: 5e3,
|
|
103499
|
+
version: 2e3,
|
|
103500
|
+
logout: 2e3
|
|
103501
|
+
};
|
|
103502
|
+
var PHASE_TEXT = {
|
|
103503
|
+
tcp: "did not accept a TCP connection on port 51200",
|
|
103504
|
+
login: "did not complete the secure login (TLS handshake or password answer)",
|
|
103505
|
+
version: "did not answer the version request",
|
|
103506
|
+
logout: "did not close the session"
|
|
103507
|
+
};
|
|
103508
|
+
/** A probe step ran out of time. Names the step, so a bound is never a silent hang. */
|
|
103509
|
+
var KlfProbeTimeoutError = class extends Error {
|
|
103510
|
+
phase;
|
|
103511
|
+
host;
|
|
103512
|
+
limitMs;
|
|
103513
|
+
constructor(phase, host, limitMs) {
|
|
103514
|
+
super(`The gateway ${host} ${PHASE_TEXT[phase]} within ${Math.round(limitMs / 100) / 10}s (timed out)`);
|
|
103515
|
+
this.phase = phase;
|
|
103516
|
+
this.host = host;
|
|
103517
|
+
this.limitMs = limitMs;
|
|
103518
|
+
this.name = "KlfProbeTimeoutError";
|
|
103519
|
+
}
|
|
103520
|
+
};
|
|
103521
|
+
async function withDeadline(work, ms, onTimeout) {
|
|
103522
|
+
let timer;
|
|
103523
|
+
try {
|
|
103524
|
+
return await Promise.race([work, new Promise((_resolve, reject) => {
|
|
103525
|
+
timer = setTimeout(() => reject(onTimeout()), ms);
|
|
103526
|
+
})]);
|
|
103527
|
+
} finally {
|
|
103528
|
+
clearTimeout(timer);
|
|
103529
|
+
}
|
|
103530
|
+
}
|
|
103531
|
+
/** A bare TCP connect to the gateway port: tells "nothing listens" from "TLS is stuck". */
|
|
103532
|
+
function tcpReachable(host, ms) {
|
|
103533
|
+
return new Promise((resolve, reject) => {
|
|
103534
|
+
const socket = (0, node_net.connect)({
|
|
103535
|
+
host,
|
|
103536
|
+
port: KLF_PORT
|
|
103537
|
+
});
|
|
103538
|
+
socket.setTimeout(ms, () => {
|
|
103539
|
+
socket.destroy();
|
|
103540
|
+
reject(new KlfProbeTimeoutError("tcp", host, ms));
|
|
103541
|
+
});
|
|
103542
|
+
socket.once("connect", () => {
|
|
103543
|
+
socket.destroy();
|
|
103544
|
+
resolve();
|
|
103545
|
+
});
|
|
103546
|
+
socket.once("error", (err) => {
|
|
103547
|
+
socket.destroy();
|
|
103548
|
+
reject(err);
|
|
103549
|
+
});
|
|
103550
|
+
});
|
|
103551
|
+
}
|
|
103436
103552
|
/**
|
|
103437
103553
|
* Opens ONE session, authenticates, reads the firmware version and closes the
|
|
103438
103554
|
* session again, whatever happens. The gateway accepts two sessions in total, so
|
|
@@ -103440,28 +103556,46 @@ var PROBE_LOGIN_TIMEOUT_S = 15;
|
|
|
103440
103556
|
* list. A refused password rejects with `VeluxLoginRefusedError`.
|
|
103441
103557
|
*/
|
|
103442
103558
|
async function probeKlfGateway(options) {
|
|
103559
|
+
const limits = {
|
|
103560
|
+
...DEFAULT_KLF_PROBE_TIMEOUTS_MS,
|
|
103561
|
+
...options.timeoutsMs
|
|
103562
|
+
};
|
|
103563
|
+
await tcpReachable(options.host, limits.tcp);
|
|
103443
103564
|
const connection = options.tls === void 0 ? new Connection(options.host) : new Connection(options.host, options.tls);
|
|
103565
|
+
const loginError = () => new KlfProbeTimeoutError("login", options.host, limits.login);
|
|
103444
103566
|
try {
|
|
103445
103567
|
try {
|
|
103446
|
-
await connection.loginAsync(options.password,
|
|
103568
|
+
await withDeadline(connection.loginAsync(options.password, limits.login / 1e3), limits.login * 2 + 500, loginError);
|
|
103447
103569
|
} catch (err) {
|
|
103570
|
+
if (err instanceof KlfProbeTimeoutError) throw err;
|
|
103571
|
+
if (/^timeout$/i.test(errMsg(err)) || err instanceof Error && err.name === "TimeoutError") throw loginError();
|
|
103448
103572
|
throw loginFailure(err);
|
|
103449
103573
|
}
|
|
103450
103574
|
try {
|
|
103451
|
-
const { SoftwareVersion } = await new Gateway(connection).getVersionAsync();
|
|
103575
|
+
const { SoftwareVersion } = await withDeadline(new Gateway(connection).getVersionAsync(), limits.version, () => new KlfProbeTimeoutError("version", options.host, limits.version));
|
|
103452
103576
|
return { firmware: SoftwareVersion.toString() };
|
|
103453
103577
|
} catch {
|
|
103454
103578
|
return { firmware: null };
|
|
103455
103579
|
}
|
|
103456
103580
|
} finally {
|
|
103457
103581
|
try {
|
|
103458
|
-
await connection.logoutAsync();
|
|
103582
|
+
await withDeadline(connection.logoutAsync(limits.logout / 1e3), limits.logout + 500, () => new KlfProbeTimeoutError("logout", options.host, limits.logout));
|
|
103459
103583
|
} catch (err) {
|
|
103460
103584
|
errMsg(err);
|
|
103585
|
+
await connection[Symbol.asyncDispose]().catch(() => void 0);
|
|
103461
103586
|
}
|
|
103462
103587
|
}
|
|
103463
103588
|
}
|
|
103464
103589
|
var TEST_LABEL = "Logs in to the gateway with this password, then closes the session";
|
|
103590
|
+
/** Under `CONNECTION_TEST_TIMEOUT_MS`, so the reason reaches the operator first. */
|
|
103591
|
+
var DEFAULT_TEST_DEADLINE_MS = CONNECTION_TEST_TIMEOUT_MS - 2e3;
|
|
103592
|
+
/** The whole test outlived its ceiling; no step of the probe answered in time. */
|
|
103593
|
+
var KlfTestDeadlineError = class extends Error {
|
|
103594
|
+
constructor(host, limitMs) {
|
|
103595
|
+
super(`The gateway ${host} did not answer within ${Math.round(limitMs / 1e3)}s (timed out)`);
|
|
103596
|
+
this.name = "KlfTestDeadlineError";
|
|
103597
|
+
}
|
|
103598
|
+
};
|
|
103465
103599
|
var UNREACHABLE_CODES = new Set([
|
|
103466
103600
|
"ECONNREFUSED",
|
|
103467
103601
|
"EHOSTUNREACH",
|
|
@@ -103483,6 +103617,10 @@ function classifyProbeFailure(err) {
|
|
|
103483
103617
|
kind: "wrong-password",
|
|
103484
103618
|
text: err.message
|
|
103485
103619
|
};
|
|
103620
|
+
if (err instanceof KlfProbeTimeoutError || err instanceof KlfTestDeadlineError) return {
|
|
103621
|
+
kind: "unreachable",
|
|
103622
|
+
text: err.message
|
|
103623
|
+
};
|
|
103486
103624
|
const text = typeof err === "string" ? err : errMsg(err);
|
|
103487
103625
|
const code = typeof err === "object" && err !== null ? Reflect.get(err, "code") : void 0;
|
|
103488
103626
|
if (typeof code === "string" && UNREACHABLE_CODES.has(code)) return {
|
|
@@ -103507,7 +103645,7 @@ function classifyProbeFailure(err) {
|
|
|
103507
103645
|
};
|
|
103508
103646
|
}
|
|
103509
103647
|
function buildVeluxConnectionTestProvider(deps) {
|
|
103510
|
-
const { logger, probe = probeKlfGateway, liveSessions = () => 0 } = deps;
|
|
103648
|
+
const { logger, probe = probeKlfGateway, liveSessions = () => 0, deadlineMs = DEFAULT_TEST_DEADLINE_MS } = deps;
|
|
103511
103649
|
/** The tail of the test queue per host: a second test waits for the first to log out. */
|
|
103512
103650
|
const queues = /* @__PURE__ */ new Map();
|
|
103513
103651
|
async function run(host, password) {
|
|
@@ -103523,10 +103661,13 @@ function buildVeluxConnectionTestProvider(deps) {
|
|
|
103523
103661
|
}
|
|
103524
103662
|
const startedAt = Date.now();
|
|
103525
103663
|
try {
|
|
103526
|
-
|
|
103664
|
+
let timer;
|
|
103665
|
+
const result = await Promise.race([probe({
|
|
103527
103666
|
host,
|
|
103528
103667
|
password
|
|
103529
|
-
})
|
|
103668
|
+
}), new Promise((_resolve, reject) => {
|
|
103669
|
+
timer = setTimeout(() => reject(new KlfTestDeadlineError(host, deadlineMs)), deadlineMs);
|
|
103670
|
+
})]).finally(() => clearTimeout(timer));
|
|
103530
103671
|
return {
|
|
103531
103672
|
outcome: "validated",
|
|
103532
103673
|
latencyMs: Date.now() - startedAt,
|
|
@@ -103761,7 +103902,12 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103761
103902
|
],
|
|
103762
103903
|
customActions: veluxActions,
|
|
103763
103904
|
actionHandlers: { discoverGateway: async () => {
|
|
103905
|
+
const startedAt = Date.now();
|
|
103764
103906
|
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
103907
|
+
this.ctx.logger.info("Velux gateway scan finished", { meta: {
|
|
103908
|
+
found: candidates.length,
|
|
103909
|
+
ms: Date.now() - startedAt
|
|
103910
|
+
} });
|
|
103765
103911
|
const hosts = candidates.map((candidate) => ({
|
|
103766
103912
|
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
103767
103913
|
name: candidate.suggestedName,
|
package/dist/addon.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { connect } from "node:tls";
|
|
3
3
|
import { setImmediate as setImmediate$1 } from "timers/promises";
|
|
4
|
+
import { connect as connect$1 } from "node:net";
|
|
4
5
|
//#region \0rolldown/runtime.js
|
|
5
6
|
var __create = Object.create;
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
@@ -66415,6 +66416,60 @@ onColorChanged: { data: object({
|
|
|
66415
66416
|
*/
|
|
66416
66417
|
history: "none"
|
|
66417
66418
|
};
|
|
66419
|
+
/**
|
|
66420
|
+
* `connection-test` — pre-creation validation of an integration's settings,
|
|
66421
|
+
* system-scoped collection (one provider per integration addon).
|
|
66422
|
+
*
|
|
66423
|
+
* ── Why this cap exists ─────────────────────────────────────────────────────
|
|
66424
|
+
*
|
|
66425
|
+
* Before it, `integrations.testConnection` had exactly two behaviours: a broker
|
|
66426
|
+
* branch (`settings.brokerId` → `broker.testConnection`) and a DEFAULT branch
|
|
66427
|
+
* that probed `settings.main_stream_url ?? settings.url` with ffprobe. Every
|
|
66428
|
+
* account-based integration — Dreo, Dreame, Tuya, Petkit, Wyze — fell into that
|
|
66429
|
+
* default and was told `{"success":false,"error":"No stream URL provided"}`.
|
|
66430
|
+
* That answer neither validated nor refused anything, so:
|
|
66431
|
+
*
|
|
66432
|
+
* - the Test button had NEVER worked for an account integration, and
|
|
66433
|
+
* - `integrations.create` had nothing to gate on, so an integration with a
|
|
66434
|
+
* wrong password was created happily and failed later, silently, at
|
|
66435
|
+
* reconcile time.
|
|
66436
|
+
*
|
|
66437
|
+
* The structural gap was that at CREATE time the integration does not exist
|
|
66438
|
+
* yet: there is no `integrationId`, no `brokerId`, no live client — and every
|
|
66439
|
+
* existing provider surface (`device-provider`, `device-adoption`) is keyed by
|
|
66440
|
+
* one of those. There was no way to ask "are these settings valid?" before the
|
|
66441
|
+
* row existed.
|
|
66442
|
+
*
|
|
66443
|
+
* ── The contract ────────────────────────────────────────────────────────────
|
|
66444
|
+
*
|
|
66445
|
+
* The provider knows how to validate its OWN settings. The framework does not
|
|
66446
|
+
* guess from the shape of the settings — a stream-URL probe is one provider's
|
|
66447
|
+
* implementation of this cap, not the universal fallback. A provider that does
|
|
66448
|
+
* not register this cap is a KNOWN "cannot validate", never a silent pass.
|
|
66449
|
+
*
|
|
66450
|
+
* `testSettings` takes the candidate settings blob exactly as the wizard's
|
|
66451
|
+
* config step collected it (the same blob that would be handed to
|
|
66452
|
+
* `integrations.create`), and MUST NOT persist anything, mint an integration,
|
|
66453
|
+
* or mutate live state. It opens a throwaway session, asks, and closes it.
|
|
66454
|
+
*
|
|
66455
|
+
* ── The three outcomes are NOT interchangeable ──────────────────────────────
|
|
66456
|
+
*
|
|
66457
|
+
* This is the whole point of the discriminated union: a `null` from a timeout
|
|
66458
|
+
* must never look like a `null` from a refusal.
|
|
66459
|
+
*
|
|
66460
|
+
* - `validated` — the remote ACCEPTED these credentials. Observed.
|
|
66461
|
+
* - `rejected` — the remote REFUSED these credentials. Observed.
|
|
66462
|
+
* This is the ONLY outcome that blocks creation.
|
|
66463
|
+
* - `inconclusive` — the check could not complete (DNS, timeout, 5xx, an
|
|
66464
|
+
* unexpected shape). NOTHING was observed about the
|
|
66465
|
+
* credentials. Never rendered, or counted, as a failure.
|
|
66466
|
+
*
|
|
66467
|
+
* A provider that cannot tell a refusal from a transport fault must return
|
|
66468
|
+
* `inconclusive`. Guessing `rejected` would block creation on a flaky network;
|
|
66469
|
+
* guessing `validated` would wave a wrong password through.
|
|
66470
|
+
*/
|
|
66471
|
+
/** Ceiling on how long a pre-creation probe may hold the wizard. */
|
|
66472
|
+
var CONNECTION_TEST_TIMEOUT_MS = 2e4;
|
|
66418
66473
|
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
66419
66474
|
object({
|
|
66420
66475
|
outcome: literal("validated"),
|
|
@@ -103432,8 +103487,69 @@ var VeluxClientRegistry = class {
|
|
|
103432
103487
|
};
|
|
103433
103488
|
//#endregion
|
|
103434
103489
|
//#region src/klf-probe.ts
|
|
103435
|
-
/**
|
|
103436
|
-
var
|
|
103490
|
+
/** The gateway's TLS API port. */
|
|
103491
|
+
var KLF_PORT = 51200;
|
|
103492
|
+
/**
|
|
103493
|
+
* Defaults add up, worst case, to 3 + 2 x 5 + 2 + 2 = 17 s, under the
|
|
103494
|
+
* `CONNECTION_TEST_TIMEOUT_MS` ceiling of the wizard, so a stuck gateway is named
|
|
103495
|
+
* here instead of the wizard giving up unnamed.
|
|
103496
|
+
*/
|
|
103497
|
+
var DEFAULT_KLF_PROBE_TIMEOUTS_MS = {
|
|
103498
|
+
tcp: 3e3,
|
|
103499
|
+
login: 5e3,
|
|
103500
|
+
version: 2e3,
|
|
103501
|
+
logout: 2e3
|
|
103502
|
+
};
|
|
103503
|
+
var PHASE_TEXT = {
|
|
103504
|
+
tcp: "did not accept a TCP connection on port 51200",
|
|
103505
|
+
login: "did not complete the secure login (TLS handshake or password answer)",
|
|
103506
|
+
version: "did not answer the version request",
|
|
103507
|
+
logout: "did not close the session"
|
|
103508
|
+
};
|
|
103509
|
+
/** A probe step ran out of time. Names the step, so a bound is never a silent hang. */
|
|
103510
|
+
var KlfProbeTimeoutError = class extends Error {
|
|
103511
|
+
phase;
|
|
103512
|
+
host;
|
|
103513
|
+
limitMs;
|
|
103514
|
+
constructor(phase, host, limitMs) {
|
|
103515
|
+
super(`The gateway ${host} ${PHASE_TEXT[phase]} within ${Math.round(limitMs / 100) / 10}s (timed out)`);
|
|
103516
|
+
this.phase = phase;
|
|
103517
|
+
this.host = host;
|
|
103518
|
+
this.limitMs = limitMs;
|
|
103519
|
+
this.name = "KlfProbeTimeoutError";
|
|
103520
|
+
}
|
|
103521
|
+
};
|
|
103522
|
+
async function withDeadline(work, ms, onTimeout) {
|
|
103523
|
+
let timer;
|
|
103524
|
+
try {
|
|
103525
|
+
return await Promise.race([work, new Promise((_resolve, reject) => {
|
|
103526
|
+
timer = setTimeout(() => reject(onTimeout()), ms);
|
|
103527
|
+
})]);
|
|
103528
|
+
} finally {
|
|
103529
|
+
clearTimeout(timer);
|
|
103530
|
+
}
|
|
103531
|
+
}
|
|
103532
|
+
/** A bare TCP connect to the gateway port: tells "nothing listens" from "TLS is stuck". */
|
|
103533
|
+
function tcpReachable(host, ms) {
|
|
103534
|
+
return new Promise((resolve, reject) => {
|
|
103535
|
+
const socket = connect$1({
|
|
103536
|
+
host,
|
|
103537
|
+
port: KLF_PORT
|
|
103538
|
+
});
|
|
103539
|
+
socket.setTimeout(ms, () => {
|
|
103540
|
+
socket.destroy();
|
|
103541
|
+
reject(new KlfProbeTimeoutError("tcp", host, ms));
|
|
103542
|
+
});
|
|
103543
|
+
socket.once("connect", () => {
|
|
103544
|
+
socket.destroy();
|
|
103545
|
+
resolve();
|
|
103546
|
+
});
|
|
103547
|
+
socket.once("error", (err) => {
|
|
103548
|
+
socket.destroy();
|
|
103549
|
+
reject(err);
|
|
103550
|
+
});
|
|
103551
|
+
});
|
|
103552
|
+
}
|
|
103437
103553
|
/**
|
|
103438
103554
|
* Opens ONE session, authenticates, reads the firmware version and closes the
|
|
103439
103555
|
* session again, whatever happens. The gateway accepts two sessions in total, so
|
|
@@ -103441,28 +103557,46 @@ var PROBE_LOGIN_TIMEOUT_S = 15;
|
|
|
103441
103557
|
* list. A refused password rejects with `VeluxLoginRefusedError`.
|
|
103442
103558
|
*/
|
|
103443
103559
|
async function probeKlfGateway(options) {
|
|
103560
|
+
const limits = {
|
|
103561
|
+
...DEFAULT_KLF_PROBE_TIMEOUTS_MS,
|
|
103562
|
+
...options.timeoutsMs
|
|
103563
|
+
};
|
|
103564
|
+
await tcpReachable(options.host, limits.tcp);
|
|
103444
103565
|
const connection = options.tls === void 0 ? new Connection(options.host) : new Connection(options.host, options.tls);
|
|
103566
|
+
const loginError = () => new KlfProbeTimeoutError("login", options.host, limits.login);
|
|
103445
103567
|
try {
|
|
103446
103568
|
try {
|
|
103447
|
-
await connection.loginAsync(options.password,
|
|
103569
|
+
await withDeadline(connection.loginAsync(options.password, limits.login / 1e3), limits.login * 2 + 500, loginError);
|
|
103448
103570
|
} catch (err) {
|
|
103571
|
+
if (err instanceof KlfProbeTimeoutError) throw err;
|
|
103572
|
+
if (/^timeout$/i.test(errMsg(err)) || err instanceof Error && err.name === "TimeoutError") throw loginError();
|
|
103449
103573
|
throw loginFailure(err);
|
|
103450
103574
|
}
|
|
103451
103575
|
try {
|
|
103452
|
-
const { SoftwareVersion } = await new Gateway(connection).getVersionAsync();
|
|
103576
|
+
const { SoftwareVersion } = await withDeadline(new Gateway(connection).getVersionAsync(), limits.version, () => new KlfProbeTimeoutError("version", options.host, limits.version));
|
|
103453
103577
|
return { firmware: SoftwareVersion.toString() };
|
|
103454
103578
|
} catch {
|
|
103455
103579
|
return { firmware: null };
|
|
103456
103580
|
}
|
|
103457
103581
|
} finally {
|
|
103458
103582
|
try {
|
|
103459
|
-
await connection.logoutAsync();
|
|
103583
|
+
await withDeadline(connection.logoutAsync(limits.logout / 1e3), limits.logout + 500, () => new KlfProbeTimeoutError("logout", options.host, limits.logout));
|
|
103460
103584
|
} catch (err) {
|
|
103461
103585
|
errMsg(err);
|
|
103586
|
+
await connection[Symbol.asyncDispose]().catch(() => void 0);
|
|
103462
103587
|
}
|
|
103463
103588
|
}
|
|
103464
103589
|
}
|
|
103465
103590
|
var TEST_LABEL = "Logs in to the gateway with this password, then closes the session";
|
|
103591
|
+
/** Under `CONNECTION_TEST_TIMEOUT_MS`, so the reason reaches the operator first. */
|
|
103592
|
+
var DEFAULT_TEST_DEADLINE_MS = CONNECTION_TEST_TIMEOUT_MS - 2e3;
|
|
103593
|
+
/** The whole test outlived its ceiling; no step of the probe answered in time. */
|
|
103594
|
+
var KlfTestDeadlineError = class extends Error {
|
|
103595
|
+
constructor(host, limitMs) {
|
|
103596
|
+
super(`The gateway ${host} did not answer within ${Math.round(limitMs / 1e3)}s (timed out)`);
|
|
103597
|
+
this.name = "KlfTestDeadlineError";
|
|
103598
|
+
}
|
|
103599
|
+
};
|
|
103466
103600
|
var UNREACHABLE_CODES = new Set([
|
|
103467
103601
|
"ECONNREFUSED",
|
|
103468
103602
|
"EHOSTUNREACH",
|
|
@@ -103484,6 +103618,10 @@ function classifyProbeFailure(err) {
|
|
|
103484
103618
|
kind: "wrong-password",
|
|
103485
103619
|
text: err.message
|
|
103486
103620
|
};
|
|
103621
|
+
if (err instanceof KlfProbeTimeoutError || err instanceof KlfTestDeadlineError) return {
|
|
103622
|
+
kind: "unreachable",
|
|
103623
|
+
text: err.message
|
|
103624
|
+
};
|
|
103487
103625
|
const text = typeof err === "string" ? err : errMsg(err);
|
|
103488
103626
|
const code = typeof err === "object" && err !== null ? Reflect.get(err, "code") : void 0;
|
|
103489
103627
|
if (typeof code === "string" && UNREACHABLE_CODES.has(code)) return {
|
|
@@ -103508,7 +103646,7 @@ function classifyProbeFailure(err) {
|
|
|
103508
103646
|
};
|
|
103509
103647
|
}
|
|
103510
103648
|
function buildVeluxConnectionTestProvider(deps) {
|
|
103511
|
-
const { logger, probe = probeKlfGateway, liveSessions = () => 0 } = deps;
|
|
103649
|
+
const { logger, probe = probeKlfGateway, liveSessions = () => 0, deadlineMs = DEFAULT_TEST_DEADLINE_MS } = deps;
|
|
103512
103650
|
/** The tail of the test queue per host: a second test waits for the first to log out. */
|
|
103513
103651
|
const queues = /* @__PURE__ */ new Map();
|
|
103514
103652
|
async function run(host, password) {
|
|
@@ -103524,10 +103662,13 @@ function buildVeluxConnectionTestProvider(deps) {
|
|
|
103524
103662
|
}
|
|
103525
103663
|
const startedAt = Date.now();
|
|
103526
103664
|
try {
|
|
103527
|
-
|
|
103665
|
+
let timer;
|
|
103666
|
+
const result = await Promise.race([probe({
|
|
103528
103667
|
host,
|
|
103529
103668
|
password
|
|
103530
|
-
})
|
|
103669
|
+
}), new Promise((_resolve, reject) => {
|
|
103670
|
+
timer = setTimeout(() => reject(new KlfTestDeadlineError(host, deadlineMs)), deadlineMs);
|
|
103671
|
+
})]).finally(() => clearTimeout(timer));
|
|
103531
103672
|
return {
|
|
103532
103673
|
outcome: "validated",
|
|
103533
103674
|
latencyMs: Date.now() - startedAt,
|
|
@@ -103762,7 +103903,12 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103762
103903
|
],
|
|
103763
103904
|
customActions: veluxActions,
|
|
103764
103905
|
actionHandlers: { discoverGateway: async () => {
|
|
103906
|
+
const startedAt = Date.now();
|
|
103765
103907
|
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
103908
|
+
this.ctx.logger.info("Velux gateway scan finished", { meta: {
|
|
103909
|
+
found: candidates.length,
|
|
103910
|
+
ms: Date.now() - startedAt
|
|
103911
|
+
} });
|
|
103766
103912
|
const hosts = candidates.map((candidate) => ({
|
|
103767
103913
|
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
103768
103914
|
name: candidate.suggestedName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-velux",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.154",
|
|
4
4
|
"description": "Velux KLF-200 integration for CamStack — the gateway, local-gateway windows (with rain sensor) and shutter covers (positional, no tilt)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
64
|
"requires": {
|
|
65
|
-
"server": ">=1.2.
|
|
65
|
+
"server": ">=1.2.723"
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"files": [
|