@camstack/addon-post-analysis 1.2.651 → 1.2.653

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (16) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{clip-model-registry-BWY8P-AK.mjs → clip-model-registry-BrazZJvr.mjs} +2000 -720
  3. package/dist/{clip-model-registry-Dte3QQEt.js → clip-model-registry-Bz1uQvQ8.js} +2000 -720
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_vVfXkDx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D6DnF6Vk.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BKMr4IqL.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CtNyIYsM.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-A7m5uu3n.mjs → hostInit-DtrcY1yz.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +145 -8
  12. package/dist/pipeline-analytics/index.mjs +145 -8
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +1 -1
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-6PIugMbv.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bk6VlAsZ.mjs +0 -26
@@ -5645,7 +5645,7 @@ var ZodIssueCode = {
5645
5645
  var ZodFirstPartyTypeKind;
5646
5646
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5647
5647
  //#endregion
5648
- //#region ../types/dist/sleep-C_u6cEAJ.mjs
5648
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5649
5649
  /**
5650
5650
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5651
5651
  * window to float samples (D455).
@@ -8746,65 +8746,6 @@ var MODEL_FORMATS = [
8746
8746
  "pt",
8747
8747
  "gguf"
8748
8748
  ];
8749
- /**
8750
- * Per-call node pinning for `ctx.api` capability calls.
8751
- *
8752
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8753
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8754
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8755
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8756
- * call to that node.
8757
- *
8758
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8759
- * method args), so capability method signatures stay `nodeId`-free — node
8760
- * targeting is a property of the CALL, not of the method. The transport lifts
8761
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8762
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8763
- * which classifies a pinned agent node as `agent-child-forward`
8764
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8765
- *
8766
- * Usage at a call site:
8767
- *
8768
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8769
- */
8770
- /** tRPC `op.context` key carrying a per-call node pin. */
8771
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8772
- /**
8773
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8774
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8775
- *
8776
- * ## The id is normalised here, and it has to be
8777
- *
8778
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8779
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8780
- * compares a pin against real node ids, so such a pin matches nothing and the
8781
- * call fails with `no provider registered for cap "…"`. The local-first
8782
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8783
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8784
- * the thing you reach for when you specifically need THIS node — silently
8785
- * addressed a node that does not exist.
8786
- *
8787
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8788
- * own node to read the host's hardware-decode backend. It never once answered,
8789
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8790
- * while D67's whole premise was that the decoder addon is the authority on
8791
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8792
- * hardware", which was false.
8793
- *
8794
- * Normalising in the ONE constructor fixes every caller at once, which is why
8795
- * it is here and not at the call sites.
8796
- */
8797
- function nodePin(nodeId) {
8798
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8799
- }
8800
- /**
8801
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8802
- * is idempotent, so passing an already-clean id costs nothing.
8803
- */
8804
- function toNodeId(idOrRunnerId) {
8805
- const head = idOrRunnerId.split("/")[0];
8806
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8807
- }
8808
8749
  object({
8809
8750
  field: string().min(1),
8810
8751
  on: literal("advance")
@@ -8897,6 +8838,111 @@ function optionalMethod(input, output, options) {
8897
8838
  function event(data) {
8898
8839
  return { data };
8899
8840
  }
8841
+ /**
8842
+ * Per-call node pinning for `ctx.api` capability calls.
8843
+ *
8844
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8845
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8846
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8847
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8848
+ * call to that node.
8849
+ *
8850
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8851
+ * method args), so capability method signatures stay `nodeId`-free — node
8852
+ * targeting is a property of the CALL, not of the method. The transport lifts
8853
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8854
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8855
+ * which classifies a pinned agent node as `agent-child-forward`
8856
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8857
+ *
8858
+ * Usage at a call site:
8859
+ *
8860
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8861
+ */
8862
+ /** tRPC `op.context` key carrying a per-call node pin. */
8863
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8864
+ /**
8865
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8866
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8867
+ *
8868
+ * ## The id is normalised here, and it has to be
8869
+ *
8870
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8871
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8872
+ * compares a pin against real node ids, so such a pin matches nothing and the
8873
+ * call fails with `no provider registered for cap "…"`. The local-first
8874
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8875
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8876
+ * the thing you reach for when you specifically need THIS node — silently
8877
+ * addressed a node that does not exist.
8878
+ *
8879
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8880
+ * own node to read the host's hardware-decode backend. It never once answered,
8881
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8882
+ * while D67's whole premise was that the decoder addon is the authority on
8883
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8884
+ * hardware", which was false.
8885
+ *
8886
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8887
+ * it is here and not at the call sites.
8888
+ */
8889
+ function nodePin(nodeId) {
8890
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8891
+ }
8892
+ /**
8893
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8894
+ * is idempotent, so passing an already-clean id costs nothing.
8895
+ */
8896
+ function toNodeId(idOrRunnerId) {
8897
+ const head = idOrRunnerId.split("/")[0];
8898
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8899
+ }
8900
+ /**
8901
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8902
+ * declarations. Every addon that contributes a UI page registers a
8903
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8904
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8905
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8906
+ * that admin-ui consumes.
8907
+ *
8908
+ * The split exists because the public listing has a different output
8909
+ * shape than the per-provider raw declarations, and we want both ends
8910
+ * to flow through codegen instead of relying on a hand-written wrapper.
8911
+ */
8912
+ var AddonPageDeclarationSchema = object({
8913
+ id: string(),
8914
+ label: string(),
8915
+ icon: string(),
8916
+ path: string(),
8917
+ /**
8918
+ * Module Federation remote name — must match the `name` field on the
8919
+ * page addon's `federation()` plugin config. Used by admin-ui's
8920
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8921
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8922
+ * contain hyphens).
8923
+ */
8924
+ remoteName: string(),
8925
+ /**
8926
+ * Bundle filename inside the addon's `dist/` dir served at
8927
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8928
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8929
+ * the static-file route can compute an mtime-based cache-buster URL
8930
+ * without a separate filesystem stat.
8931
+ */
8932
+ bundle: string(),
8933
+ /**
8934
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8935
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8936
+ * Any OTHER string creates (or joins) a custom section rendered after
8937
+ * the built-in groups; its label comes from `sectionLabel` (first
8938
+ * declaration wins), falling back to the id. Absent → the legacy
8939
+ * "Addon Pages" group.
8940
+ */
8941
+ section: string().optional(),
8942
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8943
+ sectionLabel: string().optional()
8944
+ });
8945
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8900
8946
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8901
8947
  var VersionOutputSchema$1 = object({ version: string() });
8902
8948
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -8922,6 +8968,27 @@ function customAction(input, output, options) {
8922
8968
  ...options?.caller ? { caller: "required" } : {}
8923
8969
  };
8924
8970
  }
8971
+ /**
8972
+ * The pipeline step-input shape and the run-result bridge, in a module that
8973
+ * imports nothing but zod and types.
8974
+ *
8975
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8976
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8977
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8978
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8979
+ * them, so every existing import keeps working; addon server code takes them
8980
+ * from `@camstack/types/addon`.
8981
+ */
8982
+ var PipelineStepInputSchema = lazy(() => object({
8983
+ addonId: string(),
8984
+ modelId: string().optional(),
8985
+ enabled: boolean().default(true),
8986
+ children: array(PipelineStepInputSchema).optional(),
8987
+ settings: record(string(), unknown()).optional(),
8988
+ jumpDeviceKey: string().optional()
8989
+ }));
8990
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8991
+ var PipelineRunResultBridge = custom();
8925
8992
  var StaticDirOutputSchema = object({ staticDir: string() });
8926
8993
  var VersionOutputSchema = object({ version: string() });
8927
8994
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9118,6 +9185,7 @@ var CAP_NAMES = Object.freeze([
9118
9185
  "osd",
9119
9186
  "osd-manager",
9120
9187
  "pet-feeder",
9188
+ "pet-fountain",
9121
9189
  "pipeline-analytics",
9122
9190
  "pipeline-executor",
9123
9191
  "pipeline-orchestrator",
@@ -9139,6 +9207,7 @@ var CAP_NAMES = Object.freeze([
9139
9207
  "recording-signal",
9140
9208
  "records",
9141
9209
  "remote",
9210
+ "remote-layout-components",
9142
9211
  "running-state",
9143
9212
  "scene-monitor",
9144
9213
  "script-runner",
@@ -10397,6 +10466,56 @@ Object.freeze({
10397
10466
  "valueType": "number"
10398
10467
  }
10399
10468
  ],
10469
+ "pet-fountain": [
10470
+ {
10471
+ "path": "waterLevel",
10472
+ "valueType": "number"
10473
+ },
10474
+ {
10475
+ "path": "waterVolume",
10476
+ "valueType": "number"
10477
+ },
10478
+ {
10479
+ "path": "empty",
10480
+ "valueType": "boolean"
10481
+ },
10482
+ {
10483
+ "path": "pumpRunning",
10484
+ "valueType": "boolean"
10485
+ },
10486
+ {
10487
+ "path": "pumpIntervalEnabled",
10488
+ "valueType": "boolean"
10489
+ },
10490
+ {
10491
+ "path": "pumpOnTime",
10492
+ "valueType": "number"
10493
+ },
10494
+ {
10495
+ "path": "pumpOffTime",
10496
+ "valueType": "number"
10497
+ },
10498
+ {
10499
+ "path": "lastDrink",
10500
+ "valueType": "number"
10501
+ },
10502
+ {
10503
+ "path": "currentHourDrink",
10504
+ "valueType": "number"
10505
+ },
10506
+ {
10507
+ "path": "last24hDrink",
10508
+ "valueType": "number"
10509
+ },
10510
+ {
10511
+ "path": "todayDrink",
10512
+ "valueType": "number"
10513
+ },
10514
+ {
10515
+ "path": "error",
10516
+ "valueType": "string"
10517
+ }
10518
+ ],
10400
10519
  "power-meter": [
10401
10520
  {
10402
10521
  "path": "power",
@@ -10936,7 +11055,7 @@ Object.freeze({
10936
11055
  * can answer "may a rule actuate this?" without importing the schema barrel
10937
11056
  * (~144MB RSS per runner, D28).
10938
11057
  *
10939
- * Coverage: 96 device-scoped capabilities.
11058
+ * Coverage: 97 device-scoped capabilities.
10940
11059
  */
10941
11060
  var DEVICE_SCOPED_CAPS = new Set([
10942
11061
  "accessories",
@@ -11002,6 +11121,7 @@ var DEVICE_SCOPED_CAPS = new Set([
11002
11121
  "numeric-sensor",
11003
11122
  "osd",
11004
11123
  "pet-feeder",
11124
+ "pet-fountain",
11005
11125
  "pipeline-analytics",
11006
11126
  "power-meter",
11007
11127
  "presence",
@@ -11080,7 +11200,7 @@ function sleep(ms) {
11080
11200
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11081
11201
  }
11082
11202
  //#endregion
11083
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11203
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
11084
11204
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11085
11205
  DeviceType["Camera"] = "camera";
11086
11206
  DeviceType["Hub"] = "hub";
@@ -11172,6 +11292,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11172
11292
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11173
11293
  * integrations sharing the same food/desiccant/hopper surface. */
11174
11294
  DeviceType["PetFeeder"] = "pet-feeder";
11295
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11296
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11297
+ * of a container and is BUILT as a composed device (D967). */
11298
+ DeviceType["PetFountain"] = "pet-fountain";
11175
11299
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11176
11300
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11177
11301
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11442,7 +11566,7 @@ function errMsg(err) {
11442
11566
  return String(err);
11443
11567
  }
11444
11568
  //#endregion
11445
- //#region ../types/dist/composition-bIBW5_13.mjs
11569
+ //#region ../types/dist/composition-kSuOifuG.mjs
11446
11570
  /**
11447
11571
  * Deterministic SHA-256 hash of an arbitrary serialisable value. The
11448
11572
  * canonical form sorts object keys alphabetically at every depth so two
@@ -12687,12 +12811,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12687
12811
  ])
12688
12812
  })
12689
12813
  ]);
12690
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12814
+ var forwardArgUnion = union([
12815
+ string(),
12816
+ number(),
12817
+ boolean(),
12818
+ strictObject({ ["$input"]: string().min(1) }),
12819
+ record(string().min(1), lazy(() => forwardArgUnion))
12820
+ ]);
12821
+ var CompositionForwardArgSchema = forwardArgUnion;
12822
+ /**
12823
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12824
+ * call of `cap.method` on `source`. Without `args` the built method's input
12825
+ * passes through minus `deviceId`; with `args` the template IS the source's
12826
+ * whole input. `deviceId` is always the source's own, never an argument.
12827
+ */
12828
+ var CompositionForwardCommandSchema = object({
12691
12829
  kind: literal("forward"),
12692
12830
  source: CompositionSourceRefSchema,
12693
12831
  cap: string().min(1),
12694
- method: string().min(1)
12695
- }), CompositionCodeSourceSchema]);
12832
+ method: string().min(1),
12833
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12834
+ });
12835
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12696
12836
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12697
12837
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12698
12838
  /**
@@ -12702,7 +12842,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12702
12842
  */
12703
12843
  var CompositionItemEntrySchema = object({
12704
12844
  label: string().min(1).max(80),
12705
- fields: record(string().min(1), CompositionFieldSourceSchema)
12845
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12846
+ /**
12847
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12848
+ * is resettable and a `reset` call naming its key forwards here. The item key
12849
+ * selects the command, so it is never an argument: there is no pass-through.
12850
+ */
12851
+ reset: CompositionForwardCommandSchema.optional()
12706
12852
  });
12707
12853
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12708
12854
  kind: literal("fields"),
@@ -12714,7 +12860,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12714
12860
  * owns the WHOLE array field: the composed value holds exactly these items.
12715
12861
  */
12716
12862
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12717
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12863
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12718
12864
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12719
12865
  }), object({
12720
12866
  kind: literal("passthrough"),
@@ -16857,6 +17003,538 @@ object({
16857
17003
  path: ["expr"]
16858
17004
  });
16859
17005
  });
17006
+ /**
17007
+ * Identity envelope for a device's upstream-system metadata.
17008
+ *
17009
+ * Two jobs:
17010
+ *
17011
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
17012
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
17013
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
17014
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
17015
+ * without round-tripping through `stableId` / `id` mappings.
17016
+ *
17017
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
17018
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
17019
+ * for vendors where MAC is the natural key). For systems where the
17020
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
17021
+ * providers detect renames by matching `uniqueId` and patch `id`
17022
+ * in-place so dispatch keeps working.
17023
+ *
17024
+ * **Display unit and precision are NOT stored here.** They are a read-time
17025
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
17026
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
17027
+ * neither is repeated on each sample. The `device_class` tag is an
17028
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
17029
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
17030
+ *
17031
+ * The `raw` escape hatch carries provider-specific attributes that don't
17032
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
17033
+ * details, …). Providers control what lands here.
17034
+ */
17035
+ var SourceInfoSchema = object({
17036
+ /** Live dispatch key — mutable when the source system allows rename. */
17037
+ id: string(),
17038
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
17039
+ system: string(),
17040
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
17041
+ * Used to detect rename when `id` changes. */
17042
+ uniqueId: string().optional(),
17043
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
17044
+ quantity: ReadingQuantitySchema.optional(),
17045
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
17046
+ dateTimeShape: _enum([
17047
+ "date",
17048
+ "time",
17049
+ "datetime"
17050
+ ]).optional(),
17051
+ /** Provider-specific extras that don't fit the structured slots.
17052
+ * Whitelist what goes here per provider — do NOT dump entire HA
17053
+ * attribute blobs (would explode the meta JSON column). */
17054
+ raw: record(string(), unknown()).optional()
17055
+ });
17056
+ /**
17057
+ * Build the synthetic SourceInfo every existing provider falls back to
17058
+ * when no upstream value has been persisted. Keeps non-migrated providers
17059
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
17060
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
17061
+ * id. Real upstream identity replaces this once a provider migrates and
17062
+ * calls `updateSourceInfo()` with concrete values.
17063
+ */
17064
+ function synthesizeSourceInfo(input) {
17065
+ return {
17066
+ id: input.stableId,
17067
+ system: input.addonId
17068
+ };
17069
+ }
17070
+ /**
17071
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
17072
+ * patch values are ignored (no field clobber); to clear a field
17073
+ * explicitly, providers omit it from the patch and rebuild from scratch
17074
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
17075
+ * bother with a `null`-means-delete sentinel.
17076
+ *
17077
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
17078
+ * merges over existing `raw` keys instead of replacing the whole bag.
17079
+ */
17080
+ function mergeSourceInfo(prev, patch) {
17081
+ const next = { ...prev };
17082
+ for (const [k, v] of Object.entries(patch)) {
17083
+ if (v === void 0) continue;
17084
+ if (k === "raw" && typeof v === "object" && v !== null) {
17085
+ next.raw = {
17086
+ ...prev.raw,
17087
+ ...v
17088
+ };
17089
+ continue;
17090
+ }
17091
+ next[k] = v;
17092
+ }
17093
+ return next;
17094
+ }
17095
+ /**
17096
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
17097
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
17098
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
17099
+ * lives under a single nested key to keep it composable with those.
17100
+ */
17101
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
17102
+ /**
17103
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
17104
+ * with Zod. Returns `null` when the blob is missing or the nested
17105
+ * `sourceInfo` key fails validation — callers fall back to the
17106
+ * synthetic default in that case (see `synthesizeSourceInfo`).
17107
+ */
17108
+ function extractSourceInfoFromMetadata(metadata) {
17109
+ if (!metadata) return null;
17110
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
17111
+ if (!raw) return null;
17112
+ const parsed = SourceInfoSchema.safeParse(raw);
17113
+ if (!parsed.success) return null;
17114
+ return parsed.data;
17115
+ }
17116
+ /**
17117
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
17118
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
17119
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
17120
+ * decimals). K derives from `°C = K − 273.15`.
17121
+ */
17122
+ var UNIT_TABLE = {
17123
+ "°C": {
17124
+ dimension: "temperature",
17125
+ factor: 1,
17126
+ offset: 0
17127
+ },
17128
+ "°F": {
17129
+ dimension: "temperature",
17130
+ factor: 5 / 9,
17131
+ offset: -160 / 9
17132
+ },
17133
+ K: {
17134
+ dimension: "temperature",
17135
+ factor: 1,
17136
+ offset: -273.15
17137
+ },
17138
+ hPa: {
17139
+ dimension: "pressure",
17140
+ factor: 1,
17141
+ offset: 0
17142
+ },
17143
+ kPa: {
17144
+ dimension: "pressure",
17145
+ factor: 10,
17146
+ offset: 0
17147
+ },
17148
+ Pa: {
17149
+ dimension: "pressure",
17150
+ factor: .01,
17151
+ offset: 0
17152
+ },
17153
+ mbar: {
17154
+ dimension: "pressure",
17155
+ factor: 1,
17156
+ offset: 0
17157
+ },
17158
+ bar: {
17159
+ dimension: "pressure",
17160
+ factor: 1e3,
17161
+ offset: 0
17162
+ },
17163
+ inHg: {
17164
+ dimension: "pressure",
17165
+ factor: 33.8639,
17166
+ offset: 0
17167
+ },
17168
+ mmHg: {
17169
+ dimension: "pressure",
17170
+ factor: 1.33322,
17171
+ offset: 0
17172
+ },
17173
+ psi: {
17174
+ dimension: "pressure",
17175
+ factor: 68.9476,
17176
+ offset: 0
17177
+ },
17178
+ "m/s": {
17179
+ dimension: "speed",
17180
+ factor: 1,
17181
+ offset: 0
17182
+ },
17183
+ "km/h": {
17184
+ dimension: "speed",
17185
+ factor: 1 / 3.6,
17186
+ offset: 0
17187
+ },
17188
+ mph: {
17189
+ dimension: "speed",
17190
+ factor: .44704,
17191
+ offset: 0
17192
+ },
17193
+ kn: {
17194
+ dimension: "speed",
17195
+ factor: .514444,
17196
+ offset: 0
17197
+ },
17198
+ "mm/h": {
17199
+ dimension: "precipitation-rate",
17200
+ factor: 1,
17201
+ offset: 0
17202
+ },
17203
+ "in/h": {
17204
+ dimension: "precipitation-rate",
17205
+ factor: 25.4,
17206
+ offset: 0
17207
+ },
17208
+ m: {
17209
+ dimension: "length",
17210
+ factor: 1,
17211
+ offset: 0
17212
+ },
17213
+ mm: {
17214
+ dimension: "length",
17215
+ factor: .001,
17216
+ offset: 0
17217
+ },
17218
+ cm: {
17219
+ dimension: "length",
17220
+ factor: .01,
17221
+ offset: 0
17222
+ },
17223
+ km: {
17224
+ dimension: "length",
17225
+ factor: 1e3,
17226
+ offset: 0
17227
+ },
17228
+ in: {
17229
+ dimension: "length",
17230
+ factor: .0254,
17231
+ offset: 0
17232
+ },
17233
+ ft: {
17234
+ dimension: "length",
17235
+ factor: .3048,
17236
+ offset: 0
17237
+ },
17238
+ mi: {
17239
+ dimension: "length",
17240
+ factor: 1609.344,
17241
+ offset: 0
17242
+ },
17243
+ "m²": {
17244
+ dimension: "area",
17245
+ factor: 1,
17246
+ offset: 0
17247
+ },
17248
+ lx: {
17249
+ dimension: "illuminance",
17250
+ factor: 1,
17251
+ offset: 0
17252
+ },
17253
+ "W/m²": {
17254
+ dimension: "irradiance",
17255
+ factor: 1,
17256
+ offset: 0
17257
+ },
17258
+ W: {
17259
+ dimension: "power",
17260
+ factor: 1,
17261
+ offset: 0
17262
+ },
17263
+ kW: {
17264
+ dimension: "power",
17265
+ factor: 1e3,
17266
+ offset: 0
17267
+ },
17268
+ VA: {
17269
+ dimension: "apparent-power",
17270
+ factor: 1,
17271
+ offset: 0
17272
+ },
17273
+ Wh: {
17274
+ dimension: "energy",
17275
+ factor: 1,
17276
+ offset: 0
17277
+ },
17278
+ kWh: {
17279
+ dimension: "energy",
17280
+ factor: 1e3,
17281
+ offset: 0
17282
+ },
17283
+ MWh: {
17284
+ dimension: "energy",
17285
+ factor: 1e6,
17286
+ offset: 0
17287
+ },
17288
+ V: {
17289
+ dimension: "voltage",
17290
+ factor: 1,
17291
+ offset: 0
17292
+ },
17293
+ mV: {
17294
+ dimension: "voltage",
17295
+ factor: .001,
17296
+ offset: 0
17297
+ },
17298
+ A: {
17299
+ dimension: "current",
17300
+ factor: 1,
17301
+ offset: 0
17302
+ },
17303
+ mA: {
17304
+ dimension: "current",
17305
+ factor: .001,
17306
+ offset: 0
17307
+ },
17308
+ ppm: {
17309
+ dimension: "concentration-volume",
17310
+ factor: 1,
17311
+ offset: 0
17312
+ },
17313
+ ppb: {
17314
+ dimension: "concentration-volume",
17315
+ factor: .001,
17316
+ offset: 0
17317
+ },
17318
+ "µg/m³": {
17319
+ dimension: "concentration-mass",
17320
+ factor: 1,
17321
+ offset: 0
17322
+ },
17323
+ "mg/m³": {
17324
+ dimension: "concentration-mass",
17325
+ factor: 1e3,
17326
+ offset: 0
17327
+ },
17328
+ kg: {
17329
+ dimension: "mass",
17330
+ factor: 1,
17331
+ offset: 0
17332
+ },
17333
+ g: {
17334
+ dimension: "mass",
17335
+ factor: .001,
17336
+ offset: 0
17337
+ },
17338
+ lb: {
17339
+ dimension: "mass",
17340
+ factor: .453592,
17341
+ offset: 0
17342
+ },
17343
+ oz: {
17344
+ dimension: "mass",
17345
+ factor: .0283495,
17346
+ offset: 0
17347
+ },
17348
+ "%": {
17349
+ dimension: "percentage",
17350
+ factor: 1,
17351
+ offset: 0
17352
+ },
17353
+ L: {
17354
+ dimension: "volume",
17355
+ factor: 1,
17356
+ offset: 0
17357
+ },
17358
+ mL: {
17359
+ dimension: "volume",
17360
+ factor: .001,
17361
+ offset: 0
17362
+ },
17363
+ gal: {
17364
+ dimension: "volume",
17365
+ factor: 3.785411784,
17366
+ offset: 0
17367
+ },
17368
+ s: {
17369
+ dimension: "duration",
17370
+ factor: 1,
17371
+ offset: 0
17372
+ },
17373
+ ms: {
17374
+ dimension: "duration",
17375
+ factor: .001,
17376
+ offset: 0
17377
+ },
17378
+ min: {
17379
+ dimension: "duration",
17380
+ factor: 60,
17381
+ offset: 0
17382
+ },
17383
+ h: {
17384
+ dimension: "duration",
17385
+ factor: 3600,
17386
+ offset: 0
17387
+ },
17388
+ d: {
17389
+ dimension: "duration",
17390
+ factor: 86400,
17391
+ offset: 0
17392
+ },
17393
+ B: {
17394
+ dimension: "information",
17395
+ factor: 1,
17396
+ offset: 0
17397
+ },
17398
+ KiB: {
17399
+ dimension: "information",
17400
+ factor: 1024,
17401
+ offset: 0
17402
+ },
17403
+ MiB: {
17404
+ dimension: "information",
17405
+ factor: 1024 ** 2,
17406
+ offset: 0
17407
+ },
17408
+ GiB: {
17409
+ dimension: "information",
17410
+ factor: 1024 ** 3,
17411
+ offset: 0
17412
+ },
17413
+ TiB: {
17414
+ dimension: "information",
17415
+ factor: 1024 ** 4,
17416
+ offset: 0
17417
+ },
17418
+ kB: {
17419
+ dimension: "information",
17420
+ factor: 1e3,
17421
+ offset: 0
17422
+ },
17423
+ MB: {
17424
+ dimension: "information",
17425
+ factor: 1e6,
17426
+ offset: 0
17427
+ },
17428
+ GB: {
17429
+ dimension: "information",
17430
+ factor: 1e9,
17431
+ offset: 0
17432
+ },
17433
+ "°": {
17434
+ dimension: "angle",
17435
+ factor: 1,
17436
+ offset: 0
17437
+ },
17438
+ dBFS: {
17439
+ dimension: "sound-level",
17440
+ factor: 1,
17441
+ offset: 0
17442
+ },
17443
+ dBm: {
17444
+ dimension: "signal-level",
17445
+ factor: 1,
17446
+ offset: 0
17447
+ },
17448
+ mired: {
17449
+ dimension: "colour-temperature",
17450
+ factor: 1,
17451
+ offset: 0
17452
+ },
17453
+ kbps: {
17454
+ dimension: "bitrate",
17455
+ factor: 1,
17456
+ offset: 0
17457
+ },
17458
+ Mbps: {
17459
+ dimension: "bitrate",
17460
+ factor: 1e3,
17461
+ offset: 0
17462
+ },
17463
+ fps: {
17464
+ dimension: "rate",
17465
+ factor: 1,
17466
+ offset: 0
17467
+ }
17468
+ };
17469
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
17470
+ function specFor(unit) {
17471
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
17472
+ }
17473
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
17474
+ function toCanonical(value, spec) {
17475
+ return value * spec.factor + spec.offset;
17476
+ }
17477
+ /** Canonical-unit value → the unit described by `spec`. */
17478
+ function fromCanonical(canonical, spec) {
17479
+ return (canonical - spec.offset) / spec.factor;
17480
+ }
17481
+ /**
17482
+ * Graceful variant for the render path: returns the converted value, or
17483
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
17484
+ * Never throws — a render caller falls back to the raw value + source unit.
17485
+ */
17486
+ function tryConvertUnit(value, from, to) {
17487
+ if (from === to) return value;
17488
+ const fromSpec = specFor(from);
17489
+ if (!fromSpec) return void 0;
17490
+ const toSpec = specFor(to);
17491
+ if (!toSpec) return void 0;
17492
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
17493
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
17494
+ }
17495
+ /**
17496
+ * A number a person types for a physical quantity — the input side of the
17497
+ * value + unit contract (D972).
17498
+ *
17499
+ * Readings are stored in the base unit of their quantity (D944); a cap method
17500
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
17501
+ * wire pair {@link QuantityValue} says "this number is expressed in this
17502
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
17503
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
17504
+ */
17505
+ /** A unit that cannot be applied to the number it was given with. */
17506
+ var QuantityUnitError = class extends Error {
17507
+ code;
17508
+ constructor(code, message) {
17509
+ super(`[${code}] ${message}`);
17510
+ this.name = "QuantityUnitError";
17511
+ this.code = code;
17512
+ }
17513
+ };
17514
+ /** The wire pair: `value` is expressed in `unit`. */
17515
+ var QuantityValueSchema = object({
17516
+ value: number(),
17517
+ unit: string().min(1)
17518
+ });
17519
+ /**
17520
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
17521
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
17522
+ * portion stays in grams); the unit must belong to the quantity, checked once
17523
+ * when the cap is declared.
17524
+ */
17525
+ function paramMeta(declared) {
17526
+ const base = baseUnitOf(declared.quantity);
17527
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
17528
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
17529
+ return {
17530
+ quantity: declared.quantity,
17531
+ unit: declared.unit
17532
+ };
17533
+ }
17534
+ object({
17535
+ quantity: ReadingQuantitySchema,
17536
+ unit: string().min(1)
17537
+ });
16860
17538
  /** How a leaf compares a device field to a value. Derived from the field's
16861
17539
  * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16862
17540
  var AutomationConditionOperatorSchema = _enum([
@@ -16879,8 +17557,10 @@ var AutomationConditionLeafSchema = object({
16879
17557
  string(),
16880
17558
  number(),
16881
17559
  boolean(),
16882
- array(union([string(), number()]))
16883
- ])
17560
+ array(union([string(), number()])),
17561
+ QuantityValueSchema
17562
+ ]),
17563
+ unit: string().min(1).optional()
16884
17564
  });
16885
17565
  var AutomationConditionStationaryPresentSchema = object({
16886
17566
  kind: literal("stationary-present"),
@@ -17033,7 +17713,13 @@ var AutomationCapStepSchema = object({
17033
17713
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
17034
17714
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
17035
17715
  * the expression leaf. */
17036
- args: record(string(), unknown()).optional()
17716
+ args: record(string(), unknown()).optional(),
17717
+ /**
17718
+ * The unit the person picked per arg (display only). `args` always hold the
17719
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
17720
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
17721
+ */
17722
+ argUnits: record(string(), string().min(1)).optional()
17037
17723
  });
17038
17724
  /**
17039
17725
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -17435,6 +18121,437 @@ object({
17435
18121
  lastRunAt: number().nullable(),
17436
18122
  lastError: string().nullable()
17437
18123
  });
18124
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
18125
+ var UnitWarningSchema = object({
18126
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
18127
+ path: string(),
18128
+ deviceId: number().int().optional(),
18129
+ field: string(),
18130
+ value: number(),
18131
+ assumedUnit: string(),
18132
+ message: string()
18133
+ });
18134
+ /**
18135
+ * notification-output — canonical, capability-gated notification delivery.
18136
+ *
18137
+ * Apprise-derived model (see
18138
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
18139
+ * callers emit ONE canonical `Notification`; each provider declares a
18140
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
18141
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
18142
+ * message to what the kind supports — callers never special-case a service.
18143
+ *
18144
+ * DESIGN DECISIONS (locked):
18145
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
18146
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
18147
+ * cap. Rationale: the admin UI needs one uniform surface across the
18148
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
18149
+ * alternative would fork the UI per addon and cannot host the
18150
+ * discovery→adopt flow.
18151
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
18152
+ * the generated cap-mount auto-`concatCollection`-fans them across every
18153
+ * registered provider (notifiers addon + HA addon) so one catalog is
18154
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
18155
+ * `addonId` the generated collection router extracts from the call input.
18156
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
18157
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
18158
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
18159
+ * base64 fallback needed.
18160
+ *
18161
+ * TODO (deferred, closed-set change — separate decision): add
18162
+ * `providerKind: 'notify'` so notification providers surface on the unified
18163
+ * admin "Integrations" page.
18164
+ */
18165
+ /**
18166
+ * Zentik-derived typed-media enum — the superset across every kind. Each
18167
+ * adapter picks what it supports and the degrade engine filters the rest.
18168
+ */
18169
+ var AttachmentMediaTypeSchema = _enum([
18170
+ "image",
18171
+ "video",
18172
+ "gif",
18173
+ "audio",
18174
+ "icon"
18175
+ ]);
18176
+ /**
18177
+ * A single attachment. Exactly one of `url` (remote source, most adapters
18178
+ * prefer this) or `bytes` (inline source; required for Pushover-style
18179
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
18180
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
18181
+ */
18182
+ var AttachmentSchema = object({
18183
+ mediaType: AttachmentMediaTypeSchema,
18184
+ url: string().optional(),
18185
+ bytes: _instanceof(Uint8Array).optional(),
18186
+ mime: string().optional(),
18187
+ name: string().optional()
18188
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
18189
+ var NotificationFormatSchema = _enum([
18190
+ "text",
18191
+ "markdown",
18192
+ "html"
18193
+ ]);
18194
+ /**
18195
+ * The CLOSED icon vocabulary an action button may use.
18196
+ *
18197
+ * A closed set, not a free string, and that is the whole point: an arbitrary
18198
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
18199
+ * notices — the same class of gap as a zone vocabulary nothing produced
18200
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
18201
+ * declares `actionIcons: false` and the degrade engine strips the field.
18202
+ *
18203
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
18204
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
18205
+ * differently.
18206
+ *
18207
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
18208
+ *
18209
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
18210
+ * the whole set onto its own renderer's vocabulary through a
18211
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
18212
+ * fallback, so adding a member here fails every adapter's build until someone
18213
+ * decides its glyph, which is the only place that decision can be made
18214
+ * honestly.
18215
+ *
18216
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
18217
+ * `disarm` straight through; iOS feeds that string to
18218
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
18219
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
18220
+ * mapping, and "the field is documented" is not "the value renders".
18221
+ *
18222
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
18223
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
18224
+ * so an addon that emits a token the running hub does not know does not lose an
18225
+ * icon — its whole `send` fails Zod validation and the notification never
18226
+ * arrives. Never emit a new token from an addon before the train carrying it.
18227
+ */
18228
+ var NotificationActionIconSchema = _enum([
18229
+ "acknowledge",
18230
+ "dismiss",
18231
+ "silence",
18232
+ "snooze",
18233
+ "view",
18234
+ "play",
18235
+ "open",
18236
+ "close",
18237
+ "lock",
18238
+ "unlock",
18239
+ "arm",
18240
+ "arm-home",
18241
+ "arm-away",
18242
+ "arm-night",
18243
+ "disarm",
18244
+ "light",
18245
+ "alert",
18246
+ "camera"
18247
+ ]);
18248
+ /** A single tap-through action button. */
18249
+ var NotificationActionSchema = object({
18250
+ id: string(),
18251
+ label: string(),
18252
+ url: string().optional(),
18253
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
18254
+ icon: NotificationActionIconSchema.optional(),
18255
+ /**
18256
+ * Renders in a warning style where the notifier supports it.
18257
+ *
18258
+ * A HINT, never a gate. The callback's authority is its token and nothing
18259
+ * else — see `notification-center/action-token.ts` for what that does and
18260
+ * does not buy.
18261
+ */
18262
+ destructive: boolean().optional(),
18263
+ /**
18264
+ * How the tap should REACH the url.
18265
+ *
18266
+ * `navigate` (absent, and every button authored before this field) opens it:
18267
+ * the phone leaves the notification and shows whatever the callback returns.
18268
+ * That is right for a button whose answer the operator wants to read.
18269
+ *
18270
+ * `background` fires it as a POST and stays put. It exists for the buttons
18271
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
18272
+ * an answer to the notification, and being thrown into a browser tab to
18273
+ * confirm it costs more attention than the notification did. A backend that
18274
+ * cannot do a background call renders it as an ordinary link (the adapters
18275
+ * fall back rather than dropping the button), so this is a preference, never
18276
+ * a requirement.
18277
+ */
18278
+ mode: _enum(["navigate", "background"]).optional()
18279
+ });
18280
+ /** A delivery parameter value: what a notifier declared it takes. */
18281
+ var DeliveryParamValueSchema = union([
18282
+ string(),
18283
+ number(),
18284
+ boolean()
18285
+ ]);
18286
+ /**
18287
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
18288
+ * addresses a target of that kind may set it; a kind that declares nothing is
18289
+ * never handed one. The admin UI derives its control from this descriptor —
18290
+ * no editor knows a service by name.
18291
+ *
18292
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
18293
+ * run on the addon cap path).
18294
+ */
18295
+ var DeliveryParamSchema = object({
18296
+ key: string().min(1),
18297
+ label: string(),
18298
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
18299
+ labelKey: string().optional(),
18300
+ type: _enum([
18301
+ "enum",
18302
+ "number",
18303
+ "boolean",
18304
+ "string"
18305
+ ]),
18306
+ /** `enum` only: the closed set of accepted values. */
18307
+ options: array(object({
18308
+ value: string(),
18309
+ label: string()
18310
+ })).optional(),
18311
+ /** `number` only. */
18312
+ min: number().optional(),
18313
+ max: number().optional(),
18314
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
18315
+ default: DeliveryParamValueSchema.optional()
18316
+ });
18317
+ /**
18318
+ * The canonical notification. `body` is the only hard field (Apprise model).
18319
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
18320
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
18321
+ * the adapter maps this ordinal onto its native level. `level?` is an
18322
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
18323
+ * `priority` for that one target.
18324
+ */
18325
+ var NotificationSchema = object({
18326
+ body: string(),
18327
+ title: string().optional(),
18328
+ format: NotificationFormatSchema.default("text"),
18329
+ priority: number().int().min(1).max(5).default(3),
18330
+ level: string().optional(),
18331
+ attachments: array(AttachmentSchema).optional(),
18332
+ clickUrl: string().optional(),
18333
+ actions: array(NotificationActionSchema).optional(),
18334
+ sound: string().optional(),
18335
+ ttl: number().optional(),
18336
+ tag: string().optional(),
18337
+ deviceId: number().optional(),
18338
+ eventId: string().optional(),
18339
+ metadata: record(string(), unknown()).optional(),
18340
+ /**
18341
+ * Values for the parameters the target's kind DECLARES in
18342
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
18343
+ * declaration by the sender; a notifier reads only the keys it declared.
18344
+ */
18345
+ delivery: record(string(), DeliveryParamValueSchema).optional()
18346
+ });
18347
+ /** One declared native severity/priority level for a kind. */
18348
+ var TargetKindLevelSchema = object({
18349
+ id: string(),
18350
+ label: string(),
18351
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
18352
+ ordinal: number().int().min(1).max(5).nullable(),
18353
+ flags: object({
18354
+ critical: boolean().optional(),
18355
+ silent: boolean().optional(),
18356
+ noPush: boolean().optional()
18357
+ }).optional(),
18358
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
18359
+ requires: array(string()).optional(),
18360
+ description: string().optional()
18361
+ });
18362
+ /** The full capability block consulted before dispatch. */
18363
+ var TargetKindCapsSchema = object({
18364
+ attachments: object({
18365
+ mediaTypes: array(AttachmentMediaTypeSchema),
18366
+ mode: _enum([
18367
+ "url",
18368
+ "bytes",
18369
+ "both"
18370
+ ]),
18371
+ max: number().int().nonnegative(),
18372
+ maxBytes: number().int().positive().optional()
18373
+ }),
18374
+ /** Max action buttons (0 = none). */
18375
+ actions: number().int().nonnegative(),
18376
+ /**
18377
+ * Whether this kind renders a per-action ICON.
18378
+ *
18379
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
18380
+ * run on the addon cap path — three production failures in one day taught
18381
+ * this repo that once. Absent is read as false by the degrade engine, which
18382
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
18383
+ * assumed and dropped costs the operator's trust in the field.
18384
+ */
18385
+ actionIcons: boolean().optional(),
18386
+ levels: array(TargetKindLevelSchema),
18387
+ format: array(NotificationFormatSchema),
18388
+ clickUrl: boolean(),
18389
+ sound: boolean(),
18390
+ ttl: boolean(),
18391
+ bodyMaxLen: number().int().positive()
18392
+ });
18393
+ /**
18394
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
18395
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
18396
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
18397
+ * the union is large and not meant for runtime validation here; the exported
18398
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
18399
+ */
18400
+ var ConfigSchemaPassthrough$3 = unknown();
18401
+ var TargetKindSchema = object({
18402
+ kind: string(),
18403
+ label: string(),
18404
+ icon: string(),
18405
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
18406
+ addonId: string(),
18407
+ /**
18408
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
18409
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
18410
+ * when the addon bundles no icon for that kind — the client then falls back
18411
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
18412
+ *
18413
+ * Root-relative on purpose: it resolves against whatever origin serves a web
18414
+ * client, and a native client joins it onto its own hub base.
18415
+ *
18416
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
18417
+ * field that survived only because the runtime cap-router forwards provider
18418
+ * output verbatim — so every consumer had to re-declare it by hand to stop
18419
+ * its own Zod parse from stripping it, and the whole arrangement would have
18420
+ * broken silently the moment output validation was tightened anywhere.
18421
+ */
18422
+ iconUrl: string().optional(),
18423
+ /**
18424
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
18425
+ *
18426
+ * The server knows this and therefore says it, because the client cannot
18427
+ * safely guess: a React-Native client renders SVG and raster through two
18428
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
18429
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
18430
+ * placeholder glyph for every vector icon while the web build looked fine.
18431
+ *
18432
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
18433
+ * not been updated — a client that cannot determine the type should prefer
18434
+ * its raster path, which is the safe default for an unknown image.
18435
+ */
18436
+ iconMediaType: string().optional(),
18437
+ configSchema: ConfigSchemaPassthrough$3,
18438
+ /**
18439
+ * The config keys that decide WHERE this kind's secrets are sent (server,
18440
+ * base URL, instance, chat, recipient). A save that changes any of them
18441
+ * while leaving a secret redacted is refused by the provider (D634), so an
18442
+ * editor uses this to ask for the secret on exactly those edits instead of
18443
+ * on every edit.
18444
+ *
18445
+ * Optional: absent from a provider that holds no secret (the Home Assistant
18446
+ * kinds) and from a provider older than this field — an editor that finds
18447
+ * it absent must assume ANY edit may need the secret.
18448
+ */
18449
+ destinationKeys: array(string()).optional(),
18450
+ supportsDiscovery: boolean(),
18451
+ /**
18452
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
18453
+ * Absent = none; the rule editor then offers nothing.
18454
+ */
18455
+ deliveryParams: array(DeliveryParamSchema).optional(),
18456
+ caps: TargetKindCapsSchema
18457
+ });
18458
+ /**
18459
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
18460
+ * (return a presence marker only) when serving `listTargets` — never
18461
+ * round-trip a stored secret to the UI.
18462
+ */
18463
+ var TargetSchema = object({
18464
+ id: string(),
18465
+ name: string(),
18466
+ kind: string(),
18467
+ addonId: string(),
18468
+ enabled: boolean(),
18469
+ config: record(string(), unknown())
18470
+ });
18471
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
18472
+ var DiscoveredTargetSchema = object({
18473
+ kind: string(),
18474
+ suggestedName: string(),
18475
+ config: record(string(), unknown())
18476
+ });
18477
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
18478
+ var RenderedAsSchema = object({
18479
+ level: string(),
18480
+ format: NotificationFormatSchema,
18481
+ attachmentsSent: number().int().nonnegative(),
18482
+ actionsSent: number().int().nonnegative(),
18483
+ truncated: boolean(),
18484
+ dropped: array(string())
18485
+ });
18486
+ var SendResultSchema = object({
18487
+ success: boolean(),
18488
+ error: string().optional(),
18489
+ renderedAs: RenderedAsSchema.optional()
18490
+ });
18491
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
18492
+ var TestResultSchema = SendResultSchema;
18493
+ /**
18494
+ * Pre-save credential probe against a partial (unsaved) target config.
18495
+ * `resolvedUrl` never includes secrets (token stripped from path).
18496
+ */
18497
+ var ProbeTargetConfigResultSchema = object({
18498
+ ok: boolean(),
18499
+ resolvedUrl: string(),
18500
+ httpStatus: number().int().optional(),
18501
+ stateInstance: string().optional(),
18502
+ error: string().optional()
18503
+ });
18504
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
18505
+ kind: string(),
18506
+ config: record(string(), unknown()).optional(),
18507
+ /**
18508
+ * When editing / duplicating, the blank password field never round-trips
18509
+ * the stored token. Pass the source target id so the provider can restore
18510
+ * secrets server-side before dialling (same merge as upsert).
18511
+ */
18512
+ targetId: string().optional()
18513
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
18514
+ kind: string(),
18515
+ config: record(string(), unknown()),
18516
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
18517
+ targetId: string().optional()
18518
+ }), ProbeTargetConfigResultSchema, {
18519
+ kind: "mutation",
18520
+ auth: "admin"
18521
+ }), method(object({
18522
+ targetId: string(),
18523
+ notification: NotificationSchema
18524
+ }), SendResultSchema, {
18525
+ kind: "mutation",
18526
+ auth: "admin"
18527
+ }), method(object({
18528
+ targetId: string(),
18529
+ sample: NotificationSchema.optional()
18530
+ }), TestResultSchema, {
18531
+ kind: "mutation",
18532
+ auth: "admin"
18533
+ }), method(object({
18534
+ target: TargetSchema,
18535
+ /**
18536
+ * Duplicate flow: create a NEW target id while copying secrets from an
18537
+ * existing row. The UI never holds the stored token; blank / redacted
18538
+ * password fields are restored from this source (D634 still applies if
18539
+ * destinations diverge without a re-typed secret).
18540
+ */
18541
+ cloneSecretsFrom: string().optional()
18542
+ }), TargetSchema, {
18543
+ kind: "mutation",
18544
+ auth: "admin"
18545
+ }), method(object({ targetId: string() }), _void(), {
18546
+ kind: "mutation",
18547
+ auth: "admin"
18548
+ }), method(object({
18549
+ targetId: string(),
18550
+ enabled: boolean()
18551
+ }), _void(), {
18552
+ kind: "mutation",
18553
+ auth: "admin"
18554
+ });
17438
18555
  /**
17439
18556
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17440
18557
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
@@ -17467,52 +18584,6 @@ var AddonPageInfoSchema = object({
17467
18584
  bundleUrl: string()
17468
18585
  });
17469
18586
  method(_void(), array(AddonPageInfoSchema).readonly());
17470
- /**
17471
- * `addon-pages-source` — collection cap exposing per-provider raw page
17472
- * declarations. Every addon that contributes a UI page registers a
17473
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
17474
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
17475
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
17476
- * that admin-ui consumes.
17477
- *
17478
- * The split exists because the public listing has a different output
17479
- * shape than the per-provider raw declarations, and we want both ends
17480
- * to flow through codegen instead of relying on a hand-written wrapper.
17481
- */
17482
- var AddonPageDeclarationSchema = object({
17483
- id: string(),
17484
- label: string(),
17485
- icon: string(),
17486
- path: string(),
17487
- /**
17488
- * Module Federation remote name — must match the `name` field on the
17489
- * page addon's `federation()` plugin config. Used by admin-ui's
17490
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
17491
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
17492
- * contain hyphens).
17493
- */
17494
- remoteName: string(),
17495
- /**
17496
- * Bundle filename inside the addon's `dist/` dir served at
17497
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
17498
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
17499
- * the static-file route can compute an mtime-based cache-buster URL
17500
- * without a separate filesystem stat.
17501
- */
17502
- bundle: string(),
17503
- /**
17504
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
17505
- * `'cluster'`, `'administration'` — the page renders inside that group.
17506
- * Any OTHER string creates (or joins) a custom section rendered after
17507
- * the built-in groups; its label comes from `sectionLabel` (first
17508
- * declaration wins), falling back to the id. Absent → the legacy
17509
- * "Addon Pages" group.
17510
- */
17511
- section: string().optional(),
17512
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
17513
- sectionLabel: string().optional()
17514
- });
17515
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
17516
18587
  var AddonHttpRouteSchema = object({
17517
18588
  method: _enum([
17518
18589
  "GET",
@@ -20184,7 +21255,11 @@ var CompositionProblemSchema = object({
20184
21255
  "mixed-quantity",
20185
21256
  "helper-mismatch",
20186
21257
  "member-constraint",
20187
- "retention"
21258
+ "retention",
21259
+ "command-method-unknown",
21260
+ "command-source-unknown",
21261
+ "command-input-unknown",
21262
+ "command-input-missing"
20188
21263
  ]),
20189
21264
  path: string(),
20190
21265
  message: string()
@@ -20223,7 +21298,9 @@ var CompositionFieldRoleSchema = _enum([
20223
21298
  var CompositionFieldItemSchema = object({
20224
21299
  arrayPath: string(),
20225
21300
  key: string(),
20226
- label: string()
21301
+ label: string(),
21302
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
21303
+ resettable: boolean().optional()
20227
21304
  });
20228
21305
  var CompositionFieldPlanSchema = object({
20229
21306
  cap: string(),
@@ -20428,7 +21505,7 @@ Object.freeze({
20428
21505
  * One catalog row per base reading a group helper can build (D944 §3).
20429
21506
  * Data, not a form default written into the block.
20430
21507
  */
20431
- var NOW$2 = {
21508
+ var NOW$3 = {
20432
21509
  fieldPath: "lastFetchedAt",
20433
21510
  expr: "now"
20434
21511
  };
@@ -20480,7 +21557,7 @@ var GROUP_TEMPLATES = [
20480
21557
  quantity: "temperature",
20481
21558
  defaultAggregate: "mean",
20482
21559
  empty: "null",
20483
- companions: [NOW$2],
21560
+ companions: [NOW$3],
20484
21561
  alsoFrom: {
20485
21562
  deviceTypes: CLIMATE_DEVICES,
20486
21563
  accept: {
@@ -20498,7 +21575,7 @@ var GROUP_TEMPLATES = [
20498
21575
  quantity: "humidity",
20499
21576
  defaultAggregate: "mean",
20500
21577
  empty: "null",
20501
- companions: [NOW$2],
21578
+ companions: [NOW$3],
20502
21579
  alsoFrom: {
20503
21580
  deviceTypes: CLIMATE_DEVICES,
20504
21581
  accept: {
@@ -20516,7 +21593,7 @@ var GROUP_TEMPLATES = [
20516
21593
  quantity: "pressure",
20517
21594
  defaultAggregate: "mean",
20518
21595
  empty: "null",
20519
- companions: [NOW$2]
21596
+ companions: [NOW$3]
20520
21597
  }),
20521
21598
  numeric({
20522
21599
  id: "illuminance",
@@ -20527,7 +21604,7 @@ var GROUP_TEMPLATES = [
20527
21604
  quantity: "illuminance",
20528
21605
  defaultAggregate: "mean",
20529
21606
  empty: "null",
20530
- companions: [NOW$2]
21607
+ companions: [NOW$3]
20531
21608
  }),
20532
21609
  numeric({
20533
21610
  id: "power",
@@ -20538,7 +21615,7 @@ var GROUP_TEMPLATES = [
20538
21615
  quantity: "power",
20539
21616
  defaultAggregate: "sum",
20540
21617
  empty: "omit",
20541
- companions: [NOW$2]
21618
+ companions: [NOW$3]
20542
21619
  }),
20543
21620
  numeric({
20544
21621
  id: "energy",
@@ -20549,7 +21626,7 @@ var GROUP_TEMPLATES = [
20549
21626
  quantity: "energy",
20550
21627
  defaultAggregate: "sum",
20551
21628
  empty: "omit",
20552
- companions: [NOW$2]
21629
+ companions: [NOW$3]
20553
21630
  }),
20554
21631
  numeric({
20555
21632
  id: "battery",
@@ -20584,7 +21661,7 @@ var GROUP_TEMPLATES = [
20584
21661
  quantity: "device",
20585
21662
  defaultAggregate: "mean",
20586
21663
  empty: "null",
20587
- companions: [NOW$2]
21664
+ companions: [NOW$3]
20588
21665
  }),
20589
21666
  binary({
20590
21667
  id: "motion",
@@ -22534,116 +23611,6 @@ var DeviceAdminLinkSchema = object({
22534
23611
  });
22535
23612
  method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
22536
23613
  /**
22537
- * Identity envelope for a device's upstream-system metadata.
22538
- *
22539
- * Two jobs:
22540
- *
22541
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
22542
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
22543
- * WS-Discovery endpoint UUID, …). Provider code keeps a
22544
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
22545
- * without round-tripping through `stableId` / `id` mappings.
22546
- *
22547
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
22548
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
22549
- * for vendors where MAC is the natural key). For systems where the
22550
- * live dispatch key can be renamed (HA `entity_id` can be edited),
22551
- * providers detect renames by matching `uniqueId` and patch `id`
22552
- * in-place so dispatch keeps working.
22553
- *
22554
- * **Display unit and precision are NOT stored here.** They are a read-time
22555
- * preference (D944). A polymorphic reading stores its `quantity` here once,
22556
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
22557
- * neither is repeated on each sample. The `device_class` tag is an
22558
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
22559
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
22560
- *
22561
- * The `raw` escape hatch carries provider-specific attributes that don't
22562
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
22563
- * details, …). Providers control what lands here.
22564
- */
22565
- var SourceInfoSchema = object({
22566
- /** Live dispatch key — mutable when the source system allows rename. */
22567
- id: string(),
22568
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
22569
- system: string(),
22570
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
22571
- * Used to detect rename when `id` changes. */
22572
- uniqueId: string().optional(),
22573
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
22574
- quantity: ReadingQuantitySchema.optional(),
22575
- /** Date / time / datetime shape of a string reading, written once at adopt. */
22576
- dateTimeShape: _enum([
22577
- "date",
22578
- "time",
22579
- "datetime"
22580
- ]).optional(),
22581
- /** Provider-specific extras that don't fit the structured slots.
22582
- * Whitelist what goes here per provider — do NOT dump entire HA
22583
- * attribute blobs (would explode the meta JSON column). */
22584
- raw: record(string(), unknown()).optional()
22585
- });
22586
- /**
22587
- * Build the synthetic SourceInfo every existing provider falls back to
22588
- * when no upstream value has been persisted. Keeps non-migrated providers
22589
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
22590
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
22591
- * id. Real upstream identity replaces this once a provider migrates and
22592
- * calls `updateSourceInfo()` with concrete values.
22593
- */
22594
- function synthesizeSourceInfo(input) {
22595
- return {
22596
- id: input.stableId,
22597
- system: input.addonId
22598
- };
22599
- }
22600
- /**
22601
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
22602
- * patch values are ignored (no field clobber); to clear a field
22603
- * explicitly, providers omit it from the patch and rebuild from scratch
22604
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
22605
- * bother with a `null`-means-delete sentinel.
22606
- *
22607
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
22608
- * merges over existing `raw` keys instead of replacing the whole bag.
22609
- */
22610
- function mergeSourceInfo(prev, patch) {
22611
- const next = { ...prev };
22612
- for (const [k, v] of Object.entries(patch)) {
22613
- if (v === void 0) continue;
22614
- if (k === "raw" && typeof v === "object" && v !== null) {
22615
- next.raw = {
22616
- ...prev.raw,
22617
- ...v
22618
- };
22619
- continue;
22620
- }
22621
- next[k] = v;
22622
- }
22623
- return next;
22624
- }
22625
- /**
22626
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
22627
- * blob is a free-form `Record<string, unknown>` shared with hardware-
22628
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
22629
- * lives under a single nested key to keep it composable with those.
22630
- */
22631
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
22632
- /**
22633
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
22634
- * with Zod. Returns `null` when the blob is missing or the nested
22635
- * `sourceInfo` key fails validation — callers fall back to the
22636
- * synthetic default in that case (see `synthesizeSourceInfo`).
22637
- */
22638
- function extractSourceInfoFromMetadata(metadata) {
22639
- if (!metadata) return null;
22640
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
22641
- if (!raw) return null;
22642
- const parsed = SourceInfoSchema.safeParse(raw);
22643
- if (!parsed.success) return null;
22644
- return parsed.data;
22645
- }
22646
- /**
22647
23614
  * device-discovery — device-scoped capability for parents that host /
22648
23615
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
22649
23616
  * integrations). The parent advertises a list of discoverable children,
@@ -23048,12 +24015,48 @@ var ExposeInputSchema = object({
23048
24015
  capabilities: array(string()).optional()
23049
24016
  });
23050
24017
  var UnexposeInputSchema = object({ deviceId: string() });
24018
+ /**
24019
+ * What an exporter may expose besides devices (D968). An action and a
24020
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
24021
+ * exporter's own selection, not the device registry.
24022
+ */
24023
+ var ExportEntityKindSchema = _enum([
24024
+ "device",
24025
+ "action",
24026
+ "notifier"
24027
+ ]);
24028
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
24029
+ object({
24030
+ kind: literal("device"),
24031
+ deviceId: string()
24032
+ }),
24033
+ object({
24034
+ kind: literal("action"),
24035
+ actionId: string().min(1)
24036
+ }),
24037
+ object({
24038
+ kind: literal("notifier"),
24039
+ targetId: string().min(1)
24040
+ })
24041
+ ]);
24042
+ var ExposedEntitySchema = object({
24043
+ entity: ExportEntityRefSchema,
24044
+ exposedAs: string().optional()
24045
+ });
24046
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
24047
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
23051
24048
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
23052
24049
  kind: "mutation",
23053
24050
  auth: "admin"
23054
24051
  }), method(UnexposeInputSchema, _void(), {
23055
24052
  kind: "mutation",
23056
24053
  auth: "admin"
24054
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
24055
+ kind: "mutation",
24056
+ auth: "admin"
24057
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
24058
+ kind: "mutation",
24059
+ auth: "admin"
23057
24060
  });
23058
24061
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
23059
24062
  var ICON_CATEGORY_IDS = [
@@ -45119,7 +46122,7 @@ var LlmProviderWidgetSchema = object({
45119
46122
  /** ConfigUISchema tree passed through untyped on the wire (the
45120
46123
  * notification-output `ConfigSchemaPassthrough` precedent at
45121
46124
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
45122
- var ConfigSchemaPassthrough$3 = unknown();
46125
+ var ConfigSchemaPassthrough$2 = unknown();
45123
46126
  var LlmProfileKindDescriptorSchema = object({
45124
46127
  kind: LlmProfileKindSchema,
45125
46128
  label: string(),
@@ -45128,9 +46131,9 @@ var LlmProfileKindDescriptorSchema = object({
45128
46131
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
45129
46132
  addonId: string(),
45130
46133
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
45131
- connectionSchema: ConfigSchemaPassthrough$3,
46134
+ connectionSchema: ConfigSchemaPassthrough$2,
45132
46135
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
45133
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
46136
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
45134
46137
  /** The kind's defaults; a model from `listModels` may override any of them. */
45135
46138
  features: LlmModelFeaturesSchema,
45136
46139
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -45656,10 +46659,10 @@ var CpuBreakdownSchema = object({
45656
46659
  cores: number()
45657
46660
  });
45658
46661
  var MemoryInfoSchema = object({
45659
- percent: number(),
46662
+ percent: number().nullable(),
45660
46663
  totalBytes: number(),
45661
- usedBytes: number(),
45662
- availableBytes: number(),
46664
+ usedBytes: number().nullable(),
46665
+ availableBytes: number().nullable(),
45663
46666
  swapUsedBytes: number(),
45664
46667
  swapTotalBytes: number()
45665
46668
  });
@@ -45963,8 +46966,9 @@ var GetLoadSeriesInputSchema = object({
45963
46966
  });
45964
46967
  var SystemMetricsSchema = object({
45965
46968
  cpuPercent: number(),
45966
- memoryPercent: number(),
45967
- memoryUsedMB: number(),
46969
+ /** `null` when the memory sample failed or was not taken yet (D393). */
46970
+ memoryPercent: number().nullable(),
46971
+ memoryUsedMB: number().nullable(),
45968
46972
  memoryTotalMB: number(),
45969
46973
  diskPercent: number().optional(),
45970
46974
  temperature: number().optional(),
@@ -46200,427 +47204,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46200
47204
  sourcePort: number().optional()
46201
47205
  });
46202
47206
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
46203
- /**
46204
- * notification-output — canonical, capability-gated notification delivery.
46205
- *
46206
- * Apprise-derived model (see
46207
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
46208
- * callers emit ONE canonical `Notification`; each provider declares a
46209
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
46210
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
46211
- * message to what the kind supports — callers never special-case a service.
46212
- *
46213
- * DESIGN DECISIONS (locked):
46214
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
46215
- * `setTargetEnabled`), each provider persisting via the `settings-store`
46216
- * cap. Rationale: the admin UI needs one uniform surface across the
46217
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
46218
- * alternative would fork the UI per addon and cannot host the
46219
- * discovery→adopt flow.
46220
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
46221
- * the generated cap-mount auto-`concatCollection`-fans them across every
46222
- * registered provider (notifiers addon + HA addon) so one catalog is
46223
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
46224
- * `addonId` the generated collection router extracts from the call input.
46225
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
46226
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
46227
- * `storage` / `storage-provider` / `recording` caps over the same path. No
46228
- * base64 fallback needed.
46229
- *
46230
- * TODO (deferred, closed-set change — separate decision): add
46231
- * `providerKind: 'notify'` so notification providers surface on the unified
46232
- * admin "Integrations" page.
46233
- */
46234
- /**
46235
- * Zentik-derived typed-media enum — the superset across every kind. Each
46236
- * adapter picks what it supports and the degrade engine filters the rest.
46237
- */
46238
- var AttachmentMediaTypeSchema = _enum([
46239
- "image",
46240
- "video",
46241
- "gif",
46242
- "audio",
46243
- "icon"
46244
- ]);
46245
- /**
46246
- * A single attachment. Exactly one of `url` (remote source, most adapters
46247
- * prefer this) or `bytes` (inline source; required for Pushover-style
46248
- * bytes-only kinds) MUST be present — the degrade engine expresses a
46249
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
46250
- */
46251
- var AttachmentSchema = object({
46252
- mediaType: AttachmentMediaTypeSchema,
46253
- url: string().optional(),
46254
- bytes: _instanceof(Uint8Array).optional(),
46255
- mime: string().optional(),
46256
- name: string().optional()
46257
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
46258
- var NotificationFormatSchema = _enum([
46259
- "text",
46260
- "markdown",
46261
- "html"
46262
- ]);
46263
- /**
46264
- * The CLOSED icon vocabulary an action button may use.
46265
- *
46266
- * A closed set, not a free string, and that is the whole point: an arbitrary
46267
- * icon name is one that ntfy renders, zentik silently drops, and nobody
46268
- * notices — the same class of gap as a zone vocabulary nothing produced
46269
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
46270
- * declares `actionIcons: false` and the degrade engine strips the field.
46271
- *
46272
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
46273
- * renderer's icon set; "acknowledge" survives an adapter that draws it
46274
- * differently.
46275
- *
46276
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
46277
- *
46278
- * These names are for US. **No adapter may forward one verbatim.** Each maps
46279
- * the whole set onto its own renderer's vocabulary through a
46280
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
46281
- * fallback, so adding a member here fails every adapter's build until someone
46282
- * decides its glyph, which is the only place that decision can be made
46283
- * honestly.
46284
- *
46285
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
46286
- * `disarm` straight through; iOS feeds that string to
46287
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
46288
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
46289
- * mapping, and "the field is documented" is not "the value renders".
46290
- *
46291
- * Adding a member is TRAIN-BOUND. The enum lives in the published
46292
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
46293
- * so an addon that emits a token the running hub does not know does not lose an
46294
- * icon — its whole `send` fails Zod validation and the notification never
46295
- * arrives. Never emit a new token from an addon before the train carrying it.
46296
- */
46297
- var NotificationActionIconSchema = _enum([
46298
- "acknowledge",
46299
- "dismiss",
46300
- "silence",
46301
- "snooze",
46302
- "view",
46303
- "play",
46304
- "open",
46305
- "close",
46306
- "lock",
46307
- "unlock",
46308
- "arm",
46309
- "arm-home",
46310
- "arm-away",
46311
- "arm-night",
46312
- "disarm",
46313
- "light",
46314
- "alert",
46315
- "camera"
46316
- ]);
46317
- /** A single tap-through action button. */
46318
- var NotificationActionSchema = object({
46319
- id: string(),
46320
- label: string(),
46321
- url: string().optional(),
46322
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
46323
- icon: NotificationActionIconSchema.optional(),
46324
- /**
46325
- * Renders in a warning style where the notifier supports it.
46326
- *
46327
- * A HINT, never a gate. The callback's authority is its token and nothing
46328
- * else — see `notification-center/action-token.ts` for what that does and
46329
- * does not buy.
46330
- */
46331
- destructive: boolean().optional(),
46332
- /**
46333
- * How the tap should REACH the url.
46334
- *
46335
- * `navigate` (absent, and every button authored before this field) opens it:
46336
- * the phone leaves the notification and shows whatever the callback returns.
46337
- * That is right for a button whose answer the operator wants to read.
46338
- *
46339
- * `background` fires it as a POST and stays put. It exists for the buttons
46340
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
46341
- * an answer to the notification, and being thrown into a browser tab to
46342
- * confirm it costs more attention than the notification did. A backend that
46343
- * cannot do a background call renders it as an ordinary link (the adapters
46344
- * fall back rather than dropping the button), so this is a preference, never
46345
- * a requirement.
46346
- */
46347
- mode: _enum(["navigate", "background"]).optional()
46348
- });
46349
- /** A delivery parameter value: what a notifier declared it takes. */
46350
- var DeliveryParamValueSchema = union([
46351
- string(),
46352
- number(),
46353
- boolean()
46354
- ]);
46355
- /**
46356
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
46357
- * addresses a target of that kind may set it; a kind that declares nothing is
46358
- * never handed one. The admin UI derives its control from this descriptor —
46359
- * no editor knows a service by name.
46360
- *
46361
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
46362
- * run on the addon cap path).
46363
- */
46364
- var DeliveryParamSchema = object({
46365
- key: string().min(1),
46366
- label: string(),
46367
- /** i18n key of the label, resolved by the declaring addon's namespace. */
46368
- labelKey: string().optional(),
46369
- type: _enum([
46370
- "enum",
46371
- "number",
46372
- "boolean",
46373
- "string"
46374
- ]),
46375
- /** `enum` only: the closed set of accepted values. */
46376
- options: array(object({
46377
- value: string(),
46378
- label: string()
46379
- })).optional(),
46380
- /** `number` only. */
46381
- min: number().optional(),
46382
- max: number().optional(),
46383
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
46384
- default: DeliveryParamValueSchema.optional()
46385
- });
46386
- /**
46387
- * The canonical notification. `body` is the only hard field (Apprise model).
46388
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
46389
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
46390
- * the adapter maps this ordinal onto its native level. `level?` is an
46391
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
46392
- * `priority` for that one target.
46393
- */
46394
- var NotificationSchema = object({
46395
- body: string(),
46396
- title: string().optional(),
46397
- format: NotificationFormatSchema.default("text"),
46398
- priority: number().int().min(1).max(5).default(3),
46399
- level: string().optional(),
46400
- attachments: array(AttachmentSchema).optional(),
46401
- clickUrl: string().optional(),
46402
- actions: array(NotificationActionSchema).optional(),
46403
- sound: string().optional(),
46404
- ttl: number().optional(),
46405
- tag: string().optional(),
46406
- deviceId: number().optional(),
46407
- eventId: string().optional(),
46408
- metadata: record(string(), unknown()).optional(),
46409
- /**
46410
- * Values for the parameters the target's kind DECLARES in
46411
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
46412
- * declaration by the sender; a notifier reads only the keys it declared.
46413
- */
46414
- delivery: record(string(), DeliveryParamValueSchema).optional()
46415
- });
46416
- /** One declared native severity/priority level for a kind. */
46417
- var TargetKindLevelSchema = object({
46418
- id: string(),
46419
- label: string(),
46420
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
46421
- ordinal: number().int().min(1).max(5).nullable(),
46422
- flags: object({
46423
- critical: boolean().optional(),
46424
- silent: boolean().optional(),
46425
- noPush: boolean().optional()
46426
- }).optional(),
46427
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
46428
- requires: array(string()).optional(),
46429
- description: string().optional()
46430
- });
46431
- /** The full capability block consulted before dispatch. */
46432
- var TargetKindCapsSchema = object({
46433
- attachments: object({
46434
- mediaTypes: array(AttachmentMediaTypeSchema),
46435
- mode: _enum([
46436
- "url",
46437
- "bytes",
46438
- "both"
46439
- ]),
46440
- max: number().int().nonnegative(),
46441
- maxBytes: number().int().positive().optional()
46442
- }),
46443
- /** Max action buttons (0 = none). */
46444
- actions: number().int().nonnegative(),
46445
- /**
46446
- * Whether this kind renders a per-action ICON.
46447
- *
46448
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
46449
- * run on the addon cap path — three production failures in one day taught
46450
- * this repo that once. Absent is read as false by the degrade engine, which
46451
- * is the safe direction: an icon that is not rendered costs nothing, an icon
46452
- * assumed and dropped costs the operator's trust in the field.
46453
- */
46454
- actionIcons: boolean().optional(),
46455
- levels: array(TargetKindLevelSchema),
46456
- format: array(NotificationFormatSchema),
46457
- clickUrl: boolean(),
46458
- sound: boolean(),
46459
- ttl: boolean(),
46460
- bodyMaxLen: number().int().positive()
46461
- });
46462
- /**
46463
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
46464
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
46465
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
46466
- * the union is large and not meant for runtime validation here; the exported
46467
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46468
- */
46469
- var ConfigSchemaPassthrough$2 = unknown();
46470
- var TargetKindSchema = object({
46471
- kind: string(),
46472
- label: string(),
46473
- icon: string(),
46474
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
46475
- addonId: string(),
46476
- /**
46477
- * URL of the kind's bundled BRAND icon, served by the providing addon over
46478
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
46479
- * when the addon bundles no icon for that kind — the client then falls back
46480
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
46481
- *
46482
- * Root-relative on purpose: it resolves against whatever origin serves a web
46483
- * client, and a native client joins it onto its own hub base.
46484
- *
46485
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
46486
- * field that survived only because the runtime cap-router forwards provider
46487
- * output verbatim — so every consumer had to re-declare it by hand to stop
46488
- * its own Zod parse from stripping it, and the whole arrangement would have
46489
- * broken silently the moment output validation was tightened anywhere.
46490
- */
46491
- iconUrl: string().optional(),
46492
- /**
46493
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
46494
- *
46495
- * The server knows this and therefore says it, because the client cannot
46496
- * safely guess: a React-Native client renders SVG and raster through two
46497
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
46498
- * not decode SVG on iOS/Android), so without this it silently fell back to a
46499
- * placeholder glyph for every vector icon while the web build looked fine.
46500
- *
46501
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
46502
- * not been updated — a client that cannot determine the type should prefer
46503
- * its raster path, which is the safe default for an unknown image.
46504
- */
46505
- iconMediaType: string().optional(),
46506
- configSchema: ConfigSchemaPassthrough$2,
46507
- /**
46508
- * The config keys that decide WHERE this kind's secrets are sent (server,
46509
- * base URL, instance, chat, recipient). A save that changes any of them
46510
- * while leaving a secret redacted is refused by the provider (D634), so an
46511
- * editor uses this to ask for the secret on exactly those edits instead of
46512
- * on every edit.
46513
- *
46514
- * Optional: absent from a provider that holds no secret (the Home Assistant
46515
- * kinds) and from a provider older than this field — an editor that finds
46516
- * it absent must assume ANY edit may need the secret.
46517
- */
46518
- destinationKeys: array(string()).optional(),
46519
- supportsDiscovery: boolean(),
46520
- /**
46521
- * Extra delivery parameters a rule may set for a target of this kind (D938).
46522
- * Absent = none; the rule editor then offers nothing.
46523
- */
46524
- deliveryParams: array(DeliveryParamSchema).optional(),
46525
- caps: TargetKindCapsSchema
46526
- });
46527
- /**
46528
- * A persisted target. `config` holds secrets; providers REDACT secret fields
46529
- * (return a presence marker only) when serving `listTargets` — never
46530
- * round-trip a stored secret to the UI.
46531
- */
46532
- var TargetSchema = object({
46533
- id: string(),
46534
- name: string(),
46535
- kind: string(),
46536
- addonId: string(),
46537
- enabled: boolean(),
46538
- config: record(string(), unknown())
46539
- });
46540
- /** A discovery-surfaced candidate (config is partial + non-secret). */
46541
- var DiscoveredTargetSchema = object({
46542
- kind: string(),
46543
- suggestedName: string(),
46544
- config: record(string(), unknown())
46545
- });
46546
- /** The degrade engine's report — what was resolved / dropped / degraded. */
46547
- var RenderedAsSchema = object({
46548
- level: string(),
46549
- format: NotificationFormatSchema,
46550
- attachmentsSent: number().int().nonnegative(),
46551
- actionsSent: number().int().nonnegative(),
46552
- truncated: boolean(),
46553
- dropped: array(string())
46554
- });
46555
- var SendResultSchema = object({
46556
- success: boolean(),
46557
- error: string().optional(),
46558
- renderedAs: RenderedAsSchema.optional()
46559
- });
46560
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
46561
- var TestResultSchema = SendResultSchema;
46562
- /**
46563
- * Pre-save credential probe against a partial (unsaved) target config.
46564
- * `resolvedUrl` never includes secrets (token stripped from path).
46565
- */
46566
- var ProbeTargetConfigResultSchema = object({
46567
- ok: boolean(),
46568
- resolvedUrl: string(),
46569
- httpStatus: number().int().optional(),
46570
- stateInstance: string().optional(),
46571
- error: string().optional()
46572
- });
46573
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
46574
- kind: string(),
46575
- config: record(string(), unknown()).optional(),
46576
- /**
46577
- * When editing / duplicating, the blank password field never round-trips
46578
- * the stored token. Pass the source target id so the provider can restore
46579
- * secrets server-side before dialling (same merge as upsert).
46580
- */
46581
- targetId: string().optional()
46582
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
46583
- kind: string(),
46584
- config: record(string(), unknown()),
46585
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
46586
- targetId: string().optional()
46587
- }), ProbeTargetConfigResultSchema, {
46588
- kind: "mutation",
46589
- auth: "admin"
46590
- }), method(object({
46591
- targetId: string(),
46592
- notification: NotificationSchema
46593
- }), SendResultSchema, {
46594
- kind: "mutation",
46595
- auth: "admin"
46596
- }), method(object({
46597
- targetId: string(),
46598
- sample: NotificationSchema.optional()
46599
- }), TestResultSchema, {
46600
- kind: "mutation",
46601
- auth: "admin"
46602
- }), method(object({
46603
- target: TargetSchema,
46604
- /**
46605
- * Duplicate flow: create a NEW target id while copying secrets from an
46606
- * existing row. The UI never holds the stored token; blank / redacted
46607
- * password fields are restored from this source (D634 still applies if
46608
- * destinations diverge without a re-typed secret).
46609
- */
46610
- cloneSecretsFrom: string().optional()
46611
- }), TargetSchema, {
46612
- kind: "mutation",
46613
- auth: "admin"
46614
- }), method(object({ targetId: string() }), _void(), {
46615
- kind: "mutation",
46616
- auth: "admin"
46617
- }), method(object({
46618
- targetId: string(),
46619
- enabled: boolean()
46620
- }), _void(), {
46621
- kind: "mutation",
46622
- auth: "admin"
46623
- });
46624
47207
  var AUDIO_MACRO_LABELS = [
46625
47208
  {
46626
47209
  id: "speech",
@@ -54865,14 +55448,6 @@ var EngineProvisioningSchema = object({
54865
55448
  */
54866
55449
  configIssues: array(string()).optional()
54867
55450
  });
54868
- var PipelineStepInputSchema = lazy(() => object({
54869
- addonId: string(),
54870
- modelId: string().optional(),
54871
- enabled: boolean().default(true),
54872
- children: array(PipelineStepInputSchema).optional(),
54873
- settings: record(string(), unknown()).optional(),
54874
- jumpDeviceKey: string().optional()
54875
- }));
54876
55451
  var ModelSubstitutionSchema = object({
54877
55452
  addonId: string(),
54878
55453
  chosen: string(),
@@ -54951,7 +55526,6 @@ var ConfigUISchemaBridge = custom();
54951
55526
  var ConfigUISchemaNullableBridge = custom();
54952
55527
  var InferenceCapabilitiesBridge = custom();
54953
55528
  var ModelAvailabilityListBridge = custom();
54954
- var PipelineRunResultBridge = custom();
54955
55529
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
54956
55530
  modelId: string(),
54957
55531
  settings: record(string(), unknown()).readonly()
@@ -57947,11 +58521,13 @@ var ImageContractSchema = object({
57947
58521
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
57948
58522
  * produced it.
57949
58523
  *
57950
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
57951
- * update is an ANNOUNCEMENT. The notification body carries the exact command
57952
- * for a `docker` shell and says the app restarts into the new build for
57953
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
57954
- * nothing reads is the defect this repo keeps paying for.
58524
+ * There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
58525
+ * ANNOUNCEMENT. The notification body carries the exact command for a `docker`
58526
+ * shell and says the app restarts into the new build for `electron`, and that
58527
+ * sentence is chosen from `kind` alone — a boolean nothing reads is the defect
58528
+ * this repo keeps paying for. The one shell that CAN be acted on is the
58529
+ * Electron app, and only through `installDesktopUpdate` (see
58530
+ * {@link DesktopAppStatusSchema} for what the node reads from the app first).
57955
58531
  */
57956
58532
  var WrapperIdentitySchema = object({
57957
58533
  kind: _enum([
@@ -57999,6 +58575,49 @@ var ServerRollbackInfoSchema = object({
57999
58575
  reason: string()
58000
58576
  });
58001
58577
  /**
58578
+ * What the native desktop app (Electron) that hosts this node says about
58579
+ * itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
58580
+ * Absent on the status = no desktop app, or it never wrote the file.
58581
+ *
58582
+ * `stale` is the honest answer to an app that stopped writing (closed, hung,
58583
+ * crashed): the update facts are then the LAST ones known, not current ones,
58584
+ * and a consumer must not read `update.available: false` from a stale record
58585
+ * as "no update". `stateAtMs` is when the app last wrote.
58586
+ */
58587
+ var DesktopAppStatusSchema = object({
58588
+ /** The Electron app's own version (not the node closure's). */
58589
+ version: string(),
58590
+ update: object({
58591
+ available: boolean(),
58592
+ /** The new build is downloaded and `installDesktopUpdate` can apply it. */
58593
+ downloaded: boolean(),
58594
+ version: string().nullable(),
58595
+ checking: boolean(),
58596
+ /** Why the app's last update check produced no answer; `null` when it did. */
58597
+ error: string().nullable()
58598
+ }),
58599
+ stateAtMs: number(),
58600
+ stale: boolean()
58601
+ });
58602
+ /**
58603
+ * Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
58604
+ * - `not-electron` — this node does not run inside the desktop app.
58605
+ * - `no-app-state` — the app never published its state (or the file is unreadable).
58606
+ * - `app-state-stale` — the app stopped writing; it is probably not running.
58607
+ * - `not-downloaded` — no update is downloaded yet; nothing to install.
58608
+ */
58609
+ var InstallDesktopUpdateReasonSchema = _enum([
58610
+ "not-electron",
58611
+ "no-app-state",
58612
+ "app-state-stale",
58613
+ "not-downloaded"
58614
+ ]);
58615
+ var InstallDesktopUpdateResultSchema = object({
58616
+ accepted: boolean(),
58617
+ /** Non-null exactly when `accepted` is false. */
58618
+ reason: InstallDesktopUpdateReasonSchema.nullable()
58619
+ });
58620
+ /**
58002
58621
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
58003
58622
  * `resolveRunnerId` over its forked addons; agent: the runners its process
58004
58623
  * service holds, because only the hub plans) against the states the process
@@ -58102,7 +58721,12 @@ var ServerPackageStatusSchema = object({
58102
58721
  * on a provider that predates it. Absent is "not checked yet", never "all
58103
58722
  * well": a consumer that needs the answer treats it as pending.
58104
58723
  */
58105
- runners: RunnerBootCheckSchema.optional()
58724
+ runners: RunnerBootCheckSchema.optional(),
58725
+ /**
58726
+ * The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
58727
+ * Optional for version skew AND because most nodes have no desktop app.
58728
+ */
58729
+ desktopApp: DesktopAppStatusSchema.optional()
58106
58730
  });
58107
58731
  var ServerUpdateCheckResultSchema = object({
58108
58732
  packageName: string(),
@@ -58185,6 +58809,12 @@ var ServerUpdateActionResultSchema = object({
58185
58809
  */
58186
58810
  blockers: array(UpdateBlockerSchema).optional()
58187
58811
  });
58812
+ var LaunchOverridesSchema = object({
58813
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
58814
+ heapMb: number().int().nullable() });
58815
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
58816
+ /** Always true: the override is read at launch, not by the running process. */
58817
+ restartRequired: boolean() });
58188
58818
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
58189
58819
  kind: "mutation",
58190
58820
  auth: "admin"
@@ -58201,12 +58831,18 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
58201
58831
  kind: "mutation",
58202
58832
  auth: "admin",
58203
58833
  timeoutMs: 16 * 6e4
58834
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
58835
+ kind: "mutation",
58836
+ auth: "admin"
58204
58837
  }), method(_void(), ServerUpdateActionResultSchema, {
58205
58838
  kind: "mutation",
58206
58839
  auth: "admin"
58207
58840
  }), method(_void(), ServerUpdateActionResultSchema, {
58208
58841
  kind: "mutation",
58209
58842
  auth: "admin"
58843
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
58844
+ kind: "mutation",
58845
+ auth: "admin"
58210
58846
  });
58211
58847
  /**
58212
58848
  * `smtp-provider` — pluggable email delivery surface.
@@ -61348,13 +61984,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
61348
61984
  }), method(ActionIdInput, _void(), {
61349
61985
  kind: "mutation",
61350
61986
  auth: "admin"
61351
- }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
61987
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
61988
+ action: ActionEntrySchema,
61989
+ warnings: array(UnitWarningSchema).optional()
61990
+ }), {
61352
61991
  kind: "mutation",
61353
61992
  auth: "admin"
61354
61993
  }), method(object({
61355
61994
  actionId: string().min(1),
61356
61995
  action: ActionInputSchema.partial()
61357
- }), object({ action: ActionEntrySchema }), {
61996
+ }), object({
61997
+ action: ActionEntrySchema,
61998
+ warnings: array(UnitWarningSchema).optional()
61999
+ }), {
61358
62000
  kind: "mutation",
61359
62001
  auth: "admin"
61360
62002
  }), method(ActionIdInput, _void(), {
@@ -61936,6 +62578,30 @@ object({
61936
62578
  at: number()
61937
62579
  }).nullable()
61938
62580
  });
62581
+ var RemoteButtonShapeSchema = _enum([
62582
+ "rect",
62583
+ "rounded",
62584
+ "round",
62585
+ "pill"
62586
+ ]);
62587
+ var RemoteButtonToneSchema = _enum([
62588
+ "neutral",
62589
+ "accent",
62590
+ "danger",
62591
+ "red",
62592
+ "green",
62593
+ "yellow",
62594
+ "blue"
62595
+ ]);
62596
+ var RemoteGroupFrameSchema = _enum([
62597
+ "none",
62598
+ "panel",
62599
+ "divider",
62600
+ "dpad",
62601
+ "rocker-v",
62602
+ "rocker-h",
62603
+ "keypad"
62604
+ ]);
61939
62605
  var RemoteLayoutButtonSchema = object({
61940
62606
  id: string().min(1).max(40),
61941
62607
  /** 0-based grid position. */
@@ -61945,12 +62611,94 @@ var RemoteLayoutButtonSchema = object({
61945
62611
  colSpan: number().int().min(1).max(24),
61946
62612
  icon: string().min(1).max(60).optional(),
61947
62613
  label: string().min(1).max(40).optional(),
61948
- role: RemoteRoleSchema
62614
+ role: RemoteRoleSchema,
62615
+ /** Absent = the plain rendering every layout had before shapes existed. */
62616
+ shape: RemoteButtonShapeSchema.optional(),
62617
+ tone: RemoteButtonToneSchema.optional(),
62618
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
62619
+ groupId: string().min(1).max(40).optional()
61949
62620
  });
62621
+ /**
62622
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
62623
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
62624
+ * entry it came from (informational: the layout is self-contained).
62625
+ */
62626
+ var RemoteLayoutGroupSchema = object({
62627
+ id: string().min(1).max(40),
62628
+ componentId: string().min(1).max(80),
62629
+ frame: RemoteGroupFrameSchema,
62630
+ row: number().int().min(0).max(24),
62631
+ col: number().int().min(0).max(24),
62632
+ rowSpan: number().int().min(1).max(24),
62633
+ colSpan: number().int().min(1).max(24),
62634
+ title: string().min(1).max(40).optional()
62635
+ });
62636
+ function rectsOverlap(a, b) {
62637
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
62638
+ }
62639
+ function rectContains(outer, inner) {
62640
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
62641
+ }
62642
+ /**
62643
+ * The group rules of a layout: inside the grid, no two groups overlap, a
62644
+ * member lies inside its group, a divider holds nothing, and a button that is
62645
+ * not a member never sits inside someone else's rectangle (a group moves as
62646
+ * one thing, so a stray key under it would be left behind).
62647
+ */
62648
+ function refineGroups(grid, ctx) {
62649
+ const groups = grid.groups ?? [];
62650
+ const byId = /* @__PURE__ */ new Map();
62651
+ const fail = (message, path) => {
62652
+ ctx.addIssue({
62653
+ code: "custom",
62654
+ message,
62655
+ path
62656
+ });
62657
+ };
62658
+ groups.forEach((group, index) => {
62659
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
62660
+ "groups",
62661
+ index,
62662
+ "id"
62663
+ ]);
62664
+ byId.set(group.id, group);
62665
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
62666
+ for (let other = 0; other < index; other += 1) {
62667
+ const earlier = groups[other];
62668
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
62669
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
62670
+ return;
62671
+ }
62672
+ }
62673
+ });
62674
+ grid.buttons.forEach((button, index) => {
62675
+ if (button.groupId !== void 0) {
62676
+ const group = byId.get(button.groupId);
62677
+ if (group === void 0) {
62678
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
62679
+ "buttons",
62680
+ index,
62681
+ "groupId"
62682
+ ]);
62683
+ return;
62684
+ }
62685
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
62686
+ "buttons",
62687
+ index,
62688
+ "groupId"
62689
+ ]);
62690
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
62691
+ return;
62692
+ }
62693
+ const foreign = groups.find((group) => rectsOverlap(group, button));
62694
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
62695
+ });
62696
+ }
61950
62697
  object({
61951
62698
  columns: number().int().min(1).max(24),
61952
62699
  rows: number().int().min(1).max(24),
61953
- buttons: array(RemoteLayoutButtonSchema).max(200)
62700
+ buttons: array(RemoteLayoutButtonSchema).max(200),
62701
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
61954
62702
  }).superRefine((grid, ctx) => {
61955
62703
  const ids = /* @__PURE__ */ new Set();
61956
62704
  const taken = /* @__PURE__ */ new Map();
@@ -61987,6 +62735,7 @@ object({
61987
62735
  taken.set(cell, button.id);
61988
62736
  }
61989
62737
  });
62738
+ refineGroups(grid, ctx);
61990
62739
  });
61991
62740
  var RemoteBoundCommandSchema = object({
61992
62741
  role: RemoteRoleSchema,
@@ -62118,6 +62867,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
62118
62867
  kind: "mutation",
62119
62868
  auth: "admin"
62120
62869
  });
62870
+ var RemoteComponentCategorySchema = _enum([
62871
+ "power",
62872
+ "navigation",
62873
+ "volume",
62874
+ "numbers",
62875
+ "media",
62876
+ "colour",
62877
+ "apps",
62878
+ "layout"
62879
+ ]);
62880
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
62881
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
62882
+ var RemoteComponentButtonSchema = object({
62883
+ /** Joined to the instance id when the component is inserted. */
62884
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
62885
+ row: number().int().min(0).max(24),
62886
+ col: number().int().min(0).max(24),
62887
+ rowSpan: number().int().min(1).max(24),
62888
+ colSpan: number().int().min(1).max(24),
62889
+ role: RemoteRoleSchema,
62890
+ icon: string().min(1).max(60).optional(),
62891
+ label: string().min(1).max(40).optional(),
62892
+ shape: RemoteButtonShapeSchema.optional(),
62893
+ tone: RemoteButtonToneSchema.optional()
62894
+ });
62895
+ var RemoteLayoutComponentSchema = object({
62896
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
62897
+ name: string().min(1).max(60),
62898
+ nameI18n: object({
62899
+ en: string().min(1).max(60),
62900
+ it: string().min(1).max(60)
62901
+ }).optional(),
62902
+ /** A display icon key, resolved by the UI like a button icon. */
62903
+ icon: string().min(1).max(60),
62904
+ category: RemoteComponentCategorySchema,
62905
+ size: object({
62906
+ rows: number().int().min(1).max(24),
62907
+ cols: number().int().min(1).max(24)
62908
+ }),
62909
+ /** The frame drawn around the whole component, or null for loose buttons. */
62910
+ group: object({
62911
+ frame: RemoteGroupFrameSchema,
62912
+ title: string().min(1).max(40).optional()
62913
+ }).nullable(),
62914
+ buttons: array(RemoteComponentButtonSchema).max(40)
62915
+ }).superRefine((component, ctx) => {
62916
+ const suffixes = /* @__PURE__ */ new Set();
62917
+ component.buttons.forEach((button, index) => {
62918
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
62919
+ code: "custom",
62920
+ message: `button "${button.idSuffix}" is listed twice`,
62921
+ path: [
62922
+ "buttons",
62923
+ index,
62924
+ "idSuffix"
62925
+ ]
62926
+ });
62927
+ suffixes.add(button.idSuffix);
62928
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
62929
+ ctx.addIssue({
62930
+ code: "custom",
62931
+ message: `button "${button.idSuffix}" leaves the component`,
62932
+ path: ["buttons", index]
62933
+ });
62934
+ return;
62935
+ }
62936
+ for (let other = 0; other < index; other += 1) {
62937
+ const earlier = component.buttons[other];
62938
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
62939
+ ctx.addIssue({
62940
+ code: "custom",
62941
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
62942
+ path: ["buttons", index]
62943
+ });
62944
+ return;
62945
+ }
62946
+ }
62947
+ });
62948
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
62949
+ code: "custom",
62950
+ message: "a divider component holds no buttons",
62951
+ path: ["buttons"]
62952
+ });
62953
+ });
62954
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
62121
62955
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
62122
62956
  kind: "mutation",
62123
62957
  auth: "protected"
@@ -63173,14 +64007,13 @@ reset: method(object({
63173
64007
  auth: "admin"
63174
64008
  }) },
63175
64009
  /**
63176
- * A composed consumable (item added to a device with no native consumables
63177
- * provider) refuses `reset` by name: "Only meaningful when the item's
63178
- * `resettable` is true" — a composed item is never resettable until
63179
- * commands (`forward`) arrive in slice 3 (D663).
64010
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
64011
+ * name: only a composed device of a NEW type forwards an item's reset to a
64012
+ * source button, and marks that item `resettable` (D967).
63180
64013
  */
63181
64014
  composedMethods: { reset: {
63182
64015
  kind: "refuse",
63183
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
64016
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
63184
64017
  } },
63185
64018
  status: {
63186
64019
  schema: ConsumablesStatusSchema,
@@ -63796,7 +64629,7 @@ var DishwasherStatusSchema = object({
63796
64629
  /** Ms epoch of the last value received from the appliance; 0 = never. */
63797
64630
  lastFetchedAt: number()
63798
64631
  });
63799
- var DeviceIdSchema = number().int().nonnegative();
64632
+ var DeviceIdSchema$1 = number().int().nonnegative();
63800
64633
  /** A value a program option or setting can take. */
63801
64634
  var DishwasherValueSchema = union([
63802
64635
  boolean(),
@@ -63871,77 +64704,77 @@ var dishwasherCapability = {
63871
64704
  * option feature keys; each is checked against the program's constraints.
63872
64705
  */
63873
64706
  start: method(object({
63874
- deviceId: DeviceIdSchema,
64707
+ deviceId: DeviceIdSchema$1,
63875
64708
  program: string().min(1).optional(),
63876
64709
  options: record(string(), DishwasherValueSchema).optional()
63877
64710
  }), _void(), {
63878
64711
  kind: "mutation",
63879
64712
  auth: "protected"
63880
64713
  }),
63881
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
64714
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63882
64715
  kind: "mutation",
63883
64716
  auth: "protected"
63884
64717
  }),
63885
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
64718
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63886
64719
  kind: "mutation",
63887
64720
  auth: "protected"
63888
64721
  }),
63889
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
64722
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63890
64723
  kind: "mutation",
63891
64724
  auth: "protected"
63892
64725
  }),
63893
64726
  /** Select (not start) a program; refused when the appliance does not offer it. */
63894
64727
  setProgram: method(object({
63895
- deviceId: DeviceIdSchema,
64728
+ deviceId: DeviceIdSchema$1,
63896
64729
  program: string().min(1)
63897
64730
  }), _void(), {
63898
64731
  kind: "mutation",
63899
64732
  auth: "protected"
63900
64733
  }),
63901
64734
  setPower: method(object({
63902
- deviceId: DeviceIdSchema,
64735
+ deviceId: DeviceIdSchema$1,
63903
64736
  on: boolean()
63904
64737
  }), _void(), {
63905
64738
  kind: "mutation",
63906
64739
  auth: "protected"
63907
64740
  }),
63908
64741
  setHalfLoad: method(object({
63909
- deviceId: DeviceIdSchema,
64742
+ deviceId: DeviceIdSchema$1,
63910
64743
  on: boolean()
63911
64744
  }), _void(), {
63912
64745
  kind: "mutation",
63913
64746
  auth: "protected"
63914
64747
  }),
63915
64748
  setHygienePlus: method(object({
63916
- deviceId: DeviceIdSchema,
64749
+ deviceId: DeviceIdSchema$1,
63917
64750
  on: boolean()
63918
64751
  }), _void(), {
63919
64752
  kind: "mutation",
63920
64753
  auth: "protected"
63921
64754
  }),
63922
64755
  setExtraDry: method(object({
63923
- deviceId: DeviceIdSchema,
64756
+ deviceId: DeviceIdSchema$1,
63924
64757
  on: boolean()
63925
64758
  }), _void(), {
63926
64759
  kind: "mutation",
63927
64760
  auth: "protected"
63928
64761
  }),
63929
64762
  setIntensiveZone: method(object({
63930
- deviceId: DeviceIdSchema,
64763
+ deviceId: DeviceIdSchema$1,
63931
64764
  on: boolean()
63932
64765
  }), _void(), {
63933
64766
  kind: "mutation",
63934
64767
  auth: "protected"
63935
64768
  }),
63936
64769
  setVarioSpeedPlus: method(object({
63937
- deviceId: DeviceIdSchema,
64770
+ deviceId: DeviceIdSchema$1,
63938
64771
  on: boolean()
63939
64772
  }), _void(), {
63940
64773
  kind: "mutation",
63941
64774
  auth: "protected"
63942
64775
  }),
63943
64776
  setChildLock: method(object({
63944
- deviceId: DeviceIdSchema,
64777
+ deviceId: DeviceIdSchema$1,
63945
64778
  on: boolean()
63946
64779
  }), _void(), {
63947
64780
  kind: "mutation",
@@ -63953,7 +64786,7 @@ var dishwasherCapability = {
63953
64786
  * its declared range / members.
63954
64787
  */
63955
64788
  setOption: method(object({
63956
- deviceId: DeviceIdSchema,
64789
+ deviceId: DeviceIdSchema$1,
63957
64790
  key: string().min(1),
63958
64791
  value: DishwasherValueSchema
63959
64792
  }), _void(), {
@@ -63961,7 +64794,7 @@ var dishwasherCapability = {
63961
64794
  auth: "protected"
63962
64795
  }),
63963
64796
  /** What this appliance can do, from its own description — the UI is derived from it. */
63964
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
64797
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
63965
64798
  },
63966
64799
  status: {
63967
64800
  schema: DishwasherStatusSchema,
@@ -67738,7 +68571,8 @@ var TopologyNodeSchema = object({
67738
68571
  isHub: boolean(),
67739
68572
  isOnline: boolean(),
67740
68573
  cpuPercent: number(),
67741
- memoryPercent: number(),
68574
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
68575
+ memoryPercent: number().nullable(),
67742
68576
  uptime: number(),
67743
68577
  lastSeen: string(),
67744
68578
  localIps: array(string()).readonly(),
@@ -68349,6 +69183,14 @@ var PetFeederDeviceStatusSchema = _enum([
68349
69183
  "on_batteries"
68350
69184
  ]);
68351
69185
  var gramsPortion = number().int().min(4).max(200);
69186
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
69187
+ * hardware's own unit). The bounds are grams; a caller entering another unit
69188
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
69189
+ * plain grams — they are not readings in the kilogram base. */
69190
+ var gramsParam = gramsPortion.meta(paramMeta({
69191
+ quantity: "mass",
69192
+ unit: "g"
69193
+ }));
68352
69194
  var PetFeederStatusSchema = object({
68353
69195
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
68354
69196
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -68356,9 +69198,10 @@ var PetFeederStatusSchema = object({
68356
69198
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
68357
69199
  * the device has not reported yet. */
68358
69200
  foodLevel: number().nullable().meta(readingMeta("none")),
68359
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
68360
- * feeder's own units. Null when the model reports none. */
68361
- bowlFood: number().nullable().meta(readingMeta("none")),
69201
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
69202
+ * feeder with a bowl scale; null when the model has none or its reading is
69203
+ * of an unproven unit. */
69204
+ bowlFood: number().nullable().meta(readingMeta("mass")),
68362
69205
  /** True while the pet is eating (`state.eating`). */
68363
69206
  eating: boolean(),
68364
69207
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -68436,9 +69279,9 @@ var petFeederCapability = {
68436
69279
  */
68437
69280
  feed: method(object({
68438
69281
  deviceId: number().int().nonnegative(),
68439
- grams: gramsPortion.optional(),
68440
- hopper1: gramsPortion.optional(),
68441
- hopper2: gramsPortion.optional()
69282
+ grams: gramsParam.optional(),
69283
+ hopper1: gramsParam.optional(),
69284
+ hopper2: gramsParam.optional()
68442
69285
  }), _void(), {
68443
69286
  kind: "mutation",
68444
69287
  auth: "protected"
@@ -68542,6 +69385,137 @@ var petFeederCapability = {
68542
69385
  }
68543
69386
  } }
68544
69387
  };
69388
+ /**
69389
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
69390
+ * circulating drinking fountain: how much water is in the tank, how much the
69391
+ * pet drank, whether the pump runs and on what duty cycle.
69392
+ *
69393
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
69394
+ * a container of loose entities (a pump switch, two number boxes, a "filter
69395
+ * changed" button), and an operator BUILDS the device from them — a composed
69396
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
69397
+ * method of ONE source entity, and every reading is a field derived from a
69398
+ * source entity; the provider-shaped interface below exists so the composed
69399
+ * device and any future native provider answer the same contract.
69400
+ *
69401
+ * Quantities are stored in the base unit of their kind (D944): volumes in
69402
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
69403
+ * reports millilitres or minutes is converted in the composition's expression,
69404
+ * never here. Every reading may be `null`: a source that is offline or does
69405
+ * not report is unknown, never zero.
69406
+ *
69407
+ * The filter is not a field of this cap: it is an item of the `consumables`
69408
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
69409
+ */
69410
+ var PetFountainStatusSchema = object({
69411
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
69412
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69413
+ /** Water in the tank, in litres. Null while the source has not reported. */
69414
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
69415
+ /** True when the tank is empty. Null while unknown. */
69416
+ empty: boolean().nullable(),
69417
+ /** True while the pump is running. Null while unknown. */
69418
+ pumpRunning: boolean().nullable(),
69419
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
69420
+ pumpIntervalEnabled: boolean().nullable(),
69421
+ /** How long the pump runs per cycle, in seconds. */
69422
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
69423
+ /** How long the pump rests per cycle, in seconds. */
69424
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
69425
+ /** Water drunk in the last drink, in litres. */
69426
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
69427
+ /** Water drunk in the current hour, in litres. */
69428
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
69429
+ /** Water drunk over the last 24 hours (rolling), in litres. */
69430
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
69431
+ /** Water drunk since local midnight, in litres. */
69432
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
69433
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
69434
+ error: string().nullable(),
69435
+ /** Ms epoch when the slice was last refreshed. */
69436
+ lastFetchedAt: number()
69437
+ });
69438
+ var DeviceIdSchema = number().int().nonnegative();
69439
+ var MinutesSchema = number().int().min(0);
69440
+ var petFountainCapability = {
69441
+ name: "pet-fountain",
69442
+ scope: "device",
69443
+ deviceNative: true,
69444
+ mode: "singleton",
69445
+ deviceTypes: [DeviceType.PetFountain],
69446
+ methods: {
69447
+ /** Start or stop the pump. */
69448
+ setPump: method(object({
69449
+ deviceId: DeviceIdSchema,
69450
+ on: boolean()
69451
+ }), _void(), {
69452
+ kind: "mutation",
69453
+ auth: "protected"
69454
+ }),
69455
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
69456
+ setPumpInterval: method(object({
69457
+ deviceId: DeviceIdSchema,
69458
+ on: boolean()
69459
+ }), _void(), {
69460
+ kind: "mutation",
69461
+ auth: "protected"
69462
+ }),
69463
+ /** Set how many minutes the pump runs per cycle. */
69464
+ setPumpOnMinutes: method(object({
69465
+ deviceId: DeviceIdSchema,
69466
+ minutes: MinutesSchema
69467
+ }), _void(), {
69468
+ kind: "mutation",
69469
+ auth: "protected"
69470
+ }),
69471
+ /** Set how many minutes the pump rests per cycle. */
69472
+ setPumpOffMinutes: method(object({
69473
+ deviceId: DeviceIdSchema,
69474
+ minutes: MinutesSchema
69475
+ }), _void(), {
69476
+ kind: "mutation",
69477
+ auth: "protected"
69478
+ }),
69479
+ /** Clear the rolling daily consumption counters on the device. */
69480
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69481
+ kind: "mutation",
69482
+ auth: "protected"
69483
+ })
69484
+ },
69485
+ status: {
69486
+ schema: PetFountainStatusSchema,
69487
+ kind: "push"
69488
+ },
69489
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
69490
+ runtimeState: PetFountainStatusSchema,
69491
+ /**
69492
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
69493
+ *
69494
+ * See `RuntimeStateDurability`. Enforced by
69495
+ * `scripts/check-runtime-state-durability.ts`.
69496
+ */
69497
+ durability: "session",
69498
+ /** History (D850): recorded series. */
69499
+ history: { fields: {
69500
+ waterLevel: {
69501
+ kind: "gauge",
69502
+ unit: "%",
69503
+ deadband: 1
69504
+ },
69505
+ waterVolume: {
69506
+ kind: "gauge",
69507
+ unit: "L"
69508
+ },
69509
+ todayDrink: {
69510
+ kind: "gauge",
69511
+ unit: "L"
69512
+ },
69513
+ last24hDrink: {
69514
+ kind: "gauge",
69515
+ unit: "L"
69516
+ }
69517
+ } }
69518
+ };
68545
69519
  var VehicleSchema = object({
68546
69520
  id: string(),
68547
69521
  name: string(),
@@ -74539,6 +75513,87 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
74539
75513
  kind: "mutation",
74540
75514
  auth: "admin"
74541
75515
  });
75516
+ var btn = (idSuffix, row, col, init) => ({
75517
+ idSuffix,
75518
+ row,
75519
+ col,
75520
+ rowSpan: 1,
75521
+ colSpan: 1,
75522
+ ...init
75523
+ });
75524
+ function rocker(id, name, it, frame, title, buttons, size) {
75525
+ return {
75526
+ id,
75527
+ name,
75528
+ nameI18n: {
75529
+ en: name,
75530
+ it
75531
+ },
75532
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
75533
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
75534
+ size,
75535
+ group: title === null ? { frame } : {
75536
+ frame,
75537
+ title
75538
+ },
75539
+ buttons
75540
+ };
75541
+ }
75542
+ btn("power", 0, 0, {
75543
+ role: "power",
75544
+ shape: "round",
75545
+ tone: "danger"
75546
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
75547
+ role: "ok",
75548
+ shape: "round",
75549
+ tone: "accent"
75550
+ }), btn("right", 1, 2, { role: "right" }), btn("down", 2, 1, { role: "down" }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
75551
+ role: "volume_up",
75552
+ shape: "pill"
75553
+ }), btn("down", 1, 0, {
75554
+ role: "volume_down",
75555
+ shape: "pill"
75556
+ })], {
75557
+ rows: 2,
75558
+ cols: 1
75559
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
75560
+ role: "channel_up",
75561
+ shape: "pill"
75562
+ }), btn("down", 1, 0, {
75563
+ role: "channel_down",
75564
+ shape: "pill"
75565
+ })], {
75566
+ rows: 2,
75567
+ cols: 1
75568
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
75569
+ role: "custom:minus",
75570
+ label: "−",
75571
+ shape: "pill"
75572
+ }), btn("plus", 0, 1, {
75573
+ role: "custom:plus",
75574
+ label: "+",
75575
+ shape: "pill"
75576
+ })], {
75577
+ rows: 1,
75578
+ cols: 2
75579
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
75580
+ role: "custom:color_red",
75581
+ tone: "red"
75582
+ }), btn("green", 0, 1, {
75583
+ role: "custom:color_green",
75584
+ tone: "green"
75585
+ }), btn("yellow", 0, 2, {
75586
+ role: "custom:color_yellow",
75587
+ tone: "yellow"
75588
+ }), btn("blue", 0, 3, {
75589
+ role: "custom:color_blue",
75590
+ tone: "blue"
75591
+ }), btn("rewind", 0, 0, { role: "rewind" }), btn("play", 0, 1, { role: "play" }), btn("pause", 0, 2, { role: "pause" }), btn("forward", 0, 3, { role: "fast_forward" }), btn("stop", 0, 4, { role: "stop" }), [
75592
+ 1,
75593
+ 2,
75594
+ 3,
75595
+ 4
75596
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
74542
75597
  discriminatedUnion("kind", [object({
74543
75598
  kind: literal("raw"),
74544
75599
  timings: array(number().int()),
@@ -74549,7 +75604,7 @@ discriminatedUnion("kind", [object({
74549
75604
  })]);
74550
75605
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
74551
75606
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
74552
- 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;
75607
+ 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;
74553
75608
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
74554
75609
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
74555
75610
  new Set(CAP_NAMES);
@@ -74953,6 +76008,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
74953
76008
  notifier: notifierCapability,
74954
76009
  numericSensor: numericSensorCapability,
74955
76010
  petFeeder: petFeederCapability,
76011
+ petFountain: petFountainCapability,
74956
76012
  powerMeter: powerMeterCapability,
74957
76013
  presence: presenceCapability,
74958
76014
  pressureSensor: pressureSensorCapability,
@@ -75720,7 +76776,7 @@ var DeclaredDevices = class {
75720
76776
  return removed;
75721
76777
  }
75722
76778
  };
75723
- 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;
76779
+ 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;
75724
76780
  new Set(Object.values(DeviceType));
75725
76781
  Object.freeze({
75726
76782
  "accessories.getStatus": {
@@ -77655,6 +78711,13 @@ Object.freeze({
77655
78711
  access: "create",
77656
78712
  role: "admin"
77657
78713
  },
78714
+ "deviceExport.exposeEntity": {
78715
+ capName: "device-export",
78716
+ capScope: "system",
78717
+ addonId: null,
78718
+ access: "create",
78719
+ role: "admin"
78720
+ },
77658
78721
  "deviceExport.getDeviceLiveContribution": {
77659
78722
  capName: "device-export",
77660
78723
  capScope: "system",
@@ -77683,6 +78746,13 @@ Object.freeze({
77683
78746
  access: "view",
77684
78747
  role: "admin"
77685
78748
  },
78749
+ "deviceExport.listExposedEntities": {
78750
+ capName: "device-export",
78751
+ capScope: "system",
78752
+ addonId: null,
78753
+ access: "view",
78754
+ role: "admin"
78755
+ },
77686
78756
  "deviceExport.listSupportedDeviceKinds": {
77687
78757
  capName: "device-export",
77688
78758
  capScope: "system",
@@ -77690,6 +78760,13 @@ Object.freeze({
77690
78760
  access: "view",
77691
78761
  role: "admin"
77692
78762
  },
78763
+ "deviceExport.listSupportedEntityKinds": {
78764
+ capName: "device-export",
78765
+ capScope: "system",
78766
+ addonId: null,
78767
+ access: "view",
78768
+ role: "admin"
78769
+ },
77693
78770
  "deviceExport.unexposeDevice": {
77694
78771
  capName: "device-export",
77695
78772
  capScope: "system",
@@ -77697,6 +78774,13 @@ Object.freeze({
77697
78774
  access: "create",
77698
78775
  role: "admin"
77699
78776
  },
78777
+ "deviceExport.unexposeEntity": {
78778
+ capName: "device-export",
78779
+ capScope: "system",
78780
+ addonId: null,
78781
+ access: "create",
78782
+ role: "admin"
78783
+ },
77700
78784
  "deviceExtension.listCandidates": {
77701
78785
  capName: "device-extension",
77702
78786
  capScope: "device",
@@ -81162,6 +82246,48 @@ Object.freeze({
81162
82246
  access: "create",
81163
82247
  role: "viewer"
81164
82248
  },
82249
+ "petFountain.getStatus": {
82250
+ capName: "pet-fountain",
82251
+ capScope: "device",
82252
+ addonId: null,
82253
+ access: "view",
82254
+ role: "viewer"
82255
+ },
82256
+ "petFountain.resetDailyConsumption": {
82257
+ capName: "pet-fountain",
82258
+ capScope: "device",
82259
+ addonId: null,
82260
+ access: "delete",
82261
+ role: "viewer"
82262
+ },
82263
+ "petFountain.setPump": {
82264
+ capName: "pet-fountain",
82265
+ capScope: "device",
82266
+ addonId: null,
82267
+ access: "create",
82268
+ role: "viewer"
82269
+ },
82270
+ "petFountain.setPumpInterval": {
82271
+ capName: "pet-fountain",
82272
+ capScope: "device",
82273
+ addonId: null,
82274
+ access: "create",
82275
+ role: "viewer"
82276
+ },
82277
+ "petFountain.setPumpOffMinutes": {
82278
+ capName: "pet-fountain",
82279
+ capScope: "device",
82280
+ addonId: null,
82281
+ access: "create",
82282
+ role: "viewer"
82283
+ },
82284
+ "petFountain.setPumpOnMinutes": {
82285
+ capName: "pet-fountain",
82286
+ capScope: "device",
82287
+ addonId: null,
82288
+ access: "create",
82289
+ role: "viewer"
82290
+ },
81165
82291
  "pipelineAnalytics.addRetrainFrameFromRecording": {
81166
82292
  capName: "pipeline-analytics",
81167
82293
  capScope: "device",
@@ -83451,6 +84577,13 @@ Object.freeze({
83451
84577
  access: "create",
83452
84578
  role: "admin"
83453
84579
  },
84580
+ "remoteLayoutComponents.listComponents": {
84581
+ capName: "remote-layout-components",
84582
+ capScope: "system",
84583
+ addonId: null,
84584
+ access: "view",
84585
+ role: "admin"
84586
+ },
83454
84587
  "runningState.getStatus": {
83455
84588
  capName: "running-state",
83456
84589
  capScope: "device",
@@ -83563,6 +84696,13 @@ Object.freeze({
83563
84696
  access: "create",
83564
84697
  role: "admin"
83565
84698
  },
84699
+ "serverManagement.getLaunchOverrides": {
84700
+ capName: "server-management",
84701
+ capScope: "system",
84702
+ addonId: null,
84703
+ access: "view",
84704
+ role: "admin"
84705
+ },
83566
84706
  "serverManagement.getServerPackageStatus": {
83567
84707
  capName: "server-management",
83568
84708
  capScope: "system",
@@ -83570,6 +84710,13 @@ Object.freeze({
83570
84710
  access: "view",
83571
84711
  role: "admin"
83572
84712
  },
84713
+ "serverManagement.installDesktopUpdate": {
84714
+ capName: "server-management",
84715
+ capScope: "system",
84716
+ addonId: null,
84717
+ access: "create",
84718
+ role: "admin"
84719
+ },
83573
84720
  "serverManagement.restartServer": {
83574
84721
  capName: "server-management",
83575
84722
  capScope: "system",
@@ -83584,6 +84731,13 @@ Object.freeze({
83584
84731
  access: "create",
83585
84732
  role: "admin"
83586
84733
  },
84734
+ "serverManagement.setLaunchOverrides": {
84735
+ capName: "server-management",
84736
+ capScope: "system",
84737
+ addonId: null,
84738
+ access: "create",
84739
+ role: "admin"
84740
+ },
83587
84741
  "settingsStore.aggregate": {
83588
84742
  capName: "settings-store",
83589
84743
  capScope: "system",
@@ -87473,7 +88627,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87473
88627
  min: 4,
87474
88628
  max: 200,
87475
88629
  step: 1,
87476
- unit: "g"
88630
+ unit: "g",
88631
+ quantity: "mass"
87477
88632
  },
87478
88633
  optional: true
87479
88634
  }]
@@ -87492,7 +88647,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87492
88647
  min: 4,
87493
88648
  max: 200,
87494
88649
  step: 1,
87495
- unit: "g"
88650
+ unit: "g",
88651
+ quantity: "mass"
87496
88652
  },
87497
88653
  optional: true
87498
88654
  }, {
@@ -87503,7 +88659,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87503
88659
  min: 4,
87504
88660
  max: 200,
87505
88661
  step: 1,
87506
- unit: "g"
88662
+ unit: "g",
88663
+ quantity: "mass"
87507
88664
  },
87508
88665
  optional: true
87509
88666
  }]
@@ -87644,6 +88801,100 @@ var APPLIANCE_CAP_VOCABULARY = {
87644
88801
  }
87645
88802
  ]
87646
88803
  },
88804
+ "pet-fountain": {
88805
+ scope: "device",
88806
+ noun: t("Pet fountain", "Fontana per animali"),
88807
+ actions: [
88808
+ {
88809
+ id: "petFountain.pumpOn",
88810
+ method: "setPump",
88811
+ label: t("Pump on", "Pompa accesa"),
88812
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
88813
+ icon: "play",
88814
+ args: { on: true }
88815
+ },
88816
+ {
88817
+ id: "petFountain.pumpOff",
88818
+ method: "setPump",
88819
+ label: t("Pump off", "Pompa spenta"),
88820
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
88821
+ icon: "square",
88822
+ args: { on: false }
88823
+ },
88824
+ {
88825
+ id: "petFountain.intervalOn",
88826
+ method: "setPumpInterval",
88827
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
88828
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
88829
+ icon: "timer",
88830
+ args: { on: true }
88831
+ },
88832
+ {
88833
+ id: "petFountain.intervalOff",
88834
+ method: "setPumpInterval",
88835
+ label: t("Pump cycle off", "Ciclo pompa spento"),
88836
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
88837
+ icon: "timer-off",
88838
+ args: { on: false }
88839
+ },
88840
+ {
88841
+ id: "petFountain.pumpOnMinutes",
88842
+ method: "setPumpOnMinutes",
88843
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
88844
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
88845
+ icon: "timer",
88846
+ openArgs: [{
88847
+ key: "minutes",
88848
+ label: t("Minutes", "Minuti"),
88849
+ offer: {
88850
+ kind: "number",
88851
+ min: 0,
88852
+ step: 1,
88853
+ unit: "min"
88854
+ }
88855
+ }]
88856
+ },
88857
+ {
88858
+ id: "petFountain.pumpOffMinutes",
88859
+ method: "setPumpOffMinutes",
88860
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
88861
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
88862
+ icon: "timer-off",
88863
+ openArgs: [{
88864
+ key: "minutes",
88865
+ label: t("Minutes", "Minuti"),
88866
+ offer: {
88867
+ kind: "number",
88868
+ min: 0,
88869
+ step: 1,
88870
+ unit: "min"
88871
+ }
88872
+ }]
88873
+ },
88874
+ {
88875
+ id: "petFountain.resetDailyConsumption",
88876
+ method: "resetDailyConsumption",
88877
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
88878
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
88879
+ icon: "rotate-ccw"
88880
+ }
88881
+ ],
88882
+ state: [{
88883
+ kind: "boolean",
88884
+ field: "empty",
88885
+ label: t("Tank empty", "Serbatoio vuoto"),
88886
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
88887
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
88888
+ icon: "droplet"
88889
+ }, {
88890
+ kind: "numeric",
88891
+ field: "waterLevel",
88892
+ quantity: t("the water level", "il livello dell’acqua"),
88893
+ label: t("Water level", "Livello dell’acqua"),
88894
+ unit: "%",
88895
+ icon: "droplets"
88896
+ }]
88897
+ },
87647
88898
  "media-player": {
87648
88899
  scope: "device",
87649
88900
  noun: t("Media player", "Lettore multimediale"),
@@ -89509,6 +90760,7 @@ var SYSTEM_CAP_VOCABULARY = {
89509
90760
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
89510
90761
  records: system(INTERNAL),
89511
90762
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
90763
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
89512
90764
  "server-management": system(CLUSTER),
89513
90765
  "settings-store": system(INTERNAL),
89514
90766
  "smtp-provider": system(INTERNAL),
@@ -89581,7 +90833,7 @@ function projectAction(cap, action) {
89581
90833
  };
89582
90834
  }
89583
90835
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
89584
- 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;
90836
+ 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;
89585
90837
  DeviceFeature.BatteryOperated;
89586
90838
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
89587
90839
  var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
@@ -91132,6 +92384,36 @@ Object.freeze({
91132
92384
  form: "single",
91133
92385
  optional: false
91134
92386
  }],
92387
+ "petFountain.getStatus": [{
92388
+ name: "deviceId",
92389
+ form: "single",
92390
+ optional: false
92391
+ }],
92392
+ "petFountain.resetDailyConsumption": [{
92393
+ name: "deviceId",
92394
+ form: "single",
92395
+ optional: false
92396
+ }],
92397
+ "petFountain.setPump": [{
92398
+ name: "deviceId",
92399
+ form: "single",
92400
+ optional: false
92401
+ }],
92402
+ "petFountain.setPumpInterval": [{
92403
+ name: "deviceId",
92404
+ form: "single",
92405
+ optional: false
92406
+ }],
92407
+ "petFountain.setPumpOffMinutes": [{
92408
+ name: "deviceId",
92409
+ form: "single",
92410
+ optional: false
92411
+ }],
92412
+ "petFountain.setPumpOnMinutes": [{
92413
+ name: "deviceId",
92414
+ form: "single",
92415
+ optional: false
92416
+ }],
91135
92417
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
91136
92418
  name: "deviceId",
91137
92419
  form: "single",
@@ -95528,8 +96810,6 @@ var NC_TEMPLATE_VARS = [
95528
96810
  * delete it from here then, and the served catalog names it again.
95529
96811
  */
95530
96812
  var NC_TEMPLATE_HELD_BACK_SYSTEM_EVENT_KINDS = [
95531
- "node-restarting",
95532
- "node-updating",
95533
96813
  "alert-critical",
95534
96814
  "alert-error",
95535
96815
  "alert-critical-resolved",