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