@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 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
- /** Seconds allowed for the TLS handshake plus password exchange of a probe. */
103435
- var PROBE_LOGIN_TIMEOUT_S = 15;
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, PROBE_LOGIN_TIMEOUT_S);
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
- const result = await probe({
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
- /** Seconds allowed for the TLS handshake plus password exchange of a probe. */
103436
- var PROBE_LOGIN_TIMEOUT_S = 15;
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, PROBE_LOGIN_TIMEOUT_S);
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
- const result = await probe({
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.153",
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.722"
65
+ "server": ">=1.2.723"
66
66
  }
67
67
  },
68
68
  "files": [