@camstack/addon-training 0.1.104 → 0.1.106

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 (25) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{MotionZonesSettings-Bm3-aVd_.mjs → MotionZonesSettings-B-HW_hQS.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-CHKqJ1Jl.mjs → PrivacyMaskSettings-D24QEivW.mjs} +2 -2
  4. package/dist/{SceneMonitorEditor-BLeDd1kR.mjs → SceneMonitorEditor-BJhJhwU_.mjs} +2 -2
  5. package/dist/_stub.js +6453 -6114
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-DsiAbTAk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-DhOREDNB.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CV7yw1Ol.mjs +26 -0
  8. package/dist/addon-training.css +1 -1
  9. package/dist/{dist-DkRYepCT.js → dist-BifGWu-4.js} +1265 -854
  10. package/dist/{dist-CXZDrfkr.mjs → dist-CSPYj4Bg.mjs} +1266 -855
  11. package/dist/{hostInit-CkFf4SAO.mjs → hostInit-jYho1Uin.mjs} +2 -2
  12. package/dist/{icons-CXruMajc.mjs → icons-tE-y9KlM.mjs} +521 -527
  13. package/dist/index.js +1 -1
  14. package/dist/orchestrator/training.addon.js +1 -1
  15. package/dist/orchestrator/training.addon.mjs +1 -1
  16. package/dist/{player-overlays-DxLTXMBd.mjs → player-overlays-CAEteoxw.mjs} +1 -1
  17. package/dist/providers/modal/training-modal.addon.js +1 -1
  18. package/dist/providers/modal/training-modal.addon.mjs +1 -1
  19. package/dist/remoteEntry.js +1 -1
  20. package/dist/{responsive-1P4H2DQA.mjs → responsive-BJqCg1mL.mjs} +1 -1
  21. package/dist/{scene-monitor-copy-DmazWKcQ.mjs → scene-monitor-copy-CkZtRtGd.mjs} +1 -1
  22. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-BBdigjaL.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-Bmx0UoLI.mjs} +1 -1
  23. package/dist/{zoom-out-D4rm3GoJ.mjs → zoom-out-CWDOvUsr.mjs} +133 -127
  24. package/package.json +1 -1
  25. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-rtY7yvpT.mjs +0 -26
@@ -5234,6 +5234,14 @@ function object(shape, params) {
5234
5234
  ...normalizeParams(params)
5235
5235
  });
5236
5236
  }
