@camstack/addon-provider-rademacher 0.2.114 → 0.2.115

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 +3527 -28
  2. package/dist/addon.mjs +3527 -28
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -6344,7 +6344,7 @@ var ZodIssueCode = {
6344
6344
  var ZodFirstPartyTypeKind;
6345
6345
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6346
6346
  //#endregion
6347
- //#region ../types/dist/sleep-GU_us3DG.mjs
6347
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
6348
6348
  /**
6349
6349
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6350
6350
  * window to float samples (D455).
@@ -7486,6 +7486,17 @@ var CamProfileSchema = _enum([
7486
7486
  "mid",
7487
7487
  "low"
7488
7488
  ]);
7489
+ /**
7490
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
7491
+ *
7492
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
7493
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
7494
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
7495
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
7496
+ * the operator picked. Today's one user is the audio analyser, whose rule is
7497
+ * "the lowest AVAILABLE native rung" (D537).
7498
+ */
7499
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
7489
7500
  var CamStreamKindSchema = _enum([
7490
7501
  "pull-rtsp",
7491
7502
  "pull-rtmp",
@@ -7904,6 +7915,18 @@ function systemMethod(input, output, options) {
7904
7915
  systemOnly: true
7905
7916
  };
7906
7917
  }
7918
+ /**
7919
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
7920
+ * on `InferProvider`. The `providerOptional: true` literal is what
7921
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
7922
+ * for when this is the honest answer and when it is a soft stub.
7923
+ */
7924
+ function optionalMethod(input, output, options) {
7925
+ return {
7926
+ ...method(input, output, options),
7927
+ providerOptional: true
7928
+ };
7929
+ }
7907
7930
  /** Shorthand to define an event schema */
7908
7931
  function event(data) {
7909
7932
  return { data };
@@ -8334,6 +8357,8 @@ var EVENT_OWNER_TYPES = [
8334
8357
  * nothing failing until a caller asked.
8335
8358
  */
8336
8359
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
8360
+ /** The same list as a Zod enum, for the cap input that carries it. */
8361
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
8337
8362
  new Set(EVENT_OWNER_TYPES);
8338
8363
  var EncodeProfileSchema = object({
8339
8364
  video: object({
@@ -8706,6 +8731,7 @@ var CameraSwitchIdSchema = _enum([
8706
8731
  "device-audio",
8707
8732
  "broker-audio",
8708
8733
  "audio-analysis",
8734
+ "event-saving",
8709
8735
  "recording",
8710
8736
  "notifications"
8711
8737
  ]);
@@ -8741,10 +8767,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
8741
8767
  var CameraSwitchUnavailableReasonSchema = _enum([
8742
8768
  "no-provider",
8743
8769
  "source-unreachable",
8744
- "not-configured",
8745
8770
  "device-disabled"
8746
8771
  ]);
8747
8772
  /**
8773
+ * Something true about a switch that is nonetheless OFFERED.
8774
+ *
8775
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
8776
+ * whole point: a reason replaces the control, a warning travels WITH it. The
8777
+ * privacy mask taught us the difference — greying the one button that starts
8778
+ * the feature teaches the operator the feature is broken, while a live toggle
8779
+ * that flips nothing teaches them the switch is broken. Neither is true; the
8780
+ * honest surface is a working toggle that says what is missing.
8781
+ */
8782
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
8783
+ /**
8748
8784
  * One switch, resolved for one camera.
8749
8785
  *
8750
8786
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -8763,6 +8799,11 @@ var CameraSwitchSchema = object({
8763
8799
  /** False = do not render a control. `unavailableReason` says why. */
8764
8800
  available: boolean(),
8765
8801
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
8802
+ /**
8803
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
8804
+ * never a substitute for one.
8805
+ */
8806
+ warning: CameraSwitchWarningSchema.optional(),
8766
8807
  /** Current state. Meaningless when `available` is false — read it as `true`. */
8767
8808
  enabled: boolean(),
8768
8809
  authority: CameraSwitchAuthoritySchema
@@ -9180,11 +9221,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
9180
9221
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
9181
9222
  * span wraps past midnight (handled by the band engine).
9182
9223
  */
9224
+ /**
9225
+ * Whether a band's footage carries sound.
9226
+ *
9227
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
9228
+ * camera recorded sound whether anyone wanted it or not. The broker already
9229
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
9230
+ * restream — so this was a choice the product could always have offered.
9231
+ *
9232
+ * It belongs on the BAND rather than the camera because one camera can want
9233
+ * both: silent across the shop floor during opening hours, sound overnight.
9234
+ * And it applies to BOTH modes — an events band firing on motion has the same
9235
+ * question to answer as a continuous one.
9236
+ */
9237
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
9183
9238
  var RecordingBandSchema = object({
9184
9239
  days: array(RecordingWeekdaySchema),
9185
9240
  start: string().regex(HHMM),
9186
9241
  end: string().regex(HHMM),
9187
9242
  mode: RecordingBandModeSchema,
9243
+ /**
9244
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
9245
+ * written before this existed carried sound, and a band whose key is
9246
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
9247
+ */
9248
+ audio: RecordingBandAudioSchema.optional(),
9188
9249
  triggers: RecordingBandTriggersSchema.optional(),
9189
9250
  /**
9190
9251
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -14468,6 +14529,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
14468
14529
  auth: "admin"
14469
14530
  });
14470
14531
  /**
14532
+ * device-admin-link — "this device has a management page of its own, and here
14533
+ * is its address".
14534
+ *
14535
+ * ## Why this is not a `deviceConfig` cap
14536
+ *
14537
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14538
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14539
+ * patch back through a setter; it costs a `builderId` reducer in
14540
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14541
+ * renders a form section. This cap answers ONE question with ONE read and
14542
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14543
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
14544
+ * `reboot`, the other pure-RPC device-native cap.
14545
+ *
14546
+ * ## Absent, and the difference between "no page" and "we cannot say"
14547
+ *
14548
+ * The two are answered at DIFFERENT layers, on purpose:
14549
+ *
14550
+ * - **"We cannot say"** → the provider never registers the cap for that
14551
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14552
+ * fan are reached only through a vendor cloud; there is no address to hand
14553
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14554
+ * conditioner DO have a LAN IP, and still have no HTTP management page
14555
+ * behind it. None of them register, so `deviceManager.getBindings` never
14556
+ * lists the cap and no surface asks.
14557
+ * - **"This device has no page, and I know that"** → the provider registers
14558
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
14559
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
14560
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14561
+ * transport, a Home Assistant broker authenticated by supervisor token
14562
+ * (which carries no `baseUrl` at all).
14563
+ *
14564
+ * Both draw NOTHING. A button that opens a browser error is worse than no
14565
+ * button, and D62 is the same rule from the other side: an off switch is
14566
+ * reported off, never made to look broken. There is no third state where the
14567
+ * UI renders a disabled button "because the device might have a page".
14568
+ *
14569
+ * ## The URL never carries credentials
14570
+ *
14571
+ * Not in userinfo, not in a query string. Every provider builds through
14572
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14573
+ * scheme and path as separate arguments — there is no parameter a secret could
14574
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14575
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14576
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14577
+ * keeps providers from hand-rolling one anyway.
14578
+ *
14579
+ * This matters here more than anywhere else in the repo, because every provider
14580
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14581
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14582
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14583
+ * camera's own page will ask for its own login. That is correct, and pre-
14584
+ * filling it is the operator's business, not ours.
14585
+ *
14586
+ * ## It is a LAN fact
14587
+ *
14588
+ * The URL addresses the device where the NODE can see it. It is not proxied,
14589
+ * not made reachable from outside, and not sent anywhere. A surface renders it
14590
+ * as a link the operator's own browser follows, on the operator's own network,
14591
+ * or renders nothing.
14592
+ */
14593
+ /**
14594
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
14595
+ * before clicking whether he is about to land on a camera's own web server or
14596
+ * inside Home Assistant.
14597
+ */
14598
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
14599
+ var DeviceAdminLinkSchema = object({
14600
+ /**
14601
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14602
+ * free of userinfo and of any credential-shaped query key.
14603
+ */
14604
+ url: string(),
14605
+ /**
14606
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14607
+ * The PROVIDER names it, because only the provider knows what the page is;
14608
+ * a UI that invented the label from the addon id would call the Home
14609
+ * Assistant device page "Provider Homeassistant".
14610
+ */
14611
+ label: string(),
14612
+ target: AdminLinkTargetEnum,
14613
+ /**
14614
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
14615
+ * an operator can see WHERE the button goes before he follows it. Redundant
14616
+ * with `url` by construction; carried separately so no surface has to parse
14617
+ * a URL to show it.
14618
+ */
14619
+ host: string()
14620
+ });
14621
+ var deviceAdminLinkCapability = {
14622
+ name: "device-admin-link",
14623
+ scope: "device",
14624
+ deviceNative: true,
14625
+ mode: "singleton",
14626
+ methods: {
14627
+ /**
14628
+ * The device's management page, or `null` when this device has none.
14629
+ *
14630
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
14631
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14632
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14633
+ * The URL is also a statement about the LAN, which a household member with
14634
+ * a `view` grant on a light has no reason to be handed.
14635
+ *
14636
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
14637
+ * without the right loses the query and draws nothing, which is the same
14638
+ * thing a device with no page draws. There is no path on which a button
14639
+ * appears and then fails — the D403 failure mode, from the other end.
14640
+ */
14641
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14642
+ };
14643
+ /**
14644
+ * Query keys that may never appear on an admin link. A credential smuggled as
14645
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
14646
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
14647
+ * identically.
14648
+ */
14649
+ var CREDENTIAL_QUERY_KEYS = [
14650
+ "password",
14651
+ "passwd",
14652
+ "pwd",
14653
+ "pass",
14654
+ "user",
14655
+ "username",
14656
+ "usr",
14657
+ "login",
14658
+ "token",
14659
+ "access_token",
14660
+ "auth",
14661
+ "authorization",
14662
+ "apikey",
14663
+ "api_key",
14664
+ "secret",
14665
+ "credential",
14666
+ "credentials",
14667
+ "session",
14668
+ "sessionid",
14669
+ "key"
14670
+ ];
14671
+ /**
14672
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
14673
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
14674
+ * structurally identical on purpose: a URL this function returns is a URL that
14675
+ * guard would pass.
14676
+ */
14677
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
14678
+ /** A host that is safe to place in an authority component verbatim. */
14679
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
14680
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
14681
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
14682
+ function defaultPortFor(scheme) {
14683
+ return scheme === "https" ? 443 : 80;
14684
+ }
14685
+ /**
14686
+ * Build the admin-page URL, or refuse by name.
14687
+ *
14688
+ * The refusal is never thrown: a provider answering "no page for this device"
14689
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
14690
+ * a missing host on one device into a failed query for the whole surface.
14691
+ */
14692
+ function buildDeviceAdminUrl(parts) {
14693
+ const host = parts.host.trim();
14694
+ if (host.length === 0) return {
14695
+ ok: false,
14696
+ reason: "empty-host"
14697
+ };
14698
+ if (host.includes("@")) return {
14699
+ ok: false,
14700
+ reason: "host-carries-userinfo"
14701
+ };
14702
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
14703
+ ok: false,
14704
+ reason: "host-not-plain"
14705
+ };
14706
+ if (parts.port !== null) {
14707
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
14708
+ ok: false,
14709
+ reason: "port-out-of-range"
14710
+ };
14711
+ }
14712
+ if (!parts.path.startsWith("/")) return {
14713
+ ok: false,
14714
+ reason: "path-not-absolute"
14715
+ };
14716
+ const query = parts.query ?? {};
14717
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
14718
+ ok: false,
14719
+ reason: "credential-query-key"
14720
+ };
14721
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
14722
+ const search = new URLSearchParams();
14723
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
14724
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
14725
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
14726
+ if (CREDENTIAL_URL.test(url)) return {
14727
+ ok: false,
14728
+ reason: "userinfo-in-result"
14729
+ };
14730
+ return {
14731
+ ok: true,
14732
+ url
14733
+ };
14734
+ }
14735
+ /**
14471
14736
  * `device-export` — collection cap for addons that export camstack
14472
14737
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14473
14738
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -21234,6 +21499,7 @@ var MediaFileKindEnum = _enum([
21234
21499
  "keyFrameSmall",
21235
21500
  "thumbnailSmall",
21236
21501
  "confirmationCrop",
21502
+ "debugCrop",
21237
21503
  "sceneFrame"
21238
21504
  ]);
21239
21505
  /**
@@ -22313,7 +22579,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
22313
22579
  ownerType: EventOwnerTypeSchema,
22314
22580
  eventId: number().int(),
22315
22581
  deviceId: number()
22316
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
22582
+ }), array(MediaFileInfoSchema).readonly()), method(object({
22583
+ deviceId: number(),
22584
+ ownerKind: MediaPresenceOwnerKindSchema,
22585
+ ownerIds: array(string()).max(5e3)
22586
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
22317
22587
  kind: "mutation",
22318
22588
  auth: "admin"
22319
22589
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -22898,6 +23168,18 @@ var RunnerCameraConfigSchema = object({
22898
23168
  motionStreamId: string(),
22899
23169
  detectionStreamId: string(),
22900
23170
  /**
23171
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
23172
+ * AVAILABLE native rung) or an explicit one.
23173
+ *
23174
+ * Not consumed by the runner: carried here so it travels with the camera's
23175
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
23176
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
23177
+ * with no default — absent means `auto`, which is what every camera written
23178
+ * before this field had, and an absent optional adds nothing to the
23179
+ * generated cap-input defaults (D537).
23180
+ */
23181
+ audioStreamPick: AudioStreamPickSchema.optional(),
23182
+ /**
22901
23183
  * Motion sources that can activate the pipeline. At least one must
22902
23184
  * be present. Multiple sources OR-ed: any one firing arms the
22903
23185
  * cooldown timer and transitions watching → active. The same
@@ -23968,10 +24250,9 @@ var ServerBootModeSchema = _enum([
23968
24250
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
23969
24251
  * - `pending-restart` — a version is staged and the node has NOT yet
23970
24252
  * restarted onto it (still running the OLD version).
23971
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
23972
- * (it is the active probation boot) and is waiting to confirm boot-health.
23973
- * Apply/rollback are refused in this state and the node must NOT be
23974
- * manually restarted, or the probation boot auto-rolls-back.
24253
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
24254
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
24255
+ * no provider emits it.
23975
24256
  */
23976
24257
  var ServerUpdateStateSchema = _enum([
23977
24258
  "idle",
@@ -24060,12 +24341,35 @@ var ServerRollbackInfoSchema = object({
24060
24341
  var ServerPackageStatusSchema = object({
24061
24342
  /** Root package name (`@camstack/server` on the hub). */
24062
24343
  packageName: string(),
24063
- /** Version of the code the running process ACTUALLY loaded. */
24344
+ /**
24345
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
24346
+ * package.json of the copy the code was loaded from, and never re-read. A
24347
+ * copy replaced on disk under a live process must not move this number;
24348
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
24349
+ * was a lazy disk read that would have followed such a swap silently.
24350
+ */
24064
24351
  runningVersion: string().nullable(),
24065
24352
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
24066
24353
  nodeRuntimeVersion: string().nullable(),
24067
24354
  /** Active data-dir root version; null when booted from seed/workspace. */
24068
24355
  activeVersion: string().nullable(),
24356
+ /**
24357
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
24358
+ * what the next boot would load if nothing is staged. Normally equal to
24359
+ * `runningVersion`; differs only when the copy on disk changed under the
24360
+ * live process. `null` when there is no data-root copy (baked seed or
24361
+ * workspace boot). Optional for version skew: an older provider omits it.
24362
+ */
24363
+ installedVersion: string().nullable().optional(),
24364
+ /**
24365
+ * True when the code that would run after a restart is NOT the code running
24366
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
24367
+ * from `runningVersion`. Derived on every read, never stored. This is the
24368
+ * one field that lets an operator or a script say "1.2.353 is installed and
24369
+ * will run after a restart" instead of reading it off a number that means
24370
+ * "installed". Optional for version skew.
24371
+ */
24372
+ restartRequired: boolean().optional(),
24069
24373
  /** N-1 version kept for rollback; null when no previous version exists. */
24070
24374
  previousVersion: string().nullable(),
24071
24375
  /** Version of the immutable baked seed closure (image fallback). */
@@ -24100,19 +24404,71 @@ var ServerPackageStatusSchema = object({
24100
24404
  });
24101
24405
  var ServerUpdateCheckResultSchema = object({
24102
24406
  packageName: string(),
24407
+ /** Same meaning as on the status payload: what THIS process loaded. */
24103
24408
  runningVersion: string().nullable(),
24104
24409
  latestVersion: string().nullable(),
24410
+ /**
24411
+ * `latestVersion` is newer than `runningVersion` AND is not the version
24412
+ * already staged on this node. A staged-and-waiting version is not "an
24413
+ * update available" — applying it again is refused (`already-staged`), and
24414
+ * a button that announced "updating…" on this boolean then got refused
24415
+ * would be lying. It is reported through `pendingVersion` instead.
24416
+ */
24105
24417
  updateAvailable: boolean(),
24106
24418
  checkedAtMs: number(),
24107
24419
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
24108
- error: string().nullable()
24420
+ error: string().nullable(),
24421
+ /**
24422
+ * Version staged on this node and awaiting its restart, when one is. Before
24423
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
24424
+ * node whose staged closure had never booted, and the caller had no way to
24425
+ * see the difference. Optional for version skew.
24426
+ */
24427
+ pendingVersion: string().nullable().optional(),
24428
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
24429
+ restartRequired: boolean().optional()
24109
24430
  });
24431
+ /**
24432
+ * What an action DID — the discriminant `accepted` cannot carry. Before
24433
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
24434
+ * where nothing was wrong (already current, already staged) and for a staging
24435
+ * install that died on `npm error code ECONNRESET`; the only difference was
24436
+ * prose in `message`, and an operator restarted a container on the strength of
24437
+ * it, booting the previous version.
24438
+ *
24439
+ * - `staged` — the closure is installed and validated; the node
24440
+ * restarts to apply it (`restarting:true`). NOTHING
24441
+ * runs the new code until that restart completes.
24442
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
24443
+ * - `already-current` — the target IS the running version; nothing done.
24444
+ * - `refused` — dropped by a gate; `reason` names which.
24445
+ * - `failed` — attempted and did not land; `error` says why.
24446
+ */
24447
+ var ServerUpdateOutcomeSchema = _enum([
24448
+ "staged",
24449
+ "restart-scheduled",
24450
+ "already-current",
24451
+ "refused",
24452
+ "failed"
24453
+ ]);
24110
24454
  var ServerUpdateActionResultSchema = object({
24111
24455
  accepted: boolean(),
24112
24456
  targetVersion: string().nullable(),
24113
24457
  /** True when a graceful restart was scheduled to apply the change. */
24114
24458
  restarting: boolean(),
24115
- message: string()
24459
+ message: string(),
24460
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
24461
+ outcome: ServerUpdateOutcomeSchema.optional(),
24462
+ /**
24463
+ * Stable, greppable cause for `refused` / `failed` (the engine's
24464
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
24465
+ */
24466
+ reason: string().nullable().optional(),
24467
+ /**
24468
+ * The failure itself, verbatim enough to act on (the npm output for a
24469
+ * staging install). Non-null only for `failed`.
24470
+ */
24471
+ error: string().nullable().optional()
24116
24472
  });
24117
24473
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
24118
24474
  kind: "mutation",
@@ -25221,6 +25577,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
25221
25577
  kind: "mutation",
25222
25578
  auth: "admin"
25223
25579
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
25580
+ _enum([
25581
+ "queue-full",
25582
+ "camera-backoff",
25583
+ "sleeping",
25584
+ "camera-refused",
25585
+ "no-keyframe",
25586
+ "no-catalog-row",
25587
+ "unsupported",
25588
+ "unknown-device",
25589
+ "uid-missing"
25590
+ ]);
25591
+ _enum(["deferred", "final"]);
25224
25592
  var ClipSchema = object({
25225
25593
  /** Opaque, provider-namespaced id. The default provider encodes the time
25226
25594
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -25295,29 +25663,455 @@ var ClipSchema = object({
25295
25663
  holes: array(object({
25296
25664
  startMs: number(),
25297
25665
  endMs: number()
25298
- })).optional()
25666
+ })).optional(),
25667
+ /**
25668
+ * A URL that MAY produce this clip's still, asked only for rows actually on
25669
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
25670
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
25671
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
25672
+ * — a surface latches that refusal to the instant rather than retrying.
25673
+ *
25674
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
25675
+ */
25676
+ thumbnailMint: string().optional(),
25677
+ /**
25678
+ * Why no still will be produced for this clip RIGHT NOW — set when the
25679
+ * provider already knows, so the surface draws the glyph and the reason
25680
+ * instead of firing a mint that cannot succeed.
25681
+ *
25682
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
25683
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
25684
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
25685
+ * inside no file is the measured case).
25686
+ * `unsupported` — this source cannot mint stills at all.
25687
+ */
25688
+ thumbnailUnavailable: object({ reason: _enum([
25689
+ "sleeping",
25690
+ "camera-refused",
25691
+ "no-keyframe",
25692
+ "unsupported"
25693
+ ]) }).optional(),
25694
+ /**
25695
+ * When the catalog this row came from was last CONFIRMED against the device.
25696
+ * Absent means "this row was read live". A persisted catalog served while a
25697
+ * camera sleeps carries the age it really has — a cached list is never drawn
25698
+ * as current (D549 13).
25699
+ */
25700
+ catalogAsOf: number().optional(),
25701
+ /**
25702
+ * The camera's OWN type strings for this clip, all of them, unmapped
25703
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
25704
+ * because a firmware inventing a type must not vanish: the mapping into our
25705
+ * filter vocabulary is lossy on purpose and this is the lossless copy
25706
+ * (D549 20).
25707
+ */
25708
+ nativeTypes: array(string()).optional(),
25709
+ /**
25710
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
25711
+ * travels BESIDE a class, never instead of one, and the class filter ignores
25712
+ * it: "a person crossed a line" is still a person (D549 20).
25713
+ */
25714
+ behaviours: array(string()).optional(),
25715
+ /**
25716
+ * The same recording as two files — the sub twin (what the row and its
25717
+ * thumbnail are) and its main twin, matched at listing time so a quality
25718
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
25719
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
25720
+ */
25721
+ streams: object({
25722
+ sub: object({
25723
+ id: string(),
25724
+ bytes: number().optional()
25725
+ }).optional(),
25726
+ main: object({
25727
+ id: string(),
25728
+ bytes: number().optional()
25729
+ }).optional()
25730
+ }).optional(),
25731
+ /** The camera says it holds a sub-stream copy of this recording. */
25732
+ supportSub: boolean().optional(),
25733
+ /**
25734
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
25735
+ * IS a file is playable, and only a source that lists EVENTS can produce a
25736
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
25737
+ * fell inside no file). Such a row is shown, never dropped and never offered
25738
+ * as playable-then-failing (D549 19).
25739
+ */
25740
+ playable: boolean().optional(),
25741
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
25742
+ unplayableReason: string().optional(),
25743
+ /**
25744
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
25745
+ * is not its end.
25746
+ *
25747
+ * Absent — the common case — means the row is closed and its `endMs` is the
25748
+ * end of the recording. Present and `true` means the source told us the file
25749
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
25750
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
25751
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
25752
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
25753
+ * on growing. A surface that drew 29 s there was lying about a clip that
25754
+ * plays for minutes, on the one row the operator looks at first.
25755
+ *
25756
+ * `endMs` is deliberately still a number: it is the best bound anything has
25757
+ * for a mint window or a byte fetch, and every consumer already requires it.
25758
+ * This flag says what it is WORTH, and a duration is not drawn from it.
25759
+ */
25760
+ inProgress: boolean().optional()
25299
25761
  });
25300
25762
  var ClipPlaybackSchema = object({
25301
- /** HLS master URL through the hub data-plane (Range + token in path). */
25763
+ /**
25764
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
25765
+ * says what kind — an HLS master playlist for a recording-derived clip, a
25766
+ * progressive MP4 for a native file the vendor addon muxed and serves with
25767
+ * `Range`. The URL is NOT a credential: both planes are registered
25768
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
25769
+ * the session cookie, so nothing is appended to it (D549 22).
25770
+ */
25302
25771
  playbackUrl: string(),
25772
+ /**
25773
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
25774
+ * existing consumer already assumes. A player that branches on this is the
25775
+ * one change a native clip needs; a player that ignores it will hand an MP4
25776
+ * to hls.js and fail parsing it as a manifest.
25777
+ */
25778
+ format: _enum(["hls", "mp4"]).optional(),
25779
+ /**
25780
+ * Which twin was actually served. A clip that holds only one stream answers
25781
+ * with the one it has, and the surface SAYS so — a missing main twin is
25782
+ * never served silently as if it were the asked-for quality (D549 15).
25783
+ */
25784
+ served: CamProfileSchema.optional(),
25303
25785
  /** Optional LAN/remote alternates for the same clip. */
25304
25786
  playbackEndpoints: array(string()).optional(),
25305
25787
  token: string().optional()
25306
25788
  });
25789
+ /**
25790
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
25791
+ *
25792
+ * `ok` is the only state whose clip list may be read as complete. The other
25793
+ * three exist because an empty list from a sleeping camera reads as "this
25794
+ * camera has no recordings", which is the defect this whole line of work is
25795
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
25796
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
25797
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
25798
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
25799
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
25800
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
25801
+ * files on both streams and both filters, with 177 GB free). That last one is
25802
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
25803
+ * which days it asked for and got nothing — "no clips" would be a lie about a
25804
+ * card that is full of them.
25805
+ */
25806
+ /**
25807
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
25808
+ *
25809
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
25810
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
25811
+ * `force` flag, and nothing else… a background caller must never set it…
25812
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
25813
+ * prefetch never set it; a surface sets it only behind the same confirm the
25814
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
25815
+ * exactly as that gesture is.
25816
+ *
25817
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
25818
+ * (D549) — a read that opened a session and then checked would have woken the
25819
+ * camera to find out it was not allowed to.
25820
+ *
25821
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
25822
+ * time the camera is awake for its own reasons — is deliberately not a member:
25823
+ * it was proposed, it is free on the battery, and the operator declined it on
25824
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
25825
+ * D558 "Considered and not taken").
25826
+ */
25827
+ var ClipWakeSchema = _enum(["authorised"]);
25828
+ /**
25829
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
25830
+ *
25831
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
25832
+ * and `served` says which twin the caller actually got.
25833
+ */
25834
+ var ClipBytesSchema = object({
25835
+ base64: string(),
25836
+ contentType: string(),
25837
+ /** Suggested filename, extension included. */
25838
+ name: string(),
25839
+ bytes: number().int().nonnegative(),
25840
+ /**
25841
+ * Which twin was actually served — the same contract
25842
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
25843
+ * export record persists it: a row read a week later must say the same thing
25844
+ * the panel said at the moment of the tap. A missing main twin is never
25845
+ * served silently as if it were the asked-for quality (D549 15).
25846
+ */
25847
+ served: CamProfileSchema,
25848
+ /**
25849
+ * The DECODED duration of the delivered file, when the fetch measured one.
25850
+ *
25851
+ * A clip fetch verifies its own completion against the catalog row and
25852
+ * retries a materially short pass (D568); this is that measurement, carried
25853
+ * so a consumer can say the same thing rather than re-deriving it. Absent
25854
+ * when the producer did not measure — never zero, which would say the file
25855
+ * is empty.
25856
+ */
25857
+ durationMs: number().positive().optional()
25858
+ });
25859
+ /**
25860
+ * Where a clip's STREAM can be dialled (D597) — the answer to
25861
+ * {@link videoclipsCapability.methods.dialClipStream}.
25862
+ *
25863
+ * `url` is plain `http://` on the provider's own token-gated listener, one
25864
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
25865
+ * path: that plane takes only a user credential, which an addon does not
25866
+ * hold, and the framework offers no addon→addon byte transport — the one
25867
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
25868
+ * a consumer dials directly, and this is that precedent one addon over.
25869
+ */
25870
+ /**
25871
+ * The audio a clip stream can be asked for, and the audio it carries.
25872
+ *
25873
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
25874
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
25875
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
25876
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
25877
+ * negotiates — and `none` is a stream asked for, or served, without sound.
25878
+ */
25879
+ var ClipStreamAudioSchema = _enum([
25880
+ "opus",
25881
+ "pcmu",
25882
+ "none"
25883
+ ]);
25884
+ /**
25885
+ * Why a stream that was ASKED for sound is not carrying it (D600).
25886
+ *
25887
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
25888
+ * means the ask was honoured, or there was no ask.
25889
+ *
25890
+ * - `not-requested` — the caller asked for `none`.
25891
+ * - `unsupported-ask` — the provider cannot produce that codec.
25892
+ *
25893
+ * Whether the RECORDING has sound at all is not knowable here: the camera
25894
+ * declares no audio track before its first frame arrives. That answer is in
25895
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
25896
+ * there. A dial that says `opus` promises the codec, never the content.
25897
+ */
25898
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
25899
+ var ClipStreamDialSchema = object({
25900
+ url: string().min(1),
25901
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
25902
+ expiresAtMs: number(),
25903
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
25904
+ transport: literal("forward-only"),
25905
+ /**
25906
+ * Which twin the stream ACTUALLY carries — not what was asked for.
25907
+ *
25908
+ * The stream serves every profile (D599), and a recording whose main twin
25909
+ * does not exist is streamed as its sub copy, which this field reports as
25910
+ * `low`. That is the whole reason the dial no longer refuses `high`:
25911
+ * stating what was served is what makes accepting honest, where a silent
25912
+ * substitution would be the 33x lie D576 forbids. Stated rather than
25913
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
25914
+ */
25915
+ served: CamProfileSchema,
25916
+ /**
25917
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
25918
+ * sound, and the same reasoning (D599, D600).
25919
+ *
25920
+ * The broker asks for the codec its WebRTC session negotiated, and the
25921
+ * provider either muxes that codec or carries none. Stating it is what
25922
+ * lets the broker wire the matching sink before the first byte, and what
25923
+ * keeps a session from being handed a codec it cannot send — the audio
25924
+ * shape of the substitution `served` exists to prevent.
25925
+ *
25926
+ * It is a promise about the CODEC, not about the content: a recording with
25927
+ * no microphone track answers `opus` here and then arrives with no audio
25928
+ * track at all. That second answer is in the stream, and the broker reads
25929
+ * it from the `moov`.
25930
+ */
25931
+ servedAudio: ClipStreamAudioSchema,
25932
+ /** Why `servedAudio` is `none` although sound was asked for. */
25933
+ audioReason: ClipStreamAudioReasonSchema.optional()
25934
+ });
25935
+ var ClipSourceAvailabilitySchema = object({
25936
+ state: _enum([
25937
+ "ok",
25938
+ "sleeping",
25939
+ "unreachable",
25940
+ "no-storage",
25941
+ "index-empty"
25942
+ ]),
25943
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
25944
+ reason: string().optional(),
25945
+ /** When this source's catalog was last CONFIRMED. A cached list is never
25946
+ * drawn as current: the surface shows the age whenever it is older than the
25947
+ * refresh interval. */
25948
+ catalogAsOf: number().optional()
25949
+ });
25950
+ /**
25951
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
25952
+ * every clip id from it is prefixed with (`analytics`,
25953
+ * `native:reolink:onboard`, `hksv`, …).
25954
+ *
25955
+ * A provider lists the sources IT serves for that device, and answers for each
25956
+ * of them whether it can answer at all. A provider with nothing to offer on a
25957
+ * camera returns `[]` — it is not that camera's business.
25958
+ */
25959
+ var ClipSourceSchema = object({
25960
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
25961
+ * of every clip id it mints. `getClipPlayback` routes on it. */
25962
+ source: string(),
25963
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
25964
+ label: string(),
25965
+ /**
25966
+ * The addon that SERVES this row.
25967
+ *
25968
+ * A surface cannot otherwise resolve a source to who answers it, and the
25969
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
25970
+ * widget — is a second authority on provider identity living in the one
25971
+ * package with no business knowing it, wrong the day a third source appears
25972
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
25973
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
25974
+ * served a hub child two of them until the two views were measured to be one
25975
+ * store read twice (D555) and then read ONE way (D565).
25976
+ *
25977
+ * Optional for version skew only. The collection dispatcher stamps it from
25978
+ * the registry, so a row that travelled through the fan-out carries the
25979
+ * authoritative id whatever the provider filled in.
25980
+ */
25981
+ addonId: string().optional(),
25982
+ availability: ClipSourceAvailabilitySchema
25983
+ });
25307
25984
  DeviceType.Camera, method(object({
25308
25985
  deviceId: number(),
25309
25986
  since: number(),
25310
25987
  until: number(),
25311
- limit: number().int().positive().optional()
25988
+ limit: number().int().positive().optional(),
25989
+ /**
25990
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25991
+ * carries, never a source id and never a list. **Required** (D554 amended).
25992
+ *
25993
+ * A provider the device is not bound to is refused by name rather than
25994
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25995
+ *
25996
+ * It was optional, documented as "absent means the device's BOUND
25997
+ * provider, which is CamStack on every camera". No code implemented
25998
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25999
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
26000
+ * answered SIX rows, three from each source, merged newest-first:
26001
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
26002
+ * so absence bought the union this method exists not to be, and
26003
+ * `limit` meant `limit × sources`.
26004
+ *
26005
+ * There is nothing to restore the default to. `getBindings` answers a
26006
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
26007
+ * `setWrapperActive` — the singleton authority that could name one —
26008
+ * throws for a collection cap by design. Naming CamStack here instead
26009
+ * would privilege one addon by id inside a surface whose premise is
26010
+ * that sources are peers, and would be wrong on the first camera with
26011
+ * no recorder binding. So absence is REFUSED, in the schema, where the
26012
+ * generated types make it unomittable rather than merely discouraged.
26013
+ *
26014
+ * The default belongs to the SURFACE, which has the `listSources` rows
26015
+ * and can say which one it picked (D569 § 1–3; the viewer already
26016
+ * always sends this).
26017
+ *
26018
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
26019
+ * assumed the answer was a fan-out over everything a camera has. The
26020
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
26021
+ * solo"* — so the list is asked of one provider at a time and there is
26022
+ * nothing to filter out of it.
26023
+ */
26024
+ provider: string().min(1)
25312
26025
  }), array(ClipSchema).readonly(), {
25313
26026
  kind: "query",
25314
- auth: "admin"
26027
+ auth: "protected"
26028
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
26029
+ kind: "query",
26030
+ auth: "protected"
25315
26031
  }), method(object({
25316
26032
  deviceId: number(),
25317
- clipId: string()
26033
+ clipId: string(),
26034
+ /**
26035
+ * Which twin to serve, on the ONE quality scale the system already has
26036
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
26037
+ * twin; both ids are already on the row so this never re-searches the
26038
+ * camera. Absent means the provider's own default (the sub file, which
26039
+ * every source is measured to hold).
26040
+ *
26041
+ * `auto` is deliberately NOT accepted here: a stored file has no
26042
+ * broker session, so the adaptive tier cannot be resolved for it. The
26043
+ * viewer resolves `auto` to a profile the same way live does, before
26044
+ * it calls (D549 15).
26045
+ */
26046
+ profile: CamProfileSchema.optional()
25318
26047
  }), ClipPlaybackSchema, {
25319
26048
  kind: "query",
25320
- auth: "admin"
26049
+ auth: "protected"
26050
+ }), optionalMethod(object({
26051
+ deviceId: number(),
26052
+ clipId: string().min(1),
26053
+ /**
26054
+ * WHICH provider holds the bytes — the `addonId` a
26055
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
26056
+ * `listClips` states one method above and for a second one this method
26057
+ * learned the hard way.
26058
+ *
26059
+ * The caller is the stream broker, and the broker lives in
26060
+ * `addon-pipeline` — the same addon that registers the RECORDER's
26061
+ * videoclips provider. With no provider named, the in-process
26062
+ * resolution handed it its own sibling, which implements this method
26063
+ * solely to explain that an analytics visit has no file behind it. So
26064
+ * every onboard clip answered with that sentence, a flash of video and
26065
+ * nothing more, while the SAME call from outside the addon reached the
26066
+ * right provider and returned the bytes (measured on 3628,
26067
+ * 2026-09-21).
26068
+ */
26069
+ provider: string().min(1),
26070
+ /**
26071
+ * Which twin to fetch, on the mapping D549 15 already fixed:
26072
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
26073
+ * accepted here for the same reason it is not accepted by
26074
+ * `getClipPlayback` — a stored file has no broker session, so the
26075
+ * adaptive tier cannot be resolved for it.
26076
+ */
26077
+ profile: CamProfileSchema.optional(),
26078
+ /**
26079
+ * The CALLER's byte bound, so an over-size clip is refused before it is
26080
+ * read and encoded rather than after. Capped by
26081
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
26082
+ * that ceiling.
26083
+ */
26084
+ maxBytes: number().int().positive().optional(),
26085
+ /**
26086
+ * The operator's authorisation to wake a sleeping camera for this
26087
+ * read. Absent — the default — means a sleeping standalone battery
26088
+ * camera is REFUSED by name, before any session is opened.
26089
+ */
26090
+ wake: ClipWakeSchema.optional()
26091
+ }), ClipBytesSchema, {
26092
+ kind: "query",
26093
+ auth: "protected"
26094
+ }), optionalMethod(object({
26095
+ deviceId: number(),
26096
+ clipId: string().min(1),
26097
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
26098
+ provider: string().min(1),
26099
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
26100
+ profile: CamProfileSchema.optional(),
26101
+ /**
26102
+ * Which audio codec the caller's session negotiated, so the provider
26103
+ * can mux it (D600). The ask travels on the DIAL because the mux is
26104
+ * spawned once, before the first byte, and cannot gain a track later.
26105
+ *
26106
+ * ABSENT IS `none`, never a guess: a caller that does not state an
26107
+ * ask is served the video-only stream that shipped first, so a broker
26108
+ * older than this field keeps working unchanged (D382 — an unstated
26109
+ * ask is zero).
26110
+ */
26111
+ audio: ClipStreamAudioSchema.optional()
26112
+ }), ClipStreamDialSchema, {
26113
+ kind: "query",
26114
+ auth: "protected"
25321
26115
  });
25322
26116
  /**
25323
26117
  * Optional client-side hints sent at session creation to help the provider
@@ -25389,6 +26183,104 @@ var WebrtcStreamChoiceSchema = object({
25389
26183
  /** Bitrate in kbps (null for adaptive). */
25390
26184
  bitrateKbps: number().nullable()
25391
26185
  });
26186
+ /**
26187
+ * The last move the adaptive ladder made on this session, and the numbers it
26188
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
26189
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
26190
+ *
26191
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
26192
+ * no window to report, which is NOT a link with zero loss.
26193
+ */
26194
+ var WebrtcLadderDecisionSchema = object({
26195
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
26196
+ * stopped moving; `healthy` — a promotion. */
26197
+ cause: _enum([
26198
+ "loss",
26199
+ "stall",
26200
+ "healthy"
26201
+ ]),
26202
+ /** The rung the ladder moved TO. Null for a move that changed no rung
26203
+ * (an MTU reduction, a forced transcode-down at the bottom). */
26204
+ tier: CamProfileSchema.nullable(),
26205
+ /** Whether that move forced the egress transcode-down rung. */
26206
+ forcedTranscodeDown: boolean(),
26207
+ /** Loss ratio over the tracker's window at the moment of the decision. */
26208
+ lossRatio: number().nullable(),
26209
+ /** The client's reported downlink at that moment, in kbps. */
26210
+ clientKbps: number().nullable(),
26211
+ /** How long the stall detector had been seeing nothing arrive. */
26212
+ stalledForMs: number().nullable(),
26213
+ /** Whether the broker WAS sending while that was measured — the half that
26214
+ * separates "the camera stopped" from "the link stopped". */
26215
+ serverSending: boolean().nullable(),
26216
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
26217
+ * must not read like one from this second. */
26218
+ at: number()
26219
+ });
26220
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
26221
+ var WebrtcTierFailureSchema = object({
26222
+ tier: CamProfileSchema,
26223
+ failures: number().int().nonnegative()
26224
+ });
26225
+ /**
26226
+ * What the SERVER decided for one WebRTC session — the half the client cannot
26227
+ * measure. See `getSessionDebug`.
26228
+ */
26229
+ var WebrtcSessionDebugSchema = object({
26230
+ sessionId: string(),
26231
+ /** Whether the session still exists here. Everything below is null when
26232
+ * it does not — a dead session has no measurements, not zero ones. */
26233
+ alive: boolean(),
26234
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
26235
+ observedAt: number(),
26236
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
26237
+ brokerId: string().nullable(),
26238
+ camStreamId: string().nullable(),
26239
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
26240
+ * another stream); `native` = the camera's own encode. */
26241
+ sourceKind: _enum([
26242
+ "native",
26243
+ "derived",
26244
+ "unknown"
26245
+ ]),
26246
+ /** Whether the ladder is running at all (an `adaptive` target). */
26247
+ adaptive: boolean(),
26248
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
26249
+ * is bound to no profile slot (a manually-activated stream). */
26250
+ servedTier: CamProfileSchema.nullable(),
26251
+ /** Which node ingests the source. Null when unresolved. */
26252
+ ingestNodeId: string().nullable(),
26253
+ /** Whether that node is a remote one (the media crosses a node boundary
26254
+ * before it reaches this session). Null when unresolved. */
26255
+ ingestIsRemote: boolean().nullable(),
26256
+ /** Codec off the source, and the one negotiated with this client. */
26257
+ sourceCodec: string().nullable(),
26258
+ negotiatedCodec: string().nullable(),
26259
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
26260
+ * source bytes untouched. */
26261
+ egressLeg: _enum([
26262
+ "copy",
26263
+ "transcode",
26264
+ "unknown"
26265
+ ]),
26266
+ /** What the broker itself reports about the rung it is serving. */
26267
+ brokerStatus: string().nullable(),
26268
+ brokerCodec: string().nullable(),
26269
+ brokerBitrateKbps: number().nullable(),
26270
+ brokerInputFps: number().nullable(),
26271
+ brokerUptimeMs: number().nullable(),
26272
+ /** RTP packets this session has sent — the server's own "am I sending?". */
26273
+ rtpPacketsSent: number().nullable(),
26274
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
26275
+ * is suspended there, so a frozen picture in `recorded` mode means
26276
+ * something entirely different. */
26277
+ trackMode: _enum(["live", "recorded"]).nullable(),
26278
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
26279
+ tierFailures: array(WebrtcTierFailureSchema),
26280
+ /** Successful promotions in this viewer session (drives the progressive
26281
+ * healthy timer). Null when no ladder is running. */
26282
+ upgradeCount: number().nullable()
26283
+ });
25392
26284
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
25393
26285
  deviceId: number().int().nonnegative(),
25394
26286
  target: WebrtcStreamTargetSchema,
@@ -25553,7 +26445,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
25553
26445
  * the same answer as "it ended": either way nothing is holding it up.
25554
26446
  */
25555
26447
  alive: boolean()
25556
- }));
26448
+ })), method(object({
26449
+ deviceId: number().int().nonnegative(),
26450
+ sessionId: string()
26451
+ }), WebrtcSessionDebugSchema, { kind: "query" });
25557
26452
  object({
25558
26453
  /** All accessory children of the parent. */
25559
26454
  childDeviceIds: array(number()).readonly(),
@@ -27379,6 +28274,346 @@ var dayNightCapability = {
27379
28274
  volatileStateFields: ["lastFetchedAt"]
27380
28275
  };
27381
28276
  /**
28277
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
28278
+ * writes to the CAMERA's own card, on the camera's own schedule.
28279
+ *
28280
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
28281
+ * footage ledger, our storage locations, our retention. This one has a
28282
+ * different authority — the camera's firmware — and per D62 it stores
28283
+ * nothing of its own. Every value here is read from the camera and every
28284
+ * write goes back to the camera; there is no CamStack-side mirror that
28285
+ * could disagree with the device.
28286
+ *
28287
+ * ## One shape, two firmwares
28288
+ *
28289
+ * Measured 2026-09-22 against the live fleet:
28290
+ *
28291
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
28292
+ * | --- | --- | --- |
28293
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
28294
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
28295
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
28296
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
28297
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
28298
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
28299
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
28300
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
28301
+ *
28302
+ * The two schedule models look different and are the same thing in
28303
+ * different coordinates: both answer "for this trigger, during which
28304
+ * weekly windows does the camera record". {@link RecordWindow} is that
28305
+ * question in one shape — Hikvision's ranges map straight onto it,
28306
+ * Reolink's mask expands into hour-aligned windows.
28307
+ *
28308
+ * ## Union, not intersection
28309
+ *
28310
+ * **The same fields exist on every camera.** What differs per device is
28311
+ * which VALUES that device accepts, and that is what {@link
28312
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
28313
+ * per field plus the schedule's own limits. A control a camera cannot
28314
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
28315
+ * dead: disabled must not look like broken.
28316
+ *
28317
+ * ## Refusal by name
28318
+ *
28319
+ * A write a camera cannot honour is refused with a sentence the operator
28320
+ * can read — never accepted and dropped. Both providers refuse through
28321
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
28322
+ * one test, not two hand-written vendor opinions.
28323
+ *
28324
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
28325
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
28326
+ * injected from `status`) reports the live values, and a single
28327
+ * `setSettings` mutation applies a partial change. No hand-written
28328
+ * settings-contribution methods.
28329
+ */
28330
+ /**
28331
+ * What makes the camera start recording during a window.
28332
+ *
28333
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
28334
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
28335
+ * object-class triggers are Reolink-only today and the smart-event ones
28336
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
28337
+ * firmwares measured — a camera that cannot record on a trigger simply
28338
+ * does not list it in `options.schedule.triggers`, and a window naming
28339
+ * it is REFUSED, not dropped.
28340
+ */
28341
+ var RecordTriggerSchema = _enum([
28342
+ "continuous",
28343
+ "motion",
28344
+ "person",
28345
+ "vehicle",
28346
+ "animal",
28347
+ "lineCrossing",
28348
+ "intrusion",
28349
+ "loitering",
28350
+ "alarmInput"
28351
+ ]);
28352
+ /**
28353
+ * One weekly recording window: "on `day`, from `startMinute` to
28354
+ * `endMinute`, record on `trigger`".
28355
+ *
28356
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
28357
+ * both firmwares enumerate). Minutes are local camera time since
28358
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
28359
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
28360
+ * collapsing it to 0 would turn a whole-day window into an empty one.
28361
+ */
28362
+ var RecordWindowSchema = object({
28363
+ trigger: RecordTriggerSchema,
28364
+ day: number().int().min(0).max(6),
28365
+ startMinute: number().int().min(0).max(1439),
28366
+ endMinute: number().int().min(1).max(1440)
28367
+ });
28368
+ /** Status of one physical volume, as the camera itself describes it. */
28369
+ var OnboardStorageVolumeSchema = object({
28370
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
28371
+ id: string(),
28372
+ /** The camera's own name for it, when it gives one (`hddName`). */
28373
+ label: string().optional(),
28374
+ status: _enum([
28375
+ "ok",
28376
+ "unformatted",
28377
+ "error",
28378
+ "offline",
28379
+ "unknown"
28380
+ ]),
28381
+ /**
28382
+ * Total size in MB, or **null when the camera did not say**.
28383
+ *
28384
+ * Never 0 for an unreadable value: a measurement that failed is not a
28385
+ * measurement (D393), and a card whose size is unknown must not be
28386
+ * rendered as a card of size zero.
28387
+ */
28388
+ capacityMb: number().nullable(),
28389
+ /**
28390
+ * Free space in MB, or null when unknown.
28391
+ *
28392
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
28393
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
28394
+ * card converges on once it has wrapped. At loop steady state the
28395
+ * number is identical whether the camera recorded yesterday or stopped
28396
+ * a month ago.
28397
+ */
28398
+ freeMb: number().nullable(),
28399
+ /** True when the camera reports the volume writable (`property` RW). */
28400
+ writable: boolean().optional()
28401
+ });
28402
+ /**
28403
+ * What the camera is doing with its own storage, right now.
28404
+ *
28405
+ * Every scalar is nullable and **null means the camera did not answer**,
28406
+ * never a default. A form that seeds `0` from an unanswered read invites
28407
+ * the operator to save that 0 back onto the camera.
28408
+ */
28409
+ var RecordingOnboardStatusSchema = object({
28410
+ storage: discriminatedUnion("kind", [
28411
+ object({
28412
+ kind: literal("present"),
28413
+ volumes: array(OnboardStorageVolumeSchema)
28414
+ }),
28415
+ object({
28416
+ kind: literal("absent"),
28417
+ reason: string()
28418
+ }),
28419
+ object({
28420
+ kind: literal("unknown"),
28421
+ reason: string()
28422
+ })
28423
+ ]),
28424
+ tracks: array(object({
28425
+ id: string(),
28426
+ enabled: boolean(),
28427
+ isVideo: boolean(),
28428
+ /** From the camera's own track description. Null when it does not say. */
28429
+ codec: string().nullable(),
28430
+ resolution: string().nullable(),
28431
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
28432
+ overwriteWhenFull: boolean().nullable()
28433
+ })),
28434
+ /**
28435
+ * The track the write path targets — the enabled VIDEO one. Null when
28436
+ * no track could be identified, which is itself a refusal reason.
28437
+ */
28438
+ primaryTrackId: string().nullable(),
28439
+ /** Master "record to the card at all" switch. */
28440
+ enabled: boolean().nullable(),
28441
+ overwriteWhenFull: boolean().nullable(),
28442
+ preRecordSec: number().nullable(),
28443
+ postRecordSec: number().nullable(),
28444
+ /** Length of one recorded file, in minutes. */
28445
+ segmentMinutes: number().nullable(),
28446
+ /** The primary track's weekly windows, flattened. */
28447
+ windows: array(RecordWindowSchema),
28448
+ /**
28449
+ * How many windows the camera described that CamStack could NOT read —
28450
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
28451
+ * name.
28452
+ *
28453
+ * A dropped window is work the reader threw away, and a schedule that
28454
+ * silently shows fewer rows than the camera holds is how an operator
28455
+ * saves back a schedule shorter than the one they were looking at
28456
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
28457
+ * that replaces it would delete what was not shown — which is why a
28458
+ * provider reporting a non-zero count also reports the schedule as not
28459
+ * writable.
28460
+ */
28461
+ unreadableWindows: number(),
28462
+ /**
28463
+ * The camera is scheduled to record and has NO usable storage.
28464
+ *
28465
+ * A first-class fact because it is the fleet's most common silent
28466
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
28467
+ * to a card that is not there. Neither the schedule nor the storage
28468
+ * read says anything wrong on its own; only the pair does.
28469
+ */
28470
+ recordingToNowhere: boolean(),
28471
+ lastFetchedAt: number()
28472
+ });
28473
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
28474
+ var RangeSchema = object({
28475
+ min: number(),
28476
+ max: number(),
28477
+ step: number()
28478
+ });
28479
+ /**
28480
+ * The values a camera actually takes for a numeric field, when they are a SET
28481
+ * rather than a range.
28482
+ *
28483
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
28484
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
28485
+ *
28486
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
28487
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
28488
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
28489
+ *
28490
+ * Neither is expressible as a step: the first has a sentinel two billion away
28491
+ * from its neighbours, the second doubles and then jumps. A range that tried
28492
+ * would forbid values the camera takes AND permit values it silently replaces
28493
+ * with 5 — wrong in both directions at once.
28494
+ *
28495
+ * `sentinel` names the member that is not a duration, so a surface can render
28496
+ * "no limit" instead of `2147483647` seconds.
28497
+ */
28498
+ var AllowedValuesSchema = object({
28499
+ values: array(number()).min(1),
28500
+ sentinel: object({
28501
+ value: number(),
28502
+ meaning: _enum(["no-limit", "disabled"])
28503
+ }).optional()
28504
+ });
28505
+ /**
28506
+ * Per-field availability on ONE camera.
28507
+ *
28508
+ * The field exists on every camera — this says whether this one can be
28509
+ * read and whether it can be written, and `reason` says why not when
28510
+ * either is false. The UI renders the control DISABLED with the reason
28511
+ * rather than hiding it, so a limitation is legible instead of looking
28512
+ * like a missing feature.
28513
+ */
28514
+ var OnboardFieldSupportSchema = object({
28515
+ readable: boolean(),
28516
+ writable: boolean(),
28517
+ /** Required whenever `readable` or `writable` is false. */
28518
+ reason: string().optional()
28519
+ });
28520
+ /** What this camera's schedule model can express. */
28521
+ var OnboardScheduleSupportSchema = object({
28522
+ support: OnboardFieldSupportSchema,
28523
+ /**
28524
+ * The smallest time step the camera can express, in minutes.
28525
+ *
28526
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
28527
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
28528
+ * window whose edges are not a multiple of this is REFUSED rather than
28529
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
28530
+ * and nothing says so.
28531
+ */
28532
+ granularityMinutes: number(),
28533
+ /** Triggers this camera can record on. A window naming another is refused. */
28534
+ triggers: array(RecordTriggerSchema),
28535
+ /**
28536
+ * False when the camera stores ONE trigger per time range, so two
28537
+ * windows overlapping on the same day cannot carry different triggers.
28538
+ * True on Reolink, whose mask is per-trigger and independent.
28539
+ */
28540
+ supportsOverlappingTriggers: boolean()
28541
+ });
28542
+ var RecordingOnboardOptionsSchema = object({
28543
+ enabled: OnboardFieldSupportSchema,
28544
+ overwriteWhenFull: OnboardFieldSupportSchema,
28545
+ preRecordSec: OnboardFieldSupportSchema,
28546
+ preRecordSecRange: RangeSchema.optional(),
28547
+ /** Preferred over the range when the camera takes a SET, not a span. */
28548
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
28549
+ postRecordSec: OnboardFieldSupportSchema,
28550
+ postRecordSecRange: RangeSchema.optional(),
28551
+ /** Preferred over the range when the camera takes a SET, not a span. */
28552
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
28553
+ segmentMinutes: OnboardFieldSupportSchema,
28554
+ segmentMinutesRange: RangeSchema.optional(),
28555
+ /** Preferred over the range when the camera takes a SET, not a span. */
28556
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
28557
+ schedule: OnboardScheduleSupportSchema
28558
+ });
28559
+ /**
28560
+ * A partial change. Every field optional.
28561
+ *
28562
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
28563
+ * silently ignore a field it cannot support — it refuses, by name,
28564
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
28565
+ * is the failure D62 exists to prevent: the operator believes the camera
28566
+ * is recording the way the form says, and it is not.
28567
+ */
28568
+ var RecordingOnboardPatchSchema = object({
28569
+ enabled: boolean().optional(),
28570
+ overwriteWhenFull: boolean().optional(),
28571
+ preRecordSec: number().optional(),
28572
+ postRecordSec: number().optional(),
28573
+ segmentMinutes: number().optional(),
28574
+ /** The complete new window set for the primary track — not a delta. */
28575
+ windows: array(RecordWindowSchema).optional()
28576
+ });
28577
+ var recordingOnboardCapability = {
28578
+ name: "recording-onboard",
28579
+ scope: "device",
28580
+ deviceNative: true,
28581
+ mode: "singleton",
28582
+ deviceTypes: [DeviceType.Camera],
28583
+ deviceConfig: { ui: {
28584
+ kind: "derived-form",
28585
+ builderId: "recording-onboard",
28586
+ tab: "recording"
28587
+ } },
28588
+ methods: {
28589
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
28590
+ setSettings: method(object({
28591
+ deviceId: number(),
28592
+ settings: RecordingOnboardPatchSchema
28593
+ }), _void(), {
28594
+ kind: "mutation",
28595
+ auth: "admin"
28596
+ })
28597
+ },
28598
+ status: {
28599
+ schema: RecordingOnboardStatusSchema,
28600
+ kind: "poll"
28601
+ },
28602
+ runtimeState: RecordingOnboardStatusSchema,
28603
+ /**
28604
+ * Runtime-state durability: **restored** — operator-set camera-side
28605
+ * recording config; mutation-driven, and the storage half is the last
28606
+ * thing the camera said about its own card.
28607
+ *
28608
+ * See `RuntimeStateDurability`. Enforced by
28609
+ * `scripts/check-runtime-state-durability.ts`.
28610
+ */
28611
+ durability: "restored",
28612
+ /** Clock fields: written, but excluded from the compare that decides
28613
+ * whether persisting is worth a SQLite commit. */
28614
+ volatileStateFields: ["lastFetchedAt"]
28615
+ };
28616
+ /**
27382
28617
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
27383
28618
  * for every device, regardless of provider — the kernel needs a uniform
27384
28619
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -29952,16 +31187,37 @@ var GridProfileOfferSchema = object({
29952
31187
  ]),
29953
31188
  offered: boolean(),
29954
31189
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
29955
- missingSources: array(number().int().positive())
31190
+ missingSources: array(number().int().positive()),
31191
+ /**
31192
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
31193
+ * offered. DERIVED from the cells and the sources' own size at this profile —
31194
+ * it is reported because nothing else in the system would ever say what the
31195
+ * grid came out as, and because it is the number an operator would otherwise
31196
+ * expect to type.
31197
+ */
31198
+ canvas: string(),
31199
+ /**
31200
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
31201
+ *
31202
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
31203
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
31204
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
31205
+ * Default is `mid` + `low`.
31206
+ */
31207
+ published: boolean()
29956
31208
  });
29957
31209
  var GridLayoutViewSchema = object({
29958
31210
  /** The persisted grid row this camera was declared from. */
29959
31211
  instanceId: string(),
29960
31212
  deviceId: number().int().nonnegative(),
29961
31213
  name: string(),
29962
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
29963
- width: number().int(),
29964
- height: number().int(),
31214
+ /**
31215
+ * NO canvas size. A grid's resolution is not authored: each profile derives
31216
+ * its own from the cells and its sources' dimensions. The two numbers that
31217
+ * used to be here were a text field that silently decided both how much the
31218
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
31219
+ * came out as.
31220
+ */
29965
31221
  fps: number().int(),
29966
31222
  cells: array(GridLayoutCellSchema),
29967
31223
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -29970,9 +31226,13 @@ var GridLayoutViewSchema = object({
29970
31226
  var GridLayoutPatchSchema = object({
29971
31227
  deviceId: number().int().nonnegative(),
29972
31228
  name: string().min(1).max(160).optional(),
29973
- width: number().int().min(160).max(7680).optional(),
29974
- height: number().int().min(120).max(4320).optional(),
29975
31229
  fps: number().int().min(1).max(60).optional(),
31230
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
31231
+ publishedProfiles: array(_enum([
31232
+ "high",
31233
+ "mid",
31234
+ "low"
31235
+ ])).max(3).optional(),
29976
31236
  /**
29977
31237
  * The whole cell list at once. A per-cell patch would need an ordering the
29978
31238
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -32255,6 +33515,25 @@ var ptzAutotrackCapability = {
32255
33515
  scope: "device",
32256
33516
  deviceNative: true,
32257
33517
  mode: "singleton",
33518
+ /**
33519
+ * Two kinds of provider behind one name, the same shape `snapshot` and
33520
+ * `doorbell` use:
33521
+ *
33522
+ * - **Native**: the camera's own firmware autotrack, registered per device by
33523
+ * the driver when the probe says it exists (Reolink: yes; Hikvision:
33524
+ * model-dependent).
33525
+ * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
33526
+ * which follows a subject with `ptz.move` pulses driven by the detection
33527
+ * pipeline. `defaultActive: false` — the operator binds it per camera in
33528
+ * the bindings UI, exactly as `virtual-doorbell` is bound.
33529
+ *
33530
+ * The binding chooses WHICH implementation answers; `setEnabled` stays the
33531
+ * one switch for the function either way. A camera never has both driving its
33532
+ * head, which is the whole reason this is not a second cap with a second
33533
+ * toggle (D62, D520).
33534
+ */
33535
+ kind: "wrapper",
33536
+ defaultActive: false,
32258
33537
  deviceTypes: [DeviceType.Camera],
32259
33538
  deviceConfig: { ui: {
32260
33539
  kind: "widget",
@@ -32737,7 +34016,7 @@ method(object({
32737
34016
  auth: "admin"
32738
34017
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
32739
34018
  kind: "query",
32740
- auth: "admin"
34019
+ auth: "protected"
32741
34020
  }), method(object({
32742
34021
  deviceId: number(),
32743
34022
  profile: string(),
@@ -32967,13 +34246,101 @@ var ExportStateSchema = _enum([
32967
34246
  "expired",
32968
34247
  "deleted"
32969
34248
  ]);
32970
- /** One export job / history row. */
34249
+ /**
34250
+ * WHAT an export is — the authority, as opposed to the four top-level fields
34251
+ * the Library sorts and labels on (D558 § 2.2).
34252
+ *
34253
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
34254
+ * field is optional for the history rows written before it existed, and that
34255
+ * absence has exactly one interpreter.
34256
+ */
34257
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
34258
+ kind: literal("footage"),
34259
+ deviceId: number(),
34260
+ profiles: array(string()).min(1),
34261
+ fromMs: number(),
34262
+ toMs: number()
34263
+ }), object({
34264
+ kind: literal("clip"),
34265
+ deviceId: number(),
34266
+ provider: string().min(1),
34267
+ source: string().min(1),
34268
+ sourceLabel: string().min(1),
34269
+ clipId: string().min(1),
34270
+ /**
34271
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
34272
+ * already listing when the operator picked the segment.
34273
+ *
34274
+ * It is **not** a time range control and it never becomes one: the record's
34275
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
34276
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
34277
+ * `catalog-miss`, not a silently wider search. It exists because
34278
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
34279
+ * the catalog check D558 asks for is literally that call, and a call needs a
34280
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
34281
+ * wall-clock day, which is also the only width measured to be cheap (one
34282
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
34283
+ * of one child's events took 3.6–5.4 s).
34284
+ */
34285
+ catalogWindow: object({
34286
+ sinceMs: number(),
34287
+ untilMs: number()
34288
+ }),
34289
+ profile: _enum([
34290
+ "high",
34291
+ "mid",
34292
+ "low"
34293
+ ]),
34294
+ /**
34295
+ * The operator's authorisation to wake a sleeping camera for this export.
34296
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
34297
+ * because the gate that honours it is the clip provider's sleep gate — a
34298
+ * second enum here would be a second contract. Absent by default; never
34299
+ * settable by a scheduler or a retry.
34300
+ *
34301
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
34302
+ * an operator act that asks again; a queued job that outlived its wake fails
34303
+ * with a reason rather than waking on its turn; and this subject carries
34304
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
34305
+ */
34306
+ wake: ClipWakeSchema.optional()
34307
+ })]);
34308
+ /**
34309
+ * One export job / history row.
34310
+ *
34311
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
34312
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
34313
+ * Library sorts and labels on them (`library-items.ts` orders an export by
34314
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
34315
+ * that did not fill them would sort under the epoch and render a blank range.
34316
+ * Write to the subject and read from the projection and the two will disagree;
34317
+ * the projection is derived at creation and never edited afterwards.
34318
+ *
34319
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
34320
+ * who knows only the recording export will get wrong:
34321
+ *
34322
+ * | field | `kind:'footage'` | `kind:'clip'` |
34323
+ * | --- | --- | --- |
34324
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
34325
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
34326
+ *
34327
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
34328
+ * two authorities, D224's second copy).
34329
+ */
32971
34330
  var ExportRecordSchema = object({
32972
34331
  id: string(),
32973
34332
  deviceId: number(),
32974
34333
  profile: string(),
32975
34334
  fromMs: number(),
32976
34335
  toMs: number(),
34336
+ /**
34337
+ * What this export IS. Optional ONLY for the rows written before D558: the
34338
+ * store parses every row through this schema on every read, so a required
34339
+ * field would make the export AUDIT — which is the whole reason rows survive
34340
+ * file deletion — unreadable in one release. Absence means `footage`, and
34341
+ * {@link exportSubjectOf} is the one place that says so.
34342
+ */
34343
+ subject: ExportSubjectSchema.optional(),
32977
34344
  options: ExportOptionsSchema,
32978
34345
  state: ExportStateSchema,
32979
34346
  /** 0–100 while rendering; null otherwise. */
@@ -32990,6 +34357,18 @@ var ExportRecordSchema = object({
32990
34357
  /** Failure reason when state is 'failed'; null otherwise. */
32991
34358
  error: string().nullable()
32992
34359
  });
34360
+ _enum([
34361
+ "catalog-miss",
34362
+ "catalog-unreachable",
34363
+ "no-file-for-window",
34364
+ "clip-in-progress",
34365
+ "unsupported-option",
34366
+ "sleeping",
34367
+ "camera-refused",
34368
+ "fetch-failed",
34369
+ "too-large-to-transfer",
34370
+ "wake-expired"
34371
+ ]);
32993
34372
  /** Candidate download URLs (LAN first, then operator extra hosts). */
32994
34373
  var ExportDownloadSchema = object({
32995
34374
  url: string(),
@@ -33008,16 +34387,50 @@ var ExportBytesSchema = object({
33008
34387
  name: string(),
33009
34388
  bytes: number().int().nonnegative()
33010
34389
  });
34390
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
34391
+ function resolveExportProfiles(input) {
34392
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
34393
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
34394
+ return [];
34395
+ }
33011
34396
  method(object({
33012
34397
  deviceId: number(),
33013
34398
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
33014
34399
  profile: string().optional(),
33015
34400
  profiles: array(string()).min(1).optional(),
33016
- fromMs: number(),
33017
- toMs: number(),
34401
+ /** Footage only — a clip's boundaries are the camera's. */
34402
+ fromMs: number().optional(),
34403
+ toMs: number().optional(),
34404
+ /** What to export. Absent means the legacy flat footage request. */
34405
+ subject: ExportSubjectSchema.optional(),
33018
34406
  options: ExportOptionsSchema
33019
34407
  }).superRefine((v, ctx) => {
33020
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
34408
+ if (v.subject?.kind === "clip") {
34409
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
34410
+ code: ZodIssueCode.custom,
34411
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
34412
+ path: ["subject", "deviceId"]
34413
+ });
34414
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
34415
+ code: ZodIssueCode.custom,
34416
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
34417
+ path: ["fromMs"]
34418
+ });
34419
+ return;
34420
+ }
34421
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
34422
+ code: ZodIssueCode.custom,
34423
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
34424
+ path: ["subject", "deviceId"]
34425
+ });
34426
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
34427
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
34428
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
34429
+ code: ZodIssueCode.custom,
34430
+ message: "a footage export needs fromMs and toMs",
34431
+ path: ["fromMs"]
34432
+ });
34433
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
33021
34434
  code: ZodIssueCode.custom,
33022
34435
  message: "pass profiles[] (min 1) or legacy profile",
33023
34436
  path: ["profiles"]
@@ -35829,6 +37242,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
35829
37242
  pressureSensor: pressureSensorCapability,
35830
37243
  privacyMask: privacyMaskCapability,
35831
37244
  ptzAutotrack: ptzAutotrackCapability,
37245
+ recordingOnboard: recordingOnboardCapability,
35832
37246
  recordingSignal: recordingSignalCapability,
35833
37247
  sceneMonitor: sceneMonitorCapability,
35834
37248
  scriptRunner: scriptRunnerCapability,
@@ -36990,7 +38404,1336 @@ var BaseDeviceProvider = class extends BaseAddon {
36990
38404
  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;
36991
38405
  new Set(Object.values(DeviceType));
36992
38406
  DeviceFeature.BatteryOperated;
38407
+ /**
38408
+ * The vocabulary, in the order a picker offers it.
38409
+ *
38410
+ * Adding a key here is not enough to make it pickable: both glyph tables must
38411
+ * gain it too, and their tests fail until they do. That is the intended
38412
+ * friction — it is the only thing that keeps the two surfaces honest.
38413
+ */
38414
+ var DISPLAY_ICON_VOCABULARY = [
38415
+ {
38416
+ key: "camera",
38417
+ group: "device",
38418
+ synonyms: [
38419
+ "telecamera",
38420
+ "videocamera",
38421
+ "camera",
38422
+ "cam"
38423
+ ]
38424
+ },
38425
+ {
38426
+ key: "network",
38427
+ group: "device",
38428
+ synonyms: [
38429
+ "rete",
38430
+ "network",
38431
+ "lan"
38432
+ ]
38433
+ },
38434
+ {
38435
+ key: "toggle-left",
38436
+ group: "device",
38437
+ synonyms: [
38438
+ "interruttore",
38439
+ "switch",
38440
+ "toggle"
38441
+ ]
38442
+ },
38443
+ {
38444
+ key: "toggle-right",
38445
+ group: "device",
38446
+ synonyms: [
38447
+ "interruttore",
38448
+ "switch",
38449
+ "toggle",
38450
+ "acceso"
38451
+ ]
38452
+ },
38453
+ {
38454
+ key: "circle-dot",
38455
+ group: "device",
38456
+ synonyms: [
38457
+ "pulsante",
38458
+ "bottone",
38459
+ "button",
38460
+ "dot"
38461
+ ]
38462
+ },
38463
+ {
38464
+ key: "refresh-cw",
38465
+ group: "device",
38466
+ synonyms: [
38467
+ "aggiorna",
38468
+ "ricarica",
38469
+ "refresh",
38470
+ "reload",
38471
+ "ciclo"
38472
+ ]
38473
+ },
38474
+ {
38475
+ key: "box",
38476
+ group: "device",
38477
+ synonyms: [
38478
+ "scatola",
38479
+ "box",
38480
+ "contenitore"
38481
+ ]
38482
+ },
38483
+ {
38484
+ key: "scroll-text",
38485
+ group: "device",
38486
+ synonyms: [
38487
+ "registro",
38488
+ "log",
38489
+ "testo"
38490
+ ]
38491
+ },
38492
+ {
38493
+ key: "workflow",
38494
+ group: "device",
38495
+ synonyms: [
38496
+ "automazione",
38497
+ "flusso",
38498
+ "workflow",
38499
+ "scenario"
38500
+ ]
38501
+ },
38502
+ {
38503
+ key: "lock",
38504
+ group: "device",
38505
+ synonyms: [
38506
+ "serratura",
38507
+ "chiave",
38508
+ "lucchetto",
38509
+ "chiuso",
38510
+ "locked"
38511
+ ]
38512
+ },
38513
+ {
38514
+ key: "lock-open",
38515
+ group: "device",
38516
+ synonyms: [
38517
+ "serratura",
38518
+ "aperta",
38519
+ "sbloccato",
38520
+ "unlocked"
38521
+ ]
38522
+ },
38523
+ {
38524
+ key: "blinds",
38525
+ group: "device",
38526
+ synonyms: [
38527
+ "tapparella",
38528
+ "persiana",
38529
+ "serranda",
38530
+ "avvolgibile",
38531
+ "shutter",
38532
+ "blind",
38533
+ "veneziana"
38534
+ ]
38535
+ },
38536
+ {
38537
+ key: "air-vent",
38538
+ group: "device",
38539
+ synonyms: [
38540
+ "bocchetta",
38541
+ "aria",
38542
+ "ventilazione",
38543
+ "condizionatore",
38544
+ "vent"
38545
+ ]
38546
+ },
38547
+ {
38548
+ key: "monitor-play",
38549
+ group: "device",
38550
+ synonyms: [
38551
+ "schermo",
38552
+ "riproduzione",
38553
+ "player",
38554
+ "display"
38555
+ ]
38556
+ },
38557
+ {
38558
+ key: "sliders-horizontal",
38559
+ group: "device",
38560
+ synonyms: [
38561
+ "regolazione",
38562
+ "cursori",
38563
+ "impostazioni",
38564
+ "slider"
38565
+ ]
38566
+ },
38567
+ {
38568
+ key: "map-pin",
38569
+ group: "device",
38570
+ synonyms: [
38571
+ "posizione",
38572
+ "luogo",
38573
+ "mappa",
38574
+ "pin"
38575
+ ]
38576
+ },
38577
+ {
38578
+ key: "bot",
38579
+ group: "device",
38580
+ synonyms: [
38581
+ "robot",
38582
+ "aspirapolvere",
38583
+ "bot"
38584
+ ]
38585
+ },
38586
+ {
38587
+ key: "tractor",
38588
+ group: "device",
38589
+ synonyms: [
38590
+ "trattore",
38591
+ "tosaerba",
38592
+ "giardino"
38593
+ ]
38594
+ },
38595
+ {
38596
+ key: "image",
38597
+ group: "device",
38598
+ synonyms: [
38599
+ "immagine",
38600
+ "foto",
38601
+ "snapshot"
38602
+ ]
38603
+ },
38604
+ {
38605
+ key: "bone",
38606
+ group: "device",
38607
+ synonyms: ["osso", "cane"]
38608
+ },
38609
+ {
38610
+ key: "door-closed",
38611
+ group: "device",
38612
+ synonyms: [
38613
+ "porta",
38614
+ "chiusa",
38615
+ "contatto"
38616
+ ]
38617
+ },
38618
+ {
38619
+ key: "door-open",
38620
+ group: "device",
38621
+ synonyms: [
38622
+ "porta",
38623
+ "aperta",
38624
+ "contatto",
38625
+ "ingresso",
38626
+ "uscita"
38627
+ ]
38628
+ },
38629
+ {
38630
+ key: "crosshair",
38631
+ group: "device",
38632
+ synonyms: [
38633
+ "mirino",
38634
+ "puntamento",
38635
+ "centro"
38636
+ ]
38637
+ },
38638
+ {
38639
+ key: "eye",
38640
+ group: "device",
38641
+ synonyms: [
38642
+ "occhio",
38643
+ "visibile",
38644
+ "visto"
38645
+ ]
38646
+ },
38647
+ {
38648
+ key: "eye-off",
38649
+ group: "device",
38650
+ synonyms: [
38651
+ "privacy",
38652
+ "nascosto",
38653
+ "oscurato",
38654
+ "mascherato",
38655
+ "invisibile"
38656
+ ]
38657
+ },
38658
+ {
38659
+ key: "smartphone",
38660
+ group: "device",
38661
+ synonyms: [
38662
+ "telefono",
38663
+ "cellulare",
38664
+ "smartphone"
38665
+ ]
38666
+ },
38667
+ {
38668
+ key: "phone",
38669
+ group: "device",
38670
+ synonyms: ["telefono", "chiamata"]
38671
+ },
38672
+ {
38673
+ key: "tablet",
38674
+ group: "device",
38675
+ synonyms: ["tablet", "ipad"]
38676
+ },
38677
+ {
38678
+ key: "laptop",
38679
+ group: "device",
38680
+ synonyms: [
38681
+ "portatile",
38682
+ "laptop",
38683
+ "notebook"
38684
+ ]
38685
+ },
38686
+ {
38687
+ key: "monitor",
38688
+ group: "device",
38689
+ synonyms: ["monitor", "schermo"]
38690
+ },
38691
+ {
38692
+ key: "tv",
38693
+ group: "device",
38694
+ synonyms: ["televisore", "tv"]
38695
+ },
38696
+ {
38697
+ key: "speaker",
38698
+ group: "device",
38699
+ synonyms: [
38700
+ "altoparlante",
38701
+ "cassa",
38702
+ "speaker"
38703
+ ]
38704
+ },
38705
+ {
38706
+ key: "printer",
38707
+ group: "device",
38708
+ synonyms: ["stampante"]
38709
+ },
38710
+ {
38711
+ key: "projector",
38712
+ group: "device",
38713
+ synonyms: ["proiettore"]
38714
+ },
38715
+ {
38716
+ key: "gamepad-2",
38717
+ group: "device",
38718
+ synonyms: [
38719
+ "console",
38720
+ "gioco",
38721
+ "joystick"
38722
+ ]
38723
+ },
38724
+ {
38725
+ key: "radio",
38726
+ group: "device",
38727
+ synonyms: ["radio"]
38728
+ },
38729
+ {
38730
+ key: "router",
38731
+ group: "device",
38732
+ synonyms: ["router", "modem"]
38733
+ },
38734
+ {
38735
+ key: "bluetooth",
38736
+ group: "device",
38737
+ synonyms: ["bluetooth"]
38738
+ },
38739
+ {
38740
+ key: "cpu",
38741
+ group: "device",
38742
+ synonyms: [
38743
+ "processore",
38744
+ "cpu",
38745
+ "chip",
38746
+ "dispositivo"
38747
+ ]
38748
+ },
38749
+ {
38750
+ key: "hard-drive",
38751
+ group: "device",
38752
+ synonyms: [
38753
+ "disco",
38754
+ "hdd",
38755
+ "archiviazione",
38756
+ "storage"
38757
+ ]
38758
+ },
38759
+ {
38760
+ key: "server",
38761
+ group: "device",
38762
+ synonyms: ["server", "nas"]
38763
+ },
38764
+ {
38765
+ key: "mic",
38766
+ group: "device",
38767
+ synonyms: ["microfono", "audio"]
38768
+ },
38769
+ {
38770
+ key: "mic-off",
38771
+ group: "device",
38772
+ synonyms: [
38773
+ "microfono",
38774
+ "spento",
38775
+ "muto"
38776
+ ]
38777
+ },
38778
+ {
38779
+ key: "video",
38780
+ group: "device",
38781
+ synonyms: ["video"]
38782
+ },
38783
+ {
38784
+ key: "video-off",
38785
+ group: "device",
38786
+ synonyms: ["video", "spento"]
38787
+ },
38788
+ {
38789
+ key: "music",
38790
+ group: "device",
38791
+ synonyms: ["musica"]
38792
+ },
38793
+ {
38794
+ key: "plug",
38795
+ group: "device",
38796
+ synonyms: [
38797
+ "presa",
38798
+ "spina",
38799
+ "plug"
38800
+ ]
38801
+ },
38802
+ {
38803
+ key: "plug-zap",
38804
+ group: "device",
38805
+ synonyms: ["presa", "alimentata"]
38806
+ },
38807
+ {
38808
+ key: "power",
38809
+ group: "device",
38810
+ synonyms: [
38811
+ "accensione",
38812
+ "alimentazione",
38813
+ "power"
38814
+ ]
38815
+ },
38816
+ {
38817
+ key: "power-off",
38818
+ group: "device",
38819
+ synonyms: ["spegnimento", "spento"]
38820
+ },
38821
+ {
38822
+ key: "cable",
38823
+ group: "device",
38824
+ synonyms: ["cavo"]
38825
+ },
38826
+ {
38827
+ key: "battery",
38828
+ group: "device",
38829
+ synonyms: ["batteria"]
38830
+ },
38831
+ {
38832
+ key: "battery-charging",
38833
+ group: "device",
38834
+ synonyms: [
38835
+ "batteria",
38836
+ "carica",
38837
+ "ricarica"
38838
+ ]
38839
+ },
38840
+ {
38841
+ key: "battery-full",
38842
+ group: "device",
38843
+ synonyms: [
38844
+ "batteria",
38845
+ "carica",
38846
+ "piena"
38847
+ ]
38848
+ },
38849
+ {
38850
+ key: "battery-low",
38851
+ group: "device",
38852
+ synonyms: ["batteria", "scarica"]
38853
+ },
38854
+ {
38855
+ key: "key",
38856
+ group: "device",
38857
+ synonyms: ["chiave"]
38858
+ },
38859
+ {
38860
+ key: "key-round",
38861
+ group: "device",
38862
+ synonyms: ["chiave", "tonda"]
38863
+ },
38864
+ {
38865
+ key: "mailbox",
38866
+ group: "device",
38867
+ synonyms: [
38868
+ "cassetta",
38869
+ "postale",
38870
+ "posta"
38871
+ ]
38872
+ },
38873
+ {
38874
+ key: "package",
38875
+ group: "device",
38876
+ synonyms: ["pacco", "consegna"]
38877
+ },
38878
+ {
38879
+ key: "shield",
38880
+ group: "device",
38881
+ synonyms: [
38882
+ "scudo",
38883
+ "protezione",
38884
+ "sicurezza",
38885
+ "allarme"
38886
+ ]
38887
+ },
38888
+ {
38889
+ key: "shield-check",
38890
+ group: "device",
38891
+ synonyms: [
38892
+ "protetto",
38893
+ "sicuro",
38894
+ "armato"
38895
+ ]
38896
+ },
38897
+ {
38898
+ key: "shield-off",
38899
+ group: "device",
38900
+ synonyms: [
38901
+ "protezione",
38902
+ "disattivata",
38903
+ "disarmato"
38904
+ ]
38905
+ },
38906
+ {
38907
+ key: "briefcase",
38908
+ group: "device",
38909
+ synonyms: [
38910
+ "ufficio",
38911
+ "lavoro",
38912
+ "valigetta",
38913
+ "studio"
38914
+ ]
38915
+ },
38916
+ {
38917
+ key: "lightbulb",
38918
+ group: "lighting",
38919
+ synonyms: [
38920
+ "luce",
38921
+ "lampadina",
38922
+ "lampada",
38923
+ "light"
38924
+ ]
38925
+ },
38926
+ {
38927
+ key: "lamp",
38928
+ group: "lighting",
38929
+ synonyms: ["lampada", "abatjour"]
38930
+ },
38931
+ {
38932
+ key: "lamp-ceiling",
38933
+ group: "lighting",
38934
+ synonyms: [
38935
+ "plafoniera",
38936
+ "lampadario",
38937
+ "luce",
38938
+ "soffitto"
38939
+ ]
38940
+ },
38941
+ {
38942
+ key: "lamp-desk",
38943
+ group: "lighting",
38944
+ synonyms: [
38945
+ "lampada",
38946
+ "scrivania",
38947
+ "abatjour"
38948
+ ]
38949
+ },
38950
+ {
38951
+ key: "lamp-floor",
38952
+ group: "lighting",
38953
+ synonyms: [
38954
+ "piantana",
38955
+ "lampada",
38956
+ "terra"
38957
+ ]
38958
+ },
38959
+ {
38960
+ key: "flashlight",
38961
+ group: "lighting",
38962
+ synonyms: [
38963
+ "faretto",
38964
+ "torcia",
38965
+ "spot",
38966
+ "faro"
38967
+ ]
38968
+ },
38969
+ {
38970
+ key: "heater",
38971
+ group: "lighting",
38972
+ synonyms: [
38973
+ "riscaldamento",
38974
+ "termosifone",
38975
+ "stufa",
38976
+ "calore"
38977
+ ]
38978
+ },
38979
+ {
38980
+ key: "snowflake",
38981
+ group: "lighting",
38982
+ synonyms: [
38983
+ "freddo",
38984
+ "raffrescamento",
38985
+ "aria",
38986
+ "condizionata",
38987
+ "neve"
38988
+ ]
38989
+ },
38990
+ {
38991
+ key: "thermometer-sun",
38992
+ group: "lighting",
38993
+ synonyms: [
38994
+ "caldo",
38995
+ "temperatura",
38996
+ "alta"
38997
+ ]
38998
+ },
38999
+ {
39000
+ key: "thermometer-snowflake",
39001
+ group: "lighting",
39002
+ synonyms: [
39003
+ "freddo",
39004
+ "temperatura",
39005
+ "bassa"
39006
+ ]
39007
+ },
39008
+ {
39009
+ key: "refrigerator",
39010
+ group: "lighting",
39011
+ synonyms: ["frigorifero", "frigo"]
39012
+ },
39013
+ {
39014
+ key: "microwave",
39015
+ group: "lighting",
39016
+ synonyms: ["microonde", "forno"]
39017
+ },
39018
+ {
39019
+ key: "washing-machine",
39020
+ group: "lighting",
39021
+ synonyms: [
39022
+ "lavatrice",
39023
+ "lavastoviglie",
39024
+ "asciugatrice"
39025
+ ]
39026
+ },
39027
+ {
39028
+ key: "cooking-pot",
39029
+ group: "lighting",
39030
+ synonyms: [
39031
+ "pentola",
39032
+ "cucina",
39033
+ "fornello"
39034
+ ]
39035
+ },
39036
+ {
39037
+ key: "utensils",
39038
+ group: "lighting",
39039
+ synonyms: [
39040
+ "cucina",
39041
+ "posate",
39042
+ "sala",
39043
+ "pranzo"
39044
+ ]
39045
+ },
39046
+ {
39047
+ key: "thermometer",
39048
+ group: "sensing",
39049
+ synonyms: [
39050
+ "temperatura",
39051
+ "termometro",
39052
+ "termostato"
39053
+ ]
39054
+ },
39055
+ {
39056
+ key: "droplets",
39057
+ group: "sensing",
39058
+ synonyms: [
39059
+ "umidita",
39060
+ "acqua",
39061
+ "allagamento",
39062
+ "perdita"
39063
+ ]
39064
+ },
39065
+ {
39066
+ key: "droplet",
39067
+ group: "sensing",
39068
+ synonyms: [
39069
+ "goccia",
39070
+ "acqua",
39071
+ "perdita",
39072
+ "allagamento"
39073
+ ]
39074
+ },
39075
+ {
39076
+ key: "gauge",
39077
+ group: "sensing",
39078
+ synonyms: [
39079
+ "misura",
39080
+ "livello",
39081
+ "pressione",
39082
+ "indicatore"
39083
+ ]
39084
+ },
39085
+ {
39086
+ key: "gauge2",
39087
+ group: "sensing",
39088
+ synonyms: [
39089
+ "misura",
39090
+ "livello",
39091
+ "pressione",
39092
+ "indicatore"
39093
+ ],
39094
+ aliasOf: "gauge"
39095
+ },
39096
+ {
39097
+ key: "sun",
39098
+ group: "sensing",
39099
+ synonyms: [
39100
+ "sole",
39101
+ "luminosita",
39102
+ "giorno",
39103
+ "esterno"
39104
+ ]
39105
+ },
39106
+ {
39107
+ key: "sun-medium",
39108
+ group: "sensing",
39109
+ synonyms: ["sole", "luminosita"]
39110
+ },
39111
+ {
39112
+ key: "sunrise",
39113
+ group: "sensing",
39114
+ synonyms: ["alba"]
39115
+ },
39116
+ {
39117
+ key: "moon",
39118
+ group: "sensing",
39119
+ synonyms: [
39120
+ "notte",
39121
+ "luna",
39122
+ "notturna",
39123
+ "nightvision"
39124
+ ]
39125
+ },
39126
+ {
39127
+ key: "wind",
39128
+ group: "sensing",
39129
+ synonyms: [
39130
+ "vento",
39131
+ "aria",
39132
+ "gas",
39133
+ "monossido",
39134
+ "co"
39135
+ ]
39136
+ },
39137
+ {
39138
+ key: "cloud",
39139
+ group: "sensing",
39140
+ synonyms: ["nuvola", "nuvoloso"]
39141
+ },
39142
+ {
39143
+ key: "cloud-rain",
39144
+ group: "sensing",
39145
+ synonyms: ["pioggia"]
39146
+ },
39147
+ {
39148
+ key: "cloud-sun",
39149
+ group: "sensing",
39150
+ synonyms: [
39151
+ "variabile",
39152
+ "sole",
39153
+ "nuvole"
39154
+ ]
39155
+ },
39156
+ {
39157
+ key: "waves",
39158
+ group: "sensing",
39159
+ synonyms: [
39160
+ "onde",
39161
+ "acqua",
39162
+ "vibrazione"
39163
+ ]
39164
+ },
39165
+ {
39166
+ key: "sprout",
39167
+ group: "sensing",
39168
+ synonyms: [
39169
+ "irrigazione",
39170
+ "pianta",
39171
+ "germoglio",
39172
+ "orto"
39173
+ ]
39174
+ },
39175
+ {
39176
+ key: "leaf",
39177
+ group: "sensing",
39178
+ synonyms: [
39179
+ "foglia",
39180
+ "pianta",
39181
+ "verde"
39182
+ ]
39183
+ },
39184
+ {
39185
+ key: "flower",
39186
+ group: "sensing",
39187
+ synonyms: ["fiore", "pianta"]
39188
+ },
39189
+ {
39190
+ key: "zap",
39191
+ group: "sensing",
39192
+ synonyms: [
39193
+ "energia",
39194
+ "corrente",
39195
+ "consumo",
39196
+ "watt",
39197
+ "elettricita"
39198
+ ]
39199
+ },
39200
+ {
39201
+ key: "flame",
39202
+ group: "sensing",
39203
+ synonyms: [
39204
+ "fiamma",
39205
+ "gas",
39206
+ "fuoco",
39207
+ "incendio"
39208
+ ]
39209
+ },
39210
+ {
39211
+ key: "bell",
39212
+ group: "sensing",
39213
+ synonyms: [
39214
+ "campanello",
39215
+ "suoneria",
39216
+ "notifica",
39217
+ "bell",
39218
+ "chime"
39219
+ ]
39220
+ },
39221
+ {
39222
+ key: "bell-ring",
39223
+ group: "sensing",
39224
+ synonyms: [
39225
+ "campanello",
39226
+ "suona",
39227
+ "doorbell",
39228
+ "notifica"
39229
+ ]
39230
+ },
39231
+ {
39232
+ key: "bell-off",
39233
+ group: "sensing",
39234
+ synonyms: [
39235
+ "silenzioso",
39236
+ "notifiche",
39237
+ "disattivate",
39238
+ "muto"
39239
+ ]
39240
+ },
39241
+ {
39242
+ key: "siren",
39243
+ group: "sensing",
39244
+ synonyms: ["sirena", "allarme"]
39245
+ },
39246
+ {
39247
+ key: "radar",
39248
+ group: "sensing",
39249
+ synonyms: [
39250
+ "movimento",
39251
+ "presenza",
39252
+ "pir",
39253
+ "rilevamento",
39254
+ "radar"
39255
+ ]
39256
+ },
39257
+ {
39258
+ key: "activity",
39259
+ group: "sensing",
39260
+ synonyms: [
39261
+ "attivita",
39262
+ "sensore",
39263
+ "generico"
39264
+ ]
39265
+ },
39266
+ {
39267
+ key: "hash",
39268
+ group: "sensing",
39269
+ synonyms: [
39270
+ "numero",
39271
+ "contatore",
39272
+ "valore"
39273
+ ]
39274
+ },
39275
+ {
39276
+ key: "clock",
39277
+ group: "sensing",
39278
+ synonyms: [
39279
+ "orologio",
39280
+ "ora",
39281
+ "tempo"
39282
+ ]
39283
+ },
39284
+ {
39285
+ key: "timer",
39286
+ group: "sensing",
39287
+ synonyms: [
39288
+ "timer",
39289
+ "conto",
39290
+ "rovescia"
39291
+ ]
39292
+ },
39293
+ {
39294
+ key: "calendar",
39295
+ group: "sensing",
39296
+ synonyms: [
39297
+ "calendario",
39298
+ "data",
39299
+ "pianificazione"
39300
+ ]
39301
+ },
39302
+ {
39303
+ key: "pipette",
39304
+ group: "sensing",
39305
+ synonyms: [
39306
+ "qualita",
39307
+ "aria",
39308
+ "misura",
39309
+ "campione"
39310
+ ]
39311
+ },
39312
+ {
39313
+ key: "fan",
39314
+ group: "sensing",
39315
+ synonyms: [
39316
+ "ventilatore",
39317
+ "ventola",
39318
+ "aria"
39319
+ ]
39320
+ },
39321
+ {
39322
+ key: "bath",
39323
+ group: "sensing",
39324
+ synonyms: [
39325
+ "bagno",
39326
+ "vasca",
39327
+ "doccia"
39328
+ ]
39329
+ },
39330
+ {
39331
+ key: "footprints",
39332
+ group: "sensing",
39333
+ synonyms: [
39334
+ "movimento",
39335
+ "passi",
39336
+ "presenza",
39337
+ "motion"
39338
+ ]
39339
+ },
39340
+ {
39341
+ key: "alarm-smoke",
39342
+ group: "sensing",
39343
+ synonyms: [
39344
+ "fumo",
39345
+ "antincendio",
39346
+ "rilevatore"
39347
+ ]
39348
+ },
39349
+ {
39350
+ key: "shield-alert",
39351
+ group: "sensing",
39352
+ synonyms: [
39353
+ "manomissione",
39354
+ "allarme",
39355
+ "tamper",
39356
+ "avviso"
39357
+ ]
39358
+ },
39359
+ {
39360
+ key: "vibrate",
39361
+ group: "sensing",
39362
+ synonyms: [
39363
+ "vibrazione",
39364
+ "urto",
39365
+ "tamper"
39366
+ ]
39367
+ },
39368
+ {
39369
+ key: "wifi",
39370
+ group: "sensing",
39371
+ synonyms: [
39372
+ "wifi",
39373
+ "rete",
39374
+ "segnale"
39375
+ ]
39376
+ },
39377
+ {
39378
+ key: "wifi-off",
39379
+ group: "sensing",
39380
+ synonyms: [
39381
+ "wifi",
39382
+ "assente",
39383
+ "offline",
39384
+ "disconnesso"
39385
+ ]
39386
+ },
39387
+ {
39388
+ key: "volume-2",
39389
+ group: "sensing",
39390
+ synonyms: [
39391
+ "volume",
39392
+ "audio",
39393
+ "altoparlante",
39394
+ "suono"
39395
+ ]
39396
+ },
39397
+ {
39398
+ key: "volume-x",
39399
+ group: "sensing",
39400
+ synonyms: [
39401
+ "muto",
39402
+ "silenziato",
39403
+ "volume",
39404
+ "spento"
39405
+ ]
39406
+ },
39407
+ {
39408
+ key: "heart",
39409
+ group: "sensing",
39410
+ synonyms: [
39411
+ "cuore",
39412
+ "preferito",
39413
+ "salute"
39414
+ ]
39415
+ },
39416
+ {
39417
+ key: "heart-pulse",
39418
+ group: "sensing",
39419
+ synonyms: [
39420
+ "battito",
39421
+ "salute",
39422
+ "monitoraggio"
39423
+ ]
39424
+ },
39425
+ {
39426
+ key: "star",
39427
+ group: "sensing",
39428
+ synonyms: [
39429
+ "preferito",
39430
+ "stella",
39431
+ "importante"
39432
+ ]
39433
+ },
39434
+ {
39435
+ key: "sparkles",
39436
+ group: "sensing",
39437
+ synonyms: [
39438
+ "automatico",
39439
+ "intelligente",
39440
+ "ai",
39441
+ "scena"
39442
+ ]
39443
+ },
39444
+ {
39445
+ key: "house",
39446
+ group: "place",
39447
+ synonyms: [
39448
+ "casa",
39449
+ "abitazione",
39450
+ "home"
39451
+ ]
39452
+ },
39453
+ {
39454
+ key: "sofa",
39455
+ group: "place",
39456
+ synonyms: [
39457
+ "salone",
39458
+ "soggiorno",
39459
+ "divano",
39460
+ "living"
39461
+ ]
39462
+ },
39463
+ {
39464
+ key: "bed-double",
39465
+ group: "place",
39466
+ synonyms: [
39467
+ "camera",
39468
+ "letto",
39469
+ "matrimoniale"
39470
+ ]
39471
+ },
39472
+ {
39473
+ key: "bed",
39474
+ group: "place",
39475
+ synonyms: [
39476
+ "cameretta",
39477
+ "letto",
39478
+ "singolo"
39479
+ ]
39480
+ },
39481
+ {
39482
+ key: "warehouse",
39483
+ group: "place",
39484
+ synonyms: [
39485
+ "magazzino",
39486
+ "capanno",
39487
+ "deposito",
39488
+ "cantina"
39489
+ ]
39490
+ },
39491
+ {
39492
+ key: "trees",
39493
+ group: "place",
39494
+ synonyms: [
39495
+ "giardino",
39496
+ "alberi",
39497
+ "esterno",
39498
+ "parco"
39499
+ ]
39500
+ },
39501
+ {
39502
+ key: "tree-pine",
39503
+ group: "place",
39504
+ synonyms: [
39505
+ "albero",
39506
+ "pino",
39507
+ "giardino"
39508
+ ]
39509
+ },
39510
+ {
39511
+ key: "fence",
39512
+ group: "place",
39513
+ synonyms: [
39514
+ "recinto",
39515
+ "cancello",
39516
+ "staccionata",
39517
+ "perimetro"
39518
+ ]
39519
+ },
39520
+ {
39521
+ key: "store",
39522
+ group: "place",
39523
+ synonyms: [
39524
+ "negozio",
39525
+ "ingresso",
39526
+ "vetrina"
39527
+ ]
39528
+ },
39529
+ {
39530
+ key: "building",
39531
+ group: "place",
39532
+ synonyms: ["edificio", "palazzo"]
39533
+ },
39534
+ {
39535
+ key: "building-2",
39536
+ group: "place",
39537
+ synonyms: [
39538
+ "edificio",
39539
+ "uffici",
39540
+ "condominio"
39541
+ ]
39542
+ },
39543
+ {
39544
+ key: "mountain",
39545
+ group: "place",
39546
+ synonyms: [
39547
+ "montagna",
39548
+ "esterno",
39549
+ "panorama"
39550
+ ]
39551
+ },
39552
+ {
39553
+ key: "umbrella",
39554
+ group: "place",
39555
+ synonyms: [
39556
+ "tenda",
39557
+ "markise",
39558
+ "ombrellone",
39559
+ "pergola",
39560
+ "veranda"
39561
+ ]
39562
+ },
39563
+ {
39564
+ key: "dumbbell",
39565
+ group: "place",
39566
+ synonyms: ["palestra", "attrezzi"]
39567
+ },
39568
+ {
39569
+ key: "wrench",
39570
+ group: "place",
39571
+ synonyms: [
39572
+ "officina",
39573
+ "manutenzione",
39574
+ "attrezzi",
39575
+ "utenze"
39576
+ ]
39577
+ },
39578
+ {
39579
+ key: "hammer",
39580
+ group: "place",
39581
+ synonyms: [
39582
+ "officina",
39583
+ "martello",
39584
+ "lavori"
39585
+ ]
39586
+ },
39587
+ {
39588
+ key: "drill",
39589
+ group: "place",
39590
+ synonyms: [
39591
+ "officina",
39592
+ "trapano",
39593
+ "lavori"
39594
+ ]
39595
+ },
39596
+ {
39597
+ key: "waves-ladder",
39598
+ group: "place",
39599
+ synonyms: ["piscina", "vasca"]
39600
+ },
39601
+ {
39602
+ key: "circle-parking",
39603
+ group: "place",
39604
+ synonyms: [
39605
+ "parcheggio",
39606
+ "posto",
39607
+ "auto",
39608
+ "garage"
39609
+ ]
39610
+ },
39611
+ {
39612
+ key: "route",
39613
+ group: "place",
39614
+ synonyms: [
39615
+ "percorso",
39616
+ "strada",
39617
+ "vialetto",
39618
+ "tragitto"
39619
+ ]
39620
+ },
39621
+ {
39622
+ key: "car",
39623
+ group: "life",
39624
+ synonyms: [
39625
+ "auto",
39626
+ "macchina",
39627
+ "veicolo",
39628
+ "parcheggio"
39629
+ ]
39630
+ },
39631
+ {
39632
+ key: "bike",
39633
+ group: "life",
39634
+ synonyms: [
39635
+ "bicicletta",
39636
+ "bici",
39637
+ "moto"
39638
+ ]
39639
+ },
39640
+ {
39641
+ key: "bus",
39642
+ group: "life",
39643
+ synonyms: ["autobus", "pullman"]
39644
+ },
39645
+ {
39646
+ key: "truck",
39647
+ group: "life",
39648
+ synonyms: [
39649
+ "camion",
39650
+ "furgone",
39651
+ "consegna"
39652
+ ]
39653
+ },
39654
+ {
39655
+ key: "plane",
39656
+ group: "life",
39657
+ synonyms: ["aereo"]
39658
+ },
39659
+ {
39660
+ key: "train-front",
39661
+ group: "life",
39662
+ synonyms: ["treno"]
39663
+ },
39664
+ {
39665
+ key: "sailboat",
39666
+ group: "life",
39667
+ synonyms: ["barca", "imbarcazione"]
39668
+ },
39669
+ {
39670
+ key: "dog",
39671
+ group: "life",
39672
+ synonyms: ["cane", "animale"]
39673
+ },
39674
+ {
39675
+ key: "cat",
39676
+ group: "life",
39677
+ synonyms: ["gatto", "animale"]
39678
+ },
39679
+ {
39680
+ key: "bird",
39681
+ group: "life",
39682
+ synonyms: ["uccello", "animale"]
39683
+ },
39684
+ {
39685
+ key: "paw-print",
39686
+ group: "life",
39687
+ synonyms: [
39688
+ "animale",
39689
+ "zampa",
39690
+ "pet"
39691
+ ]
39692
+ },
39693
+ {
39694
+ key: "baby",
39695
+ group: "life",
39696
+ synonyms: [
39697
+ "neonato",
39698
+ "bimbo",
39699
+ "cameretta",
39700
+ "babymonitor"
39701
+ ]
39702
+ },
39703
+ {
39704
+ key: "user",
39705
+ group: "life",
39706
+ synonyms: ["persona", "utente"]
39707
+ },
39708
+ {
39709
+ key: "users",
39710
+ group: "life",
39711
+ synonyms: [
39712
+ "persone",
39713
+ "gruppo",
39714
+ "famiglia"
39715
+ ]
39716
+ },
39717
+ {
39718
+ key: "person-standing",
39719
+ group: "life",
39720
+ synonyms: [
39721
+ "persona",
39722
+ "piedi",
39723
+ "presenza"
39724
+ ]
39725
+ }
39726
+ ];
39727
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
39728
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
39729
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
36993
39730
  Object.freeze({
39731
+ "accessories.getStatus": {
39732
+ capName: "accessories",
39733
+ capScope: "device",
39734
+ addonId: null,
39735
+ access: "view"
39736
+ },
36994
39737
  "accessories.setChildHidden": {
36995
39738
  capName: "accessories",
36996
39739
  capScope: "device",
@@ -37273,6 +40016,12 @@ Object.freeze({
37273
40016
  addonId: null,
37274
40017
  access: "view"
37275
40018
  },
40019
+ "airQualitySensor.getStatus": {
40020
+ capName: "air-quality-sensor",
40021
+ capScope: "device",
40022
+ addonId: null,
40023
+ access: "view"
40024
+ },
37276
40025
  "alarmPanel.arm": {
37277
40026
  capName: "alarm-panel",
37278
40027
  capScope: "device",
@@ -37285,6 +40034,12 @@ Object.freeze({
37285
40034
  addonId: null,
37286
40035
  access: "create"
37287
40036
  },
40037
+ "alarmPanel.getStatus": {
40038
+ capName: "alarm-panel",
40039
+ capScope: "device",
40040
+ addonId: null,
40041
+ access: "view"
40042
+ },
37288
40043
  "alarmPanel.trigger": {
37289
40044
  capName: "alarm-panel",
37290
40045
  capScope: "device",
@@ -37333,6 +40088,30 @@ Object.freeze({
37333
40088
  addonId: null,
37334
40089
  access: "create"
37335
40090
  },
40091
+ "ambientLightSensor.getStatus": {
40092
+ capName: "ambient-light-sensor",
40093
+ capScope: "device",
40094
+ addonId: null,
40095
+ access: "view"
40096
+ },
40097
+ "audioAnalysis.applyDeviceSettingsPatch": {
40098
+ capName: "audio-analysis",
40099
+ capScope: "device",
40100
+ addonId: null,
40101
+ access: "create"
40102
+ },
40103
+ "audioAnalysis.getDeviceLiveContribution": {
40104
+ capName: "audio-analysis",
40105
+ capScope: "device",
40106
+ addonId: null,
40107
+ access: "view"
40108
+ },
40109
+ "audioAnalysis.getDeviceSettingsContribution": {
40110
+ capName: "audio-analysis",
40111
+ capScope: "device",
40112
+ addonId: null,
40113
+ access: "view"
40114
+ },
37336
40115
  "audioAnalysis.resolveDeviceSettings": {
37337
40116
  capName: "audio-analysis",
37338
40117
  capScope: "device",
@@ -37495,6 +40274,12 @@ Object.freeze({
37495
40274
  addonId: null,
37496
40275
  access: "create"
37497
40276
  },
40277
+ "automationControl.getStatus": {
40278
+ capName: "automation-control",
40279
+ capScope: "device",
40280
+ addonId: null,
40281
+ access: "view"
40282
+ },
37498
40283
  "automationControl.trigger": {
37499
40284
  capName: "automation-control",
37500
40285
  capScope: "device",
@@ -37591,12 +40376,30 @@ Object.freeze({
37591
40376
  addonId: null,
37592
40377
  access: "create"
37593
40378
  },
40379
+ "battery.getStatus": {
40380
+ capName: "battery",
40381
+ capScope: "device",
40382
+ addonId: null,
40383
+ access: "view"
40384
+ },
37594
40385
  "battery.wakeForStream": {
37595
40386
  capName: "battery",
37596
40387
  capScope: "device",
37597
40388
  addonId: null,
37598
40389
  access: "create"
37599
40390
  },
40391
+ "binary.getStatus": {
40392
+ capName: "binary",
40393
+ capScope: "device",
40394
+ addonId: null,
40395
+ access: "view"
40396
+ },
40397
+ "brightness.getStatus": {
40398
+ capName: "brightness",
40399
+ capScope: "device",
40400
+ addonId: null,
40401
+ access: "view"
40402
+ },
37600
40403
  "brightness.setBrightness": {
37601
40404
  capName: "brightness",
37602
40405
  capScope: "device",
@@ -37711,6 +40514,30 @@ Object.freeze({
37711
40514
  addonId: null,
37712
40515
  access: "view"
37713
40516
  },
40517
+ "cameraCredentials.getStatus": {
40518
+ capName: "camera-credentials",
40519
+ capScope: "device",
40520
+ addonId: null,
40521
+ access: "view"
40522
+ },
40523
+ "cameraGridLayout.applyDeviceSettingsPatch": {
40524
+ capName: "camera-grid-layout",
40525
+ capScope: "device",
40526
+ addonId: null,
40527
+ access: "create"
40528
+ },
40529
+ "cameraGridLayout.getDeviceLiveContribution": {
40530
+ capName: "camera-grid-layout",
40531
+ capScope: "device",
40532
+ addonId: null,
40533
+ access: "view"
40534
+ },
40535
+ "cameraGridLayout.getDeviceSettingsContribution": {
40536
+ capName: "camera-grid-layout",
40537
+ capScope: "device",
40538
+ addonId: null,
40539
+ access: "view"
40540
+ },
37714
40541
  "cameraGridLayout.getLayout": {
37715
40542
  capName: "camera-grid-layout",
37716
40543
  capScope: "device",
@@ -37723,6 +40550,24 @@ Object.freeze({
37723
40550
  addonId: null,
37724
40551
  access: "create"
37725
40552
  },
40553
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
40554
+ capName: "camera-pipeline-config",
40555
+ capScope: "device",
40556
+ addonId: null,
40557
+ access: "create"
40558
+ },
40559
+ "cameraPipelineConfig.getDeviceLiveContribution": {
40560
+ capName: "camera-pipeline-config",
40561
+ capScope: "device",
40562
+ addonId: null,
40563
+ access: "view"
40564
+ },
40565
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
40566
+ capName: "camera-pipeline-config",
40567
+ capScope: "device",
40568
+ addonId: null,
40569
+ access: "view"
40570
+ },
37726
40571
  "cameraStreams.getBrokerStreams": {
37727
40572
  capName: "camera-streams",
37728
40573
  capScope: "device",
@@ -37753,6 +40598,18 @@ Object.freeze({
37753
40598
  addonId: null,
37754
40599
  access: "view"
37755
40600
  },
40601
+ "carbonMonoxide.getStatus": {
40602
+ capName: "carbon-monoxide",
40603
+ capScope: "device",
40604
+ addonId: null,
40605
+ access: "view"
40606
+ },
40607
+ "climateControl.getStatus": {
40608
+ capName: "climate-control",
40609
+ capScope: "device",
40610
+ addonId: null,
40611
+ access: "view"
40612
+ },
37756
40613
  "climateControl.setFanMode": {
37757
40614
  capName: "climate-control",
37758
40615
  capScope: "device",
@@ -37801,6 +40658,12 @@ Object.freeze({
37801
40658
  addonId: null,
37802
40659
  access: "create"
37803
40660
  },
40661
+ "color.getStatus": {
40662
+ capName: "color",
40663
+ capScope: "device",
40664
+ addonId: null,
40665
+ access: "view"
40666
+ },
37804
40667
  "color.setColor": {
37805
40668
  capName: "color",
37806
40669
  capScope: "device",
@@ -37819,12 +40682,36 @@ Object.freeze({
37819
40682
  addonId: null,
37820
40683
  access: "create"
37821
40684
  },
40685
+ "connectivity.getStatus": {
40686
+ capName: "connectivity",
40687
+ capScope: "device",
40688
+ addonId: null,
40689
+ access: "view"
40690
+ },
40691
+ "consumables.getStatus": {
40692
+ capName: "consumables",
40693
+ capScope: "device",
40694
+ addonId: null,
40695
+ access: "view"
40696
+ },
37822
40697
  "consumables.reset": {
37823
40698
  capName: "consumables",
37824
40699
  capScope: "device",
37825
40700
  addonId: null,
37826
40701
  access: "delete"
37827
40702
  },
40703
+ "contact.getStatus": {
40704
+ capName: "contact",
40705
+ capScope: "device",
40706
+ addonId: null,
40707
+ access: "view"
40708
+ },
40709
+ "control.getStatus": {
40710
+ capName: "control",
40711
+ capScope: "device",
40712
+ addonId: null,
40713
+ access: "view"
40714
+ },
37828
40715
  "control.setValue": {
37829
40716
  capName: "control",
37830
40717
  capScope: "device",
@@ -37891,6 +40778,12 @@ Object.freeze({
37891
40778
  addonId: null,
37892
40779
  access: "create"
37893
40780
  },
40781
+ "cover.getStatus": {
40782
+ capName: "cover",
40783
+ capScope: "device",
40784
+ addonId: null,
40785
+ access: "view"
40786
+ },
37894
40787
  "cover.open": {
37895
40788
  capName: "cover",
37896
40789
  capScope: "device",
@@ -38023,6 +40916,12 @@ Object.freeze({
38023
40916
  addonId: null,
38024
40917
  access: "view"
38025
40918
  },
40919
+ "dayNight.getStatus": {
40920
+ capName: "day-night",
40921
+ capScope: "device",
40922
+ addonId: null,
40923
+ access: "view"
40924
+ },
38026
40925
  "dayNight.setSettings": {
38027
40926
  capName: "day-night",
38028
40927
  capScope: "device",
@@ -38113,6 +41012,30 @@ Object.freeze({
38113
41012
  addonId: null,
38114
41013
  access: "view"
38115
41014
  },
41015
+ "detectionPipeline.applyDeviceSettingsPatch": {
41016
+ capName: "detection-pipeline",
41017
+ capScope: "device",
41018
+ addonId: null,
41019
+ access: "create"
41020
+ },
41021
+ "detectionPipeline.getDeviceLiveContribution": {
41022
+ capName: "detection-pipeline",
41023
+ capScope: "device",
41024
+ addonId: null,
41025
+ access: "view"
41026
+ },
41027
+ "detectionPipeline.getDeviceSettingsContribution": {
41028
+ capName: "detection-pipeline",
41029
+ capScope: "device",
41030
+ addonId: null,
41031
+ access: "view"
41032
+ },
41033
+ "deviceAdminLink.getAdminLink": {
41034
+ capName: "device-admin-link",
41035
+ capScope: "device",
41036
+ addonId: null,
41037
+ access: "view"
41038
+ },
38116
41039
  "deviceAdoption.adopt": {
38117
41040
  capName: "device-adoption",
38118
41041
  capScope: "system",
@@ -38125,6 +41048,12 @@ Object.freeze({
38125
41048
  addonId: null,
38126
41049
  access: "view"
38127
41050
  },
41051
+ "deviceAdoption.getStatus": {
41052
+ capName: "device-adoption",
41053
+ capScope: "system",
41054
+ addonId: null,
41055
+ access: "view"
41056
+ },
38128
41057
  "deviceAdoption.listCandidateFilters": {
38129
41058
  capName: "device-adoption",
38130
41059
  capScope: "system",
@@ -38161,6 +41090,12 @@ Object.freeze({
38161
41090
  addonId: null,
38162
41091
  access: "create"
38163
41092
  },
41093
+ "deviceDiscovery.getStatus": {
41094
+ capName: "device-discovery",
41095
+ capScope: "device",
41096
+ addonId: null,
41097
+ access: "view"
41098
+ },
38164
41099
  "deviceDiscovery.listDiscovered": {
38165
41100
  capName: "device-discovery",
38166
41101
  capScope: "device",
@@ -38179,12 +41114,30 @@ Object.freeze({
38179
41114
  addonId: null,
38180
41115
  access: "create"
38181
41116
  },
41117
+ "deviceExport.applyDeviceSettingsPatch": {
41118
+ capName: "device-export",
41119
+ capScope: "system",
41120
+ addonId: null,
41121
+ access: "create"
41122
+ },
38182
41123
  "deviceExport.exposeDevice": {
38183
41124
  capName: "device-export",
38184
41125
  capScope: "system",
38185
41126
  addonId: null,
38186
41127
  access: "create"
38187
41128
  },
41129
+ "deviceExport.getDeviceLiveContribution": {
41130
+ capName: "device-export",
41131
+ capScope: "system",
41132
+ addonId: null,
41133
+ access: "view"
41134
+ },
41135
+ "deviceExport.getDeviceSettingsContribution": {
41136
+ capName: "device-export",
41137
+ capScope: "system",
41138
+ addonId: null,
41139
+ access: "view"
41140
+ },
38188
41141
  "deviceExport.getStatus": {
38189
41142
  capName: "device-export",
38190
41143
  capScope: "system",
@@ -38281,6 +41234,12 @@ Object.freeze({
38281
41234
  addonId: null,
38282
41235
  access: "create"
38283
41236
  },
41237
+ "deviceManager.applyDeviceSettingsPatch": {
41238
+ capName: "device-manager",
41239
+ capScope: "system",
41240
+ addonId: null,
41241
+ access: "create"
41242
+ },
38284
41243
  "deviceManager.applyInitialMeta": {
38285
41244
  capName: "device-manager",
38286
41245
  capScope: "system",
@@ -38383,6 +41342,12 @@ Object.freeze({
38383
41342
  addonId: null,
38384
41343
  access: "view"
38385
41344
  },
41345
+ "deviceManager.getDeviceLiveContribution": {
41346
+ capName: "device-manager",
41347
+ capScope: "system",
41348
+ addonId: null,
41349
+ access: "view"
41350
+ },
38386
41351
  "deviceManager.getDeviceLiveInfoAggregate": {
38387
41352
  capName: "device-manager",
38388
41353
  capScope: "system",
@@ -38395,6 +41360,12 @@ Object.freeze({
38395
41360
  addonId: null,
38396
41361
  access: "view"
38397
41362
  },
41363
+ "deviceManager.getDeviceSettingsContribution": {
41364
+ capName: "device-manager",
41365
+ capScope: "system",
41366
+ addonId: null,
41367
+ access: "view"
41368
+ },
38398
41369
  "deviceManager.getDeviceStatusAggregate": {
38399
41370
  capName: "device-manager",
38400
41371
  capScope: "system",
@@ -38887,6 +41858,36 @@ Object.freeze({
38887
41858
  addonId: null,
38888
41859
  access: "create"
38889
41860
  },
41861
+ "deviceStatus.getStatus": {
41862
+ capName: "device-status",
41863
+ capScope: "device",
41864
+ addonId: null,
41865
+ access: "view"
41866
+ },
41867
+ "doorbell.applyDeviceSettingsPatch": {
41868
+ capName: "doorbell",
41869
+ capScope: "device",
41870
+ addonId: null,
41871
+ access: "create"
41872
+ },
41873
+ "doorbell.getDeviceLiveContribution": {
41874
+ capName: "doorbell",
41875
+ capScope: "device",
41876
+ addonId: null,
41877
+ access: "view"
41878
+ },
41879
+ "doorbell.getDeviceSettingsContribution": {
41880
+ capName: "doorbell",
41881
+ capScope: "device",
41882
+ addonId: null,
41883
+ access: "view"
41884
+ },
41885
+ "doorbell.getStatus": {
41886
+ capName: "doorbell",
41887
+ capScope: "device",
41888
+ addonId: null,
41889
+ access: "view"
41890
+ },
38890
41891
  "embeddingEncoder.encode": {
38891
41892
  capName: "embedding-encoder",
38892
41893
  capScope: "system",
@@ -38905,6 +41906,18 @@ Object.freeze({
38905
41906
  addonId: null,
38906
41907
  access: "view"
38907
41908
  },
41909
+ "enumSensor.getStatus": {
41910
+ capName: "enum-sensor",
41911
+ capScope: "device",
41912
+ addonId: null,
41913
+ access: "view"
41914
+ },
41915
+ "eventEmitter.getStatus": {
41916
+ capName: "event-emitter",
41917
+ capScope: "device",
41918
+ addonId: null,
41919
+ access: "view"
41920
+ },
38908
41921
  "events.getEventClipUrl": {
38909
41922
  capName: "events",
38910
41923
  capScope: "device",
@@ -39025,6 +42038,12 @@ Object.freeze({
39025
42038
  addonId: null,
39026
42039
  access: "view"
39027
42040
  },
42041
+ "fanControl.getStatus": {
42042
+ capName: "fan-control",
42043
+ capScope: "device",
42044
+ addonId: null,
42045
+ access: "view"
42046
+ },
39028
42047
  "fanControl.setDirection": {
39029
42048
  capName: "fan-control",
39030
42049
  capScope: "device",
@@ -39049,6 +42068,12 @@ Object.freeze({
39049
42068
  addonId: null,
39050
42069
  access: "create"
39051
42070
  },
42071
+ "featureProbe.getStatus": {
42072
+ capName: "feature-probe",
42073
+ capScope: "device",
42074
+ addonId: null,
42075
+ access: "view"
42076
+ },
39052
42077
  "filesystemBrowse.browse": {
39053
42078
  capName: "filesystem-browse",
39054
42079
  capScope: "system",
@@ -39067,6 +42092,24 @@ Object.freeze({
39067
42092
  addonId: null,
39068
42093
  access: "view"
39069
42094
  },
42095
+ "flood.getStatus": {
42096
+ capName: "flood",
42097
+ capScope: "device",
42098
+ addonId: null,
42099
+ access: "view"
42100
+ },
42101
+ "gas.getStatus": {
42102
+ capName: "gas",
42103
+ capScope: "device",
42104
+ addonId: null,
42105
+ access: "view"
42106
+ },
42107
+ "humidifier.getStatus": {
42108
+ capName: "humidifier",
42109
+ capScope: "device",
42110
+ addonId: null,
42111
+ access: "view"
42112
+ },
39070
42113
  "humidifier.setMode": {
39071
42114
  capName: "humidifier",
39072
42115
  capScope: "device",
@@ -39085,12 +42128,30 @@ Object.freeze({
39085
42128
  addonId: null,
39086
42129
  access: "create"
39087
42130
  },
42131
+ "humiditySensor.getStatus": {
42132
+ capName: "humidity-sensor",
42133
+ capScope: "device",
42134
+ addonId: null,
42135
+ access: "view"
42136
+ },
42137
+ "image.getStatus": {
42138
+ capName: "image",
42139
+ capScope: "device",
42140
+ addonId: null,
42141
+ access: "view"
42142
+ },
39088
42143
  "imageSettings.getOptions": {
39089
42144
  capName: "image-settings",
39090
42145
  capScope: "device",
39091
42146
  addonId: null,
39092
42147
  access: "view"
39093
42148
  },
42149
+ "imageSettings.getStatus": {
42150
+ capName: "image-settings",
42151
+ capScope: "device",
42152
+ addonId: null,
42153
+ access: "view"
42154
+ },
39094
42155
  "imageSettings.setSettings": {
39095
42156
  capName: "image-settings",
39096
42157
  capScope: "device",
@@ -39163,6 +42224,12 @@ Object.freeze({
39163
42224
  addonId: null,
39164
42225
  access: "create"
39165
42226
  },
42227
+ "intercom.getStatus": {
42228
+ capName: "intercom",
42229
+ capScope: "device",
42230
+ addonId: null,
42231
+ access: "view"
42232
+ },
39166
42233
  "intercom.handleAnswer": {
39167
42234
  capName: "intercom",
39168
42235
  capScope: "device",
@@ -39199,6 +42266,12 @@ Object.freeze({
39199
42266
  addonId: null,
39200
42267
  access: "create"
39201
42268
  },
42269
+ "lawnMowerControl.getStatus": {
42270
+ capName: "lawn-mower-control",
42271
+ capScope: "device",
42272
+ addonId: null,
42273
+ access: "view"
42274
+ },
39202
42275
  "lawnMowerControl.pause": {
39203
42276
  capName: "lawn-mower-control",
39204
42277
  capScope: "device",
@@ -39481,6 +42554,12 @@ Object.freeze({
39481
42554
  addonId: null,
39482
42555
  access: "create"
39483
42556
  },
42557
+ "lockControl.getStatus": {
42558
+ capName: "lock-control",
42559
+ capScope: "device",
42560
+ addonId: null,
42561
+ access: "view"
42562
+ },
39484
42563
  "lockControl.lock": {
39485
42564
  capName: "lock-control",
39486
42565
  capScope: "device",
@@ -39529,6 +42608,12 @@ Object.freeze({
39529
42608
  addonId: null,
39530
42609
  access: "view"
39531
42610
  },
42611
+ "mediaPlayer.getStatus": {
42612
+ capName: "media-player",
42613
+ capScope: "device",
42614
+ addonId: null,
42615
+ access: "view"
42616
+ },
39532
42617
  "mediaPlayer.next": {
39533
42618
  capName: "media-player",
39534
42619
  capScope: "device",
@@ -39721,6 +42806,12 @@ Object.freeze({
39721
42806
  addonId: null,
39722
42807
  access: "create"
39723
42808
  },
42809
+ "motion.getStatus": {
42810
+ capName: "motion",
42811
+ capScope: "device",
42812
+ addonId: null,
42813
+ access: "view"
42814
+ },
39724
42815
  "motion.isDetected": {
39725
42816
  capName: "motion",
39726
42817
  capScope: "device",
@@ -39733,6 +42824,24 @@ Object.freeze({
39733
42824
  addonId: null,
39734
42825
  access: "create"
39735
42826
  },
42827
+ "motionDetection.applyDeviceSettingsPatch": {
42828
+ capName: "motion-detection",
42829
+ capScope: "device",
42830
+ addonId: null,
42831
+ access: "create"
42832
+ },
42833
+ "motionDetection.getDeviceLiveContribution": {
42834
+ capName: "motion-detection",
42835
+ capScope: "device",
42836
+ addonId: null,
42837
+ access: "view"
42838
+ },
42839
+ "motionDetection.getDeviceSettingsContribution": {
42840
+ capName: "motion-detection",
42841
+ capScope: "device",
42842
+ addonId: null,
42843
+ access: "view"
42844
+ },
39736
42845
  "motionDetection.removeCamera": {
39737
42846
  capName: "motion-detection",
39738
42847
  capScope: "device",
@@ -39745,6 +42854,12 @@ Object.freeze({
39745
42854
  addonId: null,
39746
42855
  access: "delete"
39747
42856
  },
42857
+ "motionTrigger.getStatus": {
42858
+ capName: "motion-trigger",
42859
+ capScope: "device",
42860
+ addonId: null,
42861
+ access: "view"
42862
+ },
39748
42863
  "motionTrigger.setMotionTrigger": {
39749
42864
  capName: "motion-trigger",
39750
42865
  capScope: "device",
@@ -39757,6 +42872,12 @@ Object.freeze({
39757
42872
  addonId: null,
39758
42873
  access: "view"
39759
42874
  },
42875
+ "motionZones.getStatus": {
42876
+ capName: "motion-zones",
42877
+ capScope: "device",
42878
+ addonId: null,
42879
+ access: "view"
42880
+ },
39760
42881
  "motionZones.setZone": {
39761
42882
  capName: "motion-zones",
39762
42883
  capScope: "device",
@@ -39817,6 +42938,12 @@ Object.freeze({
39817
42938
  addonId: null,
39818
42939
  access: "view"
39819
42940
  },
42941
+ "nativeObjectDetection.getStatus": {
42942
+ capName: "native-object-detection",
42943
+ capScope: "device",
42944
+ addonId: null,
42945
+ access: "view"
42946
+ },
39820
42947
  "nativeObjectDetection.setEnabled": {
39821
42948
  capName: "native-object-detection",
39822
42949
  capScope: "device",
@@ -39835,6 +42962,12 @@ Object.freeze({
39835
42962
  addonId: null,
39836
42963
  access: "view"
39837
42964
  },
42965
+ "navigation.getStatus": {
42966
+ capName: "navigation",
42967
+ capScope: "device",
42968
+ addonId: null,
42969
+ access: "view"
42970
+ },
39838
42971
  "navigation.goToPoint": {
39839
42972
  capName: "navigation",
39840
42973
  capScope: "device",
@@ -39919,6 +43052,12 @@ Object.freeze({
39919
43052
  addonId: null,
39920
43053
  access: "create"
39921
43054
  },
43055
+ "networkLink.getStatus": {
43056
+ capName: "network-link",
43057
+ capScope: "device",
43058
+ addonId: null,
43059
+ access: "view"
43060
+ },
39922
43061
  "networkQuality.getAllStats": {
39923
43062
  capName: "network-quality",
39924
43063
  capScope: "system",
@@ -40177,18 +43316,36 @@ Object.freeze({
40177
43316
  addonId: null,
40178
43317
  access: "create"
40179
43318
  },
43319
+ "notifier.getStatus": {
43320
+ capName: "notifier",
43321
+ capScope: "device",
43322
+ addonId: null,
43323
+ access: "view"
43324
+ },
40180
43325
  "notifier.send": {
40181
43326
  capName: "notifier",
40182
43327
  capScope: "device",
40183
43328
  addonId: null,
40184
43329
  access: "create"
40185
43330
  },
43331
+ "numericSensor.getStatus": {
43332
+ capName: "numeric-sensor",
43333
+ capScope: "device",
43334
+ addonId: null,
43335
+ access: "view"
43336
+ },
40186
43337
  "oauthIntegration.getDescriptor": {
40187
43338
  capName: "oauth-integration",
40188
43339
  capScope: "system",
40189
43340
  addonId: null,
40190
43341
  access: "view"
40191
43342
  },
43343
+ "osd.getStatus": {
43344
+ capName: "osd",
43345
+ capScope: "device",
43346
+ addonId: null,
43347
+ access: "view"
43348
+ },
40192
43349
  "osd.setOverlay": {
40193
43350
  capName: "osd",
40194
43351
  capScope: "device",
@@ -40261,6 +43418,12 @@ Object.freeze({
40261
43418
  addonId: null,
40262
43419
  access: "create"
40263
43420
  },
43421
+ "petFeeder.getStatus": {
43422
+ capName: "pet-feeder",
43423
+ capScope: "device",
43424
+ addonId: null,
43425
+ access: "view"
43426
+ },
40264
43427
  "petFeeder.markFoodReplenished": {
40265
43428
  capName: "pet-feeder",
40266
43429
  capScope: "device",
@@ -40303,6 +43466,12 @@ Object.freeze({
40303
43466
  addonId: null,
40304
43467
  access: "create"
40305
43468
  },
43469
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
43470
+ capName: "pipeline-analytics",
43471
+ capScope: "device",
43472
+ addonId: null,
43473
+ access: "create"
43474
+ },
40306
43475
  "pipelineAnalytics.cancelRelocateMedia": {
40307
43476
  capName: "pipeline-analytics",
40308
43477
  capScope: "device",
@@ -40369,6 +43538,18 @@ Object.freeze({
40369
43538
  addonId: null,
40370
43539
  access: "view"
40371
43540
  },
43541
+ "pipelineAnalytics.getDeviceLiveContribution": {
43542
+ capName: "pipeline-analytics",
43543
+ capScope: "device",
43544
+ addonId: null,
43545
+ access: "view"
43546
+ },
43547
+ "pipelineAnalytics.getDeviceSettingsContribution": {
43548
+ capName: "pipeline-analytics",
43549
+ capScope: "device",
43550
+ addonId: null,
43551
+ access: "view"
43552
+ },
40372
43553
  "pipelineAnalytics.getEventDensity": {
40373
43554
  capName: "pipeline-analytics",
40374
43555
  capScope: "device",
@@ -40579,6 +43760,12 @@ Object.freeze({
40579
43760
  addonId: null,
40580
43761
  access: "view"
40581
43762
  },
43763
+ "pipelineAnalytics.ownersWithMedia": {
43764
+ capName: "pipeline-analytics",
43765
+ capScope: "device",
43766
+ addonId: null,
43767
+ access: "view"
43768
+ },
40582
43769
  "pipelineAnalytics.pauseForStorageMigration": {
40583
43770
  capName: "pipeline-analytics",
40584
43771
  capScope: "device",
@@ -40933,6 +44120,12 @@ Object.freeze({
40933
44120
  addonId: null,
40934
44121
  access: "view"
40935
44122
  },
44123
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
44124
+ capName: "pipeline-orchestrator",
44125
+ capScope: "system",
44126
+ addonId: null,
44127
+ access: "create"
44128
+ },
40936
44129
  "pipelineOrchestrator.assignAudio": {
40937
44130
  capName: "pipeline-orchestrator",
40938
44131
  capScope: "system",
@@ -41023,6 +44216,18 @@ Object.freeze({
41023
44216
  addonId: null,
41024
44217
  access: "view"
41025
44218
  },
44219
+ "pipelineOrchestrator.getDeviceLiveContribution": {
44220
+ capName: "pipeline-orchestrator",
44221
+ capScope: "system",
44222
+ addonId: null,
44223
+ access: "view"
44224
+ },
44225
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
44226
+ capName: "pipeline-orchestrator",
44227
+ capScope: "system",
44228
+ addonId: null,
44229
+ access: "view"
44230
+ },
41026
44231
  "pipelineOrchestrator.getGlobalMetrics": {
41027
44232
  capName: "pipeline-orchestrator",
41028
44233
  capScope: "system",
@@ -41431,12 +44636,36 @@ Object.freeze({
41431
44636
  addonId: null,
41432
44637
  access: "view"
41433
44638
  },
44639
+ "powerMeter.getStatus": {
44640
+ capName: "power-meter",
44641
+ capScope: "device",
44642
+ addonId: null,
44643
+ access: "view"
44644
+ },
44645
+ "presence.getStatus": {
44646
+ capName: "presence",
44647
+ capScope: "device",
44648
+ addonId: null,
44649
+ access: "view"
44650
+ },
44651
+ "pressureSensor.getStatus": {
44652
+ capName: "pressure-sensor",
44653
+ capScope: "device",
44654
+ addonId: null,
44655
+ access: "view"
44656
+ },
41434
44657
  "privacyMask.getOptions": {
41435
44658
  capName: "privacy-mask",
41436
44659
  capScope: "device",
41437
44660
  addonId: null,
41438
44661
  access: "view"
41439
44662
  },
44663
+ "privacyMask.getStatus": {
44664
+ capName: "privacy-mask",
44665
+ capScope: "device",
44666
+ addonId: null,
44667
+ access: "view"
44668
+ },
41440
44669
  "privacyMask.setAudioEnabled": {
41441
44670
  capName: "privacy-mask",
41442
44671
  capScope: "device",
@@ -41497,6 +44726,12 @@ Object.freeze({
41497
44726
  addonId: null,
41498
44727
  access: "view"
41499
44728
  },
44729
+ "ptz.getStatus": {
44730
+ capName: "ptz",
44731
+ capScope: "device",
44732
+ addonId: null,
44733
+ access: "view"
44734
+ },
41500
44735
  "ptz.goHome": {
41501
44736
  capName: "ptz",
41502
44737
  capScope: "device",
@@ -41575,6 +44810,12 @@ Object.freeze({
41575
44810
  addonId: null,
41576
44811
  access: "create"
41577
44812
  },
44813
+ "recording.applyDeviceSettingsPatch": {
44814
+ capName: "recording",
44815
+ capScope: "system",
44816
+ addonId: null,
44817
+ access: "create"
44818
+ },
41578
44819
  "recording.cancelRelocateJob": {
41579
44820
  capName: "recording",
41580
44821
  capScope: "system",
@@ -41623,6 +44864,18 @@ Object.freeze({
41623
44864
  addonId: null,
41624
44865
  access: "view"
41625
44866
  },
44867
+ "recording.getDeviceLiveContribution": {
44868
+ capName: "recording",
44869
+ capScope: "system",
44870
+ addonId: null,
44871
+ access: "view"
44872
+ },
44873
+ "recording.getDeviceSettingsContribution": {
44874
+ capName: "recording",
44875
+ capScope: "system",
44876
+ addonId: null,
44877
+ access: "view"
44878
+ },
41626
44879
  "recording.getPlacement": {
41627
44880
  capName: "recording",
41628
44881
  capScope: "system",
@@ -41641,6 +44894,12 @@ Object.freeze({
41641
44894
  addonId: null,
41642
44895
  access: "view"
41643
44896
  },
44897
+ "recording.getStatus": {
44898
+ capName: "recording",
44899
+ capScope: "system",
44900
+ addonId: null,
44901
+ access: "view"
44902
+ },
41644
44903
  "recording.getStorageMigrationMoveStatus": {
41645
44904
  capName: "recording",
41646
44905
  capScope: "system",
@@ -41815,6 +45074,24 @@ Object.freeze({
41815
45074
  addonId: null,
41816
45075
  access: "view"
41817
45076
  },
45077
+ "recordingOnboard.getOptions": {
45078
+ capName: "recording-onboard",
45079
+ capScope: "device",
45080
+ addonId: null,
45081
+ access: "view"
45082
+ },
45083
+ "recordingOnboard.getStatus": {
45084
+ capName: "recording-onboard",
45085
+ capScope: "device",
45086
+ addonId: null,
45087
+ access: "view"
45088
+ },
45089
+ "recordingOnboard.setSettings": {
45090
+ capName: "recording-onboard",
45091
+ capScope: "device",
45092
+ addonId: null,
45093
+ access: "create"
45094
+ },
41818
45095
  "recordingSignal.getStatus": {
41819
45096
  capName: "recording-signal",
41820
45097
  capScope: "device",
@@ -41845,6 +45122,12 @@ Object.freeze({
41845
45122
  addonId: null,
41846
45123
  access: "delete"
41847
45124
  },
45125
+ "sceneMonitor.getStatus": {
45126
+ capName: "scene-monitor",
45127
+ capScope: "device",
45128
+ addonId: null,
45129
+ access: "view"
45130
+ },
41848
45131
  "sceneMonitor.listScenes": {
41849
45132
  capName: "scene-monitor",
41850
45133
  capScope: "device",
@@ -41875,6 +45158,12 @@ Object.freeze({
41875
45158
  addonId: null,
41876
45159
  access: "create"
41877
45160
  },
45161
+ "scriptRunner.getStatus": {
45162
+ capName: "script-runner",
45163
+ capScope: "device",
45164
+ addonId: null,
45165
+ access: "view"
45166
+ },
41878
45167
  "scriptRunner.run": {
41879
45168
  capName: "script-runner",
41880
45169
  capScope: "device",
@@ -42007,6 +45296,12 @@ Object.freeze({
42007
45296
  addonId: null,
42008
45297
  access: "create"
42009
45298
  },
45299
+ "smoke.getStatus": {
45300
+ capName: "smoke",
45301
+ capScope: "device",
45302
+ addonId: null,
45303
+ access: "view"
45304
+ },
42010
45305
  "smtpProvider.getStatus": {
42011
45306
  capName: "smtp-provider",
42012
45307
  capScope: "system",
@@ -42025,12 +45320,30 @@ Object.freeze({
42025
45320
  addonId: null,
42026
45321
  access: "view"
42027
45322
  },
45323
+ "snapshot.applyDeviceSettingsPatch": {
45324
+ capName: "snapshot",
45325
+ capScope: "device",
45326
+ addonId: null,
45327
+ access: "create"
45328
+ },
42028
45329
  "snapshot.getDebugState": {
42029
45330
  capName: "snapshot",
42030
45331
  capScope: "device",
42031
45332
  addonId: null,
42032
45333
  access: "view"
42033
45334
  },
45335
+ "snapshot.getDeviceLiveContribution": {
45336
+ capName: "snapshot",
45337
+ capScope: "device",
45338
+ addonId: null,
45339
+ access: "view"
45340
+ },
45341
+ "snapshot.getDeviceSettingsContribution": {
45342
+ capName: "snapshot",
45343
+ capScope: "device",
45344
+ addonId: null,
45345
+ access: "view"
45346
+ },
42034
45347
  "snapshot.getSnapshot": {
42035
45348
  capName: "snapshot",
42036
45349
  capScope: "device",
@@ -42049,6 +45362,12 @@ Object.freeze({
42049
45362
  addonId: null,
42050
45363
  access: "view"
42051
45364
  },
45365
+ "snapshot.getStatus": {
45366
+ capName: "snapshot",
45367
+ capScope: "device",
45368
+ addonId: null,
45369
+ access: "view"
45370
+ },
42052
45371
  "snapshot.invalidateCache": {
42053
45372
  capName: "snapshot",
42054
45373
  capScope: "device",
@@ -42385,6 +45704,12 @@ Object.freeze({
42385
45704
  addonId: null,
42386
45705
  access: "create"
42387
45706
  },
45707
+ "streamBroker.applyDeviceSettingsPatch": {
45708
+ capName: "stream-broker",
45709
+ capScope: "system",
45710
+ addonId: null,
45711
+ access: "create"
45712
+ },
42388
45713
  "streamBroker.assignProfile": {
42389
45714
  capName: "stream-broker",
42390
45715
  capScope: "system",
@@ -42421,6 +45746,18 @@ Object.freeze({
42421
45746
  addonId: null,
42422
45747
  access: "view"
42423
45748
  },
45749
+ "streamBroker.getDeviceLiveContribution": {
45750
+ capName: "stream-broker",
45751
+ capScope: "system",
45752
+ addonId: null,
45753
+ access: "view"
45754
+ },
45755
+ "streamBroker.getDeviceSettingsContribution": {
45756
+ capName: "stream-broker",
45757
+ capScope: "system",
45758
+ addonId: null,
45759
+ access: "view"
45760
+ },
42424
45761
  "streamBroker.getPreBufferInfo": {
42425
45762
  capName: "stream-broker",
42426
45763
  capScope: "system",
@@ -42619,6 +45956,12 @@ Object.freeze({
42619
45956
  addonId: null,
42620
45957
  access: "view"
42621
45958
  },
45959
+ "streamParams.getStatus": {
45960
+ capName: "stream-params",
45961
+ capScope: "device",
45962
+ addonId: null,
45963
+ access: "view"
45964
+ },
42622
45965
  "streamParams.setProfile": {
42623
45966
  capName: "stream-params",
42624
45967
  capScope: "device",
@@ -42631,6 +45974,12 @@ Object.freeze({
42631
45974
  addonId: null,
42632
45975
  access: "view"
42633
45976
  },
45977
+ "switch.getStatus": {
45978
+ capName: "switch",
45979
+ capScope: "device",
45980
+ addonId: null,
45981
+ access: "view"
45982
+ },
42634
45983
  "switch.setState": {
42635
45984
  capName: "switch",
42636
45985
  capScope: "device",
@@ -42727,6 +46076,18 @@ Object.freeze({
42727
46076
  addonId: null,
42728
46077
  access: "create"
42729
46078
  },
46079
+ "tamper.getStatus": {
46080
+ capName: "tamper",
46081
+ capScope: "device",
46082
+ addonId: null,
46083
+ access: "view"
46084
+ },
46085
+ "temperatureSensor.getStatus": {
46086
+ capName: "temperature-sensor",
46087
+ capScope: "device",
46088
+ addonId: null,
46089
+ access: "view"
46090
+ },
42730
46091
  "terminalSession.adoptLegacyMonitor": {
42731
46092
  capName: "terminal-session",
42732
46093
  capScope: "system",
@@ -42823,6 +46184,12 @@ Object.freeze({
42823
46184
  addonId: null,
42824
46185
  access: "view"
42825
46186
  },
46187
+ "update.getStatus": {
46188
+ capName: "update",
46189
+ capScope: "device",
46190
+ addonId: null,
46191
+ access: "view"
46192
+ },
42826
46193
  "update.installUpdate": {
42827
46194
  capName: "update",
42828
46195
  capScope: "device",
@@ -43045,6 +46412,12 @@ Object.freeze({
43045
46412
  addonId: null,
43046
46413
  access: "create"
43047
46414
  },
46415
+ "vacuumControl.getStatus": {
46416
+ capName: "vacuum-control",
46417
+ capScope: "device",
46418
+ addonId: null,
46419
+ access: "view"
46420
+ },
43048
46421
  "vacuumControl.locate": {
43049
46422
  capName: "vacuum-control",
43050
46423
  capScope: "device",
@@ -43087,6 +46460,12 @@ Object.freeze({
43087
46460
  addonId: null,
43088
46461
  access: "create"
43089
46462
  },
46463
+ "valve.getStatus": {
46464
+ capName: "valve",
46465
+ capScope: "device",
46466
+ addonId: null,
46467
+ access: "view"
46468
+ },
43090
46469
  "valve.open": {
43091
46470
  capName: "valve",
43092
46471
  capScope: "device",
@@ -43159,6 +46538,18 @@ Object.freeze({
43159
46538
  addonId: null,
43160
46539
  access: "create"
43161
46540
  },
46541
+ "vibration.getStatus": {
46542
+ capName: "vibration",
46543
+ capScope: "device",
46544
+ addonId: null,
46545
+ access: "view"
46546
+ },
46547
+ "videoclips.dialClipStream": {
46548
+ capName: "videoclips",
46549
+ capScope: "device",
46550
+ addonId: null,
46551
+ access: "view"
46552
+ },
43162
46553
  "videoclips.getClipPlayback": {
43163
46554
  capName: "videoclips",
43164
46555
  capScope: "device",
@@ -43171,6 +46562,18 @@ Object.freeze({
43171
46562
  addonId: null,
43172
46563
  access: "view"
43173
46564
  },
46565
+ "videoclips.listSources": {
46566
+ capName: "videoclips",
46567
+ capScope: "device",
46568
+ addonId: null,
46569
+ access: "view"
46570
+ },
46571
+ "videoclips.readClipBytes": {
46572
+ capName: "videoclips",
46573
+ capScope: "device",
46574
+ addonId: null,
46575
+ access: "view"
46576
+ },
43174
46577
  "viewerUi.getStaticDir": {
43175
46578
  capName: "viewer-ui",
43176
46579
  capScope: "system",
@@ -43183,6 +46586,12 @@ Object.freeze({
43183
46586
  addonId: null,
43184
46587
  access: "view"
43185
46588
  },
46589
+ "waterHeater.getStatus": {
46590
+ capName: "water-heater",
46591
+ capScope: "device",
46592
+ addonId: null,
46593
+ access: "view"
46594
+ },
43186
46595
  "waterHeater.setAway": {
43187
46596
  capName: "water-heater",
43188
46597
  capScope: "device",
@@ -43201,6 +46610,12 @@ Object.freeze({
43201
46610
  addonId: null,
43202
46611
  access: "create"
43203
46612
  },
46613
+ "weather.getStatus": {
46614
+ capName: "weather",
46615
+ capScope: "device",
46616
+ addonId: null,
46617
+ access: "view"
46618
+ },
43204
46619
  "webrtcSession.addIceCandidate": {
43205
46620
  capName: "webrtc-session",
43206
46621
  capScope: "device",
@@ -43225,6 +46640,12 @@ Object.freeze({
43225
46640
  addonId: null,
43226
46641
  access: "view"
43227
46642
  },
46643
+ "webrtcSession.getSessionDebug": {
46644
+ capName: "webrtc-session",
46645
+ capScope: "device",
46646
+ addonId: null,
46647
+ access: "view"
46648
+ },
43228
46649
  "webrtcSession.getSessionState": {
43229
46650
  capName: "webrtc-session",
43230
46651
  capScope: "device",
@@ -43557,6 +46978,11 @@ Object.freeze({
43557
46978
  form: "single",
43558
46979
  optional: true
43559
46980
  }],
46981
+ "deviceAdminLink.getAdminLink": [{
46982
+ name: "deviceId",
46983
+ form: "single",
46984
+ optional: false
46985
+ }],
43560
46986
  "deviceAdoption.release": [{
43561
46987
  name: "camDeviceId",
43562
46988
  form: "single",
@@ -44467,6 +47893,11 @@ Object.freeze({
44467
47893
  form: "single",
44468
47894
  optional: false
44469
47895
  }],
47896
+ "pipelineAnalytics.ownersWithMedia": [{
47897
+ name: "deviceId",
47898
+ form: "single",
47899
+ optional: false
47900
+ }],
44470
47901
  "pipelineAnalytics.proposeRetrainAnnotations": [{
44471
47902
  name: "deviceId",
44472
47903
  form: "single",
@@ -44937,6 +48368,16 @@ Object.freeze({
44937
48368
  form: "single",
44938
48369
  optional: true
44939
48370
  }],
48371
+ "recordingOnboard.getOptions": [{
48372
+ name: "deviceId",
48373
+ form: "single",
48374
+ optional: false
48375
+ }],
48376
+ "recordingOnboard.setSettings": [{
48377
+ name: "deviceId",
48378
+ form: "single",
48379
+ optional: false
48380
+ }],
44940
48381
  "recordingSignal.getStatus": [{
44941
48382
  name: "deviceId",
44942
48383
  form: "single",
@@ -45163,6 +48604,11 @@ Object.freeze({
45163
48604
  form: "single",
45164
48605
  optional: false
45165
48606
  }],
48607
+ "videoclips.dialClipStream": [{
48608
+ name: "deviceId",
48609
+ form: "single",
48610
+ optional: false
48611
+ }],
45166
48612
  "videoclips.getClipPlayback": [{
45167
48613
  name: "deviceId",
45168
48614
  form: "single",
@@ -45173,6 +48619,16 @@ Object.freeze({
45173
48619
  form: "single",
45174
48620
  optional: false
45175
48621
  }],
48622
+ "videoclips.listSources": [{
48623
+ name: "deviceId",
48624
+ form: "single",
48625
+ optional: false
48626
+ }],
48627
+ "videoclips.readClipBytes": [{
48628
+ name: "deviceId",
48629
+ form: "single",
48630
+ optional: false
48631
+ }],
45176
48632
  "waterHeater.setAway": [{
45177
48633
  name: "deviceId",
45178
48634
  form: "single",
@@ -45208,6 +48664,11 @@ Object.freeze({
45208
48664
  form: "single",
45209
48665
  optional: false
45210
48666
  }],
48667
+ "webrtcSession.getSessionDebug": [{
48668
+ name: "deviceId",
48669
+ form: "single",
48670
+ optional: false
48671
+ }],
45211
48672
  "webrtcSession.getSessionState": [{
45212
48673
  name: "deviceId",
45213
48674
  form: "single",
@@ -46381,11 +49842,49 @@ var RademacherHubDevice = class extends BaseDevice {
46381
49842
  super(ctx, rademacherHubSchema, { type: DeviceType.Hub });
46382
49843
  this.online = false;
46383
49844
  this.registerDeviceDiscovery();
49845
+ this.registerAdminLinkCap();
46384
49846
  this.updateSourceInfo({
46385
49847
  id: this.stableId,
46386
49848
  system: "rademacher"
46387
49849
  });
46388
49850
  }
49851
+ /**
49852
+ * device-admin-link — the hub's own management page.
49853
+ *
49854
+ * The HomePilot serves its web UI on plain HTTP at the hub's own address.
49855
+ * `basePath` is the operator's API base-path override ("" for v1, "/hp" for
49856
+ * v2) and is carried through, because a hub reached under a prefix serves its
49857
+ * page under the same prefix.
49858
+ *
49859
+ * Only the hub gets the cap. Its children are radio actuators with no web
49860
+ * server anywhere, and the address they would borrow lives on the hub, which
49861
+ * is where the operator edits it.
49862
+ */
49863
+ registerAdminLinkCap() {
49864
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
49865
+ if (deviceId !== this.id) throw new Error(`RademacherHubDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
49866
+ const connection = this.config.values.connection;
49867
+ const built = buildDeviceAdminUrl({
49868
+ host: connection.host,
49869
+ port: null,
49870
+ scheme: "http",
49871
+ path: connection.basePath === "" ? "/" : `${connection.basePath}/`
49872
+ });
49873
+ if (!built.ok) {
49874
+ this.ctx.logger.debug("rademacher admin link unavailable", {
49875
+ tags: { deviceId: this.id },
49876
+ meta: { reason: built.reason }
49877
+ });
49878
+ return null;
49879
+ }
49880
+ return {
49881
+ url: built.url,
49882
+ label: "HomePilot web UI",
49883
+ target: "integration",
49884
+ host: connection.host
49885
+ };
49886
+ } });
49887
+ }
46389
49888
  /** Adopt a pre-built manager (created by `onCreateDevice` so the live client
46390
49889
  * connects once). Published on the resolver so children pick it up. */
46391
49890
  adoptManager(manager) {