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