@camstack/addon-post-analysis 1.2.650 → 1.2.652

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-B_mY0hd7.mjs → clip-model-registry-BrazZJvr.mjs} +2497 -726
  3. package/dist/{clip-model-registry-CC7pvitG.js → clip-model-registry-Bz1uQvQ8.js} +2497 -726
  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-HjnmptAc.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-BjUFFpLS.mjs → hostInit-DtrcY1yz.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +55 -4
  12. package/dist/pipeline-analytics/index.mjs +55 -5
  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-YNYqWfWY.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-OPayQG8i.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-BAvUCi_2.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" });
@@ -9138,6 +9205,7 @@ var CAP_NAMES = Object.freeze([
9138
9205
  "network-link",
9139
9206
  "network-quality",
9140
9207
  "node-certificates",
9208
+ "node-removal-hook",
9141
9209
  "nodes",
9142
9210
  "notification-output",
9143
9211
  "notification-rules",
@@ -9148,6 +9216,7 @@ var CAP_NAMES = Object.freeze([
9148
9216
  "osd",
9149
9217
  "osd-manager",
9150
9218
  "pet-feeder",
9219
+ "pet-fountain",
9151
9220
  "pipeline-analytics",
9152
9221
  "pipeline-executor",
9153
9222
  "pipeline-orchestrator",
@@ -9168,6 +9237,8 @@ var CAP_NAMES = Object.freeze([
9168
9237
  "recording-onboard",
9169
9238
  "recording-signal",
9170
9239
  "records",
9240
+ "remote",
9241
+ "remote-layout-components",
9171
9242
  "running-state",
9172
9243
  "scene-monitor",
9173
9244
  "script-runner",
@@ -9191,6 +9262,7 @@ var CAP_NAMES = Object.freeze([
9191
9262
  "system",
9192
9263
  "tamper",
9193
9264
  "temperature-sensor",
9265
+ "template",
9194
9266
  "terminal-session",
9195
9267
  "toast",
9196
9268
  "training",
@@ -10131,6 +10203,11 @@ Object.freeze({
10131
10203
  "standby"
10132
10204
  ]
10133
10205
  },
10206
+ {
10207
+ "path": "stateSource",
10208
+ "valueType": "enum",
10209
+ "enumValues": ["reported", "assumed"]
10210
+ },
10134
10211
  {
10135
10212
  "path": "volumeLevel",
10136
10213
  "valueType": "number"
@@ -10406,6 +10483,68 @@ Object.freeze({
10406
10483
  {
10407
10484
  "path": "volume",
10408
10485
  "valueType": "number"
10486
+ },
10487
+ {
10488
+ "path": "lastFeedGrams",
10489
+ "valueType": "number"
10490
+ },
10491
+ {
10492
+ "path": "lastFeedHopper1",
10493
+ "valueType": "number"
10494
+ },
10495
+ {
10496
+ "path": "lastFeedHopper2",
10497
+ "valueType": "number"
10498
+ }
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"
10409
10548
  }
10410
10549
  ],
10411
10550
  "power-meter": [
@@ -10947,7 +11086,7 @@ Object.freeze({
10947
11086
  * can answer "may a rule actuate this?" without importing the schema barrel
10948
11087
  * (~144MB RSS per runner, D28).
10949
11088
  *
10950
- * Coverage: 96 device-scoped capabilities.
11089
+ * Coverage: 97 device-scoped capabilities.
10951
11090
  */
10952
11091
  var DEVICE_SCOPED_CAPS = new Set([
10953
11092
  "accessories",
@@ -11013,6 +11152,7 @@ var DEVICE_SCOPED_CAPS = new Set([
11013
11152
  "numeric-sensor",
11014
11153
  "osd",
11015
11154
  "pet-feeder",
11155
+ "pet-fountain",
11016
11156
  "pipeline-analytics",
11017
11157
  "power-meter",
11018
11158
  "presence",
@@ -11091,7 +11231,7 @@ function sleep(ms) {
11091
11231
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11092
11232
  }
11093
11233
  //#endregion
11094
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11234
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
11095
11235
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11096
11236
  DeviceType["Camera"] = "camera";
11097
11237
  DeviceType["Hub"] = "hub";
@@ -11183,6 +11323,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11183
11323
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11184
11324
  * integrations sharing the same food/desiccant/hopper surface. */
11185
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";
11186
11330
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11187
11331
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11188
11332
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11453,7 +11597,7 @@ function errMsg(err) {
11453
11597
  return String(err);
11454
11598
  }
11455
11599
  //#endregion
11456
- //#region ../types/dist/composition-DpFv_J6D.mjs
11600
+ //#region ../types/dist/composition-kSuOifuG.mjs
11457
11601
  /**
11458
11602
  * Deterministic SHA-256 hash of an arbitrary serialisable value. The
11459
11603
  * canonical form sorts object keys alphabetically at every depth so two
@@ -12698,12 +12842,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12698
12842
  ])
12699
12843
  })
12700
12844
  ]);
12701
- 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({
12702
12860
  kind: literal("forward"),
12703
12861
  source: CompositionSourceRefSchema,
12704
12862
  cap: string().min(1),
12705
- method: string().min(1)
12706
- }), CompositionCodeSourceSchema]);
12863
+ method: string().min(1),
12864
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12865
+ });
12866
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12707
12867
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12708
12868
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12709
12869
  /**
@@ -12713,7 +12873,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12713
12873
  */
12714
12874
  var CompositionItemEntrySchema = object({
12715
12875
  label: string().min(1).max(80),
12716
- 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()
12717
12883
  });
12718
12884
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12719
12885
  kind: literal("fields"),
@@ -12725,7 +12891,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12725
12891
  * owns the WHOLE array field: the composed value holds exactly these items.
12726
12892
  */
12727
12893
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12728
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12894
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12729
12895
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12730
12896
  }), object({
12731
12897
  kind: literal("passthrough"),
@@ -16868,6 +17034,538 @@ object({
16868
17034
  path: ["expr"]
16869
17035
  });
16870
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
+ });
16871
17569
  /** How a leaf compares a device field to a value. Derived from the field's
16872
17570
  * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16873
17571
  var AutomationConditionOperatorSchema = _enum([
@@ -16890,8 +17588,10 @@ var AutomationConditionLeafSchema = object({
16890
17588
  string(),
16891
17589
  number(),
16892
17590
  boolean(),
16893
- array(union([string(), number()]))
16894
- ])
17591
+ array(union([string(), number()])),
17592
+ QuantityValueSchema
17593
+ ]),
17594
+ unit: string().min(1).optional()
16895
17595
  });
16896
17596
  var AutomationConditionStationaryPresentSchema = object({
16897
17597
  kind: literal("stationary-present"),
@@ -17044,7 +17744,13 @@ var AutomationCapStepSchema = object({
17044
17744
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
17045
17745
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
17046
17746
  * the expression leaf. */
17047
- 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()
17048
17754
  });
17049
17755
  /**
17050
17756
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -17446,6 +18152,437 @@ object({
17446
18152
  lastRunAt: number().nullable(),
17447
18153
  lastError: string().nullable()
17448
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
+ });
17449
18586
  /**
17450
18587
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17451
18588
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
@@ -17478,52 +18615,6 @@ var AddonPageInfoSchema = object({
17478
18615
  bundleUrl: string()
17479
18616
  });
17480
18617
  method(_void(), array(AddonPageInfoSchema).readonly());
17481
- /**
17482
- * `addon-pages-source` — collection cap exposing per-provider raw page
17483
- * declarations. Every addon that contributes a UI page registers a
17484
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
17485
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
17486
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
17487
- * that admin-ui consumes.
17488
- *
17489
- * The split exists because the public listing has a different output
17490
- * shape than the per-provider raw declarations, and we want both ends
17491
- * to flow through codegen instead of relying on a hand-written wrapper.
17492
- */
17493
- var AddonPageDeclarationSchema = object({
17494
- id: string(),
17495
- label: string(),
17496
- icon: string(),
17497
- path: string(),
17498
- /**
17499
- * Module Federation remote name — must match the `name` field on the
17500
- * page addon's `federation()` plugin config. Used by admin-ui's
17501
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
17502
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
17503
- * contain hyphens).
17504
- */
17505
- remoteName: string(),
17506
- /**
17507
- * Bundle filename inside the addon's `dist/` dir served at
17508
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
17509
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
17510
- * the static-file route can compute an mtime-based cache-buster URL
17511
- * without a separate filesystem stat.
17512
- */
17513
- bundle: string(),
17514
- /**
17515
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
17516
- * `'cluster'`, `'administration'` — the page renders inside that group.
17517
- * Any OTHER string creates (or joins) a custom section rendered after
17518
- * the built-in groups; its label comes from `sectionLabel` (first
17519
- * declaration wins), falling back to the id. Absent → the legacy
17520
- * "Addon Pages" group.
17521
- */
17522
- section: string().optional(),
17523
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
17524
- sectionLabel: string().optional()
17525
- });
17526
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
17527
18618
  var AddonHttpRouteSchema = object({
17528
18619
  method: _enum([
17529
18620
  "GET",
@@ -17824,9 +18915,21 @@ var AlertSeveritySchema = _enum([
17824
18915
  "error",
17825
18916
  "critical"
17826
18917
  ]);
18918
+ /**
18919
+ * An action that RUNS something instead of going somewhere: the admin UI calls
18920
+ * the addon's custom action (`addons.custom`) with exactly these fields. The
18921
+ * producer owns the meaning; the UI knows nothing about the addon or the kind.
18922
+ */
18923
+ var AlertActionInvokeSchema = object({
18924
+ addonId: string().min(1),
18925
+ action: string().min(1),
18926
+ input: record(string(), unknown()).optional()
18927
+ });
18928
+ /** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
17827
18929
  var AlertActionSchema = object({
17828
18930
  label: string(),
17829
18931
  href: string().optional(),
18932
+ invoke: AlertActionInvokeSchema.optional(),
17830
18933
  /**
17831
18934
  * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
17832
18935
  * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
@@ -20183,7 +21286,11 @@ var CompositionProblemSchema = object({
20183
21286
  "mixed-quantity",
20184
21287
  "helper-mismatch",
20185
21288
  "member-constraint",
20186
- "retention"
21289
+ "retention",
21290
+ "command-method-unknown",
21291
+ "command-source-unknown",
21292
+ "command-input-unknown",
21293
+ "command-input-missing"
20187
21294
  ]),
20188
21295
  path: string(),
20189
21296
  message: string()
@@ -20222,7 +21329,9 @@ var CompositionFieldRoleSchema = _enum([
20222
21329
  var CompositionFieldItemSchema = object({
20223
21330
  arrayPath: string(),
20224
21331
  key: string(),
20225
- 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()
20226
21335
  });
20227
21336
  var CompositionFieldPlanSchema = object({
20228
21337
  cap: string(),
@@ -20427,7 +21536,7 @@ Object.freeze({
20427
21536
  * One catalog row per base reading a group helper can build (D944 §3).
20428
21537
  * Data, not a form default written into the block.
20429
21538
  */
20430
- var NOW$2 = {
21539
+ var NOW$3 = {
20431
21540
  fieldPath: "lastFetchedAt",
20432
21541
  expr: "now"
20433
21542
  };
@@ -20479,7 +21588,7 @@ var GROUP_TEMPLATES = [
20479
21588
  quantity: "temperature",
20480
21589
  defaultAggregate: "mean",
20481
21590
  empty: "null",
20482
- companions: [NOW$2],
21591
+ companions: [NOW$3],
20483
21592
  alsoFrom: {
20484
21593
  deviceTypes: CLIMATE_DEVICES,
20485
21594
  accept: {
@@ -20497,7 +21606,7 @@ var GROUP_TEMPLATES = [
20497
21606
  quantity: "humidity",
20498
21607
  defaultAggregate: "mean",
20499
21608
  empty: "null",
20500
- companions: [NOW$2],
21609
+ companions: [NOW$3],
20501
21610
  alsoFrom: {
20502
21611
  deviceTypes: CLIMATE_DEVICES,
20503
21612
  accept: {
@@ -20515,7 +21624,7 @@ var GROUP_TEMPLATES = [
20515
21624
  quantity: "pressure",
20516
21625
  defaultAggregate: "mean",
20517
21626
  empty: "null",
20518
- companions: [NOW$2]
21627
+ companions: [NOW$3]
20519
21628
  }),
20520
21629
  numeric({
20521
21630
  id: "illuminance",
@@ -20526,7 +21635,7 @@ var GROUP_TEMPLATES = [
20526
21635
  quantity: "illuminance",
20527
21636
  defaultAggregate: "mean",
20528
21637
  empty: "null",
20529
- companions: [NOW$2]
21638
+ companions: [NOW$3]
20530
21639
  }),
20531
21640
  numeric({
20532
21641
  id: "power",
@@ -20537,7 +21646,7 @@ var GROUP_TEMPLATES = [
20537
21646
  quantity: "power",
20538
21647
  defaultAggregate: "sum",
20539
21648
  empty: "omit",
20540
- companions: [NOW$2]
21649
+ companions: [NOW$3]
20541
21650
  }),
20542
21651
  numeric({
20543
21652
  id: "energy",
@@ -20548,7 +21657,7 @@ var GROUP_TEMPLATES = [
20548
21657
  quantity: "energy",
20549
21658
  defaultAggregate: "sum",
20550
21659
  empty: "omit",
20551
- companions: [NOW$2]
21660
+ companions: [NOW$3]
20552
21661
  }),
20553
21662
  numeric({
20554
21663
  id: "battery",
@@ -20583,7 +21692,7 @@ var GROUP_TEMPLATES = [
20583
21692
  quantity: "device",
20584
21693
  defaultAggregate: "mean",
20585
21694
  empty: "null",
20586
- companions: [NOW$2]
21695
+ companions: [NOW$3]
20587
21696
  }),
20588
21697
  binary({
20589
21698
  id: "motion",
@@ -22533,116 +23642,6 @@ var DeviceAdminLinkSchema = object({
22533
23642
  });
22534
23643
  method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
22535
23644
  /**
22536
- * Identity envelope for a device's upstream-system metadata.
22537
- *
22538
- * Two jobs:
22539
- *
22540
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
22541
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
22542
- * WS-Discovery endpoint UUID, …). Provider code keeps a
22543
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
22544
- * without round-tripping through `stableId` / `id` mappings.
22545
- *
22546
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
22547
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
22548
- * for vendors where MAC is the natural key). For systems where the
22549
- * live dispatch key can be renamed (HA `entity_id` can be edited),
22550
- * providers detect renames by matching `uniqueId` and patch `id`
22551
- * in-place so dispatch keeps working.
22552
- *
22553
- * **Display unit and precision are NOT stored here.** They are a read-time
22554
- * preference (D944). A polymorphic reading stores its `quantity` here once,
22555
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
22556
- * neither is repeated on each sample. The `device_class` tag is an
22557
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
22558
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
22559
- *
22560
- * The `raw` escape hatch carries provider-specific attributes that don't
22561
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
22562
- * details, …). Providers control what lands here.
22563
- */
22564
- var SourceInfoSchema = object({
22565
- /** Live dispatch key — mutable when the source system allows rename. */
22566
- id: string(),
22567
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
22568
- system: string(),
22569
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
22570
- * Used to detect rename when `id` changes. */
22571
- uniqueId: string().optional(),
22572
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
22573
- quantity: ReadingQuantitySchema.optional(),
22574
- /** Date / time / datetime shape of a string reading, written once at adopt. */
22575
- dateTimeShape: _enum([
22576
- "date",
22577
- "time",
22578
- "datetime"
22579
- ]).optional(),
22580
- /** Provider-specific extras that don't fit the structured slots.
22581
- * Whitelist what goes here per provider — do NOT dump entire HA
22582
- * attribute blobs (would explode the meta JSON column). */
22583
- raw: record(string(), unknown()).optional()
22584
- });
22585
- /**
22586
- * Build the synthetic SourceInfo every existing provider falls back to
22587
- * when no upstream value has been persisted. Keeps non-migrated providers
22588
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
22589
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
22590
- * id. Real upstream identity replaces this once a provider migrates and
22591
- * calls `updateSourceInfo()` with concrete values.
22592
- */
22593
- function synthesizeSourceInfo(input) {
22594
- return {
22595
- id: input.stableId,
22596
- system: input.addonId
22597
- };
22598
- }
22599
- /**
22600
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
22601
- * patch values are ignored (no field clobber); to clear a field
22602
- * explicitly, providers omit it from the patch and rebuild from scratch
22603
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
22604
- * bother with a `null`-means-delete sentinel.
22605
- *
22606
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
22607
- * merges over existing `raw` keys instead of replacing the whole bag.
22608
- */
22609
- function mergeSourceInfo(prev, patch) {
22610
- const next = { ...prev };
22611
- for (const [k, v] of Object.entries(patch)) {
22612
- if (v === void 0) continue;
22613
- if (k === "raw" && typeof v === "object" && v !== null) {
22614
- next.raw = {
22615
- ...prev.raw,
22616
- ...v
22617
- };
22618
- continue;
22619
- }
22620
- next[k] = v;
22621
- }
22622
- return next;
22623
- }
22624
- /**
22625
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
22626
- * blob is a free-form `Record<string, unknown>` shared with hardware-
22627
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
22628
- * lives under a single nested key to keep it composable with those.
22629
- */
22630
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
22631
- /**
22632
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
22633
- * with Zod. Returns `null` when the blob is missing or the nested
22634
- * `sourceInfo` key fails validation — callers fall back to the
22635
- * synthetic default in that case (see `synthesizeSourceInfo`).
22636
- */
22637
- function extractSourceInfoFromMetadata(metadata) {
22638
- if (!metadata) return null;
22639
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
22640
- if (!raw) return null;
22641
- const parsed = SourceInfoSchema.safeParse(raw);
22642
- if (!parsed.success) return null;
22643
- return parsed.data;
22644
- }
22645
- /**
22646
23645
  * device-discovery — device-scoped capability for parents that host /
22647
23646
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
22648
23647
  * integrations). The parent advertises a list of discoverable children,
@@ -23047,12 +24046,48 @@ var ExposeInputSchema = object({
23047
24046
  capabilities: array(string()).optional()
23048
24047
  });
23049
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 });
23050
24079
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
23051
24080
  kind: "mutation",
23052
24081
  auth: "admin"
23053
24082
  }), method(UnexposeInputSchema, _void(), {
23054
24083
  kind: "mutation",
23055
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"
23056
24091
  });
23057
24092
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
23058
24093
  var ICON_CATEGORY_IDS = [
@@ -44268,6 +45303,14 @@ var embeddingEncoderCapability = {
44268
45303
  })
44269
45304
  }
44270
45305
  };
45306
+ method(object({
45307
+ nodeId: string().min(1),
45308
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
45309
+ removedAt: number()
45310
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
45311
+ kind: "mutation",
45312
+ auth: "admin"
45313
+ });
44271
45314
  /**
44272
45315
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
44273
45316
  * so it can delete what it keeps for that device (D865).
@@ -45110,7 +46153,7 @@ var LlmProviderWidgetSchema = object({
45110
46153
  /** ConfigUISchema tree passed through untyped on the wire (the
45111
46154
  * notification-output `ConfigSchemaPassthrough` precedent at
45112
46155
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
45113
- var ConfigSchemaPassthrough$3 = unknown();
46156
+ var ConfigSchemaPassthrough$2 = unknown();
45114
46157
  var LlmProfileKindDescriptorSchema = object({
45115
46158
  kind: LlmProfileKindSchema,
45116
46159
  label: string(),
@@ -45119,9 +46162,9 @@ var LlmProfileKindDescriptorSchema = object({
45119
46162
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
45120
46163
  addonId: string(),
45121
46164
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
45122
- connectionSchema: ConfigSchemaPassthrough$3,
46165
+ connectionSchema: ConfigSchemaPassthrough$2,
45123
46166
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
45124
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
46167
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
45125
46168
  /** The kind's defaults; a model from `listModels` may override any of them. */
45126
46169
  features: LlmModelFeaturesSchema,
45127
46170
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -45647,10 +46690,10 @@ var CpuBreakdownSchema = object({
45647
46690
  cores: number()
45648
46691
  });
45649
46692
  var MemoryInfoSchema = object({
45650
- percent: number(),
46693
+ percent: number().nullable(),
45651
46694
  totalBytes: number(),
45652
- usedBytes: number(),
45653
- availableBytes: number(),
46695
+ usedBytes: number().nullable(),
46696
+ availableBytes: number().nullable(),
45654
46697
  swapUsedBytes: number(),
45655
46698
  swapTotalBytes: number()
45656
46699
  });
@@ -45954,8 +46997,9 @@ var GetLoadSeriesInputSchema = object({
45954
46997
  });
45955
46998
  var SystemMetricsSchema = object({
45956
46999
  cpuPercent: number(),
45957
- memoryPercent: number(),
45958
- memoryUsedMB: number(),
47000
+ /** `null` when the memory sample failed or was not taken yet (D393). */
47001
+ memoryPercent: number().nullable(),
47002
+ memoryUsedMB: number().nullable(),
45959
47003
  memoryTotalMB: number(),
45960
47004
  diskPercent: number().optional(),
45961
47005
  temperature: number().optional(),
@@ -46191,427 +47235,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46191
47235
  sourcePort: number().optional()
46192
47236
  });
46193
47237
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
46194
- /**
46195
- * notification-output — canonical, capability-gated notification delivery.
46196
- *
46197
- * Apprise-derived model (see
46198
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
46199
- * callers emit ONE canonical `Notification`; each provider declares a
46200
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
46201
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
46202
- * message to what the kind supports — callers never special-case a service.
46203
- *
46204
- * DESIGN DECISIONS (locked):
46205
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
46206
- * `setTargetEnabled`), each provider persisting via the `settings-store`
46207
- * cap. Rationale: the admin UI needs one uniform surface across the
46208
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
46209
- * alternative would fork the UI per addon and cannot host the
46210
- * discovery→adopt flow.
46211
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
46212
- * the generated cap-mount auto-`concatCollection`-fans them across every
46213
- * registered provider (notifiers addon + HA addon) so one catalog is
46214
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
46215
- * `addonId` the generated collection router extracts from the call input.
46216
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
46217
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
46218
- * `storage` / `storage-provider` / `recording` caps over the same path. No
46219
- * base64 fallback needed.
46220
- *
46221
- * TODO (deferred, closed-set change — separate decision): add
46222
- * `providerKind: 'notify'` so notification providers surface on the unified
46223
- * admin "Integrations" page.
46224
- */
46225
- /**
46226
- * Zentik-derived typed-media enum — the superset across every kind. Each
46227
- * adapter picks what it supports and the degrade engine filters the rest.
46228
- */
46229
- var AttachmentMediaTypeSchema = _enum([
46230
- "image",
46231
- "video",
46232
- "gif",
46233
- "audio",
46234
- "icon"
46235
- ]);
46236
- /**
46237
- * A single attachment. Exactly one of `url` (remote source, most adapters
46238
- * prefer this) or `bytes` (inline source; required for Pushover-style
46239
- * bytes-only kinds) MUST be present — the degrade engine expresses a
46240
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
46241
- */
46242
- var AttachmentSchema = object({
46243
- mediaType: AttachmentMediaTypeSchema,
46244
- url: string().optional(),
46245
- bytes: _instanceof(Uint8Array).optional(),
46246
- mime: string().optional(),
46247
- name: string().optional()
46248
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
46249
- var NotificationFormatSchema = _enum([
46250
- "text",
46251
- "markdown",
46252
- "html"
46253
- ]);
46254
- /**
46255
- * The CLOSED icon vocabulary an action button may use.
46256
- *
46257
- * A closed set, not a free string, and that is the whole point: an arbitrary
46258
- * icon name is one that ntfy renders, zentik silently drops, and nobody
46259
- * notices — the same class of gap as a zone vocabulary nothing produced
46260
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
46261
- * declares `actionIcons: false` and the degrade engine strips the field.
46262
- *
46263
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
46264
- * renderer's icon set; "acknowledge" survives an adapter that draws it
46265
- * differently.
46266
- *
46267
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
46268
- *
46269
- * These names are for US. **No adapter may forward one verbatim.** Each maps
46270
- * the whole set onto its own renderer's vocabulary through a
46271
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
46272
- * fallback, so adding a member here fails every adapter's build until someone
46273
- * decides its glyph, which is the only place that decision can be made
46274
- * honestly.
46275
- *
46276
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
46277
- * `disarm` straight through; iOS feeds that string to
46278
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
46279
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
46280
- * mapping, and "the field is documented" is not "the value renders".
46281
- *
46282
- * Adding a member is TRAIN-BOUND. The enum lives in the published
46283
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
46284
- * so an addon that emits a token the running hub does not know does not lose an
46285
- * icon — its whole `send` fails Zod validation and the notification never
46286
- * arrives. Never emit a new token from an addon before the train carrying it.
46287
- */
46288
- var NotificationActionIconSchema = _enum([
46289
- "acknowledge",
46290
- "dismiss",
46291
- "silence",
46292
- "snooze",
46293
- "view",
46294
- "play",
46295
- "open",
46296
- "close",
46297
- "lock",
46298
- "unlock",
46299
- "arm",
46300
- "arm-home",
46301
- "arm-away",
46302
- "arm-night",
46303
- "disarm",
46304
- "light",
46305
- "alert",
46306
- "camera"
46307
- ]);
46308
- /** A single tap-through action button. */
46309
- var NotificationActionSchema = object({
46310
- id: string(),
46311
- label: string(),
46312
- url: string().optional(),
46313
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
46314
- icon: NotificationActionIconSchema.optional(),
46315
- /**
46316
- * Renders in a warning style where the notifier supports it.
46317
- *
46318
- * A HINT, never a gate. The callback's authority is its token and nothing
46319
- * else — see `notification-center/action-token.ts` for what that does and
46320
- * does not buy.
46321
- */
46322
- destructive: boolean().optional(),
46323
- /**
46324
- * How the tap should REACH the url.
46325
- *
46326
- * `navigate` (absent, and every button authored before this field) opens it:
46327
- * the phone leaves the notification and shows whatever the callback returns.
46328
- * That is right for a button whose answer the operator wants to read.
46329
- *
46330
- * `background` fires it as a POST and stays put. It exists for the buttons
46331
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
46332
- * an answer to the notification, and being thrown into a browser tab to
46333
- * confirm it costs more attention than the notification did. A backend that
46334
- * cannot do a background call renders it as an ordinary link (the adapters
46335
- * fall back rather than dropping the button), so this is a preference, never
46336
- * a requirement.
46337
- */
46338
- mode: _enum(["navigate", "background"]).optional()
46339
- });
46340
- /** A delivery parameter value: what a notifier declared it takes. */
46341
- var DeliveryParamValueSchema = union([
46342
- string(),
46343
- number(),
46344
- boolean()
46345
- ]);
46346
- /**
46347
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
46348
- * addresses a target of that kind may set it; a kind that declares nothing is
46349
- * never handed one. The admin UI derives its control from this descriptor —
46350
- * no editor knows a service by name.
46351
- *
46352
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
46353
- * run on the addon cap path).
46354
- */
46355
- var DeliveryParamSchema = object({
46356
- key: string().min(1),
46357
- label: string(),
46358
- /** i18n key of the label, resolved by the declaring addon's namespace. */
46359
- labelKey: string().optional(),
46360
- type: _enum([
46361
- "enum",
46362
- "number",
46363
- "boolean",
46364
- "string"
46365
- ]),
46366
- /** `enum` only: the closed set of accepted values. */
46367
- options: array(object({
46368
- value: string(),
46369
- label: string()
46370
- })).optional(),
46371
- /** `number` only. */
46372
- min: number().optional(),
46373
- max: number().optional(),
46374
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
46375
- default: DeliveryParamValueSchema.optional()
46376
- });
46377
- /**
46378
- * The canonical notification. `body` is the only hard field (Apprise model).
46379
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
46380
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
46381
- * the adapter maps this ordinal onto its native level. `level?` is an
46382
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
46383
- * `priority` for that one target.
46384
- */
46385
- var NotificationSchema = object({
46386
- body: string(),
46387
- title: string().optional(),
46388
- format: NotificationFormatSchema.default("text"),
46389
- priority: number().int().min(1).max(5).default(3),
46390
- level: string().optional(),
46391
- attachments: array(AttachmentSchema).optional(),
46392
- clickUrl: string().optional(),
46393
- actions: array(NotificationActionSchema).optional(),
46394
- sound: string().optional(),
46395
- ttl: number().optional(),
46396
- tag: string().optional(),
46397
- deviceId: number().optional(),
46398
- eventId: string().optional(),
46399
- metadata: record(string(), unknown()).optional(),
46400
- /**
46401
- * Values for the parameters the target's kind DECLARES in
46402
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
46403
- * declaration by the sender; a notifier reads only the keys it declared.
46404
- */
46405
- delivery: record(string(), DeliveryParamValueSchema).optional()
46406
- });
46407
- /** One declared native severity/priority level for a kind. */
46408
- var TargetKindLevelSchema = object({
46409
- id: string(),
46410
- label: string(),
46411
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
46412
- ordinal: number().int().min(1).max(5).nullable(),
46413
- flags: object({
46414
- critical: boolean().optional(),
46415
- silent: boolean().optional(),
46416
- noPush: boolean().optional()
46417
- }).optional(),
46418
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
46419
- requires: array(string()).optional(),
46420
- description: string().optional()
46421
- });
46422
- /** The full capability block consulted before dispatch. */
46423
- var TargetKindCapsSchema = object({
46424
- attachments: object({
46425
- mediaTypes: array(AttachmentMediaTypeSchema),
46426
- mode: _enum([
46427
- "url",
46428
- "bytes",
46429
- "both"
46430
- ]),
46431
- max: number().int().nonnegative(),
46432
- maxBytes: number().int().positive().optional()
46433
- }),
46434
- /** Max action buttons (0 = none). */
46435
- actions: number().int().nonnegative(),
46436
- /**
46437
- * Whether this kind renders a per-action ICON.
46438
- *
46439
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
46440
- * run on the addon cap path — three production failures in one day taught
46441
- * this repo that once. Absent is read as false by the degrade engine, which
46442
- * is the safe direction: an icon that is not rendered costs nothing, an icon
46443
- * assumed and dropped costs the operator's trust in the field.
46444
- */
46445
- actionIcons: boolean().optional(),
46446
- levels: array(TargetKindLevelSchema),
46447
- format: array(NotificationFormatSchema),
46448
- clickUrl: boolean(),
46449
- sound: boolean(),
46450
- ttl: boolean(),
46451
- bodyMaxLen: number().int().positive()
46452
- });
46453
- /**
46454
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
46455
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
46456
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
46457
- * the union is large and not meant for runtime validation here; the exported
46458
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46459
- */
46460
- var ConfigSchemaPassthrough$2 = unknown();
46461
- var TargetKindSchema = object({
46462
- kind: string(),
46463
- label: string(),
46464
- icon: string(),
46465
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
46466
- addonId: string(),
46467
- /**
46468
- * URL of the kind's bundled BRAND icon, served by the providing addon over
46469
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
46470
- * when the addon bundles no icon for that kind — the client then falls back
46471
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
46472
- *
46473
- * Root-relative on purpose: it resolves against whatever origin serves a web
46474
- * client, and a native client joins it onto its own hub base.
46475
- *
46476
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
46477
- * field that survived only because the runtime cap-router forwards provider
46478
- * output verbatim — so every consumer had to re-declare it by hand to stop
46479
- * its own Zod parse from stripping it, and the whole arrangement would have
46480
- * broken silently the moment output validation was tightened anywhere.
46481
- */
46482
- iconUrl: string().optional(),
46483
- /**
46484
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
46485
- *
46486
- * The server knows this and therefore says it, because the client cannot
46487
- * safely guess: a React-Native client renders SVG and raster through two
46488
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
46489
- * not decode SVG on iOS/Android), so without this it silently fell back to a
46490
- * placeholder glyph for every vector icon while the web build looked fine.
46491
- *
46492
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
46493
- * not been updated — a client that cannot determine the type should prefer
46494
- * its raster path, which is the safe default for an unknown image.
46495
- */
46496
- iconMediaType: string().optional(),
46497
- configSchema: ConfigSchemaPassthrough$2,
46498
- /**
46499
- * The config keys that decide WHERE this kind's secrets are sent (server,
46500
- * base URL, instance, chat, recipient). A save that changes any of them
46501
- * while leaving a secret redacted is refused by the provider (D634), so an
46502
- * editor uses this to ask for the secret on exactly those edits instead of
46503
- * on every edit.
46504
- *
46505
- * Optional: absent from a provider that holds no secret (the Home Assistant
46506
- * kinds) and from a provider older than this field — an editor that finds
46507
- * it absent must assume ANY edit may need the secret.
46508
- */
46509
- destinationKeys: array(string()).optional(),
46510
- supportsDiscovery: boolean(),
46511
- /**
46512
- * Extra delivery parameters a rule may set for a target of this kind (D938).
46513
- * Absent = none; the rule editor then offers nothing.
46514
- */
46515
- deliveryParams: array(DeliveryParamSchema).optional(),
46516
- caps: TargetKindCapsSchema
46517
- });
46518
- /**
46519
- * A persisted target. `config` holds secrets; providers REDACT secret fields
46520
- * (return a presence marker only) when serving `listTargets` — never
46521
- * round-trip a stored secret to the UI.
46522
- */
46523
- var TargetSchema = object({
46524
- id: string(),
46525
- name: string(),
46526
- kind: string(),
46527
- addonId: string(),
46528
- enabled: boolean(),
46529
- config: record(string(), unknown())
46530
- });
46531
- /** A discovery-surfaced candidate (config is partial + non-secret). */
46532
- var DiscoveredTargetSchema = object({
46533
- kind: string(),
46534
- suggestedName: string(),
46535
- config: record(string(), unknown())
46536
- });
46537
- /** The degrade engine's report — what was resolved / dropped / degraded. */
46538
- var RenderedAsSchema = object({
46539
- level: string(),
46540
- format: NotificationFormatSchema,
46541
- attachmentsSent: number().int().nonnegative(),
46542
- actionsSent: number().int().nonnegative(),
46543
- truncated: boolean(),
46544
- dropped: array(string())
46545
- });
46546
- var SendResultSchema = object({
46547
- success: boolean(),
46548
- error: string().optional(),
46549
- renderedAs: RenderedAsSchema.optional()
46550
- });
46551
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
46552
- var TestResultSchema = SendResultSchema;
46553
- /**
46554
- * Pre-save credential probe against a partial (unsaved) target config.
46555
- * `resolvedUrl` never includes secrets (token stripped from path).
46556
- */
46557
- var ProbeTargetConfigResultSchema = object({
46558
- ok: boolean(),
46559
- resolvedUrl: string(),
46560
- httpStatus: number().int().optional(),
46561
- stateInstance: string().optional(),
46562
- error: string().optional()
46563
- });
46564
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
46565
- kind: string(),
46566
- config: record(string(), unknown()).optional(),
46567
- /**
46568
- * When editing / duplicating, the blank password field never round-trips
46569
- * the stored token. Pass the source target id so the provider can restore
46570
- * secrets server-side before dialling (same merge as upsert).
46571
- */
46572
- targetId: string().optional()
46573
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
46574
- kind: string(),
46575
- config: record(string(), unknown()),
46576
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
46577
- targetId: string().optional()
46578
- }), ProbeTargetConfigResultSchema, {
46579
- kind: "mutation",
46580
- auth: "admin"
46581
- }), method(object({
46582
- targetId: string(),
46583
- notification: NotificationSchema
46584
- }), SendResultSchema, {
46585
- kind: "mutation",
46586
- auth: "admin"
46587
- }), method(object({
46588
- targetId: string(),
46589
- sample: NotificationSchema.optional()
46590
- }), TestResultSchema, {
46591
- kind: "mutation",
46592
- auth: "admin"
46593
- }), method(object({
46594
- target: TargetSchema,
46595
- /**
46596
- * Duplicate flow: create a NEW target id while copying secrets from an
46597
- * existing row. The UI never holds the stored token; blank / redacted
46598
- * password fields are restored from this source (D634 still applies if
46599
- * destinations diverge without a re-typed secret).
46600
- */
46601
- cloneSecretsFrom: string().optional()
46602
- }), TargetSchema, {
46603
- kind: "mutation",
46604
- auth: "admin"
46605
- }), method(object({ targetId: string() }), _void(), {
46606
- kind: "mutation",
46607
- auth: "admin"
46608
- }), method(object({
46609
- targetId: string(),
46610
- enabled: boolean()
46611
- }), _void(), {
46612
- kind: "mutation",
46613
- auth: "admin"
46614
- });
46615
47238
  var AUDIO_MACRO_LABELS = [
46616
47239
  {
46617
47240
  id: "speech",
@@ -54856,14 +55479,6 @@ var EngineProvisioningSchema = object({
54856
55479
  */
54857
55480
  configIssues: array(string()).optional()
54858
55481
  });
54859
- var PipelineStepInputSchema = lazy(() => object({
54860
- addonId: string(),
54861
- modelId: string().optional(),
54862
- enabled: boolean().default(true),
54863
- children: array(PipelineStepInputSchema).optional(),
54864
- settings: record(string(), unknown()).optional(),
54865
- jumpDeviceKey: string().optional()
54866
- }));
54867
55482
  var ModelSubstitutionSchema = object({
54868
55483
  addonId: string(),
54869
55484
  chosen: string(),
@@ -54942,7 +55557,6 @@ var ConfigUISchemaBridge = custom();
54942
55557
  var ConfigUISchemaNullableBridge = custom();
54943
55558
  var InferenceCapabilitiesBridge = custom();
54944
55559
  var ModelAvailabilityListBridge = custom();
54945
- var PipelineRunResultBridge = custom();
54946
55560
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
54947
55561
  modelId: string(),
54948
55562
  settings: record(string(), unknown()).readonly()
@@ -57938,11 +58552,13 @@ var ImageContractSchema = object({
57938
58552
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
57939
58553
  * produced it.
57940
58554
  *
57941
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
57942
- * update is an ANNOUNCEMENT. The notification body carries the exact command
57943
- * for a `docker` shell and says the app restarts into the new build for
57944
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
57945
- * 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).
57946
58562
  */
57947
58563
  var WrapperIdentitySchema = object({
57948
58564
  kind: _enum([
@@ -57990,6 +58606,49 @@ var ServerRollbackInfoSchema = object({
57990
58606
  reason: string()
57991
58607
  });
57992
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
+ /**
57993
58652
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
57994
58653
  * `resolveRunnerId` over its forked addons; agent: the runners its process
57995
58654
  * service holds, because only the hub plans) against the states the process
@@ -58093,7 +58752,12 @@ var ServerPackageStatusSchema = object({
58093
58752
  * on a provider that predates it. Absent is "not checked yet", never "all
58094
58753
  * well": a consumer that needs the answer treats it as pending.
58095
58754
  */
58096
- 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()
58097
58761
  });
58098
58762
  var ServerUpdateCheckResultSchema = object({
58099
58763
  packageName: string(),
@@ -58176,6 +58840,12 @@ var ServerUpdateActionResultSchema = object({
58176
58840
  */
58177
58841
  blockers: array(UpdateBlockerSchema).optional()
58178
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() });
58179
58849
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
58180
58850
  kind: "mutation",
58181
58851
  auth: "admin"
@@ -58192,12 +58862,18 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
58192
58862
  kind: "mutation",
58193
58863
  auth: "admin",
58194
58864
  timeoutMs: 16 * 6e4
58865
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
58866
+ kind: "mutation",
58867
+ auth: "admin"
58195
58868
  }), method(_void(), ServerUpdateActionResultSchema, {
58196
58869
  kind: "mutation",
58197
58870
  auth: "admin"
58198
58871
  }), method(_void(), ServerUpdateActionResultSchema, {
58199
58872
  kind: "mutation",
58200
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"
58201
58877
  });
58202
58878
  /**
58203
58879
  * `smtp-provider` — pluggable email delivery surface.
@@ -61339,13 +62015,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
61339
62015
  }), method(ActionIdInput, _void(), {
61340
62016
  kind: "mutation",
61341
62017
  auth: "admin"
61342
- }), 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
+ }), {
61343
62022
  kind: "mutation",
61344
62023
  auth: "admin"
61345
62024
  }), method(object({
61346
62025
  actionId: string().min(1),
61347
62026
  action: ActionInputSchema.partial()
61348
- }), object({ action: ActionEntrySchema }), {
62027
+ }), object({
62028
+ action: ActionEntrySchema,
62029
+ warnings: array(UnitWarningSchema).optional()
62030
+ }), {
61349
62031
  kind: "mutation",
61350
62032
  auth: "admin"
61351
62033
  }), method(ActionIdInput, _void(), {
@@ -61797,6 +62479,510 @@ onBrightnessChanged: { data: object({
61797
62479
  minIntervalMs: 0
61798
62480
  } } }
61799
62481
  };
62482
+ /**
62483
+ * The role vocabulary of a remote (D963): what a button MEANS, independent of
62484
+ * how it is wired. A layout template places buttons by role; a device binds a
62485
+ * role to an action; an exporter asks for a role by name. Roles are shared so
62486
+ * one layout fits every appliance and one voice command fits every binding.
62487
+ *
62488
+ * A role outside this list is `custom:<key>` — the operator's own button (a
62489
+ * SmartIR source, a channel preset). The key is a lowercase slug.
62490
+ */
62491
+ var REMOTE_KNOWN_ROLES = [
62492
+ "power",
62493
+ "power_on",
62494
+ "power_off",
62495
+ "volume_up",
62496
+ "volume_down",
62497
+ "mute",
62498
+ "channel_up",
62499
+ "channel_down",
62500
+ "up",
62501
+ "down",
62502
+ "left",
62503
+ "right",
62504
+ "ok",
62505
+ "back",
62506
+ "home",
62507
+ "menu",
62508
+ "info",
62509
+ "guide",
62510
+ "exit",
62511
+ "input",
62512
+ "play",
62513
+ "pause",
62514
+ "play_pause",
62515
+ "stop",
62516
+ "next",
62517
+ "previous",
62518
+ "fast_forward",
62519
+ "rewind",
62520
+ "digit_0",
62521
+ "digit_1",
62522
+ "digit_2",
62523
+ "digit_3",
62524
+ "digit_4",
62525
+ "digit_5",
62526
+ "digit_6",
62527
+ "digit_7",
62528
+ "digit_8",
62529
+ "digit_9"
62530
+ ];
62531
+ var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
62532
+ var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
62533
+ function isRemoteKnownRole(value) {
62534
+ return KNOWN_ROLES.has(value);
62535
+ }
62536
+ function isRemoteCustomKey(key) {
62537
+ return CUSTOM_KEY_RE.test(key);
62538
+ }
62539
+ /** The key of a `custom:<key>` role, or null for any other string. */
62540
+ function customKeyOfRole(role) {
62541
+ if (!role.startsWith("custom:")) return null;
62542
+ const key = role.slice(7);
62543
+ return isRemoteCustomKey(key) ? key : null;
62544
+ }
62545
+ function isRemoteRole(value) {
62546
+ return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
62547
+ }
62548
+ var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
62549
+ var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
62550
+ var RemoteCustomButtonSchema = object({
62551
+ /** The slug after `custom:` in the role. */
62552
+ key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
62553
+ /** What the operator calls it, and what a voice command may name. */
62554
+ name: string().min(1).max(60)
62555
+ });
62556
+ var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
62557
+ var RemoteConfigSchema = object({
62558
+ layoutTemplateId: string().min(1).nullable(),
62559
+ /**
62560
+ * The transmitter device new bindings and an import send through. A binding
62561
+ * names its own device, so one command may override it.
62562
+ */
62563
+ defaultTransmitterId: number().int().nonnegative().nullable().default(null),
62564
+ /** Keyed by role. A role with no entry falls back to the native cap. */
62565
+ bindings: record(string(), RemoteBindingSchema),
62566
+ customButtons: array(RemoteCustomButtonSchema).max(100),
62567
+ power: object({ mode: RemotePowerModeSchema })
62568
+ }).superRefine((config, ctx) => {
62569
+ const roles = Object.keys(config.bindings);
62570
+ if (roles.length > 200) ctx.addIssue({
62571
+ code: "custom",
62572
+ message: "too many bindings",
62573
+ path: ["bindings"]
62574
+ });
62575
+ for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
62576
+ code: "custom",
62577
+ message: `"${role}" is not a role`,
62578
+ path: ["bindings", role]
62579
+ });
62580
+ const keys = /* @__PURE__ */ new Set();
62581
+ config.customButtons.forEach((button, index) => {
62582
+ if (keys.has(button.key)) ctx.addIssue({
62583
+ code: "custom",
62584
+ message: `custom button "${button.key}" is listed twice`,
62585
+ path: [
62586
+ "customButtons",
62587
+ index,
62588
+ "key"
62589
+ ]
62590
+ });
62591
+ keys.add(button.key);
62592
+ });
62593
+ for (const role of roles) {
62594
+ const key = customKeyOfRole(role);
62595
+ if (key !== null && !keys.has(key)) ctx.addIssue({
62596
+ code: "custom",
62597
+ message: `${role} is bound but has no entry in customButtons`,
62598
+ path: ["bindings", role]
62599
+ });
62600
+ }
62601
+ });
62602
+ object({
62603
+ power: object({
62604
+ state: _enum(["on", "off"]),
62605
+ at: number()
62606
+ }).nullable(),
62607
+ muted: object({
62608
+ value: boolean(),
62609
+ at: number()
62610
+ }).nullable()
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
+ ]);
62636
+ var RemoteLayoutButtonSchema = object({
62637
+ id: string().min(1).max(40),
62638
+ /** 0-based grid position. */
62639
+ row: number().int().min(0).max(24),
62640
+ col: number().int().min(0).max(24),
62641
+ rowSpan: number().int().min(1).max(24),
62642
+ colSpan: number().int().min(1).max(24),
62643
+ icon: string().min(1).max(60).optional(),
62644
+ label: string().min(1).max(40).optional(),
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()
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
+ }
62728
+ object({
62729
+ columns: number().int().min(1).max(24),
62730
+ rows: number().int().min(1).max(24),
62731
+ buttons: array(RemoteLayoutButtonSchema).max(200),
62732
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
62733
+ }).superRefine((grid, ctx) => {
62734
+ const ids = /* @__PURE__ */ new Set();
62735
+ const taken = /* @__PURE__ */ new Map();
62736
+ grid.buttons.forEach((button, index) => {
62737
+ if (ids.has(button.id)) ctx.addIssue({
62738
+ code: "custom",
62739
+ message: `button id "${button.id}" is used twice`,
62740
+ path: [
62741
+ "buttons",
62742
+ index,
62743
+ "id"
62744
+ ]
62745
+ });
62746
+ ids.add(button.id);
62747
+ if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
62748
+ ctx.addIssue({
62749
+ code: "custom",
62750
+ message: `button "${button.id}" leaves the grid`,
62751
+ path: ["buttons", index]
62752
+ });
62753
+ return;
62754
+ }
62755
+ for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
62756
+ const cell = `${r}:${c}`;
62757
+ const other = taken.get(cell);
62758
+ if (other !== void 0) {
62759
+ ctx.addIssue({
62760
+ code: "custom",
62761
+ message: `buttons "${other}" and "${button.id}" overlap`,
62762
+ path: ["buttons", index]
62763
+ });
62764
+ return;
62765
+ }
62766
+ taken.set(cell, button.id);
62767
+ }
62768
+ });
62769
+ refineGroups(grid, ctx);
62770
+ });
62771
+ var RemoteBoundCommandSchema = object({
62772
+ role: RemoteRoleSchema,
62773
+ /** The operator's name for a custom button; absent for a known role. */
62774
+ label: string().optional(),
62775
+ custom: boolean()
62776
+ });
62777
+ var RemoteCommandListSchema = object({
62778
+ layoutTemplateId: string().nullable(),
62779
+ power: object({ mode: RemotePowerModeSchema }),
62780
+ /** Only the roles that are BOUND — the ones a press can reach. */
62781
+ commands: array(RemoteBoundCommandSchema)
62782
+ });
62783
+ var RemoteRefusalReasonSchema = _enum([
62784
+ "no-remote",
62785
+ "unbound-role",
62786
+ "action-failed"
62787
+ ]);
62788
+ var RemoteSendResultSchema = discriminatedUnion("status", [
62789
+ object({
62790
+ status: literal("sent"),
62791
+ /** How many times the action ran. */
62792
+ sends: number().int().min(1)
62793
+ }),
62794
+ object({
62795
+ status: literal("skipped"),
62796
+ reason: literal("already-in-state")
62797
+ }),
62798
+ object({
62799
+ status: literal("refused"),
62800
+ reason: RemoteRefusalReasonSchema,
62801
+ detail: string().optional()
62802
+ })
62803
+ ]);
62804
+ var RemoteMediaCommandSchema = _enum([
62805
+ "play",
62806
+ "pause",
62807
+ "stop",
62808
+ "next",
62809
+ "previous",
62810
+ "setMute",
62811
+ "selectSource"
62812
+ ]);
62813
+ /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
62814
+ var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
62815
+ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
62816
+ status: literal("refused"),
62817
+ reason: _enum(["unknown-template", "not-a-media-player"]),
62818
+ detail: string()
62819
+ })]);
62820
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
62821
+ DeviceType.MediaPlayer;
62822
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
62823
+ kind: "mutation",
62824
+ auth: "admin"
62825
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
62826
+ role: RemoteRoleSchema,
62827
+ /** Sends in a row (volume steps). */
62828
+ repeat: number().int().min(1).max(50).default(1)
62829
+ }), RemoteSendResultSchema, {
62830
+ kind: "mutation",
62831
+ auth: "protected"
62832
+ }), method(deviceInput.extend({
62833
+ command: RemoteMediaCommandSchema,
62834
+ muted: boolean().optional(),
62835
+ source: string().optional(),
62836
+ repeat: number().int().min(1).max(50).default(1)
62837
+ }), RemoteMediaCommandResultSchema, {
62838
+ kind: "mutation",
62839
+ auth: "protected"
62840
+ });
62841
+ var TemplateKindSchema = _enum(["remote"]);
62842
+ /**
62843
+ * template — the generic store of reusable, named operator data (D963). A
62844
+ * record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
62845
+ * button grid of a remote) and the schema each payload must satisfy are
62846
+ * declared by the system in `template-kinds.ts`. Nothing here is specific to a
62847
+ * kind: the engine validates a payload against its kind's schema and, on a
62848
+ * delete, asks the kind's owner whether anything still points at the record.
62849
+ *
62850
+ * Every refusal is a typed result — a UI says "unknown kind" or names the
62851
+ * devices that still use a template, without parsing text.
62852
+ */
62853
+ var TemplateRecordSchema = object({
62854
+ id: string().min(1),
62855
+ kind: TemplateKindSchema,
62856
+ name: string().min(1).max(120),
62857
+ /** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
62858
+ payload: unknown(),
62859
+ createdAt: number(),
62860
+ updatedAt: number()
62861
+ });
62862
+ var TemplateWriteResultSchema = discriminatedUnion("status", [object({
62863
+ status: literal("saved"),
62864
+ template: TemplateRecordSchema
62865
+ }), object({
62866
+ status: literal("refused"),
62867
+ reason: _enum(["invalid-payload", "not-found"]),
62868
+ detail: string()
62869
+ })]);
62870
+ /** A device still pointing at a template. */
62871
+ var TemplateUserSchema = object({
62872
+ deviceId: number().int().nonnegative(),
62873
+ name: string()
62874
+ });
62875
+ var TemplateDeleteResultSchema = discriminatedUnion("status", [
62876
+ object({ status: literal("deleted") }),
62877
+ object({ status: literal("not-found") }),
62878
+ object({
62879
+ status: literal("in-use"),
62880
+ devices: array(TemplateUserSchema).min(1)
62881
+ })
62882
+ ]);
62883
+ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
62884
+ kind: TemplateKindSchema,
62885
+ name: string().min(1).max(120),
62886
+ payload: unknown()
62887
+ }), TemplateWriteResultSchema, {
62888
+ kind: "mutation",
62889
+ auth: "admin"
62890
+ }), method(object({
62891
+ id: string().min(1),
62892
+ name: string().min(1).max(120).optional(),
62893
+ payload: unknown().optional()
62894
+ }), TemplateWriteResultSchema, {
62895
+ kind: "mutation",
62896
+ auth: "admin"
62897
+ }), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
62898
+ kind: "mutation",
62899
+ auth: "admin"
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" });
61800
62986
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
61801
62987
  kind: "mutation",
61802
62988
  auth: "protected"
@@ -62852,14 +64038,13 @@ reset: method(object({
62852
64038
  auth: "admin"
62853
64039
  }) },
62854
64040
  /**
62855
- * A composed consumable (item added to a device with no native consumables
62856
- * provider) refuses `reset` by name: "Only meaningful when the item's
62857
- * `resettable` is true" — a composed item is never resettable until
62858
- * 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).
62859
64044
  */
62860
64045
  composedMethods: { reset: {
62861
64046
  kind: "refuse",
62862
- 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"
62863
64048
  } },
62864
64049
  status: {
62865
64050
  schema: ConsumablesStatusSchema,
@@ -63475,7 +64660,7 @@ var DishwasherStatusSchema = object({
63475
64660
  /** Ms epoch of the last value received from the appliance; 0 = never. */
63476
64661
  lastFetchedAt: number()
63477
64662
  });
63478
- var DeviceIdSchema = number().int().nonnegative();
64663
+ var DeviceIdSchema$1 = number().int().nonnegative();
63479
64664
  /** A value a program option or setting can take. */
63480
64665
  var DishwasherValueSchema = union([
63481
64666
  boolean(),
@@ -63550,77 +64735,77 @@ var dishwasherCapability = {
63550
64735
  * option feature keys; each is checked against the program's constraints.
63551
64736
  */
63552
64737
  start: method(object({
63553
- deviceId: DeviceIdSchema,
64738
+ deviceId: DeviceIdSchema$1,
63554
64739
  program: string().min(1).optional(),
63555
64740
  options: record(string(), DishwasherValueSchema).optional()
63556
64741
  }), _void(), {
63557
64742
  kind: "mutation",
63558
64743
  auth: "protected"
63559
64744
  }),
63560
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
64745
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63561
64746
  kind: "mutation",
63562
64747
  auth: "protected"
63563
64748
  }),
63564
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
64749
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63565
64750
  kind: "mutation",
63566
64751
  auth: "protected"
63567
64752
  }),
63568
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
64753
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63569
64754
  kind: "mutation",
63570
64755
  auth: "protected"
63571
64756
  }),
63572
64757
  /** Select (not start) a program; refused when the appliance does not offer it. */
63573
64758
  setProgram: method(object({
63574
- deviceId: DeviceIdSchema,
64759
+ deviceId: DeviceIdSchema$1,
63575
64760
  program: string().min(1)
63576
64761
  }), _void(), {
63577
64762
  kind: "mutation",
63578
64763
  auth: "protected"
63579
64764
  }),
63580
64765
  setPower: method(object({
63581
- deviceId: DeviceIdSchema,
64766
+ deviceId: DeviceIdSchema$1,
63582
64767
  on: boolean()
63583
64768
  }), _void(), {
63584
64769
  kind: "mutation",
63585
64770
  auth: "protected"
63586
64771
  }),
63587
64772
  setHalfLoad: method(object({
63588
- deviceId: DeviceIdSchema,
64773
+ deviceId: DeviceIdSchema$1,
63589
64774
  on: boolean()
63590
64775
  }), _void(), {
63591
64776
  kind: "mutation",
63592
64777
  auth: "protected"
63593
64778
  }),
63594
64779
  setHygienePlus: method(object({
63595
- deviceId: DeviceIdSchema,
64780
+ deviceId: DeviceIdSchema$1,
63596
64781
  on: boolean()
63597
64782
  }), _void(), {
63598
64783
  kind: "mutation",
63599
64784
  auth: "protected"
63600
64785
  }),
63601
64786
  setExtraDry: method(object({
63602
- deviceId: DeviceIdSchema,
64787
+ deviceId: DeviceIdSchema$1,
63603
64788
  on: boolean()
63604
64789
  }), _void(), {
63605
64790
  kind: "mutation",
63606
64791
  auth: "protected"
63607
64792
  }),
63608
64793
  setIntensiveZone: method(object({
63609
- deviceId: DeviceIdSchema,
64794
+ deviceId: DeviceIdSchema$1,
63610
64795
  on: boolean()
63611
64796
  }), _void(), {
63612
64797
  kind: "mutation",
63613
64798
  auth: "protected"
63614
64799
  }),
63615
64800
  setVarioSpeedPlus: method(object({
63616
- deviceId: DeviceIdSchema,
64801
+ deviceId: DeviceIdSchema$1,
63617
64802
  on: boolean()
63618
64803
  }), _void(), {
63619
64804
  kind: "mutation",
63620
64805
  auth: "protected"
63621
64806
  }),
63622
64807
  setChildLock: method(object({
63623
- deviceId: DeviceIdSchema,
64808
+ deviceId: DeviceIdSchema$1,
63624
64809
  on: boolean()
63625
64810
  }), _void(), {
63626
64811
  kind: "mutation",
@@ -63632,7 +64817,7 @@ var dishwasherCapability = {
63632
64817
  * its declared range / members.
63633
64818
  */
63634
64819
  setOption: method(object({
63635
- deviceId: DeviceIdSchema,
64820
+ deviceId: DeviceIdSchema$1,
63636
64821
  key: string().min(1),
63637
64822
  value: DishwasherValueSchema
63638
64823
  }), _void(), {
@@ -63640,7 +64825,7 @@ var dishwasherCapability = {
63640
64825
  auth: "protected"
63641
64826
  }),
63642
64827
  /** What this appliance can do, from its own description — the UI is derived from it. */
63643
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
64828
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
63644
64829
  },
63645
64830
  status: {
63646
64831
  schema: DishwasherStatusSchema,
@@ -66072,9 +67257,35 @@ var MediaInfoSchema = object({
66072
67257
  /** Optional cover-art / thumbnail URL. */
66073
67258
  imageUrl: string().optional()
66074
67259
  });
67260
+ /**
67261
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
67262
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
67263
+ * tracks what it last SENT; it moves only after a send was accepted, and a
67264
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
67265
+ * guess, never as a reading.
67266
+ */
67267
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
67268
+ /** The fields of a media-player status a driver can only guess. */
67269
+ var MediaPlayerAssumedFieldSchema = _enum([
67270
+ "state",
67271
+ "volumeLevel",
67272
+ "isMuted",
67273
+ "source"
67274
+ ]);
66075
67275
  var MediaPlayerStatusSchema = object({
66076
67276
  /** Playback lifecycle state. */
66077
67277
  state: MediaPlayerStateSchema,
67278
+ /**
67279
+ * Whether the state fields are readings or the driver's own bookkeeping.
67280
+ * A slice written before this field existed parses as `reported`.
67281
+ */
67282
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
67283
+ /**
67284
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
67285
+ * this is not empty; a field not listed here is a reading even then (power
67286
+ * from a running-state source, mute guessed from the last send).
67287
+ */
67288
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
66078
67289
  /** Volume as 0..100 inclusive. Null when the device has no volume
66079
67290
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
66080
67291
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -67391,7 +68602,8 @@ var TopologyNodeSchema = object({
67391
68602
  isHub: boolean(),
67392
68603
  isOnline: boolean(),
67393
68604
  cpuPercent: number(),
67394
- memoryPercent: number(),
68605
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
68606
+ memoryPercent: number().nullable(),
67395
68607
  uptime: number(),
67396
68608
  lastSeen: string(),
67397
68609
  localIps: array(string()).readonly(),
@@ -68002,6 +69214,14 @@ var PetFeederDeviceStatusSchema = _enum([
68002
69214
  "on_batteries"
68003
69215
  ]);
68004
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
+ }));
68005
69225
  var PetFeederStatusSchema = object({
68006
69226
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
68007
69227
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -68009,9 +69229,10 @@ var PetFeederStatusSchema = object({
68009
69229
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
68010
69230
  * the device has not reported yet. */
68011
69231
  foodLevel: number().nullable().meta(readingMeta("none")),
68012
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
68013
- * feeder's own units. Null when the model reports none. */
68014
- 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")),
68015
69236
  /** True while the pet is eating (`state.eating`). */
68016
69237
  eating: boolean(),
68017
69238
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -68062,6 +69283,14 @@ var PetFeederStatusSchema = object({
68062
69283
  feedSound: boolean(),
68063
69284
  /** Speaker / prompt volume level (device-scaled integer). */
68064
69285
  volume: number().meta(readingMeta("none")),
69286
+ /** Portion (grams) of the last manual feed sent from a single-hopper
69287
+ * stepper — the default the next manual feed starts from. Persisted per
69288
+ * device by the provider; null until a manual feed has been sent. */
69289
+ lastFeedGrams: gramsPortion.nullable(),
69290
+ /** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
69291
+ lastFeedHopper1: gramsPortion.nullable(),
69292
+ /** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
69293
+ lastFeedHopper2: gramsPortion.nullable(),
68065
69294
  /** Ms epoch when the slice was last refreshed from the cloud. */
68066
69295
  lastFetchedAt: number()
68067
69296
  });
@@ -68081,9 +69310,9 @@ var petFeederCapability = {
68081
69310
  */
68082
69311
  feed: method(object({
68083
69312
  deviceId: number().int().nonnegative(),
68084
- grams: gramsPortion.optional(),
68085
- hopper1: gramsPortion.optional(),
68086
- hopper2: gramsPortion.optional()
69313
+ grams: gramsParam.optional(),
69314
+ hopper1: gramsParam.optional(),
69315
+ hopper2: gramsParam.optional()
68087
69316
  }), _void(), {
68088
69317
  kind: "mutation",
68089
69318
  auth: "protected"
@@ -68187,6 +69416,137 @@ var petFeederCapability = {
68187
69416
  }
68188
69417
  } }
68189
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
+ };
68190
69550
  var VehicleSchema = object({
68191
69551
  id: string(),
68192
69552
  name: string(),
@@ -74184,9 +75544,98 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
74184
75544
  kind: "mutation",
74185
75545
  auth: "admin"
74186
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" });
75628
+ discriminatedUnion("kind", [object({
75629
+ kind: literal("raw"),
75630
+ timings: array(number().int()),
75631
+ settings: TransmitSettingsSchema.optional()
75632
+ }), object({
75633
+ kind: literal("pronto"),
75634
+ code: string()
75635
+ })]);
74187
75636
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
74188
75637
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
74189
- 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.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;
74190
75639
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
74191
75640
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
74192
75641
  new Set(CAP_NAMES);
@@ -74590,6 +76039,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
74590
76039
  notifier: notifierCapability,
74591
76040
  numericSensor: numericSensorCapability,
74592
76041
  petFeeder: petFeederCapability,
76042
+ petFountain: petFountainCapability,
74593
76043
  powerMeter: powerMeterCapability,
74594
76044
  presence: presenceCapability,
74595
76045
  pressureSensor: pressureSensorCapability,
@@ -75357,7 +76807,7 @@ var DeclaredDevices = class {
75357
76807
  return removed;
75358
76808
  }
75359
76809
  };
75360
- 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;
75361
76811
  new Set(Object.values(DeviceType));
75362
76812
  Object.freeze({
75363
76813
  "accessories.getStatus": {
@@ -77292,6 +78742,13 @@ Object.freeze({
77292
78742
  access: "create",
77293
78743
  role: "admin"
77294
78744
  },
78745
+ "deviceExport.exposeEntity": {
78746
+ capName: "device-export",
78747
+ capScope: "system",
78748
+ addonId: null,
78749
+ access: "create",
78750
+ role: "admin"
78751
+ },
77295
78752
  "deviceExport.getDeviceLiveContribution": {
77296
78753
  capName: "device-export",
77297
78754
  capScope: "system",
@@ -77320,6 +78777,13 @@ Object.freeze({
77320
78777
  access: "view",
77321
78778
  role: "admin"
77322
78779
  },
78780
+ "deviceExport.listExposedEntities": {
78781
+ capName: "device-export",
78782
+ capScope: "system",
78783
+ addonId: null,
78784
+ access: "view",
78785
+ role: "admin"
78786
+ },
77323
78787
  "deviceExport.listSupportedDeviceKinds": {
77324
78788
  capName: "device-export",
77325
78789
  capScope: "system",
@@ -77327,6 +78791,13 @@ Object.freeze({
77327
78791
  access: "view",
77328
78792
  role: "admin"
77329
78793
  },
78794
+ "deviceExport.listSupportedEntityKinds": {
78795
+ capName: "device-export",
78796
+ capScope: "system",
78797
+ addonId: null,
78798
+ access: "view",
78799
+ role: "admin"
78800
+ },
77330
78801
  "deviceExport.unexposeDevice": {
77331
78802
  capName: "device-export",
77332
78803
  capScope: "system",
@@ -77334,6 +78805,13 @@ Object.freeze({
77334
78805
  access: "create",
77335
78806
  role: "admin"
77336
78807
  },
78808
+ "deviceExport.unexposeEntity": {
78809
+ capName: "device-export",
78810
+ capScope: "system",
78811
+ addonId: null,
78812
+ access: "create",
78813
+ role: "admin"
78814
+ },
77337
78815
  "deviceExtension.listCandidates": {
77338
78816
  capName: "device-extension",
77339
78817
  capScope: "device",
@@ -80155,6 +81633,13 @@ Object.freeze({
80155
81633
  access: "create",
80156
81634
  role: "admin"
80157
81635
  },
81636
+ "nodeRemovalHook.onNodeRemoved": {
81637
+ capName: "node-removal-hook",
81638
+ capScope: "system",
81639
+ addonId: null,
81640
+ access: "create",
81641
+ role: "admin"
81642
+ },
80158
81643
  "nodes.clusterAddonStatus": {
80159
81644
  capName: "nodes",
80160
81645
  capScope: "system",
@@ -80792,6 +82277,48 @@ Object.freeze({
80792
82277
  access: "create",
80793
82278
  role: "viewer"
80794
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
+ },
80795
82322
  "pipelineAnalytics.addRetrainFrameFromRecording": {
80796
82323
  capName: "pipeline-analytics",
80797
82324
  capScope: "device",
@@ -83046,6 +84573,48 @@ Object.freeze({
83046
84573
  access: "create",
83047
84574
  role: "admin"
83048
84575
  },
84576
+ "remote.getConfig": {
84577
+ capName: "remote",
84578
+ capScope: "system",
84579
+ addonId: null,
84580
+ access: "view",
84581
+ role: "admin"
84582
+ },
84583
+ "remote.listCommands": {
84584
+ capName: "remote",
84585
+ capScope: "system",
84586
+ addonId: null,
84587
+ access: "view",
84588
+ role: "admin"
84589
+ },
84590
+ "remote.sendCommand": {
84591
+ capName: "remote",
84592
+ capScope: "system",
84593
+ addonId: null,
84594
+ access: "create",
84595
+ role: "admin"
84596
+ },
84597
+ "remote.sendMediaCommand": {
84598
+ capName: "remote",
84599
+ capScope: "system",
84600
+ addonId: null,
84601
+ access: "create",
84602
+ role: "admin"
84603
+ },
84604
+ "remote.setConfig": {
84605
+ capName: "remote",
84606
+ capScope: "system",
84607
+ addonId: null,
84608
+ access: "create",
84609
+ role: "admin"
84610
+ },
84611
+ "remoteLayoutComponents.listComponents": {
84612
+ capName: "remote-layout-components",
84613
+ capScope: "system",
84614
+ addonId: null,
84615
+ access: "view",
84616
+ role: "admin"
84617
+ },
83049
84618
  "runningState.getStatus": {
83050
84619
  capName: "running-state",
83051
84620
  capScope: "device",
@@ -83158,6 +84727,13 @@ Object.freeze({
83158
84727
  access: "create",
83159
84728
  role: "admin"
83160
84729
  },
84730
+ "serverManagement.getLaunchOverrides": {
84731
+ capName: "server-management",
84732
+ capScope: "system",
84733
+ addonId: null,
84734
+ access: "view",
84735
+ role: "admin"
84736
+ },
83161
84737
  "serverManagement.getServerPackageStatus": {
83162
84738
  capName: "server-management",
83163
84739
  capScope: "system",
@@ -83165,6 +84741,13 @@ Object.freeze({
83165
84741
  access: "view",
83166
84742
  role: "admin"
83167
84743
  },
84744
+ "serverManagement.installDesktopUpdate": {
84745
+ capName: "server-management",
84746
+ capScope: "system",
84747
+ addonId: null,
84748
+ access: "create",
84749
+ role: "admin"
84750
+ },
83168
84751
  "serverManagement.restartServer": {
83169
84752
  capName: "server-management",
83170
84753
  capScope: "system",
@@ -83179,6 +84762,13 @@ Object.freeze({
83179
84762
  access: "create",
83180
84763
  role: "admin"
83181
84764
  },
84765
+ "serverManagement.setLaunchOverrides": {
84766
+ capName: "server-management",
84767
+ capScope: "system",
84768
+ addonId: null,
84769
+ access: "create",
84770
+ role: "admin"
84771
+ },
83182
84772
  "settingsStore.aggregate": {
83183
84773
  capName: "settings-store",
83184
84774
  capScope: "system",
@@ -84236,6 +85826,41 @@ Object.freeze({
84236
85826
  access: "view",
84237
85827
  role: "viewer"
84238
85828
  },
85829
+ "template.create": {
85830
+ capName: "template",
85831
+ capScope: "system",
85832
+ addonId: null,
85833
+ access: "create",
85834
+ role: "admin"
85835
+ },
85836
+ "template.delete": {
85837
+ capName: "template",
85838
+ capScope: "system",
85839
+ addonId: null,
85840
+ access: "delete",
85841
+ role: "admin"
85842
+ },
85843
+ "template.get": {
85844
+ capName: "template",
85845
+ capScope: "system",
85846
+ addonId: null,
85847
+ access: "view",
85848
+ role: "admin"
85849
+ },
85850
+ "template.list": {
85851
+ capName: "template",
85852
+ capScope: "system",
85853
+ addonId: null,
85854
+ access: "view",
85855
+ role: "admin"
85856
+ },
85857
+ "template.update": {
85858
+ capName: "template",
85859
+ capScope: "system",
85860
+ addonId: null,
85861
+ access: "create",
85862
+ role: "admin"
85863
+ },
84239
85864
  "terminalSession.adoptLegacyMonitor": {
84240
85865
  capName: "terminal-session",
84241
85866
  capScope: "system",
@@ -87033,7 +88658,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87033
88658
  min: 4,
87034
88659
  max: 200,
87035
88660
  step: 1,
87036
- unit: "g"
88661
+ unit: "g",
88662
+ quantity: "mass"
87037
88663
  },
87038
88664
  optional: true
87039
88665
  }]
@@ -87052,7 +88678,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87052
88678
  min: 4,
87053
88679
  max: 200,
87054
88680
  step: 1,
87055
- unit: "g"
88681
+ unit: "g",
88682
+ quantity: "mass"
87056
88683
  },
87057
88684
  optional: true
87058
88685
  }, {
@@ -87063,7 +88690,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87063
88690
  min: 4,
87064
88691
  max: 200,
87065
88692
  step: 1,
87066
- unit: "g"
88693
+ unit: "g",
88694
+ quantity: "mass"
87067
88695
  },
87068
88696
  optional: true
87069
88697
  }]
@@ -87204,6 +88832,100 @@ var APPLIANCE_CAP_VOCABULARY = {
87204
88832
  }
87205
88833
  ]
87206
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
+ },
87207
88929
  "media-player": {
87208
88930
  scope: "device",
87209
88931
  noun: t("Media player", "Lettore multimediale"),
@@ -89028,6 +90750,7 @@ var SYSTEM_CAP_VOCABULARY = {
89028
90750
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
89029
90751
  "device-provider": system(INTERNAL),
89030
90752
  "device-removal-hook": system(INTERNAL),
90753
+ "node-removal-hook": system(INTERNAL),
89031
90754
  "device-state": system("the runtime-state mirror every trigger already reads through"),
89032
90755
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
89033
90756
  "embedding-encoder": system(ML),
@@ -89067,6 +90790,8 @@ var SYSTEM_CAP_VOCABULARY = {
89067
90790
  "recording-archive": system(STORAGE),
89068
90791
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
89069
90792
  records: system(INTERNAL),
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"),
89070
90795
  "server-management": system(CLUSTER),
89071
90796
  "settings-store": system(INTERNAL),
89072
90797
  "smtp-provider": system(INTERNAL),
@@ -89079,6 +90804,7 @@ var SYSTEM_CAP_VOCABULARY = {
89079
90804
  "storage-provider": system(STORAGE),
89080
90805
  "stream-broker": system(PIPELINE),
89081
90806
  system: system(CLUSTER),
90807
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
89082
90808
  "terminal-session": system(CLUSTER),
89083
90809
  toast: system(UI_PLUMBING),
89084
90810
  training: system(ML),
@@ -89138,7 +90864,7 @@ function projectAction(cap, action) {
89138
90864
  };
89139
90865
  }
89140
90866
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
89141
- 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;
89142
90868
  DeviceFeature.BatteryOperated;
89143
90869
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
89144
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.";
@@ -90689,6 +92415,36 @@ Object.freeze({
90689
92415
  form: "single",
90690
92416
  optional: false
90691
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
+ }],
90692
92448
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
90693
92449
  name: "deviceId",
90694
92450
  form: "single",
@@ -91505,6 +93261,31 @@ Object.freeze({
91505
93261
  form: "single",
91506
93262
  optional: true
91507
93263
  }],
93264
+ "remote.getConfig": [{
93265
+ name: "deviceId",
93266
+ form: "single",
93267
+ optional: false
93268
+ }],
93269
+ "remote.listCommands": [{
93270
+ name: "deviceId",
93271
+ form: "single",
93272
+ optional: false
93273
+ }],
93274
+ "remote.sendCommand": [{
93275
+ name: "deviceId",
93276
+ form: "single",
93277
+ optional: false
93278
+ }],
93279
+ "remote.sendMediaCommand": [{
93280
+ name: "deviceId",
93281
+ form: "single",
93282
+ optional: false
93283
+ }],
93284
+ "remote.setConfig": [{
93285
+ name: "deviceId",
93286
+ form: "single",
93287
+ optional: false
93288
+ }],
91508
93289
  "runningState.getStatus": [{
91509
93290
  name: "deviceId",
91510
93291
  form: "single",
@@ -95060,8 +96841,6 @@ var NC_TEMPLATE_VARS = [
95060
96841
  * delete it from here then, and the served catalog names it again.
95061
96842
  */
95062
96843
  var NC_TEMPLATE_HELD_BACK_SYSTEM_EVENT_KINDS = [
95063
- "node-restarting",
95064
- "node-updating",
95065
96844
  "alert-critical",
95066
96845
  "alert-error",
95067
96846
  "alert-critical-resolved",
@@ -96874,14 +98653,6 @@ function armRemovalReconcile(opts) {
96874
98653
  timer.unref?.();
96875
98654
  return () => clearInterval(timer);
96876
98655
  }
96877
- discriminatedUnion("kind", [object({
96878
- kind: literal("raw"),
96879
- timings: array(number().int()),
96880
- settings: TransmitSettingsSchema.optional()
96881
- }), object({
96882
- kind: literal("pronto"),
96883
- code: string()
96884
- })]);
96885
98656
  object({
96886
98657
  manufacturer: string().optional(),
96887
98658
  supportedModels: array(string()).optional(),