@camstack/addon-model-studio 1.1.318 → 1.1.320

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 (20) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{MotionZonesSettings-DNN9QEA3.mjs → MotionZonesSettings-Bb_BJah5.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-CwE32Qhm.mjs → PrivacyMaskSettings-kCHid95c.mjs} +4 -4
  4. package/dist/{SceneMonitorEditor-SNXiXvXt.mjs → SceneMonitorEditor-BqNgh047.mjs} +3 -3
  5. package/dist/_stub.js +4628 -4283
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BaMcMPS8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-Dng9ZasK.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DF4vj9Lw.mjs +26 -0
  8. package/dist/addon-model-studio.css +1 -1
  9. package/dist/{hostInit-C6xHvsEb.mjs → hostInit-B87DDaID.mjs} +2 -2
  10. package/dist/model-studio.addon.js +1288 -877
  11. package/dist/model-studio.addon.mjs +1288 -877
  12. package/dist/{player-overlays-BmZ_qrdR.mjs → player-overlays-CNVhS2-N.mjs} +1 -1
  13. package/dist/remoteEntry.js +1 -1
  14. package/dist/{responsive-Dypi_Y-V.mjs → responsive-oPhme86z.mjs} +1 -1
  15. package/dist/{scene-monitor-copy-Bafz9ZMd.mjs → scene-monitor-copy-Ct1WyOQx.mjs} +1 -1
  16. package/dist/{square-BhfFFjT7.mjs → square-WmNfgZ5W.mjs} +1 -1
  17. package/dist/{trash-2-BAyfEZwH.mjs → trash-2-BUJwssR_.mjs} +1 -1
  18. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-DyWlwmuC.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-Ca412ASr.mjs} +1 -1
  19. package/package.json +1 -1
  20. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D_KssOQn.mjs +0 -26
@@ -5437,6 +5437,14 @@ function object(shape, params) {
5437
5437
  ...normalizeParams(params)
5438
5438
  });
5439
5439
  }
