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