@camstack/addon-provider-onvif 1.2.113 → 1.2.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +3470 -33
  2. package/dist/addon.mjs +3470 -33
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -5364,7 +5364,7 @@ var ZodIssueCode = {
5364
5364
  var ZodFirstPartyTypeKind;
5365
5365
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5366
5366
  //#endregion
5367
- //#region ../types/dist/sleep-Bs3xPtSh.mjs
5367
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
5368
5368
  /**
5369
5369
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5370
5370
  * window to float samples (D455).
@@ -6506,6 +6506,17 @@ var CamProfileSchema = _enum([
6506
6506
  "mid",
6507
6507
  "low"
6508
6508
  ]);
6509
+ /**
6510
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
6511
+ *
6512
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
6513
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
6514
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
6515
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
6516
+ * the operator picked. Today's one user is the audio analyser, whose rule is
6517
+ * "the lowest AVAILABLE native rung" (D537).
6518
+ */
6519
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
6509
6520
  var CamStreamKindSchema = _enum([
6510
6521
  "pull-rtsp",
6511
6522
  "pull-rtmp",
@@ -6924,6 +6935,18 @@ function systemMethod(input, output, options) {
6924
6935
  systemOnly: true
6925
6936
  };
6926
6937
  }
6938
+ /**
6939
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
6940
+ * on `InferProvider`. The `providerOptional: true` literal is what
6941
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
6942
+ * for when this is the honest answer and when it is a soft stub.
6943
+ */
6944
+ function optionalMethod(input, output, options) {
6945
+ return {
6946
+ ...method(input, output, options),
6947
+ providerOptional: true
6948
+ };
6949
+ }
6927
6950
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
6928
6951
  var VersionOutputSchema$1 = object({ version: string() });
6929
6952
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -7350,6 +7373,8 @@ var EVENT_OWNER_TYPES = [
7350
7373
  * nothing failing until a caller asked.
7351
7374
  */
7352
7375
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7376
+ /** The same list as a Zod enum, for the cap input that carries it. */
7377
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7353
7378
  new Set(EVENT_OWNER_TYPES);
7354
7379
  var EncodeProfileSchema = object({
7355
7380
  video: object({
@@ -7722,6 +7747,7 @@ var CameraSwitchIdSchema = _enum([
7722
7747
  "device-audio",
7723
7748
  "broker-audio",
7724
7749
  "audio-analysis",
7750
+ "event-saving",
7725
7751
  "recording",
7726
7752
  "notifications"
7727
7753
  ]);
@@ -7757,10 +7783,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7757
7783
  var CameraSwitchUnavailableReasonSchema = _enum([
7758
7784
  "no-provider",
7759
7785
  "source-unreachable",
7760
- "not-configured",
7761
7786
  "device-disabled"
7762
7787
  ]);
7763
7788
  /**
7789
+ * Something true about a switch that is nonetheless OFFERED.
7790
+ *
7791
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
7792
+ * whole point: a reason replaces the control, a warning travels WITH it. The
7793
+ * privacy mask taught us the difference — greying the one button that starts
7794
+ * the feature teaches the operator the feature is broken, while a live toggle
7795
+ * that flips nothing teaches them the switch is broken. Neither is true; the
7796
+ * honest surface is a working toggle that says what is missing.
7797
+ */
7798
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
7799
+ /**
7764
7800
  * One switch, resolved for one camera.
7765
7801
  *
7766
7802
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -7779,6 +7815,11 @@ var CameraSwitchSchema = object({
7779
7815
  /** False = do not render a control. `unavailableReason` says why. */
7780
7816
  available: boolean(),
7781
7817
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7818
+ /**
7819
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
7820
+ * never a substitute for one.
7821
+ */
7822
+ warning: CameraSwitchWarningSchema.optional(),
7782
7823
  /** Current state. Meaningless when `available` is false — read it as `true`. */
7783
7824
  enabled: boolean(),
7784
7825
  authority: CameraSwitchAuthoritySchema
@@ -8196,11 +8237,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
8196
8237
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
8197
8238
  * span wraps past midnight (handled by the band engine).
8198
8239
  */
8240
+ /**
8241
+ * Whether a band's footage carries sound.
8242
+ *
8243
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
8244
+ * camera recorded sound whether anyone wanted it or not. The broker already
8245
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
8246
+ * restream — so this was a choice the product could always have offered.
8247
+ *
8248
+ * It belongs on the BAND rather than the camera because one camera can want
8249
+ * both: silent across the shop floor during opening hours, sound overnight.
8250
+ * And it applies to BOTH modes — an events band firing on motion has the same
8251
+ * question to answer as a continuous one.
8252
+ */
8253
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
8199
8254
  var RecordingBandSchema = object({
8200
8255
  days: array(RecordingWeekdaySchema),
8201
8256
  start: string().regex(HHMM),
8202
8257
  end: string().regex(HHMM),
8203
8258
  mode: RecordingBandModeSchema,
8259
+ /**
8260
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
8261
+ * written before this existed carried sound, and a band whose key is
8262
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
8263
+ */
8264
+ audio: RecordingBandAudioSchema.optional(),
8204
8265
  triggers: RecordingBandTriggersSchema.optional(),
8205
8266
  /**
8206
8267
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -13246,6 +13307,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13246
13307
  auth: "admin"
13247
13308
  });
13248
13309
  /**
13310
+ * device-admin-link — "this device has a management page of its own, and here
13311
+ * is its address".
13312
+ *
13313
+ * ## Why this is not a `deviceConfig` cap
13314
+ *
13315
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13316
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13317
+ * patch back through a setter; it costs a `builderId` reducer in
13318
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13319
+ * renders a form section. This cap answers ONE question with ONE read and
13320
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13321
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13322
+ * `reboot`, the other pure-RPC device-native cap.
13323
+ *
13324
+ * ## Absent, and the difference between "no page" and "we cannot say"
13325
+ *
13326
+ * The two are answered at DIFFERENT layers, on purpose:
13327
+ *
13328
+ * - **"We cannot say"** → the provider never registers the cap for that
13329
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13330
+ * fan are reached only through a vendor cloud; there is no address to hand
13331
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13332
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13333
+ * behind it. None of them register, so `deviceManager.getBindings` never
13334
+ * lists the cap and no surface asks.
13335
+ * - **"This device has no page, and I know that"** → the provider registers
13336
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13337
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13338
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13339
+ * transport, a Home Assistant broker authenticated by supervisor token
13340
+ * (which carries no `baseUrl` at all).
13341
+ *
13342
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13343
+ * button, and D62 is the same rule from the other side: an off switch is
13344
+ * reported off, never made to look broken. There is no third state where the
13345
+ * UI renders a disabled button "because the device might have a page".
13346
+ *
13347
+ * ## The URL never carries credentials
13348
+ *
13349
+ * Not in userinfo, not in a query string. Every provider builds through
13350
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13351
+ * scheme and path as separate arguments — there is no parameter a secret could
13352
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13353
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13354
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13355
+ * keeps providers from hand-rolling one anyway.
13356
+ *
13357
+ * This matters here more than anywhere else in the repo, because every provider
13358
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13359
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13360
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13361
+ * camera's own page will ask for its own login. That is correct, and pre-
13362
+ * filling it is the operator's business, not ours.
13363
+ *
13364
+ * ## It is a LAN fact
13365
+ *
13366
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13367
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13368
+ * as a link the operator's own browser follows, on the operator's own network,
13369
+ * or renders nothing.
13370
+ */
13371
+ /**
13372
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13373
+ * before clicking whether he is about to land on a camera's own web server or
13374
+ * inside Home Assistant.
13375
+ */
13376
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13377
+ var DeviceAdminLinkSchema = object({
13378
+ /**
13379
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13380
+ * free of userinfo and of any credential-shaped query key.
13381
+ */
13382
+ url: string(),
13383
+ /**
13384
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13385
+ * The PROVIDER names it, because only the provider knows what the page is;
13386
+ * a UI that invented the label from the addon id would call the Home
13387
+ * Assistant device page "Provider Homeassistant".
13388
+ */
13389
+ label: string(),
13390
+ target: AdminLinkTargetEnum,
13391
+ /**
13392
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13393
+ * an operator can see WHERE the button goes before he follows it. Redundant
13394
+ * with `url` by construction; carried separately so no surface has to parse
13395
+ * a URL to show it.
13396
+ */
13397
+ host: string()
13398
+ });
13399
+ var deviceAdminLinkCapability = {
13400
+ name: "device-admin-link",
13401
+ scope: "device",
13402
+ deviceNative: true,
13403
+ mode: "singleton",
13404
+ methods: {
13405
+ /**
13406
+ * The device's management page, or `null` when this device has none.
13407
+ *
13408
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13409
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13410
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13411
+ * The URL is also a statement about the LAN, which a household member with
13412
+ * a `view` grant on a light has no reason to be handed.
13413
+ *
13414
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13415
+ * without the right loses the query and draws nothing, which is the same
13416
+ * thing a device with no page draws. There is no path on which a button
13417
+ * appears and then fails — the D403 failure mode, from the other end.
13418
+ */
13419
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13420
+ };
13421
+ /**
13422
+ * Query keys that may never appear on an admin link. A credential smuggled as
13423
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13424
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13425
+ * identically.
13426
+ */
13427
+ var CREDENTIAL_QUERY_KEYS = [
13428
+ "password",
13429
+ "passwd",
13430
+ "pwd",
13431
+ "pass",
13432
+ "user",
13433
+ "username",
13434
+ "usr",
13435
+ "login",
13436
+ "token",
13437
+ "access_token",
13438
+ "auth",
13439
+ "authorization",
13440
+ "apikey",
13441
+ "api_key",
13442
+ "secret",
13443
+ "credential",
13444
+ "credentials",
13445
+ "session",
13446
+ "sessionid",
13447
+ "key"
13448
+ ];
13449
+ /**
13450
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13451
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13452
+ * structurally identical on purpose: a URL this function returns is a URL that
13453
+ * guard would pass.
13454
+ */
13455
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13456
+ /** A host that is safe to place in an authority component verbatim. */
13457
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13458
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13459
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13460
+ function defaultPortFor(scheme) {
13461
+ return scheme === "https" ? 443 : 80;
13462
+ }
13463
+ /**
13464
+ * Build the admin-page URL, or refuse by name.
13465
+ *
13466
+ * The refusal is never thrown: a provider answering "no page for this device"
13467
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13468
+ * a missing host on one device into a failed query for the whole surface.
13469
+ */
13470
+ function buildDeviceAdminUrl(parts) {
13471
+ const host = parts.host.trim();
13472
+ if (host.length === 0) return {
13473
+ ok: false,
13474
+ reason: "empty-host"
13475
+ };
13476
+ if (host.includes("@")) return {
13477
+ ok: false,
13478
+ reason: "host-carries-userinfo"
13479
+ };
13480
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13481
+ ok: false,
13482
+ reason: "host-not-plain"
13483
+ };
13484
+ if (parts.port !== null) {
13485
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13486
+ ok: false,
13487
+ reason: "port-out-of-range"
13488
+ };
13489
+ }
13490
+ if (!parts.path.startsWith("/")) return {
13491
+ ok: false,
13492
+ reason: "path-not-absolute"
13493
+ };
13494
+ const query = parts.query ?? {};
13495
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13496
+ ok: false,
13497
+ reason: "credential-query-key"
13498
+ };
13499
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13500
+ const search = new URLSearchParams();
13501
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13502
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13503
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13504
+ if (CREDENTIAL_URL.test(url)) return {
13505
+ ok: false,
13506
+ reason: "userinfo-in-result"
13507
+ };
13508
+ return {
13509
+ ok: true,
13510
+ url
13511
+ };
13512
+ }
13513
+ /**
13249
13514
  * `device-export` — collection cap for addons that export camstack
13250
13515
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
13251
13516
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -19934,6 +20199,7 @@ var MediaFileKindEnum = _enum([
19934
20199
  "keyFrameSmall",
19935
20200
  "thumbnailSmall",
19936
20201
  "confirmationCrop",
20202
+ "debugCrop",
19937
20203
  "sceneFrame"
19938
20204
  ]);
19939
20205
  /**
@@ -21013,7 +21279,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21013
21279
  ownerType: EventOwnerTypeSchema,
21014
21280
  eventId: number().int(),
21015
21281
  deviceId: number()
21016
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21282
+ }), array(MediaFileInfoSchema).readonly()), method(object({
21283
+ deviceId: number(),
21284
+ ownerKind: MediaPresenceOwnerKindSchema,
21285
+ ownerIds: array(string()).max(5e3)
21286
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21017
21287
  kind: "mutation",
21018
21288
  auth: "admin"
21019
21289
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -21598,6 +21868,18 @@ var RunnerCameraConfigSchema = object({
21598
21868
  motionStreamId: string(),
21599
21869
  detectionStreamId: string(),
21600
21870
  /**
21871
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
21872
+ * AVAILABLE native rung) or an explicit one.
21873
+ *
21874
+ * Not consumed by the runner: carried here so it travels with the camera's
21875
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
21876
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
21877
+ * with no default — absent means `auto`, which is what every camera written
21878
+ * before this field had, and an absent optional adds nothing to the
21879
+ * generated cap-input defaults (D537).
21880
+ */
21881
+ audioStreamPick: AudioStreamPickSchema.optional(),
21882
+ /**
21601
21883
  * Motion sources that can activate the pipeline. At least one must
21602
21884
  * be present. Multiple sources OR-ed: any one firing arms the
21603
21885
  * cooldown timer and transitions watching → active. The same
@@ -22668,10 +22950,9 @@ var ServerBootModeSchema = _enum([
22668
22950
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
22669
22951
  * - `pending-restart` — a version is staged and the node has NOT yet
22670
22952
  * restarted onto it (still running the OLD version).
22671
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
22672
- * (it is the active probation boot) and is waiting to confirm boot-health.
22673
- * Apply/rollback are refused in this state and the node must NOT be
22674
- * manually restarted, or the probation boot auto-rolls-back.
22953
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
22954
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
22955
+ * no provider emits it.
22675
22956
  */
22676
22957
  var ServerUpdateStateSchema = _enum([
22677
22958
  "idle",
@@ -22760,12 +23041,35 @@ var ServerRollbackInfoSchema = object({
22760
23041
  var ServerPackageStatusSchema = object({
22761
23042
  /** Root package name (`@camstack/server` on the hub). */
22762
23043
  packageName: string(),
22763
- /** Version of the code the running process ACTUALLY loaded. */
23044
+ /**
23045
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
23046
+ * package.json of the copy the code was loaded from, and never re-read. A
23047
+ * copy replaced on disk under a live process must not move this number;
23048
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
23049
+ * was a lazy disk read that would have followed such a swap silently.
23050
+ */
22764
23051
  runningVersion: string().nullable(),
22765
23052
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
22766
23053
  nodeRuntimeVersion: string().nullable(),
22767
23054
  /** Active data-dir root version; null when booted from seed/workspace. */
22768
23055
  activeVersion: string().nullable(),
23056
+ /**
23057
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
23058
+ * what the next boot would load if nothing is staged. Normally equal to
23059
+ * `runningVersion`; differs only when the copy on disk changed under the
23060
+ * live process. `null` when there is no data-root copy (baked seed or
23061
+ * workspace boot). Optional for version skew: an older provider omits it.
23062
+ */
23063
+ installedVersion: string().nullable().optional(),
23064
+ /**
23065
+ * True when the code that would run after a restart is NOT the code running
23066
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
23067
+ * from `runningVersion`. Derived on every read, never stored. This is the
23068
+ * one field that lets an operator or a script say "1.2.353 is installed and
23069
+ * will run after a restart" instead of reading it off a number that means
23070
+ * "installed". Optional for version skew.
23071
+ */
23072
+ restartRequired: boolean().optional(),
22769
23073
  /** N-1 version kept for rollback; null when no previous version exists. */
22770
23074
  previousVersion: string().nullable(),
22771
23075
  /** Version of the immutable baked seed closure (image fallback). */
@@ -22800,19 +23104,71 @@ var ServerPackageStatusSchema = object({
22800
23104
  });
22801
23105
  var ServerUpdateCheckResultSchema = object({
22802
23106
  packageName: string(),
23107
+ /** Same meaning as on the status payload: what THIS process loaded. */
22803
23108
  runningVersion: string().nullable(),
22804
23109
  latestVersion: string().nullable(),
23110
+ /**
23111
+ * `latestVersion` is newer than `runningVersion` AND is not the version
23112
+ * already staged on this node. A staged-and-waiting version is not "an
23113
+ * update available" — applying it again is refused (`already-staged`), and
23114
+ * a button that announced "updating…" on this boolean then got refused
23115
+ * would be lying. It is reported through `pendingVersion` instead.
23116
+ */
22805
23117
  updateAvailable: boolean(),
22806
23118
  checkedAtMs: number(),
22807
23119
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
22808
- error: string().nullable()
23120
+ error: string().nullable(),
23121
+ /**
23122
+ * Version staged on this node and awaiting its restart, when one is. Before
23123
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
23124
+ * node whose staged closure had never booted, and the caller had no way to
23125
+ * see the difference. Optional for version skew.
23126
+ */
23127
+ pendingVersion: string().nullable().optional(),
23128
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
23129
+ restartRequired: boolean().optional()
22809
23130
  });
23131
+ /**
23132
+ * What an action DID — the discriminant `accepted` cannot carry. Before
23133
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
23134
+ * where nothing was wrong (already current, already staged) and for a staging
23135
+ * install that died on `npm error code ECONNRESET`; the only difference was
23136
+ * prose in `message`, and an operator restarted a container on the strength of
23137
+ * it, booting the previous version.
23138
+ *
23139
+ * - `staged` — the closure is installed and validated; the node
23140
+ * restarts to apply it (`restarting:true`). NOTHING
23141
+ * runs the new code until that restart completes.
23142
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
23143
+ * - `already-current` — the target IS the running version; nothing done.
23144
+ * - `refused` — dropped by a gate; `reason` names which.
23145
+ * - `failed` — attempted and did not land; `error` says why.
23146
+ */
23147
+ var ServerUpdateOutcomeSchema = _enum([
23148
+ "staged",
23149
+ "restart-scheduled",
23150
+ "already-current",
23151
+ "refused",
23152
+ "failed"
23153
+ ]);
22810
23154
  var ServerUpdateActionResultSchema = object({
22811
23155
  accepted: boolean(),
22812
23156
  targetVersion: string().nullable(),
22813
23157
  /** True when a graceful restart was scheduled to apply the change. */
22814
23158
  restarting: boolean(),
22815
- message: string()
23159
+ message: string(),
23160
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
23161
+ outcome: ServerUpdateOutcomeSchema.optional(),
23162
+ /**
23163
+ * Stable, greppable cause for `refused` / `failed` (the engine's
23164
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
23165
+ */
23166
+ reason: string().nullable().optional(),
23167
+ /**
23168
+ * The failure itself, verbatim enough to act on (the npm output for a
23169
+ * staging install). Non-null only for `failed`.
23170
+ */
23171
+ error: string().nullable().optional()
22816
23172
  });
22817
23173
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
22818
23174
  kind: "mutation",
@@ -24025,6 +24381,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
24025
24381
  kind: "mutation",
24026
24382
  auth: "admin"
24027
24383
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
24384
+ _enum([
24385
+ "queue-full",
24386
+ "camera-backoff",
24387
+ "sleeping",
24388
+ "camera-refused",
24389
+ "no-keyframe",
24390
+ "no-catalog-row",
24391
+ "unsupported",
24392
+ "unknown-device",
24393
+ "uid-missing"
24394
+ ]);
24395
+ _enum(["deferred", "final"]);
24028
24396
  var ClipSchema = object({
24029
24397
  /** Opaque, provider-namespaced id. The default provider encodes the time
24030
24398
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -24099,29 +24467,455 @@ var ClipSchema = object({
24099
24467
  holes: array(object({
24100
24468
  startMs: number(),
24101
24469
  endMs: number()
24102
- })).optional()
24470
+ })).optional(),
24471
+ /**
24472
+ * A URL that MAY produce this clip's still, asked only for rows actually on
24473
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
24474
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
24475
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
24476
+ * — a surface latches that refusal to the instant rather than retrying.
24477
+ *
24478
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
24479
+ */
24480
+ thumbnailMint: string().optional(),
24481
+ /**
24482
+ * Why no still will be produced for this clip RIGHT NOW — set when the
24483
+ * provider already knows, so the surface draws the glyph and the reason
24484
+ * instead of firing a mint that cannot succeed.
24485
+ *
24486
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
24487
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
24488
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
24489
+ * inside no file is the measured case).
24490
+ * `unsupported` — this source cannot mint stills at all.
24491
+ */
24492
+ thumbnailUnavailable: object({ reason: _enum([
24493
+ "sleeping",
24494
+ "camera-refused",
24495
+ "no-keyframe",
24496
+ "unsupported"
24497
+ ]) }).optional(),
24498
+ /**
24499
+ * When the catalog this row came from was last CONFIRMED against the device.
24500
+ * Absent means "this row was read live". A persisted catalog served while a
24501
+ * camera sleeps carries the age it really has — a cached list is never drawn
24502
+ * as current (D549 13).
24503
+ */
24504
+ catalogAsOf: number().optional(),
24505
+ /**
24506
+ * The camera's OWN type strings for this clip, all of them, unmapped
24507
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
24508
+ * because a firmware inventing a type must not vanish: the mapping into our
24509
+ * filter vocabulary is lossy on purpose and this is the lossless copy
24510
+ * (D549 20).
24511
+ */
24512
+ nativeTypes: array(string()).optional(),
24513
+ /**
24514
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
24515
+ * travels BESIDE a class, never instead of one, and the class filter ignores
24516
+ * it: "a person crossed a line" is still a person (D549 20).
24517
+ */
24518
+ behaviours: array(string()).optional(),
24519
+ /**
24520
+ * The same recording as two files — the sub twin (what the row and its
24521
+ * thumbnail are) and its main twin, matched at listing time so a quality
24522
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
24523
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
24524
+ */
24525
+ streams: object({
24526
+ sub: object({
24527
+ id: string(),
24528
+ bytes: number().optional()
24529
+ }).optional(),
24530
+ main: object({
24531
+ id: string(),
24532
+ bytes: number().optional()
24533
+ }).optional()
24534
+ }).optional(),
24535
+ /** The camera says it holds a sub-stream copy of this recording. */
24536
+ supportSub: boolean().optional(),
24537
+ /**
24538
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
24539
+ * IS a file is playable, and only a source that lists EVENTS can produce a
24540
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
24541
+ * fell inside no file). Such a row is shown, never dropped and never offered
24542
+ * as playable-then-failing (D549 19).
24543
+ */
24544
+ playable: boolean().optional(),
24545
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
24546
+ unplayableReason: string().optional(),
24547
+ /**
24548
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
24549
+ * is not its end.
24550
+ *
24551
+ * Absent — the common case — means the row is closed and its `endMs` is the
24552
+ * end of the recording. Present and `true` means the source told us the file
24553
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
24554
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
24555
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
24556
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
24557
+ * on growing. A surface that drew 29 s there was lying about a clip that
24558
+ * plays for minutes, on the one row the operator looks at first.
24559
+ *
24560
+ * `endMs` is deliberately still a number: it is the best bound anything has
24561
+ * for a mint window or a byte fetch, and every consumer already requires it.
24562
+ * This flag says what it is WORTH, and a duration is not drawn from it.
24563
+ */
24564
+ inProgress: boolean().optional()
24103
24565
  });
24104
24566
  var ClipPlaybackSchema = object({
24105
- /** HLS master URL through the hub data-plane (Range + token in path). */
24567
+ /**
24568
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
24569
+ * says what kind — an HLS master playlist for a recording-derived clip, a
24570
+ * progressive MP4 for a native file the vendor addon muxed and serves with
24571
+ * `Range`. The URL is NOT a credential: both planes are registered
24572
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
24573
+ * the session cookie, so nothing is appended to it (D549 22).
24574
+ */
24106
24575
  playbackUrl: string(),
24576
+ /**
24577
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
24578
+ * existing consumer already assumes. A player that branches on this is the
24579
+ * one change a native clip needs; a player that ignores it will hand an MP4
24580
+ * to hls.js and fail parsing it as a manifest.
24581
+ */
24582
+ format: _enum(["hls", "mp4"]).optional(),
24583
+ /**
24584
+ * Which twin was actually served. A clip that holds only one stream answers
24585
+ * with the one it has, and the surface SAYS so — a missing main twin is
24586
+ * never served silently as if it were the asked-for quality (D549 15).
24587
+ */
24588
+ served: CamProfileSchema.optional(),
24107
24589
  /** Optional LAN/remote alternates for the same clip. */
24108
24590
  playbackEndpoints: array(string()).optional(),
24109
24591
  token: string().optional()
24110
24592
  });
24593
+ /**
24594
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
24595
+ *
24596
+ * `ok` is the only state whose clip list may be read as complete. The other
24597
+ * three exist because an empty list from a sleeping camera reads as "this
24598
+ * camera has no recordings", which is the defect this whole line of work is
24599
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
24600
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
24601
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
24602
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
24603
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
24604
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
24605
+ * files on both streams and both filters, with 177 GB free). That last one is
24606
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
24607
+ * which days it asked for and got nothing — "no clips" would be a lie about a
24608
+ * card that is full of them.
24609
+ */
24610
+ /**
24611
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
24612
+ *
24613
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
24614
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
24615
+ * `force` flag, and nothing else… a background caller must never set it…
24616
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
24617
+ * prefetch never set it; a surface sets it only behind the same confirm the
24618
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
24619
+ * exactly as that gesture is.
24620
+ *
24621
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
24622
+ * (D549) — a read that opened a session and then checked would have woken the
24623
+ * camera to find out it was not allowed to.
24624
+ *
24625
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
24626
+ * time the camera is awake for its own reasons — is deliberately not a member:
24627
+ * it was proposed, it is free on the battery, and the operator declined it on
24628
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
24629
+ * D558 "Considered and not taken").
24630
+ */
24631
+ var ClipWakeSchema = _enum(["authorised"]);
24632
+ /**
24633
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
24634
+ *
24635
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
24636
+ * and `served` says which twin the caller actually got.
24637
+ */
24638
+ var ClipBytesSchema = object({
24639
+ base64: string(),
24640
+ contentType: string(),
24641
+ /** Suggested filename, extension included. */
24642
+ name: string(),
24643
+ bytes: number().int().nonnegative(),
24644
+ /**
24645
+ * Which twin was actually served — the same contract
24646
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
24647
+ * export record persists it: a row read a week later must say the same thing
24648
+ * the panel said at the moment of the tap. A missing main twin is never
24649
+ * served silently as if it were the asked-for quality (D549 15).
24650
+ */
24651
+ served: CamProfileSchema,
24652
+ /**
24653
+ * The DECODED duration of the delivered file, when the fetch measured one.
24654
+ *
24655
+ * A clip fetch verifies its own completion against the catalog row and
24656
+ * retries a materially short pass (D568); this is that measurement, carried
24657
+ * so a consumer can say the same thing rather than re-deriving it. Absent
24658
+ * when the producer did not measure — never zero, which would say the file
24659
+ * is empty.
24660
+ */
24661
+ durationMs: number().positive().optional()
24662
+ });
24663
+ /**
24664
+ * Where a clip's STREAM can be dialled (D597) — the answer to
24665
+ * {@link videoclipsCapability.methods.dialClipStream}.
24666
+ *
24667
+ * `url` is plain `http://` on the provider's own token-gated listener, one
24668
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
24669
+ * path: that plane takes only a user credential, which an addon does not
24670
+ * hold, and the framework offers no addon→addon byte transport — the one
24671
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
24672
+ * a consumer dials directly, and this is that precedent one addon over.
24673
+ */
24674
+ /**
24675
+ * The audio a clip stream can be asked for, and the audio it carries.
24676
+ *
24677
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
24678
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
24679
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
24680
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
24681
+ * negotiates — and `none` is a stream asked for, or served, without sound.
24682
+ */
24683
+ var ClipStreamAudioSchema = _enum([
24684
+ "opus",
24685
+ "pcmu",
24686
+ "none"
24687
+ ]);
24688
+ /**
24689
+ * Why a stream that was ASKED for sound is not carrying it (D600).
24690
+ *
24691
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
24692
+ * means the ask was honoured, or there was no ask.
24693
+ *
24694
+ * - `not-requested` — the caller asked for `none`.
24695
+ * - `unsupported-ask` — the provider cannot produce that codec.
24696
+ *
24697
+ * Whether the RECORDING has sound at all is not knowable here: the camera
24698
+ * declares no audio track before its first frame arrives. That answer is in
24699
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
24700
+ * there. A dial that says `opus` promises the codec, never the content.
24701
+ */
24702
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
24703
+ var ClipStreamDialSchema = object({
24704
+ url: string().min(1),
24705
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
24706
+ expiresAtMs: number(),
24707
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
24708
+ transport: literal("forward-only"),
24709
+ /**
24710
+ * Which twin the stream ACTUALLY carries — not what was asked for.
24711
+ *
24712
+ * The stream serves every profile (D599), and a recording whose main twin
24713
+ * does not exist is streamed as its sub copy, which this field reports as
24714
+ * `low`. That is the whole reason the dial no longer refuses `high`:
24715
+ * stating what was served is what makes accepting honest, where a silent
24716
+ * substitution would be the 33x lie D576 forbids. Stated rather than
24717
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
24718
+ */
24719
+ served: CamProfileSchema,
24720
+ /**
24721
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
24722
+ * sound, and the same reasoning (D599, D600).
24723
+ *
24724
+ * The broker asks for the codec its WebRTC session negotiated, and the
24725
+ * provider either muxes that codec or carries none. Stating it is what
24726
+ * lets the broker wire the matching sink before the first byte, and what
24727
+ * keeps a session from being handed a codec it cannot send — the audio
24728
+ * shape of the substitution `served` exists to prevent.
24729
+ *
24730
+ * It is a promise about the CODEC, not about the content: a recording with
24731
+ * no microphone track answers `opus` here and then arrives with no audio
24732
+ * track at all. That second answer is in the stream, and the broker reads
24733
+ * it from the `moov`.
24734
+ */
24735
+ servedAudio: ClipStreamAudioSchema,
24736
+ /** Why `servedAudio` is `none` although sound was asked for. */
24737
+ audioReason: ClipStreamAudioReasonSchema.optional()
24738
+ });
24739
+ var ClipSourceAvailabilitySchema = object({
24740
+ state: _enum([
24741
+ "ok",
24742
+ "sleeping",
24743
+ "unreachable",
24744
+ "no-storage",
24745
+ "index-empty"
24746
+ ]),
24747
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
24748
+ reason: string().optional(),
24749
+ /** When this source's catalog was last CONFIRMED. A cached list is never
24750
+ * drawn as current: the surface shows the age whenever it is older than the
24751
+ * refresh interval. */
24752
+ catalogAsOf: number().optional()
24753
+ });
24754
+ /**
24755
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
24756
+ * every clip id from it is prefixed with (`analytics`,
24757
+ * `native:reolink:onboard`, `hksv`, …).
24758
+ *
24759
+ * A provider lists the sources IT serves for that device, and answers for each
24760
+ * of them whether it can answer at all. A provider with nothing to offer on a
24761
+ * camera returns `[]` — it is not that camera's business.
24762
+ */
24763
+ var ClipSourceSchema = object({
24764
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
24765
+ * of every clip id it mints. `getClipPlayback` routes on it. */
24766
+ source: string(),
24767
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
24768
+ label: string(),
24769
+ /**
24770
+ * The addon that SERVES this row.
24771
+ *
24772
+ * A surface cannot otherwise resolve a source to who answers it, and the
24773
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
24774
+ * widget — is a second authority on provider identity living in the one
24775
+ * package with no business knowing it, wrong the day a third source appears
24776
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
24777
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
24778
+ * served a hub child two of them until the two views were measured to be one
24779
+ * store read twice (D555) and then read ONE way (D565).
24780
+ *
24781
+ * Optional for version skew only. The collection dispatcher stamps it from
24782
+ * the registry, so a row that travelled through the fan-out carries the
24783
+ * authoritative id whatever the provider filled in.
24784
+ */
24785
+ addonId: string().optional(),
24786
+ availability: ClipSourceAvailabilitySchema
24787
+ });
24111
24788
  DeviceType.Camera, method(object({
24112
24789
  deviceId: number(),
24113
24790
  since: number(),
24114
24791
  until: number(),
24115
- limit: number().int().positive().optional()
24792
+ limit: number().int().positive().optional(),
24793
+ /**
24794
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
24795
+ * carries, never a source id and never a list. **Required** (D554 amended).
24796
+ *
24797
+ * A provider the device is not bound to is refused by name rather than
24798
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
24799
+ *
24800
+ * It was optional, documented as "absent means the device's BOUND
24801
+ * provider, which is CamStack on every camera". No code implemented
24802
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
24803
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
24804
+ * answered SIX rows, three from each source, merged newest-first:
24805
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
24806
+ * so absence bought the union this method exists not to be, and
24807
+ * `limit` meant `limit × sources`.
24808
+ *
24809
+ * There is nothing to restore the default to. `getBindings` answers a
24810
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
24811
+ * `setWrapperActive` — the singleton authority that could name one —
24812
+ * throws for a collection cap by design. Naming CamStack here instead
24813
+ * would privilege one addon by id inside a surface whose premise is
24814
+ * that sources are peers, and would be wrong on the first camera with
24815
+ * no recorder binding. So absence is REFUSED, in the schema, where the
24816
+ * generated types make it unomittable rather than merely discouraged.
24817
+ *
24818
+ * The default belongs to the SURFACE, which has the `listSources` rows
24819
+ * and can say which one it picked (D569 § 1–3; the viewer already
24820
+ * always sends this).
24821
+ *
24822
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
24823
+ * assumed the answer was a fan-out over everything a camera has. The
24824
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
24825
+ * solo"* — so the list is asked of one provider at a time and there is
24826
+ * nothing to filter out of it.
24827
+ */
24828
+ provider: string().min(1)
24116
24829
  }), array(ClipSchema).readonly(), {
24117
24830
  kind: "query",
24118
- auth: "admin"
24831
+ auth: "protected"
24832
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
24833
+ kind: "query",
24834
+ auth: "protected"
24119
24835
  }), method(object({
24120
24836
  deviceId: number(),
24121
- clipId: string()
24837
+ clipId: string(),
24838
+ /**
24839
+ * Which twin to serve, on the ONE quality scale the system already has
24840
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
24841
+ * twin; both ids are already on the row so this never re-searches the
24842
+ * camera. Absent means the provider's own default (the sub file, which
24843
+ * every source is measured to hold).
24844
+ *
24845
+ * `auto` is deliberately NOT accepted here: a stored file has no
24846
+ * broker session, so the adaptive tier cannot be resolved for it. The
24847
+ * viewer resolves `auto` to a profile the same way live does, before
24848
+ * it calls (D549 15).
24849
+ */
24850
+ profile: CamProfileSchema.optional()
24122
24851
  }), ClipPlaybackSchema, {
24123
24852
  kind: "query",
24124
- auth: "admin"
24853
+ auth: "protected"
24854
+ }), optionalMethod(object({
24855
+ deviceId: number(),
24856
+ clipId: string().min(1),
24857
+ /**
24858
+ * WHICH provider holds the bytes — the `addonId` a
24859
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
24860
+ * `listClips` states one method above and for a second one this method
24861
+ * learned the hard way.
24862
+ *
24863
+ * The caller is the stream broker, and the broker lives in
24864
+ * `addon-pipeline` — the same addon that registers the RECORDER's
24865
+ * videoclips provider. With no provider named, the in-process
24866
+ * resolution handed it its own sibling, which implements this method
24867
+ * solely to explain that an analytics visit has no file behind it. So
24868
+ * every onboard clip answered with that sentence, a flash of video and
24869
+ * nothing more, while the SAME call from outside the addon reached the
24870
+ * right provider and returned the bytes (measured on 3628,
24871
+ * 2026-09-21).
24872
+ */
24873
+ provider: string().min(1),
24874
+ /**
24875
+ * Which twin to fetch, on the mapping D549 15 already fixed:
24876
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
24877
+ * accepted here for the same reason it is not accepted by
24878
+ * `getClipPlayback` — a stored file has no broker session, so the
24879
+ * adaptive tier cannot be resolved for it.
24880
+ */
24881
+ profile: CamProfileSchema.optional(),
24882
+ /**
24883
+ * The CALLER's byte bound, so an over-size clip is refused before it is
24884
+ * read and encoded rather than after. Capped by
24885
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
24886
+ * that ceiling.
24887
+ */
24888
+ maxBytes: number().int().positive().optional(),
24889
+ /**
24890
+ * The operator's authorisation to wake a sleeping camera for this
24891
+ * read. Absent — the default — means a sleeping standalone battery
24892
+ * camera is REFUSED by name, before any session is opened.
24893
+ */
24894
+ wake: ClipWakeSchema.optional()
24895
+ }), ClipBytesSchema, {
24896
+ kind: "query",
24897
+ auth: "protected"
24898
+ }), optionalMethod(object({
24899
+ deviceId: number(),
24900
+ clipId: string().min(1),
24901
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
24902
+ provider: string().min(1),
24903
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
24904
+ profile: CamProfileSchema.optional(),
24905
+ /**
24906
+ * Which audio codec the caller's session negotiated, so the provider
24907
+ * can mux it (D600). The ask travels on the DIAL because the mux is
24908
+ * spawned once, before the first byte, and cannot gain a track later.
24909
+ *
24910
+ * ABSENT IS `none`, never a guess: a caller that does not state an
24911
+ * ask is served the video-only stream that shipped first, so a broker
24912
+ * older than this field keeps working unchanged (D382 — an unstated
24913
+ * ask is zero).
24914
+ */
24915
+ audio: ClipStreamAudioSchema.optional()
24916
+ }), ClipStreamDialSchema, {
24917
+ kind: "query",
24918
+ auth: "protected"
24125
24919
  });
24126
24920
  /**
24127
24921
  * Optional client-side hints sent at session creation to help the provider
@@ -24193,6 +24987,104 @@ var WebrtcStreamChoiceSchema = object({
24193
24987
  /** Bitrate in kbps (null for adaptive). */
24194
24988
  bitrateKbps: number().nullable()
24195
24989
  });
24990
+ /**
24991
+ * The last move the adaptive ladder made on this session, and the numbers it
24992
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
24993
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
24994
+ *
24995
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
24996
+ * no window to report, which is NOT a link with zero loss.
24997
+ */
24998
+ var WebrtcLadderDecisionSchema = object({
24999
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
25000
+ * stopped moving; `healthy` — a promotion. */
25001
+ cause: _enum([
25002
+ "loss",
25003
+ "stall",
25004
+ "healthy"
25005
+ ]),
25006
+ /** The rung the ladder moved TO. Null for a move that changed no rung
25007
+ * (an MTU reduction, a forced transcode-down at the bottom). */
25008
+ tier: CamProfileSchema.nullable(),
25009
+ /** Whether that move forced the egress transcode-down rung. */
25010
+ forcedTranscodeDown: boolean(),
25011
+ /** Loss ratio over the tracker's window at the moment of the decision. */
25012
+ lossRatio: number().nullable(),
25013
+ /** The client's reported downlink at that moment, in kbps. */
25014
+ clientKbps: number().nullable(),
25015
+ /** How long the stall detector had been seeing nothing arrive. */
25016
+ stalledForMs: number().nullable(),
25017
+ /** Whether the broker WAS sending while that was measured — the half that
25018
+ * separates "the camera stopped" from "the link stopped". */
25019
+ serverSending: boolean().nullable(),
25020
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25021
+ * must not read like one from this second. */
25022
+ at: number()
25023
+ });
25024
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25025
+ var WebrtcTierFailureSchema = object({
25026
+ tier: CamProfileSchema,
25027
+ failures: number().int().nonnegative()
25028
+ });
25029
+ /**
25030
+ * What the SERVER decided for one WebRTC session — the half the client cannot
25031
+ * measure. See `getSessionDebug`.
25032
+ */
25033
+ var WebrtcSessionDebugSchema = object({
25034
+ sessionId: string(),
25035
+ /** Whether the session still exists here. Everything below is null when
25036
+ * it does not — a dead session has no measurements, not zero ones. */
25037
+ alive: boolean(),
25038
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
25039
+ observedAt: number(),
25040
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
25041
+ brokerId: string().nullable(),
25042
+ camStreamId: string().nullable(),
25043
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
25044
+ * another stream); `native` = the camera's own encode. */
25045
+ sourceKind: _enum([
25046
+ "native",
25047
+ "derived",
25048
+ "unknown"
25049
+ ]),
25050
+ /** Whether the ladder is running at all (an `adaptive` target). */
25051
+ adaptive: boolean(),
25052
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
25053
+ * is bound to no profile slot (a manually-activated stream). */
25054
+ servedTier: CamProfileSchema.nullable(),
25055
+ /** Which node ingests the source. Null when unresolved. */
25056
+ ingestNodeId: string().nullable(),
25057
+ /** Whether that node is a remote one (the media crosses a node boundary
25058
+ * before it reaches this session). Null when unresolved. */
25059
+ ingestIsRemote: boolean().nullable(),
25060
+ /** Codec off the source, and the one negotiated with this client. */
25061
+ sourceCodec: string().nullable(),
25062
+ negotiatedCodec: string().nullable(),
25063
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
25064
+ * source bytes untouched. */
25065
+ egressLeg: _enum([
25066
+ "copy",
25067
+ "transcode",
25068
+ "unknown"
25069
+ ]),
25070
+ /** What the broker itself reports about the rung it is serving. */
25071
+ brokerStatus: string().nullable(),
25072
+ brokerCodec: string().nullable(),
25073
+ brokerBitrateKbps: number().nullable(),
25074
+ brokerInputFps: number().nullable(),
25075
+ brokerUptimeMs: number().nullable(),
25076
+ /** RTP packets this session has sent — the server's own "am I sending?". */
25077
+ rtpPacketsSent: number().nullable(),
25078
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
25079
+ * is suspended there, so a frozen picture in `recorded` mode means
25080
+ * something entirely different. */
25081
+ trackMode: _enum(["live", "recorded"]).nullable(),
25082
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
25083
+ tierFailures: array(WebrtcTierFailureSchema),
25084
+ /** Successful promotions in this viewer session (drives the progressive
25085
+ * healthy timer). Null when no ladder is running. */
25086
+ upgradeCount: number().nullable()
25087
+ });
24196
25088
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
24197
25089
  deviceId: number().int().nonnegative(),
24198
25090
  target: WebrtcStreamTargetSchema,
@@ -24357,7 +25249,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24357
25249
  * the same answer as "it ended": either way nothing is holding it up.
24358
25250
  */
24359
25251
  alive: boolean()
24360
- }));
25252
+ })), method(object({
25253
+ deviceId: number().int().nonnegative(),
25254
+ sessionId: string()
25255
+ }), WebrtcSessionDebugSchema, { kind: "query" });
24361
25256
  object({
24362
25257
  /** All accessory children of the parent. */
24363
25258
  childDeviceIds: array(number()).readonly(),
@@ -25578,6 +26473,307 @@ DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema)
25578
26473
  auth: "admin"
25579
26474
  });
25580
26475
  /**
26476
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
26477
+ * writes to the CAMERA's own card, on the camera's own schedule.
26478
+ *
26479
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
26480
+ * footage ledger, our storage locations, our retention. This one has a
26481
+ * different authority — the camera's firmware — and per D62 it stores
26482
+ * nothing of its own. Every value here is read from the camera and every
26483
+ * write goes back to the camera; there is no CamStack-side mirror that
26484
+ * could disagree with the device.
26485
+ *
26486
+ * ## One shape, two firmwares
26487
+ *
26488
+ * Measured 2026-09-22 against the live fleet:
26489
+ *
26490
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
26491
+ * | --- | --- | --- |
26492
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
26493
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
26494
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
26495
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
26496
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
26497
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
26498
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
26499
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
26500
+ *
26501
+ * The two schedule models look different and are the same thing in
26502
+ * different coordinates: both answer "for this trigger, during which
26503
+ * weekly windows does the camera record". {@link RecordWindow} is that
26504
+ * question in one shape — Hikvision's ranges map straight onto it,
26505
+ * Reolink's mask expands into hour-aligned windows.
26506
+ *
26507
+ * ## Union, not intersection
26508
+ *
26509
+ * **The same fields exist on every camera.** What differs per device is
26510
+ * which VALUES that device accepts, and that is what {@link
26511
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
26512
+ * per field plus the schedule's own limits. A control a camera cannot
26513
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
26514
+ * dead: disabled must not look like broken.
26515
+ *
26516
+ * ## Refusal by name
26517
+ *
26518
+ * A write a camera cannot honour is refused with a sentence the operator
26519
+ * can read — never accepted and dropped. Both providers refuse through
26520
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
26521
+ * one test, not two hand-written vendor opinions.
26522
+ *
26523
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
26524
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
26525
+ * injected from `status`) reports the live values, and a single
26526
+ * `setSettings` mutation applies a partial change. No hand-written
26527
+ * settings-contribution methods.
26528
+ */
26529
+ /**
26530
+ * What makes the camera start recording during a window.
26531
+ *
26532
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
26533
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
26534
+ * object-class triggers are Reolink-only today and the smart-event ones
26535
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
26536
+ * firmwares measured — a camera that cannot record on a trigger simply
26537
+ * does not list it in `options.schedule.triggers`, and a window naming
26538
+ * it is REFUSED, not dropped.
26539
+ */
26540
+ var RecordTriggerSchema = _enum([
26541
+ "continuous",
26542
+ "motion",
26543
+ "person",
26544
+ "vehicle",
26545
+ "animal",
26546
+ "lineCrossing",
26547
+ "intrusion",
26548
+ "loitering",
26549
+ "alarmInput"
26550
+ ]);
26551
+ /**
26552
+ * One weekly recording window: "on `day`, from `startMinute` to
26553
+ * `endMinute`, record on `trigger`".
26554
+ *
26555
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
26556
+ * both firmwares enumerate). Minutes are local camera time since
26557
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
26558
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
26559
+ * collapsing it to 0 would turn a whole-day window into an empty one.
26560
+ */
26561
+ var RecordWindowSchema = object({
26562
+ trigger: RecordTriggerSchema,
26563
+ day: number().int().min(0).max(6),
26564
+ startMinute: number().int().min(0).max(1439),
26565
+ endMinute: number().int().min(1).max(1440)
26566
+ });
26567
+ /** Status of one physical volume, as the camera itself describes it. */
26568
+ var OnboardStorageVolumeSchema = object({
26569
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
26570
+ id: string(),
26571
+ /** The camera's own name for it, when it gives one (`hddName`). */
26572
+ label: string().optional(),
26573
+ status: _enum([
26574
+ "ok",
26575
+ "unformatted",
26576
+ "error",
26577
+ "offline",
26578
+ "unknown"
26579
+ ]),
26580
+ /**
26581
+ * Total size in MB, or **null when the camera did not say**.
26582
+ *
26583
+ * Never 0 for an unreadable value: a measurement that failed is not a
26584
+ * measurement (D393), and a card whose size is unknown must not be
26585
+ * rendered as a card of size zero.
26586
+ */
26587
+ capacityMb: number().nullable(),
26588
+ /**
26589
+ * Free space in MB, or null when unknown.
26590
+ *
26591
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
26592
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
26593
+ * card converges on once it has wrapped. At loop steady state the
26594
+ * number is identical whether the camera recorded yesterday or stopped
26595
+ * a month ago.
26596
+ */
26597
+ freeMb: number().nullable(),
26598
+ /** True when the camera reports the volume writable (`property` RW). */
26599
+ writable: boolean().optional()
26600
+ });
26601
+ object({
26602
+ storage: discriminatedUnion("kind", [
26603
+ object({
26604
+ kind: literal("present"),
26605
+ volumes: array(OnboardStorageVolumeSchema)
26606
+ }),
26607
+ object({
26608
+ kind: literal("absent"),
26609
+ reason: string()
26610
+ }),
26611
+ object({
26612
+ kind: literal("unknown"),
26613
+ reason: string()
26614
+ })
26615
+ ]),
26616
+ tracks: array(object({
26617
+ id: string(),
26618
+ enabled: boolean(),
26619
+ isVideo: boolean(),
26620
+ /** From the camera's own track description. Null when it does not say. */
26621
+ codec: string().nullable(),
26622
+ resolution: string().nullable(),
26623
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
26624
+ overwriteWhenFull: boolean().nullable()
26625
+ })),
26626
+ /**
26627
+ * The track the write path targets — the enabled VIDEO one. Null when
26628
+ * no track could be identified, which is itself a refusal reason.
26629
+ */
26630
+ primaryTrackId: string().nullable(),
26631
+ /** Master "record to the card at all" switch. */
26632
+ enabled: boolean().nullable(),
26633
+ overwriteWhenFull: boolean().nullable(),
26634
+ preRecordSec: number().nullable(),
26635
+ postRecordSec: number().nullable(),
26636
+ /** Length of one recorded file, in minutes. */
26637
+ segmentMinutes: number().nullable(),
26638
+ /** The primary track's weekly windows, flattened. */
26639
+ windows: array(RecordWindowSchema),
26640
+ /**
26641
+ * How many windows the camera described that CamStack could NOT read —
26642
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
26643
+ * name.
26644
+ *
26645
+ * A dropped window is work the reader threw away, and a schedule that
26646
+ * silently shows fewer rows than the camera holds is how an operator
26647
+ * saves back a schedule shorter than the one they were looking at
26648
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
26649
+ * that replaces it would delete what was not shown — which is why a
26650
+ * provider reporting a non-zero count also reports the schedule as not
26651
+ * writable.
26652
+ */
26653
+ unreadableWindows: number(),
26654
+ /**
26655
+ * The camera is scheduled to record and has NO usable storage.
26656
+ *
26657
+ * A first-class fact because it is the fleet's most common silent
26658
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
26659
+ * to a card that is not there. Neither the schedule nor the storage
26660
+ * read says anything wrong on its own; only the pair does.
26661
+ */
26662
+ recordingToNowhere: boolean(),
26663
+ lastFetchedAt: number()
26664
+ });
26665
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
26666
+ var RangeSchema = object({
26667
+ min: number(),
26668
+ max: number(),
26669
+ step: number()
26670
+ });
26671
+ /**
26672
+ * The values a camera actually takes for a numeric field, when they are a SET
26673
+ * rather than a range.
26674
+ *
26675
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
26676
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
26677
+ *
26678
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
26679
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
26680
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
26681
+ *
26682
+ * Neither is expressible as a step: the first has a sentinel two billion away
26683
+ * from its neighbours, the second doubles and then jumps. A range that tried
26684
+ * would forbid values the camera takes AND permit values it silently replaces
26685
+ * with 5 — wrong in both directions at once.
26686
+ *
26687
+ * `sentinel` names the member that is not a duration, so a surface can render
26688
+ * "no limit" instead of `2147483647` seconds.
26689
+ */
26690
+ var AllowedValuesSchema = object({
26691
+ values: array(number()).min(1),
26692
+ sentinel: object({
26693
+ value: number(),
26694
+ meaning: _enum(["no-limit", "disabled"])
26695
+ }).optional()
26696
+ });
26697
+ /**
26698
+ * Per-field availability on ONE camera.
26699
+ *
26700
+ * The field exists on every camera — this says whether this one can be
26701
+ * read and whether it can be written, and `reason` says why not when
26702
+ * either is false. The UI renders the control DISABLED with the reason
26703
+ * rather than hiding it, so a limitation is legible instead of looking
26704
+ * like a missing feature.
26705
+ */
26706
+ var OnboardFieldSupportSchema = object({
26707
+ readable: boolean(),
26708
+ writable: boolean(),
26709
+ /** Required whenever `readable` or `writable` is false. */
26710
+ reason: string().optional()
26711
+ });
26712
+ /** What this camera's schedule model can express. */
26713
+ var OnboardScheduleSupportSchema = object({
26714
+ support: OnboardFieldSupportSchema,
26715
+ /**
26716
+ * The smallest time step the camera can express, in minutes.
26717
+ *
26718
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
26719
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
26720
+ * window whose edges are not a multiple of this is REFUSED rather than
26721
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
26722
+ * and nothing says so.
26723
+ */
26724
+ granularityMinutes: number(),
26725
+ /** Triggers this camera can record on. A window naming another is refused. */
26726
+ triggers: array(RecordTriggerSchema),
26727
+ /**
26728
+ * False when the camera stores ONE trigger per time range, so two
26729
+ * windows overlapping on the same day cannot carry different triggers.
26730
+ * True on Reolink, whose mask is per-trigger and independent.
26731
+ */
26732
+ supportsOverlappingTriggers: boolean()
26733
+ });
26734
+ var RecordingOnboardOptionsSchema = object({
26735
+ enabled: OnboardFieldSupportSchema,
26736
+ overwriteWhenFull: OnboardFieldSupportSchema,
26737
+ preRecordSec: OnboardFieldSupportSchema,
26738
+ preRecordSecRange: RangeSchema.optional(),
26739
+ /** Preferred over the range when the camera takes a SET, not a span. */
26740
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
26741
+ postRecordSec: OnboardFieldSupportSchema,
26742
+ postRecordSecRange: RangeSchema.optional(),
26743
+ /** Preferred over the range when the camera takes a SET, not a span. */
26744
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
26745
+ segmentMinutes: OnboardFieldSupportSchema,
26746
+ segmentMinutesRange: RangeSchema.optional(),
26747
+ /** Preferred over the range when the camera takes a SET, not a span. */
26748
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
26749
+ schedule: OnboardScheduleSupportSchema
26750
+ });
26751
+ /**
26752
+ * A partial change. Every field optional.
26753
+ *
26754
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
26755
+ * silently ignore a field it cannot support — it refuses, by name,
26756
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
26757
+ * is the failure D62 exists to prevent: the operator believes the camera
26758
+ * is recording the way the form says, and it is not.
26759
+ */
26760
+ var RecordingOnboardPatchSchema = object({
26761
+ enabled: boolean().optional(),
26762
+ overwriteWhenFull: boolean().optional(),
26763
+ preRecordSec: number().optional(),
26764
+ postRecordSec: number().optional(),
26765
+ segmentMinutes: number().optional(),
26766
+ /** The complete new window set for the primary track — not a delta. */
26767
+ windows: array(RecordWindowSchema).optional()
26768
+ });
26769
+ DeviceType.Camera, method(object({ deviceId: number() }), RecordingOnboardOptionsSchema), method(object({
26770
+ deviceId: number(),
26771
+ settings: RecordingOnboardPatchSchema
26772
+ }), _void(), {
26773
+ kind: "mutation",
26774
+ auth: "admin"
26775
+ });
26776
+ /**
25581
26777
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
25582
26778
  * for every device, regardless of provider — the kernel needs a uniform
25583
26779
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -27381,11 +28577,15 @@ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
27381
28577
  *
27382
28578
  * Adding a fifth name to that list would have been the wrong fix twice over:
27383
28579
  * that page is per-camera DETECTION tuning, and a grid's geometry belongs
27384
- * beside PTZ and motion zones on the camera itself — which is exactly where the
27385
- * framework already puts a widget, when the widget is declared on a CAPABILITY.
27386
- * `deviceConfig.ui` is the mechanism (`motion-zones`, `ptz`, `ptz-autotrack`
27387
- * all use it): the framework emits the structural section and the widget
27388
- * self-persists through the cap's own mutations.
28580
+ * beside PTZ and motion zones on the camera itself. The device page is
28581
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
28582
+ * and this cap carries its section the way `recording` does, by RETURNING it
28583
+ * from `getDeviceSettingsContribution`.
28584
+ *
28585
+ * Seven other widgets are still declared the other way, through a
28586
+ * `deviceConfig.ui` block the framework derives a section from. That route
28587
+ * gives the addon no say in where its own panel lands and no way to decline
28588
+ * for a device the panel does not suit, which is why this one does not use it.
27389
28589
  *
27390
28590
  * ## Why one addon may implement it
27391
28591
  *
@@ -27441,16 +28641,37 @@ var GridProfileOfferSchema = object({
27441
28641
  ]),
27442
28642
  offered: boolean(),
27443
28643
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
27444
- missingSources: array(number().int().positive())
28644
+ missingSources: array(number().int().positive()),
28645
+ /**
28646
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
28647
+ * offered. DERIVED from the cells and the sources' own size at this profile —
28648
+ * it is reported because nothing else in the system would ever say what the
28649
+ * grid came out as, and because it is the number an operator would otherwise
28650
+ * expect to type.
28651
+ */
28652
+ canvas: string(),
28653
+ /**
28654
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
28655
+ *
28656
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
28657
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
28658
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
28659
+ * Default is `mid` + `low`.
28660
+ */
28661
+ published: boolean()
27445
28662
  });
27446
28663
  var GridLayoutViewSchema = object({
27447
28664
  /** The persisted grid row this camera was declared from. */
27448
28665
  instanceId: string(),
27449
28666
  deviceId: number().int().nonnegative(),
27450
28667
  name: string(),
27451
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
27452
- width: number().int(),
27453
- height: number().int(),
28668
+ /**
28669
+ * NO canvas size. A grid's resolution is not authored: each profile derives
28670
+ * its own from the cells and its sources' dimensions. The two numbers that
28671
+ * used to be here were a text field that silently decided both how much the
28672
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
28673
+ * came out as.
28674
+ */
27454
28675
  fps: number().int(),
27455
28676
  cells: array(GridLayoutCellSchema),
27456
28677
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -27459,9 +28680,13 @@ var GridLayoutViewSchema = object({
27459
28680
  var GridLayoutPatchSchema = object({
27460
28681
  deviceId: number().int().nonnegative(),
27461
28682
  name: string().min(1).max(160).optional(),
27462
- width: number().int().min(160).max(7680).optional(),
27463
- height: number().int().min(120).max(4320).optional(),
27464
28683
  fps: number().int().min(1).max(60).optional(),
28684
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
28685
+ publishedProfiles: array(_enum([
28686
+ "high",
28687
+ "mid",
28688
+ "low"
28689
+ ])).max(3).optional(),
27465
28690
  /**
27466
28691
  * The whole cell list at once. A per-cell patch would need an ordering the
27467
28692
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -29786,7 +31011,7 @@ method(object({
29786
31011
  auth: "admin"
29787
31012
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
29788
31013
  kind: "query",
29789
- auth: "admin"
31014
+ auth: "protected"
29790
31015
  }), method(object({
29791
31016
  deviceId: number(),
29792
31017
  profile: string(),
@@ -30016,13 +31241,101 @@ var ExportStateSchema = _enum([
30016
31241
  "expired",
30017
31242
  "deleted"
30018
31243
  ]);
30019
- /** One export job / history row. */
31244
+ /**
31245
+ * WHAT an export is — the authority, as opposed to the four top-level fields
31246
+ * the Library sorts and labels on (D558 § 2.2).
31247
+ *
31248
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
31249
+ * field is optional for the history rows written before it existed, and that
31250
+ * absence has exactly one interpreter.
31251
+ */
31252
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
31253
+ kind: literal("footage"),
31254
+ deviceId: number(),
31255
+ profiles: array(string()).min(1),
31256
+ fromMs: number(),
31257
+ toMs: number()
31258
+ }), object({
31259
+ kind: literal("clip"),
31260
+ deviceId: number(),
31261
+ provider: string().min(1),
31262
+ source: string().min(1),
31263
+ sourceLabel: string().min(1),
31264
+ clipId: string().min(1),
31265
+ /**
31266
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
31267
+ * already listing when the operator picked the segment.
31268
+ *
31269
+ * It is **not** a time range control and it never becomes one: the record's
31270
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
31271
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
31272
+ * `catalog-miss`, not a silently wider search. It exists because
31273
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
31274
+ * the catalog check D558 asks for is literally that call, and a call needs a
31275
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
31276
+ * wall-clock day, which is also the only width measured to be cheap (one
31277
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
31278
+ * of one child's events took 3.6–5.4 s).
31279
+ */
31280
+ catalogWindow: object({
31281
+ sinceMs: number(),
31282
+ untilMs: number()
31283
+ }),
31284
+ profile: _enum([
31285
+ "high",
31286
+ "mid",
31287
+ "low"
31288
+ ]),
31289
+ /**
31290
+ * The operator's authorisation to wake a sleeping camera for this export.
31291
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
31292
+ * because the gate that honours it is the clip provider's sleep gate — a
31293
+ * second enum here would be a second contract. Absent by default; never
31294
+ * settable by a scheduler or a retry.
31295
+ *
31296
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
31297
+ * an operator act that asks again; a queued job that outlived its wake fails
31298
+ * with a reason rather than waking on its turn; and this subject carries
31299
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
31300
+ */
31301
+ wake: ClipWakeSchema.optional()
31302
+ })]);
31303
+ /**
31304
+ * One export job / history row.
31305
+ *
31306
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
31307
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
31308
+ * Library sorts and labels on them (`library-items.ts` orders an export by
31309
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
31310
+ * that did not fill them would sort under the epoch and render a blank range.
31311
+ * Write to the subject and read from the projection and the two will disagree;
31312
+ * the projection is derived at creation and never edited afterwards.
31313
+ *
31314
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
31315
+ * who knows only the recording export will get wrong:
31316
+ *
31317
+ * | field | `kind:'footage'` | `kind:'clip'` |
31318
+ * | --- | --- | --- |
31319
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
31320
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
31321
+ *
31322
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
31323
+ * two authorities, D224's second copy).
31324
+ */
30020
31325
  var ExportRecordSchema = object({
30021
31326
  id: string(),
30022
31327
  deviceId: number(),
30023
31328
  profile: string(),
30024
31329
  fromMs: number(),
30025
31330
  toMs: number(),
31331
+ /**
31332
+ * What this export IS. Optional ONLY for the rows written before D558: the
31333
+ * store parses every row through this schema on every read, so a required
31334
+ * field would make the export AUDIT — which is the whole reason rows survive
31335
+ * file deletion — unreadable in one release. Absence means `footage`, and
31336
+ * {@link exportSubjectOf} is the one place that says so.
31337
+ */
31338
+ subject: ExportSubjectSchema.optional(),
30026
31339
  options: ExportOptionsSchema,
30027
31340
  state: ExportStateSchema,
30028
31341
  /** 0–100 while rendering; null otherwise. */
@@ -30039,6 +31352,18 @@ var ExportRecordSchema = object({
30039
31352
  /** Failure reason when state is 'failed'; null otherwise. */
30040
31353
  error: string().nullable()
30041
31354
  });
31355
+ _enum([
31356
+ "catalog-miss",
31357
+ "catalog-unreachable",
31358
+ "no-file-for-window",
31359
+ "clip-in-progress",
31360
+ "unsupported-option",
31361
+ "sleeping",
31362
+ "camera-refused",
31363
+ "fetch-failed",
31364
+ "too-large-to-transfer",
31365
+ "wake-expired"
31366
+ ]);
30042
31367
  /** Candidate download URLs (LAN first, then operator extra hosts). */
30043
31368
  var ExportDownloadSchema = object({
30044
31369
  url: string(),
@@ -30057,16 +31382,50 @@ var ExportBytesSchema = object({
30057
31382
  name: string(),
30058
31383
  bytes: number().int().nonnegative()
30059
31384
  });
31385
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
31386
+ function resolveExportProfiles(input) {
31387
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
31388
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
31389
+ return [];
31390
+ }
30060
31391
  method(object({
30061
31392
  deviceId: number(),
30062
31393
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
30063
31394
  profile: string().optional(),
30064
31395
  profiles: array(string()).min(1).optional(),
30065
- fromMs: number(),
30066
- toMs: number(),
31396
+ /** Footage only — a clip's boundaries are the camera's. */
31397
+ fromMs: number().optional(),
31398
+ toMs: number().optional(),
31399
+ /** What to export. Absent means the legacy flat footage request. */
31400
+ subject: ExportSubjectSchema.optional(),
30067
31401
  options: ExportOptionsSchema
30068
31402
  }).superRefine((v, ctx) => {
30069
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
31403
+ if (v.subject?.kind === "clip") {
31404
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
31405
+ code: ZodIssueCode.custom,
31406
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
31407
+ path: ["subject", "deviceId"]
31408
+ });
31409
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
31410
+ code: ZodIssueCode.custom,
31411
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
31412
+ path: ["fromMs"]
31413
+ });
31414
+ return;
31415
+ }
31416
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
31417
+ code: ZodIssueCode.custom,
31418
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
31419
+ path: ["subject", "deviceId"]
31420
+ });
31421
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
31422
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
31423
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
31424
+ code: ZodIssueCode.custom,
31425
+ message: "a footage export needs fromMs and toMs",
31426
+ path: ["fromMs"]
31427
+ });
31428
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
30070
31429
  code: ZodIssueCode.custom,
30071
31430
  message: "pass profiles[] (min 1) or legacy profile",
30072
31431
  path: ["profiles"]
@@ -32633,6 +33992,1329 @@ var BaseDeviceProvider = class extends BaseAddon {
32633
33992
  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;
32634
33993
  new Set(Object.values(DeviceType));
32635
33994
  DeviceFeature.BatteryOperated;
33995
+ /**
33996
+ * The vocabulary, in the order a picker offers it.
33997
+ *
33998
+ * Adding a key here is not enough to make it pickable: both glyph tables must
33999
+ * gain it too, and their tests fail until they do. That is the intended
34000
+ * friction — it is the only thing that keeps the two surfaces honest.
34001
+ */
34002
+ var DISPLAY_ICON_VOCABULARY = [
34003
+ {
34004
+ key: "camera",
34005
+ group: "device",
34006
+ synonyms: [
34007
+ "telecamera",
34008
+ "videocamera",
34009
+ "camera",
34010
+ "cam"
34011
+ ]
34012
+ },
34013
+ {
34014
+ key: "network",
34015
+ group: "device",
34016
+ synonyms: [
34017
+ "rete",
34018
+ "network",
34019
+ "lan"
34020
+ ]
34021
+ },
34022
+ {
34023
+ key: "toggle-left",
34024
+ group: "device",
34025
+ synonyms: [
34026
+ "interruttore",
34027
+ "switch",
34028
+ "toggle"
34029
+ ]
34030
+ },
34031
+ {
34032
+ key: "toggle-right",
34033
+ group: "device",
34034
+ synonyms: [
34035
+ "interruttore",
34036
+ "switch",
34037
+ "toggle",
34038
+ "acceso"
34039
+ ]
34040
+ },
34041
+ {
34042
+ key: "circle-dot",
34043
+ group: "device",
34044
+ synonyms: [
34045
+ "pulsante",
34046
+ "bottone",
34047
+ "button",
34048
+ "dot"
34049
+ ]
34050
+ },
34051
+ {
34052
+ key: "refresh-cw",
34053
+ group: "device",
34054
+ synonyms: [
34055
+ "aggiorna",
34056
+ "ricarica",
34057
+ "refresh",
34058
+ "reload",
34059
+ "ciclo"
34060
+ ]
34061
+ },
34062
+ {
34063
+ key: "box",
34064
+ group: "device",
34065
+ synonyms: [
34066
+ "scatola",
34067
+ "box",
34068
+ "contenitore"
34069
+ ]
34070
+ },
34071
+ {
34072
+ key: "scroll-text",
34073
+ group: "device",
34074
+ synonyms: [
34075
+ "registro",
34076
+ "log",
34077
+ "testo"
34078
+ ]
34079
+ },
34080
+ {
34081
+ key: "workflow",
34082
+ group: "device",
34083
+ synonyms: [
34084
+ "automazione",
34085
+ "flusso",
34086
+ "workflow",
34087
+ "scenario"
34088
+ ]
34089
+ },
34090
+ {
34091
+ key: "lock",
34092
+ group: "device",
34093
+ synonyms: [
34094
+ "serratura",
34095
+ "chiave",
34096
+ "lucchetto",
34097
+ "chiuso",
34098
+ "locked"
34099
+ ]
34100
+ },
34101
+ {
34102
+ key: "lock-open",
34103
+ group: "device",
34104
+ synonyms: [
34105
+ "serratura",
34106
+ "aperta",
34107
+ "sbloccato",
34108
+ "unlocked"
34109
+ ]
34110
+ },
34111
+ {
34112
+ key: "blinds",
34113
+ group: "device",
34114
+ synonyms: [
34115
+ "tapparella",
34116
+ "persiana",
34117
+ "serranda",
34118
+ "avvolgibile",
34119
+ "shutter",
34120
+ "blind",
34121
+ "veneziana"
34122
+ ]
34123
+ },
34124
+ {
34125
+ key: "air-vent",
34126
+ group: "device",
34127
+ synonyms: [
34128
+ "bocchetta",
34129
+ "aria",
34130
+ "ventilazione",
34131
+ "condizionatore",
34132
+ "vent"
34133
+ ]
34134
+ },
34135
+ {
34136
+ key: "monitor-play",
34137
+ group: "device",
34138
+ synonyms: [
34139
+ "schermo",
34140
+ "riproduzione",
34141
+ "player",
34142
+ "display"
34143
+ ]
34144
+ },
34145
+ {
34146
+ key: "sliders-horizontal",
34147
+ group: "device",
34148
+ synonyms: [
34149
+ "regolazione",
34150
+ "cursori",
34151
+ "impostazioni",
34152
+ "slider"
34153
+ ]
34154
+ },
34155
+ {
34156
+ key: "map-pin",
34157
+ group: "device",
34158
+ synonyms: [
34159
+ "posizione",
34160
+ "luogo",
34161
+ "mappa",
34162
+ "pin"
34163
+ ]
34164
+ },
34165
+ {
34166
+ key: "bot",
34167
+ group: "device",
34168
+ synonyms: [
34169
+ "robot",
34170
+ "aspirapolvere",
34171
+ "bot"
34172
+ ]
34173
+ },
34174
+ {
34175
+ key: "tractor",
34176
+ group: "device",
34177
+ synonyms: [
34178
+ "trattore",
34179
+ "tosaerba",
34180
+ "giardino"
34181
+ ]
34182
+ },
34183
+ {
34184
+ key: "image",
34185
+ group: "device",
34186
+ synonyms: [
34187
+ "immagine",
34188
+ "foto",
34189
+ "snapshot"
34190
+ ]
34191
+ },
34192
+ {
34193
+ key: "bone",
34194
+ group: "device",
34195
+ synonyms: ["osso", "cane"]
34196
+ },
34197
+ {
34198
+ key: "door-closed",
34199
+ group: "device",
34200
+ synonyms: [
34201
+ "porta",
34202
+ "chiusa",
34203
+ "contatto"
34204
+ ]
34205
+ },
34206
+ {
34207
+ key: "door-open",
34208
+ group: "device",
34209
+ synonyms: [
34210
+ "porta",
34211
+ "aperta",
34212
+ "contatto",
34213
+ "ingresso",
34214
+ "uscita"
34215
+ ]
34216
+ },
34217
+ {
34218
+ key: "crosshair",
34219
+ group: "device",
34220
+ synonyms: [
34221
+ "mirino",
34222
+ "puntamento",
34223
+ "centro"
34224
+ ]
34225
+ },
34226
+ {
34227
+ key: "eye",
34228
+ group: "device",
34229
+ synonyms: [
34230
+ "occhio",
34231
+ "visibile",
34232
+ "visto"
34233
+ ]
34234
+ },
34235
+ {
34236
+ key: "eye-off",
34237
+ group: "device",
34238
+ synonyms: [
34239
+ "privacy",
34240
+ "nascosto",
34241
+ "oscurato",
34242
+ "mascherato",
34243
+ "invisibile"
34244
+ ]
34245
+ },
34246
+ {
34247
+ key: "smartphone",
34248
+ group: "device",
34249
+ synonyms: [
34250
+ "telefono",
34251
+ "cellulare",
34252
+ "smartphone"
34253
+ ]
34254
+ },
34255
+ {
34256
+ key: "phone",
34257
+ group: "device",
34258
+ synonyms: ["telefono", "chiamata"]
34259
+ },
34260
+ {
34261
+ key: "tablet",
34262
+ group: "device",
34263
+ synonyms: ["tablet", "ipad"]
34264
+ },
34265
+ {
34266
+ key: "laptop",
34267
+ group: "device",
34268
+ synonyms: [
34269
+ "portatile",
34270
+ "laptop",
34271
+ "notebook"
34272
+ ]
34273
+ },
34274
+ {
34275
+ key: "monitor",
34276
+ group: "device",
34277
+ synonyms: ["monitor", "schermo"]
34278
+ },
34279
+ {
34280
+ key: "tv",
34281
+ group: "device",
34282
+ synonyms: ["televisore", "tv"]
34283
+ },
34284
+ {
34285
+ key: "speaker",
34286
+ group: "device",
34287
+ synonyms: [
34288
+ "altoparlante",
34289
+ "cassa",
34290
+ "speaker"
34291
+ ]
34292
+ },
34293
+ {
34294
+ key: "printer",
34295
+ group: "device",
34296
+ synonyms: ["stampante"]
34297
+ },
34298
+ {
34299
+ key: "projector",
34300
+ group: "device",
34301
+ synonyms: ["proiettore"]
34302
+ },
34303
+ {
34304
+ key: "gamepad-2",
34305
+ group: "device",
34306
+ synonyms: [
34307
+ "console",
34308
+ "gioco",
34309
+ "joystick"
34310
+ ]
34311
+ },
34312
+ {
34313
+ key: "radio",
34314
+ group: "device",
34315
+ synonyms: ["radio"]
34316
+ },
34317
+ {
34318
+ key: "router",
34319
+ group: "device",
34320
+ synonyms: ["router", "modem"]
34321
+ },
34322
+ {
34323
+ key: "bluetooth",
34324
+ group: "device",
34325
+ synonyms: ["bluetooth"]
34326
+ },
34327
+ {
34328
+ key: "cpu",
34329
+ group: "device",
34330
+ synonyms: [
34331
+ "processore",
34332
+ "cpu",
34333
+ "chip",
34334
+ "dispositivo"
34335
+ ]
34336
+ },
34337
+ {
34338
+ key: "hard-drive",
34339
+ group: "device",
34340
+ synonyms: [
34341
+ "disco",
34342
+ "hdd",
34343
+ "archiviazione",
34344
+ "storage"
34345
+ ]
34346
+ },
34347
+ {
34348
+ key: "server",
34349
+ group: "device",
34350
+ synonyms: ["server", "nas"]
34351
+ },
34352
+ {
34353
+ key: "mic",
34354
+ group: "device",
34355
+ synonyms: ["microfono", "audio"]
34356
+ },
34357
+ {
34358
+ key: "mic-off",
34359
+ group: "device",
34360
+ synonyms: [
34361
+ "microfono",
34362
+ "spento",
34363
+ "muto"
34364
+ ]
34365
+ },
34366
+ {
34367
+ key: "video",
34368
+ group: "device",
34369
+ synonyms: ["video"]
34370
+ },
34371
+ {
34372
+ key: "video-off",
34373
+ group: "device",
34374
+ synonyms: ["video", "spento"]
34375
+ },
34376
+ {
34377
+ key: "music",
34378
+ group: "device",
34379
+ synonyms: ["musica"]
34380
+ },
34381
+ {
34382
+ key: "plug",
34383
+ group: "device",
34384
+ synonyms: [
34385
+ "presa",
34386
+ "spina",
34387
+ "plug"
34388
+ ]
34389
+ },
34390
+ {
34391
+ key: "plug-zap",
34392
+ group: "device",
34393
+ synonyms: ["presa", "alimentata"]
34394
+ },
34395
+ {
34396
+ key: "power",
34397
+ group: "device",
34398
+ synonyms: [
34399
+ "accensione",
34400
+ "alimentazione",
34401
+ "power"
34402
+ ]
34403
+ },
34404
+ {
34405
+ key: "power-off",
34406
+ group: "device",
34407
+ synonyms: ["spegnimento", "spento"]
34408
+ },
34409
+ {
34410
+ key: "cable",
34411
+ group: "device",
34412
+ synonyms: ["cavo"]
34413
+ },
34414
+ {
34415
+ key: "battery",
34416
+ group: "device",
34417
+ synonyms: ["batteria"]
34418
+ },
34419
+ {
34420
+ key: "battery-charging",
34421
+ group: "device",
34422
+ synonyms: [
34423
+ "batteria",
34424
+ "carica",
34425
+ "ricarica"
34426
+ ]
34427
+ },
34428
+ {
34429
+ key: "battery-full",
34430
+ group: "device",
34431
+ synonyms: [
34432
+ "batteria",
34433
+ "carica",
34434
+ "piena"
34435
+ ]
34436
+ },
34437
+ {
34438
+ key: "battery-low",
34439
+ group: "device",
34440
+ synonyms: ["batteria", "scarica"]
34441
+ },
34442
+ {
34443
+ key: "key",
34444
+ group: "device",
34445
+ synonyms: ["chiave"]
34446
+ },
34447
+ {
34448
+ key: "key-round",
34449
+ group: "device",
34450
+ synonyms: ["chiave", "tonda"]
34451
+ },
34452
+ {
34453
+ key: "mailbox",
34454
+ group: "device",
34455
+ synonyms: [
34456
+ "cassetta",
34457
+ "postale",
34458
+ "posta"
34459
+ ]
34460
+ },
34461
+ {
34462
+ key: "package",
34463
+ group: "device",
34464
+ synonyms: ["pacco", "consegna"]
34465
+ },
34466
+ {
34467
+ key: "shield",
34468
+ group: "device",
34469
+ synonyms: [
34470
+ "scudo",
34471
+ "protezione",
34472
+ "sicurezza",
34473
+ "allarme"
34474
+ ]
34475
+ },
34476
+ {
34477
+ key: "shield-check",
34478
+ group: "device",
34479
+ synonyms: [
34480
+ "protetto",
34481
+ "sicuro",
34482
+ "armato"
34483
+ ]
34484
+ },
34485
+ {
34486
+ key: "shield-off",
34487
+ group: "device",
34488
+ synonyms: [
34489
+ "protezione",
34490
+ "disattivata",
34491
+ "disarmato"
34492
+ ]
34493
+ },
34494
+ {
34495
+ key: "briefcase",
34496
+ group: "device",
34497
+ synonyms: [
34498
+ "ufficio",
34499
+ "lavoro",
34500
+ "valigetta",
34501
+ "studio"
34502
+ ]
34503
+ },
34504
+ {
34505
+ key: "lightbulb",
34506
+ group: "lighting",
34507
+ synonyms: [
34508
+ "luce",
34509
+ "lampadina",
34510
+ "lampada",
34511
+ "light"
34512
+ ]
34513
+ },
34514
+ {
34515
+ key: "lamp",
34516
+ group: "lighting",
34517
+ synonyms: ["lampada", "abatjour"]
34518
+ },
34519
+ {
34520
+ key: "lamp-ceiling",
34521
+ group: "lighting",
34522
+ synonyms: [
34523
+ "plafoniera",
34524
+ "lampadario",
34525
+ "luce",
34526
+ "soffitto"
34527
+ ]
34528
+ },
34529
+ {
34530
+ key: "lamp-desk",
34531
+ group: "lighting",
34532
+ synonyms: [
34533
+ "lampada",
34534
+ "scrivania",
34535
+ "abatjour"
34536
+ ]
34537
+ },
34538
+ {
34539
+ key: "lamp-floor",
34540
+ group: "lighting",
34541
+ synonyms: [
34542
+ "piantana",
34543
+ "lampada",
34544
+ "terra"
34545
+ ]
34546
+ },
34547
+ {
34548
+ key: "flashlight",
34549
+ group: "lighting",
34550
+ synonyms: [
34551
+ "faretto",
34552
+ "torcia",
34553
+ "spot",
34554
+ "faro"
34555
+ ]
34556
+ },
34557
+ {
34558
+ key: "heater",
34559
+ group: "lighting",
34560
+ synonyms: [
34561
+ "riscaldamento",
34562
+ "termosifone",
34563
+ "stufa",
34564
+ "calore"
34565
+ ]
34566
+ },
34567
+ {
34568
+ key: "snowflake",
34569
+ group: "lighting",
34570
+ synonyms: [
34571
+ "freddo",
34572
+ "raffrescamento",
34573
+ "aria",
34574
+ "condizionata",
34575
+ "neve"
34576
+ ]
34577
+ },
34578
+ {
34579
+ key: "thermometer-sun",
34580
+ group: "lighting",
34581
+ synonyms: [
34582
+ "caldo",
34583
+ "temperatura",
34584
+ "alta"
34585
+ ]
34586
+ },
34587
+ {
34588
+ key: "thermometer-snowflake",
34589
+ group: "lighting",
34590
+ synonyms: [
34591
+ "freddo",
34592
+ "temperatura",
34593
+ "bassa"
34594
+ ]
34595
+ },
34596
+ {
34597
+ key: "refrigerator",
34598
+ group: "lighting",
34599
+ synonyms: ["frigorifero", "frigo"]
34600
+ },
34601
+ {
34602
+ key: "microwave",
34603
+ group: "lighting",
34604
+ synonyms: ["microonde", "forno"]
34605
+ },
34606
+ {
34607
+ key: "washing-machine",
34608
+ group: "lighting",
34609
+ synonyms: [
34610
+ "lavatrice",
34611
+ "lavastoviglie",
34612
+ "asciugatrice"
34613
+ ]
34614
+ },
34615
+ {
34616
+ key: "cooking-pot",
34617
+ group: "lighting",
34618
+ synonyms: [
34619
+ "pentola",
34620
+ "cucina",
34621
+ "fornello"
34622
+ ]
34623
+ },
34624
+ {
34625
+ key: "utensils",
34626
+ group: "lighting",
34627
+ synonyms: [
34628
+ "cucina",
34629
+ "posate",
34630
+ "sala",
34631
+ "pranzo"
34632
+ ]
34633
+ },
34634
+ {
34635
+ key: "thermometer",
34636
+ group: "sensing",
34637
+ synonyms: [
34638
+ "temperatura",
34639
+ "termometro",
34640
+ "termostato"
34641
+ ]
34642
+ },
34643
+ {
34644
+ key: "droplets",
34645
+ group: "sensing",
34646
+ synonyms: [
34647
+ "umidita",
34648
+ "acqua",
34649
+ "allagamento",
34650
+ "perdita"
34651
+ ]
34652
+ },
34653
+ {
34654
+ key: "droplet",
34655
+ group: "sensing",
34656
+ synonyms: [
34657
+ "goccia",
34658
+ "acqua",
34659
+ "perdita",
34660
+ "allagamento"
34661
+ ]
34662
+ },
34663
+ {
34664
+ key: "gauge",
34665
+ group: "sensing",
34666
+ synonyms: [
34667
+ "misura",
34668
+ "livello",
34669
+ "pressione",
34670
+ "indicatore"
34671
+ ]
34672
+ },
34673
+ {
34674
+ key: "gauge2",
34675
+ group: "sensing",
34676
+ synonyms: [
34677
+ "misura",
34678
+ "livello",
34679
+ "pressione",
34680
+ "indicatore"
34681
+ ],
34682
+ aliasOf: "gauge"
34683
+ },
34684
+ {
34685
+ key: "sun",
34686
+ group: "sensing",
34687
+ synonyms: [
34688
+ "sole",
34689
+ "luminosita",
34690
+ "giorno",
34691
+ "esterno"
34692
+ ]
34693
+ },
34694
+ {
34695
+ key: "sun-medium",
34696
+ group: "sensing",
34697
+ synonyms: ["sole", "luminosita"]
34698
+ },
34699
+ {
34700
+ key: "sunrise",
34701
+ group: "sensing",
34702
+ synonyms: ["alba"]
34703
+ },
34704
+ {
34705
+ key: "moon",
34706
+ group: "sensing",
34707
+ synonyms: [
34708
+ "notte",
34709
+ "luna",
34710
+ "notturna",
34711
+ "nightvision"
34712
+ ]
34713
+ },
34714
+ {
34715
+ key: "wind",
34716
+ group: "sensing",
34717
+ synonyms: [
34718
+ "vento",
34719
+ "aria",
34720
+ "gas",
34721
+ "monossido",
34722
+ "co"
34723
+ ]
34724
+ },
34725
+ {
34726
+ key: "cloud",
34727
+ group: "sensing",
34728
+ synonyms: ["nuvola", "nuvoloso"]
34729
+ },
34730
+ {
34731
+ key: "cloud-rain",
34732
+ group: "sensing",
34733
+ synonyms: ["pioggia"]
34734
+ },
34735
+ {
34736
+ key: "cloud-sun",
34737
+ group: "sensing",
34738
+ synonyms: [
34739
+ "variabile",
34740
+ "sole",
34741
+ "nuvole"
34742
+ ]
34743
+ },
34744
+ {
34745
+ key: "waves",
34746
+ group: "sensing",
34747
+ synonyms: [
34748
+ "onde",
34749
+ "acqua",
34750
+ "vibrazione"
34751
+ ]
34752
+ },
34753
+ {
34754
+ key: "sprout",
34755
+ group: "sensing",
34756
+ synonyms: [
34757
+ "irrigazione",
34758
+ "pianta",
34759
+ "germoglio",
34760
+ "orto"
34761
+ ]
34762
+ },
34763
+ {
34764
+ key: "leaf",
34765
+ group: "sensing",
34766
+ synonyms: [
34767
+ "foglia",
34768
+ "pianta",
34769
+ "verde"
34770
+ ]
34771
+ },
34772
+ {
34773
+ key: "flower",
34774
+ group: "sensing",
34775
+ synonyms: ["fiore", "pianta"]
34776
+ },
34777
+ {
34778
+ key: "zap",
34779
+ group: "sensing",
34780
+ synonyms: [
34781
+ "energia",
34782
+ "corrente",
34783
+ "consumo",
34784
+ "watt",
34785
+ "elettricita"
34786
+ ]
34787
+ },
34788
+ {
34789
+ key: "flame",
34790
+ group: "sensing",
34791
+ synonyms: [
34792
+ "fiamma",
34793
+ "gas",
34794
+ "fuoco",
34795
+ "incendio"
34796
+ ]
34797
+ },
34798
+ {
34799
+ key: "bell",
34800
+ group: "sensing",
34801
+ synonyms: [
34802
+ "campanello",
34803
+ "suoneria",
34804
+ "notifica",
34805
+ "bell",
34806
+ "chime"
34807
+ ]
34808
+ },
34809
+ {
34810
+ key: "bell-ring",
34811
+ group: "sensing",
34812
+ synonyms: [
34813
+ "campanello",
34814
+ "suona",
34815
+ "doorbell",
34816
+ "notifica"
34817
+ ]
34818
+ },
34819
+ {
34820
+ key: "bell-off",
34821
+ group: "sensing",
34822
+ synonyms: [
34823
+ "silenzioso",
34824
+ "notifiche",
34825
+ "disattivate",
34826
+ "muto"
34827
+ ]
34828
+ },
34829
+ {
34830
+ key: "siren",
34831
+ group: "sensing",
34832
+ synonyms: ["sirena", "allarme"]
34833
+ },
34834
+ {
34835
+ key: "radar",
34836
+ group: "sensing",
34837
+ synonyms: [
34838
+ "movimento",
34839
+ "presenza",
34840
+ "pir",
34841
+ "rilevamento",
34842
+ "radar"
34843
+ ]
34844
+ },
34845
+ {
34846
+ key: "activity",
34847
+ group: "sensing",
34848
+ synonyms: [
34849
+ "attivita",
34850
+ "sensore",
34851
+ "generico"
34852
+ ]
34853
+ },
34854
+ {
34855
+ key: "hash",
34856
+ group: "sensing",
34857
+ synonyms: [
34858
+ "numero",
34859
+ "contatore",
34860
+ "valore"
34861
+ ]
34862
+ },
34863
+ {
34864
+ key: "clock",
34865
+ group: "sensing",
34866
+ synonyms: [
34867
+ "orologio",
34868
+ "ora",
34869
+ "tempo"
34870
+ ]
34871
+ },
34872
+ {
34873
+ key: "timer",
34874
+ group: "sensing",
34875
+ synonyms: [
34876
+ "timer",
34877
+ "conto",
34878
+ "rovescia"
34879
+ ]
34880
+ },
34881
+ {
34882
+ key: "calendar",
34883
+ group: "sensing",
34884
+ synonyms: [
34885
+ "calendario",
34886
+ "data",
34887
+ "pianificazione"
34888
+ ]
34889
+ },
34890
+ {
34891
+ key: "pipette",
34892
+ group: "sensing",
34893
+ synonyms: [
34894
+ "qualita",
34895
+ "aria",
34896
+ "misura",
34897
+ "campione"
34898
+ ]
34899
+ },
34900
+ {
34901
+ key: "fan",
34902
+ group: "sensing",
34903
+ synonyms: [
34904
+ "ventilatore",
34905
+ "ventola",
34906
+ "aria"
34907
+ ]
34908
+ },
34909
+ {
34910
+ key: "bath",
34911
+ group: "sensing",
34912
+ synonyms: [
34913
+ "bagno",
34914
+ "vasca",
34915
+ "doccia"
34916
+ ]
34917
+ },
34918
+ {
34919
+ key: "footprints",
34920
+ group: "sensing",
34921
+ synonyms: [
34922
+ "movimento",
34923
+ "passi",
34924
+ "presenza",
34925
+ "motion"
34926
+ ]
34927
+ },
34928
+ {
34929
+ key: "alarm-smoke",
34930
+ group: "sensing",
34931
+ synonyms: [
34932
+ "fumo",
34933
+ "antincendio",
34934
+ "rilevatore"
34935
+ ]
34936
+ },
34937
+ {
34938
+ key: "shield-alert",
34939
+ group: "sensing",
34940
+ synonyms: [
34941
+ "manomissione",
34942
+ "allarme",
34943
+ "tamper",
34944
+ "avviso"
34945
+ ]
34946
+ },
34947
+ {
34948
+ key: "vibrate",
34949
+ group: "sensing",
34950
+ synonyms: [
34951
+ "vibrazione",
34952
+ "urto",
34953
+ "tamper"
34954
+ ]
34955
+ },
34956
+ {
34957
+ key: "wifi",
34958
+ group: "sensing",
34959
+ synonyms: [
34960
+ "wifi",
34961
+ "rete",
34962
+ "segnale"
34963
+ ]
34964
+ },
34965
+ {
34966
+ key: "wifi-off",
34967
+ group: "sensing",
34968
+ synonyms: [
34969
+ "wifi",
34970
+ "assente",
34971
+ "offline",
34972
+ "disconnesso"
34973
+ ]
34974
+ },
34975
+ {
34976
+ key: "volume-2",
34977
+ group: "sensing",
34978
+ synonyms: [
34979
+ "volume",
34980
+ "audio",
34981
+ "altoparlante",
34982
+ "suono"
34983
+ ]
34984
+ },
34985
+ {
34986
+ key: "volume-x",
34987
+ group: "sensing",
34988
+ synonyms: [
34989
+ "muto",
34990
+ "silenziato",
34991
+ "volume",
34992
+ "spento"
34993
+ ]
34994
+ },
34995
+ {
34996
+ key: "heart",
34997
+ group: "sensing",
34998
+ synonyms: [
34999
+ "cuore",
35000
+ "preferito",
35001
+ "salute"
35002
+ ]
35003
+ },
35004
+ {
35005
+ key: "heart-pulse",
35006
+ group: "sensing",
35007
+ synonyms: [
35008
+ "battito",
35009
+ "salute",
35010
+ "monitoraggio"
35011
+ ]
35012
+ },
35013
+ {
35014
+ key: "star",
35015
+ group: "sensing",
35016
+ synonyms: [
35017
+ "preferito",
35018
+ "stella",
35019
+ "importante"
35020
+ ]
35021
+ },
35022
+ {
35023
+ key: "sparkles",
35024
+ group: "sensing",
35025
+ synonyms: [
35026
+ "automatico",
35027
+ "intelligente",
35028
+ "ai",
35029
+ "scena"
35030
+ ]
35031
+ },
35032
+ {
35033
+ key: "house",
35034
+ group: "place",
35035
+ synonyms: [
35036
+ "casa",
35037
+ "abitazione",
35038
+ "home"
35039
+ ]
35040
+ },
35041
+ {
35042
+ key: "sofa",
35043
+ group: "place",
35044
+ synonyms: [
35045
+ "salone",
35046
+ "soggiorno",
35047
+ "divano",
35048
+ "living"
35049
+ ]
35050
+ },
35051
+ {
35052
+ key: "bed-double",
35053
+ group: "place",
35054
+ synonyms: [
35055
+ "camera",
35056
+ "letto",
35057
+ "matrimoniale"
35058
+ ]
35059
+ },
35060
+ {
35061
+ key: "bed",
35062
+ group: "place",
35063
+ synonyms: [
35064
+ "cameretta",
35065
+ "letto",
35066
+ "singolo"
35067
+ ]
35068
+ },
35069
+ {
35070
+ key: "warehouse",
35071
+ group: "place",
35072
+ synonyms: [
35073
+ "magazzino",
35074
+ "capanno",
35075
+ "deposito",
35076
+ "cantina"
35077
+ ]
35078
+ },
35079
+ {
35080
+ key: "trees",
35081
+ group: "place",
35082
+ synonyms: [
35083
+ "giardino",
35084
+ "alberi",
35085
+ "esterno",
35086
+ "parco"
35087
+ ]
35088
+ },
35089
+ {
35090
+ key: "tree-pine",
35091
+ group: "place",
35092
+ synonyms: [
35093
+ "albero",
35094
+ "pino",
35095
+ "giardino"
35096
+ ]
35097
+ },
35098
+ {
35099
+ key: "fence",
35100
+ group: "place",
35101
+ synonyms: [
35102
+ "recinto",
35103
+ "cancello",
35104
+ "staccionata",
35105
+ "perimetro"
35106
+ ]
35107
+ },
35108
+ {
35109
+ key: "store",
35110
+ group: "place",
35111
+ synonyms: [
35112
+ "negozio",
35113
+ "ingresso",
35114
+ "vetrina"
35115
+ ]
35116
+ },
35117
+ {
35118
+ key: "building",
35119
+ group: "place",
35120
+ synonyms: ["edificio", "palazzo"]
35121
+ },
35122
+ {
35123
+ key: "building-2",
35124
+ group: "place",
35125
+ synonyms: [
35126
+ "edificio",
35127
+ "uffici",
35128
+ "condominio"
35129
+ ]
35130
+ },
35131
+ {
35132
+ key: "mountain",
35133
+ group: "place",
35134
+ synonyms: [
35135
+ "montagna",
35136
+ "esterno",
35137
+ "panorama"
35138
+ ]
35139
+ },
35140
+ {
35141
+ key: "umbrella",
35142
+ group: "place",
35143
+ synonyms: [
35144
+ "tenda",
35145
+ "markise",
35146
+ "ombrellone",
35147
+ "pergola",
35148
+ "veranda"
35149
+ ]
35150
+ },
35151
+ {
35152
+ key: "dumbbell",
35153
+ group: "place",
35154
+ synonyms: ["palestra", "attrezzi"]
35155
+ },
35156
+ {
35157
+ key: "wrench",
35158
+ group: "place",
35159
+ synonyms: [
35160
+ "officina",
35161
+ "manutenzione",
35162
+ "attrezzi",
35163
+ "utenze"
35164
+ ]
35165
+ },
35166
+ {
35167
+ key: "hammer",
35168
+ group: "place",
35169
+ synonyms: [
35170
+ "officina",
35171
+ "martello",
35172
+ "lavori"
35173
+ ]
35174
+ },
35175
+ {
35176
+ key: "drill",
35177
+ group: "place",
35178
+ synonyms: [
35179
+ "officina",
35180
+ "trapano",
35181
+ "lavori"
35182
+ ]
35183
+ },
35184
+ {
35185
+ key: "waves-ladder",
35186
+ group: "place",
35187
+ synonyms: ["piscina", "vasca"]
35188
+ },
35189
+ {
35190
+ key: "circle-parking",
35191
+ group: "place",
35192
+ synonyms: [
35193
+ "parcheggio",
35194
+ "posto",
35195
+ "auto",
35196
+ "garage"
35197
+ ]
35198
+ },
35199
+ {
35200
+ key: "route",
35201
+ group: "place",
35202
+ synonyms: [
35203
+ "percorso",
35204
+ "strada",
35205
+ "vialetto",
35206
+ "tragitto"
35207
+ ]
35208
+ },
35209
+ {
35210
+ key: "car",
35211
+ group: "life",
35212
+ synonyms: [
35213
+ "auto",
35214
+ "macchina",
35215
+ "veicolo",
35216
+ "parcheggio"
35217
+ ]
35218
+ },
35219
+ {
35220
+ key: "bike",
35221
+ group: "life",
35222
+ synonyms: [
35223
+ "bicicletta",
35224
+ "bici",
35225
+ "moto"
35226
+ ]
35227
+ },
35228
+ {
35229
+ key: "bus",
35230
+ group: "life",
35231
+ synonyms: ["autobus", "pullman"]
35232
+ },
35233
+ {
35234
+ key: "truck",
35235
+ group: "life",
35236
+ synonyms: [
35237
+ "camion",
35238
+ "furgone",
35239
+ "consegna"
35240
+ ]
35241
+ },
35242
+ {
35243
+ key: "plane",
35244
+ group: "life",
35245
+ synonyms: ["aereo"]
35246
+ },
35247
+ {
35248
+ key: "train-front",
35249
+ group: "life",
35250
+ synonyms: ["treno"]
35251
+ },
35252
+ {
35253
+ key: "sailboat",
35254
+ group: "life",
35255
+ synonyms: ["barca", "imbarcazione"]
35256
+ },
35257
+ {
35258
+ key: "dog",
35259
+ group: "life",
35260
+ synonyms: ["cane", "animale"]
35261
+ },
35262
+ {
35263
+ key: "cat",
35264
+ group: "life",
35265
+ synonyms: ["gatto", "animale"]
35266
+ },
35267
+ {
35268
+ key: "bird",
35269
+ group: "life",
35270
+ synonyms: ["uccello", "animale"]
35271
+ },
35272
+ {
35273
+ key: "paw-print",
35274
+ group: "life",
35275
+ synonyms: [
35276
+ "animale",
35277
+ "zampa",
35278
+ "pet"
35279
+ ]
35280
+ },
35281
+ {
35282
+ key: "baby",
35283
+ group: "life",
35284
+ synonyms: [
35285
+ "neonato",
35286
+ "bimbo",
35287
+ "cameretta",
35288
+ "babymonitor"
35289
+ ]
35290
+ },
35291
+ {
35292
+ key: "user",
35293
+ group: "life",
35294
+ synonyms: ["persona", "utente"]
35295
+ },
35296
+ {
35297
+ key: "users",
35298
+ group: "life",
35299
+ synonyms: [
35300
+ "persone",
35301
+ "gruppo",
35302
+ "famiglia"
35303
+ ]
35304
+ },
35305
+ {
35306
+ key: "person-standing",
35307
+ group: "life",
35308
+ synonyms: [
35309
+ "persona",
35310
+ "piedi",
35311
+ "presenza"
35312
+ ]
35313
+ }
35314
+ ];
35315
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
35316
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
35317
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
32636
35318
  /** Reject after `ms`; always clears its own timer. */
32637
35319
  async function withTimeout$1(promise, ms, label) {
32638
35320
  let timer;
@@ -32700,6 +35382,12 @@ function startReachabilityPoll(options) {
32700
35382
  } };
32701
35383
  }
32702
35384
  Object.freeze({
35385
+ "accessories.getStatus": {
35386
+ capName: "accessories",
35387
+ capScope: "device",
35388
+ addonId: null,
35389
+ access: "view"
35390
+ },
32703
35391
  "accessories.setChildHidden": {
32704
35392
  capName: "accessories",
32705
35393
  capScope: "device",
@@ -32982,6 +35670,12 @@ Object.freeze({
32982
35670
  addonId: null,
32983
35671
  access: "view"
32984
35672
  },
35673
+ "airQualitySensor.getStatus": {
35674
+ capName: "air-quality-sensor",
35675
+ capScope: "device",
35676
+ addonId: null,
35677
+ access: "view"
35678
+ },
32985
35679
  "alarmPanel.arm": {
32986
35680
  capName: "alarm-panel",
32987
35681
  capScope: "device",
@@ -32994,6 +35688,12 @@ Object.freeze({
32994
35688
  addonId: null,
32995
35689
  access: "create"
32996
35690
  },
35691
+ "alarmPanel.getStatus": {
35692
+ capName: "alarm-panel",
35693
+ capScope: "device",
35694
+ addonId: null,
35695
+ access: "view"
35696
+ },
32997
35697
  "alarmPanel.trigger": {
32998
35698
  capName: "alarm-panel",
32999
35699
  capScope: "device",
@@ -33042,6 +35742,30 @@ Object.freeze({
33042
35742
  addonId: null,
33043
35743
  access: "create"
33044
35744
  },
35745
+ "ambientLightSensor.getStatus": {
35746
+ capName: "ambient-light-sensor",
35747
+ capScope: "device",
35748
+ addonId: null,
35749
+ access: "view"
35750
+ },
35751
+ "audioAnalysis.applyDeviceSettingsPatch": {
35752
+ capName: "audio-analysis",
35753
+ capScope: "device",
35754
+ addonId: null,
35755
+ access: "create"
35756
+ },
35757
+ "audioAnalysis.getDeviceLiveContribution": {
35758
+ capName: "audio-analysis",
35759
+ capScope: "device",
35760
+ addonId: null,
35761
+ access: "view"
35762
+ },
35763
+ "audioAnalysis.getDeviceSettingsContribution": {
35764
+ capName: "audio-analysis",
35765
+ capScope: "device",
35766
+ addonId: null,
35767
+ access: "view"
35768
+ },
33045
35769
  "audioAnalysis.resolveDeviceSettings": {
33046
35770
  capName: "audio-analysis",
33047
35771
  capScope: "device",
@@ -33204,6 +35928,12 @@ Object.freeze({
33204
35928
  addonId: null,
33205
35929
  access: "create"
33206
35930
  },
35931
+ "automationControl.getStatus": {
35932
+ capName: "automation-control",
35933
+ capScope: "device",
35934
+ addonId: null,
35935
+ access: "view"
35936
+ },
33207
35937
  "automationControl.trigger": {
33208
35938
  capName: "automation-control",
33209
35939
  capScope: "device",
@@ -33300,12 +36030,30 @@ Object.freeze({
33300
36030
  addonId: null,
33301
36031
  access: "create"
33302
36032
  },
36033
+ "battery.getStatus": {
36034
+ capName: "battery",
36035
+ capScope: "device",
36036
+ addonId: null,
36037
+ access: "view"
36038
+ },
33303
36039
  "battery.wakeForStream": {
33304
36040
  capName: "battery",
33305
36041
  capScope: "device",
33306
36042
  addonId: null,
33307
36043
  access: "create"
33308
36044
  },
36045
+ "binary.getStatus": {
36046
+ capName: "binary",
36047
+ capScope: "device",
36048
+ addonId: null,
36049
+ access: "view"
36050
+ },
36051
+ "brightness.getStatus": {
36052
+ capName: "brightness",
36053
+ capScope: "device",
36054
+ addonId: null,
36055
+ access: "view"
36056
+ },
33309
36057
  "brightness.setBrightness": {
33310
36058
  capName: "brightness",
33311
36059
  capScope: "device",
@@ -33420,6 +36168,30 @@ Object.freeze({
33420
36168
  addonId: null,
33421
36169
  access: "view"
33422
36170
  },
36171
+ "cameraCredentials.getStatus": {
36172
+ capName: "camera-credentials",
36173
+ capScope: "device",
36174
+ addonId: null,
36175
+ access: "view"
36176
+ },
36177
+ "cameraGridLayout.applyDeviceSettingsPatch": {
36178
+ capName: "camera-grid-layout",
36179
+ capScope: "device",
36180
+ addonId: null,
36181
+ access: "create"
36182
+ },
36183
+ "cameraGridLayout.getDeviceLiveContribution": {
36184
+ capName: "camera-grid-layout",
36185
+ capScope: "device",
36186
+ addonId: null,
36187
+ access: "view"
36188
+ },
36189
+ "cameraGridLayout.getDeviceSettingsContribution": {
36190
+ capName: "camera-grid-layout",
36191
+ capScope: "device",
36192
+ addonId: null,
36193
+ access: "view"
36194
+ },
33423
36195
  "cameraGridLayout.getLayout": {
33424
36196
  capName: "camera-grid-layout",
33425
36197
  capScope: "device",
@@ -33432,6 +36204,24 @@ Object.freeze({
33432
36204
  addonId: null,
33433
36205
  access: "create"
33434
36206
  },
36207
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
36208
+ capName: "camera-pipeline-config",
36209
+ capScope: "device",
36210
+ addonId: null,
36211
+ access: "create"
36212
+ },
36213
+ "cameraPipelineConfig.getDeviceLiveContribution": {
36214
+ capName: "camera-pipeline-config",
36215
+ capScope: "device",
36216
+ addonId: null,
36217
+ access: "view"
36218
+ },
36219
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
36220
+ capName: "camera-pipeline-config",
36221
+ capScope: "device",
36222
+ addonId: null,
36223
+ access: "view"
36224
+ },
33435
36225
  "cameraStreams.getBrokerStreams": {
33436
36226
  capName: "camera-streams",
33437
36227
  capScope: "device",
@@ -33462,6 +36252,18 @@ Object.freeze({
33462
36252
  addonId: null,
33463
36253
  access: "view"
33464
36254
  },
36255
+ "carbonMonoxide.getStatus": {
36256
+ capName: "carbon-monoxide",
36257
+ capScope: "device",
36258
+ addonId: null,
36259
+ access: "view"
36260
+ },
36261
+ "climateControl.getStatus": {
36262
+ capName: "climate-control",
36263
+ capScope: "device",
36264
+ addonId: null,
36265
+ access: "view"
36266
+ },
33465
36267
  "climateControl.setFanMode": {
33466
36268
  capName: "climate-control",
33467
36269
  capScope: "device",
@@ -33510,6 +36312,12 @@ Object.freeze({
33510
36312
  addonId: null,
33511
36313
  access: "create"
33512
36314
  },
36315
+ "color.getStatus": {
36316
+ capName: "color",
36317
+ capScope: "device",
36318
+ addonId: null,
36319
+ access: "view"
36320
+ },
33513
36321
  "color.setColor": {
33514
36322
  capName: "color",
33515
36323
  capScope: "device",
@@ -33528,12 +36336,36 @@ Object.freeze({
33528
36336
  addonId: null,
33529
36337
  access: "create"
33530
36338
  },
36339
+ "connectivity.getStatus": {
36340
+ capName: "connectivity",
36341
+ capScope: "device",
36342
+ addonId: null,
36343
+ access: "view"
36344
+ },
36345
+ "consumables.getStatus": {
36346
+ capName: "consumables",
36347
+ capScope: "device",
36348
+ addonId: null,
36349
+ access: "view"
36350
+ },
33531
36351
  "consumables.reset": {
33532
36352
  capName: "consumables",
33533
36353
  capScope: "device",
33534
36354
  addonId: null,
33535
36355
  access: "delete"
33536
36356
  },
36357
+ "contact.getStatus": {
36358
+ capName: "contact",
36359
+ capScope: "device",
36360
+ addonId: null,
36361
+ access: "view"
36362
+ },
36363
+ "control.getStatus": {
36364
+ capName: "control",
36365
+ capScope: "device",
36366
+ addonId: null,
36367
+ access: "view"
36368
+ },
33537
36369
  "control.setValue": {
33538
36370
  capName: "control",
33539
36371
  capScope: "device",
@@ -33600,6 +36432,12 @@ Object.freeze({
33600
36432
  addonId: null,
33601
36433
  access: "create"
33602
36434
  },
36435
+ "cover.getStatus": {
36436
+ capName: "cover",
36437
+ capScope: "device",
36438
+ addonId: null,
36439
+ access: "view"
36440
+ },
33603
36441
  "cover.open": {
33604
36442
  capName: "cover",
33605
36443
  capScope: "device",
@@ -33732,6 +36570,12 @@ Object.freeze({
33732
36570
  addonId: null,
33733
36571
  access: "view"
33734
36572
  },
36573
+ "dayNight.getStatus": {
36574
+ capName: "day-night",
36575
+ capScope: "device",
36576
+ addonId: null,
36577
+ access: "view"
36578
+ },
33735
36579
  "dayNight.setSettings": {
33736
36580
  capName: "day-night",
33737
36581
  capScope: "device",
@@ -33822,6 +36666,30 @@ Object.freeze({
33822
36666
  addonId: null,
33823
36667
  access: "view"
33824
36668
  },
36669
+ "detectionPipeline.applyDeviceSettingsPatch": {
36670
+ capName: "detection-pipeline",
36671
+ capScope: "device",
36672
+ addonId: null,
36673
+ access: "create"
36674
+ },
36675
+ "detectionPipeline.getDeviceLiveContribution": {
36676
+ capName: "detection-pipeline",
36677
+ capScope: "device",
36678
+ addonId: null,
36679
+ access: "view"
36680
+ },
36681
+ "detectionPipeline.getDeviceSettingsContribution": {
36682
+ capName: "detection-pipeline",
36683
+ capScope: "device",
36684
+ addonId: null,
36685
+ access: "view"
36686
+ },
36687
+ "deviceAdminLink.getAdminLink": {
36688
+ capName: "device-admin-link",
36689
+ capScope: "device",
36690
+ addonId: null,
36691
+ access: "view"
36692
+ },
33825
36693
  "deviceAdoption.adopt": {
33826
36694
  capName: "device-adoption",
33827
36695
  capScope: "system",
@@ -33834,6 +36702,12 @@ Object.freeze({
33834
36702
  addonId: null,
33835
36703
  access: "view"
33836
36704
  },
36705
+ "deviceAdoption.getStatus": {
36706
+ capName: "device-adoption",
36707
+ capScope: "system",
36708
+ addonId: null,
36709
+ access: "view"
36710
+ },
33837
36711
  "deviceAdoption.listCandidateFilters": {
33838
36712
  capName: "device-adoption",
33839
36713
  capScope: "system",
@@ -33870,6 +36744,12 @@ Object.freeze({
33870
36744
  addonId: null,
33871
36745
  access: "create"
33872
36746
  },
36747
+ "deviceDiscovery.getStatus": {
36748
+ capName: "device-discovery",
36749
+ capScope: "device",
36750
+ addonId: null,
36751
+ access: "view"
36752
+ },
33873
36753
  "deviceDiscovery.listDiscovered": {
33874
36754
  capName: "device-discovery",
33875
36755
  capScope: "device",
@@ -33888,12 +36768,30 @@ Object.freeze({
33888
36768
  addonId: null,
33889
36769
  access: "create"
33890
36770
  },
36771
+ "deviceExport.applyDeviceSettingsPatch": {
36772
+ capName: "device-export",
36773
+ capScope: "system",
36774
+ addonId: null,
36775
+ access: "create"
36776
+ },
33891
36777
  "deviceExport.exposeDevice": {
33892
36778
  capName: "device-export",
33893
36779
  capScope: "system",
33894
36780
  addonId: null,
33895
36781
  access: "create"
33896
36782
  },
36783
+ "deviceExport.getDeviceLiveContribution": {
36784
+ capName: "device-export",
36785
+ capScope: "system",
36786
+ addonId: null,
36787
+ access: "view"
36788
+ },
36789
+ "deviceExport.getDeviceSettingsContribution": {
36790
+ capName: "device-export",
36791
+ capScope: "system",
36792
+ addonId: null,
36793
+ access: "view"
36794
+ },
33897
36795
  "deviceExport.getStatus": {
33898
36796
  capName: "device-export",
33899
36797
  capScope: "system",
@@ -33990,6 +36888,12 @@ Object.freeze({
33990
36888
  addonId: null,
33991
36889
  access: "create"
33992
36890
  },
36891
+ "deviceManager.applyDeviceSettingsPatch": {
36892
+ capName: "device-manager",
36893
+ capScope: "system",
36894
+ addonId: null,
36895
+ access: "create"
36896
+ },
33993
36897
  "deviceManager.applyInitialMeta": {
33994
36898
  capName: "device-manager",
33995
36899
  capScope: "system",
@@ -34092,6 +36996,12 @@ Object.freeze({
34092
36996
  addonId: null,
34093
36997
  access: "view"
34094
36998
  },
36999
+ "deviceManager.getDeviceLiveContribution": {
37000
+ capName: "device-manager",
37001
+ capScope: "system",
37002
+ addonId: null,
37003
+ access: "view"
37004
+ },
34095
37005
  "deviceManager.getDeviceLiveInfoAggregate": {
34096
37006
  capName: "device-manager",
34097
37007
  capScope: "system",
@@ -34104,6 +37014,12 @@ Object.freeze({
34104
37014
  addonId: null,
34105
37015
  access: "view"
34106
37016
  },
37017
+ "deviceManager.getDeviceSettingsContribution": {
37018
+ capName: "device-manager",
37019
+ capScope: "system",
37020
+ addonId: null,
37021
+ access: "view"
37022
+ },
34107
37023
  "deviceManager.getDeviceStatusAggregate": {
34108
37024
  capName: "device-manager",
34109
37025
  capScope: "system",
@@ -34596,6 +37512,36 @@ Object.freeze({
34596
37512
  addonId: null,
34597
37513
  access: "create"
34598
37514
  },
37515
+ "deviceStatus.getStatus": {
37516
+ capName: "device-status",
37517
+ capScope: "device",
37518
+ addonId: null,
37519
+ access: "view"
37520
+ },
37521
+ "doorbell.applyDeviceSettingsPatch": {
37522
+ capName: "doorbell",
37523
+ capScope: "device",
37524
+ addonId: null,
37525
+ access: "create"
37526
+ },
37527
+ "doorbell.getDeviceLiveContribution": {
37528
+ capName: "doorbell",
37529
+ capScope: "device",
37530
+ addonId: null,
37531
+ access: "view"
37532
+ },
37533
+ "doorbell.getDeviceSettingsContribution": {
37534
+ capName: "doorbell",
37535
+ capScope: "device",
37536
+ addonId: null,
37537
+ access: "view"
37538
+ },
37539
+ "doorbell.getStatus": {
37540
+ capName: "doorbell",
37541
+ capScope: "device",
37542
+ addonId: null,
37543
+ access: "view"
37544
+ },
34599
37545
  "embeddingEncoder.encode": {
34600
37546
  capName: "embedding-encoder",
34601
37547
  capScope: "system",
@@ -34614,6 +37560,18 @@ Object.freeze({
34614
37560
  addonId: null,
34615
37561
  access: "view"
34616
37562
  },
37563
+ "enumSensor.getStatus": {
37564
+ capName: "enum-sensor",
37565
+ capScope: "device",
37566
+ addonId: null,
37567
+ access: "view"
37568
+ },
37569
+ "eventEmitter.getStatus": {
37570
+ capName: "event-emitter",
37571
+ capScope: "device",
37572
+ addonId: null,
37573
+ access: "view"
37574
+ },
34617
37575
  "events.getEventClipUrl": {
34618
37576
  capName: "events",
34619
37577
  capScope: "device",
@@ -34734,6 +37692,12 @@ Object.freeze({
34734
37692
  addonId: null,
34735
37693
  access: "view"
34736
37694
  },
37695
+ "fanControl.getStatus": {
37696
+ capName: "fan-control",
37697
+ capScope: "device",
37698
+ addonId: null,
37699
+ access: "view"
37700
+ },
34737
37701
  "fanControl.setDirection": {
34738
37702
  capName: "fan-control",
34739
37703
  capScope: "device",
@@ -34758,6 +37722,12 @@ Object.freeze({
34758
37722
  addonId: null,
34759
37723
  access: "create"
34760
37724
  },
37725
+ "featureProbe.getStatus": {
37726
+ capName: "feature-probe",
37727
+ capScope: "device",
37728
+ addonId: null,
37729
+ access: "view"
37730
+ },
34761
37731
  "filesystemBrowse.browse": {
34762
37732
  capName: "filesystem-browse",
34763
37733
  capScope: "system",
@@ -34776,6 +37746,24 @@ Object.freeze({
34776
37746
  addonId: null,
34777
37747
  access: "view"
34778
37748
  },
37749
+ "flood.getStatus": {
37750
+ capName: "flood",
37751
+ capScope: "device",
37752
+ addonId: null,
37753
+ access: "view"
37754
+ },
37755
+ "gas.getStatus": {
37756
+ capName: "gas",
37757
+ capScope: "device",
37758
+ addonId: null,
37759
+ access: "view"
37760
+ },
37761
+ "humidifier.getStatus": {
37762
+ capName: "humidifier",
37763
+ capScope: "device",
37764
+ addonId: null,
37765
+ access: "view"
37766
+ },
34779
37767
  "humidifier.setMode": {
34780
37768
  capName: "humidifier",
34781
37769
  capScope: "device",
@@ -34794,12 +37782,30 @@ Object.freeze({
34794
37782
  addonId: null,
34795
37783
  access: "create"
34796
37784
  },
37785
+ "humiditySensor.getStatus": {
37786
+ capName: "humidity-sensor",
37787
+ capScope: "device",
37788
+ addonId: null,
37789
+ access: "view"
37790
+ },
37791
+ "image.getStatus": {
37792
+ capName: "image",
37793
+ capScope: "device",
37794
+ addonId: null,
37795
+ access: "view"
37796
+ },
34797
37797
  "imageSettings.getOptions": {
34798
37798
  capName: "image-settings",
34799
37799
  capScope: "device",
34800
37800
  addonId: null,
34801
37801
  access: "view"
34802
37802
  },
37803
+ "imageSettings.getStatus": {
37804
+ capName: "image-settings",
37805
+ capScope: "device",
37806
+ addonId: null,
37807
+ access: "view"
37808
+ },
34803
37809
  "imageSettings.setSettings": {
34804
37810
  capName: "image-settings",
34805
37811
  capScope: "device",
@@ -34872,6 +37878,12 @@ Object.freeze({
34872
37878
  addonId: null,
34873
37879
  access: "create"
34874
37880
  },
37881
+ "intercom.getStatus": {
37882
+ capName: "intercom",
37883
+ capScope: "device",
37884
+ addonId: null,
37885
+ access: "view"
37886
+ },
34875
37887
  "intercom.handleAnswer": {
34876
37888
  capName: "intercom",
34877
37889
  capScope: "device",
@@ -34908,6 +37920,12 @@ Object.freeze({
34908
37920
  addonId: null,
34909
37921
  access: "create"
34910
37922
  },
37923
+ "lawnMowerControl.getStatus": {
37924
+ capName: "lawn-mower-control",
37925
+ capScope: "device",
37926
+ addonId: null,
37927
+ access: "view"
37928
+ },
34911
37929
  "lawnMowerControl.pause": {
34912
37930
  capName: "lawn-mower-control",
34913
37931
  capScope: "device",
@@ -35190,6 +38208,12 @@ Object.freeze({
35190
38208
  addonId: null,
35191
38209
  access: "create"
35192
38210
  },
38211
+ "lockControl.getStatus": {
38212
+ capName: "lock-control",
38213
+ capScope: "device",
38214
+ addonId: null,
38215
+ access: "view"
38216
+ },
35193
38217
  "lockControl.lock": {
35194
38218
  capName: "lock-control",
35195
38219
  capScope: "device",
@@ -35238,6 +38262,12 @@ Object.freeze({
35238
38262
  addonId: null,
35239
38263
  access: "view"
35240
38264
  },
38265
+ "mediaPlayer.getStatus": {
38266
+ capName: "media-player",
38267
+ capScope: "device",
38268
+ addonId: null,
38269
+ access: "view"
38270
+ },
35241
38271
  "mediaPlayer.next": {
35242
38272
  capName: "media-player",
35243
38273
  capScope: "device",
@@ -35430,6 +38460,12 @@ Object.freeze({
35430
38460
  addonId: null,
35431
38461
  access: "create"
35432
38462
  },
38463
+ "motion.getStatus": {
38464
+ capName: "motion",
38465
+ capScope: "device",
38466
+ addonId: null,
38467
+ access: "view"
38468
+ },
35433
38469
  "motion.isDetected": {
35434
38470
  capName: "motion",
35435
38471
  capScope: "device",
@@ -35442,6 +38478,24 @@ Object.freeze({
35442
38478
  addonId: null,
35443
38479
  access: "create"
35444
38480
  },
38481
+ "motionDetection.applyDeviceSettingsPatch": {
38482
+ capName: "motion-detection",
38483
+ capScope: "device",
38484
+ addonId: null,
38485
+ access: "create"
38486
+ },
38487
+ "motionDetection.getDeviceLiveContribution": {
38488
+ capName: "motion-detection",
38489
+ capScope: "device",
38490
+ addonId: null,
38491
+ access: "view"
38492
+ },
38493
+ "motionDetection.getDeviceSettingsContribution": {
38494
+ capName: "motion-detection",
38495
+ capScope: "device",
38496
+ addonId: null,
38497
+ access: "view"
38498
+ },
35445
38499
  "motionDetection.removeCamera": {
35446
38500
  capName: "motion-detection",
35447
38501
  capScope: "device",
@@ -35454,6 +38508,12 @@ Object.freeze({
35454
38508
  addonId: null,
35455
38509
  access: "delete"
35456
38510
  },
38511
+ "motionTrigger.getStatus": {
38512
+ capName: "motion-trigger",
38513
+ capScope: "device",
38514
+ addonId: null,
38515
+ access: "view"
38516
+ },
35457
38517
  "motionTrigger.setMotionTrigger": {
35458
38518
  capName: "motion-trigger",
35459
38519
  capScope: "device",
@@ -35466,6 +38526,12 @@ Object.freeze({
35466
38526
  addonId: null,
35467
38527
  access: "view"
35468
38528
  },
38529
+ "motionZones.getStatus": {
38530
+ capName: "motion-zones",
38531
+ capScope: "device",
38532
+ addonId: null,
38533
+ access: "view"
38534
+ },
35469
38535
  "motionZones.setZone": {
35470
38536
  capName: "motion-zones",
35471
38537
  capScope: "device",
@@ -35526,6 +38592,12 @@ Object.freeze({
35526
38592
  addonId: null,
35527
38593
  access: "view"
35528
38594
  },
38595
+ "nativeObjectDetection.getStatus": {
38596
+ capName: "native-object-detection",
38597
+ capScope: "device",
38598
+ addonId: null,
38599
+ access: "view"
38600
+ },
35529
38601
  "nativeObjectDetection.setEnabled": {
35530
38602
  capName: "native-object-detection",
35531
38603
  capScope: "device",
@@ -35544,6 +38616,12 @@ Object.freeze({
35544
38616
  addonId: null,
35545
38617
  access: "view"
35546
38618
  },
38619
+ "navigation.getStatus": {
38620
+ capName: "navigation",
38621
+ capScope: "device",
38622
+ addonId: null,
38623
+ access: "view"
38624
+ },
35547
38625
  "navigation.goToPoint": {
35548
38626
  capName: "navigation",
35549
38627
  capScope: "device",
@@ -35628,6 +38706,12 @@ Object.freeze({
35628
38706
  addonId: null,
35629
38707
  access: "create"
35630
38708
  },
38709
+ "networkLink.getStatus": {
38710
+ capName: "network-link",
38711
+ capScope: "device",
38712
+ addonId: null,
38713
+ access: "view"
38714
+ },
35631
38715
  "networkQuality.getAllStats": {
35632
38716
  capName: "network-quality",
35633
38717
  capScope: "system",
@@ -35886,18 +38970,36 @@ Object.freeze({
35886
38970
  addonId: null,
35887
38971
  access: "create"
35888
38972
  },
38973
+ "notifier.getStatus": {
38974
+ capName: "notifier",
38975
+ capScope: "device",
38976
+ addonId: null,
38977
+ access: "view"
38978
+ },
35889
38979
  "notifier.send": {
35890
38980
  capName: "notifier",
35891
38981
  capScope: "device",
35892
38982
  addonId: null,
35893
38983
  access: "create"
35894
38984
  },
38985
+ "numericSensor.getStatus": {
38986
+ capName: "numeric-sensor",
38987
+ capScope: "device",
38988
+ addonId: null,
38989
+ access: "view"
38990
+ },
35895
38991
  "oauthIntegration.getDescriptor": {
35896
38992
  capName: "oauth-integration",
35897
38993
  capScope: "system",
35898
38994
  addonId: null,
35899
38995
  access: "view"
35900
38996
  },
38997
+ "osd.getStatus": {
38998
+ capName: "osd",
38999
+ capScope: "device",
39000
+ addonId: null,
39001
+ access: "view"
39002
+ },
35901
39003
  "osd.setOverlay": {
35902
39004
  capName: "osd",
35903
39005
  capScope: "device",
@@ -35970,6 +39072,12 @@ Object.freeze({
35970
39072
  addonId: null,
35971
39073
  access: "create"
35972
39074
  },
39075
+ "petFeeder.getStatus": {
39076
+ capName: "pet-feeder",
39077
+ capScope: "device",
39078
+ addonId: null,
39079
+ access: "view"
39080
+ },
35973
39081
  "petFeeder.markFoodReplenished": {
35974
39082
  capName: "pet-feeder",
35975
39083
  capScope: "device",
@@ -36012,6 +39120,12 @@ Object.freeze({
36012
39120
  addonId: null,
36013
39121
  access: "create"
36014
39122
  },
39123
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
39124
+ capName: "pipeline-analytics",
39125
+ capScope: "device",
39126
+ addonId: null,
39127
+ access: "create"
39128
+ },
36015
39129
  "pipelineAnalytics.cancelRelocateMedia": {
36016
39130
  capName: "pipeline-analytics",
36017
39131
  capScope: "device",
@@ -36078,6 +39192,18 @@ Object.freeze({
36078
39192
  addonId: null,
36079
39193
  access: "view"
36080
39194
  },
39195
+ "pipelineAnalytics.getDeviceLiveContribution": {
39196
+ capName: "pipeline-analytics",
39197
+ capScope: "device",
39198
+ addonId: null,
39199
+ access: "view"
39200
+ },
39201
+ "pipelineAnalytics.getDeviceSettingsContribution": {
39202
+ capName: "pipeline-analytics",
39203
+ capScope: "device",
39204
+ addonId: null,
39205
+ access: "view"
39206
+ },
36081
39207
  "pipelineAnalytics.getEventDensity": {
36082
39208
  capName: "pipeline-analytics",
36083
39209
  capScope: "device",
@@ -36288,6 +39414,12 @@ Object.freeze({
36288
39414
  addonId: null,
36289
39415
  access: "view"
36290
39416
  },
39417
+ "pipelineAnalytics.ownersWithMedia": {
39418
+ capName: "pipeline-analytics",
39419
+ capScope: "device",
39420
+ addonId: null,
39421
+ access: "view"
39422
+ },
36291
39423
  "pipelineAnalytics.pauseForStorageMigration": {
36292
39424
  capName: "pipeline-analytics",
36293
39425
  capScope: "device",
@@ -36642,6 +39774,12 @@ Object.freeze({
36642
39774
  addonId: null,
36643
39775
  access: "view"
36644
39776
  },
39777
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
39778
+ capName: "pipeline-orchestrator",
39779
+ capScope: "system",
39780
+ addonId: null,
39781
+ access: "create"
39782
+ },
36645
39783
  "pipelineOrchestrator.assignAudio": {
36646
39784
  capName: "pipeline-orchestrator",
36647
39785
  capScope: "system",
@@ -36732,6 +39870,18 @@ Object.freeze({
36732
39870
  addonId: null,
36733
39871
  access: "view"
36734
39872
  },
39873
+ "pipelineOrchestrator.getDeviceLiveContribution": {
39874
+ capName: "pipeline-orchestrator",
39875
+ capScope: "system",
39876
+ addonId: null,
39877
+ access: "view"
39878
+ },
39879
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
39880
+ capName: "pipeline-orchestrator",
39881
+ capScope: "system",
39882
+ addonId: null,
39883
+ access: "view"
39884
+ },
36735
39885
  "pipelineOrchestrator.getGlobalMetrics": {
36736
39886
  capName: "pipeline-orchestrator",
36737
39887
  capScope: "system",
@@ -37140,12 +40290,36 @@ Object.freeze({
37140
40290
  addonId: null,
37141
40291
  access: "view"
37142
40292
  },
40293
+ "powerMeter.getStatus": {
40294
+ capName: "power-meter",
40295
+ capScope: "device",
40296
+ addonId: null,
40297
+ access: "view"
40298
+ },
40299
+ "presence.getStatus": {
40300
+ capName: "presence",
40301
+ capScope: "device",
40302
+ addonId: null,
40303
+ access: "view"
40304
+ },
40305
+ "pressureSensor.getStatus": {
40306
+ capName: "pressure-sensor",
40307
+ capScope: "device",
40308
+ addonId: null,
40309
+ access: "view"
40310
+ },
37143
40311
  "privacyMask.getOptions": {
37144
40312
  capName: "privacy-mask",
37145
40313
  capScope: "device",
37146
40314
  addonId: null,
37147
40315
  access: "view"
37148
40316
  },
40317
+ "privacyMask.getStatus": {
40318
+ capName: "privacy-mask",
40319
+ capScope: "device",
40320
+ addonId: null,
40321
+ access: "view"
40322
+ },
37149
40323
  "privacyMask.setAudioEnabled": {
37150
40324
  capName: "privacy-mask",
37151
40325
  capScope: "device",
@@ -37206,6 +40380,12 @@ Object.freeze({
37206
40380
  addonId: null,
37207
40381
  access: "view"
37208
40382
  },
40383
+ "ptz.getStatus": {
40384
+ capName: "ptz",
40385
+ capScope: "device",
40386
+ addonId: null,
40387
+ access: "view"
40388
+ },
37209
40389
  "ptz.goHome": {
37210
40390
  capName: "ptz",
37211
40391
  capScope: "device",
@@ -37284,6 +40464,12 @@ Object.freeze({
37284
40464
  addonId: null,
37285
40465
  access: "create"
37286
40466
  },
40467
+ "recording.applyDeviceSettingsPatch": {
40468
+ capName: "recording",
40469
+ capScope: "system",
40470
+ addonId: null,
40471
+ access: "create"
40472
+ },
37287
40473
  "recording.cancelRelocateJob": {
37288
40474
  capName: "recording",
37289
40475
  capScope: "system",
@@ -37332,6 +40518,18 @@ Object.freeze({
37332
40518
  addonId: null,
37333
40519
  access: "view"
37334
40520
  },
40521
+ "recording.getDeviceLiveContribution": {
40522
+ capName: "recording",
40523
+ capScope: "system",
40524
+ addonId: null,
40525
+ access: "view"
40526
+ },
40527
+ "recording.getDeviceSettingsContribution": {
40528
+ capName: "recording",
40529
+ capScope: "system",
40530
+ addonId: null,
40531
+ access: "view"
40532
+ },
37335
40533
  "recording.getPlacement": {
37336
40534
  capName: "recording",
37337
40535
  capScope: "system",
@@ -37350,6 +40548,12 @@ Object.freeze({
37350
40548
  addonId: null,
37351
40549
  access: "view"
37352
40550
  },
40551
+ "recording.getStatus": {
40552
+ capName: "recording",
40553
+ capScope: "system",
40554
+ addonId: null,
40555
+ access: "view"
40556
+ },
37353
40557
  "recording.getStorageMigrationMoveStatus": {
37354
40558
  capName: "recording",
37355
40559
  capScope: "system",
@@ -37524,6 +40728,24 @@ Object.freeze({
37524
40728
  addonId: null,
37525
40729
  access: "view"
37526
40730
  },
40731
+ "recordingOnboard.getOptions": {
40732
+ capName: "recording-onboard",
40733
+ capScope: "device",
40734
+ addonId: null,
40735
+ access: "view"
40736
+ },
40737
+ "recordingOnboard.getStatus": {
40738
+ capName: "recording-onboard",
40739
+ capScope: "device",
40740
+ addonId: null,
40741
+ access: "view"
40742
+ },
40743
+ "recordingOnboard.setSettings": {
40744
+ capName: "recording-onboard",
40745
+ capScope: "device",
40746
+ addonId: null,
40747
+ access: "create"
40748
+ },
37527
40749
  "recordingSignal.getStatus": {
37528
40750
  capName: "recording-signal",
37529
40751
  capScope: "device",
@@ -37554,6 +40776,12 @@ Object.freeze({
37554
40776
  addonId: null,
37555
40777
  access: "delete"
37556
40778
  },
40779
+ "sceneMonitor.getStatus": {
40780
+ capName: "scene-monitor",
40781
+ capScope: "device",
40782
+ addonId: null,
40783
+ access: "view"
40784
+ },
37557
40785
  "sceneMonitor.listScenes": {
37558
40786
  capName: "scene-monitor",
37559
40787
  capScope: "device",
@@ -37584,6 +40812,12 @@ Object.freeze({
37584
40812
  addonId: null,
37585
40813
  access: "create"
37586
40814
  },
40815
+ "scriptRunner.getStatus": {
40816
+ capName: "script-runner",
40817
+ capScope: "device",
40818
+ addonId: null,
40819
+ access: "view"
40820
+ },
37587
40821
  "scriptRunner.run": {
37588
40822
  capName: "script-runner",
37589
40823
  capScope: "device",
@@ -37716,6 +40950,12 @@ Object.freeze({
37716
40950
  addonId: null,
37717
40951
  access: "create"
37718
40952
  },
40953
+ "smoke.getStatus": {
40954
+ capName: "smoke",
40955
+ capScope: "device",
40956
+ addonId: null,
40957
+ access: "view"
40958
+ },
37719
40959
  "smtpProvider.getStatus": {
37720
40960
  capName: "smtp-provider",
37721
40961
  capScope: "system",
@@ -37734,12 +40974,30 @@ Object.freeze({
37734
40974
  addonId: null,
37735
40975
  access: "view"
37736
40976
  },
40977
+ "snapshot.applyDeviceSettingsPatch": {
40978
+ capName: "snapshot",
40979
+ capScope: "device",
40980
+ addonId: null,
40981
+ access: "create"
40982
+ },
37737
40983
  "snapshot.getDebugState": {
37738
40984
  capName: "snapshot",
37739
40985
  capScope: "device",
37740
40986
  addonId: null,
37741
40987
  access: "view"
37742
40988
  },
40989
+ "snapshot.getDeviceLiveContribution": {
40990
+ capName: "snapshot",
40991
+ capScope: "device",
40992
+ addonId: null,
40993
+ access: "view"
40994
+ },
40995
+ "snapshot.getDeviceSettingsContribution": {
40996
+ capName: "snapshot",
40997
+ capScope: "device",
40998
+ addonId: null,
40999
+ access: "view"
41000
+ },
37743
41001
  "snapshot.getSnapshot": {
37744
41002
  capName: "snapshot",
37745
41003
  capScope: "device",
@@ -37758,6 +41016,12 @@ Object.freeze({
37758
41016
  addonId: null,
37759
41017
  access: "view"
37760
41018
  },
41019
+ "snapshot.getStatus": {
41020
+ capName: "snapshot",
41021
+ capScope: "device",
41022
+ addonId: null,
41023
+ access: "view"
41024
+ },
37761
41025
  "snapshot.invalidateCache": {
37762
41026
  capName: "snapshot",
37763
41027
  capScope: "device",
@@ -38094,6 +41358,12 @@ Object.freeze({
38094
41358
  addonId: null,
38095
41359
  access: "create"
38096
41360
  },
41361
+ "streamBroker.applyDeviceSettingsPatch": {
41362
+ capName: "stream-broker",
41363
+ capScope: "system",
41364
+ addonId: null,
41365
+ access: "create"
41366
+ },
38097
41367
  "streamBroker.assignProfile": {
38098
41368
  capName: "stream-broker",
38099
41369
  capScope: "system",
@@ -38130,6 +41400,18 @@ Object.freeze({
38130
41400
  addonId: null,
38131
41401
  access: "view"
38132
41402
  },
41403
+ "streamBroker.getDeviceLiveContribution": {
41404
+ capName: "stream-broker",
41405
+ capScope: "system",
41406
+ addonId: null,
41407
+ access: "view"
41408
+ },
41409
+ "streamBroker.getDeviceSettingsContribution": {
41410
+ capName: "stream-broker",
41411
+ capScope: "system",
41412
+ addonId: null,
41413
+ access: "view"
41414
+ },
38133
41415
  "streamBroker.getPreBufferInfo": {
38134
41416
  capName: "stream-broker",
38135
41417
  capScope: "system",
@@ -38328,6 +41610,12 @@ Object.freeze({
38328
41610
  addonId: null,
38329
41611
  access: "view"
38330
41612
  },
41613
+ "streamParams.getStatus": {
41614
+ capName: "stream-params",
41615
+ capScope: "device",
41616
+ addonId: null,
41617
+ access: "view"
41618
+ },
38331
41619
  "streamParams.setProfile": {
38332
41620
  capName: "stream-params",
38333
41621
  capScope: "device",
@@ -38340,6 +41628,12 @@ Object.freeze({
38340
41628
  addonId: null,
38341
41629
  access: "view"
38342
41630
  },
41631
+ "switch.getStatus": {
41632
+ capName: "switch",
41633
+ capScope: "device",
41634
+ addonId: null,
41635
+ access: "view"
41636
+ },
38343
41637
  "switch.setState": {
38344
41638
  capName: "switch",
38345
41639
  capScope: "device",
@@ -38436,6 +41730,18 @@ Object.freeze({
38436
41730
  addonId: null,
38437
41731
  access: "create"
38438
41732
  },
41733
+ "tamper.getStatus": {
41734
+ capName: "tamper",
41735
+ capScope: "device",
41736
+ addonId: null,
41737
+ access: "view"
41738
+ },
41739
+ "temperatureSensor.getStatus": {
41740
+ capName: "temperature-sensor",
41741
+ capScope: "device",
41742
+ addonId: null,
41743
+ access: "view"
41744
+ },
38439
41745
  "terminalSession.adoptLegacyMonitor": {
38440
41746
  capName: "terminal-session",
38441
41747
  capScope: "system",
@@ -38532,6 +41838,12 @@ Object.freeze({
38532
41838
  addonId: null,
38533
41839
  access: "view"
38534
41840
  },
41841
+ "update.getStatus": {
41842
+ capName: "update",
41843
+ capScope: "device",
41844
+ addonId: null,
41845
+ access: "view"
41846
+ },
38535
41847
  "update.installUpdate": {
38536
41848
  capName: "update",
38537
41849
  capScope: "device",
@@ -38754,6 +42066,12 @@ Object.freeze({
38754
42066
  addonId: null,
38755
42067
  access: "create"
38756
42068
  },
42069
+ "vacuumControl.getStatus": {
42070
+ capName: "vacuum-control",
42071
+ capScope: "device",
42072
+ addonId: null,
42073
+ access: "view"
42074
+ },
38757
42075
  "vacuumControl.locate": {
38758
42076
  capName: "vacuum-control",
38759
42077
  capScope: "device",
@@ -38796,6 +42114,12 @@ Object.freeze({
38796
42114
  addonId: null,
38797
42115
  access: "create"
38798
42116
  },
42117
+ "valve.getStatus": {
42118
+ capName: "valve",
42119
+ capScope: "device",
42120
+ addonId: null,
42121
+ access: "view"
42122
+ },
38799
42123
  "valve.open": {
38800
42124
  capName: "valve",
38801
42125
  capScope: "device",
@@ -38868,6 +42192,18 @@ Object.freeze({
38868
42192
  addonId: null,
38869
42193
  access: "create"
38870
42194
  },
42195
+ "vibration.getStatus": {
42196
+ capName: "vibration",
42197
+ capScope: "device",
42198
+ addonId: null,
42199
+ access: "view"
42200
+ },
42201
+ "videoclips.dialClipStream": {
42202
+ capName: "videoclips",
42203
+ capScope: "device",
42204
+ addonId: null,
42205
+ access: "view"
42206
+ },
38871
42207
  "videoclips.getClipPlayback": {
38872
42208
  capName: "videoclips",
38873
42209
  capScope: "device",
@@ -38880,6 +42216,18 @@ Object.freeze({
38880
42216
  addonId: null,
38881
42217
  access: "view"
38882
42218
  },
42219
+ "videoclips.listSources": {
42220
+ capName: "videoclips",
42221
+ capScope: "device",
42222
+ addonId: null,
42223
+ access: "view"
42224
+ },
42225
+ "videoclips.readClipBytes": {
42226
+ capName: "videoclips",
42227
+ capScope: "device",
42228
+ addonId: null,
42229
+ access: "view"
42230
+ },
38883
42231
  "viewerUi.getStaticDir": {
38884
42232
  capName: "viewer-ui",
38885
42233
  capScope: "system",
@@ -38892,6 +42240,12 @@ Object.freeze({
38892
42240
  addonId: null,
38893
42241
  access: "view"
38894
42242
  },
42243
+ "waterHeater.getStatus": {
42244
+ capName: "water-heater",
42245
+ capScope: "device",
42246
+ addonId: null,
42247
+ access: "view"
42248
+ },
38895
42249
  "waterHeater.setAway": {
38896
42250
  capName: "water-heater",
38897
42251
  capScope: "device",
@@ -38910,6 +42264,12 @@ Object.freeze({
38910
42264
  addonId: null,
38911
42265
  access: "create"
38912
42266
  },
42267
+ "weather.getStatus": {
42268
+ capName: "weather",
42269
+ capScope: "device",
42270
+ addonId: null,
42271
+ access: "view"
42272
+ },
38913
42273
  "webrtcSession.addIceCandidate": {
38914
42274
  capName: "webrtc-session",
38915
42275
  capScope: "device",
@@ -38934,6 +42294,12 @@ Object.freeze({
38934
42294
  addonId: null,
38935
42295
  access: "view"
38936
42296
  },
42297
+ "webrtcSession.getSessionDebug": {
42298
+ capName: "webrtc-session",
42299
+ capScope: "device",
42300
+ addonId: null,
42301
+ access: "view"
42302
+ },
38937
42303
  "webrtcSession.getSessionState": {
38938
42304
  capName: "webrtc-session",
38939
42305
  capScope: "device",
@@ -39266,6 +42632,11 @@ Object.freeze({
39266
42632
  form: "single",
39267
42633
  optional: true
39268
42634
  }],
42635
+ "deviceAdminLink.getAdminLink": [{
42636
+ name: "deviceId",
42637
+ form: "single",
42638
+ optional: false
42639
+ }],
39269
42640
  "deviceAdoption.release": [{
39270
42641
  name: "camDeviceId",
39271
42642
  form: "single",
@@ -40176,6 +43547,11 @@ Object.freeze({
40176
43547
  form: "single",
40177
43548
  optional: false
40178
43549
  }],
43550
+ "pipelineAnalytics.ownersWithMedia": [{
43551
+ name: "deviceId",
43552
+ form: "single",
43553
+ optional: false
43554
+ }],
40179
43555
  "pipelineAnalytics.proposeRetrainAnnotations": [{
40180
43556
  name: "deviceId",
40181
43557
  form: "single",
@@ -40646,6 +44022,16 @@ Object.freeze({
40646
44022
  form: "single",
40647
44023
  optional: true
40648
44024
  }],
44025
+ "recordingOnboard.getOptions": [{
44026
+ name: "deviceId",
44027
+ form: "single",
44028
+ optional: false
44029
+ }],
44030
+ "recordingOnboard.setSettings": [{
44031
+ name: "deviceId",
44032
+ form: "single",
44033
+ optional: false
44034
+ }],
40649
44035
  "recordingSignal.getStatus": [{
40650
44036
  name: "deviceId",
40651
44037
  form: "single",
@@ -40872,6 +44258,11 @@ Object.freeze({
40872
44258
  form: "single",
40873
44259
  optional: false
40874
44260
  }],
44261
+ "videoclips.dialClipStream": [{
44262
+ name: "deviceId",
44263
+ form: "single",
44264
+ optional: false
44265
+ }],
40875
44266
  "videoclips.getClipPlayback": [{
40876
44267
  name: "deviceId",
40877
44268
  form: "single",
@@ -40882,6 +44273,16 @@ Object.freeze({
40882
44273
  form: "single",
40883
44274
  optional: false
40884
44275
  }],
44276
+ "videoclips.listSources": [{
44277
+ name: "deviceId",
44278
+ form: "single",
44279
+ optional: false
44280
+ }],
44281
+ "videoclips.readClipBytes": [{
44282
+ name: "deviceId",
44283
+ form: "single",
44284
+ optional: false
44285
+ }],
40885
44286
  "waterHeater.setAway": [{
40886
44287
  name: "deviceId",
40887
44288
  form: "single",
@@ -40917,6 +44318,11 @@ Object.freeze({
40917
44318
  form: "single",
40918
44319
  optional: false
40919
44320
  }],
44321
+ "webrtcSession.getSessionDebug": [{
44322
+ name: "deviceId",
44323
+ form: "single",
44324
+ optional: false
44325
+ }],
40920
44326
  "webrtcSession.getSessionState": [{
40921
44327
  name: "deviceId",
40922
44328
  form: "single",
@@ -50592,6 +53998,37 @@ var OnvifCamera = class {
50592
53998
  },
50593
53999
  invalidateCache: async () => {}
50594
54000
  });
54001
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
54002
+ if (deviceId !== this.id) throw new Error(`OnvifCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
54003
+ const host = this.config.get("host") ?? "";
54004
+ const port = this.config.get("port") ?? 80;
54005
+ if (port !== 80 && port !== 443) {
54006
+ this.ctx.logger.debug("onvif admin link unavailable: service port is not an HTTP port", {
54007
+ tags: { deviceId: this.id },
54008
+ meta: { port }
54009
+ });
54010
+ return null;
54011
+ }
54012
+ const built = buildDeviceAdminUrl({
54013
+ host,
54014
+ port,
54015
+ scheme: port === 443 ? "https" : "http",
54016
+ path: "/"
54017
+ });
54018
+ if (!built.ok) {
54019
+ this.ctx.logger.debug("onvif admin link unavailable", {
54020
+ tags: { deviceId: this.id },
54021
+ meta: { reason: built.reason }
54022
+ });
54023
+ return null;
54024
+ }
54025
+ return {
54026
+ url: built.url,
54027
+ label: "Web UI",
54028
+ target: "device",
54029
+ host
54030
+ };
54031
+ } });
50595
54032
  if (this.hasPtz()) this.ctx.registerNativeCap(ptzCapability, {
50596
54033
  move: async ({ deviceId, pan, tilt, zoom, speed }) => {
50597
54034
  if (deviceId !== this.id) throw new Error(`OnvifCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);