@camstack/addon-provider-velux 0.2.153 → 0.2.155

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/dist/addon.mjs CHANGED
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
34
  }) : target, mod));
35
35
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
36
36
  //#endregion
37
- //#region ../types/dist/event-category-CaURIwQA.mjs
37
+ //#region ../types/dist/event-category-BmPm98n0.mjs
38
38
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
39
39
  EventCategory["SystemBoot"] = "system.boot";
40
40
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -518,6 +518,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
518
518
  EventCategory["RunningStarted"] = "running.started";
519
519
  /** Mirrors `runningStateCapability.events.onFinished`. */
520
520
  EventCategory["RunningFinished"] = "running.finished";
521
+ /** Mirrors `deviceRunsCapability.events.onRunOpened` - a durable run record opened. */
522
+ EventCategory["DeviceRunOpened"] = "device-run.opened";
523
+ /** Mirrors `deviceRunsCapability.events.onRunClosed` - a durable run record closed. */
524
+ EventCategory["DeviceRunClosed"] = "device-run.closed";
521
525
  /**
522
526
  * Cap event fired by every device that registers the `event-emitter`
523
527
  * capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
@@ -5866,7 +5870,7 @@ var ZodIssueCode = {
5866
5870
  var ZodFirstPartyTypeKind;
5867
5871
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5868
5872
  //#endregion
5869
- //#region ../types/dist/sleep-CSYuxssh.mjs
5873
+ //#region ../types/dist/sleep-BKzhQTSm.mjs
5870
5874
  /**
5871
5875
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5872
5876
  * window to float samples (D455).
@@ -9379,6 +9383,7 @@ var CAP_NAMES = Object.freeze([
9379
9383
  "device-ops",
9380
9384
  "device-provider",
9381
9385
  "device-removal-hook",
9386
+ "device-runs",
9382
9387
  "device-state",
9383
9388
  "device-status",
9384
9389
  "dishwasher",
@@ -11323,7 +11328,7 @@ object({
11323
11328
  params: record(string(), union([string(), number()])).optional()
11324
11329
  });
11325
11330
  //#endregion
11326
- //#region ../types/dist/composition-BBzVf2xd.mjs
11331
+ //#region ../types/dist/composition-BiKWlUCc.mjs
11327
11332
  /**
11328
11333
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11329
11334
  *
@@ -12256,7 +12261,15 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12256
12261
  thresholdWatts: number().positive(),
12257
12262
  startDwellMs: number().int().nonnegative(),
12258
12263
  stopDwellMs: number().int().nonnegative(),
12259
- member: HelperMemberSchema
12264
+ member: HelperMemberSchema,
12265
+ /**
12266
+ * The plug that powers the appliance (`switch.on`). Optional: when set, the
12267
+ * composition carries a `power-source` feature, so the hub can cut and restore
12268
+ * that plug (power-source link of a NEW-target composition).
12269
+ */
12270
+ powerSwitch: HelperMemberSchema.optional(),
12271
+ /** How long a forced restart keeps the plug off; the cap's default when unset. */
12272
+ offSeconds: number().optional()
12260
12273
  }),
12261
12274
  object({
12262
12275
  kind: literal("statistic"),
@@ -13654,7 +13667,13 @@ var MigrationLedgerEntrySchema = object({
13654
13667
  /** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
13655
13668
  reason: string().nullable(),
13656
13669
  nodeId: string().nullable(),
13657
- bootId: string().nullable()
13670
+ bootId: string().nullable(),
13671
+ /**
13672
+ * The catalogued meta of this id (description, body kind, store, `dependsOn`…),
13673
+ * joined by the engine from the catalogues it knows this boot. Absent when no
13674
+ * catalogue lists the id (a newer database).
13675
+ */
13676
+ meta: MigrationMetaSchema.optional()
13658
13677
  });
13659
13678
  /** A catalogued migration that is not applied, with where it stands. */
13660
13679
  var MigrationPendingEntrySchema = object({
@@ -19461,6 +19480,14 @@ var APPLIANCE_CAP_VOCABULARY = {
19461
19480
  }
19462
19481
  } }
19463
19482
  },
19483
+ "device-runs": {
19484
+ scope: "device",
19485
+ noun: t("Runs", "Esecuzioni"),
19486
+ events: {
19487
+ onRunOpened: { notOperatorFacing: "the durable record of a run that the running-state start flip already announces" },
19488
+ onRunClosed: { notOperatorFacing: "the durable record of a run that the running-state finish flip already announces" }
19489
+ }
19490
+ },
19464
19491
  update: {
19465
19492
  scope: "device",
19466
19493
  noun: t("Firmware update", "Aggiornamento firmware"),
@@ -46043,6 +46070,117 @@ function actuator(input) {
46043
46070
  };
46044
46071
  }
46045
46072
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
46073
+ var PowerSourceRefusalReasonSchema = _enum([
46074
+ "no-power-source",
46075
+ "switch-missing",
46076
+ "switch-offline",
46077
+ "switch-unknown",
46078
+ "restart-in-progress",
46079
+ "switch-call-failed"
46080
+ ]);
46081
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
46082
+ accepted: literal(false),
46083
+ reason: PowerSourceRefusalReasonSchema,
46084
+ /** One line for the operator, naming the switch. */
46085
+ message: string()
46086
+ })]);
46087
+ var PowerSourceInfoSchema = object({
46088
+ /** False when the device has no power source linked; the rest is then empty. */
46089
+ linked: boolean(),
46090
+ switchDeviceId: number().nullable(),
46091
+ switchName: string().nullable(),
46092
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
46093
+ switchOn: boolean().nullable(),
46094
+ switchOnline: boolean().nullable(),
46095
+ restartInProgress: boolean(),
46096
+ offSeconds: number().int()
46097
+ });
46098
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
46099
+ var PowerSourceStateSchema = object({
46100
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
46101
+ switchOn: boolean().nullable(),
46102
+ offSeconds: number().int(),
46103
+ lastFetchedAt: number()
46104
+ });
46105
+ var POWER_SOURCE_DEVICE_TYPES = [
46106
+ DeviceType.Gateway,
46107
+ DeviceType.Hub,
46108
+ DeviceType.Camera,
46109
+ DeviceType.Sensor,
46110
+ DeviceType.MediaPlayer,
46111
+ DeviceType.Vacuum,
46112
+ DeviceType.Generic,
46113
+ DeviceType.Washer,
46114
+ DeviceType.Dryer,
46115
+ DeviceType.Dishwasher,
46116
+ DeviceType.PetFountain,
46117
+ DeviceType.Fan,
46118
+ DeviceType.Humidifier,
46119
+ DeviceType.Climate,
46120
+ DeviceType.WaterHeater
46121
+ ];
46122
+ var powerSourceCustomize = {
46123
+ version: 1,
46124
+ targets: [{
46125
+ id: "switch",
46126
+ cardinality: "scalar",
46127
+ vocabulary: {
46128
+ cap: "power-source",
46129
+ stateField: "switchOn"
46130
+ },
46131
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
46132
+ apply: "add",
46133
+ path: "switchOn",
46134
+ sources: [{
46135
+ kind: "switch",
46136
+ transforms: [{ kind: "identity" }]
46137
+ }],
46138
+ companions: [{
46139
+ kind: "constant",
46140
+ path: "offSeconds",
46141
+ value: 10,
46142
+ when: "add"
46143
+ }, {
46144
+ kind: "clock",
46145
+ path: "lastFetchedAt",
46146
+ when: "always"
46147
+ }],
46148
+ sourceReport: { status: "assignment-status" }
46149
+ }]
46150
+ };
46151
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
46152
+ var powerSourceCapability = {
46153
+ name: "power-source",
46154
+ scope: "device",
46155
+ mode: "singleton",
46156
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
46157
+ methods: {
46158
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
46159
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
46160
+ kind: "mutation",
46161
+ auth: "admin"
46162
+ }),
46163
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
46164
+ kind: "mutation",
46165
+ auth: "admin"
46166
+ }),
46167
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
46168
+ kind: "mutation",
46169
+ auth: "admin"
46170
+ })
46171
+ },
46172
+ runtimeState: PowerSourceStateSchema,
46173
+ customize: powerSourceCustomize,
46174
+ /**
46175
+ * Runtime-state durability: **session** — mirrored from the linked switch on
46176
+ * its next report; a restored value would outlive the reading it names.
46177
+ *
46178
+ * See `RuntimeStateDurability`. Enforced by
46179
+ * `scripts/check-runtime-state-durability.ts`.
46180
+ */
46181
+ durability: "session",
46182
+ history: "none"
46183
+ };
46046
46184
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
46047
46185
  DeviceType.Camera;
