@camstack/addon-provider-homeassistant 1.2.636 → 1.2.638

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.
@@ -5201,6 +5201,14 @@ function object(shape, params) {
5201
5201
  ...normalizeParams(params)
5202
5202
  });
5203
5203
  }
5204
+ function strictObject(shape, params) {
5205
+ return new ZodObject({
5206
+ type: "object",
5207
+ shape,
5208
+ catchall: never(),
5209
+ ...normalizeParams(params)
5210
+ });
5211
+ }
5204
5212
  function looseObject(shape, params) {
5205
5213
  return new ZodObject({
5206
5214
  type: "object",
@@ -5580,7 +5588,7 @@ var ZodIssueCode = {
5580
5588
  var ZodFirstPartyTypeKind;
5581
5589
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5582
5590
  //#endregion
5583
- //#region ../types/dist/sleep-DJws6ho7.mjs
5591
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5584
5592
  /**
5585
5593
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5586
5594
  * window to float samples (D455).
@@ -8726,65 +8734,6 @@ var MODEL_FORMATS = [
8726
8734
  "pt",
8727
8735
  "gguf"
8728
8736
  ];
8729
- /**
8730
- * Per-call node pinning for `ctx.api` capability calls.
8731
- *
8732
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8733
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8734
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8735
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8736
- * call to that node.
8737
- *
8738
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8739
- * method args), so capability method signatures stay `nodeId`-free — node
8740
- * targeting is a property of the CALL, not of the method. The transport lifts
8741
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8742
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8743
- * which classifies a pinned agent node as `agent-child-forward`
8744
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8745
- *
8746
- * Usage at a call site:
8747
- *
8748
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8749
- */
8750
- /** tRPC `op.context` key carrying a per-call node pin. */
8751
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8752
- /**
8753
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8754
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8755
- *
8756
- * ## The id is normalised here, and it has to be
8757
- *
8758
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8759
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8760
- * compares a pin against real node ids, so such a pin matches nothing and the
8761
- * call fails with `no provider registered for cap "…"`. The local-first
8762
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8763
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8764
- * the thing you reach for when you specifically need THIS node — silently
8765
- * addressed a node that does not exist.
8766
- *
8767
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8768
- * own node to read the host's hardware-decode backend. It never once answered,
8769
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8770
- * while D67's whole premise was that the decoder addon is the authority on
8771
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8772
- * hardware", which was false.
8773
- *
8774
- * Normalising in the ONE constructor fixes every caller at once, which is why
8775
- * it is here and not at the call sites.
8776
- */
8777
- function nodePin(nodeId) {
8778
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8779
- }
8780
- /**
8781
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8782
- * is idempotent, so passing an already-clean id costs nothing.
8783
- */
8784
- function toNodeId(idOrRunnerId) {
8785
- const head = idOrRunnerId.split("/")[0];
8786
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8787
- }
8788
8737
  object({
8789
8738
  field: string().min(1),
8790
8739
  on: literal("advance")
@@ -8877,9 +8826,135 @@ function optionalMethod(input, output, options) {
8877
8826
  function event(data) {
8878
8827
  return { data };
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
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8880
8934
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8881
8935
  var VersionOutputSchema$1 = object({ version: string() });
8882
8936
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8937
+ /**
8938
+ * The pipeline step-input shape and the run-result bridge, in a module that
8939
+ * imports nothing but zod and types.
8940
+ *
8941
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8942
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8943
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8944
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8945
+ * them, so every existing import keeps working; addon server code takes them
8946
+ * from `@camstack/types/addon`.
8947
+ */
8948
+ var PipelineStepInputSchema = lazy(() => object({
8949
+ addonId: string(),
8950
+ modelId: string().optional(),
8951
+ enabled: boolean().default(true),
8952
+ children: array(PipelineStepInputSchema).optional(),
8953
+ settings: record(string(), unknown()).optional(),
8954
+ jumpDeviceKey: string().optional()
8955
+ }));
8956
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8957
+ var PipelineRunResultBridge = custom();
8883
8958
  var StaticDirOutputSchema = object({ staticDir: string() });
8884
8959
  var VersionOutputSchema = object({ version: string() });
8885
8960
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9064,6 +9139,7 @@ var CAP_NAMES = Object.freeze([
9064
9139
  "network-link",
9065
9140
  "network-quality",
9066
9141
  "node-certificates",
9142
+ "node-removal-hook",
9067
9143
  "nodes",
9068
9144
  "notification-output",
9069
9145
  "notification-rules",
@@ -9074,6 +9150,7 @@ var CAP_NAMES = Object.freeze([
9074
9150
  "osd",
9075
9151
  "osd-manager",
9076
9152
  "pet-feeder",
9153
+ "pet-fountain",
9077
9154
  "pipeline-analytics",
9078
9155
  "pipeline-executor",
9079
9156
  "pipeline-orchestrator",
@@ -10353,6 +10430,56 @@ Object.freeze({
10353
10430
  "valueType": "number"
10354
10431
  }
10355
10432
  ],
10433
+ "pet-fountain": [
10434
+ {
10435
+ "path": "waterLevel",
10436
+ "valueType": "number"
10437
+ },
10438
+ {
10439
+ "path": "waterVolume",
10440
+ "valueType": "number"
10441
+ },
10442
+ {
10443
+ "path": "empty",
10444
+ "valueType": "boolean"
10445
+ },
10446
+ {
10447
+ "path": "pumpRunning",
10448
+ "valueType": "boolean"
10449
+ },
10450
+ {
10451
+ "path": "pumpIntervalEnabled",
10452
+ "valueType": "boolean"
10453
+ },
10454
+ {
10455
+ "path": "pumpOnTime",
10456
+ "valueType": "number"
10457
+ },
10458
+ {
10459
+ "path": "pumpOffTime",
10460
+ "valueType": "number"
10461
+ },
10462
+ {
10463
+ "path": "lastDrink",
10464
+ "valueType": "number"
10465
+ },
10466
+ {
10467
+ "path": "currentHourDrink",
10468
+ "valueType": "number"
10469
+ },
10470
+ {
10471
+ "path": "last24hDrink",
10472
+ "valueType": "number"
10473
+ },
10474
+ {
10475
+ "path": "todayDrink",
10476
+ "valueType": "number"
10477
+ },
10478
+ {
10479
+ "path": "error",
10480
+ "valueType": "string"
10481
+ }
10482
+ ],
10356
10483
  "power-meter": [
10357
10484
  {
10358
10485
  "path": "power",
@@ -10890,7 +11017,7 @@ object({
10890
11017
  params: record(string(), union([string(), number()])).optional()
10891
11018
  });
10892
11019
  //#endregion
10893
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11020
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10894
11021
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10895
11022
  DeviceType["Camera"] = "camera";
10896
11023
  DeviceType["Hub"] = "hub";
@@ -10982,6 +11109,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10982
11109
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10983
11110
  * integrations sharing the same food/desiccant/hopper surface. */
10984
11111
  DeviceType["PetFeeder"] = "pet-feeder";
11112
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11113
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11114
+ * of a container and is BUILT as a composed device (D967). */
11115
+ DeviceType["PetFountain"] = "pet-fountain";
10985
11116
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10986
11117
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10987
11118
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11252,7 +11383,7 @@ function errMsg(err) {
11252
11383
  return String(err);
11253
11384
  }
11254
11385
  //#endregion
11255
- //#region ../types/dist/composition-DpFv_J6D.mjs
11386
+ //#region ../types/dist/composition-kSuOifuG.mjs
11256
11387
  /**
11257
11388
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11258
11389
  *
@@ -12337,12 +12468,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12337
12468
  ])
12338
12469
  })
12339
12470
  ]);
12340
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12471
+ var forwardArgUnion = union([
12472
+ string(),
12473
+ number(),
12474
+ boolean(),
12475
+ strictObject({ ["$input"]: string().min(1) }),
12476
+ record(string().min(1), lazy(() => forwardArgUnion))
12477
+ ]);
12478
+ var CompositionForwardArgSchema = forwardArgUnion;
12479
+ /**
12480
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12481
+ * call of `cap.method` on `source`. Without `args` the built method's input
12482
+ * passes through minus `deviceId`; with `args` the template IS the source's
12483
+ * whole input. `deviceId` is always the source's own, never an argument.
12484
+ */
12485
+ var CompositionForwardCommandSchema = object({
12341
12486
  kind: literal("forward"),
12342
12487
  source: CompositionSourceRefSchema,
12343
12488
  cap: string().min(1),
12344
- method: string().min(1)
12345
- }), CompositionCodeSourceSchema]);
12489
+ method: string().min(1),
12490
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12491
+ });
12492
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12346
12493
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12347
12494
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12348
12495
  /**
@@ -12352,7 +12499,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12352
12499
  */
12353
12500
  var CompositionItemEntrySchema = object({
12354
12501
  label: string().min(1).max(80),
12355
- fields: record(string().min(1), CompositionFieldSourceSchema)
12502
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12503
+ /**
12504
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12505
+ * is resettable and a `reset` call naming its key forwards here. The item key
12506
+ * selects the command, so it is never an argument: there is no pass-through.
12507
+ */
12508
+ reset: CompositionForwardCommandSchema.optional()
12356
12509
  });
12357
12510
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12358
12511
  kind: literal("fields"),
@@ -12364,7 +12517,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12364
12517
  * owns the WHOLE array field: the composed value holds exactly these items.
12365
12518
  */
12366
12519
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12367
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12520
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12368
12521
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12369
12522
  }), object({
12370
12523
  kind: literal("passthrough"),
@@ -16448,6 +16601,587 @@ object({
16448
16601
  lastError: string().nullable()
16449
16602
  });
16450
16603
  /**
16604
+ * notification-output — canonical, capability-gated notification delivery.
16605
+ *
16606
+ * Apprise-derived model (see
16607
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16608
+ * callers emit ONE canonical `Notification`; each provider declares a
16609
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16610
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16611
+ * message to what the kind supports — callers never special-case a service.
16612
+ *
16613
+ * DESIGN DECISIONS (locked):
16614
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16615
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16616
+ * cap. Rationale: the admin UI needs one uniform surface across the
16617
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16618
+ * alternative would fork the UI per addon and cannot host the
16619
+ * discovery→adopt flow.
16620
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16621
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16622
+ * registered provider (notifiers addon + HA addon) so one catalog is
16623
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16624
+ * `addonId` the generated collection router extracts from the call input.
16625
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16626
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16627
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16628
+ * base64 fallback needed.
16629
+ *
16630
+ * TODO (deferred, closed-set change — separate decision): add
16631
+ * `providerKind: 'notify'` so notification providers surface on the unified
16632
+ * admin "Integrations" page.
16633
+ */
16634
+ /**
16635
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16636
+ * adapter picks what it supports and the degrade engine filters the rest.
16637
+ */
16638
+ var AttachmentMediaTypeSchema = _enum([
16639
+ "image",
16640
+ "video",
16641
+ "gif",
16642
+ "audio",
16643
+ "icon"
16644
+ ]);
16645
+ /**
16646
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16647
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16648
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16649
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16650
+ */
16651
+ var AttachmentSchema = object({
16652
+ mediaType: AttachmentMediaTypeSchema,
16653
+ url: string().optional(),
16654
+ bytes: _instanceof(Uint8Array).optional(),
16655
+ mime: string().optional(),
16656
+ name: string().optional()
16657
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16658
+ var NotificationFormatSchema = _enum([
16659
+ "text",
16660
+ "markdown",
16661
+ "html"
16662
+ ]);
16663
+ /**
16664
+ * The CLOSED icon vocabulary an action button may use.
16665
+ *
16666
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16667
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16668
+ * notices — the same class of gap as a zone vocabulary nothing produced
16669
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16670
+ * declares `actionIcons: false` and the degrade engine strips the field.
16671
+ *
16672
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16673
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16674
+ * differently.
16675
+ *
16676
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16677
+ *
16678
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16679
+ * the whole set onto its own renderer's vocabulary through a
16680
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16681
+ * fallback, so adding a member here fails every adapter's build until someone
16682
+ * decides its glyph, which is the only place that decision can be made
16683
+ * honestly.
16684
+ *
16685
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16686
+ * `disarm` straight through; iOS feeds that string to
16687
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16688
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16689
+ * mapping, and "the field is documented" is not "the value renders".
16690
+ *
16691
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16692
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16693
+ * so an addon that emits a token the running hub does not know does not lose an
16694
+ * icon — its whole `send` fails Zod validation and the notification never
16695
+ * arrives. Never emit a new token from an addon before the train carrying it.
16696
+ */
16697
+ var NotificationActionIconSchema = _enum([
16698
+ "acknowledge",
16699
+ "dismiss",
16700
+ "silence",
16701
+ "snooze",
16702
+ "view",
16703
+ "play",
16704
+ "open",
16705
+ "close",
16706
+ "lock",
16707
+ "unlock",
16708
+ "arm",
16709
+ "arm-home",
16710
+ "arm-away",
16711
+ "arm-night",
16712
+ "disarm",
16713
+ "light",
16714
+ "alert",
16715
+ "camera"
16716
+ ]);
16717
+ /** A single tap-through action button. */
16718
+ var NotificationActionSchema = object({
16719
+ id: string(),
16720
+ label: string(),
16721
+ url: string().optional(),
16722
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16723
+ icon: NotificationActionIconSchema.optional(),
16724
+ /**
16725
+ * Renders in a warning style where the notifier supports it.
16726
+ *
16727
+ * A HINT, never a gate. The callback's authority is its token and nothing
16728
+ * else — see `notification-center/action-token.ts` for what that does and
16729
+ * does not buy.
16730
+ */
16731
+ destructive: boolean().optional(),
16732
+ /**
16733
+ * How the tap should REACH the url.
16734
+ *
16735
+ * `navigate` (absent, and every button authored before this field) opens it:
16736
+ * the phone leaves the notification and shows whatever the callback returns.
16737
+ * That is right for a button whose answer the operator wants to read.
16738
+ *
16739
+ * `background` fires it as a POST and stays put. It exists for the buttons
16740
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16741
+ * an answer to the notification, and being thrown into a browser tab to
16742
+ * confirm it costs more attention than the notification did. A backend that
16743
+ * cannot do a background call renders it as an ordinary link (the adapters
16744
+ * fall back rather than dropping the button), so this is a preference, never
16745
+ * a requirement.
16746
+ */
16747
+ mode: _enum(["navigate", "background"]).optional()
16748
+ });
16749
+ /** A delivery parameter value: what a notifier declared it takes. */
16750
+ var DeliveryParamValueSchema = union([
16751
+ string(),
16752
+ number(),
16753
+ boolean()
16754
+ ]);
16755
+ /**
16756
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16757
+ * addresses a target of that kind may set it; a kind that declares nothing is
16758
+ * never handed one. The admin UI derives its control from this descriptor —
16759
+ * no editor knows a service by name.
16760
+ *
16761
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16762
+ * run on the addon cap path).
16763
+ */
16764
+ var DeliveryParamSchema = object({
16765
+ key: string().min(1),
16766
+ label: string(),
16767
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16768
+ labelKey: string().optional(),
16769
+ type: _enum([
16770
+ "enum",
16771
+ "number",
16772
+ "boolean",
16773
+ "string"
16774
+ ]),
16775
+ /** `enum` only: the closed set of accepted values. */
16776
+ options: array(object({
16777
+ value: string(),
16778
+ label: string()
16779
+ })).optional(),
16780
+ /** `number` only. */
16781
+ min: number().optional(),
16782
+ max: number().optional(),
16783
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16784
+ default: DeliveryParamValueSchema.optional()
16785
+ });
16786
+ /**
16787
+ * The canonical notification. `body` is the only hard field (Apprise model).
16788
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16789
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16790
+ * the adapter maps this ordinal onto its native level. `level?` is an
16791
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16792
+ * `priority` for that one target.
16793
+ */
16794
+ var NotificationSchema = object({
16795
+ body: string(),
16796
+ title: string().optional(),
16797
+ format: NotificationFormatSchema.default("text"),
16798
+ priority: number().int().min(1).max(5).default(3),
16799
+ level: string().optional(),
16800
+ attachments: array(AttachmentSchema).optional(),
16801
+ clickUrl: string().optional(),
16802
+ actions: array(NotificationActionSchema).optional(),
16803
+ sound: string().optional(),
16804
+ ttl: number().optional(),
16805
+ tag: string().optional(),
16806
+ deviceId: number().optional(),
16807
+ eventId: string().optional(),
16808
+ metadata: record(string(), unknown()).optional(),
16809
+ /**
16810
+ * Values for the parameters the target's kind DECLARES in
16811
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16812
+ * declaration by the sender; a notifier reads only the keys it declared.
16813
+ */
16814
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16815
+ });
16816
+ /** One declared native severity/priority level for a kind. */
16817
+ var TargetKindLevelSchema = object({
16818
+ id: string(),
16819
+ label: string(),
16820
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16821
+ ordinal: number().int().min(1).max(5).nullable(),
16822
+ flags: object({
16823
+ critical: boolean().optional(),
16824
+ silent: boolean().optional(),
16825
+ noPush: boolean().optional()
16826
+ }).optional(),
16827
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16828
+ requires: array(string()).optional(),
16829
+ description: string().optional()
16830
+ });
16831
+ /** The full capability block consulted before dispatch. */
16832
+ var TargetKindCapsSchema = object({
16833
+ attachments: object({
16834
+ mediaTypes: array(AttachmentMediaTypeSchema),
16835
+ mode: _enum([
16836
+ "url",
16837
+ "bytes",
16838
+ "both"
16839
+ ]),
16840
+ max: number().int().nonnegative(),
16841
+ maxBytes: number().int().positive().optional()
16842
+ }),
16843
+ /** Max action buttons (0 = none). */
16844
+ actions: number().int().nonnegative(),
16845
+ /**
16846
+ * Whether this kind renders a per-action ICON.
16847
+ *
16848
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16849
+ * run on the addon cap path — three production failures in one day taught
16850
+ * this repo that once. Absent is read as false by the degrade engine, which
16851
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16852
+ * assumed and dropped costs the operator's trust in the field.
16853
+ */
16854
+ actionIcons: boolean().optional(),
16855
+ levels: array(TargetKindLevelSchema),
16856
+ format: array(NotificationFormatSchema),
16857
+ clickUrl: boolean(),
16858
+ sound: boolean(),
16859
+ ttl: boolean(),
16860
+ bodyMaxLen: number().int().positive()
16861
+ });
16862
+ /**
16863
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16864
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16865
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16866
+ * the union is large and not meant for runtime validation here; the exported
16867
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16868
+ */
16869
+ var ConfigSchemaPassthrough$3 = unknown();
16870
+ var TargetKindSchema = object({
16871
+ kind: string(),
16872
+ label: string(),
16873
+ icon: string(),
16874
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16875
+ addonId: string(),
16876
+ /**
16877
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16878
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16879
+ * when the addon bundles no icon for that kind — the client then falls back
16880
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16881
+ *
16882
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16883
+ * client, and a native client joins it onto its own hub base.
16884
+ *
16885
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16886
+ * field that survived only because the runtime cap-router forwards provider
16887
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16888
+ * its own Zod parse from stripping it, and the whole arrangement would have
16889
+ * broken silently the moment output validation was tightened anywhere.
16890
+ */
16891
+ iconUrl: string().optional(),
16892
+ /**
16893
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16894
+ *
16895
+ * The server knows this and therefore says it, because the client cannot
16896
+ * safely guess: a React-Native client renders SVG and raster through two
16897
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16898
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16899
+ * placeholder glyph for every vector icon while the web build looked fine.
16900
+ *
16901
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16902
+ * not been updated — a client that cannot determine the type should prefer
16903
+ * its raster path, which is the safe default for an unknown image.
16904
+ */
16905
+ iconMediaType: string().optional(),
16906
+ configSchema: ConfigSchemaPassthrough$3,
16907
+ /**
16908
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16909
+ * base URL, instance, chat, recipient). A save that changes any of them
16910
+ * while leaving a secret redacted is refused by the provider (D634), so an
16911
+ * editor uses this to ask for the secret on exactly those edits instead of
16912
+ * on every edit.
16913
+ *
16914
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16915
+ * kinds) and from a provider older than this field — an editor that finds
16916
+ * it absent must assume ANY edit may need the secret.
16917
+ */
16918
+ destinationKeys: array(string()).optional(),
16919
+ supportsDiscovery: boolean(),
16920
+ /**
16921
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16922
+ * Absent = none; the rule editor then offers nothing.
16923
+ */
16924
+ deliveryParams: array(DeliveryParamSchema).optional(),
16925
+ caps: TargetKindCapsSchema
16926
+ });
16927
+ /**
16928
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16929
+ * (return a presence marker only) when serving `listTargets` — never
16930
+ * round-trip a stored secret to the UI.
16931
+ */
16932
+ var TargetSchema = object({
16933
+ id: string(),
16934
+ name: string(),
16935
+ kind: string(),
16936
+ addonId: string(),
16937
+ enabled: boolean(),
16938
+ config: record(string(), unknown())
16939
+ });
16940
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16941
+ var DiscoveredTargetSchema = object({
16942
+ kind: string(),
16943
+ suggestedName: string(),
16944
+ config: record(string(), unknown())
16945
+ });
16946
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16947
+ var RenderedAsSchema = object({
16948
+ level: string(),
16949
+ format: NotificationFormatSchema,
16950
+ attachmentsSent: number().int().nonnegative(),
16951
+ actionsSent: number().int().nonnegative(),
16952
+ truncated: boolean(),
16953
+ dropped: array(string())
16954
+ });
16955
+ var SendResultSchema = object({
16956
+ success: boolean(),
16957
+ error: string().optional(),
16958
+ renderedAs: RenderedAsSchema.optional()
16959
+ });
16960
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16961
+ var TestResultSchema = SendResultSchema;
16962
+ /**
16963
+ * Pre-save credential probe against a partial (unsaved) target config.
16964
+ * `resolvedUrl` never includes secrets (token stripped from path).
16965
+ */
16966
+ var ProbeTargetConfigResultSchema = object({
16967
+ ok: boolean(),
16968
+ resolvedUrl: string(),
16969
+ httpStatus: number().int().optional(),
16970
+ stateInstance: string().optional(),
16971
+ error: string().optional()
16972
+ });
16973
+ var notificationOutputCapability = {
16974
+ name: "notification-output",
16975
+ scope: "system",
16976
+ mode: "collection",
16977
+ methods: {
16978
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
16979
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
16980
+ /**
16981
+ * One target WITH secrets — admin edit / duplicate hydration only.
16982
+ * `listTargets` stays redacted; the editor loads this so password fields
16983
+ * are filled (masked in the input) and a save can round-trip the token
16984
+ * when a destination changes (Duplicate with a new chatId, etc.).
16985
+ */
16986
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
16987
+ discoverTargets: method(object({
16988
+ kind: string(),
16989
+ config: record(string(), unknown()).optional(),
16990
+ /**
16991
+ * When editing / duplicating, the blank password field never round-trips
16992
+ * the stored token. Pass the source target id so the provider can restore
16993
+ * secrets server-side before dialling (same merge as upsert).
16994
+ */
16995
+ targetId: string().optional()
16996
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
16997
+ probeTargetConfig: method(object({
16998
+ kind: string(),
16999
+ config: record(string(), unknown()),
17000
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
17001
+ targetId: string().optional()
17002
+ }), ProbeTargetConfigResultSchema, {
17003
+ kind: "mutation",
17004
+ auth: "admin"
17005
+ }),
17006
+ send: method(object({
17007
+ targetId: string(),
17008
+ notification: NotificationSchema
17009
+ }), SendResultSchema, {
17010
+ kind: "mutation",
17011
+ auth: "admin"
17012
+ }),
17013
+ testTarget: method(object({
17014
+ targetId: string(),
17015
+ sample: NotificationSchema.optional()
17016
+ }), TestResultSchema, {
17017
+ kind: "mutation",
17018
+ auth: "admin"
17019
+ }),
17020
+ upsertTarget: method(object({
17021
+ target: TargetSchema,
17022
+ /**
17023
+ * Duplicate flow: create a NEW target id while copying secrets from an
17024
+ * existing row. The UI never holds the stored token; blank / redacted
17025
+ * password fields are restored from this source (D634 still applies if
17026
+ * destinations diverge without a re-typed secret).
17027
+ */
17028
+ cloneSecretsFrom: string().optional()
17029
+ }), TargetSchema, {
17030
+ kind: "mutation",
17031
+ auth: "admin"
17032
+ }),
17033
+ deleteTarget: method(object({ targetId: string() }), _void(), {
17034
+ kind: "mutation",
17035
+ auth: "admin"
17036
+ }),
17037
+ setTargetEnabled: method(object({
17038
+ targetId: string(),
17039
+ enabled: boolean()
17040
+ }), _void(), {
17041
+ kind: "mutation",
17042
+ auth: "admin"
17043
+ })
17044
+ }
17045
+ };
17046
+ function errorText(err) {
17047
+ return err instanceof Error ? err.message : String(err);
17048
+ }
17049
+ /** `actions.run` flattens a refusal to `[code] text`; the code leads. */
17050
+ function refusalFromThrown(message) {
17051
+ if (message.startsWith("[busy]")) return "already-running";
17052
+ if (message.startsWith("[not-found]")) return "not-found";
17053
+ return null;
17054
+ }
17055
+ async function runExportedAction(api, log, input) {
17056
+ const { exporter, actionId } = input;
17057
+ const tags = { actionId };
17058
+ const refuse = (reason, detail) => {
17059
+ log.warn(`${exporter}: exported action not run — ${reason}`, {
17060
+ tags,
17061
+ meta: {
17062
+ reason,
17063
+ ...detail ? { detail } : {}
17064
+ }
17065
+ });
17066
+ return {
17067
+ status: "refused",
17068
+ reason
17069
+ };
17070
+ };
17071
+ try {
17072
+ const result = await api.actions.run.mutate({ actionId });
17073
+ if (result.outcome === "skipped-overlap") return refuse("already-running", result.message);
17074
+ if (result.outcome === "skipped-cooldown") return refuse("cooldown", result.message);
17075
+ if (result.outcome === "failed" || result.outcome === "aborted") {
17076
+ const message = result.message ?? result.outcome;
17077
+ log.warn(`${exporter}: exported action failed`, {
17078
+ tags,
17079
+ meta: {
17080
+ outcome: result.outcome,
17081
+ message
17082
+ }
17083
+ });
17084
+ return {
17085
+ status: "failed",
17086
+ message
17087
+ };
17088
+ }
17089
+ log.info(`${exporter}: exported action ran`, {
17090
+ tags,
17091
+ meta: { outcome: result.outcome }
17092
+ });
17093
+ return { status: "started" };
17094
+ } catch (err) {
17095
+ const message = errorText(err);
17096
+ const reason = refusalFromThrown(message);
17097
+ if (reason !== null) return refuse(reason, message);
17098
+ log.warn(`${exporter}: exported action failed`, {
17099
+ tags,
17100
+ meta: { message }
17101
+ });
17102
+ return {
17103
+ status: "failed",
17104
+ message
17105
+ };
17106
+ }
17107
+ }
17108
+ /** Send through one CamStack notification target on behalf of an exporter. */
17109
+ async function sendToExportedTarget(api, log, input) {
17110
+ const { exporter, targetId } = input;
17111
+ const tags = { targetId };
17112
+ try {
17113
+ const target = (await api.notificationOutput.listTargets.query({})).find((t) => t.id === targetId);
17114
+ if (target === void 0) {
17115
+ log.warn(`${exporter}: exported notifier not used — target-gone`, {
17116
+ tags,
17117
+ meta: { reason: "target-gone" }
17118
+ });
17119
+ return {
17120
+ status: "refused",
17121
+ reason: "target-gone"
17122
+ };
17123
+ }
17124
+ if (!target.enabled) {
17125
+ log.warn(`${exporter}: exported notifier not used — target-disabled`, {
17126
+ tags,
17127
+ meta: { reason: "target-disabled" }
17128
+ });
17129
+ return {
17130
+ status: "refused",
17131
+ reason: "target-disabled"
17132
+ };
17133
+ }
17134
+ const result = await api.notificationOutput.send.mutate({
17135
+ targetId,
17136
+ addonId: target.addonId,
17137
+ notification: NotificationSchema.parse(input.notification)
17138
+ });
17139
+ if (!result.success) {
17140
+ const message = result.error ?? "send failed";
17141
+ log.warn(`${exporter}: exported notifier send failed`, {
17142
+ tags,
17143
+ meta: { message }
17144
+ });
17145
+ return {
17146
+ status: "failed",
17147
+ message
17148
+ };
17149
+ }
17150
+ return { status: "sent" };
17151
+ } catch (err) {
17152
+ const message = errorText(err);
17153
+ log.warn(`${exporter}: exported notifier send failed`, {
17154
+ tags,
17155
+ meta: { message }
17156
+ });
17157
+ return {
17158
+ status: "failed",
17159
+ message
17160
+ };
17161
+ }
17162
+ }
17163
+ /**
17164
+ * Split an exporter's selection into what still exists and what was deleted.
17165
+ * Devices are never judged here (their lifecycle is the tombstone, D865).
17166
+ * Callers pass ids from a query that ANSWERED — an unanswered list must not
17167
+ * reach this function (D49: a failed read changes nothing).
17168
+ */
17169
+ function pruneDeletedEntities(selection, live, log, exporter) {
17170
+ const kept = [];
17171
+ const dropped = [];
17172
+ for (const ref of selection) if (ref.kind === "action" && !live.actionIds.has(ref.actionId)) {
17173
+ dropped.push(ref);
17174
+ log.warn(`${exporter}: exported action was deleted — dropped from the export`, { tags: { actionId: ref.actionId } });
17175
+ } else if (ref.kind === "notifier" && !live.targetIds.has(ref.targetId)) {
17176
+ dropped.push(ref);
17177
+ log.warn(`${exporter}: exported notifier target was deleted — dropped from the export`, { tags: { targetId: ref.targetId } });
17178
+ } else kept.push(ref);
17179
+ return {
17180
+ kept,
17181
+ dropped
17182
+ };
17183
+ }
17184
+ /**
16451
17185
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16452
17186
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
16453
17187
  *
@@ -16479,52 +17213,6 @@ var AddonPageInfoSchema = object({
16479
17213
  bundleUrl: string()
16480
17214
  });
16481
17215
  method(_void(), array(AddonPageInfoSchema).readonly());
16482
- /**
16483
- * `addon-pages-source` — collection cap exposing per-provider raw page
16484
- * declarations. Every addon that contributes a UI page registers a
16485
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16486
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16487
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16488
- * that admin-ui consumes.
16489
- *
16490
- * The split exists because the public listing has a different output
16491
- * shape than the per-provider raw declarations, and we want both ends
16492
- * to flow through codegen instead of relying on a hand-written wrapper.
16493
- */
16494
- var AddonPageDeclarationSchema = object({
16495
- id: string(),
16496
- label: string(),
16497
- icon: string(),
16498
- path: string(),
16499
- /**
16500
- * Module Federation remote name — must match the `name` field on the
16501
- * page addon's `federation()` plugin config. Used by admin-ui's
16502
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16503
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16504
- * contain hyphens).
16505
- */
16506
- remoteName: string(),
16507
- /**
16508
- * Bundle filename inside the addon's `dist/` dir served at
16509
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16510
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16511
- * the static-file route can compute an mtime-based cache-buster URL
16512
- * without a separate filesystem stat.
16513
- */
16514
- bundle: string(),
16515
- /**
16516
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16517
- * `'cluster'`, `'administration'` — the page renders inside that group.
16518
- * Any OTHER string creates (or joins) a custom section rendered after
16519
- * the built-in groups; its label comes from `sectionLabel` (first
16520
- * declaration wins), falling back to the id. Absent → the legacy
16521
- * "Addon Pages" group.
16522
- */
16523
- section: string().optional(),
16524
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16525
- sectionLabel: string().optional()
16526
- });
16527
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16528
17216
  var AddonHttpRouteSchema = object({
16529
17217
  method: _enum([
16530
17218
  "GET",
@@ -19241,7 +19929,11 @@ var CompositionProblemSchema = object({
19241
19929
  "mixed-quantity",
19242
19930
  "helper-mismatch",
19243
19931
  "member-constraint",
19244
- "retention"
19932
+ "retention",
19933
+ "command-method-unknown",
19934
+ "command-source-unknown",
19935
+ "command-input-unknown",
19936
+ "command-input-missing"
19245
19937
  ]),
19246
19938
  path: string(),
19247
19939
  message: string()
@@ -19280,7 +19972,9 @@ var CompositionFieldRoleSchema = _enum([
19280
19972
  var CompositionFieldItemSchema = object({
19281
19973
  arrayPath: string(),
19282
19974
  key: string(),
19283
- label: string()
19975
+ label: string(),
19976
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19977
+ resettable: boolean().optional()
19284
19978
  });
19285
19979
  var CompositionFieldPlanSchema = object({
19286
19980
  cap: string(),
@@ -19485,7 +20179,7 @@ Object.freeze({
19485
20179
  * One catalog row per base reading a group helper can build (D944 §3).
19486
20180
  * Data, not a form default written into the block.
19487
20181
  */
19488
- var NOW$2 = {
20182
+ var NOW$3 = {
19489
20183
  fieldPath: "lastFetchedAt",
19490
20184
  expr: "now"
19491
20185
  };
@@ -19537,7 +20231,7 @@ var GROUP_TEMPLATES = [
19537
20231
  quantity: "temperature",
19538
20232
  defaultAggregate: "mean",
19539
20233
  empty: "null",
19540
- companions: [NOW$2],
20234
+ companions: [NOW$3],
19541
20235
  alsoFrom: {
19542
20236
  deviceTypes: CLIMATE_DEVICES,
19543
20237
  accept: {
@@ -19555,7 +20249,7 @@ var GROUP_TEMPLATES = [
19555
20249
  quantity: "humidity",
19556
20250
  defaultAggregate: "mean",
19557
20251
  empty: "null",
19558
- companions: [NOW$2],
20252
+ companions: [NOW$3],
19559
20253
  alsoFrom: {
19560
20254
  deviceTypes: CLIMATE_DEVICES,
19561
20255
  accept: {
@@ -19573,7 +20267,7 @@ var GROUP_TEMPLATES = [
19573
20267
  quantity: "pressure",
19574
20268
  defaultAggregate: "mean",
19575
20269
  empty: "null",
19576
- companions: [NOW$2]
20270
+ companions: [NOW$3]
19577
20271
  }),
19578
20272
  numeric({
19579
20273
  id: "illuminance",
@@ -19584,7 +20278,7 @@ var GROUP_TEMPLATES = [
19584
20278
  quantity: "illuminance",
19585
20279
  defaultAggregate: "mean",
19586
20280
  empty: "null",
19587
- companions: [NOW$2]
20281
+ companions: [NOW$3]
19588
20282
  }),
19589
20283
  numeric({
19590
20284
  id: "power",
@@ -19595,7 +20289,7 @@ var GROUP_TEMPLATES = [
19595
20289
  quantity: "power",
19596
20290
  defaultAggregate: "sum",
19597
20291
  empty: "omit",
19598
- companions: [NOW$2]
20292
+ companions: [NOW$3]
19599
20293
  }),
19600
20294
  numeric({
19601
20295
  id: "energy",
@@ -19606,7 +20300,7 @@ var GROUP_TEMPLATES = [
19606
20300
  quantity: "energy",
19607
20301
  defaultAggregate: "sum",
19608
20302
  empty: "omit",
19609
- companions: [NOW$2]
20303
+ companions: [NOW$3]
19610
20304
  }),
19611
20305
  numeric({
19612
20306
  id: "battery",
@@ -19641,7 +20335,7 @@ var GROUP_TEMPLATES = [
19641
20335
  quantity: "device",
19642
20336
  defaultAggregate: "mean",
19643
20337
  empty: "null",
19644
- companions: [NOW$2]
20338
+ companions: [NOW$3]
19645
20339
  }),
19646
20340
  binary({
19647
20341
  id: "motion",
@@ -22336,6 +23030,36 @@ var ExposeInputSchema = object({
22336
23030
  capabilities: array(string()).optional()
22337
23031
  });
22338
23032
  var UnexposeInputSchema = object({ deviceId: string() });
23033
+ /**
23034
+ * What an exporter may expose besides devices (D968). An action and a
23035
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
23036
+ * exporter's own selection, not the device registry.
23037
+ */
23038
+ var ExportEntityKindSchema = _enum([
23039
+ "device",
23040
+ "action",
23041
+ "notifier"
23042
+ ]);
23043
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
23044
+ object({
23045
+ kind: literal("device"),
23046
+ deviceId: string()
23047
+ }),
23048
+ object({
23049
+ kind: literal("action"),
23050
+ actionId: string().min(1)
23051
+ }),
23052
+ object({
23053
+ kind: literal("notifier"),
23054
+ targetId: string().min(1)
23055
+ })
23056
+ ]);
23057
+ var ExposedEntitySchema = object({
23058
+ entity: ExportEntityRefSchema,
23059
+ exposedAs: string().optional()
23060
+ });
23061
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
23062
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22339
23063
  var deviceExportCapability = {
22340
23064
  name: "device-export",
22341
23065
  scope: "system",
@@ -22365,6 +23089,16 @@ var deviceExportCapability = {
22365
23089
  unexposeDevice: method(UnexposeInputSchema, _void(), {
22366
23090
  kind: "mutation",
22367
23091
  auth: "admin"
23092
+ }),
23093
+ listSupportedEntityKinds: optionalMethod(_void(), array(ExportEntityKindSchema)),
23094
+ listExposedEntities: optionalMethod(_void(), array(ExposedEntitySchema)),
23095
+ exposeEntity: optionalMethod(ExposeEntityInputSchema, _void(), {
23096
+ kind: "mutation",
23097
+ auth: "admin"
23098
+ }),
23099
+ unexposeEntity: optionalMethod(UnexposeEntityInputSchema, _void(), {
23100
+ kind: "mutation",
23101
+ auth: "admin"
22368
23102
  })
22369
23103
  }
22370
23104
  };
@@ -43588,6 +44322,14 @@ method(object({
43588
44322
  kind: "mutation",
43589
44323
  auth: "admin"
43590
44324
  });
44325
+ method(object({
44326
+ nodeId: string().min(1),
44327
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
44328
+ removedAt: number()
44329
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
44330
+ kind: "mutation",
44331
+ auth: "admin"
44332
+ });
43591
44333
  /**
43592
44334
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
43593
44335
  * so it can delete what it keeps for that device (D865).
@@ -44308,7 +45050,7 @@ var LlmProviderWidgetSchema = object({
44308
45050
  /** ConfigUISchema tree passed through untyped on the wire (the
44309
45051
  * notification-output `ConfigSchemaPassthrough` precedent at
44310
45052
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
44311
- var ConfigSchemaPassthrough$3 = unknown();
45053
+ var ConfigSchemaPassthrough$2 = unknown();
44312
45054
  var LlmProfileKindDescriptorSchema = object({
44313
45055
  kind: LlmProfileKindSchema,
44314
45056
  label: string(),
@@ -44317,9 +45059,9 @@ var LlmProfileKindDescriptorSchema = object({
44317
45059
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
44318
45060
  addonId: string(),
44319
45061
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
44320
- connectionSchema: ConfigSchemaPassthrough$3,
45062
+ connectionSchema: ConfigSchemaPassthrough$2,
44321
45063
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
44322
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
45064
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
44323
45065
  /** The kind's defaults; a model from `listModels` may override any of them. */
44324
45066
  features: LlmModelFeaturesSchema,
44325
45067
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44845,10 +45587,10 @@ var CpuBreakdownSchema = object({
44845
45587
  cores: number()
44846
45588
  });
44847
45589
  var MemoryInfoSchema = object({
44848
- percent: number(),
45590
+ percent: number().nullable(),
44849
45591
  totalBytes: number(),
44850
- usedBytes: number(),
44851
- availableBytes: number(),
45592
+ usedBytes: number().nullable(),
45593
+ availableBytes: number().nullable(),
44852
45594
  swapUsedBytes: number(),
44853
45595
  swapTotalBytes: number()
44854
45596
  });
@@ -45152,8 +45894,9 @@ var GetLoadSeriesInputSchema = object({
45152
45894
  });
45153
45895
  var SystemMetricsSchema = object({
45154
45896
  cpuPercent: number(),
45155
- memoryPercent: number(),
45156
- memoryUsedMB: number(),
45897
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45898
+ memoryPercent: number().nullable(),
45899
+ memoryUsedMB: number().nullable(),
45157
45900
  memoryTotalMB: number(),
45158
45901
  diskPercent: number().optional(),
45159
45902
  temperature: number().optional(),
@@ -45264,574 +46007,131 @@ var BrokerStatusSchema$1 = _enum([
45264
46007
  "tls-error"
45265
46008
  ]);
45266
46009
  var BrokerInfoSchema$1 = object({
45267
- id: string(),
45268
- name: string(),
45269
- url: string(),
45270
- kind: BrokerKindSchema,
45271
- status: BrokerStatusSchema$1,
45272
- latencyMs: number().nullable(),
45273
- error: string().optional(),
45274
- /** Embedded brokers only: number of MQTT clients currently connected. */
45275
- connectedClients: number().int().nonnegative().optional(),
45276
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45277
- lastCheckedAt: number().optional()
45278
- });
45279
- /**
45280
- * Connection details — what a consumer needs to call
45281
- * `mqtt.connect(url, options)`. We split URL + credentials so the
45282
- * consumer can pass them as `mqtt.connect(url, { username, password })`
45283
- * instead of stuffing creds into the URL (which leaks them into logs).
45284
- */
45285
- var BrokerConnectionDetailsSchema = object({
45286
- url: string(),
45287
- username: string().optional(),
45288
- password: string().optional(),
45289
- /**
45290
- * Suggested prefix for `clientId`. Each consumer should suffix this
45291
- * with its own discriminator (addon id, instance id) so reconnects
45292
- * don't kick each other off (MQTT spec: clientId must be unique per
45293
- * broker).
45294
- */
45295
- clientIdPrefix: string().optional()
45296
- });
45297
- var AddBrokerInputSchema = object({
45298
- name: string().min(1),
45299
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45300
- username: string().optional(),
45301
- password: string().optional(),
45302
- clientIdPrefix: string().optional()
45303
- });
45304
- var AddBrokerResultSchema = object({ id: string() });
45305
- var IdInputSchema = object({ id: string() });
45306
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
45307
- ok: literal(true),
45308
- latencyMs: number()
45309
- }), object({
45310
- ok: literal(false),
45311
- error: string()
45312
- })]);
45313
- var StartEmbeddedInputSchema = object({
45314
- port: number().int().min(1).max(65535).default(1883),
45315
- /** Allow anonymous connect (no username/password). Default: false. */
45316
- allowAnonymous: boolean().default(false),
45317
- /** Optional shared username/password for clients. */
45318
- username: string().optional(),
45319
- password: string().optional()
45320
- });
45321
- var StartEmbeddedResultSchema = object({
45322
- id: string(),
45323
- url: string()
45324
- });
45325
- var StatusSchema = object({
45326
- brokerCount: number(),
45327
- embeddedRunning: boolean()
45328
- });
45329
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45330
- kind: "mutation",
45331
- auth: "admin"
45332
- }), method(IdInputSchema, _void(), {
45333
- kind: "mutation",
45334
- auth: "admin"
45335
- }), method(IdInputSchema, TestResultSchema$1, {
45336
- kind: "mutation",
45337
- auth: "admin"
45338
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45339
- kind: "mutation",
45340
- auth: "admin"
45341
- }), method(IdInputSchema, _void(), {
45342
- kind: "mutation",
45343
- auth: "admin"
45344
- }), method(_void(), StatusSchema);
45345
- var NetworkEndpointSchema = object({
45346
- url: string(),
45347
- hostname: string(),
45348
- port: number(),
45349
- protocol: _enum(["http", "https"]),
45350
- /**
45351
- * Whether this address survives a restart of its provider. `false` = it is
45352
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45353
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
45354
- * it stays visible and an operator may still flag it. Absent = stable: every
45355
- * provider except a Quick tunnel owns a name that does not change.
45356
- */
45357
- stable: boolean().optional()
45358
- });
45359
- object({
45360
- url: string(),
45361
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45362
- stable: boolean().optional(),
45363
- updated: boolean().optional()
45364
- });
45365
- var NetworkAccessStatusSchema = object({
45366
- connected: boolean(),
45367
- endpoint: NetworkEndpointSchema.nullable(),
45368
- error: string().optional()
45369
- });
45370
- /**
45371
- * Optional, richer endpoint shape returned by providers that expose
45372
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
45373
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45374
- * the originating provider config (mode + sourcePort) so the
45375
- * orchestrator UI can label rows distinctly. Providers that expose only
45376
- * one endpoint just omit `listEndpoints` from their provider impl.
45377
- */
45378
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45379
- /**
45380
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
45381
- * the orchestrator can dedupe across `listEndpoints` polls.
45382
- */
45383
- id: string(),
45384
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45385
- label: string(),
45386
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45387
- mode: string().optional(),
45388
- /** Originating local port the ingress fronts (informational). */
45389
- sourcePort: number().optional()
45390
- });
45391
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45392
- /**
45393
- * notification-output — canonical, capability-gated notification delivery.
45394
- *
45395
- * Apprise-derived model (see
45396
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
45397
- * callers emit ONE canonical `Notification`; each provider declares a
45398
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
45399
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
45400
- * message to what the kind supports — callers never special-case a service.
45401
- *
45402
- * DESIGN DECISIONS (locked):
45403
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
45404
- * `setTargetEnabled`), each provider persisting via the `settings-store`
45405
- * cap. Rationale: the admin UI needs one uniform surface across the
45406
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
45407
- * alternative would fork the UI per addon and cannot host the
45408
- * discovery→adopt flow.
45409
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
45410
- * the generated cap-mount auto-`concatCollection`-fans them across every
45411
- * registered provider (notifiers addon + HA addon) so one catalog is
45412
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
45413
- * `addonId` the generated collection router extracts from the call input.
45414
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
45415
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
45416
- * `storage` / `storage-provider` / `recording` caps over the same path. No
45417
- * base64 fallback needed.
45418
- *
45419
- * TODO (deferred, closed-set change — separate decision): add
45420
- * `providerKind: 'notify'` so notification providers surface on the unified
45421
- * admin "Integrations" page.
45422
- */
45423
- /**
45424
- * Zentik-derived typed-media enum — the superset across every kind. Each
45425
- * adapter picks what it supports and the degrade engine filters the rest.
45426
- */
45427
- var AttachmentMediaTypeSchema = _enum([
45428
- "image",
45429
- "video",
45430
- "gif",
45431
- "audio",
45432
- "icon"
45433
- ]);
45434
- /**
45435
- * A single attachment. Exactly one of `url` (remote source, most adapters
45436
- * prefer this) or `bytes` (inline source; required for Pushover-style
45437
- * bytes-only kinds) MUST be present — the degrade engine expresses a
45438
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
45439
- */
45440
- var AttachmentSchema = object({
45441
- mediaType: AttachmentMediaTypeSchema,
45442
- url: string().optional(),
45443
- bytes: _instanceof(Uint8Array).optional(),
45444
- mime: string().optional(),
45445
- name: string().optional()
45446
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
45447
- var NotificationFormatSchema = _enum([
45448
- "text",
45449
- "markdown",
45450
- "html"
45451
- ]);
45452
- /**
45453
- * The CLOSED icon vocabulary an action button may use.
45454
- *
45455
- * A closed set, not a free string, and that is the whole point: an arbitrary
45456
- * icon name is one that ntfy renders, zentik silently drops, and nobody
45457
- * notices — the same class of gap as a zone vocabulary nothing produced
45458
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
45459
- * declares `actionIcons: false` and the degrade engine strips the field.
45460
- *
45461
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
45462
- * renderer's icon set; "acknowledge" survives an adapter that draws it
45463
- * differently.
45464
- *
45465
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
45466
- *
45467
- * These names are for US. **No adapter may forward one verbatim.** Each maps
45468
- * the whole set onto its own renderer's vocabulary through a
45469
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
45470
- * fallback, so adding a member here fails every adapter's build until someone
45471
- * decides its glyph, which is the only place that decision can be made
45472
- * honestly.
45473
- *
45474
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
45475
- * `disarm` straight through; iOS feeds that string to
45476
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
45477
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
45478
- * mapping, and "the field is documented" is not "the value renders".
45479
- *
45480
- * Adding a member is TRAIN-BOUND. The enum lives in the published
45481
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
45482
- * so an addon that emits a token the running hub does not know does not lose an
45483
- * icon — its whole `send` fails Zod validation and the notification never
45484
- * arrives. Never emit a new token from an addon before the train carrying it.
45485
- */
45486
- var NotificationActionIconSchema = _enum([
45487
- "acknowledge",
45488
- "dismiss",
45489
- "silence",
45490
- "snooze",
45491
- "view",
45492
- "play",
45493
- "open",
45494
- "close",
45495
- "lock",
45496
- "unlock",
45497
- "arm",
45498
- "arm-home",
45499
- "arm-away",
45500
- "arm-night",
45501
- "disarm",
45502
- "light",
45503
- "alert",
45504
- "camera"
45505
- ]);
45506
- /** A single tap-through action button. */
45507
- var NotificationActionSchema = object({
45508
- id: string(),
45509
- label: string(),
45510
- url: string().optional(),
45511
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
45512
- icon: NotificationActionIconSchema.optional(),
45513
- /**
45514
- * Renders in a warning style where the notifier supports it.
45515
- *
45516
- * A HINT, never a gate. The callback's authority is its token and nothing
45517
- * else — see `notification-center/action-token.ts` for what that does and
45518
- * does not buy.
45519
- */
45520
- destructive: boolean().optional(),
45521
- /**
45522
- * How the tap should REACH the url.
45523
- *
45524
- * `navigate` (absent, and every button authored before this field) opens it:
45525
- * the phone leaves the notification and shows whatever the callback returns.
45526
- * That is right for a button whose answer the operator wants to read.
45527
- *
45528
- * `background` fires it as a POST and stays put. It exists for the buttons
45529
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
45530
- * an answer to the notification, and being thrown into a browser tab to
45531
- * confirm it costs more attention than the notification did. A backend that
45532
- * cannot do a background call renders it as an ordinary link (the adapters
45533
- * fall back rather than dropping the button), so this is a preference, never
45534
- * a requirement.
45535
- */
45536
- mode: _enum(["navigate", "background"]).optional()
45537
- });
45538
- /** A delivery parameter value: what a notifier declared it takes. */
45539
- var DeliveryParamValueSchema = union([
45540
- string(),
45541
- number(),
45542
- boolean()
45543
- ]);
45544
- /**
45545
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
45546
- * addresses a target of that kind may set it; a kind that declares nothing is
45547
- * never handed one. The admin UI derives its control from this descriptor —
45548
- * no editor knows a service by name.
45549
- *
45550
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45551
- * run on the addon cap path).
45552
- */
45553
- var DeliveryParamSchema = object({
45554
- key: string().min(1),
45555
- label: string(),
45556
- /** i18n key of the label, resolved by the declaring addon's namespace. */
45557
- labelKey: string().optional(),
45558
- type: _enum([
45559
- "enum",
45560
- "number",
45561
- "boolean",
45562
- "string"
45563
- ]),
45564
- /** `enum` only: the closed set of accepted values. */
45565
- options: array(object({
45566
- value: string(),
45567
- label: string()
45568
- })).optional(),
45569
- /** `number` only. */
45570
- min: number().optional(),
45571
- max: number().optional(),
45572
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45573
- default: DeliveryParamValueSchema.optional()
45574
- });
45575
- /**
45576
- * The canonical notification. `body` is the only hard field (Apprise model).
45577
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45578
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45579
- * the adapter maps this ordinal onto its native level. `level?` is an
45580
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45581
- * `priority` for that one target.
45582
- */
45583
- var NotificationSchema = object({
45584
- body: string(),
45585
- title: string().optional(),
45586
- format: NotificationFormatSchema.default("text"),
45587
- priority: number().int().min(1).max(5).default(3),
45588
- level: string().optional(),
45589
- attachments: array(AttachmentSchema).optional(),
45590
- clickUrl: string().optional(),
45591
- actions: array(NotificationActionSchema).optional(),
45592
- sound: string().optional(),
45593
- ttl: number().optional(),
45594
- tag: string().optional(),
45595
- deviceId: number().optional(),
45596
- eventId: string().optional(),
45597
- metadata: record(string(), unknown()).optional(),
45598
- /**
45599
- * Values for the parameters the target's kind DECLARES in
45600
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45601
- * declaration by the sender; a notifier reads only the keys it declared.
45602
- */
45603
- delivery: record(string(), DeliveryParamValueSchema).optional()
45604
- });
45605
- /** One declared native severity/priority level for a kind. */
45606
- var TargetKindLevelSchema = object({
45607
- id: string(),
45608
- label: string(),
45609
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45610
- ordinal: number().int().min(1).max(5).nullable(),
45611
- flags: object({
45612
- critical: boolean().optional(),
45613
- silent: boolean().optional(),
45614
- noPush: boolean().optional()
45615
- }).optional(),
45616
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45617
- requires: array(string()).optional(),
45618
- description: string().optional()
45619
- });
45620
- /** The full capability block consulted before dispatch. */
45621
- var TargetKindCapsSchema = object({
45622
- attachments: object({
45623
- mediaTypes: array(AttachmentMediaTypeSchema),
45624
- mode: _enum([
45625
- "url",
45626
- "bytes",
45627
- "both"
45628
- ]),
45629
- max: number().int().nonnegative(),
45630
- maxBytes: number().int().positive().optional()
45631
- }),
45632
- /** Max action buttons (0 = none). */
45633
- actions: number().int().nonnegative(),
45634
- /**
45635
- * Whether this kind renders a per-action ICON.
45636
- *
45637
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45638
- * run on the addon cap path — three production failures in one day taught
45639
- * this repo that once. Absent is read as false by the degrade engine, which
45640
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45641
- * assumed and dropped costs the operator's trust in the field.
45642
- */
45643
- actionIcons: boolean().optional(),
45644
- levels: array(TargetKindLevelSchema),
45645
- format: array(NotificationFormatSchema),
45646
- clickUrl: boolean(),
45647
- sound: boolean(),
45648
- ttl: boolean(),
45649
- bodyMaxLen: number().int().positive()
46010
+ id: string(),
46011
+ name: string(),
46012
+ url: string(),
46013
+ kind: BrokerKindSchema,
46014
+ status: BrokerStatusSchema$1,
46015
+ latencyMs: number().nullable(),
46016
+ error: string().optional(),
46017
+ /** Embedded brokers only: number of MQTT clients currently connected. */
46018
+ connectedClients: number().int().nonnegative().optional(),
46019
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
46020
+ lastCheckedAt: number().optional()
45650
46021
  });
45651
46022
  /**
45652
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45653
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45654
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45655
- * the union is large and not meant for runtime validation here; the exported
45656
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46023
+ * Connection details — what a consumer needs to call
46024
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
46025
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
46026
+ * instead of stuffing creds into the URL (which leaks them into logs).
45657
46027
  */
45658
- var ConfigSchemaPassthrough$2 = unknown();
45659
- var TargetKindSchema = object({
45660
- kind: string(),
45661
- label: string(),
45662
- icon: string(),
45663
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45664
- addonId: string(),
45665
- /**
45666
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45667
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45668
- * when the addon bundles no icon for that kind — the client then falls back
45669
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45670
- *
45671
- * Root-relative on purpose: it resolves against whatever origin serves a web
45672
- * client, and a native client joins it onto its own hub base.
45673
- *
45674
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45675
- * field that survived only because the runtime cap-router forwards provider
45676
- * output verbatim — so every consumer had to re-declare it by hand to stop
45677
- * its own Zod parse from stripping it, and the whole arrangement would have
45678
- * broken silently the moment output validation was tightened anywhere.
45679
- */
45680
- iconUrl: string().optional(),
45681
- /**
45682
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45683
- *
45684
- * The server knows this and therefore says it, because the client cannot
45685
- * safely guess: a React-Native client renders SVG and raster through two
45686
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45687
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45688
- * placeholder glyph for every vector icon while the web build looked fine.
45689
- *
45690
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45691
- * not been updated — a client that cannot determine the type should prefer
45692
- * its raster path, which is the safe default for an unknown image.
45693
- */
45694
- iconMediaType: string().optional(),
45695
- configSchema: ConfigSchemaPassthrough$2,
45696
- /**
45697
- * The config keys that decide WHERE this kind's secrets are sent (server,
45698
- * base URL, instance, chat, recipient). A save that changes any of them
45699
- * while leaving a secret redacted is refused by the provider (D634), so an
45700
- * editor uses this to ask for the secret on exactly those edits instead of
45701
- * on every edit.
45702
- *
45703
- * Optional: absent from a provider that holds no secret (the Home Assistant
45704
- * kinds) and from a provider older than this field — an editor that finds
45705
- * it absent must assume ANY edit may need the secret.
45706
- */
45707
- destinationKeys: array(string()).optional(),
45708
- supportsDiscovery: boolean(),
46028
+ var BrokerConnectionDetailsSchema = object({
46029
+ url: string(),
46030
+ username: string().optional(),
46031
+ password: string().optional(),
45709
46032
  /**
45710
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45711
- * Absent = none; the rule editor then offers nothing.
46033
+ * Suggested prefix for `clientId`. Each consumer should suffix this
46034
+ * with its own discriminator (addon id, instance id) so reconnects
46035
+ * don't kick each other off (MQTT spec: clientId must be unique per
46036
+ * broker).
45712
46037
  */
45713
- deliveryParams: array(DeliveryParamSchema).optional(),
45714
- caps: TargetKindCapsSchema
46038
+ clientIdPrefix: string().optional()
45715
46039
  });
45716
- /**
45717
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45718
- * (return a presence marker only) when serving `listTargets` — never
45719
- * round-trip a stored secret to the UI.
45720
- */
45721
- var TargetSchema = object({
46040
+ var AddBrokerInputSchema = object({
46041
+ name: string().min(1),
46042
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
46043
+ username: string().optional(),
46044
+ password: string().optional(),
46045
+ clientIdPrefix: string().optional()
46046
+ });
46047
+ var AddBrokerResultSchema = object({ id: string() });
46048
+ var IdInputSchema = object({ id: string() });
46049
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
46050
+ ok: literal(true),
46051
+ latencyMs: number()
46052
+ }), object({
46053
+ ok: literal(false),
46054
+ error: string()
46055
+ })]);
46056
+ var StartEmbeddedInputSchema = object({
46057
+ port: number().int().min(1).max(65535).default(1883),
46058
+ /** Allow anonymous connect (no username/password). Default: false. */
46059
+ allowAnonymous: boolean().default(false),
46060
+ /** Optional shared username/password for clients. */
46061
+ username: string().optional(),
46062
+ password: string().optional()
46063
+ });
46064
+ var StartEmbeddedResultSchema = object({
45722
46065
  id: string(),
45723
- name: string(),
45724
- kind: string(),
45725
- addonId: string(),
45726
- enabled: boolean(),
45727
- config: record(string(), unknown())
46066
+ url: string()
45728
46067
  });
45729
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45730
- var DiscoveredTargetSchema = object({
45731
- kind: string(),
45732
- suggestedName: string(),
45733
- config: record(string(), unknown())
46068
+ var StatusSchema = object({
46069
+ brokerCount: number(),
46070
+ embeddedRunning: boolean()
45734
46071
  });
45735
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45736
- var RenderedAsSchema = object({
45737
- level: string(),
45738
- format: NotificationFormatSchema,
45739
- attachmentsSent: number().int().nonnegative(),
45740
- actionsSent: number().int().nonnegative(),
45741
- truncated: boolean(),
45742
- dropped: array(string())
46072
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
46073
+ kind: "mutation",
46074
+ auth: "admin"
46075
+ }), method(IdInputSchema, _void(), {
46076
+ kind: "mutation",
46077
+ auth: "admin"
46078
+ }), method(IdInputSchema, TestResultSchema$1, {
46079
+ kind: "mutation",
46080
+ auth: "admin"
46081
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
46082
+ kind: "mutation",
46083
+ auth: "admin"
46084
+ }), method(IdInputSchema, _void(), {
46085
+ kind: "mutation",
46086
+ auth: "admin"
46087
+ }), method(_void(), StatusSchema);
46088
+ var NetworkEndpointSchema = object({
46089
+ url: string(),
46090
+ hostname: string(),
46091
+ port: number(),
46092
+ protocol: _enum(["http", "https"]),
46093
+ /**
46094
+ * Whether this address survives a restart of its provider. `false` = it is
46095
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
46096
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
46097
+ * it stays visible and an operator may still flag it. Absent = stable: every
46098
+ * provider except a Quick tunnel owns a name that does not change.
46099
+ */
46100
+ stable: boolean().optional()
45743
46101
  });
45744
- var SendResultSchema = object({
45745
- success: boolean(),
45746
- error: string().optional(),
45747
- renderedAs: RenderedAsSchema.optional()
46102
+ object({
46103
+ url: string(),
46104
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
46105
+ stable: boolean().optional(),
46106
+ updated: boolean().optional()
46107
+ });
46108
+ var NetworkAccessStatusSchema = object({
46109
+ connected: boolean(),
46110
+ endpoint: NetworkEndpointSchema.nullable(),
46111
+ error: string().optional()
45748
46112
  });
45749
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45750
- var TestResultSchema = SendResultSchema;
45751
46113
  /**
45752
- * Pre-save credential probe against a partial (unsaved) target config.
45753
- * `resolvedUrl` never includes secrets (token stripped from path).
46114
+ * Optional, richer endpoint shape returned by providers that expose
46115
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
46116
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
46117
+ * the originating provider config (mode + sourcePort) so the
46118
+ * orchestrator UI can label rows distinctly. Providers that expose only
46119
+ * one endpoint just omit `listEndpoints` from their provider impl.
45754
46120
  */
45755
- var ProbeTargetConfigResultSchema = object({
45756
- ok: boolean(),
45757
- resolvedUrl: string(),
45758
- httpStatus: number().int().optional(),
45759
- stateInstance: string().optional(),
45760
- error: string().optional()
46121
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46122
+ /**
46123
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
46124
+ * the orchestrator can dedupe across `listEndpoints` polls.
46125
+ */
46126
+ id: string(),
46127
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
46128
+ label: string(),
46129
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
46130
+ mode: string().optional(),
46131
+ /** Originating local port the ingress fronts (informational). */
46132
+ sourcePort: number().optional()
45761
46133
  });
45762
- var notificationOutputCapability = {
45763
- name: "notification-output",
45764
- scope: "system",
45765
- mode: "collection",
45766
- methods: {
45767
- listTargetKinds: method(object({}), array(TargetKindSchema)),
45768
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
45769
- /**
45770
- * One target WITH secrets — admin edit / duplicate hydration only.
45771
- * `listTargets` stays redacted; the editor loads this so password fields
45772
- * are filled (masked in the input) and a save can round-trip the token
45773
- * when a destination changes (Duplicate with a new chatId, etc.).
45774
- */
45775
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
45776
- discoverTargets: method(object({
45777
- kind: string(),
45778
- config: record(string(), unknown()).optional(),
45779
- /**
45780
- * When editing / duplicating, the blank password field never round-trips
45781
- * the stored token. Pass the source target id so the provider can restore
45782
- * secrets server-side before dialling (same merge as upsert).
45783
- */
45784
- targetId: string().optional()
45785
- }), array(DiscoveredTargetSchema), { auth: "admin" }),
45786
- probeTargetConfig: method(object({
45787
- kind: string(),
45788
- config: record(string(), unknown()),
45789
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45790
- targetId: string().optional()
45791
- }), ProbeTargetConfigResultSchema, {
45792
- kind: "mutation",
45793
- auth: "admin"
45794
- }),
45795
- send: method(object({
45796
- targetId: string(),
45797
- notification: NotificationSchema
45798
- }), SendResultSchema, {
45799
- kind: "mutation",
45800
- auth: "admin"
45801
- }),
45802
- testTarget: method(object({
45803
- targetId: string(),
45804
- sample: NotificationSchema.optional()
45805
- }), TestResultSchema, {
45806
- kind: "mutation",
45807
- auth: "admin"
45808
- }),
45809
- upsertTarget: method(object({
45810
- target: TargetSchema,
45811
- /**
45812
- * Duplicate flow: create a NEW target id while copying secrets from an
45813
- * existing row. The UI never holds the stored token; blank / redacted
45814
- * password fields are restored from this source (D634 still applies if
45815
- * destinations diverge without a re-typed secret).
45816
- */
45817
- cloneSecretsFrom: string().optional()
45818
- }), TargetSchema, {
45819
- kind: "mutation",
45820
- auth: "admin"
45821
- }),
45822
- deleteTarget: method(object({ targetId: string() }), _void(), {
45823
- kind: "mutation",
45824
- auth: "admin"
45825
- }),
45826
- setTargetEnabled: method(object({
45827
- targetId: string(),
45828
- enabled: boolean()
45829
- }), _void(), {
45830
- kind: "mutation",
45831
- auth: "admin"
45832
- })
45833
- }
45834
- };
46134
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45835
46135
  var AUDIO_MACRO_LABELS = [
45836
46136
  {
45837
46137
  id: "speech",
@@ -52292,14 +52592,6 @@ var EngineProvisioningSchema = object({
52292
52592
  */
52293
52593
  configIssues: array(string()).optional()
52294
52594
  });
52295
- var PipelineStepInputSchema = lazy(() => object({
52296
- addonId: string(),
52297
- modelId: string().optional(),
52298
- enabled: boolean().default(true),
52299
- children: array(PipelineStepInputSchema).optional(),
52300
- settings: record(string(), unknown()).optional(),
52301
- jumpDeviceKey: string().optional()
52302
- }));
52303
52595
  var ModelSubstitutionSchema = object({
52304
52596
  addonId: string(),
52305
52597
  chosen: string(),
@@ -52378,7 +52670,6 @@ var ConfigUISchemaBridge = custom();
52378
52670
  var ConfigUISchemaNullableBridge = custom();
52379
52671
  var InferenceCapabilitiesBridge = custom();
52380
52672
  var ModelAvailabilityListBridge = custom();
52381
- var PipelineRunResultBridge = custom();
52382
52673
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
52383
52674
  modelId: string(),
52384
52675
  settings: record(string(), unknown()).readonly()
@@ -55584,6 +55875,12 @@ var ServerUpdateActionResultSchema = object({
55584
55875
  */
55585
55876
  blockers: array(UpdateBlockerSchema).optional()
55586
55877
  });
55878
+ var LaunchOverridesSchema = object({
55879
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
55880
+ heapMb: number().int().nullable() });
55881
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
55882
+ /** Always true: the override is read at launch, not by the running process. */
55883
+ restartRequired: boolean() });
55587
55884
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
55588
55885
  kind: "mutation",
55589
55886
  auth: "admin"
@@ -55606,6 +55903,9 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
55606
55903
  }), method(_void(), ServerUpdateActionResultSchema, {
55607
55904
  kind: "mutation",
55608
55905
  auth: "admin"
55906
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
55907
+ kind: "mutation",
55908
+ auth: "admin"
55609
55909
  });
55610
55910
  /**
55611
55911
  * `smtp-provider` — pluggable email delivery surface.
@@ -60610,14 +60910,13 @@ reset: method(object({
60610
60910
  auth: "admin"
60611
60911
  }) },
60612
60912
  /**
60613
- * A composed consumable (item added to a device with no native consumables
60614
- * provider) refuses `reset` by name: "Only meaningful when the item's
60615
- * `resettable` is true" — a composed item is never resettable until
60616
- * commands (`forward`) arrive in slice 3 (D663).
60913
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
60914
+ * name: only a composed device of a NEW type forwards an item's reset to a
60915
+ * source button, and marks that item `resettable` (D967).
60617
60916
  */
60618
60917
  composedMethods: { reset: {
60619
60918
  kind: "refuse",
60620
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
60919
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
60621
60920
  } },
60622
60921
  status: {
60623
60922
  schema: ConsumablesStatusSchema,
@@ -61211,7 +61510,7 @@ var DishwasherStatusSchema = object({
61211
61510
  /** Ms epoch of the last value received from the appliance; 0 = never. */
61212
61511
  lastFetchedAt: number()
61213
61512
  });
61214
- var DeviceIdSchema = number().int().nonnegative();
61513
+ var DeviceIdSchema$1 = number().int().nonnegative();
61215
61514
  /** A value a program option or setting can take. */
61216
61515
  var DishwasherValueSchema = union([
61217
61516
  boolean(),
@@ -61286,77 +61585,77 @@ var dishwasherCapability = {
61286
61585
  * option feature keys; each is checked against the program's constraints.
61287
61586
  */
61288
61587
  start: method(object({
61289
- deviceId: DeviceIdSchema,
61588
+ deviceId: DeviceIdSchema$1,
61290
61589
  program: string().min(1).optional(),
61291
61590
  options: record(string(), DishwasherValueSchema).optional()
61292
61591
  }), _void(), {
61293
61592
  kind: "mutation",
61294
61593
  auth: "protected"
61295
61594
  }),
61296
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
61595
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
61297
61596
  kind: "mutation",
61298
61597
  auth: "protected"
61299
61598
  }),
61300
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
61599
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
61301
61600
  kind: "mutation",
61302
61601
  auth: "protected"
61303
61602
  }),
61304
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
61603
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
61305
61604
  kind: "mutation",
61306
61605
  auth: "protected"
61307
61606
  }),
61308
61607
  /** Select (not start) a program; refused when the appliance does not offer it. */
61309
61608
  setProgram: method(object({
61310
- deviceId: DeviceIdSchema,
61609
+ deviceId: DeviceIdSchema$1,
61311
61610
  program: string().min(1)
61312
61611
  }), _void(), {
61313
61612
  kind: "mutation",
61314
61613
  auth: "protected"
61315
61614
  }),
61316
61615
  setPower: method(object({
61317
- deviceId: DeviceIdSchema,
61616
+ deviceId: DeviceIdSchema$1,
61318
61617
  on: boolean()
61319
61618
  }), _void(), {
61320
61619
  kind: "mutation",
61321
61620
  auth: "protected"
61322
61621
  }),
61323
61622
  setHalfLoad: method(object({
61324
- deviceId: DeviceIdSchema,
61623
+ deviceId: DeviceIdSchema$1,
61325
61624
  on: boolean()
61326
61625
  }), _void(), {
61327
61626
  kind: "mutation",
61328
61627
  auth: "protected"
61329
61628
  }),
61330
61629
  setHygienePlus: method(object({
61331
- deviceId: DeviceIdSchema,
61630
+ deviceId: DeviceIdSchema$1,
61332
61631
  on: boolean()
61333
61632
  }), _void(), {
61334
61633
  kind: "mutation",
61335
61634
  auth: "protected"
61336
61635
  }),
61337
61636
  setExtraDry: method(object({
61338
- deviceId: DeviceIdSchema,
61637
+ deviceId: DeviceIdSchema$1,
61339
61638
  on: boolean()
61340
61639
  }), _void(), {
61341
61640
  kind: "mutation",
61342
61641
  auth: "protected"
61343
61642
  }),
61344
61643
  setIntensiveZone: method(object({
61345
- deviceId: DeviceIdSchema,
61644
+ deviceId: DeviceIdSchema$1,
61346
61645
  on: boolean()
61347
61646
  }), _void(), {
61348
61647
  kind: "mutation",
61349
61648
  auth: "protected"
61350
61649
  }),
61351
61650
  setVarioSpeedPlus: method(object({
61352
- deviceId: DeviceIdSchema,
61651
+ deviceId: DeviceIdSchema$1,
61353
61652
  on: boolean()
61354
61653
  }), _void(), {
61355
61654
  kind: "mutation",
61356
61655
  auth: "protected"
61357
61656
  }),
61358
61657
  setChildLock: method(object({
61359
- deviceId: DeviceIdSchema,
61658
+ deviceId: DeviceIdSchema$1,
61360
61659
  on: boolean()
61361
61660
  }), _void(), {
61362
61661
  kind: "mutation",
@@ -61368,7 +61667,7 @@ var dishwasherCapability = {
61368
61667
  * its declared range / members.
61369
61668
  */
61370
61669
  setOption: method(object({
61371
- deviceId: DeviceIdSchema,
61670
+ deviceId: DeviceIdSchema$1,
61372
61671
  key: string().min(1),
61373
61672
  value: DishwasherValueSchema
61374
61673
  }), _void(), {
@@ -61376,7 +61675,7 @@ var dishwasherCapability = {
61376
61675
  auth: "protected"
61377
61676
  }),
61378
61677
  /** What this appliance can do, from its own description — the UI is derived from it. */
61379
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
61678
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
61380
61679
  },
61381
61680
  status: {
61382
61681
  schema: DishwasherStatusSchema,
@@ -65077,7 +65376,8 @@ var TopologyNodeSchema = object({
65077
65376
  isHub: boolean(),
65078
65377
  isOnline: boolean(),
65079
65378
  cpuPercent: number(),
65080
- memoryPercent: number(),
65379
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
65380
+ memoryPercent: number().nullable(),
65081
65381
  uptime: number(),
65082
65382
  lastSeen: string(),
65083
65383
  localIps: array(string()).readonly(),
@@ -65695,9 +65995,10 @@ var PetFeederStatusSchema = object({
65695
65995
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
65696
65996
  * the device has not reported yet. */
65697
65997
  foodLevel: number().nullable().meta(readingMeta("none")),
65698
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
65699
- * feeder's own units. Null when the model reports none. */
65700
- bowlFood: number().nullable().meta(readingMeta("none")),
65998
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
65999
+ * feeder with a bowl scale; null when the model has none or its reading is
66000
+ * of an unproven unit. */
66001
+ bowlFood: number().nullable().meta(readingMeta("mass")),
65701
66002
  /** True while the pet is eating (`state.eating`). */
65702
66003
  eating: boolean(),
65703
66004
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -65881,6 +66182,137 @@ var petFeederCapability = {
65881
66182
  }
65882
66183
  } }
65883
66184
  };
66185
+ /**
66186
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
66187
+ * circulating drinking fountain: how much water is in the tank, how much the
66188
+ * pet drank, whether the pump runs and on what duty cycle.
66189
+ *
66190
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
66191
+ * a container of loose entities (a pump switch, two number boxes, a "filter
66192
+ * changed" button), and an operator BUILDS the device from them — a composed
66193
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
66194
+ * method of ONE source entity, and every reading is a field derived from a
66195
+ * source entity; the provider-shaped interface below exists so the composed
66196
+ * device and any future native provider answer the same contract.
66197
+ *
66198
+ * Quantities are stored in the base unit of their kind (D944): volumes in
66199
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
66200
+ * reports millilitres or minutes is converted in the composition's expression,
66201
+ * never here. Every reading may be `null`: a source that is offline or does
66202
+ * not report is unknown, never zero.
66203
+ *
66204
+ * The filter is not a field of this cap: it is an item of the `consumables`
66205
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
66206
+ */
66207
+ var PetFountainStatusSchema = object({
66208
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
66209
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66210
+ /** Water in the tank, in litres. Null while the source has not reported. */
66211
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
66212
+ /** True when the tank is empty. Null while unknown. */
66213
+ empty: boolean().nullable(),
66214
+ /** True while the pump is running. Null while unknown. */
66215
+ pumpRunning: boolean().nullable(),
66216
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
66217
+ pumpIntervalEnabled: boolean().nullable(),
66218
+ /** How long the pump runs per cycle, in seconds. */
66219
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
66220
+ /** How long the pump rests per cycle, in seconds. */
66221
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
66222
+ /** Water drunk in the last drink, in litres. */
66223
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
66224
+ /** Water drunk in the current hour, in litres. */
66225
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
66226
+ /** Water drunk over the last 24 hours (rolling), in litres. */
66227
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
66228
+ /** Water drunk since local midnight, in litres. */
66229
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
66230
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
66231
+ error: string().nullable(),
66232
+ /** Ms epoch when the slice was last refreshed. */
66233
+ lastFetchedAt: number()
66234
+ });
66235
+ var DeviceIdSchema = number().int().nonnegative();
66236
+ var MinutesSchema = number().int().min(0);
66237
+ var petFountainCapability = {
66238
+ name: "pet-fountain",
66239
+ scope: "device",
66240
+ deviceNative: true,
66241
+ mode: "singleton",
66242
+ deviceTypes: [DeviceType.PetFountain],
66243
+ methods: {
66244
+ /** Start or stop the pump. */
66245
+ setPump: method(object({
66246
+ deviceId: DeviceIdSchema,
66247
+ on: boolean()
66248
+ }), _void(), {
66249
+ kind: "mutation",
66250
+ auth: "protected"
66251
+ }),
66252
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
66253
+ setPumpInterval: method(object({
66254
+ deviceId: DeviceIdSchema,
66255
+ on: boolean()
66256
+ }), _void(), {
66257
+ kind: "mutation",
66258
+ auth: "protected"
66259
+ }),
66260
+ /** Set how many minutes the pump runs per cycle. */
66261
+ setPumpOnMinutes: method(object({
66262
+ deviceId: DeviceIdSchema,
66263
+ minutes: MinutesSchema
66264
+ }), _void(), {
66265
+ kind: "mutation",
66266
+ auth: "protected"
66267
+ }),
66268
+ /** Set how many minutes the pump rests per cycle. */
66269
+ setPumpOffMinutes: method(object({
66270
+ deviceId: DeviceIdSchema,
66271
+ minutes: MinutesSchema
66272
+ }), _void(), {
66273
+ kind: "mutation",
66274
+ auth: "protected"
66275
+ }),
66276
+ /** Clear the rolling daily consumption counters on the device. */
66277
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
66278
+ kind: "mutation",
66279
+ auth: "protected"
66280
+ })
66281
+ },
66282
+ status: {
66283
+ schema: PetFountainStatusSchema,
66284
+ kind: "push"
66285
+ },
66286
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
66287
+ runtimeState: PetFountainStatusSchema,
66288
+ /**
66289
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
66290
+ *
66291
+ * See `RuntimeStateDurability`. Enforced by
66292
+ * `scripts/check-runtime-state-durability.ts`.
66293
+ */
66294
+ durability: "session",
66295
+ /** History (D850): recorded series. */
66296
+ history: { fields: {
66297
+ waterLevel: {
66298
+ kind: "gauge",
66299
+ unit: "%",
66300
+ deadband: 1
66301
+ },
66302
+ waterVolume: {
66303
+ kind: "gauge",
66304
+ unit: "L"
66305
+ },
66306
+ todayDrink: {
66307
+ kind: "gauge",
66308
+ unit: "L"
66309
+ },
66310
+ last24hDrink: {
66311
+ kind: "gauge",
66312
+ unit: "L"
66313
+ }
66314
+ } }
66315
+ };
65884
66316
  var VehicleSchema = object({
65885
66317
  id: string(),
65886
66318
  name: string(),
@@ -72246,7 +72678,7 @@ function tryConvertDelta(value, from, to) {
72246
72678
  return shifted - origin;
72247
72679
  }
72248
72680
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
72249
- 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;
72681
+ 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;
72250
72682
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
72251
72683
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
72252
72684
  new Set(CAP_NAMES);
@@ -72597,6 +73029,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
72597
73029
  notifier: notifierCapability,
72598
73030
  numericSensor: numericSensorCapability,
72599
73031
  petFeeder: petFeederCapability,
73032
+ petFountain: petFountainCapability,
72600
73033
  powerMeter: powerMeterCapability,
72601
73034
  presence: presenceCapability,
72602
73035
  pressureSensor: pressureSensorCapability,
@@ -73847,7 +74280,7 @@ var BaseDeviceProvider = class extends BaseAddon {
73847
74280
  return toDeviceSummary(device, this.addonId);
73848
74281
  }
73849
74282
  };
73850
- 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;
74283
+ 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;
73851
74284
  new Set(Object.values(DeviceType));
73852
74285
  Object.freeze({
73853
74286
  "accessories.getStatus": {
@@ -75782,6 +76215,13 @@ Object.freeze({
75782
76215
  access: "create",
75783
76216
  role: "admin"
75784
76217
  },
76218
+ "deviceExport.exposeEntity": {
76219
+ capName: "device-export",
76220
+ capScope: "system",
76221
+ addonId: null,
76222
+ access: "create",
76223
+ role: "admin"
76224
+ },
75785
76225
  "deviceExport.getDeviceLiveContribution": {
75786
76226
  capName: "device-export",
75787
76227
  capScope: "system",
@@ -75810,6 +76250,13 @@ Object.freeze({
75810
76250
  access: "view",
75811
76251
  role: "admin"
75812
76252
  },
76253
+ "deviceExport.listExposedEntities": {
76254
+ capName: "device-export",
76255
+ capScope: "system",
76256
+ addonId: null,
76257
+ access: "view",
76258
+ role: "admin"
76259
+ },
75813
76260
  "deviceExport.listSupportedDeviceKinds": {
75814
76261
  capName: "device-export",
75815
76262
  capScope: "system",
@@ -75817,6 +76264,13 @@ Object.freeze({
75817
76264
  access: "view",
75818
76265
  role: "admin"
75819
76266
  },
76267
+ "deviceExport.listSupportedEntityKinds": {
76268
+ capName: "device-export",
76269
+ capScope: "system",
76270
+ addonId: null,
76271
+ access: "view",
76272
+ role: "admin"
76273
+ },
75820
76274
  "deviceExport.unexposeDevice": {
75821
76275
  capName: "device-export",
75822
76276
  capScope: "system",
@@ -75824,6 +76278,13 @@ Object.freeze({
75824
76278
  access: "create",
75825
76279
  role: "admin"
75826
76280
  },
76281
+ "deviceExport.unexposeEntity": {
76282
+ capName: "device-export",
76283
+ capScope: "system",
76284
+ addonId: null,
76285
+ access: "create",
76286
+ role: "admin"
76287
+ },
75827
76288
  "deviceExtension.listCandidates": {
75828
76289
  capName: "device-extension",
75829
76290
  capScope: "device",
@@ -78645,6 +79106,13 @@ Object.freeze({
78645
79106
  access: "create",
78646
79107
  role: "admin"
78647
79108
  },
79109
+ "nodeRemovalHook.onNodeRemoved": {
79110
+ capName: "node-removal-hook",
79111
+ capScope: "system",
79112
+ addonId: null,
79113
+ access: "create",
79114
+ role: "admin"
79115
+ },
78648
79116
  "nodes.clusterAddonStatus": {
78649
79117
  capName: "nodes",
78650
79118
  capScope: "system",
@@ -79282,6 +79750,48 @@ Object.freeze({
79282
79750
  access: "create",
79283
79751
  role: "viewer"
79284
79752
  },
79753
+ "petFountain.getStatus": {
79754
+ capName: "pet-fountain",
79755
+ capScope: "device",
79756
+ addonId: null,
79757
+ access: "view",
79758
+ role: "viewer"
79759
+ },
79760
+ "petFountain.resetDailyConsumption": {
79761
+ capName: "pet-fountain",
79762
+ capScope: "device",
79763
+ addonId: null,
79764
+ access: "delete",
79765
+ role: "viewer"
79766
+ },
79767
+ "petFountain.setPump": {
79768
+ capName: "pet-fountain",
79769
+ capScope: "device",
79770
+ addonId: null,
79771
+ access: "create",
79772
+ role: "viewer"
79773
+ },
79774
+ "petFountain.setPumpInterval": {
79775
+ capName: "pet-fountain",
79776
+ capScope: "device",
79777
+ addonId: null,
79778
+ access: "create",
79779
+ role: "viewer"
79780
+ },
79781
+ "petFountain.setPumpOffMinutes": {
79782
+ capName: "pet-fountain",
79783
+ capScope: "device",
79784
+ addonId: null,
79785
+ access: "create",
79786
+ role: "viewer"
79787
+ },
79788
+ "petFountain.setPumpOnMinutes": {
79789
+ capName: "pet-fountain",
79790
+ capScope: "device",
79791
+ addonId: null,
79792
+ access: "create",
79793
+ role: "viewer"
79794
+ },
79285
79795
  "pipelineAnalytics.addRetrainFrameFromRecording": {
79286
79796
  capName: "pipeline-analytics",
79287
79797
  capScope: "device",
@@ -81683,6 +82193,13 @@ Object.freeze({
81683
82193
  access: "create",
81684
82194
  role: "admin"
81685
82195
  },
82196
+ "serverManagement.getLaunchOverrides": {
82197
+ capName: "server-management",
82198
+ capScope: "system",
82199
+ addonId: null,
82200
+ access: "view",
82201
+ role: "admin"
82202
+ },
81686
82203
  "serverManagement.getServerPackageStatus": {
81687
82204
  capName: "server-management",
81688
82205
  capScope: "system",
@@ -81704,6 +82221,13 @@ Object.freeze({
81704
82221
  access: "create",
81705
82222
  role: "admin"
81706
82223
  },
82224
+ "serverManagement.setLaunchOverrides": {
82225
+ capName: "server-management",
82226
+ capScope: "system",
82227
+ addonId: null,
82228
+ access: "create",
82229
+ role: "admin"
82230
+ },
81707
82231
  "settingsStore.aggregate": {
81708
82232
  capName: "settings-store",
81709
82233
  capScope: "system",
@@ -85764,6 +86288,100 @@ var APPLIANCE_CAP_VOCABULARY = {
85764
86288
  }
85765
86289
  ]
85766
86290
  },
86291
+ "pet-fountain": {
86292
+ scope: "device",
86293
+ noun: t("Pet fountain", "Fontana per animali"),
86294
+ actions: [
86295
+ {
86296
+ id: "petFountain.pumpOn",
86297
+ method: "setPump",
86298
+ label: t("Pump on", "Pompa accesa"),
86299
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
86300
+ icon: "play",
86301
+ args: { on: true }
86302
+ },
86303
+ {
86304
+ id: "petFountain.pumpOff",
86305
+ method: "setPump",
86306
+ label: t("Pump off", "Pompa spenta"),
86307
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
86308
+ icon: "square",
86309
+ args: { on: false }
86310
+ },
86311
+ {
86312
+ id: "petFountain.intervalOn",
86313
+ method: "setPumpInterval",
86314
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
86315
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
86316
+ icon: "timer",
86317
+ args: { on: true }
86318
+ },
86319
+ {
86320
+ id: "petFountain.intervalOff",
86321
+ method: "setPumpInterval",
86322
+ label: t("Pump cycle off", "Ciclo pompa spento"),
86323
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
86324
+ icon: "timer-off",
86325
+ args: { on: false }
86326
+ },
86327
+ {
86328
+ id: "petFountain.pumpOnMinutes",
86329
+ method: "setPumpOnMinutes",
86330
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
86331
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
86332
+ icon: "timer",
86333
+ openArgs: [{
86334
+ key: "minutes",
86335
+ label: t("Minutes", "Minuti"),
86336
+ offer: {
86337
+ kind: "number",
86338
+ min: 0,
86339
+ step: 1,
86340
+ unit: "min"
86341
+ }
86342
+ }]
86343
+ },
86344
+ {
86345
+ id: "petFountain.pumpOffMinutes",
86346
+ method: "setPumpOffMinutes",
86347
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
86348
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
86349
+ icon: "timer-off",
86350
+ openArgs: [{
86351
+ key: "minutes",
86352
+ label: t("Minutes", "Minuti"),
86353
+ offer: {
86354
+ kind: "number",
86355
+ min: 0,
86356
+ step: 1,
86357
+ unit: "min"
86358
+ }
86359
+ }]
86360
+ },
86361
+ {
86362
+ id: "petFountain.resetDailyConsumption",
86363
+ method: "resetDailyConsumption",
86364
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
86365
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
86366
+ icon: "rotate-ccw"
86367
+ }
86368
+ ],
86369
+ state: [{
86370
+ kind: "boolean",
86371
+ field: "empty",
86372
+ label: t("Tank empty", "Serbatoio vuoto"),
86373
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
86374
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
86375
+ icon: "droplet"
86376
+ }, {
86377
+ kind: "numeric",
86378
+ field: "waterLevel",
86379
+ quantity: t("the water level", "il livello dell’acqua"),
86380
+ label: t("Water level", "Livello dell’acqua"),
86381
+ unit: "%",
86382
+ icon: "droplets"
86383
+ }]
86384
+ },
85767
86385
  "media-player": {
85768
86386
  scope: "device",
85769
86387
  noun: t("Media player", "Lettore multimediale"),
@@ -87588,6 +88206,7 @@ var SYSTEM_CAP_VOCABULARY = {
87588
88206
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
87589
88207
  "device-provider": system(INTERNAL),
87590
88208
  "device-removal-hook": system(INTERNAL),
88209
+ "node-removal-hook": system(INTERNAL),
87591
88210
  "device-state": system("the runtime-state mirror every trigger already reads through"),
87592
88211
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
87593
88212
  "embedding-encoder": system(ML),
@@ -87700,7 +88319,7 @@ function projectAction(cap, action) {
87700
88319
  };
87701
88320
  }
87702
88321
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
87703
- 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;
88322
+ 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;
87704
88323
  DeviceFeature.BatteryOperated;
87705
88324
  /**
87706
88325
  * How a washer, dryer or dishwasher leaves CamStack.
@@ -89262,6 +89881,36 @@ Object.freeze({
89262
89881
  form: "single",
89263
89882
  optional: false
89264
89883
  }],
89884
+ "petFountain.getStatus": [{
89885
+ name: "deviceId",
89886
+ form: "single",
89887
+ optional: false
89888
+ }],
89889
+ "petFountain.resetDailyConsumption": [{
89890
+ name: "deviceId",
89891
+ form: "single",
89892
+ optional: false
89893
+ }],
89894
+ "petFountain.setPump": [{
89895
+ name: "deviceId",
89896
+ form: "single",
89897
+ optional: false
89898
+ }],
89899
+ "petFountain.setPumpInterval": [{
89900
+ name: "deviceId",
89901
+ form: "single",
89902
+ optional: false
89903
+ }],
89904
+ "petFountain.setPumpOffMinutes": [{
89905
+ name: "deviceId",
89906
+ form: "single",
89907
+ optional: false
89908
+ }],
89909
+ "petFountain.setPumpOnMinutes": [{
89910
+ name: "deviceId",
89911
+ form: "single",
89912
+ optional: false
89913
+ }],
89265
89914
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
89266
89915
  name: "deviceId",
89267
89916
  form: "single",
@@ -92791,6 +93440,12 @@ Object.defineProperty(exports, "pressureSensorCapability", {
92791
93440
  return pressureSensorCapability;
92792
93441
  }
92793
93442
  });
93443
+ Object.defineProperty(exports, "pruneDeletedEntities", {
93444
+ enumerable: true,
93445
+ get: function() {
93446
+ return pruneDeletedEntities;
93447
+ }
93448
+ });
92794
93449
  Object.defineProperty(exports, "quantityFromUnit", {
92795
93450
  enumerable: true,
92796
93451
  get: function() {
@@ -92803,6 +93458,12 @@ Object.defineProperty(exports, "record", {
92803
93458
  return record;
92804
93459
  }
92805
93460
  });
93461
+ Object.defineProperty(exports, "runExportedAction", {
93462
+ enumerable: true,
93463
+ get: function() {
93464
+ return runExportedAction;
93465
+ }
93466
+ });
92806
93467
  Object.defineProperty(exports, "runningFromLawnMower", {
92807
93468
  enumerable: true,
92808
93469
  get: function() {
@@ -92833,6 +93494,12 @@ Object.defineProperty(exports, "scriptRunnerCapability", {
92833
93494
  return scriptRunnerCapability;
92834
93495
  }
92835
93496
  });
93497
+ Object.defineProperty(exports, "sendToExportedTarget", {
93498
+ enumerable: true,
93499
+ get: function() {
93500
+ return sendToExportedTarget;
93501
+ }
93502
+ });
92836
93503
  Object.defineProperty(exports, "smokeCapability", {
92837
93504
  enumerable: true,
92838
93505
  get: function() {