@camstack/addon-provider-unifi 0.2.114 → 0.2.116

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.
package/dist/addon.js CHANGED
@@ -5360,7 +5360,7 @@ var ZodIssueCode = {
5360
5360
  var ZodFirstPartyTypeKind;
5361
5361
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5362
5362
  //#endregion
5363
- //#region ../types/dist/sleep-Bs3xPtSh.mjs
5363
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
5364
5364
  /**
5365
5365
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5366
5366
  * window to float samples (D455).
@@ -6502,6 +6502,17 @@ var CamProfileSchema = _enum([
6502
6502
  "mid",
6503
6503
  "low"
6504
6504
  ]);
6505
+ /**
6506
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
6507
+ *
6508
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
6509
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
6510
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
6511
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
6512
+ * the operator picked. Today's one user is the audio analyser, whose rule is
6513
+ * "the lowest AVAILABLE native rung" (D537).
6514
+ */
6515
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
6505
6516
  var CamStreamKindSchema = _enum([
6506
6517
  "pull-rtsp",
6507
6518
  "pull-rtmp",
@@ -6920,6 +6931,18 @@ function systemMethod(input, output, options) {
6920
6931
  systemOnly: true
6921
6932
  };
6922
6933
  }
6934
+ /**
6935
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
6936
+ * on `InferProvider`. The `providerOptional: true` literal is what
6937
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
6938
+ * for when this is the honest answer and when it is a soft stub.
6939
+ */
6940
+ function optionalMethod(input, output, options) {
6941
+ return {
6942
+ ...method(input, output, options),
6943
+ providerOptional: true
6944
+ };
6945
+ }
6923
6946
  /** Shorthand to define an event schema */
6924
6947
  function event(data) {
6925
6948
  return { data };
@@ -7350,6 +7373,8 @@ var EVENT_OWNER_TYPES = [
7350
7373
  * nothing failing until a caller asked.
7351
7374
  */
7352
7375
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7376
+ /** The same list as a Zod enum, for the cap input that carries it. */
7377
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7353
7378
  new Set(EVENT_OWNER_TYPES);
7354
7379
  var EncodeProfileSchema = object({
7355
7380
  video: object({
@@ -7722,6 +7747,7 @@ var CameraSwitchIdSchema = _enum([
7722
7747
  "device-audio",
7723
7748
  "broker-audio",
7724
7749
  "audio-analysis",
7750
+ "event-saving",
7725
7751
  "recording",
7726
7752
  "notifications"
7727
7753
  ]);
@@ -7757,10 +7783,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7757
7783
  var CameraSwitchUnavailableReasonSchema = _enum([
7758
7784
  "no-provider",
7759
7785
  "source-unreachable",
7760
- "not-configured",
7761
7786
  "device-disabled"
7762
7787
  ]);
7763
7788
  /**
7789
+ * Something true about a switch that is nonetheless OFFERED.
7790
+ *
7791
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
7792
+ * whole point: a reason replaces the control, a warning travels WITH it. The
7793
+ * privacy mask taught us the difference — greying the one button that starts
7794
+ * the feature teaches the operator the feature is broken, while a live toggle
7795
+ * that flips nothing teaches them the switch is broken. Neither is true; the
7796
+ * honest surface is a working toggle that says what is missing.
7797
+ */
7798
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
7799
+ /**
7764
7800
  * One switch, resolved for one camera.
7765
7801
  *
7766
7802
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -7779,6 +7815,11 @@ var CameraSwitchSchema = object({
7779
7815
  /** False = do not render a control. `unavailableReason` says why. */
7780
7816
  available: boolean(),
7781
7817
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7818
+ /**
7819
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
7820
+ * never a substitute for one.
7821
+ */
7822
+ warning: CameraSwitchWarningSchema.optional(),
7782
7823
  /** Current state. Meaningless when `available` is false — read it as `true`. */
7783
7824
  enabled: boolean(),
7784
7825
  authority: CameraSwitchAuthoritySchema
@@ -8196,11 +8237,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
8196
8237
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
8197
8238
  * span wraps past midnight (handled by the band engine).
8198
8239
  */
8240
+ /**
8241
+ * Whether a band's footage carries sound.
8242
+ *
8243
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
8244
+ * camera recorded sound whether anyone wanted it or not. The broker already
8245
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
8246
+ * restream — so this was a choice the product could always have offered.
8247
+ *
8248
+ * It belongs on the BAND rather than the camera because one camera can want
8249
+ * both: silent across the shop floor during opening hours, sound overnight.
8250
+ * And it applies to BOTH modes — an events band firing on motion has the same
8251
+ * question to answer as a continuous one.
8252
+ */
8253
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
8199
8254
  var RecordingBandSchema = object({
8200
8255
  days: array(RecordingWeekdaySchema),
8201
8256
  start: string().regex(HHMM),
8202
8257
  end: string().regex(HHMM),
8203
8258
  mode: RecordingBandModeSchema,
8259
+ /**
8260
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
8261
+ * written before this existed carried sound, and a band whose key is
8262
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
8263
+ */
8264
+ audio: RecordingBandAudioSchema.optional(),
8204
8265
  triggers: RecordingBandTriggersSchema.optional(),
8205
8266
  /**
8206
8267
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -13484,6 +13545,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13484
13545
  auth: "admin"
13485
13546
  });
13486
13547
  /**
13548
+ * device-admin-link — "this device has a management page of its own, and here
13549
+ * is its address".
13550
+ *
13551
+ * ## Why this is not a `deviceConfig` cap
13552
+ *
13553
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13554
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13555
+ * patch back through a setter; it costs a `builderId` reducer in
13556
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13557
+ * renders a form section. This cap answers ONE question with ONE read and
13558
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13559
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13560
+ * `reboot`, the other pure-RPC device-native cap.
13561
+ *
13562
+ * ## Absent, and the difference between "no page" and "we cannot say"
13563
+ *
13564
+ * The two are answered at DIFFERENT layers, on purpose:
13565
+ *
13566
+ * - **"We cannot say"** → the provider never registers the cap for that
13567
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13568
+ * fan are reached only through a vendor cloud; there is no address to hand
13569
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13570
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13571
+ * behind it. None of them register, so `deviceManager.getBindings` never
13572
+ * lists the cap and no surface asks.
13573
+ * - **"This device has no page, and I know that"** → the provider registers
13574
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13575
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13576
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13577
+ * transport, a Home Assistant broker authenticated by supervisor token
13578
+ * (which carries no `baseUrl` at all).
13579
+ *
13580
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13581
+ * button, and D62 is the same rule from the other side: an off switch is
13582
+ * reported off, never made to look broken. There is no third state where the
13583
+ * UI renders a disabled button "because the device might have a page".
13584
+ *
13585
+ * ## The URL never carries credentials
13586
+ *
13587
+ * Not in userinfo, not in a query string. Every provider builds through
13588
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13589
+ * scheme and path as separate arguments — there is no parameter a secret could
13590
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13591
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13592
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13593
+ * keeps providers from hand-rolling one anyway.
13594
+ *
13595
+ * This matters here more than anywhere else in the repo, because every provider
13596
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13597
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13598
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13599
+ * camera's own page will ask for its own login. That is correct, and pre-
13600
+ * filling it is the operator's business, not ours.
13601
+ *
13602
+ * ## It is a LAN fact
13603
+ *
13604
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13605
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13606
+ * as a link the operator's own browser follows, on the operator's own network,
13607
+ * or renders nothing.
13608
+ */
13609
+ /**
13610
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13611
+ * before clicking whether he is about to land on a camera's own web server or
13612
+ * inside Home Assistant.
13613
+ */
13614
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13615
+ var DeviceAdminLinkSchema = object({
13616
+ /**
13617
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13618
+ * free of userinfo and of any credential-shaped query key.
13619
+ */
13620
+ url: string(),
13621
+ /**
13622
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13623
+ * The PROVIDER names it, because only the provider knows what the page is;
13624
+ * a UI that invented the label from the addon id would call the Home
13625
+ * Assistant device page "Provider Homeassistant".
13626
+ */
13627
+ label: string(),
13628
+ target: AdminLinkTargetEnum,
13629
+ /**
13630
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13631
+ * an operator can see WHERE the button goes before he follows it. Redundant
13632
+ * with `url` by construction; carried separately so no surface has to parse
13633
+ * a URL to show it.
13634
+ */
13635
+ host: string()
13636
+ });
13637
+ var deviceAdminLinkCapability = {
13638
+ name: "device-admin-link",
13639
+ scope: "device",
13640
+ deviceNative: true,
13641
+ mode: "singleton",
13642
+ methods: {
13643
+ /**
13644
+ * The device's management page, or `null` when this device has none.
13645
+ *
13646
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13647
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13648
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13649
+ * The URL is also a statement about the LAN, which a household member with
13650
+ * a `view` grant on a light has no reason to be handed.
13651
+ *
13652
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13653
+ * without the right loses the query and draws nothing, which is the same
13654
+ * thing a device with no page draws. There is no path on which a button
13655
+ * appears and then fails — the D403 failure mode, from the other end.
13656
+ */
13657
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13658
+ };
13659
+ /**
13660
+ * Query keys that may never appear on an admin link. A credential smuggled as
13661
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13662
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13663
+ * identically.
13664
+ */
13665
+ var CREDENTIAL_QUERY_KEYS = [
13666
+ "password",
13667
+ "passwd",
13668
+ "pwd",
13669
+ "pass",
13670
+ "user",
13671
+ "username",
13672
+ "usr",
13673
+ "login",
13674
+ "token",
13675
+ "access_token",
13676
+ "auth",
13677
+ "authorization",
13678
+ "apikey",
13679
+ "api_key",
13680
+ "secret",
13681
+ "credential",
13682
+ "credentials",
13683
+ "session",
13684
+ "sessionid",
13685
+ "key"
13686
+ ];
13687
+ /**
13688
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13689
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13690
+ * structurally identical on purpose: a URL this function returns is a URL that
13691
+ * guard would pass.
13692
+ */
13693
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13694
+ /** A host that is safe to place in an authority component verbatim. */
13695
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13696
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13697
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13698
+ function defaultPortFor(scheme) {
13699
+ return scheme === "https" ? 443 : 80;
13700
+ }
13701
+ /**
13702
+ * Build the admin-page URL, or refuse by name.
13703
+ *
13704
+ * The refusal is never thrown: a provider answering "no page for this device"
13705
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13706
+ * a missing host on one device into a failed query for the whole surface.
13707
+ */
13708
+ function buildDeviceAdminUrl(parts) {
13709
+ const host = parts.host.trim();
13710
+ if (host.length === 0) return {
13711
+ ok: false,
13712
+ reason: "empty-host"
13713
+ };
13714
+ if (host.includes("@")) return {
13715
+ ok: false,
13716
+ reason: "host-carries-userinfo"
13717
+ };
13718
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13719
+ ok: false,
13720
+ reason: "host-not-plain"
13721
+ };
13722
+ if (parts.port !== null) {
13723
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13724
+ ok: false,
13725
+ reason: "port-out-of-range"
13726
+ };
13727
+ }
13728
+ if (!parts.path.startsWith("/")) return {
13729
+ ok: false,
13730
+ reason: "path-not-absolute"
13731
+ };
13732
+ const query = parts.query ?? {};
13733
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13734
+ ok: false,
13735
+ reason: "credential-query-key"
13736
+ };
13737
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13738
+ const search = new URLSearchParams();
13739
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13740
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13741
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13742
+ if (CREDENTIAL_URL.test(url)) return {
13743
+ ok: false,
13744
+ reason: "userinfo-in-result"
13745
+ };
13746
+ return {
13747
+ ok: true,
13748
+ url
13749
+ };
13750
+ }
13751
+ /**
13487
13752
  * `device-export` — collection cap for addons that export camstack
13488
13753
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
13489
13754
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -20250,6 +20515,7 @@ var MediaFileKindEnum = _enum([
20250
20515
  "keyFrameSmall",
20251
20516
  "thumbnailSmall",
20252
20517
  "confirmationCrop",
20518
+ "debugCrop",
20253
20519
  "sceneFrame"
20254
20520
  ]);
20255
20521
  /**
@@ -21329,7 +21595,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21329
21595
  ownerType: EventOwnerTypeSchema,
21330
21596
  eventId: number().int(),
21331
21597
  deviceId: number()
21332
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21598
+ }), array(MediaFileInfoSchema).readonly()), method(object({
21599
+ deviceId: number(),
21600
+ ownerKind: MediaPresenceOwnerKindSchema,
21601
+ ownerIds: array(string()).max(5e3)
21602
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21333
21603
  kind: "mutation",
21334
21604
  auth: "admin"
21335
21605
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -21914,6 +22184,18 @@ var RunnerCameraConfigSchema = object({
21914
22184
  motionStreamId: string(),
21915
22185
  detectionStreamId: string(),
21916
22186
  /**
22187
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
22188
+ * AVAILABLE native rung) or an explicit one.
22189
+ *
22190
+ * Not consumed by the runner: carried here so it travels with the camera's
22191
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
22192
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
22193
+ * with no default — absent means `auto`, which is what every camera written
22194
+ * before this field had, and an absent optional adds nothing to the
22195
+ * generated cap-input defaults (D537).
22196
+ */
22197
+ audioStreamPick: AudioStreamPickSchema.optional(),
22198
+ /**
21917
22199
  * Motion sources that can activate the pipeline. At least one must
21918
22200
  * be present. Multiple sources OR-ed: any one firing arms the
21919
22201
  * cooldown timer and transitions watching → active. The same
@@ -22984,10 +23266,9 @@ var ServerBootModeSchema = _enum([
22984
23266
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
22985
23267
  * - `pending-restart` — a version is staged and the node has NOT yet
22986
23268
  * restarted onto it (still running the OLD version).
22987
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
22988
- * (it is the active probation boot) and is waiting to confirm boot-health.
22989
- * Apply/rollback are refused in this state and the node must NOT be
22990
- * manually restarted, or the probation boot auto-rolls-back.
23269
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
23270
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
23271
+ * no provider emits it.
22991
23272
  */
22992
23273
  var ServerUpdateStateSchema = _enum([
22993
23274
  "idle",
@@ -23076,12 +23357,35 @@ var ServerRollbackInfoSchema = object({
23076
23357
  var ServerPackageStatusSchema = object({
23077
23358
  /** Root package name (`@camstack/server` on the hub). */
23078
23359
  packageName: string(),
23079
- /** Version of the code the running process ACTUALLY loaded. */
23360
+ /**
23361
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
23362
+ * package.json of the copy the code was loaded from, and never re-read. A
23363
+ * copy replaced on disk under a live process must not move this number;
23364
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
23365
+ * was a lazy disk read that would have followed such a swap silently.
23366
+ */
23080
23367
  runningVersion: string().nullable(),
23081
23368
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
23082
23369
  nodeRuntimeVersion: string().nullable(),
23083
23370
  /** Active data-dir root version; null when booted from seed/workspace. */
23084
23371
  activeVersion: string().nullable(),
23372
+ /**
23373
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
23374
+ * what the next boot would load if nothing is staged. Normally equal to
23375
+ * `runningVersion`; differs only when the copy on disk changed under the
23376
+ * live process. `null` when there is no data-root copy (baked seed or
23377
+ * workspace boot). Optional for version skew: an older provider omits it.
23378
+ */
23379
+ installedVersion: string().nullable().optional(),
23380
+ /**
23381
+ * True when the code that would run after a restart is NOT the code running
23382
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
23383
+ * from `runningVersion`. Derived on every read, never stored. This is the
23384
+ * one field that lets an operator or a script say "1.2.353 is installed and
23385
+ * will run after a restart" instead of reading it off a number that means
23386
+ * "installed". Optional for version skew.
23387
+ */
23388
+ restartRequired: boolean().optional(),
23085
23389
  /** N-1 version kept for rollback; null when no previous version exists. */
23086
23390
  previousVersion: string().nullable(),
23087
23391
  /** Version of the immutable baked seed closure (image fallback). */
@@ -23116,19 +23420,71 @@ var ServerPackageStatusSchema = object({
23116
23420
  });
23117
23421
  var ServerUpdateCheckResultSchema = object({
23118
23422
  packageName: string(),
23423
+ /** Same meaning as on the status payload: what THIS process loaded. */
23119
23424
  runningVersion: string().nullable(),
23120
23425
  latestVersion: string().nullable(),
23426
+ /**
23427
+ * `latestVersion` is newer than `runningVersion` AND is not the version
23428
+ * already staged on this node. A staged-and-waiting version is not "an
23429
+ * update available" — applying it again is refused (`already-staged`), and
23430
+ * a button that announced "updating…" on this boolean then got refused
23431
+ * would be lying. It is reported through `pendingVersion` instead.
23432
+ */
23121
23433
  updateAvailable: boolean(),
23122
23434
  checkedAtMs: number(),
23123
23435
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
23124
- error: string().nullable()
23436
+ error: string().nullable(),
23437
+ /**
23438
+ * Version staged on this node and awaiting its restart, when one is. Before
23439
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
23440
+ * node whose staged closure had never booted, and the caller had no way to
23441
+ * see the difference. Optional for version skew.
23442
+ */
23443
+ pendingVersion: string().nullable().optional(),
23444
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
23445
+ restartRequired: boolean().optional()
23125
23446
  });
23447
+ /**
23448
+ * What an action DID — the discriminant `accepted` cannot carry. Before
23449
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
23450
+ * where nothing was wrong (already current, already staged) and for a staging
23451
+ * install that died on `npm error code ECONNRESET`; the only difference was
23452
+ * prose in `message`, and an operator restarted a container on the strength of
23453
+ * it, booting the previous version.
23454
+ *
23455
+ * - `staged` — the closure is installed and validated; the node
23456
+ * restarts to apply it (`restarting:true`). NOTHING
23457
+ * runs the new code until that restart completes.
23458
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
23459
+ * - `already-current` — the target IS the running version; nothing done.
23460
+ * - `refused` — dropped by a gate; `reason` names which.
23461
+ * - `failed` — attempted and did not land; `error` says why.
23462
+ */
23463
+ var ServerUpdateOutcomeSchema = _enum([
23464
+ "staged",
23465
+ "restart-scheduled",
23466
+ "already-current",
23467
+ "refused",
23468
+ "failed"
23469
+ ]);
23126
23470
  var ServerUpdateActionResultSchema = object({
23127
23471
  accepted: boolean(),
23128
23472
  targetVersion: string().nullable(),
23129
23473
  /** True when a graceful restart was scheduled to apply the change. */
23130
23474
  restarting: boolean(),
23131
- message: string()
23475
+ message: string(),
23476
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
23477
+ outcome: ServerUpdateOutcomeSchema.optional(),
23478
+ /**
23479
+ * Stable, greppable cause for `refused` / `failed` (the engine's
23480
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
23481
+ */
23482
+ reason: string().nullable().optional(),
23483
+ /**
23484
+ * The failure itself, verbatim enough to act on (the npm output for a
23485
+ * staging install). Non-null only for `failed`.
23486
+ */
23487
+ error: string().nullable().optional()
23132
23488
  });
23133
23489
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
23134
23490
  kind: "mutation",
@@ -24237,6 +24593,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
24237
24593
  kind: "mutation",
24238
24594
  auth: "admin"
24239
24595
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
24596
+ _enum([
24597
+ "queue-full",
24598
+ "camera-backoff",
24599
+ "sleeping",
24600
+ "camera-refused",
24601
+ "no-keyframe",
24602
+ "no-catalog-row",
24603
+ "unsupported",
24604
+ "unknown-device",
24605
+ "uid-missing"
24606
+ ]);
24607
+ _enum(["deferred", "final"]);
24240
24608
  var ClipSchema = object({
24241
24609
  /** Opaque, provider-namespaced id. The default provider encodes the time
24242
24610
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -24311,29 +24679,455 @@ var ClipSchema = object({
24311
24679
  holes: array(object({
24312
24680
  startMs: number(),
24313
24681
  endMs: number()
24314
- })).optional()
24682
+ })).optional(),
24683
+ /**
24684
+ * A URL that MAY produce this clip's still, asked only for rows actually on
24685
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
24686
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
24687
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
24688
+ * — a surface latches that refusal to the instant rather than retrying.
24689
+ *
24690
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
24691
+ */
24692
+ thumbnailMint: string().optional(),
24693
+ /**
24694
+ * Why no still will be produced for this clip RIGHT NOW — set when the
24695
+ * provider already knows, so the surface draws the glyph and the reason
24696
+ * instead of firing a mint that cannot succeed.
24697
+ *
24698
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
24699
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
24700
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
24701
+ * inside no file is the measured case).
24702
+ * `unsupported` — this source cannot mint stills at all.
24703
+ */
24704
+ thumbnailUnavailable: object({ reason: _enum([
24705
+ "sleeping",
24706
+ "camera-refused",
24707
+ "no-keyframe",
24708
+ "unsupported"
24709
+ ]) }).optional(),
24710
+ /**
24711
+ * When the catalog this row came from was last CONFIRMED against the device.
24712
+ * Absent means "this row was read live". A persisted catalog served while a
24713
+ * camera sleeps carries the age it really has — a cached list is never drawn
24714
+ * as current (D549 13).
24715
+ */
24716
+ catalogAsOf: number().optional(),
24717
+ /**
24718
+ * The camera's OWN type strings for this clip, all of them, unmapped
24719
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
24720
+ * because a firmware inventing a type must not vanish: the mapping into our
24721
+ * filter vocabulary is lossy on purpose and this is the lossless copy
24722
+ * (D549 20).
24723
+ */
24724
+ nativeTypes: array(string()).optional(),
24725
+ /**
24726
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
24727
+ * travels BESIDE a class, never instead of one, and the class filter ignores
24728
+ * it: "a person crossed a line" is still a person (D549 20).
24729
+ */
24730
+ behaviours: array(string()).optional(),
24731
+ /**
24732
+ * The same recording as two files — the sub twin (what the row and its
24733
+ * thumbnail are) and its main twin, matched at listing time so a quality
24734
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
24735
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
24736
+ */
24737
+ streams: object({
24738
+ sub: object({
24739
+ id: string(),
24740
+ bytes: number().optional()
24741
+ }).optional(),
24742
+ main: object({
24743
+ id: string(),
24744
+ bytes: number().optional()
24745
+ }).optional()
24746
+ }).optional(),
24747
+ /** The camera says it holds a sub-stream copy of this recording. */
24748
+ supportSub: boolean().optional(),
24749
+ /**
24750
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
24751
+ * IS a file is playable, and only a source that lists EVENTS can produce a
24752
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
24753
+ * fell inside no file). Such a row is shown, never dropped and never offered
24754
+ * as playable-then-failing (D549 19).
24755
+ */
24756
+ playable: boolean().optional(),
24757
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
24758
+ unplayableReason: string().optional(),
24759
+ /**
24760
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
24761
+ * is not its end.
24762
+ *
24763
+ * Absent — the common case — means the row is closed and its `endMs` is the
24764
+ * end of the recording. Present and `true` means the source told us the file
24765
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
24766
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
24767
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
24768
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
24769
+ * on growing. A surface that drew 29 s there was lying about a clip that
24770
+ * plays for minutes, on the one row the operator looks at first.
24771
+ *
24772
+ * `endMs` is deliberately still a number: it is the best bound anything has
24773
+ * for a mint window or a byte fetch, and every consumer already requires it.
24774
+ * This flag says what it is WORTH, and a duration is not drawn from it.
24775
+ */
24776
+ inProgress: boolean().optional()
24315
24777
  });
24316
24778
  var ClipPlaybackSchema = object({
24317
- /** HLS master URL through the hub data-plane (Range + token in path). */
24779
+ /**
24780
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
24781
+ * says what kind — an HLS master playlist for a recording-derived clip, a
24782
+ * progressive MP4 for a native file the vendor addon muxed and serves with
24783
+ * `Range`. The URL is NOT a credential: both planes are registered
24784
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
24785
+ * the session cookie, so nothing is appended to it (D549 22).
24786
+ */
24318
24787
  playbackUrl: string(),
24788
+ /**
24789
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
24790
+ * existing consumer already assumes. A player that branches on this is the
24791
+ * one change a native clip needs; a player that ignores it will hand an MP4
24792
+ * to hls.js and fail parsing it as a manifest.
24793
+ */
24794
+ format: _enum(["hls", "mp4"]).optional(),
24795
+ /**
24796
+ * Which twin was actually served. A clip that holds only one stream answers
24797
+ * with the one it has, and the surface SAYS so — a missing main twin is
24798
+ * never served silently as if it were the asked-for quality (D549 15).
24799
+ */
24800
+ served: CamProfileSchema.optional(),
24319
24801
  /** Optional LAN/remote alternates for the same clip. */
24320
24802
  playbackEndpoints: array(string()).optional(),
24321
24803
  token: string().optional()
24322
24804
  });
24805
+ /**
24806
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
24807
+ *
24808
+ * `ok` is the only state whose clip list may be read as complete. The other
24809
+ * three exist because an empty list from a sleeping camera reads as "this
24810
+ * camera has no recordings", which is the defect this whole line of work is
24811
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
24812
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
24813
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
24814
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
24815
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
24816
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
24817
+ * files on both streams and both filters, with 177 GB free). That last one is
24818
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
24819
+ * which days it asked for and got nothing — "no clips" would be a lie about a
24820
+ * card that is full of them.
24821
+ */
24822
+ /**
24823
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
24824
+ *
24825
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
24826
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
24827
+ * `force` flag, and nothing else… a background caller must never set it…
24828
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
24829
+ * prefetch never set it; a surface sets it only behind the same confirm the
24830
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
24831
+ * exactly as that gesture is.
24832
+ *
24833
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
24834
+ * (D549) — a read that opened a session and then checked would have woken the
24835
+ * camera to find out it was not allowed to.
24836
+ *
24837
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
24838
+ * time the camera is awake for its own reasons — is deliberately not a member:
24839
+ * it was proposed, it is free on the battery, and the operator declined it on
24840
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
24841
+ * D558 "Considered and not taken").
24842
+ */
24843
+ var ClipWakeSchema = _enum(["authorised"]);
24844
+ /**
24845
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
24846
+ *
24847
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
24848
+ * and `served` says which twin the caller actually got.
24849
+ */
24850
+ var ClipBytesSchema = object({
24851
+ base64: string(),
24852
+ contentType: string(),
24853
+ /** Suggested filename, extension included. */
24854
+ name: string(),
24855
+ bytes: number().int().nonnegative(),
24856
+ /**
24857
+ * Which twin was actually served — the same contract
24858
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
24859
+ * export record persists it: a row read a week later must say the same thing
24860
+ * the panel said at the moment of the tap. A missing main twin is never
24861
+ * served silently as if it were the asked-for quality (D549 15).
24862
+ */
24863
+ served: CamProfileSchema,
24864
+ /**
24865
+ * The DECODED duration of the delivered file, when the fetch measured one.
24866
+ *
24867
+ * A clip fetch verifies its own completion against the catalog row and
24868
+ * retries a materially short pass (D568); this is that measurement, carried
24869
+ * so a consumer can say the same thing rather than re-deriving it. Absent
24870
+ * when the producer did not measure — never zero, which would say the file
24871
+ * is empty.
24872
+ */
24873
+ durationMs: number().positive().optional()
24874
+ });
24875
+ /**
24876
+ * Where a clip's STREAM can be dialled (D597) — the answer to
24877
+ * {@link videoclipsCapability.methods.dialClipStream}.
24878
+ *
24879
+ * `url` is plain `http://` on the provider's own token-gated listener, one
24880
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
24881
+ * path: that plane takes only a user credential, which an addon does not
24882
+ * hold, and the framework offers no addon→addon byte transport — the one
24883
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
24884
+ * a consumer dials directly, and this is that precedent one addon over.
24885
+ */
24886
+ /**
24887
+ * The audio a clip stream can be asked for, and the audio it carries.
24888
+ *
24889
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
24890
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
24891
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
24892
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
24893
+ * negotiates — and `none` is a stream asked for, or served, without sound.
24894
+ */
24895
+ var ClipStreamAudioSchema = _enum([
24896
+ "opus",
24897
+ "pcmu",
24898
+ "none"
24899
+ ]);
24900
+ /**
24901
+ * Why a stream that was ASKED for sound is not carrying it (D600).
24902
+ *
24903
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
24904
+ * means the ask was honoured, or there was no ask.
24905
+ *
24906
+ * - `not-requested` — the caller asked for `none`.
24907
+ * - `unsupported-ask` — the provider cannot produce that codec.
24908
+ *
24909
+ * Whether the RECORDING has sound at all is not knowable here: the camera
24910
+ * declares no audio track before its first frame arrives. That answer is in
24911
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
24912
+ * there. A dial that says `opus` promises the codec, never the content.
24913
+ */
24914
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
24915
+ var ClipStreamDialSchema = object({
24916
+ url: string().min(1),
24917
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
24918
+ expiresAtMs: number(),
24919
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
24920
+ transport: literal("forward-only"),
24921
+ /**
24922
+ * Which twin the stream ACTUALLY carries — not what was asked for.
24923
+ *
24924
+ * The stream serves every profile (D599), and a recording whose main twin
24925
+ * does not exist is streamed as its sub copy, which this field reports as
24926
+ * `low`. That is the whole reason the dial no longer refuses `high`:
24927
+ * stating what was served is what makes accepting honest, where a silent
24928
+ * substitution would be the 33x lie D576 forbids. Stated rather than
24929
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
24930
+ */
24931
+ served: CamProfileSchema,
24932
+ /**
24933
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
24934
+ * sound, and the same reasoning (D599, D600).
24935
+ *
24936
+ * The broker asks for the codec its WebRTC session negotiated, and the
24937
+ * provider either muxes that codec or carries none. Stating it is what
24938
+ * lets the broker wire the matching sink before the first byte, and what
24939
+ * keeps a session from being handed a codec it cannot send — the audio
24940
+ * shape of the substitution `served` exists to prevent.
24941
+ *
24942
+ * It is a promise about the CODEC, not about the content: a recording with
24943
+ * no microphone track answers `opus` here and then arrives with no audio
24944
+ * track at all. That second answer is in the stream, and the broker reads
24945
+ * it from the `moov`.
24946
+ */
24947
+ servedAudio: ClipStreamAudioSchema,
24948
+ /** Why `servedAudio` is `none` although sound was asked for. */
24949
+ audioReason: ClipStreamAudioReasonSchema.optional()
24950
+ });
24951
+ var ClipSourceAvailabilitySchema = object({
24952
+ state: _enum([
24953
+ "ok",
24954
+ "sleeping",
24955
+ "unreachable",
24956
+ "no-storage",
24957
+ "index-empty"
24958
+ ]),
24959
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
24960
+ reason: string().optional(),
24961
+ /** When this source's catalog was last CONFIRMED. A cached list is never
24962
+ * drawn as current: the surface shows the age whenever it is older than the
24963
+ * refresh interval. */
24964
+ catalogAsOf: number().optional()
24965
+ });
24966
+ /**
24967
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
24968
+ * every clip id from it is prefixed with (`analytics`,
24969
+ * `native:reolink:onboard`, `hksv`, …).
24970
+ *
24971
+ * A provider lists the sources IT serves for that device, and answers for each
24972
+ * of them whether it can answer at all. A provider with nothing to offer on a
24973
+ * camera returns `[]` — it is not that camera's business.
24974
+ */
24975
+ var ClipSourceSchema = object({
24976
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
24977
+ * of every clip id it mints. `getClipPlayback` routes on it. */
24978
+ source: string(),
24979
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
24980
+ label: string(),
24981
+ /**
24982
+ * The addon that SERVES this row.
24983
+ *
24984
+ * A surface cannot otherwise resolve a source to who answers it, and the
24985
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
24986
+ * widget — is a second authority on provider identity living in the one
24987
+ * package with no business knowing it, wrong the day a third source appears
24988
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
24989
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
24990
+ * served a hub child two of them until the two views were measured to be one
24991
+ * store read twice (D555) and then read ONE way (D565).
24992
+ *
24993
+ * Optional for version skew only. The collection dispatcher stamps it from
24994
+ * the registry, so a row that travelled through the fan-out carries the
24995
+ * authoritative id whatever the provider filled in.
24996
+ */
24997
+ addonId: string().optional(),
24998
+ availability: ClipSourceAvailabilitySchema
24999
+ });
24323
25000
  DeviceType.Camera, method(object({
24324
25001
  deviceId: number(),
24325
25002
  since: number(),
24326
25003
  until: number(),
24327
- limit: number().int().positive().optional()
25004
+ limit: number().int().positive().optional(),
25005
+ /**
25006
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25007
+ * carries, never a source id and never a list. **Required** (D554 amended).
25008
+ *
25009
+ * A provider the device is not bound to is refused by name rather than
25010
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25011
+ *
25012
+ * It was optional, documented as "absent means the device's BOUND
25013
+ * provider, which is CamStack on every camera". No code implemented
25014
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25015
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25016
+ * answered SIX rows, three from each source, merged newest-first:
25017
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25018
+ * so absence bought the union this method exists not to be, and
25019
+ * `limit` meant `limit × sources`.
25020
+ *
25021
+ * There is nothing to restore the default to. `getBindings` answers a
25022
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25023
+ * `setWrapperActive` — the singleton authority that could name one —
25024
+ * throws for a collection cap by design. Naming CamStack here instead
25025
+ * would privilege one addon by id inside a surface whose premise is
25026
+ * that sources are peers, and would be wrong on the first camera with
25027
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25028
+ * generated types make it unomittable rather than merely discouraged.
25029
+ *
25030
+ * The default belongs to the SURFACE, which has the `listSources` rows
25031
+ * and can say which one it picked (D569 § 1–3; the viewer already
25032
+ * always sends this).
25033
+ *
25034
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
25035
+ * assumed the answer was a fan-out over everything a camera has. The
25036
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25037
+ * solo"* — so the list is asked of one provider at a time and there is
25038
+ * nothing to filter out of it.
25039
+ */
25040
+ provider: string().min(1)
24328
25041
  }), array(ClipSchema).readonly(), {
24329
25042
  kind: "query",
24330
- auth: "admin"
25043
+ auth: "protected"
25044
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
25045
+ kind: "query",
25046
+ auth: "protected"
24331
25047
  }), method(object({
24332
25048
  deviceId: number(),
24333
- clipId: string()
25049
+ clipId: string(),
25050
+ /**
25051
+ * Which twin to serve, on the ONE quality scale the system already has
25052
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
25053
+ * twin; both ids are already on the row so this never re-searches the
25054
+ * camera. Absent means the provider's own default (the sub file, which
25055
+ * every source is measured to hold).
25056
+ *
25057
+ * `auto` is deliberately NOT accepted here: a stored file has no
25058
+ * broker session, so the adaptive tier cannot be resolved for it. The
25059
+ * viewer resolves `auto` to a profile the same way live does, before
25060
+ * it calls (D549 15).
25061
+ */
25062
+ profile: CamProfileSchema.optional()
24334
25063
  }), ClipPlaybackSchema, {
24335
25064
  kind: "query",
24336
- auth: "admin"
25065
+ auth: "protected"
25066
+ }), optionalMethod(object({
25067
+ deviceId: number(),
25068
+ clipId: string().min(1),
25069
+ /**
25070
+ * WHICH provider holds the bytes — the `addonId` a
25071
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
25072
+ * `listClips` states one method above and for a second one this method
25073
+ * learned the hard way.
25074
+ *
25075
+ * The caller is the stream broker, and the broker lives in
25076
+ * `addon-pipeline` — the same addon that registers the RECORDER's
25077
+ * videoclips provider. With no provider named, the in-process
25078
+ * resolution handed it its own sibling, which implements this method
25079
+ * solely to explain that an analytics visit has no file behind it. So
25080
+ * every onboard clip answered with that sentence, a flash of video and
25081
+ * nothing more, while the SAME call from outside the addon reached the
25082
+ * right provider and returned the bytes (measured on 3628,
25083
+ * 2026-09-21).
25084
+ */
25085
+ provider: string().min(1),
25086
+ /**
25087
+ * Which twin to fetch, on the mapping D549 15 already fixed:
25088
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
25089
+ * accepted here for the same reason it is not accepted by
25090
+ * `getClipPlayback` — a stored file has no broker session, so the
25091
+ * adaptive tier cannot be resolved for it.
25092
+ */
25093
+ profile: CamProfileSchema.optional(),
25094
+ /**
25095
+ * The CALLER's byte bound, so an over-size clip is refused before it is
25096
+ * read and encoded rather than after. Capped by
25097
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
25098
+ * that ceiling.
25099
+ */
25100
+ maxBytes: number().int().positive().optional(),
25101
+ /**
25102
+ * The operator's authorisation to wake a sleeping camera for this
25103
+ * read. Absent — the default — means a sleeping standalone battery
25104
+ * camera is REFUSED by name, before any session is opened.
25105
+ */
25106
+ wake: ClipWakeSchema.optional()
25107
+ }), ClipBytesSchema, {
25108
+ kind: "query",
25109
+ auth: "protected"
25110
+ }), optionalMethod(object({
25111
+ deviceId: number(),
25112
+ clipId: string().min(1),
25113
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
25114
+ provider: string().min(1),
25115
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
25116
+ profile: CamProfileSchema.optional(),
25117
+ /**
25118
+ * Which audio codec the caller's session negotiated, so the provider
25119
+ * can mux it (D600). The ask travels on the DIAL because the mux is
25120
+ * spawned once, before the first byte, and cannot gain a track later.
25121
+ *
25122
+ * ABSENT IS `none`, never a guess: a caller that does not state an
25123
+ * ask is served the video-only stream that shipped first, so a broker
25124
+ * older than this field keeps working unchanged (D382 — an unstated
25125
+ * ask is zero).
25126
+ */
25127
+ audio: ClipStreamAudioSchema.optional()
25128
+ }), ClipStreamDialSchema, {
25129
+ kind: "query",
25130
+ auth: "protected"
24337
25131
  });
24338
25132
  /**
24339
25133
  * Optional client-side hints sent at session creation to help the provider
@@ -24405,6 +25199,104 @@ var WebrtcStreamChoiceSchema = object({
24405
25199
  /** Bitrate in kbps (null for adaptive). */
24406
25200
  bitrateKbps: number().nullable()
24407
25201
  });
25202
+ /**
25203
+ * The last move the adaptive ladder made on this session, and the numbers it
25204
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
25205
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
25206
+ *
25207
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
25208
+ * no window to report, which is NOT a link with zero loss.
25209
+ */
25210
+ var WebrtcLadderDecisionSchema = object({
25211
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
25212
+ * stopped moving; `healthy` — a promotion. */
25213
+ cause: _enum([
25214
+ "loss",
25215
+ "stall",
25216
+ "healthy"
25217
+ ]),
25218
+ /** The rung the ladder moved TO. Null for a move that changed no rung
25219
+ * (an MTU reduction, a forced transcode-down at the bottom). */
25220
+ tier: CamProfileSchema.nullable(),
25221
+ /** Whether that move forced the egress transcode-down rung. */
25222
+ forcedTranscodeDown: boolean(),
25223
+ /** Loss ratio over the tracker's window at the moment of the decision. */
25224
+ lossRatio: number().nullable(),
25225
+ /** The client's reported downlink at that moment, in kbps. */
25226
+ clientKbps: number().nullable(),
25227
+ /** How long the stall detector had been seeing nothing arrive. */
25228
+ stalledForMs: number().nullable(),
25229
+ /** Whether the broker WAS sending while that was measured — the half that
25230
+ * separates "the camera stopped" from "the link stopped". */
25231
+ serverSending: boolean().nullable(),
25232
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25233
+ * must not read like one from this second. */
25234
+ at: number()
25235
+ });
25236
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25237
+ var WebrtcTierFailureSchema = object({
25238
+ tier: CamProfileSchema,
25239
+ failures: number().int().nonnegative()
25240
+ });
25241
+ /**
25242
+ * What the SERVER decided for one WebRTC session — the half the client cannot
25243
+ * measure. See `getSessionDebug`.
25244
+ */
25245
+ var WebrtcSessionDebugSchema = object({
25246
+ sessionId: string(),
25247
+ /** Whether the session still exists here. Everything below is null when
25248
+ * it does not — a dead session has no measurements, not zero ones. */
25249
+ alive: boolean(),
25250
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
25251
+ observedAt: number(),
25252
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
25253
+ brokerId: string().nullable(),
25254
+ camStreamId: string().nullable(),
25255
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
25256
+ * another stream); `native` = the camera's own encode. */
25257
+ sourceKind: _enum([
25258
+ "native",
25259
+ "derived",
25260
+ "unknown"
25261
+ ]),
25262
+ /** Whether the ladder is running at all (an `adaptive` target). */
25263
+ adaptive: boolean(),
25264
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
25265
+ * is bound to no profile slot (a manually-activated stream). */
25266
+ servedTier: CamProfileSchema.nullable(),
25267
+ /** Which node ingests the source. Null when unresolved. */
25268
+ ingestNodeId: string().nullable(),
25269
+ /** Whether that node is a remote one (the media crosses a node boundary
25270
+ * before it reaches this session). Null when unresolved. */
25271
+ ingestIsRemote: boolean().nullable(),
25272
+ /** Codec off the source, and the one negotiated with this client. */
25273
+ sourceCodec: string().nullable(),
25274
+ negotiatedCodec: string().nullable(),
25275
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
25276
+ * source bytes untouched. */
25277
+ egressLeg: _enum([
25278
+ "copy",
25279
+ "transcode",
25280
+ "unknown"
25281
+ ]),
25282
+ /** What the broker itself reports about the rung it is serving. */
25283
+ brokerStatus: string().nullable(),
25284
+ brokerCodec: string().nullable(),
25285
+ brokerBitrateKbps: number().nullable(),
25286
+ brokerInputFps: number().nullable(),
25287
+ brokerUptimeMs: number().nullable(),
25288
+ /** RTP packets this session has sent — the server's own "am I sending?". */
25289
+ rtpPacketsSent: number().nullable(),
25290
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
25291
+ * is suspended there, so a frozen picture in `recorded` mode means
25292
+ * something entirely different. */
25293
+ trackMode: _enum(["live", "recorded"]).nullable(),
25294
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
25295
+ tierFailures: array(WebrtcTierFailureSchema),
25296
+ /** Successful promotions in this viewer session (drives the progressive
25297
+ * healthy timer). Null when no ladder is running. */
25298
+ upgradeCount: number().nullable()
25299
+ });
24408
25300
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
24409
25301
  deviceId: number().int().nonnegative(),
24410
25302
  target: WebrtcStreamTargetSchema,
@@ -24569,7 +25461,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24569
25461
  * the same answer as "it ended": either way nothing is holding it up.
24570
25462
  */
24571
25463
  alive: boolean()
24572
- }));
25464
+ })), method(object({
25465
+ deviceId: number().int().nonnegative(),
25466
+ sessionId: string()
25467
+ }), WebrtcSessionDebugSchema, { kind: "query" });
24573
25468
  object({
24574
25469
  /** All accessory children of the parent. */
24575
25470
  childDeviceIds: array(number()).readonly(),
@@ -26412,6 +27307,346 @@ var dayNightCapability = {
26412
27307
  volatileStateFields: ["lastFetchedAt"]
26413
27308
  };
26414
27309
  /**
27310
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
27311
+ * writes to the CAMERA's own card, on the camera's own schedule.
27312
+ *
27313
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
27314
+ * footage ledger, our storage locations, our retention. This one has a
27315
+ * different authority — the camera's firmware — and per D62 it stores
27316
+ * nothing of its own. Every value here is read from the camera and every
27317
+ * write goes back to the camera; there is no CamStack-side mirror that
27318
+ * could disagree with the device.
27319
+ *
27320
+ * ## One shape, two firmwares
27321
+ *
27322
+ * Measured 2026-09-22 against the live fleet:
27323
+ *
27324
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
27325
+ * | --- | --- | --- |
27326
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
27327
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
27328
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
27329
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
27330
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
27331
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
27332
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
27333
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
27334
+ *
27335
+ * The two schedule models look different and are the same thing in
27336
+ * different coordinates: both answer "for this trigger, during which
27337
+ * weekly windows does the camera record". {@link RecordWindow} is that
27338
+ * question in one shape — Hikvision's ranges map straight onto it,
27339
+ * Reolink's mask expands into hour-aligned windows.
27340
+ *
27341
+ * ## Union, not intersection
27342
+ *
27343
+ * **The same fields exist on every camera.** What differs per device is
27344
+ * which VALUES that device accepts, and that is what {@link
27345
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
27346
+ * per field plus the schedule's own limits. A control a camera cannot
27347
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
27348
+ * dead: disabled must not look like broken.
27349
+ *
27350
+ * ## Refusal by name
27351
+ *
27352
+ * A write a camera cannot honour is refused with a sentence the operator
27353
+ * can read — never accepted and dropped. Both providers refuse through
27354
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
27355
+ * one test, not two hand-written vendor opinions.
27356
+ *
27357
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
27358
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
27359
+ * injected from `status`) reports the live values, and a single
27360
+ * `setSettings` mutation applies a partial change. No hand-written
27361
+ * settings-contribution methods.
27362
+ */
27363
+ /**
27364
+ * What makes the camera start recording during a window.
27365
+ *
27366
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
27367
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
27368
+ * object-class triggers are Reolink-only today and the smart-event ones
27369
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
27370
+ * firmwares measured — a camera that cannot record on a trigger simply
27371
+ * does not list it in `options.schedule.triggers`, and a window naming
27372
+ * it is REFUSED, not dropped.
27373
+ */
27374
+ var RecordTriggerSchema = _enum([
27375
+ "continuous",
27376
+ "motion",
27377
+ "person",
27378
+ "vehicle",
27379
+ "animal",
27380
+ "lineCrossing",
27381
+ "intrusion",
27382
+ "loitering",
27383
+ "alarmInput"
27384
+ ]);
27385
+ /**
27386
+ * One weekly recording window: "on `day`, from `startMinute` to
27387
+ * `endMinute`, record on `trigger`".
27388
+ *
27389
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
27390
+ * both firmwares enumerate). Minutes are local camera time since
27391
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
27392
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
27393
+ * collapsing it to 0 would turn a whole-day window into an empty one.
27394
+ */
27395
+ var RecordWindowSchema = object({
27396
+ trigger: RecordTriggerSchema,
27397
+ day: number().int().min(0).max(6),
27398
+ startMinute: number().int().min(0).max(1439),
27399
+ endMinute: number().int().min(1).max(1440)
27400
+ });
27401
+ /** Status of one physical volume, as the camera itself describes it. */
27402
+ var OnboardStorageVolumeSchema = object({
27403
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
27404
+ id: string(),
27405
+ /** The camera's own name for it, when it gives one (`hddName`). */
27406
+ label: string().optional(),
27407
+ status: _enum([
27408
+ "ok",
27409
+ "unformatted",
27410
+ "error",
27411
+ "offline",
27412
+ "unknown"
27413
+ ]),
27414
+ /**
27415
+ * Total size in MB, or **null when the camera did not say**.
27416
+ *
27417
+ * Never 0 for an unreadable value: a measurement that failed is not a
27418
+ * measurement (D393), and a card whose size is unknown must not be
27419
+ * rendered as a card of size zero.
27420
+ */
27421
+ capacityMb: number().nullable(),
27422
+ /**
27423
+ * Free space in MB, or null when unknown.
27424
+ *
27425
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
27426
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
27427
+ * card converges on once it has wrapped. At loop steady state the
27428
+ * number is identical whether the camera recorded yesterday or stopped
27429
+ * a month ago.
27430
+ */
27431
+ freeMb: number().nullable(),
27432
+ /** True when the camera reports the volume writable (`property` RW). */
27433
+ writable: boolean().optional()
27434
+ });
27435
+ /**
27436
+ * What the camera is doing with its own storage, right now.
27437
+ *
27438
+ * Every scalar is nullable and **null means the camera did not answer**,
27439
+ * never a default. A form that seeds `0` from an unanswered read invites
27440
+ * the operator to save that 0 back onto the camera.
27441
+ */
27442
+ var RecordingOnboardStatusSchema = object({
27443
+ storage: discriminatedUnion("kind", [
27444
+ object({
27445
+ kind: literal("present"),
27446
+ volumes: array(OnboardStorageVolumeSchema)
27447
+ }),
27448
+ object({
27449
+ kind: literal("absent"),
27450
+ reason: string()
27451
+ }),
27452
+ object({
27453
+ kind: literal("unknown"),
27454
+ reason: string()
27455
+ })
27456
+ ]),
27457
+ tracks: array(object({
27458
+ id: string(),
27459
+ enabled: boolean(),
27460
+ isVideo: boolean(),
27461
+ /** From the camera's own track description. Null when it does not say. */
27462
+ codec: string().nullable(),
27463
+ resolution: string().nullable(),
27464
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
27465
+ overwriteWhenFull: boolean().nullable()
27466
+ })),
27467
+ /**
27468
+ * The track the write path targets — the enabled VIDEO one. Null when
27469
+ * no track could be identified, which is itself a refusal reason.
27470
+ */
27471
+ primaryTrackId: string().nullable(),
27472
+ /** Master "record to the card at all" switch. */
27473
+ enabled: boolean().nullable(),
27474
+ overwriteWhenFull: boolean().nullable(),
27475
+ preRecordSec: number().nullable(),
27476
+ postRecordSec: number().nullable(),
27477
+ /** Length of one recorded file, in minutes. */
27478
+ segmentMinutes: number().nullable(),
27479
+ /** The primary track's weekly windows, flattened. */
27480
+ windows: array(RecordWindowSchema),
27481
+ /**
27482
+ * How many windows the camera described that CamStack could NOT read —
27483
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
27484
+ * name.
27485
+ *
27486
+ * A dropped window is work the reader threw away, and a schedule that
27487
+ * silently shows fewer rows than the camera holds is how an operator
27488
+ * saves back a schedule shorter than the one they were looking at
27489
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
27490
+ * that replaces it would delete what was not shown — which is why a
27491
+ * provider reporting a non-zero count also reports the schedule as not
27492
+ * writable.
27493
+ */
27494
+ unreadableWindows: number(),
27495
+ /**
27496
+ * The camera is scheduled to record and has NO usable storage.
27497
+ *
27498
+ * A first-class fact because it is the fleet's most common silent
27499
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
27500
+ * to a card that is not there. Neither the schedule nor the storage
27501
+ * read says anything wrong on its own; only the pair does.
27502
+ */
27503
+ recordingToNowhere: boolean(),
27504
+ lastFetchedAt: number()
27505
+ });
27506
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
27507
+ var RangeSchema = object({
27508
+ min: number(),
27509
+ max: number(),
27510
+ step: number()
27511
+ });
27512
+ /**
27513
+ * The values a camera actually takes for a numeric field, when they are a SET
27514
+ * rather than a range.
27515
+ *
27516
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
27517
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
27518
+ *
27519
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
27520
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
27521
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
27522
+ *
27523
+ * Neither is expressible as a step: the first has a sentinel two billion away
27524
+ * from its neighbours, the second doubles and then jumps. A range that tried
27525
+ * would forbid values the camera takes AND permit values it silently replaces
27526
+ * with 5 — wrong in both directions at once.
27527
+ *
27528
+ * `sentinel` names the member that is not a duration, so a surface can render
27529
+ * "no limit" instead of `2147483647` seconds.
27530
+ */
27531
+ var AllowedValuesSchema = object({
27532
+ values: array(number()).min(1),
27533
+ sentinel: object({
27534
+ value: number(),
27535
+ meaning: _enum(["no-limit", "disabled"])
27536
+ }).optional()
27537
+ });
27538
+ /**
27539
+ * Per-field availability on ONE camera.
27540
+ *
27541
+ * The field exists on every camera — this says whether this one can be
27542
+ * read and whether it can be written, and `reason` says why not when
27543
+ * either is false. The UI renders the control DISABLED with the reason
27544
+ * rather than hiding it, so a limitation is legible instead of looking
27545
+ * like a missing feature.
27546
+ */
27547
+ var OnboardFieldSupportSchema = object({
27548
+ readable: boolean(),
27549
+ writable: boolean(),
27550
+ /** Required whenever `readable` or `writable` is false. */
27551
+ reason: string().optional()
27552
+ });
27553
+ /** What this camera's schedule model can express. */
27554
+ var OnboardScheduleSupportSchema = object({
27555
+ support: OnboardFieldSupportSchema,
27556
+ /**
27557
+ * The smallest time step the camera can express, in minutes.
27558
+ *
27559
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
27560
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
27561
+ * window whose edges are not a multiple of this is REFUSED rather than
27562
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
27563
+ * and nothing says so.
27564
+ */
27565
+ granularityMinutes: number(),
27566
+ /** Triggers this camera can record on. A window naming another is refused. */
27567
+ triggers: array(RecordTriggerSchema),
27568
+ /**
27569
+ * False when the camera stores ONE trigger per time range, so two
27570
+ * windows overlapping on the same day cannot carry different triggers.
27571
+ * True on Reolink, whose mask is per-trigger and independent.
27572
+ */
27573
+ supportsOverlappingTriggers: boolean()
27574
+ });
27575
+ var RecordingOnboardOptionsSchema = object({
27576
+ enabled: OnboardFieldSupportSchema,
27577
+ overwriteWhenFull: OnboardFieldSupportSchema,
27578
+ preRecordSec: OnboardFieldSupportSchema,
27579
+ preRecordSecRange: RangeSchema.optional(),
27580
+ /** Preferred over the range when the camera takes a SET, not a span. */
27581
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
27582
+ postRecordSec: OnboardFieldSupportSchema,
27583
+ postRecordSecRange: RangeSchema.optional(),
27584
+ /** Preferred over the range when the camera takes a SET, not a span. */
27585
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
27586
+ segmentMinutes: OnboardFieldSupportSchema,
27587
+ segmentMinutesRange: RangeSchema.optional(),
27588
+ /** Preferred over the range when the camera takes a SET, not a span. */
27589
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
27590
+ schedule: OnboardScheduleSupportSchema
27591
+ });
27592
+ /**
27593
+ * A partial change. Every field optional.
27594
+ *
27595
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
27596
+ * silently ignore a field it cannot support — it refuses, by name,
27597
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
27598
+ * is the failure D62 exists to prevent: the operator believes the camera
27599
+ * is recording the way the form says, and it is not.
27600
+ */
27601
+ var RecordingOnboardPatchSchema = object({
27602
+ enabled: boolean().optional(),
27603
+ overwriteWhenFull: boolean().optional(),
27604
+ preRecordSec: number().optional(),
27605
+ postRecordSec: number().optional(),
27606
+ segmentMinutes: number().optional(),
27607
+ /** The complete new window set for the primary track — not a delta. */
27608
+ windows: array(RecordWindowSchema).optional()
27609
+ });
27610
+ var recordingOnboardCapability = {
27611
+ name: "recording-onboard",
27612
+ scope: "device",
27613
+ deviceNative: true,
27614
+ mode: "singleton",
27615
+ deviceTypes: [DeviceType.Camera],
27616
+ deviceConfig: { ui: {
27617
+ kind: "derived-form",
27618
+ builderId: "recording-onboard",
27619
+ tab: "recording"
27620
+ } },
27621
+ methods: {
27622
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
27623
+ setSettings: method(object({
27624
+ deviceId: number(),
27625
+ settings: RecordingOnboardPatchSchema
27626
+ }), _void(), {
27627
+ kind: "mutation",
27628
+ auth: "admin"
27629
+ })
27630
+ },
27631
+ status: {
27632
+ schema: RecordingOnboardStatusSchema,
27633
+ kind: "poll"
27634
+ },
27635
+ runtimeState: RecordingOnboardStatusSchema,
27636
+ /**
27637
+ * Runtime-state durability: **restored** — operator-set camera-side
27638
+ * recording config; mutation-driven, and the storage half is the last
27639
+ * thing the camera said about its own card.
27640
+ *
27641
+ * See `RuntimeStateDurability`. Enforced by
27642
+ * `scripts/check-runtime-state-durability.ts`.
27643
+ */
27644
+ durability: "restored",
27645
+ /** Clock fields: written, but excluded from the compare that decides
27646
+ * whether persisting is worth a SQLite commit. */
27647
+ volatileStateFields: ["lastFetchedAt"]
27648
+ };
27649
+ /**
26415
27650
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
26416
27651
  * for every device, regardless of provider — the kernel needs a uniform
26417
27652
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -28921,11 +30156,15 @@ var motionTriggerCapability = {
28921
30156
  *
28922
30157
  * Adding a fifth name to that list would have been the wrong fix twice over:
28923
30158
  * that page is per-camera DETECTION tuning, and a grid's geometry belongs
28924
- * beside PTZ and motion zones on the camera itself — which is exactly where the
28925
- * framework already puts a widget, when the widget is declared on a CAPABILITY.
28926
- * `deviceConfig.ui` is the mechanism (`motion-zones`, `ptz`, `ptz-autotrack`
28927
- * all use it): the framework emits the structural section and the widget
28928
- * self-persists through the cap's own mutations.
30159
+ * beside PTZ and motion zones on the camera itself. The device page is
30160
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
30161
+ * and this cap carries its section the way `recording` does, by RETURNING it
30162
+ * from `getDeviceSettingsContribution`.
30163
+ *
30164
+ * Seven other widgets are still declared the other way, through a
30165
+ * `deviceConfig.ui` block the framework derives a section from. That route
30166
+ * gives the addon no say in where its own panel lands and no way to decline
30167
+ * for a device the panel does not suit, which is why this one does not use it.
28929
30168
  *
28930
30169
  * ## Why one addon may implement it
28931
30170
  *
@@ -28981,16 +30220,37 @@ var GridProfileOfferSchema = object({
28981
30220
  ]),
28982
30221
  offered: boolean(),
28983
30222
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28984
- missingSources: array(number().int().positive())
30223
+ missingSources: array(number().int().positive()),
30224
+ /**
30225
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
30226
+ * offered. DERIVED from the cells and the sources' own size at this profile —
30227
+ * it is reported because nothing else in the system would ever say what the
30228
+ * grid came out as, and because it is the number an operator would otherwise
30229
+ * expect to type.
30230
+ */
30231
+ canvas: string(),
30232
+ /**
30233
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
30234
+ *
30235
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
30236
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
30237
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
30238
+ * Default is `mid` + `low`.
30239
+ */
30240
+ published: boolean()
28985
30241
  });
28986
30242
  var GridLayoutViewSchema = object({
28987
30243
  /** The persisted grid row this camera was declared from. */
28988
30244
  instanceId: string(),
28989
30245
  deviceId: number().int().nonnegative(),
28990
30246
  name: string(),
28991
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
28992
- width: number().int(),
28993
- height: number().int(),
30247
+ /**
30248
+ * NO canvas size. A grid's resolution is not authored: each profile derives
30249
+ * its own from the cells and its sources' dimensions. The two numbers that
30250
+ * used to be here were a text field that silently decided both how much the
30251
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
30252
+ * came out as.
30253
+ */
28994
30254
  fps: number().int(),
28995
30255
  cells: array(GridLayoutCellSchema),
28996
30256
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -28999,9 +30259,13 @@ var GridLayoutViewSchema = object({
28999
30259
  var GridLayoutPatchSchema = object({
29000
30260
  deviceId: number().int().nonnegative(),
29001
30261
  name: string().min(1).max(160).optional(),
29002
- width: number().int().min(160).max(7680).optional(),
29003
- height: number().int().min(120).max(4320).optional(),
29004
30262
  fps: number().int().min(1).max(60).optional(),
30263
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
30264
+ publishedProfiles: array(_enum([
30265
+ "high",
30266
+ "mid",
30267
+ "low"
30268
+ ])).max(3).optional(),
29005
30269
  /**
29006
30270
  * The whole cell list at once. A per-cell patch would need an ordering the
29007
30271
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -31284,6 +32548,25 @@ var ptzAutotrackCapability = {
31284
32548
  scope: "device",
31285
32549
  deviceNative: true,
31286
32550
  mode: "singleton",
32551
+ /**
32552
+ * Two kinds of provider behind one name, the same shape `snapshot` and
32553
+ * `doorbell` use:
32554
+ *
32555
+ * - **Native**: the camera's own firmware autotrack, registered per device by
32556
+ * the driver when the probe says it exists (Reolink: yes; Hikvision:
32557
+ * model-dependent).
32558
+ * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
32559
+ * which follows a subject with `ptz.move` pulses driven by the detection
32560
+ * pipeline. `defaultActive: false` — the operator binds it per camera in
32561
+ * the bindings UI, exactly as `virtual-doorbell` is bound.
32562
+ *
32563
+ * The binding chooses WHICH implementation answers; `setEnabled` stays the
32564
+ * one switch for the function either way. A camera never has both driving its
32565
+ * head, which is the whole reason this is not a second cap with a second
32566
+ * toggle (D62, D520).
32567
+ */
32568
+ kind: "wrapper",
32569
+ defaultActive: false,
31287
32570
  deviceTypes: [DeviceType.Camera],
31288
32571
  deviceConfig: { ui: {
31289
32572
  kind: "widget",
@@ -31766,7 +33049,7 @@ method(object({
31766
33049
  auth: "admin"
31767
33050
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
31768
33051
  kind: "query",
31769
- auth: "admin"
33052
+ auth: "protected"
31770
33053
  }), method(object({
31771
33054
  deviceId: number(),
31772
33055
  profile: string(),
@@ -31996,13 +33279,101 @@ var ExportStateSchema = _enum([
31996
33279
  "expired",
31997
33280
  "deleted"
31998
33281
  ]);
31999
- /** One export job / history row. */
33282
+ /**
33283
+ * WHAT an export is — the authority, as opposed to the four top-level fields
33284
+ * the Library sorts and labels on (D558 § 2.2).
33285
+ *
33286
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
33287
+ * field is optional for the history rows written before it existed, and that
33288
+ * absence has exactly one interpreter.
33289
+ */
33290
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
33291
+ kind: literal("footage"),
33292
+ deviceId: number(),
33293
+ profiles: array(string()).min(1),
33294
+ fromMs: number(),
33295
+ toMs: number()
33296
+ }), object({
33297
+ kind: literal("clip"),
33298
+ deviceId: number(),
33299
+ provider: string().min(1),
33300
+ source: string().min(1),
33301
+ sourceLabel: string().min(1),
33302
+ clipId: string().min(1),
33303
+ /**
33304
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
33305
+ * already listing when the operator picked the segment.
33306
+ *
33307
+ * It is **not** a time range control and it never becomes one: the record's
33308
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
33309
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
33310
+ * `catalog-miss`, not a silently wider search. It exists because
33311
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
33312
+ * the catalog check D558 asks for is literally that call, and a call needs a
33313
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
33314
+ * wall-clock day, which is also the only width measured to be cheap (one
33315
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
33316
+ * of one child's events took 3.6–5.4 s).
33317
+ */
33318
+ catalogWindow: object({
33319
+ sinceMs: number(),
33320
+ untilMs: number()
33321
+ }),
33322
+ profile: _enum([
33323
+ "high",
33324
+ "mid",
33325
+ "low"
33326
+ ]),
33327
+ /**
33328
+ * The operator's authorisation to wake a sleeping camera for this export.
33329
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
33330
+ * because the gate that honours it is the clip provider's sleep gate — a
33331
+ * second enum here would be a second contract. Absent by default; never
33332
+ * settable by a scheduler or a retry.
33333
+ *
33334
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
33335
+ * an operator act that asks again; a queued job that outlived its wake fails
33336
+ * with a reason rather than waking on its turn; and this subject carries
33337
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
33338
+ */
33339
+ wake: ClipWakeSchema.optional()
33340
+ })]);
33341
+ /**
33342
+ * One export job / history row.
33343
+ *
33344
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
33345
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
33346
+ * Library sorts and labels on them (`library-items.ts` orders an export by
33347
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
33348
+ * that did not fill them would sort under the epoch and render a blank range.
33349
+ * Write to the subject and read from the projection and the two will disagree;
33350
+ * the projection is derived at creation and never edited afterwards.
33351
+ *
33352
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
33353
+ * who knows only the recording export will get wrong:
33354
+ *
33355
+ * | field | `kind:'footage'` | `kind:'clip'` |
33356
+ * | --- | --- | --- |
33357
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
33358
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
33359
+ *
33360
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
33361
+ * two authorities, D224's second copy).
33362
+ */
32000
33363
  var ExportRecordSchema = object({
32001
33364
  id: string(),
32002
33365
  deviceId: number(),
32003
33366
  profile: string(),
32004
33367
  fromMs: number(),
32005
33368
  toMs: number(),
33369
+ /**
33370
+ * What this export IS. Optional ONLY for the rows written before D558: the
33371
+ * store parses every row through this schema on every read, so a required
33372
+ * field would make the export AUDIT — which is the whole reason rows survive
33373
+ * file deletion — unreadable in one release. Absence means `footage`, and
33374
+ * {@link exportSubjectOf} is the one place that says so.
33375
+ */
33376
+ subject: ExportSubjectSchema.optional(),
32006
33377
  options: ExportOptionsSchema,
32007
33378
  state: ExportStateSchema,
32008
33379
  /** 0–100 while rendering; null otherwise. */
@@ -32019,6 +33390,18 @@ var ExportRecordSchema = object({
32019
33390
  /** Failure reason when state is 'failed'; null otherwise. */
32020
33391
  error: string().nullable()
32021
33392
  });
33393
+ _enum([
33394
+ "catalog-miss",
33395
+ "catalog-unreachable",
33396
+ "no-file-for-window",
33397
+ "clip-in-progress",
33398
+ "unsupported-option",
33399
+ "sleeping",
33400
+ "camera-refused",
33401
+ "fetch-failed",
33402
+ "too-large-to-transfer",
33403
+ "wake-expired"
33404
+ ]);
32022
33405
  /** Candidate download URLs (LAN first, then operator extra hosts). */
32023
33406
  var ExportDownloadSchema = object({
32024
33407
  url: string(),
@@ -32037,16 +33420,50 @@ var ExportBytesSchema = object({
32037
33420
  name: string(),
32038
33421
  bytes: number().int().nonnegative()
32039
33422
  });
33423
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
33424
+ function resolveExportProfiles(input) {
33425
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
33426
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
33427
+ return [];
33428
+ }
32040
33429
  method(object({
32041
33430
  deviceId: number(),
32042
33431
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
32043
33432
  profile: string().optional(),
32044
33433
  profiles: array(string()).min(1).optional(),
32045
- fromMs: number(),
32046
- toMs: number(),
33434
+ /** Footage only — a clip's boundaries are the camera's. */
33435
+ fromMs: number().optional(),
33436
+ toMs: number().optional(),
33437
+ /** What to export. Absent means the legacy flat footage request. */
33438
+ subject: ExportSubjectSchema.optional(),
32047
33439
  options: ExportOptionsSchema
32048
33440
  }).superRefine((v, ctx) => {
32049
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
33441
+ if (v.subject?.kind === "clip") {
33442
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
33443
+ code: ZodIssueCode.custom,
33444
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
33445
+ path: ["subject", "deviceId"]
33446
+ });
33447
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
33448
+ code: ZodIssueCode.custom,
33449
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
33450
+ path: ["fromMs"]
33451
+ });
33452
+ return;
33453
+ }
33454
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
33455
+ code: ZodIssueCode.custom,
33456
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
33457
+ path: ["subject", "deviceId"]
33458
+ });
33459
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
33460
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
33461
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
33462
+ code: ZodIssueCode.custom,
33463
+ message: "a footage export needs fromMs and toMs",
33464
+ path: ["fromMs"]
33465
+ });
33466
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
32050
33467
  code: ZodIssueCode.custom,
32051
33468
  message: "pass profiles[] (min 1) or legacy profile",
32052
33469
  path: ["profiles"]
@@ -34858,6 +36275,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34858
36275
  pressureSensor: pressureSensorCapability,
34859
36276
  privacyMask: privacyMaskCapability,
34860
36277
  ptzAutotrack: ptzAutotrackCapability,
36278
+ recordingOnboard: recordingOnboardCapability,
34861
36279
  recordingSignal: recordingSignalCapability,
34862
36280
  sceneMonitor: sceneMonitorCapability,
34863
36281
  scriptRunner: scriptRunnerCapability,
@@ -36019,7 +37437,1336 @@ var BaseDeviceProvider = class extends BaseAddon {
36019
37437
  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;
36020
37438
  new Set(Object.values(DeviceType));
36021
37439
  DeviceFeature.BatteryOperated;
37440
+ /**
37441
+ * The vocabulary, in the order a picker offers it.
37442
+ *
37443
+ * Adding a key here is not enough to make it pickable: both glyph tables must
37444
+ * gain it too, and their tests fail until they do. That is the intended
37445
+ * friction — it is the only thing that keeps the two surfaces honest.
37446
+ */
37447
+ var DISPLAY_ICON_VOCABULARY = [
37448
+ {
37449
+ key: "camera",
37450
+ group: "device",
37451
+ synonyms: [
37452
+ "telecamera",
37453
+ "videocamera",
37454
+ "camera",
37455
+ "cam"
37456
+ ]
37457
+ },
37458
+ {
37459
+ key: "network",
37460
+ group: "device",
37461
+ synonyms: [
37462
+ "rete",
37463
+ "network",
37464
+ "lan"
37465
+ ]
37466
+ },
37467
+ {
37468
+ key: "toggle-left",
37469
+ group: "device",
37470
+ synonyms: [
37471
+ "interruttore",
37472
+ "switch",
37473
+ "toggle"
37474
+ ]
37475
+ },
37476
+ {
37477
+ key: "toggle-right",
37478
+ group: "device",
37479
+ synonyms: [
37480
+ "interruttore",
37481
+ "switch",
37482
+ "toggle",
37483
+ "acceso"
37484
+ ]
37485
+ },
37486
+ {
37487
+ key: "circle-dot",
37488
+ group: "device",
37489
+ synonyms: [
37490
+ "pulsante",
37491
+ "bottone",
37492
+ "button",
37493
+ "dot"
37494
+ ]
37495
+ },
37496
+ {
37497
+ key: "refresh-cw",
37498
+ group: "device",
37499
+ synonyms: [
37500
+ "aggiorna",
37501
+ "ricarica",
37502
+ "refresh",
37503
+ "reload",
37504
+ "ciclo"
37505
+ ]
37506
+ },
37507
+ {
37508
+ key: "box",
37509
+ group: "device",
37510
+ synonyms: [
37511
+ "scatola",
37512
+ "box",
37513
+ "contenitore"
37514
+ ]
37515
+ },
37516
+ {
37517
+ key: "scroll-text",
37518
+ group: "device",
37519
+ synonyms: [
37520
+ "registro",
37521
+ "log",
37522
+ "testo"
37523
+ ]
37524
+ },
37525
+ {
37526
+ key: "workflow",
37527
+ group: "device",
37528
+ synonyms: [
37529
+ "automazione",
37530
+ "flusso",
37531
+ "workflow",
37532
+ "scenario"
37533
+ ]
37534
+ },
37535
+ {
37536
+ key: "lock",
37537
+ group: "device",
37538
+ synonyms: [
37539
+ "serratura",
37540
+ "chiave",
37541
+ "lucchetto",
37542
+ "chiuso",
37543
+ "locked"
37544
+ ]
37545
+ },
37546
+ {
37547
+ key: "lock-open",
37548
+ group: "device",
37549
+ synonyms: [
37550
+ "serratura",
37551
+ "aperta",
37552
+ "sbloccato",
37553
+ "unlocked"
37554
+ ]
37555
+ },
37556
+ {
37557
+ key: "blinds",
37558
+ group: "device",
37559
+ synonyms: [
37560
+ "tapparella",
37561
+ "persiana",
37562
+ "serranda",
37563
+ "avvolgibile",
37564
+ "shutter",
37565
+ "blind",
37566
+ "veneziana"
37567
+ ]
37568
+ },
37569
+ {
37570
+ key: "air-vent",
37571
+ group: "device",
37572
+ synonyms: [
37573
+ "bocchetta",
37574
+ "aria",
37575
+ "ventilazione",
37576
+ "condizionatore",
37577
+ "vent"
37578
+ ]
37579
+ },
37580
+ {
37581
+ key: "monitor-play",
37582
+ group: "device",
37583
+ synonyms: [
37584
+ "schermo",
37585
+ "riproduzione",
37586
+ "player",
37587
+ "display"
37588
+ ]
37589
+ },
37590
+ {
37591
+ key: "sliders-horizontal",
37592
+ group: "device",
37593
+ synonyms: [
37594
+ "regolazione",
37595
+ "cursori",
37596
+ "impostazioni",
37597
+ "slider"
37598
+ ]
37599
+ },
37600
+ {
37601
+ key: "map-pin",
37602
+ group: "device",
37603
+ synonyms: [
37604
+ "posizione",
37605
+ "luogo",
37606
+ "mappa",
37607
+ "pin"
37608
+ ]
37609
+ },
37610
+ {
37611
+ key: "bot",
37612
+ group: "device",
37613
+ synonyms: [
37614
+ "robot",
37615
+ "aspirapolvere",
37616
+ "bot"
37617
+ ]
37618
+ },
37619
+ {
37620
+ key: "tractor",
37621
+ group: "device",
37622
+ synonyms: [
37623
+ "trattore",
37624
+ "tosaerba",
37625
+ "giardino"
37626
+ ]
37627
+ },
37628
+ {
37629
+ key: "image",
37630
+ group: "device",
37631
+ synonyms: [
37632
+ "immagine",
37633
+ "foto",
37634
+ "snapshot"
37635
+ ]
37636
+ },
37637
+ {
37638
+ key: "bone",
37639
+ group: "device",
37640
+ synonyms: ["osso", "cane"]
37641
+ },
37642
+ {
37643
+ key: "door-closed",
37644
+ group: "device",
37645
+ synonyms: [
37646
+ "porta",
37647
+ "chiusa",
37648
+ "contatto"
37649
+ ]
37650
+ },
37651
+ {
37652
+ key: "door-open",
37653
+ group: "device",
37654
+ synonyms: [
37655
+ "porta",
37656
+ "aperta",
37657
+ "contatto",
37658
+ "ingresso",
37659
+ "uscita"
37660
+ ]
37661
+ },
37662
+ {
37663
+ key: "crosshair",
37664
+ group: "device",
37665
+ synonyms: [
37666
+ "mirino",
37667
+ "puntamento",
37668
+ "centro"
37669
+ ]
37670
+ },
37671
+ {
37672
+ key: "eye",
37673
+ group: "device",
37674
+ synonyms: [
37675
+ "occhio",
37676
+ "visibile",
37677
+ "visto"
37678
+ ]
37679
+ },
37680
+ {
37681
+ key: "eye-off",
37682
+ group: "device",
37683
+ synonyms: [
37684
+ "privacy",
37685
+ "nascosto",
37686
+ "oscurato",
37687
+ "mascherato",
37688
+ "invisibile"
37689
+ ]
37690
+ },
37691
+ {
37692
+ key: "smartphone",
37693
+ group: "device",
37694
+ synonyms: [
37695
+ "telefono",
37696
+ "cellulare",
37697
+ "smartphone"
37698
+ ]
37699
+ },
37700
+ {
37701
+ key: "phone",
37702
+ group: "device",
37703
+ synonyms: ["telefono", "chiamata"]
37704
+ },
37705
+ {
37706
+ key: "tablet",
37707
+ group: "device",
37708
+ synonyms: ["tablet", "ipad"]
37709
+ },
37710
+ {
37711
+ key: "laptop",
37712
+ group: "device",
37713
+ synonyms: [
37714
+ "portatile",
37715
+ "laptop",
37716
+ "notebook"
37717
+ ]
37718
+ },
37719
+ {
37720
+ key: "monitor",
37721
+ group: "device",
37722
+ synonyms: ["monitor", "schermo"]
37723
+ },
37724
+ {
37725
+ key: "tv",
37726
+ group: "device",
37727
+ synonyms: ["televisore", "tv"]
37728
+ },
37729
+ {
37730
+ key: "speaker",
37731
+ group: "device",
37732
+ synonyms: [
37733
+ "altoparlante",
37734
+ "cassa",
37735
+ "speaker"
37736
+ ]
37737
+ },
37738
+ {
37739
+ key: "printer",
37740
+ group: "device",
37741
+ synonyms: ["stampante"]
37742
+ },
37743
+ {
37744
+ key: "projector",
37745
+ group: "device",
37746
+ synonyms: ["proiettore"]
37747
+ },
37748
+ {
37749
+ key: "gamepad-2",
37750
+ group: "device",
37751
+ synonyms: [
37752
+ "console",
37753
+ "gioco",
37754
+ "joystick"
37755
+ ]
37756
+ },
37757
+ {
37758
+ key: "radio",
37759
+ group: "device",
37760
+ synonyms: ["radio"]
37761
+ },
37762
+ {
37763
+ key: "router",
37764
+ group: "device",
37765
+ synonyms: ["router", "modem"]
37766
+ },
37767
+ {
37768
+ key: "bluetooth",
37769
+ group: "device",
37770
+ synonyms: ["bluetooth"]
37771
+ },
37772
+ {
37773
+ key: "cpu",
37774
+ group: "device",
37775
+ synonyms: [
37776
+ "processore",
37777
+ "cpu",
37778
+ "chip",
37779
+ "dispositivo"
37780
+ ]
37781
+ },
37782
+ {
37783
+ key: "hard-drive",
37784
+ group: "device",
37785
+ synonyms: [
37786
+ "disco",
37787
+ "hdd",
37788
+ "archiviazione",
37789
+ "storage"
37790
+ ]
37791
+ },
37792
+ {
37793
+ key: "server",
37794
+ group: "device",
37795
+ synonyms: ["server", "nas"]
37796
+ },
37797
+ {
37798
+ key: "mic",
37799
+ group: "device",
37800
+ synonyms: ["microfono", "audio"]
37801
+ },
37802
+ {
37803
+ key: "mic-off",
37804
+ group: "device",
37805
+ synonyms: [
37806
+ "microfono",
37807
+ "spento",
37808
+ "muto"
37809
+ ]
37810
+ },
37811
+ {
37812
+ key: "video",
37813
+ group: "device",
37814
+ synonyms: ["video"]
37815
+ },
37816
+ {
37817
+ key: "video-off",
37818
+ group: "device",
37819
+ synonyms: ["video", "spento"]
37820
+ },
37821
+ {
37822
+ key: "music",
37823
+ group: "device",
37824
+ synonyms: ["musica"]
37825
+ },
37826
+ {
37827
+ key: "plug",
37828
+ group: "device",
37829
+ synonyms: [
37830
+ "presa",
37831
+ "spina",
37832
+ "plug"
37833
+ ]
37834
+ },
37835
+ {
37836
+ key: "plug-zap",
37837
+ group: "device",
37838
+ synonyms: ["presa", "alimentata"]
37839
+ },
37840
+ {
37841
+ key: "power",
37842
+ group: "device",
37843
+ synonyms: [
37844
+ "accensione",
37845
+ "alimentazione",
37846
+ "power"
37847
+ ]
37848
+ },
37849
+ {
37850
+ key: "power-off",
37851
+ group: "device",
37852
+ synonyms: ["spegnimento", "spento"]
37853
+ },
37854
+ {
37855
+ key: "cable",
37856
+ group: "device",
37857
+ synonyms: ["cavo"]
37858
+ },
37859
+ {
37860
+ key: "battery",
37861
+ group: "device",
37862
+ synonyms: ["batteria"]
37863
+ },
37864
+ {
37865
+ key: "battery-charging",
37866
+ group: "device",
37867
+ synonyms: [
37868
+ "batteria",
37869
+ "carica",
37870
+ "ricarica"
37871
+ ]
37872
+ },
37873
+ {
37874
+ key: "battery-full",
37875
+ group: "device",
37876
+ synonyms: [
37877
+ "batteria",
37878
+ "carica",
37879
+ "piena"
37880
+ ]
37881
+ },
37882
+ {
37883
+ key: "battery-low",
37884
+ group: "device",
37885
+ synonyms: ["batteria", "scarica"]
37886
+ },
37887
+ {
37888
+ key: "key",
37889
+ group: "device",
37890
+ synonyms: ["chiave"]
37891
+ },
37892
+ {
37893
+ key: "key-round",
37894
+ group: "device",
37895
+ synonyms: ["chiave", "tonda"]
37896
+ },
37897
+ {
37898
+ key: "mailbox",
37899
+ group: "device",
37900
+ synonyms: [
37901
+ "cassetta",
37902
+ "postale",
37903
+ "posta"
37904
+ ]
37905
+ },
37906
+ {
37907
+ key: "package",
37908
+ group: "device",
37909
+ synonyms: ["pacco", "consegna"]
37910
+ },
37911
+ {
37912
+ key: "shield",
37913
+ group: "device",
37914
+ synonyms: [
37915
+ "scudo",
37916
+ "protezione",
37917
+ "sicurezza",
37918
+ "allarme"
37919
+ ]
37920
+ },
37921
+ {
37922
+ key: "shield-check",
37923
+ group: "device",
37924
+ synonyms: [
37925
+ "protetto",
37926
+ "sicuro",
37927
+ "armato"
37928
+ ]
37929
+ },
37930
+ {
37931
+ key: "shield-off",
37932
+ group: "device",
37933
+ synonyms: [
37934
+ "protezione",
37935
+ "disattivata",
37936
+ "disarmato"
37937
+ ]
37938
+ },
37939
+ {
37940
+ key: "briefcase",
37941
+ group: "device",
37942
+ synonyms: [
37943
+ "ufficio",
37944
+ "lavoro",
37945
+ "valigetta",
37946
+ "studio"
37947
+ ]
37948
+ },
37949
+ {
37950
+ key: "lightbulb",
37951
+ group: "lighting",
37952
+ synonyms: [
37953
+ "luce",
37954
+ "lampadina",
37955
+ "lampada",
37956
+ "light"
37957
+ ]
37958
+ },
37959
+ {
37960
+ key: "lamp",
37961
+ group: "lighting",
37962
+ synonyms: ["lampada", "abatjour"]
37963
+ },
37964
+ {
37965
+ key: "lamp-ceiling",
37966
+ group: "lighting",
37967
+ synonyms: [
37968
+ "plafoniera",
37969
+ "lampadario",
37970
+ "luce",
37971
+ "soffitto"
37972
+ ]
37973
+ },
37974
+ {
37975
+ key: "lamp-desk",
37976
+ group: "lighting",
37977
+ synonyms: [
37978
+ "lampada",
37979
+ "scrivania",
37980
+ "abatjour"
37981
+ ]
37982
+ },
37983
+ {
37984
+ key: "lamp-floor",
37985
+ group: "lighting",
37986
+ synonyms: [
37987
+ "piantana",
37988
+ "lampada",
37989
+ "terra"
37990
+ ]
37991
+ },
37992
+ {
37993
+ key: "flashlight",
37994
+ group: "lighting",
37995
+ synonyms: [
37996
+ "faretto",
37997
+ "torcia",
37998
+ "spot",
37999
+ "faro"
38000
+ ]
38001
+ },
38002
+ {
38003
+ key: "heater",
38004
+ group: "lighting",
38005
+ synonyms: [
38006
+ "riscaldamento",
38007
+ "termosifone",
38008
+ "stufa",
38009
+ "calore"
38010
+ ]
38011
+ },
38012
+ {
38013
+ key: "snowflake",
38014
+ group: "lighting",
38015
+ synonyms: [
38016
+ "freddo",
38017
+ "raffrescamento",
38018
+ "aria",
38019
+ "condizionata",
38020
+ "neve"
38021
+ ]
38022
+ },
38023
+ {
38024
+ key: "thermometer-sun",
38025
+ group: "lighting",
38026
+ synonyms: [
38027
+ "caldo",
38028
+ "temperatura",
38029
+ "alta"
38030
+ ]
38031
+ },
38032
+ {
38033
+ key: "thermometer-snowflake",
38034
+ group: "lighting",
38035
+ synonyms: [
38036
+ "freddo",
38037
+ "temperatura",
38038
+ "bassa"
38039
+ ]
38040
+ },
38041
+ {
38042
+ key: "refrigerator",
38043
+ group: "lighting",
38044
+ synonyms: ["frigorifero", "frigo"]
38045
+ },
38046
+ {
38047
+ key: "microwave",
38048
+ group: "lighting",
38049
+ synonyms: ["microonde", "forno"]
38050
+ },
38051
+ {
38052
+ key: "washing-machine",
38053
+ group: "lighting",
38054
+ synonyms: [
38055
+ "lavatrice",
38056
+ "lavastoviglie",
38057
+ "asciugatrice"
38058
+ ]
38059
+ },
38060
+ {
38061
+ key: "cooking-pot",
38062
+ group: "lighting",
38063
+ synonyms: [
38064
+ "pentola",
38065
+ "cucina",
38066
+ "fornello"
38067
+ ]
38068
+ },
38069
+ {
38070
+ key: "utensils",
38071
+ group: "lighting",
38072
+ synonyms: [
38073
+ "cucina",
38074
+ "posate",
38075
+ "sala",
38076
+ "pranzo"
38077
+ ]
38078
+ },
38079
+ {
38080
+ key: "thermometer",
38081
+ group: "sensing",
38082
+ synonyms: [
38083
+ "temperatura",
38084
+ "termometro",
38085
+ "termostato"
38086
+ ]
38087
+ },
38088
+ {
38089
+ key: "droplets",
38090
+ group: "sensing",
38091
+ synonyms: [
38092
+ "umidita",
38093
+ "acqua",
38094
+ "allagamento",
38095
+ "perdita"
38096
+ ]
38097
+ },
38098
+ {
38099
+ key: "droplet",
38100
+ group: "sensing",
38101
+ synonyms: [
38102
+ "goccia",
38103
+ "acqua",
38104
+ "perdita",
38105
+ "allagamento"
38106
+ ]
38107
+ },
38108
+ {
38109
+ key: "gauge",
38110
+ group: "sensing",
38111
+ synonyms: [
38112
+ "misura",
38113
+ "livello",
38114
+ "pressione",
38115
+ "indicatore"
38116
+ ]
38117
+ },
38118
+ {
38119
+ key: "gauge2",
38120
+ group: "sensing",
38121
+ synonyms: [
38122
+ "misura",
38123
+ "livello",
38124
+ "pressione",
38125
+ "indicatore"
38126
+ ],
38127
+ aliasOf: "gauge"
38128
+ },
38129
+ {
38130
+ key: "sun",
38131
+ group: "sensing",
38132
+ synonyms: [
38133
+ "sole",
38134
+ "luminosita",
38135
+ "giorno",
38136
+ "esterno"
38137
+ ]
38138
+ },
38139
+ {
38140
+ key: "sun-medium",
38141
+ group: "sensing",
38142
+ synonyms: ["sole", "luminosita"]
38143
+ },
38144
+ {
38145
+ key: "sunrise",
38146
+ group: "sensing",
38147
+ synonyms: ["alba"]
38148
+ },
38149
+ {
38150
+ key: "moon",
38151
+ group: "sensing",
38152
+ synonyms: [
38153
+ "notte",
38154
+ "luna",
38155
+ "notturna",
38156
+ "nightvision"
38157
+ ]
38158
+ },
38159
+ {
38160
+ key: "wind",
38161
+ group: "sensing",
38162
+ synonyms: [
38163
+ "vento",
38164
+ "aria",
38165
+ "gas",
38166
+ "monossido",
38167
+ "co"
38168
+ ]
38169
+ },
38170
+ {
38171
+ key: "cloud",
38172
+ group: "sensing",
38173
+ synonyms: ["nuvola", "nuvoloso"]
38174
+ },
38175
+ {
38176
+ key: "cloud-rain",
38177
+ group: "sensing",
38178
+ synonyms: ["pioggia"]
38179
+ },
38180
+ {
38181
+ key: "cloud-sun",
38182
+ group: "sensing",
38183
+ synonyms: [
38184
+ "variabile",
38185
+ "sole",
38186
+ "nuvole"
38187
+ ]
38188
+ },
38189
+ {
38190
+ key: "waves",
38191
+ group: "sensing",
38192
+ synonyms: [
38193
+ "onde",
38194
+ "acqua",
38195
+ "vibrazione"
38196
+ ]
38197
+ },
38198
+ {
38199
+ key: "sprout",
38200
+ group: "sensing",
38201
+ synonyms: [
38202
+ "irrigazione",
38203
+ "pianta",
38204
+ "germoglio",
38205
+ "orto"
38206
+ ]
38207
+ },
38208
+ {
38209
+ key: "leaf",
38210
+ group: "sensing",
38211
+ synonyms: [
38212
+ "foglia",
38213
+ "pianta",
38214
+ "verde"
38215
+ ]
38216
+ },
38217
+ {
38218
+ key: "flower",
38219
+ group: "sensing",
38220
+ synonyms: ["fiore", "pianta"]
38221
+ },
38222
+ {
38223
+ key: "zap",
38224
+ group: "sensing",
38225
+ synonyms: [
38226
+ "energia",
38227
+ "corrente",
38228
+ "consumo",
38229
+ "watt",
38230
+ "elettricita"
38231
+ ]
38232
+ },
38233
+ {
38234
+ key: "flame",
38235
+ group: "sensing",
38236
+ synonyms: [
38237
+ "fiamma",
38238
+ "gas",
38239
+ "fuoco",
38240
+ "incendio"
38241
+ ]
38242
+ },
38243
+ {
38244
+ key: "bell",
38245
+ group: "sensing",
38246
+ synonyms: [
38247
+ "campanello",
38248
+ "suoneria",
38249
+ "notifica",
38250
+ "bell",
38251
+ "chime"
38252
+ ]
38253
+ },
38254
+ {
38255
+ key: "bell-ring",
38256
+ group: "sensing",
38257
+ synonyms: [
38258
+ "campanello",
38259
+ "suona",
38260
+ "doorbell",
38261
+ "notifica"
38262
+ ]
38263
+ },
38264
+ {
38265
+ key: "bell-off",
38266
+ group: "sensing",
38267
+ synonyms: [
38268
+ "silenzioso",
38269
+ "notifiche",
38270
+ "disattivate",
38271
+ "muto"
38272
+ ]
38273
+ },
38274
+ {
38275
+ key: "siren",
38276
+ group: "sensing",
38277
+ synonyms: ["sirena", "allarme"]
38278
+ },
38279
+ {
38280
+ key: "radar",
38281
+ group: "sensing",
38282
+ synonyms: [
38283
+ "movimento",
38284
+ "presenza",
38285
+ "pir",
38286
+ "rilevamento",
38287
+ "radar"
38288
+ ]
38289
+ },
38290
+ {
38291
+ key: "activity",
38292
+ group: "sensing",
38293
+ synonyms: [
38294
+ "attivita",
38295
+ "sensore",
38296
+ "generico"
38297
+ ]
38298
+ },
38299
+ {
38300
+ key: "hash",
38301
+ group: "sensing",
38302
+ synonyms: [
38303
+ "numero",
38304
+ "contatore",
38305
+ "valore"
38306
+ ]
38307
+ },
38308
+ {
38309
+ key: "clock",
38310
+ group: "sensing",
38311
+ synonyms: [
38312
+ "orologio",
38313
+ "ora",
38314
+ "tempo"
38315
+ ]
38316
+ },
38317
+ {
38318
+ key: "timer",
38319
+ group: "sensing",
38320
+ synonyms: [
38321
+ "timer",
38322
+ "conto",
38323
+ "rovescia"
38324
+ ]
38325
+ },
38326
+ {
38327
+ key: "calendar",
38328
+ group: "sensing",
38329
+ synonyms: [
38330
+ "calendario",
38331
+ "data",
38332
+ "pianificazione"
38333
+ ]
38334
+ },
38335
+ {
38336
+ key: "pipette",
38337
+ group: "sensing",
38338
+ synonyms: [
38339
+ "qualita",
38340
+ "aria",
38341
+ "misura",
38342
+ "campione"
38343
+ ]
38344
+ },
38345
+ {
38346
+ key: "fan",
38347
+ group: "sensing",
38348
+ synonyms: [
38349
+ "ventilatore",
38350
+ "ventola",
38351
+ "aria"
38352
+ ]
38353
+ },
38354
+ {
38355
+ key: "bath",
38356
+ group: "sensing",
38357
+ synonyms: [
38358
+ "bagno",
38359
+ "vasca",
38360
+ "doccia"
38361
+ ]
38362
+ },
38363
+ {
38364
+ key: "footprints",
38365
+ group: "sensing",
38366
+ synonyms: [
38367
+ "movimento",
38368
+ "passi",
38369
+ "presenza",
38370
+ "motion"
38371
+ ]
38372
+ },
38373
+ {
38374
+ key: "alarm-smoke",
38375
+ group: "sensing",
38376
+ synonyms: [
38377
+ "fumo",
38378
+ "antincendio",
38379
+ "rilevatore"
38380
+ ]
38381
+ },
38382
+ {
38383
+ key: "shield-alert",
38384
+ group: "sensing",
38385
+ synonyms: [
38386
+ "manomissione",
38387
+ "allarme",
38388
+ "tamper",
38389
+ "avviso"
38390
+ ]
38391
+ },
38392
+ {
38393
+ key: "vibrate",
38394
+ group: "sensing",
38395
+ synonyms: [
38396
+ "vibrazione",
38397
+ "urto",
38398
+ "tamper"
38399
+ ]
38400
+ },
38401
+ {
38402
+ key: "wifi",
38403
+ group: "sensing",
38404
+ synonyms: [
38405
+ "wifi",
38406
+ "rete",
38407
+ "segnale"
38408
+ ]
38409
+ },
38410
+ {
38411
+ key: "wifi-off",
38412
+ group: "sensing",
38413
+ synonyms: [
38414
+ "wifi",
38415
+ "assente",
38416
+ "offline",
38417
+ "disconnesso"
38418
+ ]
38419
+ },
38420
+ {
38421
+ key: "volume-2",
38422
+ group: "sensing",
38423
+ synonyms: [
38424
+ "volume",
38425
+ "audio",
38426
+ "altoparlante",
38427
+ "suono"
38428
+ ]
38429
+ },
38430
+ {
38431
+ key: "volume-x",
38432
+ group: "sensing",
38433
+ synonyms: [
38434
+ "muto",
38435
+ "silenziato",
38436
+ "volume",
38437
+ "spento"
38438
+ ]
38439
+ },
38440
+ {
38441
+ key: "heart",
38442
+ group: "sensing",
38443
+ synonyms: [
38444
+ "cuore",
38445
+ "preferito",
38446
+ "salute"
38447
+ ]
38448
+ },
38449
+ {
38450
+ key: "heart-pulse",
38451
+ group: "sensing",
38452
+ synonyms: [
38453
+ "battito",
38454
+ "salute",
38455
+ "monitoraggio"
38456
+ ]
38457
+ },
38458
+ {
38459
+ key: "star",
38460
+ group: "sensing",
38461
+ synonyms: [
38462
+ "preferito",
38463
+ "stella",
38464
+ "importante"
38465
+ ]
38466
+ },
38467
+ {
38468
+ key: "sparkles",
38469
+ group: "sensing",
38470
+ synonyms: [
38471
+ "automatico",
38472
+ "intelligente",
38473
+ "ai",
38474
+ "scena"
38475
+ ]
38476
+ },
38477
+ {
38478
+ key: "house",
38479
+ group: "place",
38480
+ synonyms: [
38481
+ "casa",
38482
+ "abitazione",
38483
+ "home"
38484
+ ]
38485
+ },
38486
+ {
38487
+ key: "sofa",
38488
+ group: "place",
38489
+ synonyms: [
38490
+ "salone",
38491
+ "soggiorno",
38492
+ "divano",
38493
+ "living"
38494
+ ]
38495
+ },
38496
+ {
38497
+ key: "bed-double",
38498
+ group: "place",
38499
+ synonyms: [
38500
+ "camera",
38501
+ "letto",
38502
+ "matrimoniale"
38503
+ ]
38504
+ },
38505
+ {
38506
+ key: "bed",
38507
+ group: "place",
38508
+ synonyms: [
38509
+ "cameretta",
38510
+ "letto",
38511
+ "singolo"
38512
+ ]
38513
+ },
38514
+ {
38515
+ key: "warehouse",
38516
+ group: "place",
38517
+ synonyms: [
38518
+ "magazzino",
38519
+ "capanno",
38520
+ "deposito",
38521
+ "cantina"
38522
+ ]
38523
+ },
38524
+ {
38525
+ key: "trees",
38526
+ group: "place",
38527
+ synonyms: [
38528
+ "giardino",
38529
+ "alberi",
38530
+ "esterno",
38531
+ "parco"
38532
+ ]
38533
+ },
38534
+ {
38535
+ key: "tree-pine",
38536
+ group: "place",
38537
+ synonyms: [
38538
+ "albero",
38539
+ "pino",
38540
+ "giardino"
38541
+ ]
38542
+ },
38543
+ {
38544
+ key: "fence",
38545
+ group: "place",
38546
+ synonyms: [
38547
+ "recinto",
38548
+ "cancello",
38549
+ "staccionata",
38550
+ "perimetro"
38551
+ ]
38552
+ },
38553
+ {
38554
+ key: "store",
38555
+ group: "place",
38556
+ synonyms: [
38557
+ "negozio",
38558
+ "ingresso",
38559
+ "vetrina"
38560
+ ]
38561
+ },
38562
+ {
38563
+ key: "building",
38564
+ group: "place",
38565
+ synonyms: ["edificio", "palazzo"]
38566
+ },
38567
+ {
38568
+ key: "building-2",
38569
+ group: "place",
38570
+ synonyms: [
38571
+ "edificio",
38572
+ "uffici",
38573
+ "condominio"
38574
+ ]
38575
+ },
38576
+ {
38577
+ key: "mountain",
38578
+ group: "place",
38579
+ synonyms: [
38580
+ "montagna",
38581
+ "esterno",
38582
+ "panorama"
38583
+ ]
38584
+ },
38585
+ {
38586
+ key: "umbrella",
38587
+ group: "place",
38588
+ synonyms: [
38589
+ "tenda",
38590
+ "markise",
38591
+ "ombrellone",
38592
+ "pergola",
38593
+ "veranda"
38594
+ ]
38595
+ },
38596
+ {
38597
+ key: "dumbbell",
38598
+ group: "place",
38599
+ synonyms: ["palestra", "attrezzi"]
38600
+ },
38601
+ {
38602
+ key: "wrench",
38603
+ group: "place",
38604
+ synonyms: [
38605
+ "officina",
38606
+ "manutenzione",
38607
+ "attrezzi",
38608
+ "utenze"
38609
+ ]
38610
+ },
38611
+ {
38612
+ key: "hammer",
38613
+ group: "place",
38614
+ synonyms: [
38615
+ "officina",
38616
+ "martello",
38617
+ "lavori"
38618
+ ]
38619
+ },
38620
+ {
38621
+ key: "drill",
38622
+ group: "place",
38623
+ synonyms: [
38624
+ "officina",
38625
+ "trapano",
38626
+ "lavori"
38627
+ ]
38628
+ },
38629
+ {
38630
+ key: "waves-ladder",
38631
+ group: "place",
38632
+ synonyms: ["piscina", "vasca"]
38633
+ },
38634
+ {
38635
+ key: "circle-parking",
38636
+ group: "place",
38637
+ synonyms: [
38638
+ "parcheggio",
38639
+ "posto",
38640
+ "auto",
38641
+ "garage"
38642
+ ]
38643
+ },
38644
+ {
38645
+ key: "route",
38646
+ group: "place",
38647
+ synonyms: [
38648
+ "percorso",
38649
+ "strada",
38650
+ "vialetto",
38651
+ "tragitto"
38652
+ ]
38653
+ },
38654
+ {
38655
+ key: "car",
38656
+ group: "life",
38657
+ synonyms: [
38658
+ "auto",
38659
+ "macchina",
38660
+ "veicolo",
38661
+ "parcheggio"
38662
+ ]
38663
+ },
38664
+ {
38665
+ key: "bike",
38666
+ group: "life",
38667
+ synonyms: [
38668
+ "bicicletta",
38669
+ "bici",
38670
+ "moto"
38671
+ ]
38672
+ },
38673
+ {
38674
+ key: "bus",
38675
+ group: "life",
38676
+ synonyms: ["autobus", "pullman"]
38677
+ },
38678
+ {
38679
+ key: "truck",
38680
+ group: "life",
38681
+ synonyms: [
38682
+ "camion",
38683
+ "furgone",
38684
+ "consegna"
38685
+ ]
38686
+ },
38687
+ {
38688
+ key: "plane",
38689
+ group: "life",
38690
+ synonyms: ["aereo"]
38691
+ },
38692
+ {
38693
+ key: "train-front",
38694
+ group: "life",
38695
+ synonyms: ["treno"]
38696
+ },
38697
+ {
38698
+ key: "sailboat",
38699
+ group: "life",
38700
+ synonyms: ["barca", "imbarcazione"]
38701
+ },
38702
+ {
38703
+ key: "dog",
38704
+ group: "life",
38705
+ synonyms: ["cane", "animale"]
38706
+ },
38707
+ {
38708
+ key: "cat",
38709
+ group: "life",
38710
+ synonyms: ["gatto", "animale"]
38711
+ },
38712
+ {
38713
+ key: "bird",
38714
+ group: "life",
38715
+ synonyms: ["uccello", "animale"]
38716
+ },
38717
+ {
38718
+ key: "paw-print",
38719
+ group: "life",
38720
+ synonyms: [
38721
+ "animale",
38722
+ "zampa",
38723
+ "pet"
38724
+ ]
38725
+ },
38726
+ {
38727
+ key: "baby",
38728
+ group: "life",
38729
+ synonyms: [
38730
+ "neonato",
38731
+ "bimbo",
38732
+ "cameretta",
38733
+ "babymonitor"
38734
+ ]
38735
+ },
38736
+ {
38737
+ key: "user",
38738
+ group: "life",
38739
+ synonyms: ["persona", "utente"]
38740
+ },
38741
+ {
38742
+ key: "users",
38743
+ group: "life",
38744
+ synonyms: [
38745
+ "persone",
38746
+ "gruppo",
38747
+ "famiglia"
38748
+ ]
38749
+ },
38750
+ {
38751
+ key: "person-standing",
38752
+ group: "life",
38753
+ synonyms: [
38754
+ "persona",
38755
+ "piedi",
38756
+ "presenza"
38757
+ ]
38758
+ }
38759
+ ];
38760
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
38761
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
38762
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
36022
38763
  Object.freeze({
38764
+ "accessories.getStatus": {
38765
+ capName: "accessories",
38766
+ capScope: "device",
38767
+ addonId: null,
38768
+ access: "view"
38769
+ },
36023
38770
  "accessories.setChildHidden": {
36024
38771
  capName: "accessories",
36025
38772
  capScope: "device",
@@ -36302,6 +39049,12 @@ Object.freeze({
36302
39049
  addonId: null,
36303
39050
  access: "view"
36304
39051
  },
39052
+ "airQualitySensor.getStatus": {
39053
+ capName: "air-quality-sensor",
39054
+ capScope: "device",
39055
+ addonId: null,
39056
+ access: "view"
39057
+ },
36305
39058
  "alarmPanel.arm": {
36306
39059
  capName: "alarm-panel",
36307
39060
  capScope: "device",
@@ -36314,6 +39067,12 @@ Object.freeze({
36314
39067
  addonId: null,
36315
39068
  access: "create"
36316
39069
  },
39070
+ "alarmPanel.getStatus": {
39071
+ capName: "alarm-panel",
39072
+ capScope: "device",
39073
+ addonId: null,
39074
+ access: "view"
39075
+ },
36317
39076
  "alarmPanel.trigger": {
36318
39077
  capName: "alarm-panel",
36319
39078
  capScope: "device",
@@ -36362,6 +39121,30 @@ Object.freeze({
36362
39121
  addonId: null,
36363
39122
  access: "create"
36364
39123
  },
39124
+ "ambientLightSensor.getStatus": {
39125
+ capName: "ambient-light-sensor",
39126
+ capScope: "device",
39127
+ addonId: null,
39128
+ access: "view"
39129
+ },
39130
+ "audioAnalysis.applyDeviceSettingsPatch": {
39131
+ capName: "audio-analysis",
39132
+ capScope: "device",
39133
+ addonId: null,
39134
+ access: "create"
39135
+ },
39136
+ "audioAnalysis.getDeviceLiveContribution": {
39137
+ capName: "audio-analysis",
39138
+ capScope: "device",
39139
+ addonId: null,
39140
+ access: "view"
39141
+ },
39142
+ "audioAnalysis.getDeviceSettingsContribution": {
39143
+ capName: "audio-analysis",
39144
+ capScope: "device",
39145
+ addonId: null,
39146
+ access: "view"
39147
+ },
36365
39148
  "audioAnalysis.resolveDeviceSettings": {
36366
39149
  capName: "audio-analysis",
36367
39150
  capScope: "device",
@@ -36524,6 +39307,12 @@ Object.freeze({
36524
39307
  addonId: null,
36525
39308
  access: "create"
36526
39309
  },
39310
+ "automationControl.getStatus": {
39311
+ capName: "automation-control",
39312
+ capScope: "device",
39313
+ addonId: null,
39314
+ access: "view"
39315
+ },
36527
39316
  "automationControl.trigger": {
36528
39317
  capName: "automation-control",
36529
39318
  capScope: "device",
@@ -36620,12 +39409,30 @@ Object.freeze({
36620
39409
  addonId: null,
36621
39410
  access: "create"
36622
39411
  },
39412
+ "battery.getStatus": {
39413
+ capName: "battery",
39414
+ capScope: "device",
39415
+ addonId: null,
39416
+ access: "view"
39417
+ },
36623
39418
  "battery.wakeForStream": {
36624
39419
  capName: "battery",
36625
39420
  capScope: "device",
36626
39421
  addonId: null,
36627
39422
  access: "create"
36628
39423
  },
39424
+ "binary.getStatus": {
39425
+ capName: "binary",
39426
+ capScope: "device",
39427
+ addonId: null,
39428
+ access: "view"
39429
+ },
39430
+ "brightness.getStatus": {
39431
+ capName: "brightness",
39432
+ capScope: "device",
39433
+ addonId: null,
39434
+ access: "view"
39435
+ },
36629
39436
  "brightness.setBrightness": {
36630
39437
  capName: "brightness",
36631
39438
  capScope: "device",
@@ -36740,6 +39547,30 @@ Object.freeze({
36740
39547
  addonId: null,
36741
39548
  access: "view"
36742
39549
  },
39550
+ "cameraCredentials.getStatus": {
39551
+ capName: "camera-credentials",
39552
+ capScope: "device",
39553
+ addonId: null,
39554
+ access: "view"
39555
+ },
39556
+ "cameraGridLayout.applyDeviceSettingsPatch": {
39557
+ capName: "camera-grid-layout",
39558
+ capScope: "device",
39559
+ addonId: null,
39560
+ access: "create"
39561
+ },
39562
+ "cameraGridLayout.getDeviceLiveContribution": {
39563
+ capName: "camera-grid-layout",
39564
+ capScope: "device",
39565
+ addonId: null,
39566
+ access: "view"
39567
+ },
39568
+ "cameraGridLayout.getDeviceSettingsContribution": {
39569
+ capName: "camera-grid-layout",
39570
+ capScope: "device",
39571
+ addonId: null,
39572
+ access: "view"
39573
+ },
36743
39574
  "cameraGridLayout.getLayout": {
36744
39575
  capName: "camera-grid-layout",
36745
39576
  capScope: "device",
@@ -36752,6 +39583,24 @@ Object.freeze({
36752
39583
  addonId: null,
36753
39584
  access: "create"
36754
39585
  },
39586
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
39587
+ capName: "camera-pipeline-config",
39588
+ capScope: "device",
39589
+ addonId: null,
39590
+ access: "create"
39591
+ },
39592
+ "cameraPipelineConfig.getDeviceLiveContribution": {
39593
+ capName: "camera-pipeline-config",
39594
+ capScope: "device",
39595
+ addonId: null,
39596
+ access: "view"
39597
+ },
39598
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
39599
+ capName: "camera-pipeline-config",
39600
+ capScope: "device",
39601
+ addonId: null,
39602
+ access: "view"
39603
+ },
36755
39604
  "cameraStreams.getBrokerStreams": {
36756
39605
  capName: "camera-streams",
36757
39606
  capScope: "device",
@@ -36782,6 +39631,18 @@ Object.freeze({
36782
39631
  addonId: null,
36783
39632
  access: "view"
36784
39633
  },
39634
+ "carbonMonoxide.getStatus": {
39635
+ capName: "carbon-monoxide",
39636
+ capScope: "device",
39637
+ addonId: null,
39638
+ access: "view"
39639
+ },
39640
+ "climateControl.getStatus": {
39641
+ capName: "climate-control",
39642
+ capScope: "device",
39643
+ addonId: null,
39644
+ access: "view"
39645
+ },
36785
39646
  "climateControl.setFanMode": {
36786
39647
  capName: "climate-control",
36787
39648
  capScope: "device",
@@ -36830,6 +39691,12 @@ Object.freeze({
36830
39691
  addonId: null,
36831
39692
  access: "create"
36832
39693
  },
39694
+ "color.getStatus": {
39695
+ capName: "color",
39696
+ capScope: "device",
39697
+ addonId: null,
39698
+ access: "view"
39699
+ },
36833
39700
  "color.setColor": {
36834
39701
  capName: "color",
36835
39702
  capScope: "device",
@@ -36848,12 +39715,36 @@ Object.freeze({
36848
39715
  addonId: null,
36849
39716
  access: "create"
36850
39717
  },
39718
+ "connectivity.getStatus": {
39719
+ capName: "connectivity",
39720
+ capScope: "device",
39721
+ addonId: null,
39722
+ access: "view"
39723
+ },
39724
+ "consumables.getStatus": {
39725
+ capName: "consumables",
39726
+ capScope: "device",
39727
+ addonId: null,
39728
+ access: "view"
39729
+ },
36851
39730
  "consumables.reset": {
36852
39731
  capName: "consumables",
36853
39732
  capScope: "device",
36854
39733
  addonId: null,
36855
39734
  access: "delete"
36856
39735
  },
39736
+ "contact.getStatus": {
39737
+ capName: "contact",
39738
+ capScope: "device",
39739
+ addonId: null,
39740
+ access: "view"
39741
+ },
39742
+ "control.getStatus": {
39743
+ capName: "control",
39744
+ capScope: "device",
39745
+ addonId: null,
39746
+ access: "view"
39747
+ },
36857
39748
  "control.setValue": {
36858
39749
  capName: "control",
36859
39750
  capScope: "device",
@@ -36920,6 +39811,12 @@ Object.freeze({
36920
39811
  addonId: null,
36921
39812
  access: "create"
36922
39813
  },
39814
+ "cover.getStatus": {
39815
+ capName: "cover",
39816
+ capScope: "device",
39817
+ addonId: null,
39818
+ access: "view"
39819
+ },
36923
39820
  "cover.open": {
36924
39821
  capName: "cover",
36925
39822
  capScope: "device",
@@ -37052,6 +39949,12 @@ Object.freeze({
37052
39949
  addonId: null,
37053
39950
  access: "view"
37054
39951
  },
39952
+ "dayNight.getStatus": {
39953
+ capName: "day-night",
39954
+ capScope: "device",
39955
+ addonId: null,
39956
+ access: "view"
39957
+ },
37055
39958
  "dayNight.setSettings": {
37056
39959
  capName: "day-night",
37057
39960
  capScope: "device",
@@ -37142,6 +40045,30 @@ Object.freeze({
37142
40045
  addonId: null,
37143
40046
  access: "view"
37144
40047
  },
40048
+ "detectionPipeline.applyDeviceSettingsPatch": {
40049
+ capName: "detection-pipeline",
40050
+ capScope: "device",
40051
+ addonId: null,
40052
+ access: "create"
40053
+ },
40054
+ "detectionPipeline.getDeviceLiveContribution": {
40055
+ capName: "detection-pipeline",
40056
+ capScope: "device",
40057
+ addonId: null,
40058
+ access: "view"
40059
+ },
40060
+ "detectionPipeline.getDeviceSettingsContribution": {
40061
+ capName: "detection-pipeline",
40062
+ capScope: "device",
40063
+ addonId: null,
40064
+ access: "view"
40065
+ },
40066
+ "deviceAdminLink.getAdminLink": {
40067
+ capName: "device-admin-link",
40068
+ capScope: "device",
40069
+ addonId: null,
40070
+ access: "view"
40071
+ },
37145
40072
  "deviceAdoption.adopt": {
37146
40073
  capName: "device-adoption",
37147
40074
  capScope: "system",
@@ -37154,6 +40081,12 @@ Object.freeze({
37154
40081
  addonId: null,
37155
40082
  access: "view"
37156
40083
  },
40084
+ "deviceAdoption.getStatus": {
40085
+ capName: "device-adoption",
40086
+ capScope: "system",
40087
+ addonId: null,
40088
+ access: "view"
40089
+ },
37157
40090
  "deviceAdoption.listCandidateFilters": {
37158
40091
  capName: "device-adoption",
37159
40092
  capScope: "system",
@@ -37190,6 +40123,12 @@ Object.freeze({
37190
40123
  addonId: null,
37191
40124
  access: "create"
37192
40125
  },
40126
+ "deviceDiscovery.getStatus": {
40127
+ capName: "device-discovery",
40128
+ capScope: "device",
40129
+ addonId: null,
40130
+ access: "view"
40131
+ },
37193
40132
  "deviceDiscovery.listDiscovered": {
37194
40133
  capName: "device-discovery",
37195
40134
  capScope: "device",
@@ -37208,12 +40147,30 @@ Object.freeze({
37208
40147
  addonId: null,
37209
40148
  access: "create"
37210
40149
  },
40150
+ "deviceExport.applyDeviceSettingsPatch": {
40151
+ capName: "device-export",
40152
+ capScope: "system",
40153
+ addonId: null,
40154
+ access: "create"
40155
+ },
37211
40156
  "deviceExport.exposeDevice": {
37212
40157
  capName: "device-export",
37213
40158
  capScope: "system",
37214
40159
  addonId: null,
37215
40160
  access: "create"
37216
40161
  },
40162
+ "deviceExport.getDeviceLiveContribution": {
40163
+ capName: "device-export",
40164
+ capScope: "system",
40165
+ addonId: null,
40166
+ access: "view"
40167
+ },
40168
+ "deviceExport.getDeviceSettingsContribution": {
40169
+ capName: "device-export",
40170
+ capScope: "system",
40171
+ addonId: null,
40172
+ access: "view"
40173
+ },
37217
40174
  "deviceExport.getStatus": {
37218
40175
  capName: "device-export",
37219
40176
  capScope: "system",
@@ -37310,6 +40267,12 @@ Object.freeze({
37310
40267
  addonId: null,
37311
40268
  access: "create"
37312
40269
  },
40270
+ "deviceManager.applyDeviceSettingsPatch": {
40271
+ capName: "device-manager",
40272
+ capScope: "system",
40273
+ addonId: null,
40274
+ access: "create"
40275
+ },
37313
40276
  "deviceManager.applyInitialMeta": {
37314
40277
  capName: "device-manager",
37315
40278
  capScope: "system",
@@ -37412,6 +40375,12 @@ Object.freeze({
37412
40375
  addonId: null,
37413
40376
  access: "view"
37414
40377
  },
40378
+ "deviceManager.getDeviceLiveContribution": {
40379
+ capName: "device-manager",
40380
+ capScope: "system",
40381
+ addonId: null,
40382
+ access: "view"
40383
+ },
37415
40384
  "deviceManager.getDeviceLiveInfoAggregate": {
37416
40385
  capName: "device-manager",
37417
40386
  capScope: "system",
@@ -37424,6 +40393,12 @@ Object.freeze({
37424
40393
  addonId: null,
37425
40394
  access: "view"
37426
40395
  },
40396
+ "deviceManager.getDeviceSettingsContribution": {
40397
+ capName: "device-manager",
40398
+ capScope: "system",
40399
+ addonId: null,
40400
+ access: "view"
40401
+ },
37427
40402
  "deviceManager.getDeviceStatusAggregate": {
37428
40403
  capName: "device-manager",
37429
40404
  capScope: "system",
@@ -37916,6 +40891,36 @@ Object.freeze({
37916
40891
  addonId: null,
37917
40892
  access: "create"
37918
40893
  },
40894
+ "deviceStatus.getStatus": {
40895
+ capName: "device-status",
40896
+ capScope: "device",
40897
+ addonId: null,
40898
+ access: "view"
40899
+ },
40900
+ "doorbell.applyDeviceSettingsPatch": {
40901
+ capName: "doorbell",
40902
+ capScope: "device",
40903
+ addonId: null,
40904
+ access: "create"
40905
+ },
40906
+ "doorbell.getDeviceLiveContribution": {
40907
+ capName: "doorbell",
40908
+ capScope: "device",
40909
+ addonId: null,
40910
+ access: "view"
40911
+ },
40912
+ "doorbell.getDeviceSettingsContribution": {
40913
+ capName: "doorbell",
40914
+ capScope: "device",
40915
+ addonId: null,
40916
+ access: "view"
40917
+ },
40918
+ "doorbell.getStatus": {
40919
+ capName: "doorbell",
40920
+ capScope: "device",
40921
+ addonId: null,
40922
+ access: "view"
40923
+ },
37919
40924
  "embeddingEncoder.encode": {
37920
40925
  capName: "embedding-encoder",
37921
40926
  capScope: "system",
@@ -37934,6 +40939,18 @@ Object.freeze({
37934
40939
  addonId: null,
37935
40940
  access: "view"
37936
40941
  },
40942
+ "enumSensor.getStatus": {
40943
+ capName: "enum-sensor",
40944
+ capScope: "device",
40945
+ addonId: null,
40946
+ access: "view"
40947
+ },
40948
+ "eventEmitter.getStatus": {
40949
+ capName: "event-emitter",
40950
+ capScope: "device",
40951
+ addonId: null,
40952
+ access: "view"
40953
+ },
37937
40954
  "events.getEventClipUrl": {
37938
40955
  capName: "events",
37939
40956
  capScope: "device",
@@ -38054,6 +41071,12 @@ Object.freeze({
38054
41071
  addonId: null,
38055
41072
  access: "view"
38056
41073
  },
41074
+ "fanControl.getStatus": {
41075
+ capName: "fan-control",
41076
+ capScope: "device",
41077
+ addonId: null,
41078
+ access: "view"
41079
+ },
38057
41080
  "fanControl.setDirection": {
38058
41081
  capName: "fan-control",
38059
41082
  capScope: "device",
@@ -38078,6 +41101,12 @@ Object.freeze({
38078
41101
  addonId: null,
38079
41102
  access: "create"
38080
41103
  },
41104
+ "featureProbe.getStatus": {
41105
+ capName: "feature-probe",
41106
+ capScope: "device",
41107
+ addonId: null,
41108
+ access: "view"
41109
+ },
38081
41110
  "filesystemBrowse.browse": {
38082
41111
  capName: "filesystem-browse",
38083
41112
  capScope: "system",
@@ -38096,6 +41125,24 @@ Object.freeze({
38096
41125
  addonId: null,
38097
41126
  access: "view"
38098
41127
  },
41128
+ "flood.getStatus": {
41129
+ capName: "flood",
41130
+ capScope: "device",
41131
+ addonId: null,
41132
+ access: "view"
41133
+ },
41134
+ "gas.getStatus": {
41135
+ capName: "gas",
41136
+ capScope: "device",
41137
+ addonId: null,
41138
+ access: "view"
41139
+ },
41140
+ "humidifier.getStatus": {
41141
+ capName: "humidifier",
41142
+ capScope: "device",
41143
+ addonId: null,
41144
+ access: "view"
41145
+ },
38099
41146
  "humidifier.setMode": {
38100
41147
  capName: "humidifier",
38101
41148
  capScope: "device",
@@ -38114,12 +41161,30 @@ Object.freeze({
38114
41161
  addonId: null,
38115
41162
  access: "create"
38116
41163
  },
41164
+ "humiditySensor.getStatus": {
41165
+ capName: "humidity-sensor",
41166
+ capScope: "device",
41167
+ addonId: null,
41168
+ access: "view"
41169
+ },
41170
+ "image.getStatus": {
41171
+ capName: "image",
41172
+ capScope: "device",
41173
+ addonId: null,
41174
+ access: "view"
41175
+ },
38117
41176
  "imageSettings.getOptions": {
38118
41177
  capName: "image-settings",
38119
41178
  capScope: "device",
38120
41179
  addonId: null,
38121
41180
  access: "view"
38122
41181
  },
41182
+ "imageSettings.getStatus": {
41183
+ capName: "image-settings",
41184
+ capScope: "device",
41185
+ addonId: null,
41186
+ access: "view"
41187
+ },
38123
41188
  "imageSettings.setSettings": {
38124
41189
  capName: "image-settings",
38125
41190
  capScope: "device",
@@ -38192,6 +41257,12 @@ Object.freeze({
38192
41257
  addonId: null,
38193
41258
  access: "create"
38194
41259
  },
41260
+ "intercom.getStatus": {
41261
+ capName: "intercom",
41262
+ capScope: "device",
41263
+ addonId: null,
41264
+ access: "view"
41265
+ },
38195
41266
  "intercom.handleAnswer": {
38196
41267
  capName: "intercom",
38197
41268
  capScope: "device",
@@ -38228,6 +41299,12 @@ Object.freeze({
38228
41299
  addonId: null,
38229
41300
  access: "create"
38230
41301
  },
41302
+ "lawnMowerControl.getStatus": {
41303
+ capName: "lawn-mower-control",
41304
+ capScope: "device",
41305
+ addonId: null,
41306
+ access: "view"
41307
+ },
38231
41308
  "lawnMowerControl.pause": {
38232
41309
  capName: "lawn-mower-control",
38233
41310
  capScope: "device",
@@ -38510,6 +41587,12 @@ Object.freeze({
38510
41587
  addonId: null,
38511
41588
  access: "create"
38512
41589
  },
41590
+ "lockControl.getStatus": {
41591
+ capName: "lock-control",
41592
+ capScope: "device",
41593
+ addonId: null,
41594
+ access: "view"
41595
+ },
38513
41596
  "lockControl.lock": {
38514
41597
  capName: "lock-control",
38515
41598
  capScope: "device",
@@ -38558,6 +41641,12 @@ Object.freeze({
38558
41641
  addonId: null,
38559
41642
  access: "view"
38560
41643
  },
41644
+ "mediaPlayer.getStatus": {
41645
+ capName: "media-player",
41646
+ capScope: "device",
41647
+ addonId: null,
41648
+ access: "view"
41649
+ },
38561
41650
  "mediaPlayer.next": {
38562
41651
  capName: "media-player",
38563
41652
  capScope: "device",
@@ -38750,6 +41839,12 @@ Object.freeze({
38750
41839
  addonId: null,
38751
41840
  access: "create"
38752
41841
  },
41842
+ "motion.getStatus": {
41843
+ capName: "motion",
41844
+ capScope: "device",
41845
+ addonId: null,
41846
+ access: "view"
41847
+ },
38753
41848
  "motion.isDetected": {
38754
41849
  capName: "motion",
38755
41850
  capScope: "device",
@@ -38762,6 +41857,24 @@ Object.freeze({
38762
41857
  addonId: null,
38763
41858
  access: "create"
38764
41859
  },
41860
+ "motionDetection.applyDeviceSettingsPatch": {
41861
+ capName: "motion-detection",
41862
+ capScope: "device",
41863
+ addonId: null,
41864
+ access: "create"
41865
+ },
41866
+ "motionDetection.getDeviceLiveContribution": {
41867
+ capName: "motion-detection",
41868
+ capScope: "device",
41869
+ addonId: null,
41870
+ access: "view"
41871
+ },
41872
+ "motionDetection.getDeviceSettingsContribution": {
41873
+ capName: "motion-detection",
41874
+ capScope: "device",
41875
+ addonId: null,
41876
+ access: "view"
41877
+ },
38765
41878
  "motionDetection.removeCamera": {
38766
41879
  capName: "motion-detection",
38767
41880
  capScope: "device",
@@ -38774,6 +41887,12 @@ Object.freeze({
38774
41887
  addonId: null,
38775
41888
  access: "delete"
38776
41889
  },
41890
+ "motionTrigger.getStatus": {
41891
+ capName: "motion-trigger",
41892
+ capScope: "device",
41893
+ addonId: null,
41894
+ access: "view"
41895
+ },
38777
41896
  "motionTrigger.setMotionTrigger": {
38778
41897
  capName: "motion-trigger",
38779
41898
  capScope: "device",
@@ -38786,6 +41905,12 @@ Object.freeze({
38786
41905
  addonId: null,
38787
41906
  access: "view"
38788
41907
  },
41908
+ "motionZones.getStatus": {
41909
+ capName: "motion-zones",
41910
+ capScope: "device",
41911
+ addonId: null,
41912
+ access: "view"
41913
+ },
38789
41914
  "motionZones.setZone": {
38790
41915
  capName: "motion-zones",
38791
41916
  capScope: "device",
@@ -38846,6 +41971,12 @@ Object.freeze({
38846
41971
  addonId: null,
38847
41972
  access: "view"
38848
41973
  },
41974
+ "nativeObjectDetection.getStatus": {
41975
+ capName: "native-object-detection",
41976
+ capScope: "device",
41977
+ addonId: null,
41978
+ access: "view"
41979
+ },
38849
41980
  "nativeObjectDetection.setEnabled": {
38850
41981
  capName: "native-object-detection",
38851
41982
  capScope: "device",
@@ -38864,6 +41995,12 @@ Object.freeze({
38864
41995
  addonId: null,
38865
41996
  access: "view"
38866
41997
  },
41998
+ "navigation.getStatus": {
41999
+ capName: "navigation",
42000
+ capScope: "device",
42001
+ addonId: null,
42002
+ access: "view"
42003
+ },
38867
42004
  "navigation.goToPoint": {
38868
42005
  capName: "navigation",
38869
42006
  capScope: "device",
@@ -38948,6 +42085,12 @@ Object.freeze({
38948
42085
  addonId: null,
38949
42086
  access: "create"
38950
42087
  },
42088
+ "networkLink.getStatus": {
42089
+ capName: "network-link",
42090
+ capScope: "device",
42091
+ addonId: null,
42092
+ access: "view"
42093
+ },
38951
42094
  "networkQuality.getAllStats": {
38952
42095
  capName: "network-quality",
38953
42096
  capScope: "system",
@@ -39206,18 +42349,36 @@ Object.freeze({
39206
42349
  addonId: null,
39207
42350
  access: "create"
39208
42351
  },
42352
+ "notifier.getStatus": {
42353
+ capName: "notifier",
42354
+ capScope: "device",
42355
+ addonId: null,
42356
+ access: "view"
42357
+ },
39209
42358
  "notifier.send": {
39210
42359
  capName: "notifier",
39211
42360
  capScope: "device",
39212
42361
  addonId: null,
39213
42362
  access: "create"
39214
42363
  },
42364
+ "numericSensor.getStatus": {
42365
+ capName: "numeric-sensor",
42366
+ capScope: "device",
42367
+ addonId: null,
42368
+ access: "view"
42369
+ },
39215
42370
  "oauthIntegration.getDescriptor": {
39216
42371
  capName: "oauth-integration",
39217
42372
  capScope: "system",
39218
42373
  addonId: null,
39219
42374
  access: "view"
39220
42375
  },
42376
+ "osd.getStatus": {
42377
+ capName: "osd",
42378
+ capScope: "device",
42379
+ addonId: null,
42380
+ access: "view"
42381
+ },
39221
42382
  "osd.setOverlay": {
39222
42383
  capName: "osd",
39223
42384
  capScope: "device",
@@ -39290,6 +42451,12 @@ Object.freeze({
39290
42451
  addonId: null,
39291
42452
  access: "create"
39292
42453
  },
42454
+ "petFeeder.getStatus": {
42455
+ capName: "pet-feeder",
42456
+ capScope: "device",
42457
+ addonId: null,
42458
+ access: "view"
42459
+ },
39293
42460
  "petFeeder.markFoodReplenished": {
39294
42461
  capName: "pet-feeder",
39295
42462
  capScope: "device",
@@ -39332,6 +42499,12 @@ Object.freeze({
39332
42499
  addonId: null,
39333
42500
  access: "create"
39334
42501
  },
42502
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
42503
+ capName: "pipeline-analytics",
42504
+ capScope: "device",
42505
+ addonId: null,
42506
+ access: "create"
42507
+ },
39335
42508
  "pipelineAnalytics.cancelRelocateMedia": {
39336
42509
  capName: "pipeline-analytics",
39337
42510
  capScope: "device",
@@ -39398,6 +42571,18 @@ Object.freeze({
39398
42571
  addonId: null,
39399
42572
  access: "view"
39400
42573
  },
42574
+ "pipelineAnalytics.getDeviceLiveContribution": {
42575
+ capName: "pipeline-analytics",
42576
+ capScope: "device",
42577
+ addonId: null,
42578
+ access: "view"
42579
+ },
42580
+ "pipelineAnalytics.getDeviceSettingsContribution": {
42581
+ capName: "pipeline-analytics",
42582
+ capScope: "device",
42583
+ addonId: null,
42584
+ access: "view"
42585
+ },
39401
42586
  "pipelineAnalytics.getEventDensity": {
39402
42587
  capName: "pipeline-analytics",
39403
42588
  capScope: "device",
@@ -39608,6 +42793,12 @@ Object.freeze({
39608
42793
  addonId: null,
39609
42794
  access: "view"
39610
42795
  },
42796
+ "pipelineAnalytics.ownersWithMedia": {
42797
+ capName: "pipeline-analytics",
42798
+ capScope: "device",
42799
+ addonId: null,
42800
+ access: "view"
42801
+ },
39611
42802
  "pipelineAnalytics.pauseForStorageMigration": {
39612
42803
  capName: "pipeline-analytics",
39613
42804
  capScope: "device",
@@ -39962,6 +43153,12 @@ Object.freeze({
39962
43153
  addonId: null,
39963
43154
  access: "view"
39964
43155
  },
43156
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
43157
+ capName: "pipeline-orchestrator",
43158
+ capScope: "system",
43159
+ addonId: null,
43160
+ access: "create"
43161
+ },
39965
43162
  "pipelineOrchestrator.assignAudio": {
39966
43163
  capName: "pipeline-orchestrator",
39967
43164
  capScope: "system",
@@ -40052,6 +43249,18 @@ Object.freeze({
40052
43249
  addonId: null,
40053
43250
  access: "view"
40054
43251
  },
43252
+ "pipelineOrchestrator.getDeviceLiveContribution": {
43253
+ capName: "pipeline-orchestrator",
43254
+ capScope: "system",
43255
+ addonId: null,
43256
+ access: "view"
43257
+ },
43258
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
43259
+ capName: "pipeline-orchestrator",
43260
+ capScope: "system",
43261
+ addonId: null,
43262
+ access: "view"
43263
+ },
40055
43264
  "pipelineOrchestrator.getGlobalMetrics": {
40056
43265
  capName: "pipeline-orchestrator",
40057
43266
  capScope: "system",
@@ -40460,12 +43669,36 @@ Object.freeze({
40460
43669
  addonId: null,
40461
43670
  access: "view"
40462
43671
  },
43672
+ "powerMeter.getStatus": {
43673
+ capName: "power-meter",
43674
+ capScope: "device",
43675
+ addonId: null,
43676
+ access: "view"
43677
+ },
43678
+ "presence.getStatus": {
43679
+ capName: "presence",
43680
+ capScope: "device",
43681
+ addonId: null,
43682
+ access: "view"
43683
+ },
43684
+ "pressureSensor.getStatus": {
43685
+ capName: "pressure-sensor",
43686
+ capScope: "device",
43687
+ addonId: null,
43688
+ access: "view"
43689
+ },
40463
43690
  "privacyMask.getOptions": {
40464
43691
  capName: "privacy-mask",
40465
43692
  capScope: "device",
40466
43693
  addonId: null,
40467
43694
  access: "view"
40468
43695
  },
43696
+ "privacyMask.getStatus": {
43697
+ capName: "privacy-mask",
43698
+ capScope: "device",
43699
+ addonId: null,
43700
+ access: "view"
43701
+ },
40469
43702
  "privacyMask.setAudioEnabled": {
40470
43703
  capName: "privacy-mask",
40471
43704
  capScope: "device",
@@ -40526,6 +43759,12 @@ Object.freeze({
40526
43759
  addonId: null,
40527
43760
  access: "view"
40528
43761
  },
43762
+ "ptz.getStatus": {
43763
+ capName: "ptz",
43764
+ capScope: "device",
43765
+ addonId: null,
43766
+ access: "view"
43767
+ },
40529
43768
  "ptz.goHome": {
40530
43769
  capName: "ptz",
40531
43770
  capScope: "device",
@@ -40604,6 +43843,12 @@ Object.freeze({
40604
43843
  addonId: null,
40605
43844
  access: "create"
40606
43845
  },
43846
+ "recording.applyDeviceSettingsPatch": {
43847
+ capName: "recording",
43848
+ capScope: "system",
43849
+ addonId: null,
43850
+ access: "create"
43851
+ },
40607
43852
  "recording.cancelRelocateJob": {
40608
43853
  capName: "recording",
40609
43854
  capScope: "system",
@@ -40652,6 +43897,18 @@ Object.freeze({
40652
43897
  addonId: null,
40653
43898
  access: "view"
40654
43899
  },
43900
+ "recording.getDeviceLiveContribution": {
43901
+ capName: "recording",
43902
+ capScope: "system",
43903
+ addonId: null,
43904
+ access: "view"
43905
+ },
43906
+ "recording.getDeviceSettingsContribution": {
43907
+ capName: "recording",
43908
+ capScope: "system",
43909
+ addonId: null,
43910
+ access: "view"
43911
+ },
40655
43912
  "recording.getPlacement": {
40656
43913
  capName: "recording",
40657
43914
  capScope: "system",
@@ -40670,6 +43927,12 @@ Object.freeze({
40670
43927
  addonId: null,
40671
43928
  access: "view"
40672
43929
  },
43930
+ "recording.getStatus": {
43931
+ capName: "recording",
43932
+ capScope: "system",
43933
+ addonId: null,
43934
+ access: "view"
43935
+ },
40673
43936
  "recording.getStorageMigrationMoveStatus": {
40674
43937
  capName: "recording",
40675
43938
  capScope: "system",
@@ -40844,6 +44107,24 @@ Object.freeze({
40844
44107
  addonId: null,
40845
44108
  access: "view"
40846
44109
  },
44110
+ "recordingOnboard.getOptions": {
44111
+ capName: "recording-onboard",
44112
+ capScope: "device",
44113
+ addonId: null,
44114
+ access: "view"
44115
+ },
44116
+ "recordingOnboard.getStatus": {
44117
+ capName: "recording-onboard",
44118
+ capScope: "device",
44119
+ addonId: null,
44120
+ access: "view"
44121
+ },
44122
+ "recordingOnboard.setSettings": {
44123
+ capName: "recording-onboard",
44124
+ capScope: "device",
44125
+ addonId: null,
44126
+ access: "create"
44127
+ },
40847
44128
  "recordingSignal.getStatus": {
40848
44129
  capName: "recording-signal",
40849
44130
  capScope: "device",
@@ -40874,6 +44155,12 @@ Object.freeze({
40874
44155
  addonId: null,
40875
44156
  access: "delete"
40876
44157
  },
44158
+ "sceneMonitor.getStatus": {
44159
+ capName: "scene-monitor",
44160
+ capScope: "device",
44161
+ addonId: null,
44162
+ access: "view"
44163
+ },
40877
44164
  "sceneMonitor.listScenes": {
40878
44165
  capName: "scene-monitor",
40879
44166
  capScope: "device",
@@ -40904,6 +44191,12 @@ Object.freeze({
40904
44191
  addonId: null,
40905
44192
  access: "create"
40906
44193
  },
44194
+ "scriptRunner.getStatus": {
44195
+ capName: "script-runner",
44196
+ capScope: "device",
44197
+ addonId: null,
44198
+ access: "view"
44199
+ },
40907
44200
  "scriptRunner.run": {
40908
44201
  capName: "script-runner",
40909
44202
  capScope: "device",
@@ -41036,6 +44329,12 @@ Object.freeze({
41036
44329
  addonId: null,
41037
44330
  access: "create"
41038
44331
  },
44332
+ "smoke.getStatus": {
44333
+ capName: "smoke",
44334
+ capScope: "device",
44335
+ addonId: null,
44336
+ access: "view"
44337
+ },
41039
44338
  "smtpProvider.getStatus": {
41040
44339
  capName: "smtp-provider",
41041
44340
  capScope: "system",
@@ -41054,12 +44353,30 @@ Object.freeze({
41054
44353
  addonId: null,
41055
44354
  access: "view"
41056
44355
  },
44356
+ "snapshot.applyDeviceSettingsPatch": {
44357
+ capName: "snapshot",
44358
+ capScope: "device",
44359
+ addonId: null,
44360
+ access: "create"
44361
+ },
41057
44362
  "snapshot.getDebugState": {
41058
44363
  capName: "snapshot",
41059
44364
  capScope: "device",
41060
44365
  addonId: null,
41061
44366
  access: "view"
41062
44367
  },
44368
+ "snapshot.getDeviceLiveContribution": {
44369
+ capName: "snapshot",
44370
+ capScope: "device",
44371
+ addonId: null,
44372
+ access: "view"
44373
+ },
44374
+ "snapshot.getDeviceSettingsContribution": {
44375
+ capName: "snapshot",
44376
+ capScope: "device",
44377
+ addonId: null,
44378
+ access: "view"
44379
+ },
41063
44380
  "snapshot.getSnapshot": {
41064
44381
  capName: "snapshot",
41065
44382
  capScope: "device",
@@ -41078,6 +44395,12 @@ Object.freeze({
41078
44395
  addonId: null,
41079
44396
  access: "view"
41080
44397
  },
44398
+ "snapshot.getStatus": {
44399
+ capName: "snapshot",
44400
+ capScope: "device",
44401
+ addonId: null,
44402
+ access: "view"
44403
+ },
41081
44404
  "snapshot.invalidateCache": {
41082
44405
  capName: "snapshot",
41083
44406
  capScope: "device",
@@ -41414,6 +44737,12 @@ Object.freeze({
41414
44737
  addonId: null,
41415
44738
  access: "create"
41416
44739
  },
44740
+ "streamBroker.applyDeviceSettingsPatch": {
44741
+ capName: "stream-broker",
44742
+ capScope: "system",
44743
+ addonId: null,
44744
+ access: "create"
44745
+ },
41417
44746
  "streamBroker.assignProfile": {
41418
44747
  capName: "stream-broker",
41419
44748
  capScope: "system",
@@ -41450,6 +44779,18 @@ Object.freeze({
41450
44779
  addonId: null,
41451
44780
  access: "view"
41452
44781
  },
44782
+ "streamBroker.getDeviceLiveContribution": {
44783
+ capName: "stream-broker",
44784
+ capScope: "system",
44785
+ addonId: null,
44786
+ access: "view"
44787
+ },
44788
+ "streamBroker.getDeviceSettingsContribution": {
44789
+ capName: "stream-broker",
44790
+ capScope: "system",
44791
+ addonId: null,
44792
+ access: "view"
44793
+ },
41453
44794
  "streamBroker.getPreBufferInfo": {
41454
44795
  capName: "stream-broker",
41455
44796
  capScope: "system",
@@ -41648,6 +44989,12 @@ Object.freeze({
41648
44989
  addonId: null,
41649
44990
  access: "view"
41650
44991
  },
44992
+ "streamParams.getStatus": {
44993
+ capName: "stream-params",
44994
+ capScope: "device",
44995
+ addonId: null,
44996
+ access: "view"
44997
+ },
41651
44998
  "streamParams.setProfile": {
41652
44999
  capName: "stream-params",
41653
45000
  capScope: "device",
@@ -41660,6 +45007,12 @@ Object.freeze({
41660
45007
  addonId: null,
41661
45008
  access: "view"
41662
45009
  },
45010
+ "switch.getStatus": {
45011
+ capName: "switch",
45012
+ capScope: "device",
45013
+ addonId: null,
45014
+ access: "view"
45015
+ },
41663
45016
  "switch.setState": {
41664
45017
  capName: "switch",
41665
45018
  capScope: "device",
@@ -41756,6 +45109,18 @@ Object.freeze({
41756
45109
  addonId: null,
41757
45110
  access: "create"
41758
45111
  },
45112
+ "tamper.getStatus": {
45113
+ capName: "tamper",
45114
+ capScope: "device",
45115
+ addonId: null,
45116
+ access: "view"
45117
+ },
45118
+ "temperatureSensor.getStatus": {
45119
+ capName: "temperature-sensor",
45120
+ capScope: "device",
45121
+ addonId: null,
45122
+ access: "view"
45123
+ },
41759
45124
  "terminalSession.adoptLegacyMonitor": {
41760
45125
  capName: "terminal-session",
41761
45126
  capScope: "system",
@@ -41852,6 +45217,12 @@ Object.freeze({
41852
45217
  addonId: null,
41853
45218
  access: "view"
41854
45219
  },
45220
+ "update.getStatus": {
45221
+ capName: "update",
45222
+ capScope: "device",
45223
+ addonId: null,
45224
+ access: "view"
45225
+ },
41855
45226
  "update.installUpdate": {
41856
45227
  capName: "update",
41857
45228
  capScope: "device",
@@ -42074,6 +45445,12 @@ Object.freeze({
42074
45445
  addonId: null,
42075
45446
  access: "create"
42076
45447
  },
45448
+ "vacuumControl.getStatus": {
45449
+ capName: "vacuum-control",
45450
+ capScope: "device",
45451
+ addonId: null,
45452
+ access: "view"
45453
+ },
42077
45454
  "vacuumControl.locate": {
42078
45455
  capName: "vacuum-control",
42079
45456
  capScope: "device",
@@ -42116,6 +45493,12 @@ Object.freeze({
42116
45493
  addonId: null,
42117
45494
  access: "create"
42118
45495
  },
45496
+ "valve.getStatus": {
45497
+ capName: "valve",
45498
+ capScope: "device",
45499
+ addonId: null,
45500
+ access: "view"
45501
+ },
42119
45502
  "valve.open": {
42120
45503
  capName: "valve",
42121
45504
  capScope: "device",
@@ -42188,6 +45571,18 @@ Object.freeze({
42188
45571
  addonId: null,
42189
45572
  access: "create"
42190
45573
  },
45574
+ "vibration.getStatus": {
45575
+ capName: "vibration",
45576
+ capScope: "device",
45577
+ addonId: null,
45578
+ access: "view"
45579
+ },
45580
+ "videoclips.dialClipStream": {
45581
+ capName: "videoclips",
45582
+ capScope: "device",
45583
+ addonId: null,
45584
+ access: "view"
45585
+ },
42191
45586
  "videoclips.getClipPlayback": {
42192
45587
  capName: "videoclips",
42193
45588
  capScope: "device",
@@ -42200,6 +45595,18 @@ Object.freeze({
42200
45595
  addonId: null,
42201
45596
  access: "view"
42202
45597
  },
45598
+ "videoclips.listSources": {
45599
+ capName: "videoclips",
45600
+ capScope: "device",
45601
+ addonId: null,
45602
+ access: "view"
45603
+ },
45604
+ "videoclips.readClipBytes": {
45605
+ capName: "videoclips",
45606
+ capScope: "device",
45607
+ addonId: null,
45608
+ access: "view"
45609
+ },
42203
45610
  "viewerUi.getStaticDir": {
42204
45611
  capName: "viewer-ui",
42205
45612
  capScope: "system",
@@ -42212,6 +45619,12 @@ Object.freeze({
42212
45619
  addonId: null,
42213
45620
  access: "view"
42214
45621
  },
45622
+ "waterHeater.getStatus": {
45623
+ capName: "water-heater",
45624
+ capScope: "device",
45625
+ addonId: null,
45626
+ access: "view"
45627
+ },
42215
45628
  "waterHeater.setAway": {
42216
45629
  capName: "water-heater",
42217
45630
  capScope: "device",
@@ -42230,6 +45643,12 @@ Object.freeze({
42230
45643
  addonId: null,
42231
45644
  access: "create"
42232
45645
  },
45646
+ "weather.getStatus": {
45647
+ capName: "weather",
45648
+ capScope: "device",
45649
+ addonId: null,
45650
+ access: "view"
45651
+ },
42233
45652
  "webrtcSession.addIceCandidate": {
42234
45653
  capName: "webrtc-session",
42235
45654
  capScope: "device",
@@ -42254,6 +45673,12 @@ Object.freeze({
42254
45673
  addonId: null,
42255
45674
  access: "view"
42256
45675
  },
45676
+ "webrtcSession.getSessionDebug": {
45677
+ capName: "webrtc-session",
45678
+ capScope: "device",
45679
+ addonId: null,
45680
+ access: "view"
45681
+ },
42257
45682
  "webrtcSession.getSessionState": {
42258
45683
  capName: "webrtc-session",
42259
45684
  capScope: "device",
@@ -42586,6 +46011,11 @@ Object.freeze({
42586
46011
  form: "single",
42587
46012
  optional: true
42588
46013
  }],
46014
+ "deviceAdminLink.getAdminLink": [{
46015
+ name: "deviceId",
46016
+ form: "single",
46017
+ optional: false
46018
+ }],
42589
46019
  "deviceAdoption.release": [{
42590
46020
  name: "camDeviceId",
42591
46021
  form: "single",
@@ -43496,6 +46926,11 @@ Object.freeze({
43496
46926
  form: "single",
43497
46927
  optional: false
43498
46928
  }],
46929
+ "pipelineAnalytics.ownersWithMedia": [{
46930
+ name: "deviceId",
46931
+ form: "single",
46932
+ optional: false
46933
+ }],
43499
46934
  "pipelineAnalytics.proposeRetrainAnnotations": [{
43500
46935
  name: "deviceId",
43501
46936
  form: "single",
@@ -43966,6 +47401,16 @@ Object.freeze({
43966
47401
  form: "single",
43967
47402
  optional: true
43968
47403
  }],
47404
+ "recordingOnboard.getOptions": [{
47405
+ name: "deviceId",
47406
+ form: "single",
47407
+ optional: false
47408
+ }],
47409
+ "recordingOnboard.setSettings": [{
47410
+ name: "deviceId",
47411
+ form: "single",
47412
+ optional: false
47413
+ }],
43969
47414
  "recordingSignal.getStatus": [{
43970
47415
  name: "deviceId",
43971
47416
  form: "single",
@@ -44192,6 +47637,11 @@ Object.freeze({
44192
47637
  form: "single",
44193
47638
  optional: false
44194
47639
  }],
47640
+ "videoclips.dialClipStream": [{
47641
+ name: "deviceId",
47642
+ form: "single",
47643
+ optional: false
47644
+ }],
44195
47645
  "videoclips.getClipPlayback": [{
44196
47646
  name: "deviceId",
44197
47647
  form: "single",
@@ -44202,6 +47652,16 @@ Object.freeze({
44202
47652
  form: "single",
44203
47653
  optional: false
44204
47654
  }],
47655
+ "videoclips.listSources": [{
47656
+ name: "deviceId",
47657
+ form: "single",
47658
+ optional: false
47659
+ }],
47660
+ "videoclips.readClipBytes": [{
47661
+ name: "deviceId",
47662
+ form: "single",
47663
+ optional: false
47664
+ }],
44205
47665
  "waterHeater.setAway": [{
44206
47666
  name: "deviceId",
44207
47667
  form: "single",
@@ -44237,6 +47697,11 @@ Object.freeze({
44237
47697
  form: "single",
44238
47698
  optional: false
44239
47699
  }],
47700
+ "webrtcSession.getSessionDebug": [{
47701
+ name: "deviceId",
47702
+ form: "single",
47703
+ optional: false
47704
+ }],
44240
47705
  "webrtcSession.getSessionState": [{
44241
47706
  name: "deviceId",
44242
47707
  form: "single",
@@ -45064,6 +48529,154 @@ function buildControllerFormSchema() {
45064
48529
  }] };
45065
48530
  }
45066
48531
  //#endregion
48532
+ //#region src/unifi-node-resolver.ts
48533
+ /**
48534
+ * Per-controller registry that device classes use to reach the live
48535
+ * {@link UnifiIntegrationManager} (and its infra + client snapshots) for a given
48536
+ * controller. Keyed on the controller
48537
+ * {@link import('@camstack/types').DeviceType.Hub} device's `stableId`
48538
+ * (standalone mode — the connection lives on the controller device, not a
48539
+ * broker). Mirrors the Velux `VeluxNodeResolver` / Ecowitt `EcowittFacadeResolver`.
48540
+ *
48541
+ * The controller device recreates the manager on reconnect / settings change and
48542
+ * re-publishes it here; infra + client children always read through the resolver,
48543
+ * so they transparently pick up the new manager.
48544
+ */
48545
+ var UnifiControllerResolver = class {
48546
+ #managers = /* @__PURE__ */ new Map();
48547
+ /**
48548
+ * Per-controller poll-tick fan-out. Children subscribe here instead of
48549
+ * attaching a listener per child — the controller device pumps this bus once
48550
+ * per poll cycle. A station with 100+ clients would otherwise trip Node's
48551
+ * default MaxListeners cap; a plain `Set` of callbacks has no listener limit.
48552
+ */
48553
+ #snapshotSubs = /* @__PURE__ */ new Map();
48554
+ /** Publish or remove the manager for a controller id. `null` removes the entry. */
48555
+ set(controllerId, manager) {
48556
+ if (manager === null) {
48557
+ this.#managers.delete(controllerId);
48558
+ return;
48559
+ }
48560
+ this.#managers.set(controllerId, manager);
48561
+ }
48562
+ /** The live manager for a controller id, or null when unknown / stopped. */
48563
+ getManager(controllerId) {
48564
+ return this.#managers.get(controllerId) ?? null;
48565
+ }
48566
+ /** All infra snapshots for a controller id, or [] when unknown. */
48567
+ getInfraList(controllerId) {
48568
+ return this.#managers.get(controllerId)?.getInfraList() ?? [];
48569
+ }
48570
+ /** Resolve a single infra snapshot for a controller, or null. */
48571
+ getInfra(controllerId, id) {
48572
+ return this.#managers.get(controllerId)?.getInfra(id) ?? null;
48573
+ }
48574
+ /** All client snapshots for a controller id, or [] when unknown. */
48575
+ getClientList(controllerId) {
48576
+ return this.#managers.get(controllerId)?.getClientList() ?? [];
48577
+ }
48578
+ /** Resolve a single client snapshot for a controller, or null. */
48579
+ getClient(controllerId, mac) {
48580
+ return this.#managers.get(controllerId)?.getClient(mac) ?? null;
48581
+ }
48582
+ /** Subscribe a child to per-controller poll ticks. Returns an unsubscribe fn. */
48583
+ onSnapshot(controllerId, cb) {
48584
+ let set = this.#snapshotSubs.get(controllerId);
48585
+ if (!set) {
48586
+ set = /* @__PURE__ */ new Set();
48587
+ this.#snapshotSubs.set(controllerId, set);
48588
+ }
48589
+ set.add(cb);
48590
+ return () => {
48591
+ const current = this.#snapshotSubs.get(controllerId);
48592
+ current?.delete(cb);
48593
+ if (current && current.size === 0) this.#snapshotSubs.delete(controllerId);
48594
+ };
48595
+ }
48596
+ /** Fan a single poll tick out to every subscribed child for a controller. */
48597
+ emitSnapshot(controllerId) {
48598
+ const set = this.#snapshotSubs.get(controllerId);
48599
+ if (!set) return;
48600
+ for (const cb of set) try {
48601
+ cb();
48602
+ } catch {}
48603
+ }
48604
+ /** Remove all registered managers (called on full shutdown). */
48605
+ clear() {
48606
+ this.#managers.clear();
48607
+ this.#snapshotSubs.clear();
48608
+ }
48609
+ };
48610
+ /** The single in-process per-controller manager resolver shared between the
48611
+ * controller device and its infra / client children. */
48612
+ var unifiControllers = new UnifiControllerResolver();
48613
+ //#endregion
48614
+ //#region src/unifi-admin-link.ts
48615
+ /**
48616
+ * `device-admin-link` for UniFi-backed devices.
48617
+ *
48618
+ * A UniFi access point or switch has no management page of its own — it is
48619
+ * adopted by, and configured from, the controller. So every device this addon
48620
+ * owns (the controller Hub, an infra Container, a port/feature child) resolves
48621
+ * to the CONTROLLER's UI, `target: 'integration'`, labelled as such so an
48622
+ * operator knows before clicking that he is not landing on the AP.
48623
+ *
48624
+ * ## What is known, and the one thing that is not
48625
+ *
48626
+ * `host` and `port` are the operator's own controller settings — `port` is
48627
+ * described in the schema as "Controller HTTPS port (443 on UniFi OS, 8443 on
48628
+ * legacy)", so it IS the UI's port. There is no `https` boolean because UniFi
48629
+ * has no plaintext mode; https is a fact about the product, not an assumption.
48630
+ *
48631
+ * What is NOT known is a per-device deep link. `UnifiInfraSnapshot` carries no
48632
+ * `ip` (id, name, model, cpu, clients, state, uptime, ports, led, online — that
48633
+ * is the whole shape), and the controller's own per-device route embeds the
48634
+ * site slug and a UI-version-dependent path. Inventing one would produce a
48635
+ * button that lands on a redirect-to-dashboard at best and a 404 after the next
48636
+ * controller upgrade. The controller root is what this provider can honestly
48637
+ * say, so it is what it says.
48638
+ *
48639
+ * `apiKey` / `username` / `password` sit in the same connection object and
48640
+ * never travel: `buildDeviceAdminUrl` has no parameter they could ride in.
48641
+ */
48642
+ function registerUnifiAdminLink(ctx, deviceId, source) {
48643
+ ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async (input) => {
48644
+ if (input.deviceId !== deviceId) throw new Error(`UnifiAdminLink: deviceId mismatch, expected ${deviceId}, got ${input.deviceId}`);
48645
+ let endpoint;
48646
+ if (source.kind === "own") endpoint = source.read();
48647
+ else {
48648
+ const manager = unifiControllers.getManager(source.controllerId);
48649
+ if (manager === null) {
48650
+ ctx.logger.debug("unifi admin link unavailable: no live manager", {
48651
+ tags: { deviceId },
48652
+ meta: { controllerId: source.controllerId }
48653
+ });
48654
+ return null;
48655
+ }
48656
+ endpoint = manager.getConnection();
48657
+ }
48658
+ const built = buildDeviceAdminUrl({
48659
+ host: endpoint.host,
48660
+ port: endpoint.port,
48661
+ scheme: "https",
48662
+ path: "/"
48663
+ });
48664
+ if (!built.ok) {
48665
+ ctx.logger.debug("unifi admin link unavailable", {
48666
+ tags: { deviceId },
48667
+ meta: { reason: built.reason }
48668
+ });
48669
+ return null;
48670
+ }
48671
+ return {
48672
+ url: built.url,
48673
+ label: "UniFi controller",
48674
+ target: "integration",
48675
+ host: endpoint.host
48676
+ };
48677
+ } });
48678
+ }
48679
+ //#endregion
45067
48680
  //#region src/unifi-discovery.ts
45068
48681
  /**
45069
48682
  * The `childNativeId` an infra Container candidate is keyed on. The controller
@@ -45270,88 +48883,6 @@ function featuresEqual(a, b) {
45270
48883
  return true;
45271
48884
  }
45272
48885
  //#endregion
45273
- //#region src/unifi-node-resolver.ts
45274
- /**
45275
- * Per-controller registry that device classes use to reach the live
45276
- * {@link UnifiIntegrationManager} (and its infra + client snapshots) for a given
45277
- * controller. Keyed on the controller
45278
- * {@link import('@camstack/types').DeviceType.Hub} device's `stableId`
45279
- * (standalone mode — the connection lives on the controller device, not a
45280
- * broker). Mirrors the Velux `VeluxNodeResolver` / Ecowitt `EcowittFacadeResolver`.
45281
- *
45282
- * The controller device recreates the manager on reconnect / settings change and
45283
- * re-publishes it here; infra + client children always read through the resolver,
45284
- * so they transparently pick up the new manager.
45285
- */
45286
- var UnifiControllerResolver = class {
45287
- #managers = /* @__PURE__ */ new Map();
45288
- /**
45289
- * Per-controller poll-tick fan-out. Children subscribe here instead of
45290
- * attaching a listener per child — the controller device pumps this bus once
45291
- * per poll cycle. A station with 100+ clients would otherwise trip Node's
45292
- * default MaxListeners cap; a plain `Set` of callbacks has no listener limit.
45293
- */
45294
- #snapshotSubs = /* @__PURE__ */ new Map();
45295
- /** Publish or remove the manager for a controller id. `null` removes the entry. */
45296
- set(controllerId, manager) {
45297
- if (manager === null) {
45298
- this.#managers.delete(controllerId);
45299
- return;
45300
- }
45301
- this.#managers.set(controllerId, manager);
45302
- }
45303
- /** The live manager for a controller id, or null when unknown / stopped. */
45304
- getManager(controllerId) {
45305
- return this.#managers.get(controllerId) ?? null;
45306
- }
45307
- /** All infra snapshots for a controller id, or [] when unknown. */
45308
- getInfraList(controllerId) {
45309
- return this.#managers.get(controllerId)?.getInfraList() ?? [];
45310
- }
45311
- /** Resolve a single infra snapshot for a controller, or null. */
45312
- getInfra(controllerId, id) {
45313
- return this.#managers.get(controllerId)?.getInfra(id) ?? null;
45314
- }
45315
- /** All client snapshots for a controller id, or [] when unknown. */
45316
- getClientList(controllerId) {
45317
- return this.#managers.get(controllerId)?.getClientList() ?? [];
45318
- }
45319
- /** Resolve a single client snapshot for a controller, or null. */
45320
- getClient(controllerId, mac) {
45321
- return this.#managers.get(controllerId)?.getClient(mac) ?? null;
45322
- }
45323
- /** Subscribe a child to per-controller poll ticks. Returns an unsubscribe fn. */
45324
- onSnapshot(controllerId, cb) {
45325
- let set = this.#snapshotSubs.get(controllerId);
45326
- if (!set) {
45327
- set = /* @__PURE__ */ new Set();
45328
- this.#snapshotSubs.set(controllerId, set);
45329
- }
45330
- set.add(cb);
45331
- return () => {
45332
- const current = this.#snapshotSubs.get(controllerId);
45333
- current?.delete(cb);
45334
- if (current && current.size === 0) this.#snapshotSubs.delete(controllerId);
45335
- };
45336
- }
45337
- /** Fan a single poll tick out to every subscribed child for a controller. */
45338
- emitSnapshot(controllerId) {
45339
- const set = this.#snapshotSubs.get(controllerId);
45340
- if (!set) return;
45341
- for (const cb of set) try {
45342
- cb();
45343
- } catch {}
45344
- }
45345
- /** Remove all registered managers (called on full shutdown). */
45346
- clear() {
45347
- this.#managers.clear();
45348
- this.#snapshotSubs.clear();
45349
- }
45350
- };
45351
- /** The single in-process per-controller manager resolver shared between the
45352
- * controller device and its infra / client children. */
45353
- var unifiControllers = new UnifiControllerResolver();
45354
- //#endregion
45355
48886
  //#region src/unifi-integration-manager.ts
45356
48887
  /**
45357
48888
  * Default client factory — INTENTIONALLY unimplemented. No UniFi controller-API
@@ -45576,6 +49107,10 @@ var UnifiFeatureDevice = class extends BaseDevice {
45576
49107
  this.kind = cfg.kind;
45577
49108
  this.controllerId = cfg.controllerId;
45578
49109
  this.infraNativeId = cfg.infraNativeId;
49110
+ registerUnifiAdminLink(ctx, this.id, {
49111
+ kind: "controller",
49112
+ controllerId: this.controllerId
49113
+ });
45579
49114
  this.switchFeature = cfg.switchFeature;
45580
49115
  this.buttonAction = cfg.buttonAction;
45581
49116
  this.portIdx = cfg.portIdx;
@@ -45803,6 +49338,10 @@ var UnifiInfraDevice = class extends BaseDevice {
45803
49338
  super(ctx, unifiInfraSchema, { type: DeviceType.Container });
45804
49339
  this.controllerId = cfg.controllerId;
45805
49340
  this.infraNativeId = cfg.infraNativeId;
49341
+ registerUnifiAdminLink(ctx, this.id, {
49342
+ kind: "controller",
49343
+ controllerId: this.controllerId
49344
+ });
45806
49345
  this.online = unifiControllers.getInfra(this.controllerId, this.infraNativeId)?.online ?? false;
45807
49346
  this.updateSourceInfo({
45808
49347
  id: `${this.controllerId}:${this.infraNativeId}`,
@@ -45868,6 +49407,10 @@ var UnifiControllerDevice = class extends BaseDevice {
45868
49407
  super(ctx, unifiControllerSchema, { type: DeviceType.Hub });
45869
49408
  this.online = false;
45870
49409
  this.registerDeviceDiscovery();
49410
+ registerUnifiAdminLink(ctx, this.id, {
49411
+ kind: "own",
49412
+ read: () => this.config.values.connection
49413
+ });
45871
49414
  this.updateSourceInfo({
45872
49415
  id: this.stableId,
45873
49416
  system: "unifi"