@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
@@ -5676,7 +5676,7 @@ var ZodIssueCode = {
5676
5676
  var ZodFirstPartyTypeKind;
5677
5677
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5678
5678
  //#endregion
5679
- //#region ../types/dist/sleep-C_u6cEAJ.mjs
5679
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5680
5680
  /**
5681
5681
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5682
5682
  * window to float samples (D455).
@@ -8777,65 +8777,6 @@ var MODEL_FORMATS = [
8777
8777
  "pt",
8778
8778
  "gguf"
8779
8779
  ];
8780
- /**
8781
- * Per-call node pinning for `ctx.api` capability calls.
8782
- *
8783
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8784
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8785
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8786
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8787
- * call to that node.
8788
- *
8789
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8790
- * method args), so capability method signatures stay `nodeId`-free — node
8791
- * targeting is a property of the CALL, not of the method. The transport lifts
8792
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8793
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8794
- * which classifies a pinned agent node as `agent-child-forward`
8795
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8796
- *
8797
- * Usage at a call site:
8798
- *
8799
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8800
- */
8801
- /** tRPC `op.context` key carrying a per-call node pin. */
8802
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8803
- /**
8804
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8805
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8806
- *
8807
- * ## The id is normalised here, and it has to be
8808
- *
8809
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8810
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8811
- * compares a pin against real node ids, so such a pin matches nothing and the
8812
- * call fails with `no provider registered for cap "…"`. The local-first
8813
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8814
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8815
- * the thing you reach for when you specifically need THIS node — silently
8816
- * addressed a node that does not exist.
8817
- *
8818
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8819
- * own node to read the host's hardware-decode backend. It never once answered,
8820
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8821
- * while D67's whole premise was that the decoder addon is the authority on
8822
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8823
- * hardware", which was false.
8824
- *
8825
- * Normalising in the ONE constructor fixes every caller at once, which is why
8826
- * it is here and not at the call sites.
8827
- */
8828
- function nodePin(nodeId) {
8829
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8830
- }
8831
- /**
8832
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8833
- * is idempotent, so passing an already-clean id costs nothing.
8834
- */
8835
- function toNodeId(idOrRunnerId) {
8836
- const head = idOrRunnerId.split("/")[0];
8837
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8838
- }
8839
8780
  object({
8840
8781
  field: string().min(1),
8841
8782
  on: literal("advance")
@@ -8928,6 +8869,111 @@ function optionalMethod(input, output, options) {
8928
8869
  function event(data) {
8929
8870
  return { data };
8930
8871
  }
8872
+ /**
8873
+ * Per-call node pinning for `ctx.api` capability calls.
8874
+ *
8875
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8876
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8877
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8878
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8879
+ * call to that node.
8880
+ *
8881
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8882
+ * method args), so capability method signatures stay `nodeId`-free — node
8883
+ * targeting is a property of the CALL, not of the method. The transport lifts
8884
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8885
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8886
+ * which classifies a pinned agent node as `agent-child-forward`
8887
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8888
+ *
8889
+ * Usage at a call site:
8890
+ *
8891
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8892
+ */
8893
+ /** tRPC `op.context` key carrying a per-call node pin. */
8894
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8895
+ /**
8896
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8897
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8898
+ *
8899
+ * ## The id is normalised here, and it has to be
8900
+ *
8901
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8902
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8903
+ * compares a pin against real node ids, so such a pin matches nothing and the
8904
+ * call fails with `no provider registered for cap "…"`. The local-first
8905
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8906
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8907
+ * the thing you reach for when you specifically need THIS node — silently
8908
+ * addressed a node that does not exist.
8909
+ *
8910
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8911
+ * own node to read the host's hardware-decode backend. It never once answered,
8912
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8913
+ * while D67's whole premise was that the decoder addon is the authority on
8914
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8915
+ * hardware", which was false.
8916
+ *
8917
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8918
+ * it is here and not at the call sites.
8919
+ */
8920
+ function nodePin(nodeId) {
8921
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8922
+ }
8923
+ /**
8924
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8925
+ * is idempotent, so passing an already-clean id costs nothing.
8926
+ */
8927
+ function toNodeId(idOrRunnerId) {
8928
+ const head = idOrRunnerId.split("/")[0];
8929
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8930
+ }
8931
+ /**
8932
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8933
+ * declarations. Every addon that contributes a UI page registers a
8934
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8935
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8936
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8937
+ * that admin-ui consumes.
8938
+ *
8939
+ * The split exists because the public listing has a different output
8940
+ * shape than the per-provider raw declarations, and we want both ends
8941
+ * to flow through codegen instead of relying on a hand-written wrapper.
8942
+ */
8943
+ var AddonPageDeclarationSchema = object({
8944
+ id: string(),
8945
+ label: string(),
8946
+ icon: string(),
8947
+ path: string(),
8948
+ /**
8949
+ * Module Federation remote name — must match the `name` field on the
8950
+ * page addon's `federation()` plugin config. Used by admin-ui's
8951
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8952
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8953
+ * contain hyphens).
8954
+ */
8955
+ remoteName: string(),
8956
+ /**
8957
+ * Bundle filename inside the addon's `dist/` dir served at
8958
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8959
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8960
+ * the static-file route can compute an mtime-based cache-buster URL
8961
+ * without a separate filesystem stat.
8962
+ */
8963
+ bundle: string(),
8964
+ /**
8965
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8966
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8967
+ * Any OTHER string creates (or joins) a custom section rendered after
8968
+ * the built-in groups; its label comes from `sectionLabel` (first
8969
+ * declaration wins), falling back to the id. Absent → the legacy
8970
+ * "Addon Pages" group.
8971
+ */
8972
+ section: string().optional(),
8973
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8974
+ sectionLabel: string().optional()
8975
+ });
8976
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8931
8977
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8932
8978
  var VersionOutputSchema$1 = object({ version: string() });
8933
8979
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -8953,6 +8999,27 @@ function customAction(input, output, options) {
8953
8999
  ...options?.caller ? { caller: "required" } : {}
8954
9000
  };
8955
9001
  }
9002
+ /**
9003
+ * The pipeline step-input shape and the run-result bridge, in a module that
9004
+ * imports nothing but zod and types.
9005
+ *
9006
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
9007
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
9008
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
9009
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
9010
+ * them, so every existing import keeps working; addon server code takes them
9011
+ * from `@camstack/types/addon`.
9012
+ */
9013
+ var PipelineStepInputSchema = lazy(() => object({
9014
+ addonId: string(),
9015
+ modelId: string().optional(),
9016
+ enabled: boolean().default(true),
9017
+ children: array(PipelineStepInputSchema).optional(),
9018
+ settings: record(string(), unknown()).optional(),
9019
+ jumpDeviceKey: string().optional()
9020
+ }));
9021
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
9022
+ var PipelineRunResultBridge = custom();
8956
9023
  var StaticDirOutputSchema = object({ staticDir: string() });
8957
9024
  var VersionOutputSchema = object({ version: string() });
8958
9025
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9149,6 +9216,7 @@ var CAP_NAMES = Object.freeze([
9149
9216
  "osd",
9150
9217
  "osd-manager",
9151
9218
  "pet-feeder",
9219
+ "pet-fountain",
9152
9220
  "pipeline-analytics",
9153
9221
  "pipeline-executor",
9154
9222
  "pipeline-orchestrator",
@@ -9170,6 +9238,7 @@ var CAP_NAMES = Object.freeze([
9170
9238
  "recording-signal",
9171
9239
  "records",
9172
9240
  "remote",
9241
+ "remote-layout-components",
9173
9242
  "running-state",
9174
9243
  "scene-monitor",
9175
9244
  "script-runner",
@@ -10428,6 +10497,56 @@ Object.freeze({
10428
10497
  "valueType": "number"
10429
10498
  }
10430
10499
  ],
10500
+ "pet-fountain": [
10501
+ {
10502
+ "path": "waterLevel",
10503
+ "valueType": "number"
10504
+ },
10505
+ {
10506
+ "path": "waterVolume",
10507
+ "valueType": "number"
10508
+ },
10509
+ {
10510
+ "path": "empty",
10511
+ "valueType": "boolean"
10512
+ },
10513
+ {
10514
+ "path": "pumpRunning",
10515
+ "valueType": "boolean"
10516
+ },
10517
+ {
10518
+ "path": "pumpIntervalEnabled",
10519
+ "valueType": "boolean"
10520
+ },
10521
+ {
10522
+ "path": "pumpOnTime",
10523
+ "valueType": "number"
10524
+ },
10525
+ {
10526
+ "path": "pumpOffTime",
10527
+ "valueType": "number"
10528
+ },
10529
+ {
10530
+ "path": "lastDrink",
10531
+ "valueType": "number"
10532
+ },
10533
+ {
10534
+ "path": "currentHourDrink",
10535
+ "valueType": "number"
10536
+ },
10537
+ {
10538
+ "path": "last24hDrink",
10539
+ "valueType": "number"
10540
+ },
10541
+ {
10542
+ "path": "todayDrink",
10543
+ "valueType": "number"
10544
+ },
10545
+ {
10546
+ "path": "error",
10547
+ "valueType": "string"
10548
+ }
10549
+ ],
10431
10550
  "power-meter": [
10432
10551
  {
10433
10552
  "path": "power",
@@ -10967,7 +11086,7 @@ Object.freeze({
10967
11086
  * can answer "may a rule actuate this?" without importing the schema barrel
10968
11087
  * (~144MB RSS per runner, D28).
10969
11088
  *
10970
- * Coverage: 96 device-scoped capabilities.
11089
+ * Coverage: 97 device-scoped capabilities.
10971
11090
  */
10972
11091
  var DEVICE_SCOPED_CAPS = new Set([
10973
11092
  "accessories",
@@ -11033,6 +11152,7 @@ var DEVICE_SCOPED_CAPS = new Set([
11033
11152
  "numeric-sensor",
11034
11153
  "osd",
11035
11154
  "pet-feeder",
11155
+ "pet-fountain",
11036
11156
  "pipeline-analytics",
11037
11157
  "power-meter",
11038
11158
  "presence",
@@ -11111,7 +11231,7 @@ function sleep(ms) {
11111
11231
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11112
11232
  }
11113
11233
  //#endregion
11114
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11234
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
11115
11235
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11116
11236
  DeviceType["Camera"] = "camera";
11117
11237
  DeviceType["Hub"] = "hub";
@@ -11203,6 +11323,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11203
11323
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11204
11324
  * integrations sharing the same food/desiccant/hopper surface. */
11205
11325
  DeviceType["PetFeeder"] = "pet-feeder";
11326
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11327
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11328
+ * of a container and is BUILT as a composed device (D967). */
11329
+ DeviceType["PetFountain"] = "pet-fountain";
11206
11330
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11207
11331
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11208
11332
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11473,7 +11597,7 @@ function errMsg(err) {
11473
11597
  return String(err);
11474
11598
  }
11475
11599
  //#endregion
11476
- //#region ../types/dist/composition-bIBW5_13.mjs
11600
+ //#region ../types/dist/composition-kSuOifuG.mjs
11477
11601
  /**
11478
11602
  * Deterministic SHA-256 hash of an arbitrary serialisable value. The
11479
11603
  * canonical form sorts object keys alphabetically at every depth so two
@@ -12718,12 +12842,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12718
12842
  ])
12719
12843
  })
12720
12844
  ]);
12721
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12845
+ var forwardArgUnion = union([
12846
+ string(),
12847
+ number(),
12848
+ boolean(),
12849
+ strictObject({ ["$input"]: string().min(1) }),
12850
+ record(string().min(1), lazy(() => forwardArgUnion))
12851
+ ]);
12852
+ var CompositionForwardArgSchema = forwardArgUnion;
12853
+ /**
12854
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12855
+ * call of `cap.method` on `source`. Without `args` the built method's input
12856
+ * passes through minus `deviceId`; with `args` the template IS the source's
12857
+ * whole input. `deviceId` is always the source's own, never an argument.
12858
+ */
12859
+ var CompositionForwardCommandSchema = object({
12722
12860
  kind: literal("forward"),
12723
12861
  source: CompositionSourceRefSchema,
12724
12862
  cap: string().min(1),
12725
- method: string().min(1)
12726
- }), CompositionCodeSourceSchema]);
12863
+ method: string().min(1),
12864
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12865
+ });
12866
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12727
12867
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12728
12868
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12729
12869
  /**
@@ -12733,7 +12873,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12733
12873
  */
12734
12874
  var CompositionItemEntrySchema = object({
12735
12875
  label: string().min(1).max(80),
12736
- fields: record(string().min(1), CompositionFieldSourceSchema)
12876
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12877
+ /**
12878
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12879
+ * is resettable and a `reset` call naming its key forwards here. The item key
12880
+ * selects the command, so it is never an argument: there is no pass-through.
12881
+ */
12882
+ reset: CompositionForwardCommandSchema.optional()
12737
12883
  });
12738
12884
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12739
12885
  kind: literal("fields"),
@@ -12745,7 +12891,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12745
12891
  * owns the WHOLE array field: the composed value holds exactly these items.
12746
12892
  */
12747
12893
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12748
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12894
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12749
12895
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12750
12896
  }), object({
12751
12897
  kind: literal("passthrough"),
@@ -16888,6 +17034,538 @@ object({
16888
17034
  path: ["expr"]
16889
17035
  });
16890
17036
  });
17037
+ /**
17038
+ * Identity envelope for a device's upstream-system metadata.
17039
+ *
17040
+ * Two jobs:
17041
+ *
17042
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
17043
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
17044
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
17045
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
17046
+ * without round-tripping through `stableId` / `id` mappings.
17047
+ *
17048
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
17049
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
17050
+ * for vendors where MAC is the natural key). For systems where the
17051
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
17052
+ * providers detect renames by matching `uniqueId` and patch `id`
17053
+ * in-place so dispatch keeps working.
17054
+ *
17055
+ * **Display unit and precision are NOT stored here.** They are a read-time
17056
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
17057
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
17058
+ * neither is repeated on each sample. The `device_class` tag is an
17059
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
17060
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
17061
+ *
17062
+ * The `raw` escape hatch carries provider-specific attributes that don't
17063
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
17064
+ * details, …). Providers control what lands here.
17065
+ */
17066
+ var SourceInfoSchema = object({
17067
+ /** Live dispatch key — mutable when the source system allows rename. */
17068
+ id: string(),
17069
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
17070
+ system: string(),
17071
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
17072
+ * Used to detect rename when `id` changes. */
17073
+ uniqueId: string().optional(),
17074
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
17075
+ quantity: ReadingQuantitySchema.optional(),
17076
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
17077
+ dateTimeShape: _enum([
17078
+ "date",
17079
+ "time",
17080
+ "datetime"
17081
+ ]).optional(),
17082
+ /** Provider-specific extras that don't fit the structured slots.
17083
+ * Whitelist what goes here per provider — do NOT dump entire HA
17084
+ * attribute blobs (would explode the meta JSON column). */
17085
+ raw: record(string(), unknown()).optional()
17086
+ });
17087
+ /**
17088
+ * Build the synthetic SourceInfo every existing provider falls back to
17089
+ * when no upstream value has been persisted. Keeps non-migrated providers
17090
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
17091
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
17092
+ * id. Real upstream identity replaces this once a provider migrates and
17093
+ * calls `updateSourceInfo()` with concrete values.
17094
+ */
17095
+ function synthesizeSourceInfo(input) {
17096
+ return {
17097
+ id: input.stableId,
17098
+ system: input.addonId
17099
+ };
17100
+ }
17101
+ /**
17102
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
17103
+ * patch values are ignored (no field clobber); to clear a field
17104
+ * explicitly, providers omit it from the patch and rebuild from scratch
17105
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
17106
+ * bother with a `null`-means-delete sentinel.
17107
+ *
17108
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
17109
+ * merges over existing `raw` keys instead of replacing the whole bag.
17110
+ */
17111
+ function mergeSourceInfo(prev, patch) {
17112
+ const next = { ...prev };
17113
+ for (const [k, v] of Object.entries(patch)) {
17114
+ if (v === void 0) continue;
17115
+ if (k === "raw" && typeof v === "object" && v !== null) {
17116
+ next.raw = {
17117
+ ...prev.raw,
17118
+ ...v
17119
+ };
17120
+ continue;
17121
+ }
17122
+ next[k] = v;
17123
+ }
17124
+ return next;
17125
+ }
17126
+ /**
17127
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
17128
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
17129
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
17130
+ * lives under a single nested key to keep it composable with those.
17131
+ */
17132
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
17133
+ /**
17134
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
17135
+ * with Zod. Returns `null` when the blob is missing or the nested
17136
+ * `sourceInfo` key fails validation — callers fall back to the
17137
+ * synthetic default in that case (see `synthesizeSourceInfo`).
17138
+ */
17139
+ function extractSourceInfoFromMetadata(metadata) {
17140
+ if (!metadata) return null;
17141
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
17142
+ if (!raw) return null;
17143
+ const parsed = SourceInfoSchema.safeParse(raw);
17144
+ if (!parsed.success) return null;
17145
+ return parsed.data;
17146
+ }
17147
+ /**
17148
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
17149
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
17150
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
17151
+ * decimals). K derives from `°C = K − 273.15`.
17152
+ */
17153
+ var UNIT_TABLE = {
17154
+ "°C": {
17155
+ dimension: "temperature",
17156
+ factor: 1,
17157
+ offset: 0
17158
+ },
17159
+ "°F": {
17160
+ dimension: "temperature",
17161
+ factor: 5 / 9,
17162
+ offset: -160 / 9
17163
+ },
17164
+ K: {
17165
+ dimension: "temperature",
17166
+ factor: 1,
17167
+ offset: -273.15
17168
+ },
17169
+ hPa: {
17170
+ dimension: "pressure",
17171
+ factor: 1,
17172
+ offset: 0
17173
+ },
17174
+ kPa: {
17175
+ dimension: "pressure",
17176
+ factor: 10,
17177
+ offset: 0
17178
+ },
17179
+ Pa: {
17180
+ dimension: "pressure",
17181
+ factor: .01,
17182
+ offset: 0
17183
+ },
17184
+ mbar: {
17185
+ dimension: "pressure",
17186
+ factor: 1,
17187
+ offset: 0
17188
+ },
17189
+ bar: {
17190
+ dimension: "pressure",
17191
+ factor: 1e3,
17192
+ offset: 0
17193
+ },
17194
+ inHg: {
17195
+ dimension: "pressure",
17196
+ factor: 33.8639,
17197
+ offset: 0
17198
+ },
17199
+ mmHg: {
17200
+ dimension: "pressure",
17201
+ factor: 1.33322,
17202
+ offset: 0
17203
+ },
17204
+ psi: {
17205
+ dimension: "pressure",
17206
+ factor: 68.9476,
17207
+ offset: 0
17208
+ },
17209
+ "m/s": {
17210
+ dimension: "speed",
17211
+ factor: 1,
17212
+ offset: 0
17213
+ },
17214
+ "km/h": {
17215
+ dimension: "speed",
17216
+ factor: 1 / 3.6,
17217
+ offset: 0
17218
+ },
17219
+ mph: {
17220
+ dimension: "speed",
17221
+ factor: .44704,
17222
+ offset: 0
17223
+ },
17224
+ kn: {
17225
+ dimension: "speed",
17226
+ factor: .514444,
17227
+ offset: 0
17228
+ },
17229
+ "mm/h": {
17230
+ dimension: "precipitation-rate",
17231
+ factor: 1,
17232
+ offset: 0
17233
+ },
17234
+ "in/h": {
17235
+ dimension: "precipitation-rate",
17236
+ factor: 25.4,
17237
+ offset: 0
17238
+ },
17239
+ m: {
17240
+ dimension: "length",
17241
+ factor: 1,
17242
+ offset: 0
17243
+ },
17244
+ mm: {
17245
+ dimension: "length",
17246
+ factor: .001,
17247
+ offset: 0
17248
+ },
17249
+ cm: {
17250
+ dimension: "length",
17251
+ factor: .01,
17252
+ offset: 0
17253
+ },
17254
+ km: {
17255
+ dimension: "length",
17256
+ factor: 1e3,
17257
+ offset: 0
17258
+ },
17259
+ in: {
17260
+ dimension: "length",
17261
+ factor: .0254,
17262
+ offset: 0
17263
+ },
17264
+ ft: {
17265
+ dimension: "length",
17266
+ factor: .3048,
17267
+ offset: 0
17268
+ },
17269
+ mi: {
17270
+ dimension: "length",
17271
+ factor: 1609.344,
17272
+ offset: 0
17273
+ },
17274
+ "m²": {
17275
+ dimension: "area",
17276
+ factor: 1,
17277
+ offset: 0
17278
+ },
17279
+ lx: {
17280
+ dimension: "illuminance",
17281
+ factor: 1,
17282
+ offset: 0
17283
+ },
17284
+ "W/m²": {
17285
+ dimension: "irradiance",
17286
+ factor: 1,
17287
+ offset: 0
17288
+ },
17289
+ W: {
17290
+ dimension: "power",
17291
+ factor: 1,
17292
+ offset: 0
17293
+ },
17294
+ kW: {
17295
+ dimension: "power",
17296
+ factor: 1e3,
17297
+ offset: 0
17298
+ },
17299
+ VA: {
17300
+ dimension: "apparent-power",
17301
+ factor: 1,
17302
+ offset: 0
17303
+ },
17304
+ Wh: {
17305
+ dimension: "energy",
17306
+ factor: 1,
17307
+ offset: 0
17308
+ },
17309
+ kWh: {
17310
+ dimension: "energy",
17311
+ factor: 1e3,
17312
+ offset: 0
17313
+ },
17314
+ MWh: {
17315
+ dimension: "energy",
17316
+ factor: 1e6,
17317
+ offset: 0
17318
+ },
17319
+ V: {
17320
+ dimension: "voltage",
17321
+ factor: 1,
17322
+ offset: 0
17323
+ },
17324
+ mV: {
17325
+ dimension: "voltage",
17326
+ factor: .001,
17327
+ offset: 0
17328
+ },
17329
+ A: {
17330
+ dimension: "current",
17331
+ factor: 1,
17332
+ offset: 0
17333
+ },
17334
+ mA: {
17335
+ dimension: "current",
17336
+ factor: .001,
17337
+ offset: 0
17338
+ },
17339
+ ppm: {
17340
+ dimension: "concentration-volume",
17341
+ factor: 1,
17342
+ offset: 0
17343
+ },
17344
+ ppb: {
17345
+ dimension: "concentration-volume",
17346
+ factor: .001,
17347
+ offset: 0
17348
+ },
17349
+ "µg/m³": {
17350
+ dimension: "concentration-mass",
17351
+ factor: 1,
17352
+ offset: 0
17353
+ },
17354
+ "mg/m³": {
17355
+ dimension: "concentration-mass",
17356
+ factor: 1e3,
17357
+ offset: 0
17358
+ },
17359
+ kg: {
17360
+ dimension: "mass",
17361
+ factor: 1,
17362
+ offset: 0
17363
+ },
17364
+ g: {
17365
+ dimension: "mass",
17366
+ factor: .001,
17367
+ offset: 0
17368
+ },
17369
+ lb: {
17370
+ dimension: "mass",
17371
+ factor: .453592,
17372
+ offset: 0
17373
+ },
17374
+ oz: {
17375
+ dimension: "mass",
17376
+ factor: .0283495,
17377
+ offset: 0
17378
+ },
17379
+ "%": {
17380
+ dimension: "percentage",
17381
+ factor: 1,
17382
+ offset: 0
17383
+ },
17384
+ L: {
17385
+ dimension: "volume",
17386
+ factor: 1,
17387
+ offset: 0
17388
+ },
17389
+ mL: {
17390
+ dimension: "volume",
17391
+ factor: .001,
17392
+ offset: 0
17393
+ },
17394
+ gal: {
17395
+ dimension: "volume",
17396
+ factor: 3.785411784,
17397
+ offset: 0
17398
+ },
17399
+ s: {
17400
+ dimension: "duration",
17401
+ factor: 1,
17402
+ offset: 0
17403
+ },
17404
+ ms: {
17405
+ dimension: "duration",
17406
+ factor: .001,
17407
+ offset: 0
17408
+ },
17409
+ min: {
17410
+ dimension: "duration",
17411
+ factor: 60,
17412
+ offset: 0
17413
+ },
17414
+ h: {
17415
+ dimension: "duration",
17416
+ factor: 3600,
17417
+ offset: 0
17418
+ },
17419
+ d: {
17420
+ dimension: "duration",
17421
+ factor: 86400,
17422
+ offset: 0
17423
+ },
17424
+ B: {
17425
+ dimension: "information",
17426
+ factor: 1,
17427
+ offset: 0
17428
+ },
17429
+ KiB: {
17430
+ dimension: "information",
17431
+ factor: 1024,
17432
+ offset: 0
17433
+ },
17434
+ MiB: {
17435
+ dimension: "information",
17436
+ factor: 1024 ** 2,
17437
+ offset: 0
17438
+ },
17439
+ GiB: {
17440
+ dimension: "information",
17441
+ factor: 1024 ** 3,
17442
+ offset: 0
17443
+ },
17444
+ TiB: {
17445
+ dimension: "information",
17446
+ factor: 1024 ** 4,
17447
+ offset: 0
17448
+ },
17449
+ kB: {
17450
+ dimension: "information",
17451
+ factor: 1e3,
17452
+ offset: 0
17453
+ },
17454
+ MB: {
17455
+ dimension: "information",
17456
+ factor: 1e6,
17457
+ offset: 0
17458
+ },
17459
+ GB: {
17460
+ dimension: "information",
17461
+ factor: 1e9,
17462
+ offset: 0
17463
+ },
17464
+ "°": {
17465
+ dimension: "angle",
17466
+ factor: 1,
17467
+ offset: 0
17468
+ },
17469
+ dBFS: {
17470
+ dimension: "sound-level",
17471
+ factor: 1,
17472
+ offset: 0
17473
+ },
17474
+ dBm: {
17475
+ dimension: "signal-level",
17476
+ factor: 1,
17477
+ offset: 0
17478
+ },
17479
+ mired: {
17480
+ dimension: "colour-temperature",
17481
+ factor: 1,
17482
+ offset: 0
17483
+ },
17484
+ kbps: {
17485
+ dimension: "bitrate",
17486
+ factor: 1,
17487
+ offset: 0
17488
+ },
17489
+ Mbps: {
17490
+ dimension: "bitrate",
17491
+ factor: 1e3,
17492
+ offset: 0
17493
+ },
17494
+ fps: {
17495
+ dimension: "rate",
17496
+ factor: 1,
17497
+ offset: 0
17498
+ }
17499
+ };
17500
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
17501
+ function specFor(unit) {
17502
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
17503
+ }
17504
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
17505
+ function toCanonical(value, spec) {
17506
+ return value * spec.factor + spec.offset;
17507
+ }
17508
+ /** Canonical-unit value → the unit described by `spec`. */
17509
+ function fromCanonical(canonical, spec) {
17510
+ return (canonical - spec.offset) / spec.factor;
17511
+ }
17512
+ /**
17513
+ * Graceful variant for the render path: returns the converted value, or
17514
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
17515
+ * Never throws — a render caller falls back to the raw value + source unit.
17516
+ */
17517
+ function tryConvertUnit(value, from, to) {
17518
+ if (from === to) return value;
17519
+ const fromSpec = specFor(from);
17520
+ if (!fromSpec) return void 0;
17521
+ const toSpec = specFor(to);
17522
+ if (!toSpec) return void 0;
17523
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
17524
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
17525
+ }
17526
+ /**
17527
+ * A number a person types for a physical quantity — the input side of the
17528
+ * value + unit contract (D972).
17529
+ *
17530
+ * Readings are stored in the base unit of their quantity (D944); a cap method
17531
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
17532
+ * wire pair {@link QuantityValue} says "this number is expressed in this
17533
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
17534
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
17535
+ */
17536
+ /** A unit that cannot be applied to the number it was given with. */
17537
+ var QuantityUnitError = class extends Error {
17538
+ code;
17539
+ constructor(code, message) {
17540
+ super(`[${code}] ${message}`);
17541
+ this.name = "QuantityUnitError";
17542
+ this.code = code;
17543
+ }
17544
+ };
17545
+ /** The wire pair: `value` is expressed in `unit`. */
17546
+ var QuantityValueSchema = object({
17547
+ value: number(),
17548
+ unit: string().min(1)
17549
+ });
17550
+ /**
17551
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
17552
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
17553
+ * portion stays in grams); the unit must belong to the quantity, checked once
17554
+ * when the cap is declared.
17555
+ */
17556
+ function paramMeta(declared) {
17557
+ const base = baseUnitOf(declared.quantity);
17558
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
17559
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
17560
+ return {
17561
+ quantity: declared.quantity,
17562
+ unit: declared.unit
17563
+ };
17564
+ }
17565
+ object({
17566
+ quantity: ReadingQuantitySchema,
17567
+ unit: string().min(1)
17568
+ });
16891
17569
  /** How a leaf compares a device field to a value. Derived from the field's
16892
17570
  * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16893
17571
  var AutomationConditionOperatorSchema = _enum([
@@ -16910,8 +17588,10 @@ var AutomationConditionLeafSchema = object({
16910
17588
  string(),
16911
17589
  number(),
16912
17590
  boolean(),
16913
- array(union([string(), number()]))
16914
- ])
17591
+ array(union([string(), number()])),
17592
+ QuantityValueSchema
17593
+ ]),
17594
+ unit: string().min(1).optional()
16915
17595
  });
16916
17596
  var AutomationConditionStationaryPresentSchema = object({
16917
17597
  kind: literal("stationary-present"),
@@ -17064,7 +17744,13 @@ var AutomationCapStepSchema = object({
17064
17744
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
17065
17745
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
17066
17746
  * the expression leaf. */
17067
- args: record(string(), unknown()).optional()
17747
+ args: record(string(), unknown()).optional(),
17748
+ /**
17749
+ * The unit the person picked per arg (display only). `args` always hold the
17750
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
17751
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
17752
+ */
17753
+ argUnits: record(string(), string().min(1)).optional()
17068
17754
  });
17069
17755
  /**
17070
17756
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -17466,6 +18152,437 @@ object({
17466
18152
  lastRunAt: number().nullable(),
17467
18153
  lastError: string().nullable()
17468
18154
  });
18155
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
18156
+ var UnitWarningSchema = object({
18157
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
18158
+ path: string(),
18159
+ deviceId: number().int().optional(),
18160
+ field: string(),
18161
+ value: number(),
18162
+ assumedUnit: string(),
18163
+ message: string()
18164
+ });
18165
+ /**
18166
+ * notification-output — canonical, capability-gated notification delivery.
18167
+ *
18168
+ * Apprise-derived model (see
18169
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
18170
+ * callers emit ONE canonical `Notification`; each provider declares a
18171
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
18172
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
18173
+ * message to what the kind supports — callers never special-case a service.
18174
+ *
18175
+ * DESIGN DECISIONS (locked):
18176
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
18177
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
18178
+ * cap. Rationale: the admin UI needs one uniform surface across the
18179
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
18180
+ * alternative would fork the UI per addon and cannot host the
18181
+ * discovery→adopt flow.
18182
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
18183
+ * the generated cap-mount auto-`concatCollection`-fans them across every
18184
+ * registered provider (notifiers addon + HA addon) so one catalog is
18185
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
18186
+ * `addonId` the generated collection router extracts from the call input.
18187
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
18188
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
18189
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
18190
+ * base64 fallback needed.
18191
+ *
18192
+ * TODO (deferred, closed-set change — separate decision): add
18193
+ * `providerKind: 'notify'` so notification providers surface on the unified
18194
+ * admin "Integrations" page.
18195
+ */
18196
+ /**
18197
+ * Zentik-derived typed-media enum — the superset across every kind. Each
18198
+ * adapter picks what it supports and the degrade engine filters the rest.
18199
+ */
18200
+ var AttachmentMediaTypeSchema = _enum([
18201
+ "image",
18202
+ "video",
18203
+ "gif",
18204
+ "audio",
18205
+ "icon"
18206
+ ]);
18207
+ /**
18208
+ * A single attachment. Exactly one of `url` (remote source, most adapters
18209
+ * prefer this) or `bytes` (inline source; required for Pushover-style
18210
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
18211
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
18212
+ */
18213
+ var AttachmentSchema = object({
18214
+ mediaType: AttachmentMediaTypeSchema,
18215
+ url: string().optional(),
18216
+ bytes: _instanceof(Uint8Array).optional(),
18217
+ mime: string().optional(),
18218
+ name: string().optional()
18219
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
18220
+ var NotificationFormatSchema = _enum([
18221
+ "text",
18222
+ "markdown",
18223
+ "html"
18224
+ ]);
18225
+ /**
18226
+ * The CLOSED icon vocabulary an action button may use.
18227
+ *
18228
+ * A closed set, not a free string, and that is the whole point: an arbitrary
18229
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
18230
+ * notices — the same class of gap as a zone vocabulary nothing produced
18231
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
18232
+ * declares `actionIcons: false` and the degrade engine strips the field.
18233
+ *
18234
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
18235
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
18236
+ * differently.
18237
+ *
18238
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
18239
+ *
18240
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
18241
+ * the whole set onto its own renderer's vocabulary through a
18242
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
18243
+ * fallback, so adding a member here fails every adapter's build until someone
18244
+ * decides its glyph, which is the only place that decision can be made
18245
+ * honestly.
18246
+ *
18247
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
18248
+ * `disarm` straight through; iOS feeds that string to
18249
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
18250
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
18251
+ * mapping, and "the field is documented" is not "the value renders".
18252
+ *
18253
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
18254
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
18255
+ * so an addon that emits a token the running hub does not know does not lose an
18256
+ * icon — its whole `send` fails Zod validation and the notification never
18257
+ * arrives. Never emit a new token from an addon before the train carrying it.
18258
+ */
18259
+ var NotificationActionIconSchema = _enum([
18260
+ "acknowledge",
18261
+ "dismiss",
18262
+ "silence",
18263
+ "snooze",
18264
+ "view",
18265
+ "play",
18266
+ "open",
18267
+ "close",
18268
+ "lock",
18269
+ "unlock",
18270
+ "arm",
18271
+ "arm-home",
18272
+ "arm-away",
18273
+ "arm-night",
18274
+ "disarm",
18275
+ "light",
18276
+ "alert",
18277
+ "camera"
18278
+ ]);
18279
+ /** A single tap-through action button. */
18280
+ var NotificationActionSchema = object({
18281
+ id: string(),
18282
+ label: string(),
18283
+ url: string().optional(),
18284
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
18285
+ icon: NotificationActionIconSchema.optional(),
18286
+ /**
18287
+ * Renders in a warning style where the notifier supports it.
18288
+ *
18289
+ * A HINT, never a gate. The callback's authority is its token and nothing
18290
+ * else — see `notification-center/action-token.ts` for what that does and
18291
+ * does not buy.
18292
+ */
18293
+ destructive: boolean().optional(),
18294
+ /**
18295
+ * How the tap should REACH the url.
18296
+ *
18297
+ * `navigate` (absent, and every button authored before this field) opens it:
18298
+ * the phone leaves the notification and shows whatever the callback returns.
18299
+ * That is right for a button whose answer the operator wants to read.
18300
+ *
18301
+ * `background` fires it as a POST and stays put. It exists for the buttons
18302
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
18303
+ * an answer to the notification, and being thrown into a browser tab to
18304
+ * confirm it costs more attention than the notification did. A backend that
18305
+ * cannot do a background call renders it as an ordinary link (the adapters
18306
+ * fall back rather than dropping the button), so this is a preference, never
18307
+ * a requirement.
18308
+ */
18309
+ mode: _enum(["navigate", "background"]).optional()
18310
+ });
18311
+ /** A delivery parameter value: what a notifier declared it takes. */
18312
+ var DeliveryParamValueSchema = union([
18313
+ string(),
18314
+ number(),
18315
+ boolean()
18316
+ ]);
18317
+ /**
18318
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
18319
+ * addresses a target of that kind may set it; a kind that declares nothing is
18320
+ * never handed one. The admin UI derives its control from this descriptor —
18321
+ * no editor knows a service by name.
18322
+ *
18323
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
18324
+ * run on the addon cap path).
18325
+ */
18326
+ var DeliveryParamSchema = object({
18327
+ key: string().min(1),
18328
+ label: string(),
18329
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
18330
+ labelKey: string().optional(),
18331
+ type: _enum([
18332
+ "enum",
18333
+ "number",
18334
+ "boolean",
18335
+ "string"
18336
+ ]),
18337
+ /** `enum` only: the closed set of accepted values. */
18338
+ options: array(object({
18339
+ value: string(),
18340
+ label: string()
18341
+ })).optional(),
18342
+ /** `number` only. */
18343
+ min: number().optional(),
18344
+ max: number().optional(),
18345
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
18346
+ default: DeliveryParamValueSchema.optional()
18347
+ });
18348
+ /**
18349
+ * The canonical notification. `body` is the only hard field (Apprise model).
18350
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
18351
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
18352
+ * the adapter maps this ordinal onto its native level. `level?` is an
18353
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
18354
+ * `priority` for that one target.
18355
+ */
18356
+ var NotificationSchema = object({
18357
+ body: string(),
18358
+ title: string().optional(),
18359
+ format: NotificationFormatSchema.default("text"),
18360
+ priority: number().int().min(1).max(5).default(3),
18361
+ level: string().optional(),
18362
+ attachments: array(AttachmentSchema).optional(),
18363
+ clickUrl: string().optional(),
18364
+ actions: array(NotificationActionSchema).optional(),
18365
+ sound: string().optional(),
18366
+ ttl: number().optional(),
18367
+ tag: string().optional(),
18368
+ deviceId: number().optional(),
18369
+ eventId: string().optional(),
18370
+ metadata: record(string(), unknown()).optional(),
18371
+ /**
18372
+ * Values for the parameters the target's kind DECLARES in
18373
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
18374
+ * declaration by the sender; a notifier reads only the keys it declared.
18375
+ */
18376
+ delivery: record(string(), DeliveryParamValueSchema).optional()
18377
+ });
18378
+ /** One declared native severity/priority level for a kind. */
18379
+ var TargetKindLevelSchema = object({
18380
+ id: string(),
18381
+ label: string(),
18382
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
18383
+ ordinal: number().int().min(1).max(5).nullable(),
18384
+ flags: object({
18385
+ critical: boolean().optional(),
18386
+ silent: boolean().optional(),
18387
+ noPush: boolean().optional()
18388
+ }).optional(),
18389
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
18390
+ requires: array(string()).optional(),
18391
+ description: string().optional()
18392
+ });
18393
+ /** The full capability block consulted before dispatch. */
18394
+ var TargetKindCapsSchema = object({
18395
+ attachments: object({
18396
+ mediaTypes: array(AttachmentMediaTypeSchema),
18397
+ mode: _enum([
18398
+ "url",
18399
+ "bytes",
18400
+ "both"
18401
+ ]),
18402
+ max: number().int().nonnegative(),
18403
+ maxBytes: number().int().positive().optional()
18404
+ }),
18405
+ /** Max action buttons (0 = none). */
18406
+ actions: number().int().nonnegative(),
18407
+ /**
18408
+ * Whether this kind renders a per-action ICON.
18409
+ *
18410
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
18411
+ * run on the addon cap path — three production failures in one day taught
18412
+ * this repo that once. Absent is read as false by the degrade engine, which
18413
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
18414
+ * assumed and dropped costs the operator's trust in the field.
18415
+ */
18416
+ actionIcons: boolean().optional(),
18417
+ levels: array(TargetKindLevelSchema),
18418
+ format: array(NotificationFormatSchema),
18419
+ clickUrl: boolean(),
18420
+ sound: boolean(),
18421
+ ttl: boolean(),
18422
+ bodyMaxLen: number().int().positive()
18423
+ });
18424
+ /**
18425
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
18426
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
18427
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
18428
+ * the union is large and not meant for runtime validation here; the exported
18429
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
18430
+ */
18431
+ var ConfigSchemaPassthrough$3 = unknown();
18432
+ var TargetKindSchema = object({
18433
+ kind: string(),
18434
+ label: string(),
18435
+ icon: string(),
18436
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
18437
+ addonId: string(),
18438
+ /**
18439
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
18440
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
18441
+ * when the addon bundles no icon for that kind — the client then falls back
18442
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
18443
+ *
18444
+ * Root-relative on purpose: it resolves against whatever origin serves a web
18445
+ * client, and a native client joins it onto its own hub base.
18446
+ *
18447
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
18448
+ * field that survived only because the runtime cap-router forwards provider
18449
+ * output verbatim — so every consumer had to re-declare it by hand to stop
18450
+ * its own Zod parse from stripping it, and the whole arrangement would have
18451
+ * broken silently the moment output validation was tightened anywhere.
18452
+ */
18453
+ iconUrl: string().optional(),
18454
+ /**
18455
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
18456
+ *
18457
+ * The server knows this and therefore says it, because the client cannot
18458
+ * safely guess: a React-Native client renders SVG and raster through two
18459
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
18460
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
18461
+ * placeholder glyph for every vector icon while the web build looked fine.
18462
+ *
18463
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
18464
+ * not been updated — a client that cannot determine the type should prefer
18465
+ * its raster path, which is the safe default for an unknown image.
18466
+ */
18467
+ iconMediaType: string().optional(),
18468
+ configSchema: ConfigSchemaPassthrough$3,
18469
+ /**
18470
+ * The config keys that decide WHERE this kind's secrets are sent (server,
18471
+ * base URL, instance, chat, recipient). A save that changes any of them
18472
+ * while leaving a secret redacted is refused by the provider (D634), so an
18473
+ * editor uses this to ask for the secret on exactly those edits instead of
18474
+ * on every edit.
18475
+ *
18476
+ * Optional: absent from a provider that holds no secret (the Home Assistant
18477
+ * kinds) and from a provider older than this field — an editor that finds
18478
+ * it absent must assume ANY edit may need the secret.
18479
+ */
18480
+ destinationKeys: array(string()).optional(),
18481
+ supportsDiscovery: boolean(),
18482
+ /**
18483
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
18484
+ * Absent = none; the rule editor then offers nothing.
18485
+ */
18486
+ deliveryParams: array(DeliveryParamSchema).optional(),
18487
+ caps: TargetKindCapsSchema
18488
+ });
18489
+ /**
18490
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
18491
+ * (return a presence marker only) when serving `listTargets` — never
18492
+ * round-trip a stored secret to the UI.
18493
+ */
18494
+ var TargetSchema = object({
18495
+ id: string(),
18496
+ name: string(),
18497
+ kind: string(),
18498
+ addonId: string(),
18499
+ enabled: boolean(),
18500
+ config: record(string(), unknown())
18501
+ });
18502
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
18503
+ var DiscoveredTargetSchema = object({
18504
+ kind: string(),
18505
+ suggestedName: string(),
18506
+ config: record(string(), unknown())
18507
+ });
18508
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
18509
+ var RenderedAsSchema = object({
18510
+ level: string(),
18511
+ format: NotificationFormatSchema,
18512
+ attachmentsSent: number().int().nonnegative(),
18513
+ actionsSent: number().int().nonnegative(),
18514
+ truncated: boolean(),
18515
+ dropped: array(string())
18516
+ });
18517
+ var SendResultSchema = object({
18518
+ success: boolean(),
18519
+ error: string().optional(),
18520
+ renderedAs: RenderedAsSchema.optional()
18521
+ });
18522
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
18523
+ var TestResultSchema = SendResultSchema;
18524
+ /**
18525
+ * Pre-save credential probe against a partial (unsaved) target config.
18526
+ * `resolvedUrl` never includes secrets (token stripped from path).
18527
+ */
18528
+ var ProbeTargetConfigResultSchema = object({
18529
+ ok: boolean(),
18530
+ resolvedUrl: string(),
18531
+ httpStatus: number().int().optional(),
18532
+ stateInstance: string().optional(),
18533
+ error: string().optional()
18534
+ });
18535
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
18536
+ kind: string(),
18537
+ config: record(string(), unknown()).optional(),
18538
+ /**
18539
+ * When editing / duplicating, the blank password field never round-trips
18540
+ * the stored token. Pass the source target id so the provider can restore
18541
+ * secrets server-side before dialling (same merge as upsert).
18542
+ */
18543
+ targetId: string().optional()
18544
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
18545
+ kind: string(),
18546
+ config: record(string(), unknown()),
18547
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
18548
+ targetId: string().optional()
18549
+ }), ProbeTargetConfigResultSchema, {
18550
+ kind: "mutation",
18551
+ auth: "admin"
18552
+ }), method(object({
18553
+ targetId: string(),
18554
+ notification: NotificationSchema
18555
+ }), SendResultSchema, {
18556
+ kind: "mutation",
18557
+ auth: "admin"
18558
+ }), method(object({
18559
+ targetId: string(),
18560
+ sample: NotificationSchema.optional()
18561
+ }), TestResultSchema, {
18562
+ kind: "mutation",
18563
+ auth: "admin"
18564
+ }), method(object({
18565
+ target: TargetSchema,
18566
+ /**
18567
+ * Duplicate flow: create a NEW target id while copying secrets from an
18568
+ * existing row. The UI never holds the stored token; blank / redacted
18569
+ * password fields are restored from this source (D634 still applies if
18570
+ * destinations diverge without a re-typed secret).
18571
+ */
18572
+ cloneSecretsFrom: string().optional()
18573
+ }), TargetSchema, {
18574
+ kind: "mutation",
18575
+ auth: "admin"
18576
+ }), method(object({ targetId: string() }), _void(), {
18577
+ kind: "mutation",
18578
+ auth: "admin"
18579
+ }), method(object({
18580
+ targetId: string(),
18581
+ enabled: boolean()
18582
+ }), _void(), {
18583
+ kind: "mutation",
18584
+ auth: "admin"
18585
+ });
17469
18586
  /**
17470
18587
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17471
18588
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
@@ -17498,52 +18615,6 @@ var AddonPageInfoSchema = object({
17498
18615
  bundleUrl: string()
17499
18616
  });
17500
18617
  method(_void(), array(AddonPageInfoSchema).readonly());
17501
- /**
17502
- * `addon-pages-source` — collection cap exposing per-provider raw page
17503
- * declarations. Every addon that contributes a UI page registers a
17504
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
17505
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
17506
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
17507
- * that admin-ui consumes.
17508
- *
17509
- * The split exists because the public listing has a different output
17510
- * shape than the per-provider raw declarations, and we want both ends
17511
- * to flow through codegen instead of relying on a hand-written wrapper.
17512
- */
17513
- var AddonPageDeclarationSchema = object({
17514
- id: string(),
17515
- label: string(),
17516
- icon: string(),
17517
- path: string(),
17518
- /**
17519
- * Module Federation remote name — must match the `name` field on the
17520
- * page addon's `federation()` plugin config. Used by admin-ui's
17521
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
17522
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
17523
- * contain hyphens).
17524
- */
17525
- remoteName: string(),
17526
- /**
17527
- * Bundle filename inside the addon's `dist/` dir served at
17528
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
17529
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
17530
- * the static-file route can compute an mtime-based cache-buster URL
17531
- * without a separate filesystem stat.
17532
- */
17533
- bundle: string(),
17534
- /**
17535
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
17536
- * `'cluster'`, `'administration'` — the page renders inside that group.
17537
- * Any OTHER string creates (or joins) a custom section rendered after
17538
- * the built-in groups; its label comes from `sectionLabel` (first
17539
- * declaration wins), falling back to the id. Absent → the legacy
17540
- * "Addon Pages" group.
17541
- */
17542
- section: string().optional(),
17543
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
17544
- sectionLabel: string().optional()
17545
- });
17546
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
17547
18618
  var AddonHttpRouteSchema = object({
17548
18619
  method: _enum([
17549
18620
  "GET",
@@ -20215,7 +21286,11 @@ var CompositionProblemSchema = object({
20215
21286
  "mixed-quantity",
20216
21287
  "helper-mismatch",
20217
21288
  "member-constraint",
20218
- "retention"
21289
+ "retention",
21290
+ "command-method-unknown",
21291
+ "command-source-unknown",
21292
+ "command-input-unknown",
21293
+ "command-input-missing"
20219
21294
  ]),
20220
21295
  path: string(),
20221
21296
  message: string()
@@ -20254,7 +21329,9 @@ var CompositionFieldRoleSchema = _enum([
20254
21329
  var CompositionFieldItemSchema = object({
20255
21330
  arrayPath: string(),
20256
21331
  key: string(),
20257
- label: string()
21332
+ label: string(),
21333
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
21334
+ resettable: boolean().optional()
20258
21335
  });
20259
21336
  var CompositionFieldPlanSchema = object({
20260
21337
  cap: string(),
@@ -20459,7 +21536,7 @@ Object.freeze({
20459
21536
  * One catalog row per base reading a group helper can build (D944 §3).
20460
21537
  * Data, not a form default written into the block.
20461
21538
  */
20462
- var NOW$2 = {
21539
+ var NOW$3 = {
20463
21540
  fieldPath: "lastFetchedAt",
20464
21541
  expr: "now"
20465
21542
  };
@@ -20511,7 +21588,7 @@ var GROUP_TEMPLATES = [
20511
21588
  quantity: "temperature",
20512
21589
  defaultAggregate: "mean",
20513
21590
  empty: "null",
20514
- companions: [NOW$2],
21591
+ companions: [NOW$3],
20515
21592
  alsoFrom: {
20516
21593
  deviceTypes: CLIMATE_DEVICES,
20517
21594
  accept: {
@@ -20529,7 +21606,7 @@ var GROUP_TEMPLATES = [
20529
21606
  quantity: "humidity",
20530
21607
  defaultAggregate: "mean",
20531
21608
  empty: "null",
20532
- companions: [NOW$2],
21609
+ companions: [NOW$3],
20533
21610
  alsoFrom: {
20534
21611
  deviceTypes: CLIMATE_DEVICES,
20535
21612
  accept: {
@@ -20547,7 +21624,7 @@ var GROUP_TEMPLATES = [
20547
21624
  quantity: "pressure",
20548
21625
  defaultAggregate: "mean",
20549
21626
  empty: "null",
20550
- companions: [NOW$2]
21627
+ companions: [NOW$3]
20551
21628
  }),
20552
21629
  numeric({
20553
21630
  id: "illuminance",
@@ -20558,7 +21635,7 @@ var GROUP_TEMPLATES = [
20558
21635
  quantity: "illuminance",
20559
21636
  defaultAggregate: "mean",
20560
21637
  empty: "null",
20561
- companions: [NOW$2]
21638
+ companions: [NOW$3]
20562
21639
  }),
20563
21640
  numeric({
20564
21641
  id: "power",
@@ -20569,7 +21646,7 @@ var GROUP_TEMPLATES = [
20569
21646
  quantity: "power",
20570
21647
  defaultAggregate: "sum",
20571
21648
  empty: "omit",
20572
- companions: [NOW$2]
21649
+ companions: [NOW$3]
20573
21650
  }),
20574
21651
  numeric({
20575
21652
  id: "energy",
@@ -20580,7 +21657,7 @@ var GROUP_TEMPLATES = [
20580
21657
  quantity: "energy",
20581
21658
  defaultAggregate: "sum",
20582
21659
  empty: "omit",
20583
- companions: [NOW$2]
21660
+ companions: [NOW$3]
20584
21661
  }),
20585
21662
  numeric({
20586
21663
  id: "battery",
@@ -20615,7 +21692,7 @@ var GROUP_TEMPLATES = [
20615
21692
  quantity: "device",
20616
21693
  defaultAggregate: "mean",
20617
21694
  empty: "null",
20618
- companions: [NOW$2]
21695
+ companions: [NOW$3]
20619
21696
  }),
20620
21697
  binary({
20621
21698
  id: "motion",
@@ -22565,116 +23642,6 @@ var DeviceAdminLinkSchema = object({
22565
23642
  });
22566
23643
  method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
22567
23644
  /**
22568
- * Identity envelope for a device's upstream-system metadata.
22569
- *
22570
- * Two jobs:
22571
- *
22572
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
22573
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
22574
- * WS-Discovery endpoint UUID, …). Provider code keeps a
22575
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
22576
- * without round-tripping through `stableId` / `id` mappings.
22577
- *
22578
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
22579
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
22580
- * for vendors where MAC is the natural key). For systems where the
22581
- * live dispatch key can be renamed (HA `entity_id` can be edited),
22582
- * providers detect renames by matching `uniqueId` and patch `id`
22583
- * in-place so dispatch keeps working.
22584
- *
22585
- * **Display unit and precision are NOT stored here.** They are a read-time
22586
- * preference (D944). A polymorphic reading stores its `quantity` here once,
22587
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
22588
- * neither is repeated on each sample. The `device_class` tag is an
22589
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
22590
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
22591
- *
22592
- * The `raw` escape hatch carries provider-specific attributes that don't
22593
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
22594
- * details, …). Providers control what lands here.
22595
- */
22596
- var SourceInfoSchema = object({
22597
- /** Live dispatch key — mutable when the source system allows rename. */
22598
- id: string(),
22599
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
22600
- system: string(),
22601
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
22602
- * Used to detect rename when `id` changes. */
22603
- uniqueId: string().optional(),
22604
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
22605
- quantity: ReadingQuantitySchema.optional(),
22606
- /** Date / time / datetime shape of a string reading, written once at adopt. */
22607
- dateTimeShape: _enum([
22608
- "date",
22609
- "time",
22610
- "datetime"
22611
- ]).optional(),
22612
- /** Provider-specific extras that don't fit the structured slots.
22613
- * Whitelist what goes here per provider — do NOT dump entire HA
22614
- * attribute blobs (would explode the meta JSON column). */
22615
- raw: record(string(), unknown()).optional()
22616
- });
22617
- /**
22618
- * Build the synthetic SourceInfo every existing provider falls back to
22619
- * when no upstream value has been persisted. Keeps non-migrated providers
22620
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
22621
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
22622
- * id. Real upstream identity replaces this once a provider migrates and
22623
- * calls `updateSourceInfo()` with concrete values.
22624
- */
22625
- function synthesizeSourceInfo(input) {
22626
- return {
22627
- id: input.stableId,
22628
- system: input.addonId
22629
- };
22630
- }
22631
- /**
22632
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
22633
- * patch values are ignored (no field clobber); to clear a field
22634
- * explicitly, providers omit it from the patch and rebuild from scratch
22635
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
22636
- * bother with a `null`-means-delete sentinel.
22637
- *
22638
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
22639
- * merges over existing `raw` keys instead of replacing the whole bag.
22640
- */
22641
- function mergeSourceInfo(prev, patch) {
22642
- const next = { ...prev };
22643
- for (const [k, v] of Object.entries(patch)) {
22644
- if (v === void 0) continue;
22645
- if (k === "raw" && typeof v === "object" && v !== null) {
22646
- next.raw = {
22647
- ...prev.raw,
22648
- ...v
22649
- };
22650
- continue;
22651
- }
22652
- next[k] = v;
22653
- }
22654
- return next;
22655
- }
22656
- /**
22657
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
22658
- * blob is a free-form `Record<string, unknown>` shared with hardware-
22659
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
22660
- * lives under a single nested key to keep it composable with those.
22661
- */
22662
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
22663
- /**
22664
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
22665
- * with Zod. Returns `null` when the blob is missing or the nested
22666
- * `sourceInfo` key fails validation — callers fall back to the
22667
- * synthetic default in that case (see `synthesizeSourceInfo`).
22668
- */
22669
- function extractSourceInfoFromMetadata(metadata) {
22670
- if (!metadata) return null;
22671
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
22672
- if (!raw) return null;
22673
- const parsed = SourceInfoSchema.safeParse(raw);
22674
- if (!parsed.success) return null;
22675
- return parsed.data;
22676
- }
22677
- /**
22678
23645
  * device-discovery — device-scoped capability for parents that host /
22679
23646
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
22680
23647
  * integrations). The parent advertises a list of discoverable children,
@@ -23079,12 +24046,48 @@ var ExposeInputSchema = object({
23079
24046
  capabilities: array(string()).optional()
23080
24047
  });
23081
24048
  var UnexposeInputSchema = object({ deviceId: string() });
24049
+ /**
24050
+ * What an exporter may expose besides devices (D968). An action and a
24051
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
24052
+ * exporter's own selection, not the device registry.
24053
+ */
24054
+ var ExportEntityKindSchema = _enum([
24055
+ "device",
24056
+ "action",
24057
+ "notifier"
24058
+ ]);
24059
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
24060
+ object({
24061
+ kind: literal("device"),
24062
+ deviceId: string()
24063
+ }),
24064
+ object({
24065
+ kind: literal("action"),
24066
+ actionId: string().min(1)
24067
+ }),
24068
+ object({
24069
+ kind: literal("notifier"),
24070
+ targetId: string().min(1)
24071
+ })
24072
+ ]);
24073
+ var ExposedEntitySchema = object({
24074
+ entity: ExportEntityRefSchema,
24075
+ exposedAs: string().optional()
24076
+ });
24077
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
24078
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
23082
24079
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
23083
24080
  kind: "mutation",
23084
24081
  auth: "admin"
23085
24082
  }), method(UnexposeInputSchema, _void(), {
23086
24083
  kind: "mutation",
23087
24084
  auth: "admin"
24085
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
24086
+ kind: "mutation",
24087
+ auth: "admin"
24088
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
24089
+ kind: "mutation",
24090
+ auth: "admin"
23088
24091
  });
23089
24092
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
23090
24093
  var ICON_CATEGORY_IDS = [
@@ -45150,7 +46153,7 @@ var LlmProviderWidgetSchema = object({
45150
46153
  /** ConfigUISchema tree passed through untyped on the wire (the
45151
46154
  * notification-output `ConfigSchemaPassthrough` precedent at
45152
46155
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
45153
- var ConfigSchemaPassthrough$3 = unknown();
46156
+ var ConfigSchemaPassthrough$2 = unknown();
45154
46157
  var LlmProfileKindDescriptorSchema = object({
45155
46158
  kind: LlmProfileKindSchema,
45156
46159
  label: string(),
@@ -45159,9 +46162,9 @@ var LlmProfileKindDescriptorSchema = object({
45159
46162
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
45160
46163
  addonId: string(),
45161
46164
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
45162
- connectionSchema: ConfigSchemaPassthrough$3,
46165
+ connectionSchema: ConfigSchemaPassthrough$2,
45163
46166
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
45164
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
46167
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
45165
46168
  /** The kind's defaults; a model from `listModels` may override any of them. */
45166
46169
  features: LlmModelFeaturesSchema,
45167
46170
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -45687,10 +46690,10 @@ var CpuBreakdownSchema = object({
45687
46690
  cores: number()
45688
46691
  });
45689
46692
  var MemoryInfoSchema = object({
45690
- percent: number(),
46693
+ percent: number().nullable(),
45691
46694
  totalBytes: number(),
45692
- usedBytes: number(),
45693
- availableBytes: number(),
46695
+ usedBytes: number().nullable(),
46696
+ availableBytes: number().nullable(),
45694
46697
  swapUsedBytes: number(),
45695
46698
  swapTotalBytes: number()
45696
46699
  });
@@ -45994,8 +46997,9 @@ var GetLoadSeriesInputSchema = object({
45994
46997
  });
45995
46998
  var SystemMetricsSchema = object({
45996
46999
  cpuPercent: number(),
45997
- memoryPercent: number(),
45998
- memoryUsedMB: number(),
47000
+ /** `null` when the memory sample failed or was not taken yet (D393). */
47001
+ memoryPercent: number().nullable(),
47002
+ memoryUsedMB: number().nullable(),
45999
47003
  memoryTotalMB: number(),
46000
47004
  diskPercent: number().optional(),
46001
47005
  temperature: number().optional(),
@@ -46231,427 +47235,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46231
47235
  sourcePort: number().optional()
46232
47236
  });
46233
47237
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
46234
- /**
46235
- * notification-output — canonical, capability-gated notification delivery.
46236
- *
46237
- * Apprise-derived model (see
46238
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
46239
- * callers emit ONE canonical `Notification`; each provider declares a
46240
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
46241
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
46242
- * message to what the kind supports — callers never special-case a service.
46243
- *
46244
- * DESIGN DECISIONS (locked):
46245
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
46246
- * `setTargetEnabled`), each provider persisting via the `settings-store`
46247
- * cap. Rationale: the admin UI needs one uniform surface across the
46248
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
46249
- * alternative would fork the UI per addon and cannot host the
46250
- * discovery→adopt flow.
46251
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
46252
- * the generated cap-mount auto-`concatCollection`-fans them across every
46253
- * registered provider (notifiers addon + HA addon) so one catalog is
46254
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
46255
- * `addonId` the generated collection router extracts from the call input.
46256
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
46257
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
46258
- * `storage` / `storage-provider` / `recording` caps over the same path. No
46259
- * base64 fallback needed.
46260
- *
46261
- * TODO (deferred, closed-set change — separate decision): add
46262
- * `providerKind: 'notify'` so notification providers surface on the unified
46263
- * admin "Integrations" page.
46264
- */
46265
- /**
46266
- * Zentik-derived typed-media enum — the superset across every kind. Each
46267
- * adapter picks what it supports and the degrade engine filters the rest.
46268
- */
46269
- var AttachmentMediaTypeSchema = _enum([
46270
- "image",
46271
- "video",
46272
- "gif",
46273
- "audio",
46274
- "icon"
46275
- ]);
46276
- /**
46277
- * A single attachment. Exactly one of `url` (remote source, most adapters
46278
- * prefer this) or `bytes` (inline source; required for Pushover-style
46279
- * bytes-only kinds) MUST be present — the degrade engine expresses a
46280
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
46281
- */
46282
- var AttachmentSchema = object({
46283
- mediaType: AttachmentMediaTypeSchema,
46284
- url: string().optional(),
46285
- bytes: _instanceof(Uint8Array).optional(),
46286
- mime: string().optional(),
46287
- name: string().optional()
46288
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
46289
- var NotificationFormatSchema = _enum([
46290
- "text",
46291
- "markdown",
46292
- "html"
46293
- ]);
46294
- /**
46295
- * The CLOSED icon vocabulary an action button may use.
46296
- *
46297
- * A closed set, not a free string, and that is the whole point: an arbitrary
46298
- * icon name is one that ntfy renders, zentik silently drops, and nobody
46299
- * notices — the same class of gap as a zone vocabulary nothing produced
46300
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
46301
- * declares `actionIcons: false` and the degrade engine strips the field.
46302
- *
46303
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
46304
- * renderer's icon set; "acknowledge" survives an adapter that draws it
46305
- * differently.
46306
- *
46307
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
46308
- *
46309
- * These names are for US. **No adapter may forward one verbatim.** Each maps
46310
- * the whole set onto its own renderer's vocabulary through a
46311
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
46312
- * fallback, so adding a member here fails every adapter's build until someone
46313
- * decides its glyph, which is the only place that decision can be made
46314
- * honestly.
46315
- *
46316
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
46317
- * `disarm` straight through; iOS feeds that string to
46318
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
46319
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
46320
- * mapping, and "the field is documented" is not "the value renders".
46321
- *
46322
- * Adding a member is TRAIN-BOUND. The enum lives in the published
46323
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
46324
- * so an addon that emits a token the running hub does not know does not lose an
46325
- * icon — its whole `send` fails Zod validation and the notification never
46326
- * arrives. Never emit a new token from an addon before the train carrying it.
46327
- */
46328
- var NotificationActionIconSchema = _enum([
46329
- "acknowledge",
46330
- "dismiss",
46331
- "silence",
46332
- "snooze",
46333
- "view",
46334
- "play",
46335
- "open",
46336
- "close",
46337
- "lock",
46338
- "unlock",
46339
- "arm",
46340
- "arm-home",
46341
- "arm-away",
46342
- "arm-night",
46343
- "disarm",
46344
- "light",
46345
- "alert",
46346
- "camera"
46347
- ]);
46348
- /** A single tap-through action button. */
46349
- var NotificationActionSchema = object({
46350
- id: string(),
46351
- label: string(),
46352
- url: string().optional(),
46353
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
46354
- icon: NotificationActionIconSchema.optional(),
46355
- /**
46356
- * Renders in a warning style where the notifier supports it.
46357
- *
46358
- * A HINT, never a gate. The callback's authority is its token and nothing
46359
- * else — see `notification-center/action-token.ts` for what that does and
46360
- * does not buy.
46361
- */
46362
- destructive: boolean().optional(),
46363
- /**
46364
- * How the tap should REACH the url.
46365
- *
46366
- * `navigate` (absent, and every button authored before this field) opens it:
46367
- * the phone leaves the notification and shows whatever the callback returns.
46368
- * That is right for a button whose answer the operator wants to read.
46369
- *
46370
- * `background` fires it as a POST and stays put. It exists for the buttons
46371
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
46372
- * an answer to the notification, and being thrown into a browser tab to
46373
- * confirm it costs more attention than the notification did. A backend that
46374
- * cannot do a background call renders it as an ordinary link (the adapters
46375
- * fall back rather than dropping the button), so this is a preference, never
46376
- * a requirement.
46377
- */
46378
- mode: _enum(["navigate", "background"]).optional()
46379
- });
46380
- /** A delivery parameter value: what a notifier declared it takes. */
46381
- var DeliveryParamValueSchema = union([
46382
- string(),
46383
- number(),
46384
- boolean()
46385
- ]);
46386
- /**
46387
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
46388
- * addresses a target of that kind may set it; a kind that declares nothing is
46389
- * never handed one. The admin UI derives its control from this descriptor —
46390
- * no editor knows a service by name.
46391
- *
46392
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
46393
- * run on the addon cap path).
46394
- */
46395
- var DeliveryParamSchema = object({
46396
- key: string().min(1),
46397
- label: string(),
46398
- /** i18n key of the label, resolved by the declaring addon's namespace. */
46399
- labelKey: string().optional(),
46400
- type: _enum([
46401
- "enum",
46402
- "number",
46403
- "boolean",
46404
- "string"
46405
- ]),
46406
- /** `enum` only: the closed set of accepted values. */
46407
- options: array(object({
46408
- value: string(),
46409
- label: string()
46410
- })).optional(),
46411
- /** `number` only. */
46412
- min: number().optional(),
46413
- max: number().optional(),
46414
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
46415
- default: DeliveryParamValueSchema.optional()
46416
- });
46417
- /**
46418
- * The canonical notification. `body` is the only hard field (Apprise model).
46419
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
46420
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
46421
- * the adapter maps this ordinal onto its native level. `level?` is an
46422
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
46423
- * `priority` for that one target.
46424
- */
46425
- var NotificationSchema = object({
46426
- body: string(),
46427
- title: string().optional(),
46428
- format: NotificationFormatSchema.default("text"),
46429
- priority: number().int().min(1).max(5).default(3),
46430
- level: string().optional(),
46431
- attachments: array(AttachmentSchema).optional(),
46432
- clickUrl: string().optional(),
46433
- actions: array(NotificationActionSchema).optional(),
46434
- sound: string().optional(),
46435
- ttl: number().optional(),
46436
- tag: string().optional(),
46437
- deviceId: number().optional(),
46438
- eventId: string().optional(),
46439
- metadata: record(string(), unknown()).optional(),
46440
- /**
46441
- * Values for the parameters the target's kind DECLARES in
46442
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
46443
- * declaration by the sender; a notifier reads only the keys it declared.
46444
- */
46445
- delivery: record(string(), DeliveryParamValueSchema).optional()
46446
- });
46447
- /** One declared native severity/priority level for a kind. */
46448
- var TargetKindLevelSchema = object({
46449
- id: string(),
46450
- label: string(),
46451
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
46452
- ordinal: number().int().min(1).max(5).nullable(),
46453
- flags: object({
46454
- critical: boolean().optional(),
46455
- silent: boolean().optional(),
46456
- noPush: boolean().optional()
46457
- }).optional(),
46458
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
46459
- requires: array(string()).optional(),
46460
- description: string().optional()
46461
- });
46462
- /** The full capability block consulted before dispatch. */
46463
- var TargetKindCapsSchema = object({
46464
- attachments: object({
46465
- mediaTypes: array(AttachmentMediaTypeSchema),
46466
- mode: _enum([
46467
- "url",
46468
- "bytes",
46469
- "both"
46470
- ]),
46471
- max: number().int().nonnegative(),
46472
- maxBytes: number().int().positive().optional()
46473
- }),
46474
- /** Max action buttons (0 = none). */
46475
- actions: number().int().nonnegative(),
46476
- /**
46477
- * Whether this kind renders a per-action ICON.
46478
- *
46479
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
46480
- * run on the addon cap path — three production failures in one day taught
46481
- * this repo that once. Absent is read as false by the degrade engine, which
46482
- * is the safe direction: an icon that is not rendered costs nothing, an icon
46483
- * assumed and dropped costs the operator's trust in the field.
46484
- */
46485
- actionIcons: boolean().optional(),
46486
- levels: array(TargetKindLevelSchema),
46487
- format: array(NotificationFormatSchema),
46488
- clickUrl: boolean(),
46489
- sound: boolean(),
46490
- ttl: boolean(),
46491
- bodyMaxLen: number().int().positive()
46492
- });
46493
- /**
46494
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
46495
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
46496
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
46497
- * the union is large and not meant for runtime validation here; the exported
46498
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46499
- */
46500
- var ConfigSchemaPassthrough$2 = unknown();
46501
- var TargetKindSchema = object({
46502
- kind: string(),
46503
- label: string(),
46504
- icon: string(),
46505
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
46506
- addonId: string(),
46507
- /**
46508
- * URL of the kind's bundled BRAND icon, served by the providing addon over
46509
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
46510
- * when the addon bundles no icon for that kind — the client then falls back
46511
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
46512
- *
46513
- * Root-relative on purpose: it resolves against whatever origin serves a web
46514
- * client, and a native client joins it onto its own hub base.
46515
- *
46516
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
46517
- * field that survived only because the runtime cap-router forwards provider
46518
- * output verbatim — so every consumer had to re-declare it by hand to stop
46519
- * its own Zod parse from stripping it, and the whole arrangement would have
46520
- * broken silently the moment output validation was tightened anywhere.
46521
- */
46522
- iconUrl: string().optional(),
46523
- /**
46524
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
46525
- *
46526
- * The server knows this and therefore says it, because the client cannot
46527
- * safely guess: a React-Native client renders SVG and raster through two
46528
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
46529
- * not decode SVG on iOS/Android), so without this it silently fell back to a
46530
- * placeholder glyph for every vector icon while the web build looked fine.
46531
- *
46532
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
46533
- * not been updated — a client that cannot determine the type should prefer
46534
- * its raster path, which is the safe default for an unknown image.
46535
- */
46536
- iconMediaType: string().optional(),
46537
- configSchema: ConfigSchemaPassthrough$2,
46538
- /**
46539
- * The config keys that decide WHERE this kind's secrets are sent (server,
46540
- * base URL, instance, chat, recipient). A save that changes any of them
46541
- * while leaving a secret redacted is refused by the provider (D634), so an
46542
- * editor uses this to ask for the secret on exactly those edits instead of
46543
- * on every edit.
46544
- *
46545
- * Optional: absent from a provider that holds no secret (the Home Assistant
46546
- * kinds) and from a provider older than this field — an editor that finds
46547
- * it absent must assume ANY edit may need the secret.
46548
- */
46549
- destinationKeys: array(string()).optional(),
46550
- supportsDiscovery: boolean(),
46551
- /**
46552
- * Extra delivery parameters a rule may set for a target of this kind (D938).
46553
- * Absent = none; the rule editor then offers nothing.
46554
- */
46555
- deliveryParams: array(DeliveryParamSchema).optional(),
46556
- caps: TargetKindCapsSchema
46557
- });
46558
- /**
46559
- * A persisted target. `config` holds secrets; providers REDACT secret fields
46560
- * (return a presence marker only) when serving `listTargets` — never
46561
- * round-trip a stored secret to the UI.
46562
- */
46563
- var TargetSchema = object({
46564
- id: string(),
46565
- name: string(),
46566
- kind: string(),
46567
- addonId: string(),
46568
- enabled: boolean(),
46569
- config: record(string(), unknown())
46570
- });
46571
- /** A discovery-surfaced candidate (config is partial + non-secret). */
46572
- var DiscoveredTargetSchema = object({
46573
- kind: string(),
46574
- suggestedName: string(),
46575
- config: record(string(), unknown())
46576
- });
46577
- /** The degrade engine's report — what was resolved / dropped / degraded. */
46578
- var RenderedAsSchema = object({
46579
- level: string(),
46580
- format: NotificationFormatSchema,
46581
- attachmentsSent: number().int().nonnegative(),
46582
- actionsSent: number().int().nonnegative(),
46583
- truncated: boolean(),
46584
- dropped: array(string())
46585
- });
46586
- var SendResultSchema = object({
46587
- success: boolean(),
46588
- error: string().optional(),
46589
- renderedAs: RenderedAsSchema.optional()
46590
- });
46591
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
46592
- var TestResultSchema = SendResultSchema;
46593
- /**
46594
- * Pre-save credential probe against a partial (unsaved) target config.
46595
- * `resolvedUrl` never includes secrets (token stripped from path).
46596
- */
46597
- var ProbeTargetConfigResultSchema = object({
46598
- ok: boolean(),
46599
- resolvedUrl: string(),
46600
- httpStatus: number().int().optional(),
46601
- stateInstance: string().optional(),
46602
- error: string().optional()
46603
- });
46604
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
46605
- kind: string(),
46606
- config: record(string(), unknown()).optional(),
46607
- /**
46608
- * When editing / duplicating, the blank password field never round-trips
46609
- * the stored token. Pass the source target id so the provider can restore
46610
- * secrets server-side before dialling (same merge as upsert).
46611
- */
46612
- targetId: string().optional()
46613
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
46614
- kind: string(),
46615
- config: record(string(), unknown()),
46616
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
46617
- targetId: string().optional()
46618
- }), ProbeTargetConfigResultSchema, {
46619
- kind: "mutation",
46620
- auth: "admin"
46621
- }), method(object({
46622
- targetId: string(),
46623
- notification: NotificationSchema
46624
- }), SendResultSchema, {
46625
- kind: "mutation",
46626
- auth: "admin"
46627
- }), method(object({
46628
- targetId: string(),
46629
- sample: NotificationSchema.optional()
46630
- }), TestResultSchema, {
46631
- kind: "mutation",
46632
- auth: "admin"
46633
- }), method(object({
46634
- target: TargetSchema,
46635
- /**
46636
- * Duplicate flow: create a NEW target id while copying secrets from an
46637
- * existing row. The UI never holds the stored token; blank / redacted
46638
- * password fields are restored from this source (D634 still applies if
46639
- * destinations diverge without a re-typed secret).
46640
- */
46641
- cloneSecretsFrom: string().optional()
46642
- }), TargetSchema, {
46643
- kind: "mutation",
46644
- auth: "admin"
46645
- }), method(object({ targetId: string() }), _void(), {
46646
- kind: "mutation",
46647
- auth: "admin"
46648
- }), method(object({
46649
- targetId: string(),
46650
- enabled: boolean()
46651
- }), _void(), {
46652
- kind: "mutation",
46653
- auth: "admin"
46654
- });
46655
47238
  var AUDIO_MACRO_LABELS = [
46656
47239
  {
46657
47240
  id: "speech",
@@ -54896,14 +55479,6 @@ var EngineProvisioningSchema = object({
54896
55479
  */
54897
55480
  configIssues: array(string()).optional()
54898
55481
  });
54899
- var PipelineStepInputSchema = lazy(() => object({
54900
- addonId: string(),
54901
- modelId: string().optional(),
54902
- enabled: boolean().default(true),
54903
- children: array(PipelineStepInputSchema).optional(),
54904
- settings: record(string(), unknown()).optional(),
54905
- jumpDeviceKey: string().optional()
54906
- }));
54907
55482
  var ModelSubstitutionSchema = object({
54908
55483
  addonId: string(),
54909
55484
  chosen: string(),
@@ -54982,7 +55557,6 @@ var ConfigUISchemaBridge = custom();
54982
55557
  var ConfigUISchemaNullableBridge = custom();
54983
55558
  var InferenceCapabilitiesBridge = custom();
54984
55559
  var ModelAvailabilityListBridge = custom();
54985
- var PipelineRunResultBridge = custom();
54986
55560
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
54987
55561
  modelId: string(),
54988
55562
  settings: record(string(), unknown()).readonly()
@@ -57978,11 +58552,13 @@ var ImageContractSchema = object({
57978
58552
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
57979
58553
  * produced it.
57980
58554
  *
57981
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
57982
- * update is an ANNOUNCEMENT. The notification body carries the exact command
57983
- * for a `docker` shell and says the app restarts into the new build for
57984
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
57985
- * nothing reads is the defect this repo keeps paying for.
58555
+ * There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
58556
+ * ANNOUNCEMENT. The notification body carries the exact command for a `docker`
58557
+ * shell and says the app restarts into the new build for `electron`, and that
58558
+ * sentence is chosen from `kind` alone — a boolean nothing reads is the defect
58559
+ * this repo keeps paying for. The one shell that CAN be acted on is the
58560
+ * Electron app, and only through `installDesktopUpdate` (see
58561
+ * {@link DesktopAppStatusSchema} for what the node reads from the app first).
57986
58562
  */
57987
58563
  var WrapperIdentitySchema = object({
57988
58564
  kind: _enum([
@@ -58030,6 +58606,49 @@ var ServerRollbackInfoSchema = object({
58030
58606
  reason: string()
58031
58607
  });
58032
58608
  /**
58609
+ * What the native desktop app (Electron) that hosts this node says about
58610
+ * itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
58611
+ * Absent on the status = no desktop app, or it never wrote the file.
58612
+ *
58613
+ * `stale` is the honest answer to an app that stopped writing (closed, hung,
58614
+ * crashed): the update facts are then the LAST ones known, not current ones,
58615
+ * and a consumer must not read `update.available: false` from a stale record
58616
+ * as "no update". `stateAtMs` is when the app last wrote.
58617
+ */
58618
+ var DesktopAppStatusSchema = object({
58619
+ /** The Electron app's own version (not the node closure's). */
58620
+ version: string(),
58621
+ update: object({
58622
+ available: boolean(),
58623
+ /** The new build is downloaded and `installDesktopUpdate` can apply it. */
58624
+ downloaded: boolean(),
58625
+ version: string().nullable(),
58626
+ checking: boolean(),
58627
+ /** Why the app's last update check produced no answer; `null` when it did. */
58628
+ error: string().nullable()
58629
+ }),
58630
+ stateAtMs: number(),
58631
+ stale: boolean()
58632
+ });
58633
+ /**
58634
+ * Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
58635
+ * - `not-electron` — this node does not run inside the desktop app.
58636
+ * - `no-app-state` — the app never published its state (or the file is unreadable).
58637
+ * - `app-state-stale` — the app stopped writing; it is probably not running.
58638
+ * - `not-downloaded` — no update is downloaded yet; nothing to install.
58639
+ */
58640
+ var InstallDesktopUpdateReasonSchema = _enum([
58641
+ "not-electron",
58642
+ "no-app-state",
58643
+ "app-state-stale",
58644
+ "not-downloaded"
58645
+ ]);
58646
+ var InstallDesktopUpdateResultSchema = object({
58647
+ accepted: boolean(),
58648
+ /** Non-null exactly when `accepted` is false. */
58649
+ reason: InstallDesktopUpdateReasonSchema.nullable()
58650
+ });
58651
+ /**
58033
58652
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
58034
58653
  * `resolveRunnerId` over its forked addons; agent: the runners its process
58035
58654
  * service holds, because only the hub plans) against the states the process
@@ -58133,7 +58752,12 @@ var ServerPackageStatusSchema = object({
58133
58752
  * on a provider that predates it. Absent is "not checked yet", never "all
58134
58753
  * well": a consumer that needs the answer treats it as pending.
58135
58754
  */
58136
- runners: RunnerBootCheckSchema.optional()
58755
+ runners: RunnerBootCheckSchema.optional(),
58756
+ /**
58757
+ * The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
58758
+ * Optional for version skew AND because most nodes have no desktop app.
58759
+ */
58760
+ desktopApp: DesktopAppStatusSchema.optional()
58137
58761
  });
58138
58762
  var ServerUpdateCheckResultSchema = object({
58139
58763
  packageName: string(),
@@ -58216,6 +58840,12 @@ var ServerUpdateActionResultSchema = object({
58216
58840
  */
58217
58841
  blockers: array(UpdateBlockerSchema).optional()
58218
58842
  });
58843
+ var LaunchOverridesSchema = object({
58844
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
58845
+ heapMb: number().int().nullable() });
58846
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
58847
+ /** Always true: the override is read at launch, not by the running process. */
58848
+ restartRequired: boolean() });
58219
58849
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
58220
58850
  kind: "mutation",
58221
58851
  auth: "admin"
@@ -58232,12 +58862,18 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
58232
58862
  kind: "mutation",
58233
58863
  auth: "admin",
58234
58864
  timeoutMs: 16 * 6e4
58865
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
58866
+ kind: "mutation",
58867
+ auth: "admin"
58235
58868
  }), method(_void(), ServerUpdateActionResultSchema, {
58236
58869
  kind: "mutation",
58237
58870
  auth: "admin"
58238
58871
  }), method(_void(), ServerUpdateActionResultSchema, {
58239
58872
  kind: "mutation",
58240
58873
  auth: "admin"
58874
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
58875
+ kind: "mutation",
58876
+ auth: "admin"
58241
58877
  });
58242
58878
  /**
58243
58879
  * `smtp-provider` — pluggable email delivery surface.
@@ -61379,13 +62015,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
61379
62015
  }), method(ActionIdInput, _void(), {
61380
62016
  kind: "mutation",
61381
62017
  auth: "admin"
61382
- }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
62018
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
62019
+ action: ActionEntrySchema,
62020
+ warnings: array(UnitWarningSchema).optional()
62021
+ }), {
61383
62022
  kind: "mutation",
61384
62023
  auth: "admin"
61385
62024
  }), method(object({
61386
62025
  actionId: string().min(1),
61387
62026
  action: ActionInputSchema.partial()
61388
- }), object({ action: ActionEntrySchema }), {
62027
+ }), object({
62028
+ action: ActionEntrySchema,
62029
+ warnings: array(UnitWarningSchema).optional()
62030
+ }), {
61389
62031
  kind: "mutation",
61390
62032
  auth: "admin"
61391
62033
  }), method(ActionIdInput, _void(), {
@@ -61967,6 +62609,30 @@ object({
61967
62609
  at: number()
61968
62610
  }).nullable()
61969
62611
  });
62612
+ var RemoteButtonShapeSchema = _enum([
62613
+ "rect",
62614
+ "rounded",
62615
+ "round",
62616
+ "pill"
62617
+ ]);
62618
+ var RemoteButtonToneSchema = _enum([
62619
+ "neutral",
62620
+ "accent",
62621
+ "danger",
62622
+ "red",
62623
+ "green",
62624
+ "yellow",
62625
+ "blue"
62626
+ ]);
62627
+ var RemoteGroupFrameSchema = _enum([
62628
+ "none",
62629
+ "panel",
62630
+ "divider",
62631
+ "dpad",
62632
+ "rocker-v",
62633
+ "rocker-h",
62634
+ "keypad"
62635
+ ]);
61970
62636
  var RemoteLayoutButtonSchema = object({
61971
62637
  id: string().min(1).max(40),
61972
62638
  /** 0-based grid position. */
@@ -61976,12 +62642,94 @@ var RemoteLayoutButtonSchema = object({
61976
62642
  colSpan: number().int().min(1).max(24),
61977
62643
  icon: string().min(1).max(60).optional(),
61978
62644
  label: string().min(1).max(40).optional(),
61979
- role: RemoteRoleSchema
62645
+ role: RemoteRoleSchema,
62646
+ /** Absent = the plain rendering every layout had before shapes existed. */
62647
+ shape: RemoteButtonShapeSchema.optional(),
62648
+ tone: RemoteButtonToneSchema.optional(),
62649
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
62650
+ groupId: string().min(1).max(40).optional()
61980
62651
  });
62652
+ /**
62653
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
62654
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
62655
+ * entry it came from (informational: the layout is self-contained).
62656
+ */
62657
+ var RemoteLayoutGroupSchema = object({
62658
+ id: string().min(1).max(40),
62659
+ componentId: string().min(1).max(80),
62660
+ frame: RemoteGroupFrameSchema,
62661
+ row: number().int().min(0).max(24),
62662
+ col: number().int().min(0).max(24),
62663
+ rowSpan: number().int().min(1).max(24),
62664
+ colSpan: number().int().min(1).max(24),
62665
+ title: string().min(1).max(40).optional()
62666
+ });
62667
+ function rectsOverlap(a, b) {
62668
+ 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;
62669
+ }
62670
+ function rectContains(outer, inner) {
62671
+ 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;
62672
+ }
62673
+ /**
62674
+ * The group rules of a layout: inside the grid, no two groups overlap, a
62675
+ * member lies inside its group, a divider holds nothing, and a button that is
62676
+ * not a member never sits inside someone else's rectangle (a group moves as
62677
+ * one thing, so a stray key under it would be left behind).
62678
+ */
62679
+ function refineGroups(grid, ctx) {
62680
+ const groups = grid.groups ?? [];
62681
+ const byId = /* @__PURE__ */ new Map();
62682
+ const fail = (message, path) => {
62683
+ ctx.addIssue({
62684
+ code: "custom",
62685
+ message,
62686
+ path
62687
+ });
62688
+ };
62689
+ groups.forEach((group, index) => {
62690
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
62691
+ "groups",
62692
+ index,
62693
+ "id"
62694
+ ]);
62695
+ byId.set(group.id, group);
62696
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
62697
+ for (let other = 0; other < index; other += 1) {
62698
+ const earlier = groups[other];
62699
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
62700
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
62701
+ return;
62702
+ }
62703
+ }
62704
+ });
62705
+ grid.buttons.forEach((button, index) => {
62706
+ if (button.groupId !== void 0) {
62707
+ const group = byId.get(button.groupId);
62708
+ if (group === void 0) {
62709
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
62710
+ "buttons",
62711
+ index,
62712
+ "groupId"
62713
+ ]);
62714
+ return;
62715
+ }
62716
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
62717
+ "buttons",
62718
+ index,
62719
+ "groupId"
62720
+ ]);
62721
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
62722
+ return;
62723
+ }
62724
+ const foreign = groups.find((group) => rectsOverlap(group, button));
62725
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
62726
+ });
62727
+ }
61981
62728
  object({
61982
62729
  columns: number().int().min(1).max(24),
61983
62730
  rows: number().int().min(1).max(24),
61984
- buttons: array(RemoteLayoutButtonSchema).max(200)
62731
+ buttons: array(RemoteLayoutButtonSchema).max(200),
62732
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
61985
62733
  }).superRefine((grid, ctx) => {
61986
62734
  const ids = /* @__PURE__ */ new Set();
61987
62735
  const taken = /* @__PURE__ */ new Map();
@@ -62018,6 +62766,7 @@ object({
62018
62766
  taken.set(cell, button.id);
62019
62767
  }
62020
62768
  });
62769
+ refineGroups(grid, ctx);
62021
62770
  });
62022
62771
  var RemoteBoundCommandSchema = object({
62023
62772
  role: RemoteRoleSchema,
@@ -62149,6 +62898,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
62149
62898
  kind: "mutation",
62150
62899
  auth: "admin"
62151
62900
  });
62901
+ var RemoteComponentCategorySchema = _enum([
62902
+ "power",
62903
+ "navigation",
62904
+ "volume",
62905
+ "numbers",
62906
+ "media",
62907
+ "colour",
62908
+ "apps",
62909
+ "layout"
62910
+ ]);
62911
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
62912
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
62913
+ var RemoteComponentButtonSchema = object({
62914
+ /** Joined to the instance id when the component is inserted. */
62915
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
62916
+ row: number().int().min(0).max(24),
62917
+ col: number().int().min(0).max(24),
62918
+ rowSpan: number().int().min(1).max(24),
62919
+ colSpan: number().int().min(1).max(24),
62920
+ role: RemoteRoleSchema,
62921
+ icon: string().min(1).max(60).optional(),
62922
+ label: string().min(1).max(40).optional(),
62923
+ shape: RemoteButtonShapeSchema.optional(),
62924
+ tone: RemoteButtonToneSchema.optional()
62925
+ });
62926
+ var RemoteLayoutComponentSchema = object({
62927
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
62928
+ name: string().min(1).max(60),
62929
+ nameI18n: object({
62930
+ en: string().min(1).max(60),
62931
+ it: string().min(1).max(60)
62932
+ }).optional(),
62933
+ /** A display icon key, resolved by the UI like a button icon. */
62934
+ icon: string().min(1).max(60),
62935
+ category: RemoteComponentCategorySchema,
62936
+ size: object({
62937
+ rows: number().int().min(1).max(24),
62938
+ cols: number().int().min(1).max(24)
62939
+ }),
62940
+ /** The frame drawn around the whole component, or null for loose buttons. */
62941
+ group: object({
62942
+ frame: RemoteGroupFrameSchema,
62943
+ title: string().min(1).max(40).optional()
62944
+ }).nullable(),
62945
+ buttons: array(RemoteComponentButtonSchema).max(40)
62946
+ }).superRefine((component, ctx) => {
62947
+ const suffixes = /* @__PURE__ */ new Set();
62948
+ component.buttons.forEach((button, index) => {
62949
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
62950
+ code: "custom",
62951
+ message: `button "${button.idSuffix}" is listed twice`,
62952
+ path: [
62953
+ "buttons",
62954
+ index,
62955
+ "idSuffix"
62956
+ ]
62957
+ });
62958
+ suffixes.add(button.idSuffix);
62959
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
62960
+ ctx.addIssue({
62961
+ code: "custom",
62962
+ message: `button "${button.idSuffix}" leaves the component`,
62963
+ path: ["buttons", index]
62964
+ });
62965
+ return;
62966
+ }
62967
+ for (let other = 0; other < index; other += 1) {
62968
+ const earlier = component.buttons[other];
62969
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
62970
+ ctx.addIssue({
62971
+ code: "custom",
62972
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
62973
+ path: ["buttons", index]
62974
+ });
62975
+ return;
62976
+ }
62977
+ }
62978
+ });
62979
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
62980
+ code: "custom",
62981
+ message: "a divider component holds no buttons",
62982
+ path: ["buttons"]
62983
+ });
62984
+ });
62985
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
62152
62986
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
62153
62987
  kind: "mutation",
62154
62988
  auth: "protected"
@@ -63204,14 +64038,13 @@ reset: method(object({
63204
64038
  auth: "admin"
63205
64039
  }) },
63206
64040
  /**
63207
- * A composed consumable (item added to a device with no native consumables
63208
- * provider) refuses `reset` by name: "Only meaningful when the item's
63209
- * `resettable` is true" — a composed item is never resettable until
63210
- * commands (`forward`) arrive in slice 3 (D663).
64041
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
64042
+ * name: only a composed device of a NEW type forwards an item's reset to a
64043
+ * source button, and marks that item `resettable` (D967).
63211
64044
  */
63212
64045
  composedMethods: { reset: {
63213
64046
  kind: "refuse",
63214
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
64047
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
63215
64048
  } },
63216
64049
  status: {
63217
64050
  schema: ConsumablesStatusSchema,
@@ -63827,7 +64660,7 @@ var DishwasherStatusSchema = object({
63827
64660
  /** Ms epoch of the last value received from the appliance; 0 = never. */
63828
64661
  lastFetchedAt: number()
63829
64662
  });
63830
- var DeviceIdSchema = number().int().nonnegative();
64663
+ var DeviceIdSchema$1 = number().int().nonnegative();
63831
64664
  /** A value a program option or setting can take. */
63832
64665
  var DishwasherValueSchema = union([
63833
64666
  boolean(),
@@ -63902,77 +64735,77 @@ var dishwasherCapability = {
63902
64735
  * option feature keys; each is checked against the program's constraints.
63903
64736
  */
63904
64737
  start: method(object({
63905
- deviceId: DeviceIdSchema,
64738
+ deviceId: DeviceIdSchema$1,
63906
64739
  program: string().min(1).optional(),
63907
64740
  options: record(string(), DishwasherValueSchema).optional()
63908
64741
  }), _void(), {
63909
64742
  kind: "mutation",
63910
64743
  auth: "protected"
63911
64744
  }),
63912
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
64745
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63913
64746
  kind: "mutation",
63914
64747
  auth: "protected"
63915
64748
  }),
63916
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
64749
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63917
64750
  kind: "mutation",
63918
64751
  auth: "protected"
63919
64752
  }),
63920
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
64753
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63921
64754
  kind: "mutation",
63922
64755
  auth: "protected"
63923
64756
  }),
63924
64757
  /** Select (not start) a program; refused when the appliance does not offer it. */
63925
64758
  setProgram: method(object({
63926
- deviceId: DeviceIdSchema,
64759
+ deviceId: DeviceIdSchema$1,
63927
64760
  program: string().min(1)
63928
64761
  }), _void(), {
63929
64762
  kind: "mutation",
63930
64763
  auth: "protected"
63931
64764
  }),
63932
64765
  setPower: method(object({
63933
- deviceId: DeviceIdSchema,
64766
+ deviceId: DeviceIdSchema$1,
63934
64767
  on: boolean()
63935
64768
  }), _void(), {
63936
64769
  kind: "mutation",
63937
64770
  auth: "protected"
63938
64771
  }),
63939
64772
  setHalfLoad: method(object({
63940
- deviceId: DeviceIdSchema,
64773
+ deviceId: DeviceIdSchema$1,
63941
64774
  on: boolean()
63942
64775
  }), _void(), {
63943
64776
  kind: "mutation",
63944
64777
  auth: "protected"
63945
64778
  }),
63946
64779
  setHygienePlus: method(object({
63947
- deviceId: DeviceIdSchema,
64780
+ deviceId: DeviceIdSchema$1,
63948
64781
  on: boolean()
63949
64782
  }), _void(), {
63950
64783
  kind: "mutation",
63951
64784
  auth: "protected"
63952
64785
  }),
63953
64786
  setExtraDry: method(object({
63954
- deviceId: DeviceIdSchema,
64787
+ deviceId: DeviceIdSchema$1,
63955
64788
  on: boolean()
63956
64789
  }), _void(), {
63957
64790
  kind: "mutation",
63958
64791
  auth: "protected"
63959
64792
  }),
63960
64793
  setIntensiveZone: method(object({
63961
- deviceId: DeviceIdSchema,
64794
+ deviceId: DeviceIdSchema$1,
63962
64795
  on: boolean()
63963
64796
  }), _void(), {
63964
64797
  kind: "mutation",
63965
64798
  auth: "protected"
63966
64799
  }),
63967
64800
  setVarioSpeedPlus: method(object({
63968
- deviceId: DeviceIdSchema,
64801
+ deviceId: DeviceIdSchema$1,
63969
64802
  on: boolean()
63970
64803
  }), _void(), {
63971
64804
  kind: "mutation",
63972
64805
  auth: "protected"
63973
64806
  }),
63974
64807
  setChildLock: method(object({
63975
- deviceId: DeviceIdSchema,
64808
+ deviceId: DeviceIdSchema$1,
63976
64809
  on: boolean()
63977
64810
  }), _void(), {
63978
64811
  kind: "mutation",
@@ -63984,7 +64817,7 @@ var dishwasherCapability = {
63984
64817
  * its declared range / members.
63985
64818
  */
63986
64819
  setOption: method(object({
63987
- deviceId: DeviceIdSchema,
64820
+ deviceId: DeviceIdSchema$1,
63988
64821
  key: string().min(1),
63989
64822
  value: DishwasherValueSchema
63990
64823
  }), _void(), {
@@ -63992,7 +64825,7 @@ var dishwasherCapability = {
63992
64825
  auth: "protected"
63993
64826
  }),
63994
64827
  /** What this appliance can do, from its own description — the UI is derived from it. */
63995
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
64828
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
63996
64829
  },
63997
64830
  status: {
63998
64831
  schema: DishwasherStatusSchema,
@@ -67769,7 +68602,8 @@ var TopologyNodeSchema = object({
67769
68602
  isHub: boolean(),
67770
68603
  isOnline: boolean(),
67771
68604
  cpuPercent: number(),
67772
- memoryPercent: number(),
68605
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
68606
+ memoryPercent: number().nullable(),
67773
68607
  uptime: number(),
67774
68608
  lastSeen: string(),
67775
68609
  localIps: array(string()).readonly(),
@@ -68380,6 +69214,14 @@ var PetFeederDeviceStatusSchema = _enum([
68380
69214
  "on_batteries"
68381
69215
  ]);
68382
69216
  var gramsPortion = number().int().min(4).max(200);
69217
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
69218
+ * hardware's own unit). The bounds are grams; a caller entering another unit
69219
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
69220
+ * plain grams — they are not readings in the kilogram base. */
69221
+ var gramsParam = gramsPortion.meta(paramMeta({
69222
+ quantity: "mass",
69223
+ unit: "g"
69224
+ }));
68383
69225
  var PetFeederStatusSchema = object({
68384
69226
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
68385
69227
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -68387,9 +69229,10 @@ var PetFeederStatusSchema = object({
68387
69229
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
68388
69230
  * the device has not reported yet. */
68389
69231
  foodLevel: number().nullable().meta(readingMeta("none")),
68390
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
68391
- * feeder's own units. Null when the model reports none. */
68392
- bowlFood: number().nullable().meta(readingMeta("none")),
69232
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
69233
+ * feeder with a bowl scale; null when the model has none or its reading is
69234
+ * of an unproven unit. */
69235
+ bowlFood: number().nullable().meta(readingMeta("mass")),
68393
69236
  /** True while the pet is eating (`state.eating`). */
68394
69237
  eating: boolean(),
68395
69238
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -68467,9 +69310,9 @@ var petFeederCapability = {
68467
69310
  */
68468
69311
  feed: method(object({
68469
69312
  deviceId: number().int().nonnegative(),
68470
- grams: gramsPortion.optional(),
68471
- hopper1: gramsPortion.optional(),
68472
- hopper2: gramsPortion.optional()
69313
+ grams: gramsParam.optional(),
69314
+ hopper1: gramsParam.optional(),
69315
+ hopper2: gramsParam.optional()
68473
69316
  }), _void(), {
68474
69317
  kind: "mutation",
68475
69318
  auth: "protected"
@@ -68573,6 +69416,137 @@ var petFeederCapability = {
68573
69416
  }
68574
69417
  } }
68575
69418
  };
69419
+ /**
69420
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
69421
+ * circulating drinking fountain: how much water is in the tank, how much the
69422
+ * pet drank, whether the pump runs and on what duty cycle.
69423
+ *
69424
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
69425
+ * a container of loose entities (a pump switch, two number boxes, a "filter
69426
+ * changed" button), and an operator BUILDS the device from them — a composed
69427
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
69428
+ * method of ONE source entity, and every reading is a field derived from a
69429
+ * source entity; the provider-shaped interface below exists so the composed
69430
+ * device and any future native provider answer the same contract.
69431
+ *
69432
+ * Quantities are stored in the base unit of their kind (D944): volumes in
69433
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
69434
+ * reports millilitres or minutes is converted in the composition's expression,
69435
+ * never here. Every reading may be `null`: a source that is offline or does
69436
+ * not report is unknown, never zero.
69437
+ *
69438
+ * The filter is not a field of this cap: it is an item of the `consumables`
69439
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
69440
+ */
69441
+ var PetFountainStatusSchema = object({
69442
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
69443
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69444
+ /** Water in the tank, in litres. Null while the source has not reported. */
69445
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
69446
+ /** True when the tank is empty. Null while unknown. */
69447
+ empty: boolean().nullable(),
69448
+ /** True while the pump is running. Null while unknown. */
69449
+ pumpRunning: boolean().nullable(),
69450
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
69451
+ pumpIntervalEnabled: boolean().nullable(),
69452
+ /** How long the pump runs per cycle, in seconds. */
69453
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
69454
+ /** How long the pump rests per cycle, in seconds. */
69455
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
69456
+ /** Water drunk in the last drink, in litres. */
69457
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
69458
+ /** Water drunk in the current hour, in litres. */
69459
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
69460
+ /** Water drunk over the last 24 hours (rolling), in litres. */
69461
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
69462
+ /** Water drunk since local midnight, in litres. */
69463
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
69464
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
69465
+ error: string().nullable(),
69466
+ /** Ms epoch when the slice was last refreshed. */
69467
+ lastFetchedAt: number()
69468
+ });
69469
+ var DeviceIdSchema = number().int().nonnegative();
69470
+ var MinutesSchema = number().int().min(0);
69471
+ var petFountainCapability = {
69472
+ name: "pet-fountain",
69473
+ scope: "device",
69474
+ deviceNative: true,
69475
+ mode: "singleton",
69476
+ deviceTypes: [DeviceType.PetFountain],
69477
+ methods: {
69478
+ /** Start or stop the pump. */
69479
+ setPump: method(object({
69480
+ deviceId: DeviceIdSchema,
69481
+ on: boolean()
69482
+ }), _void(), {
69483
+ kind: "mutation",
69484
+ auth: "protected"
69485
+ }),
69486
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
69487
+ setPumpInterval: method(object({
69488
+ deviceId: DeviceIdSchema,
69489
+ on: boolean()
69490
+ }), _void(), {
69491
+ kind: "mutation",
69492
+ auth: "protected"
69493
+ }),
69494
+ /** Set how many minutes the pump runs per cycle. */
69495
+ setPumpOnMinutes: method(object({
69496
+ deviceId: DeviceIdSchema,
69497
+ minutes: MinutesSchema
69498
+ }), _void(), {
69499
+ kind: "mutation",
69500
+ auth: "protected"
69501
+ }),
69502
+ /** Set how many minutes the pump rests per cycle. */
69503
+ setPumpOffMinutes: method(object({
69504
+ deviceId: DeviceIdSchema,
69505
+ minutes: MinutesSchema
69506
+ }), _void(), {
69507
+ kind: "mutation",
69508
+ auth: "protected"
69509
+ }),
69510
+ /** Clear the rolling daily consumption counters on the device. */
69511
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69512
+ kind: "mutation",
69513
+ auth: "protected"
69514
+ })
69515
+ },
69516
+ status: {
69517
+ schema: PetFountainStatusSchema,
69518
+ kind: "push"
69519
+ },
69520
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
69521
+ runtimeState: PetFountainStatusSchema,
69522
+ /**
69523
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
69524
+ *
69525
+ * See `RuntimeStateDurability`. Enforced by
69526
+ * `scripts/check-runtime-state-durability.ts`.
69527
+ */
69528
+ durability: "session",
69529
+ /** History (D850): recorded series. */
69530
+ history: { fields: {
69531
+ waterLevel: {
69532
+ kind: "gauge",
69533
+ unit: "%",
69534
+ deadband: 1
69535
+ },
69536
+ waterVolume: {
69537
+ kind: "gauge",
69538
+ unit: "L"
69539
+ },
69540
+ todayDrink: {
69541
+ kind: "gauge",
69542
+ unit: "L"
69543
+ },
69544
+ last24hDrink: {
69545
+ kind: "gauge",
69546
+ unit: "L"
69547
+ }
69548
+ } }
69549
+ };
68576
69550
  var VehicleSchema = object({
68577
69551
  id: string(),
68578
69552
  name: string(),
@@ -74570,6 +75544,87 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
74570
75544
  kind: "mutation",
74571
75545
  auth: "admin"
74572
75546
  });
75547
+ var btn = (idSuffix, row, col, init) => ({
75548
+ idSuffix,
75549
+ row,
75550
+ col,
75551
+ rowSpan: 1,
75552
+ colSpan: 1,
75553
+ ...init
75554
+ });
75555
+ function rocker(id, name, it, frame, title, buttons, size) {
75556
+ return {
75557
+ id,
75558
+ name,
75559
+ nameI18n: {
75560
+ en: name,
75561
+ it
75562
+ },
75563
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
75564
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
75565
+ size,
75566
+ group: title === null ? { frame } : {
75567
+ frame,
75568
+ title
75569
+ },
75570
+ buttons
75571
+ };
75572
+ }
75573
+ btn("power", 0, 0, {
75574
+ role: "power",
75575
+ shape: "round",
75576
+ tone: "danger"
75577
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
75578
+ role: "ok",
75579
+ shape: "round",
75580
+ tone: "accent"
75581
+ }), 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, {
75582
+ role: "volume_up",
75583
+ shape: "pill"
75584
+ }), btn("down", 1, 0, {
75585
+ role: "volume_down",
75586
+ shape: "pill"
75587
+ })], {
75588
+ rows: 2,
75589
+ cols: 1
75590
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
75591
+ role: "channel_up",
75592
+ shape: "pill"
75593
+ }), btn("down", 1, 0, {
75594
+ role: "channel_down",
75595
+ shape: "pill"
75596
+ })], {
75597
+ rows: 2,
75598
+ cols: 1
75599
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
75600
+ role: "custom:minus",
75601
+ label: "−",
75602
+ shape: "pill"
75603
+ }), btn("plus", 0, 1, {
75604
+ role: "custom:plus",
75605
+ label: "+",
75606
+ shape: "pill"
75607
+ })], {
75608
+ rows: 1,
75609
+ cols: 2
75610
+ }), [...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, {
75611
+ role: "custom:color_red",
75612
+ tone: "red"
75613
+ }), btn("green", 0, 1, {
75614
+ role: "custom:color_green",
75615
+ tone: "green"
75616
+ }), btn("yellow", 0, 2, {
75617
+ role: "custom:color_yellow",
75618
+ tone: "yellow"
75619
+ }), btn("blue", 0, 3, {
75620
+ role: "custom:color_blue",
75621
+ tone: "blue"
75622
+ }), 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" }), [
75623
+ 1,
75624
+ 2,
75625
+ 3,
75626
+ 4
75627
+ ].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" });
74573
75628
  discriminatedUnion("kind", [object({
74574
75629
  kind: literal("raw"),
74575
75630
  timings: array(number().int()),
@@ -74580,7 +75635,7 @@ discriminatedUnion("kind", [object({
74580
75635
  })]);
74581
75636
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
74582
75637
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
74583
- 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;
75638
+ 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;
74584
75639
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
74585
75640
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
74586
75641
  new Set(CAP_NAMES);
@@ -74984,6 +76039,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
74984
76039
  notifier: notifierCapability,
74985
76040
  numericSensor: numericSensorCapability,
74986
76041
  petFeeder: petFeederCapability,
76042
+ petFountain: petFountainCapability,
74987
76043
  powerMeter: powerMeterCapability,
74988
76044
  presence: presenceCapability,
74989
76045
  pressureSensor: pressureSensorCapability,
@@ -75751,7 +76807,7 @@ var DeclaredDevices = class {
75751
76807
  return removed;
75752
76808
  }
75753
76809
  };
75754
- 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;
76810
+ 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;
75755
76811
  new Set(Object.values(DeviceType));
75756
76812
  Object.freeze({
75757
76813
  "accessories.getStatus": {
@@ -77686,6 +78742,13 @@ Object.freeze({
77686
78742
  access: "create",
77687
78743
  role: "admin"
77688
78744
  },
78745
+ "deviceExport.exposeEntity": {
78746
+ capName: "device-export",
78747
+ capScope: "system",
78748
+ addonId: null,
78749
+ access: "create",
78750
+ role: "admin"
78751
+ },
77689
78752
  "deviceExport.getDeviceLiveContribution": {
77690
78753
  capName: "device-export",
77691
78754
  capScope: "system",
@@ -77714,6 +78777,13 @@ Object.freeze({
77714
78777
  access: "view",
77715
78778
  role: "admin"
77716
78779
  },
78780
+ "deviceExport.listExposedEntities": {
78781
+ capName: "device-export",
78782
+ capScope: "system",
78783
+ addonId: null,
78784
+ access: "view",
78785
+ role: "admin"
78786
+ },
77717
78787
  "deviceExport.listSupportedDeviceKinds": {
77718
78788
  capName: "device-export",
77719
78789
  capScope: "system",
@@ -77721,6 +78791,13 @@ Object.freeze({
77721
78791
  access: "view",
77722
78792
  role: "admin"
77723
78793
  },
78794
+ "deviceExport.listSupportedEntityKinds": {
78795
+ capName: "device-export",
78796
+ capScope: "system",
78797
+ addonId: null,
78798
+ access: "view",
78799
+ role: "admin"
78800
+ },
77724
78801
  "deviceExport.unexposeDevice": {
77725
78802
  capName: "device-export",
77726
78803
  capScope: "system",
@@ -77728,6 +78805,13 @@ Object.freeze({
77728
78805
  access: "create",
77729
78806
  role: "admin"
77730
78807
  },
78808
+ "deviceExport.unexposeEntity": {
78809
+ capName: "device-export",
78810
+ capScope: "system",
78811
+ addonId: null,
78812
+ access: "create",
78813
+ role: "admin"
78814
+ },
77731
78815
  "deviceExtension.listCandidates": {
77732
78816
  capName: "device-extension",
77733
78817
  capScope: "device",
@@ -81193,6 +82277,48 @@ Object.freeze({
81193
82277
  access: "create",
81194
82278
  role: "viewer"
81195
82279
  },
82280
+ "petFountain.getStatus": {
82281
+ capName: "pet-fountain",
82282
+ capScope: "device",
82283
+ addonId: null,
82284
+ access: "view",
82285
+ role: "viewer"
82286
+ },
82287
+ "petFountain.resetDailyConsumption": {
82288
+ capName: "pet-fountain",
82289
+ capScope: "device",
82290
+ addonId: null,
82291
+ access: "delete",
82292
+ role: "viewer"
82293
+ },
82294
+ "petFountain.setPump": {
82295
+ capName: "pet-fountain",
82296
+ capScope: "device",
82297
+ addonId: null,
82298
+ access: "create",
82299
+ role: "viewer"
82300
+ },
82301
+ "petFountain.setPumpInterval": {
82302
+ capName: "pet-fountain",
82303
+ capScope: "device",
82304
+ addonId: null,
82305
+ access: "create",
82306
+ role: "viewer"
82307
+ },
82308
+ "petFountain.setPumpOffMinutes": {
82309
+ capName: "pet-fountain",
82310
+ capScope: "device",
82311
+ addonId: null,
82312
+ access: "create",
82313
+ role: "viewer"
82314
+ },
82315
+ "petFountain.setPumpOnMinutes": {
82316
+ capName: "pet-fountain",
82317
+ capScope: "device",
82318
+ addonId: null,
82319
+ access: "create",
82320
+ role: "viewer"
82321
+ },
81196
82322
  "pipelineAnalytics.addRetrainFrameFromRecording": {
81197
82323
  capName: "pipeline-analytics",
81198
82324
  capScope: "device",
@@ -83482,6 +84608,13 @@ Object.freeze({
83482
84608
  access: "create",
83483
84609
  role: "admin"
83484
84610
  },
84611
+ "remoteLayoutComponents.listComponents": {
84612
+ capName: "remote-layout-components",
84613
+ capScope: "system",
84614
+ addonId: null,
84615
+ access: "view",
84616
+ role: "admin"
84617
+ },
83485
84618
  "runningState.getStatus": {
83486
84619
  capName: "running-state",
83487
84620
  capScope: "device",
@@ -83594,6 +84727,13 @@ Object.freeze({
83594
84727
  access: "create",
83595
84728
  role: "admin"
83596
84729
  },
84730
+ "serverManagement.getLaunchOverrides": {
84731
+ capName: "server-management",
84732
+ capScope: "system",
84733
+ addonId: null,
84734
+ access: "view",
84735
+ role: "admin"
84736
+ },
83597
84737
  "serverManagement.getServerPackageStatus": {
83598
84738
  capName: "server-management",
83599
84739
  capScope: "system",
@@ -83601,6 +84741,13 @@ Object.freeze({
83601
84741
  access: "view",
83602
84742
  role: "admin"
83603
84743
  },
84744
+ "serverManagement.installDesktopUpdate": {
84745
+ capName: "server-management",
84746
+ capScope: "system",
84747
+ addonId: null,
84748
+ access: "create",
84749
+ role: "admin"
84750
+ },
83604
84751
  "serverManagement.restartServer": {
83605
84752
  capName: "server-management",
83606
84753
  capScope: "system",
@@ -83615,6 +84762,13 @@ Object.freeze({
83615
84762
  access: "create",
83616
84763
  role: "admin"
83617
84764
  },
84765
+ "serverManagement.setLaunchOverrides": {
84766
+ capName: "server-management",
84767
+ capScope: "system",
84768
+ addonId: null,
84769
+ access: "create",
84770
+ role: "admin"
84771
+ },
83618
84772
  "settingsStore.aggregate": {
83619
84773
  capName: "settings-store",
83620
84774
  capScope: "system",
@@ -87504,7 +88658,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87504
88658
  min: 4,
87505
88659
  max: 200,
87506
88660
  step: 1,
87507
- unit: "g"
88661
+ unit: "g",
88662
+ quantity: "mass"
87508
88663
  },
87509
88664
  optional: true
87510
88665
  }]
@@ -87523,7 +88678,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87523
88678
  min: 4,
87524
88679
  max: 200,
87525
88680
  step: 1,
87526
- unit: "g"
88681
+ unit: "g",
88682
+ quantity: "mass"
87527
88683
  },
87528
88684
  optional: true
87529
88685
  }, {
@@ -87534,7 +88690,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87534
88690
  min: 4,
87535
88691
  max: 200,
87536
88692
  step: 1,
87537
- unit: "g"
88693
+ unit: "g",
88694
+ quantity: "mass"
87538
88695
  },
87539
88696
  optional: true
87540
88697
  }]
@@ -87675,6 +88832,100 @@ var APPLIANCE_CAP_VOCABULARY = {
87675
88832
  }
87676
88833
  ]
87677
88834
  },
88835
+ "pet-fountain": {
88836
+ scope: "device",
88837
+ noun: t("Pet fountain", "Fontana per animali"),
88838
+ actions: [
88839
+ {
88840
+ id: "petFountain.pumpOn",
88841
+ method: "setPump",
88842
+ label: t("Pump on", "Pompa accesa"),
88843
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
88844
+ icon: "play",
88845
+ args: { on: true }
88846
+ },
88847
+ {
88848
+ id: "petFountain.pumpOff",
88849
+ method: "setPump",
88850
+ label: t("Pump off", "Pompa spenta"),
88851
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
88852
+ icon: "square",
88853
+ args: { on: false }
88854
+ },
88855
+ {
88856
+ id: "petFountain.intervalOn",
88857
+ method: "setPumpInterval",
88858
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
88859
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
88860
+ icon: "timer",
88861
+ args: { on: true }
88862
+ },
88863
+ {
88864
+ id: "petFountain.intervalOff",
88865
+ method: "setPumpInterval",
88866
+ label: t("Pump cycle off", "Ciclo pompa spento"),
88867
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
88868
+ icon: "timer-off",
88869
+ args: { on: false }
88870
+ },
88871
+ {
88872
+ id: "petFountain.pumpOnMinutes",
88873
+ method: "setPumpOnMinutes",
88874
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
88875
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
88876
+ icon: "timer",
88877
+ openArgs: [{
88878
+ key: "minutes",
88879
+ label: t("Minutes", "Minuti"),
88880
+ offer: {
88881
+ kind: "number",
88882
+ min: 0,
88883
+ step: 1,
88884
+ unit: "min"
88885
+ }
88886
+ }]
88887
+ },
88888
+ {
88889
+ id: "petFountain.pumpOffMinutes",
88890
+ method: "setPumpOffMinutes",
88891
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
88892
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
88893
+ icon: "timer-off",
88894
+ openArgs: [{
88895
+ key: "minutes",
88896
+ label: t("Minutes", "Minuti"),
88897
+ offer: {
88898
+ kind: "number",
88899
+ min: 0,
88900
+ step: 1,
88901
+ unit: "min"
88902
+ }
88903
+ }]
88904
+ },
88905
+ {
88906
+ id: "petFountain.resetDailyConsumption",
88907
+ method: "resetDailyConsumption",
88908
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
88909
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
88910
+ icon: "rotate-ccw"
88911
+ }
88912
+ ],
88913
+ state: [{
88914
+ kind: "boolean",
88915
+ field: "empty",
88916
+ label: t("Tank empty", "Serbatoio vuoto"),
88917
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
88918
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
88919
+ icon: "droplet"
88920
+ }, {
88921
+ kind: "numeric",
88922
+ field: "waterLevel",
88923
+ quantity: t("the water level", "il livello dell’acqua"),
88924
+ label: t("Water level", "Livello dell’acqua"),
88925
+ unit: "%",
88926
+ icon: "droplets"
88927
+ }]
88928
+ },
87678
88929
  "media-player": {
87679
88930
  scope: "device",
87680
88931
  noun: t("Media player", "Lettore multimediale"),
@@ -89540,6 +90791,7 @@ var SYSTEM_CAP_VOCABULARY = {
89540
90791
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
89541
90792
  records: system(INTERNAL),
89542
90793
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
90794
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
89543
90795
  "server-management": system(CLUSTER),
89544
90796
  "settings-store": system(INTERNAL),
89545
90797
  "smtp-provider": system(INTERNAL),
@@ -89612,7 +90864,7 @@ function projectAction(cap, action) {
89612
90864
  };
89613
90865
  }
89614
90866
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
89615
- 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;
90867
+ 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;
89616
90868
  DeviceFeature.BatteryOperated;
89617
90869
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
89618
90870
  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.";
@@ -91163,6 +92415,36 @@ Object.freeze({
91163
92415
  form: "single",
91164
92416
  optional: false
91165
92417
  }],
92418
+ "petFountain.getStatus": [{
92419
+ name: "deviceId",
92420
+ form: "single",
92421
+ optional: false
92422
+ }],
92423
+ "petFountain.resetDailyConsumption": [{
92424
+ name: "deviceId",
92425
+ form: "single",
92426
+ optional: false
92427
+ }],
92428
+ "petFountain.setPump": [{
92429
+ name: "deviceId",
92430
+ form: "single",
92431
+ optional: false
92432
+ }],
92433
+ "petFountain.setPumpInterval": [{
92434
+ name: "deviceId",
92435
+ form: "single",
92436
+ optional: false
92437
+ }],
92438
+ "petFountain.setPumpOffMinutes": [{
92439
+ name: "deviceId",
92440
+ form: "single",
92441
+ optional: false
92442
+ }],
92443
+ "petFountain.setPumpOnMinutes": [{
92444
+ name: "deviceId",
92445
+ form: "single",
92446
+ optional: false
92447
+ }],
91166
92448
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
91167
92449
  name: "deviceId",
91168
92450
  form: "single",
@@ -95559,8 +96841,6 @@ var NC_TEMPLATE_VARS = [
95559
96841
  * delete it from here then, and the served catalog names it again.
95560
96842
  */
95561
96843
  var NC_TEMPLATE_HELD_BACK_SYSTEM_EVENT_KINDS = [
95562
- "node-restarting",
95563
- "node-updating",
95564
96844
  "alert-critical",
95565
96845
  "alert-error",
95566
96846
  "alert-critical-resolved",