@camstack/addon-provider-amcrest 0.2.116 → 0.2.118

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 +3532 -33
  2. package/dist/addon.mjs +3532 -33
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -5368,7 +5368,7 @@ var ZodIssueCode = {
5368
5368
  var ZodFirstPartyTypeKind;
5369
5369
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5370
5370
  //#endregion
5371
- //#region ../types/dist/sleep-Bs3xPtSh.mjs
5371
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
5372
5372
  /**
5373
5373
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5374
5374
  * window to float samples (D455).
@@ -6510,6 +6510,17 @@ var CamProfileSchema = _enum([
6510
6510
  "mid",
6511
6511
  "low"
6512
6512
  ]);
6513
+ /**
6514
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
6515
+ *
6516
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
6517
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
6518
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
6519
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
6520
+ * the operator picked. Today's one user is the audio analyser, whose rule is
6521
+ * "the lowest AVAILABLE native rung" (D537).
6522
+ */
6523
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
6513
6524
  var CamStreamKindSchema = _enum([
6514
6525
  "pull-rtsp",
6515
6526
  "pull-rtmp",
@@ -6928,6 +6939,18 @@ function systemMethod(input, output, options) {
6928
6939
  systemOnly: true
6929
6940
  };
6930
6941
  }
6942
+ /**
6943
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
6944
+ * on `InferProvider`. The `providerOptional: true` literal is what
6945
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
6946
+ * for when this is the honest answer and when it is a soft stub.
6947
+ */
6948
+ function optionalMethod(input, output, options) {
6949
+ return {
6950
+ ...method(input, output, options),
6951
+ providerOptional: true
6952
+ };
6953
+ }
6931
6954
  /** Shorthand to define an event schema */
6932
6955
  function event(data) {
6933
6956
  return { data };
@@ -7346,6 +7369,8 @@ var EVENT_OWNER_TYPES = [
7346
7369
  * nothing failing until a caller asked.
7347
7370
  */
7348
7371
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7372
+ /** The same list as a Zod enum, for the cap input that carries it. */
7373
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7349
7374
  new Set(EVENT_OWNER_TYPES);
7350
7375
  var EncodeProfileSchema = object({
7351
7376
  video: object({
@@ -7718,6 +7743,7 @@ var CameraSwitchIdSchema = _enum([
7718
7743
  "device-audio",
7719
7744
  "broker-audio",
7720
7745
  "audio-analysis",
7746
+ "event-saving",
7721
7747
  "recording",
7722
7748
  "notifications"
7723
7749
  ]);
@@ -7753,10 +7779,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7753
7779
  var CameraSwitchUnavailableReasonSchema = _enum([
7754
7780
  "no-provider",
7755
7781
  "source-unreachable",
7756
- "not-configured",
7757
7782
  "device-disabled"
7758
7783
  ]);
7759
7784
  /**
7785
+ * Something true about a switch that is nonetheless OFFERED.
7786
+ *
7787
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
7788
+ * whole point: a reason replaces the control, a warning travels WITH it. The
7789
+ * privacy mask taught us the difference — greying the one button that starts
7790
+ * the feature teaches the operator the feature is broken, while a live toggle
7791
+ * that flips nothing teaches them the switch is broken. Neither is true; the
7792
+ * honest surface is a working toggle that says what is missing.
7793
+ */
7794
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
7795
+ /**
7760
7796
  * One switch, resolved for one camera.
7761
7797
  *
7762
7798
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -7775,6 +7811,11 @@ var CameraSwitchSchema = object({
7775
7811
  /** False = do not render a control. `unavailableReason` says why. */
7776
7812
  available: boolean(),
7777
7813
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7814
+ /**
7815
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
7816
+ * never a substitute for one.
7817
+ */
7818
+ warning: CameraSwitchWarningSchema.optional(),
7778
7819
  /** Current state. Meaningless when `available` is false — read it as `true`. */
7779
7820
  enabled: boolean(),
7780
7821
  authority: CameraSwitchAuthoritySchema
@@ -8192,11 +8233,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
8192
8233
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
8193
8234
  * span wraps past midnight (handled by the band engine).
8194
8235
  */
8236
+ /**
8237
+ * Whether a band's footage carries sound.
8238
+ *
8239
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
8240
+ * camera recorded sound whether anyone wanted it or not. The broker already
8241
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
8242
+ * restream — so this was a choice the product could always have offered.
8243
+ *
8244
+ * It belongs on the BAND rather than the camera because one camera can want
8245
+ * both: silent across the shop floor during opening hours, sound overnight.
8246
+ * And it applies to BOTH modes — an events band firing on motion has the same
8247
+ * question to answer as a continuous one.
8248
+ */
8249
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
8195
8250
  var RecordingBandSchema = object({
8196
8251
  days: array(RecordingWeekdaySchema),
8197
8252
  start: string().regex(HHMM),
8198
8253
  end: string().regex(HHMM),
8199
8254
  mode: RecordingBandModeSchema,
8255
+ /**
8256
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
8257
+ * written before this existed carried sound, and a band whose key is
8258
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
8259
+ */
8260
+ audio: RecordingBandAudioSchema.optional(),
8200
8261
  triggers: RecordingBandTriggersSchema.optional(),
8201
8262
  /**
8202
8263
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -13480,6 +13541,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13480
13541
  auth: "admin"
13481
13542
  });
13482
13543
  /**
13544
+ * device-admin-link — "this device has a management page of its own, and here
13545
+ * is its address".
13546
+ *
13547
+ * ## Why this is not a `deviceConfig` cap
13548
+ *
13549
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13550
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13551
+ * patch back through a setter; it costs a `builderId` reducer in
13552
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13553
+ * renders a form section. This cap answers ONE question with ONE read and
13554
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13555
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13556
+ * `reboot`, the other pure-RPC device-native cap.
13557
+ *
13558
+ * ## Absent, and the difference between "no page" and "we cannot say"
13559
+ *
13560
+ * The two are answered at DIFFERENT layers, on purpose:
13561
+ *
13562
+ * - **"We cannot say"** → the provider never registers the cap for that
13563
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13564
+ * fan are reached only through a vendor cloud; there is no address to hand
13565
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13566
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13567
+ * behind it. None of them register, so `deviceManager.getBindings` never
13568
+ * lists the cap and no surface asks.
13569
+ * - **"This device has no page, and I know that"** → the provider registers
13570
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13571
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13572
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13573
+ * transport, a Home Assistant broker authenticated by supervisor token
13574
+ * (which carries no `baseUrl` at all).
13575
+ *
13576
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13577
+ * button, and D62 is the same rule from the other side: an off switch is
13578
+ * reported off, never made to look broken. There is no third state where the
13579
+ * UI renders a disabled button "because the device might have a page".
13580
+ *
13581
+ * ## The URL never carries credentials
13582
+ *
13583
+ * Not in userinfo, not in a query string. Every provider builds through
13584
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13585
+ * scheme and path as separate arguments — there is no parameter a secret could
13586
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13587
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13588
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13589
+ * keeps providers from hand-rolling one anyway.
13590
+ *
13591
+ * This matters here more than anywhere else in the repo, because every provider
13592
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13593
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13594
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13595
+ * camera's own page will ask for its own login. That is correct, and pre-
13596
+ * filling it is the operator's business, not ours.
13597
+ *
13598
+ * ## It is a LAN fact
13599
+ *
13600
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13601
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13602
+ * as a link the operator's own browser follows, on the operator's own network,
13603
+ * or renders nothing.
13604
+ */
13605
+ /**
13606
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13607
+ * before clicking whether he is about to land on a camera's own web server or
13608
+ * inside Home Assistant.
13609
+ */
13610
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13611
+ var DeviceAdminLinkSchema = object({
13612
+ /**
13613
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13614
+ * free of userinfo and of any credential-shaped query key.
13615
+ */
13616
+ url: string(),
13617
+ /**
13618
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13619
+ * The PROVIDER names it, because only the provider knows what the page is;
13620
+ * a UI that invented the label from the addon id would call the Home
13621
+ * Assistant device page "Provider Homeassistant".
13622
+ */
13623
+ label: string(),
13624
+ target: AdminLinkTargetEnum,
13625
+ /**
13626
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13627
+ * an operator can see WHERE the button goes before he follows it. Redundant
13628
+ * with `url` by construction; carried separately so no surface has to parse
13629
+ * a URL to show it.
13630
+ */
13631
+ host: string()
13632
+ });
13633
+ var deviceAdminLinkCapability = {
13634
+ name: "device-admin-link",
13635
+ scope: "device",
13636
+ deviceNative: true,
13637
+ mode: "singleton",
13638
+ methods: {
13639
+ /**
13640
+ * The device's management page, or `null` when this device has none.
13641
+ *
13642
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13643
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13644
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13645
+ * The URL is also a statement about the LAN, which a household member with
13646
+ * a `view` grant on a light has no reason to be handed.
13647
+ *
13648
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13649
+ * without the right loses the query and draws nothing, which is the same
13650
+ * thing a device with no page draws. There is no path on which a button
13651
+ * appears and then fails — the D403 failure mode, from the other end.
13652
+ */
13653
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13654
+ };
13655
+ /**
13656
+ * Query keys that may never appear on an admin link. A credential smuggled as
13657
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13658
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13659
+ * identically.
13660
+ */
13661
+ var CREDENTIAL_QUERY_KEYS = [
13662
+ "password",
13663
+ "passwd",
13664
+ "pwd",
13665
+ "pass",
13666
+ "user",
13667
+ "username",
13668
+ "usr",
13669
+ "login",
13670
+ "token",
13671
+ "access_token",
13672
+ "auth",
13673
+ "authorization",
13674
+ "apikey",
13675
+ "api_key",
13676
+ "secret",
13677
+ "credential",
13678
+ "credentials",
13679
+ "session",
13680
+ "sessionid",
13681
+ "key"
13682
+ ];
13683
+ /**
13684
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13685
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13686
+ * structurally identical on purpose: a URL this function returns is a URL that
13687
+ * guard would pass.
13688
+ */
13689
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13690
+ /** A host that is safe to place in an authority component verbatim. */
13691
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13692
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13693
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13694
+ function defaultPortFor(scheme) {
13695
+ return scheme === "https" ? 443 : 80;
13696
+ }
13697
+ /**
13698
+ * Build the admin-page URL, or refuse by name.
13699
+ *
13700
+ * The refusal is never thrown: a provider answering "no page for this device"
13701
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13702
+ * a missing host on one device into a failed query for the whole surface.
13703
+ */
13704
+ function buildDeviceAdminUrl(parts) {
13705
+ const host = parts.host.trim();
13706
+ if (host.length === 0) return {
13707
+ ok: false,
13708
+ reason: "empty-host"
13709
+ };
13710
+ if (host.includes("@")) return {
13711
+ ok: false,
13712
+ reason: "host-carries-userinfo"
13713
+ };
13714
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13715
+ ok: false,
13716
+ reason: "host-not-plain"
13717
+ };
13718
+ if (parts.port !== null) {
13719
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13720
+ ok: false,
13721
+ reason: "port-out-of-range"
13722
+ };
13723
+ }
13724
+ if (!parts.path.startsWith("/")) return {
13725
+ ok: false,
13726
+ reason: "path-not-absolute"
13727
+ };
13728
+ const query = parts.query ?? {};
13729
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13730
+ ok: false,
13731
+ reason: "credential-query-key"
13732
+ };
13733
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13734
+ const search = new URLSearchParams();
13735
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13736
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13737
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13738
+ if (CREDENTIAL_URL.test(url)) return {
13739
+ ok: false,
13740
+ reason: "userinfo-in-result"
13741
+ };
13742
+ return {
13743
+ ok: true,
13744
+ url
13745
+ };
13746
+ }
13747
+ /**
13483
13748
  * `device-export` — collection cap for addons that export camstack
13484
13749
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
13485
13750
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -20246,6 +20511,7 @@ var MediaFileKindEnum = _enum([
20246
20511
  "keyFrameSmall",
20247
20512
  "thumbnailSmall",
20248
20513
  "confirmationCrop",
20514
+ "debugCrop",
20249
20515
  "sceneFrame"
20250
20516
  ]);
20251
20517
  /**
@@ -21325,7 +21591,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21325
21591
  ownerType: EventOwnerTypeSchema,
21326
21592
  eventId: number().int(),
21327
21593
  deviceId: number()
21328
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21594
+ }), array(MediaFileInfoSchema).readonly()), method(object({
21595
+ deviceId: number(),
21596
+ ownerKind: MediaPresenceOwnerKindSchema,
21597
+ ownerIds: array(string()).max(5e3)
21598
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21329
21599
  kind: "mutation",
21330
21600
  auth: "admin"
21331
21601
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -21910,6 +22180,18 @@ var RunnerCameraConfigSchema = object({
21910
22180
  motionStreamId: string(),
21911
22181
  detectionStreamId: string(),
21912
22182
  /**
22183
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
22184
+ * AVAILABLE native rung) or an explicit one.
22185
+ *
22186
+ * Not consumed by the runner: carried here so it travels with the camera's
22187
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
22188
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
22189
+ * with no default — absent means `auto`, which is what every camera written
22190
+ * before this field had, and an absent optional adds nothing to the
22191
+ * generated cap-input defaults (D537).
22192
+ */
22193
+ audioStreamPick: AudioStreamPickSchema.optional(),
22194
+ /**
21913
22195
  * Motion sources that can activate the pipeline. At least one must
21914
22196
  * be present. Multiple sources OR-ed: any one firing arms the
21915
22197
  * cooldown timer and transitions watching → active. The same
@@ -22980,10 +23262,9 @@ var ServerBootModeSchema = _enum([
22980
23262
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
22981
23263
  * - `pending-restart` — a version is staged and the node has NOT yet
22982
23264
  * restarted onto it (still running the OLD version).
22983
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
22984
- * (it is the active probation boot) and is waiting to confirm boot-health.
22985
- * Apply/rollback are refused in this state and the node must NOT be
22986
- * manually restarted, or the probation boot auto-rolls-back.
23265
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
23266
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
23267
+ * no provider emits it.
22987
23268
  */
22988
23269
  var ServerUpdateStateSchema = _enum([
22989
23270
  "idle",
@@ -23072,12 +23353,35 @@ var ServerRollbackInfoSchema = object({
23072
23353
  var ServerPackageStatusSchema = object({
23073
23354
  /** Root package name (`@camstack/server` on the hub). */
23074
23355
  packageName: string(),
23075
- /** Version of the code the running process ACTUALLY loaded. */
23356
+ /**
23357
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
23358
+ * package.json of the copy the code was loaded from, and never re-read. A
23359
+ * copy replaced on disk under a live process must not move this number;
23360
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
23361
+ * was a lazy disk read that would have followed such a swap silently.
23362
+ */
23076
23363
  runningVersion: string().nullable(),
23077
23364
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
23078
23365
  nodeRuntimeVersion: string().nullable(),
23079
23366
  /** Active data-dir root version; null when booted from seed/workspace. */
23080
23367
  activeVersion: string().nullable(),
23368
+ /**
23369
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
23370
+ * what the next boot would load if nothing is staged. Normally equal to
23371
+ * `runningVersion`; differs only when the copy on disk changed under the
23372
+ * live process. `null` when there is no data-root copy (baked seed or
23373
+ * workspace boot). Optional for version skew: an older provider omits it.
23374
+ */
23375
+ installedVersion: string().nullable().optional(),
23376
+ /**
23377
+ * True when the code that would run after a restart is NOT the code running
23378
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
23379
+ * from `runningVersion`. Derived on every read, never stored. This is the
23380
+ * one field that lets an operator or a script say "1.2.353 is installed and
23381
+ * will run after a restart" instead of reading it off a number that means
23382
+ * "installed". Optional for version skew.
23383
+ */
23384
+ restartRequired: boolean().optional(),
23081
23385
  /** N-1 version kept for rollback; null when no previous version exists. */
23082
23386
  previousVersion: string().nullable(),
23083
23387
  /** Version of the immutable baked seed closure (image fallback). */
@@ -23112,19 +23416,71 @@ var ServerPackageStatusSchema = object({
23112
23416
  });
23113
23417
  var ServerUpdateCheckResultSchema = object({
23114
23418
  packageName: string(),
23419
+ /** Same meaning as on the status payload: what THIS process loaded. */
23115
23420
  runningVersion: string().nullable(),
23116
23421
  latestVersion: string().nullable(),
23422
+ /**
23423
+ * `latestVersion` is newer than `runningVersion` AND is not the version
23424
+ * already staged on this node. A staged-and-waiting version is not "an
23425
+ * update available" — applying it again is refused (`already-staged`), and
23426
+ * a button that announced "updating…" on this boolean then got refused
23427
+ * would be lying. It is reported through `pendingVersion` instead.
23428
+ */
23117
23429
  updateAvailable: boolean(),
23118
23430
  checkedAtMs: number(),
23119
23431
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
23120
- error: string().nullable()
23432
+ error: string().nullable(),
23433
+ /**
23434
+ * Version staged on this node and awaiting its restart, when one is. Before
23435
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
23436
+ * node whose staged closure had never booted, and the caller had no way to
23437
+ * see the difference. Optional for version skew.
23438
+ */
23439
+ pendingVersion: string().nullable().optional(),
23440
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
23441
+ restartRequired: boolean().optional()
23121
23442
  });
23443
+ /**
23444
+ * What an action DID — the discriminant `accepted` cannot carry. Before
23445
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
23446
+ * where nothing was wrong (already current, already staged) and for a staging
23447
+ * install that died on `npm error code ECONNRESET`; the only difference was
23448
+ * prose in `message`, and an operator restarted a container on the strength of
23449
+ * it, booting the previous version.
23450
+ *
23451
+ * - `staged` — the closure is installed and validated; the node
23452
+ * restarts to apply it (`restarting:true`). NOTHING
23453
+ * runs the new code until that restart completes.
23454
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
23455
+ * - `already-current` — the target IS the running version; nothing done.
23456
+ * - `refused` — dropped by a gate; `reason` names which.
23457
+ * - `failed` — attempted and did not land; `error` says why.
23458
+ */
23459
+ var ServerUpdateOutcomeSchema = _enum([
23460
+ "staged",
23461
+ "restart-scheduled",
23462
+ "already-current",
23463
+ "refused",
23464
+ "failed"
23465
+ ]);
23122
23466
  var ServerUpdateActionResultSchema = object({
23123
23467
  accepted: boolean(),
23124
23468
  targetVersion: string().nullable(),
23125
23469
  /** True when a graceful restart was scheduled to apply the change. */
23126
23470
  restarting: boolean(),
23127
- message: string()
23471
+ message: string(),
23472
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
23473
+ outcome: ServerUpdateOutcomeSchema.optional(),
23474
+ /**
23475
+ * Stable, greppable cause for `refused` / `failed` (the engine's
23476
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
23477
+ */
23478
+ reason: string().nullable().optional(),
23479
+ /**
23480
+ * The failure itself, verbatim enough to act on (the npm output for a
23481
+ * staging install). Non-null only for `failed`.
23482
+ */
23483
+ error: string().nullable().optional()
23128
23484
  });
23129
23485
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
23130
23486
  kind: "mutation",
@@ -24337,6 +24693,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
24337
24693
  kind: "mutation",
24338
24694
  auth: "admin"
24339
24695
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
24696
+ _enum([
24697
+ "queue-full",
24698
+ "camera-backoff",
24699
+ "sleeping",
24700
+ "camera-refused",
24701
+ "no-keyframe",
24702
+ "no-catalog-row",
24703
+ "unsupported",
24704
+ "unknown-device",
24705
+ "uid-missing"
24706
+ ]);
24707
+ _enum(["deferred", "final"]);
24340
24708
  var ClipSchema = object({
24341
24709
  /** Opaque, provider-namespaced id. The default provider encodes the time
24342
24710
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -24411,29 +24779,455 @@ var ClipSchema = object({
24411
24779
  holes: array(object({
24412
24780
  startMs: number(),
24413
24781
  endMs: number()
24414
- })).optional()
24782
+ })).optional(),
24783
+ /**
24784
+ * A URL that MAY produce this clip's still, asked only for rows actually on
24785
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
24786
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
24787
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
24788
+ * — a surface latches that refusal to the instant rather than retrying.
24789
+ *
24790
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
24791
+ */
24792
+ thumbnailMint: string().optional(),
24793
+ /**
24794
+ * Why no still will be produced for this clip RIGHT NOW — set when the
24795
+ * provider already knows, so the surface draws the glyph and the reason
24796
+ * instead of firing a mint that cannot succeed.
24797
+ *
24798
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
24799
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
24800
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
24801
+ * inside no file is the measured case).
24802
+ * `unsupported` — this source cannot mint stills at all.
24803
+ */
24804
+ thumbnailUnavailable: object({ reason: _enum([
24805
+ "sleeping",
24806
+ "camera-refused",
24807
+ "no-keyframe",
24808
+ "unsupported"
24809
+ ]) }).optional(),
24810
+ /**
24811
+ * When the catalog this row came from was last CONFIRMED against the device.
24812
+ * Absent means "this row was read live". A persisted catalog served while a
24813
+ * camera sleeps carries the age it really has — a cached list is never drawn
24814
+ * as current (D549 13).
24815
+ */
24816
+ catalogAsOf: number().optional(),
24817
+ /**
24818
+ * The camera's OWN type strings for this clip, all of them, unmapped
24819
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
24820
+ * because a firmware inventing a type must not vanish: the mapping into our
24821
+ * filter vocabulary is lossy on purpose and this is the lossless copy
24822
+ * (D549 20).
24823
+ */
24824
+ nativeTypes: array(string()).optional(),
24825
+ /**
24826
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
24827
+ * travels BESIDE a class, never instead of one, and the class filter ignores
24828
+ * it: "a person crossed a line" is still a person (D549 20).
24829
+ */
24830
+ behaviours: array(string()).optional(),
24831
+ /**
24832
+ * The same recording as two files — the sub twin (what the row and its
24833
+ * thumbnail are) and its main twin, matched at listing time so a quality
24834
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
24835
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
24836
+ */
24837
+ streams: object({
24838
+ sub: object({
24839
+ id: string(),
24840
+ bytes: number().optional()
24841
+ }).optional(),
24842
+ main: object({
24843
+ id: string(),
24844
+ bytes: number().optional()
24845
+ }).optional()
24846
+ }).optional(),
24847
+ /** The camera says it holds a sub-stream copy of this recording. */
24848
+ supportSub: boolean().optional(),
24849
+ /**
24850
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
24851
+ * IS a file is playable, and only a source that lists EVENTS can produce a
24852
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
24853
+ * fell inside no file). Such a row is shown, never dropped and never offered
24854
+ * as playable-then-failing (D549 19).
24855
+ */
24856
+ playable: boolean().optional(),
24857
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
24858
+ unplayableReason: string().optional(),
24859
+ /**
24860
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
24861
+ * is not its end.
24862
+ *
24863
+ * Absent — the common case — means the row is closed and its `endMs` is the
24864
+ * end of the recording. Present and `true` means the source told us the file
24865
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
24866
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
24867
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
24868
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
24869
+ * on growing. A surface that drew 29 s there was lying about a clip that
24870
+ * plays for minutes, on the one row the operator looks at first.
24871
+ *
24872
+ * `endMs` is deliberately still a number: it is the best bound anything has
24873
+ * for a mint window or a byte fetch, and every consumer already requires it.
24874
+ * This flag says what it is WORTH, and a duration is not drawn from it.
24875
+ */
24876
+ inProgress: boolean().optional()
24415
24877
  });
24416
24878
  var ClipPlaybackSchema = object({
24417
- /** HLS master URL through the hub data-plane (Range + token in path). */
24879
+ /**
24880
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
24881
+ * says what kind — an HLS master playlist for a recording-derived clip, a
24882
+ * progressive MP4 for a native file the vendor addon muxed and serves with
24883
+ * `Range`. The URL is NOT a credential: both planes are registered
24884
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
24885
+ * the session cookie, so nothing is appended to it (D549 22).
24886
+ */
24418
24887
  playbackUrl: string(),
24888
+ /**
24889
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
24890
+ * existing consumer already assumes. A player that branches on this is the
24891
+ * one change a native clip needs; a player that ignores it will hand an MP4
24892
+ * to hls.js and fail parsing it as a manifest.
24893
+ */
24894
+ format: _enum(["hls", "mp4"]).optional(),
24895
+ /**
24896
+ * Which twin was actually served. A clip that holds only one stream answers
24897
+ * with the one it has, and the surface SAYS so — a missing main twin is
24898
+ * never served silently as if it were the asked-for quality (D549 15).
24899
+ */
24900
+ served: CamProfileSchema.optional(),
24419
24901
  /** Optional LAN/remote alternates for the same clip. */
24420
24902
  playbackEndpoints: array(string()).optional(),
24421
24903
  token: string().optional()
24422
24904
  });
24905
+ /**
24906
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
24907
+ *
24908
+ * `ok` is the only state whose clip list may be read as complete. The other
24909
+ * three exist because an empty list from a sleeping camera reads as "this
24910
+ * camera has no recordings", which is the defect this whole line of work is
24911
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
24912
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
24913
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
24914
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
24915
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
24916
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
24917
+ * files on both streams and both filters, with 177 GB free). That last one is
24918
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
24919
+ * which days it asked for and got nothing — "no clips" would be a lie about a
24920
+ * card that is full of them.
24921
+ */
24922
+ /**
24923
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
24924
+ *
24925
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
24926
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
24927
+ * `force` flag, and nothing else… a background caller must never set it…
24928
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
24929
+ * prefetch never set it; a surface sets it only behind the same confirm the
24930
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
24931
+ * exactly as that gesture is.
24932
+ *
24933
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
24934
+ * (D549) — a read that opened a session and then checked would have woken the
24935
+ * camera to find out it was not allowed to.
24936
+ *
24937
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
24938
+ * time the camera is awake for its own reasons — is deliberately not a member:
24939
+ * it was proposed, it is free on the battery, and the operator declined it on
24940
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
24941
+ * D558 "Considered and not taken").
24942
+ */
24943
+ var ClipWakeSchema = _enum(["authorised"]);
24944
+ /**
24945
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
24946
+ *
24947
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
24948
+ * and `served` says which twin the caller actually got.
24949
+ */
24950
+ var ClipBytesSchema = object({
24951
+ base64: string(),
24952
+ contentType: string(),
24953
+ /** Suggested filename, extension included. */
24954
+ name: string(),
24955
+ bytes: number().int().nonnegative(),
24956
+ /**
24957
+ * Which twin was actually served — the same contract
24958
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
24959
+ * export record persists it: a row read a week later must say the same thing
24960
+ * the panel said at the moment of the tap. A missing main twin is never
24961
+ * served silently as if it were the asked-for quality (D549 15).
24962
+ */
24963
+ served: CamProfileSchema,
24964
+ /**
24965
+ * The DECODED duration of the delivered file, when the fetch measured one.
24966
+ *
24967
+ * A clip fetch verifies its own completion against the catalog row and
24968
+ * retries a materially short pass (D568); this is that measurement, carried
24969
+ * so a consumer can say the same thing rather than re-deriving it. Absent
24970
+ * when the producer did not measure — never zero, which would say the file
24971
+ * is empty.
24972
+ */
24973
+ durationMs: number().positive().optional()
24974
+ });
24975
+ /**
24976
+ * Where a clip's STREAM can be dialled (D597) — the answer to
24977
+ * {@link videoclipsCapability.methods.dialClipStream}.
24978
+ *
24979
+ * `url` is plain `http://` on the provider's own token-gated listener, one
24980
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
24981
+ * path: that plane takes only a user credential, which an addon does not
24982
+ * hold, and the framework offers no addon→addon byte transport — the one
24983
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
24984
+ * a consumer dials directly, and this is that precedent one addon over.
24985
+ */
24986
+ /**
24987
+ * The audio a clip stream can be asked for, and the audio it carries.
24988
+ *
24989
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
24990
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
24991
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
24992
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
24993
+ * negotiates — and `none` is a stream asked for, or served, without sound.
24994
+ */
24995
+ var ClipStreamAudioSchema = _enum([
24996
+ "opus",
24997
+ "pcmu",
24998
+ "none"
24999
+ ]);
25000
+ /**
25001
+ * Why a stream that was ASKED for sound is not carrying it (D600).
25002
+ *
25003
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
25004
+ * means the ask was honoured, or there was no ask.
25005
+ *
25006
+ * - `not-requested` — the caller asked for `none`.
25007
+ * - `unsupported-ask` — the provider cannot produce that codec.
25008
+ *
25009
+ * Whether the RECORDING has sound at all is not knowable here: the camera
25010
+ * declares no audio track before its first frame arrives. That answer is in
25011
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
25012
+ * there. A dial that says `opus` promises the codec, never the content.
25013
+ */
25014
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
25015
+ var ClipStreamDialSchema = object({
25016
+ url: string().min(1),
25017
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
25018
+ expiresAtMs: number(),
25019
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
25020
+ transport: literal("forward-only"),
25021
+ /**
25022
+ * Which twin the stream ACTUALLY carries — not what was asked for.
25023
+ *
25024
+ * The stream serves every profile (D599), and a recording whose main twin
25025
+ * does not exist is streamed as its sub copy, which this field reports as
25026
+ * `low`. That is the whole reason the dial no longer refuses `high`:
25027
+ * stating what was served is what makes accepting honest, where a silent
25028
+ * substitution would be the 33x lie D576 forbids. Stated rather than
25029
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
25030
+ */
25031
+ served: CamProfileSchema,
25032
+ /**
25033
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
25034
+ * sound, and the same reasoning (D599, D600).
25035
+ *
25036
+ * The broker asks for the codec its WebRTC session negotiated, and the
25037
+ * provider either muxes that codec or carries none. Stating it is what
25038
+ * lets the broker wire the matching sink before the first byte, and what
25039
+ * keeps a session from being handed a codec it cannot send — the audio
25040
+ * shape of the substitution `served` exists to prevent.
25041
+ *
25042
+ * It is a promise about the CODEC, not about the content: a recording with
25043
+ * no microphone track answers `opus` here and then arrives with no audio
25044
+ * track at all. That second answer is in the stream, and the broker reads
25045
+ * it from the `moov`.
25046
+ */
25047
+ servedAudio: ClipStreamAudioSchema,
25048
+ /** Why `servedAudio` is `none` although sound was asked for. */
25049
+ audioReason: ClipStreamAudioReasonSchema.optional()
25050
+ });
25051
+ var ClipSourceAvailabilitySchema = object({
25052
+ state: _enum([
25053
+ "ok",
25054
+ "sleeping",
25055
+ "unreachable",
25056
+ "no-storage",
25057
+ "index-empty"
25058
+ ]),
25059
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
25060
+ reason: string().optional(),
25061
+ /** When this source's catalog was last CONFIRMED. A cached list is never
25062
+ * drawn as current: the surface shows the age whenever it is older than the
25063
+ * refresh interval. */
25064
+ catalogAsOf: number().optional()
25065
+ });
25066
+ /**
25067
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
25068
+ * every clip id from it is prefixed with (`analytics`,
25069
+ * `native:reolink:onboard`, `hksv`, …).
25070
+ *
25071
+ * A provider lists the sources IT serves for that device, and answers for each
25072
+ * of them whether it can answer at all. A provider with nothing to offer on a
25073
+ * camera returns `[]` — it is not that camera's business.
25074
+ */
25075
+ var ClipSourceSchema = object({
25076
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
25077
+ * of every clip id it mints. `getClipPlayback` routes on it. */
25078
+ source: string(),
25079
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
25080
+ label: string(),
25081
+ /**
25082
+ * The addon that SERVES this row.
25083
+ *
25084
+ * A surface cannot otherwise resolve a source to who answers it, and the
25085
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
25086
+ * widget — is a second authority on provider identity living in the one
25087
+ * package with no business knowing it, wrong the day a third source appears
25088
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
25089
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
25090
+ * served a hub child two of them until the two views were measured to be one
25091
+ * store read twice (D555) and then read ONE way (D565).
25092
+ *
25093
+ * Optional for version skew only. The collection dispatcher stamps it from
25094
+ * the registry, so a row that travelled through the fan-out carries the
25095
+ * authoritative id whatever the provider filled in.
25096
+ */
25097
+ addonId: string().optional(),
25098
+ availability: ClipSourceAvailabilitySchema
25099
+ });
24423
25100
  DeviceType.Camera, method(object({
24424
25101
  deviceId: number(),
24425
25102
  since: number(),
24426
25103
  until: number(),
24427
- limit: number().int().positive().optional()
25104
+ limit: number().int().positive().optional(),
25105
+ /**
25106
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25107
+ * carries, never a source id and never a list. **Required** (D554 amended).
25108
+ *
25109
+ * A provider the device is not bound to is refused by name rather than
25110
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25111
+ *
25112
+ * It was optional, documented as "absent means the device's BOUND
25113
+ * provider, which is CamStack on every camera". No code implemented
25114
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25115
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25116
+ * answered SIX rows, three from each source, merged newest-first:
25117
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25118
+ * so absence bought the union this method exists not to be, and
25119
+ * `limit` meant `limit × sources`.
25120
+ *
25121
+ * There is nothing to restore the default to. `getBindings` answers a
25122
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25123
+ * `setWrapperActive` — the singleton authority that could name one —
25124
+ * throws for a collection cap by design. Naming CamStack here instead
25125
+ * would privilege one addon by id inside a surface whose premise is
25126
+ * that sources are peers, and would be wrong on the first camera with
25127
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25128
+ * generated types make it unomittable rather than merely discouraged.
25129
+ *
25130
+ * The default belongs to the SURFACE, which has the `listSources` rows
25131
+ * and can say which one it picked (D569 § 1–3; the viewer already
25132
+ * always sends this).
25133
+ *
25134
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
25135
+ * assumed the answer was a fan-out over everything a camera has. The
25136
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25137
+ * solo"* — so the list is asked of one provider at a time and there is
25138
+ * nothing to filter out of it.
25139
+ */
25140
+ provider: string().min(1)
24428
25141
  }), array(ClipSchema).readonly(), {
24429
25142
  kind: "query",
24430
- auth: "admin"
25143
+ auth: "protected"
25144
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
25145
+ kind: "query",
25146
+ auth: "protected"
24431
25147
  }), method(object({
24432
25148
  deviceId: number(),
24433
- clipId: string()
25149
+ clipId: string(),
25150
+ /**
25151
+ * Which twin to serve, on the ONE quality scale the system already has
25152
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
25153
+ * twin; both ids are already on the row so this never re-searches the
25154
+ * camera. Absent means the provider's own default (the sub file, which
25155
+ * every source is measured to hold).
25156
+ *
25157
+ * `auto` is deliberately NOT accepted here: a stored file has no
25158
+ * broker session, so the adaptive tier cannot be resolved for it. The
25159
+ * viewer resolves `auto` to a profile the same way live does, before
25160
+ * it calls (D549 15).
25161
+ */
25162
+ profile: CamProfileSchema.optional()
24434
25163
  }), ClipPlaybackSchema, {
24435
25164
  kind: "query",
24436
- auth: "admin"
25165
+ auth: "protected"
25166
+ }), optionalMethod(object({
25167
+ deviceId: number(),
25168
+ clipId: string().min(1),
25169
+ /**
25170
+ * WHICH provider holds the bytes — the `addonId` a
25171
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
25172
+ * `listClips` states one method above and for a second one this method
25173
+ * learned the hard way.
25174
+ *
25175
+ * The caller is the stream broker, and the broker lives in
25176
+ * `addon-pipeline` — the same addon that registers the RECORDER's
25177
+ * videoclips provider. With no provider named, the in-process
25178
+ * resolution handed it its own sibling, which implements this method
25179
+ * solely to explain that an analytics visit has no file behind it. So
25180
+ * every onboard clip answered with that sentence, a flash of video and
25181
+ * nothing more, while the SAME call from outside the addon reached the
25182
+ * right provider and returned the bytes (measured on 3628,
25183
+ * 2026-09-21).
25184
+ */
25185
+ provider: string().min(1),
25186
+ /**
25187
+ * Which twin to fetch, on the mapping D549 15 already fixed:
25188
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
25189
+ * accepted here for the same reason it is not accepted by
25190
+ * `getClipPlayback` — a stored file has no broker session, so the
25191
+ * adaptive tier cannot be resolved for it.
25192
+ */
25193
+ profile: CamProfileSchema.optional(),
25194
+ /**
25195
+ * The CALLER's byte bound, so an over-size clip is refused before it is
25196
+ * read and encoded rather than after. Capped by
25197
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
25198
+ * that ceiling.
25199
+ */
25200
+ maxBytes: number().int().positive().optional(),
25201
+ /**
25202
+ * The operator's authorisation to wake a sleeping camera for this
25203
+ * read. Absent — the default — means a sleeping standalone battery
25204
+ * camera is REFUSED by name, before any session is opened.
25205
+ */
25206
+ wake: ClipWakeSchema.optional()
25207
+ }), ClipBytesSchema, {
25208
+ kind: "query",
25209
+ auth: "protected"
25210
+ }), optionalMethod(object({
25211
+ deviceId: number(),
25212
+ clipId: string().min(1),
25213
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
25214
+ provider: string().min(1),
25215
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
25216
+ profile: CamProfileSchema.optional(),
25217
+ /**
25218
+ * Which audio codec the caller's session negotiated, so the provider
25219
+ * can mux it (D600). The ask travels on the DIAL because the mux is
25220
+ * spawned once, before the first byte, and cannot gain a track later.
25221
+ *
25222
+ * ABSENT IS `none`, never a guess: a caller that does not state an
25223
+ * ask is served the video-only stream that shipped first, so a broker
25224
+ * older than this field keeps working unchanged (D382 — an unstated
25225
+ * ask is zero).
25226
+ */
25227
+ audio: ClipStreamAudioSchema.optional()
25228
+ }), ClipStreamDialSchema, {
25229
+ kind: "query",
25230
+ auth: "protected"
24437
25231
  });
24438
25232
  /**
24439
25233
  * Optional client-side hints sent at session creation to help the provider
@@ -24505,6 +25299,104 @@ var WebrtcStreamChoiceSchema = object({
24505
25299
  /** Bitrate in kbps (null for adaptive). */
24506
25300
  bitrateKbps: number().nullable()
24507
25301
  });
25302
+ /**
25303
+ * The last move the adaptive ladder made on this session, and the numbers it
25304
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
25305
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
25306
+ *
25307
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
25308
+ * no window to report, which is NOT a link with zero loss.
25309
+ */
25310
+ var WebrtcLadderDecisionSchema = object({
25311
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
25312
+ * stopped moving; `healthy` — a promotion. */
25313
+ cause: _enum([
25314
+ "loss",
25315
+ "stall",
25316
+ "healthy"
25317
+ ]),
25318
+ /** The rung the ladder moved TO. Null for a move that changed no rung
25319
+ * (an MTU reduction, a forced transcode-down at the bottom). */
25320
+ tier: CamProfileSchema.nullable(),
25321
+ /** Whether that move forced the egress transcode-down rung. */
25322
+ forcedTranscodeDown: boolean(),
25323
+ /** Loss ratio over the tracker's window at the moment of the decision. */
25324
+ lossRatio: number().nullable(),
25325
+ /** The client's reported downlink at that moment, in kbps. */
25326
+ clientKbps: number().nullable(),
25327
+ /** How long the stall detector had been seeing nothing arrive. */
25328
+ stalledForMs: number().nullable(),
25329
+ /** Whether the broker WAS sending while that was measured — the half that
25330
+ * separates "the camera stopped" from "the link stopped". */
25331
+ serverSending: boolean().nullable(),
25332
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25333
+ * must not read like one from this second. */
25334
+ at: number()
25335
+ });
25336
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25337
+ var WebrtcTierFailureSchema = object({
25338
+ tier: CamProfileSchema,
25339
+ failures: number().int().nonnegative()
25340
+ });
25341
+ /**
25342
+ * What the SERVER decided for one WebRTC session — the half the client cannot
25343
+ * measure. See `getSessionDebug`.
25344
+ */
25345
+ var WebrtcSessionDebugSchema = object({
25346
+ sessionId: string(),
25347
+ /** Whether the session still exists here. Everything below is null when
25348
+ * it does not — a dead session has no measurements, not zero ones. */
25349
+ alive: boolean(),
25350
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
25351
+ observedAt: number(),
25352
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
25353
+ brokerId: string().nullable(),
25354
+ camStreamId: string().nullable(),
25355
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
25356
+ * another stream); `native` = the camera's own encode. */
25357
+ sourceKind: _enum([
25358
+ "native",
25359
+ "derived",
25360
+ "unknown"
25361
+ ]),
25362
+ /** Whether the ladder is running at all (an `adaptive` target). */
25363
+ adaptive: boolean(),
25364
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
25365
+ * is bound to no profile slot (a manually-activated stream). */
25366
+ servedTier: CamProfileSchema.nullable(),
25367
+ /** Which node ingests the source. Null when unresolved. */
25368
+ ingestNodeId: string().nullable(),
25369
+ /** Whether that node is a remote one (the media crosses a node boundary
25370
+ * before it reaches this session). Null when unresolved. */
25371
+ ingestIsRemote: boolean().nullable(),
25372
+ /** Codec off the source, and the one negotiated with this client. */
25373
+ sourceCodec: string().nullable(),
25374
+ negotiatedCodec: string().nullable(),
25375
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
25376
+ * source bytes untouched. */
25377
+ egressLeg: _enum([
25378
+ "copy",
25379
+ "transcode",
25380
+ "unknown"
25381
+ ]),
25382
+ /** What the broker itself reports about the rung it is serving. */
25383
+ brokerStatus: string().nullable(),
25384
+ brokerCodec: string().nullable(),
25385
+ brokerBitrateKbps: number().nullable(),
25386
+ brokerInputFps: number().nullable(),
25387
+ brokerUptimeMs: number().nullable(),
25388
+ /** RTP packets this session has sent — the server's own "am I sending?". */
25389
+ rtpPacketsSent: number().nullable(),
25390
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
25391
+ * is suspended there, so a frozen picture in `recorded` mode means
25392
+ * something entirely different. */
25393
+ trackMode: _enum(["live", "recorded"]).nullable(),
25394
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
25395
+ tierFailures: array(WebrtcTierFailureSchema),
25396
+ /** Successful promotions in this viewer session (drives the progressive
25397
+ * healthy timer). Null when no ladder is running. */
25398
+ upgradeCount: number().nullable()
25399
+ });
24508
25400
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
24509
25401
  deviceId: number().int().nonnegative(),
24510
25402
  target: WebrtcStreamTargetSchema,
@@ -24669,7 +25561,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24669
25561
  * the same answer as "it ended": either way nothing is holding it up.
24670
25562
  */
24671
25563
  alive: boolean()
24672
- }));
25564
+ })), method(object({
25565
+ deviceId: number().int().nonnegative(),
25566
+ sessionId: string()
25567
+ }), WebrtcSessionDebugSchema, { kind: "query" });
24673
25568
  object({
24674
25569
  /** All accessory children of the parent. */
24675
25570
  childDeviceIds: array(number()).readonly(),
@@ -26495,6 +27390,346 @@ var dayNightCapability = {
26495
27390
  volatileStateFields: ["lastFetchedAt"]
26496
27391
  };
26497
27392
  /**
27393
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
27394
+ * writes to the CAMERA's own card, on the camera's own schedule.
27395
+ *
27396
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
27397
+ * footage ledger, our storage locations, our retention. This one has a
27398
+ * different authority — the camera's firmware — and per D62 it stores
27399
+ * nothing of its own. Every value here is read from the camera and every
27400
+ * write goes back to the camera; there is no CamStack-side mirror that
27401
+ * could disagree with the device.
27402
+ *
27403
+ * ## One shape, two firmwares
27404
+ *
27405
+ * Measured 2026-09-22 against the live fleet:
27406
+ *
27407
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
27408
+ * | --- | --- | --- |
27409
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
27410
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
27411
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
27412
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
27413
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
27414
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
27415
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
27416
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
27417
+ *
27418
+ * The two schedule models look different and are the same thing in
27419
+ * different coordinates: both answer "for this trigger, during which
27420
+ * weekly windows does the camera record". {@link RecordWindow} is that
27421
+ * question in one shape — Hikvision's ranges map straight onto it,
27422
+ * Reolink's mask expands into hour-aligned windows.
27423
+ *
27424
+ * ## Union, not intersection
27425
+ *
27426
+ * **The same fields exist on every camera.** What differs per device is
27427
+ * which VALUES that device accepts, and that is what {@link
27428
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
27429
+ * per field plus the schedule's own limits. A control a camera cannot
27430
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
27431
+ * dead: disabled must not look like broken.
27432
+ *
27433
+ * ## Refusal by name
27434
+ *
27435
+ * A write a camera cannot honour is refused with a sentence the operator
27436
+ * can read — never accepted and dropped. Both providers refuse through
27437
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
27438
+ * one test, not two hand-written vendor opinions.
27439
+ *
27440
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
27441
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
27442
+ * injected from `status`) reports the live values, and a single
27443
+ * `setSettings` mutation applies a partial change. No hand-written
27444
+ * settings-contribution methods.
27445
+ */
27446
+ /**
27447
+ * What makes the camera start recording during a window.
27448
+ *
27449
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
27450
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
27451
+ * object-class triggers are Reolink-only today and the smart-event ones
27452
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
27453
+ * firmwares measured — a camera that cannot record on a trigger simply
27454
+ * does not list it in `options.schedule.triggers`, and a window naming
27455
+ * it is REFUSED, not dropped.
27456
+ */
27457
+ var RecordTriggerSchema = _enum([
27458
+ "continuous",
27459
+ "motion",
27460
+ "person",
27461
+ "vehicle",
27462
+ "animal",
27463
+ "lineCrossing",
27464
+ "intrusion",
27465
+ "loitering",
27466
+ "alarmInput"
27467
+ ]);
27468
+ /**
27469
+ * One weekly recording window: "on `day`, from `startMinute` to
27470
+ * `endMinute`, record on `trigger`".
27471
+ *
27472
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
27473
+ * both firmwares enumerate). Minutes are local camera time since
27474
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
27475
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
27476
+ * collapsing it to 0 would turn a whole-day window into an empty one.
27477
+ */
27478
+ var RecordWindowSchema = object({
27479
+ trigger: RecordTriggerSchema,
27480
+ day: number().int().min(0).max(6),
27481
+ startMinute: number().int().min(0).max(1439),
27482
+ endMinute: number().int().min(1).max(1440)
27483
+ });
27484
+ /** Status of one physical volume, as the camera itself describes it. */
27485
+ var OnboardStorageVolumeSchema = object({
27486
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
27487
+ id: string(),
27488
+ /** The camera's own name for it, when it gives one (`hddName`). */
27489
+ label: string().optional(),
27490
+ status: _enum([
27491
+ "ok",
27492
+ "unformatted",
27493
+ "error",
27494
+ "offline",
27495
+ "unknown"
27496
+ ]),
27497
+ /**
27498
+ * Total size in MB, or **null when the camera did not say**.
27499
+ *
27500
+ * Never 0 for an unreadable value: a measurement that failed is not a
27501
+ * measurement (D393), and a card whose size is unknown must not be
27502
+ * rendered as a card of size zero.
27503
+ */
27504
+ capacityMb: number().nullable(),
27505
+ /**
27506
+ * Free space in MB, or null when unknown.
27507
+ *
27508
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
27509
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
27510
+ * card converges on once it has wrapped. At loop steady state the
27511
+ * number is identical whether the camera recorded yesterday or stopped
27512
+ * a month ago.
27513
+ */
27514
+ freeMb: number().nullable(),
27515
+ /** True when the camera reports the volume writable (`property` RW). */
27516
+ writable: boolean().optional()
27517
+ });
27518
+ /**
27519
+ * What the camera is doing with its own storage, right now.
27520
+ *
27521
+ * Every scalar is nullable and **null means the camera did not answer**,
27522
+ * never a default. A form that seeds `0` from an unanswered read invites
27523
+ * the operator to save that 0 back onto the camera.
27524
+ */
27525
+ var RecordingOnboardStatusSchema = object({
27526
+ storage: discriminatedUnion("kind", [
27527
+ object({
27528
+ kind: literal("present"),
27529
+ volumes: array(OnboardStorageVolumeSchema)
27530
+ }),
27531
+ object({
27532
+ kind: literal("absent"),
27533
+ reason: string()
27534
+ }),
27535
+ object({
27536
+ kind: literal("unknown"),
27537
+ reason: string()
27538
+ })
27539
+ ]),
27540
+ tracks: array(object({
27541
+ id: string(),
27542
+ enabled: boolean(),
27543
+ isVideo: boolean(),
27544
+ /** From the camera's own track description. Null when it does not say. */
27545
+ codec: string().nullable(),
27546
+ resolution: string().nullable(),
27547
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
27548
+ overwriteWhenFull: boolean().nullable()
27549
+ })),
27550
+ /**
27551
+ * The track the write path targets — the enabled VIDEO one. Null when
27552
+ * no track could be identified, which is itself a refusal reason.
27553
+ */
27554
+ primaryTrackId: string().nullable(),
27555
+ /** Master "record to the card at all" switch. */
27556
+ enabled: boolean().nullable(),
27557
+ overwriteWhenFull: boolean().nullable(),
27558
+ preRecordSec: number().nullable(),
27559
+ postRecordSec: number().nullable(),
27560
+ /** Length of one recorded file, in minutes. */
27561
+ segmentMinutes: number().nullable(),
27562
+ /** The primary track's weekly windows, flattened. */
27563
+ windows: array(RecordWindowSchema),
27564
+ /**
27565
+ * How many windows the camera described that CamStack could NOT read —
27566
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
27567
+ * name.
27568
+ *
27569
+ * A dropped window is work the reader threw away, and a schedule that
27570
+ * silently shows fewer rows than the camera holds is how an operator
27571
+ * saves back a schedule shorter than the one they were looking at
27572
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
27573
+ * that replaces it would delete what was not shown — which is why a
27574
+ * provider reporting a non-zero count also reports the schedule as not
27575
+ * writable.
27576
+ */
27577
+ unreadableWindows: number(),
27578
+ /**
27579
+ * The camera is scheduled to record and has NO usable storage.
27580
+ *
27581
+ * A first-class fact because it is the fleet's most common silent
27582
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
27583
+ * to a card that is not there. Neither the schedule nor the storage
27584
+ * read says anything wrong on its own; only the pair does.
27585
+ */
27586
+ recordingToNowhere: boolean(),
27587
+ lastFetchedAt: number()
27588
+ });
27589
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
27590
+ var RangeSchema = object({
27591
+ min: number(),
27592
+ max: number(),
27593
+ step: number()
27594
+ });
27595
+ /**
27596
+ * The values a camera actually takes for a numeric field, when they are a SET
27597
+ * rather than a range.
27598
+ *
27599
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
27600
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
27601
+ *
27602
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
27603
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
27604
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
27605
+ *
27606
+ * Neither is expressible as a step: the first has a sentinel two billion away
27607
+ * from its neighbours, the second doubles and then jumps. A range that tried
27608
+ * would forbid values the camera takes AND permit values it silently replaces
27609
+ * with 5 — wrong in both directions at once.
27610
+ *
27611
+ * `sentinel` names the member that is not a duration, so a surface can render
27612
+ * "no limit" instead of `2147483647` seconds.
27613
+ */
27614
+ var AllowedValuesSchema = object({
27615
+ values: array(number()).min(1),
27616
+ sentinel: object({
27617
+ value: number(),
27618
+ meaning: _enum(["no-limit", "disabled"])
27619
+ }).optional()
27620
+ });
27621
+ /**
27622
+ * Per-field availability on ONE camera.
27623
+ *
27624
+ * The field exists on every camera — this says whether this one can be
27625
+ * read and whether it can be written, and `reason` says why not when
27626
+ * either is false. The UI renders the control DISABLED with the reason
27627
+ * rather than hiding it, so a limitation is legible instead of looking
27628
+ * like a missing feature.
27629
+ */
27630
+ var OnboardFieldSupportSchema = object({
27631
+ readable: boolean(),
27632
+ writable: boolean(),
27633
+ /** Required whenever `readable` or `writable` is false. */
27634
+ reason: string().optional()
27635
+ });
27636
+ /** What this camera's schedule model can express. */
27637
+ var OnboardScheduleSupportSchema = object({
27638
+ support: OnboardFieldSupportSchema,
27639
+ /**
27640
+ * The smallest time step the camera can express, in minutes.
27641
+ *
27642
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
27643
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
27644
+ * window whose edges are not a multiple of this is REFUSED rather than
27645
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
27646
+ * and nothing says so.
27647
+ */
27648
+ granularityMinutes: number(),
27649
+ /** Triggers this camera can record on. A window naming another is refused. */
27650
+ triggers: array(RecordTriggerSchema),
27651
+ /**
27652
+ * False when the camera stores ONE trigger per time range, so two
27653
+ * windows overlapping on the same day cannot carry different triggers.
27654
+ * True on Reolink, whose mask is per-trigger and independent.
27655
+ */
27656
+ supportsOverlappingTriggers: boolean()
27657
+ });
27658
+ var RecordingOnboardOptionsSchema = object({
27659
+ enabled: OnboardFieldSupportSchema,
27660
+ overwriteWhenFull: OnboardFieldSupportSchema,
27661
+ preRecordSec: OnboardFieldSupportSchema,
27662
+ preRecordSecRange: RangeSchema.optional(),
27663
+ /** Preferred over the range when the camera takes a SET, not a span. */
27664
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
27665
+ postRecordSec: OnboardFieldSupportSchema,
27666
+ postRecordSecRange: RangeSchema.optional(),
27667
+ /** Preferred over the range when the camera takes a SET, not a span. */
27668
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
27669
+ segmentMinutes: OnboardFieldSupportSchema,
27670
+ segmentMinutesRange: RangeSchema.optional(),
27671
+ /** Preferred over the range when the camera takes a SET, not a span. */
27672
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
27673
+ schedule: OnboardScheduleSupportSchema
27674
+ });
27675
+ /**
27676
+ * A partial change. Every field optional.
27677
+ *
27678
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
27679
+ * silently ignore a field it cannot support — it refuses, by name,
27680
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
27681
+ * is the failure D62 exists to prevent: the operator believes the camera
27682
+ * is recording the way the form says, and it is not.
27683
+ */
27684
+ var RecordingOnboardPatchSchema = object({
27685
+ enabled: boolean().optional(),
27686
+ overwriteWhenFull: boolean().optional(),
27687
+ preRecordSec: number().optional(),
27688
+ postRecordSec: number().optional(),
27689
+ segmentMinutes: number().optional(),
27690
+ /** The complete new window set for the primary track — not a delta. */
27691
+ windows: array(RecordWindowSchema).optional()
27692
+ });
27693
+ var recordingOnboardCapability = {
27694
+ name: "recording-onboard",
27695
+ scope: "device",
27696
+ deviceNative: true,
27697
+ mode: "singleton",
27698
+ deviceTypes: [DeviceType.Camera],
27699
+ deviceConfig: { ui: {
27700
+ kind: "derived-form",
27701
+ builderId: "recording-onboard",
27702
+ tab: "recording"
27703
+ } },
27704
+ methods: {
27705
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
27706
+ setSettings: method(object({
27707
+ deviceId: number(),
27708
+ settings: RecordingOnboardPatchSchema
27709
+ }), _void(), {
27710
+ kind: "mutation",
27711
+ auth: "admin"
27712
+ })
27713
+ },
27714
+ status: {
27715
+ schema: RecordingOnboardStatusSchema,
27716
+ kind: "poll"
27717
+ },
27718
+ runtimeState: RecordingOnboardStatusSchema,
27719
+ /**
27720
+ * Runtime-state durability: **restored** — operator-set camera-side
27721
+ * recording config; mutation-driven, and the storage half is the last
27722
+ * thing the camera said about its own card.
27723
+ *
27724
+ * See `RuntimeStateDurability`. Enforced by
27725
+ * `scripts/check-runtime-state-durability.ts`.
27726
+ */
27727
+ durability: "restored",
27728
+ /** Clock fields: written, but excluded from the compare that decides
27729
+ * whether persisting is worth a SQLite commit. */
27730
+ volatileStateFields: ["lastFetchedAt"]
27731
+ };
27732
+ /**
26498
27733
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
26499
27734
  * for every device, regardless of provider — the kernel needs a uniform
26500
27735
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -29029,11 +30264,15 @@ var motionTriggerCapability = {
29029
30264
  *
29030
30265
  * Adding a fifth name to that list would have been the wrong fix twice over:
29031
30266
  * that page is per-camera DETECTION tuning, and a grid's geometry belongs
29032
- * beside PTZ and motion zones on the camera itself — which is exactly where the
29033
- * framework already puts a widget, when the widget is declared on a CAPABILITY.
29034
- * `deviceConfig.ui` is the mechanism (`motion-zones`, `ptz`, `ptz-autotrack`
29035
- * all use it): the framework emits the structural section and the widget
29036
- * self-persists through the cap's own mutations.
30267
+ * beside PTZ and motion zones on the camera itself. The device page is
30268
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
30269
+ * and this cap carries its section the way `recording` does, by RETURNING it
30270
+ * from `getDeviceSettingsContribution`.
30271
+ *
30272
+ * Seven other widgets are still declared the other way, through a
30273
+ * `deviceConfig.ui` block the framework derives a section from. That route
30274
+ * gives the addon no say in where its own panel lands and no way to decline
30275
+ * for a device the panel does not suit, which is why this one does not use it.
29037
30276
  *
29038
30277
  * ## Why one addon may implement it
29039
30278
  *
@@ -29089,16 +30328,37 @@ var GridProfileOfferSchema = object({
29089
30328
  ]),
29090
30329
  offered: boolean(),
29091
30330
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
29092
- missingSources: array(number().int().positive())
30331
+ missingSources: array(number().int().positive()),
30332
+ /**
30333
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
30334
+ * offered. DERIVED from the cells and the sources' own size at this profile —
30335
+ * it is reported because nothing else in the system would ever say what the
30336
+ * grid came out as, and because it is the number an operator would otherwise
30337
+ * expect to type.
30338
+ */
30339
+ canvas: string(),
30340
+ /**
30341
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
30342
+ *
30343
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
30344
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
30345
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
30346
+ * Default is `mid` + `low`.
30347
+ */
30348
+ published: boolean()
29093
30349
  });
29094
30350
  var GridLayoutViewSchema = object({
29095
30351
  /** The persisted grid row this camera was declared from. */
29096
30352
  instanceId: string(),
29097
30353
  deviceId: number().int().nonnegative(),
29098
30354
  name: string(),
29099
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
29100
- width: number().int(),
29101
- height: number().int(),
30355
+ /**
30356
+ * NO canvas size. A grid's resolution is not authored: each profile derives
30357
+ * its own from the cells and its sources' dimensions. The two numbers that
30358
+ * used to be here were a text field that silently decided both how much the
30359
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
30360
+ * came out as.
30361
+ */
29102
30362
  fps: number().int(),
29103
30363
  cells: array(GridLayoutCellSchema),
29104
30364
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -29107,9 +30367,13 @@ var GridLayoutViewSchema = object({
29107
30367
  var GridLayoutPatchSchema = object({
29108
30368
  deviceId: number().int().nonnegative(),
29109
30369
  name: string().min(1).max(160).optional(),
29110
- width: number().int().min(160).max(7680).optional(),
29111
- height: number().int().min(120).max(4320).optional(),
29112
30370
  fps: number().int().min(1).max(60).optional(),
30371
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
30372
+ publishedProfiles: array(_enum([
30373
+ "high",
30374
+ "mid",
30375
+ "low"
30376
+ ])).max(3).optional(),
29113
30377
  /**
29114
30378
  * The whole cell list at once. A per-cell patch would need an ordering the
29115
30379
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -31550,6 +32814,25 @@ var ptzAutotrackCapability = {
31550
32814
  scope: "device",
31551
32815
  deviceNative: true,
31552
32816
  mode: "singleton",
32817
+ /**
32818
+ * Two kinds of provider behind one name, the same shape `snapshot` and
32819
+ * `doorbell` use:
32820
+ *
32821
+ * - **Native**: the camera's own firmware autotrack, registered per device by
32822
+ * the driver when the probe says it exists (Reolink: yes; Hikvision:
32823
+ * model-dependent).
32824
+ * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
32825
+ * which follows a subject with `ptz.move` pulses driven by the detection
32826
+ * pipeline. `defaultActive: false` — the operator binds it per camera in
32827
+ * the bindings UI, exactly as `virtual-doorbell` is bound.
32828
+ *
32829
+ * The binding chooses WHICH implementation answers; `setEnabled` stays the
32830
+ * one switch for the function either way. A camera never has both driving its
32831
+ * head, which is the whole reason this is not a second cap with a second
32832
+ * toggle (D62, D520).
32833
+ */
32834
+ kind: "wrapper",
32835
+ defaultActive: false,
31553
32836
  deviceTypes: [DeviceType.Camera],
31554
32837
  deviceConfig: { ui: {
31555
32838
  kind: "widget",
@@ -32032,7 +33315,7 @@ method(object({
32032
33315
  auth: "admin"
32033
33316
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
32034
33317
  kind: "query",
32035
- auth: "admin"
33318
+ auth: "protected"
32036
33319
  }), method(object({
32037
33320
  deviceId: number(),
32038
33321
  profile: string(),
@@ -32262,13 +33545,101 @@ var ExportStateSchema = _enum([
32262
33545
  "expired",
32263
33546
  "deleted"
32264
33547
  ]);
32265
- /** One export job / history row. */
33548
+ /**
33549
+ * WHAT an export is — the authority, as opposed to the four top-level fields
33550
+ * the Library sorts and labels on (D558 § 2.2).
33551
+ *
33552
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
33553
+ * field is optional for the history rows written before it existed, and that
33554
+ * absence has exactly one interpreter.
33555
+ */
33556
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
33557
+ kind: literal("footage"),
33558
+ deviceId: number(),
33559
+ profiles: array(string()).min(1),
33560
+ fromMs: number(),
33561
+ toMs: number()
33562
+ }), object({
33563
+ kind: literal("clip"),
33564
+ deviceId: number(),
33565
+ provider: string().min(1),
33566
+ source: string().min(1),
33567
+ sourceLabel: string().min(1),
33568
+ clipId: string().min(1),
33569
+ /**
33570
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
33571
+ * already listing when the operator picked the segment.
33572
+ *
33573
+ * It is **not** a time range control and it never becomes one: the record's
33574
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
33575
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
33576
+ * `catalog-miss`, not a silently wider search. It exists because
33577
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
33578
+ * the catalog check D558 asks for is literally that call, and a call needs a
33579
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
33580
+ * wall-clock day, which is also the only width measured to be cheap (one
33581
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
33582
+ * of one child's events took 3.6–5.4 s).
33583
+ */
33584
+ catalogWindow: object({
33585
+ sinceMs: number(),
33586
+ untilMs: number()
33587
+ }),
33588
+ profile: _enum([
33589
+ "high",
33590
+ "mid",
33591
+ "low"
33592
+ ]),
33593
+ /**
33594
+ * The operator's authorisation to wake a sleeping camera for this export.
33595
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
33596
+ * because the gate that honours it is the clip provider's sleep gate — a
33597
+ * second enum here would be a second contract. Absent by default; never
33598
+ * settable by a scheduler or a retry.
33599
+ *
33600
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
33601
+ * an operator act that asks again; a queued job that outlived its wake fails
33602
+ * with a reason rather than waking on its turn; and this subject carries
33603
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
33604
+ */
33605
+ wake: ClipWakeSchema.optional()
33606
+ })]);
33607
+ /**
33608
+ * One export job / history row.
33609
+ *
33610
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
33611
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
33612
+ * Library sorts and labels on them (`library-items.ts` orders an export by
33613
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
33614
+ * that did not fill them would sort under the epoch and render a blank range.
33615
+ * Write to the subject and read from the projection and the two will disagree;
33616
+ * the projection is derived at creation and never edited afterwards.
33617
+ *
33618
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
33619
+ * who knows only the recording export will get wrong:
33620
+ *
33621
+ * | field | `kind:'footage'` | `kind:'clip'` |
33622
+ * | --- | --- | --- |
33623
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
33624
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
33625
+ *
33626
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
33627
+ * two authorities, D224's second copy).
33628
+ */
32266
33629
  var ExportRecordSchema = object({
32267
33630
  id: string(),
32268
33631
  deviceId: number(),
32269
33632
  profile: string(),
32270
33633
  fromMs: number(),
32271
33634
  toMs: number(),
33635
+ /**
33636
+ * What this export IS. Optional ONLY for the rows written before D558: the
33637
+ * store parses every row through this schema on every read, so a required
33638
+ * field would make the export AUDIT — which is the whole reason rows survive
33639
+ * file deletion — unreadable in one release. Absence means `footage`, and
33640
+ * {@link exportSubjectOf} is the one place that says so.
33641
+ */
33642
+ subject: ExportSubjectSchema.optional(),
32272
33643
  options: ExportOptionsSchema,
32273
33644
  state: ExportStateSchema,
32274
33645
  /** 0–100 while rendering; null otherwise. */
@@ -32285,6 +33656,18 @@ var ExportRecordSchema = object({
32285
33656
  /** Failure reason when state is 'failed'; null otherwise. */
32286
33657
  error: string().nullable()
32287
33658
  });
33659
+ _enum([
33660
+ "catalog-miss",
33661
+ "catalog-unreachable",
33662
+ "no-file-for-window",
33663
+ "clip-in-progress",
33664
+ "unsupported-option",
33665
+ "sleeping",
33666
+ "camera-refused",
33667
+ "fetch-failed",
33668
+ "too-large-to-transfer",
33669
+ "wake-expired"
33670
+ ]);
32288
33671
  /** Candidate download URLs (LAN first, then operator extra hosts). */
32289
33672
  var ExportDownloadSchema = object({
32290
33673
  url: string(),
@@ -32303,16 +33686,50 @@ var ExportBytesSchema = object({
32303
33686
  name: string(),
32304
33687
  bytes: number().int().nonnegative()
32305
33688
  });
33689
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
33690
+ function resolveExportProfiles(input) {
33691
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
33692
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
33693
+ return [];
33694
+ }
32306
33695
  method(object({
32307
33696
  deviceId: number(),
32308
33697
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
32309
33698
  profile: string().optional(),
32310
33699
  profiles: array(string()).min(1).optional(),
32311
- fromMs: number(),
32312
- toMs: number(),
33700
+ /** Footage only — a clip's boundaries are the camera's. */
33701
+ fromMs: number().optional(),
33702
+ toMs: number().optional(),
33703
+ /** What to export. Absent means the legacy flat footage request. */
33704
+ subject: ExportSubjectSchema.optional(),
32313
33705
  options: ExportOptionsSchema
32314
33706
  }).superRefine((v, ctx) => {
32315
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
33707
+ if (v.subject?.kind === "clip") {
33708
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
33709
+ code: ZodIssueCode.custom,
33710
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
33711
+ path: ["subject", "deviceId"]
33712
+ });
33713
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
33714
+ code: ZodIssueCode.custom,
33715
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
33716
+ path: ["fromMs"]
33717
+ });
33718
+ return;
33719
+ }
33720
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
33721
+ code: ZodIssueCode.custom,
33722
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
33723
+ path: ["subject", "deviceId"]
33724
+ });
33725
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
33726
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
33727
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
33728
+ code: ZodIssueCode.custom,
33729
+ message: "a footage export needs fromMs and toMs",
33730
+ path: ["fromMs"]
33731
+ });
33732
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
32316
33733
  code: ZodIssueCode.custom,
32317
33734
  message: "pass profiles[] (min 1) or legacy profile",
32318
33735
  path: ["profiles"]
@@ -35322,6 +36739,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
35322
36739
  pressureSensor: pressureSensorCapability,
35323
36740
  privacyMask: privacyMaskCapability,
35324
36741
  ptzAutotrack: ptzAutotrackCapability,
36742
+ recordingOnboard: recordingOnboardCapability,
35325
36743
  recordingSignal: recordingSignalCapability,
35326
36744
  sceneMonitor: sceneMonitorCapability,
35327
36745
  scriptRunner: scriptRunnerCapability,
@@ -36483,6 +37901,1329 @@ var BaseDeviceProvider = class extends BaseAddon {
36483
37901
  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;
36484
37902
  new Set(Object.values(DeviceType));
36485
37903
  DeviceFeature.BatteryOperated;
37904
+ /**
37905
+ * The vocabulary, in the order a picker offers it.
37906
+ *
37907
+ * Adding a key here is not enough to make it pickable: both glyph tables must
37908
+ * gain it too, and their tests fail until they do. That is the intended
37909
+ * friction — it is the only thing that keeps the two surfaces honest.
37910
+ */
37911
+ var DISPLAY_ICON_VOCABULARY = [
37912
+ {
37913
+ key: "camera",
37914
+ group: "device",
37915
+ synonyms: [
37916
+ "telecamera",
37917
+ "videocamera",
37918
+ "camera",
37919
+ "cam"
37920
+ ]
37921
+ },
37922
+ {
37923
+ key: "network",
37924
+ group: "device",
37925
+ synonyms: [
37926
+ "rete",
37927
+ "network",
37928
+ "lan"
37929
+ ]
37930
+ },
37931
+ {
37932
+ key: "toggle-left",
37933
+ group: "device",
37934
+ synonyms: [
37935
+ "interruttore",
37936
+ "switch",
37937
+ "toggle"
37938
+ ]
37939
+ },
37940
+ {
37941
+ key: "toggle-right",
37942
+ group: "device",
37943
+ synonyms: [
37944
+ "interruttore",
37945
+ "switch",
37946
+ "toggle",
37947
+ "acceso"
37948
+ ]
37949
+ },
37950
+ {
37951
+ key: "circle-dot",
37952
+ group: "device",
37953
+ synonyms: [
37954
+ "pulsante",
37955
+ "bottone",
37956
+ "button",
37957
+ "dot"
37958
+ ]
37959
+ },
37960
+ {
37961
+ key: "refresh-cw",
37962
+ group: "device",
37963
+ synonyms: [
37964
+ "aggiorna",
37965
+ "ricarica",
37966
+ "refresh",
37967
+ "reload",
37968
+ "ciclo"
37969
+ ]
37970
+ },
37971
+ {
37972
+ key: "box",
37973
+ group: "device",
37974
+ synonyms: [
37975
+ "scatola",
37976
+ "box",
37977
+ "contenitore"
37978
+ ]
37979
+ },
37980
+ {
37981
+ key: "scroll-text",
37982
+ group: "device",
37983
+ synonyms: [
37984
+ "registro",
37985
+ "log",
37986
+ "testo"
37987
+ ]
37988
+ },
37989
+ {
37990
+ key: "workflow",
37991
+ group: "device",
37992
+ synonyms: [
37993
+ "automazione",
37994
+ "flusso",
37995
+ "workflow",
37996
+ "scenario"
37997
+ ]
37998
+ },
37999
+ {
38000
+ key: "lock",
38001
+ group: "device",
38002
+ synonyms: [
38003
+ "serratura",
38004
+ "chiave",
38005
+ "lucchetto",
38006
+ "chiuso",
38007
+ "locked"
38008
+ ]
38009
+ },
38010
+ {
38011
+ key: "lock-open",
38012
+ group: "device",
38013
+ synonyms: [
38014
+ "serratura",
38015
+ "aperta",
38016
+ "sbloccato",
38017
+ "unlocked"
38018
+ ]
38019
+ },
38020
+ {
38021
+ key: "blinds",
38022
+ group: "device",
38023
+ synonyms: [
38024
+ "tapparella",
38025
+ "persiana",
38026
+ "serranda",
38027
+ "avvolgibile",
38028
+ "shutter",
38029
+ "blind",
38030
+ "veneziana"
38031
+ ]
38032
+ },
38033
+ {
38034
+ key: "air-vent",
38035
+ group: "device",
38036
+ synonyms: [
38037
+ "bocchetta",
38038
+ "aria",
38039
+ "ventilazione",
38040
+ "condizionatore",
38041
+ "vent"
38042
+ ]
38043
+ },
38044
+ {
38045
+ key: "monitor-play",
38046
+ group: "device",
38047
+ synonyms: [
38048
+ "schermo",
38049
+ "riproduzione",
38050
+ "player",
38051
+ "display"
38052
+ ]
38053
+ },
38054
+ {
38055
+ key: "sliders-horizontal",
38056
+ group: "device",
38057
+ synonyms: [
38058
+ "regolazione",
38059
+ "cursori",
38060
+ "impostazioni",
38061
+ "slider"
38062
+ ]
38063
+ },
38064
+ {
38065
+ key: "map-pin",
38066
+ group: "device",
38067
+ synonyms: [
38068
+ "posizione",
38069
+ "luogo",
38070
+ "mappa",
38071
+ "pin"
38072
+ ]
38073
+ },
38074
+ {
38075
+ key: "bot",
38076
+ group: "device",
38077
+ synonyms: [
38078
+ "robot",
38079
+ "aspirapolvere",
38080
+ "bot"
38081
+ ]
38082
+ },
38083
+ {
38084
+ key: "tractor",
38085
+ group: "device",
38086
+ synonyms: [
38087
+ "trattore",
38088
+ "tosaerba",
38089
+ "giardino"
38090
+ ]
38091
+ },
38092
+ {
38093
+ key: "image",
38094
+ group: "device",
38095
+ synonyms: [
38096
+ "immagine",
38097
+ "foto",
38098
+ "snapshot"
38099
+ ]
38100
+ },
38101
+ {
38102
+ key: "bone",
38103
+ group: "device",
38104
+ synonyms: ["osso", "cane"]
38105
+ },
38106
+ {
38107
+ key: "door-closed",
38108
+ group: "device",
38109
+ synonyms: [
38110
+ "porta",
38111
+ "chiusa",
38112
+ "contatto"
38113
+ ]
38114
+ },
38115
+ {
38116
+ key: "door-open",
38117
+ group: "device",
38118
+ synonyms: [
38119
+ "porta",
38120
+ "aperta",
38121
+ "contatto",
38122
+ "ingresso",
38123
+ "uscita"
38124
+ ]
38125
+ },
38126
+ {
38127
+ key: "crosshair",
38128
+ group: "device",
38129
+ synonyms: [
38130
+ "mirino",
38131
+ "puntamento",
38132
+ "centro"
38133
+ ]
38134
+ },
38135
+ {
38136
+ key: "eye",
38137
+ group: "device",
38138
+ synonyms: [
38139
+ "occhio",
38140
+ "visibile",
38141
+ "visto"
38142
+ ]
38143
+ },
38144
+ {
38145
+ key: "eye-off",
38146
+ group: "device",
38147
+ synonyms: [
38148
+ "privacy",
38149
+ "nascosto",
38150
+ "oscurato",
38151
+ "mascherato",
38152
+ "invisibile"
38153
+ ]
38154
+ },
38155
+ {
38156
+ key: "smartphone",
38157
+ group: "device",
38158
+ synonyms: [
38159
+ "telefono",
38160
+ "cellulare",
38161
+ "smartphone"
38162
+ ]
38163
+ },
38164
+ {
38165
+ key: "phone",
38166
+ group: "device",
38167
+ synonyms: ["telefono", "chiamata"]
38168
+ },
38169
+ {
38170
+ key: "tablet",
38171
+ group: "device",
38172
+ synonyms: ["tablet", "ipad"]
38173
+ },
38174
+ {
38175
+ key: "laptop",
38176
+ group: "device",
38177
+ synonyms: [
38178
+ "portatile",
38179
+ "laptop",
38180
+ "notebook"
38181
+ ]
38182
+ },
38183
+ {
38184
+ key: "monitor",
38185
+ group: "device",
38186
+ synonyms: ["monitor", "schermo"]
38187
+ },
38188
+ {
38189
+ key: "tv",
38190
+ group: "device",
38191
+ synonyms: ["televisore", "tv"]
38192
+ },
38193
+ {
38194
+ key: "speaker",
38195
+ group: "device",
38196
+ synonyms: [
38197
+ "altoparlante",
38198
+ "cassa",
38199
+ "speaker"
38200
+ ]
38201
+ },
38202
+ {
38203
+ key: "printer",
38204
+ group: "device",
38205
+ synonyms: ["stampante"]
38206
+ },
38207
+ {
38208
+ key: "projector",
38209
+ group: "device",
38210
+ synonyms: ["proiettore"]
38211
+ },
38212
+ {
38213
+ key: "gamepad-2",
38214
+ group: "device",
38215
+ synonyms: [
38216
+ "console",
38217
+ "gioco",
38218
+ "joystick"
38219
+ ]
38220
+ },
38221
+ {
38222
+ key: "radio",
38223
+ group: "device",
38224
+ synonyms: ["radio"]
38225
+ },
38226
+ {
38227
+ key: "router",
38228
+ group: "device",
38229
+ synonyms: ["router", "modem"]
38230
+ },
38231
+ {
38232
+ key: "bluetooth",
38233
+ group: "device",
38234
+ synonyms: ["bluetooth"]
38235
+ },
38236
+ {
38237
+ key: "cpu",
38238
+ group: "device",
38239
+ synonyms: [
38240
+ "processore",
38241
+ "cpu",
38242
+ "chip",
38243
+ "dispositivo"
38244
+ ]
38245
+ },
38246
+ {
38247
+ key: "hard-drive",
38248
+ group: "device",
38249
+ synonyms: [
38250
+ "disco",
38251
+ "hdd",
38252
+ "archiviazione",
38253
+ "storage"
38254
+ ]
38255
+ },
38256
+ {
38257
+ key: "server",
38258
+ group: "device",
38259
+ synonyms: ["server", "nas"]
38260
+ },
38261
+ {
38262
+ key: "mic",
38263
+ group: "device",
38264
+ synonyms: ["microfono", "audio"]
38265
+ },
38266
+ {
38267
+ key: "mic-off",
38268
+ group: "device",
38269
+ synonyms: [
38270
+ "microfono",
38271
+ "spento",
38272
+ "muto"
38273
+ ]
38274
+ },
38275
+ {
38276
+ key: "video",
38277
+ group: "device",
38278
+ synonyms: ["video"]
38279
+ },
38280
+ {
38281
+ key: "video-off",
38282
+ group: "device",
38283
+ synonyms: ["video", "spento"]
38284
+ },
38285
+ {
38286
+ key: "music",
38287
+ group: "device",
38288
+ synonyms: ["musica"]
38289
+ },
38290
+ {
38291
+ key: "plug",
38292
+ group: "device",
38293
+ synonyms: [
38294
+ "presa",
38295
+ "spina",
38296
+ "plug"
38297
+ ]
38298
+ },
38299
+ {
38300
+ key: "plug-zap",
38301
+ group: "device",
38302
+ synonyms: ["presa", "alimentata"]
38303
+ },
38304
+ {
38305
+ key: "power",
38306
+ group: "device",
38307
+ synonyms: [
38308
+ "accensione",
38309
+ "alimentazione",
38310
+ "power"
38311
+ ]
38312
+ },
38313
+ {
38314
+ key: "power-off",
38315
+ group: "device",
38316
+ synonyms: ["spegnimento", "spento"]
38317
+ },
38318
+ {
38319
+ key: "cable",
38320
+ group: "device",
38321
+ synonyms: ["cavo"]
38322
+ },
38323
+ {
38324
+ key: "battery",
38325
+ group: "device",
38326
+ synonyms: ["batteria"]
38327
+ },
38328
+ {
38329
+ key: "battery-charging",
38330
+ group: "device",
38331
+ synonyms: [
38332
+ "batteria",
38333
+ "carica",
38334
+ "ricarica"
38335
+ ]
38336
+ },
38337
+ {
38338
+ key: "battery-full",
38339
+ group: "device",
38340
+ synonyms: [
38341
+ "batteria",
38342
+ "carica",
38343
+ "piena"
38344
+ ]
38345
+ },
38346
+ {
38347
+ key: "battery-low",
38348
+ group: "device",
38349
+ synonyms: ["batteria", "scarica"]
38350
+ },
38351
+ {
38352
+ key: "key",
38353
+ group: "device",
38354
+ synonyms: ["chiave"]
38355
+ },
38356
+ {
38357
+ key: "key-round",
38358
+ group: "device",
38359
+ synonyms: ["chiave", "tonda"]
38360
+ },
38361
+ {
38362
+ key: "mailbox",
38363
+ group: "device",
38364
+ synonyms: [
38365
+ "cassetta",
38366
+ "postale",
38367
+ "posta"
38368
+ ]
38369
+ },
38370
+ {
38371
+ key: "package",
38372
+ group: "device",
38373
+ synonyms: ["pacco", "consegna"]
38374
+ },
38375
+ {
38376
+ key: "shield",
38377
+ group: "device",
38378
+ synonyms: [
38379
+ "scudo",
38380
+ "protezione",
38381
+ "sicurezza",
38382
+ "allarme"
38383
+ ]
38384
+ },
38385
+ {
38386
+ key: "shield-check",
38387
+ group: "device",
38388
+ synonyms: [
38389
+ "protetto",
38390
+ "sicuro",
38391
+ "armato"
38392
+ ]
38393
+ },
38394
+ {
38395
+ key: "shield-off",
38396
+ group: "device",
38397
+ synonyms: [
38398
+ "protezione",
38399
+ "disattivata",
38400
+ "disarmato"
38401
+ ]
38402
+ },
38403
+ {
38404
+ key: "briefcase",
38405
+ group: "device",
38406
+ synonyms: [
38407
+ "ufficio",
38408
+ "lavoro",
38409
+ "valigetta",
38410
+ "studio"
38411
+ ]
38412
+ },
38413
+ {
38414
+ key: "lightbulb",
38415
+ group: "lighting",
38416
+ synonyms: [
38417
+ "luce",
38418
+ "lampadina",
38419
+ "lampada",
38420
+ "light"
38421
+ ]
38422
+ },
38423
+ {
38424
+ key: "lamp",
38425
+ group: "lighting",
38426
+ synonyms: ["lampada", "abatjour"]
38427
+ },
38428
+ {
38429
+ key: "lamp-ceiling",
38430
+ group: "lighting",
38431
+ synonyms: [
38432
+ "plafoniera",
38433
+ "lampadario",
38434
+ "luce",
38435
+ "soffitto"
38436
+ ]
38437
+ },
38438
+ {
38439
+ key: "lamp-desk",
38440
+ group: "lighting",
38441
+ synonyms: [
38442
+ "lampada",
38443
+ "scrivania",
38444
+ "abatjour"
38445
+ ]
38446
+ },
38447
+ {
38448
+ key: "lamp-floor",
38449
+ group: "lighting",
38450
+ synonyms: [
38451
+ "piantana",
38452
+ "lampada",
38453
+ "terra"
38454
+ ]
38455
+ },
38456
+ {
38457
+ key: "flashlight",
38458
+ group: "lighting",
38459
+ synonyms: [
38460
+ "faretto",
38461
+ "torcia",
38462
+ "spot",
38463
+ "faro"
38464
+ ]
38465
+ },
38466
+ {
38467
+ key: "heater",
38468
+ group: "lighting",
38469
+ synonyms: [
38470
+ "riscaldamento",
38471
+ "termosifone",
38472
+ "stufa",
38473
+ "calore"
38474
+ ]
38475
+ },
38476
+ {
38477
+ key: "snowflake",
38478
+ group: "lighting",
38479
+ synonyms: [
38480
+ "freddo",
38481
+ "raffrescamento",
38482
+ "aria",
38483
+ "condizionata",
38484
+ "neve"
38485
+ ]
38486
+ },
38487
+ {
38488
+ key: "thermometer-sun",
38489
+ group: "lighting",
38490
+ synonyms: [
38491
+ "caldo",
38492
+ "temperatura",
38493
+ "alta"
38494
+ ]
38495
+ },
38496
+ {
38497
+ key: "thermometer-snowflake",
38498
+ group: "lighting",
38499
+ synonyms: [
38500
+ "freddo",
38501
+ "temperatura",
38502
+ "bassa"
38503
+ ]
38504
+ },
38505
+ {
38506
+ key: "refrigerator",
38507
+ group: "lighting",
38508
+ synonyms: ["frigorifero", "frigo"]
38509
+ },
38510
+ {
38511
+ key: "microwave",
38512
+ group: "lighting",
38513
+ synonyms: ["microonde", "forno"]
38514
+ },
38515
+ {
38516
+ key: "washing-machine",
38517
+ group: "lighting",
38518
+ synonyms: [
38519
+ "lavatrice",
38520
+ "lavastoviglie",
38521
+ "asciugatrice"
38522
+ ]
38523
+ },
38524
+ {
38525
+ key: "cooking-pot",
38526
+ group: "lighting",
38527
+ synonyms: [
38528
+ "pentola",
38529
+ "cucina",
38530
+ "fornello"
38531
+ ]
38532
+ },
38533
+ {
38534
+ key: "utensils",
38535
+ group: "lighting",
38536
+ synonyms: [
38537
+ "cucina",
38538
+ "posate",
38539
+ "sala",
38540
+ "pranzo"
38541
+ ]
38542
+ },
38543
+ {
38544
+ key: "thermometer",
38545
+ group: "sensing",
38546
+ synonyms: [
38547
+ "temperatura",
38548
+ "termometro",
38549
+ "termostato"
38550
+ ]
38551
+ },
38552
+ {
38553
+ key: "droplets",
38554
+ group: "sensing",
38555
+ synonyms: [
38556
+ "umidita",
38557
+ "acqua",
38558
+ "allagamento",
38559
+ "perdita"
38560
+ ]
38561
+ },
38562
+ {
38563
+ key: "droplet",
38564
+ group: "sensing",
38565
+ synonyms: [
38566
+ "goccia",
38567
+ "acqua",
38568
+ "perdita",
38569
+ "allagamento"
38570
+ ]
38571
+ },
38572
+ {
38573
+ key: "gauge",
38574
+ group: "sensing",
38575
+ synonyms: [
38576
+ "misura",
38577
+ "livello",
38578
+ "pressione",
38579
+ "indicatore"
38580
+ ]
38581
+ },
38582
+ {
38583
+ key: "gauge2",
38584
+ group: "sensing",
38585
+ synonyms: [
38586
+ "misura",
38587
+ "livello",
38588
+ "pressione",
38589
+ "indicatore"
38590
+ ],
38591
+ aliasOf: "gauge"
38592
+ },
38593
+ {
38594
+ key: "sun",
38595
+ group: "sensing",
38596
+ synonyms: [
38597
+ "sole",
38598
+ "luminosita",
38599
+ "giorno",
38600
+ "esterno"
38601
+ ]
38602
+ },
38603
+ {
38604
+ key: "sun-medium",
38605
+ group: "sensing",
38606
+ synonyms: ["sole", "luminosita"]
38607
+ },
38608
+ {
38609
+ key: "sunrise",
38610
+ group: "sensing",
38611
+ synonyms: ["alba"]
38612
+ },
38613
+ {
38614
+ key: "moon",
38615
+ group: "sensing",
38616
+ synonyms: [
38617
+ "notte",
38618
+ "luna",
38619
+ "notturna",
38620
+ "nightvision"
38621
+ ]
38622
+ },
38623
+ {
38624
+ key: "wind",
38625
+ group: "sensing",
38626
+ synonyms: [
38627
+ "vento",
38628
+ "aria",
38629
+ "gas",
38630
+ "monossido",
38631
+ "co"
38632
+ ]
38633
+ },
38634
+ {
38635
+ key: "cloud",
38636
+ group: "sensing",
38637
+ synonyms: ["nuvola", "nuvoloso"]
38638
+ },
38639
+ {
38640
+ key: "cloud-rain",
38641
+ group: "sensing",
38642
+ synonyms: ["pioggia"]
38643
+ },
38644
+ {
38645
+ key: "cloud-sun",
38646
+ group: "sensing",
38647
+ synonyms: [
38648
+ "variabile",
38649
+ "sole",
38650
+ "nuvole"
38651
+ ]
38652
+ },
38653
+ {
38654
+ key: "waves",
38655
+ group: "sensing",
38656
+ synonyms: [
38657
+ "onde",
38658
+ "acqua",
38659
+ "vibrazione"
38660
+ ]
38661
+ },
38662
+ {
38663
+ key: "sprout",
38664
+ group: "sensing",
38665
+ synonyms: [
38666
+ "irrigazione",
38667
+ "pianta",
38668
+ "germoglio",
38669
+ "orto"
38670
+ ]
38671
+ },
38672
+ {
38673
+ key: "leaf",
38674
+ group: "sensing",
38675
+ synonyms: [
38676
+ "foglia",
38677
+ "pianta",
38678
+ "verde"
38679
+ ]
38680
+ },
38681
+ {
38682
+ key: "flower",
38683
+ group: "sensing",
38684
+ synonyms: ["fiore", "pianta"]
38685
+ },
38686
+ {
38687
+ key: "zap",
38688
+ group: "sensing",
38689
+ synonyms: [
38690
+ "energia",
38691
+ "corrente",
38692
+ "consumo",
38693
+ "watt",
38694
+ "elettricita"
38695
+ ]
38696
+ },
38697
+ {
38698
+ key: "flame",
38699
+ group: "sensing",
38700
+ synonyms: [
38701
+ "fiamma",
38702
+ "gas",
38703
+ "fuoco",
38704
+ "incendio"
38705
+ ]
38706
+ },
38707
+ {
38708
+ key: "bell",
38709
+ group: "sensing",
38710
+ synonyms: [
38711
+ "campanello",
38712
+ "suoneria",
38713
+ "notifica",
38714
+ "bell",
38715
+ "chime"
38716
+ ]
38717
+ },
38718
+ {
38719
+ key: "bell-ring",
38720
+ group: "sensing",
38721
+ synonyms: [
38722
+ "campanello",
38723
+ "suona",
38724
+ "doorbell",
38725
+ "notifica"
38726
+ ]
38727
+ },
38728
+ {
38729
+ key: "bell-off",
38730
+ group: "sensing",
38731
+ synonyms: [
38732
+ "silenzioso",
38733
+ "notifiche",
38734
+ "disattivate",
38735
+ "muto"
38736
+ ]
38737
+ },
38738
+ {
38739
+ key: "siren",
38740
+ group: "sensing",
38741
+ synonyms: ["sirena", "allarme"]
38742
+ },
38743
+ {
38744
+ key: "radar",
38745
+ group: "sensing",
38746
+ synonyms: [
38747
+ "movimento",
38748
+ "presenza",
38749
+ "pir",
38750
+ "rilevamento",
38751
+ "radar"
38752
+ ]
38753
+ },
38754
+ {
38755
+ key: "activity",
38756
+ group: "sensing",
38757
+ synonyms: [
38758
+ "attivita",
38759
+ "sensore",
38760
+ "generico"
38761
+ ]
38762
+ },
38763
+ {
38764
+ key: "hash",
38765
+ group: "sensing",
38766
+ synonyms: [
38767
+ "numero",
38768
+ "contatore",
38769
+ "valore"
38770
+ ]
38771
+ },
38772
+ {
38773
+ key: "clock",
38774
+ group: "sensing",
38775
+ synonyms: [
38776
+ "orologio",
38777
+ "ora",
38778
+ "tempo"
38779
+ ]
38780
+ },
38781
+ {
38782
+ key: "timer",
38783
+ group: "sensing",
38784
+ synonyms: [
38785
+ "timer",
38786
+ "conto",
38787
+ "rovescia"
38788
+ ]
38789
+ },
38790
+ {
38791
+ key: "calendar",
38792
+ group: "sensing",
38793
+ synonyms: [
38794
+ "calendario",
38795
+ "data",
38796
+ "pianificazione"
38797
+ ]
38798
+ },
38799
+ {
38800
+ key: "pipette",
38801
+ group: "sensing",
38802
+ synonyms: [
38803
+ "qualita",
38804
+ "aria",
38805
+ "misura",
38806
+ "campione"
38807
+ ]
38808
+ },
38809
+ {
38810
+ key: "fan",
38811
+ group: "sensing",
38812
+ synonyms: [
38813
+ "ventilatore",
38814
+ "ventola",
38815
+ "aria"
38816
+ ]
38817
+ },
38818
+ {
38819
+ key: "bath",
38820
+ group: "sensing",
38821
+ synonyms: [
38822
+ "bagno",
38823
+ "vasca",
38824
+ "doccia"
38825
+ ]
38826
+ },
38827
+ {
38828
+ key: "footprints",
38829
+ group: "sensing",
38830
+ synonyms: [
38831
+ "movimento",
38832
+ "passi",
38833
+ "presenza",
38834
+ "motion"
38835
+ ]
38836
+ },
38837
+ {
38838
+ key: "alarm-smoke",
38839
+ group: "sensing",
38840
+ synonyms: [
38841
+ "fumo",
38842
+ "antincendio",
38843
+ "rilevatore"
38844
+ ]
38845
+ },
38846
+ {
38847
+ key: "shield-alert",
38848
+ group: "sensing",
38849
+ synonyms: [
38850
+ "manomissione",
38851
+ "allarme",
38852
+ "tamper",
38853
+ "avviso"
38854
+ ]
38855
+ },
38856
+ {
38857
+ key: "vibrate",
38858
+ group: "sensing",
38859
+ synonyms: [
38860
+ "vibrazione",
38861
+ "urto",
38862
+ "tamper"
38863
+ ]
38864
+ },
38865
+ {
38866
+ key: "wifi",
38867
+ group: "sensing",
38868
+ synonyms: [
38869
+ "wifi",
38870
+ "rete",
38871
+ "segnale"
38872
+ ]
38873
+ },
38874
+ {
38875
+ key: "wifi-off",
38876
+ group: "sensing",
38877
+ synonyms: [
38878
+ "wifi",
38879
+ "assente",
38880
+ "offline",
38881
+ "disconnesso"
38882
+ ]
38883
+ },
38884
+ {
38885
+ key: "volume-2",
38886
+ group: "sensing",
38887
+ synonyms: [
38888
+ "volume",
38889
+ "audio",
38890
+ "altoparlante",
38891
+ "suono"
38892
+ ]
38893
+ },
38894
+ {
38895
+ key: "volume-x",
38896
+ group: "sensing",
38897
+ synonyms: [
38898
+ "muto",
38899
+ "silenziato",
38900
+ "volume",
38901
+ "spento"
38902
+ ]
38903
+ },
38904
+ {
38905
+ key: "heart",
38906
+ group: "sensing",
38907
+ synonyms: [
38908
+ "cuore",
38909
+ "preferito",
38910
+ "salute"
38911
+ ]
38912
+ },
38913
+ {
38914
+ key: "heart-pulse",
38915
+ group: "sensing",
38916
+ synonyms: [
38917
+ "battito",
38918
+ "salute",
38919
+ "monitoraggio"
38920
+ ]
38921
+ },
38922
+ {
38923
+ key: "star",
38924
+ group: "sensing",
38925
+ synonyms: [
38926
+ "preferito",
38927
+ "stella",
38928
+ "importante"
38929
+ ]
38930
+ },
38931
+ {
38932
+ key: "sparkles",
38933
+ group: "sensing",
38934
+ synonyms: [
38935
+ "automatico",
38936
+ "intelligente",
38937
+ "ai",
38938
+ "scena"
38939
+ ]
38940
+ },
38941
+ {
38942
+ key: "house",
38943
+ group: "place",
38944
+ synonyms: [
38945
+ "casa",
38946
+ "abitazione",
38947
+ "home"
38948
+ ]
38949
+ },
38950
+ {
38951
+ key: "sofa",
38952
+ group: "place",
38953
+ synonyms: [
38954
+ "salone",
38955
+ "soggiorno",
38956
+ "divano",
38957
+ "living"
38958
+ ]
38959
+ },
38960
+ {
38961
+ key: "bed-double",
38962
+ group: "place",
38963
+ synonyms: [
38964
+ "camera",
38965
+ "letto",
38966
+ "matrimoniale"
38967
+ ]
38968
+ },
38969
+ {
38970
+ key: "bed",
38971
+ group: "place",
38972
+ synonyms: [
38973
+ "cameretta",
38974
+ "letto",
38975
+ "singolo"
38976
+ ]
38977
+ },
38978
+ {
38979
+ key: "warehouse",
38980
+ group: "place",
38981
+ synonyms: [
38982
+ "magazzino",
38983
+ "capanno",
38984
+ "deposito",
38985
+ "cantina"
38986
+ ]
38987
+ },
38988
+ {
38989
+ key: "trees",
38990
+ group: "place",
38991
+ synonyms: [
38992
+ "giardino",
38993
+ "alberi",
38994
+ "esterno",
38995
+ "parco"
38996
+ ]
38997
+ },
38998
+ {
38999
+ key: "tree-pine",
39000
+ group: "place",
39001
+ synonyms: [
39002
+ "albero",
39003
+ "pino",
39004
+ "giardino"
39005
+ ]
39006
+ },
39007
+ {
39008
+ key: "fence",
39009
+ group: "place",
39010
+ synonyms: [
39011
+ "recinto",
39012
+ "cancello",
39013
+ "staccionata",
39014
+ "perimetro"
39015
+ ]
39016
+ },
39017
+ {
39018
+ key: "store",
39019
+ group: "place",
39020
+ synonyms: [
39021
+ "negozio",
39022
+ "ingresso",
39023
+ "vetrina"
39024
+ ]
39025
+ },
39026
+ {
39027
+ key: "building",
39028
+ group: "place",
39029
+ synonyms: ["edificio", "palazzo"]
39030
+ },
39031
+ {
39032
+ key: "building-2",
39033
+ group: "place",
39034
+ synonyms: [
39035
+ "edificio",
39036
+ "uffici",
39037
+ "condominio"
39038
+ ]
39039
+ },
39040
+ {
39041
+ key: "mountain",
39042
+ group: "place",
39043
+ synonyms: [
39044
+ "montagna",
39045
+ "esterno",
39046
+ "panorama"
39047
+ ]
39048
+ },
39049
+ {
39050
+ key: "umbrella",
39051
+ group: "place",
39052
+ synonyms: [
39053
+ "tenda",
39054
+ "markise",
39055
+ "ombrellone",
39056
+ "pergola",
39057
+ "veranda"
39058
+ ]
39059
+ },
39060
+ {
39061
+ key: "dumbbell",
39062
+ group: "place",
39063
+ synonyms: ["palestra", "attrezzi"]
39064
+ },
39065
+ {
39066
+ key: "wrench",
39067
+ group: "place",
39068
+ synonyms: [
39069
+ "officina",
39070
+ "manutenzione",
39071
+ "attrezzi",
39072
+ "utenze"
39073
+ ]
39074
+ },
39075
+ {
39076
+ key: "hammer",
39077
+ group: "place",
39078
+ synonyms: [
39079
+ "officina",
39080
+ "martello",
39081
+ "lavori"
39082
+ ]
39083
+ },
39084
+ {
39085
+ key: "drill",
39086
+ group: "place",
39087
+ synonyms: [
39088
+ "officina",
39089
+ "trapano",
39090
+ "lavori"
39091
+ ]
39092
+ },
39093
+ {
39094
+ key: "waves-ladder",
39095
+ group: "place",
39096
+ synonyms: ["piscina", "vasca"]
39097
+ },
39098
+ {
39099
+ key: "circle-parking",
39100
+ group: "place",
39101
+ synonyms: [
39102
+ "parcheggio",
39103
+ "posto",
39104
+ "auto",
39105
+ "garage"
39106
+ ]
39107
+ },
39108
+ {
39109
+ key: "route",
39110
+ group: "place",
39111
+ synonyms: [
39112
+ "percorso",
39113
+ "strada",
39114
+ "vialetto",
39115
+ "tragitto"
39116
+ ]
39117
+ },
39118
+ {
39119
+ key: "car",
39120
+ group: "life",
39121
+ synonyms: [
39122
+ "auto",
39123
+ "macchina",
39124
+ "veicolo",
39125
+ "parcheggio"
39126
+ ]
39127
+ },
39128
+ {
39129
+ key: "bike",
39130
+ group: "life",
39131
+ synonyms: [
39132
+ "bicicletta",
39133
+ "bici",
39134
+ "moto"
39135
+ ]
39136
+ },
39137
+ {
39138
+ key: "bus",
39139
+ group: "life",
39140
+ synonyms: ["autobus", "pullman"]
39141
+ },
39142
+ {
39143
+ key: "truck",
39144
+ group: "life",
39145
+ synonyms: [
39146
+ "camion",
39147
+ "furgone",
39148
+ "consegna"
39149
+ ]
39150
+ },
39151
+ {
39152
+ key: "plane",
39153
+ group: "life",
39154
+ synonyms: ["aereo"]
39155
+ },
39156
+ {
39157
+ key: "train-front",
39158
+ group: "life",
39159
+ synonyms: ["treno"]
39160
+ },
39161
+ {
39162
+ key: "sailboat",
39163
+ group: "life",
39164
+ synonyms: ["barca", "imbarcazione"]
39165
+ },
39166
+ {
39167
+ key: "dog",
39168
+ group: "life",
39169
+ synonyms: ["cane", "animale"]
39170
+ },
39171
+ {
39172
+ key: "cat",
39173
+ group: "life",
39174
+ synonyms: ["gatto", "animale"]
39175
+ },
39176
+ {
39177
+ key: "bird",
39178
+ group: "life",
39179
+ synonyms: ["uccello", "animale"]
39180
+ },
39181
+ {
39182
+ key: "paw-print",
39183
+ group: "life",
39184
+ synonyms: [
39185
+ "animale",
39186
+ "zampa",
39187
+ "pet"
39188
+ ]
39189
+ },
39190
+ {
39191
+ key: "baby",
39192
+ group: "life",
39193
+ synonyms: [
39194
+ "neonato",
39195
+ "bimbo",
39196
+ "cameretta",
39197
+ "babymonitor"
39198
+ ]
39199
+ },
39200
+ {
39201
+ key: "user",
39202
+ group: "life",
39203
+ synonyms: ["persona", "utente"]
39204
+ },
39205
+ {
39206
+ key: "users",
39207
+ group: "life",
39208
+ synonyms: [
39209
+ "persone",
39210
+ "gruppo",
39211
+ "famiglia"
39212
+ ]
39213
+ },
39214
+ {
39215
+ key: "person-standing",
39216
+ group: "life",
39217
+ synonyms: [
39218
+ "persona",
39219
+ "piedi",
39220
+ "presenza"
39221
+ ]
39222
+ }
39223
+ ];
39224
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
39225
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
39226
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
36486
39227
  /** Reject after `ms`; always clears its own timer. */
36487
39228
  async function withTimeout$1(promise, ms, label) {
36488
39229
  let timer;
@@ -36690,6 +39431,12 @@ function createRuntimeStateBridge(params) {
36690
39431
  };
36691
39432
  }
36692
39433
  Object.freeze({
39434
+ "accessories.getStatus": {
39435
+ capName: "accessories",
39436
+ capScope: "device",
39437
+ addonId: null,
39438
+ access: "view"
39439
+ },
36693
39440
  "accessories.setChildHidden": {
36694
39441
  capName: "accessories",
36695
39442
  capScope: "device",
@@ -36972,6 +39719,12 @@ Object.freeze({
36972
39719
  addonId: null,
36973
39720
  access: "view"
36974
39721
  },
39722
+ "airQualitySensor.getStatus": {
39723
+ capName: "air-quality-sensor",
39724
+ capScope: "device",
39725
+ addonId: null,
39726
+ access: "view"
39727
+ },
36975
39728
  "alarmPanel.arm": {
36976
39729
  capName: "alarm-panel",
36977
39730
  capScope: "device",
@@ -36984,6 +39737,12 @@ Object.freeze({
36984
39737
  addonId: null,
36985
39738
  access: "create"
36986
39739
  },
39740
+ "alarmPanel.getStatus": {
39741
+ capName: "alarm-panel",
39742
+ capScope: "device",
39743
+ addonId: null,
39744
+ access: "view"
39745
+ },
36987
39746
  "alarmPanel.trigger": {
36988
39747
  capName: "alarm-panel",
36989
39748
  capScope: "device",
@@ -37032,6 +39791,30 @@ Object.freeze({
37032
39791
  addonId: null,
37033
39792
  access: "create"
37034
39793
  },
39794
+ "ambientLightSensor.getStatus": {
39795
+ capName: "ambient-light-sensor",
39796
+ capScope: "device",
39797
+ addonId: null,
39798
+ access: "view"
39799
+ },
39800
+ "audioAnalysis.applyDeviceSettingsPatch": {
39801
+ capName: "audio-analysis",
39802
+ capScope: "device",
39803
+ addonId: null,
39804
+ access: "create"
39805
+ },
39806
+ "audioAnalysis.getDeviceLiveContribution": {
39807
+ capName: "audio-analysis",
39808
+ capScope: "device",
39809
+ addonId: null,
39810
+ access: "view"
39811
+ },
39812
+ "audioAnalysis.getDeviceSettingsContribution": {
39813
+ capName: "audio-analysis",
39814
+ capScope: "device",
39815
+ addonId: null,
39816
+ access: "view"
39817
+ },
37035
39818
  "audioAnalysis.resolveDeviceSettings": {
37036
39819
  capName: "audio-analysis",
37037
39820
  capScope: "device",
@@ -37194,6 +39977,12 @@ Object.freeze({
37194
39977
  addonId: null,
37195
39978
  access: "create"
37196
39979
  },
39980
+ "automationControl.getStatus": {
39981
+ capName: "automation-control",
39982
+ capScope: "device",
39983
+ addonId: null,
39984
+ access: "view"
39985
+ },
37197
39986
  "automationControl.trigger": {
37198
39987
  capName: "automation-control",
37199
39988
  capScope: "device",
@@ -37290,12 +40079,30 @@ Object.freeze({
37290
40079
  addonId: null,
37291
40080
  access: "create"
37292
40081
  },
40082
+ "battery.getStatus": {
40083
+ capName: "battery",
40084
+ capScope: "device",
40085
+ addonId: null,
40086
+ access: "view"
40087
+ },
37293
40088
  "battery.wakeForStream": {
37294
40089
  capName: "battery",
37295
40090
  capScope: "device",
37296
40091
  addonId: null,
37297
40092
  access: "create"
37298
40093
  },
40094
+ "binary.getStatus": {
40095
+ capName: "binary",
40096
+ capScope: "device",
40097
+ addonId: null,
40098
+ access: "view"
40099
+ },
40100
+ "brightness.getStatus": {
40101
+ capName: "brightness",
40102
+ capScope: "device",
40103
+ addonId: null,
40104
+ access: "view"
40105
+ },
37299
40106
  "brightness.setBrightness": {
37300
40107
  capName: "brightness",
37301
40108
  capScope: "device",
@@ -37410,6 +40217,30 @@ Object.freeze({
37410
40217
  addonId: null,
37411
40218
  access: "view"
37412
40219
  },
40220
+ "cameraCredentials.getStatus": {
40221
+ capName: "camera-credentials",
40222
+ capScope: "device",
40223
+ addonId: null,
40224
+ access: "view"
40225
+ },
40226
+ "cameraGridLayout.applyDeviceSettingsPatch": {
40227
+ capName: "camera-grid-layout",
40228
+ capScope: "device",
40229
+ addonId: null,
40230
+ access: "create"
40231
+ },
40232
+ "cameraGridLayout.getDeviceLiveContribution": {
40233
+ capName: "camera-grid-layout",
40234
+ capScope: "device",
40235
+ addonId: null,
40236
+ access: "view"
40237
+ },
40238
+ "cameraGridLayout.getDeviceSettingsContribution": {
40239
+ capName: "camera-grid-layout",
40240
+ capScope: "device",
40241
+ addonId: null,
40242
+ access: "view"
40243
+ },
37413
40244
  "cameraGridLayout.getLayout": {
37414
40245
  capName: "camera-grid-layout",
37415
40246
  capScope: "device",
@@ -37422,6 +40253,24 @@ Object.freeze({
37422
40253
  addonId: null,
37423
40254
  access: "create"
37424
40255
  },
40256
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
40257
+ capName: "camera-pipeline-config",
40258
+ capScope: "device",
40259
+ addonId: null,
40260
+ access: "create"
40261
+ },
40262
+ "cameraPipelineConfig.getDeviceLiveContribution": {
40263
+ capName: "camera-pipeline-config",
40264
+ capScope: "device",
40265
+ addonId: null,
40266
+ access: "view"
40267
+ },
40268
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
40269
+ capName: "camera-pipeline-config",
40270
+ capScope: "device",
40271
+ addonId: null,
40272
+ access: "view"
40273
+ },
37425
40274
  "cameraStreams.getBrokerStreams": {
37426
40275
  capName: "camera-streams",
37427
40276
  capScope: "device",
@@ -37452,6 +40301,18 @@ Object.freeze({
37452
40301
  addonId: null,
37453
40302
  access: "view"
37454
40303
  },
40304
+ "carbonMonoxide.getStatus": {
40305
+ capName: "carbon-monoxide",
40306
+ capScope: "device",
40307
+ addonId: null,
40308
+ access: "view"
40309
+ },
40310
+ "climateControl.getStatus": {
40311
+ capName: "climate-control",
40312
+ capScope: "device",
40313
+ addonId: null,
40314
+ access: "view"
40315
+ },
37455
40316
  "climateControl.setFanMode": {
37456
40317
  capName: "climate-control",
37457
40318
  capScope: "device",
@@ -37500,6 +40361,12 @@ Object.freeze({
37500
40361
  addonId: null,
37501
40362
  access: "create"
37502
40363
  },
40364
+ "color.getStatus": {
40365
+ capName: "color",
40366
+ capScope: "device",
40367
+ addonId: null,
40368
+ access: "view"
40369
+ },
37503
40370
  "color.setColor": {
37504
40371
  capName: "color",
37505
40372
  capScope: "device",
@@ -37518,12 +40385,36 @@ Object.freeze({
37518
40385
  addonId: null,
37519
40386
  access: "create"
37520
40387
  },
40388
+ "connectivity.getStatus": {
40389
+ capName: "connectivity",
40390
+ capScope: "device",
40391
+ addonId: null,
40392
+ access: "view"
40393
+ },
40394
+ "consumables.getStatus": {
40395
+ capName: "consumables",
40396
+ capScope: "device",
40397
+ addonId: null,
40398
+ access: "view"
40399
+ },
37521
40400
  "consumables.reset": {
37522
40401
  capName: "consumables",
37523
40402
  capScope: "device",
37524
40403
  addonId: null,
37525
40404
  access: "delete"
37526
40405
  },
40406
+ "contact.getStatus": {
40407
+ capName: "contact",
40408
+ capScope: "device",
40409
+ addonId: null,
40410
+ access: "view"
40411
+ },
40412
+ "control.getStatus": {
40413
+ capName: "control",
40414
+ capScope: "device",
40415
+ addonId: null,
40416
+ access: "view"
40417
+ },
37527
40418
  "control.setValue": {
37528
40419
  capName: "control",
37529
40420
  capScope: "device",
@@ -37590,6 +40481,12 @@ Object.freeze({
37590
40481
  addonId: null,
37591
40482
  access: "create"
37592
40483
  },
40484
+ "cover.getStatus": {
40485
+ capName: "cover",
40486
+ capScope: "device",
40487
+ addonId: null,
40488
+ access: "view"
40489
+ },
37593
40490
  "cover.open": {
37594
40491
  capName: "cover",
37595
40492
  capScope: "device",
@@ -37722,6 +40619,12 @@ Object.freeze({
37722
40619
  addonId: null,
37723
40620
  access: "view"
37724
40621
  },
40622
+ "dayNight.getStatus": {
40623
+ capName: "day-night",
40624
+ capScope: "device",
40625
+ addonId: null,
40626
+ access: "view"
40627
+ },
37725
40628
  "dayNight.setSettings": {
37726
40629
  capName: "day-night",
37727
40630
  capScope: "device",
@@ -37812,6 +40715,30 @@ Object.freeze({
37812
40715
  addonId: null,
37813
40716
  access: "view"
37814
40717
  },
40718
+ "detectionPipeline.applyDeviceSettingsPatch": {
40719
+ capName: "detection-pipeline",
40720
+ capScope: "device",
40721
+ addonId: null,
40722
+ access: "create"
40723
+ },
40724
+ "detectionPipeline.getDeviceLiveContribution": {
40725
+ capName: "detection-pipeline",
40726
+ capScope: "device",
40727
+ addonId: null,
40728
+ access: "view"
40729
+ },
40730
+ "detectionPipeline.getDeviceSettingsContribution": {
40731
+ capName: "detection-pipeline",
40732
+ capScope: "device",
40733
+ addonId: null,
40734
+ access: "view"
40735
+ },
40736
+ "deviceAdminLink.getAdminLink": {
40737
+ capName: "device-admin-link",
40738
+ capScope: "device",
40739
+ addonId: null,
40740
+ access: "view"
40741
+ },
37815
40742
  "deviceAdoption.adopt": {
37816
40743
  capName: "device-adoption",
37817
40744
  capScope: "system",
@@ -37824,6 +40751,12 @@ Object.freeze({
37824
40751
  addonId: null,
37825
40752
  access: "view"
37826
40753
  },
40754
+ "deviceAdoption.getStatus": {
40755
+ capName: "device-adoption",
40756
+ capScope: "system",
40757
+ addonId: null,
40758
+ access: "view"
40759
+ },
37827
40760
  "deviceAdoption.listCandidateFilters": {
37828
40761
  capName: "device-adoption",
37829
40762
  capScope: "system",
@@ -37860,6 +40793,12 @@ Object.freeze({
37860
40793
  addonId: null,
37861
40794
  access: "create"
37862
40795
  },
40796
+ "deviceDiscovery.getStatus": {
40797
+ capName: "device-discovery",
40798
+ capScope: "device",
40799
+ addonId: null,
40800
+ access: "view"
40801
+ },
37863
40802
  "deviceDiscovery.listDiscovered": {
37864
40803
  capName: "device-discovery",
37865
40804
  capScope: "device",
@@ -37878,12 +40817,30 @@ Object.freeze({
37878
40817
  addonId: null,
37879
40818
  access: "create"
37880
40819
  },
40820
+ "deviceExport.applyDeviceSettingsPatch": {
40821
+ capName: "device-export",
40822
+ capScope: "system",
40823
+ addonId: null,
40824
+ access: "create"
40825
+ },
37881
40826
  "deviceExport.exposeDevice": {
37882
40827
  capName: "device-export",
37883
40828
  capScope: "system",
37884
40829
  addonId: null,
37885
40830
  access: "create"
37886
40831
  },
40832
+ "deviceExport.getDeviceLiveContribution": {
40833
+ capName: "device-export",
40834
+ capScope: "system",
40835
+ addonId: null,
40836
+ access: "view"
40837
+ },
40838
+ "deviceExport.getDeviceSettingsContribution": {
40839
+ capName: "device-export",
40840
+ capScope: "system",
40841
+ addonId: null,
40842
+ access: "view"
40843
+ },
37887
40844
  "deviceExport.getStatus": {
37888
40845
  capName: "device-export",
37889
40846
  capScope: "system",
@@ -37980,6 +40937,12 @@ Object.freeze({
37980
40937
  addonId: null,
37981
40938
  access: "create"
37982
40939
  },
40940
+ "deviceManager.applyDeviceSettingsPatch": {
40941
+ capName: "device-manager",
40942
+ capScope: "system",
40943
+ addonId: null,
40944
+ access: "create"
40945
+ },
37983
40946
  "deviceManager.applyInitialMeta": {
37984
40947
  capName: "device-manager",
37985
40948
  capScope: "system",
@@ -38082,6 +41045,12 @@ Object.freeze({
38082
41045
  addonId: null,
38083
41046
  access: "view"
38084
41047
  },
41048
+ "deviceManager.getDeviceLiveContribution": {
41049
+ capName: "device-manager",
41050
+ capScope: "system",
41051
+ addonId: null,
41052
+ access: "view"
41053
+ },
38085
41054
  "deviceManager.getDeviceLiveInfoAggregate": {
38086
41055
  capName: "device-manager",
38087
41056
  capScope: "system",
@@ -38094,6 +41063,12 @@ Object.freeze({
38094
41063
  addonId: null,
38095
41064
  access: "view"
38096
41065
  },
41066
+ "deviceManager.getDeviceSettingsContribution": {
41067
+ capName: "device-manager",
41068
+ capScope: "system",
41069
+ addonId: null,
41070
+ access: "view"
41071
+ },
38097
41072
  "deviceManager.getDeviceStatusAggregate": {
38098
41073
  capName: "device-manager",
38099
41074
  capScope: "system",
@@ -38586,6 +41561,36 @@ Object.freeze({
38586
41561
  addonId: null,
38587
41562
  access: "create"
38588
41563
  },
41564
+ "deviceStatus.getStatus": {
41565
+ capName: "device-status",
41566
+ capScope: "device",
41567
+ addonId: null,
41568
+ access: "view"
41569
+ },
41570
+ "doorbell.applyDeviceSettingsPatch": {
41571
+ capName: "doorbell",
41572
+ capScope: "device",
41573
+ addonId: null,
41574
+ access: "create"
41575
+ },
41576
+ "doorbell.getDeviceLiveContribution": {
41577
+ capName: "doorbell",
41578
+ capScope: "device",
41579
+ addonId: null,
41580
+ access: "view"
41581
+ },
41582
+ "doorbell.getDeviceSettingsContribution": {
41583
+ capName: "doorbell",
41584
+ capScope: "device",
41585
+ addonId: null,
41586
+ access: "view"
41587
+ },
41588
+ "doorbell.getStatus": {
41589
+ capName: "doorbell",
41590
+ capScope: "device",
41591
+ addonId: null,
41592
+ access: "view"
41593
+ },
38589
41594
  "embeddingEncoder.encode": {
38590
41595
  capName: "embedding-encoder",
38591
41596
  capScope: "system",
@@ -38604,6 +41609,18 @@ Object.freeze({
38604
41609
  addonId: null,
38605
41610
  access: "view"
38606
41611
  },
41612
+ "enumSensor.getStatus": {
41613
+ capName: "enum-sensor",
41614
+ capScope: "device",
41615
+ addonId: null,
41616
+ access: "view"
41617
+ },
41618
+ "eventEmitter.getStatus": {
41619
+ capName: "event-emitter",
41620
+ capScope: "device",
41621
+ addonId: null,
41622
+ access: "view"
41623
+ },
38607
41624
  "events.getEventClipUrl": {
38608
41625
  capName: "events",
38609
41626
  capScope: "device",
@@ -38724,6 +41741,12 @@ Object.freeze({
38724
41741
  addonId: null,
38725
41742
  access: "view"
38726
41743
  },
41744
+ "fanControl.getStatus": {
41745
+ capName: "fan-control",
41746
+ capScope: "device",
41747
+ addonId: null,
41748
+ access: "view"
41749
+ },
38727
41750
  "fanControl.setDirection": {
38728
41751
  capName: "fan-control",
38729
41752
  capScope: "device",
@@ -38748,6 +41771,12 @@ Object.freeze({
38748
41771
  addonId: null,
38749
41772
  access: "create"
38750
41773
  },
41774
+ "featureProbe.getStatus": {
41775
+ capName: "feature-probe",
41776
+ capScope: "device",
41777
+ addonId: null,
41778
+ access: "view"
41779
+ },
38751
41780
  "filesystemBrowse.browse": {
38752
41781
  capName: "filesystem-browse",
38753
41782
  capScope: "system",
@@ -38766,6 +41795,24 @@ Object.freeze({
38766
41795
  addonId: null,
38767
41796
  access: "view"
38768
41797
  },
41798
+ "flood.getStatus": {
41799
+ capName: "flood",
41800
+ capScope: "device",
41801
+ addonId: null,
41802
+ access: "view"
41803
+ },
41804
+ "gas.getStatus": {
41805
+ capName: "gas",
41806
+ capScope: "device",
41807
+ addonId: null,
41808
+ access: "view"
41809
+ },
41810
+ "humidifier.getStatus": {
41811
+ capName: "humidifier",
41812
+ capScope: "device",
41813
+ addonId: null,
41814
+ access: "view"
41815
+ },
38769
41816
  "humidifier.setMode": {
38770
41817
  capName: "humidifier",
38771
41818
  capScope: "device",
@@ -38784,12 +41831,30 @@ Object.freeze({
38784
41831
  addonId: null,
38785
41832
  access: "create"
38786
41833
  },
41834
+ "humiditySensor.getStatus": {
41835
+ capName: "humidity-sensor",
41836
+ capScope: "device",
41837
+ addonId: null,
41838
+ access: "view"
41839
+ },
41840
+ "image.getStatus": {
41841
+ capName: "image",
41842
+ capScope: "device",
41843
+ addonId: null,
41844
+ access: "view"
41845
+ },
38787
41846
  "imageSettings.getOptions": {
38788
41847
  capName: "image-settings",
38789
41848
  capScope: "device",
38790
41849
  addonId: null,
38791
41850
  access: "view"
38792
41851
  },
41852
+ "imageSettings.getStatus": {
41853
+ capName: "image-settings",
41854
+ capScope: "device",
41855
+ addonId: null,
41856
+ access: "view"
41857
+ },
38793
41858
  "imageSettings.setSettings": {
38794
41859
  capName: "image-settings",
38795
41860
  capScope: "device",
@@ -38862,6 +41927,12 @@ Object.freeze({
38862
41927
  addonId: null,
38863
41928
  access: "create"
38864
41929
  },
41930
+ "intercom.getStatus": {
41931
+ capName: "intercom",
41932
+ capScope: "device",
41933
+ addonId: null,
41934
+ access: "view"
41935
+ },
38865
41936
  "intercom.handleAnswer": {
38866
41937
  capName: "intercom",
38867
41938
  capScope: "device",
@@ -38898,6 +41969,12 @@ Object.freeze({
38898
41969
  addonId: null,
38899
41970
  access: "create"
38900
41971
  },
41972
+ "lawnMowerControl.getStatus": {
41973
+ capName: "lawn-mower-control",
41974
+ capScope: "device",
41975
+ addonId: null,
41976
+ access: "view"
41977
+ },
38901
41978
  "lawnMowerControl.pause": {
38902
41979
  capName: "lawn-mower-control",
38903
41980
  capScope: "device",
@@ -39180,6 +42257,12 @@ Object.freeze({
39180
42257
  addonId: null,
39181
42258
  access: "create"
39182
42259
  },
42260
+ "lockControl.getStatus": {
42261
+ capName: "lock-control",
42262
+ capScope: "device",
42263
+ addonId: null,
42264
+ access: "view"
42265
+ },
39183
42266
  "lockControl.lock": {
39184
42267
  capName: "lock-control",
39185
42268
  capScope: "device",
@@ -39228,6 +42311,12 @@ Object.freeze({
39228
42311
  addonId: null,
39229
42312
  access: "view"
39230
42313
  },
42314
+ "mediaPlayer.getStatus": {
42315
+ capName: "media-player",
42316
+ capScope: "device",
42317
+ addonId: null,
42318
+ access: "view"
42319
+ },
39231
42320
  "mediaPlayer.next": {
39232
42321
  capName: "media-player",
39233
42322
  capScope: "device",
@@ -39420,6 +42509,12 @@ Object.freeze({
39420
42509
  addonId: null,
39421
42510
  access: "create"
39422
42511
  },
42512
+ "motion.getStatus": {
42513
+ capName: "motion",
42514
+ capScope: "device",
42515
+ addonId: null,
42516
+ access: "view"
42517
+ },
39423
42518
  "motion.isDetected": {
39424
42519
  capName: "motion",
39425
42520
  capScope: "device",
@@ -39432,6 +42527,24 @@ Object.freeze({
39432
42527
  addonId: null,
39433
42528
  access: "create"
39434
42529
  },
42530
+ "motionDetection.applyDeviceSettingsPatch": {
42531
+ capName: "motion-detection",
42532
+ capScope: "device",
42533
+ addonId: null,
42534
+ access: "create"
42535
+ },
42536
+ "motionDetection.getDeviceLiveContribution": {
42537
+ capName: "motion-detection",
42538
+ capScope: "device",
42539
+ addonId: null,
42540
+ access: "view"
42541
+ },
42542
+ "motionDetection.getDeviceSettingsContribution": {
42543
+ capName: "motion-detection",
42544
+ capScope: "device",
42545
+ addonId: null,
42546
+ access: "view"
42547
+ },
39435
42548
  "motionDetection.removeCamera": {
39436
42549
  capName: "motion-detection",
39437
42550
  capScope: "device",
@@ -39444,6 +42557,12 @@ Object.freeze({
39444
42557
  addonId: null,
39445
42558
  access: "delete"
39446
42559
  },
42560
+ "motionTrigger.getStatus": {
42561
+ capName: "motion-trigger",
42562
+ capScope: "device",
42563
+ addonId: null,
42564
+ access: "view"
42565
+ },
39447
42566
  "motionTrigger.setMotionTrigger": {
39448
42567
  capName: "motion-trigger",
39449
42568
  capScope: "device",
@@ -39456,6 +42575,12 @@ Object.freeze({
39456
42575
  addonId: null,
39457
42576
  access: "view"
39458
42577
  },
42578
+ "motionZones.getStatus": {
42579
+ capName: "motion-zones",
42580
+ capScope: "device",
42581
+ addonId: null,
42582
+ access: "view"
42583
+ },
39459
42584
  "motionZones.setZone": {
39460
42585
  capName: "motion-zones",
39461
42586
  capScope: "device",
@@ -39516,6 +42641,12 @@ Object.freeze({
39516
42641
  addonId: null,
39517
42642
  access: "view"
39518
42643
  },
42644
+ "nativeObjectDetection.getStatus": {
42645
+ capName: "native-object-detection",
42646
+ capScope: "device",
42647
+ addonId: null,
42648
+ access: "view"
42649
+ },
39519
42650
  "nativeObjectDetection.setEnabled": {
39520
42651
  capName: "native-object-detection",
39521
42652
  capScope: "device",
@@ -39534,6 +42665,12 @@ Object.freeze({
39534
42665
  addonId: null,
39535
42666
  access: "view"
39536
42667
  },
42668
+ "navigation.getStatus": {
42669
+ capName: "navigation",
42670
+ capScope: "device",
42671
+ addonId: null,
42672
+ access: "view"
42673
+ },
39537
42674
  "navigation.goToPoint": {
39538
42675
  capName: "navigation",
39539
42676
  capScope: "device",
@@ -39618,6 +42755,12 @@ Object.freeze({
39618
42755
  addonId: null,
39619
42756
  access: "create"
39620
42757
  },
42758
+ "networkLink.getStatus": {
42759
+ capName: "network-link",
42760
+ capScope: "device",
42761
+ addonId: null,
42762
+ access: "view"
42763
+ },
39621
42764
  "networkQuality.getAllStats": {
39622
42765
  capName: "network-quality",
39623
42766
  capScope: "system",
@@ -39876,18 +43019,36 @@ Object.freeze({
39876
43019
  addonId: null,
39877
43020
  access: "create"
39878
43021
  },
43022
+ "notifier.getStatus": {
43023
+ capName: "notifier",
43024
+ capScope: "device",
43025
+ addonId: null,
43026
+ access: "view"
43027
+ },
39879
43028
  "notifier.send": {
39880
43029
  capName: "notifier",
39881
43030
  capScope: "device",
39882
43031
  addonId: null,
39883
43032
  access: "create"
39884
43033
  },
43034
+ "numericSensor.getStatus": {
43035
+ capName: "numeric-sensor",
43036
+ capScope: "device",
43037
+ addonId: null,
43038
+ access: "view"
43039
+ },
39885
43040
  "oauthIntegration.getDescriptor": {
39886
43041
  capName: "oauth-integration",
39887
43042
  capScope: "system",
39888
43043
  addonId: null,
39889
43044
  access: "view"
39890
43045
  },
43046
+ "osd.getStatus": {
43047
+ capName: "osd",
43048
+ capScope: "device",
43049
+ addonId: null,
43050
+ access: "view"
43051
+ },
39891
43052
  "osd.setOverlay": {
39892
43053
  capName: "osd",
39893
43054
  capScope: "device",
@@ -39960,6 +43121,12 @@ Object.freeze({
39960
43121
  addonId: null,
39961
43122
  access: "create"
39962
43123
  },
43124
+ "petFeeder.getStatus": {
43125
+ capName: "pet-feeder",
43126
+ capScope: "device",
43127
+ addonId: null,
43128
+ access: "view"
43129
+ },
39963
43130
  "petFeeder.markFoodReplenished": {
39964
43131
  capName: "pet-feeder",
39965
43132
  capScope: "device",
@@ -40002,6 +43169,12 @@ Object.freeze({
40002
43169
  addonId: null,
40003
43170
  access: "create"
40004
43171
  },
43172
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
43173
+ capName: "pipeline-analytics",
43174
+ capScope: "device",
43175
+ addonId: null,
43176
+ access: "create"
43177
+ },
40005
43178
  "pipelineAnalytics.cancelRelocateMedia": {
40006
43179
  capName: "pipeline-analytics",
40007
43180
  capScope: "device",
@@ -40068,6 +43241,18 @@ Object.freeze({
40068
43241
  addonId: null,
40069
43242
  access: "view"
40070
43243
  },
43244
+ "pipelineAnalytics.getDeviceLiveContribution": {
43245
+ capName: "pipeline-analytics",
43246
+ capScope: "device",
43247
+ addonId: null,
43248
+ access: "view"
43249
+ },
43250
+ "pipelineAnalytics.getDeviceSettingsContribution": {
43251
+ capName: "pipeline-analytics",
43252
+ capScope: "device",
43253
+ addonId: null,
43254
+ access: "view"
43255
+ },
40071
43256
  "pipelineAnalytics.getEventDensity": {
40072
43257
  capName: "pipeline-analytics",
40073
43258
  capScope: "device",
@@ -40278,6 +43463,12 @@ Object.freeze({
40278
43463
  addonId: null,
40279
43464
  access: "view"
40280
43465
  },
43466
+ "pipelineAnalytics.ownersWithMedia": {
43467
+ capName: "pipeline-analytics",
43468
+ capScope: "device",
43469
+ addonId: null,
43470
+ access: "view"
43471
+ },
40281
43472
  "pipelineAnalytics.pauseForStorageMigration": {
40282
43473
  capName: "pipeline-analytics",
40283
43474
  capScope: "device",
@@ -40632,6 +43823,12 @@ Object.freeze({
40632
43823
  addonId: null,
40633
43824
  access: "view"
40634
43825
  },
43826
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
43827
+ capName: "pipeline-orchestrator",
43828
+ capScope: "system",
43829
+ addonId: null,
43830
+ access: "create"
43831
+ },
40635
43832
  "pipelineOrchestrator.assignAudio": {
40636
43833
  capName: "pipeline-orchestrator",
40637
43834
  capScope: "system",
@@ -40722,6 +43919,18 @@ Object.freeze({
40722
43919
  addonId: null,
40723
43920
  access: "view"
40724
43921
  },
43922
+ "pipelineOrchestrator.getDeviceLiveContribution": {
43923
+ capName: "pipeline-orchestrator",
43924
+ capScope: "system",
43925
+ addonId: null,
43926
+ access: "view"
43927
+ },
43928
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
43929
+ capName: "pipeline-orchestrator",
43930
+ capScope: "system",
43931
+ addonId: null,
43932
+ access: "view"
43933
+ },
40725
43934
  "pipelineOrchestrator.getGlobalMetrics": {
40726
43935
  capName: "pipeline-orchestrator",
40727
43936
  capScope: "system",
@@ -41130,12 +44339,36 @@ Object.freeze({
41130
44339
  addonId: null,
41131
44340
  access: "view"
41132
44341
  },
44342
+ "powerMeter.getStatus": {
44343
+ capName: "power-meter",
44344
+ capScope: "device",
44345
+ addonId: null,
44346
+ access: "view"
44347
+ },
44348
+ "presence.getStatus": {
44349
+ capName: "presence",
44350
+ capScope: "device",
44351
+ addonId: null,
44352
+ access: "view"
44353
+ },
44354
+ "pressureSensor.getStatus": {
44355
+ capName: "pressure-sensor",
44356
+ capScope: "device",
44357
+ addonId: null,
44358
+ access: "view"
44359
+ },
41133
44360
  "privacyMask.getOptions": {
41134
44361
  capName: "privacy-mask",
41135
44362
  capScope: "device",
41136
44363
  addonId: null,
41137
44364
  access: "view"
41138
44365
  },
44366
+ "privacyMask.getStatus": {
44367
+ capName: "privacy-mask",
44368
+ capScope: "device",
44369
+ addonId: null,
44370
+ access: "view"
44371
+ },
41139
44372
  "privacyMask.setAudioEnabled": {
41140
44373
  capName: "privacy-mask",
41141
44374
  capScope: "device",
@@ -41196,6 +44429,12 @@ Object.freeze({
41196
44429
  addonId: null,
41197
44430
  access: "view"
41198
44431
  },
44432
+ "ptz.getStatus": {
44433
+ capName: "ptz",
44434
+ capScope: "device",
44435
+ addonId: null,
44436
+ access: "view"
44437
+ },
41199
44438
  "ptz.goHome": {
41200
44439
  capName: "ptz",
41201
44440
  capScope: "device",
@@ -41274,6 +44513,12 @@ Object.freeze({
41274
44513
  addonId: null,
41275
44514
  access: "create"
41276
44515
  },
44516
+ "recording.applyDeviceSettingsPatch": {
44517
+ capName: "recording",
44518
+ capScope: "system",
44519
+ addonId: null,
44520
+ access: "create"
44521
+ },
41277
44522
  "recording.cancelRelocateJob": {
41278
44523
  capName: "recording",
41279
44524
  capScope: "system",
@@ -41322,6 +44567,18 @@ Object.freeze({
41322
44567
  addonId: null,
41323
44568
  access: "view"
41324
44569
  },
44570
+ "recording.getDeviceLiveContribution": {
44571
+ capName: "recording",
44572
+ capScope: "system",
44573
+ addonId: null,
44574
+ access: "view"
44575
+ },
44576
+ "recording.getDeviceSettingsContribution": {
44577
+ capName: "recording",
44578
+ capScope: "system",
44579
+ addonId: null,
44580
+ access: "view"
44581
+ },
41325
44582
  "recording.getPlacement": {
41326
44583
  capName: "recording",
41327
44584
  capScope: "system",
@@ -41340,6 +44597,12 @@ Object.freeze({
41340
44597
  addonId: null,
41341
44598
  access: "view"
41342
44599
  },
44600
+ "recording.getStatus": {
44601
+ capName: "recording",
44602
+ capScope: "system",
44603
+ addonId: null,
44604
+ access: "view"
44605
+ },
41343
44606
  "recording.getStorageMigrationMoveStatus": {
41344
44607
  capName: "recording",
41345
44608
  capScope: "system",
@@ -41514,6 +44777,24 @@ Object.freeze({
41514
44777
  addonId: null,
41515
44778
  access: "view"
41516
44779
  },
44780
+ "recordingOnboard.getOptions": {
44781
+ capName: "recording-onboard",
44782
+ capScope: "device",
44783
+ addonId: null,
44784
+ access: "view"
44785
+ },
44786
+ "recordingOnboard.getStatus": {
44787
+ capName: "recording-onboard",
44788
+ capScope: "device",
44789
+ addonId: null,
44790
+ access: "view"
44791
+ },
44792
+ "recordingOnboard.setSettings": {
44793
+ capName: "recording-onboard",
44794
+ capScope: "device",
44795
+ addonId: null,
44796
+ access: "create"
44797
+ },
41517
44798
  "recordingSignal.getStatus": {
41518
44799
  capName: "recording-signal",
41519
44800
  capScope: "device",
@@ -41544,6 +44825,12 @@ Object.freeze({
41544
44825
  addonId: null,
41545
44826
  access: "delete"
41546
44827
  },
44828
+ "sceneMonitor.getStatus": {
44829
+ capName: "scene-monitor",
44830
+ capScope: "device",
44831
+ addonId: null,
44832
+ access: "view"
44833
+ },
41547
44834
  "sceneMonitor.listScenes": {
41548
44835
  capName: "scene-monitor",
41549
44836
  capScope: "device",
@@ -41574,6 +44861,12 @@ Object.freeze({
41574
44861
  addonId: null,
41575
44862
  access: "create"
41576
44863
  },
44864
+ "scriptRunner.getStatus": {
44865
+ capName: "script-runner",
44866
+ capScope: "device",
44867
+ addonId: null,
44868
+ access: "view"
44869
+ },
41577
44870
  "scriptRunner.run": {
41578
44871
  capName: "script-runner",
41579
44872
  capScope: "device",
@@ -41706,6 +44999,12 @@ Object.freeze({
41706
44999
  addonId: null,
41707
45000
  access: "create"
41708
45001
  },
45002
+ "smoke.getStatus": {
45003
+ capName: "smoke",
45004
+ capScope: "device",
45005
+ addonId: null,
45006
+ access: "view"
45007
+ },
41709
45008
  "smtpProvider.getStatus": {
41710
45009
  capName: "smtp-provider",
41711
45010
  capScope: "system",
@@ -41724,12 +45023,30 @@ Object.freeze({
41724
45023
  addonId: null,
41725
45024
  access: "view"
41726
45025
  },
45026
+ "snapshot.applyDeviceSettingsPatch": {
45027
+ capName: "snapshot",
45028
+ capScope: "device",
45029
+ addonId: null,
45030
+ access: "create"
45031
+ },
41727
45032
  "snapshot.getDebugState": {
41728
45033
  capName: "snapshot",
41729
45034
  capScope: "device",
41730
45035
  addonId: null,
41731
45036
  access: "view"
41732
45037
  },
45038
+ "snapshot.getDeviceLiveContribution": {
45039
+ capName: "snapshot",
45040
+ capScope: "device",
45041
+ addonId: null,
45042
+ access: "view"
45043
+ },
45044
+ "snapshot.getDeviceSettingsContribution": {
45045
+ capName: "snapshot",
45046
+ capScope: "device",
45047
+ addonId: null,
45048
+ access: "view"
45049
+ },
41733
45050
  "snapshot.getSnapshot": {
41734
45051
  capName: "snapshot",
41735
45052
  capScope: "device",
@@ -41748,6 +45065,12 @@ Object.freeze({
41748
45065
  addonId: null,
41749
45066
  access: "view"
41750
45067
  },
45068
+ "snapshot.getStatus": {
45069
+ capName: "snapshot",
45070
+ capScope: "device",
45071
+ addonId: null,
45072
+ access: "view"
45073
+ },
41751
45074
  "snapshot.invalidateCache": {
41752
45075
  capName: "snapshot",
41753
45076
  capScope: "device",
@@ -42084,6 +45407,12 @@ Object.freeze({
42084
45407
  addonId: null,
42085
45408
  access: "create"
42086
45409
  },
45410
+ "streamBroker.applyDeviceSettingsPatch": {
45411
+ capName: "stream-broker",
45412
+ capScope: "system",
45413
+ addonId: null,
45414
+ access: "create"
45415
+ },
42087
45416
  "streamBroker.assignProfile": {
42088
45417
  capName: "stream-broker",
42089
45418
  capScope: "system",
@@ -42120,6 +45449,18 @@ Object.freeze({
42120
45449
  addonId: null,
42121
45450
  access: "view"
42122
45451
  },
45452
+ "streamBroker.getDeviceLiveContribution": {
45453
+ capName: "stream-broker",
45454
+ capScope: "system",
45455
+ addonId: null,
45456
+ access: "view"
45457
+ },
45458
+ "streamBroker.getDeviceSettingsContribution": {
45459
+ capName: "stream-broker",
45460
+ capScope: "system",
45461
+ addonId: null,
45462
+ access: "view"
45463
+ },
42123
45464
  "streamBroker.getPreBufferInfo": {
42124
45465
  capName: "stream-broker",
42125
45466
  capScope: "system",
@@ -42318,6 +45659,12 @@ Object.freeze({
42318
45659
  addonId: null,
42319
45660
  access: "view"
42320
45661
  },
45662
+ "streamParams.getStatus": {
45663
+ capName: "stream-params",
45664
+ capScope: "device",
45665
+ addonId: null,
45666
+ access: "view"
45667
+ },
42321
45668
  "streamParams.setProfile": {
42322
45669
  capName: "stream-params",
42323
45670
  capScope: "device",
@@ -42330,6 +45677,12 @@ Object.freeze({
42330
45677
  addonId: null,
42331
45678
  access: "view"
42332
45679
  },
45680
+ "switch.getStatus": {
45681
+ capName: "switch",
45682
+ capScope: "device",
45683
+ addonId: null,
45684
+ access: "view"
45685
+ },
42333
45686
  "switch.setState": {
42334
45687
  capName: "switch",
42335
45688
  capScope: "device",
@@ -42426,6 +45779,18 @@ Object.freeze({
42426
45779
  addonId: null,
42427
45780
  access: "create"
42428
45781
  },
45782
+ "tamper.getStatus": {
45783
+ capName: "tamper",
45784
+ capScope: "device",
45785
+ addonId: null,
45786
+ access: "view"
45787
+ },
45788
+ "temperatureSensor.getStatus": {
45789
+ capName: "temperature-sensor",
45790
+ capScope: "device",
45791
+ addonId: null,
45792
+ access: "view"
45793
+ },
42429
45794
  "terminalSession.adoptLegacyMonitor": {
42430
45795
  capName: "terminal-session",
42431
45796
  capScope: "system",
@@ -42522,6 +45887,12 @@ Object.freeze({
42522
45887
  addonId: null,
42523
45888
  access: "view"
42524
45889
  },
45890
+ "update.getStatus": {
45891
+ capName: "update",
45892
+ capScope: "device",
45893
+ addonId: null,
45894
+ access: "view"
45895
+ },
42525
45896
  "update.installUpdate": {
42526
45897
  capName: "update",
42527
45898
  capScope: "device",
@@ -42744,6 +46115,12 @@ Object.freeze({
42744
46115
  addonId: null,
42745
46116
  access: "create"
42746
46117
  },
46118
+ "vacuumControl.getStatus": {
46119
+ capName: "vacuum-control",
46120
+ capScope: "device",
46121
+ addonId: null,
46122
+ access: "view"
46123
+ },
42747
46124
  "vacuumControl.locate": {
42748
46125
  capName: "vacuum-control",
42749
46126
  capScope: "device",
@@ -42786,6 +46163,12 @@ Object.freeze({
42786
46163
  addonId: null,
42787
46164
  access: "create"
42788
46165
  },
46166
+ "valve.getStatus": {
46167
+ capName: "valve",
46168
+ capScope: "device",
46169
+ addonId: null,
46170
+ access: "view"
46171
+ },
42789
46172
  "valve.open": {
42790
46173
  capName: "valve",
42791
46174
  capScope: "device",
@@ -42858,6 +46241,18 @@ Object.freeze({
42858
46241
  addonId: null,
42859
46242
  access: "create"
42860
46243
  },
46244
+ "vibration.getStatus": {
46245
+ capName: "vibration",
46246
+ capScope: "device",
46247
+ addonId: null,
46248
+ access: "view"
46249
+ },
46250
+ "videoclips.dialClipStream": {
46251
+ capName: "videoclips",
46252
+ capScope: "device",
46253
+ addonId: null,
46254
+ access: "view"
46255
+ },
42861
46256
  "videoclips.getClipPlayback": {
42862
46257
  capName: "videoclips",
42863
46258
  capScope: "device",
@@ -42870,6 +46265,18 @@ Object.freeze({
42870
46265
  addonId: null,
42871
46266
  access: "view"
42872
46267
  },
46268
+ "videoclips.listSources": {
46269
+ capName: "videoclips",
46270
+ capScope: "device",
46271
+ addonId: null,
46272
+ access: "view"
46273
+ },
46274
+ "videoclips.readClipBytes": {
46275
+ capName: "videoclips",
46276
+ capScope: "device",
46277
+ addonId: null,
46278
+ access: "view"
46279
+ },
42873
46280
  "viewerUi.getStaticDir": {
42874
46281
  capName: "viewer-ui",
42875
46282
  capScope: "system",
@@ -42882,6 +46289,12 @@ Object.freeze({
42882
46289
  addonId: null,
42883
46290
  access: "view"
42884
46291
  },
46292
+ "waterHeater.getStatus": {
46293
+ capName: "water-heater",
46294
+ capScope: "device",
46295
+ addonId: null,
46296
+ access: "view"
46297
+ },
42885
46298
  "waterHeater.setAway": {
42886
46299
  capName: "water-heater",
42887
46300
  capScope: "device",
@@ -42900,6 +46313,12 @@ Object.freeze({
42900
46313
  addonId: null,
42901
46314
  access: "create"
42902
46315
  },
46316
+ "weather.getStatus": {
46317
+ capName: "weather",
46318
+ capScope: "device",
46319
+ addonId: null,
46320
+ access: "view"
46321
+ },
42903
46322
  "webrtcSession.addIceCandidate": {
42904
46323
  capName: "webrtc-session",
42905
46324
  capScope: "device",
@@ -42924,6 +46343,12 @@ Object.freeze({
42924
46343
  addonId: null,
42925
46344
  access: "view"
42926
46345
  },
46346
+ "webrtcSession.getSessionDebug": {
46347
+ capName: "webrtc-session",
46348
+ capScope: "device",
46349
+ addonId: null,
46350
+ access: "view"
46351
+ },
42927
46352
  "webrtcSession.getSessionState": {
42928
46353
  capName: "webrtc-session",
42929
46354
  capScope: "device",
@@ -43256,6 +46681,11 @@ Object.freeze({
43256
46681
  form: "single",
43257
46682
  optional: true
43258
46683
  }],
46684
+ "deviceAdminLink.getAdminLink": [{
46685
+ name: "deviceId",
46686
+ form: "single",
46687
+ optional: false
46688
+ }],
43259
46689
  "deviceAdoption.release": [{
43260
46690
  name: "camDeviceId",
43261
46691
  form: "single",
@@ -44166,6 +47596,11 @@ Object.freeze({
44166
47596
  form: "single",
44167
47597
  optional: false
44168
47598
  }],
47599
+ "pipelineAnalytics.ownersWithMedia": [{
47600
+ name: "deviceId",
47601
+ form: "single",
47602
+ optional: false
47603
+ }],
44169
47604
  "pipelineAnalytics.proposeRetrainAnnotations": [{
44170
47605
  name: "deviceId",
44171
47606
  form: "single",
@@ -44636,6 +48071,16 @@ Object.freeze({
44636
48071
  form: "single",
44637
48072
  optional: true
44638
48073
  }],
48074
+ "recordingOnboard.getOptions": [{
48075
+ name: "deviceId",
48076
+ form: "single",
48077
+ optional: false
48078
+ }],
48079
+ "recordingOnboard.setSettings": [{
48080
+ name: "deviceId",
48081
+ form: "single",
48082
+ optional: false
48083
+ }],
44639
48084
  "recordingSignal.getStatus": [{
44640
48085
  name: "deviceId",
44641
48086
  form: "single",
@@ -44862,6 +48307,11 @@ Object.freeze({
44862
48307
  form: "single",
44863
48308
  optional: false
44864
48309
  }],
48310
+ "videoclips.dialClipStream": [{
48311
+ name: "deviceId",
48312
+ form: "single",
48313
+ optional: false
48314
+ }],
44865
48315
  "videoclips.getClipPlayback": [{
44866
48316
  name: "deviceId",
44867
48317
  form: "single",
@@ -44872,6 +48322,16 @@ Object.freeze({
44872
48322
  form: "single",
44873
48323
  optional: false
44874
48324
  }],
48325
+ "videoclips.listSources": [{
48326
+ name: "deviceId",
48327
+ form: "single",
48328
+ optional: false
48329
+ }],
48330
+ "videoclips.readClipBytes": [{
48331
+ name: "deviceId",
48332
+ form: "single",
48333
+ optional: false
48334
+ }],
44875
48335
  "waterHeater.setAway": [{
44876
48336
  name: "deviceId",
44877
48337
  form: "single",
@@ -44907,6 +48367,11 @@ Object.freeze({
44907
48367
  form: "single",
44908
48368
  optional: false
44909
48369
  }],
48370
+ "webrtcSession.getSessionDebug": [{
48371
+ name: "deviceId",
48372
+ form: "single",
48373
+ optional: false
48374
+ }],
44910
48375
  "webrtcSession.getSessionState": [{
44911
48376
  name: "deviceId",
44912
48377
  form: "single",
@@ -46633,6 +50098,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
46633
50098
  registerNativeCapabilities() {
46634
50099
  this.registerStreamCatalogProvider();
46635
50100
  this.registerSnapshotCap();
50101
+ this.registerAdminLinkCap();
46636
50102
  this.registerPtzCap();
46637
50103
  this.registerStreamParamsCap();
46638
50104
  this.registerMotionZonesCap();
@@ -47203,6 +50669,39 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
47203
50669
  invalidateCache: async () => {}
47204
50670
  });
47205
50671
  }
50672
+ /**
50673
+ * The camera's own web console. Amcrest is the same exact case as
50674
+ * Hikvision: `port` IS the HTTP CGI port, which is the port the web UI is
50675
+ * served on, and `https` is a real per-device flag — nothing is assumed.
50676
+ *
50677
+ * No credentials in the URL, although `username`/`password` are two fields
50678
+ * away in the same config object. See `buildDeviceAdminUrl`.
50679
+ */
50680
+ registerAdminLinkCap() {
50681
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
50682
+ if (deviceId !== this.id) throw new Error(`AmcrestCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
50683
+ const host = this.config.get("host") ?? "";
50684
+ const built = buildDeviceAdminUrl({
50685
+ host,
50686
+ port: this.config.get("port") ?? null,
50687
+ scheme: this.config.get("https") === true ? "https" : "http",
50688
+ path: "/"
50689
+ });
50690
+ if (!built.ok) {
50691
+ this.ctx.logger.debug("amcrest admin link unavailable", {
50692
+ tags: { deviceId: this.id },
50693
+ meta: { reason: built.reason }
50694
+ });
50695
+ return null;
50696
+ }
50697
+ return {
50698
+ url: built.url,
50699
+ label: "Web UI",
50700
+ target: "device",
50701
+ host
50702
+ };
50703
+ } });
50704
+ }
47206
50705
  /** Duration (ms) a one-shot `move` pulse runs before its safety
47207
50706
  * auto-stop fires. The `usePTZ` UI hook already issues its own paired
47208
50707
  * `stop` after a client-side pulse window; this timer is a backstop so