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