46048
46186
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -47468,6 +47606,101 @@ method(object({ deviceId: number().int() }), array(SeriesInfoSchema).readonly())
47468
47606
  kind: "mutation",
47469
47607
  auth: "admin"
47470
47608
  });
47609
+ /**
47610
+ * device-runs - the durable record of ONE run of a device: a washing cycle, a
47611
+ * dishwasher cycle, later a mow or a clean (D992).
47612
+ *
47613
+ * A run is a DOCUMENT (opened at one instant, closed at another, carrying what
47614
+ * was measured over it), never a sample - device-history keeps the numbers over
47615
+ * time and its `device-runs` provider keeps these documents beside them, because
47616
+ * the energy of a run is a question about device-history's own series. The
47617
+ * record is written from the `running-state` flip on `DeviceStateChanged`; it
47618
+ * survives a restart (the open row is on disk) and a missed flip (boot compares
47619
+ * the open rows against the live snapshots).
47620
+ *
47621
+ * Every measurement is `number | null` and `null` means "could not be
47622
+ * measured", never `0` (D393). A run closed by the boot reconcile because the
47623
+ * device stopped while nobody was watching has `outcome: 'unknown'`.
47624
+ */
47625
+ var DeviceRunKindSchema = _enum([
47626
+ "appliance",
47627
+ "mower",
47628
+ "vacuum"
47629
+ ]);
47630
+ var DeviceRunStateSchema = _enum(["open", "closed"]);
47631
+ var DeviceRunOutcomeSchema = _enum([
47632
+ "completed",
47633
+ "aborted",
47634
+ "error",
47635
+ "unknown"
47636
+ ]);
47637
+ /** What opened or closed the run. `reconcile` is the boot comparison against live state. */
47638
+ var DeviceRunSourceSchema = _enum([
47639
+ "running-state",
47640
+ "activity",
47641
+ "reconcile"
47642
+ ]);
47643
+ /** How the energy of a run was obtained; `null` when it could not be. */
47644
+ var DeviceRunEnergyMethodSchema = _enum(["counter-delta", "history-integral"]);
47645
+ var DeviceRunPhaseSchema = object({
47646
+ phase: string(),
47647
+ at: number()
47648
+ });
47649
+ /** The kind-specific block; a later phase adds a branch, nothing else changes. */
47650
+ var DeviceRunDetailSchema = discriminatedUnion("kind", [object({
47651
+ kind: literal("appliance"),
47652
+ /** kWh used over the run. */
47653
+ energyKwh: number().nullable().meta(readingMeta("energy")),
47654
+ energyMethod: DeviceRunEnergyMethodSchema.nullable(),
47655
+ /** Highest power seen while the run was open. */
47656
+ peakW: number().nullable().meta(readingMeta("power")),
47657
+ /** Time-weighted mean power while the run was open. */
47658
+ avgW: number().nullable().meta(readingMeta("power")),
47659
+ /** The program the appliance said it ran when the run opened. */
47660
+ program: string().nullable(),
47661
+ remainingAtStartSec: number().nullable().meta(readingMeta("duration")),
47662
+ /** The last phase the device reported before the run closed. */
47663
+ finalPhase: string().nullable(),
47664
+ /**
47665
+ * The meter's energy counter when the run opened; `null` when the meter had
47666
+ * not said (or the run was opened late, by the boot reconcile). It lives in
47667
+ * the row so a run that outlives a restart still ends with `end - start`.
47668
+ */
47669
+ energyStartKwh: number().nullable().meta(readingMeta("energy")),
47670
+ /** Milliseconds of power readings behind `avgW` - the weight a restart resumes the mean with. */
47671
+ powerObservedMs: number()
47672
+ })]);
47673
+ var DeviceRunSchema = object({
47674
+ /** `${deviceId}:${startedAt}`. */
47675
+ runId: string(),
47676
+ deviceId: number().int(),
47677
+ kind: DeviceRunKindSchema,
47678
+ state: DeviceRunStateSchema,
47679
+ startedAt: number(),
47680
+ endedAt: number().nullable(),
47681
+ durationMs: number().nullable(),
47682
+ outcome: DeviceRunOutcomeSchema.nullable(),
47683
+ startSource: DeviceRunSourceSchema,
47684
+ endSource: DeviceRunSourceSchema.nullable(),
47685
+ /** Where the device's running bit came from (`running-state.source`); `null` when it did not say. */
47686
+ runningSource: _enum(["native", "assigned"]).nullable(),
47687
+ phases: array(DeviceRunPhaseSchema),
47688
+ detail: DeviceRunDetailSchema
47689
+ });
47690
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, method(object({
47691
+ deviceId: number().int(),
47692
+ outcome: DeviceRunOutcomeSchema.optional(),
47693
+ atFrom: number().optional(),
47694
+ atTo: number().optional(),
47695
+ limit: number().int().positive().max(200).default(50),
47696
+ cursor: string().optional()
47697
+ }), object({
47698
+ runs: array(DeviceRunSchema).readonly(),
47699
+ nextCursor: string().nullable()
47700
+ })), method(object({
47701
+ deviceId: number().int(),
47702
+ runId: string()
47703
+ }), DeviceRunSchema.nullable()), method(object({ deviceId: number().int() }), DeviceRunSchema.nullable());
47471
47704
  var DeviceCaStatusSchema = discriminatedUnion("state", [
47472
47705
  object({ state: literal("absent") }),
47473
47706
  object({
@@ -47603,8 +47836,8 @@ method(object({}), object({
47603
47836
  });
47604
47837
  var KeychainKeySourceSchema = object({
47605
47838
  kind: _enum([
47606
- "config-file",
47607
- "previous-config-file",
47839
+ "mounted-file",
47840
+ "previous-mounted-file",
47608
47841
  "platform",
47609
47842
  "default-file"
47610
47843
  ]),
@@ -52100,7 +52333,8 @@ var PALETTE_ENTRY_IDS = [
52100
52333
  "switch",
52101
52334
  "siren",
52102
52335
  "button",
52103
- "doorbell"
52336
+ "doorbell",
52337
+ "appliance-cycle"
52104
52338
  ];
52105
52339
  var AUDIO_ENTRY_ID = "audio";
52106
52340
  /** Six rungs, quiet → loud. The opaque end is the entry's hex. */
@@ -52171,7 +52405,8 @@ var CAMSTACK_HEX = {
52171
52405
  switch: "#7a812a",
52172
52406
  siren: "#ac5d7f",
52173
52407
  button: "#b276cd",
52174
- doorbell: "#0087ab"
52408
+ doorbell: "#0087ab",
52409
+ "appliance-cycle": "#2a8c8c"
52175
52410
  };
52176
52411
  var CAMSTACK_FALLBACK = [
52177
52412
  "#b34dc7",
@@ -52236,7 +52471,8 @@ var HIGH_CONTRAST_HEX = {
52236
52471
  switch: "#d05259",
52237
52472
  siren: "#cd67b3",
52238
52473
  button: "#c9746a",
52239
- doorbell: "#338eeb"
52474
+ doorbell: "#338eeb",
52475
+ "appliance-cycle": "#00848a"
52240
52476
  };
52241
52477
  var HIGH_CONTRAST_FALLBACK = [
52242
52478
  "#817f3b",
@@ -52301,7 +52537,8 @@ var OKABE_ITO_HEX = {
52301
52537
  switch: "#ad2c4e",
52302
52538
  siren: "#348e08",
52303
52539
  button: "#d27303",
52304
- doorbell: "#55770f"
52540
+ doorbell: "#55770f",
52541
+ "appliance-cycle": "#00737d"
52305
52542
  };
52306
52543
  var OKABE_ITO_FALLBACK = [
52307
52544
  "#9a3784",
@@ -52510,6 +52747,7 @@ sub("switch", "control", "control", "switch", "Switch");
52510
52747
  sub("siren", "control", "control", "siren", "Siren");
52511
52748
  sub("button", "control", "control", "button", "Button");
52512
52749
  sub("doorbell", "control", "control", "doorbell", "Doorbell");
52750
+ sub("appliance-cycle", "control", "control", "switch", "Appliance cycle");
52513
52751
  for (const l of AUDIO_MACRO_LABELS) {
52514
52752
  const kind = `audio-${l.id}`;
52515
52753
  if (entries.has(kind)) continue;
@@ -61141,7 +61379,8 @@ var CAP_TO_KIND = {
61141
61379
  cover: "cover",
61142
61380
  switch: "switch",
61143
61381
  button: "button",
61144
- doorbell: "doorbell"
61382
+ doorbell: "doorbell",
61383
+ "running-state": "appliance-cycle"
61145
61384
  };
61146
61385
  function buildDescriptor(capName, kind) {
61147
61386
  const t = EVENT_TAXONOMY[kind];
@@ -64271,10 +64510,7 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
64271
64510
  }), InstallPackageResultSchema, {
64272
64511
  kind: "mutation",
64273
64512
  auth: "admin"
64274
- }), method(object({ packageName: string() }), InstallPackageResultSchema, {
64275
- kind: "mutation",
64276
- auth: "admin"
64277
- }), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
64513
+ }), method(object({
64278
64514
  packageName: string().min(1),
64279
64515
  nodeId: string().optional(),
64280
64516
  everywhere: boolean().optional()
@@ -65535,19 +65771,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
65535
65771
  reason: _enum(["unknown-template", "not-a-media-player"]),
65536
65772
  detail: string()
65537
65773
  })]);
65538
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
65774
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
65539
65775
  DeviceType.MediaPlayer;
65540
- method(deviceInput$1, RemoteConfigSchema, { auth: "admin" }), method(deviceInput$1.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65776
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
65541
65777
  kind: "mutation",
65542
65778
  auth: "admin"
65543
- }), method(deviceInput$1, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput$1.extend({
65779
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
65544
65780
  role: RemoteRoleSchema,
65545
65781
  /** Sends in a row (volume steps). */
65546
65782
  repeat: number().int().min(1).max(50).default(1)
65547
65783
  }), RemoteSendResultSchema, {
65548
65784
  kind: "mutation",
65549
65785
  auth: "protected"
65550
- }), method(deviceInput$1.extend({
65786
+ }), method(deviceInput.extend({
65551
65787
  command: RemoteMediaCommandSchema,
65552
65788
  muted: boolean().optional(),
65553
65789
  source: string().optional(),
@@ -66415,6 +66651,60 @@ onColorChanged: { data: object({
66415
66651
  */
66416
66652
  history: "none"
66417
66653
  };
66654
+ /**
66655
+ * `connection-test` — pre-creation validation of an integration's settings,
66656
+ * system-scoped collection (one provider per integration addon).
66657
+ *
66658
+ * ── Why this cap exists ─────────────────────────────────────────────────────
66659
+ *
66660
+ * Before it, `integrations.testConnection` had exactly two behaviours: a broker
66661
+ * branch (`settings.brokerId` → `broker.testConnection`) and a DEFAULT branch
66662
+ * that probed `settings.main_stream_url ?? settings.url` with ffprobe. Every
66663
+ * account-based integration — Dreo, Dreame, Tuya, Petkit, Wyze — fell into that
66664
+ * default and was told `{"success":false,"error":"No stream URL provided"}`.
66665
+ * That answer neither validated nor refused anything, so:
66666
+ *
66667
+ * - the Test button had NEVER worked for an account integration, and
66668
+ * - `integrations.create` had nothing to gate on, so an integration with a
66669
+ * wrong password was created happily and failed later, silently, at
66670
+ * reconcile time.
66671
+ *
66672
+ * The structural gap was that at CREATE time the integration does not exist
66673
+ * yet: there is no `integrationId`, no `brokerId`, no live client — and every
66674
+ * existing provider surface (`device-provider`, `device-adoption`) is keyed by
66675
+ * one of those. There was no way to ask "are these settings valid?" before the
66676
+ * row existed.
66677
+ *
66678
+ * ── The contract ────────────────────────────────────────────────────────────
66679
+ *
66680
+ * The provider knows how to validate its OWN settings. The framework does not
66681
+ * guess from the shape of the settings — a stream-URL probe is one provider's
66682
+ * implementation of this cap, not the universal fallback. A provider that does
66683
+ * not register this cap is a KNOWN "cannot validate", never a silent pass.
66684
+ *
66685
+ * `testSettings` takes the candidate settings blob exactly as the wizard's
66686
+ * config step collected it (the same blob that would be handed to
66687
+ * `integrations.create`), and MUST NOT persist anything, mint an integration,
66688
+ * or mutate live state. It opens a throwaway session, asks, and closes it.
66689
+ *
66690
+ * ── The three outcomes are NOT interchangeable ──────────────────────────────
66691
+ *
66692
+ * This is the whole point of the discriminated union: a `null` from a timeout
66693
+ * must never look like a `null` from a refusal.
66694
+ *
66695
+ * - `validated` — the remote ACCEPTED these credentials. Observed.
66696
+ * - `rejected` — the remote REFUSED these credentials. Observed.
66697
+ * This is the ONLY outcome that blocks creation.
66698
+ * - `inconclusive` — the check could not complete (DNS, timeout, 5xx, an
66699
+ * unexpected shape). NOTHING was observed about the
66700
+ * credentials. Never rendered, or counted, as a failure.
66701
+ *
66702
+ * A provider that cannot tell a refusal from a transport fault must return
66703
+ * `inconclusive`. Guessing `rejected` would block creation on a flaky network;
66704
+ * guessing `validated` would wave a wrong password through.
66705
+ */
66706
+ /** Ceiling on how long a pre-creation probe may hold the wizard. */
66707
+ var CONNECTION_TEST_TIMEOUT_MS = 2e4;
66418
66708
  var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
66419
66709
  object({
66420
66710
  outcome: literal("validated"),
@@ -69867,117 +70157,6 @@ var lockControlCapability = {
69867
70157
  * whether persisting is worth a SQLite commit. */
69868
70158
  volatileStateFields: ["lastChangedAt"]
69869
70159
  };
69870
- var PowerSourceRefusalReasonSchema = _enum([
69871
- "no-power-source",
69872
- "switch-missing",
69873
- "switch-offline",
69874
- "switch-unknown",
69875
- "restart-in-progress",
69876
- "switch-call-failed"
69877
- ]);
69878
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
69879
- accepted: literal(false),
69880
- reason: PowerSourceRefusalReasonSchema,
69881
- /** One line for the operator, naming the switch. */
69882
- message: string()
69883
- })]);
69884
- var PowerSourceInfoSchema = object({
69885
- /** False when the device has no power source linked; the rest is then empty. */
69886
- linked: boolean(),
69887
- switchDeviceId: number().nullable(),
69888
- switchName: string().nullable(),
69889
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
69890
- switchOn: boolean().nullable(),
69891
- switchOnline: boolean().nullable(),
69892
- restartInProgress: boolean(),
69893
- offSeconds: number().int()
69894
- });
69895
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
69896
- var PowerSourceStateSchema = object({
69897
- /** Null while the switch has not reported: unknown is never `false` (D393). */
69898
- switchOn: boolean().nullable(),
69899
- offSeconds: number().int(),
69900
- lastFetchedAt: number()
69901
- });
69902
- var POWER_SOURCE_DEVICE_TYPES = [
69903
- DeviceType.Gateway,
69904
- DeviceType.Hub,
69905
- DeviceType.Camera,
69906
- DeviceType.Sensor,
69907
- DeviceType.MediaPlayer,
69908
- DeviceType.Vacuum,
69909
- DeviceType.Generic,
69910
- DeviceType.Washer,
69911
- DeviceType.Dryer,
69912
- DeviceType.Dishwasher,
69913
- DeviceType.PetFountain,
69914
- DeviceType.Fan,
69915
- DeviceType.Humidifier,
69916
- DeviceType.Climate,
69917
- DeviceType.WaterHeater
69918
- ];
69919
- var powerSourceCustomize = {
69920
- version: 1,
69921
- targets: [{
69922
- id: "switch",
69923
- cardinality: "scalar",
69924
- vocabulary: {
69925
- cap: "power-source",
69926
- stateField: "switchOn"
69927
- },
69928
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69929
- apply: "add",
69930
- path: "switchOn",
69931
- sources: [{
69932
- kind: "switch",
69933
- transforms: [{ kind: "identity" }]
69934
- }],
69935
- companions: [{
69936
- kind: "constant",
69937
- path: "offSeconds",
69938
- value: 10,
69939
- when: "add"
69940
- }, {
69941
- kind: "clock",
69942
- path: "lastFetchedAt",
69943
- when: "always"
69944
- }],
69945
- sourceReport: { status: "assignment-status" }
69946
- }]
69947
- };
69948
- var deviceInput = object({ deviceId: number().int().nonnegative() });
69949
- var powerSourceCapability = {
69950
- name: "power-source",
69951
- scope: "device",
69952
- mode: "singleton",
69953
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
69954
- methods: {
69955
- getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
69956
- powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69957
- kind: "mutation",
69958
- auth: "admin"
69959
- }),
69960
- powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69961
- kind: "mutation",
69962
- auth: "admin"
69963
- }),
69964
- forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
69965
- kind: "mutation",
69966
- auth: "admin"
69967
- })
69968
- },
69969
- runtimeState: PowerSourceStateSchema,
69970
- customize: powerSourceCustomize,
69971
- /**
69972
- * Runtime-state durability: **session** — mirrored from the linked switch on
69973
- * its next report; a restored value would outlive the reading it names.
69974
- *
69975
- * See `RuntimeStateDurability`. Enforced by
69976
- * `scripts/check-runtime-state-durability.ts`.
69977
- */
69978
- durability: "session",
69979
- history: "none"
69980
- };
69981
70160
  /**
69982
70161
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69983
70162
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -73519,6 +73698,10 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
73519
73698
  /** Generic Customize target for the running boolean (D936). */
73520
73699
  var runningCustomize = {
73521
73700
  version: 1,
73701
+ overridable: [{
73702
+ path: "running",
73703
+ by: "running"
73704
+ }],
73522
73705
  targets: [{
73523
73706
  id: "running",
73524
73707
  cardinality: "scalar",
@@ -73576,7 +73759,7 @@ var runningCustomize = {
73576
73759
  kind: "constant",
73577
73760
  path: "phase",
73578
73761
  value: null,
73579
- when: "always"
73762
+ when: "add"
73580
73763
  },
73581
73764
  {
73582
73765
  kind: "clock",
@@ -76886,7 +77069,7 @@ var ExternalIdentityKeySchema = object({
76886
77069
  provider: string().min(1),
76887
77070
  subject: string().min(1)
76888
77071
  });
76889
- method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserInputSchema, UserSummarySchema, {
77072
+ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), object({ defaultPasswordInUse: boolean() }), { auth: "admin" }), method(CreateUserInputSchema, UserSummarySchema, {
76890
77073
  kind: "mutation",
76891
77074
  auth: "admin",
76892
77075
  access: "create"
@@ -80225,13 +80408,6 @@ Object.freeze({
80225
80408
  access: "view",
80226
80409
  role: "admin"
80227
80410
  },
80228
- "addons.installFromWorkspace": {
80229
- capName: "addons",
80230
- capScope: "system",
80231
- addonId: null,
80232
- access: "create",
80233
- role: "admin"
80234
- },
80235
80411
  "addons.installPackage": {
80236
80412
  capName: "addons",
80237
80413
  capScope: "system",
@@ -80239,13 +80415,6 @@ Object.freeze({
80239
80415
  access: "create",
80240
80416
  role: "admin"
80241
80417
  },
80242
- "addons.isWorkspaceAvailable": {
80243
- capName: "addons",
80244
- capScope: "system",
80245
- addonId: null,
80246
- access: "view",
80247
- role: "admin"
80248
- },
80249
80418
  "addons.list": {
80250
80419
  capName: "addons",
80251
80420
  capScope: "system",
@@ -80295,13 +80464,6 @@ Object.freeze({
80295
80464
  access: "view",
80296
80465
  role: "admin"
80297
80466
  },
80298
- "addons.listWorkspacePackages": {
80299
- capName: "addons",
80300
- capScope: "system",
80301
- addonId: null,
80302
- access: "view",
80303
- role: "admin"
80304
- },
80305
80467
  "addons.onAddonLogs": {
80306
80468
  capName: "addons",
80307
80469
  capScope: "system",
@@ -82969,6 +83131,27 @@ Object.freeze({
82969
83131
  access: "create",
82970
83132
  role: "admin"
82971
83133
  },
83134
+ "deviceRuns.getCurrentRun": {
83135
+ capName: "device-runs",
83136
+ capScope: "device",
83137
+ addonId: null,
83138
+ access: "view",
83139
+ role: "viewer"
83140
+ },
83141
+ "deviceRuns.getRun": {
83142
+ capName: "device-runs",
83143
+ capScope: "device",
83144
+ addonId: null,
83145
+ access: "view",
83146
+ role: "viewer"
83147
+ },
83148
+ "deviceRuns.listRuns": {
83149
+ capName: "device-runs",
83150
+ capScope: "device",
83151
+ addonId: null,
83152
+ access: "view",
83153
+ role: "viewer"
83154
+ },
82972
83155
  "deviceState.claimFields": {
82973
83156
  capName: "device-state",
82974
83157
  capScope: "system",
@@ -89682,6 +89865,13 @@ Object.freeze({
89682
89865
  access: "view",
89683
89866
  role: "admin"
89684
89867
  },
89868
+ "userManagement.getAdminBootstrapStatus": {
89869
+ capName: "user-management",
89870
+ capScope: "system",
89871
+ addonId: null,
89872
+ access: "view",
89873
+ role: "admin"
89874
+ },
89685
89875
  "userManagement.getTotpStatus": {
89686
89876
  capName: "user-management",
89687
89877
  capScope: "system",
@@ -91167,6 +91357,21 @@ Object.freeze({
91167
91357
  form: "single",
91168
91358
  optional: false
91169
91359
  }],
91360
+ "deviceRuns.getCurrentRun": [{
91361
+ name: "deviceId",
91362
+ form: "single",
91363
+ optional: false
91364
+ }],
91365
+ "deviceRuns.getRun": [{
91366
+ name: "deviceId",
91367
+ form: "single",
91368
+ optional: false
91369
+ }],
91370
+ "deviceRuns.listRuns": [{
91371
+ name: "deviceId",
91372
+ form: "single",
91373
+ optional: false
91374
+ }],
91170
91375
  "deviceState.claimFields": [{
91171
91376
  name: "deviceId",
91172
91377
  form: "single",
@@ -93649,13 +93854,114 @@ NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
93649
93854
  *
93650
93855
  * Pure, RN-safe: the viewer imports the support rule from here.
93651
93856
  */
93857
+ /**
93858
+ * Control caps that still have a raise and a clear an operator names: an
93859
+ * appliance cycle raises when it STARTS and clears when it FINISHES. Kept out
93860
+ * of `SOURCE_CAP_ACTIVE_FIELD` on purpose — that table also makes a device a
93861
+ * camera-sensor / recorder-trigger source, and a dishwasher is neither.
93862
+ */
93863
+ var CYCLE_RAISED_FIELD_BY_CAP = { "running-state": "running" };
93652
93864
  /** Event kind → the boolean field of its cap slice that means "raised". */
93653
93865
  var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.entries(EVENT_KIND_BY_CAP).flatMap(([capName, descriptor]) => {
93866
+ const cycleField = CYCLE_RAISED_FIELD_BY_CAP[capName];
93867
+ if (cycleField !== void 0) return [[descriptor.kind, cycleField]];
93654
93868
  const field = SOURCE_CAP_ACTIVE_FIELD[capName];
93655
93869
  if (field === void 0 || descriptor.category !== "sensor") return [];
93656
93870
  return [[descriptor.kind, field]];
93657
93871
  })));
93658
93872
  Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
93873
+ Object.freeze({
93874
+ contact: {
93875
+ starts: {
93876
+ en: "opened",
93877
+ it: "si apre"
93878
+ },
93879
+ finishes: {
93880
+ en: "closed",
93881
+ it: "si chiude"
93882
+ }
93883
+ },
93884
+ "motion-sensor": {
93885
+ starts: {
93886
+ en: "motion detected",
93887
+ it: "movimento rilevato"
93888
+ },
93889
+ finishes: {
93890
+ en: "motion stopped",
93891
+ it: "movimento terminato"
93892
+ }
93893
+ },
93894
+ smoke: {
93895
+ starts: {
93896
+ en: "smoke detected",
93897
+ it: "fumo rilevato"
93898
+ },
93899
+ finishes: {
93900
+ en: "smoke gone",
93901
+ it: "fumo scomparso"
93902
+ }
93903
+ },
93904
+ flood: {
93905
+ starts: {
93906
+ en: "water detected",
93907
+ it: "acqua rilevata"
93908
+ },
93909
+ finishes: {
93910
+ en: "dry again",
93911
+ it: "di nuovo asciutto"
93912
+ }
93913
+ },
93914
+ gas: {
93915
+ starts: {
93916
+ en: "gas detected",
93917
+ it: "gas rilevato"
93918
+ },
93919
+ finishes: {
93920
+ en: "gas gone",
93921
+ it: "gas scomparso"
93922
+ }
93923
+ },
93924
+ "carbon-monoxide": {
93925
+ starts: {
93926
+ en: "carbon monoxide detected",
93927
+ it: "monossido rilevato"
93928
+ },
93929
+ finishes: {
93930
+ en: "carbon monoxide gone",
93931
+ it: "monossido scomparso"
93932
+ }
93933
+ },
93934
+ vibration: {
93935
+ starts: {
93936
+ en: "vibration detected",
93937
+ it: "vibrazione rilevata"
93938
+ },
93939
+ finishes: {
93940
+ en: "vibration stopped",
93941
+ it: "vibrazione terminata"
93942
+ }
93943
+ },
93944
+ tamper: {
93945
+ starts: {
93946
+ en: "tampered with",
93947
+ it: "manomesso"
93948
+ },
93949
+ finishes: {
93950
+ en: "tamper cleared",
93951
+ it: "manomissione rientrata"
93952
+ }
93953
+ },
93954
+ "appliance-cycle": {
93955
+ starts: {
93956
+ en: "the cycle started",
93957
+ it: "il ciclo parte"
93958
+ },
93959
+ finishes: {
93960
+ en: "the cycle finished",
93961
+ it: "il ciclo è terminato"
93962
+ }
93963
+ }
93964
+ });
93659
93965
  object({
93660
93966
  phase: _enum(["raised", "resolved"]),
93661
93967
  episodeId: string().min(1),
@@ -93862,6 +94168,10 @@ var STATE_SUMMARY_VARS = [
93862
94168
  [...RULE_TEXT],
93863
94169
  [...RULE_TEXT],
93864
94170
  [...RULE_TEXT],
94171
+ [...RULE_TEXT],
94172
+ [...RULE_TEXT],
94173
+ [...RULE_TEXT],
94174
+ [...RULE_TEXT],
93865
94175
  [...RULE_BODY_ONLY],
93866
94176
  [...RULE_TEXT],
93867
94177
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -93879,6 +94189,7 @@ var STATE_SUMMARY_VARS = [
93879
94189
  [...RULE_TEXT],
93880
94190
  [...RULE_TEXT],
93881
94191
  [...RULE_TEXT],
94192
+ [...RULE_TEXT],
93882
94193
  ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
93883
94194
  [...RULE_TEXT],
93884
94195
  [...RULE_TEXT],
@@ -94123,6 +94434,7 @@ new Set([
94123
94434
  "siren",
94124
94435
  "button",
94125
94436
  "doorbell",
94437
+ "appliance-cycle",
94126
94438
  "audio-speech",
94127
94439
  "audio-scream",
94128
94440
  "audio-crying",
@@ -103432,8 +103744,90 @@ var VeluxClientRegistry = class {
103432
103744
  };
103433
103745
  //#endregion
103434
103746
  //#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;
103747
+ /**
103748
+ * Defaults add up, worst case, to 12 + 3 + 1.5 + 1.5 = 18 s, which is the
103749
+ * wizard's `CONNECTION_TEST_TIMEOUT_MS` ceiling (20 s) minus the 2 s margin of
103750
+ * `DEFAULT_TEST_DEADLINE_MS`, so a stuck gateway is named here instead of the
103751
+ * wizard giving up unnamed. The handshake gets 12 s because a real KLF-200 needs
103752
+ * about 4.2 s for ONE handshake (TLSv1.2, DHE-RSA-AES256-SHA256 on a weak CPU).
103753
+ *
103754
+ * A probe opens exactly ONE connection: the gateway frees a closed slot very
103755
+ * slowly (measured 2026-11-01: a bare TCP connect and close before the TLS connect
103756
+ * left the real session without a handshake for 20 s), so no pre-check connects first.
103757
+ */
103758
+ var DEFAULT_KLF_PROBE_TIMEOUTS_MS = {
103759
+ tls: 12e3,
103760
+ password: 3e3,
103761
+ version: 1500,
103762
+ logout: 1500
103763
+ };
103764
+ var PHASE_TEXT = {
103765
+ tls: "did not accept a connection or complete the TLS handshake on port 51200",
103766
+ password: "did not answer the password",
103767
+ version: "did not answer the version request",
103768
+ logout: "did not close the session"
103769
+ };
103770
+ /** A probe step ran out of time. Names the step, so a bound is never a silent hang. */
103771
+ var KlfProbeTimeoutError = class extends Error {
103772
+ phase;
103773
+ host;
103774
+ limitMs;
103775
+ constructor(phase, host, limitMs) {
103776
+ super(`The gateway ${host} ${PHASE_TEXT[phase]} within ${Math.round(limitMs / 100) / 10}s (timed out)`);
103777
+ this.phase = phase;
103778
+ this.host = host;
103779
+ this.limitMs = limitMs;
103780
+ this.name = "KlfProbeTimeoutError";
103781
+ }
103782
+ };
103783
+ async function withDeadline(work, ms, onTimeout) {
103784
+ let timer;
103785
+ try {
103786
+ return await Promise.race([work, new Promise((_resolve, reject) => {
103787
+ timer = setTimeout(() => reject(onTimeout()), ms);
103788
+ })]);
103789
+ } finally {
103790
+ clearTimeout(timer);
103791
+ }
103792
+ }
103793
+ /** How often the login watcher checks whether the handshake has finished. */
103794
+ var PHASE_POLL_MS = 20;
103795
+ /**
103796
+ * The library's `loginAsync` runs the TLS handshake and the password answer as one
103797
+ * call. `KLF200SocketProtocol` appears when the handshake is done and the password
103798
+ * is on its way, so watching it splits the call into two bounded, named steps.
103799
+ */
103800
+ async function loginInPhases(connection, host, password, limits) {
103801
+ let handshakeDone = false;
103802
+ let passwordTimer;
103803
+ let tlsTimer;
103804
+ let poll;
103805
+ const tlsError = () => new KlfProbeTimeoutError("tls", host, limits.tls);
103806
+ const passwordError = () => new KlfProbeTimeoutError("password", host, limits.password);
103807
+ const bounds = new Promise((_resolve, reject) => {
103808
+ tlsTimer = setTimeout(() => reject(tlsError()), limits.tls);
103809
+ poll = setInterval(() => {
103810
+ if (handshakeDone || connection.KLF200SocketProtocol === void 0) return;
103811
+ handshakeDone = true;
103812
+ clearTimeout(tlsTimer);
103813
+ clearInterval(poll);
103814
+ passwordTimer = setTimeout(() => reject(passwordError()), limits.password);
103815
+ }, PHASE_POLL_MS);
103816
+ });
103817
+ const libraryS = (limits.tls + limits.password) / 1e3;
103818
+ try {
103819
+ await Promise.race([connection.loginAsync(password, libraryS), bounds]);
103820
+ } catch (err) {
103821
+ if (err instanceof KlfProbeTimeoutError) throw err;
103822
+ if (/^timeout$/i.test(errMsg(err)) || err instanceof Error && err.name === "TimeoutError") throw handshakeDone ? passwordError() : tlsError();
103823
+ throw err;
103824
+ } finally {
103825
+ clearTimeout(tlsTimer);
103826
+ clearTimeout(passwordTimer);
103827
+ clearInterval(poll);
103828
+ bounds.catch(() => void 0);
103829
+ }
103830
+ }
103437
103831
  /**
103438
103832
  * Opens ONE session, authenticates, reads the firmware version and closes the
103439
103833
  * session again, whatever happens. The gateway accepts two sessions in total, so
@@ -103441,28 +103835,44 @@ var PROBE_LOGIN_TIMEOUT_S = 15;
103441
103835
  * list. A refused password rejects with `VeluxLoginRefusedError`.
103442
103836
  */
103443
103837
  async function probeKlfGateway(options) {
103838
+ const limits = {
103839
+ ...DEFAULT_KLF_PROBE_TIMEOUTS_MS,
103840
+ ...options.timeoutsMs
103841
+ };
103444
103842
  const connection = options.tls === void 0 ? new Connection(options.host) : new Connection(options.host, options.tls);
103445
103843
  try {
103446
103844
  try {
103447
- await connection.loginAsync(options.password, PROBE_LOGIN_TIMEOUT_S);
103845
+ await loginInPhases(connection, options.host, options.password, limits);
103448
103846
  } catch (err) {
103847
+ if (err instanceof KlfProbeTimeoutError) throw err;
103848
+ if (err instanceof Error && typeof Reflect.get(err, "code") === "string") throw err;
103449
103849
  throw loginFailure(err);
103450
103850
  }
103451
103851
  try {
103452
- const { SoftwareVersion } = await new Gateway(connection).getVersionAsync();
103852
+ const { SoftwareVersion } = await withDeadline(new Gateway(connection).getVersionAsync(), limits.version, () => new KlfProbeTimeoutError("version", options.host, limits.version));
103453
103853
  return { firmware: SoftwareVersion.toString() };
103454
103854
  } catch {
103455
103855
  return { firmware: null };
103456
103856
  }
103457
103857
  } finally {
103458
103858
  try {
103459
- await connection.logoutAsync();
103859
+ await withDeadline(connection.logoutAsync(limits.logout / 1e3), limits.logout + 500, () => new KlfProbeTimeoutError("logout", options.host, limits.logout));
103460
103860
  } catch (err) {
103461
103861
  errMsg(err);
103862
+ await connection[Symbol.asyncDispose]().catch(() => void 0);
103462
103863
  }
103463
103864
  }
103464
103865
  }
103465
103866
  var TEST_LABEL = "Logs in to the gateway with this password, then closes the session";
103867
+ /** Under `CONNECTION_TEST_TIMEOUT_MS`, so the reason reaches the operator first. */
103868
+ var DEFAULT_TEST_DEADLINE_MS = CONNECTION_TEST_TIMEOUT_MS - 2e3;
103869
+ /** The whole test outlived its ceiling; no step of the probe answered in time. */
103870
+ var KlfTestDeadlineError = class extends Error {
103871
+ constructor(host, limitMs) {
103872
+ super(`The gateway ${host} did not answer within ${Math.round(limitMs / 1e3)}s (timed out)`);
103873
+ this.name = "KlfTestDeadlineError";
103874
+ }
103875
+ };
103466
103876
  var UNREACHABLE_CODES = new Set([
103467
103877
  "ECONNREFUSED",
103468
103878
  "EHOSTUNREACH",
@@ -103484,6 +103894,10 @@ function classifyProbeFailure(err) {
103484
103894
  kind: "wrong-password",
103485
103895
  text: err.message
103486
103896
  };
103897
+ if (err instanceof KlfProbeTimeoutError || err instanceof KlfTestDeadlineError) return {
103898
+ kind: "unreachable",
103899
+ text: err.message
103900
+ };
103487
103901
  const text = typeof err === "string" ? err : errMsg(err);
103488
103902
  const code = typeof err === "object" && err !== null ? Reflect.get(err, "code") : void 0;
103489
103903
  if (typeof code === "string" && UNREACHABLE_CODES.has(code)) return {
@@ -103508,7 +103922,7 @@ function classifyProbeFailure(err) {
103508
103922
  };
103509
103923
  }
103510
103924
  function buildVeluxConnectionTestProvider(deps) {
103511
- const { logger, probe = probeKlfGateway, liveSessions = () => 0 } = deps;
103925
+ const { logger, probe = probeKlfGateway, liveSessions = () => 0, deadlineMs = DEFAULT_TEST_DEADLINE_MS } = deps;
103512
103926
  /** The tail of the test queue per host: a second test waits for the first to log out. */
103513
103927
  const queues = /* @__PURE__ */ new Map();
103514
103928
  async function run(host, password) {
@@ -103524,10 +103938,13 @@ function buildVeluxConnectionTestProvider(deps) {
103524
103938
  }
103525
103939
  const startedAt = Date.now();
103526
103940
  try {
103527
- const result = await probe({
103941
+ let timer;
103942
+ const result = await Promise.race([probe({
103528
103943
  host,
103529
103944
  password
103530
- });
103945
+ }), new Promise((_resolve, reject) => {
103946
+ timer = setTimeout(() => reject(new KlfTestDeadlineError(host, deadlineMs)), deadlineMs);
103947
+ })]).finally(() => clearTimeout(timer));
103531
103948
  return {
103532
103949
  outcome: "validated",
103533
103950
  latencyMs: Date.now() - startedAt,
@@ -103762,7 +104179,12 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
103762
104179
  ],
103763
104180
  customActions: veluxActions,
103764
104181
  actionHandlers: { discoverGateway: async () => {
104182
+ const startedAt = Date.now();
103765
104183
  const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
104184
+ this.ctx.logger.info("Velux gateway scan finished", { meta: {
104185
+ found: candidates.length,
104186
+ ms: Date.now() - startedAt
104187
+ } });
103766
104188
  const hosts = candidates.map((candidate) => ({
103767
104189
  host: String(candidate.prefilledConfig["host"] ?? ""),
103768
104190
  name: candidate.suggestedName,