5440
+ function strictObject(shape, params) {
5441
+ return new ZodObject({
5442
+ type: "object",
5443
+ shape,
5444
+ catchall: never(),
5445
+ ...normalizeParams(params)
5446
+ });
5447
+ }
5440
5448
  function looseObject(shape, params) {
5441
5449
  return new ZodObject({
5442
5450
  type: "object",
@@ -5816,7 +5824,7 @@ var ZodIssueCode = {
5816
5824
  var ZodFirstPartyTypeKind;
5817
5825
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5818
5826
  //#endregion
5819
- //#region ../types/dist/sleep-DJws6ho7.mjs
5827
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5820
5828
  /**
5821
5829
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5822
5830
  * window to float samples (D455).
@@ -8704,65 +8712,6 @@ var MODEL_FORMATS = [
8704
8712
  "pt",
8705
8713
  "gguf"
8706
8714
  ];
8707
- /**
8708
- * Per-call node pinning for `ctx.api` capability calls.
8709
- *
8710
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8711
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8712
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8713
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8714
- * call to that node.
8715
- *
8716
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8717
- * method args), so capability method signatures stay `nodeId`-free — node
8718
- * targeting is a property of the CALL, not of the method. The transport lifts
8719
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8720
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8721
- * which classifies a pinned agent node as `agent-child-forward`
8722
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8723
- *
8724
- * Usage at a call site:
8725
- *
8726
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8727
- */
8728
- /** tRPC `op.context` key carrying a per-call node pin. */
8729
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8730
- /**
8731
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8732
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8733
- *
8734
- * ## The id is normalised here, and it has to be
8735
- *
8736
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8737
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8738
- * compares a pin against real node ids, so such a pin matches nothing and the
8739
- * call fails with `no provider registered for cap "…"`. The local-first
8740
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8741
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8742
- * the thing you reach for when you specifically need THIS node — silently
8743
- * addressed a node that does not exist.
8744
- *
8745
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8746
- * own node to read the host's hardware-decode backend. It never once answered,
8747
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8748
- * while D67's whole premise was that the decoder addon is the authority on
8749
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8750
- * hardware", which was false.
8751
- *
8752
- * Normalising in the ONE constructor fixes every caller at once, which is why
8753
- * it is here and not at the call sites.
8754
- */
8755
- function nodePin(nodeId) {
8756
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8757
- }
8758
- /**
8759
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8760
- * is idempotent, so passing an already-clean id costs nothing.
8761
- */
8762
- function toNodeId(idOrRunnerId) {
8763
- const head = idOrRunnerId.split("/")[0];
8764
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8765
- }
8766
8715
  object({
8767
8716
  field: string().min(1),
8768
8717
  on: literal("advance")
@@ -8851,6 +8800,128 @@ function optionalMethod(input, output, options) {
8851
8800
  providerOptional: true
8852
8801
  };
8853
8802
  }
8803
+ /**
8804
+ * Per-call node pinning for `ctx.api` capability calls.
8805
+ *
8806
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8807
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8808
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8809
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8810
+ * call to that node.
8811
+ *
8812
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8813
+ * method args), so capability method signatures stay `nodeId`-free — node
8814
+ * targeting is a property of the CALL, not of the method. The transport lifts
8815
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8816
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8817
+ * which classifies a pinned agent node as `agent-child-forward`
8818
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8819
+ *
8820
+ * Usage at a call site:
8821
+ *
8822
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8823
+ */
8824
+ /** tRPC `op.context` key carrying a per-call node pin. */
8825
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8826
+ /**
8827
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8828
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8829
+ *
8830
+ * ## The id is normalised here, and it has to be
8831
+ *
8832
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8833
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8834
+ * compares a pin against real node ids, so such a pin matches nothing and the
8835
+ * call fails with `no provider registered for cap "…"`. The local-first
8836
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8837
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8838
+ * the thing you reach for when you specifically need THIS node — silently
8839
+ * addressed a node that does not exist.
8840
+ *
8841
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8842
+ * own node to read the host's hardware-decode backend. It never once answered,
8843
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8844
+ * while D67's whole premise was that the decoder addon is the authority on
8845
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8846
+ * hardware", which was false.
8847
+ *
8848
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8849
+ * it is here and not at the call sites.
8850
+ */
8851
+ function nodePin(nodeId) {
8852
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8853
+ }
8854
+ /**
8855
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8856
+ * is idempotent, so passing an already-clean id costs nothing.
8857
+ */
8858
+ function toNodeId(idOrRunnerId) {
8859
+ const head = idOrRunnerId.split("/")[0];
8860
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8861
+ }
8862
+ /**
8863
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8864
+ * declarations. Every addon that contributes a UI page registers a
8865
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8866
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8867
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8868
+ * that admin-ui consumes.
8869
+ *
8870
+ * The split exists because the public listing has a different output
8871
+ * shape than the per-provider raw declarations, and we want both ends
8872
+ * to flow through codegen instead of relying on a hand-written wrapper.
8873
+ */
8874
+ var AddonPageDeclarationSchema = object({
8875
+ id: string(),
8876
+ label: string(),
8877
+ icon: string(),
8878
+ path: string(),
8879
+ /**
8880
+ * Module Federation remote name — must match the `name` field on the
8881
+ * page addon's `federation()` plugin config. Used by admin-ui's
8882
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8883
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8884
+ * contain hyphens).
8885
+ */
8886
+ remoteName: string(),
8887
+ /**
8888
+ * Bundle filename inside the addon's `dist/` dir served at
8889
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8890
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8891
+ * the static-file route can compute an mtime-based cache-buster URL
8892
+ * without a separate filesystem stat.
8893
+ */
8894
+ bundle: string(),
8895
+ /**
8896
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8897
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8898
+ * Any OTHER string creates (or joins) a custom section rendered after
8899
+ * the built-in groups; its label comes from `sectionLabel` (first
8900
+ * declaration wins), falling back to the id. Absent → the legacy
8901
+ * "Addon Pages" group.
8902
+ */
8903
+ section: string().optional(),
8904
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8905
+ sectionLabel: string().optional()
8906
+ });
8907
+ var addonPagesSourceCapability = {
8908
+ name: "addon-pages-source",
8909
+ scope: "system",
8910
+ mode: "collection",
8911
+ internal: true,
8912
+ methods: {
8913
+ /**
8914
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
8915
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
8916
+ * authenticated session at the default `auth: 'protected'`. The only
8917
+ * consumer, the `addon-pages-aggregator` builtin, reads it via
8918
+ * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
8919
+ * only, never tRPC (per `ctx.capabilities` seeing only the local
8920
+ * process). The public, enriched listing admin-ui actually consumes is
8921
+ * the separate `addon-pages` cap, unaffected by this change.
8922
+ */
8923
+ listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
8924
+ };
8854
8925
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8855
8926
  var VersionOutputSchema$1 = object({ version: string() });
8856
8927
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -8876,6 +8947,27 @@ function customAction(input, output, options) {
8876
8947
  ...options?.caller ? { caller: "required" } : {}
8877
8948
  };
8878
8949
  }
8950
+ /**
8951
+ * The pipeline step-input shape and the run-result bridge, in a module that
8952
+ * imports nothing but zod and types.
8953
+ *
8954
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8955
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8956
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8957
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8958
+ * them, so every existing import keeps working; addon server code takes them
8959
+ * from `@camstack/types/addon`.
8960
+ */
8961
+ var PipelineStepInputSchema = lazy(() => object({
8962
+ addonId: string(),
8963
+ modelId: string().optional(),
8964
+ enabled: boolean().default(true),
8965
+ children: array(PipelineStepInputSchema).optional(),
8966
+ settings: record(string(), unknown()).optional(),
8967
+ jumpDeviceKey: string().optional()
8968
+ }));
8969
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8970
+ var PipelineRunResultBridge = custom();
8879
8971
  var StaticDirOutputSchema = object({ staticDir: string() });
8880
8972
  var VersionOutputSchema = object({ version: string() });
8881
8973
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9060,6 +9152,7 @@ var CAP_NAMES = Object.freeze([
9060
9152
  "network-link",
9061
9153
  "network-quality",
9062
9154
  "node-certificates",
9155
+ "node-removal-hook",
9063
9156
  "nodes",
9064
9157
  "notification-output",
9065
9158
  "notification-rules",
@@ -9070,6 +9163,7 @@ var CAP_NAMES = Object.freeze([
9070
9163
  "osd",
9071
9164
  "osd-manager",
9072
9165
  "pet-feeder",
9166
+ "pet-fountain",
9073
9167
  "pipeline-analytics",
9074
9168
  "pipeline-executor",
9075
9169
  "pipeline-orchestrator",
@@ -10349,6 +10443,56 @@ Object.freeze({
10349
10443
  "valueType": "number"
10350
10444
  }
10351
10445
  ],
10446
+ "pet-fountain": [
10447
+ {
10448
+ "path": "waterLevel",
10449
+ "valueType": "number"
10450
+ },
10451
+ {
10452
+ "path": "waterVolume",
10453
+ "valueType": "number"
10454
+ },
10455
+ {
10456
+ "path": "empty",
10457
+ "valueType": "boolean"
10458
+ },
10459
+ {
10460
+ "path": "pumpRunning",
10461
+ "valueType": "boolean"
10462
+ },
10463
+ {
10464
+ "path": "pumpIntervalEnabled",
10465
+ "valueType": "boolean"
10466
+ },
10467
+ {
10468
+ "path": "pumpOnTime",
10469
+ "valueType": "number"
10470
+ },
10471
+ {
10472
+ "path": "pumpOffTime",
10473
+ "valueType": "number"
10474
+ },
10475
+ {
10476
+ "path": "lastDrink",
10477
+ "valueType": "number"
10478
+ },
10479
+ {
10480
+ "path": "currentHourDrink",
10481
+ "valueType": "number"
10482
+ },
10483
+ {
10484
+ "path": "last24hDrink",
10485
+ "valueType": "number"
10486
+ },
10487
+ {
10488
+ "path": "todayDrink",
10489
+ "valueType": "number"
10490
+ },
10491
+ {
10492
+ "path": "error",
10493
+ "valueType": "string"
10494
+ }
10495
+ ],
10352
10496
  "power-meter": [
10353
10497
  {
10354
10498
  "path": "power",
@@ -10886,7 +11030,7 @@ object({
10886
11030
  params: record(string(), union([string(), number()])).optional()
10887
11031
  });
10888
11032
  //#endregion
10889
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11033
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10890
11034
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10891
11035
  DeviceType["Camera"] = "camera";
10892
11036
  DeviceType["Hub"] = "hub";
@@ -10978,6 +11122,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10978
11122
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10979
11123
  * integrations sharing the same food/desiccant/hopper surface. */
10980
11124
  DeviceType["PetFeeder"] = "pet-feeder";
11125
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11126
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11127
+ * of a container and is BUILT as a composed device (D967). */
11128
+ DeviceType["PetFountain"] = "pet-fountain";
10981
11129
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10982
11130
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10983
11131
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11238,7 +11386,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11238
11386
  return DeviceRole;
11239
11387
  }({});
11240
11388
  //#endregion
11241
- //#region ../types/dist/composition-DpFv_J6D.mjs
11389
+ //#region ../types/dist/composition-kSuOifuG.mjs
11242
11390
  /**
11243
11391
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11244
11392
  *
@@ -12323,12 +12471,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12323
12471
  ])
12324
12472
  })
12325
12473
  ]);
12326
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12474
+ var forwardArgUnion = union([
12475
+ string(),
12476
+ number(),
12477
+ boolean(),
12478
+ strictObject({ ["$input"]: string().min(1) }),
12479
+ record(string().min(1), lazy(() => forwardArgUnion))
12480
+ ]);
12481
+ var CompositionForwardArgSchema = forwardArgUnion;
12482
+ /**
12483
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12484
+ * call of `cap.method` on `source`. Without `args` the built method's input
12485
+ * passes through minus `deviceId`; with `args` the template IS the source's
12486
+ * whole input. `deviceId` is always the source's own, never an argument.
12487
+ */
12488
+ var CompositionForwardCommandSchema = object({
12327
12489
  kind: literal("forward"),
12328
12490
  source: CompositionSourceRefSchema,
12329
12491
  cap: string().min(1),
12330
- method: string().min(1)
12331
- }), CompositionCodeSourceSchema]);
12492
+ method: string().min(1),
12493
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12494
+ });
12495
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12332
12496
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12333
12497
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12334
12498
  /**
@@ -12338,7 +12502,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12338
12502
  */
12339
12503
  var CompositionItemEntrySchema = object({
12340
12504
  label: string().min(1).max(80),
12341
- fields: record(string().min(1), CompositionFieldSourceSchema)
12505
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12506
+ /**
12507
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12508
+ * is resettable and a `reset` call naming its key forwards here. The item key
12509
+ * selects the command, so it is never an argument: there is no pass-through.
12510
+ */
12511
+ reset: CompositionForwardCommandSchema.optional()
12342
12512
  });
12343
12513
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12344
12514
  kind: literal("fields"),
@@ -12350,7 +12520,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12350
12520
  * owns the WHOLE array field: the composed value holds exactly these items.
12351
12521
  */
12352
12522
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12353
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12523
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12354
12524
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12355
12525
  }), object({
12356
12526
  kind: literal("passthrough"),
@@ -16429,120 +16599,478 @@ object({
16429
16599
  lastError: string().nullable()
16430
16600
  });
16431
16601
  /**
16432
- * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16433
- * surface that admin-ui consumes through `useAddonPagesListPages()`.
16602
+ * notification-output — canonical, capability-gated notification delivery.
16434
16603
  *
16435
- * The provider iterates every `addon-pages-source` (collection) provider
16436
- * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
16437
- * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
16438
- * filesystem `mtime` cache-buster lets the browser pick up addon
16439
- * rebuilds without manual reload.
16604
+ * Apprise-derived model (see
16605
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16606
+ * callers emit ONE canonical `Notification`; each provider declares a
16607
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16608
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16609
+ * message to what the kind supports — callers never special-case a service.
16440
16610
  *
16441
- * The hub-local builtin `addon-pages-aggregator` (see
16442
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
16443
- * provider. Splitting the public aggregator from the raw collection
16444
- * keeps both ends in codegen — there's no hand-written
16445
- * `addon-pages.router.ts` wrapper anymore.
16611
+ * DESIGN DECISIONS (locked):
16612
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16613
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16614
+ * cap. Rationale: the admin UI needs one uniform surface across the
16615
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16616
+ * alternative would fork the UI per addon and cannot host the
16617
+ * discovery→adopt flow.
16618
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16619
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16620
+ * registered provider (notifiers addon + HA addon) so one catalog is
16621
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16622
+ * `addonId` the generated collection router extracts from the call input.
16623
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16624
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16625
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16626
+ * base64 fallback needed.
16627
+ *
16628
+ * TODO (deferred, closed-set change — separate decision): add
16629
+ * `providerKind: 'notify'` so notification providers surface on the unified
16630
+ * admin "Integrations" page.
16446
16631
  */
16447
- var AddonPageDeclarationSchema$1 = object({
16448
- id: string(),
16449
- label: string(),
16450
- icon: string(),
16451
- path: string(),
16452
- remoteName: string(),
16453
- bundle: string(),
16454
- section: string().optional(),
16455
- sectionLabel: string().optional()
16456
- });
16457
- var AddonPageInfoSchema = object({
16458
- addonId: string(),
16459
- page: AddonPageDeclarationSchema$1,
16460
- bundleUrl: string()
16461
- });
16462
- method(_void(), array(AddonPageInfoSchema).readonly());
16463
16632
  /**
16464
- * `addon-pages-source` — collection cap exposing per-provider raw page
16465
- * declarations. Every addon that contributes a UI page registers a
16466
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16467
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16468
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16469
- * that admin-ui consumes.
16633
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16634
+ * adapter picks what it supports and the degrade engine filters the rest.
16635
+ */
16636
+ var AttachmentMediaTypeSchema = _enum([
16637
+ "image",
16638
+ "video",
16639
+ "gif",
16640
+ "audio",
16641
+ "icon"
16642
+ ]);
16643
+ /**
16644
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16645
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16646
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16647
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16648
+ */
16649
+ var AttachmentSchema = object({
16650
+ mediaType: AttachmentMediaTypeSchema,
16651
+ url: string().optional(),
16652
+ bytes: _instanceof(Uint8Array).optional(),
16653
+ mime: string().optional(),
16654
+ name: string().optional()
16655
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16656
+ var NotificationFormatSchema = _enum([
16657
+ "text",
16658
+ "markdown",
16659
+ "html"
16660
+ ]);
16661
+ /**
16662
+ * The CLOSED icon vocabulary an action button may use.
16470
16663
  *
16471
- * The split exists because the public listing has a different output
16472
- * shape than the per-provider raw declarations, and we want both ends
16473
- * to flow through codegen instead of relying on a hand-written wrapper.
16664
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16665
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16666
+ * notices — the same class of gap as a zone vocabulary nothing produced
16667
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16668
+ * declares `actionIcons: false` and the degrade engine strips the field.
16669
+ *
16670
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16671
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16672
+ * differently.
16673
+ *
16674
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16675
+ *
16676
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16677
+ * the whole set onto its own renderer's vocabulary through a
16678
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16679
+ * fallback, so adding a member here fails every adapter's build until someone
16680
+ * decides its glyph, which is the only place that decision can be made
16681
+ * honestly.
16682
+ *
16683
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16684
+ * `disarm` straight through; iOS feeds that string to
16685
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16686
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16687
+ * mapping, and "the field is documented" is not "the value renders".
16688
+ *
16689
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16690
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16691
+ * so an addon that emits a token the running hub does not know does not lose an
16692
+ * icon — its whole `send` fails Zod validation and the notification never
16693
+ * arrives. Never emit a new token from an addon before the train carrying it.
16474
16694
  */
16475
- var AddonPageDeclarationSchema = object({
16695
+ var NotificationActionIconSchema = _enum([
16696
+ "acknowledge",
16697
+ "dismiss",
16698
+ "silence",
16699
+ "snooze",
16700
+ "view",
16701
+ "play",
16702
+ "open",
16703
+ "close",
16704
+ "lock",
16705
+ "unlock",
16706
+ "arm",
16707
+ "arm-home",
16708
+ "arm-away",
16709
+ "arm-night",
16710
+ "disarm",
16711
+ "light",
16712
+ "alert",
16713
+ "camera"
16714
+ ]);
16715
+ /** A single tap-through action button. */
16716
+ var NotificationActionSchema = object({
16476
16717
  id: string(),
16477
16718
  label: string(),
16478
- icon: string(),
16479
- path: string(),
16480
- /**
16481
- * Module Federation remote name — must match the `name` field on the
16482
- * page addon's `federation()` plugin config. Used by admin-ui's
16483
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16484
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16485
- * contain hyphens).
16486
- */
16487
- remoteName: string(),
16719
+ url: string().optional(),
16720
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16721
+ icon: NotificationActionIconSchema.optional(),
16488
16722
  /**
16489
- * Bundle filename inside the addon's `dist/` dir served at
16490
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16491
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16492
- * the static-file route can compute an mtime-based cache-buster URL
16493
- * without a separate filesystem stat.
16723
+ * Renders in a warning style where the notifier supports it.
16724
+ *
16725
+ * A HINT, never a gate. The callback's authority is its token and nothing
16726
+ * else — see `notification-center/action-token.ts` for what that does and
16727
+ * does not buy.
16494
16728
  */
16495
- bundle: string(),
16729
+ destructive: boolean().optional(),
16496
16730
  /**
16497
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16498
- * `'cluster'`, `'administration'` — the page renders inside that group.
16499
- * Any OTHER string creates (or joins) a custom section rendered after
16500
- * the built-in groups; its label comes from `sectionLabel` (first
16501
- * declaration wins), falling back to the id. Absent → the legacy
16502
- * "Addon Pages" group.
16731
+ * How the tap should REACH the url.
16732
+ *
16733
+ * `navigate` (absent, and every button authored before this field) opens it:
16734
+ * the phone leaves the notification and shows whatever the callback returns.
16735
+ * That is right for a button whose answer the operator wants to read.
16736
+ *
16737
+ * `background` fires it as a POST and stays put. It exists for the buttons
16738
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16739
+ * an answer to the notification, and being thrown into a browser tab to
16740
+ * confirm it costs more attention than the notification did. A backend that
16741
+ * cannot do a background call renders it as an ordinary link (the adapters
16742
+ * fall back rather than dropping the button), so this is a preference, never
16743
+ * a requirement.
16503
16744
  */
16504
- section: string().optional(),
16505
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16506
- sectionLabel: string().optional()
16745
+ mode: _enum(["navigate", "background"]).optional()
16507
16746
  });
16508
- var addonPagesSourceCapability = {
16509
- name: "addon-pages-source",
16510
- scope: "system",
16511
- mode: "collection",
16512
- internal: true,
16513
- methods: {
16514
- /**
16515
- * `internal: true` did not gate the mount (see the 2026-08-26 note on
16516
- * `data-store-provider`) — this was reachable on the AppRouter by ANY
16517
- * authenticated session at the default `auth: 'protected'`. The only
16518
- * consumer, the `addon-pages-aggregator` builtin, reads it via
16519
- * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
16520
- * only, never tRPC (per `ctx.capabilities` seeing only the local
16521
- * process). The public, enriched listing admin-ui actually consumes is
16522
- * the separate `addon-pages` cap, unaffected by this change.
16523
- */
16524
- listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
16525
- };
16526
- var AddonHttpRouteSchema = object({
16527
- method: _enum([
16528
- "GET",
16529
- "POST",
16530
- "PUT",
16531
- "DELETE",
16532
- "PATCH"
16747
+ /** A delivery parameter value: what a notifier declared it takes. */
16748
+ var DeliveryParamValueSchema = union([
16749
+ string(),
16750
+ number(),
16751
+ boolean()
16752
+ ]);
16753
+ /**
16754
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16755
+ * addresses a target of that kind may set it; a kind that declares nothing is
16756
+ * never handed one. The admin UI derives its control from this descriptor —
16757
+ * no editor knows a service by name.
16758
+ *
16759
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16760
+ * run on the addon cap path).
16761
+ */
16762
+ var DeliveryParamSchema = object({
16763
+ key: string().min(1),
16764
+ label: string(),
16765
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16766
+ labelKey: string().optional(),
16767
+ type: _enum([
16768
+ "enum",
16769
+ "number",
16770
+ "boolean",
16771
+ "string"
16533
16772
  ]),
16534
- path: string(),
16535
- access: _enum([
16536
- "public",
16537
- "authenticated",
16538
- "admin"
16539
- ]).optional(),
16540
- description: string().optional(),
16541
- /** The hub limits this route per client address before the handler runs. */
16542
- rateLimit: _enum(["auth"]).optional()
16773
+ /** `enum` only: the closed set of accepted values. */
16774
+ options: array(object({
16775
+ value: string(),
16776
+ label: string()
16777
+ })).optional(),
16778
+ /** `number` only. */
16779
+ min: number().optional(),
16780
+ max: number().optional(),
16781
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16782
+ default: DeliveryParamValueSchema.optional()
16543
16783
  });
16544
16784
  /**
16545
- * Cross-process route invocation envelope. The hub captures the
16785
+ * The canonical notification. `body` is the only hard field (Apprise model).
16786
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16787
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16788
+ * the adapter maps this ordinal onto its native level. `level?` is an
16789
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16790
+ * `priority` for that one target.
16791
+ */
16792
+ var NotificationSchema = object({
16793
+ body: string(),
16794
+ title: string().optional(),
16795
+ format: NotificationFormatSchema.default("text"),
16796
+ priority: number().int().min(1).max(5).default(3),
16797
+ level: string().optional(),
16798
+ attachments: array(AttachmentSchema).optional(),
16799
+ clickUrl: string().optional(),
16800
+ actions: array(NotificationActionSchema).optional(),
16801
+ sound: string().optional(),
16802
+ ttl: number().optional(),
16803
+ tag: string().optional(),
16804
+ deviceId: number().optional(),
16805
+ eventId: string().optional(),
16806
+ metadata: record(string(), unknown()).optional(),
16807
+ /**
16808
+ * Values for the parameters the target's kind DECLARES in
16809
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16810
+ * declaration by the sender; a notifier reads only the keys it declared.
16811
+ */
16812
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16813
+ });
16814
+ /** One declared native severity/priority level for a kind. */
16815
+ var TargetKindLevelSchema = object({
16816
+ id: string(),
16817
+ label: string(),
16818
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16819
+ ordinal: number().int().min(1).max(5).nullable(),
16820
+ flags: object({
16821
+ critical: boolean().optional(),
16822
+ silent: boolean().optional(),
16823
+ noPush: boolean().optional()
16824
+ }).optional(),
16825
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16826
+ requires: array(string()).optional(),
16827
+ description: string().optional()
16828
+ });
16829
+ /** The full capability block consulted before dispatch. */
16830
+ var TargetKindCapsSchema = object({
16831
+ attachments: object({
16832
+ mediaTypes: array(AttachmentMediaTypeSchema),
16833
+ mode: _enum([
16834
+ "url",
16835
+ "bytes",
16836
+ "both"
16837
+ ]),
16838
+ max: number().int().nonnegative(),
16839
+ maxBytes: number().int().positive().optional()
16840
+ }),
16841
+ /** Max action buttons (0 = none). */
16842
+ actions: number().int().nonnegative(),
16843
+ /**
16844
+ * Whether this kind renders a per-action ICON.
16845
+ *
16846
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16847
+ * run on the addon cap path — three production failures in one day taught
16848
+ * this repo that once. Absent is read as false by the degrade engine, which
16849
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16850
+ * assumed and dropped costs the operator's trust in the field.
16851
+ */
16852
+ actionIcons: boolean().optional(),
16853
+ levels: array(TargetKindLevelSchema),
16854
+ format: array(NotificationFormatSchema),
16855
+ clickUrl: boolean(),
16856
+ sound: boolean(),
16857
+ ttl: boolean(),
16858
+ bodyMaxLen: number().int().positive()
16859
+ });
16860
+ /**
16861
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16862
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16863
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16864
+ * the union is large and not meant for runtime validation here; the exported
16865
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16866
+ */
16867
+ var ConfigSchemaPassthrough$3 = unknown();
16868
+ var TargetKindSchema = object({
16869
+ kind: string(),
16870
+ label: string(),
16871
+ icon: string(),
16872
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16873
+ addonId: string(),
16874
+ /**
16875
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16876
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16877
+ * when the addon bundles no icon for that kind — the client then falls back
16878
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16879
+ *
16880
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16881
+ * client, and a native client joins it onto its own hub base.
16882
+ *
16883
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16884
+ * field that survived only because the runtime cap-router forwards provider
16885
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16886
+ * its own Zod parse from stripping it, and the whole arrangement would have
16887
+ * broken silently the moment output validation was tightened anywhere.
16888
+ */
16889
+ iconUrl: string().optional(),
16890
+ /**
16891
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16892
+ *
16893
+ * The server knows this and therefore says it, because the client cannot
16894
+ * safely guess: a React-Native client renders SVG and raster through two
16895
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16896
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16897
+ * placeholder glyph for every vector icon while the web build looked fine.
16898
+ *
16899
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16900
+ * not been updated — a client that cannot determine the type should prefer
16901
+ * its raster path, which is the safe default for an unknown image.
16902
+ */
16903
+ iconMediaType: string().optional(),
16904
+ configSchema: ConfigSchemaPassthrough$3,
16905
+ /**
16906
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16907
+ * base URL, instance, chat, recipient). A save that changes any of them
16908
+ * while leaving a secret redacted is refused by the provider (D634), so an
16909
+ * editor uses this to ask for the secret on exactly those edits instead of
16910
+ * on every edit.
16911
+ *
16912
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16913
+ * kinds) and from a provider older than this field — an editor that finds
16914
+ * it absent must assume ANY edit may need the secret.
16915
+ */
16916
+ destinationKeys: array(string()).optional(),
16917
+ supportsDiscovery: boolean(),
16918
+ /**
16919
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16920
+ * Absent = none; the rule editor then offers nothing.
16921
+ */
16922
+ deliveryParams: array(DeliveryParamSchema).optional(),
16923
+ caps: TargetKindCapsSchema
16924
+ });
16925
+ /**
16926
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16927
+ * (return a presence marker only) when serving `listTargets` — never
16928
+ * round-trip a stored secret to the UI.
16929
+ */
16930
+ var TargetSchema = object({
16931
+ id: string(),
16932
+ name: string(),
16933
+ kind: string(),
16934
+ addonId: string(),
16935
+ enabled: boolean(),
16936
+ config: record(string(), unknown())
16937
+ });
16938
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16939
+ var DiscoveredTargetSchema = object({
16940
+ kind: string(),
16941
+ suggestedName: string(),
16942
+ config: record(string(), unknown())
16943
+ });
16944
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16945
+ var RenderedAsSchema = object({
16946
+ level: string(),
16947
+ format: NotificationFormatSchema,
16948
+ attachmentsSent: number().int().nonnegative(),
16949
+ actionsSent: number().int().nonnegative(),
16950
+ truncated: boolean(),
16951
+ dropped: array(string())
16952
+ });
16953
+ var SendResultSchema = object({
16954
+ success: boolean(),
16955
+ error: string().optional(),
16956
+ renderedAs: RenderedAsSchema.optional()
16957
+ });
16958
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16959
+ var TestResultSchema = SendResultSchema;
16960
+ /**
16961
+ * Pre-save credential probe against a partial (unsaved) target config.
16962
+ * `resolvedUrl` never includes secrets (token stripped from path).
16963
+ */
16964
+ var ProbeTargetConfigResultSchema = object({
16965
+ ok: boolean(),
16966
+ resolvedUrl: string(),
16967
+ httpStatus: number().int().optional(),
16968
+ stateInstance: string().optional(),
16969
+ error: string().optional()
16970
+ });
16971
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
16972
+ kind: string(),
16973
+ config: record(string(), unknown()).optional(),
16974
+ /**
16975
+ * When editing / duplicating, the blank password field never round-trips
16976
+ * the stored token. Pass the source target id so the provider can restore
16977
+ * secrets server-side before dialling (same merge as upsert).
16978
+ */
16979
+ targetId: string().optional()
16980
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
16981
+ kind: string(),
16982
+ config: record(string(), unknown()),
16983
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
16984
+ targetId: string().optional()
16985
+ }), ProbeTargetConfigResultSchema, {
16986
+ kind: "mutation",
16987
+ auth: "admin"
16988
+ }), method(object({
16989
+ targetId: string(),
16990
+ notification: NotificationSchema
16991
+ }), SendResultSchema, {
16992
+ kind: "mutation",
16993
+ auth: "admin"
16994
+ }), method(object({
16995
+ targetId: string(),
16996
+ sample: NotificationSchema.optional()
16997
+ }), TestResultSchema, {
16998
+ kind: "mutation",
16999
+ auth: "admin"
17000
+ }), method(object({
17001
+ target: TargetSchema,
17002
+ /**
17003
+ * Duplicate flow: create a NEW target id while copying secrets from an
17004
+ * existing row. The UI never holds the stored token; blank / redacted
17005
+ * password fields are restored from this source (D634 still applies if
17006
+ * destinations diverge without a re-typed secret).
17007
+ */
17008
+ cloneSecretsFrom: string().optional()
17009
+ }), TargetSchema, {
17010
+ kind: "mutation",
17011
+ auth: "admin"
17012
+ }), method(object({ targetId: string() }), _void(), {
17013
+ kind: "mutation",
17014
+ auth: "admin"
17015
+ }), method(object({
17016
+ targetId: string(),
17017
+ enabled: boolean()
17018
+ }), _void(), {
17019
+ kind: "mutation",
17020
+ auth: "admin"
17021
+ });
17022
+ /**
17023
+ * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17024
+ * surface that admin-ui consumes through `useAddonPagesListPages()`.
17025
+ *
17026
+ * The provider iterates every `addon-pages-source` (collection) provider
17027
+ * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
17028
+ * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
17029
+ * filesystem `mtime` cache-buster lets the browser pick up addon
17030
+ * rebuilds without manual reload.
17031
+ *
17032
+ * The hub-local builtin `addon-pages-aggregator` (see
17033
+ * `@camstack/system/builtins/addon-pages-aggregator`) registers the
17034
+ * provider. Splitting the public aggregator from the raw collection
17035
+ * keeps both ends in codegen — there's no hand-written
17036
+ * `addon-pages.router.ts` wrapper anymore.
17037
+ */
17038
+ var AddonPageDeclarationSchema$1 = object({
17039
+ id: string(),
17040
+ label: string(),
17041
+ icon: string(),
17042
+ path: string(),
17043
+ remoteName: string(),
17044
+ bundle: string(),
17045
+ section: string().optional(),
17046
+ sectionLabel: string().optional()
17047
+ });
17048
+ var AddonPageInfoSchema = object({
17049
+ addonId: string(),
17050
+ page: AddonPageDeclarationSchema$1,
17051
+ bundleUrl: string()
17052
+ });
17053
+ method(_void(), array(AddonPageInfoSchema).readonly());
17054
+ var AddonHttpRouteSchema = object({
17055
+ method: _enum([
17056
+ "GET",
17057
+ "POST",
17058
+ "PUT",
17059
+ "DELETE",
17060
+ "PATCH"
17061
+ ]),
17062
+ path: string(),
17063
+ access: _enum([
17064
+ "public",
17065
+ "authenticated",
17066
+ "admin"
17067
+ ]).optional(),
17068
+ description: string().optional(),
17069
+ /** The hub limits this route per client address before the handler runs. */
17070
+ rateLimit: _enum(["auth"]).optional()
17071
+ });
17072
+ /**
17073
+ * Cross-process route invocation envelope. The hub captures the
16546
17074
  * request as plain data, ships it to the worker via Moleculer, and
16547
17075
  * the worker runs the local handler against a capturing reply. The
16548
17076
  * envelope returned describes what the handler intended (status,
@@ -19065,7 +19593,11 @@ var CompositionProblemSchema = object({
19065
19593
  "mixed-quantity",
19066
19594
  "helper-mismatch",
19067
19595
  "member-constraint",
19068
- "retention"
19596
+ "retention",
19597
+ "command-method-unknown",
19598
+ "command-source-unknown",
19599
+ "command-input-unknown",
19600
+ "command-input-missing"
19069
19601
  ]),
19070
19602
  path: string(),
19071
19603
  message: string()
@@ -19104,7 +19636,9 @@ var CompositionFieldRoleSchema = _enum([
19104
19636
  var CompositionFieldItemSchema = object({
19105
19637
  arrayPath: string(),
19106
19638
  key: string(),
19107
- label: string()
19639
+ label: string(),
19640
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19641
+ resettable: boolean().optional()
19108
19642
  });
19109
19643
  var CompositionFieldPlanSchema = object({
19110
19644
  cap: string(),
@@ -19309,7 +19843,7 @@ Object.freeze({
19309
19843
  * One catalog row per base reading a group helper can build (D944 §3).
19310
19844
  * Data, not a form default written into the block.
19311
19845
  */
19312
- var NOW$2 = {
19846
+ var NOW$3 = {
19313
19847
  fieldPath: "lastFetchedAt",
19314
19848
  expr: "now"
19315
19849
  };
@@ -19361,7 +19895,7 @@ var GROUP_TEMPLATES = [
19361
19895
  quantity: "temperature",
19362
19896
  defaultAggregate: "mean",
19363
19897
  empty: "null",
19364
- companions: [NOW$2],
19898
+ companions: [NOW$3],
19365
19899
  alsoFrom: {
19366
19900
  deviceTypes: CLIMATE_DEVICES,
19367
19901
  accept: {
@@ -19379,7 +19913,7 @@ var GROUP_TEMPLATES = [
19379
19913
  quantity: "humidity",
19380
19914
  defaultAggregate: "mean",
19381
19915
  empty: "null",
19382
- companions: [NOW$2],
19916
+ companions: [NOW$3],
19383
19917
  alsoFrom: {
19384
19918
  deviceTypes: CLIMATE_DEVICES,
19385
19919
  accept: {
@@ -19397,7 +19931,7 @@ var GROUP_TEMPLATES = [
19397
19931
  quantity: "pressure",
19398
19932
  defaultAggregate: "mean",
19399
19933
  empty: "null",
19400
- companions: [NOW$2]
19934
+ companions: [NOW$3]
19401
19935
  }),
19402
19936
  numeric({
19403
19937
  id: "illuminance",
@@ -19408,7 +19942,7 @@ var GROUP_TEMPLATES = [
19408
19942
  quantity: "illuminance",
19409
19943
  defaultAggregate: "mean",
19410
19944
  empty: "null",
19411
- companions: [NOW$2]
19945
+ companions: [NOW$3]
19412
19946
  }),
19413
19947
  numeric({
19414
19948
  id: "power",
@@ -19419,7 +19953,7 @@ var GROUP_TEMPLATES = [
19419
19953
  quantity: "power",
19420
19954
  defaultAggregate: "sum",
19421
19955
  empty: "omit",
19422
- companions: [NOW$2]
19956
+ companions: [NOW$3]
19423
19957
  }),
19424
19958
  numeric({
19425
19959
  id: "energy",
@@ -19430,7 +19964,7 @@ var GROUP_TEMPLATES = [
19430
19964
  quantity: "energy",
19431
19965
  defaultAggregate: "sum",
19432
19966
  empty: "omit",
19433
- companions: [NOW$2]
19967
+ companions: [NOW$3]
19434
19968
  }),
19435
19969
  numeric({
19436
19970
  id: "battery",
@@ -19465,7 +19999,7 @@ var GROUP_TEMPLATES = [
19465
19999
  quantity: "device",
19466
20000
  defaultAggregate: "mean",
19467
20001
  empty: "null",
19468
- companions: [NOW$2]
20002
+ companions: [NOW$3]
19469
20003
  }),
19470
20004
  binary({
19471
20005
  id: "motion",
@@ -21839,12 +22373,48 @@ var ExposeInputSchema = object({
21839
22373
  capabilities: array(string()).optional()
21840
22374
  });
21841
22375
  var UnexposeInputSchema = object({ deviceId: string() });
22376
+ /**
22377
+ * What an exporter may expose besides devices (D968). An action and a
22378
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22379
+ * exporter's own selection, not the device registry.
22380
+ */
22381
+ var ExportEntityKindSchema = _enum([
22382
+ "device",
22383
+ "action",
22384
+ "notifier"
22385
+ ]);
22386
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22387
+ object({
22388
+ kind: literal("device"),
22389
+ deviceId: string()
22390
+ }),
22391
+ object({
22392
+ kind: literal("action"),
22393
+ actionId: string().min(1)
22394
+ }),
22395
+ object({
22396
+ kind: literal("notifier"),
22397
+ targetId: string().min(1)
22398
+ })
22399
+ ]);
22400
+ var ExposedEntitySchema = object({
22401
+ entity: ExportEntityRefSchema,
22402
+ exposedAs: string().optional()
22403
+ });
22404
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22405
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
21842
22406
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
21843
22407
  kind: "mutation",
21844
22408
  auth: "admin"
21845
22409
  }), method(UnexposeInputSchema, _void(), {
21846
22410
  kind: "mutation",
21847
22411
  auth: "admin"
22412
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22413
+ kind: "mutation",
22414
+ auth: "admin"
22415
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22416
+ kind: "mutation",
22417
+ auth: "admin"
21848
22418
  });
21849
22419
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
21850
22420
  var ICON_CATEGORY_IDS = [
@@ -42985,6 +43555,14 @@ method(object({
42985
43555
  kind: "mutation",
42986
43556
  auth: "admin"
42987
43557
  });
43558
+ method(object({
43559
+ nodeId: string().min(1),
43560
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
43561
+ removedAt: number()
43562
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
43563
+ kind: "mutation",
43564
+ auth: "admin"
43565
+ });
42988
43566
  /**
42989
43567
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
42990
43568
  * so it can delete what it keeps for that device (D865).
@@ -43705,7 +44283,7 @@ var LlmProviderWidgetSchema = object({
43705
44283
  /** ConfigUISchema tree passed through untyped on the wire (the
43706
44284
  * notification-output `ConfigSchemaPassthrough` precedent at
43707
44285
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
43708
- var ConfigSchemaPassthrough$3 = unknown();
44286
+ var ConfigSchemaPassthrough$2 = unknown();
43709
44287
  var LlmProfileKindDescriptorSchema = object({
43710
44288
  kind: LlmProfileKindSchema,
43711
44289
  label: string(),
@@ -43714,9 +44292,9 @@ var LlmProfileKindDescriptorSchema = object({
43714
44292
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
43715
44293
  addonId: string(),
43716
44294
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
43717
- connectionSchema: ConfigSchemaPassthrough$3,
44295
+ connectionSchema: ConfigSchemaPassthrough$2,
43718
44296
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
43719
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44297
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
43720
44298
  /** The kind's defaults; a model from `listModels` may override any of them. */
43721
44299
  features: LlmModelFeaturesSchema,
43722
44300
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44242,10 +44820,10 @@ var CpuBreakdownSchema = object({
44242
44820
  cores: number()
44243
44821
  });
44244
44822
  var MemoryInfoSchema = object({
44245
- percent: number(),
44823
+ percent: number().nullable(),
44246
44824
  totalBytes: number(),
44247
- usedBytes: number(),
44248
- availableBytes: number(),
44825
+ usedBytes: number().nullable(),
44826
+ availableBytes: number().nullable(),
44249
44827
  swapUsedBytes: number(),
44250
44828
  swapTotalBytes: number()
44251
44829
  });
@@ -44522,697 +45100,277 @@ var NodeLoadSeriesSchema = object({
44522
45100
  });
44523
45101
  var GetLoadSeriesInputSchema = object({
44524
45102
  /**
44525
- * The node whose series is wanted.
44526
- *
44527
- * NOT named `nodeId`: the generated cap router strips a top-level
44528
- * `nodeId` from every method input and uses it to ROUTE the call to
44529
- * that node's provider (`generated-cap-routers.ts`). A series target
44530
- * called `nodeId` would silently become a routing pin and never reach
44531
- * the provider. The hub holds every node it hears from, so the
44532
- * ordinary call is unpinned — answered by the hub, for any node.
44533
- */
44534
- forNodeId: string(),
44535
- /**
44536
- * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
44537
- * holds and receives only what it is missing, so seeding a live chart
44538
- * from this method cannot double a point already drawn.
44539
- */
44540
- sinceMs: number().optional(),
44541
- /**
44542
- * Most points the caller wants PER FUNCTION. The window is reduced to fit,
44543
- * preserving min and max per bucket.
44544
- *
44545
- * Absent means NO reduction — legitimate for a short window and a trap for a
44546
- * long one, which is why a chart passes its own pixel width.
44547
- */
44548
- maxPoints: number().int().positive().optional()
44549
- });
44550
- var SystemMetricsSchema = object({
44551
- cpuPercent: number(),
44552
- memoryPercent: number(),
44553
- memoryUsedMB: number(),
44554
- memoryTotalMB: number(),
44555
- diskPercent: number().optional(),
44556
- temperature: number().optional(),
44557
- gpuPercent: number().optional(),
44558
- gpuMemoryPercent: number().optional()
44559
- });
44560
- 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);
44561
- var modelConvertCapability = {
44562
- name: "model-convert",
44563
- scope: "system",
44564
- mode: "singleton",
44565
- internal: true,
44566
- methods: { convert: method(object({
44567
- sourceUrl: string(),
44568
- metadata: ModelConvertMetadataSchema,
44569
- targets: array(ConvertTargetSchema).min(1).readonly(),
44570
- calibrationRef: string().optional(),
44571
- sessionId: string().optional()
44572
- }), ConvertResultSchema, {
44573
- kind: "mutation",
44574
- auth: "admin",
44575
- timeoutMs: 6e5
44576
- }) }
44577
- };
44578
- method(object({
44579
- targetNodeId: string(),
44580
- modelId: string(),
44581
- format: _enum(MODEL_FORMATS),
44582
- entry: ModelCatalogEntrySchema
44583
- }), object({
44584
- ok: boolean(),
44585
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
44586
- sha256: string(),
44587
- bytes: number(),
44588
- /** The target node's modelsDir the artifact landed in. */
44589
- path: string()
44590
- }), {
44591
- kind: "mutation",
44592
- auth: "admin"
44593
- });
44594
- var MotionRegionSchema = object({
44595
- bbox: BoundingBoxSchema,
44596
- pixelCount: number(),
44597
- intensity: number()
44598
- });
44599
- var MotionAnalysisResultSchema = object({
44600
- detected: boolean(),
44601
- regionCount: number(),
44602
- /** Regions passing minArea filter — used for pipeline triggering. */
44603
- regions: array(MotionRegionSchema).readonly(),
44604
- /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
44605
- rawRegions: array(MotionRegionSchema).readonly(),
44606
- frameWidth: number(),
44607
- frameHeight: number(),
44608
- analysisMs: number()
44609
- });
44610
- DeviceType.Camera, method(object({
44611
- deviceId: number(),
44612
- frame: FrameInputSchema.optional(),
44613
- frameHandle: FrameHandleSchema.optional()
44614
- }), MotionAnalysisResultSchema, {
44615
- role: "admin",
44616
- kind: "mutation"
44617
- }), method(object({ deviceId: number() }), _void(), {
44618
- role: "admin",
44619
- kind: "mutation"
44620
- }), method(_void(), _void(), {
44621
- role: "admin",
44622
- kind: "mutation"
44623
- }), object({
44624
- deviceId: number(),
44625
- detected: boolean(),
44626
- regionCount: number()
44627
- });
44628
- /**
44629
- * `mqtt-broker` — broker-registry cap.
44630
- *
44631
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
44632
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
44633
- * and (b) the connection details a consumer addon needs to spin up
44634
- * its OWN `mqtt.js` client.
44635
- *
44636
- * Why: pub/sub routing over the system event-bus loses fidelity
44637
- * (callback shape, QoS guarantees, will/retain semantics) and adds
44638
- * refcount bookkeeping that addons would rather own themselves. The
44639
- * canonical consumer needs raw `mqtt.js`
44640
- * features anyway — give it the connection config, get out of the way.
44641
- *
44642
- * Consumer flow:
44643
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
44644
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
44645
- * client.subscribe('zigbee2mqtt/+')
44646
- *
44647
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
44648
- * cloud bridge). The "embedded" entry (when present) is just another
44649
- * broker in the registry — its lifecycle is owned by the addon that
44650
- * spawned it.
44651
- */
44652
- var BrokerKindSchema = _enum(["external", "embedded"]);
44653
- /**
44654
- * Broker live-probe status.
44655
- *
44656
- * - `connected` — last probe completed a clean CONNACK
44657
- * - `disconnected` — no probe has run yet (cold cache)
44658
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
44659
- * - `unreachable` — TCP connect timed out / refused
44660
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
44661
- */
44662
- var BrokerStatusSchema$1 = _enum([
44663
- "connected",
44664
- "disconnected",
44665
- "auth-failed",
44666
- "unreachable",
44667
- "tls-error"
44668
- ]);
44669
- var BrokerInfoSchema$1 = object({
44670
- id: string(),
44671
- name: string(),
44672
- url: string(),
44673
- kind: BrokerKindSchema,
44674
- status: BrokerStatusSchema$1,
44675
- latencyMs: number().nullable(),
44676
- error: string().optional(),
44677
- /** Embedded brokers only: number of MQTT clients currently connected. */
44678
- connectedClients: number().int().nonnegative().optional(),
44679
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
44680
- lastCheckedAt: number().optional()
44681
- });
44682
- /**
44683
- * Connection details — what a consumer needs to call
44684
- * `mqtt.connect(url, options)`. We split URL + credentials so the
44685
- * consumer can pass them as `mqtt.connect(url, { username, password })`
44686
- * instead of stuffing creds into the URL (which leaks them into logs).
44687
- */
44688
- var BrokerConnectionDetailsSchema = object({
44689
- url: string(),
44690
- username: string().optional(),
44691
- password: string().optional(),
44692
- /**
44693
- * Suggested prefix for `clientId`. Each consumer should suffix this
44694
- * with its own discriminator (addon id, instance id) so reconnects
44695
- * don't kick each other off (MQTT spec: clientId must be unique per
44696
- * broker).
44697
- */
44698
- clientIdPrefix: string().optional()
44699
- });
44700
- var AddBrokerInputSchema = object({
44701
- name: string().min(1),
44702
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
44703
- username: string().optional(),
44704
- password: string().optional(),
44705
- clientIdPrefix: string().optional()
44706
- });
44707
- var AddBrokerResultSchema = object({ id: string() });
44708
- var IdInputSchema = object({ id: string() });
44709
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
44710
- ok: literal(true),
44711
- latencyMs: number()
44712
- }), object({
44713
- ok: literal(false),
44714
- error: string()
44715
- })]);
44716
- var StartEmbeddedInputSchema = object({
44717
- port: number().int().min(1).max(65535).default(1883),
44718
- /** Allow anonymous connect (no username/password). Default: false. */
44719
- allowAnonymous: boolean().default(false),
44720
- /** Optional shared username/password for clients. */
44721
- username: string().optional(),
44722
- password: string().optional()
44723
- });
44724
- var StartEmbeddedResultSchema = object({
44725
- id: string(),
44726
- url: string()
44727
- });
44728
- var StatusSchema = object({
44729
- brokerCount: number(),
44730
- embeddedRunning: boolean()
44731
- });
44732
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
44733
- kind: "mutation",
44734
- auth: "admin"
44735
- }), method(IdInputSchema, _void(), {
44736
- kind: "mutation",
44737
- auth: "admin"
44738
- }), method(IdInputSchema, TestResultSchema$1, {
44739
- kind: "mutation",
44740
- auth: "admin"
44741
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
44742
- kind: "mutation",
44743
- auth: "admin"
44744
- }), method(IdInputSchema, _void(), {
44745
- kind: "mutation",
44746
- auth: "admin"
44747
- }), method(_void(), StatusSchema);
44748
- var NetworkEndpointSchema = object({
44749
- url: string(),
44750
- hostname: string(),
44751
- port: number(),
44752
- protocol: _enum(["http", "https"]),
44753
- /**
44754
- * Whether this address survives a restart of its provider. `false` = it is
44755
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
44756
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
44757
- * it stays visible and an operator may still flag it. Absent = stable: every
44758
- * provider except a Quick tunnel owns a name that does not change.
44759
- */
44760
- stable: boolean().optional()
44761
- });
44762
- object({
44763
- url: string(),
44764
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
44765
- stable: boolean().optional(),
44766
- updated: boolean().optional()
44767
- });
44768
- var NetworkAccessStatusSchema = object({
44769
- connected: boolean(),
44770
- endpoint: NetworkEndpointSchema.nullable(),
44771
- error: string().optional()
44772
- });
44773
- /**
44774
- * Optional, richer endpoint shape returned by providers that expose
44775
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
44776
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44777
- * the originating provider config (mode + sourcePort) so the
44778
- * orchestrator UI can label rows distinctly. Providers that expose only
44779
- * one endpoint just omit `listEndpoints` from their provider impl.
44780
- */
44781
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
44782
- /**
44783
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
44784
- * the orchestrator can dedupe across `listEndpoints` polls.
44785
- */
44786
- id: string(),
44787
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
44788
- label: string(),
44789
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
44790
- mode: string().optional(),
44791
- /** Originating local port the ingress fronts (informational). */
44792
- sourcePort: number().optional()
44793
- });
44794
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
44795
- /**
44796
- * notification-output — canonical, capability-gated notification delivery.
44797
- *
44798
- * Apprise-derived model (see
44799
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44800
- * callers emit ONE canonical `Notification`; each provider declares a
44801
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44802
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44803
- * message to what the kind supports — callers never special-case a service.
44804
- *
44805
- * DESIGN DECISIONS (locked):
44806
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44807
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44808
- * cap. Rationale: the admin UI needs one uniform surface across the
44809
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44810
- * alternative would fork the UI per addon and cannot host the
44811
- * discovery→adopt flow.
44812
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44813
- * the generated cap-mount auto-`concatCollection`-fans them across every
44814
- * registered provider (notifiers addon + HA addon) so one catalog is
44815
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44816
- * `addonId` the generated collection router extracts from the call input.
44817
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44818
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44819
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44820
- * base64 fallback needed.
44821
- *
44822
- * TODO (deferred, closed-set change — separate decision): add
44823
- * `providerKind: 'notify'` so notification providers surface on the unified
44824
- * admin "Integrations" page.
44825
- */
44826
- /**
44827
- * Zentik-derived typed-media enum — the superset across every kind. Each
44828
- * adapter picks what it supports and the degrade engine filters the rest.
44829
- */
44830
- var AttachmentMediaTypeSchema = _enum([
44831
- "image",
44832
- "video",
44833
- "gif",
44834
- "audio",
44835
- "icon"
44836
- ]);
44837
- /**
44838
- * A single attachment. Exactly one of `url` (remote source, most adapters
44839
- * prefer this) or `bytes` (inline source; required for Pushover-style
44840
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44841
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44842
- */
44843
- var AttachmentSchema = object({
44844
- mediaType: AttachmentMediaTypeSchema,
44845
- url: string().optional(),
44846
- bytes: _instanceof(Uint8Array).optional(),
44847
- mime: string().optional(),
44848
- name: string().optional()
44849
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44850
- var NotificationFormatSchema = _enum([
44851
- "text",
44852
- "markdown",
44853
- "html"
44854
- ]);
44855
- /**
44856
- * The CLOSED icon vocabulary an action button may use.
44857
- *
44858
- * A closed set, not a free string, and that is the whole point: an arbitrary
44859
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44860
- * notices — the same class of gap as a zone vocabulary nothing produced
44861
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44862
- * declares `actionIcons: false` and the degrade engine strips the field.
44863
- *
44864
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44865
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44866
- * differently.
44867
- *
44868
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44869
- *
44870
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44871
- * the whole set onto its own renderer's vocabulary through a
44872
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44873
- * fallback, so adding a member here fails every adapter's build until someone
44874
- * decides its glyph, which is the only place that decision can be made
44875
- * honestly.
44876
- *
44877
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44878
- * `disarm` straight through; iOS feeds that string to
44879
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44880
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44881
- * mapping, and "the field is documented" is not "the value renders".
44882
- *
44883
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44884
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44885
- * so an addon that emits a token the running hub does not know does not lose an
44886
- * icon — its whole `send` fails Zod validation and the notification never
44887
- * arrives. Never emit a new token from an addon before the train carrying it.
44888
- */
44889
- var NotificationActionIconSchema = _enum([
44890
- "acknowledge",
44891
- "dismiss",
44892
- "silence",
44893
- "snooze",
44894
- "view",
44895
- "play",
44896
- "open",
44897
- "close",
44898
- "lock",
44899
- "unlock",
44900
- "arm",
44901
- "arm-home",
44902
- "arm-away",
44903
- "arm-night",
44904
- "disarm",
44905
- "light",
44906
- "alert",
44907
- "camera"
44908
- ]);
44909
- /** A single tap-through action button. */
44910
- var NotificationActionSchema = object({
44911
- id: string(),
44912
- label: string(),
44913
- url: string().optional(),
44914
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44915
- icon: NotificationActionIconSchema.optional(),
44916
- /**
44917
- * Renders in a warning style where the notifier supports it.
44918
- *
44919
- * A HINT, never a gate. The callback's authority is its token and nothing
44920
- * else — see `notification-center/action-token.ts` for what that does and
44921
- * does not buy.
44922
- */
44923
- destructive: boolean().optional(),
44924
- /**
44925
- * How the tap should REACH the url.
44926
- *
44927
- * `navigate` (absent, and every button authored before this field) opens it:
44928
- * the phone leaves the notification and shows whatever the callback returns.
44929
- * That is right for a button whose answer the operator wants to read.
44930
- *
44931
- * `background` fires it as a POST and stays put. It exists for the buttons
44932
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44933
- * an answer to the notification, and being thrown into a browser tab to
44934
- * confirm it costs more attention than the notification did. A backend that
44935
- * cannot do a background call renders it as an ordinary link (the adapters
44936
- * fall back rather than dropping the button), so this is a preference, never
44937
- * a requirement.
44938
- */
44939
- mode: _enum(["navigate", "background"]).optional()
44940
- });
44941
- /** A delivery parameter value: what a notifier declared it takes. */
44942
- var DeliveryParamValueSchema = union([
44943
- string(),
44944
- number(),
44945
- boolean()
44946
- ]);
44947
- /**
44948
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44949
- * addresses a target of that kind may set it; a kind that declares nothing is
44950
- * never handed one. The admin UI derives its control from this descriptor —
44951
- * no editor knows a service by name.
44952
- *
44953
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44954
- * run on the addon cap path).
44955
- */
44956
- var DeliveryParamSchema = object({
44957
- key: string().min(1),
44958
- label: string(),
44959
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44960
- labelKey: string().optional(),
44961
- type: _enum([
44962
- "enum",
44963
- "number",
44964
- "boolean",
44965
- "string"
44966
- ]),
44967
- /** `enum` only: the closed set of accepted values. */
44968
- options: array(object({
44969
- value: string(),
44970
- label: string()
44971
- })).optional(),
44972
- /** `number` only. */
44973
- min: number().optional(),
44974
- max: number().optional(),
44975
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44976
- default: DeliveryParamValueSchema.optional()
44977
- });
44978
- /**
44979
- * The canonical notification. `body` is the only hard field (Apprise model).
44980
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44981
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44982
- * the adapter maps this ordinal onto its native level. `level?` is an
44983
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44984
- * `priority` for that one target.
44985
- */
44986
- var NotificationSchema = object({
44987
- body: string(),
44988
- title: string().optional(),
44989
- format: NotificationFormatSchema.default("text"),
44990
- priority: number().int().min(1).max(5).default(3),
44991
- level: string().optional(),
44992
- attachments: array(AttachmentSchema).optional(),
44993
- clickUrl: string().optional(),
44994
- actions: array(NotificationActionSchema).optional(),
44995
- sound: string().optional(),
44996
- ttl: number().optional(),
44997
- tag: string().optional(),
44998
- deviceId: number().optional(),
44999
- eventId: string().optional(),
45000
- metadata: record(string(), unknown()).optional(),
45001
- /**
45002
- * Values for the parameters the target's kind DECLARES in
45003
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45004
- * declaration by the sender; a notifier reads only the keys it declared.
45005
- */
45006
- delivery: record(string(), DeliveryParamValueSchema).optional()
45007
- });
45008
- /** One declared native severity/priority level for a kind. */
45009
- var TargetKindLevelSchema = object({
45010
- id: string(),
45011
- label: string(),
45012
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45013
- ordinal: number().int().min(1).max(5).nullable(),
45014
- flags: object({
45015
- critical: boolean().optional(),
45016
- silent: boolean().optional(),
45017
- noPush: boolean().optional()
45018
- }).optional(),
45019
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45020
- requires: array(string()).optional(),
45021
- description: string().optional()
45022
- });
45023
- /** The full capability block consulted before dispatch. */
45024
- var TargetKindCapsSchema = object({
45025
- attachments: object({
45026
- mediaTypes: array(AttachmentMediaTypeSchema),
45027
- mode: _enum([
45028
- "url",
45029
- "bytes",
45030
- "both"
45031
- ]),
45032
- max: number().int().nonnegative(),
45033
- maxBytes: number().int().positive().optional()
45034
- }),
45035
- /** Max action buttons (0 = none). */
45036
- actions: number().int().nonnegative(),
45037
- /**
45038
- * Whether this kind renders a per-action ICON.
45039
- *
45040
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45041
- * run on the addon cap path — three production failures in one day taught
45042
- * this repo that once. Absent is read as false by the degrade engine, which
45043
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45044
- * assumed and dropped costs the operator's trust in the field.
45045
- */
45046
- actionIcons: boolean().optional(),
45047
- levels: array(TargetKindLevelSchema),
45048
- format: array(NotificationFormatSchema),
45049
- clickUrl: boolean(),
45050
- sound: boolean(),
45051
- ttl: boolean(),
45052
- bodyMaxLen: number().int().positive()
45053
- });
45054
- /**
45055
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45056
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45057
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45058
- * the union is large and not meant for runtime validation here; the exported
45059
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45060
- */
45061
- var ConfigSchemaPassthrough$2 = unknown();
45062
- var TargetKindSchema = object({
45063
- kind: string(),
45064
- label: string(),
45065
- icon: string(),
45066
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45067
- addonId: string(),
45068
- /**
45069
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45070
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45071
- * when the addon bundles no icon for that kind — the client then falls back
45072
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45073
- *
45074
- * Root-relative on purpose: it resolves against whatever origin serves a web
45075
- * client, and a native client joins it onto its own hub base.
45076
- *
45077
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45078
- * field that survived only because the runtime cap-router forwards provider
45079
- * output verbatim — so every consumer had to re-declare it by hand to stop
45080
- * its own Zod parse from stripping it, and the whole arrangement would have
45081
- * broken silently the moment output validation was tightened anywhere.
45082
- */
45083
- iconUrl: string().optional(),
45084
- /**
45085
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45086
- *
45087
- * The server knows this and therefore says it, because the client cannot
45088
- * safely guess: a React-Native client renders SVG and raster through two
45089
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45090
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45091
- * placeholder glyph for every vector icon while the web build looked fine.
45092
- *
45093
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45094
- * not been updated — a client that cannot determine the type should prefer
45095
- * its raster path, which is the safe default for an unknown image.
45096
- */
45097
- iconMediaType: string().optional(),
45098
- configSchema: ConfigSchemaPassthrough$2,
45099
- /**
45100
- * The config keys that decide WHERE this kind's secrets are sent (server,
45101
- * base URL, instance, chat, recipient). A save that changes any of them
45102
- * while leaving a secret redacted is refused by the provider (D634), so an
45103
- * editor uses this to ask for the secret on exactly those edits instead of
45104
- * on every edit.
45103
+ * The node whose series is wanted.
45105
45104
  *
45106
- * Optional: absent from a provider that holds no secret (the Home Assistant
45107
- * kinds) and from a provider older than this field — an editor that finds
45108
- * it absent must assume ANY edit may need the secret.
45105
+ * NOT named `nodeId`: the generated cap router strips a top-level
45106
+ * `nodeId` from every method input and uses it to ROUTE the call to
45107
+ * that node's provider (`generated-cap-routers.ts`). A series target
45108
+ * called `nodeId` would silently become a routing pin and never reach
45109
+ * the provider. The hub holds every node it hears from, so the
45110
+ * ordinary call is unpinned — answered by the hub, for any node.
45109
45111
  */
45110
- destinationKeys: array(string()).optional(),
45111
- supportsDiscovery: boolean(),
45112
+ forNodeId: string(),
45112
45113
  /**
45113
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45114
- * Absent = none; the rule editor then offers nothing.
45114
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
45115
+ * holds and receives only what it is missing, so seeding a live chart
45116
+ * from this method cannot double a point already drawn.
45115
45117
  */
45116
- deliveryParams: array(DeliveryParamSchema).optional(),
45117
- caps: TargetKindCapsSchema
45118
+ sinceMs: number().optional(),
45119
+ /**
45120
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
45121
+ * preserving min and max per bucket.
45122
+ *
45123
+ * Absent means NO reduction — legitimate for a short window and a trap for a
45124
+ * long one, which is why a chart passes its own pixel width.
45125
+ */
45126
+ maxPoints: number().int().positive().optional()
45127
+ });
45128
+ var SystemMetricsSchema = object({
45129
+ cpuPercent: number(),
45130
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45131
+ memoryPercent: number().nullable(),
45132
+ memoryUsedMB: number().nullable(),
45133
+ memoryTotalMB: number(),
45134
+ diskPercent: number().optional(),
45135
+ temperature: number().optional(),
45136
+ gpuPercent: number().optional(),
45137
+ gpuMemoryPercent: number().optional()
45138
+ });
45139
+ 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);
45140
+ var modelConvertCapability = {
45141
+ name: "model-convert",
45142
+ scope: "system",
45143
+ mode: "singleton",
45144
+ internal: true,
45145
+ methods: { convert: method(object({
45146
+ sourceUrl: string(),
45147
+ metadata: ModelConvertMetadataSchema,
45148
+ targets: array(ConvertTargetSchema).min(1).readonly(),
45149
+ calibrationRef: string().optional(),
45150
+ sessionId: string().optional()
45151
+ }), ConvertResultSchema, {
45152
+ kind: "mutation",
45153
+ auth: "admin",
45154
+ timeoutMs: 6e5
45155
+ }) }
45156
+ };
45157
+ method(object({
45158
+ targetNodeId: string(),
45159
+ modelId: string(),
45160
+ format: _enum(MODEL_FORMATS),
45161
+ entry: ModelCatalogEntrySchema
45162
+ }), object({
45163
+ ok: boolean(),
45164
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
45165
+ sha256: string(),
45166
+ bytes: number(),
45167
+ /** The target node's modelsDir the artifact landed in. */
45168
+ path: string()
45169
+ }), {
45170
+ kind: "mutation",
45171
+ auth: "admin"
45172
+ });
45173
+ var MotionRegionSchema = object({
45174
+ bbox: BoundingBoxSchema,
45175
+ pixelCount: number(),
45176
+ intensity: number()
45177
+ });
45178
+ var MotionAnalysisResultSchema = object({
45179
+ detected: boolean(),
45180
+ regionCount: number(),
45181
+ /** Regions passing minArea filter — used for pipeline triggering. */
45182
+ regions: array(MotionRegionSchema).readonly(),
45183
+ /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
45184
+ rawRegions: array(MotionRegionSchema).readonly(),
45185
+ frameWidth: number(),
45186
+ frameHeight: number(),
45187
+ analysisMs: number()
45188
+ });
45189
+ DeviceType.Camera, method(object({
45190
+ deviceId: number(),
45191
+ frame: FrameInputSchema.optional(),
45192
+ frameHandle: FrameHandleSchema.optional()
45193
+ }), MotionAnalysisResultSchema, {
45194
+ role: "admin",
45195
+ kind: "mutation"
45196
+ }), method(object({ deviceId: number() }), _void(), {
45197
+ role: "admin",
45198
+ kind: "mutation"
45199
+ }), method(_void(), _void(), {
45200
+ role: "admin",
45201
+ kind: "mutation"
45202
+ }), object({
45203
+ deviceId: number(),
45204
+ detected: boolean(),
45205
+ regionCount: number()
45118
45206
  });
45119
45207
  /**
45120
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45121
- * (return a presence marker only) when serving `listTargets` — never
45122
- * round-trip a stored secret to the UI.
45208
+ * `mqtt-broker` — broker-registry cap.
45209
+ *
45210
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
45211
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
45212
+ * and (b) the connection details a consumer addon needs to spin up
45213
+ * its OWN `mqtt.js` client.
45214
+ *
45215
+ * Why: pub/sub routing over the system event-bus loses fidelity
45216
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
45217
+ * refcount bookkeeping that addons would rather own themselves. The
45218
+ * canonical consumer needs raw `mqtt.js`
45219
+ * features anyway — give it the connection config, get out of the way.
45220
+ *
45221
+ * Consumer flow:
45222
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
45223
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
45224
+ * client.subscribe('zigbee2mqtt/+')
45225
+ *
45226
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
45227
+ * cloud bridge). The "embedded" entry (when present) is just another
45228
+ * broker in the registry — its lifecycle is owned by the addon that
45229
+ * spawned it.
45123
45230
  */
45124
- var TargetSchema = object({
45231
+ var BrokerKindSchema = _enum(["external", "embedded"]);
45232
+ /**
45233
+ * Broker live-probe status.
45234
+ *
45235
+ * - `connected` — last probe completed a clean CONNACK
45236
+ * - `disconnected` — no probe has run yet (cold cache)
45237
+ * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
45238
+ * - `unreachable` — TCP connect timed out / refused
45239
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
45240
+ */
45241
+ var BrokerStatusSchema$1 = _enum([
45242
+ "connected",
45243
+ "disconnected",
45244
+ "auth-failed",
45245
+ "unreachable",
45246
+ "tls-error"
45247
+ ]);
45248
+ var BrokerInfoSchema$1 = object({
45125
45249
  id: string(),
45126
45250
  name: string(),
45127
- kind: string(),
45128
- addonId: string(),
45129
- enabled: boolean(),
45130
- config: record(string(), unknown())
45131
- });
45132
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45133
- var DiscoveredTargetSchema = object({
45134
- kind: string(),
45135
- suggestedName: string(),
45136
- config: record(string(), unknown())
45137
- });
45138
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45139
- var RenderedAsSchema = object({
45140
- level: string(),
45141
- format: NotificationFormatSchema,
45142
- attachmentsSent: number().int().nonnegative(),
45143
- actionsSent: number().int().nonnegative(),
45144
- truncated: boolean(),
45145
- dropped: array(string())
45146
- });
45147
- var SendResultSchema = object({
45148
- success: boolean(),
45251
+ url: string(),
45252
+ kind: BrokerKindSchema,
45253
+ status: BrokerStatusSchema$1,
45254
+ latencyMs: number().nullable(),
45149
45255
  error: string().optional(),
45150
- renderedAs: RenderedAsSchema.optional()
45256
+ /** Embedded brokers only: number of MQTT clients currently connected. */
45257
+ connectedClients: number().int().nonnegative().optional(),
45258
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45259
+ lastCheckedAt: number().optional()
45151
45260
  });
45152
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45153
- var TestResultSchema = SendResultSchema;
45154
45261
  /**
45155
- * Pre-save credential probe against a partial (unsaved) target config.
45156
- * `resolvedUrl` never includes secrets (token stripped from path).
45262
+ * Connection details — what a consumer needs to call
45263
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
45264
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
45265
+ * instead of stuffing creds into the URL (which leaks them into logs).
45157
45266
  */
45158
- var ProbeTargetConfigResultSchema = object({
45159
- ok: boolean(),
45160
- resolvedUrl: string(),
45161
- httpStatus: number().int().optional(),
45162
- stateInstance: string().optional(),
45163
- error: string().optional()
45164
- });
45165
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
45166
- kind: string(),
45167
- config: record(string(), unknown()).optional(),
45267
+ var BrokerConnectionDetailsSchema = object({
45268
+ url: string(),
45269
+ username: string().optional(),
45270
+ password: string().optional(),
45168
45271
  /**
45169
- * When editing / duplicating, the blank password field never round-trips
45170
- * the stored token. Pass the source target id so the provider can restore
45171
- * secrets server-side before dialling (same merge as upsert).
45272
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45273
+ * with its own discriminator (addon id, instance id) so reconnects
45274
+ * don't kick each other off (MQTT spec: clientId must be unique per
45275
+ * broker).
45172
45276
  */
45173
- targetId: string().optional()
45174
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
45175
- kind: string(),
45176
- config: record(string(), unknown()),
45177
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45178
- targetId: string().optional()
45179
- }), ProbeTargetConfigResultSchema, {
45180
- kind: "mutation",
45181
- auth: "admin"
45182
- }), method(object({
45183
- targetId: string(),
45184
- notification: NotificationSchema
45185
- }), SendResultSchema, {
45277
+ clientIdPrefix: string().optional()
45278
+ });
45279
+ var AddBrokerInputSchema = object({
45280
+ name: string().min(1),
45281
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45282
+ username: string().optional(),
45283
+ password: string().optional(),
45284
+ clientIdPrefix: string().optional()
45285
+ });
45286
+ var AddBrokerResultSchema = object({ id: string() });
45287
+ var IdInputSchema = object({ id: string() });
45288
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45289
+ ok: literal(true),
45290
+ latencyMs: number()
45291
+ }), object({
45292
+ ok: literal(false),
45293
+ error: string()
45294
+ })]);
45295
+ var StartEmbeddedInputSchema = object({
45296
+ port: number().int().min(1).max(65535).default(1883),
45297
+ /** Allow anonymous connect (no username/password). Default: false. */
45298
+ allowAnonymous: boolean().default(false),
45299
+ /** Optional shared username/password for clients. */
45300
+ username: string().optional(),
45301
+ password: string().optional()
45302
+ });
45303
+ var StartEmbeddedResultSchema = object({
45304
+ id: string(),
45305
+ url: string()
45306
+ });
45307
+ var StatusSchema = object({
45308
+ brokerCount: number(),
45309
+ embeddedRunning: boolean()
45310
+ });
45311
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45186
45312
  kind: "mutation",
45187
45313
  auth: "admin"
45188
- }), method(object({
45189
- targetId: string(),
45190
- sample: NotificationSchema.optional()
45191
- }), TestResultSchema, {
45314
+ }), method(IdInputSchema, _void(), {
45192
45315
  kind: "mutation",
45193
45316
  auth: "admin"
45194
- }), method(object({
45195
- target: TargetSchema,
45196
- /**
45197
- * Duplicate flow: create a NEW target id while copying secrets from an
45198
- * existing row. The UI never holds the stored token; blank / redacted
45199
- * password fields are restored from this source (D634 still applies if
45200
- * destinations diverge without a re-typed secret).
45201
- */
45202
- cloneSecretsFrom: string().optional()
45203
- }), TargetSchema, {
45317
+ }), method(IdInputSchema, TestResultSchema$1, {
45204
45318
  kind: "mutation",
45205
45319
  auth: "admin"
45206
- }), method(object({ targetId: string() }), _void(), {
45320
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45207
45321
  kind: "mutation",
45208
45322
  auth: "admin"
45209
- }), method(object({
45210
- targetId: string(),
45211
- enabled: boolean()
45212
- }), _void(), {
45323
+ }), method(IdInputSchema, _void(), {
45213
45324
  kind: "mutation",
45214
45325
  auth: "admin"
45326
+ }), method(_void(), StatusSchema);
45327
+ var NetworkEndpointSchema = object({
45328
+ url: string(),
45329
+ hostname: string(),
45330
+ port: number(),
45331
+ protocol: _enum(["http", "https"]),
45332
+ /**
45333
+ * Whether this address survives a restart of its provider. `false` = it is
45334
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45335
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45336
+ * it stays visible and an operator may still flag it. Absent = stable: every
45337
+ * provider except a Quick tunnel owns a name that does not change.
45338
+ */
45339
+ stable: boolean().optional()
45340
+ });
45341
+ object({
45342
+ url: string(),
45343
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45344
+ stable: boolean().optional(),
45345
+ updated: boolean().optional()
45346
+ });
45347
+ var NetworkAccessStatusSchema = object({
45348
+ connected: boolean(),
45349
+ endpoint: NetworkEndpointSchema.nullable(),
45350
+ error: string().optional()
45351
+ });
45352
+ /**
45353
+ * Optional, richer endpoint shape returned by providers that expose
45354
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45355
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45356
+ * the originating provider config (mode + sourcePort) so the
45357
+ * orchestrator UI can label rows distinctly. Providers that expose only
45358
+ * one endpoint just omit `listEndpoints` from their provider impl.
45359
+ */
45360
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45361
+ /**
45362
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45363
+ * the orchestrator can dedupe across `listEndpoints` polls.
45364
+ */
45365
+ id: string(),
45366
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45367
+ label: string(),
45368
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45369
+ mode: string().optional(),
45370
+ /** Originating local port the ingress fronts (informational). */
45371
+ sourcePort: number().optional()
45215
45372
  });
45373
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45216
45374
  var AUDIO_MACRO_LABELS = [
45217
45375
  {
45218
45376
  id: "speech",
@@ -51613,14 +51771,6 @@ var EngineProvisioningSchema = object({
51613
51771
  */
51614
51772
  configIssues: array(string()).optional()
51615
51773
  });
51616
- var PipelineStepInputSchema = lazy(() => object({
51617
- addonId: string(),
51618
- modelId: string().optional(),
51619
- enabled: boolean().default(true),
51620
- children: array(PipelineStepInputSchema).optional(),
51621
- settings: record(string(), unknown()).optional(),
51622
- jumpDeviceKey: string().optional()
51623
- }));
51624
51774
  var ModelSubstitutionSchema = object({
51625
51775
  addonId: string(),
51626
51776
  chosen: string(),
@@ -51699,7 +51849,6 @@ var ConfigUISchemaBridge = custom();
51699
51849
  var ConfigUISchemaNullableBridge = custom();
51700
51850
  var InferenceCapabilitiesBridge = custom();
51701
51851
  var ModelAvailabilityListBridge = custom();
51702
- var PipelineRunResultBridge = custom();
51703
51852
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
51704
51853
  modelId: string(),
51705
51854
  settings: record(string(), unknown()).readonly()
@@ -59373,7 +59522,7 @@ object({
59373
59522
  /** Ms epoch of the last value received from the appliance; 0 = never. */
59374
59523
  lastFetchedAt: number()
59375
59524
  });
59376
- var DeviceIdSchema = number().int().nonnegative();
59525
+ var DeviceIdSchema$1 = number().int().nonnegative();
59377
59526
  /** A value a program option or setting can take. */
59378
59527
  var DishwasherValueSchema = union([
59379
59528
  boolean(),
@@ -59437,77 +59586,77 @@ var DishwasherOptionsSchema = object({
59437
59586
  options: array(DishwasherOptionDescriptorSchema)
59438
59587
  });
59439
59588
  DeviceType.Dishwasher, method(object({
59440
- deviceId: DeviceIdSchema,
59589
+ deviceId: DeviceIdSchema$1,
59441
59590
  program: string().min(1).optional(),
59442
59591
  options: record(string(), DishwasherValueSchema).optional()
59443
59592
  }), _void(), {
59444
59593
  kind: "mutation",
59445
59594
  auth: "protected"
59446
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59595
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59447
59596
  kind: "mutation",
59448
59597
  auth: "protected"
59449
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59598
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59450
59599
  kind: "mutation",
59451
59600
  auth: "protected"
59452
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59601
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59453
59602
  kind: "mutation",
59454
59603
  auth: "protected"
59455
59604
  }), method(object({
59456
- deviceId: DeviceIdSchema,
59605
+ deviceId: DeviceIdSchema$1,
59457
59606
  program: string().min(1)
59458
59607
  }), _void(), {
59459
59608
  kind: "mutation",
59460
59609
  auth: "protected"
59461
59610
  }), method(object({
59462
- deviceId: DeviceIdSchema,
59611
+ deviceId: DeviceIdSchema$1,
59463
59612
  on: boolean()
59464
59613
  }), _void(), {
59465
59614
  kind: "mutation",
59466
59615
  auth: "protected"
59467
59616
  }), method(object({
59468
- deviceId: DeviceIdSchema,
59617
+ deviceId: DeviceIdSchema$1,
59469
59618
  on: boolean()
59470
59619
  }), _void(), {
59471
59620
  kind: "mutation",
59472
59621
  auth: "protected"
59473
59622
  }), method(object({
59474
- deviceId: DeviceIdSchema,
59623
+ deviceId: DeviceIdSchema$1,
59475
59624
  on: boolean()
59476
59625
  }), _void(), {
59477
59626
  kind: "mutation",
59478
59627
  auth: "protected"
59479
59628
  }), method(object({
59480
- deviceId: DeviceIdSchema,
59629
+ deviceId: DeviceIdSchema$1,
59481
59630
  on: boolean()
59482
59631
  }), _void(), {
59483
59632
  kind: "mutation",
59484
59633
  auth: "protected"
59485
59634
  }), method(object({
59486
- deviceId: DeviceIdSchema,
59635
+ deviceId: DeviceIdSchema$1,
59487
59636
  on: boolean()
59488
59637
  }), _void(), {
59489
59638
  kind: "mutation",
59490
59639
  auth: "protected"
59491
59640
  }), method(object({
59492
- deviceId: DeviceIdSchema,
59641
+ deviceId: DeviceIdSchema$1,
59493
59642
  on: boolean()
59494
59643
  }), _void(), {
59495
59644
  kind: "mutation",
59496
59645
  auth: "protected"
59497
59646
  }), method(object({
59498
- deviceId: DeviceIdSchema,
59647
+ deviceId: DeviceIdSchema$1,
59499
59648
  on: boolean()
59500
59649
  }), _void(), {
59501
59650
  kind: "mutation",
59502
59651
  auth: "protected"
59503
59652
  }), method(object({
59504
- deviceId: DeviceIdSchema,
59653
+ deviceId: DeviceIdSchema$1,
59505
59654
  key: string().min(1),
59506
59655
  value: DishwasherValueSchema
59507
59656
  }), _void(), {
59508
59657
  kind: "mutation",
59509
59658
  auth: "protected"
59510
- }), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
59659
+ }), method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" });
59511
59660
  DeviceType.Camera;
59512
59661
  object({
59513
59662
  /** Ms epoch of the last press. null = never observed since this provider started. */
@@ -61945,7 +62094,8 @@ var TopologyNodeSchema = object({
61945
62094
  isHub: boolean(),
61946
62095
  isOnline: boolean(),
61947
62096
  cpuPercent: number(),
61948
- memoryPercent: number(),
62097
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
62098
+ memoryPercent: number().nullable(),
61949
62099
  uptime: number(),
61950
62100
  lastSeen: string(),
61951
62101
  localIps: array(string()).readonly(),
@@ -62480,9 +62630,10 @@ object({
62480
62630
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
62481
62631
  * the device has not reported yet. */
62482
62632
  foodLevel: number().nullable().meta(readingMeta("none")),
62483
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
62484
- * feeder's own units. Null when the model reports none. */
62485
- bowlFood: number().nullable().meta(readingMeta("none")),
62633
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
62634
+ * feeder with a bowl scale; null when the model has none or its reading is
62635
+ * of an unproven unit. */
62636
+ bowlFood: number().nullable().meta(readingMeta("mass")),
62486
62637
  /** True while the pet is eating (`state.eating`). */
62487
62638
  eating: boolean(),
62488
62639
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -62595,6 +62746,64 @@ DeviceType.PetFeeder, method(object({
62595
62746
  kind: "mutation",
62596
62747
  auth: "protected"
62597
62748
  });
62749
+ object({
62750
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
62751
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62752
+ /** Water in the tank, in litres. Null while the source has not reported. */
62753
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
62754
+ /** True when the tank is empty. Null while unknown. */
62755
+ empty: boolean().nullable(),
62756
+ /** True while the pump is running. Null while unknown. */
62757
+ pumpRunning: boolean().nullable(),
62758
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
62759
+ pumpIntervalEnabled: boolean().nullable(),
62760
+ /** How long the pump runs per cycle, in seconds. */
62761
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
62762
+ /** How long the pump rests per cycle, in seconds. */
62763
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
62764
+ /** Water drunk in the last drink, in litres. */
62765
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
62766
+ /** Water drunk in the current hour, in litres. */
62767
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
62768
+ /** Water drunk over the last 24 hours (rolling), in litres. */
62769
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
62770
+ /** Water drunk since local midnight, in litres. */
62771
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
62772
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
62773
+ error: string().nullable(),
62774
+ /** Ms epoch when the slice was last refreshed. */
62775
+ lastFetchedAt: number()
62776
+ });
62777
+ var DeviceIdSchema = number().int().nonnegative();
62778
+ var MinutesSchema = number().int().min(0);
62779
+ DeviceType.PetFountain, method(object({
62780
+ deviceId: DeviceIdSchema,
62781
+ on: boolean()
62782
+ }), _void(), {
62783
+ kind: "mutation",
62784
+ auth: "protected"
62785
+ }), method(object({
62786
+ deviceId: DeviceIdSchema,
62787
+ on: boolean()
62788
+ }), _void(), {
62789
+ kind: "mutation",
62790
+ auth: "protected"
62791
+ }), method(object({
62792
+ deviceId: DeviceIdSchema,
62793
+ minutes: MinutesSchema
62794
+ }), _void(), {
62795
+ kind: "mutation",
62796
+ auth: "protected"
62797
+ }), method(object({
62798
+ deviceId: DeviceIdSchema,
62799
+ minutes: MinutesSchema
62800
+ }), _void(), {
62801
+ kind: "mutation",
62802
+ auth: "protected"
62803
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
62804
+ kind: "mutation",
62805
+ auth: "protected"
62806
+ });
62598
62807
  var VehicleSchema = object({
62599
62808
  id: string(),
62600
62809
  name: string(),
@@ -67192,7 +67401,7 @@ discriminatedUnion("kind", [object({
67192
67401
  })]);
67193
67402
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
67194
67403
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
67195
- 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;
67404
+ 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;
67196
67405
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
67197
67406
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
67198
67407
  new Set(CAP_NAMES);
@@ -67343,7 +67552,7 @@ var AccessoryKind = {
67343
67552
  PrivacyMask: DeviceRole.PrivacyMask
67344
67553
  };
67345
67554
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
67346
- 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;
67555
+ 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;
67347
67556
  new Set(Object.values(DeviceType));
67348
67557
  Object.freeze({
67349
67558
  "accessories.getStatus": {
@@ -69278,6 +69487,13 @@ Object.freeze({
69278
69487
  access: "create",
69279
69488
  role: "admin"
69280
69489
  },
69490
+ "deviceExport.exposeEntity": {
69491
+ capName: "device-export",
69492
+ capScope: "system",
69493
+ addonId: null,
69494
+ access: "create",
69495
+ role: "admin"
69496
+ },
69281
69497
  "deviceExport.getDeviceLiveContribution": {
69282
69498
  capName: "device-export",
69283
69499
  capScope: "system",
@@ -69306,6 +69522,13 @@ Object.freeze({
69306
69522
  access: "view",
69307
69523
  role: "admin"
69308
69524
  },
69525
+ "deviceExport.listExposedEntities": {
69526
+ capName: "device-export",
69527
+ capScope: "system",
69528
+ addonId: null,
69529
+ access: "view",
69530
+ role: "admin"
69531
+ },
69309
69532
  "deviceExport.listSupportedDeviceKinds": {
69310
69533
  capName: "device-export",
69311
69534
  capScope: "system",
@@ -69313,6 +69536,13 @@ Object.freeze({
69313
69536
  access: "view",
69314
69537
  role: "admin"
69315
69538
  },
69539
+ "deviceExport.listSupportedEntityKinds": {
69540
+ capName: "device-export",
69541
+ capScope: "system",
69542
+ addonId: null,
69543
+ access: "view",
69544
+ role: "admin"
69545
+ },
69316
69546
  "deviceExport.unexposeDevice": {
69317
69547
  capName: "device-export",
69318
69548
  capScope: "system",
@@ -69320,6 +69550,13 @@ Object.freeze({
69320
69550
  access: "create",
69321
69551
  role: "admin"
69322
69552
  },
69553
+ "deviceExport.unexposeEntity": {
69554
+ capName: "device-export",
69555
+ capScope: "system",
69556
+ addonId: null,
69557
+ access: "create",
69558
+ role: "admin"
69559
+ },
69323
69560
  "deviceExtension.listCandidates": {
69324
69561
  capName: "device-extension",
69325
69562
  capScope: "device",
@@ -72141,6 +72378,13 @@ Object.freeze({
72141
72378
  access: "create",
72142
72379
  role: "admin"
72143
72380
  },
72381
+ "nodeRemovalHook.onNodeRemoved": {
72382
+ capName: "node-removal-hook",
72383
+ capScope: "system",
72384
+ addonId: null,
72385
+ access: "create",
72386
+ role: "admin"
72387
+ },
72144
72388
  "nodes.clusterAddonStatus": {
72145
72389
  capName: "nodes",
72146
72390
  capScope: "system",
@@ -72778,6 +73022,48 @@ Object.freeze({
72778
73022
  access: "create",
72779
73023
  role: "viewer"
72780
73024
  },
73025
+ "petFountain.getStatus": {
73026
+ capName: "pet-fountain",
73027
+ capScope: "device",
73028
+ addonId: null,
73029
+ access: "view",
73030
+ role: "viewer"
73031
+ },
73032
+ "petFountain.resetDailyConsumption": {
73033
+ capName: "pet-fountain",
73034
+ capScope: "device",
73035
+ addonId: null,
73036
+ access: "delete",
73037
+ role: "viewer"
73038
+ },
73039
+ "petFountain.setPump": {
73040
+ capName: "pet-fountain",
73041
+ capScope: "device",
73042
+ addonId: null,
73043
+ access: "create",
73044
+ role: "viewer"
73045
+ },
73046
+ "petFountain.setPumpInterval": {
73047
+ capName: "pet-fountain",
73048
+ capScope: "device",
73049
+ addonId: null,
73050
+ access: "create",
73051
+ role: "viewer"
73052
+ },
73053
+ "petFountain.setPumpOffMinutes": {
73054
+ capName: "pet-fountain",
73055
+ capScope: "device",
73056
+ addonId: null,
73057
+ access: "create",
73058
+ role: "viewer"
73059
+ },
73060
+ "petFountain.setPumpOnMinutes": {
73061
+ capName: "pet-fountain",
73062
+ capScope: "device",
73063
+ addonId: null,
73064
+ access: "create",
73065
+ role: "viewer"
73066
+ },
72781
73067
  "pipelineAnalytics.addRetrainFrameFromRecording": {
72782
73068
  capName: "pipeline-analytics",
72783
73069
  capScope: "device",
@@ -79260,6 +79546,100 @@ var APPLIANCE_CAP_VOCABULARY = {
79260
79546
  }
79261
79547
  ]
79262
79548
  },
79549
+ "pet-fountain": {
79550
+ scope: "device",
79551
+ noun: t("Pet fountain", "Fontana per animali"),
79552
+ actions: [
79553
+ {
79554
+ id: "petFountain.pumpOn",
79555
+ method: "setPump",
79556
+ label: t("Pump on", "Pompa accesa"),
79557
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
79558
+ icon: "play",
79559
+ args: { on: true }
79560
+ },
79561
+ {
79562
+ id: "petFountain.pumpOff",
79563
+ method: "setPump",
79564
+ label: t("Pump off", "Pompa spenta"),
79565
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
79566
+ icon: "square",
79567
+ args: { on: false }
79568
+ },
79569
+ {
79570
+ id: "petFountain.intervalOn",
79571
+ method: "setPumpInterval",
79572
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
79573
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
79574
+ icon: "timer",
79575
+ args: { on: true }
79576
+ },
79577
+ {
79578
+ id: "petFountain.intervalOff",
79579
+ method: "setPumpInterval",
79580
+ label: t("Pump cycle off", "Ciclo pompa spento"),
79581
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
79582
+ icon: "timer-off",
79583
+ args: { on: false }
79584
+ },
79585
+ {
79586
+ id: "petFountain.pumpOnMinutes",
79587
+ method: "setPumpOnMinutes",
79588
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
79589
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
79590
+ icon: "timer",
79591
+ openArgs: [{
79592
+ key: "minutes",
79593
+ label: t("Minutes", "Minuti"),
79594
+ offer: {
79595
+ kind: "number",
79596
+ min: 0,
79597
+ step: 1,
79598
+ unit: "min"
79599
+ }
79600
+ }]
79601
+ },
79602
+ {
79603
+ id: "petFountain.pumpOffMinutes",
79604
+ method: "setPumpOffMinutes",
79605
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
79606
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
79607
+ icon: "timer-off",
79608
+ openArgs: [{
79609
+ key: "minutes",
79610
+ label: t("Minutes", "Minuti"),
79611
+ offer: {
79612
+ kind: "number",
79613
+ min: 0,
79614
+ step: 1,
79615
+ unit: "min"
79616
+ }
79617
+ }]
79618
+ },
79619
+ {
79620
+ id: "petFountain.resetDailyConsumption",
79621
+ method: "resetDailyConsumption",
79622
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
79623
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
79624
+ icon: "rotate-ccw"
79625
+ }
79626
+ ],
79627
+ state: [{
79628
+ kind: "boolean",
79629
+ field: "empty",
79630
+ label: t("Tank empty", "Serbatoio vuoto"),
79631
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
79632
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
79633
+ icon: "droplet"
79634
+ }, {
79635
+ kind: "numeric",
79636
+ field: "waterLevel",
79637
+ quantity: t("the water level", "il livello dell’acqua"),
79638
+ label: t("Water level", "Livello dell’acqua"),
79639
+ unit: "%",
79640
+ icon: "droplets"
79641
+ }]
79642
+ },
79263
79643
  "media-player": {
79264
79644
  scope: "device",
79265
79645
  noun: t("Media player", "Lettore multimediale"),
@@ -81084,6 +81464,7 @@ var SYSTEM_CAP_VOCABULARY = {
81084
81464
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
81085
81465
  "device-provider": system(INTERNAL),
81086
81466
  "device-removal-hook": system(INTERNAL),
81467
+ "node-removal-hook": system(INTERNAL),
81087
81468
  "device-state": system("the runtime-state mirror every trigger already reads through"),
81088
81469
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
81089
81470
  "embedding-encoder": system(ML),
@@ -81196,7 +81577,7 @@ function projectAction(cap, action) {
81196
81577
  };
81197
81578
  }
81198
81579
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
81199
- 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;
81580
+ 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;
81200
81581
  DeviceFeature.BatteryOperated;
81201
81582
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
81202
81583
  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.";
@@ -82747,6 +83128,36 @@ Object.freeze({
82747
83128
  form: "single",
82748
83129
  optional: false
82749
83130
  }],
83131
+ "petFountain.getStatus": [{
83132
+ name: "deviceId",
83133
+ form: "single",
83134
+ optional: false
83135
+ }],
83136
+ "petFountain.resetDailyConsumption": [{
83137
+ name: "deviceId",
83138
+ form: "single",
83139
+ optional: false
83140
+ }],
83141
+ "petFountain.setPump": [{
83142
+ name: "deviceId",
83143
+ form: "single",
83144
+ optional: false
83145
+ }],
83146
+ "petFountain.setPumpInterval": [{
83147
+ name: "deviceId",
83148
+ form: "single",
83149
+ optional: false
83150
+ }],
83151
+ "petFountain.setPumpOffMinutes": [{
83152
+ name: "deviceId",
83153
+ form: "single",
83154
+ optional: false
83155
+ }],
83156
+ "petFountain.setPumpOnMinutes": [{
83157
+ name: "deviceId",
83158
+ form: "single",
83159
+ optional: false
83160
+ }],
82750
83161
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
82751
83162
  name: "deviceId",
82752
83163
  form: "single",