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