5237
+ function strictObject(shape, params) {
5238
+ return new ZodObject({
5239
+ type: "object",
5240
+ shape,
5241
+ catchall: never(),
5242
+ ...normalizeParams(params)
5243
+ });
5244
+ }
5237
5245
  function looseObject(shape, params) {
5238
5246
  return new ZodObject({
5239
5247
  type: "object",
@@ -5613,7 +5621,7 @@ var ZodIssueCode = {
5613
5621
  var ZodFirstPartyTypeKind;
5614
5622
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5615
5623
  //#endregion
5616
- //#region ../types/dist/sleep-DJws6ho7.mjs
5624
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5617
5625
  /**
5618
5626
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5619
5627
  * window to float samples (D455).
@@ -8837,9 +8845,93 @@ function optionalMethod(input, output, options) {
8837
8845
  providerOptional: true
8838
8846
  };
8839
8847
  }
8848
+ /**
8849
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8850
+ * declarations. Every addon that contributes a UI page registers a
8851
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8852
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8853
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8854
+ * that admin-ui consumes.
8855
+ *
8856
+ * The split exists because the public listing has a different output
8857
+ * shape than the per-provider raw declarations, and we want both ends
8858
+ * to flow through codegen instead of relying on a hand-written wrapper.
8859
+ */
8860
+ var AddonPageDeclarationSchema = object({
8861
+ id: string(),
8862
+ label: string(),
8863
+ icon: string(),
8864
+ path: string(),
8865
+ /**
8866
+ * Module Federation remote name — must match the `name` field on the
8867
+ * page addon's `federation()` plugin config. Used by admin-ui's
8868
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8869
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8870
+ * contain hyphens).
8871
+ */
8872
+ remoteName: string(),
8873
+ /**
8874
+ * Bundle filename inside the addon's `dist/` dir served at
8875
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8876
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8877
+ * the static-file route can compute an mtime-based cache-buster URL
8878
+ * without a separate filesystem stat.
8879
+ */
8880
+ bundle: string(),
8881
+ /**
8882
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8883
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8884
+ * Any OTHER string creates (or joins) a custom section rendered after
8885
+ * the built-in groups; its label comes from `sectionLabel` (first
8886
+ * declaration wins), falling back to the id. Absent → the legacy
8887
+ * "Addon Pages" group.
8888
+ */
8889
+ section: string().optional(),
8890
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8891
+ sectionLabel: string().optional()
8892
+ });
8893
+ var addonPagesSourceCapability = {
8894
+ name: "addon-pages-source",
8895
+ scope: "system",
8896
+ mode: "collection",
8897
+ internal: true,
8898
+ methods: {
8899
+ /**
8900
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
8901
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
8902
+ * authenticated session at the default `auth: 'protected'`. The only
8903
+ * consumer, the `addon-pages-aggregator` builtin, reads it via
8904
+ * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
8905
+ * only, never tRPC (per `ctx.capabilities` seeing only the local
8906
+ * process). The public, enriched listing admin-ui actually consumes is
8907
+ * the separate `addon-pages` cap, unaffected by this change.
8908
+ */
8909
+ listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
8910
+ };
8840
8911
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8841
8912
  var VersionOutputSchema$1 = object({ version: string() });
8842
8913
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8914
+ /**
8915
+ * The pipeline step-input shape and the run-result bridge, in a module that
8916
+ * imports nothing but zod and types.
8917
+ *
8918
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8919
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8920
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8921
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8922
+ * them, so every existing import keeps working; addon server code takes them
8923
+ * from `@camstack/types/addon`.
8924
+ */
8925
+ var PipelineStepInputSchema = lazy(() => object({
8926
+ addonId: string(),
8927
+ modelId: string().optional(),
8928
+ enabled: boolean().default(true),
8929
+ children: array(PipelineStepInputSchema).optional(),
8930
+ settings: record(string(), unknown()).optional(),
8931
+ jumpDeviceKey: string().optional()
8932
+ }));
8933
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8934
+ var PipelineRunResultBridge = custom();
8843
8935
  var StaticDirOutputSchema = object({ staticDir: string() });
8844
8936
  var VersionOutputSchema = object({ version: string() });
8845
8937
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9024,6 +9116,7 @@ var CAP_NAMES = Object.freeze([
9024
9116
  "network-link",
9025
9117
  "network-quality",
9026
9118
  "node-certificates",
9119
+ "node-removal-hook",
9027
9120
  "nodes",
9028
9121
  "notification-output",
9029
9122
  "notification-rules",
@@ -9034,6 +9127,7 @@ var CAP_NAMES = Object.freeze([
9034
9127
  "osd",
9035
9128
  "osd-manager",
9036
9129
  "pet-feeder",
9130
+ "pet-fountain",
9037
9131
  "pipeline-analytics",
9038
9132
  "pipeline-executor",
9039
9133
  "pipeline-orchestrator",
@@ -10313,6 +10407,56 @@ Object.freeze({
10313
10407
  "valueType": "number"
10314
10408
  }
10315
10409
  ],
10410
+ "pet-fountain": [
10411
+ {
10412
+ "path": "waterLevel",
10413
+ "valueType": "number"
10414
+ },
10415
+ {
10416
+ "path": "waterVolume",
10417
+ "valueType": "number"
10418
+ },
10419
+ {
10420
+ "path": "empty",
10421
+ "valueType": "boolean"
10422
+ },
10423
+ {
10424
+ "path": "pumpRunning",
10425
+ "valueType": "boolean"
10426
+ },
10427
+ {
10428
+ "path": "pumpIntervalEnabled",
10429
+ "valueType": "boolean"
10430
+ },
10431
+ {
10432
+ "path": "pumpOnTime",
10433
+ "valueType": "number"
10434
+ },
10435
+ {
10436
+ "path": "pumpOffTime",
10437
+ "valueType": "number"
10438
+ },
10439
+ {
10440
+ "path": "lastDrink",
10441
+ "valueType": "number"
10442
+ },
10443
+ {
10444
+ "path": "currentHourDrink",
10445
+ "valueType": "number"
10446
+ },
10447
+ {
10448
+ "path": "last24hDrink",
10449
+ "valueType": "number"
10450
+ },
10451
+ {
10452
+ "path": "todayDrink",
10453
+ "valueType": "number"
10454
+ },
10455
+ {
10456
+ "path": "error",
10457
+ "valueType": "string"
10458
+ }
10459
+ ],
10316
10460
  "power-meter": [
10317
10461
  {
10318
10462
  "path": "power",
@@ -10850,7 +10994,7 @@ object({
10850
10994
  params: record(string(), union([string(), number()])).optional()
10851
10995
  });
10852
10996
  //#endregion
10853
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
10997
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10854
10998
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10855
10999
  DeviceType["Camera"] = "camera";
10856
11000
  DeviceType["Hub"] = "hub";
@@ -10942,6 +11086,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10942
11086
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10943
11087
  * integrations sharing the same food/desiccant/hopper surface. */
10944
11088
  DeviceType["PetFeeder"] = "pet-feeder";
11089
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11090
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11091
+ * of a container and is BUILT as a composed device (D967). */
11092
+ DeviceType["PetFountain"] = "pet-fountain";
10945
11093
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10946
11094
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10947
11095
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11202,7 +11350,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11202
11350
  return DeviceRole;
11203
11351
  }({});
11204
11352
  //#endregion
11205
- //#region ../types/dist/composition-DpFv_J6D.mjs
11353
+ //#region ../types/dist/composition-kSuOifuG.mjs
11206
11354
  /**
11207
11355
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11208
11356
  *
@@ -12287,12 +12435,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12287
12435
  ])
12288
12436
  })
12289
12437
  ]);
12290
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12438
+ var forwardArgUnion = union([
12439
+ string(),
12440
+ number(),
12441
+ boolean(),
12442
+ strictObject({ ["$input"]: string().min(1) }),
12443
+ record(string().min(1), lazy(() => forwardArgUnion))
12444
+ ]);
12445
+ var CompositionForwardArgSchema = forwardArgUnion;
12446
+ /**
12447
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12448
+ * call of `cap.method` on `source`. Without `args` the built method's input
12449
+ * passes through minus `deviceId`; with `args` the template IS the source's
12450
+ * whole input. `deviceId` is always the source's own, never an argument.
12451
+ */
12452
+ var CompositionForwardCommandSchema = object({
12291
12453
  kind: literal("forward"),
12292
12454
  source: CompositionSourceRefSchema,
12293
12455
  cap: string().min(1),
12294
- method: string().min(1)
12295
- }), CompositionCodeSourceSchema]);
12456
+ method: string().min(1),
12457
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12458
+ });
12459
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12296
12460
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12297
12461
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12298
12462
  /**
@@ -12302,7 +12466,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12302
12466
  */
12303
12467
  var CompositionItemEntrySchema = object({
12304
12468
  label: string().min(1).max(80),
12305
- fields: record(string().min(1), CompositionFieldSourceSchema)
12469
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12470
+ /**
12471
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12472
+ * is resettable and a `reset` call naming its key forwards here. The item key
12473
+ * selects the command, so it is never an argument: there is no pass-through.
12474
+ */
12475
+ reset: CompositionForwardCommandSchema.optional()
12306
12476
  });
12307
12477
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12308
12478
  kind: literal("fields"),
@@ -12314,7 +12484,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12314
12484
  * owns the WHOLE array field: the composed value holds exactly these items.
12315
12485
  */
12316
12486
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12317
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12487
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12318
12488
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12319
12489
  }), object({
12320
12490
  kind: literal("passthrough"),
@@ -16272,163 +16442,521 @@ object({
16272
16442
  lastError: string().nullable()
16273
16443
  });
16274
16444
  /**
16275
- * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16276
- * surface that admin-ui consumes through `useAddonPagesListPages()`.
16445
+ * notification-output — canonical, capability-gated notification delivery.
16277
16446
  *
16278
- * The provider iterates every `addon-pages-source` (collection) provider
16279
- * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
16280
- * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
16281
- * filesystem `mtime` cache-buster lets the browser pick up addon
16282
- * rebuilds without manual reload.
16447
+ * Apprise-derived model (see
16448
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16449
+ * callers emit ONE canonical `Notification`; each provider declares a
16450
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16451
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16452
+ * message to what the kind supports — callers never special-case a service.
16283
16453
  *
16284
- * The hub-local builtin `addon-pages-aggregator` (see
16285
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
16286
- * provider. Splitting the public aggregator from the raw collection
16287
- * keeps both ends in codegen — there's no hand-written
16288
- * `addon-pages.router.ts` wrapper anymore.
16454
+ * DESIGN DECISIONS (locked):
16455
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16456
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16457
+ * cap. Rationale: the admin UI needs one uniform surface across the
16458
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16459
+ * alternative would fork the UI per addon and cannot host the
16460
+ * discovery→adopt flow.
16461
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16462
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16463
+ * registered provider (notifiers addon + HA addon) so one catalog is
16464
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16465
+ * `addonId` the generated collection router extracts from the call input.
16466
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16467
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16468
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16469
+ * base64 fallback needed.
16470
+ *
16471
+ * TODO (deferred, closed-set change — separate decision): add
16472
+ * `providerKind: 'notify'` so notification providers surface on the unified
16473
+ * admin "Integrations" page.
16289
16474
  */
16290
- var AddonPageDeclarationSchema$1 = object({
16291
- id: string(),
16292
- label: string(),
16293
- icon: string(),
16294
- path: string(),
16295
- remoteName: string(),
16296
- bundle: string(),
16297
- section: string().optional(),
16298
- sectionLabel: string().optional()
16299
- });
16300
- var AddonPageInfoSchema = object({
16301
- addonId: string(),
16302
- page: AddonPageDeclarationSchema$1,
16303
- bundleUrl: string()
16304
- });
16305
- method(_void(), array(AddonPageInfoSchema).readonly());
16306
16475
  /**
16307
- * `addon-pages-source` — collection cap exposing per-provider raw page
16308
- * declarations. Every addon that contributes a UI page registers a
16309
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16310
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16311
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16312
- * that admin-ui consumes.
16476
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16477
+ * adapter picks what it supports and the degrade engine filters the rest.
16478
+ */
16479
+ var AttachmentMediaTypeSchema = _enum([
16480
+ "image",
16481
+ "video",
16482
+ "gif",
16483
+ "audio",
16484
+ "icon"
16485
+ ]);
16486
+ /**
16487
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16488
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16489
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16490
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16491
+ */
16492
+ var AttachmentSchema = object({
16493
+ mediaType: AttachmentMediaTypeSchema,
16494
+ url: string().optional(),
16495
+ bytes: _instanceof(Uint8Array).optional(),
16496
+ mime: string().optional(),
16497
+ name: string().optional()
16498
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16499
+ var NotificationFormatSchema = _enum([
16500
+ "text",
16501
+ "markdown",
16502
+ "html"
16503
+ ]);
16504
+ /**
16505
+ * The CLOSED icon vocabulary an action button may use.
16313
16506
  *
16314
- * The split exists because the public listing has a different output
16315
- * shape than the per-provider raw declarations, and we want both ends
16316
- * to flow through codegen instead of relying on a hand-written wrapper.
16507
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16508
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16509
+ * notices — the same class of gap as a zone vocabulary nothing produced
16510
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16511
+ * declares `actionIcons: false` and the degrade engine strips the field.
16512
+ *
16513
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16514
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16515
+ * differently.
16516
+ *
16517
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16518
+ *
16519
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16520
+ * the whole set onto its own renderer's vocabulary through a
16521
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16522
+ * fallback, so adding a member here fails every adapter's build until someone
16523
+ * decides its glyph, which is the only place that decision can be made
16524
+ * honestly.
16525
+ *
16526
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16527
+ * `disarm` straight through; iOS feeds that string to
16528
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16529
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16530
+ * mapping, and "the field is documented" is not "the value renders".
16531
+ *
16532
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16533
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16534
+ * so an addon that emits a token the running hub does not know does not lose an
16535
+ * icon — its whole `send` fails Zod validation and the notification never
16536
+ * arrives. Never emit a new token from an addon before the train carrying it.
16317
16537
  */
16318
- var AddonPageDeclarationSchema = object({
16538
+ var NotificationActionIconSchema = _enum([
16539
+ "acknowledge",
16540
+ "dismiss",
16541
+ "silence",
16542
+ "snooze",
16543
+ "view",
16544
+ "play",
16545
+ "open",
16546
+ "close",
16547
+ "lock",
16548
+ "unlock",
16549
+ "arm",
16550
+ "arm-home",
16551
+ "arm-away",
16552
+ "arm-night",
16553
+ "disarm",
16554
+ "light",
16555
+ "alert",
16556
+ "camera"
16557
+ ]);
16558
+ /** A single tap-through action button. */
16559
+ var NotificationActionSchema = object({
16319
16560
  id: string(),
16320
16561
  label: string(),
16321
- icon: string(),
16322
- path: string(),
16323
- /**
16324
- * Module Federation remote name — must match the `name` field on the
16325
- * page addon's `federation()` plugin config. Used by admin-ui's
16326
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16327
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16328
- * contain hyphens).
16329
- */
16330
- remoteName: string(),
16562
+ url: string().optional(),
16563
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16564
+ icon: NotificationActionIconSchema.optional(),
16331
16565
  /**
16332
- * Bundle filename inside the addon's `dist/` dir served at
16333
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16334
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16335
- * the static-file route can compute an mtime-based cache-buster URL
16336
- * without a separate filesystem stat.
16566
+ * Renders in a warning style where the notifier supports it.
16567
+ *
16568
+ * A HINT, never a gate. The callback's authority is its token and nothing
16569
+ * else — see `notification-center/action-token.ts` for what that does and
16570
+ * does not buy.
16337
16571
  */
16338
- bundle: string(),
16572
+ destructive: boolean().optional(),
16339
16573
  /**
16340
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16341
- * `'cluster'`, `'administration'` — the page renders inside that group.
16342
- * Any OTHER string creates (or joins) a custom section rendered after
16343
- * the built-in groups; its label comes from `sectionLabel` (first
16344
- * declaration wins), falling back to the id. Absent → the legacy
16345
- * "Addon Pages" group.
16574
+ * How the tap should REACH the url.
16575
+ *
16576
+ * `navigate` (absent, and every button authored before this field) opens it:
16577
+ * the phone leaves the notification and shows whatever the callback returns.
16578
+ * That is right for a button whose answer the operator wants to read.
16579
+ *
16580
+ * `background` fires it as a POST and stays put. It exists for the buttons
16581
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16582
+ * an answer to the notification, and being thrown into a browser tab to
16583
+ * confirm it costs more attention than the notification did. A backend that
16584
+ * cannot do a background call renders it as an ordinary link (the adapters
16585
+ * fall back rather than dropping the button), so this is a preference, never
16586
+ * a requirement.
16346
16587
  */
16347
- section: string().optional(),
16348
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16349
- sectionLabel: string().optional()
16588
+ mode: _enum(["navigate", "background"]).optional()
16350
16589
  });
16351
- var addonPagesSourceCapability = {
16352
- name: "addon-pages-source",
16353
- scope: "system",
16354
- mode: "collection",
16355
- internal: true,
16356
- methods: {
16357
- /**
16358
- * `internal: true` did not gate the mount (see the 2026-08-26 note on
16359
- * `data-store-provider`) — this was reachable on the AppRouter by ANY
16360
- * authenticated session at the default `auth: 'protected'`. The only
16361
- * consumer, the `addon-pages-aggregator` builtin, reads it via
16362
- * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
16363
- * only, never tRPC (per `ctx.capabilities` seeing only the local
16364
- * process). The public, enriched listing admin-ui actually consumes is
16365
- * the separate `addon-pages` cap, unaffected by this change.
16366
- */
16367
- listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
16368
- };
16369
- var AddonHttpRouteSchema = object({
16370
- method: _enum([
16371
- "GET",
16372
- "POST",
16373
- "PUT",
16374
- "DELETE",
16375
- "PATCH"
16590
+ /** A delivery parameter value: what a notifier declared it takes. */
16591
+ var DeliveryParamValueSchema = union([
16592
+ string(),
16593
+ number(),
16594
+ boolean()
16595
+ ]);
16596
+ /**
16597
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16598
+ * addresses a target of that kind may set it; a kind that declares nothing is
16599
+ * never handed one. The admin UI derives its control from this descriptor —
16600
+ * no editor knows a service by name.
16601
+ *
16602
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16603
+ * run on the addon cap path).
16604
+ */
16605
+ var DeliveryParamSchema = object({
16606
+ key: string().min(1),
16607
+ label: string(),
16608
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16609
+ labelKey: string().optional(),
16610
+ type: _enum([
16611
+ "enum",
16612
+ "number",
16613
+ "boolean",
16614
+ "string"
16376
16615
  ]),
16377
- path: string(),
16378
- access: _enum([
16379
- "public",
16380
- "authenticated",
16381
- "admin"
16382
- ]).optional(),
16383
- description: string().optional(),
16384
- /** The hub limits this route per client address before the handler runs. */
16385
- rateLimit: _enum(["auth"]).optional()
16616
+ /** `enum` only: the closed set of accepted values. */
16617
+ options: array(object({
16618
+ value: string(),
16619
+ label: string()
16620
+ })).optional(),
16621
+ /** `number` only. */
16622
+ min: number().optional(),
16623
+ max: number().optional(),
16624
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16625
+ default: DeliveryParamValueSchema.optional()
16386
16626
  });
16387
16627
  /**
16388
- * Cross-process route invocation envelope. The hub captures the
16389
- * request as plain data, ships it to the worker via Moleculer, and
16390
- * the worker runs the local handler against a capturing reply. The
16391
- * envelope returned describes what the handler intended (status,
16392
- * headers, body, or a redirect) so the hub can translate it back to
16393
- * the Fastify reply that's actually wired to the socket.
16628
+ * The canonical notification. `body` is the only hard field (Apprise model).
16629
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16630
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16631
+ * the adapter maps this ordinal onto its native level. `level?` is an
16632
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16633
+ * `priority` for that one target.
16394
16634
  */
16395
- var InvokeRequestSchema = object({
16396
- method: string(),
16397
- path: string(),
16398
- params: record(string(), string()),
16399
- query: record(string(), string()),
16400
- body: unknown(),
16401
- headers: record(string(), string()),
16402
- user: object({
16403
- id: string(),
16404
- username: string(),
16405
- isAdmin: boolean()
16635
+ var NotificationSchema = object({
16636
+ body: string(),
16637
+ title: string().optional(),
16638
+ format: NotificationFormatSchema.default("text"),
16639
+ priority: number().int().min(1).max(5).default(3),
16640
+ level: string().optional(),
16641
+ attachments: array(AttachmentSchema).optional(),
16642
+ clickUrl: string().optional(),
16643
+ actions: array(NotificationActionSchema).optional(),
16644
+ sound: string().optional(),
16645
+ ttl: number().optional(),
16646
+ tag: string().optional(),
16647
+ deviceId: number().optional(),
16648
+ eventId: string().optional(),
16649
+ metadata: record(string(), unknown()).optional(),
16650
+ /**
16651
+ * Values for the parameters the target's kind DECLARES in
16652
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16653
+ * declaration by the sender; a notifier reads only the keys it declared.
16654
+ */
16655
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16656
+ });
16657
+ /** One declared native severity/priority level for a kind. */
16658
+ var TargetKindLevelSchema = object({
16659
+ id: string(),
16660
+ label: string(),
16661
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16662
+ ordinal: number().int().min(1).max(5).nullable(),
16663
+ flags: object({
16664
+ critical: boolean().optional(),
16665
+ silent: boolean().optional(),
16666
+ noPush: boolean().optional()
16406
16667
  }).optional(),
16407
- scopedToken: unknown().optional()
16668
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16669
+ requires: array(string()).optional(),
16670
+ description: string().optional()
16408
16671
  });
16409
- var InvokeReplyEnvelopeSchema = object({
16410
- status: number().int(),
16411
- headers: record(string(), string()),
16412
- /** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
16413
- * sending `body`. Status defaults to 302 when this is set unless
16414
- * the handler called `reply.code(...)` explicitly. */
16415
- redirectUrl: string().nullable(),
16416
- /** JSON-serializable body. `undefined` is treated as "no body". */
16417
- body: unknown().optional(),
16418
- /** Set when the handler called `reply.type(mime)`. */
16419
- contentType: string().optional()
16672
+ /** The full capability block consulted before dispatch. */
16673
+ var TargetKindCapsSchema = object({
16674
+ attachments: object({
16675
+ mediaTypes: array(AttachmentMediaTypeSchema),
16676
+ mode: _enum([
16677
+ "url",
16678
+ "bytes",
16679
+ "both"
16680
+ ]),
16681
+ max: number().int().nonnegative(),
16682
+ maxBytes: number().int().positive().optional()
16683
+ }),
16684
+ /** Max action buttons (0 = none). */
16685
+ actions: number().int().nonnegative(),
16686
+ /**
16687
+ * Whether this kind renders a per-action ICON.
16688
+ *
16689
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16690
+ * run on the addon cap path — three production failures in one day taught
16691
+ * this repo that once. Absent is read as false by the degrade engine, which
16692
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16693
+ * assumed and dropped costs the operator's trust in the field.
16694
+ */
16695
+ actionIcons: boolean().optional(),
16696
+ levels: array(TargetKindLevelSchema),
16697
+ format: array(NotificationFormatSchema),
16698
+ clickUrl: boolean(),
16699
+ sound: boolean(),
16700
+ ttl: boolean(),
16701
+ bodyMaxLen: number().int().positive()
16420
16702
  });
16421
- method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
16422
- var ConfigTabDeclarationSchema = object({
16423
- id: string(),
16703
+ /**
16704
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16705
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16706
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16707
+ * the union is large and not meant for runtime validation here; the exported
16708
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16709
+ */
16710
+ var ConfigSchemaPassthrough$3 = unknown();
16711
+ var TargetKindSchema = object({
16712
+ kind: string(),
16424
16713
  label: string(),
16425
16714
  icon: string(),
16426
- order: number().optional()
16427
- });
16428
- var ConfigSectionWithValuesSchema = object({
16429
- id: string(),
16430
- title: string(),
16431
- titleKey: string().optional(),
16715
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16716
+ addonId: string(),
16717
+ /**
16718
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16719
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16720
+ * when the addon bundles no icon for that kind — the client then falls back
16721
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16722
+ *
16723
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16724
+ * client, and a native client joins it onto its own hub base.
16725
+ *
16726
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16727
+ * field that survived only because the runtime cap-router forwards provider
16728
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16729
+ * its own Zod parse from stripping it, and the whole arrangement would have
16730
+ * broken silently the moment output validation was tightened anywhere.
16731
+ */
16732
+ iconUrl: string().optional(),
16733
+ /**
16734
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16735
+ *
16736
+ * The server knows this and therefore says it, because the client cannot
16737
+ * safely guess: a React-Native client renders SVG and raster through two
16738
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16739
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16740
+ * placeholder glyph for every vector icon while the web build looked fine.
16741
+ *
16742
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16743
+ * not been updated — a client that cannot determine the type should prefer
16744
+ * its raster path, which is the safe default for an unknown image.
16745
+ */
16746
+ iconMediaType: string().optional(),
16747
+ configSchema: ConfigSchemaPassthrough$3,
16748
+ /**
16749
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16750
+ * base URL, instance, chat, recipient). A save that changes any of them
16751
+ * while leaving a secret redacted is refused by the provider (D634), so an
16752
+ * editor uses this to ask for the secret on exactly those edits instead of
16753
+ * on every edit.
16754
+ *
16755
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16756
+ * kinds) and from a provider older than this field — an editor that finds
16757
+ * it absent must assume ANY edit may need the secret.
16758
+ */
16759
+ destinationKeys: array(string()).optional(),
16760
+ supportsDiscovery: boolean(),
16761
+ /**
16762
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16763
+ * Absent = none; the rule editor then offers nothing.
16764
+ */
16765
+ deliveryParams: array(DeliveryParamSchema).optional(),
16766
+ caps: TargetKindCapsSchema
16767
+ });
16768
+ /**
16769
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16770
+ * (return a presence marker only) when serving `listTargets` — never
16771
+ * round-trip a stored secret to the UI.
16772
+ */
16773
+ var TargetSchema = object({
16774
+ id: string(),
16775
+ name: string(),
16776
+ kind: string(),
16777
+ addonId: string(),
16778
+ enabled: boolean(),
16779
+ config: record(string(), unknown())
16780
+ });
16781
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16782
+ var DiscoveredTargetSchema = object({
16783
+ kind: string(),
16784
+ suggestedName: string(),
16785
+ config: record(string(), unknown())
16786
+ });
16787
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16788
+ var RenderedAsSchema = object({
16789
+ level: string(),
16790
+ format: NotificationFormatSchema,
16791
+ attachmentsSent: number().int().nonnegative(),
16792
+ actionsSent: number().int().nonnegative(),
16793
+ truncated: boolean(),
16794
+ dropped: array(string())
16795
+ });
16796
+ var SendResultSchema = object({
16797
+ success: boolean(),
16798
+ error: string().optional(),
16799
+ renderedAs: RenderedAsSchema.optional()
16800
+ });
16801
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16802
+ var TestResultSchema = SendResultSchema;
16803
+ /**
16804
+ * Pre-save credential probe against a partial (unsaved) target config.
16805
+ * `resolvedUrl` never includes secrets (token stripped from path).
16806
+ */
16807
+ var ProbeTargetConfigResultSchema = object({
16808
+ ok: boolean(),
16809
+ resolvedUrl: string(),
16810
+ httpStatus: number().int().optional(),
16811
+ stateInstance: string().optional(),
16812
+ error: string().optional()
16813
+ });
16814
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
16815
+ kind: string(),
16816
+ config: record(string(), unknown()).optional(),
16817
+ /**
16818
+ * When editing / duplicating, the blank password field never round-trips
16819
+ * the stored token. Pass the source target id so the provider can restore
16820
+ * secrets server-side before dialling (same merge as upsert).
16821
+ */
16822
+ targetId: string().optional()
16823
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
16824
+ kind: string(),
16825
+ config: record(string(), unknown()),
16826
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
16827
+ targetId: string().optional()
16828
+ }), ProbeTargetConfigResultSchema, {
16829
+ kind: "mutation",
16830
+ auth: "admin"
16831
+ }), method(object({
16832
+ targetId: string(),
16833
+ notification: NotificationSchema
16834
+ }), SendResultSchema, {
16835
+ kind: "mutation",
16836
+ auth: "admin"
16837
+ }), method(object({
16838
+ targetId: string(),
16839
+ sample: NotificationSchema.optional()
16840
+ }), TestResultSchema, {
16841
+ kind: "mutation",
16842
+ auth: "admin"
16843
+ }), method(object({
16844
+ target: TargetSchema,
16845
+ /**
16846
+ * Duplicate flow: create a NEW target id while copying secrets from an
16847
+ * existing row. The UI never holds the stored token; blank / redacted
16848
+ * password fields are restored from this source (D634 still applies if
16849
+ * destinations diverge without a re-typed secret).
16850
+ */
16851
+ cloneSecretsFrom: string().optional()
16852
+ }), TargetSchema, {
16853
+ kind: "mutation",
16854
+ auth: "admin"
16855
+ }), method(object({ targetId: string() }), _void(), {
16856
+ kind: "mutation",
16857
+ auth: "admin"
16858
+ }), method(object({
16859
+ targetId: string(),
16860
+ enabled: boolean()
16861
+ }), _void(), {
16862
+ kind: "mutation",
16863
+ auth: "admin"
16864
+ });
16865
+ /**
16866
+ * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16867
+ * surface that admin-ui consumes through `useAddonPagesListPages()`.
16868
+ *
16869
+ * The provider iterates every `addon-pages-source` (collection) provider
16870
+ * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
16871
+ * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
16872
+ * filesystem `mtime` cache-buster lets the browser pick up addon
16873
+ * rebuilds without manual reload.
16874
+ *
16875
+ * The hub-local builtin `addon-pages-aggregator` (see
16876
+ * `@camstack/system/builtins/addon-pages-aggregator`) registers the
16877
+ * provider. Splitting the public aggregator from the raw collection
16878
+ * keeps both ends in codegen — there's no hand-written
16879
+ * `addon-pages.router.ts` wrapper anymore.
16880
+ */
16881
+ var AddonPageDeclarationSchema$1 = object({
16882
+ id: string(),
16883
+ label: string(),
16884
+ icon: string(),
16885
+ path: string(),
16886
+ remoteName: string(),
16887
+ bundle: string(),
16888
+ section: string().optional(),
16889
+ sectionLabel: string().optional()
16890
+ });
16891
+ var AddonPageInfoSchema = object({
16892
+ addonId: string(),
16893
+ page: AddonPageDeclarationSchema$1,
16894
+ bundleUrl: string()
16895
+ });
16896
+ method(_void(), array(AddonPageInfoSchema).readonly());
16897
+ var AddonHttpRouteSchema = object({
16898
+ method: _enum([
16899
+ "GET",
16900
+ "POST",
16901
+ "PUT",
16902
+ "DELETE",
16903
+ "PATCH"
16904
+ ]),
16905
+ path: string(),
16906
+ access: _enum([
16907
+ "public",
16908
+ "authenticated",
16909
+ "admin"
16910
+ ]).optional(),
16911
+ description: string().optional(),
16912
+ /** The hub limits this route per client address before the handler runs. */
16913
+ rateLimit: _enum(["auth"]).optional()
16914
+ });
16915
+ /**
16916
+ * Cross-process route invocation envelope. The hub captures the
16917
+ * request as plain data, ships it to the worker via Moleculer, and
16918
+ * the worker runs the local handler against a capturing reply. The
16919
+ * envelope returned describes what the handler intended (status,
16920
+ * headers, body, or a redirect) so the hub can translate it back to
16921
+ * the Fastify reply that's actually wired to the socket.
16922
+ */
16923
+ var InvokeRequestSchema = object({
16924
+ method: string(),
16925
+ path: string(),
16926
+ params: record(string(), string()),
16927
+ query: record(string(), string()),
16928
+ body: unknown(),
16929
+ headers: record(string(), string()),
16930
+ user: object({
16931
+ id: string(),
16932
+ username: string(),
16933
+ isAdmin: boolean()
16934
+ }).optional(),
16935
+ scopedToken: unknown().optional()
16936
+ });
16937
+ var InvokeReplyEnvelopeSchema = object({
16938
+ status: number().int(),
16939
+ headers: record(string(), string()),
16940
+ /** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
16941
+ * sending `body`. Status defaults to 302 when this is set unless
16942
+ * the handler called `reply.code(...)` explicitly. */
16943
+ redirectUrl: string().nullable(),
16944
+ /** JSON-serializable body. `undefined` is treated as "no body". */
16945
+ body: unknown().optional(),
16946
+ /** Set when the handler called `reply.type(mime)`. */
16947
+ contentType: string().optional()
16948
+ });
16949
+ method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
16950
+ var ConfigTabDeclarationSchema = object({
16951
+ id: string(),
16952
+ label: string(),
16953
+ icon: string(),
16954
+ order: number().optional()
16955
+ });
16956
+ var ConfigSectionWithValuesSchema = object({
16957
+ id: string(),
16958
+ title: string(),
16959
+ titleKey: string().optional(),
16432
16960
  description: string().optional(),
16433
16961
  descriptionKey: string().optional(),
16434
16962
  style: _enum(["card", "accordion"]).optional(),
@@ -18908,7 +19436,11 @@ var CompositionProblemSchema = object({
18908
19436
  "mixed-quantity",
18909
19437
  "helper-mismatch",
18910
19438
  "member-constraint",
18911
- "retention"
19439
+ "retention",
19440
+ "command-method-unknown",
19441
+ "command-source-unknown",
19442
+ "command-input-unknown",
19443
+ "command-input-missing"
18912
19444
  ]),
18913
19445
  path: string(),
18914
19446
  message: string()
@@ -18947,7 +19479,9 @@ var CompositionFieldRoleSchema = _enum([
18947
19479
  var CompositionFieldItemSchema = object({
18948
19480
  arrayPath: string(),
18949
19481
  key: string(),
18950
- label: string()
19482
+ label: string(),
19483
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19484
+ resettable: boolean().optional()
18951
19485
  });
18952
19486
  var CompositionFieldPlanSchema = object({
18953
19487
  cap: string(),
@@ -19152,7 +19686,7 @@ Object.freeze({
19152
19686
  * One catalog row per base reading a group helper can build (D944 §3).
19153
19687
  * Data, not a form default written into the block.
19154
19688
  */
19155
- var NOW$2 = {
19689
+ var NOW$3 = {
19156
19690
  fieldPath: "lastFetchedAt",
19157
19691
  expr: "now"
19158
19692
  };
@@ -19204,7 +19738,7 @@ var GROUP_TEMPLATES = [
19204
19738
  quantity: "temperature",
19205
19739
  defaultAggregate: "mean",
19206
19740
  empty: "null",
19207
- companions: [NOW$2],
19741
+ companions: [NOW$3],
19208
19742
  alsoFrom: {
19209
19743
  deviceTypes: CLIMATE_DEVICES,
19210
19744
  accept: {
@@ -19222,7 +19756,7 @@ var GROUP_TEMPLATES = [
19222
19756
  quantity: "humidity",
19223
19757
  defaultAggregate: "mean",
19224
19758
  empty: "null",
19225
- companions: [NOW$2],
19759
+ companions: [NOW$3],
19226
19760
  alsoFrom: {
19227
19761
  deviceTypes: CLIMATE_DEVICES,
19228
19762
  accept: {
@@ -19240,7 +19774,7 @@ var GROUP_TEMPLATES = [
19240
19774
  quantity: "pressure",
19241
19775
  defaultAggregate: "mean",
19242
19776
  empty: "null",
19243
- companions: [NOW$2]
19777
+ companions: [NOW$3]
19244
19778
  }),
19245
19779
  numeric({
19246
19780
  id: "illuminance",
@@ -19251,7 +19785,7 @@ var GROUP_TEMPLATES = [
19251
19785
  quantity: "illuminance",
19252
19786
  defaultAggregate: "mean",
19253
19787
  empty: "null",
19254
- companions: [NOW$2]
19788
+ companions: [NOW$3]
19255
19789
  }),
19256
19790
  numeric({
19257
19791
  id: "power",
@@ -19262,7 +19796,7 @@ var GROUP_TEMPLATES = [
19262
19796
  quantity: "power",
19263
19797
  defaultAggregate: "sum",
19264
19798
  empty: "omit",
19265
- companions: [NOW$2]
19799
+ companions: [NOW$3]
19266
19800
  }),
19267
19801
  numeric({
19268
19802
  id: "energy",
@@ -19273,7 +19807,7 @@ var GROUP_TEMPLATES = [
19273
19807
  quantity: "energy",
19274
19808
  defaultAggregate: "sum",
19275
19809
  empty: "omit",
19276
- companions: [NOW$2]
19810
+ companions: [NOW$3]
19277
19811
  }),
19278
19812
  numeric({
19279
19813
  id: "battery",
@@ -19308,7 +19842,7 @@ var GROUP_TEMPLATES = [
19308
19842
  quantity: "device",
19309
19843
  defaultAggregate: "mean",
19310
19844
  empty: "null",
19311
- companions: [NOW$2]
19845
+ companions: [NOW$3]
19312
19846
  }),
19313
19847
  binary({
19314
19848
  id: "motion",
@@ -21654,12 +22188,48 @@ var ExposeInputSchema = object({
21654
22188
  capabilities: array(string()).optional()
21655
22189
  });
21656
22190
  var UnexposeInputSchema = object({ deviceId: string() });
22191
+ /**
22192
+ * What an exporter may expose besides devices (D968). An action and a
22193
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22194
+ * exporter's own selection, not the device registry.
22195
+ */
22196
+ var ExportEntityKindSchema = _enum([
22197
+ "device",
22198
+ "action",
22199
+ "notifier"
22200
+ ]);
22201
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22202
+ object({
22203
+ kind: literal("device"),
22204
+ deviceId: string()
22205
+ }),
22206
+ object({
22207
+ kind: literal("action"),
22208
+ actionId: string().min(1)
22209
+ }),
22210
+ object({
22211
+ kind: literal("notifier"),
22212
+ targetId: string().min(1)
22213
+ })
22214
+ ]);
22215
+ var ExposedEntitySchema = object({
22216
+ entity: ExportEntityRefSchema,
22217
+ exposedAs: string().optional()
22218
+ });
22219
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22220
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
21657
22221
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
21658
22222
  kind: "mutation",
21659
22223
  auth: "admin"
21660
22224
  }), method(UnexposeInputSchema, _void(), {
21661
22225
  kind: "mutation",
21662
22226
  auth: "admin"
22227
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22228
+ kind: "mutation",
22229
+ auth: "admin"
22230
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22231
+ kind: "mutation",
22232
+ auth: "admin"
21663
22233
  });
21664
22234
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
21665
22235
  var ICON_CATEGORY_IDS = [
@@ -42800,6 +43370,14 @@ method(object({
42800
43370
  kind: "mutation",
42801
43371
  auth: "admin"
42802
43372
  });
43373
+ method(object({
43374
+ nodeId: string().min(1),
43375
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
43376
+ removedAt: number()
43377
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
43378
+ kind: "mutation",
43379
+ auth: "admin"
43380
+ });
42803
43381
  /**
42804
43382
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
42805
43383
  * so it can delete what it keeps for that device (D865).
@@ -43520,7 +44098,7 @@ var LlmProviderWidgetSchema = object({
43520
44098
  /** ConfigUISchema tree passed through untyped on the wire (the
43521
44099
  * notification-output `ConfigSchemaPassthrough` precedent at
43522
44100
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
43523
- var ConfigSchemaPassthrough$3 = unknown();
44101
+ var ConfigSchemaPassthrough$2 = unknown();
43524
44102
  var LlmProfileKindDescriptorSchema = object({
43525
44103
  kind: LlmProfileKindSchema,
43526
44104
  label: string(),
@@ -43529,9 +44107,9 @@ var LlmProfileKindDescriptorSchema = object({
43529
44107
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
43530
44108
  addonId: string(),
43531
44109
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
43532
- connectionSchema: ConfigSchemaPassthrough$3,
44110
+ connectionSchema: ConfigSchemaPassthrough$2,
43533
44111
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
43534
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44112
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
43535
44113
  /** The kind's defaults; a model from `listModels` may override any of them. */
43536
44114
  features: LlmModelFeaturesSchema,
43537
44115
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44057,10 +44635,10 @@ var CpuBreakdownSchema = object({
44057
44635
  cores: number()
44058
44636
  });
44059
44637
  var MemoryInfoSchema = object({
44060
- percent: number(),
44638
+ percent: number().nullable(),
44061
44639
  totalBytes: number(),
44062
- usedBytes: number(),
44063
- availableBytes: number(),
44640
+ usedBytes: number().nullable(),
44641
+ availableBytes: number().nullable(),
44064
44642
  swapUsedBytes: number(),
44065
44643
  swapTotalBytes: number()
44066
44644
  });
@@ -44330,698 +44908,278 @@ var NodeLoadSeriesSchema = object({
44330
44908
  /**
44331
44909
  * Did the DURABLE tier contribute? `false` means the answer is the hot ring
44332
44910
  * alone — an agent (which holds no table), or a store that refused.
44333
- * Reported because "the last hour" and "the last six hours" are different
44334
- * questions and an operator must not have to guess which was answered.
44335
- */
44336
- durable: boolean()
44337
- });
44338
- var GetLoadSeriesInputSchema = object({
44339
- /**
44340
- * The node whose series is wanted.
44341
- *
44342
- * NOT named `nodeId`: the generated cap router strips a top-level
44343
- * `nodeId` from every method input and uses it to ROUTE the call to
44344
- * that node's provider (`generated-cap-routers.ts`). A series target
44345
- * called `nodeId` would silently become a routing pin and never reach
44346
- * the provider. The hub holds every node it hears from, so the
44347
- * ordinary call is unpinned — answered by the hub, for any node.
44348
- */
44349
- forNodeId: string(),
44350
- /**
44351
- * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
44352
- * holds and receives only what it is missing, so seeding a live chart
44353
- * from this method cannot double a point already drawn.
44354
- */
44355
- sinceMs: number().optional(),
44356
- /**
44357
- * Most points the caller wants PER FUNCTION. The window is reduced to fit,
44358
- * preserving min and max per bucket.
44359
- *
44360
- * Absent means NO reduction — legitimate for a short window and a trap for a
44361
- * long one, which is why a chart passes its own pixel width.
44362
- */
44363
- maxPoints: number().int().positive().optional()
44364
- });
44365
- var SystemMetricsSchema = object({
44366
- cpuPercent: number(),
44367
- memoryPercent: number(),
44368
- memoryUsedMB: number(),
44369
- memoryTotalMB: number(),
44370
- diskPercent: number().optional(),
44371
- temperature: number().optional(),
44372
- gpuPercent: number().optional(),
44373
- gpuMemoryPercent: number().optional()
44374
- });
44375
- method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
44376
- method(object({
44377
- sourceUrl: string(),
44378
- metadata: ModelConvertMetadataSchema,
44379
- targets: array(ConvertTargetSchema).min(1).readonly(),
44380
- calibrationRef: string().optional(),
44381
- sessionId: string().optional()
44382
- }), ConvertResultSchema, {
44383
- kind: "mutation",
44384
- auth: "admin",
44385
- timeoutMs: 6e5
44386
- });
44387
- method(object({
44388
- targetNodeId: string(),
44389
- modelId: string(),
44390
- format: _enum(MODEL_FORMATS),
44391
- entry: ModelCatalogEntrySchema
44392
- }), object({
44393
- ok: boolean(),
44394
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
44395
- sha256: string(),
44396
- bytes: number(),
44397
- /** The target node's modelsDir the artifact landed in. */
44398
- path: string()
44399
- }), {
44400
- kind: "mutation",
44401
- auth: "admin"
44402
- });
44403
- var MotionRegionSchema = object({
44404
- bbox: BoundingBoxSchema,
44405
- pixelCount: number(),
44406
- intensity: number()
44407
- });
44408
- var MotionAnalysisResultSchema = object({
44409
- detected: boolean(),
44410
- regionCount: number(),
44411
- /** Regions passing minArea filter — used for pipeline triggering. */
44412
- regions: array(MotionRegionSchema).readonly(),
44413
- /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
44414
- rawRegions: array(MotionRegionSchema).readonly(),
44415
- frameWidth: number(),
44416
- frameHeight: number(),
44417
- analysisMs: number()
44418
- });
44419
- DeviceType.Camera, method(object({
44420
- deviceId: number(),
44421
- frame: FrameInputSchema.optional(),
44422
- frameHandle: FrameHandleSchema.optional()
44423
- }), MotionAnalysisResultSchema, {
44424
- role: "admin",
44425
- kind: "mutation"
44426
- }), method(object({ deviceId: number() }), _void(), {
44427
- role: "admin",
44428
- kind: "mutation"
44429
- }), method(_void(), _void(), {
44430
- role: "admin",
44431
- kind: "mutation"
44432
- }), object({
44433
- deviceId: number(),
44434
- detected: boolean(),
44435
- regionCount: number()
44436
- });
44437
- /**
44438
- * `mqtt-broker` — broker-registry cap.
44439
- *
44440
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
44441
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
44442
- * and (b) the connection details a consumer addon needs to spin up
44443
- * its OWN `mqtt.js` client.
44444
- *
44445
- * Why: pub/sub routing over the system event-bus loses fidelity
44446
- * (callback shape, QoS guarantees, will/retain semantics) and adds
44447
- * refcount bookkeeping that addons would rather own themselves. The
44448
- * canonical consumer needs raw `mqtt.js`
44449
- * features anyway — give it the connection config, get out of the way.
44450
- *
44451
- * Consumer flow:
44452
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
44453
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
44454
- * client.subscribe('zigbee2mqtt/+')
44455
- *
44456
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
44457
- * cloud bridge). The "embedded" entry (when present) is just another
44458
- * broker in the registry — its lifecycle is owned by the addon that
44459
- * spawned it.
44460
- */
44461
- var BrokerKindSchema = _enum(["external", "embedded"]);
44462
- /**
44463
- * Broker live-probe status.
44464
- *
44465
- * - `connected` — last probe completed a clean CONNACK
44466
- * - `disconnected` — no probe has run yet (cold cache)
44467
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
44468
- * - `unreachable` — TCP connect timed out / refused
44469
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
44470
- */
44471
- var BrokerStatusSchema$1 = _enum([
44472
- "connected",
44473
- "disconnected",
44474
- "auth-failed",
44475
- "unreachable",
44476
- "tls-error"
44477
- ]);
44478
- var BrokerInfoSchema$1 = object({
44479
- id: string(),
44480
- name: string(),
44481
- url: string(),
44482
- kind: BrokerKindSchema,
44483
- status: BrokerStatusSchema$1,
44484
- latencyMs: number().nullable(),
44485
- error: string().optional(),
44486
- /** Embedded brokers only: number of MQTT clients currently connected. */
44487
- connectedClients: number().int().nonnegative().optional(),
44488
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
44489
- lastCheckedAt: number().optional()
44490
- });
44491
- /**
44492
- * Connection details — what a consumer needs to call
44493
- * `mqtt.connect(url, options)`. We split URL + credentials so the
44494
- * consumer can pass them as `mqtt.connect(url, { username, password })`
44495
- * instead of stuffing creds into the URL (which leaks them into logs).
44496
- */
44497
- var BrokerConnectionDetailsSchema = object({
44498
- url: string(),
44499
- username: string().optional(),
44500
- password: string().optional(),
44501
- /**
44502
- * Suggested prefix for `clientId`. Each consumer should suffix this
44503
- * with its own discriminator (addon id, instance id) so reconnects
44504
- * don't kick each other off (MQTT spec: clientId must be unique per
44505
- * broker).
44506
- */
44507
- clientIdPrefix: string().optional()
44508
- });
44509
- var AddBrokerInputSchema = object({
44510
- name: string().min(1),
44511
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
44512
- username: string().optional(),
44513
- password: string().optional(),
44514
- clientIdPrefix: string().optional()
44515
- });
44516
- var AddBrokerResultSchema = object({ id: string() });
44517
- var IdInputSchema = object({ id: string() });
44518
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
44519
- ok: literal(true),
44520
- latencyMs: number()
44521
- }), object({
44522
- ok: literal(false),
44523
- error: string()
44524
- })]);
44525
- var StartEmbeddedInputSchema = object({
44526
- port: number().int().min(1).max(65535).default(1883),
44527
- /** Allow anonymous connect (no username/password). Default: false. */
44528
- allowAnonymous: boolean().default(false),
44529
- /** Optional shared username/password for clients. */
44530
- username: string().optional(),
44531
- password: string().optional()
44532
- });
44533
- var StartEmbeddedResultSchema = object({
44534
- id: string(),
44535
- url: string()
44536
- });
44537
- var StatusSchema = object({
44538
- brokerCount: number(),
44539
- embeddedRunning: boolean()
44540
- });
44541
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
44542
- kind: "mutation",
44543
- auth: "admin"
44544
- }), method(IdInputSchema, _void(), {
44545
- kind: "mutation",
44546
- auth: "admin"
44547
- }), method(IdInputSchema, TestResultSchema$1, {
44548
- kind: "mutation",
44549
- auth: "admin"
44550
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
44551
- kind: "mutation",
44552
- auth: "admin"
44553
- }), method(IdInputSchema, _void(), {
44554
- kind: "mutation",
44555
- auth: "admin"
44556
- }), method(_void(), StatusSchema);
44557
- var NetworkEndpointSchema = object({
44558
- url: string(),
44559
- hostname: string(),
44560
- port: number(),
44561
- protocol: _enum(["http", "https"]),
44562
- /**
44563
- * Whether this address survives a restart of its provider. `false` = it is
44564
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
44565
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
44566
- * it stays visible and an operator may still flag it. Absent = stable: every
44567
- * provider except a Quick tunnel owns a name that does not change.
44568
- */
44569
- stable: boolean().optional()
44570
- });
44571
- object({
44572
- url: string(),
44573
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
44574
- stable: boolean().optional(),
44575
- updated: boolean().optional()
44576
- });
44577
- var NetworkAccessStatusSchema = object({
44578
- connected: boolean(),
44579
- endpoint: NetworkEndpointSchema.nullable(),
44580
- error: string().optional()
44581
- });
44582
- /**
44583
- * Optional, richer endpoint shape returned by providers that expose
44584
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
44585
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44586
- * the originating provider config (mode + sourcePort) so the
44587
- * orchestrator UI can label rows distinctly. Providers that expose only
44588
- * one endpoint just omit `listEndpoints` from their provider impl.
44589
- */
44590
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
44591
- /**
44592
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
44593
- * the orchestrator can dedupe across `listEndpoints` polls.
44594
- */
44595
- id: string(),
44596
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
44597
- label: string(),
44598
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
44599
- mode: string().optional(),
44600
- /** Originating local port the ingress fronts (informational). */
44601
- sourcePort: number().optional()
44602
- });
44603
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
44604
- /**
44605
- * notification-output — canonical, capability-gated notification delivery.
44606
- *
44607
- * Apprise-derived model (see
44608
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44609
- * callers emit ONE canonical `Notification`; each provider declares a
44610
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44611
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44612
- * message to what the kind supports — callers never special-case a service.
44613
- *
44614
- * DESIGN DECISIONS (locked):
44615
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44616
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44617
- * cap. Rationale: the admin UI needs one uniform surface across the
44618
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44619
- * alternative would fork the UI per addon and cannot host the
44620
- * discovery→adopt flow.
44621
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44622
- * the generated cap-mount auto-`concatCollection`-fans them across every
44623
- * registered provider (notifiers addon + HA addon) so one catalog is
44624
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44625
- * `addonId` the generated collection router extracts from the call input.
44626
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44627
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44628
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44629
- * base64 fallback needed.
44630
- *
44631
- * TODO (deferred, closed-set change — separate decision): add
44632
- * `providerKind: 'notify'` so notification providers surface on the unified
44633
- * admin "Integrations" page.
44634
- */
44635
- /**
44636
- * Zentik-derived typed-media enum — the superset across every kind. Each
44637
- * adapter picks what it supports and the degrade engine filters the rest.
44638
- */
44639
- var AttachmentMediaTypeSchema = _enum([
44640
- "image",
44641
- "video",
44642
- "gif",
44643
- "audio",
44644
- "icon"
44645
- ]);
44646
- /**
44647
- * A single attachment. Exactly one of `url` (remote source, most adapters
44648
- * prefer this) or `bytes` (inline source; required for Pushover-style
44649
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44650
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44651
- */
44652
- var AttachmentSchema = object({
44653
- mediaType: AttachmentMediaTypeSchema,
44654
- url: string().optional(),
44655
- bytes: _instanceof(Uint8Array).optional(),
44656
- mime: string().optional(),
44657
- name: string().optional()
44658
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44659
- var NotificationFormatSchema = _enum([
44660
- "text",
44661
- "markdown",
44662
- "html"
44663
- ]);
44664
- /**
44665
- * The CLOSED icon vocabulary an action button may use.
44666
- *
44667
- * A closed set, not a free string, and that is the whole point: an arbitrary
44668
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44669
- * notices — the same class of gap as a zone vocabulary nothing produced
44670
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44671
- * declares `actionIcons: false` and the degrade engine strips the field.
44672
- *
44673
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44674
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44675
- * differently.
44676
- *
44677
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44678
- *
44679
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44680
- * the whole set onto its own renderer's vocabulary through a
44681
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44682
- * fallback, so adding a member here fails every adapter's build until someone
44683
- * decides its glyph, which is the only place that decision can be made
44684
- * honestly.
44685
- *
44686
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44687
- * `disarm` straight through; iOS feeds that string to
44688
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44689
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44690
- * mapping, and "the field is documented" is not "the value renders".
44691
- *
44692
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44693
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44694
- * so an addon that emits a token the running hub does not know does not lose an
44695
- * icon — its whole `send` fails Zod validation and the notification never
44696
- * arrives. Never emit a new token from an addon before the train carrying it.
44697
- */
44698
- var NotificationActionIconSchema = _enum([
44699
- "acknowledge",
44700
- "dismiss",
44701
- "silence",
44702
- "snooze",
44703
- "view",
44704
- "play",
44705
- "open",
44706
- "close",
44707
- "lock",
44708
- "unlock",
44709
- "arm",
44710
- "arm-home",
44711
- "arm-away",
44712
- "arm-night",
44713
- "disarm",
44714
- "light",
44715
- "alert",
44716
- "camera"
44717
- ]);
44718
- /** A single tap-through action button. */
44719
- var NotificationActionSchema = object({
44720
- id: string(),
44721
- label: string(),
44722
- url: string().optional(),
44723
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44724
- icon: NotificationActionIconSchema.optional(),
44725
- /**
44726
- * Renders in a warning style where the notifier supports it.
44727
- *
44728
- * A HINT, never a gate. The callback's authority is its token and nothing
44729
- * else — see `notification-center/action-token.ts` for what that does and
44730
- * does not buy.
44731
- */
44732
- destructive: boolean().optional(),
44733
- /**
44734
- * How the tap should REACH the url.
44735
- *
44736
- * `navigate` (absent, and every button authored before this field) opens it:
44737
- * the phone leaves the notification and shows whatever the callback returns.
44738
- * That is right for a button whose answer the operator wants to read.
44739
- *
44740
- * `background` fires it as a POST and stays put. It exists for the buttons
44741
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44742
- * an answer to the notification, and being thrown into a browser tab to
44743
- * confirm it costs more attention than the notification did. A backend that
44744
- * cannot do a background call renders it as an ordinary link (the adapters
44745
- * fall back rather than dropping the button), so this is a preference, never
44746
- * a requirement.
44747
- */
44748
- mode: _enum(["navigate", "background"]).optional()
44749
- });
44750
- /** A delivery parameter value: what a notifier declared it takes. */
44751
- var DeliveryParamValueSchema = union([
44752
- string(),
44753
- number(),
44754
- boolean()
44755
- ]);
44756
- /**
44757
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44758
- * addresses a target of that kind may set it; a kind that declares nothing is
44759
- * never handed one. The admin UI derives its control from this descriptor —
44760
- * no editor knows a service by name.
44761
- *
44762
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44763
- * run on the addon cap path).
44764
- */
44765
- var DeliveryParamSchema = object({
44766
- key: string().min(1),
44767
- label: string(),
44768
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44769
- labelKey: string().optional(),
44770
- type: _enum([
44771
- "enum",
44772
- "number",
44773
- "boolean",
44774
- "string"
44775
- ]),
44776
- /** `enum` only: the closed set of accepted values. */
44777
- options: array(object({
44778
- value: string(),
44779
- label: string()
44780
- })).optional(),
44781
- /** `number` only. */
44782
- min: number().optional(),
44783
- max: number().optional(),
44784
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44785
- default: DeliveryParamValueSchema.optional()
44786
- });
44787
- /**
44788
- * The canonical notification. `body` is the only hard field (Apprise model).
44789
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44790
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44791
- * the adapter maps this ordinal onto its native level. `level?` is an
44792
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44793
- * `priority` for that one target.
44794
- */
44795
- var NotificationSchema = object({
44796
- body: string(),
44797
- title: string().optional(),
44798
- format: NotificationFormatSchema.default("text"),
44799
- priority: number().int().min(1).max(5).default(3),
44800
- level: string().optional(),
44801
- attachments: array(AttachmentSchema).optional(),
44802
- clickUrl: string().optional(),
44803
- actions: array(NotificationActionSchema).optional(),
44804
- sound: string().optional(),
44805
- ttl: number().optional(),
44806
- tag: string().optional(),
44807
- deviceId: number().optional(),
44808
- eventId: string().optional(),
44809
- metadata: record(string(), unknown()).optional(),
44810
- /**
44811
- * Values for the parameters the target's kind DECLARES in
44812
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44813
- * declaration by the sender; a notifier reads only the keys it declared.
44814
- */
44815
- delivery: record(string(), DeliveryParamValueSchema).optional()
44816
- });
44817
- /** One declared native severity/priority level for a kind. */
44818
- var TargetKindLevelSchema = object({
44819
- id: string(),
44820
- label: string(),
44821
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44822
- ordinal: number().int().min(1).max(5).nullable(),
44823
- flags: object({
44824
- critical: boolean().optional(),
44825
- silent: boolean().optional(),
44826
- noPush: boolean().optional()
44827
- }).optional(),
44828
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44829
- requires: array(string()).optional(),
44830
- description: string().optional()
44831
- });
44832
- /** The full capability block consulted before dispatch. */
44833
- var TargetKindCapsSchema = object({
44834
- attachments: object({
44835
- mediaTypes: array(AttachmentMediaTypeSchema),
44836
- mode: _enum([
44837
- "url",
44838
- "bytes",
44839
- "both"
44840
- ]),
44841
- max: number().int().nonnegative(),
44842
- maxBytes: number().int().positive().optional()
44843
- }),
44844
- /** Max action buttons (0 = none). */
44845
- actions: number().int().nonnegative(),
44846
- /**
44847
- * Whether this kind renders a per-action ICON.
44848
- *
44849
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44850
- * run on the addon cap path — three production failures in one day taught
44851
- * this repo that once. Absent is read as false by the degrade engine, which
44852
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44853
- * assumed and dropped costs the operator's trust in the field.
44854
- */
44855
- actionIcons: boolean().optional(),
44856
- levels: array(TargetKindLevelSchema),
44857
- format: array(NotificationFormatSchema),
44858
- clickUrl: boolean(),
44859
- sound: boolean(),
44860
- ttl: boolean(),
44861
- bodyMaxLen: number().int().positive()
44862
- });
44863
- /**
44864
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44865
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44866
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44867
- * the union is large and not meant for runtime validation here; the exported
44868
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44869
- */
44870
- var ConfigSchemaPassthrough$2 = unknown();
44871
- var TargetKindSchema = object({
44872
- kind: string(),
44873
- label: string(),
44874
- icon: string(),
44875
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44876
- addonId: string(),
44877
- /**
44878
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44879
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44880
- * when the addon bundles no icon for that kind — the client then falls back
44881
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44882
- *
44883
- * Root-relative on purpose: it resolves against whatever origin serves a web
44884
- * client, and a native client joins it onto its own hub base.
44885
- *
44886
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44887
- * field that survived only because the runtime cap-router forwards provider
44888
- * output verbatim — so every consumer had to re-declare it by hand to stop
44889
- * its own Zod parse from stripping it, and the whole arrangement would have
44890
- * broken silently the moment output validation was tightened anywhere.
44891
- */
44892
- iconUrl: string().optional(),
44893
- /**
44894
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44895
- *
44896
- * The server knows this and therefore says it, because the client cannot
44897
- * safely guess: a React-Native client renders SVG and raster through two
44898
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44899
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44900
- * placeholder glyph for every vector icon while the web build looked fine.
44901
- *
44902
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44903
- * not been updated — a client that cannot determine the type should prefer
44904
- * its raster path, which is the safe default for an unknown image.
44911
+ * Reported because "the last hour" and "the last six hours" are different
44912
+ * questions and an operator must not have to guess which was answered.
44905
44913
  */
44906
- iconMediaType: string().optional(),
44907
- configSchema: ConfigSchemaPassthrough$2,
44914
+ durable: boolean()
44915
+ });
44916
+ var GetLoadSeriesInputSchema = object({
44908
44917
  /**
44909
- * The config keys that decide WHERE this kind's secrets are sent (server,
44910
- * base URL, instance, chat, recipient). A save that changes any of them
44911
- * while leaving a secret redacted is refused by the provider (D634), so an
44912
- * editor uses this to ask for the secret on exactly those edits instead of
44913
- * on every edit.
44918
+ * The node whose series is wanted.
44914
44919
  *
44915
- * Optional: absent from a provider that holds no secret (the Home Assistant
44916
- * kinds) and from a provider older than this field — an editor that finds
44917
- * it absent must assume ANY edit may need the secret.
44920
+ * NOT named `nodeId`: the generated cap router strips a top-level
44921
+ * `nodeId` from every method input and uses it to ROUTE the call to
44922
+ * that node's provider (`generated-cap-routers.ts`). A series target
44923
+ * called `nodeId` would silently become a routing pin and never reach
44924
+ * the provider. The hub holds every node it hears from, so the
44925
+ * ordinary call is unpinned — answered by the hub, for any node.
44918
44926
  */
44919
- destinationKeys: array(string()).optional(),
44920
- supportsDiscovery: boolean(),
44927
+ forNodeId: string(),
44921
44928
  /**
44922
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44923
- * Absent = none; the rule editor then offers nothing.
44929
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
44930
+ * holds and receives only what it is missing, so seeding a live chart
44931
+ * from this method cannot double a point already drawn.
44924
44932
  */
44925
- deliveryParams: array(DeliveryParamSchema).optional(),
44926
- caps: TargetKindCapsSchema
44933
+ sinceMs: number().optional(),
44934
+ /**
44935
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
44936
+ * preserving min and max per bucket.
44937
+ *
44938
+ * Absent means NO reduction — legitimate for a short window and a trap for a
44939
+ * long one, which is why a chart passes its own pixel width.
44940
+ */
44941
+ maxPoints: number().int().positive().optional()
44942
+ });
44943
+ var SystemMetricsSchema = object({
44944
+ cpuPercent: number(),
44945
+ /** `null` when the memory sample failed or was not taken yet (D393). */
44946
+ memoryPercent: number().nullable(),
44947
+ memoryUsedMB: number().nullable(),
44948
+ memoryTotalMB: number(),
44949
+ diskPercent: number().optional(),
44950
+ temperature: number().optional(),
44951
+ gpuPercent: number().optional(),
44952
+ gpuMemoryPercent: number().optional()
44953
+ });
44954
+ method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
44955
+ method(object({
44956
+ sourceUrl: string(),
44957
+ metadata: ModelConvertMetadataSchema,
44958
+ targets: array(ConvertTargetSchema).min(1).readonly(),
44959
+ calibrationRef: string().optional(),
44960
+ sessionId: string().optional()
44961
+ }), ConvertResultSchema, {
44962
+ kind: "mutation",
44963
+ auth: "admin",
44964
+ timeoutMs: 6e5
44965
+ });
44966
+ method(object({
44967
+ targetNodeId: string(),
44968
+ modelId: string(),
44969
+ format: _enum(MODEL_FORMATS),
44970
+ entry: ModelCatalogEntrySchema
44971
+ }), object({
44972
+ ok: boolean(),
44973
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
44974
+ sha256: string(),
44975
+ bytes: number(),
44976
+ /** The target node's modelsDir the artifact landed in. */
44977
+ path: string()
44978
+ }), {
44979
+ kind: "mutation",
44980
+ auth: "admin"
44981
+ });
44982
+ var MotionRegionSchema = object({
44983
+ bbox: BoundingBoxSchema,
44984
+ pixelCount: number(),
44985
+ intensity: number()
44986
+ });
44987
+ var MotionAnalysisResultSchema = object({
44988
+ detected: boolean(),
44989
+ regionCount: number(),
44990
+ /** Regions passing minArea filter — used for pipeline triggering. */
44991
+ regions: array(MotionRegionSchema).readonly(),
44992
+ /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
44993
+ rawRegions: array(MotionRegionSchema).readonly(),
44994
+ frameWidth: number(),
44995
+ frameHeight: number(),
44996
+ analysisMs: number()
44997
+ });
44998
+ DeviceType.Camera, method(object({
44999
+ deviceId: number(),
45000
+ frame: FrameInputSchema.optional(),
45001
+ frameHandle: FrameHandleSchema.optional()
45002
+ }), MotionAnalysisResultSchema, {
45003
+ role: "admin",
45004
+ kind: "mutation"
45005
+ }), method(object({ deviceId: number() }), _void(), {
45006
+ role: "admin",
45007
+ kind: "mutation"
45008
+ }), method(_void(), _void(), {
45009
+ role: "admin",
45010
+ kind: "mutation"
45011
+ }), object({
45012
+ deviceId: number(),
45013
+ detected: boolean(),
45014
+ regionCount: number()
44927
45015
  });
44928
45016
  /**
44929
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44930
- * (return a presence marker only) when serving `listTargets` — never
44931
- * round-trip a stored secret to the UI.
45017
+ * `mqtt-broker` — broker-registry cap.
45018
+ *
45019
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
45020
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
45021
+ * and (b) the connection details a consumer addon needs to spin up
45022
+ * its OWN `mqtt.js` client.
45023
+ *
45024
+ * Why: pub/sub routing over the system event-bus loses fidelity
45025
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
45026
+ * refcount bookkeeping that addons would rather own themselves. The
45027
+ * canonical consumer needs raw `mqtt.js`
45028
+ * features anyway — give it the connection config, get out of the way.
45029
+ *
45030
+ * Consumer flow:
45031
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
45032
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
45033
+ * client.subscribe('zigbee2mqtt/+')
45034
+ *
45035
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
45036
+ * cloud bridge). The "embedded" entry (when present) is just another
45037
+ * broker in the registry — its lifecycle is owned by the addon that
45038
+ * spawned it.
44932
45039
  */
44933
- var TargetSchema = object({
45040
+ var BrokerKindSchema = _enum(["external", "embedded"]);
45041
+ /**
45042
+ * Broker live-probe status.
45043
+ *
45044
+ * - `connected` — last probe completed a clean CONNACK
45045
+ * - `disconnected` — no probe has run yet (cold cache)
45046
+ * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
45047
+ * - `unreachable` — TCP connect timed out / refused
45048
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
45049
+ */
45050
+ var BrokerStatusSchema$1 = _enum([
45051
+ "connected",
45052
+ "disconnected",
45053
+ "auth-failed",
45054
+ "unreachable",
45055
+ "tls-error"
45056
+ ]);
45057
+ var BrokerInfoSchema$1 = object({
44934
45058
  id: string(),
44935
45059
  name: string(),
44936
- kind: string(),
44937
- addonId: string(),
44938
- enabled: boolean(),
44939
- config: record(string(), unknown())
44940
- });
44941
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44942
- var DiscoveredTargetSchema = object({
44943
- kind: string(),
44944
- suggestedName: string(),
44945
- config: record(string(), unknown())
44946
- });
44947
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44948
- var RenderedAsSchema = object({
44949
- level: string(),
44950
- format: NotificationFormatSchema,
44951
- attachmentsSent: number().int().nonnegative(),
44952
- actionsSent: number().int().nonnegative(),
44953
- truncated: boolean(),
44954
- dropped: array(string())
44955
- });
44956
- var SendResultSchema = object({
44957
- success: boolean(),
45060
+ url: string(),
45061
+ kind: BrokerKindSchema,
45062
+ status: BrokerStatusSchema$1,
45063
+ latencyMs: number().nullable(),
44958
45064
  error: string().optional(),
44959
- renderedAs: RenderedAsSchema.optional()
45065
+ /** Embedded brokers only: number of MQTT clients currently connected. */
45066
+ connectedClients: number().int().nonnegative().optional(),
45067
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45068
+ lastCheckedAt: number().optional()
44960
45069
  });
44961
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44962
- var TestResultSchema = SendResultSchema;
44963
45070
  /**
44964
- * Pre-save credential probe against a partial (unsaved) target config.
44965
- * `resolvedUrl` never includes secrets (token stripped from path).
45071
+ * Connection details — what a consumer needs to call
45072
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
45073
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
45074
+ * instead of stuffing creds into the URL (which leaks them into logs).
44966
45075
  */
44967
- var ProbeTargetConfigResultSchema = object({
44968
- ok: boolean(),
44969
- resolvedUrl: string(),
44970
- httpStatus: number().int().optional(),
44971
- stateInstance: string().optional(),
44972
- error: string().optional()
44973
- });
44974
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44975
- kind: string(),
44976
- config: record(string(), unknown()).optional(),
45076
+ var BrokerConnectionDetailsSchema = object({
45077
+ url: string(),
45078
+ username: string().optional(),
45079
+ password: string().optional(),
44977
45080
  /**
44978
- * When editing / duplicating, the blank password field never round-trips
44979
- * the stored token. Pass the source target id so the provider can restore
44980
- * secrets server-side before dialling (same merge as upsert).
45081
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45082
+ * with its own discriminator (addon id, instance id) so reconnects
45083
+ * don't kick each other off (MQTT spec: clientId must be unique per
45084
+ * broker).
44981
45085
  */
44982
- targetId: string().optional()
44983
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44984
- kind: string(),
44985
- config: record(string(), unknown()),
44986
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44987
- targetId: string().optional()
44988
- }), ProbeTargetConfigResultSchema, {
44989
- kind: "mutation",
44990
- auth: "admin"
44991
- }), method(object({
44992
- targetId: string(),
44993
- notification: NotificationSchema
44994
- }), SendResultSchema, {
45086
+ clientIdPrefix: string().optional()
45087
+ });
45088
+ var AddBrokerInputSchema = object({
45089
+ name: string().min(1),
45090
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45091
+ username: string().optional(),
45092
+ password: string().optional(),
45093
+ clientIdPrefix: string().optional()
45094
+ });
45095
+ var AddBrokerResultSchema = object({ id: string() });
45096
+ var IdInputSchema = object({ id: string() });
45097
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45098
+ ok: literal(true),
45099
+ latencyMs: number()
45100
+ }), object({
45101
+ ok: literal(false),
45102
+ error: string()
45103
+ })]);
45104
+ var StartEmbeddedInputSchema = object({
45105
+ port: number().int().min(1).max(65535).default(1883),
45106
+ /** Allow anonymous connect (no username/password). Default: false. */
45107
+ allowAnonymous: boolean().default(false),
45108
+ /** Optional shared username/password for clients. */
45109
+ username: string().optional(),
45110
+ password: string().optional()
45111
+ });
45112
+ var StartEmbeddedResultSchema = object({
45113
+ id: string(),
45114
+ url: string()
45115
+ });
45116
+ var StatusSchema = object({
45117
+ brokerCount: number(),
45118
+ embeddedRunning: boolean()
45119
+ });
45120
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
44995
45121
  kind: "mutation",
44996
45122
  auth: "admin"
44997
- }), method(object({
44998
- targetId: string(),
44999
- sample: NotificationSchema.optional()
45000
- }), TestResultSchema, {
45123
+ }), method(IdInputSchema, _void(), {
45001
45124
  kind: "mutation",
45002
45125
  auth: "admin"
45003
- }), method(object({
45004
- target: TargetSchema,
45005
- /**
45006
- * Duplicate flow: create a NEW target id while copying secrets from an
45007
- * existing row. The UI never holds the stored token; blank / redacted
45008
- * password fields are restored from this source (D634 still applies if
45009
- * destinations diverge without a re-typed secret).
45010
- */
45011
- cloneSecretsFrom: string().optional()
45012
- }), TargetSchema, {
45126
+ }), method(IdInputSchema, TestResultSchema$1, {
45013
45127
  kind: "mutation",
45014
45128
  auth: "admin"
45015
- }), method(object({ targetId: string() }), _void(), {
45129
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45016
45130
  kind: "mutation",
45017
45131
  auth: "admin"
45018
- }), method(object({
45019
- targetId: string(),
45020
- enabled: boolean()
45021
- }), _void(), {
45132
+ }), method(IdInputSchema, _void(), {
45022
45133
  kind: "mutation",
45023
45134
  auth: "admin"
45135
+ }), method(_void(), StatusSchema);
45136
+ var NetworkEndpointSchema = object({
45137
+ url: string(),
45138
+ hostname: string(),
45139
+ port: number(),
45140
+ protocol: _enum(["http", "https"]),
45141
+ /**
45142
+ * Whether this address survives a restart of its provider. `false` = it is
45143
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45144
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45145
+ * it stays visible and an operator may still flag it. Absent = stable: every
45146
+ * provider except a Quick tunnel owns a name that does not change.
45147
+ */
45148
+ stable: boolean().optional()
45149
+ });
45150
+ object({
45151
+ url: string(),
45152
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45153
+ stable: boolean().optional(),
45154
+ updated: boolean().optional()
45155
+ });
45156
+ var NetworkAccessStatusSchema = object({
45157
+ connected: boolean(),
45158
+ endpoint: NetworkEndpointSchema.nullable(),
45159
+ error: string().optional()
45160
+ });
45161
+ /**
45162
+ * Optional, richer endpoint shape returned by providers that expose
45163
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45164
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45165
+ * the originating provider config (mode + sourcePort) so the
45166
+ * orchestrator UI can label rows distinctly. Providers that expose only
45167
+ * one endpoint just omit `listEndpoints` from their provider impl.
45168
+ */
45169
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45170
+ /**
45171
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45172
+ * the orchestrator can dedupe across `listEndpoints` polls.
45173
+ */
45174
+ id: string(),
45175
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45176
+ label: string(),
45177
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45178
+ mode: string().optional(),
45179
+ /** Originating local port the ingress fronts (informational). */
45180
+ sourcePort: number().optional()
45024
45181
  });
45182
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45025
45183
  var AUDIO_MACRO_LABELS = [
45026
45184
  {
45027
45185
  id: "speech",
@@ -51422,14 +51580,6 @@ var EngineProvisioningSchema = object({
51422
51580
  */
51423
51581
  configIssues: array(string()).optional()
51424
51582
  });
51425
- var PipelineStepInputSchema = lazy(() => object({
51426
- addonId: string(),
51427
- modelId: string().optional(),
51428
- enabled: boolean().default(true),
51429
- children: array(PipelineStepInputSchema).optional(),
51430
- settings: record(string(), unknown()).optional(),
51431
- jumpDeviceKey: string().optional()
51432
- }));
51433
51583
  var ModelSubstitutionSchema = object({
51434
51584
  addonId: string(),
51435
51585
  chosen: string(),
@@ -51508,7 +51658,6 @@ var ConfigUISchemaBridge = custom();
51508
51658
  var ConfigUISchemaNullableBridge = custom();
51509
51659
  var InferenceCapabilitiesBridge = custom();
51510
51660
  var ModelAvailabilityListBridge = custom();
51511
- var PipelineRunResultBridge = custom();
51512
51661
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
51513
51662
  modelId: string(),
51514
51663
  settings: record(string(), unknown()).readonly()
@@ -59182,7 +59331,7 @@ object({
59182
59331
  /** Ms epoch of the last value received from the appliance; 0 = never. */
59183
59332
  lastFetchedAt: number()
59184
59333
  });
59185
- var DeviceIdSchema = number().int().nonnegative();
59334
+ var DeviceIdSchema$1 = number().int().nonnegative();
59186
59335
  /** A value a program option or setting can take. */
59187
59336
  var DishwasherValueSchema = union([
59188
59337
  boolean(),
@@ -59246,77 +59395,77 @@ var DishwasherOptionsSchema = object({
59246
59395
  options: array(DishwasherOptionDescriptorSchema)
59247
59396
  });
59248
59397
  DeviceType.Dishwasher, method(object({
59249
- deviceId: DeviceIdSchema,
59398
+ deviceId: DeviceIdSchema$1,
59250
59399
  program: string().min(1).optional(),
59251
59400
  options: record(string(), DishwasherValueSchema).optional()
59252
59401
  }), _void(), {
59253
59402
  kind: "mutation",
59254
59403
  auth: "protected"
59255
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59404
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59256
59405
  kind: "mutation",
59257
59406
  auth: "protected"
59258
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59407
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59259
59408
  kind: "mutation",
59260
59409
  auth: "protected"
59261
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59410
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59262
59411
  kind: "mutation",
59263
59412
  auth: "protected"
59264
59413
  }), method(object({
59265
- deviceId: DeviceIdSchema,
59414
+ deviceId: DeviceIdSchema$1,
59266
59415
  program: string().min(1)
59267
59416
  }), _void(), {
59268
59417
  kind: "mutation",
59269
59418
  auth: "protected"
59270
59419
  }), method(object({
59271
- deviceId: DeviceIdSchema,
59420
+ deviceId: DeviceIdSchema$1,
59272
59421
  on: boolean()
59273
59422
  }), _void(), {
59274
59423
  kind: "mutation",
59275
59424
  auth: "protected"
59276
59425
  }), method(object({
59277
- deviceId: DeviceIdSchema,
59426
+ deviceId: DeviceIdSchema$1,
59278
59427
  on: boolean()
59279
59428
  }), _void(), {
59280
59429
  kind: "mutation",
59281
59430
  auth: "protected"
59282
59431
  }), method(object({
59283
- deviceId: DeviceIdSchema,
59432
+ deviceId: DeviceIdSchema$1,
59284
59433
  on: boolean()
59285
59434
  }), _void(), {
59286
59435
  kind: "mutation",
59287
59436
  auth: "protected"
59288
59437
  }), method(object({
59289
- deviceId: DeviceIdSchema,
59438
+ deviceId: DeviceIdSchema$1,
59290
59439
  on: boolean()
59291
59440
  }), _void(), {
59292
59441
  kind: "mutation",
59293
59442
  auth: "protected"
59294
59443
  }), method(object({
59295
- deviceId: DeviceIdSchema,
59444
+ deviceId: DeviceIdSchema$1,
59296
59445
  on: boolean()
59297
59446
  }), _void(), {
59298
59447
  kind: "mutation",
59299
59448
  auth: "protected"
59300
59449
  }), method(object({
59301
- deviceId: DeviceIdSchema,
59450
+ deviceId: DeviceIdSchema$1,
59302
59451
  on: boolean()
59303
59452
  }), _void(), {
59304
59453
  kind: "mutation",
59305
59454
  auth: "protected"
59306
59455
  }), method(object({
59307
- deviceId: DeviceIdSchema,
59456
+ deviceId: DeviceIdSchema$1,
59308
59457
  on: boolean()
59309
59458
  }), _void(), {
59310
59459
  kind: "mutation",
59311
59460
  auth: "protected"
59312
59461
  }), method(object({
59313
- deviceId: DeviceIdSchema,
59462
+ deviceId: DeviceIdSchema$1,
59314
59463
  key: string().min(1),
59315
59464
  value: DishwasherValueSchema
59316
59465
  }), _void(), {
59317
59466
  kind: "mutation",
59318
59467
  auth: "protected"
59319
- }), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
59468
+ }), method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" });
59320
59469
  DeviceType.Camera;
59321
59470
  object({
59322
59471
  /** Ms epoch of the last press. null = never observed since this provider started. */
@@ -61754,7 +61903,8 @@ var TopologyNodeSchema = object({
61754
61903
  isHub: boolean(),
61755
61904
  isOnline: boolean(),
61756
61905
  cpuPercent: number(),
61757
- memoryPercent: number(),
61906
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
61907
+ memoryPercent: number().nullable(),
61758
61908
  uptime: number(),
61759
61909
  lastSeen: string(),
61760
61910
  localIps: array(string()).readonly(),
@@ -62289,9 +62439,10 @@ object({
62289
62439
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
62290
62440
  * the device has not reported yet. */
62291
62441
  foodLevel: number().nullable().meta(readingMeta("none")),
62292
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
62293
- * feeder's own units. Null when the model reports none. */
62294
- bowlFood: number().nullable().meta(readingMeta("none")),
62442
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
62443
+ * feeder with a bowl scale; null when the model has none or its reading is
62444
+ * of an unproven unit. */
62445
+ bowlFood: number().nullable().meta(readingMeta("mass")),
62295
62446
  /** True while the pet is eating (`state.eating`). */
62296
62447
  eating: boolean(),
62297
62448
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -62404,6 +62555,64 @@ DeviceType.PetFeeder, method(object({
62404
62555
  kind: "mutation",
62405
62556
  auth: "protected"
62406
62557
  });
62558
+ object({
62559
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
62560
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62561
+ /** Water in the tank, in litres. Null while the source has not reported. */
62562
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
62563
+ /** True when the tank is empty. Null while unknown. */
62564
+ empty: boolean().nullable(),
62565
+ /** True while the pump is running. Null while unknown. */
62566
+ pumpRunning: boolean().nullable(),
62567
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
62568
+ pumpIntervalEnabled: boolean().nullable(),
62569
+ /** How long the pump runs per cycle, in seconds. */
62570
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
62571
+ /** How long the pump rests per cycle, in seconds. */
62572
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
62573
+ /** Water drunk in the last drink, in litres. */
62574
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
62575
+ /** Water drunk in the current hour, in litres. */
62576
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
62577
+ /** Water drunk over the last 24 hours (rolling), in litres. */
62578
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
62579
+ /** Water drunk since local midnight, in litres. */
62580
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
62581
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
62582
+ error: string().nullable(),
62583
+ /** Ms epoch when the slice was last refreshed. */
62584
+ lastFetchedAt: number()
62585
+ });
62586
+ var DeviceIdSchema = number().int().nonnegative();
62587
+ var MinutesSchema = number().int().min(0);
62588
+ DeviceType.PetFountain, method(object({
62589
+ deviceId: DeviceIdSchema,
62590
+ on: boolean()
62591
+ }), _void(), {
62592
+ kind: "mutation",
62593
+ auth: "protected"
62594
+ }), method(object({
62595
+ deviceId: DeviceIdSchema,
62596
+ on: boolean()
62597
+ }), _void(), {
62598
+ kind: "mutation",
62599
+ auth: "protected"
62600
+ }), method(object({
62601
+ deviceId: DeviceIdSchema,
62602
+ minutes: MinutesSchema
62603
+ }), _void(), {
62604
+ kind: "mutation",
62605
+ auth: "protected"
62606
+ }), method(object({
62607
+ deviceId: DeviceIdSchema,
62608
+ minutes: MinutesSchema
62609
+ }), _void(), {
62610
+ kind: "mutation",
62611
+ auth: "protected"
62612
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
62613
+ kind: "mutation",
62614
+ auth: "protected"
62615
+ });
62407
62616
  var VehicleSchema = object({
62408
62617
  id: string(),
62409
62618
  name: string(),
@@ -67103,7 +67312,7 @@ discriminatedUnion("kind", [object({
67103
67312
  })]);
67104
67313
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
67105
67314
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
67106
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
67315
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
67107
67316
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
67108
67317
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
67109
67318
  new Set(CAP_NAMES);
@@ -67156,7 +67365,7 @@ var AccessoryKind = {
67156
67365
  PrivacyMask: DeviceRole.PrivacyMask
67157
67366
  };
67158
67367
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
67159
- 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, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
67368
+ 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, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
67160
67369
  new Set(Object.values(DeviceType));
67161
67370
  Object.freeze({
67162
67371
  "accessories.getStatus": {
@@ -69091,6 +69300,13 @@ Object.freeze({
69091
69300
  access: "create",
69092
69301
  role: "admin"
69093
69302
  },
69303
+ "deviceExport.exposeEntity": {
69304
+ capName: "device-export",
69305
+ capScope: "system",
69306
+ addonId: null,
69307
+ access: "create",
69308
+ role: "admin"
69309
+ },
69094
69310
  "deviceExport.getDeviceLiveContribution": {
69095
69311
  capName: "device-export",
69096
69312
  capScope: "system",
@@ -69119,6 +69335,13 @@ Object.freeze({
69119
69335
  access: "view",
69120
69336
  role: "admin"
69121
69337
  },
69338
+ "deviceExport.listExposedEntities": {
69339
+ capName: "device-export",
69340
+ capScope: "system",
69341
+ addonId: null,
69342
+ access: "view",
69343
+ role: "admin"
69344
+ },
69122
69345
  "deviceExport.listSupportedDeviceKinds": {
69123
69346
  capName: "device-export",
69124
69347
  capScope: "system",
@@ -69126,6 +69349,13 @@ Object.freeze({
69126
69349
  access: "view",
69127
69350
  role: "admin"
69128
69351
  },
69352
+ "deviceExport.listSupportedEntityKinds": {
69353
+ capName: "device-export",
69354
+ capScope: "system",
69355
+ addonId: null,
69356
+ access: "view",
69357
+ role: "admin"
69358
+ },
69129
69359
  "deviceExport.unexposeDevice": {
69130
69360
  capName: "device-export",
69131
69361
  capScope: "system",
@@ -69133,6 +69363,13 @@ Object.freeze({
69133
69363
  access: "create",
69134
69364
  role: "admin"
69135
69365
  },
69366
+ "deviceExport.unexposeEntity": {
69367
+ capName: "device-export",
69368
+ capScope: "system",
69369
+ addonId: null,
69370
+ access: "create",
69371
+ role: "admin"
69372
+ },
69136
69373
  "deviceExtension.listCandidates": {
69137
69374
  capName: "device-extension",
69138
69375
  capScope: "device",
@@ -71954,6 +72191,13 @@ Object.freeze({
71954
72191
  access: "create",
71955
72192
  role: "admin"
71956
72193
  },
72194
+ "nodeRemovalHook.onNodeRemoved": {
72195
+ capName: "node-removal-hook",
72196
+ capScope: "system",
72197
+ addonId: null,
72198
+ access: "create",
72199
+ role: "admin"
72200
+ },
71957
72201
  "nodes.clusterAddonStatus": {
71958
72202
  capName: "nodes",
71959
72203
  capScope: "system",
@@ -72591,6 +72835,48 @@ Object.freeze({
72591
72835
  access: "create",
72592
72836
  role: "viewer"
72593
72837
  },
72838
+ "petFountain.getStatus": {
72839
+ capName: "pet-fountain",
72840
+ capScope: "device",
72841
+ addonId: null,
72842
+ access: "view",
72843
+ role: "viewer"
72844
+ },
72845
+ "petFountain.resetDailyConsumption": {
72846
+ capName: "pet-fountain",
72847
+ capScope: "device",
72848
+ addonId: null,
72849
+ access: "delete",
72850
+ role: "viewer"
72851
+ },
72852
+ "petFountain.setPump": {
72853
+ capName: "pet-fountain",
72854
+ capScope: "device",
72855
+ addonId: null,
72856
+ access: "create",
72857
+ role: "viewer"
72858
+ },
72859
+ "petFountain.setPumpInterval": {
72860
+ capName: "pet-fountain",
72861
+ capScope: "device",
72862
+ addonId: null,
72863
+ access: "create",
72864
+ role: "viewer"
72865
+ },
72866
+ "petFountain.setPumpOffMinutes": {
72867
+ capName: "pet-fountain",
72868
+ capScope: "device",
72869
+ addonId: null,
72870
+ access: "create",
72871
+ role: "viewer"
72872
+ },
72873
+ "petFountain.setPumpOnMinutes": {
72874
+ capName: "pet-fountain",
72875
+ capScope: "device",
72876
+ addonId: null,
72877
+ access: "create",
72878
+ role: "viewer"
72879
+ },
72594
72880
  "pipelineAnalytics.addRetrainFrameFromRecording": {
72595
72881
  capName: "pipeline-analytics",
72596
72882
  capScope: "device",
@@ -79073,6 +79359,100 @@ var APPLIANCE_CAP_VOCABULARY = {
79073
79359
  }
79074
79360
  ]
79075
79361
  },
79362
+ "pet-fountain": {
79363
+ scope: "device",
79364
+ noun: t("Pet fountain", "Fontana per animali"),
79365
+ actions: [
79366
+ {
79367
+ id: "petFountain.pumpOn",
79368
+ method: "setPump",
79369
+ label: t("Pump on", "Pompa accesa"),
79370
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
79371
+ icon: "play",
79372
+ args: { on: true }
79373
+ },
79374
+ {
79375
+ id: "petFountain.pumpOff",
79376
+ method: "setPump",
79377
+ label: t("Pump off", "Pompa spenta"),
79378
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
79379
+ icon: "square",
79380
+ args: { on: false }
79381
+ },
79382
+ {
79383
+ id: "petFountain.intervalOn",
79384
+ method: "setPumpInterval",
79385
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
79386
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
79387
+ icon: "timer",
79388
+ args: { on: true }
79389
+ },
79390
+ {
79391
+ id: "petFountain.intervalOff",
79392
+ method: "setPumpInterval",
79393
+ label: t("Pump cycle off", "Ciclo pompa spento"),
79394
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
79395
+ icon: "timer-off",
79396
+ args: { on: false }
79397
+ },
79398
+ {
79399
+ id: "petFountain.pumpOnMinutes",
79400
+ method: "setPumpOnMinutes",
79401
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
79402
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
79403
+ icon: "timer",
79404
+ openArgs: [{
79405
+ key: "minutes",
79406
+ label: t("Minutes", "Minuti"),
79407
+ offer: {
79408
+ kind: "number",
79409
+ min: 0,
79410
+ step: 1,
79411
+ unit: "min"
79412
+ }
79413
+ }]
79414
+ },
79415
+ {
79416
+ id: "petFountain.pumpOffMinutes",
79417
+ method: "setPumpOffMinutes",
79418
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
79419
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
79420
+ icon: "timer-off",
79421
+ openArgs: [{
79422
+ key: "minutes",
79423
+ label: t("Minutes", "Minuti"),
79424
+ offer: {
79425
+ kind: "number",
79426
+ min: 0,
79427
+ step: 1,
79428
+ unit: "min"
79429
+ }
79430
+ }]
79431
+ },
79432
+ {
79433
+ id: "petFountain.resetDailyConsumption",
79434
+ method: "resetDailyConsumption",
79435
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
79436
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
79437
+ icon: "rotate-ccw"
79438
+ }
79439
+ ],
79440
+ state: [{
79441
+ kind: "boolean",
79442
+ field: "empty",
79443
+ label: t("Tank empty", "Serbatoio vuoto"),
79444
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
79445
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
79446
+ icon: "droplet"
79447
+ }, {
79448
+ kind: "numeric",
79449
+ field: "waterLevel",
79450
+ quantity: t("the water level", "il livello dell’acqua"),
79451
+ label: t("Water level", "Livello dell’acqua"),
79452
+ unit: "%",
79453
+ icon: "droplets"
79454
+ }]
79455
+ },
79076
79456
  "media-player": {
79077
79457
  scope: "device",
79078
79458
  noun: t("Media player", "Lettore multimediale"),
@@ -80897,6 +81277,7 @@ var SYSTEM_CAP_VOCABULARY = {
80897
81277
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
80898
81278
  "device-provider": system(INTERNAL),
80899
81279
  "device-removal-hook": system(INTERNAL),
81280
+ "node-removal-hook": system(INTERNAL),
80900
81281
  "device-state": system("the runtime-state mirror every trigger already reads through"),
80901
81282
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
80902
81283
  "embedding-encoder": system(ML),
@@ -81009,7 +81390,7 @@ function projectAction(cap, action) {
81009
81390
  };
81010
81391
  }
81011
81392
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
81012
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
81393
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
81013
81394
  DeviceFeature.BatteryOperated;
81014
81395
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
81015
81396
  var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
@@ -82560,6 +82941,36 @@ Object.freeze({
82560
82941
  form: "single",
82561
82942
  optional: false
82562
82943
  }],
82944
+ "petFountain.getStatus": [{
82945
+ name: "deviceId",
82946
+ form: "single",
82947
+ optional: false
82948
+ }],
82949
+ "petFountain.resetDailyConsumption": [{
82950
+ name: "deviceId",
82951
+ form: "single",
82952
+ optional: false
82953
+ }],
82954
+ "petFountain.setPump": [{
82955
+ name: "deviceId",
82956
+ form: "single",
82957
+ optional: false
82958
+ }],
82959
+ "petFountain.setPumpInterval": [{
82960
+ name: "deviceId",
82961
+ form: "single",
82962
+ optional: false
82963
+ }],
82964
+ "petFountain.setPumpOffMinutes": [{
82965
+ name: "deviceId",
82966
+ form: "single",
82967
+ optional: false
82968
+ }],
82969
+ "petFountain.setPumpOnMinutes": [{
82970
+ name: "deviceId",
82971
+ form: "single",
82972
+ optional: false
82973
+ }],
82563
82974
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
82564
82975
  name: "deviceId",
82565
82976
  form: "single",