@camstack/addon-provider-velux 0.2.114 → 0.2.116

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.
Files changed (3) hide show
  1. package/dist/addon.js +3537 -33
  2. package/dist/addon.mjs +3537 -33
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -5359,7 +5359,7 @@ var ZodIssueCode = {
5359
5359
  var ZodFirstPartyTypeKind;
5360
5360
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5361
  //#endregion
5362
- //#region ../types/dist/sleep-Bs3xPtSh.mjs
5362
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
5363
5363
  /**
5364
5364
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5365
  * window to float samples (D455).
@@ -6501,6 +6501,17 @@ var CamProfileSchema = _enum([
6501
6501
  "mid",
6502
6502
  "low"
6503
6503
  ]);
6504
+ /**
6505
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
6506
+ *
6507
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
6508
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
6509
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
6510
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
6511
+ * the operator picked. Today's one user is the audio analyser, whose rule is
6512
+ * "the lowest AVAILABLE native rung" (D537).
6513
+ */
6514
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
6504
6515
  var CamStreamKindSchema = _enum([
6505
6516
  "pull-rtsp",
6506
6517
  "pull-rtmp",
@@ -6919,6 +6930,18 @@ function systemMethod(input, output, options) {
6919
6930
  systemOnly: true
6920
6931
  };
6921
6932
  }
6933
+ /**
6934
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
6935
+ * on `InferProvider`. The `providerOptional: true` literal is what
6936
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
6937
+ * for when this is the honest answer and when it is a soft stub.
6938
+ */
6939
+ function optionalMethod(input, output, options) {
6940
+ return {
6941
+ ...method(input, output, options),
6942
+ providerOptional: true
6943
+ };
6944
+ }
6922
6945
  /** Shorthand to define an event schema */
6923
6946
  function event(data) {
6924
6947
  return { data };
@@ -7349,6 +7372,8 @@ var EVENT_OWNER_TYPES = [
7349
7372
  * nothing failing until a caller asked.
7350
7373
  */
7351
7374
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7375
+ /** The same list as a Zod enum, for the cap input that carries it. */
7376
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7352
7377
  new Set(EVENT_OWNER_TYPES);
7353
7378
  var EncodeProfileSchema = object({
7354
7379
  video: object({
@@ -7721,6 +7746,7 @@ var CameraSwitchIdSchema = _enum([
7721
7746
  "device-audio",
7722
7747
  "broker-audio",
7723
7748
  "audio-analysis",
7749
+ "event-saving",
7724
7750
  "recording",
7725
7751
  "notifications"
7726
7752
  ]);
@@ -7756,10 +7782,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7756
7782
  var CameraSwitchUnavailableReasonSchema = _enum([
7757
7783
  "no-provider",
7758
7784
  "source-unreachable",
7759
- "not-configured",
7760
7785
  "device-disabled"
7761
7786
  ]);
7762
7787
  /**
7788
+ * Something true about a switch that is nonetheless OFFERED.
7789
+ *
7790
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
7791
+ * whole point: a reason replaces the control, a warning travels WITH it. The
7792
+ * privacy mask taught us the difference — greying the one button that starts
7793
+ * the feature teaches the operator the feature is broken, while a live toggle
7794
+ * that flips nothing teaches them the switch is broken. Neither is true; the
7795
+ * honest surface is a working toggle that says what is missing.
7796
+ */
7797
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
7798
+ /**
7763
7799
  * One switch, resolved for one camera.
7764
7800
  *
7765
7801
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -7778,6 +7814,11 @@ var CameraSwitchSchema = object({
7778
7814
  /** False = do not render a control. `unavailableReason` says why. */
7779
7815
  available: boolean(),
7780
7816
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7817
+ /**
7818
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
7819
+ * never a substitute for one.
7820
+ */
7821
+ warning: CameraSwitchWarningSchema.optional(),
7781
7822
  /** Current state. Meaningless when `available` is false — read it as `true`. */
7782
7823
  enabled: boolean(),
7783
7824
  authority: CameraSwitchAuthoritySchema
@@ -8195,11 +8236,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
8195
8236
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
8196
8237
  * span wraps past midnight (handled by the band engine).
8197
8238
  */
8239
+ /**
8240
+ * Whether a band's footage carries sound.
8241
+ *
8242
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
8243
+ * camera recorded sound whether anyone wanted it or not. The broker already
8244
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
8245
+ * restream — so this was a choice the product could always have offered.
8246
+ *
8247
+ * It belongs on the BAND rather than the camera because one camera can want
8248
+ * both: silent across the shop floor during opening hours, sound overnight.
8249
+ * And it applies to BOTH modes — an events band firing on motion has the same
8250
+ * question to answer as a continuous one.
8251
+ */
8252
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
8198
8253
  var RecordingBandSchema = object({
8199
8254
  days: array(RecordingWeekdaySchema),
8200
8255
  start: string().regex(HHMM),
8201
8256
  end: string().regex(HHMM),
8202
8257
  mode: RecordingBandModeSchema,
8258
+ /**
8259
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
8260
+ * written before this existed carried sound, and a band whose key is
8261
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
8262
+ */
8263
+ audio: RecordingBandAudioSchema.optional(),
8203
8264
  triggers: RecordingBandTriggersSchema.optional(),
8204
8265
  /**
8205
8266
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -13483,6 +13544,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13483
13544
  auth: "admin"
13484
13545
  });
13485
13546
  /**
13547
+ * device-admin-link — "this device has a management page of its own, and here
13548
+ * is its address".
13549
+ *
13550
+ * ## Why this is not a `deviceConfig` cap
13551
+ *
13552
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13553
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13554
+ * patch back through a setter; it costs a `builderId` reducer in
13555
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13556
+ * renders a form section. This cap answers ONE question with ONE read and
13557
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13558
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13559
+ * `reboot`, the other pure-RPC device-native cap.
13560
+ *
13561
+ * ## Absent, and the difference between "no page" and "we cannot say"
13562
+ *
13563
+ * The two are answered at DIFFERENT layers, on purpose:
13564
+ *
13565
+ * - **"We cannot say"** → the provider never registers the cap for that
13566
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13567
+ * fan are reached only through a vendor cloud; there is no address to hand
13568
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13569
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13570
+ * behind it. None of them register, so `deviceManager.getBindings` never
13571
+ * lists the cap and no surface asks.
13572
+ * - **"This device has no page, and I know that"** → the provider registers
13573
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13574
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13575
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13576
+ * transport, a Home Assistant broker authenticated by supervisor token
13577
+ * (which carries no `baseUrl` at all).
13578
+ *
13579
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13580
+ * button, and D62 is the same rule from the other side: an off switch is
13581
+ * reported off, never made to look broken. There is no third state where the
13582
+ * UI renders a disabled button "because the device might have a page".
13583
+ *
13584
+ * ## The URL never carries credentials
13585
+ *
13586
+ * Not in userinfo, not in a query string. Every provider builds through
13587
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13588
+ * scheme and path as separate arguments — there is no parameter a secret could
13589
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13590
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13591
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13592
+ * keeps providers from hand-rolling one anyway.
13593
+ *
13594
+ * This matters here more than anywhere else in the repo, because every provider
13595
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13596
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13597
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13598
+ * camera's own page will ask for its own login. That is correct, and pre-
13599
+ * filling it is the operator's business, not ours.
13600
+ *
13601
+ * ## It is a LAN fact
13602
+ *
13603
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13604
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13605
+ * as a link the operator's own browser follows, on the operator's own network,
13606
+ * or renders nothing.
13607
+ */
13608
+ /**
13609
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13610
+ * before clicking whether he is about to land on a camera's own web server or
13611
+ * inside Home Assistant.
13612
+ */
13613
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13614
+ var DeviceAdminLinkSchema = object({
13615
+ /**
13616
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13617
+ * free of userinfo and of any credential-shaped query key.
13618
+ */
13619
+ url: string(),
13620
+ /**
13621
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13622
+ * The PROVIDER names it, because only the provider knows what the page is;
13623
+ * a UI that invented the label from the addon id would call the Home
13624
+ * Assistant device page "Provider Homeassistant".
13625
+ */
13626
+ label: string(),
13627
+ target: AdminLinkTargetEnum,
13628
+ /**
13629
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13630
+ * an operator can see WHERE the button goes before he follows it. Redundant
13631
+ * with `url` by construction; carried separately so no surface has to parse
13632
+ * a URL to show it.
13633
+ */
13634
+ host: string()
13635
+ });
13636
+ var deviceAdminLinkCapability = {
13637
+ name: "device-admin-link",
13638
+ scope: "device",
13639
+ deviceNative: true,
13640
+ mode: "singleton",
13641
+ methods: {
13642
+ /**
13643
+ * The device's management page, or `null` when this device has none.
13644
+ *
13645
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13646
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13647
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13648
+ * The URL is also a statement about the LAN, which a household member with
13649
+ * a `view` grant on a light has no reason to be handed.
13650
+ *
13651
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13652
+ * without the right loses the query and draws nothing, which is the same
13653
+ * thing a device with no page draws. There is no path on which a button
13654
+ * appears and then fails — the D403 failure mode, from the other end.
13655
+ */
13656
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13657
+ };
13658
+ /**
13659
+ * Query keys that may never appear on an admin link. A credential smuggled as
13660
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13661
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13662
+ * identically.
13663
+ */
13664
+ var CREDENTIAL_QUERY_KEYS = [
13665
+ "password",
13666
+ "passwd",
13667
+ "pwd",
13668
+ "pass",
13669
+ "user",
13670
+ "username",
13671
+ "usr",
13672
+ "login",
13673
+ "token",
13674
+ "access_token",
13675
+ "auth",
13676
+ "authorization",
13677
+ "apikey",
13678
+ "api_key",
13679
+ "secret",
13680
+ "credential",
13681
+ "credentials",
13682
+ "session",
13683
+ "sessionid",
13684
+ "key"
13685
+ ];
13686
+ /**
13687
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13688
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13689
+ * structurally identical on purpose: a URL this function returns is a URL that
13690
+ * guard would pass.
13691
+ */
13692
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13693
+ /** A host that is safe to place in an authority component verbatim. */
13694
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13695
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13696
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13697
+ function defaultPortFor(scheme) {
13698
+ return scheme === "https" ? 443 : 80;
13699
+ }
13700
+ /**
13701
+ * Build the admin-page URL, or refuse by name.
13702
+ *
13703
+ * The refusal is never thrown: a provider answering "no page for this device"
13704
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13705
+ * a missing host on one device into a failed query for the whole surface.
13706
+ */
13707
+ function buildDeviceAdminUrl(parts) {
13708
+ const host = parts.host.trim();
13709
+ if (host.length === 0) return {
13710
+ ok: false,
13711
+ reason: "empty-host"
13712
+ };
13713
+ if (host.includes("@")) return {
13714
+ ok: false,
13715
+ reason: "host-carries-userinfo"
13716
+ };
13717
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13718
+ ok: false,
13719
+ reason: "host-not-plain"
13720
+ };
13721
+ if (parts.port !== null) {
13722
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13723
+ ok: false,
13724
+ reason: "port-out-of-range"
13725
+ };
13726
+ }
13727
+ if (!parts.path.startsWith("/")) return {
13728
+ ok: false,
13729
+ reason: "path-not-absolute"
13730
+ };
13731
+ const query = parts.query ?? {};
13732
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13733
+ ok: false,
13734
+ reason: "credential-query-key"
13735
+ };
13736
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13737
+ const search = new URLSearchParams();
13738
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13739
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13740
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13741
+ if (CREDENTIAL_URL.test(url)) return {
13742
+ ok: false,
13743
+ reason: "userinfo-in-result"
13744
+ };
13745
+ return {
13746
+ ok: true,
13747
+ url
13748
+ };
13749
+ }
13750
+ /**
13486
13751
  * `device-export` — collection cap for addons that export camstack
13487
13752
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
13488
13753
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -20249,6 +20514,7 @@ var MediaFileKindEnum = _enum([
20249
20514
  "keyFrameSmall",
20250
20515
  "thumbnailSmall",
20251
20516
  "confirmationCrop",
20517
+ "debugCrop",
20252
20518
  "sceneFrame"
20253
20519
  ]);
20254
20520
  /**
@@ -21328,7 +21594,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21328
21594
  ownerType: EventOwnerTypeSchema,
21329
21595
  eventId: number().int(),
21330
21596
  deviceId: number()
21331
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21597
+ }), array(MediaFileInfoSchema).readonly()), method(object({
21598
+ deviceId: number(),
21599
+ ownerKind: MediaPresenceOwnerKindSchema,
21600
+ ownerIds: array(string()).max(5e3)
21601
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21332
21602
  kind: "mutation",
21333
21603
  auth: "admin"
21334
21604
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -21913,6 +22183,18 @@ var RunnerCameraConfigSchema = object({
21913
22183
  motionStreamId: string(),
21914
22184
  detectionStreamId: string(),
21915
22185
  /**
22186
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
22187
+ * AVAILABLE native rung) or an explicit one.
22188
+ *
22189
+ * Not consumed by the runner: carried here so it travels with the camera's
22190
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
22191
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
22192
+ * with no default — absent means `auto`, which is what every camera written
22193
+ * before this field had, and an absent optional adds nothing to the
22194
+ * generated cap-input defaults (D537).
22195
+ */
22196
+ audioStreamPick: AudioStreamPickSchema.optional(),
22197
+ /**
21916
22198
  * Motion sources that can activate the pipeline. At least one must
21917
22199
  * be present. Multiple sources OR-ed: any one firing arms the
21918
22200
  * cooldown timer and transitions watching → active. The same
@@ -22983,10 +23265,9 @@ var ServerBootModeSchema = _enum([
22983
23265
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
22984
23266
  * - `pending-restart` — a version is staged and the node has NOT yet
22985
23267
  * restarted onto it (still running the OLD version).
22986
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
22987
- * (it is the active probation boot) and is waiting to confirm boot-health.
22988
- * Apply/rollback are refused in this state and the node must NOT be
22989
- * manually restarted, or the probation boot auto-rolls-back.
23268
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
23269
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
23270
+ * no provider emits it.
22990
23271
  */
22991
23272
  var ServerUpdateStateSchema = _enum([
22992
23273
  "idle",
@@ -23075,12 +23356,35 @@ var ServerRollbackInfoSchema = object({
23075
23356
  var ServerPackageStatusSchema = object({
23076
23357
  /** Root package name (`@camstack/server` on the hub). */
23077
23358
  packageName: string(),
23078
- /** Version of the code the running process ACTUALLY loaded. */
23359
+ /**
23360
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
23361
+ * package.json of the copy the code was loaded from, and never re-read. A
23362
+ * copy replaced on disk under a live process must not move this number;
23363
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
23364
+ * was a lazy disk read that would have followed such a swap silently.
23365
+ */
23079
23366
  runningVersion: string().nullable(),
23080
23367
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
23081
23368
  nodeRuntimeVersion: string().nullable(),
23082
23369
  /** Active data-dir root version; null when booted from seed/workspace. */
23083
23370
  activeVersion: string().nullable(),
23371
+ /**
23372
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
23373
+ * what the next boot would load if nothing is staged. Normally equal to
23374
+ * `runningVersion`; differs only when the copy on disk changed under the
23375
+ * live process. `null` when there is no data-root copy (baked seed or
23376
+ * workspace boot). Optional for version skew: an older provider omits it.
23377
+ */
23378
+ installedVersion: string().nullable().optional(),
23379
+ /**
23380
+ * True when the code that would run after a restart is NOT the code running
23381
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
23382
+ * from `runningVersion`. Derived on every read, never stored. This is the
23383
+ * one field that lets an operator or a script say "1.2.353 is installed and
23384
+ * will run after a restart" instead of reading it off a number that means
23385
+ * "installed". Optional for version skew.
23386
+ */
23387
+ restartRequired: boolean().optional(),
23084
23388
  /** N-1 version kept for rollback; null when no previous version exists. */
23085
23389
  previousVersion: string().nullable(),
23086
23390
  /** Version of the immutable baked seed closure (image fallback). */
@@ -23115,19 +23419,71 @@ var ServerPackageStatusSchema = object({
23115
23419
  });
23116
23420
  var ServerUpdateCheckResultSchema = object({
23117
23421
  packageName: string(),
23422
+ /** Same meaning as on the status payload: what THIS process loaded. */
23118
23423
  runningVersion: string().nullable(),
23119
23424
  latestVersion: string().nullable(),
23425
+ /**
23426
+ * `latestVersion` is newer than `runningVersion` AND is not the version
23427
+ * already staged on this node. A staged-and-waiting version is not "an
23428
+ * update available" — applying it again is refused (`already-staged`), and
23429
+ * a button that announced "updating…" on this boolean then got refused
23430
+ * would be lying. It is reported through `pendingVersion` instead.
23431
+ */
23120
23432
  updateAvailable: boolean(),
23121
23433
  checkedAtMs: number(),
23122
23434
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
23123
- error: string().nullable()
23435
+ error: string().nullable(),
23436
+ /**
23437
+ * Version staged on this node and awaiting its restart, when one is. Before
23438
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
23439
+ * node whose staged closure had never booted, and the caller had no way to
23440
+ * see the difference. Optional for version skew.
23441
+ */
23442
+ pendingVersion: string().nullable().optional(),
23443
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
23444
+ restartRequired: boolean().optional()
23124
23445
  });
23446
+ /**
23447
+ * What an action DID — the discriminant `accepted` cannot carry. Before
23448
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
23449
+ * where nothing was wrong (already current, already staged) and for a staging
23450
+ * install that died on `npm error code ECONNRESET`; the only difference was
23451
+ * prose in `message`, and an operator restarted a container on the strength of
23452
+ * it, booting the previous version.
23453
+ *
23454
+ * - `staged` — the closure is installed and validated; the node
23455
+ * restarts to apply it (`restarting:true`). NOTHING
23456
+ * runs the new code until that restart completes.
23457
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
23458
+ * - `already-current` — the target IS the running version; nothing done.
23459
+ * - `refused` — dropped by a gate; `reason` names which.
23460
+ * - `failed` — attempted and did not land; `error` says why.
23461
+ */
23462
+ var ServerUpdateOutcomeSchema = _enum([
23463
+ "staged",
23464
+ "restart-scheduled",
23465
+ "already-current",
23466
+ "refused",
23467
+ "failed"
23468
+ ]);
23125
23469
  var ServerUpdateActionResultSchema = object({
23126
23470
  accepted: boolean(),
23127
23471
  targetVersion: string().nullable(),
23128
23472
  /** True when a graceful restart was scheduled to apply the change. */
23129
23473
  restarting: boolean(),
23130
- message: string()
23474
+ message: string(),
23475
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
23476
+ outcome: ServerUpdateOutcomeSchema.optional(),
23477
+ /**
23478
+ * Stable, greppable cause for `refused` / `failed` (the engine's
23479
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
23480
+ */
23481
+ reason: string().nullable().optional(),
23482
+ /**
23483
+ * The failure itself, verbatim enough to act on (the npm output for a
23484
+ * staging install). Non-null only for `failed`.
23485
+ */
23486
+ error: string().nullable().optional()
23131
23487
  });
23132
23488
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
23133
23489
  kind: "mutation",
@@ -24236,6 +24592,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
24236
24592
  kind: "mutation",
24237
24593
  auth: "admin"
24238
24594
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
24595
+ _enum([
24596
+ "queue-full",
24597
+ "camera-backoff",
24598
+ "sleeping",
24599
+ "camera-refused",
24600
+ "no-keyframe",
24601
+ "no-catalog-row",
24602
+ "unsupported",
24603
+ "unknown-device",
24604
+ "uid-missing"
24605
+ ]);
24606
+ _enum(["deferred", "final"]);
24239
24607
  var ClipSchema = object({
24240
24608
  /** Opaque, provider-namespaced id. The default provider encodes the time
24241
24609
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -24310,29 +24678,455 @@ var ClipSchema = object({
24310
24678
  holes: array(object({
24311
24679
  startMs: number(),
24312
24680
  endMs: number()
24313
- })).optional()
24681
+ })).optional(),
24682
+ /**
24683
+ * A URL that MAY produce this clip's still, asked only for rows actually on
24684
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
24685
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
24686
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
24687
+ * — a surface latches that refusal to the instant rather than retrying.
24688
+ *
24689
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
24690
+ */
24691
+ thumbnailMint: string().optional(),
24692
+ /**
24693
+ * Why no still will be produced for this clip RIGHT NOW — set when the
24694
+ * provider already knows, so the surface draws the glyph and the reason
24695
+ * instead of firing a mint that cannot succeed.
24696
+ *
24697
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
24698
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
24699
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
24700
+ * inside no file is the measured case).
24701
+ * `unsupported` — this source cannot mint stills at all.
24702
+ */
24703
+ thumbnailUnavailable: object({ reason: _enum([
24704
+ "sleeping",
24705
+ "camera-refused",
24706
+ "no-keyframe",
24707
+ "unsupported"
24708
+ ]) }).optional(),
24709
+ /**
24710
+ * When the catalog this row came from was last CONFIRMED against the device.
24711
+ * Absent means "this row was read live". A persisted catalog served while a
24712
+ * camera sleeps carries the age it really has — a cached list is never drawn
24713
+ * as current (D549 13).
24714
+ */
24715
+ catalogAsOf: number().optional(),
24716
+ /**
24717
+ * The camera's OWN type strings for this clip, all of them, unmapped
24718
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
24719
+ * because a firmware inventing a type must not vanish: the mapping into our
24720
+ * filter vocabulary is lossy on purpose and this is the lossless copy
24721
+ * (D549 20).
24722
+ */
24723
+ nativeTypes: array(string()).optional(),
24724
+ /**
24725
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
24726
+ * travels BESIDE a class, never instead of one, and the class filter ignores
24727
+ * it: "a person crossed a line" is still a person (D549 20).
24728
+ */
24729
+ behaviours: array(string()).optional(),
24730
+ /**
24731
+ * The same recording as two files — the sub twin (what the row and its
24732
+ * thumbnail are) and its main twin, matched at listing time so a quality
24733
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
24734
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
24735
+ */
24736
+ streams: object({
24737
+ sub: object({
24738
+ id: string(),
24739
+ bytes: number().optional()
24740
+ }).optional(),
24741
+ main: object({
24742
+ id: string(),
24743
+ bytes: number().optional()
24744
+ }).optional()
24745
+ }).optional(),
24746
+ /** The camera says it holds a sub-stream copy of this recording. */
24747
+ supportSub: boolean().optional(),
24748
+ /**
24749
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
24750
+ * IS a file is playable, and only a source that lists EVENTS can produce a
24751
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
24752
+ * fell inside no file). Such a row is shown, never dropped and never offered
24753
+ * as playable-then-failing (D549 19).
24754
+ */
24755
+ playable: boolean().optional(),
24756
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
24757
+ unplayableReason: string().optional(),
24758
+ /**
24759
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
24760
+ * is not its end.
24761
+ *
24762
+ * Absent — the common case — means the row is closed and its `endMs` is the
24763
+ * end of the recording. Present and `true` means the source told us the file
24764
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
24765
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
24766
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
24767
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
24768
+ * on growing. A surface that drew 29 s there was lying about a clip that
24769
+ * plays for minutes, on the one row the operator looks at first.
24770
+ *
24771
+ * `endMs` is deliberately still a number: it is the best bound anything has
24772
+ * for a mint window or a byte fetch, and every consumer already requires it.
24773
+ * This flag says what it is WORTH, and a duration is not drawn from it.
24774
+ */
24775
+ inProgress: boolean().optional()
24314
24776
  });
24315
24777
  var ClipPlaybackSchema = object({
24316
- /** HLS master URL through the hub data-plane (Range + token in path). */
24778
+ /**
24779
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
24780
+ * says what kind — an HLS master playlist for a recording-derived clip, a
24781
+ * progressive MP4 for a native file the vendor addon muxed and serves with
24782
+ * `Range`. The URL is NOT a credential: both planes are registered
24783
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
24784
+ * the session cookie, so nothing is appended to it (D549 22).
24785
+ */
24317
24786
  playbackUrl: string(),
24787
+ /**
24788
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
24789
+ * existing consumer already assumes. A player that branches on this is the
24790
+ * one change a native clip needs; a player that ignores it will hand an MP4
24791
+ * to hls.js and fail parsing it as a manifest.
24792
+ */
24793
+ format: _enum(["hls", "mp4"]).optional(),
24794
+ /**
24795
+ * Which twin was actually served. A clip that holds only one stream answers
24796
+ * with the one it has, and the surface SAYS so — a missing main twin is
24797
+ * never served silently as if it were the asked-for quality (D549 15).
24798
+ */
24799
+ served: CamProfileSchema.optional(),
24318
24800
  /** Optional LAN/remote alternates for the same clip. */
24319
24801
  playbackEndpoints: array(string()).optional(),
24320
24802
  token: string().optional()
24321
24803
  });
24804
+ /**
24805
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
24806
+ *
24807
+ * `ok` is the only state whose clip list may be read as complete. The other
24808
+ * three exist because an empty list from a sleeping camera reads as "this
24809
+ * camera has no recordings", which is the defect this whole line of work is
24810
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
24811
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
24812
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
24813
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
24814
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
24815
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
24816
+ * files on both streams and both filters, with 177 GB free). That last one is
24817
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
24818
+ * which days it asked for and got nothing — "no clips" would be a lie about a
24819
+ * card that is full of them.
24820
+ */
24821
+ /**
24822
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
24823
+ *
24824
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
24825
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
24826
+ * `force` flag, and nothing else… a background caller must never set it…
24827
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
24828
+ * prefetch never set it; a surface sets it only behind the same confirm the
24829
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
24830
+ * exactly as that gesture is.
24831
+ *
24832
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
24833
+ * (D549) — a read that opened a session and then checked would have woken the
24834
+ * camera to find out it was not allowed to.
24835
+ *
24836
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
24837
+ * time the camera is awake for its own reasons — is deliberately not a member:
24838
+ * it was proposed, it is free on the battery, and the operator declined it on
24839
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
24840
+ * D558 "Considered and not taken").
24841
+ */
24842
+ var ClipWakeSchema = _enum(["authorised"]);
24843
+ /**
24844
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
24845
+ *
24846
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
24847
+ * and `served` says which twin the caller actually got.
24848
+ */
24849
+ var ClipBytesSchema = object({
24850
+ base64: string(),
24851
+ contentType: string(),
24852
+ /** Suggested filename, extension included. */
24853
+ name: string(),
24854
+ bytes: number().int().nonnegative(),
24855
+ /**
24856
+ * Which twin was actually served — the same contract
24857
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
24858
+ * export record persists it: a row read a week later must say the same thing
24859
+ * the panel said at the moment of the tap. A missing main twin is never
24860
+ * served silently as if it were the asked-for quality (D549 15).
24861
+ */
24862
+ served: CamProfileSchema,
24863
+ /**
24864
+ * The DECODED duration of the delivered file, when the fetch measured one.
24865
+ *
24866
+ * A clip fetch verifies its own completion against the catalog row and
24867
+ * retries a materially short pass (D568); this is that measurement, carried
24868
+ * so a consumer can say the same thing rather than re-deriving it. Absent
24869
+ * when the producer did not measure — never zero, which would say the file
24870
+ * is empty.
24871
+ */
24872
+ durationMs: number().positive().optional()
24873
+ });
24874
+ /**
24875
+ * Where a clip's STREAM can be dialled (D597) — the answer to
24876
+ * {@link videoclipsCapability.methods.dialClipStream}.
24877
+ *
24878
+ * `url` is plain `http://` on the provider's own token-gated listener, one
24879
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
24880
+ * path: that plane takes only a user credential, which an addon does not
24881
+ * hold, and the framework offers no addon→addon byte transport — the one
24882
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
24883
+ * a consumer dials directly, and this is that precedent one addon over.
24884
+ */
24885
+ /**
24886
+ * The audio a clip stream can be asked for, and the audio it carries.
24887
+ *
24888
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
24889
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
24890
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
24891
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
24892
+ * negotiates — and `none` is a stream asked for, or served, without sound.
24893
+ */
24894
+ var ClipStreamAudioSchema = _enum([
24895
+ "opus",
24896
+ "pcmu",
24897
+ "none"
24898
+ ]);
24899
+ /**
24900
+ * Why a stream that was ASKED for sound is not carrying it (D600).
24901
+ *
24902
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
24903
+ * means the ask was honoured, or there was no ask.
24904
+ *
24905
+ * - `not-requested` — the caller asked for `none`.
24906
+ * - `unsupported-ask` — the provider cannot produce that codec.
24907
+ *
24908
+ * Whether the RECORDING has sound at all is not knowable here: the camera
24909
+ * declares no audio track before its first frame arrives. That answer is in
24910
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
24911
+ * there. A dial that says `opus` promises the codec, never the content.
24912
+ */
24913
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
24914
+ var ClipStreamDialSchema = object({
24915
+ url: string().min(1),
24916
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
24917
+ expiresAtMs: number(),
24918
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
24919
+ transport: literal("forward-only"),
24920
+ /**
24921
+ * Which twin the stream ACTUALLY carries — not what was asked for.
24922
+ *
24923
+ * The stream serves every profile (D599), and a recording whose main twin
24924
+ * does not exist is streamed as its sub copy, which this field reports as
24925
+ * `low`. That is the whole reason the dial no longer refuses `high`:
24926
+ * stating what was served is what makes accepting honest, where a silent
24927
+ * substitution would be the 33x lie D576 forbids. Stated rather than
24928
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
24929
+ */
24930
+ served: CamProfileSchema,
24931
+ /**
24932
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
24933
+ * sound, and the same reasoning (D599, D600).
24934
+ *
24935
+ * The broker asks for the codec its WebRTC session negotiated, and the
24936
+ * provider either muxes that codec or carries none. Stating it is what
24937
+ * lets the broker wire the matching sink before the first byte, and what
24938
+ * keeps a session from being handed a codec it cannot send — the audio
24939
+ * shape of the substitution `served` exists to prevent.
24940
+ *
24941
+ * It is a promise about the CODEC, not about the content: a recording with
24942
+ * no microphone track answers `opus` here and then arrives with no audio
24943
+ * track at all. That second answer is in the stream, and the broker reads
24944
+ * it from the `moov`.
24945
+ */
24946
+ servedAudio: ClipStreamAudioSchema,
24947
+ /** Why `servedAudio` is `none` although sound was asked for. */
24948
+ audioReason: ClipStreamAudioReasonSchema.optional()
24949
+ });
24950
+ var ClipSourceAvailabilitySchema = object({
24951
+ state: _enum([
24952
+ "ok",
24953
+ "sleeping",
24954
+ "unreachable",
24955
+ "no-storage",
24956
+ "index-empty"
24957
+ ]),
24958
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
24959
+ reason: string().optional(),
24960
+ /** When this source's catalog was last CONFIRMED. A cached list is never
24961
+ * drawn as current: the surface shows the age whenever it is older than the
24962
+ * refresh interval. */
24963
+ catalogAsOf: number().optional()
24964
+ });
24965
+ /**
24966
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
24967
+ * every clip id from it is prefixed with (`analytics`,
24968
+ * `native:reolink:onboard`, `hksv`, …).
24969
+ *
24970
+ * A provider lists the sources IT serves for that device, and answers for each
24971
+ * of them whether it can answer at all. A provider with nothing to offer on a
24972
+ * camera returns `[]` — it is not that camera's business.
24973
+ */
24974
+ var ClipSourceSchema = object({
24975
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
24976
+ * of every clip id it mints. `getClipPlayback` routes on it. */
24977
+ source: string(),
24978
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
24979
+ label: string(),
24980
+ /**
24981
+ * The addon that SERVES this row.
24982
+ *
24983
+ * A surface cannot otherwise resolve a source to who answers it, and the
24984
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
24985
+ * widget — is a second authority on provider identity living in the one
24986
+ * package with no business knowing it, wrong the day a third source appears
24987
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
24988
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
24989
+ * served a hub child two of them until the two views were measured to be one
24990
+ * store read twice (D555) and then read ONE way (D565).
24991
+ *
24992
+ * Optional for version skew only. The collection dispatcher stamps it from
24993
+ * the registry, so a row that travelled through the fan-out carries the
24994
+ * authoritative id whatever the provider filled in.
24995
+ */
24996
+ addonId: string().optional(),
24997
+ availability: ClipSourceAvailabilitySchema
24998
+ });
24322
24999
  DeviceType.Camera, method(object({
24323
25000
  deviceId: number(),
24324
25001
  since: number(),
24325
25002
  until: number(),
24326
- limit: number().int().positive().optional()
25003
+ limit: number().int().positive().optional(),
25004
+ /**
25005
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25006
+ * carries, never a source id and never a list. **Required** (D554 amended).
25007
+ *
25008
+ * A provider the device is not bound to is refused by name rather than
25009
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25010
+ *
25011
+ * It was optional, documented as "absent means the device's BOUND
25012
+ * provider, which is CamStack on every camera". No code implemented
25013
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25014
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25015
+ * answered SIX rows, three from each source, merged newest-first:
25016
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25017
+ * so absence bought the union this method exists not to be, and
25018
+ * `limit` meant `limit × sources`.
25019
+ *
25020
+ * There is nothing to restore the default to. `getBindings` answers a
25021
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25022
+ * `setWrapperActive` — the singleton authority that could name one —
25023
+ * throws for a collection cap by design. Naming CamStack here instead
25024
+ * would privilege one addon by id inside a surface whose premise is
25025
+ * that sources are peers, and would be wrong on the first camera with
25026
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25027
+ * generated types make it unomittable rather than merely discouraged.
25028
+ *
25029
+ * The default belongs to the SURFACE, which has the `listSources` rows
25030
+ * and can say which one it picked (D569 § 1–3; the viewer already
25031
+ * always sends this).
25032
+ *
25033
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
25034
+ * assumed the answer was a fan-out over everything a camera has. The
25035
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25036
+ * solo"* — so the list is asked of one provider at a time and there is
25037
+ * nothing to filter out of it.
25038
+ */
25039
+ provider: string().min(1)
24327
25040
  }), array(ClipSchema).readonly(), {
24328
25041
  kind: "query",
24329
- auth: "admin"
25042
+ auth: "protected"
25043
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
25044
+ kind: "query",
25045
+ auth: "protected"
24330
25046
  }), method(object({
24331
25047
  deviceId: number(),
24332
- clipId: string()
25048
+ clipId: string(),
25049
+ /**
25050
+ * Which twin to serve, on the ONE quality scale the system already has
25051
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
25052
+ * twin; both ids are already on the row so this never re-searches the
25053
+ * camera. Absent means the provider's own default (the sub file, which
25054
+ * every source is measured to hold).
25055
+ *
25056
+ * `auto` is deliberately NOT accepted here: a stored file has no
25057
+ * broker session, so the adaptive tier cannot be resolved for it. The
25058
+ * viewer resolves `auto` to a profile the same way live does, before
25059
+ * it calls (D549 15).
25060
+ */
25061
+ profile: CamProfileSchema.optional()
24333
25062
  }), ClipPlaybackSchema, {
24334
25063
  kind: "query",
24335
- auth: "admin"
25064
+ auth: "protected"
25065
+ }), optionalMethod(object({
25066
+ deviceId: number(),
25067
+ clipId: string().min(1),
25068
+ /**
25069
+ * WHICH provider holds the bytes — the `addonId` a
25070
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
25071
+ * `listClips` states one method above and for a second one this method
25072
+ * learned the hard way.
25073
+ *
25074
+ * The caller is the stream broker, and the broker lives in
25075
+ * `addon-pipeline` — the same addon that registers the RECORDER's
25076
+ * videoclips provider. With no provider named, the in-process
25077
+ * resolution handed it its own sibling, which implements this method
25078
+ * solely to explain that an analytics visit has no file behind it. So
25079
+ * every onboard clip answered with that sentence, a flash of video and
25080
+ * nothing more, while the SAME call from outside the addon reached the
25081
+ * right provider and returned the bytes (measured on 3628,
25082
+ * 2026-09-21).
25083
+ */
25084
+ provider: string().min(1),
25085
+ /**
25086
+ * Which twin to fetch, on the mapping D549 15 already fixed:
25087
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
25088
+ * accepted here for the same reason it is not accepted by
25089
+ * `getClipPlayback` — a stored file has no broker session, so the
25090
+ * adaptive tier cannot be resolved for it.
25091
+ */
25092
+ profile: CamProfileSchema.optional(),
25093
+ /**
25094
+ * The CALLER's byte bound, so an over-size clip is refused before it is
25095
+ * read and encoded rather than after. Capped by
25096
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
25097
+ * that ceiling.
25098
+ */
25099
+ maxBytes: number().int().positive().optional(),
25100
+ /**
25101
+ * The operator's authorisation to wake a sleeping camera for this
25102
+ * read. Absent — the default — means a sleeping standalone battery
25103
+ * camera is REFUSED by name, before any session is opened.
25104
+ */
25105
+ wake: ClipWakeSchema.optional()
25106
+ }), ClipBytesSchema, {
25107
+ kind: "query",
25108
+ auth: "protected"
25109
+ }), optionalMethod(object({
25110
+ deviceId: number(),
25111
+ clipId: string().min(1),
25112
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
25113
+ provider: string().min(1),
25114
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
25115
+ profile: CamProfileSchema.optional(),
25116
+ /**
25117
+ * Which audio codec the caller's session negotiated, so the provider
25118
+ * can mux it (D600). The ask travels on the DIAL because the mux is
25119
+ * spawned once, before the first byte, and cannot gain a track later.
25120
+ *
25121
+ * ABSENT IS `none`, never a guess: a caller that does not state an
25122
+ * ask is served the video-only stream that shipped first, so a broker
25123
+ * older than this field keeps working unchanged (D382 — an unstated
25124
+ * ask is zero).
25125
+ */
25126
+ audio: ClipStreamAudioSchema.optional()
25127
+ }), ClipStreamDialSchema, {
25128
+ kind: "query",
25129
+ auth: "protected"
24336
25130
  });
24337
25131
  /**
24338
25132
  * Optional client-side hints sent at session creation to help the provider
@@ -24404,6 +25198,104 @@ var WebrtcStreamChoiceSchema = object({
24404
25198
  /** Bitrate in kbps (null for adaptive). */
24405
25199
  bitrateKbps: number().nullable()
24406
25200
  });
25201
+ /**
25202
+ * The last move the adaptive ladder made on this session, and the numbers it
25203
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
25204
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
25205
+ *
25206
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
25207
+ * no window to report, which is NOT a link with zero loss.
25208
+ */
25209
+ var WebrtcLadderDecisionSchema = object({
25210
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
25211
+ * stopped moving; `healthy` — a promotion. */
25212
+ cause: _enum([
25213
+ "loss",
25214
+ "stall",
25215
+ "healthy"
25216
+ ]),
25217
+ /** The rung the ladder moved TO. Null for a move that changed no rung
25218
+ * (an MTU reduction, a forced transcode-down at the bottom). */
25219
+ tier: CamProfileSchema.nullable(),
25220
+ /** Whether that move forced the egress transcode-down rung. */
25221
+ forcedTranscodeDown: boolean(),
25222
+ /** Loss ratio over the tracker's window at the moment of the decision. */
25223
+ lossRatio: number().nullable(),
25224
+ /** The client's reported downlink at that moment, in kbps. */
25225
+ clientKbps: number().nullable(),
25226
+ /** How long the stall detector had been seeing nothing arrive. */
25227
+ stalledForMs: number().nullable(),
25228
+ /** Whether the broker WAS sending while that was measured — the half that
25229
+ * separates "the camera stopped" from "the link stopped". */
25230
+ serverSending: boolean().nullable(),
25231
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25232
+ * must not read like one from this second. */
25233
+ at: number()
25234
+ });
25235
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25236
+ var WebrtcTierFailureSchema = object({
25237
+ tier: CamProfileSchema,
25238
+ failures: number().int().nonnegative()
25239
+ });
25240
+ /**
25241
+ * What the SERVER decided for one WebRTC session — the half the client cannot
25242
+ * measure. See `getSessionDebug`.
25243
+ */
25244
+ var WebrtcSessionDebugSchema = object({
25245
+ sessionId: string(),
25246
+ /** Whether the session still exists here. Everything below is null when
25247
+ * it does not — a dead session has no measurements, not zero ones. */
25248
+ alive: boolean(),
25249
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
25250
+ observedAt: number(),
25251
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
25252
+ brokerId: string().nullable(),
25253
+ camStreamId: string().nullable(),
25254
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
25255
+ * another stream); `native` = the camera's own encode. */
25256
+ sourceKind: _enum([
25257
+ "native",
25258
+ "derived",
25259
+ "unknown"
25260
+ ]),
25261
+ /** Whether the ladder is running at all (an `adaptive` target). */
25262
+ adaptive: boolean(),
25263
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
25264
+ * is bound to no profile slot (a manually-activated stream). */
25265
+ servedTier: CamProfileSchema.nullable(),
25266
+ /** Which node ingests the source. Null when unresolved. */
25267
+ ingestNodeId: string().nullable(),
25268
+ /** Whether that node is a remote one (the media crosses a node boundary
25269
+ * before it reaches this session). Null when unresolved. */
25270
+ ingestIsRemote: boolean().nullable(),
25271
+ /** Codec off the source, and the one negotiated with this client. */
25272
+ sourceCodec: string().nullable(),
25273
+ negotiatedCodec: string().nullable(),
25274
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
25275
+ * source bytes untouched. */
25276
+ egressLeg: _enum([
25277
+ "copy",
25278
+ "transcode",
25279
+ "unknown"
25280
+ ]),
25281
+ /** What the broker itself reports about the rung it is serving. */
25282
+ brokerStatus: string().nullable(),
25283
+ brokerCodec: string().nullable(),
25284
+ brokerBitrateKbps: number().nullable(),
25285
+ brokerInputFps: number().nullable(),
25286
+ brokerUptimeMs: number().nullable(),
25287
+ /** RTP packets this session has sent — the server's own "am I sending?". */
25288
+ rtpPacketsSent: number().nullable(),
25289
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
25290
+ * is suspended there, so a frozen picture in `recorded` mode means
25291
+ * something entirely different. */
25292
+ trackMode: _enum(["live", "recorded"]).nullable(),
25293
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
25294
+ tierFailures: array(WebrtcTierFailureSchema),
25295
+ /** Successful promotions in this viewer session (drives the progressive
25296
+ * healthy timer). Null when no ladder is running. */
25297
+ upgradeCount: number().nullable()
25298
+ });
24407
25299
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
24408
25300
  deviceId: number().int().nonnegative(),
24409
25301
  target: WebrtcStreamTargetSchema,
@@ -24568,7 +25460,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24568
25460
  * the same answer as "it ended": either way nothing is holding it up.
24569
25461
  */
24570
25462
  alive: boolean()
24571
- }));
25463
+ })), method(object({
25464
+ deviceId: number().int().nonnegative(),
25465
+ sessionId: string()
25466
+ }), WebrtcSessionDebugSchema, { kind: "query" });
24572
25467
  object({
24573
25468
  /** All accessory children of the parent. */
24574
25469
  childDeviceIds: array(number()).readonly(),
@@ -26394,6 +27289,346 @@ var dayNightCapability = {
26394
27289
  volatileStateFields: ["lastFetchedAt"]
26395
27290
  };
26396
27291
  /**
27292
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
27293
+ * writes to the CAMERA's own card, on the camera's own schedule.
27294
+ *
27295
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
27296
+ * footage ledger, our storage locations, our retention. This one has a
27297
+ * different authority — the camera's firmware — and per D62 it stores
27298
+ * nothing of its own. Every value here is read from the camera and every
27299
+ * write goes back to the camera; there is no CamStack-side mirror that
27300
+ * could disagree with the device.
27301
+ *
27302
+ * ## One shape, two firmwares
27303
+ *
27304
+ * Measured 2026-09-22 against the live fleet:
27305
+ *
27306
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
27307
+ * | --- | --- | --- |
27308
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
27309
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
27310
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
27311
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
27312
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
27313
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
27314
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
27315
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
27316
+ *
27317
+ * The two schedule models look different and are the same thing in
27318
+ * different coordinates: both answer "for this trigger, during which
27319
+ * weekly windows does the camera record". {@link RecordWindow} is that
27320
+ * question in one shape — Hikvision's ranges map straight onto it,
27321
+ * Reolink's mask expands into hour-aligned windows.
27322
+ *
27323
+ * ## Union, not intersection
27324
+ *
27325
+ * **The same fields exist on every camera.** What differs per device is
27326
+ * which VALUES that device accepts, and that is what {@link
27327
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
27328
+ * per field plus the schedule's own limits. A control a camera cannot
27329
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
27330
+ * dead: disabled must not look like broken.
27331
+ *
27332
+ * ## Refusal by name
27333
+ *
27334
+ * A write a camera cannot honour is refused with a sentence the operator
27335
+ * can read — never accepted and dropped. Both providers refuse through
27336
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
27337
+ * one test, not two hand-written vendor opinions.
27338
+ *
27339
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
27340
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
27341
+ * injected from `status`) reports the live values, and a single
27342
+ * `setSettings` mutation applies a partial change. No hand-written
27343
+ * settings-contribution methods.
27344
+ */
27345
+ /**
27346
+ * What makes the camera start recording during a window.
27347
+ *
27348
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
27349
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
27350
+ * object-class triggers are Reolink-only today and the smart-event ones
27351
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
27352
+ * firmwares measured — a camera that cannot record on a trigger simply
27353
+ * does not list it in `options.schedule.triggers`, and a window naming
27354
+ * it is REFUSED, not dropped.
27355
+ */
27356
+ var RecordTriggerSchema = _enum([
27357
+ "continuous",
27358
+ "motion",
27359
+ "person",
27360
+ "vehicle",
27361
+ "animal",
27362
+ "lineCrossing",
27363
+ "intrusion",
27364
+ "loitering",
27365
+ "alarmInput"
27366
+ ]);
27367
+ /**
27368
+ * One weekly recording window: "on `day`, from `startMinute` to
27369
+ * `endMinute`, record on `trigger`".
27370
+ *
27371
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
27372
+ * both firmwares enumerate). Minutes are local camera time since
27373
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
27374
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
27375
+ * collapsing it to 0 would turn a whole-day window into an empty one.
27376
+ */
27377
+ var RecordWindowSchema = object({
27378
+ trigger: RecordTriggerSchema,
27379
+ day: number().int().min(0).max(6),
27380
+ startMinute: number().int().min(0).max(1439),
27381
+ endMinute: number().int().min(1).max(1440)
27382
+ });
27383
+ /** Status of one physical volume, as the camera itself describes it. */
27384
+ var OnboardStorageVolumeSchema = object({
27385
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
27386
+ id: string(),
27387
+ /** The camera's own name for it, when it gives one (`hddName`). */
27388
+ label: string().optional(),
27389
+ status: _enum([
27390
+ "ok",
27391
+ "unformatted",
27392
+ "error",
27393
+ "offline",
27394
+ "unknown"
27395
+ ]),
27396
+ /**
27397
+ * Total size in MB, or **null when the camera did not say**.
27398
+ *
27399
+ * Never 0 for an unreadable value: a measurement that failed is not a
27400
+ * measurement (D393), and a card whose size is unknown must not be
27401
+ * rendered as a card of size zero.
27402
+ */
27403
+ capacityMb: number().nullable(),
27404
+ /**
27405
+ * Free space in MB, or null when unknown.
27406
+ *
27407
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
27408
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
27409
+ * card converges on once it has wrapped. At loop steady state the
27410
+ * number is identical whether the camera recorded yesterday or stopped
27411
+ * a month ago.
27412
+ */
27413
+ freeMb: number().nullable(),
27414
+ /** True when the camera reports the volume writable (`property` RW). */
27415
+ writable: boolean().optional()
27416
+ });
27417
+ /**
27418
+ * What the camera is doing with its own storage, right now.
27419
+ *
27420
+ * Every scalar is nullable and **null means the camera did not answer**,
27421
+ * never a default. A form that seeds `0` from an unanswered read invites
27422
+ * the operator to save that 0 back onto the camera.
27423
+ */
27424
+ var RecordingOnboardStatusSchema = object({
27425
+ storage: discriminatedUnion("kind", [
27426
+ object({
27427
+ kind: literal("present"),
27428
+ volumes: array(OnboardStorageVolumeSchema)
27429
+ }),
27430
+ object({
27431
+ kind: literal("absent"),
27432
+ reason: string()
27433
+ }),
27434
+ object({
27435
+ kind: literal("unknown"),
27436
+ reason: string()
27437
+ })
27438
+ ]),
27439
+ tracks: array(object({
27440
+ id: string(),
27441
+ enabled: boolean(),
27442
+ isVideo: boolean(),
27443
+ /** From the camera's own track description. Null when it does not say. */
27444
+ codec: string().nullable(),
27445
+ resolution: string().nullable(),
27446
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
27447
+ overwriteWhenFull: boolean().nullable()
27448
+ })),
27449
+ /**
27450
+ * The track the write path targets — the enabled VIDEO one. Null when
27451
+ * no track could be identified, which is itself a refusal reason.
27452
+ */
27453
+ primaryTrackId: string().nullable(),
27454
+ /** Master "record to the card at all" switch. */
27455
+ enabled: boolean().nullable(),
27456
+ overwriteWhenFull: boolean().nullable(),
27457
+ preRecordSec: number().nullable(),
27458
+ postRecordSec: number().nullable(),
27459
+ /** Length of one recorded file, in minutes. */
27460
+ segmentMinutes: number().nullable(),
27461
+ /** The primary track's weekly windows, flattened. */
27462
+ windows: array(RecordWindowSchema),
27463
+ /**
27464
+ * How many windows the camera described that CamStack could NOT read —
27465
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
27466
+ * name.
27467
+ *
27468
+ * A dropped window is work the reader threw away, and a schedule that
27469
+ * silently shows fewer rows than the camera holds is how an operator
27470
+ * saves back a schedule shorter than the one they were looking at
27471
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
27472
+ * that replaces it would delete what was not shown — which is why a
27473
+ * provider reporting a non-zero count also reports the schedule as not
27474
+ * writable.
27475
+ */
27476
+ unreadableWindows: number(),
27477
+ /**
27478
+ * The camera is scheduled to record and has NO usable storage.
27479
+ *
27480
+ * A first-class fact because it is the fleet's most common silent
27481
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
27482
+ * to a card that is not there. Neither the schedule nor the storage
27483
+ * read says anything wrong on its own; only the pair does.
27484
+ */
27485
+ recordingToNowhere: boolean(),
27486
+ lastFetchedAt: number()
27487
+ });
27488
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
27489
+ var RangeSchema = object({
27490
+ min: number(),
27491
+ max: number(),
27492
+ step: number()
27493
+ });
27494
+ /**
27495
+ * The values a camera actually takes for a numeric field, when they are a SET
27496
+ * rather than a range.
27497
+ *
27498
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
27499
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
27500
+ *
27501
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
27502
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
27503
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
27504
+ *
27505
+ * Neither is expressible as a step: the first has a sentinel two billion away
27506
+ * from its neighbours, the second doubles and then jumps. A range that tried
27507
+ * would forbid values the camera takes AND permit values it silently replaces
27508
+ * with 5 — wrong in both directions at once.
27509
+ *
27510
+ * `sentinel` names the member that is not a duration, so a surface can render
27511
+ * "no limit" instead of `2147483647` seconds.
27512
+ */
27513
+ var AllowedValuesSchema = object({
27514
+ values: array(number()).min(1),
27515
+ sentinel: object({
27516
+ value: number(),
27517
+ meaning: _enum(["no-limit", "disabled"])
27518
+ }).optional()
27519
+ });
27520
+ /**
27521
+ * Per-field availability on ONE camera.
27522
+ *
27523
+ * The field exists on every camera — this says whether this one can be
27524
+ * read and whether it can be written, and `reason` says why not when
27525
+ * either is false. The UI renders the control DISABLED with the reason
27526
+ * rather than hiding it, so a limitation is legible instead of looking
27527
+ * like a missing feature.
27528
+ */
27529
+ var OnboardFieldSupportSchema = object({
27530
+ readable: boolean(),
27531
+ writable: boolean(),
27532
+ /** Required whenever `readable` or `writable` is false. */
27533
+ reason: string().optional()
27534
+ });
27535
+ /** What this camera's schedule model can express. */
27536
+ var OnboardScheduleSupportSchema = object({
27537
+ support: OnboardFieldSupportSchema,
27538
+ /**
27539
+ * The smallest time step the camera can express, in minutes.
27540
+ *
27541
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
27542
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
27543
+ * window whose edges are not a multiple of this is REFUSED rather than
27544
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
27545
+ * and nothing says so.
27546
+ */
27547
+ granularityMinutes: number(),
27548
+ /** Triggers this camera can record on. A window naming another is refused. */
27549
+ triggers: array(RecordTriggerSchema),
27550
+ /**
27551
+ * False when the camera stores ONE trigger per time range, so two
27552
+ * windows overlapping on the same day cannot carry different triggers.
27553
+ * True on Reolink, whose mask is per-trigger and independent.
27554
+ */
27555
+ supportsOverlappingTriggers: boolean()
27556
+ });
27557
+ var RecordingOnboardOptionsSchema = object({
27558
+ enabled: OnboardFieldSupportSchema,
27559
+ overwriteWhenFull: OnboardFieldSupportSchema,
27560
+ preRecordSec: OnboardFieldSupportSchema,
27561
+ preRecordSecRange: RangeSchema.optional(),
27562
+ /** Preferred over the range when the camera takes a SET, not a span. */
27563
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
27564
+ postRecordSec: OnboardFieldSupportSchema,
27565
+ postRecordSecRange: RangeSchema.optional(),
27566
+ /** Preferred over the range when the camera takes a SET, not a span. */
27567
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
27568
+ segmentMinutes: OnboardFieldSupportSchema,
27569
+ segmentMinutesRange: RangeSchema.optional(),
27570
+ /** Preferred over the range when the camera takes a SET, not a span. */
27571
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
27572
+ schedule: OnboardScheduleSupportSchema
27573
+ });
27574
+ /**
27575
+ * A partial change. Every field optional.
27576
+ *
27577
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
27578
+ * silently ignore a field it cannot support — it refuses, by name,
27579
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
27580
+ * is the failure D62 exists to prevent: the operator believes the camera
27581
+ * is recording the way the form says, and it is not.
27582
+ */
27583
+ var RecordingOnboardPatchSchema = object({
27584
+ enabled: boolean().optional(),
27585
+ overwriteWhenFull: boolean().optional(),
27586
+ preRecordSec: number().optional(),
27587
+ postRecordSec: number().optional(),
27588
+ segmentMinutes: number().optional(),
27589
+ /** The complete new window set for the primary track — not a delta. */
27590
+ windows: array(RecordWindowSchema).optional()
27591
+ });
27592
+ var recordingOnboardCapability = {
27593
+ name: "recording-onboard",
27594
+ scope: "device",
27595
+ deviceNative: true,
27596
+ mode: "singleton",
27597
+ deviceTypes: [DeviceType.Camera],
27598
+ deviceConfig: { ui: {
27599
+ kind: "derived-form",
27600
+ builderId: "recording-onboard",
27601
+ tab: "recording"
27602
+ } },
27603
+ methods: {
27604
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
27605
+ setSettings: method(object({
27606
+ deviceId: number(),
27607
+ settings: RecordingOnboardPatchSchema
27608
+ }), _void(), {
27609
+ kind: "mutation",
27610
+ auth: "admin"
27611
+ })
27612
+ },
27613
+ status: {
27614
+ schema: RecordingOnboardStatusSchema,
27615
+ kind: "poll"
27616
+ },
27617
+ runtimeState: RecordingOnboardStatusSchema,
27618
+ /**
27619
+ * Runtime-state durability: **restored** — operator-set camera-side
27620
+ * recording config; mutation-driven, and the storage half is the last
27621
+ * thing the camera said about its own card.
27622
+ *
27623
+ * See `RuntimeStateDurability`. Enforced by
27624
+ * `scripts/check-runtime-state-durability.ts`.
27625
+ */
27626
+ durability: "restored",
27627
+ /** Clock fields: written, but excluded from the compare that decides
27628
+ * whether persisting is worth a SQLite commit. */
27629
+ volatileStateFields: ["lastFetchedAt"]
27630
+ };
27631
+ /**
26397
27632
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
26398
27633
  * for every device, regardless of provider — the kernel needs a uniform
26399
27634
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -28903,11 +30138,15 @@ var motionTriggerCapability = {
28903
30138
  *
28904
30139
  * Adding a fifth name to that list would have been the wrong fix twice over:
28905
30140
  * that page is per-camera DETECTION tuning, and a grid's geometry belongs
28906
- * beside PTZ and motion zones on the camera itself — which is exactly where the
28907
- * framework already puts a widget, when the widget is declared on a CAPABILITY.
28908
- * `deviceConfig.ui` is the mechanism (`motion-zones`, `ptz`, `ptz-autotrack`
28909
- * all use it): the framework emits the structural section and the widget
28910
- * self-persists through the cap's own mutations.
30141
+ * beside PTZ and motion zones on the camera itself. The device page is
30142
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
30143
+ * and this cap carries its section the way `recording` does, by RETURNING it
30144
+ * from `getDeviceSettingsContribution`.
30145
+ *
30146
+ * Seven other widgets are still declared the other way, through a
30147
+ * `deviceConfig.ui` block the framework derives a section from. That route
30148
+ * gives the addon no say in where its own panel lands and no way to decline
30149
+ * for a device the panel does not suit, which is why this one does not use it.
28911
30150
  *
28912
30151
  * ## Why one addon may implement it
28913
30152
  *
@@ -28963,16 +30202,37 @@ var GridProfileOfferSchema = object({
28963
30202
  ]),
28964
30203
  offered: boolean(),
28965
30204
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28966
- missingSources: array(number().int().positive())
30205
+ missingSources: array(number().int().positive()),
30206
+ /**
30207
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
30208
+ * offered. DERIVED from the cells and the sources' own size at this profile —
30209
+ * it is reported because nothing else in the system would ever say what the
30210
+ * grid came out as, and because it is the number an operator would otherwise
30211
+ * expect to type.
30212
+ */
30213
+ canvas: string(),
30214
+ /**
30215
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
30216
+ *
30217
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
30218
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
30219
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
30220
+ * Default is `mid` + `low`.
30221
+ */
30222
+ published: boolean()
28967
30223
  });
28968
30224
  var GridLayoutViewSchema = object({
28969
30225
  /** The persisted grid row this camera was declared from. */
28970
30226
  instanceId: string(),
28971
30227
  deviceId: number().int().nonnegative(),
28972
30228
  name: string(),
28973
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
28974
- width: number().int(),
28975
- height: number().int(),
30229
+ /**
30230
+ * NO canvas size. A grid's resolution is not authored: each profile derives
30231
+ * its own from the cells and its sources' dimensions. The two numbers that
30232
+ * used to be here were a text field that silently decided both how much the
30233
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
30234
+ * came out as.
30235
+ */
28976
30236
  fps: number().int(),
28977
30237
  cells: array(GridLayoutCellSchema),
28978
30238
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -28981,9 +30241,13 @@ var GridLayoutViewSchema = object({
28981
30241
  var GridLayoutPatchSchema = object({
28982
30242
  deviceId: number().int().nonnegative(),
28983
30243
  name: string().min(1).max(160).optional(),
28984
- width: number().int().min(160).max(7680).optional(),
28985
- height: number().int().min(120).max(4320).optional(),
28986
30244
  fps: number().int().min(1).max(60).optional(),
30245
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
30246
+ publishedProfiles: array(_enum([
30247
+ "high",
30248
+ "mid",
30249
+ "low"
30250
+ ])).max(3).optional(),
28987
30251
  /**
28988
30252
  * The whole cell list at once. A per-cell patch would need an ordering the
28989
30253
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -31266,6 +32530,25 @@ var ptzAutotrackCapability = {
31266
32530
  scope: "device",
31267
32531
  deviceNative: true,
31268
32532
  mode: "singleton",
32533
+ /**
32534
+ * Two kinds of provider behind one name, the same shape `snapshot` and
32535
+ * `doorbell` use:
32536
+ *
32537
+ * - **Native**: the camera's own firmware autotrack, registered per device by
32538
+ * the driver when the probe says it exists (Reolink: yes; Hikvision:
32539
+ * model-dependent).
32540
+ * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
32541
+ * which follows a subject with `ptz.move` pulses driven by the detection
32542
+ * pipeline. `defaultActive: false` — the operator binds it per camera in
32543
+ * the bindings UI, exactly as `virtual-doorbell` is bound.
32544
+ *
32545
+ * The binding chooses WHICH implementation answers; `setEnabled` stays the
32546
+ * one switch for the function either way. A camera never has both driving its
32547
+ * head, which is the whole reason this is not a second cap with a second
32548
+ * toggle (D62, D520).
32549
+ */
32550
+ kind: "wrapper",
32551
+ defaultActive: false,
31269
32552
  deviceTypes: [DeviceType.Camera],
31270
32553
  deviceConfig: { ui: {
31271
32554
  kind: "widget",
@@ -31748,7 +33031,7 @@ method(object({
31748
33031
  auth: "admin"
31749
33032
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
31750
33033
  kind: "query",
31751
- auth: "admin"
33034
+ auth: "protected"
31752
33035
  }), method(object({
31753
33036
  deviceId: number(),
31754
33037
  profile: string(),
@@ -31978,13 +33261,101 @@ var ExportStateSchema = _enum([
31978
33261
  "expired",
31979
33262
  "deleted"
31980
33263
  ]);
31981
- /** One export job / history row. */
33264
+ /**
33265
+ * WHAT an export is — the authority, as opposed to the four top-level fields
33266
+ * the Library sorts and labels on (D558 § 2.2).
33267
+ *
33268
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
33269
+ * field is optional for the history rows written before it existed, and that
33270
+ * absence has exactly one interpreter.
33271
+ */
33272
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
33273
+ kind: literal("footage"),
33274
+ deviceId: number(),
33275
+ profiles: array(string()).min(1),
33276
+ fromMs: number(),
33277
+ toMs: number()
33278
+ }), object({
33279
+ kind: literal("clip"),
33280
+ deviceId: number(),
33281
+ provider: string().min(1),
33282
+ source: string().min(1),
33283
+ sourceLabel: string().min(1),
33284
+ clipId: string().min(1),
33285
+ /**
33286
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
33287
+ * already listing when the operator picked the segment.
33288
+ *
33289
+ * It is **not** a time range control and it never becomes one: the record's
33290
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
33291
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
33292
+ * `catalog-miss`, not a silently wider search. It exists because
33293
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
33294
+ * the catalog check D558 asks for is literally that call, and a call needs a
33295
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
33296
+ * wall-clock day, which is also the only width measured to be cheap (one
33297
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
33298
+ * of one child's events took 3.6–5.4 s).
33299
+ */
33300
+ catalogWindow: object({
33301
+ sinceMs: number(),
33302
+ untilMs: number()
33303
+ }),
33304
+ profile: _enum([
33305
+ "high",
33306
+ "mid",
33307
+ "low"
33308
+ ]),
33309
+ /**
33310
+ * The operator's authorisation to wake a sleeping camera for this export.
33311
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
33312
+ * because the gate that honours it is the clip provider's sleep gate — a
33313
+ * second enum here would be a second contract. Absent by default; never
33314
+ * settable by a scheduler or a retry.
33315
+ *
33316
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
33317
+ * an operator act that asks again; a queued job that outlived its wake fails
33318
+ * with a reason rather than waking on its turn; and this subject carries
33319
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
33320
+ */
33321
+ wake: ClipWakeSchema.optional()
33322
+ })]);
33323
+ /**
33324
+ * One export job / history row.
33325
+ *
33326
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
33327
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
33328
+ * Library sorts and labels on them (`library-items.ts` orders an export by
33329
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
33330
+ * that did not fill them would sort under the epoch and render a blank range.
33331
+ * Write to the subject and read from the projection and the two will disagree;
33332
+ * the projection is derived at creation and never edited afterwards.
33333
+ *
33334
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
33335
+ * who knows only the recording export will get wrong:
33336
+ *
33337
+ * | field | `kind:'footage'` | `kind:'clip'` |
33338
+ * | --- | --- | --- |
33339
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
33340
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
33341
+ *
33342
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
33343
+ * two authorities, D224's second copy).
33344
+ */
31982
33345
  var ExportRecordSchema = object({
31983
33346
  id: string(),
31984
33347
  deviceId: number(),
31985
33348
  profile: string(),
31986
33349
  fromMs: number(),
31987
33350
  toMs: number(),
33351
+ /**
33352
+ * What this export IS. Optional ONLY for the rows written before D558: the
33353
+ * store parses every row through this schema on every read, so a required
33354
+ * field would make the export AUDIT — which is the whole reason rows survive
33355
+ * file deletion — unreadable in one release. Absence means `footage`, and
33356
+ * {@link exportSubjectOf} is the one place that says so.
33357
+ */
33358
+ subject: ExportSubjectSchema.optional(),
31988
33359
  options: ExportOptionsSchema,
31989
33360
  state: ExportStateSchema,
31990
33361
  /** 0–100 while rendering; null otherwise. */
@@ -32001,6 +33372,18 @@ var ExportRecordSchema = object({
32001
33372
  /** Failure reason when state is 'failed'; null otherwise. */
32002
33373
  error: string().nullable()
32003
33374
  });
33375
+ _enum([
33376
+ "catalog-miss",
33377
+ "catalog-unreachable",
33378
+ "no-file-for-window",
33379
+ "clip-in-progress",
33380
+ "unsupported-option",
33381
+ "sleeping",
33382
+ "camera-refused",
33383
+ "fetch-failed",
33384
+ "too-large-to-transfer",
33385
+ "wake-expired"
33386
+ ]);
32004
33387
  /** Candidate download URLs (LAN first, then operator extra hosts). */
32005
33388
  var ExportDownloadSchema = object({
32006
33389
  url: string(),
@@ -32019,16 +33402,50 @@ var ExportBytesSchema = object({
32019
33402
  name: string(),
32020
33403
  bytes: number().int().nonnegative()
32021
33404
  });
33405
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
33406
+ function resolveExportProfiles(input) {
33407
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
33408
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
33409
+ return [];
33410
+ }
32022
33411
  method(object({
32023
33412
  deviceId: number(),
32024
33413
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
32025
33414
  profile: string().optional(),
32026
33415
  profiles: array(string()).min(1).optional(),
32027
- fromMs: number(),
32028
- toMs: number(),
33416
+ /** Footage only — a clip's boundaries are the camera's. */
33417
+ fromMs: number().optional(),
33418
+ toMs: number().optional(),
33419
+ /** What to export. Absent means the legacy flat footage request. */
33420
+ subject: ExportSubjectSchema.optional(),
32029
33421
  options: ExportOptionsSchema
32030
33422
  }).superRefine((v, ctx) => {
32031
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
33423
+ if (v.subject?.kind === "clip") {
33424
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
33425
+ code: ZodIssueCode.custom,
33426
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
33427
+ path: ["subject", "deviceId"]
33428
+ });
33429
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
33430
+ code: ZodIssueCode.custom,
33431
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
33432
+ path: ["fromMs"]
33433
+ });
33434
+ return;
33435
+ }
33436
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
33437
+ code: ZodIssueCode.custom,
33438
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
33439
+ path: ["subject", "deviceId"]
33440
+ });
33441
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
33442
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
33443
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
33444
+ code: ZodIssueCode.custom,
33445
+ message: "a footage export needs fromMs and toMs",
33446
+ path: ["fromMs"]
33447
+ });
33448
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
32032
33449
  code: ZodIssueCode.custom,
32033
33450
  message: "pass profiles[] (min 1) or legacy profile",
32034
33451
  path: ["profiles"]
@@ -34840,6 +36257,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34840
36257
  pressureSensor: pressureSensorCapability,
34841
36258
  privacyMask: privacyMaskCapability,
34842
36259
  ptzAutotrack: ptzAutotrackCapability,
36260
+ recordingOnboard: recordingOnboardCapability,
34843
36261
  recordingSignal: recordingSignalCapability,
34844
36262
  sceneMonitor: sceneMonitorCapability,
34845
36263
  scriptRunner: scriptRunnerCapability,
@@ -36001,7 +37419,1336 @@ var BaseDeviceProvider = class extends BaseAddon {
36001
37419
  DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
36002
37420
  new Set(Object.values(DeviceType));
36003
37421
  DeviceFeature.BatteryOperated;
37422
+ /**
37423
+ * The vocabulary, in the order a picker offers it.
37424
+ *
37425
+ * Adding a key here is not enough to make it pickable: both glyph tables must
37426
+ * gain it too, and their tests fail until they do. That is the intended
37427
+ * friction — it is the only thing that keeps the two surfaces honest.
37428
+ */
37429
+ var DISPLAY_ICON_VOCABULARY = [
37430
+ {
37431
+ key: "camera",
37432
+ group: "device",
37433
+ synonyms: [
37434
+ "telecamera",
37435
+ "videocamera",
37436
+ "camera",
37437
+ "cam"
37438
+ ]
37439
+ },
37440
+ {
37441
+ key: "network",
37442
+ group: "device",
37443
+ synonyms: [
37444
+ "rete",
37445
+ "network",
37446
+ "lan"
37447
+ ]
37448
+ },
37449
+ {
37450
+ key: "toggle-left",
37451
+ group: "device",
37452
+ synonyms: [
37453
+ "interruttore",
37454
+ "switch",
37455
+ "toggle"
37456
+ ]
37457
+ },
37458
+ {
37459
+ key: "toggle-right",
37460
+ group: "device",
37461
+ synonyms: [
37462
+ "interruttore",
37463
+ "switch",
37464
+ "toggle",
37465
+ "acceso"
37466
+ ]
37467
+ },
37468
+ {
37469
+ key: "circle-dot",
37470
+ group: "device",
37471
+ synonyms: [
37472
+ "pulsante",
37473
+ "bottone",
37474
+ "button",
37475
+ "dot"
37476
+ ]
37477
+ },
37478
+ {
37479
+ key: "refresh-cw",
37480
+ group: "device",
37481
+ synonyms: [
37482
+ "aggiorna",
37483
+ "ricarica",
37484
+ "refresh",
37485
+ "reload",
37486
+ "ciclo"
37487
+ ]
37488
+ },
37489
+ {
37490
+ key: "box",
37491
+ group: "device",
37492
+ synonyms: [
37493
+ "scatola",
37494
+ "box",
37495
+ "contenitore"
37496
+ ]
37497
+ },
37498
+ {
37499
+ key: "scroll-text",
37500
+ group: "device",
37501
+ synonyms: [
37502
+ "registro",
37503
+ "log",
37504
+ "testo"
37505
+ ]
37506
+ },
37507
+ {
37508
+ key: "workflow",
37509
+ group: "device",
37510
+ synonyms: [
37511
+ "automazione",
37512
+ "flusso",
37513
+ "workflow",
37514
+ "scenario"
37515
+ ]
37516
+ },
37517
+ {
37518
+ key: "lock",
37519
+ group: "device",
37520
+ synonyms: [
37521
+ "serratura",
37522
+ "chiave",
37523
+ "lucchetto",
37524
+ "chiuso",
37525
+ "locked"
37526
+ ]
37527
+ },
37528
+ {
37529
+ key: "lock-open",
37530
+ group: "device",
37531
+ synonyms: [
37532
+ "serratura",
37533
+ "aperta",
37534
+ "sbloccato",
37535
+ "unlocked"
37536
+ ]
37537
+ },
37538
+ {
37539
+ key: "blinds",
37540
+ group: "device",
37541
+ synonyms: [
37542
+ "tapparella",
37543
+ "persiana",
37544
+ "serranda",
37545
+ "avvolgibile",
37546
+ "shutter",
37547
+ "blind",
37548
+ "veneziana"
37549
+ ]
37550
+ },
37551
+ {
37552
+ key: "air-vent",
37553
+ group: "device",
37554
+ synonyms: [
37555
+ "bocchetta",
37556
+ "aria",
37557
+ "ventilazione",
37558
+ "condizionatore",
37559
+ "vent"
37560
+ ]
37561
+ },
37562
+ {
37563
+ key: "monitor-play",
37564
+ group: "device",
37565
+ synonyms: [
37566
+ "schermo",
37567
+ "riproduzione",
37568
+ "player",
37569
+ "display"
37570
+ ]
37571
+ },
37572
+ {
37573
+ key: "sliders-horizontal",
37574
+ group: "device",
37575
+ synonyms: [
37576
+ "regolazione",
37577
+ "cursori",
37578
+ "impostazioni",
37579
+ "slider"
37580
+ ]
37581
+ },
37582
+ {
37583
+ key: "map-pin",
37584
+ group: "device",
37585
+ synonyms: [
37586
+ "posizione",
37587
+ "luogo",
37588
+ "mappa",
37589
+ "pin"
37590
+ ]
37591
+ },
37592
+ {
37593
+ key: "bot",
37594
+ group: "device",
37595
+ synonyms: [
37596
+ "robot",
37597
+ "aspirapolvere",
37598
+ "bot"
37599
+ ]
37600
+ },
37601
+ {
37602
+ key: "tractor",
37603
+ group: "device",
37604
+ synonyms: [
37605
+ "trattore",
37606
+ "tosaerba",
37607
+ "giardino"
37608
+ ]
37609
+ },
37610
+ {
37611
+ key: "image",
37612
+ group: "device",
37613
+ synonyms: [
37614
+ "immagine",
37615
+ "foto",
37616
+ "snapshot"
37617
+ ]
37618
+ },
37619
+ {
37620
+ key: "bone",
37621
+ group: "device",
37622
+ synonyms: ["osso", "cane"]
37623
+ },
37624
+ {
37625
+ key: "door-closed",
37626
+ group: "device",
37627
+ synonyms: [
37628
+ "porta",
37629
+ "chiusa",
37630
+ "contatto"
37631
+ ]
37632
+ },
37633
+ {
37634
+ key: "door-open",
37635
+ group: "device",
37636
+ synonyms: [
37637
+ "porta",
37638
+ "aperta",
37639
+ "contatto",
37640
+ "ingresso",
37641
+ "uscita"
37642
+ ]
37643
+ },
37644
+ {
37645
+ key: "crosshair",
37646
+ group: "device",
37647
+ synonyms: [
37648
+ "mirino",
37649
+ "puntamento",
37650
+ "centro"
37651
+ ]
37652
+ },
37653
+ {
37654
+ key: "eye",
37655
+ group: "device",
37656
+ synonyms: [
37657
+ "occhio",
37658
+ "visibile",
37659
+ "visto"
37660
+ ]
37661
+ },
37662
+ {
37663
+ key: "eye-off",
37664
+ group: "device",
37665
+ synonyms: [
37666
+ "privacy",
37667
+ "nascosto",
37668
+ "oscurato",
37669
+ "mascherato",
37670
+ "invisibile"
37671
+ ]
37672
+ },
37673
+ {
37674
+ key: "smartphone",
37675
+ group: "device",
37676
+ synonyms: [
37677
+ "telefono",
37678
+ "cellulare",
37679
+ "smartphone"
37680
+ ]
37681
+ },
37682
+ {
37683
+ key: "phone",
37684
+ group: "device",
37685
+ synonyms: ["telefono", "chiamata"]
37686
+ },
37687
+ {
37688
+ key: "tablet",
37689
+ group: "device",
37690
+ synonyms: ["tablet", "ipad"]
37691
+ },
37692
+ {
37693
+ key: "laptop",
37694
+ group: "device",
37695
+ synonyms: [
37696
+ "portatile",
37697
+ "laptop",
37698
+ "notebook"
37699
+ ]
37700
+ },
37701
+ {
37702
+ key: "monitor",
37703
+ group: "device",
37704
+ synonyms: ["monitor", "schermo"]
37705
+ },
37706
+ {
37707
+ key: "tv",
37708
+ group: "device",
37709
+ synonyms: ["televisore", "tv"]
37710
+ },
37711
+ {
37712
+ key: "speaker",
37713
+ group: "device",
37714
+ synonyms: [
37715
+ "altoparlante",
37716
+ "cassa",
37717
+ "speaker"
37718
+ ]
37719
+ },
37720
+ {
37721
+ key: "printer",
37722
+ group: "device",
37723
+ synonyms: ["stampante"]
37724
+ },
37725
+ {
37726
+ key: "projector",
37727
+ group: "device",
37728
+ synonyms: ["proiettore"]
37729
+ },
37730
+ {
37731
+ key: "gamepad-2",
37732
+ group: "device",
37733
+ synonyms: [
37734
+ "console",
37735
+ "gioco",
37736
+ "joystick"
37737
+ ]
37738
+ },
37739
+ {
37740
+ key: "radio",
37741
+ group: "device",
37742
+ synonyms: ["radio"]
37743
+ },
37744
+ {
37745
+ key: "router",
37746
+ group: "device",
37747
+ synonyms: ["router", "modem"]
37748
+ },
37749
+ {
37750
+ key: "bluetooth",
37751
+ group: "device",
37752
+ synonyms: ["bluetooth"]
37753
+ },
37754
+ {
37755
+ key: "cpu",
37756
+ group: "device",
37757
+ synonyms: [
37758
+ "processore",
37759
+ "cpu",
37760
+ "chip",
37761
+ "dispositivo"
37762
+ ]
37763
+ },
37764
+ {
37765
+ key: "hard-drive",
37766
+ group: "device",
37767
+ synonyms: [
37768
+ "disco",
37769
+ "hdd",
37770
+ "archiviazione",
37771
+ "storage"
37772
+ ]
37773
+ },
37774
+ {
37775
+ key: "server",
37776
+ group: "device",
37777
+ synonyms: ["server", "nas"]
37778
+ },
37779
+ {
37780
+ key: "mic",
37781
+ group: "device",
37782
+ synonyms: ["microfono", "audio"]
37783
+ },
37784
+ {
37785
+ key: "mic-off",
37786
+ group: "device",
37787
+ synonyms: [
37788
+ "microfono",
37789
+ "spento",
37790
+ "muto"
37791
+ ]
37792
+ },
37793
+ {
37794
+ key: "video",
37795
+ group: "device",
37796
+ synonyms: ["video"]
37797
+ },
37798
+ {
37799
+ key: "video-off",
37800
+ group: "device",
37801
+ synonyms: ["video", "spento"]
37802
+ },
37803
+ {
37804
+ key: "music",
37805
+ group: "device",
37806
+ synonyms: ["musica"]
37807
+ },
37808
+ {
37809
+ key: "plug",
37810
+ group: "device",
37811
+ synonyms: [
37812
+ "presa",
37813
+ "spina",
37814
+ "plug"
37815
+ ]
37816
+ },
37817
+ {
37818
+ key: "plug-zap",
37819
+ group: "device",
37820
+ synonyms: ["presa", "alimentata"]
37821
+ },
37822
+ {
37823
+ key: "power",
37824
+ group: "device",
37825
+ synonyms: [
37826
+ "accensione",
37827
+ "alimentazione",
37828
+ "power"
37829
+ ]
37830
+ },
37831
+ {
37832
+ key: "power-off",
37833
+ group: "device",
37834
+ synonyms: ["spegnimento", "spento"]
37835
+ },
37836
+ {
37837
+ key: "cable",
37838
+ group: "device",
37839
+ synonyms: ["cavo"]
37840
+ },
37841
+ {
37842
+ key: "battery",
37843
+ group: "device",
37844
+ synonyms: ["batteria"]
37845
+ },
37846
+ {
37847
+ key: "battery-charging",
37848
+ group: "device",
37849
+ synonyms: [
37850
+ "batteria",
37851
+ "carica",
37852
+ "ricarica"
37853
+ ]
37854
+ },
37855
+ {
37856
+ key: "battery-full",
37857
+ group: "device",
37858
+ synonyms: [
37859
+ "batteria",
37860
+ "carica",
37861
+ "piena"
37862
+ ]
37863
+ },
37864
+ {
37865
+ key: "battery-low",
37866
+ group: "device",
37867
+ synonyms: ["batteria", "scarica"]
37868
+ },
37869
+ {
37870
+ key: "key",
37871
+ group: "device",
37872
+ synonyms: ["chiave"]
37873
+ },
37874
+ {
37875
+ key: "key-round",
37876
+ group: "device",
37877
+ synonyms: ["chiave", "tonda"]
37878
+ },
37879
+ {
37880
+ key: "mailbox",
37881
+ group: "device",
37882
+ synonyms: [
37883
+ "cassetta",
37884
+ "postale",
37885
+ "posta"
37886
+ ]
37887
+ },
37888
+ {
37889
+ key: "package",
37890
+ group: "device",
37891
+ synonyms: ["pacco", "consegna"]
37892
+ },
37893
+ {
37894
+ key: "shield",
37895
+ group: "device",
37896
+ synonyms: [
37897
+ "scudo",
37898
+ "protezione",
37899
+ "sicurezza",
37900
+ "allarme"
37901
+ ]
37902
+ },
37903
+ {
37904
+ key: "shield-check",
37905
+ group: "device",
37906
+ synonyms: [
37907
+ "protetto",
37908
+ "sicuro",
37909
+ "armato"
37910
+ ]
37911
+ },
37912
+ {
37913
+ key: "shield-off",
37914
+ group: "device",
37915
+ synonyms: [
37916
+ "protezione",
37917
+ "disattivata",
37918
+ "disarmato"
37919
+ ]
37920
+ },
37921
+ {
37922
+ key: "briefcase",
37923
+ group: "device",
37924
+ synonyms: [
37925
+ "ufficio",
37926
+ "lavoro",
37927
+ "valigetta",
37928
+ "studio"
37929
+ ]
37930
+ },
37931
+ {
37932
+ key: "lightbulb",
37933
+ group: "lighting",
37934
+ synonyms: [
37935
+ "luce",
37936
+ "lampadina",
37937
+ "lampada",
37938
+ "light"
37939
+ ]
37940
+ },
37941
+ {
37942
+ key: "lamp",
37943
+ group: "lighting",
37944
+ synonyms: ["lampada", "abatjour"]
37945
+ },
37946
+ {
37947
+ key: "lamp-ceiling",
37948
+ group: "lighting",
37949
+ synonyms: [
37950
+ "plafoniera",
37951
+ "lampadario",
37952
+ "luce",
37953
+ "soffitto"
37954
+ ]
37955
+ },
37956
+ {
37957
+ key: "lamp-desk",
37958
+ group: "lighting",
37959
+ synonyms: [
37960
+ "lampada",
37961
+ "scrivania",
37962
+ "abatjour"
37963
+ ]
37964
+ },
37965
+ {
37966
+ key: "lamp-floor",
37967
+ group: "lighting",
37968
+ synonyms: [
37969
+ "piantana",
37970
+ "lampada",
37971
+ "terra"
37972
+ ]
37973
+ },
37974
+ {
37975
+ key: "flashlight",
37976
+ group: "lighting",
37977
+ synonyms: [
37978
+ "faretto",
37979
+ "torcia",
37980
+ "spot",
37981
+ "faro"
37982
+ ]
37983
+ },
37984
+ {
37985
+ key: "heater",
37986
+ group: "lighting",
37987
+ synonyms: [
37988
+ "riscaldamento",
37989
+ "termosifone",
37990
+ "stufa",
37991
+ "calore"
37992
+ ]
37993
+ },
37994
+ {
37995
+ key: "snowflake",
37996
+ group: "lighting",
37997
+ synonyms: [
37998
+ "freddo",
37999
+ "raffrescamento",
38000
+ "aria",
38001
+ "condizionata",
38002
+ "neve"
38003
+ ]
38004
+ },
38005
+ {
38006
+ key: "thermometer-sun",
38007
+ group: "lighting",
38008
+ synonyms: [
38009
+ "caldo",
38010
+ "temperatura",
38011
+ "alta"
38012
+ ]
38013
+ },
38014
+ {
38015
+ key: "thermometer-snowflake",
38016
+ group: "lighting",
38017
+ synonyms: [
38018
+ "freddo",
38019
+ "temperatura",
38020
+ "bassa"
38021
+ ]
38022
+ },
38023
+ {
38024
+ key: "refrigerator",
38025
+ group: "lighting",
38026
+ synonyms: ["frigorifero", "frigo"]
38027
+ },
38028
+ {
38029
+ key: "microwave",
38030
+ group: "lighting",
38031
+ synonyms: ["microonde", "forno"]
38032
+ },
38033
+ {
38034
+ key: "washing-machine",
38035
+ group: "lighting",
38036
+ synonyms: [
38037
+ "lavatrice",
38038
+ "lavastoviglie",
38039
+ "asciugatrice"
38040
+ ]
38041
+ },
38042
+ {
38043
+ key: "cooking-pot",
38044
+ group: "lighting",
38045
+ synonyms: [
38046
+ "pentola",
38047
+ "cucina",
38048
+ "fornello"
38049
+ ]
38050
+ },
38051
+ {
38052
+ key: "utensils",
38053
+ group: "lighting",
38054
+ synonyms: [
38055
+ "cucina",
38056
+ "posate",
38057
+ "sala",
38058
+ "pranzo"
38059
+ ]
38060
+ },
38061
+ {
38062
+ key: "thermometer",
38063
+ group: "sensing",
38064
+ synonyms: [
38065
+ "temperatura",
38066
+ "termometro",
38067
+ "termostato"
38068
+ ]
38069
+ },
38070
+ {
38071
+ key: "droplets",
38072
+ group: "sensing",
38073
+ synonyms: [
38074
+ "umidita",
38075
+ "acqua",
38076
+ "allagamento",
38077
+ "perdita"
38078
+ ]
38079
+ },
38080
+ {
38081
+ key: "droplet",
38082
+ group: "sensing",
38083
+ synonyms: [
38084
+ "goccia",
38085
+ "acqua",
38086
+ "perdita",
38087
+ "allagamento"
38088
+ ]
38089
+ },
38090
+ {
38091
+ key: "gauge",
38092
+ group: "sensing",
38093
+ synonyms: [
38094
+ "misura",
38095
+ "livello",
38096
+ "pressione",
38097
+ "indicatore"
38098
+ ]
38099
+ },
38100
+ {
38101
+ key: "gauge2",
38102
+ group: "sensing",
38103
+ synonyms: [
38104
+ "misura",
38105
+ "livello",
38106
+ "pressione",
38107
+ "indicatore"
38108
+ ],
38109
+ aliasOf: "gauge"
38110
+ },
38111
+ {
38112
+ key: "sun",
38113
+ group: "sensing",
38114
+ synonyms: [
38115
+ "sole",
38116
+ "luminosita",
38117
+ "giorno",
38118
+ "esterno"
38119
+ ]
38120
+ },
38121
+ {
38122
+ key: "sun-medium",
38123
+ group: "sensing",
38124
+ synonyms: ["sole", "luminosita"]
38125
+ },
38126
+ {
38127
+ key: "sunrise",
38128
+ group: "sensing",
38129
+ synonyms: ["alba"]
38130
+ },
38131
+ {
38132
+ key: "moon",
38133
+ group: "sensing",
38134
+ synonyms: [
38135
+ "notte",
38136
+ "luna",
38137
+ "notturna",
38138
+ "nightvision"
38139
+ ]
38140
+ },
38141
+ {
38142
+ key: "wind",
38143
+ group: "sensing",
38144
+ synonyms: [
38145
+ "vento",
38146
+ "aria",
38147
+ "gas",
38148
+ "monossido",
38149
+ "co"
38150
+ ]
38151
+ },
38152
+ {
38153
+ key: "cloud",
38154
+ group: "sensing",
38155
+ synonyms: ["nuvola", "nuvoloso"]
38156
+ },
38157
+ {
38158
+ key: "cloud-rain",
38159
+ group: "sensing",
38160
+ synonyms: ["pioggia"]
38161
+ },
38162
+ {
38163
+ key: "cloud-sun",
38164
+ group: "sensing",
38165
+ synonyms: [
38166
+ "variabile",
38167
+ "sole",
38168
+ "nuvole"
38169
+ ]
38170
+ },
38171
+ {
38172
+ key: "waves",
38173
+ group: "sensing",
38174
+ synonyms: [
38175
+ "onde",
38176
+ "acqua",
38177
+ "vibrazione"
38178
+ ]
38179
+ },
38180
+ {
38181
+ key: "sprout",
38182
+ group: "sensing",
38183
+ synonyms: [
38184
+ "irrigazione",
38185
+ "pianta",
38186
+ "germoglio",
38187
+ "orto"
38188
+ ]
38189
+ },
38190
+ {
38191
+ key: "leaf",
38192
+ group: "sensing",
38193
+ synonyms: [
38194
+ "foglia",
38195
+ "pianta",
38196
+ "verde"
38197
+ ]
38198
+ },
38199
+ {
38200
+ key: "flower",
38201
+ group: "sensing",
38202
+ synonyms: ["fiore", "pianta"]
38203
+ },
38204
+ {
38205
+ key: "zap",
38206
+ group: "sensing",
38207
+ synonyms: [
38208
+ "energia",
38209
+ "corrente",
38210
+ "consumo",
38211
+ "watt",
38212
+ "elettricita"
38213
+ ]
38214
+ },
38215
+ {
38216
+ key: "flame",
38217
+ group: "sensing",
38218
+ synonyms: [
38219
+ "fiamma",
38220
+ "gas",
38221
+ "fuoco",
38222
+ "incendio"
38223
+ ]
38224
+ },
38225
+ {
38226
+ key: "bell",
38227
+ group: "sensing",
38228
+ synonyms: [
38229
+ "campanello",
38230
+ "suoneria",
38231
+ "notifica",
38232
+ "bell",
38233
+ "chime"
38234
+ ]
38235
+ },
38236
+ {
38237
+ key: "bell-ring",
38238
+ group: "sensing",
38239
+ synonyms: [
38240
+ "campanello",
38241
+ "suona",
38242
+ "doorbell",
38243
+ "notifica"
38244
+ ]
38245
+ },
38246
+ {
38247
+ key: "bell-off",
38248
+ group: "sensing",
38249
+ synonyms: [
38250
+ "silenzioso",
38251
+ "notifiche",
38252
+ "disattivate",
38253
+ "muto"
38254
+ ]
38255
+ },
38256
+ {
38257
+ key: "siren",
38258
+ group: "sensing",
38259
+ synonyms: ["sirena", "allarme"]
38260
+ },
38261
+ {
38262
+ key: "radar",
38263
+ group: "sensing",
38264
+ synonyms: [
38265
+ "movimento",
38266
+ "presenza",
38267
+ "pir",
38268
+ "rilevamento",
38269
+ "radar"
38270
+ ]
38271
+ },
38272
+ {
38273
+ key: "activity",
38274
+ group: "sensing",
38275
+ synonyms: [
38276
+ "attivita",
38277
+ "sensore",
38278
+ "generico"
38279
+ ]
38280
+ },
38281
+ {
38282
+ key: "hash",
38283
+ group: "sensing",
38284
+ synonyms: [
38285
+ "numero",
38286
+ "contatore",
38287
+ "valore"
38288
+ ]
38289
+ },
38290
+ {
38291
+ key: "clock",
38292
+ group: "sensing",
38293
+ synonyms: [
38294
+ "orologio",
38295
+ "ora",
38296
+ "tempo"
38297
+ ]
38298
+ },
38299
+ {
38300
+ key: "timer",
38301
+ group: "sensing",
38302
+ synonyms: [
38303
+ "timer",
38304
+ "conto",
38305
+ "rovescia"
38306
+ ]
38307
+ },
38308
+ {
38309
+ key: "calendar",
38310
+ group: "sensing",
38311
+ synonyms: [
38312
+ "calendario",
38313
+ "data",
38314
+ "pianificazione"
38315
+ ]
38316
+ },
38317
+ {
38318
+ key: "pipette",
38319
+ group: "sensing",
38320
+ synonyms: [
38321
+ "qualita",
38322
+ "aria",
38323
+ "misura",
38324
+ "campione"
38325
+ ]
38326
+ },
38327
+ {
38328
+ key: "fan",
38329
+ group: "sensing",
38330
+ synonyms: [
38331
+ "ventilatore",
38332
+ "ventola",
38333
+ "aria"
38334
+ ]
38335
+ },
38336
+ {
38337
+ key: "bath",
38338
+ group: "sensing",
38339
+ synonyms: [
38340
+ "bagno",
38341
+ "vasca",
38342
+ "doccia"
38343
+ ]
38344
+ },
38345
+ {
38346
+ key: "footprints",
38347
+ group: "sensing",
38348
+ synonyms: [
38349
+ "movimento",
38350
+ "passi",
38351
+ "presenza",
38352
+ "motion"
38353
+ ]
38354
+ },
38355
+ {
38356
+ key: "alarm-smoke",
38357
+ group: "sensing",
38358
+ synonyms: [
38359
+ "fumo",
38360
+ "antincendio",
38361
+ "rilevatore"
38362
+ ]
38363
+ },
38364
+ {
38365
+ key: "shield-alert",
38366
+ group: "sensing",
38367
+ synonyms: [
38368
+ "manomissione",
38369
+ "allarme",
38370
+ "tamper",
38371
+ "avviso"
38372
+ ]
38373
+ },
38374
+ {
38375
+ key: "vibrate",
38376
+ group: "sensing",
38377
+ synonyms: [
38378
+ "vibrazione",
38379
+ "urto",
38380
+ "tamper"
38381
+ ]
38382
+ },
38383
+ {
38384
+ key: "wifi",
38385
+ group: "sensing",
38386
+ synonyms: [
38387
+ "wifi",
38388
+ "rete",
38389
+ "segnale"
38390
+ ]
38391
+ },
38392
+ {
38393
+ key: "wifi-off",
38394
+ group: "sensing",
38395
+ synonyms: [
38396
+ "wifi",
38397
+ "assente",
38398
+ "offline",
38399
+ "disconnesso"
38400
+ ]
38401
+ },
38402
+ {
38403
+ key: "volume-2",
38404
+ group: "sensing",
38405
+ synonyms: [
38406
+ "volume",
38407
+ "audio",
38408
+ "altoparlante",
38409
+ "suono"
38410
+ ]
38411
+ },
38412
+ {
38413
+ key: "volume-x",
38414
+ group: "sensing",
38415
+ synonyms: [
38416
+ "muto",
38417
+ "silenziato",
38418
+ "volume",
38419
+ "spento"
38420
+ ]
38421
+ },
38422
+ {
38423
+ key: "heart",
38424
+ group: "sensing",
38425
+ synonyms: [
38426
+ "cuore",
38427
+ "preferito",
38428
+ "salute"
38429
+ ]
38430
+ },
38431
+ {
38432
+ key: "heart-pulse",
38433
+ group: "sensing",
38434
+ synonyms: [
38435
+ "battito",
38436
+ "salute",
38437
+ "monitoraggio"
38438
+ ]
38439
+ },
38440
+ {
38441
+ key: "star",
38442
+ group: "sensing",
38443
+ synonyms: [
38444
+ "preferito",
38445
+ "stella",
38446
+ "importante"
38447
+ ]
38448
+ },
38449
+ {
38450
+ key: "sparkles",
38451
+ group: "sensing",
38452
+ synonyms: [
38453
+ "automatico",
38454
+ "intelligente",
38455
+ "ai",
38456
+ "scena"
38457
+ ]
38458
+ },
38459
+ {
38460
+ key: "house",
38461
+ group: "place",
38462
+ synonyms: [
38463
+ "casa",
38464
+ "abitazione",
38465
+ "home"
38466
+ ]
38467
+ },
38468
+ {
38469
+ key: "sofa",
38470
+ group: "place",
38471
+ synonyms: [
38472
+ "salone",
38473
+ "soggiorno",
38474
+ "divano",
38475
+ "living"
38476
+ ]
38477
+ },
38478
+ {
38479
+ key: "bed-double",
38480
+ group: "place",
38481
+ synonyms: [
38482
+ "camera",
38483
+ "letto",
38484
+ "matrimoniale"
38485
+ ]
38486
+ },
38487
+ {
38488
+ key: "bed",
38489
+ group: "place",
38490
+ synonyms: [
38491
+ "cameretta",
38492
+ "letto",
38493
+ "singolo"
38494
+ ]
38495
+ },
38496
+ {
38497
+ key: "warehouse",
38498
+ group: "place",
38499
+ synonyms: [
38500
+ "magazzino",
38501
+ "capanno",
38502
+ "deposito",
38503
+ "cantina"
38504
+ ]
38505
+ },
38506
+ {
38507
+ key: "trees",
38508
+ group: "place",
38509
+ synonyms: [
38510
+ "giardino",
38511
+ "alberi",
38512
+ "esterno",
38513
+ "parco"
38514
+ ]
38515
+ },
38516
+ {
38517
+ key: "tree-pine",
38518
+ group: "place",
38519
+ synonyms: [
38520
+ "albero",
38521
+ "pino",
38522
+ "giardino"
38523
+ ]
38524
+ },
38525
+ {
38526
+ key: "fence",
38527
+ group: "place",
38528
+ synonyms: [
38529
+ "recinto",
38530
+ "cancello",
38531
+ "staccionata",
38532
+ "perimetro"
38533
+ ]
38534
+ },
38535
+ {
38536
+ key: "store",
38537
+ group: "place",
38538
+ synonyms: [
38539
+ "negozio",
38540
+ "ingresso",
38541
+ "vetrina"
38542
+ ]
38543
+ },
38544
+ {
38545
+ key: "building",
38546
+ group: "place",
38547
+ synonyms: ["edificio", "palazzo"]
38548
+ },
38549
+ {
38550
+ key: "building-2",
38551
+ group: "place",
38552
+ synonyms: [
38553
+ "edificio",
38554
+ "uffici",
38555
+ "condominio"
38556
+ ]
38557
+ },
38558
+ {
38559
+ key: "mountain",
38560
+ group: "place",
38561
+ synonyms: [
38562
+ "montagna",
38563
+ "esterno",
38564
+ "panorama"
38565
+ ]
38566
+ },
38567
+ {
38568
+ key: "umbrella",
38569
+ group: "place",
38570
+ synonyms: [
38571
+ "tenda",
38572
+ "markise",
38573
+ "ombrellone",
38574
+ "pergola",
38575
+ "veranda"
38576
+ ]
38577
+ },
38578
+ {
38579
+ key: "dumbbell",
38580
+ group: "place",
38581
+ synonyms: ["palestra", "attrezzi"]
38582
+ },
38583
+ {
38584
+ key: "wrench",
38585
+ group: "place",
38586
+ synonyms: [
38587
+ "officina",
38588
+ "manutenzione",
38589
+ "attrezzi",
38590
+ "utenze"
38591
+ ]
38592
+ },
38593
+ {
38594
+ key: "hammer",
38595
+ group: "place",
38596
+ synonyms: [
38597
+ "officina",
38598
+ "martello",
38599
+ "lavori"
38600
+ ]
38601
+ },
38602
+ {
38603
+ key: "drill",
38604
+ group: "place",
38605
+ synonyms: [
38606
+ "officina",
38607
+ "trapano",
38608
+ "lavori"
38609
+ ]
38610
+ },
38611
+ {
38612
+ key: "waves-ladder",
38613
+ group: "place",
38614
+ synonyms: ["piscina", "vasca"]
38615
+ },
38616
+ {
38617
+ key: "circle-parking",
38618
+ group: "place",
38619
+ synonyms: [
38620
+ "parcheggio",
38621
+ "posto",
38622
+ "auto",
38623
+ "garage"
38624
+ ]
38625
+ },
38626
+ {
38627
+ key: "route",
38628
+ group: "place",
38629
+ synonyms: [
38630
+ "percorso",
38631
+ "strada",
38632
+ "vialetto",
38633
+ "tragitto"
38634
+ ]
38635
+ },
38636
+ {
38637
+ key: "car",
38638
+ group: "life",
38639
+ synonyms: [
38640
+ "auto",
38641
+ "macchina",
38642
+ "veicolo",
38643
+ "parcheggio"
38644
+ ]
38645
+ },
38646
+ {
38647
+ key: "bike",
38648
+ group: "life",
38649
+ synonyms: [
38650
+ "bicicletta",
38651
+ "bici",
38652
+ "moto"
38653
+ ]
38654
+ },
38655
+ {
38656
+ key: "bus",
38657
+ group: "life",
38658
+ synonyms: ["autobus", "pullman"]
38659
+ },
38660
+ {
38661
+ key: "truck",
38662
+ group: "life",
38663
+ synonyms: [
38664
+ "camion",
38665
+ "furgone",
38666
+ "consegna"
38667
+ ]
38668
+ },
38669
+ {
38670
+ key: "plane",
38671
+ group: "life",
38672
+ synonyms: ["aereo"]
38673
+ },
38674
+ {
38675
+ key: "train-front",
38676
+ group: "life",
38677
+ synonyms: ["treno"]
38678
+ },
38679
+ {
38680
+ key: "sailboat",
38681
+ group: "life",
38682
+ synonyms: ["barca", "imbarcazione"]
38683
+ },
38684
+ {
38685
+ key: "dog",
38686
+ group: "life",
38687
+ synonyms: ["cane", "animale"]
38688
+ },
38689
+ {
38690
+ key: "cat",
38691
+ group: "life",
38692
+ synonyms: ["gatto", "animale"]
38693
+ },
38694
+ {
38695
+ key: "bird",
38696
+ group: "life",
38697
+ synonyms: ["uccello", "animale"]
38698
+ },
38699
+ {
38700
+ key: "paw-print",
38701
+ group: "life",
38702
+ synonyms: [
38703
+ "animale",
38704
+ "zampa",
38705
+ "pet"
38706
+ ]
38707
+ },
38708
+ {
38709
+ key: "baby",
38710
+ group: "life",
38711
+ synonyms: [
38712
+ "neonato",
38713
+ "bimbo",
38714
+ "cameretta",
38715
+ "babymonitor"
38716
+ ]
38717
+ },
38718
+ {
38719
+ key: "user",
38720
+ group: "life",
38721
+ synonyms: ["persona", "utente"]
38722
+ },
38723
+ {
38724
+ key: "users",
38725
+ group: "life",
38726
+ synonyms: [
38727
+ "persone",
38728
+ "gruppo",
38729
+ "famiglia"
38730
+ ]
38731
+ },
38732
+ {
38733
+ key: "person-standing",
38734
+ group: "life",
38735
+ synonyms: [
38736
+ "persona",
38737
+ "piedi",
38738
+ "presenza"
38739
+ ]
38740
+ }
38741
+ ];
38742
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
38743
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
38744
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
36004
38745
  Object.freeze({
38746
+ "accessories.getStatus": {
38747
+ capName: "accessories",
38748
+ capScope: "device",
38749
+ addonId: null,
38750
+ access: "view"
38751
+ },
36005
38752
  "accessories.setChildHidden": {
36006
38753
  capName: "accessories",
36007
38754
  capScope: "device",
@@ -36284,6 +39031,12 @@ Object.freeze({
36284
39031
  addonId: null,
36285
39032
  access: "view"
36286
39033
  },
39034
+ "airQualitySensor.getStatus": {
39035
+ capName: "air-quality-sensor",
39036
+ capScope: "device",
39037
+ addonId: null,
39038
+ access: "view"
39039
+ },
36287
39040
  "alarmPanel.arm": {
36288
39041
  capName: "alarm-panel",
36289
39042
  capScope: "device",
@@ -36296,6 +39049,12 @@ Object.freeze({
36296
39049
  addonId: null,
36297
39050
  access: "create"
36298
39051
  },
39052
+ "alarmPanel.getStatus": {
39053
+ capName: "alarm-panel",
39054
+ capScope: "device",
39055
+ addonId: null,
39056
+ access: "view"
39057
+ },
36299
39058
  "alarmPanel.trigger": {
36300
39059
  capName: "alarm-panel",
36301
39060
  capScope: "device",
@@ -36344,6 +39103,30 @@ Object.freeze({
36344
39103
  addonId: null,
36345
39104
  access: "create"
36346
39105
  },
39106
+ "ambientLightSensor.getStatus": {
39107
+ capName: "ambient-light-sensor",
39108
+ capScope: "device",
39109
+ addonId: null,
39110
+ access: "view"
39111
+ },
39112
+ "audioAnalysis.applyDeviceSettingsPatch": {
39113
+ capName: "audio-analysis",
39114
+ capScope: "device",
39115
+ addonId: null,
39116
+ access: "create"
39117
+ },
39118
+ "audioAnalysis.getDeviceLiveContribution": {
39119
+ capName: "audio-analysis",
39120
+ capScope: "device",
39121
+ addonId: null,
39122
+ access: "view"
39123
+ },
39124
+ "audioAnalysis.getDeviceSettingsContribution": {
39125
+ capName: "audio-analysis",
39126
+ capScope: "device",
39127
+ addonId: null,
39128
+ access: "view"
39129
+ },
36347
39130
  "audioAnalysis.resolveDeviceSettings": {
36348
39131
  capName: "audio-analysis",
36349
39132
  capScope: "device",
@@ -36506,6 +39289,12 @@ Object.freeze({
36506
39289
  addonId: null,
36507
39290
  access: "create"
36508
39291
  },
39292
+ "automationControl.getStatus": {
39293
+ capName: "automation-control",
39294
+ capScope: "device",
39295
+ addonId: null,
39296
+ access: "view"
39297
+ },
36509
39298
  "automationControl.trigger": {
36510
39299
  capName: "automation-control",
36511
39300
  capScope: "device",
@@ -36602,12 +39391,30 @@ Object.freeze({
36602
39391
  addonId: null,
36603
39392
  access: "create"
36604
39393
  },
39394
+ "battery.getStatus": {
39395
+ capName: "battery",
39396
+ capScope: "device",
39397
+ addonId: null,
39398
+ access: "view"
39399
+ },
36605
39400
  "battery.wakeForStream": {
36606
39401
  capName: "battery",
36607
39402
  capScope: "device",
36608
39403
  addonId: null,
36609
39404
  access: "create"
36610
39405
  },
39406
+ "binary.getStatus": {
39407
+ capName: "binary",
39408
+ capScope: "device",
39409
+ addonId: null,
39410
+ access: "view"
39411
+ },
39412
+ "brightness.getStatus": {
39413
+ capName: "brightness",
39414
+ capScope: "device",
39415
+ addonId: null,
39416
+ access: "view"
39417
+ },
36611
39418
  "brightness.setBrightness": {
36612
39419
  capName: "brightness",
36613
39420
  capScope: "device",
@@ -36722,6 +39529,30 @@ Object.freeze({
36722
39529
  addonId: null,
36723
39530
  access: "view"
36724
39531
  },
39532
+ "cameraCredentials.getStatus": {
39533
+ capName: "camera-credentials",
39534
+ capScope: "device",
39535
+ addonId: null,
39536
+ access: "view"
39537
+ },
39538
+ "cameraGridLayout.applyDeviceSettingsPatch": {
39539
+ capName: "camera-grid-layout",
39540
+ capScope: "device",
39541
+ addonId: null,
39542
+ access: "create"
39543
+ },
39544
+ "cameraGridLayout.getDeviceLiveContribution": {
39545
+ capName: "camera-grid-layout",
39546
+ capScope: "device",
39547
+ addonId: null,
39548
+ access: "view"
39549
+ },
39550
+ "cameraGridLayout.getDeviceSettingsContribution": {
39551
+ capName: "camera-grid-layout",
39552
+ capScope: "device",
39553
+ addonId: null,
39554
+ access: "view"
39555
+ },
36725
39556
  "cameraGridLayout.getLayout": {
36726
39557
  capName: "camera-grid-layout",
36727
39558
  capScope: "device",
@@ -36734,6 +39565,24 @@ Object.freeze({
36734
39565
  addonId: null,
36735
39566
  access: "create"
36736
39567
  },
39568
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
39569
+ capName: "camera-pipeline-config",
39570
+ capScope: "device",
39571
+ addonId: null,
39572
+ access: "create"
39573
+ },
39574
+ "cameraPipelineConfig.getDeviceLiveContribution": {
39575
+ capName: "camera-pipeline-config",
39576
+ capScope: "device",
39577
+ addonId: null,
39578
+ access: "view"
39579
+ },
39580
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
39581
+ capName: "camera-pipeline-config",
39582
+ capScope: "device",
39583
+ addonId: null,
39584
+ access: "view"
39585
+ },
36737
39586
  "cameraStreams.getBrokerStreams": {
36738
39587
  capName: "camera-streams",
36739
39588
  capScope: "device",
@@ -36764,6 +39613,18 @@ Object.freeze({
36764
39613
  addonId: null,
36765
39614
  access: "view"
36766
39615
  },
39616
+ "carbonMonoxide.getStatus": {
39617
+ capName: "carbon-monoxide",
39618
+ capScope: "device",
39619
+ addonId: null,
39620
+ access: "view"
39621
+ },
39622
+ "climateControl.getStatus": {
39623
+ capName: "climate-control",
39624
+ capScope: "device",
39625
+ addonId: null,
39626
+ access: "view"
39627
+ },
36767
39628
  "climateControl.setFanMode": {
36768
39629
  capName: "climate-control",
36769
39630
  capScope: "device",
@@ -36812,6 +39673,12 @@ Object.freeze({
36812
39673
  addonId: null,
36813
39674
  access: "create"
36814
39675
  },
39676
+ "color.getStatus": {
39677
+ capName: "color",
39678
+ capScope: "device",
39679
+ addonId: null,
39680
+ access: "view"
39681
+ },
36815
39682
  "color.setColor": {
36816
39683
  capName: "color",
36817
39684
  capScope: "device",
@@ -36830,12 +39697,36 @@ Object.freeze({
36830
39697
  addonId: null,
36831
39698
  access: "create"
36832
39699
  },
39700
+ "connectivity.getStatus": {
39701
+ capName: "connectivity",
39702
+ capScope: "device",
39703
+ addonId: null,
39704
+ access: "view"
39705
+ },
39706
+ "consumables.getStatus": {
39707
+ capName: "consumables",
39708
+ capScope: "device",
39709
+ addonId: null,
39710
+ access: "view"
39711
+ },
36833
39712
  "consumables.reset": {
36834
39713
  capName: "consumables",
36835
39714
  capScope: "device",
36836
39715
  addonId: null,
36837
39716
  access: "delete"
36838
39717
  },
39718
+ "contact.getStatus": {
39719
+ capName: "contact",
39720
+ capScope: "device",
39721
+ addonId: null,
39722
+ access: "view"
39723
+ },
39724
+ "control.getStatus": {
39725
+ capName: "control",
39726
+ capScope: "device",
39727
+ addonId: null,
39728
+ access: "view"
39729
+ },
36839
39730
  "control.setValue": {
36840
39731
  capName: "control",
36841
39732
  capScope: "device",
@@ -36902,6 +39793,12 @@ Object.freeze({
36902
39793
  addonId: null,
36903
39794
  access: "create"
36904
39795
  },
39796
+ "cover.getStatus": {
39797
+ capName: "cover",
39798
+ capScope: "device",
39799
+ addonId: null,
39800
+ access: "view"
39801
+ },
36905
39802
  "cover.open": {
36906
39803
  capName: "cover",
36907
39804
  capScope: "device",
@@ -37034,6 +39931,12 @@ Object.freeze({
37034
39931
  addonId: null,
37035
39932
  access: "view"
37036
39933
  },
39934
+ "dayNight.getStatus": {
39935
+ capName: "day-night",
39936
+ capScope: "device",
39937
+ addonId: null,
39938
+ access: "view"
39939
+ },
37037
39940
  "dayNight.setSettings": {
37038
39941
  capName: "day-night",
37039
39942
  capScope: "device",
@@ -37124,6 +40027,30 @@ Object.freeze({
37124
40027
  addonId: null,
37125
40028
  access: "view"
37126
40029
  },
40030
+ "detectionPipeline.applyDeviceSettingsPatch": {
40031
+ capName: "detection-pipeline",
40032
+ capScope: "device",
40033
+ addonId: null,
40034
+ access: "create"
40035
+ },
40036
+ "detectionPipeline.getDeviceLiveContribution": {
40037
+ capName: "detection-pipeline",
40038
+ capScope: "device",
40039
+ addonId: null,
40040
+ access: "view"
40041
+ },
40042
+ "detectionPipeline.getDeviceSettingsContribution": {
40043
+ capName: "detection-pipeline",
40044
+ capScope: "device",
40045
+ addonId: null,
40046
+ access: "view"
40047
+ },
40048
+ "deviceAdminLink.getAdminLink": {
40049
+ capName: "device-admin-link",
40050
+ capScope: "device",
40051
+ addonId: null,
40052
+ access: "view"
40053
+ },
37127
40054
  "deviceAdoption.adopt": {
37128
40055
  capName: "device-adoption",
37129
40056
  capScope: "system",
@@ -37136,6 +40063,12 @@ Object.freeze({
37136
40063
  addonId: null,
37137
40064
  access: "view"
37138
40065
  },
40066
+ "deviceAdoption.getStatus": {
40067
+ capName: "device-adoption",
40068
+ capScope: "system",
40069
+ addonId: null,
40070
+ access: "view"
40071
+ },
37139
40072
  "deviceAdoption.listCandidateFilters": {
37140
40073
  capName: "device-adoption",
37141
40074
  capScope: "system",
@@ -37172,6 +40105,12 @@ Object.freeze({
37172
40105
  addonId: null,
37173
40106
  access: "create"
37174
40107
  },
40108
+ "deviceDiscovery.getStatus": {
40109
+ capName: "device-discovery",
40110
+ capScope: "device",
40111
+ addonId: null,
40112
+ access: "view"
40113
+ },
37175
40114
  "deviceDiscovery.listDiscovered": {
37176
40115
  capName: "device-discovery",
37177
40116
  capScope: "device",
@@ -37190,12 +40129,30 @@ Object.freeze({
37190
40129
  addonId: null,
37191
40130
  access: "create"
37192
40131
  },
40132
+ "deviceExport.applyDeviceSettingsPatch": {
40133
+ capName: "device-export",
40134
+ capScope: "system",
40135
+ addonId: null,
40136
+ access: "create"
40137
+ },
37193
40138
  "deviceExport.exposeDevice": {
37194
40139
  capName: "device-export",
37195
40140
  capScope: "system",
37196
40141
  addonId: null,
37197
40142
  access: "create"
37198
40143
  },
40144
+ "deviceExport.getDeviceLiveContribution": {
40145
+ capName: "device-export",
40146
+ capScope: "system",
40147
+ addonId: null,
40148
+ access: "view"
40149
+ },
40150
+ "deviceExport.getDeviceSettingsContribution": {
40151
+ capName: "device-export",
40152
+ capScope: "system",
40153
+ addonId: null,
40154
+ access: "view"
40155
+ },
37199
40156
  "deviceExport.getStatus": {
37200
40157
  capName: "device-export",
37201
40158
  capScope: "system",
@@ -37292,6 +40249,12 @@ Object.freeze({
37292
40249
  addonId: null,
37293
40250
  access: "create"
37294
40251
  },
40252
+ "deviceManager.applyDeviceSettingsPatch": {
40253
+ capName: "device-manager",
40254
+ capScope: "system",
40255
+ addonId: null,
40256
+ access: "create"
40257
+ },
37295
40258
  "deviceManager.applyInitialMeta": {
37296
40259
  capName: "device-manager",
37297
40260
  capScope: "system",
@@ -37394,6 +40357,12 @@ Object.freeze({
37394
40357
  addonId: null,
37395
40358
  access: "view"
37396
40359
  },
40360
+ "deviceManager.getDeviceLiveContribution": {
40361
+ capName: "device-manager",
40362
+ capScope: "system",
40363
+ addonId: null,
40364
+ access: "view"
40365
+ },
37397
40366
  "deviceManager.getDeviceLiveInfoAggregate": {
37398
40367
  capName: "device-manager",
37399
40368
  capScope: "system",
@@ -37406,6 +40375,12 @@ Object.freeze({
37406
40375
  addonId: null,
37407
40376
  access: "view"
37408
40377
  },
40378
+ "deviceManager.getDeviceSettingsContribution": {
40379
+ capName: "device-manager",
40380
+ capScope: "system",
40381
+ addonId: null,
40382
+ access: "view"
40383
+ },
37409
40384
  "deviceManager.getDeviceStatusAggregate": {
37410
40385
  capName: "device-manager",
37411
40386
  capScope: "system",
@@ -37898,6 +40873,36 @@ Object.freeze({
37898
40873
  addonId: null,
37899
40874
  access: "create"
37900
40875
  },
40876
+ "deviceStatus.getStatus": {
40877
+ capName: "device-status",
40878
+ capScope: "device",
40879
+ addonId: null,
40880
+ access: "view"
40881
+ },
40882
+ "doorbell.applyDeviceSettingsPatch": {
40883
+ capName: "doorbell",
40884
+ capScope: "device",
40885
+ addonId: null,
40886
+ access: "create"
40887
+ },
40888
+ "doorbell.getDeviceLiveContribution": {
40889
+ capName: "doorbell",
40890
+ capScope: "device",
40891
+ addonId: null,
40892
+ access: "view"
40893
+ },
40894
+ "doorbell.getDeviceSettingsContribution": {
40895
+ capName: "doorbell",
40896
+ capScope: "device",
40897
+ addonId: null,
40898
+ access: "view"
40899
+ },
40900
+ "doorbell.getStatus": {
40901
+ capName: "doorbell",
40902
+ capScope: "device",
40903
+ addonId: null,
40904
+ access: "view"
40905
+ },
37901
40906
  "embeddingEncoder.encode": {
37902
40907
  capName: "embedding-encoder",
37903
40908
  capScope: "system",
@@ -37916,6 +40921,18 @@ Object.freeze({
37916
40921
  addonId: null,
37917
40922
  access: "view"
37918
40923
  },
40924
+ "enumSensor.getStatus": {
40925
+ capName: "enum-sensor",
40926
+ capScope: "device",
40927
+ addonId: null,
40928
+ access: "view"
40929
+ },
40930
+ "eventEmitter.getStatus": {
40931
+ capName: "event-emitter",
40932
+ capScope: "device",
40933
+ addonId: null,
40934
+ access: "view"
40935
+ },
37919
40936
  "events.getEventClipUrl": {
37920
40937
  capName: "events",
37921
40938
  capScope: "device",
@@ -38036,6 +41053,12 @@ Object.freeze({
38036
41053
  addonId: null,
38037
41054
  access: "view"
38038
41055
  },
41056
+ "fanControl.getStatus": {
41057
+ capName: "fan-control",
41058
+ capScope: "device",
41059
+ addonId: null,
41060
+ access: "view"
41061
+ },
38039
41062
  "fanControl.setDirection": {
38040
41063
  capName: "fan-control",
38041
41064
  capScope: "device",
@@ -38060,6 +41083,12 @@ Object.freeze({
38060
41083
  addonId: null,
38061
41084
  access: "create"
38062
41085
  },
41086
+ "featureProbe.getStatus": {
41087
+ capName: "feature-probe",
41088
+ capScope: "device",
41089
+ addonId: null,
41090
+ access: "view"
41091
+ },
38063
41092
  "filesystemBrowse.browse": {
38064
41093
  capName: "filesystem-browse",
38065
41094
  capScope: "system",
@@ -38078,6 +41107,24 @@ Object.freeze({
38078
41107
  addonId: null,
38079
41108
  access: "view"
38080
41109
  },
41110
+ "flood.getStatus": {
41111
+ capName: "flood",
41112
+ capScope: "device",
41113
+ addonId: null,
41114
+ access: "view"
41115
+ },
41116
+ "gas.getStatus": {
41117
+ capName: "gas",
41118
+ capScope: "device",
41119
+ addonId: null,
41120
+ access: "view"
41121
+ },
41122
+ "humidifier.getStatus": {
41123
+ capName: "humidifier",
41124
+ capScope: "device",
41125
+ addonId: null,
41126
+ access: "view"
41127
+ },
38081
41128
  "humidifier.setMode": {
38082
41129
  capName: "humidifier",
38083
41130
  capScope: "device",
@@ -38096,12 +41143,30 @@ Object.freeze({
38096
41143
  addonId: null,
38097
41144
  access: "create"
38098
41145
  },
41146
+ "humiditySensor.getStatus": {
41147
+ capName: "humidity-sensor",
41148
+ capScope: "device",
41149
+ addonId: null,
41150
+ access: "view"
41151
+ },
41152
+ "image.getStatus": {
41153
+ capName: "image",
41154
+ capScope: "device",
41155
+ addonId: null,
41156
+ access: "view"
41157
+ },
38099
41158
  "imageSettings.getOptions": {
38100
41159
  capName: "image-settings",
38101
41160
  capScope: "device",
38102
41161
  addonId: null,
38103
41162
  access: "view"
38104
41163
  },
41164
+ "imageSettings.getStatus": {
41165
+ capName: "image-settings",
41166
+ capScope: "device",
41167
+ addonId: null,
41168
+ access: "view"
41169
+ },
38105
41170
  "imageSettings.setSettings": {
38106
41171
  capName: "image-settings",
38107
41172
  capScope: "device",
@@ -38174,6 +41239,12 @@ Object.freeze({
38174
41239
  addonId: null,
38175
41240
  access: "create"
38176
41241
  },
41242
+ "intercom.getStatus": {
41243
+ capName: "intercom",
41244
+ capScope: "device",
41245
+ addonId: null,
41246
+ access: "view"
41247
+ },
38177
41248
  "intercom.handleAnswer": {
38178
41249
  capName: "intercom",
38179
41250
  capScope: "device",
@@ -38210,6 +41281,12 @@ Object.freeze({
38210
41281
  addonId: null,
38211
41282
  access: "create"
38212
41283
  },
41284
+ "lawnMowerControl.getStatus": {
41285
+ capName: "lawn-mower-control",
41286
+ capScope: "device",
41287
+ addonId: null,
41288
+ access: "view"
41289
+ },
38213
41290
  "lawnMowerControl.pause": {
38214
41291
  capName: "lawn-mower-control",
38215
41292
  capScope: "device",
@@ -38492,6 +41569,12 @@ Object.freeze({
38492
41569
  addonId: null,
38493
41570
  access: "create"
38494
41571
  },
41572
+ "lockControl.getStatus": {
41573
+ capName: "lock-control",
41574
+ capScope: "device",
41575
+ addonId: null,
41576
+ access: "view"
41577
+ },
38495
41578
  "lockControl.lock": {
38496
41579
  capName: "lock-control",
38497
41580
  capScope: "device",
@@ -38540,6 +41623,12 @@ Object.freeze({
38540
41623
  addonId: null,
38541
41624
  access: "view"
38542
41625
  },
41626
+ "mediaPlayer.getStatus": {
41627
+ capName: "media-player",
41628
+ capScope: "device",
41629
+ addonId: null,
41630
+ access: "view"
41631
+ },
38543
41632
  "mediaPlayer.next": {
38544
41633
  capName: "media-player",
38545
41634
  capScope: "device",
@@ -38732,6 +41821,12 @@ Object.freeze({
38732
41821
  addonId: null,
38733
41822
  access: "create"
38734
41823
  },
41824
+ "motion.getStatus": {
41825
+ capName: "motion",
41826
+ capScope: "device",
41827
+ addonId: null,
41828
+ access: "view"
41829
+ },
38735
41830
  "motion.isDetected": {
38736
41831
  capName: "motion",
38737
41832
  capScope: "device",
@@ -38744,6 +41839,24 @@ Object.freeze({
38744
41839
  addonId: null,
38745
41840
  access: "create"
38746
41841
  },
41842
+ "motionDetection.applyDeviceSettingsPatch": {
41843
+ capName: "motion-detection",
41844
+ capScope: "device",
41845
+ addonId: null,
41846
+ access: "create"
41847
+ },
41848
+ "motionDetection.getDeviceLiveContribution": {
41849
+ capName: "motion-detection",
41850
+ capScope: "device",
41851
+ addonId: null,
41852
+ access: "view"
41853
+ },
41854
+ "motionDetection.getDeviceSettingsContribution": {
41855
+ capName: "motion-detection",
41856
+ capScope: "device",
41857
+ addonId: null,
41858
+ access: "view"
41859
+ },
38747
41860
  "motionDetection.removeCamera": {
38748
41861
  capName: "motion-detection",
38749
41862
  capScope: "device",
@@ -38756,6 +41869,12 @@ Object.freeze({
38756
41869
  addonId: null,
38757
41870
  access: "delete"
38758
41871
  },
41872
+ "motionTrigger.getStatus": {
41873
+ capName: "motion-trigger",
41874
+ capScope: "device",
41875
+ addonId: null,
41876
+ access: "view"
41877
+ },
38759
41878
  "motionTrigger.setMotionTrigger": {
38760
41879
  capName: "motion-trigger",
38761
41880
  capScope: "device",
@@ -38768,6 +41887,12 @@ Object.freeze({
38768
41887
  addonId: null,
38769
41888
  access: "view"
38770
41889
  },
41890
+ "motionZones.getStatus": {
41891
+ capName: "motion-zones",
41892
+ capScope: "device",
41893
+ addonId: null,
41894
+ access: "view"
41895
+ },
38771
41896
  "motionZones.setZone": {
38772
41897
  capName: "motion-zones",
38773
41898
  capScope: "device",
@@ -38828,6 +41953,12 @@ Object.freeze({
38828
41953
  addonId: null,
38829
41954
  access: "view"
38830
41955
  },
41956
+ "nativeObjectDetection.getStatus": {
41957
+ capName: "native-object-detection",
41958
+ capScope: "device",
41959
+ addonId: null,
41960
+ access: "view"
41961
+ },
38831
41962
  "nativeObjectDetection.setEnabled": {
38832
41963
  capName: "native-object-detection",
38833
41964
  capScope: "device",
@@ -38846,6 +41977,12 @@ Object.freeze({
38846
41977
  addonId: null,
38847
41978
  access: "view"
38848
41979
  },
41980
+ "navigation.getStatus": {
41981
+ capName: "navigation",
41982
+ capScope: "device",
41983
+ addonId: null,
41984
+ access: "view"
41985
+ },
38849
41986
  "navigation.goToPoint": {
38850
41987
  capName: "navigation",
38851
41988
  capScope: "device",
@@ -38930,6 +42067,12 @@ Object.freeze({
38930
42067
  addonId: null,
38931
42068
  access: "create"
38932
42069
  },
42070
+ "networkLink.getStatus": {
42071
+ capName: "network-link",
42072
+ capScope: "device",
42073
+ addonId: null,
42074
+ access: "view"
42075
+ },
38933
42076
  "networkQuality.getAllStats": {
38934
42077
  capName: "network-quality",
38935
42078
  capScope: "system",
@@ -39188,18 +42331,36 @@ Object.freeze({
39188
42331
  addonId: null,
39189
42332
  access: "create"
39190
42333
  },
42334
+ "notifier.getStatus": {
42335
+ capName: "notifier",
42336
+ capScope: "device",
42337
+ addonId: null,
42338
+ access: "view"
42339
+ },
39191
42340
  "notifier.send": {
39192
42341
  capName: "notifier",
39193
42342
  capScope: "device",
39194
42343
  addonId: null,
39195
42344
  access: "create"
39196
42345
  },
42346
+ "numericSensor.getStatus": {
42347
+ capName: "numeric-sensor",
42348
+ capScope: "device",
42349
+ addonId: null,
42350
+ access: "view"
42351
+ },
39197
42352
  "oauthIntegration.getDescriptor": {
39198
42353
  capName: "oauth-integration",
39199
42354
  capScope: "system",
39200
42355
  addonId: null,
39201
42356
  access: "view"
39202
42357
  },
42358
+ "osd.getStatus": {
42359
+ capName: "osd",
42360
+ capScope: "device",
42361
+ addonId: null,
42362
+ access: "view"
42363
+ },
39203
42364
  "osd.setOverlay": {
39204
42365
  capName: "osd",
39205
42366
  capScope: "device",
@@ -39272,6 +42433,12 @@ Object.freeze({
39272
42433
  addonId: null,
39273
42434
  access: "create"
39274
42435
  },
42436
+ "petFeeder.getStatus": {
42437
+ capName: "pet-feeder",
42438
+ capScope: "device",
42439
+ addonId: null,
42440
+ access: "view"
42441
+ },
39275
42442
  "petFeeder.markFoodReplenished": {
39276
42443
  capName: "pet-feeder",
39277
42444
  capScope: "device",
@@ -39314,6 +42481,12 @@ Object.freeze({
39314
42481
  addonId: null,
39315
42482
  access: "create"
39316
42483
  },
42484
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
42485
+ capName: "pipeline-analytics",
42486
+ capScope: "device",
42487
+ addonId: null,
42488
+ access: "create"
42489
+ },
39317
42490
  "pipelineAnalytics.cancelRelocateMedia": {
39318
42491
  capName: "pipeline-analytics",
39319
42492
  capScope: "device",
@@ -39380,6 +42553,18 @@ Object.freeze({
39380
42553
  addonId: null,
39381
42554
  access: "view"
39382
42555
  },
42556
+ "pipelineAnalytics.getDeviceLiveContribution": {
42557
+ capName: "pipeline-analytics",
42558
+ capScope: "device",
42559
+ addonId: null,
42560
+ access: "view"
42561
+ },
42562
+ "pipelineAnalytics.getDeviceSettingsContribution": {
42563
+ capName: "pipeline-analytics",
42564
+ capScope: "device",
42565
+ addonId: null,
42566
+ access: "view"
42567
+ },
39383
42568
  "pipelineAnalytics.getEventDensity": {
39384
42569
  capName: "pipeline-analytics",
39385
42570
  capScope: "device",
@@ -39590,6 +42775,12 @@ Object.freeze({
39590
42775
  addonId: null,
39591
42776
  access: "view"
39592
42777
  },
42778
+ "pipelineAnalytics.ownersWithMedia": {
42779
+ capName: "pipeline-analytics",
42780
+ capScope: "device",
42781
+ addonId: null,
42782
+ access: "view"
42783
+ },
39593
42784
  "pipelineAnalytics.pauseForStorageMigration": {
39594
42785
  capName: "pipeline-analytics",
39595
42786
  capScope: "device",
@@ -39944,6 +43135,12 @@ Object.freeze({
39944
43135
  addonId: null,
39945
43136
  access: "view"
39946
43137
  },
43138
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
43139
+ capName: "pipeline-orchestrator",
43140
+ capScope: "system",
43141
+ addonId: null,
43142
+ access: "create"
43143
+ },
39947
43144
  "pipelineOrchestrator.assignAudio": {
39948
43145
  capName: "pipeline-orchestrator",
39949
43146
  capScope: "system",
@@ -40034,6 +43231,18 @@ Object.freeze({
40034
43231
  addonId: null,
40035
43232
  access: "view"
40036
43233
  },
43234
+ "pipelineOrchestrator.getDeviceLiveContribution": {
43235
+ capName: "pipeline-orchestrator",
43236
+ capScope: "system",
43237
+ addonId: null,
43238
+ access: "view"
43239
+ },
43240
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
43241
+ capName: "pipeline-orchestrator",
43242
+ capScope: "system",
43243
+ addonId: null,
43244
+ access: "view"
43245
+ },
40037
43246
  "pipelineOrchestrator.getGlobalMetrics": {
40038
43247
  capName: "pipeline-orchestrator",
40039
43248
  capScope: "system",
@@ -40442,12 +43651,36 @@ Object.freeze({
40442
43651
  addonId: null,
40443
43652
  access: "view"
40444
43653
  },
43654
+ "powerMeter.getStatus": {
43655
+ capName: "power-meter",
43656
+ capScope: "device",
43657
+ addonId: null,
43658
+ access: "view"
43659
+ },
43660
+ "presence.getStatus": {
43661
+ capName: "presence",
43662
+ capScope: "device",
43663
+ addonId: null,
43664
+ access: "view"
43665
+ },
43666
+ "pressureSensor.getStatus": {
43667
+ capName: "pressure-sensor",
43668
+ capScope: "device",
43669
+ addonId: null,
43670
+ access: "view"
43671
+ },
40445
43672
  "privacyMask.getOptions": {
40446
43673
  capName: "privacy-mask",
40447
43674
  capScope: "device",
40448
43675
  addonId: null,
40449
43676
  access: "view"
40450
43677
  },
43678
+ "privacyMask.getStatus": {
43679
+ capName: "privacy-mask",
43680
+ capScope: "device",
43681
+ addonId: null,
43682
+ access: "view"
43683
+ },
40451
43684
  "privacyMask.setAudioEnabled": {
40452
43685
  capName: "privacy-mask",
40453
43686
  capScope: "device",
@@ -40508,6 +43741,12 @@ Object.freeze({
40508
43741
  addonId: null,
40509
43742
  access: "view"
40510
43743
  },
43744
+ "ptz.getStatus": {
43745
+ capName: "ptz",
43746
+ capScope: "device",
43747
+ addonId: null,
43748
+ access: "view"
43749
+ },
40511
43750
  "ptz.goHome": {
40512
43751
  capName: "ptz",
40513
43752
  capScope: "device",
@@ -40586,6 +43825,12 @@ Object.freeze({
40586
43825
  addonId: null,
40587
43826
  access: "create"
40588
43827
  },
43828
+ "recording.applyDeviceSettingsPatch": {
43829
+ capName: "recording",
43830
+ capScope: "system",
43831
+ addonId: null,
43832
+ access: "create"
43833
+ },
40589
43834
  "recording.cancelRelocateJob": {
40590
43835
  capName: "recording",
40591
43836
  capScope: "system",
@@ -40634,6 +43879,18 @@ Object.freeze({
40634
43879
  addonId: null,
40635
43880
  access: "view"
40636
43881
  },
43882
+ "recording.getDeviceLiveContribution": {
43883
+ capName: "recording",
43884
+ capScope: "system",
43885
+ addonId: null,
43886
+ access: "view"
43887
+ },
43888
+ "recording.getDeviceSettingsContribution": {
43889
+ capName: "recording",
43890
+ capScope: "system",
43891
+ addonId: null,
43892
+ access: "view"
43893
+ },
40637
43894
  "recording.getPlacement": {
40638
43895
  capName: "recording",
40639
43896
  capScope: "system",
@@ -40652,6 +43909,12 @@ Object.freeze({
40652
43909
  addonId: null,
40653
43910
  access: "view"
40654
43911
  },
43912
+ "recording.getStatus": {
43913
+ capName: "recording",
43914
+ capScope: "system",
43915
+ addonId: null,
43916
+ access: "view"
43917
+ },
40655
43918
  "recording.getStorageMigrationMoveStatus": {
40656
43919
  capName: "recording",
40657
43920
  capScope: "system",
@@ -40826,6 +44089,24 @@ Object.freeze({
40826
44089
  addonId: null,
40827
44090
  access: "view"
40828
44091
  },
44092
+ "recordingOnboard.getOptions": {
44093
+ capName: "recording-onboard",
44094
+ capScope: "device",
44095
+ addonId: null,
44096
+ access: "view"
44097
+ },
44098
+ "recordingOnboard.getStatus": {
44099
+ capName: "recording-onboard",
44100
+ capScope: "device",
44101
+ addonId: null,
44102
+ access: "view"
44103
+ },
44104
+ "recordingOnboard.setSettings": {
44105
+ capName: "recording-onboard",
44106
+ capScope: "device",
44107
+ addonId: null,
44108
+ access: "create"
44109
+ },
40829
44110
  "recordingSignal.getStatus": {
40830
44111
  capName: "recording-signal",
40831
44112
  capScope: "device",
@@ -40856,6 +44137,12 @@ Object.freeze({
40856
44137
  addonId: null,
40857
44138
  access: "delete"
40858
44139
  },
44140
+ "sceneMonitor.getStatus": {
44141
+ capName: "scene-monitor",
44142
+ capScope: "device",
44143
+ addonId: null,
44144
+ access: "view"
44145
+ },
40859
44146
  "sceneMonitor.listScenes": {
40860
44147
  capName: "scene-monitor",
40861
44148
  capScope: "device",
@@ -40886,6 +44173,12 @@ Object.freeze({
40886
44173
  addonId: null,
40887
44174
  access: "create"
40888
44175
  },
44176
+ "scriptRunner.getStatus": {
44177
+ capName: "script-runner",
44178
+ capScope: "device",
44179
+ addonId: null,
44180
+ access: "view"
44181
+ },
40889
44182
  "scriptRunner.run": {
40890
44183
  capName: "script-runner",
40891
44184
  capScope: "device",
@@ -41018,6 +44311,12 @@ Object.freeze({
41018
44311
  addonId: null,
41019
44312
  access: "create"
41020
44313
  },
44314
+ "smoke.getStatus": {
44315
+ capName: "smoke",
44316
+ capScope: "device",
44317
+ addonId: null,
44318
+ access: "view"
44319
+ },
41021
44320
  "smtpProvider.getStatus": {
41022
44321
  capName: "smtp-provider",
41023
44322
  capScope: "system",
@@ -41036,12 +44335,30 @@ Object.freeze({
41036
44335
  addonId: null,
41037
44336
  access: "view"
41038
44337
  },
44338
+ "snapshot.applyDeviceSettingsPatch": {
44339
+ capName: "snapshot",
44340
+ capScope: "device",
44341
+ addonId: null,
44342
+ access: "create"
44343
+ },
41039
44344
  "snapshot.getDebugState": {
41040
44345
  capName: "snapshot",
41041
44346
  capScope: "device",
41042
44347
  addonId: null,
41043
44348
  access: "view"
41044
44349
  },
44350
+ "snapshot.getDeviceLiveContribution": {
44351
+ capName: "snapshot",
44352
+ capScope: "device",
44353
+ addonId: null,
44354
+ access: "view"
44355
+ },
44356
+ "snapshot.getDeviceSettingsContribution": {
44357
+ capName: "snapshot",
44358
+ capScope: "device",
44359
+ addonId: null,
44360
+ access: "view"
44361
+ },
41045
44362
  "snapshot.getSnapshot": {
41046
44363
  capName: "snapshot",
41047
44364
  capScope: "device",
@@ -41060,6 +44377,12 @@ Object.freeze({
41060
44377
  addonId: null,
41061
44378
  access: "view"
41062
44379
  },
44380
+ "snapshot.getStatus": {
44381
+ capName: "snapshot",
44382
+ capScope: "device",
44383
+ addonId: null,
44384
+ access: "view"
44385
+ },
41063
44386
  "snapshot.invalidateCache": {
41064
44387
  capName: "snapshot",
41065
44388
  capScope: "device",
@@ -41396,6 +44719,12 @@ Object.freeze({
41396
44719
  addonId: null,
41397
44720
  access: "create"
41398
44721
  },
44722
+ "streamBroker.applyDeviceSettingsPatch": {
44723
+ capName: "stream-broker",
44724
+ capScope: "system",
44725
+ addonId: null,
44726
+ access: "create"
44727
+ },
41399
44728
  "streamBroker.assignProfile": {
41400
44729
  capName: "stream-broker",
41401
44730
  capScope: "system",
@@ -41432,6 +44761,18 @@ Object.freeze({
41432
44761
  addonId: null,
41433
44762
  access: "view"
41434
44763
  },
44764
+ "streamBroker.getDeviceLiveContribution": {
44765
+ capName: "stream-broker",
44766
+ capScope: "system",
44767
+ addonId: null,
44768
+ access: "view"
44769
+ },
44770
+ "streamBroker.getDeviceSettingsContribution": {
44771
+ capName: "stream-broker",
44772
+ capScope: "system",
44773
+ addonId: null,
44774
+ access: "view"
44775
+ },
41435
44776
  "streamBroker.getPreBufferInfo": {
41436
44777
  capName: "stream-broker",
41437
44778
  capScope: "system",
@@ -41630,6 +44971,12 @@ Object.freeze({
41630
44971
  addonId: null,
41631
44972
  access: "view"
41632
44973
  },
44974
+ "streamParams.getStatus": {
44975
+ capName: "stream-params",
44976
+ capScope: "device",
44977
+ addonId: null,
44978
+ access: "view"
44979
+ },
41633
44980
  "streamParams.setProfile": {
41634
44981
  capName: "stream-params",
41635
44982
  capScope: "device",
@@ -41642,6 +44989,12 @@ Object.freeze({
41642
44989
  addonId: null,
41643
44990
  access: "view"
41644
44991
  },
44992
+ "switch.getStatus": {
44993
+ capName: "switch",
44994
+ capScope: "device",
44995
+ addonId: null,
44996
+ access: "view"
44997
+ },
41645
44998
  "switch.setState": {
41646
44999
  capName: "switch",
41647
45000
  capScope: "device",
@@ -41738,6 +45091,18 @@ Object.freeze({
41738
45091
  addonId: null,
41739
45092
  access: "create"
41740
45093
  },
45094
+ "tamper.getStatus": {
45095
+ capName: "tamper",
45096
+ capScope: "device",
45097
+ addonId: null,
45098
+ access: "view"
45099
+ },
45100
+ "temperatureSensor.getStatus": {
45101
+ capName: "temperature-sensor",
45102
+ capScope: "device",
45103
+ addonId: null,
45104
+ access: "view"
45105
+ },
41741
45106
  "terminalSession.adoptLegacyMonitor": {
41742
45107
  capName: "terminal-session",
41743
45108
  capScope: "system",
@@ -41834,6 +45199,12 @@ Object.freeze({
41834
45199
  addonId: null,
41835
45200
  access: "view"
41836
45201
  },
45202
+ "update.getStatus": {
45203
+ capName: "update",
45204
+ capScope: "device",
45205
+ addonId: null,
45206
+ access: "view"
45207
+ },
41837
45208
  "update.installUpdate": {
41838
45209
  capName: "update",
41839
45210
  capScope: "device",
@@ -42056,6 +45427,12 @@ Object.freeze({
42056
45427
  addonId: null,
42057
45428
  access: "create"
42058
45429
  },
45430
+ "vacuumControl.getStatus": {
45431
+ capName: "vacuum-control",
45432
+ capScope: "device",
45433
+ addonId: null,
45434
+ access: "view"
45435
+ },
42059
45436
  "vacuumControl.locate": {
42060
45437
  capName: "vacuum-control",
42061
45438
  capScope: "device",
@@ -42098,6 +45475,12 @@ Object.freeze({
42098
45475
  addonId: null,
42099
45476
  access: "create"
42100
45477
  },
45478
+ "valve.getStatus": {
45479
+ capName: "valve",
45480
+ capScope: "device",
45481
+ addonId: null,
45482
+ access: "view"
45483
+ },
42101
45484
  "valve.open": {
42102
45485
  capName: "valve",
42103
45486
  capScope: "device",
@@ -42170,6 +45553,18 @@ Object.freeze({
42170
45553
  addonId: null,
42171
45554
  access: "create"
42172
45555
  },
45556
+ "vibration.getStatus": {
45557
+ capName: "vibration",
45558
+ capScope: "device",
45559
+ addonId: null,
45560
+ access: "view"
45561
+ },
45562
+ "videoclips.dialClipStream": {
45563
+ capName: "videoclips",
45564
+ capScope: "device",
45565
+ addonId: null,
45566
+ access: "view"
45567
+ },
42173
45568
  "videoclips.getClipPlayback": {
42174
45569
  capName: "videoclips",
42175
45570
  capScope: "device",
@@ -42182,6 +45577,18 @@ Object.freeze({
42182
45577
  addonId: null,
42183
45578
  access: "view"
42184
45579
  },
45580
+ "videoclips.listSources": {
45581
+ capName: "videoclips",
45582
+ capScope: "device",
45583
+ addonId: null,
45584
+ access: "view"
45585
+ },
45586
+ "videoclips.readClipBytes": {
45587
+ capName: "videoclips",
45588
+ capScope: "device",
45589
+ addonId: null,
45590
+ access: "view"
45591
+ },
42185
45592
  "viewerUi.getStaticDir": {
42186
45593
  capName: "viewer-ui",
42187
45594
  capScope: "system",
@@ -42194,6 +45601,12 @@ Object.freeze({
42194
45601
  addonId: null,
42195
45602
  access: "view"
42196
45603
  },
45604
+ "waterHeater.getStatus": {
45605
+ capName: "water-heater",
45606
+ capScope: "device",
45607
+ addonId: null,
45608
+ access: "view"
45609
+ },
42197
45610
  "waterHeater.setAway": {
42198
45611
  capName: "water-heater",
42199
45612
  capScope: "device",
@@ -42212,6 +45625,12 @@ Object.freeze({
42212
45625
  addonId: null,
42213
45626
  access: "create"
42214
45627
  },
45628
+ "weather.getStatus": {
45629
+ capName: "weather",
45630
+ capScope: "device",
45631
+ addonId: null,
45632
+ access: "view"
45633
+ },
42215
45634
  "webrtcSession.addIceCandidate": {
42216
45635
  capName: "webrtc-session",
42217
45636
  capScope: "device",
@@ -42236,6 +45655,12 @@ Object.freeze({
42236
45655
  addonId: null,
42237
45656
  access: "view"
42238
45657
  },
45658
+ "webrtcSession.getSessionDebug": {
45659
+ capName: "webrtc-session",
45660
+ capScope: "device",
45661
+ addonId: null,
45662
+ access: "view"
45663
+ },
42239
45664
  "webrtcSession.getSessionState": {
42240
45665
  capName: "webrtc-session",
42241
45666
  capScope: "device",
@@ -42568,6 +45993,11 @@ Object.freeze({
42568
45993
  form: "single",
42569
45994
  optional: true
42570
45995
  }],
45996
+ "deviceAdminLink.getAdminLink": [{
45997
+ name: "deviceId",
45998
+ form: "single",
45999
+ optional: false
46000
+ }],
42571
46001
  "deviceAdoption.release": [{
42572
46002
  name: "camDeviceId",
42573
46003
  form: "single",
@@ -43478,6 +46908,11 @@ Object.freeze({
43478
46908
  form: "single",
43479
46909
  optional: false
43480
46910
  }],
46911
+ "pipelineAnalytics.ownersWithMedia": [{
46912
+ name: "deviceId",
46913
+ form: "single",
46914
+ optional: false
46915
+ }],
43481
46916
  "pipelineAnalytics.proposeRetrainAnnotations": [{
43482
46917
  name: "deviceId",
43483
46918
  form: "single",
@@ -43948,6 +47383,16 @@ Object.freeze({
43948
47383
  form: "single",
43949
47384
  optional: true
43950
47385
  }],
47386
+ "recordingOnboard.getOptions": [{
47387
+ name: "deviceId",
47388
+ form: "single",
47389
+ optional: false
47390
+ }],
47391
+ "recordingOnboard.setSettings": [{
47392
+ name: "deviceId",
47393
+ form: "single",
47394
+ optional: false
47395
+ }],
43951
47396
  "recordingSignal.getStatus": [{
43952
47397
  name: "deviceId",
43953
47398
  form: "single",
@@ -44174,6 +47619,11 @@ Object.freeze({
44174
47619
  form: "single",
44175
47620
  optional: false
44176
47621
  }],
47622
+ "videoclips.dialClipStream": [{
47623
+ name: "deviceId",
47624
+ form: "single",
47625
+ optional: false
47626
+ }],
44177
47627
  "videoclips.getClipPlayback": [{
44178
47628
  name: "deviceId",
44179
47629
  form: "single",
@@ -44184,6 +47634,16 @@ Object.freeze({
44184
47634
  form: "single",
44185
47635
  optional: false
44186
47636
  }],
47637
+ "videoclips.listSources": [{
47638
+ name: "deviceId",
47639
+ form: "single",
47640
+ optional: false
47641
+ }],
47642
+ "videoclips.readClipBytes": [{
47643
+ name: "deviceId",
47644
+ form: "single",
47645
+ optional: false
47646
+ }],
44187
47647
  "waterHeater.setAway": [{
44188
47648
  name: "deviceId",
44189
47649
  form: "single",
@@ -44219,6 +47679,11 @@ Object.freeze({
44219
47679
  form: "single",
44220
47680
  optional: false
44221
47681
  }],
47682
+ "webrtcSession.getSessionDebug": [{
47683
+ name: "deviceId",
47684
+ form: "single",
47685
+ optional: false
47686
+ }],
44222
47687
  "webrtcSession.getSessionState": [{
44223
47688
  name: "deviceId",
44224
47689
  form: "single",
@@ -45340,11 +48805,50 @@ var VeluxGatewayDevice = class extends BaseDevice {
45340
48805
  super(ctx, veluxGatewaySchema, { type: DeviceType.Hub });
45341
48806
  this.online = false;
45342
48807
  this.registerDeviceDiscovery();
48808
+ this.registerAdminLinkCap();
45343
48809
  this.updateSourceInfo({
45344
48810
  id: this.stableId,
45345
48811
  system: "velux"
45346
48812
  });
45347
48813
  }
48814
+ /**
48815
+ * device-admin-link — the hub's own management page.
48816
+ *
48817
+ * The KLF-200 serves its web interface over HTTPS only, with a self-signed
48818
+ * certificate — the browser will warn, which is the unit's own behaviour and
48819
+ * not something to paper over. The password printed on the back of the unit
48820
+ * is in the same connection object and is NOT placed in the URL; the page
48821
+ * asks for it.
48822
+ *
48823
+ * Only the hub gets the cap. Its children are radio actuators with no web
48824
+ * server anywhere, and the address they would borrow lives on the hub, which
48825
+ * is where the operator edits it.
48826
+ */
48827
+ registerAdminLinkCap() {
48828
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
48829
+ if (deviceId !== this.id) throw new Error(`VeluxGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
48830
+ const connection = this.config.values.connection;
48831
+ const built = buildDeviceAdminUrl({
48832
+ host: connection.host,
48833
+ port: null,
48834
+ scheme: "https",
48835
+ path: "/"
48836
+ });
48837
+ if (!built.ok) {
48838
+ this.ctx.logger.debug("velux admin link unavailable", {
48839
+ tags: { deviceId: this.id },
48840
+ meta: { reason: built.reason }
48841
+ });
48842
+ return null;
48843
+ }
48844
+ return {
48845
+ url: built.url,
48846
+ label: "KLF-200 web interface",
48847
+ target: "integration",
48848
+ host: connection.host
48849
+ };
48850
+ } });
48851
+ }
45348
48852
  /** Adopt a pre-built manager (created by `onCreateDevice` so the live client
45349
48853
  * connects once). Published on the resolver so children pick it up. */
45350
48854
  adoptManager(manager) {