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