@camstack/addon-provider-ecowitt 0.2.113 → 0.2.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +3546 -33
  2. package/dist/addon.mjs +3546 -33
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5363,7 +5363,7 @@ var ZodIssueCode$1 = {
5363
5363
  var ZodFirstPartyTypeKind$1;
5364
5364
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5365
5365
  //#endregion
5366
- //#region ../types/dist/sleep-Bs3xPtSh.mjs
5366
+ //#region ../types/dist/sleep-i3eUVc-d.mjs
5367
5367
  /**
5368
5368
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5369
5369
  * window to float samples (D455).
@@ -6505,6 +6505,17 @@ var CamProfileSchema = _enum([
6505
6505
  "mid",
6506
6506
  "low"
6507
6507
  ]);
6508
+ /**
6509
+ * Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
6510
+ *
6511
+ * `auto` is a REAL value, not an absent one — it means "resolve it by the rule
6512
+ * that consumer owns". It lives beside `CamProfile` because a rung is the only
6513
+ * operator-facing way to name a stream: a cam-stream id is broker-internal and
6514
+ * moves when a slot is reassigned, so a stored id silently stops meaning what
6515
+ * the operator picked. Today's one user is the audio analyser, whose rule is
6516
+ * "the lowest AVAILABLE native rung" (D537).
6517
+ */
6518
+ var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
6508
6519
  var CamStreamKindSchema = _enum([
6509
6520
  "pull-rtsp",
6510
6521
  "pull-rtmp",
@@ -6923,6 +6934,18 @@ function systemMethod(input, output, options) {
6923
6934
  systemOnly: true
6924
6935
  };
6925
6936
  }
6937
+ /**
6938
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
6939
+ * on `InferProvider`. The `providerOptional: true` literal is what
6940
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
6941
+ * for when this is the honest answer and when it is a soft stub.
6942
+ */
6943
+ function optionalMethod(input, output, options) {
6944
+ return {
6945
+ ...method(input, output, options),
6946
+ providerOptional: true
6947
+ };
6948
+ }
6926
6949
  /** Shorthand to define an event schema */
6927
6950
  function event(data) {
6928
6951
  return { data };
@@ -7353,6 +7376,8 @@ var EVENT_OWNER_TYPES = [
7353
7376
  * nothing failing until a caller asked.
7354
7377
  */
7355
7378
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7379
+ /** The same list as a Zod enum, for the cap input that carries it. */
7380
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7356
7381
  new Set(EVENT_OWNER_TYPES);
7357
7382
  var EncodeProfileSchema = object({
7358
7383
  video: object({
@@ -7725,6 +7750,7 @@ var CameraSwitchIdSchema = _enum([
7725
7750
  "device-audio",
7726
7751
  "broker-audio",
7727
7752
  "audio-analysis",
7753
+ "event-saving",
7728
7754
  "recording",
7729
7755
  "notifications"
7730
7756
  ]);
@@ -7760,10 +7786,20 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7760
7786
  var CameraSwitchUnavailableReasonSchema = _enum([
7761
7787
  "no-provider",
7762
7788
  "source-unreachable",
7763
- "not-configured",
7764
7789
  "device-disabled"
7765
7790
  ]);
7766
7791
  /**
7792
+ * Something true about a switch that is nonetheless OFFERED.
7793
+ *
7794
+ * The distinction from {@link CameraSwitchUnavailableReasonSchema} is the
7795
+ * whole point: a reason replaces the control, a warning travels WITH it. The
7796
+ * privacy mask taught us the difference — greying the one button that starts
7797
+ * the feature teaches the operator the feature is broken, while a live toggle
7798
+ * that flips nothing teaches them the switch is broken. Neither is true; the
7799
+ * honest surface is a working toggle that says what is missing.
7800
+ */
7801
+ var CameraSwitchWarningSchema = _enum(["no-privacy-zones"]);
7802
+ /**
7767
7803
  * One switch, resolved for one camera.
7768
7804
  *
7769
7805
  * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
@@ -7782,6 +7818,11 @@ var CameraSwitchSchema = object({
7782
7818
  /** False = do not render a control. `unavailableReason` says why. */
7783
7819
  available: boolean(),
7784
7820
  unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7821
+ /**
7822
+ * Present on an AVAILABLE switch only. Rendered beside the live control;
7823
+ * never a substitute for one.
7824
+ */
7825
+ warning: CameraSwitchWarningSchema.optional(),
7785
7826
  /** Current state. Meaningless when `available` is false — read it as `true`. */
7786
7827
  enabled: boolean(),
7787
7828
  authority: CameraSwitchAuthoritySchema
@@ -8199,11 +8240,31 @@ var RecordingBandTriggersSchema = RecordingTriggersSchema;
8199
8240
  * band engine's `applies` rule). `start`/`end` are `HH:MM`; an `end <= start`
8200
8241
  * span wraps past midnight (handled by the band engine).
8201
8242
  */
8243
+ /**
8244
+ * Whether a band's footage carries sound.
8245
+ *
8246
+ * The recorder has always attached with `audio: 'aac'`, so every band of every
8247
+ * camera recorded sound whether anyone wanted it or not. The broker already
8248
+ * serves the other answer — `audio: 'none'` takes the video-only `/muted`
8249
+ * restream — so this was a choice the product could always have offered.
8250
+ *
8251
+ * It belongs on the BAND rather than the camera because one camera can want
8252
+ * both: silent across the shop floor during opening hours, sound overnight.
8253
+ * And it applies to BOTH modes — an events band firing on motion has the same
8254
+ * question to answer as a continuous one.
8255
+ */
8256
+ var RecordingBandAudioSchema = _enum(["on", "muted"]);
8202
8257
  var RecordingBandSchema = object({
8203
8258
  days: array(RecordingWeekdaySchema),
8204
8259
  start: string().regex(HHMM),
8205
8260
  end: string().regex(HHMM),
8206
8261
  mode: RecordingBandModeSchema,
8262
+ /**
8263
+ * Sound, or silence. OPTIONAL and absent-means-`on`: every recording
8264
+ * written before this existed carried sound, and a band whose key is
8265
+ * missing must keep meaning what it meant. See {@link RecordingBandAudioSchema}.
8266
+ */
8267
+ audio: RecordingBandAudioSchema.optional(),
8207
8268
  triggers: RecordingBandTriggersSchema.optional(),
8208
8269
  /**
8209
8270
  * RETIRED (D381). A band no longer carries a pre-buffer of its own.
@@ -13487,6 +13548,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13487
13548
  auth: "admin"
13488
13549
  });
13489
13550
  /**
13551
+ * device-admin-link — "this device has a management page of its own, and here
13552
+ * is its address".
13553
+ *
13554
+ * ## Why this is not a `deviceConfig` cap
13555
+ *
13556
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13557
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13558
+ * patch back through a setter; it costs a `builderId` reducer in
13559
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13560
+ * renders a form section. This cap answers ONE question with ONE read and
13561
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13562
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13563
+ * `reboot`, the other pure-RPC device-native cap.
13564
+ *
13565
+ * ## Absent, and the difference between "no page" and "we cannot say"
13566
+ *
13567
+ * The two are answered at DIFFERENT layers, on purpose:
13568
+ *
13569
+ * - **"We cannot say"** → the provider never registers the cap for that
13570
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13571
+ * fan are reached only through a vendor cloud; there is no address to hand
13572
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13573
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13574
+ * behind it. None of them register, so `deviceManager.getBindings` never
13575
+ * lists the cap and no surface asks.
13576
+ * - **"This device has no page, and I know that"** → the provider registers
13577
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13578
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13579
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13580
+ * transport, a Home Assistant broker authenticated by supervisor token
13581
+ * (which carries no `baseUrl` at all).
13582
+ *
13583
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13584
+ * button, and D62 is the same rule from the other side: an off switch is
13585
+ * reported off, never made to look broken. There is no third state where the
13586
+ * UI renders a disabled button "because the device might have a page".
13587
+ *
13588
+ * ## The URL never carries credentials
13589
+ *
13590
+ * Not in userinfo, not in a query string. Every provider builds through
13591
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13592
+ * scheme and path as separate arguments — there is no parameter a secret could
13593
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13594
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13595
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13596
+ * keeps providers from hand-rolling one anyway.
13597
+ *
13598
+ * This matters here more than anywhere else in the repo, because every provider
13599
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13600
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13601
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13602
+ * camera's own page will ask for its own login. That is correct, and pre-
13603
+ * filling it is the operator's business, not ours.
13604
+ *
13605
+ * ## It is a LAN fact
13606
+ *
13607
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13608
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13609
+ * as a link the operator's own browser follows, on the operator's own network,
13610
+ * or renders nothing.
13611
+ */
13612
+ /**
13613
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13614
+ * before clicking whether he is about to land on a camera's own web server or
13615
+ * inside Home Assistant.
13616
+ */
13617
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13618
+ var DeviceAdminLinkSchema = object({
13619
+ /**
13620
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13621
+ * free of userinfo and of any credential-shaped query key.
13622
+ */
13623
+ url: string(),
13624
+ /**
13625
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13626
+ * The PROVIDER names it, because only the provider knows what the page is;
13627
+ * a UI that invented the label from the addon id would call the Home
13628
+ * Assistant device page "Provider Homeassistant".
13629
+ */
13630
+ label: string(),
13631
+ target: AdminLinkTargetEnum,
13632
+ /**
13633
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13634
+ * an operator can see WHERE the button goes before he follows it. Redundant
13635
+ * with `url` by construction; carried separately so no surface has to parse
13636
+ * a URL to show it.
13637
+ */
13638
+ host: string()
13639
+ });
13640
+ var deviceAdminLinkCapability = {
13641
+ name: "device-admin-link",
13642
+ scope: "device",
13643
+ deviceNative: true,
13644
+ mode: "singleton",
13645
+ methods: {
13646
+ /**
13647
+ * The device's management page, or `null` when this device has none.
13648
+ *
13649
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13650
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13651
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13652
+ * The URL is also a statement about the LAN, which a household member with
13653
+ * a `view` grant on a light has no reason to be handed.
13654
+ *
13655
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13656
+ * without the right loses the query and draws nothing, which is the same
13657
+ * thing a device with no page draws. There is no path on which a button
13658
+ * appears and then fails — the D403 failure mode, from the other end.
13659
+ */
13660
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13661
+ };
13662
+ /**
13663
+ * Query keys that may never appear on an admin link. A credential smuggled as
13664
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13665
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13666
+ * identically.
13667
+ */
13668
+ var CREDENTIAL_QUERY_KEYS = [
13669
+ "password",
13670
+ "passwd",
13671
+ "pwd",
13672
+ "pass",
13673
+ "user",
13674
+ "username",
13675
+ "usr",
13676
+ "login",
13677
+ "token",
13678
+ "access_token",
13679
+ "auth",
13680
+ "authorization",
13681
+ "apikey",
13682
+ "api_key",
13683
+ "secret",
13684
+ "credential",
13685
+ "credentials",
13686
+ "session",
13687
+ "sessionid",
13688
+ "key"
13689
+ ];
13690
+ /**
13691
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13692
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13693
+ * structurally identical on purpose: a URL this function returns is a URL that
13694
+ * guard would pass.
13695
+ */
13696
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13697
+ /** A host that is safe to place in an authority component verbatim. */
13698
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13699
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13700
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13701
+ function defaultPortFor(scheme) {
13702
+ return scheme === "https" ? 443 : 80;
13703
+ }
13704
+ /**
13705
+ * Build the admin-page URL, or refuse by name.
13706
+ *
13707
+ * The refusal is never thrown: a provider answering "no page for this device"
13708
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13709
+ * a missing host on one device into a failed query for the whole surface.
13710
+ */
13711
+ function buildDeviceAdminUrl(parts) {
13712
+ const host = parts.host.trim();
13713
+ if (host.length === 0) return {
13714
+ ok: false,
13715
+ reason: "empty-host"
13716
+ };
13717
+ if (host.includes("@")) return {
13718
+ ok: false,
13719
+ reason: "host-carries-userinfo"
13720
+ };
13721
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13722
+ ok: false,
13723
+ reason: "host-not-plain"
13724
+ };
13725
+ if (parts.port !== null) {
13726
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13727
+ ok: false,
13728
+ reason: "port-out-of-range"
13729
+ };
13730
+ }
13731
+ if (!parts.path.startsWith("/")) return {
13732
+ ok: false,
13733
+ reason: "path-not-absolute"
13734
+ };
13735
+ const query = parts.query ?? {};
13736
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13737
+ ok: false,
13738
+ reason: "credential-query-key"
13739
+ };
13740
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13741
+ const search = new URLSearchParams();
13742
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13743
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13744
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13745
+ if (CREDENTIAL_URL.test(url)) return {
13746
+ ok: false,
13747
+ reason: "userinfo-in-result"
13748
+ };
13749
+ return {
13750
+ ok: true,
13751
+ url
13752
+ };
13753
+ }
13754
+ /**
13490
13755
  * `device-export` — collection cap for addons that export camstack
13491
13756
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
13492
13757
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -20253,6 +20518,7 @@ var MediaFileKindEnum = _enum([
20253
20518
  "keyFrameSmall",
20254
20519
  "thumbnailSmall",
20255
20520
  "confirmationCrop",
20521
+ "debugCrop",
20256
20522
  "sceneFrame"
20257
20523
  ]);
20258
20524
  /**
@@ -21332,7 +21598,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21332
21598
  ownerType: EventOwnerTypeSchema,
21333
21599
  eventId: number().int(),
21334
21600
  deviceId: number()
21335
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21601
+ }), array(MediaFileInfoSchema).readonly()), method(object({
21602
+ deviceId: number(),
21603
+ ownerKind: MediaPresenceOwnerKindSchema,
21604
+ ownerIds: array(string()).max(5e3)
21605
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
21336
21606
  kind: "mutation",
21337
21607
  auth: "admin"
21338
21608
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -21917,6 +22187,18 @@ var RunnerCameraConfigSchema = object({
21917
22187
  motionStreamId: string(),
21918
22188
  detectionStreamId: string(),
21919
22189
  /**
22190
+ * Which rung this camera's audio analysis listens on — `auto` (the lowest
22191
+ * AVAILABLE native rung) or an explicit one.
22192
+ *
22193
+ * Not consumed by the runner: carried here so it travels with the camera's
22194
+ * config the way `audioMotionWindowMs` does, and so the orchestrator's
22195
+ * watchdog resolves the same stream the audio subscription did. OPTIONAL
22196
+ * with no default — absent means `auto`, which is what every camera written
22197
+ * before this field had, and an absent optional adds nothing to the
22198
+ * generated cap-input defaults (D537).
22199
+ */
22200
+ audioStreamPick: AudioStreamPickSchema.optional(),
22201
+ /**
21920
22202
  * Motion sources that can activate the pipeline. At least one must
21921
22203
  * be present. Multiple sources OR-ed: any one firing arms the
21922
22204
  * cooldown timer and transitions watching → active. The same
@@ -22987,10 +23269,9 @@ var ServerBootModeSchema = _enum([
22987
23269
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
22988
23270
  * - `pending-restart` — a version is staged and the node has NOT yet
22989
23271
  * restarted onto it (still running the OLD version).
22990
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
22991
- * (it is the active probation boot) and is waiting to confirm boot-health.
22992
- * Apply/rollback are refused in this state and the node must NOT be
22993
- * manually restarted, or the probation boot auto-rolls-back.
23272
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
23273
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
23274
+ * no provider emits it.
22994
23275
  */
22995
23276
  var ServerUpdateStateSchema = _enum([
22996
23277
  "idle",
@@ -23079,12 +23360,35 @@ var ServerRollbackInfoSchema = object({
23079
23360
  var ServerPackageStatusSchema = object({
23080
23361
  /** Root package name (`@camstack/server` on the hub). */
23081
23362
  packageName: string(),
23082
- /** Version of the code the running process ACTUALLY loaded. */
23363
+ /**
23364
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
23365
+ * package.json of the copy the code was loaded from, and never re-read. A
23366
+ * copy replaced on disk under a live process must not move this number;
23367
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
23368
+ * was a lazy disk read that would have followed such a swap silently.
23369
+ */
23083
23370
  runningVersion: string().nullable(),
23084
23371
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
23085
23372
  nodeRuntimeVersion: string().nullable(),
23086
23373
  /** Active data-dir root version; null when booted from seed/workspace. */
23087
23374
  activeVersion: string().nullable(),
23375
+ /**
23376
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
23377
+ * what the next boot would load if nothing is staged. Normally equal to
23378
+ * `runningVersion`; differs only when the copy on disk changed under the
23379
+ * live process. `null` when there is no data-root copy (baked seed or
23380
+ * workspace boot). Optional for version skew: an older provider omits it.
23381
+ */
23382
+ installedVersion: string().nullable().optional(),
23383
+ /**
23384
+ * True when the code that would run after a restart is NOT the code running
23385
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
23386
+ * from `runningVersion`. Derived on every read, never stored. This is the
23387
+ * one field that lets an operator or a script say "1.2.353 is installed and
23388
+ * will run after a restart" instead of reading it off a number that means
23389
+ * "installed". Optional for version skew.
23390
+ */
23391
+ restartRequired: boolean().optional(),
23088
23392
  /** N-1 version kept for rollback; null when no previous version exists. */
23089
23393
  previousVersion: string().nullable(),
23090
23394
  /** Version of the immutable baked seed closure (image fallback). */
@@ -23119,19 +23423,71 @@ var ServerPackageStatusSchema = object({
23119
23423
  });
23120
23424
  var ServerUpdateCheckResultSchema = object({
23121
23425
  packageName: string(),
23426
+ /** Same meaning as on the status payload: what THIS process loaded. */
23122
23427
  runningVersion: string().nullable(),
23123
23428
  latestVersion: string().nullable(),
23429
+ /**
23430
+ * `latestVersion` is newer than `runningVersion` AND is not the version
23431
+ * already staged on this node. A staged-and-waiting version is not "an
23432
+ * update available" — applying it again is refused (`already-staged`), and
23433
+ * a button that announced "updating…" on this boolean then got refused
23434
+ * would be lying. It is reported through `pendingVersion` instead.
23435
+ */
23124
23436
  updateAvailable: boolean(),
23125
23437
  checkedAtMs: number(),
23126
23438
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
23127
- error: string().nullable()
23439
+ error: string().nullable(),
23440
+ /**
23441
+ * Version staged on this node and awaiting its restart, when one is. Before
23442
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
23443
+ * node whose staged closure had never booted, and the caller had no way to
23444
+ * see the difference. Optional for version skew.
23445
+ */
23446
+ pendingVersion: string().nullable().optional(),
23447
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
23448
+ restartRequired: boolean().optional()
23128
23449
  });
23450
+ /**
23451
+ * What an action DID — the discriminant `accepted` cannot carry. Before
23452
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
23453
+ * where nothing was wrong (already current, already staged) and for a staging
23454
+ * install that died on `npm error code ECONNRESET`; the only difference was
23455
+ * prose in `message`, and an operator restarted a container on the strength of
23456
+ * it, booting the previous version.
23457
+ *
23458
+ * - `staged` — the closure is installed and validated; the node
23459
+ * restarts to apply it (`restarting:true`). NOTHING
23460
+ * runs the new code until that restart completes.
23461
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
23462
+ * - `already-current` — the target IS the running version; nothing done.
23463
+ * - `refused` — dropped by a gate; `reason` names which.
23464
+ * - `failed` — attempted and did not land; `error` says why.
23465
+ */
23466
+ var ServerUpdateOutcomeSchema = _enum([
23467
+ "staged",
23468
+ "restart-scheduled",
23469
+ "already-current",
23470
+ "refused",
23471
+ "failed"
23472
+ ]);
23129
23473
  var ServerUpdateActionResultSchema = object({
23130
23474
  accepted: boolean(),
23131
23475
  targetVersion: string().nullable(),
23132
23476
  /** True when a graceful restart was scheduled to apply the change. */
23133
23477
  restarting: boolean(),
23134
- message: string()
23478
+ message: string(),
23479
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
23480
+ outcome: ServerUpdateOutcomeSchema.optional(),
23481
+ /**
23482
+ * Stable, greppable cause for `refused` / `failed` (the engine's
23483
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
23484
+ */
23485
+ reason: string().nullable().optional(),
23486
+ /**
23487
+ * The failure itself, verbatim enough to act on (the npm output for a
23488
+ * staging install). Non-null only for `failed`.
23489
+ */
23490
+ error: string().nullable().optional()
23135
23491
  });
23136
23492
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
23137
23493
  kind: "mutation",
@@ -24240,6 +24596,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
24240
24596
  kind: "mutation",
24241
24597
  auth: "admin"
24242
24598
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
24599
+ _enum([
24600
+ "queue-full",
24601
+ "camera-backoff",
24602
+ "sleeping",
24603
+ "camera-refused",
24604
+ "no-keyframe",
24605
+ "no-catalog-row",
24606
+ "unsupported",
24607
+ "unknown-device",
24608
+ "uid-missing"
24609
+ ]);
24610
+ _enum(["deferred", "final"]);
24243
24611
  var ClipSchema = object({
24244
24612
  /** Opaque, provider-namespaced id. The default provider encodes the time
24245
24613
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -24314,29 +24682,455 @@ var ClipSchema = object({
24314
24682
  holes: array(object({
24315
24683
  startMs: number(),
24316
24684
  endMs: number()
24317
- })).optional()
24685
+ })).optional(),
24686
+ /**
24687
+ * A URL that MAY produce this clip's still, asked only for rows actually on
24688
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
24689
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
24690
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
24691
+ * — a surface latches that refusal to the instant rather than retrying.
24692
+ *
24693
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
24694
+ */
24695
+ thumbnailMint: string().optional(),
24696
+ /**
24697
+ * Why no still will be produced for this clip RIGHT NOW — set when the
24698
+ * provider already knows, so the surface draws the glyph and the reason
24699
+ * instead of firing a mint that cannot succeed.
24700
+ *
24701
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
24702
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
24703
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
24704
+ * inside no file is the measured case).
24705
+ * `unsupported` — this source cannot mint stills at all.
24706
+ */
24707
+ thumbnailUnavailable: object({ reason: _enum([
24708
+ "sleeping",
24709
+ "camera-refused",
24710
+ "no-keyframe",
24711
+ "unsupported"
24712
+ ]) }).optional(),
24713
+ /**
24714
+ * When the catalog this row came from was last CONFIRMED against the device.
24715
+ * Absent means "this row was read live". A persisted catalog served while a
24716
+ * camera sleeps carries the age it really has — a cached list is never drawn
24717
+ * as current (D549 13).
24718
+ */
24719
+ catalogAsOf: number().optional(),
24720
+ /**
24721
+ * The camera's OWN type strings for this clip, all of them, unmapped
24722
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
24723
+ * because a firmware inventing a type must not vanish: the mapping into our
24724
+ * filter vocabulary is lossy on purpose and this is the lossless copy
24725
+ * (D549 20).
24726
+ */
24727
+ nativeTypes: array(string()).optional(),
24728
+ /**
24729
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
24730
+ * travels BESIDE a class, never instead of one, and the class filter ignores
24731
+ * it: "a person crossed a line" is still a person (D549 20).
24732
+ */
24733
+ behaviours: array(string()).optional(),
24734
+ /**
24735
+ * The same recording as two files — the sub twin (what the row and its
24736
+ * thumbnail are) and its main twin, matched at listing time so a quality
24737
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
24738
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
24739
+ */
24740
+ streams: object({
24741
+ sub: object({
24742
+ id: string(),
24743
+ bytes: number().optional()
24744
+ }).optional(),
24745
+ main: object({
24746
+ id: string(),
24747
+ bytes: number().optional()
24748
+ }).optional()
24749
+ }).optional(),
24750
+ /** The camera says it holds a sub-stream copy of this recording. */
24751
+ supportSub: boolean().optional(),
24752
+ /**
24753
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
24754
+ * IS a file is playable, and only a source that lists EVENTS can produce a
24755
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
24756
+ * fell inside no file). Such a row is shown, never dropped and never offered
24757
+ * as playable-then-failing (D549 19).
24758
+ */
24759
+ playable: boolean().optional(),
24760
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
24761
+ unplayableReason: string().optional(),
24762
+ /**
24763
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
24764
+ * is not its end.
24765
+ *
24766
+ * Absent — the common case — means the row is closed and its `endMs` is the
24767
+ * end of the recording. Present and `true` means the source told us the file
24768
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
24769
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
24770
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
24771
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
24772
+ * on growing. A surface that drew 29 s there was lying about a clip that
24773
+ * plays for minutes, on the one row the operator looks at first.
24774
+ *
24775
+ * `endMs` is deliberately still a number: it is the best bound anything has
24776
+ * for a mint window or a byte fetch, and every consumer already requires it.
24777
+ * This flag says what it is WORTH, and a duration is not drawn from it.
24778
+ */
24779
+ inProgress: boolean().optional()
24318
24780
  });
24319
24781
  var ClipPlaybackSchema = object({
24320
- /** HLS master URL through the hub data-plane (Range + token in path). */
24782
+ /**
24783
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
24784
+ * says what kind — an HLS master playlist for a recording-derived clip, a
24785
+ * progressive MP4 for a native file the vendor addon muxed and serves with
24786
+ * `Range`. The URL is NOT a credential: both planes are registered
24787
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
24788
+ * the session cookie, so nothing is appended to it (D549 22).
24789
+ */
24321
24790
  playbackUrl: string(),
24791
+ /**
24792
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
24793
+ * existing consumer already assumes. A player that branches on this is the
24794
+ * one change a native clip needs; a player that ignores it will hand an MP4
24795
+ * to hls.js and fail parsing it as a manifest.
24796
+ */
24797
+ format: _enum(["hls", "mp4"]).optional(),
24798
+ /**
24799
+ * Which twin was actually served. A clip that holds only one stream answers
24800
+ * with the one it has, and the surface SAYS so — a missing main twin is
24801
+ * never served silently as if it were the asked-for quality (D549 15).
24802
+ */
24803
+ served: CamProfileSchema.optional(),
24322
24804
  /** Optional LAN/remote alternates for the same clip. */
24323
24805
  playbackEndpoints: array(string()).optional(),
24324
24806
  token: string().optional()
24325
24807
  });
24808
+ /**
24809
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
24810
+ *
24811
+ * `ok` is the only state whose clip list may be read as complete. The other
24812
+ * three exist because an empty list from a sleeping camera reads as "this
24813
+ * camera has no recordings", which is the defect this whole line of work is
24814
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
24815
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
24816
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
24817
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
24818
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
24819
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
24820
+ * files on both streams and both filters, with 177 GB free). That last one is
24821
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
24822
+ * which days it asked for and got nothing — "no clips" would be a lie about a
24823
+ * card that is full of them.
24824
+ */
24825
+ /**
24826
+ * The operator's authorisation to WAKE a sleeping camera for one clip read.
24827
+ *
24828
+ * ONE value, absent by default, and it is a `force`-shaped operator signal in
24829
+ * exactly the sense `snapshot-wake-gate.ts` uses the word: *"`snapshot.getSnapshot`'s
24830
+ * `force` flag, and nothing else… a background caller must never set it…
24831
+ * Stale but honest beats woken"*. A scheduler, a retry, a reconcile and a
24832
+ * prefetch never set it; a surface sets it only behind the same confirm the
24833
+ * "Wake and refresh" gesture uses, and it is refused below 15 % battery
24834
+ * exactly as that gesture is.
24835
+ *
24836
+ * The gate is decided BEFORE `getApi()`, because on UDP the login IS the wake
24837
+ * (D549) — a read that opened a session and then checked would have woken the
24838
+ * camera to find out it was not allowed to.
24839
+ *
24840
+ * **One authorised yes is one wake.** `next-natural` — take the clip the next
24841
+ * time the camera is awake for its own reasons — is deliberately not a member:
24842
+ * it was proposed, it is free on the battery, and the operator declined it on
24843
+ * 2026-09-20 (*"Quando un export viene richiesto si sveglia la camera."*,
24844
+ * D558 "Considered and not taken").
24845
+ */
24846
+ var ClipWakeSchema = _enum(["authorised"]);
24847
+ /**
24848
+ * A clip's finished bytes, inline — the twin of `recordingExport.readExportBytes`.
24849
+ *
24850
+ * `bytes` is the DECODED length, so nobody infers it from the base64 length,
24851
+ * and `served` says which twin the caller actually got.
24852
+ */
24853
+ var ClipBytesSchema = object({
24854
+ base64: string(),
24855
+ contentType: string(),
24856
+ /** Suggested filename, extension included. */
24857
+ name: string(),
24858
+ bytes: number().int().nonnegative(),
24859
+ /**
24860
+ * Which twin was actually served — the same contract
24861
+ * {@link ClipPlaybackSchema.served} carries, and REQUIRED here because the
24862
+ * export record persists it: a row read a week later must say the same thing
24863
+ * the panel said at the moment of the tap. A missing main twin is never
24864
+ * served silently as if it were the asked-for quality (D549 15).
24865
+ */
24866
+ served: CamProfileSchema,
24867
+ /**
24868
+ * The DECODED duration of the delivered file, when the fetch measured one.
24869
+ *
24870
+ * A clip fetch verifies its own completion against the catalog row and
24871
+ * retries a materially short pass (D568); this is that measurement, carried
24872
+ * so a consumer can say the same thing rather than re-deriving it. Absent
24873
+ * when the producer did not measure — never zero, which would say the file
24874
+ * is empty.
24875
+ */
24876
+ durationMs: number().positive().optional()
24877
+ });
24878
+ /**
24879
+ * Where a clip's STREAM can be dialled (D597) — the answer to
24880
+ * {@link videoclipsCapability.methods.dialClipStream}.
24881
+ *
24882
+ * `url` is plain `http://` on the provider's own token-gated listener, one
24883
+ * shot, short-lived. It is NOT the hub-proxied `/addon/<id>/clip-stream/…`
24884
+ * path: that plane takes only a user credential, which an addon does not
24885
+ * hold, and the framework offers no addon→addon byte transport — the one
24886
+ * precedent is the broker's own restream URLs (`getStreamWithCodec`), which
24887
+ * a consumer dials directly, and this is that precedent one addon over.
24888
+ */
24889
+ /**
24890
+ * The audio a clip stream can be asked for, and the audio it carries.
24891
+ *
24892
+ * The camera stores AAC and no WebRTC session speaks it, so a stream that
24893
+ * carries sound carries it TRANSCODED, muxed as a second track beside the
24894
+ * video by the one ffmpeg the provider already runs (D600). `opus` is
24895
+ * 48 kHz mono, `pcmu` is 8 kHz µ-law — the two codecs a broker session
24896
+ * negotiates — and `none` is a stream asked for, or served, without sound.
24897
+ */
24898
+ var ClipStreamAudioSchema = _enum([
24899
+ "opus",
24900
+ "pcmu",
24901
+ "none"
24902
+ ]);
24903
+ /**
24904
+ * Why a stream that was ASKED for sound is not carrying it (D600).
24905
+ *
24906
+ * Only ever set beside `servedAudio: 'none'` after a non-`none` ask. Absent
24907
+ * means the ask was honoured, or there was no ask.
24908
+ *
24909
+ * - `not-requested` — the caller asked for `none`.
24910
+ * - `unsupported-ask` — the provider cannot produce that codec.
24911
+ *
24912
+ * Whether the RECORDING has sound at all is not knowable here: the camera
24913
+ * declares no audio track before its first frame arrives. That answer is in
24914
+ * the stream itself — an fMP4 with no audio track — and the broker reads it
24915
+ * there. A dial that says `opus` promises the codec, never the content.
24916
+ */
24917
+ var ClipStreamAudioReasonSchema = _enum(["not-requested", "unsupported-ask"]);
24918
+ var ClipStreamDialSchema = object({
24919
+ url: string().min(1),
24920
+ /** After this the token is gone and the URL answers 404 `dial-expired`. */
24921
+ expiresAtMs: number(),
24922
+ /** What the stream IS — a consumer that cannot honour it must not dial. */
24923
+ transport: literal("forward-only"),
24924
+ /**
24925
+ * Which twin the stream ACTUALLY carries — not what was asked for.
24926
+ *
24927
+ * The stream serves every profile (D599), and a recording whose main twin
24928
+ * does not exist is streamed as its sub copy, which this field reports as
24929
+ * `low`. That is the whole reason the dial no longer refuses `high`:
24930
+ * stating what was served is what makes accepting honest, where a silent
24931
+ * substitution would be the 33x lie D576 forbids. Stated rather than
24932
+ * assumed, the way {@link ClipPlaybackSchema.served} is (D549 15).
24933
+ */
24934
+ served: CamProfileSchema,
24935
+ /**
24936
+ * Which audio the stream will ACTUALLY carry — `served`'s companion for
24937
+ * sound, and the same reasoning (D599, D600).
24938
+ *
24939
+ * The broker asks for the codec its WebRTC session negotiated, and the
24940
+ * provider either muxes that codec or carries none. Stating it is what
24941
+ * lets the broker wire the matching sink before the first byte, and what
24942
+ * keeps a session from being handed a codec it cannot send — the audio
24943
+ * shape of the substitution `served` exists to prevent.
24944
+ *
24945
+ * It is a promise about the CODEC, not about the content: a recording with
24946
+ * no microphone track answers `opus` here and then arrives with no audio
24947
+ * track at all. That second answer is in the stream, and the broker reads
24948
+ * it from the `moov`.
24949
+ */
24950
+ servedAudio: ClipStreamAudioSchema,
24951
+ /** Why `servedAudio` is `none` although sound was asked for. */
24952
+ audioReason: ClipStreamAudioReasonSchema.optional()
24953
+ });
24954
+ var ClipSourceAvailabilitySchema = object({
24955
+ state: _enum([
24956
+ "ok",
24957
+ "sleeping",
24958
+ "unreachable",
24959
+ "no-storage",
24960
+ "index-empty"
24961
+ ]),
24962
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
24963
+ reason: string().optional(),
24964
+ /** When this source's catalog was last CONFIRMED. A cached list is never
24965
+ * drawn as current: the surface shows the age whenever it is older than the
24966
+ * refresh interval. */
24967
+ catalogAsOf: number().optional()
24968
+ });
24969
+ /**
24970
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
24971
+ * every clip id from it is prefixed with (`analytics`,
24972
+ * `native:reolink:onboard`, `hksv`, …).
24973
+ *
24974
+ * A provider lists the sources IT serves for that device, and answers for each
24975
+ * of them whether it can answer at all. A provider with nothing to offer on a
24976
+ * camera returns `[]` — it is not that camera's business.
24977
+ */
24978
+ var ClipSourceSchema = object({
24979
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
24980
+ * of every clip id it mints. `getClipPlayback` routes on it. */
24981
+ source: string(),
24982
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
24983
+ label: string(),
24984
+ /**
24985
+ * The addon that SERVES this row.
24986
+ *
24987
+ * A surface cannot otherwise resolve a source to who answers it, and the
24988
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
24989
+ * widget — is a second authority on provider identity living in the one
24990
+ * package with no business knowing it, wrong the day a third source appears
24991
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
24992
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
24993
+ * served a hub child two of them until the two views were measured to be one
24994
+ * store read twice (D555) and then read ONE way (D565).
24995
+ *
24996
+ * Optional for version skew only. The collection dispatcher stamps it from
24997
+ * the registry, so a row that travelled through the fan-out carries the
24998
+ * authoritative id whatever the provider filled in.
24999
+ */
25000
+ addonId: string().optional(),
25001
+ availability: ClipSourceAvailabilitySchema
25002
+ });
24326
25003
  DeviceType.Camera, method(object({
24327
25004
  deviceId: number(),
24328
25005
  since: number(),
24329
25006
  until: number(),
24330
- limit: number().int().positive().optional()
25007
+ limit: number().int().positive().optional(),
25008
+ /**
25009
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25010
+ * carries, never a source id and never a list. **Required** (D554 amended).
25011
+ *
25012
+ * A provider the device is not bound to is refused by name rather than
25013
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25014
+ *
25015
+ * It was optional, documented as "absent means the device's BOUND
25016
+ * provider, which is CamStack on every camera". No code implemented
25017
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25018
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25019
+ * answered SIX rows, three from each source, merged newest-first:
25020
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25021
+ * so absence bought the union this method exists not to be, and
25022
+ * `limit` meant `limit × sources`.
25023
+ *
25024
+ * There is nothing to restore the default to. `getBindings` answers a
25025
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25026
+ * `setWrapperActive` — the singleton authority that could name one —
25027
+ * throws for a collection cap by design. Naming CamStack here instead
25028
+ * would privilege one addon by id inside a surface whose premise is
25029
+ * that sources are peers, and would be wrong on the first camera with
25030
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25031
+ * generated types make it unomittable rather than merely discouraged.
25032
+ *
25033
+ * The default belongs to the SURFACE, which has the `listSources` rows
25034
+ * and can say which one it picked (D569 § 1–3; the viewer already
25035
+ * always sends this).
25036
+ *
25037
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
25038
+ * assumed the answer was a fan-out over everything a camera has. The
25039
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25040
+ * solo"* — so the list is asked of one provider at a time and there is
25041
+ * nothing to filter out of it.
25042
+ */
25043
+ provider: string().min(1)
24331
25044
  }), array(ClipSchema).readonly(), {
24332
25045
  kind: "query",
24333
- auth: "admin"
25046
+ auth: "protected"
25047
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
25048
+ kind: "query",
25049
+ auth: "protected"
24334
25050
  }), method(object({
24335
25051
  deviceId: number(),
24336
- clipId: string()
25052
+ clipId: string(),
25053
+ /**
25054
+ * Which twin to serve, on the ONE quality scale the system already has
25055
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
25056
+ * twin; both ids are already on the row so this never re-searches the
25057
+ * camera. Absent means the provider's own default (the sub file, which
25058
+ * every source is measured to hold).
25059
+ *
25060
+ * `auto` is deliberately NOT accepted here: a stored file has no
25061
+ * broker session, so the adaptive tier cannot be resolved for it. The
25062
+ * viewer resolves `auto` to a profile the same way live does, before
25063
+ * it calls (D549 15).
25064
+ */
25065
+ profile: CamProfileSchema.optional()
24337
25066
  }), ClipPlaybackSchema, {
24338
25067
  kind: "query",
24339
- auth: "admin"
25068
+ auth: "protected"
25069
+ }), optionalMethod(object({
25070
+ deviceId: number(),
25071
+ clipId: string().min(1),
25072
+ /**
25073
+ * WHICH provider holds the bytes — the `addonId` a
25074
+ * {@link ClipSourceSchema} row carries. **Required**, for the reason
25075
+ * `listClips` states one method above and for a second one this method
25076
+ * learned the hard way.
25077
+ *
25078
+ * The caller is the stream broker, and the broker lives in
25079
+ * `addon-pipeline` — the same addon that registers the RECORDER's
25080
+ * videoclips provider. With no provider named, the in-process
25081
+ * resolution handed it its own sibling, which implements this method
25082
+ * solely to explain that an analytics visit has no file behind it. So
25083
+ * every onboard clip answered with that sentence, a flash of video and
25084
+ * nothing more, while the SAME call from outside the addon reached the
25085
+ * right provider and returned the bytes (measured on 3628,
25086
+ * 2026-09-21).
25087
+ */
25088
+ provider: string().min(1),
25089
+ /**
25090
+ * Which twin to fetch, on the mapping D549 15 already fixed:
25091
+ * `low | mid` → the sub file, `high` → the main twin. `auto` is not
25092
+ * accepted here for the same reason it is not accepted by
25093
+ * `getClipPlayback` — a stored file has no broker session, so the
25094
+ * adaptive tier cannot be resolved for it.
25095
+ */
25096
+ profile: CamProfileSchema.optional(),
25097
+ /**
25098
+ * The CALLER's byte bound, so an over-size clip is refused before it is
25099
+ * read and encoded rather than after. Capped by
25100
+ * {@link VIDEOCLIPS_MAX_READ_BYTES} whatever is passed; absent means
25101
+ * that ceiling.
25102
+ */
25103
+ maxBytes: number().int().positive().optional(),
25104
+ /**
25105
+ * The operator's authorisation to wake a sleeping camera for this
25106
+ * read. Absent — the default — means a sleeping standalone battery
25107
+ * camera is REFUSED by name, before any session is opened.
25108
+ */
25109
+ wake: ClipWakeSchema.optional()
25110
+ }), ClipBytesSchema, {
25111
+ kind: "query",
25112
+ auth: "protected"
25113
+ }), optionalMethod(object({
25114
+ deviceId: number(),
25115
+ clipId: string().min(1),
25116
+ /** WHICH provider holds the camera — see `readClipBytes.provider`. */
25117
+ provider: string().min(1),
25118
+ /** Which twin — `high` → the main file, everything else the sub (D599). */
25119
+ profile: CamProfileSchema.optional(),
25120
+ /**
25121
+ * Which audio codec the caller's session negotiated, so the provider
25122
+ * can mux it (D600). The ask travels on the DIAL because the mux is
25123
+ * spawned once, before the first byte, and cannot gain a track later.
25124
+ *
25125
+ * ABSENT IS `none`, never a guess: a caller that does not state an
25126
+ * ask is served the video-only stream that shipped first, so a broker
25127
+ * older than this field keeps working unchanged (D382 — an unstated
25128
+ * ask is zero).
25129
+ */
25130
+ audio: ClipStreamAudioSchema.optional()
25131
+ }), ClipStreamDialSchema, {
25132
+ kind: "query",
25133
+ auth: "protected"
24340
25134
  });
24341
25135
  /**
24342
25136
  * Optional client-side hints sent at session creation to help the provider
@@ -24408,6 +25202,104 @@ var WebrtcStreamChoiceSchema = object({
24408
25202
  /** Bitrate in kbps (null for adaptive). */
24409
25203
  bitrateKbps: number().nullable()
24410
25204
  });
25205
+ /**
25206
+ * The last move the adaptive ladder made on this session, and the numbers it
25207
+ * rested on. Mirrors `LadderDecision` in the stream-broker's
25208
+ * `adaptive-controller.ts` — the log line's evidence, made readable.
25209
+ *
25210
+ * Nullability is load-bearing: `lossRatio: null` means the loss tracker had
25211
+ * no window to report, which is NOT a link with zero loss.
25212
+ */
25213
+ var WebrtcLadderDecisionSchema = object({
25214
+ /** `loss` — RTCP / client-reported packet loss; `stall` — the picture
25215
+ * stopped moving; `healthy` — a promotion. */
25216
+ cause: _enum([
25217
+ "loss",
25218
+ "stall",
25219
+ "healthy"
25220
+ ]),
25221
+ /** The rung the ladder moved TO. Null for a move that changed no rung
25222
+ * (an MTU reduction, a forced transcode-down at the bottom). */
25223
+ tier: CamProfileSchema.nullable(),
25224
+ /** Whether that move forced the egress transcode-down rung. */
25225
+ forcedTranscodeDown: boolean(),
25226
+ /** Loss ratio over the tracker's window at the moment of the decision. */
25227
+ lossRatio: number().nullable(),
25228
+ /** The client's reported downlink at that moment, in kbps. */
25229
+ clientKbps: number().nullable(),
25230
+ /** How long the stall detector had been seeing nothing arrive. */
25231
+ stalledForMs: number().nullable(),
25232
+ /** Whether the broker WAS sending while that was measured — the half that
25233
+ * separates "the camera stopped" from "the link stopped". */
25234
+ serverSending: boolean().nullable(),
25235
+ /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25236
+ * must not read like one from this second. */
25237
+ at: number()
25238
+ });
25239
+ /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25240
+ var WebrtcTierFailureSchema = object({
25241
+ tier: CamProfileSchema,
25242
+ failures: number().int().nonnegative()
25243
+ });
25244
+ /**
25245
+ * What the SERVER decided for one WebRTC session — the half the client cannot
25246
+ * measure. See `getSessionDebug`.
25247
+ */
25248
+ var WebrtcSessionDebugSchema = object({
25249
+ sessionId: string(),
25250
+ /** Whether the session still exists here. Everything below is null when
25251
+ * it does not — a dead session has no measurements, not zero ones. */
25252
+ alive: boolean(),
25253
+ /** Epoch ms this snapshot was taken, so the overlay can show its age. */
25254
+ observedAt: number(),
25255
+ /** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
25256
+ brokerId: string().nullable(),
25257
+ camStreamId: string().nullable(),
25258
+ /** `derived` = this hub BUILT the rung being served (a transcode/scale of
25259
+ * another stream); `native` = the camera's own encode. */
25260
+ sourceKind: _enum([
25261
+ "native",
25262
+ "derived",
25263
+ "unknown"
25264
+ ]),
25265
+ /** Whether the ladder is running at all (an `adaptive` target). */
25266
+ adaptive: boolean(),
25267
+ /** The rung `auto` ACTUALLY resolved to right now. Null when the broker
25268
+ * is bound to no profile slot (a manually-activated stream). */
25269
+ servedTier: CamProfileSchema.nullable(),
25270
+ /** Which node ingests the source. Null when unresolved. */
25271
+ ingestNodeId: string().nullable(),
25272
+ /** Whether that node is a remote one (the media crosses a node boundary
25273
+ * before it reaches this session). Null when unresolved. */
25274
+ ingestIsRemote: boolean().nullable(),
25275
+ /** Codec off the source, and the one negotiated with this client. */
25276
+ sourceCodec: string().nullable(),
25277
+ negotiatedCodec: string().nullable(),
25278
+ /** Whether the EGRESS leg re-encodes for this client or forwards the
25279
+ * source bytes untouched. */
25280
+ egressLeg: _enum([
25281
+ "copy",
25282
+ "transcode",
25283
+ "unknown"
25284
+ ]),
25285
+ /** What the broker itself reports about the rung it is serving. */
25286
+ brokerStatus: string().nullable(),
25287
+ brokerCodec: string().nullable(),
25288
+ brokerBitrateKbps: number().nullable(),
25289
+ brokerInputFps: number().nullable(),
25290
+ brokerUptimeMs: number().nullable(),
25291
+ /** RTP packets this session has sent — the server's own "am I sending?". */
25292
+ rtpPacketsSent: number().nullable(),
25293
+ /** D42: the track may be carrying the recorded feeder, not live. The ladder
25294
+ * is suspended there, so a frozen picture in `recorded` mode means
25295
+ * something entirely different. */
25296
+ trackMode: _enum(["live", "recorded"]).nullable(),
25297
+ lastDecision: WebrtcLadderDecisionSchema.nullable(),
25298
+ tierFailures: array(WebrtcTierFailureSchema),
25299
+ /** Successful promotions in this viewer session (drives the progressive
25300
+ * healthy timer). Null when no ladder is running. */
25301
+ upgradeCount: number().nullable()
25302
+ });
24411
25303
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
24412
25304
  deviceId: number().int().nonnegative(),
24413
25305
  target: WebrtcStreamTargetSchema,
@@ -24572,7 +25464,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
24572
25464
  * the same answer as "it ended": either way nothing is holding it up.
24573
25465
  */
24574
25466
  alive: boolean()
24575
- }));
25467
+ })), method(object({
25468
+ deviceId: number().int().nonnegative(),
25469
+ sessionId: string()
25470
+ }), WebrtcSessionDebugSchema, { kind: "query" });
24576
25471
  object({
24577
25472
  /** All accessory children of the parent. */
24578
25473
  childDeviceIds: array(number()).readonly(),
@@ -26398,6 +27293,346 @@ var dayNightCapability = {
26398
27293
  volatileStateFields: ["lastFetchedAt"]
26399
27294
  };
26400
27295
  /**
27296
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
27297
+ * writes to the CAMERA's own card, on the camera's own schedule.
27298
+ *
27299
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
27300
+ * footage ledger, our storage locations, our retention. This one has a
27301
+ * different authority — the camera's firmware — and per D62 it stores
27302
+ * nothing of its own. Every value here is read from the camera and every
27303
+ * write goes back to the camera; there is no CamStack-side mirror that
27304
+ * could disagree with the device.
27305
+ *
27306
+ * ## One shape, two firmwares
27307
+ *
27308
+ * Measured 2026-09-22 against the live fleet:
27309
+ *
27310
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
27311
+ * | --- | --- | --- |
27312
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
27313
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
27314
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
27315
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
27316
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
27317
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
27318
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
27319
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
27320
+ *
27321
+ * The two schedule models look different and are the same thing in
27322
+ * different coordinates: both answer "for this trigger, during which
27323
+ * weekly windows does the camera record". {@link RecordWindow} is that
27324
+ * question in one shape — Hikvision's ranges map straight onto it,
27325
+ * Reolink's mask expands into hour-aligned windows.
27326
+ *
27327
+ * ## Union, not intersection
27328
+ *
27329
+ * **The same fields exist on every camera.** What differs per device is
27330
+ * which VALUES that device accepts, and that is what {@link
27331
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
27332
+ * per field plus the schedule's own limits. A control a camera cannot
27333
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
27334
+ * dead: disabled must not look like broken.
27335
+ *
27336
+ * ## Refusal by name
27337
+ *
27338
+ * A write a camera cannot honour is refused with a sentence the operator
27339
+ * can read — never accepted and dropped. Both providers refuse through
27340
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
27341
+ * one test, not two hand-written vendor opinions.
27342
+ *
27343
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
27344
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
27345
+ * injected from `status`) reports the live values, and a single
27346
+ * `setSettings` mutation applies a partial change. No hand-written
27347
+ * settings-contribution methods.
27348
+ */
27349
+ /**
27350
+ * What makes the camera start recording during a window.
27351
+ *
27352
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
27353
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
27354
+ * object-class triggers are Reolink-only today and the smart-event ones
27355
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
27356
+ * firmwares measured — a camera that cannot record on a trigger simply
27357
+ * does not list it in `options.schedule.triggers`, and a window naming
27358
+ * it is REFUSED, not dropped.
27359
+ */
27360
+ var RecordTriggerSchema = _enum([
27361
+ "continuous",
27362
+ "motion",
27363
+ "person",
27364
+ "vehicle",
27365
+ "animal",
27366
+ "lineCrossing",
27367
+ "intrusion",
27368
+ "loitering",
27369
+ "alarmInput"
27370
+ ]);
27371
+ /**
27372
+ * One weekly recording window: "on `day`, from `startMinute` to
27373
+ * `endMinute`, record on `trigger`".
27374
+ *
27375
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
27376
+ * both firmwares enumerate). Minutes are local camera time since
27377
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
27378
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
27379
+ * collapsing it to 0 would turn a whole-day window into an empty one.
27380
+ */
27381
+ var RecordWindowSchema = object({
27382
+ trigger: RecordTriggerSchema,
27383
+ day: number().int().min(0).max(6),
27384
+ startMinute: number().int().min(0).max(1439),
27385
+ endMinute: number().int().min(1).max(1440)
27386
+ });
27387
+ /** Status of one physical volume, as the camera itself describes it. */
27388
+ var OnboardStorageVolumeSchema = object({
27389
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
27390
+ id: string(),
27391
+ /** The camera's own name for it, when it gives one (`hddName`). */
27392
+ label: string().optional(),
27393
+ status: _enum([
27394
+ "ok",
27395
+ "unformatted",
27396
+ "error",
27397
+ "offline",
27398
+ "unknown"
27399
+ ]),
27400
+ /**
27401
+ * Total size in MB, or **null when the camera did not say**.
27402
+ *
27403
+ * Never 0 for an unreadable value: a measurement that failed is not a
27404
+ * measurement (D393), and a card whose size is unknown must not be
27405
+ * rendered as a card of size zero.
27406
+ */
27407
+ capacityMb: number().nullable(),
27408
+ /**
27409
+ * Free space in MB, or null when unknown.
27410
+ *
27411
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
27412
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
27413
+ * card converges on once it has wrapped. At loop steady state the
27414
+ * number is identical whether the camera recorded yesterday or stopped
27415
+ * a month ago.
27416
+ */
27417
+ freeMb: number().nullable(),
27418
+ /** True when the camera reports the volume writable (`property` RW). */
27419
+ writable: boolean().optional()
27420
+ });
27421
+ /**
27422
+ * What the camera is doing with its own storage, right now.
27423
+ *
27424
+ * Every scalar is nullable and **null means the camera did not answer**,
27425
+ * never a default. A form that seeds `0` from an unanswered read invites
27426
+ * the operator to save that 0 back onto the camera.
27427
+ */
27428
+ var RecordingOnboardStatusSchema = object({
27429
+ storage: discriminatedUnion("kind", [
27430
+ object({
27431
+ kind: literal("present"),
27432
+ volumes: array(OnboardStorageVolumeSchema)
27433
+ }),
27434
+ object({
27435
+ kind: literal("absent"),
27436
+ reason: string()
27437
+ }),
27438
+ object({
27439
+ kind: literal("unknown"),
27440
+ reason: string()
27441
+ })
27442
+ ]),
27443
+ tracks: array(object({
27444
+ id: string(),
27445
+ enabled: boolean(),
27446
+ isVideo: boolean(),
27447
+ /** From the camera's own track description. Null when it does not say. */
27448
+ codec: string().nullable(),
27449
+ resolution: string().nullable(),
27450
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
27451
+ overwriteWhenFull: boolean().nullable()
27452
+ })),
27453
+ /**
27454
+ * The track the write path targets — the enabled VIDEO one. Null when
27455
+ * no track could be identified, which is itself a refusal reason.
27456
+ */
27457
+ primaryTrackId: string().nullable(),
27458
+ /** Master "record to the card at all" switch. */
27459
+ enabled: boolean().nullable(),
27460
+ overwriteWhenFull: boolean().nullable(),
27461
+ preRecordSec: number().nullable(),
27462
+ postRecordSec: number().nullable(),
27463
+ /** Length of one recorded file, in minutes. */
27464
+ segmentMinutes: number().nullable(),
27465
+ /** The primary track's weekly windows, flattened. */
27466
+ windows: array(RecordWindowSchema),
27467
+ /**
27468
+ * How many windows the camera described that CamStack could NOT read —
27469
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
27470
+ * name.
27471
+ *
27472
+ * A dropped window is work the reader threw away, and a schedule that
27473
+ * silently shows fewer rows than the camera holds is how an operator
27474
+ * saves back a schedule shorter than the one they were looking at
27475
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
27476
+ * that replaces it would delete what was not shown — which is why a
27477
+ * provider reporting a non-zero count also reports the schedule as not
27478
+ * writable.
27479
+ */
27480
+ unreadableWindows: number(),
27481
+ /**
27482
+ * The camera is scheduled to record and has NO usable storage.
27483
+ *
27484
+ * A first-class fact because it is the fleet's most common silent
27485
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
27486
+ * to a card that is not there. Neither the schedule nor the storage
27487
+ * read says anything wrong on its own; only the pair does.
27488
+ */
27489
+ recordingToNowhere: boolean(),
27490
+ lastFetchedAt: number()
27491
+ });
27492
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
27493
+ var RangeSchema = object({
27494
+ min: number(),
27495
+ max: number(),
27496
+ step: number()
27497
+ });
27498
+ /**
27499
+ * The values a camera actually takes for a numeric field, when they are a SET
27500
+ * rather than a range.
27501
+ *
27502
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
27503
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
27504
+ *
27505
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
27506
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
27507
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
27508
+ *
27509
+ * Neither is expressible as a step: the first has a sentinel two billion away
27510
+ * from its neighbours, the second doubles and then jumps. A range that tried
27511
+ * would forbid values the camera takes AND permit values it silently replaces
27512
+ * with 5 — wrong in both directions at once.
27513
+ *
27514
+ * `sentinel` names the member that is not a duration, so a surface can render
27515
+ * "no limit" instead of `2147483647` seconds.
27516
+ */
27517
+ var AllowedValuesSchema = object({
27518
+ values: array(number()).min(1),
27519
+ sentinel: object({
27520
+ value: number(),
27521
+ meaning: _enum(["no-limit", "disabled"])
27522
+ }).optional()
27523
+ });
27524
+ /**
27525
+ * Per-field availability on ONE camera.
27526
+ *
27527
+ * The field exists on every camera — this says whether this one can be
27528
+ * read and whether it can be written, and `reason` says why not when
27529
+ * either is false. The UI renders the control DISABLED with the reason
27530
+ * rather than hiding it, so a limitation is legible instead of looking
27531
+ * like a missing feature.
27532
+ */
27533
+ var OnboardFieldSupportSchema = object({
27534
+ readable: boolean(),
27535
+ writable: boolean(),
27536
+ /** Required whenever `readable` or `writable` is false. */
27537
+ reason: string().optional()
27538
+ });
27539
+ /** What this camera's schedule model can express. */
27540
+ var OnboardScheduleSupportSchema = object({
27541
+ support: OnboardFieldSupportSchema,
27542
+ /**
27543
+ * The smallest time step the camera can express, in minutes.
27544
+ *
27545
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
27546
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
27547
+ * window whose edges are not a multiple of this is REFUSED rather than
27548
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
27549
+ * and nothing says so.
27550
+ */
27551
+ granularityMinutes: number(),
27552
+ /** Triggers this camera can record on. A window naming another is refused. */
27553
+ triggers: array(RecordTriggerSchema),
27554
+ /**
27555
+ * False when the camera stores ONE trigger per time range, so two
27556
+ * windows overlapping on the same day cannot carry different triggers.
27557
+ * True on Reolink, whose mask is per-trigger and independent.
27558
+ */
27559
+ supportsOverlappingTriggers: boolean()
27560
+ });
27561
+ var RecordingOnboardOptionsSchema = object({
27562
+ enabled: OnboardFieldSupportSchema,
27563
+ overwriteWhenFull: OnboardFieldSupportSchema,
27564
+ preRecordSec: OnboardFieldSupportSchema,
27565
+ preRecordSecRange: RangeSchema.optional(),
27566
+ /** Preferred over the range when the camera takes a SET, not a span. */
27567
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
27568
+ postRecordSec: OnboardFieldSupportSchema,
27569
+ postRecordSecRange: RangeSchema.optional(),
27570
+ /** Preferred over the range when the camera takes a SET, not a span. */
27571
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
27572
+ segmentMinutes: OnboardFieldSupportSchema,
27573
+ segmentMinutesRange: RangeSchema.optional(),
27574
+ /** Preferred over the range when the camera takes a SET, not a span. */
27575
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
27576
+ schedule: OnboardScheduleSupportSchema
27577
+ });
27578
+ /**
27579
+ * A partial change. Every field optional.
27580
+ *
27581
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
27582
+ * silently ignore a field it cannot support — it refuses, by name,
27583
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
27584
+ * is the failure D62 exists to prevent: the operator believes the camera
27585
+ * is recording the way the form says, and it is not.
27586
+ */
27587
+ var RecordingOnboardPatchSchema = object({
27588
+ enabled: boolean().optional(),
27589
+ overwriteWhenFull: boolean().optional(),
27590
+ preRecordSec: number().optional(),
27591
+ postRecordSec: number().optional(),
27592
+ segmentMinutes: number().optional(),
27593
+ /** The complete new window set for the primary track — not a delta. */
27594
+ windows: array(RecordWindowSchema).optional()
27595
+ });
27596
+ var recordingOnboardCapability = {
27597
+ name: "recording-onboard",
27598
+ scope: "device",
27599
+ deviceNative: true,
27600
+ mode: "singleton",
27601
+ deviceTypes: [DeviceType.Camera],
27602
+ deviceConfig: { ui: {
27603
+ kind: "derived-form",
27604
+ builderId: "recording-onboard",
27605
+ tab: "recording"
27606
+ } },
27607
+ methods: {
27608
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
27609
+ setSettings: method(object({
27610
+ deviceId: number(),
27611
+ settings: RecordingOnboardPatchSchema
27612
+ }), _void(), {
27613
+ kind: "mutation",
27614
+ auth: "admin"
27615
+ })
27616
+ },
27617
+ status: {
27618
+ schema: RecordingOnboardStatusSchema,
27619
+ kind: "poll"
27620
+ },
27621
+ runtimeState: RecordingOnboardStatusSchema,
27622
+ /**
27623
+ * Runtime-state durability: **restored** — operator-set camera-side
27624
+ * recording config; mutation-driven, and the storage half is the last
27625
+ * thing the camera said about its own card.
27626
+ *
27627
+ * See `RuntimeStateDurability`. Enforced by
27628
+ * `scripts/check-runtime-state-durability.ts`.
27629
+ */
27630
+ durability: "restored",
27631
+ /** Clock fields: written, but excluded from the compare that decides
27632
+ * whether persisting is worth a SQLite commit. */
27633
+ volatileStateFields: ["lastFetchedAt"]
27634
+ };
27635
+ /**
26401
27636
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
26402
27637
  * for every device, regardless of provider — the kernel needs a uniform
26403
27638
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -28907,11 +30142,15 @@ var motionTriggerCapability = {
28907
30142
  *
28908
30143
  * Adding a fifth name to that list would have been the wrong fix twice over:
28909
30144
  * that page is per-camera DETECTION tuning, and a grid's geometry belongs
28910
- * beside PTZ and motion zones on the camera itself — which is exactly where the
28911
- * framework already puts a widget, when the widget is declared on a CAPABILITY.
28912
- * `deviceConfig.ui` is the mechanism (`motion-zones`, `ptz`, `ptz-autotrack`
28913
- * all use it): the framework emits the structural section and the widget
28914
- * self-persists through the cap's own mutations.
30145
+ * beside PTZ and motion zones on the camera itself. The device page is
30146
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
30147
+ * and this cap carries its section the way `recording` does, by RETURNING it
30148
+ * from `getDeviceSettingsContribution`.
30149
+ *
30150
+ * Seven other widgets are still declared the other way, through a
30151
+ * `deviceConfig.ui` block the framework derives a section from. That route
30152
+ * gives the addon no say in where its own panel lands and no way to decline
30153
+ * for a device the panel does not suit, which is why this one does not use it.
28915
30154
  *
28916
30155
  * ## Why one addon may implement it
28917
30156
  *
@@ -28967,16 +30206,37 @@ var GridProfileOfferSchema = object({
28967
30206
  ]),
28968
30207
  offered: boolean(),
28969
30208
  /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28970
- missingSources: array(number().int().positive())
30209
+ missingSources: array(number().int().positive()),
30210
+ /**
30211
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
30212
+ * offered. DERIVED from the cells and the sources' own size at this profile —
30213
+ * it is reported because nothing else in the system would ever say what the
30214
+ * grid came out as, and because it is the number an operator would otherwise
30215
+ * expect to type.
30216
+ */
30217
+ canvas: string(),
30218
+ /**
30219
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
30220
+ *
30221
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
30222
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
30223
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
30224
+ * Default is `mid` + `low`.
30225
+ */
30226
+ published: boolean()
28971
30227
  });
28972
30228
  var GridLayoutViewSchema = object({
28973
30229
  /** The persisted grid row this camera was declared from. */
28974
30230
  instanceId: string(),
28975
30231
  deviceId: number().int().nonnegative(),
28976
30232
  name: string(),
28977
- /** The canvas the operator laid out — this grid's `high`. mid/low are scaled from it. */
28978
- width: number().int(),
28979
- height: number().int(),
30233
+ /**
30234
+ * NO canvas size. A grid's resolution is not authored: each profile derives
30235
+ * its own from the cells and its sources' dimensions. The two numbers that
30236
+ * used to be here were a text field that silently decided both how much the
30237
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
30238
+ * came out as.
30239
+ */
28980
30240
  fps: number().int(),
28981
30241
  cells: array(GridLayoutCellSchema),
28982
30242
  /** What the catalog will publish, and what it refuses to. Read-only. */
@@ -28985,9 +30245,13 @@ var GridLayoutViewSchema = object({
28985
30245
  var GridLayoutPatchSchema = object({
28986
30246
  deviceId: number().int().nonnegative(),
28987
30247
  name: string().min(1).max(160).optional(),
28988
- width: number().int().min(160).max(7680).optional(),
28989
- height: number().int().min(120).max(4320).optional(),
28990
30248
  fps: number().int().min(1).max(60).optional(),
30249
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
30250
+ publishedProfiles: array(_enum([
30251
+ "high",
30252
+ "mid",
30253
+ "low"
30254
+ ])).max(3).optional(),
28991
30255
  /**
28992
30256
  * The whole cell list at once. A per-cell patch would need an ordering the
28993
30257
  * editor does not have, and a half-applied layout is a picture nobody asked
@@ -31270,6 +32534,25 @@ var ptzAutotrackCapability = {
31270
32534
  scope: "device",
31271
32535
  deviceNative: true,
31272
32536
  mode: "singleton",
32537
+ /**
32538
+ * Two kinds of provider behind one name, the same shape `snapshot` and
32539
+ * `doorbell` use:
32540
+ *
32541
+ * - **Native**: the camera's own firmware autotrack, registered per device by
32542
+ * the driver when the probe says it exists (Reolink: yes; Hikvision:
32543
+ * model-dependent).
32544
+ * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
32545
+ * which follows a subject with `ptz.move` pulses driven by the detection
32546
+ * pipeline. `defaultActive: false` — the operator binds it per camera in
32547
+ * the bindings UI, exactly as `virtual-doorbell` is bound.
32548
+ *
32549
+ * The binding chooses WHICH implementation answers; `setEnabled` stays the
32550
+ * one switch for the function either way. A camera never has both driving its
32551
+ * head, which is the whole reason this is not a second cap with a second
32552
+ * toggle (D62, D520).
32553
+ */
32554
+ kind: "wrapper",
32555
+ defaultActive: false,
31273
32556
  deviceTypes: [DeviceType.Camera],
31274
32557
  deviceConfig: { ui: {
31275
32558
  kind: "widget",
@@ -31752,7 +33035,7 @@ method(object({
31752
33035
  auth: "admin"
31753
33036
  }), method(object({ deviceId: number() }), RecordingConfigSchema, {
31754
33037
  kind: "query",
31755
- auth: "admin"
33038
+ auth: "protected"
31756
33039
  }), method(object({
31757
33040
  deviceId: number(),
31758
33041
  profile: string(),
@@ -31982,13 +33265,101 @@ var ExportStateSchema = _enum([
31982
33265
  "expired",
31983
33266
  "deleted"
31984
33267
  ]);
31985
- /** One export job / history row. */
33268
+ /**
33269
+ * WHAT an export is — the authority, as opposed to the four top-level fields
33270
+ * the Library sorts and labels on (D558 § 2.2).
33271
+ *
33272
+ * Read it through {@link exportSubjectOf}, never off the record directly: the
33273
+ * field is optional for the history rows written before it existed, and that
33274
+ * absence has exactly one interpreter.
33275
+ */
33276
+ var ExportSubjectSchema = discriminatedUnion("kind", [object({
33277
+ kind: literal("footage"),
33278
+ deviceId: number(),
33279
+ profiles: array(string()).min(1),
33280
+ fromMs: number(),
33281
+ toMs: number()
33282
+ }), object({
33283
+ kind: literal("clip"),
33284
+ deviceId: number(),
33285
+ provider: string().min(1),
33286
+ source: string().min(1),
33287
+ sourceLabel: string().min(1),
33288
+ clipId: string().min(1),
33289
+ /**
33290
+ * WHERE IN THE CATALOG to confirm this clip — the day window the surface was
33291
+ * already listing when the operator picked the segment.
33292
+ *
33293
+ * It is **not** a time range control and it never becomes one: the record's
33294
+ * `fromMs`/`toMs` come from the catalog ROW and from nothing a caller
33295
+ * supplied (D558 § 5.4.3), and a window that does not contain the clip is a
33296
+ * `catalog-miss`, not a silently wider search. It exists because
33297
+ * `videoclips.listClips` takes `since`/`until` and has no by-handle twin:
33298
+ * the catalog check D558 asks for is literally that call, and a call needs a
33299
+ * window. The surface has one — `ClipsBrowser`'s `dayWindow`, one local
33300
+ * wall-clock day, which is also the only width measured to be cheap (one
33301
+ * day on 592 lists 171 clips in 712 ms, a busy day 546 in 2.0 s; twenty days
33302
+ * of one child's events took 3.6–5.4 s).
33303
+ */
33304
+ catalogWindow: object({
33305
+ sinceMs: number(),
33306
+ untilMs: number()
33307
+ }),
33308
+ profile: _enum([
33309
+ "high",
33310
+ "mid",
33311
+ "low"
33312
+ ]),
33313
+ /**
33314
+ * The operator's authorisation to wake a sleeping camera for this export.
33315
+ * ONE definition, in the cap that owns the clip read ({@link ClipWakeSchema}),
33316
+ * because the gate that honours it is the clip provider's sleep gate — a
33317
+ * second enum here would be a second contract. Absent by default; never
33318
+ * settable by a scheduler or a retry.
33319
+ *
33320
+ * **One authorised yes is ONE wake.** A failed clip export is retried only by
33321
+ * an operator act that asks again; a queued job that outlived its wake fails
33322
+ * with a reason rather than waking on its turn; and this subject carries
33323
+ * exactly one profile precisely so one tap is never two fetches (D558 § 5.2).
33324
+ */
33325
+ wake: ClipWakeSchema.optional()
33326
+ })]);
33327
+ /**
33328
+ * One export job / history row.
33329
+ *
33330
+ * **`subject` is the AUTHORITY on what was exported. `deviceId`, `profile`,
33331
+ * `fromMs` and `toMs` are its PROJECTION** — kept top-level because the whole
33332
+ * Library sorts and labels on them (`library-items.ts` orders an export by
33333
+ * `fromMs`; `export-format.ts` draws `rangeLabel` from the pair), and a row
33334
+ * that did not fill them would sort under the epoch and render a blank range.
33335
+ * Write to the subject and read from the projection and the two will disagree;
33336
+ * the projection is derived at creation and never edited afterwards.
33337
+ *
33338
+ * And they mean DIFFERENT FACTS for the two kinds, which is the part a reader
33339
+ * who knows only the recording export will get wrong:
33340
+ *
33341
+ * | field | `kind:'footage'` | `kind:'clip'` |
33342
+ * | --- | --- | --- |
33343
+ * | `fromMs`/`toMs` | the stretch the operator ASKED for | the camera's own boundaries, always `clip.timeRange`, never anything a caller supplied |
33344
+ * | `profile` | the stream rendered | the twin actually SERVED (`subject.profile` is the one asked for) |
33345
+ *
33346
+ * Same type, different fact — the shape this repo keeps getting wrong (D385's
33347
+ * two authorities, D224's second copy).
33348
+ */
31986
33349
  var ExportRecordSchema = object({
31987
33350
  id: string(),
31988
33351
  deviceId: number(),
31989
33352
  profile: string(),
31990
33353
  fromMs: number(),
31991
33354
  toMs: number(),
33355
+ /**
33356
+ * What this export IS. Optional ONLY for the rows written before D558: the
33357
+ * store parses every row through this schema on every read, so a required
33358
+ * field would make the export AUDIT — which is the whole reason rows survive
33359
+ * file deletion — unreadable in one release. Absence means `footage`, and
33360
+ * {@link exportSubjectOf} is the one place that says so.
33361
+ */
33362
+ subject: ExportSubjectSchema.optional(),
31992
33363
  options: ExportOptionsSchema,
31993
33364
  state: ExportStateSchema,
31994
33365
  /** 0–100 while rendering; null otherwise. */
@@ -32005,6 +33376,18 @@ var ExportRecordSchema = object({
32005
33376
  /** Failure reason when state is 'failed'; null otherwise. */
32006
33377
  error: string().nullable()
32007
33378
  });
33379
+ _enum([
33380
+ "catalog-miss",
33381
+ "catalog-unreachable",
33382
+ "no-file-for-window",
33383
+ "clip-in-progress",
33384
+ "unsupported-option",
33385
+ "sleeping",
33386
+ "camera-refused",
33387
+ "fetch-failed",
33388
+ "too-large-to-transfer",
33389
+ "wake-expired"
33390
+ ]);
32008
33391
  /** Candidate download URLs (LAN first, then operator extra hosts). */
32009
33392
  var ExportDownloadSchema = object({
32010
33393
  url: string(),
@@ -32023,16 +33406,50 @@ var ExportBytesSchema = object({
32023
33406
  name: string(),
32024
33407
  bytes: number().int().nonnegative()
32025
33408
  });
33409
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
33410
+ function resolveExportProfiles(input) {
33411
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
33412
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
33413
+ return [];
33414
+ }
32026
33415
  method(object({
32027
33416
  deviceId: number(),
32028
33417
  /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
32029
33418
  profile: string().optional(),
32030
33419
  profiles: array(string()).min(1).optional(),
32031
- fromMs: number(),
32032
- toMs: number(),
33420
+ /** Footage only — a clip's boundaries are the camera's. */
33421
+ fromMs: number().optional(),
33422
+ toMs: number().optional(),
33423
+ /** What to export. Absent means the legacy flat footage request. */
33424
+ subject: ExportSubjectSchema.optional(),
32033
33425
  options: ExportOptionsSchema
32034
33426
  }).superRefine((v, ctx) => {
32035
- if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
33427
+ if (v.subject?.kind === "clip") {
33428
+ if (v.subject.deviceId !== v.deviceId) ctx.addIssue({
33429
+ code: ZodIssueCode$1.custom,
33430
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId}) — the top-level field is what per-device scope enforcement reads`,
33431
+ path: ["subject", "deviceId"]
33432
+ });
33433
+ if (v.fromMs !== void 0 || v.toMs !== void 0) ctx.addIssue({
33434
+ code: ZodIssueCode$1.custom,
33435
+ message: "a clip export asks for no time range: the camera chose the boundaries and they are read from the catalog row",
33436
+ path: ["fromMs"]
33437
+ });
33438
+ return;
33439
+ }
33440
+ if (v.subject?.kind === "footage" && v.subject.deviceId !== v.deviceId) ctx.addIssue({
33441
+ code: ZodIssueCode$1.custom,
33442
+ message: `subject.deviceId (${v.subject.deviceId}) must equal deviceId (${v.deviceId})`,
33443
+ path: ["subject", "deviceId"]
33444
+ });
33445
+ const fromMs = v.subject?.kind === "footage" ? v.subject.fromMs : v.fromMs;
33446
+ const toMs = v.subject?.kind === "footage" ? v.subject.toMs : v.toMs;
33447
+ if (typeof fromMs !== "number" || typeof toMs !== "number") ctx.addIssue({
33448
+ code: ZodIssueCode$1.custom,
33449
+ message: "a footage export needs fromMs and toMs",
33450
+ path: ["fromMs"]
33451
+ });
33452
+ if ((v.subject?.kind === "footage" ? [...v.subject.profiles] : resolveExportProfiles(v)).length < 1) ctx.addIssue({
32036
33453
  code: ZodIssueCode$1.custom,
32037
33454
  message: "pass profiles[] (min 1) or legacy profile",
32038
33455
  path: ["profiles"]
@@ -34844,6 +36261,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
34844
36261
  pressureSensor: pressureSensorCapability,
34845
36262
  privacyMask: privacyMaskCapability,
34846
36263
  ptzAutotrack: ptzAutotrackCapability,
36264
+ recordingOnboard: recordingOnboardCapability,
34847
36265
  recordingSignal: recordingSignalCapability,
34848
36266
  sceneMonitor: sceneMonitorCapability,
34849
36267
  scriptRunner: scriptRunnerCapability,
@@ -36005,7 +37423,1336 @@ var BaseDeviceProvider = class extends BaseAddon {
36005
37423
  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;
36006
37424
  new Set(Object.values(DeviceType));
36007
37425
  DeviceFeature.BatteryOperated;
37426
+ /**
37427
+ * The vocabulary, in the order a picker offers it.
37428
+ *
37429
+ * Adding a key here is not enough to make it pickable: both glyph tables must
37430
+ * gain it too, and their tests fail until they do. That is the intended
37431
+ * friction — it is the only thing that keeps the two surfaces honest.
37432
+ */
37433
+ var DISPLAY_ICON_VOCABULARY = [
37434
+ {
37435
+ key: "camera",
37436
+ group: "device",
37437
+ synonyms: [
37438
+ "telecamera",
37439
+ "videocamera",
37440
+ "camera",
37441
+ "cam"
37442
+ ]
37443
+ },
37444
+ {
37445
+ key: "network",
37446
+ group: "device",
37447
+ synonyms: [
37448
+ "rete",
37449
+ "network",
37450
+ "lan"
37451
+ ]
37452
+ },
37453
+ {
37454
+ key: "toggle-left",
37455
+ group: "device",
37456
+ synonyms: [
37457
+ "interruttore",
37458
+ "switch",
37459
+ "toggle"
37460
+ ]
37461
+ },
37462
+ {
37463
+ key: "toggle-right",
37464
+ group: "device",
37465
+ synonyms: [
37466
+ "interruttore",
37467
+ "switch",
37468
+ "toggle",
37469
+ "acceso"
37470
+ ]
37471
+ },
37472
+ {
37473
+ key: "circle-dot",
37474
+ group: "device",
37475
+ synonyms: [
37476
+ "pulsante",
37477
+ "bottone",
37478
+ "button",
37479
+ "dot"
37480
+ ]
37481
+ },
37482
+ {
37483
+ key: "refresh-cw",
37484
+ group: "device",
37485
+ synonyms: [
37486
+ "aggiorna",
37487
+ "ricarica",
37488
+ "refresh",
37489
+ "reload",
37490
+ "ciclo"
37491
+ ]
37492
+ },
37493
+ {
37494
+ key: "box",
37495
+ group: "device",
37496
+ synonyms: [
37497
+ "scatola",
37498
+ "box",
37499
+ "contenitore"
37500
+ ]
37501
+ },
37502
+ {
37503
+ key: "scroll-text",
37504
+ group: "device",
37505
+ synonyms: [
37506
+ "registro",
37507
+ "log",
37508
+ "testo"
37509
+ ]
37510
+ },
37511
+ {
37512
+ key: "workflow",
37513
+ group: "device",
37514
+ synonyms: [
37515
+ "automazione",
37516
+ "flusso",
37517
+ "workflow",
37518
+ "scenario"
37519
+ ]
37520
+ },
37521
+ {
37522
+ key: "lock",
37523
+ group: "device",
37524
+ synonyms: [
37525
+ "serratura",
37526
+ "chiave",
37527
+ "lucchetto",
37528
+ "chiuso",
37529
+ "locked"
37530
+ ]
37531
+ },
37532
+ {
37533
+ key: "lock-open",
37534
+ group: "device",
37535
+ synonyms: [
37536
+ "serratura",
37537
+ "aperta",
37538
+ "sbloccato",
37539
+ "unlocked"
37540
+ ]
37541
+ },
37542
+ {
37543
+ key: "blinds",
37544
+ group: "device",
37545
+ synonyms: [
37546
+ "tapparella",
37547
+ "persiana",
37548
+ "serranda",
37549
+ "avvolgibile",
37550
+ "shutter",
37551
+ "blind",
37552
+ "veneziana"
37553
+ ]
37554
+ },
37555
+ {
37556
+ key: "air-vent",
37557
+ group: "device",
37558
+ synonyms: [
37559
+ "bocchetta",
37560
+ "aria",
37561
+ "ventilazione",
37562
+ "condizionatore",
37563
+ "vent"
37564
+ ]
37565
+ },
37566
+ {
37567
+ key: "monitor-play",
37568
+ group: "device",
37569
+ synonyms: [
37570
+ "schermo",
37571
+ "riproduzione",
37572
+ "player",
37573
+ "display"
37574
+ ]
37575
+ },
37576
+ {
37577
+ key: "sliders-horizontal",
37578
+ group: "device",
37579
+ synonyms: [
37580
+ "regolazione",
37581
+ "cursori",
37582
+ "impostazioni",
37583
+ "slider"
37584
+ ]
37585
+ },
37586
+ {
37587
+ key: "map-pin",
37588
+ group: "device",
37589
+ synonyms: [
37590
+ "posizione",
37591
+ "luogo",
37592
+ "mappa",
37593
+ "pin"
37594
+ ]
37595
+ },
37596
+ {
37597
+ key: "bot",
37598
+ group: "device",
37599
+ synonyms: [
37600
+ "robot",
37601
+ "aspirapolvere",
37602
+ "bot"
37603
+ ]
37604
+ },
37605
+ {
37606
+ key: "tractor",
37607
+ group: "device",
37608
+ synonyms: [
37609
+ "trattore",
37610
+ "tosaerba",
37611
+ "giardino"
37612
+ ]
37613
+ },
37614
+ {
37615
+ key: "image",
37616
+ group: "device",
37617
+ synonyms: [
37618
+ "immagine",
37619
+ "foto",
37620
+ "snapshot"
37621
+ ]
37622
+ },
37623
+ {
37624
+ key: "bone",
37625
+ group: "device",
37626
+ synonyms: ["osso", "cane"]
37627
+ },
37628
+ {
37629
+ key: "door-closed",
37630
+ group: "device",
37631
+ synonyms: [
37632
+ "porta",
37633
+ "chiusa",
37634
+ "contatto"
37635
+ ]
37636
+ },
37637
+ {
37638
+ key: "door-open",
37639
+ group: "device",
37640
+ synonyms: [
37641
+ "porta",
37642
+ "aperta",
37643
+ "contatto",
37644
+ "ingresso",
37645
+ "uscita"
37646
+ ]
37647
+ },
37648
+ {
37649
+ key: "crosshair",
37650
+ group: "device",
37651
+ synonyms: [
37652
+ "mirino",
37653
+ "puntamento",
37654
+ "centro"
37655
+ ]
37656
+ },
37657
+ {
37658
+ key: "eye",
37659
+ group: "device",
37660
+ synonyms: [
37661
+ "occhio",
37662
+ "visibile",
37663
+ "visto"
37664
+ ]
37665
+ },
37666
+ {
37667
+ key: "eye-off",
37668
+ group: "device",
37669
+ synonyms: [
37670
+ "privacy",
37671
+ "nascosto",
37672
+ "oscurato",
37673
+ "mascherato",
37674
+ "invisibile"
37675
+ ]
37676
+ },
37677
+ {
37678
+ key: "smartphone",
37679
+ group: "device",
37680
+ synonyms: [
37681
+ "telefono",
37682
+ "cellulare",
37683
+ "smartphone"
37684
+ ]
37685
+ },
37686
+ {
37687
+ key: "phone",
37688
+ group: "device",
37689
+ synonyms: ["telefono", "chiamata"]
37690
+ },
37691
+ {
37692
+ key: "tablet",
37693
+ group: "device",
37694
+ synonyms: ["tablet", "ipad"]
37695
+ },
37696
+ {
37697
+ key: "laptop",
37698
+ group: "device",
37699
+ synonyms: [
37700
+ "portatile",
37701
+ "laptop",
37702
+ "notebook"
37703
+ ]
37704
+ },
37705
+ {
37706
+ key: "monitor",
37707
+ group: "device",
37708
+ synonyms: ["monitor", "schermo"]
37709
+ },
37710
+ {
37711
+ key: "tv",
37712
+ group: "device",
37713
+ synonyms: ["televisore", "tv"]
37714
+ },
37715
+ {
37716
+ key: "speaker",
37717
+ group: "device",
37718
+ synonyms: [
37719
+ "altoparlante",
37720
+ "cassa",
37721
+ "speaker"
37722
+ ]
37723
+ },
37724
+ {
37725
+ key: "printer",
37726
+ group: "device",
37727
+ synonyms: ["stampante"]
37728
+ },
37729
+ {
37730
+ key: "projector",
37731
+ group: "device",
37732
+ synonyms: ["proiettore"]
37733
+ },
37734
+ {
37735
+ key: "gamepad-2",
37736
+ group: "device",
37737
+ synonyms: [
37738
+ "console",
37739
+ "gioco",
37740
+ "joystick"
37741
+ ]
37742
+ },
37743
+ {
37744
+ key: "radio",
37745
+ group: "device",
37746
+ synonyms: ["radio"]
37747
+ },
37748
+ {
37749
+ key: "router",
37750
+ group: "device",
37751
+ synonyms: ["router", "modem"]
37752
+ },
37753
+ {
37754
+ key: "bluetooth",
37755
+ group: "device",
37756
+ synonyms: ["bluetooth"]
37757
+ },
37758
+ {
37759
+ key: "cpu",
37760
+ group: "device",
37761
+ synonyms: [
37762
+ "processore",
37763
+ "cpu",
37764
+ "chip",
37765
+ "dispositivo"
37766
+ ]
37767
+ },
37768
+ {
37769
+ key: "hard-drive",
37770
+ group: "device",
37771
+ synonyms: [
37772
+ "disco",
37773
+ "hdd",
37774
+ "archiviazione",
37775
+ "storage"
37776
+ ]
37777
+ },
37778
+ {
37779
+ key: "server",
37780
+ group: "device",
37781
+ synonyms: ["server", "nas"]
37782
+ },
37783
+ {
37784
+ key: "mic",
37785
+ group: "device",
37786
+ synonyms: ["microfono", "audio"]
37787
+ },
37788
+ {
37789
+ key: "mic-off",
37790
+ group: "device",
37791
+ synonyms: [
37792
+ "microfono",
37793
+ "spento",
37794
+ "muto"
37795
+ ]
37796
+ },
37797
+ {
37798
+ key: "video",
37799
+ group: "device",
37800
+ synonyms: ["video"]
37801
+ },
37802
+ {
37803
+ key: "video-off",
37804
+ group: "device",
37805
+ synonyms: ["video", "spento"]
37806
+ },
37807
+ {
37808
+ key: "music",
37809
+ group: "device",
37810
+ synonyms: ["musica"]
37811
+ },
37812
+ {
37813
+ key: "plug",
37814
+ group: "device",
37815
+ synonyms: [
37816
+ "presa",
37817
+ "spina",
37818
+ "plug"
37819
+ ]
37820
+ },
37821
+ {
37822
+ key: "plug-zap",
37823
+ group: "device",
37824
+ synonyms: ["presa", "alimentata"]
37825
+ },
37826
+ {
37827
+ key: "power",
37828
+ group: "device",
37829
+ synonyms: [
37830
+ "accensione",
37831
+ "alimentazione",
37832
+ "power"
37833
+ ]
37834
+ },
37835
+ {
37836
+ key: "power-off",
37837
+ group: "device",
37838
+ synonyms: ["spegnimento", "spento"]
37839
+ },
37840
+ {
37841
+ key: "cable",
37842
+ group: "device",
37843
+ synonyms: ["cavo"]
37844
+ },
37845
+ {
37846
+ key: "battery",
37847
+ group: "device",
37848
+ synonyms: ["batteria"]
37849
+ },
37850
+ {
37851
+ key: "battery-charging",
37852
+ group: "device",
37853
+ synonyms: [
37854
+ "batteria",
37855
+ "carica",
37856
+ "ricarica"
37857
+ ]
37858
+ },
37859
+ {
37860
+ key: "battery-full",
37861
+ group: "device",
37862
+ synonyms: [
37863
+ "batteria",
37864
+ "carica",
37865
+ "piena"
37866
+ ]
37867
+ },
37868
+ {
37869
+ key: "battery-low",
37870
+ group: "device",
37871
+ synonyms: ["batteria", "scarica"]
37872
+ },
37873
+ {
37874
+ key: "key",
37875
+ group: "device",
37876
+ synonyms: ["chiave"]
37877
+ },
37878
+ {
37879
+ key: "key-round",
37880
+ group: "device",
37881
+ synonyms: ["chiave", "tonda"]
37882
+ },
37883
+ {
37884
+ key: "mailbox",
37885
+ group: "device",
37886
+ synonyms: [
37887
+ "cassetta",
37888
+ "postale",
37889
+ "posta"
37890
+ ]
37891
+ },
37892
+ {
37893
+ key: "package",
37894
+ group: "device",
37895
+ synonyms: ["pacco", "consegna"]
37896
+ },
37897
+ {
37898
+ key: "shield",
37899
+ group: "device",
37900
+ synonyms: [
37901
+ "scudo",
37902
+ "protezione",
37903
+ "sicurezza",
37904
+ "allarme"
37905
+ ]
37906
+ },
37907
+ {
37908
+ key: "shield-check",
37909
+ group: "device",
37910
+ synonyms: [
37911
+ "protetto",
37912
+ "sicuro",
37913
+ "armato"
37914
+ ]
37915
+ },
37916
+ {
37917
+ key: "shield-off",
37918
+ group: "device",
37919
+ synonyms: [
37920
+ "protezione",
37921
+ "disattivata",
37922
+ "disarmato"
37923
+ ]
37924
+ },
37925
+ {
37926
+ key: "briefcase",
37927
+ group: "device",
37928
+ synonyms: [
37929
+ "ufficio",
37930
+ "lavoro",
37931
+ "valigetta",
37932
+ "studio"
37933
+ ]
37934
+ },
37935
+ {
37936
+ key: "lightbulb",
37937
+ group: "lighting",
37938
+ synonyms: [
37939
+ "luce",
37940
+ "lampadina",
37941
+ "lampada",
37942
+ "light"
37943
+ ]
37944
+ },
37945
+ {
37946
+ key: "lamp",
37947
+ group: "lighting",
37948
+ synonyms: ["lampada", "abatjour"]
37949
+ },
37950
+ {
37951
+ key: "lamp-ceiling",
37952
+ group: "lighting",
37953
+ synonyms: [
37954
+ "plafoniera",
37955
+ "lampadario",
37956
+ "luce",
37957
+ "soffitto"
37958
+ ]
37959
+ },
37960
+ {
37961
+ key: "lamp-desk",
37962
+ group: "lighting",
37963
+ synonyms: [
37964
+ "lampada",
37965
+ "scrivania",
37966
+ "abatjour"
37967
+ ]
37968
+ },
37969
+ {
37970
+ key: "lamp-floor",
37971
+ group: "lighting",
37972
+ synonyms: [
37973
+ "piantana",
37974
+ "lampada",
37975
+ "terra"
37976
+ ]
37977
+ },
37978
+ {
37979
+ key: "flashlight",
37980
+ group: "lighting",
37981
+ synonyms: [
37982
+ "faretto",
37983
+ "torcia",
37984
+ "spot",
37985
+ "faro"
37986
+ ]
37987
+ },
37988
+ {
37989
+ key: "heater",
37990
+ group: "lighting",
37991
+ synonyms: [
37992
+ "riscaldamento",
37993
+ "termosifone",
37994
+ "stufa",
37995
+ "calore"
37996
+ ]
37997
+ },
37998
+ {
37999
+ key: "snowflake",
38000
+ group: "lighting",
38001
+ synonyms: [
38002
+ "freddo",
38003
+ "raffrescamento",
38004
+ "aria",
38005
+ "condizionata",
38006
+ "neve"
38007
+ ]
38008
+ },
38009
+ {
38010
+ key: "thermometer-sun",
38011
+ group: "lighting",
38012
+ synonyms: [
38013
+ "caldo",
38014
+ "temperatura",
38015
+ "alta"
38016
+ ]
38017
+ },
38018
+ {
38019
+ key: "thermometer-snowflake",
38020
+ group: "lighting",
38021
+ synonyms: [
38022
+ "freddo",
38023
+ "temperatura",
38024
+ "bassa"
38025
+ ]
38026
+ },
38027
+ {
38028
+ key: "refrigerator",
38029
+ group: "lighting",
38030
+ synonyms: ["frigorifero", "frigo"]
38031
+ },
38032
+ {
38033
+ key: "microwave",
38034
+ group: "lighting",
38035
+ synonyms: ["microonde", "forno"]
38036
+ },
38037
+ {
38038
+ key: "washing-machine",
38039
+ group: "lighting",
38040
+ synonyms: [
38041
+ "lavatrice",
38042
+ "lavastoviglie",
38043
+ "asciugatrice"
38044
+ ]
38045
+ },
38046
+ {
38047
+ key: "cooking-pot",
38048
+ group: "lighting",
38049
+ synonyms: [
38050
+ "pentola",
38051
+ "cucina",
38052
+ "fornello"
38053
+ ]
38054
+ },
38055
+ {
38056
+ key: "utensils",
38057
+ group: "lighting",
38058
+ synonyms: [
38059
+ "cucina",
38060
+ "posate",
38061
+ "sala",
38062
+ "pranzo"
38063
+ ]
38064
+ },
38065
+ {
38066
+ key: "thermometer",
38067
+ group: "sensing",
38068
+ synonyms: [
38069
+ "temperatura",
38070
+ "termometro",
38071
+ "termostato"
38072
+ ]
38073
+ },
38074
+ {
38075
+ key: "droplets",
38076
+ group: "sensing",
38077
+ synonyms: [
38078
+ "umidita",
38079
+ "acqua",
38080
+ "allagamento",
38081
+ "perdita"
38082
+ ]
38083
+ },
38084
+ {
38085
+ key: "droplet",
38086
+ group: "sensing",
38087
+ synonyms: [
38088
+ "goccia",
38089
+ "acqua",
38090
+ "perdita",
38091
+ "allagamento"
38092
+ ]
38093
+ },
38094
+ {
38095
+ key: "gauge",
38096
+ group: "sensing",
38097
+ synonyms: [
38098
+ "misura",
38099
+ "livello",
38100
+ "pressione",
38101
+ "indicatore"
38102
+ ]
38103
+ },
38104
+ {
38105
+ key: "gauge2",
38106
+ group: "sensing",
38107
+ synonyms: [
38108
+ "misura",
38109
+ "livello",
38110
+ "pressione",
38111
+ "indicatore"
38112
+ ],
38113
+ aliasOf: "gauge"
38114
+ },
38115
+ {
38116
+ key: "sun",
38117
+ group: "sensing",
38118
+ synonyms: [
38119
+ "sole",
38120
+ "luminosita",
38121
+ "giorno",
38122
+ "esterno"
38123
+ ]
38124
+ },
38125
+ {
38126
+ key: "sun-medium",
38127
+ group: "sensing",
38128
+ synonyms: ["sole", "luminosita"]
38129
+ },
38130
+ {
38131
+ key: "sunrise",
38132
+ group: "sensing",
38133
+ synonyms: ["alba"]
38134
+ },
38135
+ {
38136
+ key: "moon",
38137
+ group: "sensing",
38138
+ synonyms: [
38139
+ "notte",
38140
+ "luna",
38141
+ "notturna",
38142
+ "nightvision"
38143
+ ]
38144
+ },
38145
+ {
38146
+ key: "wind",
38147
+ group: "sensing",
38148
+ synonyms: [
38149
+ "vento",
38150
+ "aria",
38151
+ "gas",
38152
+ "monossido",
38153
+ "co"
38154
+ ]
38155
+ },
38156
+ {
38157
+ key: "cloud",
38158
+ group: "sensing",
38159
+ synonyms: ["nuvola", "nuvoloso"]
38160
+ },
38161
+ {
38162
+ key: "cloud-rain",
38163
+ group: "sensing",
38164
+ synonyms: ["pioggia"]
38165
+ },
38166
+ {
38167
+ key: "cloud-sun",
38168
+ group: "sensing",
38169
+ synonyms: [
38170
+ "variabile",
38171
+ "sole",
38172
+ "nuvole"
38173
+ ]
38174
+ },
38175
+ {
38176
+ key: "waves",
38177
+ group: "sensing",
38178
+ synonyms: [
38179
+ "onde",
38180
+ "acqua",
38181
+ "vibrazione"
38182
+ ]
38183
+ },
38184
+ {
38185
+ key: "sprout",
38186
+ group: "sensing",
38187
+ synonyms: [
38188
+ "irrigazione",
38189
+ "pianta",
38190
+ "germoglio",
38191
+ "orto"
38192
+ ]
38193
+ },
38194
+ {
38195
+ key: "leaf",
38196
+ group: "sensing",
38197
+ synonyms: [
38198
+ "foglia",
38199
+ "pianta",
38200
+ "verde"
38201
+ ]
38202
+ },
38203
+ {
38204
+ key: "flower",
38205
+ group: "sensing",
38206
+ synonyms: ["fiore", "pianta"]
38207
+ },
38208
+ {
38209
+ key: "zap",
38210
+ group: "sensing",
38211
+ synonyms: [
38212
+ "energia",
38213
+ "corrente",
38214
+ "consumo",
38215
+ "watt",
38216
+ "elettricita"
38217
+ ]
38218
+ },
38219
+ {
38220
+ key: "flame",
38221
+ group: "sensing",
38222
+ synonyms: [
38223
+ "fiamma",
38224
+ "gas",
38225
+ "fuoco",
38226
+ "incendio"
38227
+ ]
38228
+ },
38229
+ {
38230
+ key: "bell",
38231
+ group: "sensing",
38232
+ synonyms: [
38233
+ "campanello",
38234
+ "suoneria",
38235
+ "notifica",
38236
+ "bell",
38237
+ "chime"
38238
+ ]
38239
+ },
38240
+ {
38241
+ key: "bell-ring",
38242
+ group: "sensing",
38243
+ synonyms: [
38244
+ "campanello",
38245
+ "suona",
38246
+ "doorbell",
38247
+ "notifica"
38248
+ ]
38249
+ },
38250
+ {
38251
+ key: "bell-off",
38252
+ group: "sensing",
38253
+ synonyms: [
38254
+ "silenzioso",
38255
+ "notifiche",
38256
+ "disattivate",
38257
+ "muto"
38258
+ ]
38259
+ },
38260
+ {
38261
+ key: "siren",
38262
+ group: "sensing",
38263
+ synonyms: ["sirena", "allarme"]
38264
+ },
38265
+ {
38266
+ key: "radar",
38267
+ group: "sensing",
38268
+ synonyms: [
38269
+ "movimento",
38270
+ "presenza",
38271
+ "pir",
38272
+ "rilevamento",
38273
+ "radar"
38274
+ ]
38275
+ },
38276
+ {
38277
+ key: "activity",
38278
+ group: "sensing",
38279
+ synonyms: [
38280
+ "attivita",
38281
+ "sensore",
38282
+ "generico"
38283
+ ]
38284
+ },
38285
+ {
38286
+ key: "hash",
38287
+ group: "sensing",
38288
+ synonyms: [
38289
+ "numero",
38290
+ "contatore",
38291
+ "valore"
38292
+ ]
38293
+ },
38294
+ {
38295
+ key: "clock",
38296
+ group: "sensing",
38297
+ synonyms: [
38298
+ "orologio",
38299
+ "ora",
38300
+ "tempo"
38301
+ ]
38302
+ },
38303
+ {
38304
+ key: "timer",
38305
+ group: "sensing",
38306
+ synonyms: [
38307
+ "timer",
38308
+ "conto",
38309
+ "rovescia"
38310
+ ]
38311
+ },
38312
+ {
38313
+ key: "calendar",
38314
+ group: "sensing",
38315
+ synonyms: [
38316
+ "calendario",
38317
+ "data",
38318
+ "pianificazione"
38319
+ ]
38320
+ },
38321
+ {
38322
+ key: "pipette",
38323
+ group: "sensing",
38324
+ synonyms: [
38325
+ "qualita",
38326
+ "aria",
38327
+ "misura",
38328
+ "campione"
38329
+ ]
38330
+ },
38331
+ {
38332
+ key: "fan",
38333
+ group: "sensing",
38334
+ synonyms: [
38335
+ "ventilatore",
38336
+ "ventola",
38337
+ "aria"
38338
+ ]
38339
+ },
38340
+ {
38341
+ key: "bath",
38342
+ group: "sensing",
38343
+ synonyms: [
38344
+ "bagno",
38345
+ "vasca",
38346
+ "doccia"
38347
+ ]
38348
+ },
38349
+ {
38350
+ key: "footprints",
38351
+ group: "sensing",
38352
+ synonyms: [
38353
+ "movimento",
38354
+ "passi",
38355
+ "presenza",
38356
+ "motion"
38357
+ ]
38358
+ },
38359
+ {
38360
+ key: "alarm-smoke",
38361
+ group: "sensing",
38362
+ synonyms: [
38363
+ "fumo",
38364
+ "antincendio",
38365
+ "rilevatore"
38366
+ ]
38367
+ },
38368
+ {
38369
+ key: "shield-alert",
38370
+ group: "sensing",
38371
+ synonyms: [
38372
+ "manomissione",
38373
+ "allarme",
38374
+ "tamper",
38375
+ "avviso"
38376
+ ]
38377
+ },
38378
+ {
38379
+ key: "vibrate",
38380
+ group: "sensing",
38381
+ synonyms: [
38382
+ "vibrazione",
38383
+ "urto",
38384
+ "tamper"
38385
+ ]
38386
+ },
38387
+ {
38388
+ key: "wifi",
38389
+ group: "sensing",
38390
+ synonyms: [
38391
+ "wifi",
38392
+ "rete",
38393
+ "segnale"
38394
+ ]
38395
+ },
38396
+ {
38397
+ key: "wifi-off",
38398
+ group: "sensing",
38399
+ synonyms: [
38400
+ "wifi",
38401
+ "assente",
38402
+ "offline",
38403
+ "disconnesso"
38404
+ ]
38405
+ },
38406
+ {
38407
+ key: "volume-2",
38408
+ group: "sensing",
38409
+ synonyms: [
38410
+ "volume",
38411
+ "audio",
38412
+ "altoparlante",
38413
+ "suono"
38414
+ ]
38415
+ },
38416
+ {
38417
+ key: "volume-x",
38418
+ group: "sensing",
38419
+ synonyms: [
38420
+ "muto",
38421
+ "silenziato",
38422
+ "volume",
38423
+ "spento"
38424
+ ]
38425
+ },
38426
+ {
38427
+ key: "heart",
38428
+ group: "sensing",
38429
+ synonyms: [
38430
+ "cuore",
38431
+ "preferito",
38432
+ "salute"
38433
+ ]
38434
+ },
38435
+ {
38436
+ key: "heart-pulse",
38437
+ group: "sensing",
38438
+ synonyms: [
38439
+ "battito",
38440
+ "salute",
38441
+ "monitoraggio"
38442
+ ]
38443
+ },
38444
+ {
38445
+ key: "star",
38446
+ group: "sensing",
38447
+ synonyms: [
38448
+ "preferito",
38449
+ "stella",
38450
+ "importante"
38451
+ ]
38452
+ },
38453
+ {
38454
+ key: "sparkles",
38455
+ group: "sensing",
38456
+ synonyms: [
38457
+ "automatico",
38458
+ "intelligente",
38459
+ "ai",
38460
+ "scena"
38461
+ ]
38462
+ },
38463
+ {
38464
+ key: "house",
38465
+ group: "place",
38466
+ synonyms: [
38467
+ "casa",
38468
+ "abitazione",
38469
+ "home"
38470
+ ]
38471
+ },
38472
+ {
38473
+ key: "sofa",
38474
+ group: "place",
38475
+ synonyms: [
38476
+ "salone",
38477
+ "soggiorno",
38478
+ "divano",
38479
+ "living"
38480
+ ]
38481
+ },
38482
+ {
38483
+ key: "bed-double",
38484
+ group: "place",
38485
+ synonyms: [
38486
+ "camera",
38487
+ "letto",
38488
+ "matrimoniale"
38489
+ ]
38490
+ },
38491
+ {
38492
+ key: "bed",
38493
+ group: "place",
38494
+ synonyms: [
38495
+ "cameretta",
38496
+ "letto",
38497
+ "singolo"
38498
+ ]
38499
+ },
38500
+ {
38501
+ key: "warehouse",
38502
+ group: "place",
38503
+ synonyms: [
38504
+ "magazzino",
38505
+ "capanno",
38506
+ "deposito",
38507
+ "cantina"
38508
+ ]
38509
+ },
38510
+ {
38511
+ key: "trees",
38512
+ group: "place",
38513
+ synonyms: [
38514
+ "giardino",
38515
+ "alberi",
38516
+ "esterno",
38517
+ "parco"
38518
+ ]
38519
+ },
38520
+ {
38521
+ key: "tree-pine",
38522
+ group: "place",
38523
+ synonyms: [
38524
+ "albero",
38525
+ "pino",
38526
+ "giardino"
38527
+ ]
38528
+ },
38529
+ {
38530
+ key: "fence",
38531
+ group: "place",
38532
+ synonyms: [
38533
+ "recinto",
38534
+ "cancello",
38535
+ "staccionata",
38536
+ "perimetro"
38537
+ ]
38538
+ },
38539
+ {
38540
+ key: "store",
38541
+ group: "place",
38542
+ synonyms: [
38543
+ "negozio",
38544
+ "ingresso",
38545
+ "vetrina"
38546
+ ]
38547
+ },
38548
+ {
38549
+ key: "building",
38550
+ group: "place",
38551
+ synonyms: ["edificio", "palazzo"]
38552
+ },
38553
+ {
38554
+ key: "building-2",
38555
+ group: "place",
38556
+ synonyms: [
38557
+ "edificio",
38558
+ "uffici",
38559
+ "condominio"
38560
+ ]
38561
+ },
38562
+ {
38563
+ key: "mountain",
38564
+ group: "place",
38565
+ synonyms: [
38566
+ "montagna",
38567
+ "esterno",
38568
+ "panorama"
38569
+ ]
38570
+ },
38571
+ {
38572
+ key: "umbrella",
38573
+ group: "place",
38574
+ synonyms: [
38575
+ "tenda",
38576
+ "markise",
38577
+ "ombrellone",
38578
+ "pergola",
38579
+ "veranda"
38580
+ ]
38581
+ },
38582
+ {
38583
+ key: "dumbbell",
38584
+ group: "place",
38585
+ synonyms: ["palestra", "attrezzi"]
38586
+ },
38587
+ {
38588
+ key: "wrench",
38589
+ group: "place",
38590
+ synonyms: [
38591
+ "officina",
38592
+ "manutenzione",
38593
+ "attrezzi",
38594
+ "utenze"
38595
+ ]
38596
+ },
38597
+ {
38598
+ key: "hammer",
38599
+ group: "place",
38600
+ synonyms: [
38601
+ "officina",
38602
+ "martello",
38603
+ "lavori"
38604
+ ]
38605
+ },
38606
+ {
38607
+ key: "drill",
38608
+ group: "place",
38609
+ synonyms: [
38610
+ "officina",
38611
+ "trapano",
38612
+ "lavori"
38613
+ ]
38614
+ },
38615
+ {
38616
+ key: "waves-ladder",
38617
+ group: "place",
38618
+ synonyms: ["piscina", "vasca"]
38619
+ },
38620
+ {
38621
+ key: "circle-parking",
38622
+ group: "place",
38623
+ synonyms: [
38624
+ "parcheggio",
38625
+ "posto",
38626
+ "auto",
38627
+ "garage"
38628
+ ]
38629
+ },
38630
+ {
38631
+ key: "route",
38632
+ group: "place",
38633
+ synonyms: [
38634
+ "percorso",
38635
+ "strada",
38636
+ "vialetto",
38637
+ "tragitto"
38638
+ ]
38639
+ },
38640
+ {
38641
+ key: "car",
38642
+ group: "life",
38643
+ synonyms: [
38644
+ "auto",
38645
+ "macchina",
38646
+ "veicolo",
38647
+ "parcheggio"
38648
+ ]
38649
+ },
38650
+ {
38651
+ key: "bike",
38652
+ group: "life",
38653
+ synonyms: [
38654
+ "bicicletta",
38655
+ "bici",
38656
+ "moto"
38657
+ ]
38658
+ },
38659
+ {
38660
+ key: "bus",
38661
+ group: "life",
38662
+ synonyms: ["autobus", "pullman"]
38663
+ },
38664
+ {
38665
+ key: "truck",
38666
+ group: "life",
38667
+ synonyms: [
38668
+ "camion",
38669
+ "furgone",
38670
+ "consegna"
38671
+ ]
38672
+ },
38673
+ {
38674
+ key: "plane",
38675
+ group: "life",
38676
+ synonyms: ["aereo"]
38677
+ },
38678
+ {
38679
+ key: "train-front",
38680
+ group: "life",
38681
+ synonyms: ["treno"]
38682
+ },
38683
+ {
38684
+ key: "sailboat",
38685
+ group: "life",
38686
+ synonyms: ["barca", "imbarcazione"]
38687
+ },
38688
+ {
38689
+ key: "dog",
38690
+ group: "life",
38691
+ synonyms: ["cane", "animale"]
38692
+ },
38693
+ {
38694
+ key: "cat",
38695
+ group: "life",
38696
+ synonyms: ["gatto", "animale"]
38697
+ },
38698
+ {
38699
+ key: "bird",
38700
+ group: "life",
38701
+ synonyms: ["uccello", "animale"]
38702
+ },
38703
+ {
38704
+ key: "paw-print",
38705
+ group: "life",
38706
+ synonyms: [
38707
+ "animale",
38708
+ "zampa",
38709
+ "pet"
38710
+ ]
38711
+ },
38712
+ {
38713
+ key: "baby",
38714
+ group: "life",
38715
+ synonyms: [
38716
+ "neonato",
38717
+ "bimbo",
38718
+ "cameretta",
38719
+ "babymonitor"
38720
+ ]
38721
+ },
38722
+ {
38723
+ key: "user",
38724
+ group: "life",
38725
+ synonyms: ["persona", "utente"]
38726
+ },
38727
+ {
38728
+ key: "users",
38729
+ group: "life",
38730
+ synonyms: [
38731
+ "persone",
38732
+ "gruppo",
38733
+ "famiglia"
38734
+ ]
38735
+ },
38736
+ {
38737
+ key: "person-standing",
38738
+ group: "life",
38739
+ synonyms: [
38740
+ "persona",
38741
+ "piedi",
38742
+ "presenza"
38743
+ ]
38744
+ }
38745
+ ];
38746
+ DISPLAY_ICON_VOCABULARY.map((e) => e.key);
38747
+ DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
38748
+ new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
36008
38749
  Object.freeze({
38750
+ "accessories.getStatus": {
38751
+ capName: "accessories",
38752
+ capScope: "device",
38753
+ addonId: null,
38754
+ access: "view"
38755
+ },
36009
38756
  "accessories.setChildHidden": {
36010
38757
  capName: "accessories",
36011
38758
  capScope: "device",
@@ -36288,6 +39035,12 @@ Object.freeze({
36288
39035
  addonId: null,
36289
39036
  access: "view"
36290
39037
  },
39038
+ "airQualitySensor.getStatus": {
39039
+ capName: "air-quality-sensor",
39040
+ capScope: "device",
39041
+ addonId: null,
39042
+ access: "view"
39043
+ },
36291
39044
  "alarmPanel.arm": {
36292
39045
  capName: "alarm-panel",
36293
39046
  capScope: "device",
@@ -36300,6 +39053,12 @@ Object.freeze({
36300
39053
  addonId: null,
36301
39054
  access: "create"
36302
39055
  },
39056
+ "alarmPanel.getStatus": {
39057
+ capName: "alarm-panel",
39058
+ capScope: "device",
39059
+ addonId: null,
39060
+ access: "view"
39061
+ },
36303
39062
  "alarmPanel.trigger": {
36304
39063
  capName: "alarm-panel",
36305
39064
  capScope: "device",
@@ -36348,6 +39107,30 @@ Object.freeze({
36348
39107
  addonId: null,
36349
39108
  access: "create"
36350
39109
  },
39110
+ "ambientLightSensor.getStatus": {
39111
+ capName: "ambient-light-sensor",
39112
+ capScope: "device",
39113
+ addonId: null,
39114
+ access: "view"
39115
+ },
39116
+ "audioAnalysis.applyDeviceSettingsPatch": {
39117
+ capName: "audio-analysis",
39118
+ capScope: "device",
39119
+ addonId: null,
39120
+ access: "create"
39121
+ },
39122
+ "audioAnalysis.getDeviceLiveContribution": {
39123
+ capName: "audio-analysis",
39124
+ capScope: "device",
39125
+ addonId: null,
39126
+ access: "view"
39127
+ },
39128
+ "audioAnalysis.getDeviceSettingsContribution": {
39129
+ capName: "audio-analysis",
39130
+ capScope: "device",
39131
+ addonId: null,
39132
+ access: "view"
39133
+ },
36351
39134
  "audioAnalysis.resolveDeviceSettings": {
36352
39135
  capName: "audio-analysis",
36353
39136
  capScope: "device",
@@ -36510,6 +39293,12 @@ Object.freeze({
36510
39293
  addonId: null,
36511
39294
  access: "create"
36512
39295
  },
39296
+ "automationControl.getStatus": {
39297
+ capName: "automation-control",
39298
+ capScope: "device",
39299
+ addonId: null,
39300
+ access: "view"
39301
+ },
36513
39302
  "automationControl.trigger": {
36514
39303
  capName: "automation-control",
36515
39304
  capScope: "device",
@@ -36606,12 +39395,30 @@ Object.freeze({
36606
39395
  addonId: null,
36607
39396
  access: "create"
36608
39397
  },
39398
+ "battery.getStatus": {
39399
+ capName: "battery",
39400
+ capScope: "device",
39401
+ addonId: null,
39402
+ access: "view"
39403
+ },
36609
39404
  "battery.wakeForStream": {
36610
39405
  capName: "battery",
36611
39406
  capScope: "device",
36612
39407
  addonId: null,
36613
39408
  access: "create"
36614
39409
  },
39410
+ "binary.getStatus": {
39411
+ capName: "binary",
39412
+ capScope: "device",
39413
+ addonId: null,
39414
+ access: "view"
39415
+ },
39416
+ "brightness.getStatus": {
39417
+ capName: "brightness",
39418
+ capScope: "device",
39419
+ addonId: null,
39420
+ access: "view"
39421
+ },
36615
39422
  "brightness.setBrightness": {
36616
39423
  capName: "brightness",
36617
39424
  capScope: "device",
@@ -36726,6 +39533,30 @@ Object.freeze({
36726
39533
  addonId: null,
36727
39534
  access: "view"
36728
39535
  },
39536
+ "cameraCredentials.getStatus": {
39537
+ capName: "camera-credentials",
39538
+ capScope: "device",
39539
+ addonId: null,
39540
+ access: "view"
39541
+ },
39542
+ "cameraGridLayout.applyDeviceSettingsPatch": {
39543
+ capName: "camera-grid-layout",
39544
+ capScope: "device",
39545
+ addonId: null,
39546
+ access: "create"
39547
+ },
39548
+ "cameraGridLayout.getDeviceLiveContribution": {
39549
+ capName: "camera-grid-layout",
39550
+ capScope: "device",
39551
+ addonId: null,
39552
+ access: "view"
39553
+ },
39554
+ "cameraGridLayout.getDeviceSettingsContribution": {
39555
+ capName: "camera-grid-layout",
39556
+ capScope: "device",
39557
+ addonId: null,
39558
+ access: "view"
39559
+ },
36729
39560
  "cameraGridLayout.getLayout": {
36730
39561
  capName: "camera-grid-layout",
36731
39562
  capScope: "device",
@@ -36738,6 +39569,24 @@ Object.freeze({
36738
39569
  addonId: null,
36739
39570
  access: "create"
36740
39571
  },
39572
+ "cameraPipelineConfig.applyDeviceSettingsPatch": {
39573
+ capName: "camera-pipeline-config",
39574
+ capScope: "device",
39575
+ addonId: null,
39576
+ access: "create"
39577
+ },
39578
+ "cameraPipelineConfig.getDeviceLiveContribution": {
39579
+ capName: "camera-pipeline-config",
39580
+ capScope: "device",
39581
+ addonId: null,
39582
+ access: "view"
39583
+ },
39584
+ "cameraPipelineConfig.getDeviceSettingsContribution": {
39585
+ capName: "camera-pipeline-config",
39586
+ capScope: "device",
39587
+ addonId: null,
39588
+ access: "view"
39589
+ },
36741
39590
  "cameraStreams.getBrokerStreams": {
36742
39591
  capName: "camera-streams",
36743
39592
  capScope: "device",
@@ -36768,6 +39617,18 @@ Object.freeze({
36768
39617
  addonId: null,
36769
39618
  access: "view"
36770
39619
  },
39620
+ "carbonMonoxide.getStatus": {
39621
+ capName: "carbon-monoxide",
39622
+ capScope: "device",
39623
+ addonId: null,
39624
+ access: "view"
39625
+ },
39626
+ "climateControl.getStatus": {
39627
+ capName: "climate-control",
39628
+ capScope: "device",
39629
+ addonId: null,
39630
+ access: "view"
39631
+ },
36771
39632
  "climateControl.setFanMode": {
36772
39633
  capName: "climate-control",
36773
39634
  capScope: "device",
@@ -36816,6 +39677,12 @@ Object.freeze({
36816
39677
  addonId: null,
36817
39678
  access: "create"
36818
39679
  },
39680
+ "color.getStatus": {
39681
+ capName: "color",
39682
+ capScope: "device",
39683
+ addonId: null,
39684
+ access: "view"
39685
+ },
36819
39686
  "color.setColor": {
36820
39687
  capName: "color",
36821
39688
  capScope: "device",
@@ -36834,12 +39701,36 @@ Object.freeze({
36834
39701
  addonId: null,
36835
39702
  access: "create"
36836
39703
  },
39704
+ "connectivity.getStatus": {
39705
+ capName: "connectivity",
39706
+ capScope: "device",
39707
+ addonId: null,
39708
+ access: "view"
39709
+ },
39710
+ "consumables.getStatus": {
39711
+ capName: "consumables",
39712
+ capScope: "device",
39713
+ addonId: null,
39714
+ access: "view"
39715
+ },
36837
39716
  "consumables.reset": {
36838
39717
  capName: "consumables",
36839
39718
  capScope: "device",
36840
39719
  addonId: null,
36841
39720
  access: "delete"
36842
39721
  },
39722
+ "contact.getStatus": {
39723
+ capName: "contact",
39724
+ capScope: "device",
39725
+ addonId: null,
39726
+ access: "view"
39727
+ },
39728
+ "control.getStatus": {
39729
+ capName: "control",
39730
+ capScope: "device",
39731
+ addonId: null,
39732
+ access: "view"
39733
+ },
36843
39734
  "control.setValue": {
36844
39735
  capName: "control",
36845
39736
  capScope: "device",
@@ -36906,6 +39797,12 @@ Object.freeze({
36906
39797
  addonId: null,
36907
39798
  access: "create"
36908
39799
  },
39800
+ "cover.getStatus": {
39801
+ capName: "cover",
39802
+ capScope: "device",
39803
+ addonId: null,
39804
+ access: "view"
39805
+ },
36909
39806
  "cover.open": {
36910
39807
  capName: "cover",
36911
39808
  capScope: "device",
@@ -37038,6 +39935,12 @@ Object.freeze({
37038
39935
  addonId: null,
37039
39936
  access: "view"
37040
39937
  },
39938
+ "dayNight.getStatus": {
39939
+ capName: "day-night",
39940
+ capScope: "device",
39941
+ addonId: null,
39942
+ access: "view"
39943
+ },
37041
39944
  "dayNight.setSettings": {
37042
39945
  capName: "day-night",
37043
39946
  capScope: "device",
@@ -37128,6 +40031,30 @@ Object.freeze({
37128
40031
  addonId: null,
37129
40032
  access: "view"
37130
40033
  },
40034
+ "detectionPipeline.applyDeviceSettingsPatch": {
40035
+ capName: "detection-pipeline",
40036
+ capScope: "device",
40037
+ addonId: null,
40038
+ access: "create"
40039
+ },
40040
+ "detectionPipeline.getDeviceLiveContribution": {
40041
+ capName: "detection-pipeline",
40042
+ capScope: "device",
40043
+ addonId: null,
40044
+ access: "view"
40045
+ },
40046
+ "detectionPipeline.getDeviceSettingsContribution": {
40047
+ capName: "detection-pipeline",
40048
+ capScope: "device",
40049
+ addonId: null,
40050
+ access: "view"
40051
+ },
40052
+ "deviceAdminLink.getAdminLink": {
40053
+ capName: "device-admin-link",
40054
+ capScope: "device",
40055
+ addonId: null,
40056
+ access: "view"
40057
+ },
37131
40058
  "deviceAdoption.adopt": {
37132
40059
  capName: "device-adoption",
37133
40060
  capScope: "system",
@@ -37140,6 +40067,12 @@ Object.freeze({
37140
40067
  addonId: null,
37141
40068
  access: "view"
37142
40069
  },
40070
+ "deviceAdoption.getStatus": {
40071
+ capName: "device-adoption",
40072
+ capScope: "system",
40073
+ addonId: null,
40074
+ access: "view"
40075
+ },
37143
40076
  "deviceAdoption.listCandidateFilters": {
37144
40077
  capName: "device-adoption",
37145
40078
  capScope: "system",
@@ -37176,6 +40109,12 @@ Object.freeze({
37176
40109
  addonId: null,
37177
40110
  access: "create"
37178
40111
  },
40112
+ "deviceDiscovery.getStatus": {
40113
+ capName: "device-discovery",
40114
+ capScope: "device",
40115
+ addonId: null,
40116
+ access: "view"
40117
+ },
37179
40118
  "deviceDiscovery.listDiscovered": {
37180
40119
  capName: "device-discovery",
37181
40120
  capScope: "device",
@@ -37194,12 +40133,30 @@ Object.freeze({
37194
40133
  addonId: null,
37195
40134
  access: "create"
37196
40135
  },
40136
+ "deviceExport.applyDeviceSettingsPatch": {
40137
+ capName: "device-export",
40138
+ capScope: "system",
40139
+ addonId: null,
40140
+ access: "create"
40141
+ },
37197
40142
  "deviceExport.exposeDevice": {
37198
40143
  capName: "device-export",
37199
40144
  capScope: "system",
37200
40145
  addonId: null,
37201
40146
  access: "create"
37202
40147
  },
40148
+ "deviceExport.getDeviceLiveContribution": {
40149
+ capName: "device-export",
40150
+ capScope: "system",
40151
+ addonId: null,
40152
+ access: "view"
40153
+ },
40154
+ "deviceExport.getDeviceSettingsContribution": {
40155
+ capName: "device-export",
40156
+ capScope: "system",
40157
+ addonId: null,
40158
+ access: "view"
40159
+ },
37203
40160
  "deviceExport.getStatus": {
37204
40161
  capName: "device-export",
37205
40162
  capScope: "system",
@@ -37296,6 +40253,12 @@ Object.freeze({
37296
40253
  addonId: null,
37297
40254
  access: "create"
37298
40255
  },
40256
+ "deviceManager.applyDeviceSettingsPatch": {
40257
+ capName: "device-manager",
40258
+ capScope: "system",
40259
+ addonId: null,
40260
+ access: "create"
40261
+ },
37299
40262
  "deviceManager.applyInitialMeta": {
37300
40263
  capName: "device-manager",
37301
40264
  capScope: "system",
@@ -37398,6 +40361,12 @@ Object.freeze({
37398
40361
  addonId: null,
37399
40362
  access: "view"
37400
40363
  },
40364
+ "deviceManager.getDeviceLiveContribution": {
40365
+ capName: "device-manager",
40366
+ capScope: "system",
40367
+ addonId: null,
40368
+ access: "view"
40369
+ },
37401
40370
  "deviceManager.getDeviceLiveInfoAggregate": {
37402
40371
  capName: "device-manager",
37403
40372
  capScope: "system",
@@ -37410,6 +40379,12 @@ Object.freeze({
37410
40379
  addonId: null,
37411
40380
  access: "view"
37412
40381
  },
40382
+ "deviceManager.getDeviceSettingsContribution": {
40383
+ capName: "device-manager",
40384
+ capScope: "system",
40385
+ addonId: null,
40386
+ access: "view"
40387
+ },
37413
40388
  "deviceManager.getDeviceStatusAggregate": {
37414
40389
  capName: "device-manager",
37415
40390
  capScope: "system",
@@ -37902,6 +40877,36 @@ Object.freeze({
37902
40877
  addonId: null,
37903
40878
  access: "create"
37904
40879
  },
40880
+ "deviceStatus.getStatus": {
40881
+ capName: "device-status",
40882
+ capScope: "device",
40883
+ addonId: null,
40884
+ access: "view"
40885
+ },
40886
+ "doorbell.applyDeviceSettingsPatch": {
40887
+ capName: "doorbell",
40888
+ capScope: "device",
40889
+ addonId: null,
40890
+ access: "create"
40891
+ },
40892
+ "doorbell.getDeviceLiveContribution": {
40893
+ capName: "doorbell",
40894
+ capScope: "device",
40895
+ addonId: null,
40896
+ access: "view"
40897
+ },
40898
+ "doorbell.getDeviceSettingsContribution": {
40899
+ capName: "doorbell",
40900
+ capScope: "device",
40901
+ addonId: null,
40902
+ access: "view"
40903
+ },
40904
+ "doorbell.getStatus": {
40905
+ capName: "doorbell",
40906
+ capScope: "device",
40907
+ addonId: null,
40908
+ access: "view"
40909
+ },
37905
40910
  "embeddingEncoder.encode": {
37906
40911
  capName: "embedding-encoder",
37907
40912
  capScope: "system",
@@ -37920,6 +40925,18 @@ Object.freeze({
37920
40925
  addonId: null,
37921
40926
  access: "view"
37922
40927
  },
40928
+ "enumSensor.getStatus": {
40929
+ capName: "enum-sensor",
40930
+ capScope: "device",
40931
+ addonId: null,
40932
+ access: "view"
40933
+ },
40934
+ "eventEmitter.getStatus": {
40935
+ capName: "event-emitter",
40936
+ capScope: "device",
40937
+ addonId: null,
40938
+ access: "view"
40939
+ },
37923
40940
  "events.getEventClipUrl": {
37924
40941
  capName: "events",
37925
40942
  capScope: "device",
@@ -38040,6 +41057,12 @@ Object.freeze({
38040
41057
  addonId: null,
38041
41058
  access: "view"
38042
41059
  },
41060
+ "fanControl.getStatus": {
41061
+ capName: "fan-control",
41062
+ capScope: "device",
41063
+ addonId: null,
41064
+ access: "view"
41065
+ },
38043
41066
  "fanControl.setDirection": {
38044
41067
  capName: "fan-control",
38045
41068
  capScope: "device",
@@ -38064,6 +41087,12 @@ Object.freeze({
38064
41087
  addonId: null,
38065
41088
  access: "create"
38066
41089
  },
41090
+ "featureProbe.getStatus": {
41091
+ capName: "feature-probe",
41092
+ capScope: "device",
41093
+ addonId: null,
41094
+ access: "view"
41095
+ },
38067
41096
  "filesystemBrowse.browse": {
38068
41097
  capName: "filesystem-browse",
38069
41098
  capScope: "system",
@@ -38082,6 +41111,24 @@ Object.freeze({
38082
41111
  addonId: null,
38083
41112
  access: "view"
38084
41113
  },
41114
+ "flood.getStatus": {
41115
+ capName: "flood",
41116
+ capScope: "device",
41117
+ addonId: null,
41118
+ access: "view"
41119
+ },
41120
+ "gas.getStatus": {
41121
+ capName: "gas",
41122
+ capScope: "device",
41123
+ addonId: null,
41124
+ access: "view"
41125
+ },
41126
+ "humidifier.getStatus": {
41127
+ capName: "humidifier",
41128
+ capScope: "device",
41129
+ addonId: null,
41130
+ access: "view"
41131
+ },
38085
41132
  "humidifier.setMode": {
38086
41133
  capName: "humidifier",
38087
41134
  capScope: "device",
@@ -38100,12 +41147,30 @@ Object.freeze({
38100
41147
  addonId: null,
38101
41148
  access: "create"
38102
41149
  },
41150
+ "humiditySensor.getStatus": {
41151
+ capName: "humidity-sensor",
41152
+ capScope: "device",
41153
+ addonId: null,
41154
+ access: "view"
41155
+ },
41156
+ "image.getStatus": {
41157
+ capName: "image",
41158
+ capScope: "device",
41159
+ addonId: null,
41160
+ access: "view"
41161
+ },
38103
41162
  "imageSettings.getOptions": {
38104
41163
  capName: "image-settings",
38105
41164
  capScope: "device",
38106
41165
  addonId: null,
38107
41166
  access: "view"
38108
41167
  },
41168
+ "imageSettings.getStatus": {
41169
+ capName: "image-settings",
41170
+ capScope: "device",
41171
+ addonId: null,
41172
+ access: "view"
41173
+ },
38109
41174
  "imageSettings.setSettings": {
38110
41175
  capName: "image-settings",
38111
41176
  capScope: "device",
@@ -38178,6 +41243,12 @@ Object.freeze({
38178
41243
  addonId: null,
38179
41244
  access: "create"
38180
41245
  },
41246
+ "intercom.getStatus": {
41247
+ capName: "intercom",
41248
+ capScope: "device",
41249
+ addonId: null,
41250
+ access: "view"
41251
+ },
38181
41252
  "intercom.handleAnswer": {
38182
41253
  capName: "intercom",
38183
41254
  capScope: "device",
@@ -38214,6 +41285,12 @@ Object.freeze({
38214
41285
  addonId: null,
38215
41286
  access: "create"
38216
41287
  },
41288
+ "lawnMowerControl.getStatus": {
41289
+ capName: "lawn-mower-control",
41290
+ capScope: "device",
41291
+ addonId: null,
41292
+ access: "view"
41293
+ },
38217
41294
  "lawnMowerControl.pause": {
38218
41295
  capName: "lawn-mower-control",
38219
41296
  capScope: "device",
@@ -38496,6 +41573,12 @@ Object.freeze({
38496
41573
  addonId: null,
38497
41574
  access: "create"
38498
41575
  },
41576
+ "lockControl.getStatus": {
41577
+ capName: "lock-control",
41578
+ capScope: "device",
41579
+ addonId: null,
41580
+ access: "view"
41581
+ },
38499
41582
  "lockControl.lock": {
38500
41583
  capName: "lock-control",
38501
41584
  capScope: "device",
@@ -38544,6 +41627,12 @@ Object.freeze({
38544
41627
  addonId: null,
38545
41628
  access: "view"
38546
41629
  },
41630
+ "mediaPlayer.getStatus": {
41631
+ capName: "media-player",
41632
+ capScope: "device",
41633
+ addonId: null,
41634
+ access: "view"
41635
+ },
38547
41636
  "mediaPlayer.next": {
38548
41637
  capName: "media-player",
38549
41638
  capScope: "device",
@@ -38736,6 +41825,12 @@ Object.freeze({
38736
41825
  addonId: null,
38737
41826
  access: "create"
38738
41827
  },
41828
+ "motion.getStatus": {
41829
+ capName: "motion",
41830
+ capScope: "device",
41831
+ addonId: null,
41832
+ access: "view"
41833
+ },
38739
41834
  "motion.isDetected": {
38740
41835
  capName: "motion",
38741
41836
  capScope: "device",
@@ -38748,6 +41843,24 @@ Object.freeze({
38748
41843
  addonId: null,
38749
41844
  access: "create"
38750
41845
  },
41846
+ "motionDetection.applyDeviceSettingsPatch": {
41847
+ capName: "motion-detection",
41848
+ capScope: "device",
41849
+ addonId: null,
41850
+ access: "create"
41851
+ },
41852
+ "motionDetection.getDeviceLiveContribution": {
41853
+ capName: "motion-detection",
41854
+ capScope: "device",
41855
+ addonId: null,
41856
+ access: "view"
41857
+ },
41858
+ "motionDetection.getDeviceSettingsContribution": {
41859
+ capName: "motion-detection",
41860
+ capScope: "device",
41861
+ addonId: null,
41862
+ access: "view"
41863
+ },
38751
41864
  "motionDetection.removeCamera": {
38752
41865
  capName: "motion-detection",
38753
41866
  capScope: "device",
@@ -38760,6 +41873,12 @@ Object.freeze({
38760
41873
  addonId: null,
38761
41874
  access: "delete"
38762
41875
  },
41876
+ "motionTrigger.getStatus": {
41877
+ capName: "motion-trigger",
41878
+ capScope: "device",
41879
+ addonId: null,
41880
+ access: "view"
41881
+ },
38763
41882
  "motionTrigger.setMotionTrigger": {
38764
41883
  capName: "motion-trigger",
38765
41884
  capScope: "device",
@@ -38772,6 +41891,12 @@ Object.freeze({
38772
41891
  addonId: null,
38773
41892
  access: "view"
38774
41893
  },
41894
+ "motionZones.getStatus": {
41895
+ capName: "motion-zones",
41896
+ capScope: "device",
41897
+ addonId: null,
41898
+ access: "view"
41899
+ },
38775
41900
  "motionZones.setZone": {
38776
41901
  capName: "motion-zones",
38777
41902
  capScope: "device",
@@ -38832,6 +41957,12 @@ Object.freeze({
38832
41957
  addonId: null,
38833
41958
  access: "view"
38834
41959
  },
41960
+ "nativeObjectDetection.getStatus": {
41961
+ capName: "native-object-detection",
41962
+ capScope: "device",
41963
+ addonId: null,
41964
+ access: "view"
41965
+ },
38835
41966
  "nativeObjectDetection.setEnabled": {
38836
41967
  capName: "native-object-detection",
38837
41968
  capScope: "device",
@@ -38850,6 +41981,12 @@ Object.freeze({
38850
41981
  addonId: null,
38851
41982
  access: "view"
38852
41983
  },
41984
+ "navigation.getStatus": {
41985
+ capName: "navigation",
41986
+ capScope: "device",
41987
+ addonId: null,
41988
+ access: "view"
41989
+ },
38853
41990
  "navigation.goToPoint": {
38854
41991
  capName: "navigation",
38855
41992
  capScope: "device",
@@ -38934,6 +42071,12 @@ Object.freeze({
38934
42071
  addonId: null,
38935
42072
  access: "create"
38936
42073
  },
42074
+ "networkLink.getStatus": {
42075
+ capName: "network-link",
42076
+ capScope: "device",
42077
+ addonId: null,
42078
+ access: "view"
42079
+ },
38937
42080
  "networkQuality.getAllStats": {
38938
42081
  capName: "network-quality",
38939
42082
  capScope: "system",
@@ -39192,18 +42335,36 @@ Object.freeze({
39192
42335
  addonId: null,
39193
42336
  access: "create"
39194
42337
  },
42338
+ "notifier.getStatus": {
42339
+ capName: "notifier",
42340
+ capScope: "device",
42341
+ addonId: null,
42342
+ access: "view"
42343
+ },
39195
42344
  "notifier.send": {
39196
42345
  capName: "notifier",
39197
42346
  capScope: "device",
39198
42347
  addonId: null,
39199
42348
  access: "create"
39200
42349
  },
42350
+ "numericSensor.getStatus": {
42351
+ capName: "numeric-sensor",
42352
+ capScope: "device",
42353
+ addonId: null,
42354
+ access: "view"
42355
+ },
39201
42356
  "oauthIntegration.getDescriptor": {
39202
42357
  capName: "oauth-integration",
39203
42358
  capScope: "system",
39204
42359
  addonId: null,
39205
42360
  access: "view"
39206
42361
  },
42362
+ "osd.getStatus": {
42363
+ capName: "osd",
42364
+ capScope: "device",
42365
+ addonId: null,
42366
+ access: "view"
42367
+ },
39207
42368
  "osd.setOverlay": {
39208
42369
  capName: "osd",
39209
42370
  capScope: "device",
@@ -39276,6 +42437,12 @@ Object.freeze({
39276
42437
  addonId: null,
39277
42438
  access: "create"
39278
42439
  },
42440
+ "petFeeder.getStatus": {
42441
+ capName: "pet-feeder",
42442
+ capScope: "device",
42443
+ addonId: null,
42444
+ access: "view"
42445
+ },
39279
42446
  "petFeeder.markFoodReplenished": {
39280
42447
  capName: "pet-feeder",
39281
42448
  capScope: "device",
@@ -39318,6 +42485,12 @@ Object.freeze({
39318
42485
  addonId: null,
39319
42486
  access: "create"
39320
42487
  },
42488
+ "pipelineAnalytics.applyDeviceSettingsPatch": {
42489
+ capName: "pipeline-analytics",
42490
+ capScope: "device",
42491
+ addonId: null,
42492
+ access: "create"
42493
+ },
39321
42494
  "pipelineAnalytics.cancelRelocateMedia": {
39322
42495
  capName: "pipeline-analytics",
39323
42496
  capScope: "device",
@@ -39384,6 +42557,18 @@ Object.freeze({
39384
42557
  addonId: null,
39385
42558
  access: "view"
39386
42559
  },
42560
+ "pipelineAnalytics.getDeviceLiveContribution": {
42561
+ capName: "pipeline-analytics",
42562
+ capScope: "device",
42563
+ addonId: null,
42564
+ access: "view"
42565
+ },
42566
+ "pipelineAnalytics.getDeviceSettingsContribution": {
42567
+ capName: "pipeline-analytics",
42568
+ capScope: "device",
42569
+ addonId: null,
42570
+ access: "view"
42571
+ },
39387
42572
  "pipelineAnalytics.getEventDensity": {
39388
42573
  capName: "pipeline-analytics",
39389
42574
  capScope: "device",
@@ -39594,6 +42779,12 @@ Object.freeze({
39594
42779
  addonId: null,
39595
42780
  access: "view"
39596
42781
  },
42782
+ "pipelineAnalytics.ownersWithMedia": {
42783
+ capName: "pipeline-analytics",
42784
+ capScope: "device",
42785
+ addonId: null,
42786
+ access: "view"
42787
+ },
39597
42788
  "pipelineAnalytics.pauseForStorageMigration": {
39598
42789
  capName: "pipeline-analytics",
39599
42790
  capScope: "device",
@@ -39948,6 +43139,12 @@ Object.freeze({
39948
43139
  addonId: null,
39949
43140
  access: "view"
39950
43141
  },
43142
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
43143
+ capName: "pipeline-orchestrator",
43144
+ capScope: "system",
43145
+ addonId: null,
43146
+ access: "create"
43147
+ },
39951
43148
  "pipelineOrchestrator.assignAudio": {
39952
43149
  capName: "pipeline-orchestrator",
39953
43150
  capScope: "system",
@@ -40038,6 +43235,18 @@ Object.freeze({
40038
43235
  addonId: null,
40039
43236
  access: "view"
40040
43237
  },
43238
+ "pipelineOrchestrator.getDeviceLiveContribution": {
43239
+ capName: "pipeline-orchestrator",
43240
+ capScope: "system",
43241
+ addonId: null,
43242
+ access: "view"
43243
+ },
43244
+ "pipelineOrchestrator.getDeviceSettingsContribution": {
43245
+ capName: "pipeline-orchestrator",
43246
+ capScope: "system",
43247
+ addonId: null,
43248
+ access: "view"
43249
+ },
40041
43250
  "pipelineOrchestrator.getGlobalMetrics": {
40042
43251
  capName: "pipeline-orchestrator",
40043
43252
  capScope: "system",
@@ -40446,12 +43655,36 @@ Object.freeze({
40446
43655
  addonId: null,
40447
43656
  access: "view"
40448
43657
  },
43658
+ "powerMeter.getStatus": {
43659
+ capName: "power-meter",
43660
+ capScope: "device",
43661
+ addonId: null,
43662
+ access: "view"
43663
+ },
43664
+ "presence.getStatus": {
43665
+ capName: "presence",
43666
+ capScope: "device",
43667
+ addonId: null,
43668
+ access: "view"
43669
+ },
43670
+ "pressureSensor.getStatus": {
43671
+ capName: "pressure-sensor",
43672
+ capScope: "device",
43673
+ addonId: null,
43674
+ access: "view"
43675
+ },
40449
43676
  "privacyMask.getOptions": {
40450
43677
  capName: "privacy-mask",
40451
43678
  capScope: "device",
40452
43679
  addonId: null,
40453
43680
  access: "view"
40454
43681
  },
43682
+ "privacyMask.getStatus": {
43683
+ capName: "privacy-mask",
43684
+ capScope: "device",
43685
+ addonId: null,
43686
+ access: "view"
43687
+ },
40455
43688
  "privacyMask.setAudioEnabled": {
40456
43689
  capName: "privacy-mask",
40457
43690
  capScope: "device",
@@ -40512,6 +43745,12 @@ Object.freeze({
40512
43745
  addonId: null,
40513
43746
  access: "view"
40514
43747
  },
43748
+ "ptz.getStatus": {
43749
+ capName: "ptz",
43750
+ capScope: "device",
43751
+ addonId: null,
43752
+ access: "view"
43753
+ },
40515
43754
  "ptz.goHome": {
40516
43755
  capName: "ptz",
40517
43756
  capScope: "device",
@@ -40590,6 +43829,12 @@ Object.freeze({
40590
43829
  addonId: null,
40591
43830
  access: "create"
40592
43831
  },
43832
+ "recording.applyDeviceSettingsPatch": {
43833
+ capName: "recording",
43834
+ capScope: "system",
43835
+ addonId: null,
43836
+ access: "create"
43837
+ },
40593
43838
  "recording.cancelRelocateJob": {
40594
43839
  capName: "recording",
40595
43840
  capScope: "system",
@@ -40638,6 +43883,18 @@ Object.freeze({
40638
43883
  addonId: null,
40639
43884
  access: "view"
40640
43885
  },
43886
+ "recording.getDeviceLiveContribution": {
43887
+ capName: "recording",
43888
+ capScope: "system",
43889
+ addonId: null,
43890
+ access: "view"
43891
+ },
43892
+ "recording.getDeviceSettingsContribution": {
43893
+ capName: "recording",
43894
+ capScope: "system",
43895
+ addonId: null,
43896
+ access: "view"
43897
+ },
40641
43898
  "recording.getPlacement": {
40642
43899
  capName: "recording",
40643
43900
  capScope: "system",
@@ -40656,6 +43913,12 @@ Object.freeze({
40656
43913
  addonId: null,
40657
43914
  access: "view"
40658
43915
  },
43916
+ "recording.getStatus": {
43917
+ capName: "recording",
43918
+ capScope: "system",
43919
+ addonId: null,
43920
+ access: "view"
43921
+ },
40659
43922
  "recording.getStorageMigrationMoveStatus": {
40660
43923
  capName: "recording",
40661
43924
  capScope: "system",
@@ -40830,6 +44093,24 @@ Object.freeze({
40830
44093
  addonId: null,
40831
44094
  access: "view"
40832
44095
  },
44096
+ "recordingOnboard.getOptions": {
44097
+ capName: "recording-onboard",
44098
+ capScope: "device",
44099
+ addonId: null,
44100
+ access: "view"
44101
+ },
44102
+ "recordingOnboard.getStatus": {
44103
+ capName: "recording-onboard",
44104
+ capScope: "device",
44105
+ addonId: null,
44106
+ access: "view"
44107
+ },
44108
+ "recordingOnboard.setSettings": {
44109
+ capName: "recording-onboard",
44110
+ capScope: "device",
44111
+ addonId: null,
44112
+ access: "create"
44113
+ },
40833
44114
  "recordingSignal.getStatus": {
40834
44115
  capName: "recording-signal",
40835
44116
  capScope: "device",
@@ -40860,6 +44141,12 @@ Object.freeze({
40860
44141
  addonId: null,
40861
44142
  access: "delete"
40862
44143
  },
44144
+ "sceneMonitor.getStatus": {
44145
+ capName: "scene-monitor",
44146
+ capScope: "device",
44147
+ addonId: null,
44148
+ access: "view"
44149
+ },
40863
44150
  "sceneMonitor.listScenes": {
40864
44151
  capName: "scene-monitor",
40865
44152
  capScope: "device",
@@ -40890,6 +44177,12 @@ Object.freeze({
40890
44177
  addonId: null,
40891
44178
  access: "create"
40892
44179
  },
44180
+ "scriptRunner.getStatus": {
44181
+ capName: "script-runner",
44182
+ capScope: "device",
44183
+ addonId: null,
44184
+ access: "view"
44185
+ },
40893
44186
  "scriptRunner.run": {
40894
44187
  capName: "script-runner",
40895
44188
  capScope: "device",
@@ -41022,6 +44315,12 @@ Object.freeze({
41022
44315
  addonId: null,
41023
44316
  access: "create"
41024
44317
  },
44318
+ "smoke.getStatus": {
44319
+ capName: "smoke",
44320
+ capScope: "device",
44321
+ addonId: null,
44322
+ access: "view"
44323
+ },
41025
44324
  "smtpProvider.getStatus": {
41026
44325
  capName: "smtp-provider",
41027
44326
  capScope: "system",
@@ -41040,12 +44339,30 @@ Object.freeze({
41040
44339
  addonId: null,
41041
44340
  access: "view"
41042
44341
  },
44342
+ "snapshot.applyDeviceSettingsPatch": {
44343
+ capName: "snapshot",
44344
+ capScope: "device",
44345
+ addonId: null,
44346
+ access: "create"
44347
+ },
41043
44348
  "snapshot.getDebugState": {
41044
44349
  capName: "snapshot",
41045
44350
  capScope: "device",
41046
44351
  addonId: null,
41047
44352
  access: "view"
41048
44353
  },
44354
+ "snapshot.getDeviceLiveContribution": {
44355
+ capName: "snapshot",
44356
+ capScope: "device",
44357
+ addonId: null,
44358
+ access: "view"
44359
+ },
44360
+ "snapshot.getDeviceSettingsContribution": {
44361
+ capName: "snapshot",
44362
+ capScope: "device",
44363
+ addonId: null,
44364
+ access: "view"
44365
+ },
41049
44366
  "snapshot.getSnapshot": {
41050
44367
  capName: "snapshot",
41051
44368
  capScope: "device",
@@ -41064,6 +44381,12 @@ Object.freeze({
41064
44381
  addonId: null,
41065
44382
  access: "view"
41066
44383
  },
44384
+ "snapshot.getStatus": {
44385
+ capName: "snapshot",
44386
+ capScope: "device",
44387
+ addonId: null,
44388
+ access: "view"
44389
+ },
41067
44390
  "snapshot.invalidateCache": {
41068
44391
  capName: "snapshot",
41069
44392
  capScope: "device",
@@ -41400,6 +44723,12 @@ Object.freeze({
41400
44723
  addonId: null,
41401
44724
  access: "create"
41402
44725
  },
44726
+ "streamBroker.applyDeviceSettingsPatch": {
44727
+ capName: "stream-broker",
44728
+ capScope: "system",
44729
+ addonId: null,
44730
+ access: "create"
44731
+ },
41403
44732
  "streamBroker.assignProfile": {
41404
44733
  capName: "stream-broker",
41405
44734
  capScope: "system",
@@ -41436,6 +44765,18 @@ Object.freeze({
41436
44765
  addonId: null,
41437
44766
  access: "view"
41438
44767
  },
44768
+ "streamBroker.getDeviceLiveContribution": {
44769
+ capName: "stream-broker",
44770
+ capScope: "system",
44771
+ addonId: null,
44772
+ access: "view"
44773
+ },
44774
+ "streamBroker.getDeviceSettingsContribution": {
44775
+ capName: "stream-broker",
44776
+ capScope: "system",
44777
+ addonId: null,
44778
+ access: "view"
44779
+ },
41439
44780
  "streamBroker.getPreBufferInfo": {
41440
44781
  capName: "stream-broker",
41441
44782
  capScope: "system",
@@ -41634,6 +44975,12 @@ Object.freeze({
41634
44975
  addonId: null,
41635
44976
  access: "view"
41636
44977
  },
44978
+ "streamParams.getStatus": {
44979
+ capName: "stream-params",
44980
+ capScope: "device",
44981
+ addonId: null,
44982
+ access: "view"
44983
+ },
41637
44984
  "streamParams.setProfile": {
41638
44985
  capName: "stream-params",
41639
44986
  capScope: "device",
@@ -41646,6 +44993,12 @@ Object.freeze({
41646
44993
  addonId: null,
41647
44994
  access: "view"
41648
44995
  },
44996
+ "switch.getStatus": {
44997
+ capName: "switch",
44998
+ capScope: "device",
44999
+ addonId: null,
45000
+ access: "view"
45001
+ },
41649
45002
  "switch.setState": {
41650
45003
  capName: "switch",
41651
45004
  capScope: "device",
@@ -41742,6 +45095,18 @@ Object.freeze({
41742
45095
  addonId: null,
41743
45096
  access: "create"
41744
45097
  },
45098
+ "tamper.getStatus": {
45099
+ capName: "tamper",
45100
+ capScope: "device",
45101
+ addonId: null,
45102
+ access: "view"
45103
+ },
45104
+ "temperatureSensor.getStatus": {
45105
+ capName: "temperature-sensor",
45106
+ capScope: "device",
45107
+ addonId: null,
45108
+ access: "view"
45109
+ },
41745
45110
  "terminalSession.adoptLegacyMonitor": {
41746
45111
  capName: "terminal-session",
41747
45112
  capScope: "system",
@@ -41838,6 +45203,12 @@ Object.freeze({
41838
45203
  addonId: null,
41839
45204
  access: "view"
41840
45205
  },
45206
+ "update.getStatus": {
45207
+ capName: "update",
45208
+ capScope: "device",
45209
+ addonId: null,
45210
+ access: "view"
45211
+ },
41841
45212
  "update.installUpdate": {
41842
45213
  capName: "update",
41843
45214
  capScope: "device",
@@ -42060,6 +45431,12 @@ Object.freeze({
42060
45431
  addonId: null,
42061
45432
  access: "create"
42062
45433
  },
45434
+ "vacuumControl.getStatus": {
45435
+ capName: "vacuum-control",
45436
+ capScope: "device",
45437
+ addonId: null,
45438
+ access: "view"
45439
+ },
42063
45440
  "vacuumControl.locate": {
42064
45441
  capName: "vacuum-control",
42065
45442
  capScope: "device",
@@ -42102,6 +45479,12 @@ Object.freeze({
42102
45479
  addonId: null,
42103
45480
  access: "create"
42104
45481
  },
45482
+ "valve.getStatus": {
45483
+ capName: "valve",
45484
+ capScope: "device",
45485
+ addonId: null,
45486
+ access: "view"
45487
+ },
42105
45488
  "valve.open": {
42106
45489
  capName: "valve",
42107
45490
  capScope: "device",
@@ -42174,6 +45557,18 @@ Object.freeze({
42174
45557
  addonId: null,
42175
45558
  access: "create"
42176
45559
  },
45560
+ "vibration.getStatus": {
45561
+ capName: "vibration",
45562
+ capScope: "device",
45563
+ addonId: null,
45564
+ access: "view"
45565
+ },
45566
+ "videoclips.dialClipStream": {
45567
+ capName: "videoclips",
45568
+ capScope: "device",
45569
+ addonId: null,
45570
+ access: "view"
45571
+ },
42177
45572
  "videoclips.getClipPlayback": {
42178
45573
  capName: "videoclips",
42179
45574
  capScope: "device",
@@ -42186,6 +45581,18 @@ Object.freeze({
42186
45581
  addonId: null,
42187
45582
  access: "view"
42188
45583
  },
45584
+ "videoclips.listSources": {
45585
+ capName: "videoclips",
45586
+ capScope: "device",
45587
+ addonId: null,
45588
+ access: "view"
45589
+ },
45590
+ "videoclips.readClipBytes": {
45591
+ capName: "videoclips",
45592
+ capScope: "device",
45593
+ addonId: null,
45594
+ access: "view"
45595
+ },
42189
45596
  "viewerUi.getStaticDir": {
42190
45597
  capName: "viewer-ui",
42191
45598
  capScope: "system",
@@ -42198,6 +45605,12 @@ Object.freeze({
42198
45605
  addonId: null,
42199
45606
  access: "view"
42200
45607
  },
45608
+ "waterHeater.getStatus": {
45609
+ capName: "water-heater",
45610
+ capScope: "device",
45611
+ addonId: null,
45612
+ access: "view"
45613
+ },
42201
45614
  "waterHeater.setAway": {
42202
45615
  capName: "water-heater",
42203
45616
  capScope: "device",
@@ -42216,6 +45629,12 @@ Object.freeze({
42216
45629
  addonId: null,
42217
45630
  access: "create"
42218
45631
  },
45632
+ "weather.getStatus": {
45633
+ capName: "weather",
45634
+ capScope: "device",
45635
+ addonId: null,
45636
+ access: "view"
45637
+ },
42219
45638
  "webrtcSession.addIceCandidate": {
42220
45639
  capName: "webrtc-session",
42221
45640
  capScope: "device",
@@ -42240,6 +45659,12 @@ Object.freeze({
42240
45659
  addonId: null,
42241
45660
  access: "view"
42242
45661
  },
45662
+ "webrtcSession.getSessionDebug": {
45663
+ capName: "webrtc-session",
45664
+ capScope: "device",
45665
+ addonId: null,
45666
+ access: "view"
45667
+ },
42243
45668
  "webrtcSession.getSessionState": {
42244
45669
  capName: "webrtc-session",
42245
45670
  capScope: "device",
@@ -42572,6 +45997,11 @@ Object.freeze({
42572
45997
  form: "single",
42573
45998
  optional: true
42574
45999
  }],
46000
+ "deviceAdminLink.getAdminLink": [{
46001
+ name: "deviceId",
46002
+ form: "single",
46003
+ optional: false
46004
+ }],
42575
46005
  "deviceAdoption.release": [{
42576
46006
  name: "camDeviceId",
42577
46007
  form: "single",
@@ -43482,6 +46912,11 @@ Object.freeze({
43482
46912
  form: "single",
43483
46913
  optional: false
43484
46914
  }],
46915
+ "pipelineAnalytics.ownersWithMedia": [{
46916
+ name: "deviceId",
46917
+ form: "single",
46918
+ optional: false
46919
+ }],
43485
46920
  "pipelineAnalytics.proposeRetrainAnnotations": [{
43486
46921
  name: "deviceId",
43487
46922
  form: "single",
@@ -43952,6 +47387,16 @@ Object.freeze({
43952
47387
  form: "single",
43953
47388
  optional: true
43954
47389
  }],
47390
+ "recordingOnboard.getOptions": [{
47391
+ name: "deviceId",
47392
+ form: "single",
47393
+ optional: false
47394
+ }],
47395
+ "recordingOnboard.setSettings": [{
47396
+ name: "deviceId",
47397
+ form: "single",
47398
+ optional: false
47399
+ }],
43955
47400
  "recordingSignal.getStatus": [{
43956
47401
  name: "deviceId",
43957
47402
  form: "single",
@@ -44178,6 +47623,11 @@ Object.freeze({
44178
47623
  form: "single",
44179
47624
  optional: false
44180
47625
  }],
47626
+ "videoclips.dialClipStream": [{
47627
+ name: "deviceId",
47628
+ form: "single",
47629
+ optional: false
47630
+ }],
44181
47631
  "videoclips.getClipPlayback": [{
44182
47632
  name: "deviceId",
44183
47633
  form: "single",
@@ -44188,6 +47638,16 @@ Object.freeze({
44188
47638
  form: "single",
44189
47639
  optional: false
44190
47640
  }],
47641
+ "videoclips.listSources": [{
47642
+ name: "deviceId",
47643
+ form: "single",
47644
+ optional: false
47645
+ }],
47646
+ "videoclips.readClipBytes": [{
47647
+ name: "deviceId",
47648
+ form: "single",
47649
+ optional: false
47650
+ }],
44191
47651
  "waterHeater.setAway": [{
44192
47652
  name: "deviceId",
44193
47653
  form: "single",
@@ -44223,6 +47683,11 @@ Object.freeze({
44223
47683
  form: "single",
44224
47684
  optional: false
44225
47685
  }],
47686
+ "webrtcSession.getSessionDebug": [{
47687
+ name: "deviceId",
47688
+ form: "single",
47689
+ optional: false
47690
+ }],
44226
47691
  "webrtcSession.getSessionState": [{
44227
47692
  name: "deviceId",
44228
47693
  form: "single",
@@ -51816,11 +55281,59 @@ var EcowittGatewayDevice = class extends BaseDevice {
51816
55281
  super(ctx, ecowittGatewaySchema, { type: DeviceType.Hub });
51817
55282
  this.online = false;
51818
55283
  this.registerDeviceDiscovery();
55284
+ this.registerAdminLinkCap();
51819
55285
  this.updateSourceInfo({
51820
55286
  id: this.stableId,
51821
55287
  system: "ecowitt"
51822
55288
  });
51823
55289
  }
55290
+ /**
55291
+ * device-admin-link — the gateway's own web page.
55292
+ *
55293
+ * Only the `local` transport knows an address. In `listener` transport the
55294
+ * gateway PUSHES to this hub and the addon never learns where it lives, so
55295
+ * `getAdminLink` answers null with the reason logged — a real "I cannot say"
55296
+ * for a device whose sibling gateway can.
55297
+ *
55298
+ * The children (the Container and each sensor) do NOT get this cap: a
55299
+ * battery radio sensor has no page anywhere, and the resolver they reach the
55300
+ * gateway through carries the live nodewitt client, not its connection
55301
+ * settings. Registering it on them would mean inventing a second copy of the
55302
+ * gateway's address in a consumer, which D224 forbids for exactly the reason
55303
+ * it would bite here — the operator edits that address on the gateway.
55304
+ */
55305
+ registerAdminLinkCap() {
55306
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
55307
+ if (deviceId !== this.id) throw new Error(`EcowittGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
55308
+ const connection = this.config.values.connection;
55309
+ if (connection.transport !== "local") {
55310
+ this.ctx.logger.debug("ecowitt admin link unavailable: listener transport has no address", {
55311
+ tags: { deviceId: this.id },
55312
+ meta: { transport: connection.transport }
55313
+ });
55314
+ return null;
55315
+ }
55316
+ const built = buildDeviceAdminUrl({
55317
+ host: connection.host,
55318
+ port: connection.port,
55319
+ scheme: "http",
55320
+ path: "/"
55321
+ });
55322
+ if (!built.ok) {
55323
+ this.ctx.logger.debug("ecowitt admin link unavailable", {
55324
+ tags: { deviceId: this.id },
55325
+ meta: { reason: built.reason }
55326
+ });
55327
+ return null;
55328
+ }
55329
+ return {
55330
+ url: built.url,
55331
+ label: "Gateway page",
55332
+ target: "device",
55333
+ host: connection.host
55334
+ };
55335
+ } });
55336
+ }
51824
55337
  /** Adopt a pre-built manager (created by `onCreateDevice` so the live client
51825
55338
  * starts once) — mirrors `ReolinkHub.adoptApi`. Published on the resolver so
51826
55339
  * children pick it up. */