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