@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
@@ -5463,6 +5463,14 @@ function object(shape, params) {
5463
5463
  ...normalizeParams(params)
5464
5464
  });
5465
5465
  }
5466
+ function strictObject(shape, params) {
5467
+ return new ZodObject({
5468
+ type: "object",
5469
+ shape,
5470
+ catchall: never(),
5471
+ ...normalizeParams(params)
5472
+ });
5473
+ }
5466
5474
  function looseObject(shape, params) {
5467
5475
  return new ZodObject({
5468
5476
  type: "object",
@@ -5842,7 +5850,7 @@ var ZodIssueCode = {
5842
5850
  var ZodFirstPartyTypeKind;
5843
5851
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5844
5852
  //#endregion
5845
- //#region ../types/dist/sleep-DJws6ho7.mjs
5853
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5846
5854
  /**
5847
5855
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5848
5856
  * window to float samples (D455).
@@ -8730,65 +8738,6 @@ var MODEL_FORMATS = [
8730
8738
  "pt",
8731
8739
  "gguf"
8732
8740
  ];
8733
- /**
8734
- * Per-call node pinning for `ctx.api` capability calls.
8735
- *
8736
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8737
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8738
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8739
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8740
- * call to that node.
8741
- *
8742
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8743
- * method args), so capability method signatures stay `nodeId`-free — node
8744
- * targeting is a property of the CALL, not of the method. The transport lifts
8745
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8746
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8747
- * which classifies a pinned agent node as `agent-child-forward`
8748
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8749
- *
8750
- * Usage at a call site:
8751
- *
8752
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8753
- */
8754
- /** tRPC `op.context` key carrying a per-call node pin. */
8755
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8756
- /**
8757
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8758
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8759
- *
8760
- * ## The id is normalised here, and it has to be
8761
- *
8762
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8763
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8764
- * compares a pin against real node ids, so such a pin matches nothing and the
8765
- * call fails with `no provider registered for cap "…"`. The local-first
8766
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8767
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8768
- * the thing you reach for when you specifically need THIS node — silently
8769
- * addressed a node that does not exist.
8770
- *
8771
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8772
- * own node to read the host's hardware-decode backend. It never once answered,
8773
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8774
- * while D67's whole premise was that the decoder addon is the authority on
8775
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8776
- * hardware", which was false.
8777
- *
8778
- * Normalising in the ONE constructor fixes every caller at once, which is why
8779
- * it is here and not at the call sites.
8780
- */
8781
- function nodePin(nodeId) {
8782
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8783
- }
8784
- /**
8785
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8786
- * is idempotent, so passing an already-clean id costs nothing.
8787
- */
8788
- function toNodeId(idOrRunnerId) {
8789
- const head = idOrRunnerId.split("/")[0];
8790
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8791
- }
8792
8741
  object({
8793
8742
  field: string().min(1),
8794
8743
  on: literal("advance")
@@ -8877,6 +8826,128 @@ function optionalMethod(input, output, options) {
8877
8826
  providerOptional: true
8878
8827
  };
8879
8828
  }
8829
+ /**
8830
+ * Per-call node pinning for `ctx.api` capability calls.
8831
+ *
8832
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8833
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8834
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8835
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8836
+ * call to that node.
8837
+ *
8838
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8839
+ * method args), so capability method signatures stay `nodeId`-free — node
8840
+ * targeting is a property of the CALL, not of the method. The transport lifts
8841
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8842
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8843
+ * which classifies a pinned agent node as `agent-child-forward`
8844
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8845
+ *
8846
+ * Usage at a call site:
8847
+ *
8848
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8849
+ */
8850
+ /** tRPC `op.context` key carrying a per-call node pin. */
8851
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8852
+ /**
8853
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8854
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8855
+ *
8856
+ * ## The id is normalised here, and it has to be
8857
+ *
8858
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8859
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8860
+ * compares a pin against real node ids, so such a pin matches nothing and the
8861
+ * call fails with `no provider registered for cap "…"`. The local-first
8862
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8863
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8864
+ * the thing you reach for when you specifically need THIS node — silently
8865
+ * addressed a node that does not exist.
8866
+ *
8867
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8868
+ * own node to read the host's hardware-decode backend. It never once answered,
8869
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8870
+ * while D67's whole premise was that the decoder addon is the authority on
8871
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8872
+ * hardware", which was false.
8873
+ *
8874
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8875
+ * it is here and not at the call sites.
8876
+ */
8877
+ function nodePin(nodeId) {
8878
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8879
+ }
8880
+ /**
8881
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8882
+ * is idempotent, so passing an already-clean id costs nothing.
8883
+ */
8884
+ function toNodeId(idOrRunnerId) {
8885
+ const head = idOrRunnerId.split("/")[0];
8886
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8887
+ }
8888
+ /**
8889
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8890
+ * declarations. Every addon that contributes a UI page registers a
8891
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8892
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8893
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8894
+ * that admin-ui consumes.
8895
+ *
8896
+ * The split exists because the public listing has a different output
8897
+ * shape than the per-provider raw declarations, and we want both ends
8898
+ * to flow through codegen instead of relying on a hand-written wrapper.
8899
+ */
8900
+ var AddonPageDeclarationSchema = object({
8901
+ id: string(),
8902
+ label: string(),
8903
+ icon: string(),
8904
+ path: string(),
8905
+ /**
8906
+ * Module Federation remote name — must match the `name` field on the
8907
+ * page addon's `federation()` plugin config. Used by admin-ui's
8908
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8909
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8910
+ * contain hyphens).
8911
+ */
8912
+ remoteName: string(),
8913
+ /**
8914
+ * Bundle filename inside the addon's `dist/` dir served at
8915
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8916
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8917
+ * the static-file route can compute an mtime-based cache-buster URL
8918
+ * without a separate filesystem stat.
8919
+ */
8920
+ bundle: string(),
8921
+ /**
8922
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8923
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8924
+ * Any OTHER string creates (or joins) a custom section rendered after
8925
+ * the built-in groups; its label comes from `sectionLabel` (first
8926
+ * declaration wins), falling back to the id. Absent → the legacy
8927
+ * "Addon Pages" group.
8928
+ */
8929
+ section: string().optional(),
8930
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8931
+ sectionLabel: string().optional()
8932
+ });
8933
+ var addonPagesSourceCapability = {
8934
+ name: "addon-pages-source",
8935
+ scope: "system",
8936
+ mode: "collection",
8937
+ internal: true,
8938
+ methods: {
8939
+ /**
8940
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
8941
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
8942
+ * authenticated session at the default `auth: 'protected'`. The only
8943
+ * consumer, the `addon-pages-aggregator` builtin, reads it via
8944
+ * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
8945
+ * only, never tRPC (per `ctx.capabilities` seeing only the local
8946
+ * process). The public, enriched listing admin-ui actually consumes is
8947
+ * the separate `addon-pages` cap, unaffected by this change.
8948
+ */
8949
+ listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
8950
+ };
8880
8951
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8881
8952
  var VersionOutputSchema$1 = object({ version: string() });
8882
8953
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -8902,6 +8973,27 @@ function customAction(input, output, options) {
8902
8973
  ...options?.caller ? { caller: "required" } : {}
8903
8974
  };
8904
8975
  }
8976
+ /**
8977
+ * The pipeline step-input shape and the run-result bridge, in a module that
8978
+ * imports nothing but zod and types.
8979
+ *
8980
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8981
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8982
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8983
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8984
+ * them, so every existing import keeps working; addon server code takes them
8985
+ * from `@camstack/types/addon`.
8986
+ */
8987
+ var PipelineStepInputSchema = lazy(() => object({
8988
+ addonId: string(),
8989
+ modelId: string().optional(),
8990
+ enabled: boolean().default(true),
8991
+ children: array(PipelineStepInputSchema).optional(),
8992
+ settings: record(string(), unknown()).optional(),
8993
+ jumpDeviceKey: string().optional()
8994
+ }));
8995
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8996
+ var PipelineRunResultBridge = custom();
8905
8997
  var StaticDirOutputSchema = object({ staticDir: string() });
8906
8998
  var VersionOutputSchema = object({ version: string() });
8907
8999
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9086,6 +9178,7 @@ var CAP_NAMES = Object.freeze([
9086
9178
  "network-link",
9087
9179
  "network-quality",
9088
9180
  "node-certificates",
9181
+ "node-removal-hook",
9089
9182
  "nodes",
9090
9183
  "notification-output",
9091
9184
  "notification-rules",
@@ -9096,6 +9189,7 @@ var CAP_NAMES = Object.freeze([
9096
9189
  "osd",
9097
9190
  "osd-manager",
9098
9191
  "pet-feeder",
9192
+ "pet-fountain",
9099
9193
  "pipeline-analytics",
9100
9194
  "pipeline-executor",
9101
9195
  "pipeline-orchestrator",
@@ -10375,6 +10469,56 @@ Object.freeze({
10375
10469
  "valueType": "number"
10376
10470
  }
10377
10471
  ],
10472
+ "pet-fountain": [
10473
+ {
10474
+ "path": "waterLevel",
10475
+ "valueType": "number"
10476
+ },
10477
+ {
10478
+ "path": "waterVolume",
10479
+ "valueType": "number"
10480
+ },
10481
+ {
10482
+ "path": "empty",
10483
+ "valueType": "boolean"
10484
+ },
10485
+ {
10486
+ "path": "pumpRunning",
10487
+ "valueType": "boolean"
10488
+ },
10489
+ {
10490
+ "path": "pumpIntervalEnabled",
10491
+ "valueType": "boolean"
10492
+ },
10493
+ {
10494
+ "path": "pumpOnTime",
10495
+ "valueType": "number"
10496
+ },
10497
+ {
10498
+ "path": "pumpOffTime",
10499
+ "valueType": "number"
10500
+ },
10501
+ {
10502
+ "path": "lastDrink",
10503
+ "valueType": "number"
10504
+ },
10505
+ {
10506
+ "path": "currentHourDrink",
10507
+ "valueType": "number"
10508
+ },
10509
+ {
10510
+ "path": "last24hDrink",
10511
+ "valueType": "number"
10512
+ },
10513
+ {
10514
+ "path": "todayDrink",
10515
+ "valueType": "number"
10516
+ },
10517
+ {
10518
+ "path": "error",
10519
+ "valueType": "string"
10520
+ }
10521
+ ],
10378
10522
  "power-meter": [
10379
10523
  {
10380
10524
  "path": "power",
@@ -10912,7 +11056,7 @@ object({
10912
11056
  params: record(string(), union([string(), number()])).optional()
10913
11057
  });
10914
11058
  //#endregion
10915
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11059
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10916
11060
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10917
11061
  DeviceType["Camera"] = "camera";
10918
11062
  DeviceType["Hub"] = "hub";
@@ -11004,6 +11148,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11004
11148
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11005
11149
  * integrations sharing the same food/desiccant/hopper surface. */
11006
11150
  DeviceType["PetFeeder"] = "pet-feeder";
11151
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11152
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11153
+ * of a container and is BUILT as a composed device (D967). */
11154
+ DeviceType["PetFountain"] = "pet-fountain";
11007
11155
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11008
11156
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11009
11157
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11264,7 +11412,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11264
11412
  return DeviceRole;
11265
11413
  }({});
11266
11414
  //#endregion
11267
- //#region ../types/dist/composition-DpFv_J6D.mjs
11415
+ //#region ../types/dist/composition-kSuOifuG.mjs
11268
11416
  /**
11269
11417
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11270
11418
  *
@@ -12349,12 +12497,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12349
12497
  ])
12350
12498
  })
12351
12499
  ]);
12352
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12500
+ var forwardArgUnion = union([
12501
+ string(),
12502
+ number(),
12503
+ boolean(),
12504
+ strictObject({ ["$input"]: string().min(1) }),
12505
+ record(string().min(1), lazy(() => forwardArgUnion))
12506
+ ]);
12507
+ var CompositionForwardArgSchema = forwardArgUnion;
12508
+ /**
12509
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12510
+ * call of `cap.method` on `source`. Without `args` the built method's input
12511
+ * passes through minus `deviceId`; with `args` the template IS the source's
12512
+ * whole input. `deviceId` is always the source's own, never an argument.
12513
+ */
12514
+ var CompositionForwardCommandSchema = object({
12353
12515
  kind: literal("forward"),
12354
12516
  source: CompositionSourceRefSchema,
12355
12517
  cap: string().min(1),
12356
- method: string().min(1)
12357
- }), CompositionCodeSourceSchema]);
12518
+ method: string().min(1),
12519
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12520
+ });
12521
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12358
12522
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12359
12523
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12360
12524
  /**
@@ -12364,7 +12528,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12364
12528
  */
12365
12529
  var CompositionItemEntrySchema = object({
12366
12530
  label: string().min(1).max(80),
12367
- fields: record(string().min(1), CompositionFieldSourceSchema)
12531
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12532
+ /**
12533
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12534
+ * is resettable and a `reset` call naming its key forwards here. The item key
12535
+ * selects the command, so it is never an argument: there is no pass-through.
12536
+ */
12537
+ reset: CompositionForwardCommandSchema.optional()
12368
12538
  });
12369
12539
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12370
12540
  kind: literal("fields"),
@@ -12376,7 +12546,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12376
12546
  * owns the WHOLE array field: the composed value holds exactly these items.
12377
12547
  */
12378
12548
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12379
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12549
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12380
12550
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12381
12551
  }), object({
12382
12552
  kind: literal("passthrough"),
@@ -16455,120 +16625,478 @@ object({
16455
16625
  lastError: string().nullable()
16456
16626
  });
16457
16627
  /**
16458
- * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16459
- * surface that admin-ui consumes through `useAddonPagesListPages()`.
16628
+ * notification-output — canonical, capability-gated notification delivery.
16460
16629
  *
16461
- * The provider iterates every `addon-pages-source` (collection) provider
16462
- * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
16463
- * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
16464
- * filesystem `mtime` cache-buster lets the browser pick up addon
16465
- * rebuilds without manual reload.
16630
+ * Apprise-derived model (see
16631
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16632
+ * callers emit ONE canonical `Notification`; each provider declares a
16633
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16634
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16635
+ * message to what the kind supports — callers never special-case a service.
16466
16636
  *
16467
- * The hub-local builtin `addon-pages-aggregator` (see
16468
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
16469
- * provider. Splitting the public aggregator from the raw collection
16470
- * keeps both ends in codegen — there's no hand-written
16471
- * `addon-pages.router.ts` wrapper anymore.
16637
+ * DESIGN DECISIONS (locked):
16638
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16639
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16640
+ * cap. Rationale: the admin UI needs one uniform surface across the
16641
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16642
+ * alternative would fork the UI per addon and cannot host the
16643
+ * discovery→adopt flow.
16644
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16645
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16646
+ * registered provider (notifiers addon + HA addon) so one catalog is
16647
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16648
+ * `addonId` the generated collection router extracts from the call input.
16649
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16650
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16651
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16652
+ * base64 fallback needed.
16653
+ *
16654
+ * TODO (deferred, closed-set change — separate decision): add
16655
+ * `providerKind: 'notify'` so notification providers surface on the unified
16656
+ * admin "Integrations" page.
16472
16657
  */
16473
- var AddonPageDeclarationSchema$1 = object({
16474
- id: string(),
16475
- label: string(),
16476
- icon: string(),
16477
- path: string(),
16478
- remoteName: string(),
16479
- bundle: string(),
16480
- section: string().optional(),
16481
- sectionLabel: string().optional()
16482
- });
16483
- var AddonPageInfoSchema = object({
16484
- addonId: string(),
16485
- page: AddonPageDeclarationSchema$1,
16486
- bundleUrl: string()
16487
- });
16488
- method(_void(), array(AddonPageInfoSchema).readonly());
16489
16658
  /**
16490
- * `addon-pages-source` — collection cap exposing per-provider raw page
16491
- * declarations. Every addon that contributes a UI page registers a
16492
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16493
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16494
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16495
- * that admin-ui consumes.
16659
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16660
+ * adapter picks what it supports and the degrade engine filters the rest.
16661
+ */
16662
+ var AttachmentMediaTypeSchema = _enum([
16663
+ "image",
16664
+ "video",
16665
+ "gif",
16666
+ "audio",
16667
+ "icon"
16668
+ ]);
16669
+ /**
16670
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16671
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16672
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16673
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16674
+ */
16675
+ var AttachmentSchema = object({
16676
+ mediaType: AttachmentMediaTypeSchema,
16677
+ url: string().optional(),
16678
+ bytes: _instanceof(Uint8Array).optional(),
16679
+ mime: string().optional(),
16680
+ name: string().optional()
16681
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16682
+ var NotificationFormatSchema = _enum([
16683
+ "text",
16684
+ "markdown",
16685
+ "html"
16686
+ ]);
16687
+ /**
16688
+ * The CLOSED icon vocabulary an action button may use.
16496
16689
  *
16497
- * The split exists because the public listing has a different output
16498
- * shape than the per-provider raw declarations, and we want both ends
16499
- * to flow through codegen instead of relying on a hand-written wrapper.
16690
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16691
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16692
+ * notices — the same class of gap as a zone vocabulary nothing produced
16693
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16694
+ * declares `actionIcons: false` and the degrade engine strips the field.
16695
+ *
16696
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16697
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16698
+ * differently.
16699
+ *
16700
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16701
+ *
16702
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16703
+ * the whole set onto its own renderer's vocabulary through a
16704
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16705
+ * fallback, so adding a member here fails every adapter's build until someone
16706
+ * decides its glyph, which is the only place that decision can be made
16707
+ * honestly.
16708
+ *
16709
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16710
+ * `disarm` straight through; iOS feeds that string to
16711
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16712
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16713
+ * mapping, and "the field is documented" is not "the value renders".
16714
+ *
16715
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16716
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16717
+ * so an addon that emits a token the running hub does not know does not lose an
16718
+ * icon — its whole `send` fails Zod validation and the notification never
16719
+ * arrives. Never emit a new token from an addon before the train carrying it.
16500
16720
  */
16501
- var AddonPageDeclarationSchema = object({
16721
+ var NotificationActionIconSchema = _enum([
16722
+ "acknowledge",
16723
+ "dismiss",
16724
+ "silence",
16725
+ "snooze",
16726
+ "view",
16727
+ "play",
16728
+ "open",
16729
+ "close",
16730
+ "lock",
16731
+ "unlock",
16732
+ "arm",
16733
+ "arm-home",
16734
+ "arm-away",
16735
+ "arm-night",
16736
+ "disarm",
16737
+ "light",
16738
+ "alert",
16739
+ "camera"
16740
+ ]);
16741
+ /** A single tap-through action button. */
16742
+ var NotificationActionSchema = object({
16502
16743
  id: string(),
16503
16744
  label: string(),
16504
- icon: string(),
16505
- path: string(),
16506
- /**
16507
- * Module Federation remote name — must match the `name` field on the
16508
- * page addon's `federation()` plugin config. Used by admin-ui's
16509
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16510
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16511
- * contain hyphens).
16512
- */
16513
- remoteName: string(),
16745
+ url: string().optional(),
16746
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16747
+ icon: NotificationActionIconSchema.optional(),
16514
16748
  /**
16515
- * Bundle filename inside the addon's `dist/` dir served at
16516
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16517
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16518
- * the static-file route can compute an mtime-based cache-buster URL
16519
- * without a separate filesystem stat.
16749
+ * Renders in a warning style where the notifier supports it.
16750
+ *
16751
+ * A HINT, never a gate. The callback's authority is its token and nothing
16752
+ * else — see `notification-center/action-token.ts` for what that does and
16753
+ * does not buy.
16520
16754
  */
16521
- bundle: string(),
16755
+ destructive: boolean().optional(),
16522
16756
  /**
16523
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16524
- * `'cluster'`, `'administration'` — the page renders inside that group.
16525
- * Any OTHER string creates (or joins) a custom section rendered after
16526
- * the built-in groups; its label comes from `sectionLabel` (first
16527
- * declaration wins), falling back to the id. Absent → the legacy
16528
- * "Addon Pages" group.
16757
+ * How the tap should REACH the url.
16758
+ *
16759
+ * `navigate` (absent, and every button authored before this field) opens it:
16760
+ * the phone leaves the notification and shows whatever the callback returns.
16761
+ * That is right for a button whose answer the operator wants to read.
16762
+ *
16763
+ * `background` fires it as a POST and stays put. It exists for the buttons
16764
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16765
+ * an answer to the notification, and being thrown into a browser tab to
16766
+ * confirm it costs more attention than the notification did. A backend that
16767
+ * cannot do a background call renders it as an ordinary link (the adapters
16768
+ * fall back rather than dropping the button), so this is a preference, never
16769
+ * a requirement.
16529
16770
  */
16530
- section: string().optional(),
16531
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16532
- sectionLabel: string().optional()
16771
+ mode: _enum(["navigate", "background"]).optional()
16533
16772
  });
16534
- var addonPagesSourceCapability = {
16535
- name: "addon-pages-source",
16536
- scope: "system",
16537
- mode: "collection",
16538
- internal: true,
16539
- methods: {
16540
- /**
16541
- * `internal: true` did not gate the mount (see the 2026-08-26 note on
16542
- * `data-store-provider`) — this was reachable on the AppRouter by ANY
16543
- * authenticated session at the default `auth: 'protected'`. The only
16544
- * consumer, the `addon-pages-aggregator` builtin, reads it via
16545
- * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
16546
- * only, never tRPC (per `ctx.capabilities` seeing only the local
16547
- * process). The public, enriched listing admin-ui actually consumes is
16548
- * the separate `addon-pages` cap, unaffected by this change.
16549
- */
16550
- listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
16551
- };
16552
- var AddonHttpRouteSchema = object({
16553
- method: _enum([
16554
- "GET",
16555
- "POST",
16556
- "PUT",
16557
- "DELETE",
16558
- "PATCH"
16773
+ /** A delivery parameter value: what a notifier declared it takes. */
16774
+ var DeliveryParamValueSchema = union([
16775
+ string(),
16776
+ number(),
16777
+ boolean()
16778
+ ]);
16779
+ /**
16780
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16781
+ * addresses a target of that kind may set it; a kind that declares nothing is
16782
+ * never handed one. The admin UI derives its control from this descriptor —
16783
+ * no editor knows a service by name.
16784
+ *
16785
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16786
+ * run on the addon cap path).
16787
+ */
16788
+ var DeliveryParamSchema = object({
16789
+ key: string().min(1),
16790
+ label: string(),
16791
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16792
+ labelKey: string().optional(),
16793
+ type: _enum([
16794
+ "enum",
16795
+ "number",
16796
+ "boolean",
16797
+ "string"
16559
16798
  ]),
16560
- path: string(),
16561
- access: _enum([
16562
- "public",
16563
- "authenticated",
16564
- "admin"
16565
- ]).optional(),
16566
- description: string().optional(),
16567
- /** The hub limits this route per client address before the handler runs. */
16568
- rateLimit: _enum(["auth"]).optional()
16799
+ /** `enum` only: the closed set of accepted values. */
16800
+ options: array(object({
16801
+ value: string(),
16802
+ label: string()
16803
+ })).optional(),
16804
+ /** `number` only. */
16805
+ min: number().optional(),
16806
+ max: number().optional(),
16807
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16808
+ default: DeliveryParamValueSchema.optional()
16569
16809
  });
16570
16810
  /**
16571
- * Cross-process route invocation envelope. The hub captures the
16811
+ * The canonical notification. `body` is the only hard field (Apprise model).
16812
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16813
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16814
+ * the adapter maps this ordinal onto its native level. `level?` is an
16815
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16816
+ * `priority` for that one target.
16817
+ */
16818
+ var NotificationSchema = object({
16819
+ body: string(),
16820
+ title: string().optional(),
16821
+ format: NotificationFormatSchema.default("text"),
16822
+ priority: number().int().min(1).max(5).default(3),
16823
+ level: string().optional(),
16824
+ attachments: array(AttachmentSchema).optional(),
16825
+ clickUrl: string().optional(),
16826
+ actions: array(NotificationActionSchema).optional(),
16827
+ sound: string().optional(),
16828
+ ttl: number().optional(),
16829
+ tag: string().optional(),
16830
+ deviceId: number().optional(),
16831
+ eventId: string().optional(),
16832
+ metadata: record(string(), unknown()).optional(),
16833
+ /**
16834
+ * Values for the parameters the target's kind DECLARES in
16835
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16836
+ * declaration by the sender; a notifier reads only the keys it declared.
16837
+ */
16838
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16839
+ });
16840
+ /** One declared native severity/priority level for a kind. */
16841
+ var TargetKindLevelSchema = object({
16842
+ id: string(),
16843
+ label: string(),
16844
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16845
+ ordinal: number().int().min(1).max(5).nullable(),
16846
+ flags: object({
16847
+ critical: boolean().optional(),
16848
+ silent: boolean().optional(),
16849
+ noPush: boolean().optional()
16850
+ }).optional(),
16851
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16852
+ requires: array(string()).optional(),
16853
+ description: string().optional()
16854
+ });
16855
+ /** The full capability block consulted before dispatch. */
16856
+ var TargetKindCapsSchema = object({
16857
+ attachments: object({
16858
+ mediaTypes: array(AttachmentMediaTypeSchema),
16859
+ mode: _enum([
16860
+ "url",
16861
+ "bytes",
16862
+ "both"
16863
+ ]),
16864
+ max: number().int().nonnegative(),
16865
+ maxBytes: number().int().positive().optional()
16866
+ }),
16867
+ /** Max action buttons (0 = none). */
16868
+ actions: number().int().nonnegative(),
16869
+ /**
16870
+ * Whether this kind renders a per-action ICON.
16871
+ *
16872
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16873
+ * run on the addon cap path — three production failures in one day taught
16874
+ * this repo that once. Absent is read as false by the degrade engine, which
16875
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16876
+ * assumed and dropped costs the operator's trust in the field.
16877
+ */
16878
+ actionIcons: boolean().optional(),
16879
+ levels: array(TargetKindLevelSchema),
16880
+ format: array(NotificationFormatSchema),
16881
+ clickUrl: boolean(),
16882
+ sound: boolean(),
16883
+ ttl: boolean(),
16884
+ bodyMaxLen: number().int().positive()
16885
+ });
16886
+ /**
16887
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16888
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16889
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16890
+ * the union is large and not meant for runtime validation here; the exported
16891
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16892
+ */
16893
+ var ConfigSchemaPassthrough$3 = unknown();
16894
+ var TargetKindSchema = object({
16895
+ kind: string(),
16896
+ label: string(),
16897
+ icon: string(),
16898
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16899
+ addonId: string(),
16900
+ /**
16901
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16902
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16903
+ * when the addon bundles no icon for that kind — the client then falls back
16904
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16905
+ *
16906
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16907
+ * client, and a native client joins it onto its own hub base.
16908
+ *
16909
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16910
+ * field that survived only because the runtime cap-router forwards provider
16911
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16912
+ * its own Zod parse from stripping it, and the whole arrangement would have
16913
+ * broken silently the moment output validation was tightened anywhere.
16914
+ */
16915
+ iconUrl: string().optional(),
16916
+ /**
16917
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16918
+ *
16919
+ * The server knows this and therefore says it, because the client cannot
16920
+ * safely guess: a React-Native client renders SVG and raster through two
16921
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16922
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16923
+ * placeholder glyph for every vector icon while the web build looked fine.
16924
+ *
16925
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16926
+ * not been updated — a client that cannot determine the type should prefer
16927
+ * its raster path, which is the safe default for an unknown image.
16928
+ */
16929
+ iconMediaType: string().optional(),
16930
+ configSchema: ConfigSchemaPassthrough$3,
16931
+ /**
16932
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16933
+ * base URL, instance, chat, recipient). A save that changes any of them
16934
+ * while leaving a secret redacted is refused by the provider (D634), so an
16935
+ * editor uses this to ask for the secret on exactly those edits instead of
16936
+ * on every edit.
16937
+ *
16938
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16939
+ * kinds) and from a provider older than this field — an editor that finds
16940
+ * it absent must assume ANY edit may need the secret.
16941
+ */
16942
+ destinationKeys: array(string()).optional(),
16943
+ supportsDiscovery: boolean(),
16944
+ /**
16945
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16946
+ * Absent = none; the rule editor then offers nothing.
16947
+ */
16948
+ deliveryParams: array(DeliveryParamSchema).optional(),
16949
+ caps: TargetKindCapsSchema
16950
+ });
16951
+ /**
16952
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16953
+ * (return a presence marker only) when serving `listTargets` — never
16954
+ * round-trip a stored secret to the UI.
16955
+ */
16956
+ var TargetSchema = object({
16957
+ id: string(),
16958
+ name: string(),
16959
+ kind: string(),
16960
+ addonId: string(),
16961
+ enabled: boolean(),
16962
+ config: record(string(), unknown())
16963
+ });
16964
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16965
+ var DiscoveredTargetSchema = object({
16966
+ kind: string(),
16967
+ suggestedName: string(),
16968
+ config: record(string(), unknown())
16969
+ });
16970
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16971
+ var RenderedAsSchema = object({
16972
+ level: string(),
16973
+ format: NotificationFormatSchema,
16974
+ attachmentsSent: number().int().nonnegative(),
16975
+ actionsSent: number().int().nonnegative(),
16976
+ truncated: boolean(),
16977
+ dropped: array(string())
16978
+ });
16979
+ var SendResultSchema = object({
16980
+ success: boolean(),
16981
+ error: string().optional(),
16982
+ renderedAs: RenderedAsSchema.optional()
16983
+ });
16984
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16985
+ var TestResultSchema = SendResultSchema;
16986
+ /**
16987
+ * Pre-save credential probe against a partial (unsaved) target config.
16988
+ * `resolvedUrl` never includes secrets (token stripped from path).
16989
+ */
16990
+ var ProbeTargetConfigResultSchema = object({
16991
+ ok: boolean(),
16992
+ resolvedUrl: string(),
16993
+ httpStatus: number().int().optional(),
16994
+ stateInstance: string().optional(),
16995
+ error: string().optional()
16996
+ });
16997
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
16998
+ kind: string(),
16999
+ config: record(string(), unknown()).optional(),
17000
+ /**
17001
+ * When editing / duplicating, the blank password field never round-trips
17002
+ * the stored token. Pass the source target id so the provider can restore
17003
+ * secrets server-side before dialling (same merge as upsert).
17004
+ */
17005
+ targetId: string().optional()
17006
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
17007
+ kind: string(),
17008
+ config: record(string(), unknown()),
17009
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
17010
+ targetId: string().optional()
17011
+ }), ProbeTargetConfigResultSchema, {
17012
+ kind: "mutation",
17013
+ auth: "admin"
17014
+ }), method(object({
17015
+ targetId: string(),
17016
+ notification: NotificationSchema
17017
+ }), SendResultSchema, {
17018
+ kind: "mutation",
17019
+ auth: "admin"
17020
+ }), method(object({
17021
+ targetId: string(),
17022
+ sample: NotificationSchema.optional()
17023
+ }), TestResultSchema, {
17024
+ kind: "mutation",
17025
+ auth: "admin"
17026
+ }), method(object({
17027
+ target: TargetSchema,
17028
+ /**
17029
+ * Duplicate flow: create a NEW target id while copying secrets from an
17030
+ * existing row. The UI never holds the stored token; blank / redacted
17031
+ * password fields are restored from this source (D634 still applies if
17032
+ * destinations diverge without a re-typed secret).
17033
+ */
17034
+ cloneSecretsFrom: string().optional()
17035
+ }), TargetSchema, {
17036
+ kind: "mutation",
17037
+ auth: "admin"
17038
+ }), method(object({ targetId: string() }), _void(), {
17039
+ kind: "mutation",
17040
+ auth: "admin"
17041
+ }), method(object({
17042
+ targetId: string(),
17043
+ enabled: boolean()
17044
+ }), _void(), {
17045
+ kind: "mutation",
17046
+ auth: "admin"
17047
+ });
17048
+ /**
17049
+ * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17050
+ * surface that admin-ui consumes through `useAddonPagesListPages()`.
17051
+ *
17052
+ * The provider iterates every `addon-pages-source` (collection) provider
17053
+ * and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
17054
+ * pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
17055
+ * filesystem `mtime` cache-buster lets the browser pick up addon
17056
+ * rebuilds without manual reload.
17057
+ *
17058
+ * The hub-local builtin `addon-pages-aggregator` (see
17059
+ * `@camstack/system/builtins/addon-pages-aggregator`) registers the
17060
+ * provider. Splitting the public aggregator from the raw collection
17061
+ * keeps both ends in codegen — there's no hand-written
17062
+ * `addon-pages.router.ts` wrapper anymore.
17063
+ */
17064
+ var AddonPageDeclarationSchema$1 = object({
17065
+ id: string(),
17066
+ label: string(),
17067
+ icon: string(),
17068
+ path: string(),
17069
+ remoteName: string(),
17070
+ bundle: string(),
17071
+ section: string().optional(),
17072
+ sectionLabel: string().optional()
17073
+ });
17074
+ var AddonPageInfoSchema = object({
17075
+ addonId: string(),
17076
+ page: AddonPageDeclarationSchema$1,
17077
+ bundleUrl: string()
17078
+ });
17079
+ method(_void(), array(AddonPageInfoSchema).readonly());
17080
+ var AddonHttpRouteSchema = object({
17081
+ method: _enum([
17082
+ "GET",
17083
+ "POST",
17084
+ "PUT",
17085
+ "DELETE",
17086
+ "PATCH"
17087
+ ]),
17088
+ path: string(),
17089
+ access: _enum([
17090
+ "public",
17091
+ "authenticated",
17092
+ "admin"
17093
+ ]).optional(),
17094
+ description: string().optional(),
17095
+ /** The hub limits this route per client address before the handler runs. */
17096
+ rateLimit: _enum(["auth"]).optional()
17097
+ });
17098
+ /**
17099
+ * Cross-process route invocation envelope. The hub captures the
16572
17100
  * request as plain data, ships it to the worker via Moleculer, and
16573
17101
  * the worker runs the local handler against a capturing reply. The
16574
17102
  * envelope returned describes what the handler intended (status,
@@ -19091,7 +19619,11 @@ var CompositionProblemSchema = object({
19091
19619
  "mixed-quantity",
19092
19620
  "helper-mismatch",
19093
19621
  "member-constraint",
19094
- "retention"
19622
+ "retention",
19623
+ "command-method-unknown",
19624
+ "command-source-unknown",
19625
+ "command-input-unknown",
19626
+ "command-input-missing"
19095
19627
  ]),
19096
19628
  path: string(),
19097
19629
  message: string()
@@ -19130,7 +19662,9 @@ var CompositionFieldRoleSchema = _enum([
19130
19662
  var CompositionFieldItemSchema = object({
19131
19663
  arrayPath: string(),
19132
19664
  key: string(),
19133
- label: string()
19665
+ label: string(),
19666
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19667
+ resettable: boolean().optional()
19134
19668
  });
19135
19669
  var CompositionFieldPlanSchema = object({
19136
19670
  cap: string(),
@@ -19335,7 +19869,7 @@ Object.freeze({
19335
19869
  * One catalog row per base reading a group helper can build (D944 §3).
19336
19870
  * Data, not a form default written into the block.
19337
19871
  */
19338
- var NOW$2 = {
19872
+ var NOW$3 = {
19339
19873
  fieldPath: "lastFetchedAt",
19340
19874
  expr: "now"
19341
19875
  };
@@ -19387,7 +19921,7 @@ var GROUP_TEMPLATES = [
19387
19921
  quantity: "temperature",
19388
19922
  defaultAggregate: "mean",
19389
19923
  empty: "null",
19390
- companions: [NOW$2],
19924
+ companions: [NOW$3],
19391
19925
  alsoFrom: {
19392
19926
  deviceTypes: CLIMATE_DEVICES,
19393
19927
  accept: {
@@ -19405,7 +19939,7 @@ var GROUP_TEMPLATES = [
19405
19939
  quantity: "humidity",
19406
19940
  defaultAggregate: "mean",
19407
19941
  empty: "null",
19408
- companions: [NOW$2],
19942
+ companions: [NOW$3],
19409
19943
  alsoFrom: {
19410
19944
  deviceTypes: CLIMATE_DEVICES,
19411
19945
  accept: {
@@ -19423,7 +19957,7 @@ var GROUP_TEMPLATES = [
19423
19957
  quantity: "pressure",
19424
19958
  defaultAggregate: "mean",
19425
19959
  empty: "null",
19426
- companions: [NOW$2]
19960
+ companions: [NOW$3]
19427
19961
  }),
19428
19962
  numeric({
19429
19963
  id: "illuminance",
@@ -19434,7 +19968,7 @@ var GROUP_TEMPLATES = [
19434
19968
  quantity: "illuminance",
19435
19969
  defaultAggregate: "mean",
19436
19970
  empty: "null",
19437
- companions: [NOW$2]
19971
+ companions: [NOW$3]
19438
19972
  }),
19439
19973
  numeric({
19440
19974
  id: "power",
@@ -19445,7 +19979,7 @@ var GROUP_TEMPLATES = [
19445
19979
  quantity: "power",
19446
19980
  defaultAggregate: "sum",
19447
19981
  empty: "omit",
19448
- companions: [NOW$2]
19982
+ companions: [NOW$3]
19449
19983
  }),
19450
19984
  numeric({
19451
19985
  id: "energy",
@@ -19456,7 +19990,7 @@ var GROUP_TEMPLATES = [
19456
19990
  quantity: "energy",
19457
19991
  defaultAggregate: "sum",
19458
19992
  empty: "omit",
19459
- companions: [NOW$2]
19993
+ companions: [NOW$3]
19460
19994
  }),
19461
19995
  numeric({
19462
19996
  id: "battery",
@@ -19491,7 +20025,7 @@ var GROUP_TEMPLATES = [
19491
20025
  quantity: "device",
19492
20026
  defaultAggregate: "mean",
19493
20027
  empty: "null",
19494
- companions: [NOW$2]
20028
+ companions: [NOW$3]
19495
20029
  }),
19496
20030
  binary({
19497
20031
  id: "motion",
@@ -21865,12 +22399,48 @@ var ExposeInputSchema = object({
21865
22399
  capabilities: array(string()).optional()
21866
22400
  });
21867
22401
  var UnexposeInputSchema = object({ deviceId: string() });
22402
+ /**
22403
+ * What an exporter may expose besides devices (D968). An action and a
22404
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22405
+ * exporter's own selection, not the device registry.
22406
+ */
22407
+ var ExportEntityKindSchema = _enum([
22408
+ "device",
22409
+ "action",
22410
+ "notifier"
22411
+ ]);
22412
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22413
+ object({
22414
+ kind: literal("device"),
22415
+ deviceId: string()
22416
+ }),
22417
+ object({
22418
+ kind: literal("action"),
22419
+ actionId: string().min(1)
22420
+ }),
22421
+ object({
22422
+ kind: literal("notifier"),
22423
+ targetId: string().min(1)
22424
+ })
22425
+ ]);
22426
+ var ExposedEntitySchema = object({
22427
+ entity: ExportEntityRefSchema,
22428
+ exposedAs: string().optional()
22429
+ });
22430
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22431
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
21868
22432
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
21869
22433
  kind: "mutation",
21870
22434
  auth: "admin"
21871
22435
  }), method(UnexposeInputSchema, _void(), {
21872
22436
  kind: "mutation",
21873
22437
  auth: "admin"
22438
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22439
+ kind: "mutation",
22440
+ auth: "admin"
22441
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22442
+ kind: "mutation",
22443
+ auth: "admin"
21874
22444
  });
21875
22445
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
21876
22446
  var ICON_CATEGORY_IDS = [
@@ -43011,6 +43581,14 @@ method(object({
43011
43581
  kind: "mutation",
43012
43582
  auth: "admin"
43013
43583
  });
43584
+ method(object({
43585
+ nodeId: string().min(1),
43586
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
43587
+ removedAt: number()
43588
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
43589
+ kind: "mutation",
43590
+ auth: "admin"
43591
+ });
43014
43592
  /**
43015
43593
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
43016
43594
  * so it can delete what it keeps for that device (D865).
@@ -43731,7 +44309,7 @@ var LlmProviderWidgetSchema = object({
43731
44309
  /** ConfigUISchema tree passed through untyped on the wire (the
43732
44310
  * notification-output `ConfigSchemaPassthrough` precedent at
43733
44311
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
43734
- var ConfigSchemaPassthrough$3 = unknown();
44312
+ var ConfigSchemaPassthrough$2 = unknown();
43735
44313
  var LlmProfileKindDescriptorSchema = object({
43736
44314
  kind: LlmProfileKindSchema,
43737
44315
  label: string(),
@@ -43740,9 +44318,9 @@ var LlmProfileKindDescriptorSchema = object({
43740
44318
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
43741
44319
  addonId: string(),
43742
44320
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
43743
- connectionSchema: ConfigSchemaPassthrough$3,
44321
+ connectionSchema: ConfigSchemaPassthrough$2,
43744
44322
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
43745
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44323
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
43746
44324
  /** The kind's defaults; a model from `listModels` may override any of them. */
43747
44325
  features: LlmModelFeaturesSchema,
43748
44326
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44268,10 +44846,10 @@ var CpuBreakdownSchema = object({
44268
44846
  cores: number()
44269
44847
  });
44270
44848
  var MemoryInfoSchema = object({
44271
- percent: number(),
44849
+ percent: number().nullable(),
44272
44850
  totalBytes: number(),
44273
- usedBytes: number(),
44274
- availableBytes: number(),
44851
+ usedBytes: number().nullable(),
44852
+ availableBytes: number().nullable(),
44275
44853
  swapUsedBytes: number(),
44276
44854
  swapTotalBytes: number()
44277
44855
  });
@@ -44548,697 +45126,277 @@ var NodeLoadSeriesSchema = object({
44548
45126
  });
44549
45127
  var GetLoadSeriesInputSchema = object({
44550
45128
  /**
44551
- * The node whose series is wanted.
44552
- *
44553
- * NOT named `nodeId`: the generated cap router strips a top-level
44554
- * `nodeId` from every method input and uses it to ROUTE the call to
44555
- * that node's provider (`generated-cap-routers.ts`). A series target
44556
- * called `nodeId` would silently become a routing pin and never reach
44557
- * the provider. The hub holds every node it hears from, so the
44558
- * ordinary call is unpinned — answered by the hub, for any node.
44559
- */
44560
- forNodeId: string(),
44561
- /**
44562
- * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
44563
- * holds and receives only what it is missing, so seeding a live chart
44564
- * from this method cannot double a point already drawn.
44565
- */
44566
- sinceMs: number().optional(),
44567
- /**
44568
- * Most points the caller wants PER FUNCTION. The window is reduced to fit,
44569
- * preserving min and max per bucket.
44570
- *
44571
- * Absent means NO reduction — legitimate for a short window and a trap for a
44572
- * long one, which is why a chart passes its own pixel width.
44573
- */
44574
- maxPoints: number().int().positive().optional()
44575
- });
44576
- var SystemMetricsSchema = object({
44577
- cpuPercent: number(),
44578
- memoryPercent: number(),
44579
- memoryUsedMB: number(),
44580
- memoryTotalMB: number(),
44581
- diskPercent: number().optional(),
44582
- temperature: number().optional(),
44583
- gpuPercent: number().optional(),
44584
- gpuMemoryPercent: number().optional()
44585
- });
44586
- 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);
44587
- var modelConvertCapability = {
44588
- name: "model-convert",
44589
- scope: "system",
44590
- mode: "singleton",
44591
- internal: true,
44592
- methods: { convert: method(object({
44593
- sourceUrl: string(),
44594
- metadata: ModelConvertMetadataSchema,
44595
- targets: array(ConvertTargetSchema).min(1).readonly(),
44596
- calibrationRef: string().optional(),
44597
- sessionId: string().optional()
44598
- }), ConvertResultSchema, {
44599
- kind: "mutation",
44600
- auth: "admin",
44601
- timeoutMs: 6e5
44602
- }) }
44603
- };
44604
- method(object({
44605
- targetNodeId: string(),
44606
- modelId: string(),
44607
- format: _enum(MODEL_FORMATS),
44608
- entry: ModelCatalogEntrySchema
44609
- }), object({
44610
- ok: boolean(),
44611
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
44612
- sha256: string(),
44613
- bytes: number(),
44614
- /** The target node's modelsDir the artifact landed in. */
44615
- path: string()
44616
- }), {
44617
- kind: "mutation",
44618
- auth: "admin"
44619
- });
44620
- var MotionRegionSchema = object({
44621
- bbox: BoundingBoxSchema,
44622
- pixelCount: number(),
44623
- intensity: number()
44624
- });
44625
- var MotionAnalysisResultSchema = object({
44626
- detected: boolean(),
44627
- regionCount: number(),
44628
- /** Regions passing minArea filter — used for pipeline triggering. */
44629
- regions: array(MotionRegionSchema).readonly(),
44630
- /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
44631
- rawRegions: array(MotionRegionSchema).readonly(),
44632
- frameWidth: number(),
44633
- frameHeight: number(),
44634
- analysisMs: number()
44635
- });
44636
- DeviceType.Camera, method(object({
44637
- deviceId: number(),
44638
- frame: FrameInputSchema.optional(),
44639
- frameHandle: FrameHandleSchema.optional()
44640
- }), MotionAnalysisResultSchema, {
44641
- role: "admin",
44642
- kind: "mutation"
44643
- }), method(object({ deviceId: number() }), _void(), {
44644
- role: "admin",
44645
- kind: "mutation"
44646
- }), method(_void(), _void(), {
44647
- role: "admin",
44648
- kind: "mutation"
44649
- }), object({
44650
- deviceId: number(),
44651
- detected: boolean(),
44652
- regionCount: number()
44653
- });
44654
- /**
44655
- * `mqtt-broker` — broker-registry cap.
44656
- *
44657
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
44658
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
44659
- * and (b) the connection details a consumer addon needs to spin up
44660
- * its OWN `mqtt.js` client.
44661
- *
44662
- * Why: pub/sub routing over the system event-bus loses fidelity
44663
- * (callback shape, QoS guarantees, will/retain semantics) and adds
44664
- * refcount bookkeeping that addons would rather own themselves. The
44665
- * canonical consumer needs raw `mqtt.js`
44666
- * features anyway — give it the connection config, get out of the way.
44667
- *
44668
- * Consumer flow:
44669
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
44670
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
44671
- * client.subscribe('zigbee2mqtt/+')
44672
- *
44673
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
44674
- * cloud bridge). The "embedded" entry (when present) is just another
44675
- * broker in the registry — its lifecycle is owned by the addon that
44676
- * spawned it.
44677
- */
44678
- var BrokerKindSchema = _enum(["external", "embedded"]);
44679
- /**
44680
- * Broker live-probe status.
44681
- *
44682
- * - `connected` — last probe completed a clean CONNACK
44683
- * - `disconnected` — no probe has run yet (cold cache)
44684
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
44685
- * - `unreachable` — TCP connect timed out / refused
44686
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
44687
- */
44688
- var BrokerStatusSchema$1 = _enum([
44689
- "connected",
44690
- "disconnected",
44691
- "auth-failed",
44692
- "unreachable",
44693
- "tls-error"
44694
- ]);
44695
- var BrokerInfoSchema$1 = object({
44696
- id: string(),
44697
- name: string(),
44698
- url: string(),
44699
- kind: BrokerKindSchema,
44700
- status: BrokerStatusSchema$1,
44701
- latencyMs: number().nullable(),
44702
- error: string().optional(),
44703
- /** Embedded brokers only: number of MQTT clients currently connected. */
44704
- connectedClients: number().int().nonnegative().optional(),
44705
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
44706
- lastCheckedAt: number().optional()
44707
- });
44708
- /**
44709
- * Connection details — what a consumer needs to call
44710
- * `mqtt.connect(url, options)`. We split URL + credentials so the
44711
- * consumer can pass them as `mqtt.connect(url, { username, password })`
44712
- * instead of stuffing creds into the URL (which leaks them into logs).
44713
- */
44714
- var BrokerConnectionDetailsSchema = object({
44715
- url: string(),
44716
- username: string().optional(),
44717
- password: string().optional(),
44718
- /**
44719
- * Suggested prefix for `clientId`. Each consumer should suffix this
44720
- * with its own discriminator (addon id, instance id) so reconnects
44721
- * don't kick each other off (MQTT spec: clientId must be unique per
44722
- * broker).
44723
- */
44724
- clientIdPrefix: string().optional()
44725
- });
44726
- var AddBrokerInputSchema = object({
44727
- name: string().min(1),
44728
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
44729
- username: string().optional(),
44730
- password: string().optional(),
44731
- clientIdPrefix: string().optional()
44732
- });
44733
- var AddBrokerResultSchema = object({ id: string() });
44734
- var IdInputSchema = object({ id: string() });
44735
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
44736
- ok: literal(true),
44737
- latencyMs: number()
44738
- }), object({
44739
- ok: literal(false),
44740
- error: string()
44741
- })]);
44742
- var StartEmbeddedInputSchema = object({
44743
- port: number().int().min(1).max(65535).default(1883),
44744
- /** Allow anonymous connect (no username/password). Default: false. */
44745
- allowAnonymous: boolean().default(false),
44746
- /** Optional shared username/password for clients. */
44747
- username: string().optional(),
44748
- password: string().optional()
44749
- });
44750
- var StartEmbeddedResultSchema = object({
44751
- id: string(),
44752
- url: string()
44753
- });
44754
- var StatusSchema = object({
44755
- brokerCount: number(),
44756
- embeddedRunning: boolean()
44757
- });
44758
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
44759
- kind: "mutation",
44760
- auth: "admin"
44761
- }), method(IdInputSchema, _void(), {
44762
- kind: "mutation",
44763
- auth: "admin"
44764
- }), method(IdInputSchema, TestResultSchema$1, {
44765
- kind: "mutation",
44766
- auth: "admin"
44767
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
44768
- kind: "mutation",
44769
- auth: "admin"
44770
- }), method(IdInputSchema, _void(), {
44771
- kind: "mutation",
44772
- auth: "admin"
44773
- }), method(_void(), StatusSchema);
44774
- var NetworkEndpointSchema = object({
44775
- url: string(),
44776
- hostname: string(),
44777
- port: number(),
44778
- protocol: _enum(["http", "https"]),
44779
- /**
44780
- * Whether this address survives a restart of its provider. `false` = it is
44781
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
44782
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
44783
- * it stays visible and an operator may still flag it. Absent = stable: every
44784
- * provider except a Quick tunnel owns a name that does not change.
44785
- */
44786
- stable: boolean().optional()
44787
- });
44788
- object({
44789
- url: string(),
44790
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
44791
- stable: boolean().optional(),
44792
- updated: boolean().optional()
44793
- });
44794
- var NetworkAccessStatusSchema = object({
44795
- connected: boolean(),
44796
- endpoint: NetworkEndpointSchema.nullable(),
44797
- error: string().optional()
44798
- });
44799
- /**
44800
- * Optional, richer endpoint shape returned by providers that expose
44801
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
44802
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44803
- * the originating provider config (mode + sourcePort) so the
44804
- * orchestrator UI can label rows distinctly. Providers that expose only
44805
- * one endpoint just omit `listEndpoints` from their provider impl.
44806
- */
44807
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
44808
- /**
44809
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
44810
- * the orchestrator can dedupe across `listEndpoints` polls.
44811
- */
44812
- id: string(),
44813
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
44814
- label: string(),
44815
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
44816
- mode: string().optional(),
44817
- /** Originating local port the ingress fronts (informational). */
44818
- sourcePort: number().optional()
44819
- });
44820
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
44821
- /**
44822
- * notification-output — canonical, capability-gated notification delivery.
44823
- *
44824
- * Apprise-derived model (see
44825
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44826
- * callers emit ONE canonical `Notification`; each provider declares a
44827
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44828
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44829
- * message to what the kind supports — callers never special-case a service.
44830
- *
44831
- * DESIGN DECISIONS (locked):
44832
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44833
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44834
- * cap. Rationale: the admin UI needs one uniform surface across the
44835
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44836
- * alternative would fork the UI per addon and cannot host the
44837
- * discovery→adopt flow.
44838
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44839
- * the generated cap-mount auto-`concatCollection`-fans them across every
44840
- * registered provider (notifiers addon + HA addon) so one catalog is
44841
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44842
- * `addonId` the generated collection router extracts from the call input.
44843
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44844
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44845
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44846
- * base64 fallback needed.
44847
- *
44848
- * TODO (deferred, closed-set change — separate decision): add
44849
- * `providerKind: 'notify'` so notification providers surface on the unified
44850
- * admin "Integrations" page.
44851
- */
44852
- /**
44853
- * Zentik-derived typed-media enum — the superset across every kind. Each
44854
- * adapter picks what it supports and the degrade engine filters the rest.
44855
- */
44856
- var AttachmentMediaTypeSchema = _enum([
44857
- "image",
44858
- "video",
44859
- "gif",
44860
- "audio",
44861
- "icon"
44862
- ]);
44863
- /**
44864
- * A single attachment. Exactly one of `url` (remote source, most adapters
44865
- * prefer this) or `bytes` (inline source; required for Pushover-style
44866
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44867
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44868
- */
44869
- var AttachmentSchema = object({
44870
- mediaType: AttachmentMediaTypeSchema,
44871
- url: string().optional(),
44872
- bytes: _instanceof(Uint8Array).optional(),
44873
- mime: string().optional(),
44874
- name: string().optional()
44875
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44876
- var NotificationFormatSchema = _enum([
44877
- "text",
44878
- "markdown",
44879
- "html"
44880
- ]);
44881
- /**
44882
- * The CLOSED icon vocabulary an action button may use.
44883
- *
44884
- * A closed set, not a free string, and that is the whole point: an arbitrary
44885
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44886
- * notices — the same class of gap as a zone vocabulary nothing produced
44887
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44888
- * declares `actionIcons: false` and the degrade engine strips the field.
44889
- *
44890
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44891
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44892
- * differently.
44893
- *
44894
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44895
- *
44896
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44897
- * the whole set onto its own renderer's vocabulary through a
44898
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44899
- * fallback, so adding a member here fails every adapter's build until someone
44900
- * decides its glyph, which is the only place that decision can be made
44901
- * honestly.
44902
- *
44903
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44904
- * `disarm` straight through; iOS feeds that string to
44905
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44906
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44907
- * mapping, and "the field is documented" is not "the value renders".
44908
- *
44909
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44910
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44911
- * so an addon that emits a token the running hub does not know does not lose an
44912
- * icon — its whole `send` fails Zod validation and the notification never
44913
- * arrives. Never emit a new token from an addon before the train carrying it.
44914
- */
44915
- var NotificationActionIconSchema = _enum([
44916
- "acknowledge",
44917
- "dismiss",
44918
- "silence",
44919
- "snooze",
44920
- "view",
44921
- "play",
44922
- "open",
44923
- "close",
44924
- "lock",
44925
- "unlock",
44926
- "arm",
44927
- "arm-home",
44928
- "arm-away",
44929
- "arm-night",
44930
- "disarm",
44931
- "light",
44932
- "alert",
44933
- "camera"
44934
- ]);
44935
- /** A single tap-through action button. */
44936
- var NotificationActionSchema = object({
44937
- id: string(),
44938
- label: string(),
44939
- url: string().optional(),
44940
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44941
- icon: NotificationActionIconSchema.optional(),
44942
- /**
44943
- * Renders in a warning style where the notifier supports it.
44944
- *
44945
- * A HINT, never a gate. The callback's authority is its token and nothing
44946
- * else — see `notification-center/action-token.ts` for what that does and
44947
- * does not buy.
44948
- */
44949
- destructive: boolean().optional(),
44950
- /**
44951
- * How the tap should REACH the url.
44952
- *
44953
- * `navigate` (absent, and every button authored before this field) opens it:
44954
- * the phone leaves the notification and shows whatever the callback returns.
44955
- * That is right for a button whose answer the operator wants to read.
44956
- *
44957
- * `background` fires it as a POST and stays put. It exists for the buttons
44958
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44959
- * an answer to the notification, and being thrown into a browser tab to
44960
- * confirm it costs more attention than the notification did. A backend that
44961
- * cannot do a background call renders it as an ordinary link (the adapters
44962
- * fall back rather than dropping the button), so this is a preference, never
44963
- * a requirement.
44964
- */
44965
- mode: _enum(["navigate", "background"]).optional()
44966
- });
44967
- /** A delivery parameter value: what a notifier declared it takes. */
44968
- var DeliveryParamValueSchema = union([
44969
- string(),
44970
- number(),
44971
- boolean()
44972
- ]);
44973
- /**
44974
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44975
- * addresses a target of that kind may set it; a kind that declares nothing is
44976
- * never handed one. The admin UI derives its control from this descriptor —
44977
- * no editor knows a service by name.
44978
- *
44979
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44980
- * run on the addon cap path).
44981
- */
44982
- var DeliveryParamSchema = object({
44983
- key: string().min(1),
44984
- label: string(),
44985
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44986
- labelKey: string().optional(),
44987
- type: _enum([
44988
- "enum",
44989
- "number",
44990
- "boolean",
44991
- "string"
44992
- ]),
44993
- /** `enum` only: the closed set of accepted values. */
44994
- options: array(object({
44995
- value: string(),
44996
- label: string()
44997
- })).optional(),
44998
- /** `number` only. */
44999
- min: number().optional(),
45000
- max: number().optional(),
45001
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45002
- default: DeliveryParamValueSchema.optional()
45003
- });
45004
- /**
45005
- * The canonical notification. `body` is the only hard field (Apprise model).
45006
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45007
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45008
- * the adapter maps this ordinal onto its native level. `level?` is an
45009
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45010
- * `priority` for that one target.
45011
- */
45012
- var NotificationSchema = object({
45013
- body: string(),
45014
- title: string().optional(),
45015
- format: NotificationFormatSchema.default("text"),
45016
- priority: number().int().min(1).max(5).default(3),
45017
- level: string().optional(),
45018
- attachments: array(AttachmentSchema).optional(),
45019
- clickUrl: string().optional(),
45020
- actions: array(NotificationActionSchema).optional(),
45021
- sound: string().optional(),
45022
- ttl: number().optional(),
45023
- tag: string().optional(),
45024
- deviceId: number().optional(),
45025
- eventId: string().optional(),
45026
- metadata: record(string(), unknown()).optional(),
45027
- /**
45028
- * Values for the parameters the target's kind DECLARES in
45029
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45030
- * declaration by the sender; a notifier reads only the keys it declared.
45031
- */
45032
- delivery: record(string(), DeliveryParamValueSchema).optional()
45033
- });
45034
- /** One declared native severity/priority level for a kind. */
45035
- var TargetKindLevelSchema = object({
45036
- id: string(),
45037
- label: string(),
45038
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45039
- ordinal: number().int().min(1).max(5).nullable(),
45040
- flags: object({
45041
- critical: boolean().optional(),
45042
- silent: boolean().optional(),
45043
- noPush: boolean().optional()
45044
- }).optional(),
45045
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45046
- requires: array(string()).optional(),
45047
- description: string().optional()
45048
- });
45049
- /** The full capability block consulted before dispatch. */
45050
- var TargetKindCapsSchema = object({
45051
- attachments: object({
45052
- mediaTypes: array(AttachmentMediaTypeSchema),
45053
- mode: _enum([
45054
- "url",
45055
- "bytes",
45056
- "both"
45057
- ]),
45058
- max: number().int().nonnegative(),
45059
- maxBytes: number().int().positive().optional()
45060
- }),
45061
- /** Max action buttons (0 = none). */
45062
- actions: number().int().nonnegative(),
45063
- /**
45064
- * Whether this kind renders a per-action ICON.
45065
- *
45066
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45067
- * run on the addon cap path — three production failures in one day taught
45068
- * this repo that once. Absent is read as false by the degrade engine, which
45069
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45070
- * assumed and dropped costs the operator's trust in the field.
45071
- */
45072
- actionIcons: boolean().optional(),
45073
- levels: array(TargetKindLevelSchema),
45074
- format: array(NotificationFormatSchema),
45075
- clickUrl: boolean(),
45076
- sound: boolean(),
45077
- ttl: boolean(),
45078
- bodyMaxLen: number().int().positive()
45079
- });
45080
- /**
45081
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45082
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45083
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45084
- * the union is large and not meant for runtime validation here; the exported
45085
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45086
- */
45087
- var ConfigSchemaPassthrough$2 = unknown();
45088
- var TargetKindSchema = object({
45089
- kind: string(),
45090
- label: string(),
45091
- icon: string(),
45092
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45093
- addonId: string(),
45094
- /**
45095
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45096
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45097
- * when the addon bundles no icon for that kind — the client then falls back
45098
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45099
- *
45100
- * Root-relative on purpose: it resolves against whatever origin serves a web
45101
- * client, and a native client joins it onto its own hub base.
45102
- *
45103
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45104
- * field that survived only because the runtime cap-router forwards provider
45105
- * output verbatim — so every consumer had to re-declare it by hand to stop
45106
- * its own Zod parse from stripping it, and the whole arrangement would have
45107
- * broken silently the moment output validation was tightened anywhere.
45108
- */
45109
- iconUrl: string().optional(),
45110
- /**
45111
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45112
- *
45113
- * The server knows this and therefore says it, because the client cannot
45114
- * safely guess: a React-Native client renders SVG and raster through two
45115
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45116
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45117
- * placeholder glyph for every vector icon while the web build looked fine.
45118
- *
45119
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45120
- * not been updated — a client that cannot determine the type should prefer
45121
- * its raster path, which is the safe default for an unknown image.
45122
- */
45123
- iconMediaType: string().optional(),
45124
- configSchema: ConfigSchemaPassthrough$2,
45125
- /**
45126
- * The config keys that decide WHERE this kind's secrets are sent (server,
45127
- * base URL, instance, chat, recipient). A save that changes any of them
45128
- * while leaving a secret redacted is refused by the provider (D634), so an
45129
- * editor uses this to ask for the secret on exactly those edits instead of
45130
- * on every edit.
45129
+ * The node whose series is wanted.
45131
45130
  *
45132
- * Optional: absent from a provider that holds no secret (the Home Assistant
45133
- * kinds) and from a provider older than this field — an editor that finds
45134
- * it absent must assume ANY edit may need the secret.
45131
+ * NOT named `nodeId`: the generated cap router strips a top-level
45132
+ * `nodeId` from every method input and uses it to ROUTE the call to
45133
+ * that node's provider (`generated-cap-routers.ts`). A series target
45134
+ * called `nodeId` would silently become a routing pin and never reach
45135
+ * the provider. The hub holds every node it hears from, so the
45136
+ * ordinary call is unpinned — answered by the hub, for any node.
45135
45137
  */
45136
- destinationKeys: array(string()).optional(),
45137
- supportsDiscovery: boolean(),
45138
+ forNodeId: string(),
45138
45139
  /**
45139
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45140
- * Absent = none; the rule editor then offers nothing.
45140
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
45141
+ * holds and receives only what it is missing, so seeding a live chart
45142
+ * from this method cannot double a point already drawn.
45141
45143
  */
45142
- deliveryParams: array(DeliveryParamSchema).optional(),
45143
- caps: TargetKindCapsSchema
45144
+ sinceMs: number().optional(),
45145
+ /**
45146
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
45147
+ * preserving min and max per bucket.
45148
+ *
45149
+ * Absent means NO reduction — legitimate for a short window and a trap for a
45150
+ * long one, which is why a chart passes its own pixel width.
45151
+ */
45152
+ maxPoints: number().int().positive().optional()
45153
+ });
45154
+ var SystemMetricsSchema = object({
45155
+ cpuPercent: number(),
45156
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45157
+ memoryPercent: number().nullable(),
45158
+ memoryUsedMB: number().nullable(),
45159
+ memoryTotalMB: number(),
45160
+ diskPercent: number().optional(),
45161
+ temperature: number().optional(),
45162
+ gpuPercent: number().optional(),
45163
+ gpuMemoryPercent: number().optional()
45164
+ });
45165
+ 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);
45166
+ var modelConvertCapability = {
45167
+ name: "model-convert",
45168
+ scope: "system",
45169
+ mode: "singleton",
45170
+ internal: true,
45171
+ methods: { convert: method(object({
45172
+ sourceUrl: string(),
45173
+ metadata: ModelConvertMetadataSchema,
45174
+ targets: array(ConvertTargetSchema).min(1).readonly(),
45175
+ calibrationRef: string().optional(),
45176
+ sessionId: string().optional()
45177
+ }), ConvertResultSchema, {
45178
+ kind: "mutation",
45179
+ auth: "admin",
45180
+ timeoutMs: 6e5
45181
+ }) }
45182
+ };
45183
+ method(object({
45184
+ targetNodeId: string(),
45185
+ modelId: string(),
45186
+ format: _enum(MODEL_FORMATS),
45187
+ entry: ModelCatalogEntrySchema
45188
+ }), object({
45189
+ ok: boolean(),
45190
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
45191
+ sha256: string(),
45192
+ bytes: number(),
45193
+ /** The target node's modelsDir the artifact landed in. */
45194
+ path: string()
45195
+ }), {
45196
+ kind: "mutation",
45197
+ auth: "admin"
45198
+ });
45199
+ var MotionRegionSchema = object({
45200
+ bbox: BoundingBoxSchema,
45201
+ pixelCount: number(),
45202
+ intensity: number()
45203
+ });
45204
+ var MotionAnalysisResultSchema = object({
45205
+ detected: boolean(),
45206
+ regionCount: number(),
45207
+ /** Regions passing minArea filter — used for pipeline triggering. */
45208
+ regions: array(MotionRegionSchema).readonly(),
45209
+ /** All raw regions from CCL before minArea filter — used for UI debug overlay. */
45210
+ rawRegions: array(MotionRegionSchema).readonly(),
45211
+ frameWidth: number(),
45212
+ frameHeight: number(),
45213
+ analysisMs: number()
45214
+ });
45215
+ DeviceType.Camera, method(object({
45216
+ deviceId: number(),
45217
+ frame: FrameInputSchema.optional(),
45218
+ frameHandle: FrameHandleSchema.optional()
45219
+ }), MotionAnalysisResultSchema, {
45220
+ role: "admin",
45221
+ kind: "mutation"
45222
+ }), method(object({ deviceId: number() }), _void(), {
45223
+ role: "admin",
45224
+ kind: "mutation"
45225
+ }), method(_void(), _void(), {
45226
+ role: "admin",
45227
+ kind: "mutation"
45228
+ }), object({
45229
+ deviceId: number(),
45230
+ detected: boolean(),
45231
+ regionCount: number()
45144
45232
  });
45145
45233
  /**
45146
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45147
- * (return a presence marker only) when serving `listTargets` — never
45148
- * round-trip a stored secret to the UI.
45234
+ * `mqtt-broker` — broker-registry cap.
45235
+ *
45236
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
45237
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
45238
+ * and (b) the connection details a consumer addon needs to spin up
45239
+ * its OWN `mqtt.js` client.
45240
+ *
45241
+ * Why: pub/sub routing over the system event-bus loses fidelity
45242
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
45243
+ * refcount bookkeeping that addons would rather own themselves. The
45244
+ * canonical consumer needs raw `mqtt.js`
45245
+ * features anyway — give it the connection config, get out of the way.
45246
+ *
45247
+ * Consumer flow:
45248
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
45249
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
45250
+ * client.subscribe('zigbee2mqtt/+')
45251
+ *
45252
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
45253
+ * cloud bridge). The "embedded" entry (when present) is just another
45254
+ * broker in the registry — its lifecycle is owned by the addon that
45255
+ * spawned it.
45149
45256
  */
45150
- var TargetSchema = object({
45257
+ var BrokerKindSchema = _enum(["external", "embedded"]);
45258
+ /**
45259
+ * Broker live-probe status.
45260
+ *
45261
+ * - `connected` — last probe completed a clean CONNACK
45262
+ * - `disconnected` — no probe has run yet (cold cache)
45263
+ * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
45264
+ * - `unreachable` — TCP connect timed out / refused
45265
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
45266
+ */
45267
+ var BrokerStatusSchema$1 = _enum([
45268
+ "connected",
45269
+ "disconnected",
45270
+ "auth-failed",
45271
+ "unreachable",
45272
+ "tls-error"
45273
+ ]);
45274
+ var BrokerInfoSchema$1 = object({
45151
45275
  id: string(),
45152
45276
  name: string(),
45153
- kind: string(),
45154
- addonId: string(),
45155
- enabled: boolean(),
45156
- config: record(string(), unknown())
45157
- });
45158
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45159
- var DiscoveredTargetSchema = object({
45160
- kind: string(),
45161
- suggestedName: string(),
45162
- config: record(string(), unknown())
45163
- });
45164
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45165
- var RenderedAsSchema = object({
45166
- level: string(),
45167
- format: NotificationFormatSchema,
45168
- attachmentsSent: number().int().nonnegative(),
45169
- actionsSent: number().int().nonnegative(),
45170
- truncated: boolean(),
45171
- dropped: array(string())
45172
- });
45173
- var SendResultSchema = object({
45174
- success: boolean(),
45277
+ url: string(),
45278
+ kind: BrokerKindSchema,
45279
+ status: BrokerStatusSchema$1,
45280
+ latencyMs: number().nullable(),
45175
45281
  error: string().optional(),
45176
- renderedAs: RenderedAsSchema.optional()
45282
+ /** Embedded brokers only: number of MQTT clients currently connected. */
45283
+ connectedClients: number().int().nonnegative().optional(),
45284
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45285
+ lastCheckedAt: number().optional()
45177
45286
  });
45178
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45179
- var TestResultSchema = SendResultSchema;
45180
45287
  /**
45181
- * Pre-save credential probe against a partial (unsaved) target config.
45182
- * `resolvedUrl` never includes secrets (token stripped from path).
45288
+ * Connection details — what a consumer needs to call
45289
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
45290
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
45291
+ * instead of stuffing creds into the URL (which leaks them into logs).
45183
45292
  */
45184
- var ProbeTargetConfigResultSchema = object({
45185
- ok: boolean(),
45186
- resolvedUrl: string(),
45187
- httpStatus: number().int().optional(),
45188
- stateInstance: string().optional(),
45189
- error: string().optional()
45190
- });
45191
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
45192
- kind: string(),
45193
- config: record(string(), unknown()).optional(),
45293
+ var BrokerConnectionDetailsSchema = object({
45294
+ url: string(),
45295
+ username: string().optional(),
45296
+ password: string().optional(),
45194
45297
  /**
45195
- * When editing / duplicating, the blank password field never round-trips
45196
- * the stored token. Pass the source target id so the provider can restore
45197
- * secrets server-side before dialling (same merge as upsert).
45298
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45299
+ * with its own discriminator (addon id, instance id) so reconnects
45300
+ * don't kick each other off (MQTT spec: clientId must be unique per
45301
+ * broker).
45198
45302
  */
45199
- targetId: string().optional()
45200
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
45201
- kind: string(),
45202
- config: record(string(), unknown()),
45203
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45204
- targetId: string().optional()
45205
- }), ProbeTargetConfigResultSchema, {
45206
- kind: "mutation",
45207
- auth: "admin"
45208
- }), method(object({
45209
- targetId: string(),
45210
- notification: NotificationSchema
45211
- }), SendResultSchema, {
45303
+ clientIdPrefix: string().optional()
45304
+ });
45305
+ var AddBrokerInputSchema = object({
45306
+ name: string().min(1),
45307
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45308
+ username: string().optional(),
45309
+ password: string().optional(),
45310
+ clientIdPrefix: string().optional()
45311
+ });
45312
+ var AddBrokerResultSchema = object({ id: string() });
45313
+ var IdInputSchema = object({ id: string() });
45314
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45315
+ ok: literal(true),
45316
+ latencyMs: number()
45317
+ }), object({
45318
+ ok: literal(false),
45319
+ error: string()
45320
+ })]);
45321
+ var StartEmbeddedInputSchema = object({
45322
+ port: number().int().min(1).max(65535).default(1883),
45323
+ /** Allow anonymous connect (no username/password). Default: false. */
45324
+ allowAnonymous: boolean().default(false),
45325
+ /** Optional shared username/password for clients. */
45326
+ username: string().optional(),
45327
+ password: string().optional()
45328
+ });
45329
+ var StartEmbeddedResultSchema = object({
45330
+ id: string(),
45331
+ url: string()
45332
+ });
45333
+ var StatusSchema = object({
45334
+ brokerCount: number(),
45335
+ embeddedRunning: boolean()
45336
+ });
45337
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45212
45338
  kind: "mutation",
45213
45339
  auth: "admin"
45214
- }), method(object({
45215
- targetId: string(),
45216
- sample: NotificationSchema.optional()
45217
- }), TestResultSchema, {
45340
+ }), method(IdInputSchema, _void(), {
45218
45341
  kind: "mutation",
45219
45342
  auth: "admin"
45220
- }), method(object({
45221
- target: TargetSchema,
45222
- /**
45223
- * Duplicate flow: create a NEW target id while copying secrets from an
45224
- * existing row. The UI never holds the stored token; blank / redacted
45225
- * password fields are restored from this source (D634 still applies if
45226
- * destinations diverge without a re-typed secret).
45227
- */
45228
- cloneSecretsFrom: string().optional()
45229
- }), TargetSchema, {
45343
+ }), method(IdInputSchema, TestResultSchema$1, {
45230
45344
  kind: "mutation",
45231
45345
  auth: "admin"
45232
- }), method(object({ targetId: string() }), _void(), {
45346
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45233
45347
  kind: "mutation",
45234
45348
  auth: "admin"
45235
- }), method(object({
45236
- targetId: string(),
45237
- enabled: boolean()
45238
- }), _void(), {
45349
+ }), method(IdInputSchema, _void(), {
45239
45350
  kind: "mutation",
45240
45351
  auth: "admin"
45352
+ }), method(_void(), StatusSchema);
45353
+ var NetworkEndpointSchema = object({
45354
+ url: string(),
45355
+ hostname: string(),
45356
+ port: number(),
45357
+ protocol: _enum(["http", "https"]),
45358
+ /**
45359
+ * Whether this address survives a restart of its provider. `false` = it is
45360
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45361
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45362
+ * it stays visible and an operator may still flag it. Absent = stable: every
45363
+ * provider except a Quick tunnel owns a name that does not change.
45364
+ */
45365
+ stable: boolean().optional()
45366
+ });
45367
+ object({
45368
+ url: string(),
45369
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45370
+ stable: boolean().optional(),
45371
+ updated: boolean().optional()
45372
+ });
45373
+ var NetworkAccessStatusSchema = object({
45374
+ connected: boolean(),
45375
+ endpoint: NetworkEndpointSchema.nullable(),
45376
+ error: string().optional()
45377
+ });
45378
+ /**
45379
+ * Optional, richer endpoint shape returned by providers that expose
45380
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45381
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45382
+ * the originating provider config (mode + sourcePort) so the
45383
+ * orchestrator UI can label rows distinctly. Providers that expose only
45384
+ * one endpoint just omit `listEndpoints` from their provider impl.
45385
+ */
45386
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45387
+ /**
45388
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45389
+ * the orchestrator can dedupe across `listEndpoints` polls.
45390
+ */
45391
+ id: string(),
45392
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45393
+ label: string(),
45394
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45395
+ mode: string().optional(),
45396
+ /** Originating local port the ingress fronts (informational). */
45397
+ sourcePort: number().optional()
45241
45398
  });
45399
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45242
45400
  var AUDIO_MACRO_LABELS = [
45243
45401
  {
45244
45402
  id: "speech",
@@ -51639,14 +51797,6 @@ var EngineProvisioningSchema = object({
51639
51797
  */
51640
51798
  configIssues: array(string()).optional()
51641
51799
  });
51642
- var PipelineStepInputSchema = lazy(() => object({
51643
- addonId: string(),
51644
- modelId: string().optional(),
51645
- enabled: boolean().default(true),
51646
- children: array(PipelineStepInputSchema).optional(),
51647
- settings: record(string(), unknown()).optional(),
51648
- jumpDeviceKey: string().optional()
51649
- }));
51650
51800
  var ModelSubstitutionSchema = object({
51651
51801
  addonId: string(),
51652
51802
  chosen: string(),
@@ -51725,7 +51875,6 @@ var ConfigUISchemaBridge = custom();
51725
51875
  var ConfigUISchemaNullableBridge = custom();
51726
51876
  var InferenceCapabilitiesBridge = custom();
51727
51877
  var ModelAvailabilityListBridge = custom();
51728
- var PipelineRunResultBridge = custom();
51729
51878
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
51730
51879
  modelId: string(),
51731
51880
  settings: record(string(), unknown()).readonly()
@@ -59399,7 +59548,7 @@ object({
59399
59548
  /** Ms epoch of the last value received from the appliance; 0 = never. */
59400
59549
  lastFetchedAt: number()
59401
59550
  });
59402
- var DeviceIdSchema = number().int().nonnegative();
59551
+ var DeviceIdSchema$1 = number().int().nonnegative();
59403
59552
  /** A value a program option or setting can take. */
59404
59553
  var DishwasherValueSchema = union([
59405
59554
  boolean(),
@@ -59463,77 +59612,77 @@ var DishwasherOptionsSchema = object({
59463
59612
  options: array(DishwasherOptionDescriptorSchema)
59464
59613
  });
59465
59614
  DeviceType.Dishwasher, method(object({
59466
- deviceId: DeviceIdSchema,
59615
+ deviceId: DeviceIdSchema$1,
59467
59616
  program: string().min(1).optional(),
59468
59617
  options: record(string(), DishwasherValueSchema).optional()
59469
59618
  }), _void(), {
59470
59619
  kind: "mutation",
59471
59620
  auth: "protected"
59472
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59621
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59473
59622
  kind: "mutation",
59474
59623
  auth: "protected"
59475
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59624
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59476
59625
  kind: "mutation",
59477
59626
  auth: "protected"
59478
- }), method(object({ deviceId: DeviceIdSchema }), _void(), {
59627
+ }), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
59479
59628
  kind: "mutation",
59480
59629
  auth: "protected"
59481
59630
  }), method(object({
59482
- deviceId: DeviceIdSchema,
59631
+ deviceId: DeviceIdSchema$1,
59483
59632
  program: string().min(1)
59484
59633
  }), _void(), {
59485
59634
  kind: "mutation",
59486
59635
  auth: "protected"
59487
59636
  }), method(object({
59488
- deviceId: DeviceIdSchema,
59637
+ deviceId: DeviceIdSchema$1,
59489
59638
  on: boolean()
59490
59639
  }), _void(), {
59491
59640
  kind: "mutation",
59492
59641
  auth: "protected"
59493
59642
  }), method(object({
59494
- deviceId: DeviceIdSchema,
59643
+ deviceId: DeviceIdSchema$1,
59495
59644
  on: boolean()
59496
59645
  }), _void(), {
59497
59646
  kind: "mutation",
59498
59647
  auth: "protected"
59499
59648
  }), method(object({
59500
- deviceId: DeviceIdSchema,
59649
+ deviceId: DeviceIdSchema$1,
59501
59650
  on: boolean()
59502
59651
  }), _void(), {
59503
59652
  kind: "mutation",
59504
59653
  auth: "protected"
59505
59654
  }), method(object({
59506
- deviceId: DeviceIdSchema,
59655
+ deviceId: DeviceIdSchema$1,
59507
59656
  on: boolean()
59508
59657
  }), _void(), {
59509
59658
  kind: "mutation",
59510
59659
  auth: "protected"
59511
59660
  }), method(object({
59512
- deviceId: DeviceIdSchema,
59661
+ deviceId: DeviceIdSchema$1,
59513
59662
  on: boolean()
59514
59663
  }), _void(), {
59515
59664
  kind: "mutation",
59516
59665
  auth: "protected"
59517
59666
  }), method(object({
59518
- deviceId: DeviceIdSchema,
59667
+ deviceId: DeviceIdSchema$1,
59519
59668
  on: boolean()
59520
59669
  }), _void(), {
59521
59670
  kind: "mutation",
59522
59671
  auth: "protected"
59523
59672
  }), method(object({
59524
- deviceId: DeviceIdSchema,
59673
+ deviceId: DeviceIdSchema$1,
59525
59674
  on: boolean()
59526
59675
  }), _void(), {
59527
59676
  kind: "mutation",
59528
59677
  auth: "protected"
59529
59678
  }), method(object({
59530
- deviceId: DeviceIdSchema,
59679
+ deviceId: DeviceIdSchema$1,
59531
59680
  key: string().min(1),
59532
59681
  value: DishwasherValueSchema
59533
59682
  }), _void(), {
59534
59683
  kind: "mutation",
59535
59684
  auth: "protected"
59536
- }), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
59685
+ }), method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" });
59537
59686
  DeviceType.Camera;
59538
59687
  object({
59539
59688
  /** Ms epoch of the last press. null = never observed since this provider started. */
@@ -61971,7 +62120,8 @@ var TopologyNodeSchema = object({
61971
62120
  isHub: boolean(),
61972
62121
  isOnline: boolean(),
61973
62122
  cpuPercent: number(),
61974
- memoryPercent: number(),
62123
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
62124
+ memoryPercent: number().nullable(),
61975
62125
  uptime: number(),
61976
62126
  lastSeen: string(),
61977
62127
  localIps: array(string()).readonly(),
@@ -62506,9 +62656,10 @@ object({
62506
62656
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
62507
62657
  * the device has not reported yet. */
62508
62658
  foodLevel: number().nullable().meta(readingMeta("none")),
62509
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
62510
- * feeder's own units. Null when the model reports none. */
62511
- bowlFood: number().nullable().meta(readingMeta("none")),
62659
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
62660
+ * feeder with a bowl scale; null when the model has none or its reading is
62661
+ * of an unproven unit. */
62662
+ bowlFood: number().nullable().meta(readingMeta("mass")),
62512
62663
  /** True while the pet is eating (`state.eating`). */
62513
62664
  eating: boolean(),
62514
62665
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -62621,6 +62772,64 @@ DeviceType.PetFeeder, method(object({
62621
62772
  kind: "mutation",
62622
62773
  auth: "protected"
62623
62774
  });
62775
+ object({
62776
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
62777
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62778
+ /** Water in the tank, in litres. Null while the source has not reported. */
62779
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
62780
+ /** True when the tank is empty. Null while unknown. */
62781
+ empty: boolean().nullable(),
62782
+ /** True while the pump is running. Null while unknown. */
62783
+ pumpRunning: boolean().nullable(),
62784
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
62785
+ pumpIntervalEnabled: boolean().nullable(),
62786
+ /** How long the pump runs per cycle, in seconds. */
62787
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
62788
+ /** How long the pump rests per cycle, in seconds. */
62789
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
62790
+ /** Water drunk in the last drink, in litres. */
62791
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
62792
+ /** Water drunk in the current hour, in litres. */
62793
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
62794
+ /** Water drunk over the last 24 hours (rolling), in litres. */
62795
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
62796
+ /** Water drunk since local midnight, in litres. */
62797
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
62798
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
62799
+ error: string().nullable(),
62800
+ /** Ms epoch when the slice was last refreshed. */
62801
+ lastFetchedAt: number()
62802
+ });
62803
+ var DeviceIdSchema = number().int().nonnegative();
62804
+ var MinutesSchema = number().int().min(0);
62805
+ DeviceType.PetFountain, method(object({
62806
+ deviceId: DeviceIdSchema,
62807
+ on: boolean()
62808
+ }), _void(), {
62809
+ kind: "mutation",
62810
+ auth: "protected"
62811
+ }), method(object({
62812
+ deviceId: DeviceIdSchema,
62813
+ on: boolean()
62814
+ }), _void(), {
62815
+ kind: "mutation",
62816
+ auth: "protected"
62817
+ }), method(object({
62818
+ deviceId: DeviceIdSchema,
62819
+ minutes: MinutesSchema
62820
+ }), _void(), {
62821
+ kind: "mutation",
62822
+ auth: "protected"
62823
+ }), method(object({
62824
+ deviceId: DeviceIdSchema,
62825
+ minutes: MinutesSchema
62826
+ }), _void(), {
62827
+ kind: "mutation",
62828
+ auth: "protected"
62829
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
62830
+ kind: "mutation",
62831
+ auth: "protected"
62832
+ });
62624
62833
  var VehicleSchema = object({
62625
62834
  id: string(),
62626
62835
  name: string(),
@@ -67218,7 +67427,7 @@ discriminatedUnion("kind", [object({
67218
67427
  })]);
67219
67428
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
67220
67429
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
67221
- 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;
67430
+ 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;
67222
67431
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
67223
67432
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
67224
67433
  new Set(CAP_NAMES);
@@ -67369,7 +67578,7 @@ var AccessoryKind = {
67369
67578
  PrivacyMask: DeviceRole.PrivacyMask
67370
67579
  };
67371
67580
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
67372
- 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;
67581
+ 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;
67373
67582
  new Set(Object.values(DeviceType));
67374
67583
  Object.freeze({
67375
67584
  "accessories.getStatus": {
@@ -69304,6 +69513,13 @@ Object.freeze({
69304
69513
  access: "create",
69305
69514
  role: "admin"
69306
69515
  },
69516
+ "deviceExport.exposeEntity": {
69517
+ capName: "device-export",
69518
+ capScope: "system",
69519
+ addonId: null,
69520
+ access: "create",
69521
+ role: "admin"
69522
+ },
69307
69523
  "deviceExport.getDeviceLiveContribution": {
69308
69524
  capName: "device-export",
69309
69525
  capScope: "system",
@@ -69332,6 +69548,13 @@ Object.freeze({
69332
69548
  access: "view",
69333
69549
  role: "admin"
69334
69550
  },
69551
+ "deviceExport.listExposedEntities": {
69552
+ capName: "device-export",
69553
+ capScope: "system",
69554
+ addonId: null,
69555
+ access: "view",
69556
+ role: "admin"
69557
+ },
69335
69558
  "deviceExport.listSupportedDeviceKinds": {
69336
69559
  capName: "device-export",
69337
69560
  capScope: "system",
@@ -69339,6 +69562,13 @@ Object.freeze({
69339
69562
  access: "view",
69340
69563
  role: "admin"
69341
69564
  },
69565
+ "deviceExport.listSupportedEntityKinds": {
69566
+ capName: "device-export",
69567
+ capScope: "system",
69568
+ addonId: null,
69569
+ access: "view",
69570
+ role: "admin"
69571
+ },
69342
69572
  "deviceExport.unexposeDevice": {
69343
69573
  capName: "device-export",
69344
69574
  capScope: "system",
@@ -69346,6 +69576,13 @@ Object.freeze({
69346
69576
  access: "create",
69347
69577
  role: "admin"
69348
69578
  },
69579
+ "deviceExport.unexposeEntity": {
69580
+ capName: "device-export",
69581
+ capScope: "system",
69582
+ addonId: null,
69583
+ access: "create",
69584
+ role: "admin"
69585
+ },
69349
69586
  "deviceExtension.listCandidates": {
69350
69587
  capName: "device-extension",
69351
69588
  capScope: "device",
@@ -72167,6 +72404,13 @@ Object.freeze({
72167
72404
  access: "create",
72168
72405
  role: "admin"
72169
72406
  },
72407
+ "nodeRemovalHook.onNodeRemoved": {
72408
+ capName: "node-removal-hook",
72409
+ capScope: "system",
72410
+ addonId: null,
72411
+ access: "create",
72412
+ role: "admin"
72413
+ },
72170
72414
  "nodes.clusterAddonStatus": {
72171
72415
  capName: "nodes",
72172
72416
  capScope: "system",
@@ -72804,6 +73048,48 @@ Object.freeze({
72804
73048
  access: "create",
72805
73049
  role: "viewer"
72806
73050
  },
73051
+ "petFountain.getStatus": {
73052
+ capName: "pet-fountain",
73053
+ capScope: "device",
73054
+ addonId: null,
73055
+ access: "view",
73056
+ role: "viewer"
73057
+ },
73058
+ "petFountain.resetDailyConsumption": {
73059
+ capName: "pet-fountain",
73060
+ capScope: "device",
73061
+ addonId: null,
73062
+ access: "delete",
73063
+ role: "viewer"
73064
+ },
73065
+ "petFountain.setPump": {
73066
+ capName: "pet-fountain",
73067
+ capScope: "device",
73068
+ addonId: null,
73069
+ access: "create",
73070
+ role: "viewer"
73071
+ },
73072
+ "petFountain.setPumpInterval": {
73073
+ capName: "pet-fountain",
73074
+ capScope: "device",
73075
+ addonId: null,
73076
+ access: "create",
73077
+ role: "viewer"
73078
+ },
73079
+ "petFountain.setPumpOffMinutes": {
73080
+ capName: "pet-fountain",
73081
+ capScope: "device",
73082
+ addonId: null,
73083
+ access: "create",
73084
+ role: "viewer"
73085
+ },
73086
+ "petFountain.setPumpOnMinutes": {
73087
+ capName: "pet-fountain",
73088
+ capScope: "device",
73089
+ addonId: null,
73090
+ access: "create",
73091
+ role: "viewer"
73092
+ },
72807
73093
  "pipelineAnalytics.addRetrainFrameFromRecording": {
72808
73094
  capName: "pipeline-analytics",
72809
73095
  capScope: "device",
@@ -79286,6 +79572,100 @@ var APPLIANCE_CAP_VOCABULARY = {
79286
79572
  }
79287
79573
  ]
79288
79574
  },
79575
+ "pet-fountain": {
79576
+ scope: "device",
79577
+ noun: t("Pet fountain", "Fontana per animali"),
79578
+ actions: [
79579
+ {
79580
+ id: "petFountain.pumpOn",
79581
+ method: "setPump",
79582
+ label: t("Pump on", "Pompa accesa"),
79583
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
79584
+ icon: "play",
79585
+ args: { on: true }
79586
+ },
79587
+ {
79588
+ id: "petFountain.pumpOff",
79589
+ method: "setPump",
79590
+ label: t("Pump off", "Pompa spenta"),
79591
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
79592
+ icon: "square",
79593
+ args: { on: false }
79594
+ },
79595
+ {
79596
+ id: "petFountain.intervalOn",
79597
+ method: "setPumpInterval",
79598
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
79599
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
79600
+ icon: "timer",
79601
+ args: { on: true }
79602
+ },
79603
+ {
79604
+ id: "petFountain.intervalOff",
79605
+ method: "setPumpInterval",
79606
+ label: t("Pump cycle off", "Ciclo pompa spento"),
79607
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
79608
+ icon: "timer-off",
79609
+ args: { on: false }
79610
+ },
79611
+ {
79612
+ id: "petFountain.pumpOnMinutes",
79613
+ method: "setPumpOnMinutes",
79614
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
79615
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
79616
+ icon: "timer",
79617
+ openArgs: [{
79618
+ key: "minutes",
79619
+ label: t("Minutes", "Minuti"),
79620
+ offer: {
79621
+ kind: "number",
79622
+ min: 0,
79623
+ step: 1,
79624
+ unit: "min"
79625
+ }
79626
+ }]
79627
+ },
79628
+ {
79629
+ id: "petFountain.pumpOffMinutes",
79630
+ method: "setPumpOffMinutes",
79631
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
79632
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
79633
+ icon: "timer-off",
79634
+ openArgs: [{
79635
+ key: "minutes",
79636
+ label: t("Minutes", "Minuti"),
79637
+ offer: {
79638
+ kind: "number",
79639
+ min: 0,
79640
+ step: 1,
79641
+ unit: "min"
79642
+ }
79643
+ }]
79644
+ },
79645
+ {
79646
+ id: "petFountain.resetDailyConsumption",
79647
+ method: "resetDailyConsumption",
79648
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
79649
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
79650
+ icon: "rotate-ccw"
79651
+ }
79652
+ ],
79653
+ state: [{
79654
+ kind: "boolean",
79655
+ field: "empty",
79656
+ label: t("Tank empty", "Serbatoio vuoto"),
79657
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
79658
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
79659
+ icon: "droplet"
79660
+ }, {
79661
+ kind: "numeric",
79662
+ field: "waterLevel",
79663
+ quantity: t("the water level", "il livello dell’acqua"),
79664
+ label: t("Water level", "Livello dell’acqua"),
79665
+ unit: "%",
79666
+ icon: "droplets"
79667
+ }]
79668
+ },
79289
79669
  "media-player": {
79290
79670
  scope: "device",
79291
79671
  noun: t("Media player", "Lettore multimediale"),
@@ -81110,6 +81490,7 @@ var SYSTEM_CAP_VOCABULARY = {
81110
81490
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
81111
81491
  "device-provider": system(INTERNAL),
81112
81492
  "device-removal-hook": system(INTERNAL),
81493
+ "node-removal-hook": system(INTERNAL),
81113
81494
  "device-state": system("the runtime-state mirror every trigger already reads through"),
81114
81495
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
81115
81496
  "embedding-encoder": system(ML),
@@ -81222,7 +81603,7 @@ function projectAction(cap, action) {
81222
81603
  };
81223
81604
  }
81224
81605
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
81225
- 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;
81606
+ 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;
81226
81607
  DeviceFeature.BatteryOperated;
81227
81608
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
81228
81609
  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.";
@@ -82773,6 +83154,36 @@ Object.freeze({
82773
83154
  form: "single",
82774
83155
  optional: false
82775
83156
  }],
83157
+ "petFountain.getStatus": [{
83158
+ name: "deviceId",
83159
+ form: "single",
83160
+ optional: false
83161
+ }],
83162
+ "petFountain.resetDailyConsumption": [{
83163
+ name: "deviceId",
83164
+ form: "single",
83165
+ optional: false
83166
+ }],
83167
+ "petFountain.setPump": [{
83168
+ name: "deviceId",
83169
+ form: "single",
83170
+ optional: false
83171
+ }],
83172
+ "petFountain.setPumpInterval": [{
83173
+ name: "deviceId",
83174
+ form: "single",
83175
+ optional: false
83176
+ }],
83177
+ "petFountain.setPumpOffMinutes": [{
83178
+ name: "deviceId",
83179
+ form: "single",
83180
+ optional: false
83181
+ }],
83182
+ "petFountain.setPumpOnMinutes": [{
83183
+ name: "deviceId",
83184
+ form: "single",
83185
+ optional: false
83186
+ }],
82776
83187
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
82777
83188
  name: "deviceId",
82778
83189
  form: "single",