@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
@@ -5645,7 +5645,7 @@ var ZodIssueCode = {
5645
5645
  var ZodFirstPartyTypeKind;
5646
5646
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5647
5647
  //#endregion
5648
- //#region ../types/dist/sleep-BAvUCi_2.mjs
5648
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5649
5649
  /**
5650
5650
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5651
5651
  * window to float samples (D455).
@@ -8746,65 +8746,6 @@ var MODEL_FORMATS = [
8746
8746
  "pt",
8747
8747
  "gguf"
8748
8748
  ];
8749
- /**
8750
- * Per-call node pinning for `ctx.api` capability calls.
8751
- *
8752
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8753
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8754
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8755
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8756
- * call to that node.
8757
- *
8758
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8759
- * method args), so capability method signatures stay `nodeId`-free — node
8760
- * targeting is a property of the CALL, not of the method. The transport lifts
8761
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8762
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8763
- * which classifies a pinned agent node as `agent-child-forward`
8764
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8765
- *
8766
- * Usage at a call site:
8767
- *
8768
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8769
- */
8770
- /** tRPC `op.context` key carrying a per-call node pin. */
8771
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8772
- /**
8773
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8774
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8775
- *
8776
- * ## The id is normalised here, and it has to be
8777
- *
8778
- * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8779
- * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8780
- * compares a pin against real node ids, so such a pin matches nothing and the
8781
- * call fails with `no provider registered for cap "…"`. The local-first
8782
- * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8783
- * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8784
- * the thing you reach for when you specifically need THIS node — silently
8785
- * addressed a node that does not exist.
8786
- *
8787
- * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8788
- * own node to read the host's hardware-decode backend. It never once answered,
8789
- * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8790
- * while D67's whole premise was that the decoder addon is the authority on
8791
- * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8792
- * hardware", which was false.
8793
- *
8794
- * Normalising in the ONE constructor fixes every caller at once, which is why
8795
- * it is here and not at the call sites.
8796
- */
8797
- function nodePin(nodeId) {
8798
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8799
- }
8800
- /**
8801
- * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8802
- * is idempotent, so passing an already-clean id costs nothing.
8803
- */
8804
- function toNodeId(idOrRunnerId) {
8805
- const head = idOrRunnerId.split("/")[0];
8806
- return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8807
- }
8808
8749
  object({
8809
8750
  field: string().min(1),
8810
8751
  on: literal("advance")
@@ -8897,6 +8838,111 @@ function optionalMethod(input, output, options) {
8897
8838
  function event(data) {
8898
8839
  return { data };
8899
8840
  }
8841
+ /**
8842
+ * Per-call node pinning for `ctx.api` capability calls.
8843
+ *
8844
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
8845
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8846
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8847
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8848
+ * call to that node.
8849
+ *
8850
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8851
+ * method args), so capability method signatures stay `nodeId`-free — node
8852
+ * targeting is a property of the CALL, not of the method. The transport lifts
8853
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8854
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8855
+ * which classifies a pinned agent node as `agent-child-forward`
8856
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8857
+ *
8858
+ * Usage at a call site:
8859
+ *
8860
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8861
+ */
8862
+ /** tRPC `op.context` key carrying a per-call node pin. */
8863
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8864
+ /**
8865
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
8866
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8867
+ *
8868
+ * ## The id is normalised here, and it has to be
8869
+ *
8870
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
8871
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
8872
+ * compares a pin against real node ids, so such a pin matches nothing and the
8873
+ * call fails with `no provider registered for cap "…"`. The local-first
8874
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
8875
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
8876
+ * the thing you reach for when you specifically need THIS node — silently
8877
+ * addressed a node that does not exist.
8878
+ *
8879
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
8880
+ * own node to read the host's hardware-decode backend. It never once answered,
8881
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
8882
+ * while D67's whole premise was that the decoder addon is the authority on
8883
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
8884
+ * hardware", which was false.
8885
+ *
8886
+ * Normalising in the ONE constructor fixes every caller at once, which is why
8887
+ * it is here and not at the call sites.
8888
+ */
8889
+ function nodePin(nodeId) {
8890
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
8891
+ }
8892
+ /**
8893
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
8894
+ * is idempotent, so passing an already-clean id costs nothing.
8895
+ */
8896
+ function toNodeId(idOrRunnerId) {
8897
+ const head = idOrRunnerId.split("/")[0];
8898
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
8899
+ }
8900
+ /**
8901
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8902
+ * declarations. Every addon that contributes a UI page registers a
8903
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8904
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8905
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8906
+ * that admin-ui consumes.
8907
+ *
8908
+ * The split exists because the public listing has a different output
8909
+ * shape than the per-provider raw declarations, and we want both ends
8910
+ * to flow through codegen instead of relying on a hand-written wrapper.
8911
+ */
8912
+ var AddonPageDeclarationSchema = object({
8913
+ id: string(),
8914
+ label: string(),
8915
+ icon: string(),
8916
+ path: string(),
8917
+ /**
8918
+ * Module Federation remote name — must match the `name` field on the
8919
+ * page addon's `federation()` plugin config. Used by admin-ui's
8920
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8921
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8922
+ * contain hyphens).
8923
+ */
8924
+ remoteName: string(),
8925
+ /**
8926
+ * Bundle filename inside the addon's `dist/` dir served at
8927
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8928
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8929
+ * the static-file route can compute an mtime-based cache-buster URL
8930
+ * without a separate filesystem stat.
8931
+ */
8932
+ bundle: string(),
8933
+ /**
8934
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8935
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8936
+ * Any OTHER string creates (or joins) a custom section rendered after
8937
+ * the built-in groups; its label comes from `sectionLabel` (first
8938
+ * declaration wins), falling back to the id. Absent → the legacy
8939
+ * "Addon Pages" group.
8940
+ */
8941
+ section: string().optional(),
8942
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8943
+ sectionLabel: string().optional()
8944
+ });
8945
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8900
8946
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8901
8947
  var VersionOutputSchema$1 = object({ version: string() });
8902
8948
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
@@ -8922,6 +8968,27 @@ function customAction(input, output, options) {
8922
8968
  ...options?.caller ? { caller: "required" } : {}
8923
8969
  };
8924
8970
  }
8971
+ /**
8972
+ * The pipeline step-input shape and the run-result bridge, in a module that
8973
+ * imports nothing but zod and types.
8974
+ *
8975
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8976
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8977
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8978
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8979
+ * them, so every existing import keeps working; addon server code takes them
8980
+ * from `@camstack/types/addon`.
8981
+ */
8982
+ var PipelineStepInputSchema = lazy(() => object({
8983
+ addonId: string(),
8984
+ modelId: string().optional(),
8985
+ enabled: boolean().default(true),
8986
+ children: array(PipelineStepInputSchema).optional(),
8987
+ settings: record(string(), unknown()).optional(),
8988
+ jumpDeviceKey: string().optional()
8989
+ }));
8990
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8991
+ var PipelineRunResultBridge = custom();
8925
8992
  var StaticDirOutputSchema = object({ staticDir: string() });
8926
8993
  var VersionOutputSchema = object({ version: string() });
8927
8994
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9107,6 +9174,7 @@ var CAP_NAMES = Object.freeze([
9107
9174
  "network-link",
9108
9175
  "network-quality",
9109
9176
  "node-certificates",
9177
+ "node-removal-hook",
9110
9178
  "nodes",
9111
9179
  "notification-output",
9112
9180
  "notification-rules",
@@ -9117,6 +9185,7 @@ var CAP_NAMES = Object.freeze([
9117
9185
  "osd",
9118
9186
  "osd-manager",
9119
9187
  "pet-feeder",
9188
+ "pet-fountain",
9120
9189
  "pipeline-analytics",
9121
9190
  "pipeline-executor",
9122
9191
  "pipeline-orchestrator",
@@ -9137,6 +9206,8 @@ var CAP_NAMES = Object.freeze([
9137
9206
  "recording-onboard",
9138
9207
  "recording-signal",
9139
9208
  "records",
9209
+ "remote",
9210
+ "remote-layout-components",
9140
9211
  "running-state",
9141
9212
  "scene-monitor",
9142
9213
  "script-runner",
@@ -9160,6 +9231,7 @@ var CAP_NAMES = Object.freeze([
9160
9231
  "system",
9161
9232
  "tamper",
9162
9233
  "temperature-sensor",
9234
+ "template",
9163
9235
  "terminal-session",
9164
9236
  "toast",
9165
9237
  "training",
@@ -10100,6 +10172,11 @@ Object.freeze({
10100
10172
  "standby"
10101
10173
  ]
10102
10174
  },
10175
+ {
10176
+ "path": "stateSource",
10177
+ "valueType": "enum",
10178
+ "enumValues": ["reported", "assumed"]
10179
+ },
10103
10180
  {
10104
10181
  "path": "volumeLevel",
10105
10182
  "valueType": "number"
@@ -10375,6 +10452,68 @@ Object.freeze({
10375
10452
  {
10376
10453
  "path": "volume",
10377
10454
  "valueType": "number"
10455
+ },
10456
+ {
10457
+ "path": "lastFeedGrams",
10458
+ "valueType": "number"
10459
+ },
10460
+ {
10461
+ "path": "lastFeedHopper1",
10462
+ "valueType": "number"
10463
+ },
10464
+ {
10465
+ "path": "lastFeedHopper2",
10466
+ "valueType": "number"
10467
+ }
10468
+ ],
10469
+ "pet-fountain": [
10470
+ {
10471
+ "path": "waterLevel",
10472
+ "valueType": "number"
10473
+ },
10474
+ {
10475
+ "path": "waterVolume",
10476
+ "valueType": "number"
10477
+ },
10478
+ {
10479
+ "path": "empty",
10480
+ "valueType": "boolean"
10481
+ },
10482
+ {
10483
+ "path": "pumpRunning",
10484
+ "valueType": "boolean"
10485
+ },
10486
+ {
10487
+ "path": "pumpIntervalEnabled",
10488
+ "valueType": "boolean"
10489
+ },
10490
+ {
10491
+ "path": "pumpOnTime",
10492
+ "valueType": "number"
10493
+ },
10494
+ {
10495
+ "path": "pumpOffTime",
10496
+ "valueType": "number"
10497
+ },
10498
+ {
10499
+ "path": "lastDrink",
10500
+ "valueType": "number"
10501
+ },
10502
+ {
10503
+ "path": "currentHourDrink",
10504
+ "valueType": "number"
10505
+ },
10506
+ {
10507
+ "path": "last24hDrink",
10508
+ "valueType": "number"
10509
+ },
10510
+ {
10511
+ "path": "todayDrink",
10512
+ "valueType": "number"
10513
+ },
10514
+ {
10515
+ "path": "error",
10516
+ "valueType": "string"
10378
10517
  }
10379
10518
  ],
10380
10519
  "power-meter": [
@@ -10916,7 +11055,7 @@ Object.freeze({
10916
11055
  * can answer "may a rule actuate this?" without importing the schema barrel
10917
11056
  * (~144MB RSS per runner, D28).
10918
11057
  *
10919
- * Coverage: 96 device-scoped capabilities.
11058
+ * Coverage: 97 device-scoped capabilities.
10920
11059
  */
10921
11060
  var DEVICE_SCOPED_CAPS = new Set([
10922
11061
  "accessories",
@@ -10982,6 +11121,7 @@ var DEVICE_SCOPED_CAPS = new Set([
10982
11121
  "numeric-sensor",
10983
11122
  "osd",
10984
11123
  "pet-feeder",
11124
+ "pet-fountain",
10985
11125
  "pipeline-analytics",
10986
11126
  "power-meter",
10987
11127
  "presence",
@@ -11060,7 +11200,7 @@ function sleep(ms) {
11060
11200
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11061
11201
  }
11062
11202
  //#endregion
11063
- //#region ../types/dist/err-msg-BCdlR2Li.mjs
11203
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
11064
11204
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11065
11205
  DeviceType["Camera"] = "camera";
11066
11206
  DeviceType["Hub"] = "hub";
@@ -11152,6 +11292,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11152
11292
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
11153
11293
  * integrations sharing the same food/desiccant/hopper surface. */
11154
11294
  DeviceType["PetFeeder"] = "pet-feeder";
11295
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11296
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11297
+ * of a container and is BUILT as a composed device (D967). */
11298
+ DeviceType["PetFountain"] = "pet-fountain";
11155
11299
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11156
11300
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11157
11301
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11422,7 +11566,7 @@ function errMsg(err) {
11422
11566
  return String(err);
11423
11567
  }
11424
11568
  //#endregion
11425
- //#region ../types/dist/composition-DpFv_J6D.mjs
11569
+ //#region ../types/dist/composition-kSuOifuG.mjs
11426
11570
  /**
11427
11571
  * Deterministic SHA-256 hash of an arbitrary serialisable value. The
11428
11572
  * canonical form sorts object keys alphabetically at every depth so two
@@ -12667,12 +12811,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12667
12811
  ])
12668
12812
  })
12669
12813
  ]);
12670
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12814
+ var forwardArgUnion = union([
12815
+ string(),
12816
+ number(),
12817
+ boolean(),
12818
+ strictObject({ ["$input"]: string().min(1) }),
12819
+ record(string().min(1), lazy(() => forwardArgUnion))
12820
+ ]);
12821
+ var CompositionForwardArgSchema = forwardArgUnion;
12822
+ /**
12823
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12824
+ * call of `cap.method` on `source`. Without `args` the built method's input
12825
+ * passes through minus `deviceId`; with `args` the template IS the source's
12826
+ * whole input. `deviceId` is always the source's own, never an argument.
12827
+ */
12828
+ var CompositionForwardCommandSchema = object({
12671
12829
  kind: literal("forward"),
12672
12830
  source: CompositionSourceRefSchema,
12673
12831
  cap: string().min(1),
12674
- method: string().min(1)
12675
- }), CompositionCodeSourceSchema]);
12832
+ method: string().min(1),
12833
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12834
+ });
12835
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12676
12836
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12677
12837
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12678
12838
  /**
@@ -12682,7 +12842,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12682
12842
  */
12683
12843
  var CompositionItemEntrySchema = object({
12684
12844
  label: string().min(1).max(80),
12685
- fields: record(string().min(1), CompositionFieldSourceSchema)
12845
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12846
+ /**
12847
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12848
+ * is resettable and a `reset` call naming its key forwards here. The item key
12849
+ * selects the command, so it is never an argument: there is no pass-through.
12850
+ */
12851
+ reset: CompositionForwardCommandSchema.optional()
12686
12852
  });
12687
12853
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12688
12854
  kind: literal("fields"),
@@ -12694,7 +12860,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12694
12860
  * owns the WHOLE array field: the composed value holds exactly these items.
12695
12861
  */
12696
12862
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12697
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12863
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12698
12864
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12699
12865
  }), object({
12700
12866
  kind: literal("passthrough"),
@@ -16837,6 +17003,538 @@ object({
16837
17003
  path: ["expr"]
16838
17004
  });
16839
17005
  });
17006
+ /**
17007
+ * Identity envelope for a device's upstream-system metadata.
17008
+ *
17009
+ * Two jobs:
17010
+ *
17011
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
17012
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
17013
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
17014
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
17015
+ * without round-tripping through `stableId` / `id` mappings.
17016
+ *
17017
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
17018
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
17019
+ * for vendors where MAC is the natural key). For systems where the
17020
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
17021
+ * providers detect renames by matching `uniqueId` and patch `id`
17022
+ * in-place so dispatch keeps working.
17023
+ *
17024
+ * **Display unit and precision are NOT stored here.** They are a read-time
17025
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
17026
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
17027
+ * neither is repeated on each sample. The `device_class` tag is an
17028
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
17029
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
17030
+ *
17031
+ * The `raw` escape hatch carries provider-specific attributes that don't
17032
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
17033
+ * details, …). Providers control what lands here.
17034
+ */
17035
+ var SourceInfoSchema = object({
17036
+ /** Live dispatch key — mutable when the source system allows rename. */
17037
+ id: string(),
17038
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
17039
+ system: string(),
17040
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
17041
+ * Used to detect rename when `id` changes. */
17042
+ uniqueId: string().optional(),
17043
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
17044
+ quantity: ReadingQuantitySchema.optional(),
17045
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
17046
+ dateTimeShape: _enum([
17047
+ "date",
17048
+ "time",
17049
+ "datetime"
17050
+ ]).optional(),
17051
+ /** Provider-specific extras that don't fit the structured slots.
17052
+ * Whitelist what goes here per provider — do NOT dump entire HA
17053
+ * attribute blobs (would explode the meta JSON column). */
17054
+ raw: record(string(), unknown()).optional()
17055
+ });
17056
+ /**
17057
+ * Build the synthetic SourceInfo every existing provider falls back to
17058
+ * when no upstream value has been persisted. Keeps non-migrated providers
17059
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
17060
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
17061
+ * id. Real upstream identity replaces this once a provider migrates and
17062
+ * calls `updateSourceInfo()` with concrete values.
17063
+ */
17064
+ function synthesizeSourceInfo(input) {
17065
+ return {
17066
+ id: input.stableId,
17067
+ system: input.addonId
17068
+ };
17069
+ }
17070
+ /**
17071
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
17072
+ * patch values are ignored (no field clobber); to clear a field
17073
+ * explicitly, providers omit it from the patch and rebuild from scratch
17074
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
17075
+ * bother with a `null`-means-delete sentinel.
17076
+ *
17077
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
17078
+ * merges over existing `raw` keys instead of replacing the whole bag.
17079
+ */
17080
+ function mergeSourceInfo(prev, patch) {
17081
+ const next = { ...prev };
17082
+ for (const [k, v] of Object.entries(patch)) {
17083
+ if (v === void 0) continue;
17084
+ if (k === "raw" && typeof v === "object" && v !== null) {
17085
+ next.raw = {
17086
+ ...prev.raw,
17087
+ ...v
17088
+ };
17089
+ continue;
17090
+ }
17091
+ next[k] = v;
17092
+ }
17093
+ return next;
17094
+ }
17095
+ /**
17096
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
17097
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
17098
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
17099
+ * lives under a single nested key to keep it composable with those.
17100
+ */
17101
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
17102
+ /**
17103
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
17104
+ * with Zod. Returns `null` when the blob is missing or the nested
17105
+ * `sourceInfo` key fails validation — callers fall back to the
17106
+ * synthetic default in that case (see `synthesizeSourceInfo`).
17107
+ */
17108
+ function extractSourceInfoFromMetadata(metadata) {
17109
+ if (!metadata) return null;
17110
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
17111
+ if (!raw) return null;
17112
+ const parsed = SourceInfoSchema.safeParse(raw);
17113
+ if (!parsed.success) return null;
17114
+ return parsed.data;
17115
+ }
17116
+ /**
17117
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
17118
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
17119
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
17120
+ * decimals). K derives from `°C = K − 273.15`.
17121
+ */
17122
+ var UNIT_TABLE = {
17123
+ "°C": {
17124
+ dimension: "temperature",
17125
+ factor: 1,
17126
+ offset: 0
17127
+ },
17128
+ "°F": {
17129
+ dimension: "temperature",
17130
+ factor: 5 / 9,
17131
+ offset: -160 / 9
17132
+ },
17133
+ K: {
17134
+ dimension: "temperature",
17135
+ factor: 1,
17136
+ offset: -273.15
17137
+ },
17138
+ hPa: {
17139
+ dimension: "pressure",
17140
+ factor: 1,
17141
+ offset: 0
17142
+ },
17143
+ kPa: {
17144
+ dimension: "pressure",
17145
+ factor: 10,
17146
+ offset: 0
17147
+ },
17148
+ Pa: {
17149
+ dimension: "pressure",
17150
+ factor: .01,
17151
+ offset: 0
17152
+ },
17153
+ mbar: {
17154
+ dimension: "pressure",
17155
+ factor: 1,
17156
+ offset: 0
17157
+ },
17158
+ bar: {
17159
+ dimension: "pressure",
17160
+ factor: 1e3,
17161
+ offset: 0
17162
+ },
17163
+ inHg: {
17164
+ dimension: "pressure",
17165
+ factor: 33.8639,
17166
+ offset: 0
17167
+ },
17168
+ mmHg: {
17169
+ dimension: "pressure",
17170
+ factor: 1.33322,
17171
+ offset: 0
17172
+ },
17173
+ psi: {
17174
+ dimension: "pressure",
17175
+ factor: 68.9476,
17176
+ offset: 0
17177
+ },
17178
+ "m/s": {
17179
+ dimension: "speed",
17180
+ factor: 1,
17181
+ offset: 0
17182
+ },
17183
+ "km/h": {
17184
+ dimension: "speed",
17185
+ factor: 1 / 3.6,
17186
+ offset: 0
17187
+ },
17188
+ mph: {
17189
+ dimension: "speed",
17190
+ factor: .44704,
17191
+ offset: 0
17192
+ },
17193
+ kn: {
17194
+ dimension: "speed",
17195
+ factor: .514444,
17196
+ offset: 0
17197
+ },
17198
+ "mm/h": {
17199
+ dimension: "precipitation-rate",
17200
+ factor: 1,
17201
+ offset: 0
17202
+ },
17203
+ "in/h": {
17204
+ dimension: "precipitation-rate",
17205
+ factor: 25.4,
17206
+ offset: 0
17207
+ },
17208
+ m: {
17209
+ dimension: "length",
17210
+ factor: 1,
17211
+ offset: 0
17212
+ },
17213
+ mm: {
17214
+ dimension: "length",
17215
+ factor: .001,
17216
+ offset: 0
17217
+ },
17218
+ cm: {
17219
+ dimension: "length",
17220
+ factor: .01,
17221
+ offset: 0
17222
+ },
17223
+ km: {
17224
+ dimension: "length",
17225
+ factor: 1e3,
17226
+ offset: 0
17227
+ },
17228
+ in: {
17229
+ dimension: "length",
17230
+ factor: .0254,
17231
+ offset: 0
17232
+ },
17233
+ ft: {
17234
+ dimension: "length",
17235
+ factor: .3048,
17236
+ offset: 0
17237
+ },
17238
+ mi: {
17239
+ dimension: "length",
17240
+ factor: 1609.344,
17241
+ offset: 0
17242
+ },
17243
+ "m²": {
17244
+ dimension: "area",
17245
+ factor: 1,
17246
+ offset: 0
17247
+ },
17248
+ lx: {
17249
+ dimension: "illuminance",
17250
+ factor: 1,
17251
+ offset: 0
17252
+ },
17253
+ "W/m²": {
17254
+ dimension: "irradiance",
17255
+ factor: 1,
17256
+ offset: 0
17257
+ },
17258
+ W: {
17259
+ dimension: "power",
17260
+ factor: 1,
17261
+ offset: 0
17262
+ },
17263
+ kW: {
17264
+ dimension: "power",
17265
+ factor: 1e3,
17266
+ offset: 0
17267
+ },
17268
+ VA: {
17269
+ dimension: "apparent-power",
17270
+ factor: 1,
17271
+ offset: 0
17272
+ },
17273
+ Wh: {
17274
+ dimension: "energy",
17275
+ factor: 1,
17276
+ offset: 0
17277
+ },
17278
+ kWh: {
17279
+ dimension: "energy",
17280
+ factor: 1e3,
17281
+ offset: 0
17282
+ },
17283
+ MWh: {
17284
+ dimension: "energy",
17285
+ factor: 1e6,
17286
+ offset: 0
17287
+ },
17288
+ V: {
17289
+ dimension: "voltage",
17290
+ factor: 1,
17291
+ offset: 0
17292
+ },
17293
+ mV: {
17294
+ dimension: "voltage",
17295
+ factor: .001,
17296
+ offset: 0
17297
+ },
17298
+ A: {
17299
+ dimension: "current",
17300
+ factor: 1,
17301
+ offset: 0
17302
+ },
17303
+ mA: {
17304
+ dimension: "current",
17305
+ factor: .001,
17306
+ offset: 0
17307
+ },
17308
+ ppm: {
17309
+ dimension: "concentration-volume",
17310
+ factor: 1,
17311
+ offset: 0
17312
+ },
17313
+ ppb: {
17314
+ dimension: "concentration-volume",
17315
+ factor: .001,
17316
+ offset: 0
17317
+ },
17318
+ "µg/m³": {
17319
+ dimension: "concentration-mass",
17320
+ factor: 1,
17321
+ offset: 0
17322
+ },
17323
+ "mg/m³": {
17324
+ dimension: "concentration-mass",
17325
+ factor: 1e3,
17326
+ offset: 0
17327
+ },
17328
+ kg: {
17329
+ dimension: "mass",
17330
+ factor: 1,
17331
+ offset: 0
17332
+ },
17333
+ g: {
17334
+ dimension: "mass",
17335
+ factor: .001,
17336
+ offset: 0
17337
+ },
17338
+ lb: {
17339
+ dimension: "mass",
17340
+ factor: .453592,
17341
+ offset: 0
17342
+ },
17343
+ oz: {
17344
+ dimension: "mass",
17345
+ factor: .0283495,
17346
+ offset: 0
17347
+ },
17348
+ "%": {
17349
+ dimension: "percentage",
17350
+ factor: 1,
17351
+ offset: 0
17352
+ },
17353
+ L: {
17354
+ dimension: "volume",
17355
+ factor: 1,
17356
+ offset: 0
17357
+ },
17358
+ mL: {
17359
+ dimension: "volume",
17360
+ factor: .001,
17361
+ offset: 0
17362
+ },
17363
+ gal: {
17364
+ dimension: "volume",
17365
+ factor: 3.785411784,
17366
+ offset: 0
17367
+ },
17368
+ s: {
17369
+ dimension: "duration",
17370
+ factor: 1,
17371
+ offset: 0
17372
+ },
17373
+ ms: {
17374
+ dimension: "duration",
17375
+ factor: .001,
17376
+ offset: 0
17377
+ },
17378
+ min: {
17379
+ dimension: "duration",
17380
+ factor: 60,
17381
+ offset: 0
17382
+ },
17383
+ h: {
17384
+ dimension: "duration",
17385
+ factor: 3600,
17386
+ offset: 0
17387
+ },
17388
+ d: {
17389
+ dimension: "duration",
17390
+ factor: 86400,
17391
+ offset: 0
17392
+ },
17393
+ B: {
17394
+ dimension: "information",
17395
+ factor: 1,
17396
+ offset: 0
17397
+ },
17398
+ KiB: {
17399
+ dimension: "information",
17400
+ factor: 1024,
17401
+ offset: 0
17402
+ },
17403
+ MiB: {
17404
+ dimension: "information",
17405
+ factor: 1024 ** 2,
17406
+ offset: 0
17407
+ },
17408
+ GiB: {
17409
+ dimension: "information",
17410
+ factor: 1024 ** 3,
17411
+ offset: 0
17412
+ },
17413
+ TiB: {
17414
+ dimension: "information",
17415
+ factor: 1024 ** 4,
17416
+ offset: 0
17417
+ },
17418
+ kB: {
17419
+ dimension: "information",
17420
+ factor: 1e3,
17421
+ offset: 0
17422
+ },
17423
+ MB: {
17424
+ dimension: "information",
17425
+ factor: 1e6,
17426
+ offset: 0
17427
+ },
17428
+ GB: {
17429
+ dimension: "information",
17430
+ factor: 1e9,
17431
+ offset: 0
17432
+ },
17433
+ "°": {
17434
+ dimension: "angle",
17435
+ factor: 1,
17436
+ offset: 0
17437
+ },
17438
+ dBFS: {
17439
+ dimension: "sound-level",
17440
+ factor: 1,
17441
+ offset: 0
17442
+ },
17443
+ dBm: {
17444
+ dimension: "signal-level",
17445
+ factor: 1,
17446
+ offset: 0
17447
+ },
17448
+ mired: {
17449
+ dimension: "colour-temperature",
17450
+ factor: 1,
17451
+ offset: 0
17452
+ },
17453
+ kbps: {
17454
+ dimension: "bitrate",
17455
+ factor: 1,
17456
+ offset: 0
17457
+ },
17458
+ Mbps: {
17459
+ dimension: "bitrate",
17460
+ factor: 1e3,
17461
+ offset: 0
17462
+ },
17463
+ fps: {
17464
+ dimension: "rate",
17465
+ factor: 1,
17466
+ offset: 0
17467
+ }
17468
+ };
17469
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
17470
+ function specFor(unit) {
17471
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
17472
+ }
17473
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
17474
+ function toCanonical(value, spec) {
17475
+ return value * spec.factor + spec.offset;
17476
+ }
17477
+ /** Canonical-unit value → the unit described by `spec`. */
17478
+ function fromCanonical(canonical, spec) {
17479
+ return (canonical - spec.offset) / spec.factor;
17480
+ }
17481
+ /**
17482
+ * Graceful variant for the render path: returns the converted value, or
17483
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
17484
+ * Never throws — a render caller falls back to the raw value + source unit.
17485
+ */
17486
+ function tryConvertUnit(value, from, to) {
17487
+ if (from === to) return value;
17488
+ const fromSpec = specFor(from);
17489
+ if (!fromSpec) return void 0;
17490
+ const toSpec = specFor(to);
17491
+ if (!toSpec) return void 0;
17492
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
17493
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
17494
+ }
17495
+ /**
17496
+ * A number a person types for a physical quantity — the input side of the
17497
+ * value + unit contract (D972).
17498
+ *
17499
+ * Readings are stored in the base unit of their quantity (D944); a cap method
17500
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
17501
+ * wire pair {@link QuantityValue} says "this number is expressed in this
17502
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
17503
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
17504
+ */
17505
+ /** A unit that cannot be applied to the number it was given with. */
17506
+ var QuantityUnitError = class extends Error {
17507
+ code;
17508
+ constructor(code, message) {
17509
+ super(`[${code}] ${message}`);
17510
+ this.name = "QuantityUnitError";
17511
+ this.code = code;
17512
+ }
17513
+ };
17514
+ /** The wire pair: `value` is expressed in `unit`. */
17515
+ var QuantityValueSchema = object({
17516
+ value: number(),
17517
+ unit: string().min(1)
17518
+ });
17519
+ /**
17520
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
17521
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
17522
+ * portion stays in grams); the unit must belong to the quantity, checked once
17523
+ * when the cap is declared.
17524
+ */
17525
+ function paramMeta(declared) {
17526
+ const base = baseUnitOf(declared.quantity);
17527
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
17528
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
17529
+ return {
17530
+ quantity: declared.quantity,
17531
+ unit: declared.unit
17532
+ };
17533
+ }
17534
+ object({
17535
+ quantity: ReadingQuantitySchema,
17536
+ unit: string().min(1)
17537
+ });
16840
17538
  /** How a leaf compares a device field to a value. Derived from the field's
16841
17539
  * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16842
17540
  var AutomationConditionOperatorSchema = _enum([
@@ -16859,8 +17557,10 @@ var AutomationConditionLeafSchema = object({
16859
17557
  string(),
16860
17558
  number(),
16861
17559
  boolean(),
16862
- array(union([string(), number()]))
16863
- ])
17560
+ array(union([string(), number()])),
17561
+ QuantityValueSchema
17562
+ ]),
17563
+ unit: string().min(1).optional()
16864
17564
  });
16865
17565
  var AutomationConditionStationaryPresentSchema = object({
16866
17566
  kind: literal("stationary-present"),
@@ -17013,7 +17713,13 @@ var AutomationCapStepSchema = object({
17013
17713
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
17014
17714
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
17015
17715
  * the expression leaf. */
17016
- args: record(string(), unknown()).optional()
17716
+ args: record(string(), unknown()).optional(),
17717
+ /**
17718
+ * The unit the person picked per arg (display only). `args` always hold the
17719
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
17720
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
17721
+ */
17722
+ argUnits: record(string(), string().min(1)).optional()
17017
17723
  });
17018
17724
  /**
17019
17725
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -17415,6 +18121,437 @@ object({
17415
18121
  lastRunAt: number().nullable(),
17416
18122
  lastError: string().nullable()
17417
18123
  });
18124
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
18125
+ var UnitWarningSchema = object({
18126
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
18127
+ path: string(),
18128
+ deviceId: number().int().optional(),
18129
+ field: string(),
18130
+ value: number(),
18131
+ assumedUnit: string(),
18132
+ message: string()
18133
+ });
18134
+ /**
18135
+ * notification-output — canonical, capability-gated notification delivery.
18136
+ *
18137
+ * Apprise-derived model (see
18138
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
18139
+ * callers emit ONE canonical `Notification`; each provider declares a
18140
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
18141
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
18142
+ * message to what the kind supports — callers never special-case a service.
18143
+ *
18144
+ * DESIGN DECISIONS (locked):
18145
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
18146
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
18147
+ * cap. Rationale: the admin UI needs one uniform surface across the
18148
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
18149
+ * alternative would fork the UI per addon and cannot host the
18150
+ * discovery→adopt flow.
18151
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
18152
+ * the generated cap-mount auto-`concatCollection`-fans them across every
18153
+ * registered provider (notifiers addon + HA addon) so one catalog is
18154
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
18155
+ * `addonId` the generated collection router extracts from the call input.
18156
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
18157
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
18158
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
18159
+ * base64 fallback needed.
18160
+ *
18161
+ * TODO (deferred, closed-set change — separate decision): add
18162
+ * `providerKind: 'notify'` so notification providers surface on the unified
18163
+ * admin "Integrations" page.
18164
+ */
18165
+ /**
18166
+ * Zentik-derived typed-media enum — the superset across every kind. Each
18167
+ * adapter picks what it supports and the degrade engine filters the rest.
18168
+ */
18169
+ var AttachmentMediaTypeSchema = _enum([
18170
+ "image",
18171
+ "video",
18172
+ "gif",
18173
+ "audio",
18174
+ "icon"
18175
+ ]);
18176
+ /**
18177
+ * A single attachment. Exactly one of `url` (remote source, most adapters
18178
+ * prefer this) or `bytes` (inline source; required for Pushover-style
18179
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
18180
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
18181
+ */
18182
+ var AttachmentSchema = object({
18183
+ mediaType: AttachmentMediaTypeSchema,
18184
+ url: string().optional(),
18185
+ bytes: _instanceof(Uint8Array).optional(),
18186
+ mime: string().optional(),
18187
+ name: string().optional()
18188
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
18189
+ var NotificationFormatSchema = _enum([
18190
+ "text",
18191
+ "markdown",
18192
+ "html"
18193
+ ]);
18194
+ /**
18195
+ * The CLOSED icon vocabulary an action button may use.
18196
+ *
18197
+ * A closed set, not a free string, and that is the whole point: an arbitrary
18198
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
18199
+ * notices — the same class of gap as a zone vocabulary nothing produced
18200
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
18201
+ * declares `actionIcons: false` and the degrade engine strips the field.
18202
+ *
18203
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
18204
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
18205
+ * differently.
18206
+ *
18207
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
18208
+ *
18209
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
18210
+ * the whole set onto its own renderer's vocabulary through a
18211
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
18212
+ * fallback, so adding a member here fails every adapter's build until someone
18213
+ * decides its glyph, which is the only place that decision can be made
18214
+ * honestly.
18215
+ *
18216
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
18217
+ * `disarm` straight through; iOS feeds that string to
18218
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
18219
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
18220
+ * mapping, and "the field is documented" is not "the value renders".
18221
+ *
18222
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
18223
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
18224
+ * so an addon that emits a token the running hub does not know does not lose an
18225
+ * icon — its whole `send` fails Zod validation and the notification never
18226
+ * arrives. Never emit a new token from an addon before the train carrying it.
18227
+ */
18228
+ var NotificationActionIconSchema = _enum([
18229
+ "acknowledge",
18230
+ "dismiss",
18231
+ "silence",
18232
+ "snooze",
18233
+ "view",
18234
+ "play",
18235
+ "open",
18236
+ "close",
18237
+ "lock",
18238
+ "unlock",
18239
+ "arm",
18240
+ "arm-home",
18241
+ "arm-away",
18242
+ "arm-night",
18243
+ "disarm",
18244
+ "light",
18245
+ "alert",
18246
+ "camera"
18247
+ ]);
18248
+ /** A single tap-through action button. */
18249
+ var NotificationActionSchema = object({
18250
+ id: string(),
18251
+ label: string(),
18252
+ url: string().optional(),
18253
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
18254
+ icon: NotificationActionIconSchema.optional(),
18255
+ /**
18256
+ * Renders in a warning style where the notifier supports it.
18257
+ *
18258
+ * A HINT, never a gate. The callback's authority is its token and nothing
18259
+ * else — see `notification-center/action-token.ts` for what that does and
18260
+ * does not buy.
18261
+ */
18262
+ destructive: boolean().optional(),
18263
+ /**
18264
+ * How the tap should REACH the url.
18265
+ *
18266
+ * `navigate` (absent, and every button authored before this field) opens it:
18267
+ * the phone leaves the notification and shows whatever the callback returns.
18268
+ * That is right for a button whose answer the operator wants to read.
18269
+ *
18270
+ * `background` fires it as a POST and stays put. It exists for the buttons
18271
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
18272
+ * an answer to the notification, and being thrown into a browser tab to
18273
+ * confirm it costs more attention than the notification did. A backend that
18274
+ * cannot do a background call renders it as an ordinary link (the adapters
18275
+ * fall back rather than dropping the button), so this is a preference, never
18276
+ * a requirement.
18277
+ */
18278
+ mode: _enum(["navigate", "background"]).optional()
18279
+ });
18280
+ /** A delivery parameter value: what a notifier declared it takes. */
18281
+ var DeliveryParamValueSchema = union([
18282
+ string(),
18283
+ number(),
18284
+ boolean()
18285
+ ]);
18286
+ /**
18287
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
18288
+ * addresses a target of that kind may set it; a kind that declares nothing is
18289
+ * never handed one. The admin UI derives its control from this descriptor —
18290
+ * no editor knows a service by name.
18291
+ *
18292
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
18293
+ * run on the addon cap path).
18294
+ */
18295
+ var DeliveryParamSchema = object({
18296
+ key: string().min(1),
18297
+ label: string(),
18298
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
18299
+ labelKey: string().optional(),
18300
+ type: _enum([
18301
+ "enum",
18302
+ "number",
18303
+ "boolean",
18304
+ "string"
18305
+ ]),
18306
+ /** `enum` only: the closed set of accepted values. */
18307
+ options: array(object({
18308
+ value: string(),
18309
+ label: string()
18310
+ })).optional(),
18311
+ /** `number` only. */
18312
+ min: number().optional(),
18313
+ max: number().optional(),
18314
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
18315
+ default: DeliveryParamValueSchema.optional()
18316
+ });
18317
+ /**
18318
+ * The canonical notification. `body` is the only hard field (Apprise model).
18319
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
18320
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
18321
+ * the adapter maps this ordinal onto its native level. `level?` is an
18322
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
18323
+ * `priority` for that one target.
18324
+ */
18325
+ var NotificationSchema = object({
18326
+ body: string(),
18327
+ title: string().optional(),
18328
+ format: NotificationFormatSchema.default("text"),
18329
+ priority: number().int().min(1).max(5).default(3),
18330
+ level: string().optional(),
18331
+ attachments: array(AttachmentSchema).optional(),
18332
+ clickUrl: string().optional(),
18333
+ actions: array(NotificationActionSchema).optional(),
18334
+ sound: string().optional(),
18335
+ ttl: number().optional(),
18336
+ tag: string().optional(),
18337
+ deviceId: number().optional(),
18338
+ eventId: string().optional(),
18339
+ metadata: record(string(), unknown()).optional(),
18340
+ /**
18341
+ * Values for the parameters the target's kind DECLARES in
18342
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
18343
+ * declaration by the sender; a notifier reads only the keys it declared.
18344
+ */
18345
+ delivery: record(string(), DeliveryParamValueSchema).optional()
18346
+ });
18347
+ /** One declared native severity/priority level for a kind. */
18348
+ var TargetKindLevelSchema = object({
18349
+ id: string(),
18350
+ label: string(),
18351
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
18352
+ ordinal: number().int().min(1).max(5).nullable(),
18353
+ flags: object({
18354
+ critical: boolean().optional(),
18355
+ silent: boolean().optional(),
18356
+ noPush: boolean().optional()
18357
+ }).optional(),
18358
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
18359
+ requires: array(string()).optional(),
18360
+ description: string().optional()
18361
+ });
18362
+ /** The full capability block consulted before dispatch. */
18363
+ var TargetKindCapsSchema = object({
18364
+ attachments: object({
18365
+ mediaTypes: array(AttachmentMediaTypeSchema),
18366
+ mode: _enum([
18367
+ "url",
18368
+ "bytes",
18369
+ "both"
18370
+ ]),
18371
+ max: number().int().nonnegative(),
18372
+ maxBytes: number().int().positive().optional()
18373
+ }),
18374
+ /** Max action buttons (0 = none). */
18375
+ actions: number().int().nonnegative(),
18376
+ /**
18377
+ * Whether this kind renders a per-action ICON.
18378
+ *
18379
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
18380
+ * run on the addon cap path — three production failures in one day taught
18381
+ * this repo that once. Absent is read as false by the degrade engine, which
18382
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
18383
+ * assumed and dropped costs the operator's trust in the field.
18384
+ */
18385
+ actionIcons: boolean().optional(),
18386
+ levels: array(TargetKindLevelSchema),
18387
+ format: array(NotificationFormatSchema),
18388
+ clickUrl: boolean(),
18389
+ sound: boolean(),
18390
+ ttl: boolean(),
18391
+ bodyMaxLen: number().int().positive()
18392
+ });
18393
+ /**
18394
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
18395
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
18396
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
18397
+ * the union is large and not meant for runtime validation here; the exported
18398
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
18399
+ */
18400
+ var ConfigSchemaPassthrough$3 = unknown();
18401
+ var TargetKindSchema = object({
18402
+ kind: string(),
18403
+ label: string(),
18404
+ icon: string(),
18405
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
18406
+ addonId: string(),
18407
+ /**
18408
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
18409
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
18410
+ * when the addon bundles no icon for that kind — the client then falls back
18411
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
18412
+ *
18413
+ * Root-relative on purpose: it resolves against whatever origin serves a web
18414
+ * client, and a native client joins it onto its own hub base.
18415
+ *
18416
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
18417
+ * field that survived only because the runtime cap-router forwards provider
18418
+ * output verbatim — so every consumer had to re-declare it by hand to stop
18419
+ * its own Zod parse from stripping it, and the whole arrangement would have
18420
+ * broken silently the moment output validation was tightened anywhere.
18421
+ */
18422
+ iconUrl: string().optional(),
18423
+ /**
18424
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
18425
+ *
18426
+ * The server knows this and therefore says it, because the client cannot
18427
+ * safely guess: a React-Native client renders SVG and raster through two
18428
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
18429
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
18430
+ * placeholder glyph for every vector icon while the web build looked fine.
18431
+ *
18432
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
18433
+ * not been updated — a client that cannot determine the type should prefer
18434
+ * its raster path, which is the safe default for an unknown image.
18435
+ */
18436
+ iconMediaType: string().optional(),
18437
+ configSchema: ConfigSchemaPassthrough$3,
18438
+ /**
18439
+ * The config keys that decide WHERE this kind's secrets are sent (server,
18440
+ * base URL, instance, chat, recipient). A save that changes any of them
18441
+ * while leaving a secret redacted is refused by the provider (D634), so an
18442
+ * editor uses this to ask for the secret on exactly those edits instead of
18443
+ * on every edit.
18444
+ *
18445
+ * Optional: absent from a provider that holds no secret (the Home Assistant
18446
+ * kinds) and from a provider older than this field — an editor that finds
18447
+ * it absent must assume ANY edit may need the secret.
18448
+ */
18449
+ destinationKeys: array(string()).optional(),
18450
+ supportsDiscovery: boolean(),
18451
+ /**
18452
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
18453
+ * Absent = none; the rule editor then offers nothing.
18454
+ */
18455
+ deliveryParams: array(DeliveryParamSchema).optional(),
18456
+ caps: TargetKindCapsSchema
18457
+ });
18458
+ /**
18459
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
18460
+ * (return a presence marker only) when serving `listTargets` — never
18461
+ * round-trip a stored secret to the UI.
18462
+ */
18463
+ var TargetSchema = object({
18464
+ id: string(),
18465
+ name: string(),
18466
+ kind: string(),
18467
+ addonId: string(),
18468
+ enabled: boolean(),
18469
+ config: record(string(), unknown())
18470
+ });
18471
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
18472
+ var DiscoveredTargetSchema = object({
18473
+ kind: string(),
18474
+ suggestedName: string(),
18475
+ config: record(string(), unknown())
18476
+ });
18477
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
18478
+ var RenderedAsSchema = object({
18479
+ level: string(),
18480
+ format: NotificationFormatSchema,
18481
+ attachmentsSent: number().int().nonnegative(),
18482
+ actionsSent: number().int().nonnegative(),
18483
+ truncated: boolean(),
18484
+ dropped: array(string())
18485
+ });
18486
+ var SendResultSchema = object({
18487
+ success: boolean(),
18488
+ error: string().optional(),
18489
+ renderedAs: RenderedAsSchema.optional()
18490
+ });
18491
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
18492
+ var TestResultSchema = SendResultSchema;
18493
+ /**
18494
+ * Pre-save credential probe against a partial (unsaved) target config.
18495
+ * `resolvedUrl` never includes secrets (token stripped from path).
18496
+ */
18497
+ var ProbeTargetConfigResultSchema = object({
18498
+ ok: boolean(),
18499
+ resolvedUrl: string(),
18500
+ httpStatus: number().int().optional(),
18501
+ stateInstance: string().optional(),
18502
+ error: string().optional()
18503
+ });
18504
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
18505
+ kind: string(),
18506
+ config: record(string(), unknown()).optional(),
18507
+ /**
18508
+ * When editing / duplicating, the blank password field never round-trips
18509
+ * the stored token. Pass the source target id so the provider can restore
18510
+ * secrets server-side before dialling (same merge as upsert).
18511
+ */
18512
+ targetId: string().optional()
18513
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
18514
+ kind: string(),
18515
+ config: record(string(), unknown()),
18516
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
18517
+ targetId: string().optional()
18518
+ }), ProbeTargetConfigResultSchema, {
18519
+ kind: "mutation",
18520
+ auth: "admin"
18521
+ }), method(object({
18522
+ targetId: string(),
18523
+ notification: NotificationSchema
18524
+ }), SendResultSchema, {
18525
+ kind: "mutation",
18526
+ auth: "admin"
18527
+ }), method(object({
18528
+ targetId: string(),
18529
+ sample: NotificationSchema.optional()
18530
+ }), TestResultSchema, {
18531
+ kind: "mutation",
18532
+ auth: "admin"
18533
+ }), method(object({
18534
+ target: TargetSchema,
18535
+ /**
18536
+ * Duplicate flow: create a NEW target id while copying secrets from an
18537
+ * existing row. The UI never holds the stored token; blank / redacted
18538
+ * password fields are restored from this source (D634 still applies if
18539
+ * destinations diverge without a re-typed secret).
18540
+ */
18541
+ cloneSecretsFrom: string().optional()
18542
+ }), TargetSchema, {
18543
+ kind: "mutation",
18544
+ auth: "admin"
18545
+ }), method(object({ targetId: string() }), _void(), {
18546
+ kind: "mutation",
18547
+ auth: "admin"
18548
+ }), method(object({
18549
+ targetId: string(),
18550
+ enabled: boolean()
18551
+ }), _void(), {
18552
+ kind: "mutation",
18553
+ auth: "admin"
18554
+ });
17418
18555
  /**
17419
18556
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
17420
18557
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
@@ -17447,52 +18584,6 @@ var AddonPageInfoSchema = object({
17447
18584
  bundleUrl: string()
17448
18585
  });
17449
18586
  method(_void(), array(AddonPageInfoSchema).readonly());
17450
- /**
17451
- * `addon-pages-source` — collection cap exposing per-provider raw page
17452
- * declarations. Every addon that contributes a UI page registers a
17453
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
17454
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
17455
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
17456
- * that admin-ui consumes.
17457
- *
17458
- * The split exists because the public listing has a different output
17459
- * shape than the per-provider raw declarations, and we want both ends
17460
- * to flow through codegen instead of relying on a hand-written wrapper.
17461
- */
17462
- var AddonPageDeclarationSchema = object({
17463
- id: string(),
17464
- label: string(),
17465
- icon: string(),
17466
- path: string(),
17467
- /**
17468
- * Module Federation remote name — must match the `name` field on the
17469
- * page addon's `federation()` plugin config. Used by admin-ui's
17470
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
17471
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
17472
- * contain hyphens).
17473
- */
17474
- remoteName: string(),
17475
- /**
17476
- * Bundle filename inside the addon's `dist/` dir served at
17477
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
17478
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
17479
- * the static-file route can compute an mtime-based cache-buster URL
17480
- * without a separate filesystem stat.
17481
- */
17482
- bundle: string(),
17483
- /**
17484
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
17485
- * `'cluster'`, `'administration'` — the page renders inside that group.
17486
- * Any OTHER string creates (or joins) a custom section rendered after
17487
- * the built-in groups; its label comes from `sectionLabel` (first
17488
- * declaration wins), falling back to the id. Absent → the legacy
17489
- * "Addon Pages" group.
17490
- */
17491
- section: string().optional(),
17492
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
17493
- sectionLabel: string().optional()
17494
- });
17495
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
17496
18587
  var AddonHttpRouteSchema = object({
17497
18588
  method: _enum([
17498
18589
  "GET",
@@ -17793,9 +18884,21 @@ var AlertSeveritySchema = _enum([
17793
18884
  "error",
17794
18885
  "critical"
17795
18886
  ]);
18887
+ /**
18888
+ * An action that RUNS something instead of going somewhere: the admin UI calls
18889
+ * the addon's custom action (`addons.custom`) with exactly these fields. The
18890
+ * producer owns the meaning; the UI knows nothing about the addon or the kind.
18891
+ */
18892
+ var AlertActionInvokeSchema = object({
18893
+ addonId: string().min(1),
18894
+ action: string().min(1),
18895
+ input: record(string(), unknown()).optional()
18896
+ });
18897
+ /** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
17796
18898
  var AlertActionSchema = object({
17797
18899
  label: string(),
17798
18900
  href: string().optional(),
18901
+ invoke: AlertActionInvokeSchema.optional(),
17799
18902
  /**
17800
18903
  * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
17801
18904
  * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
@@ -20152,7 +21255,11 @@ var CompositionProblemSchema = object({
20152
21255
  "mixed-quantity",
20153
21256
  "helper-mismatch",
20154
21257
  "member-constraint",
20155
- "retention"
21258
+ "retention",
21259
+ "command-method-unknown",
21260
+ "command-source-unknown",
21261
+ "command-input-unknown",
21262
+ "command-input-missing"
20156
21263
  ]),
20157
21264
  path: string(),
20158
21265
  message: string()
@@ -20191,7 +21298,9 @@ var CompositionFieldRoleSchema = _enum([
20191
21298
  var CompositionFieldItemSchema = object({
20192
21299
  arrayPath: string(),
20193
21300
  key: string(),
20194
- label: string()
21301
+ label: string(),
21302
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
21303
+ resettable: boolean().optional()
20195
21304
  });
20196
21305
  var CompositionFieldPlanSchema = object({
20197
21306
  cap: string(),
@@ -20396,7 +21505,7 @@ Object.freeze({
20396
21505
  * One catalog row per base reading a group helper can build (D944 §3).
20397
21506
  * Data, not a form default written into the block.
20398
21507
  */
20399
- var NOW$2 = {
21508
+ var NOW$3 = {
20400
21509
  fieldPath: "lastFetchedAt",
20401
21510
  expr: "now"
20402
21511
  };
@@ -20448,7 +21557,7 @@ var GROUP_TEMPLATES = [
20448
21557
  quantity: "temperature",
20449
21558
  defaultAggregate: "mean",
20450
21559
  empty: "null",
20451
- companions: [NOW$2],
21560
+ companions: [NOW$3],
20452
21561
  alsoFrom: {
20453
21562
  deviceTypes: CLIMATE_DEVICES,
20454
21563
  accept: {
@@ -20466,7 +21575,7 @@ var GROUP_TEMPLATES = [
20466
21575
  quantity: "humidity",
20467
21576
  defaultAggregate: "mean",
20468
21577
  empty: "null",
20469
- companions: [NOW$2],
21578
+ companions: [NOW$3],
20470
21579
  alsoFrom: {
20471
21580
  deviceTypes: CLIMATE_DEVICES,
20472
21581
  accept: {
@@ -20484,7 +21593,7 @@ var GROUP_TEMPLATES = [
20484
21593
  quantity: "pressure",
20485
21594
  defaultAggregate: "mean",
20486
21595
  empty: "null",
20487
- companions: [NOW$2]
21596
+ companions: [NOW$3]
20488
21597
  }),
20489
21598
  numeric({
20490
21599
  id: "illuminance",
@@ -20495,7 +21604,7 @@ var GROUP_TEMPLATES = [
20495
21604
  quantity: "illuminance",
20496
21605
  defaultAggregate: "mean",
20497
21606
  empty: "null",
20498
- companions: [NOW$2]
21607
+ companions: [NOW$3]
20499
21608
  }),
20500
21609
  numeric({
20501
21610
  id: "power",
@@ -20506,7 +21615,7 @@ var GROUP_TEMPLATES = [
20506
21615
  quantity: "power",
20507
21616
  defaultAggregate: "sum",
20508
21617
  empty: "omit",
20509
- companions: [NOW$2]
21618
+ companions: [NOW$3]
20510
21619
  }),
20511
21620
  numeric({
20512
21621
  id: "energy",
@@ -20517,7 +21626,7 @@ var GROUP_TEMPLATES = [
20517
21626
  quantity: "energy",
20518
21627
  defaultAggregate: "sum",
20519
21628
  empty: "omit",
20520
- companions: [NOW$2]
21629
+ companions: [NOW$3]
20521
21630
  }),
20522
21631
  numeric({
20523
21632
  id: "battery",
@@ -20552,7 +21661,7 @@ var GROUP_TEMPLATES = [
20552
21661
  quantity: "device",
20553
21662
  defaultAggregate: "mean",
20554
21663
  empty: "null",
20555
- companions: [NOW$2]
21664
+ companions: [NOW$3]
20556
21665
  }),
20557
21666
  binary({
20558
21667
  id: "motion",
@@ -22502,116 +23611,6 @@ var DeviceAdminLinkSchema = object({
22502
23611
  });
22503
23612
  method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
22504
23613
  /**
22505
- * Identity envelope for a device's upstream-system metadata.
22506
- *
22507
- * Two jobs:
22508
- *
22509
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
22510
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
22511
- * WS-Discovery endpoint UUID, …). Provider code keeps a
22512
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
22513
- * without round-tripping through `stableId` / `id` mappings.
22514
- *
22515
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
22516
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
22517
- * for vendors where MAC is the natural key). For systems where the
22518
- * live dispatch key can be renamed (HA `entity_id` can be edited),
22519
- * providers detect renames by matching `uniqueId` and patch `id`
22520
- * in-place so dispatch keeps working.
22521
- *
22522
- * **Display unit and precision are NOT stored here.** They are a read-time
22523
- * preference (D944). A polymorphic reading stores its `quantity` here once,
22524
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
22525
- * neither is repeated on each sample. The `device_class` tag is an
22526
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
22527
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
22528
- *
22529
- * The `raw` escape hatch carries provider-specific attributes that don't
22530
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
22531
- * details, …). Providers control what lands here.
22532
- */
22533
- var SourceInfoSchema = object({
22534
- /** Live dispatch key — mutable when the source system allows rename. */
22535
- id: string(),
22536
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
22537
- system: string(),
22538
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
22539
- * Used to detect rename when `id` changes. */
22540
- uniqueId: string().optional(),
22541
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
22542
- quantity: ReadingQuantitySchema.optional(),
22543
- /** Date / time / datetime shape of a string reading, written once at adopt. */
22544
- dateTimeShape: _enum([
22545
- "date",
22546
- "time",
22547
- "datetime"
22548
- ]).optional(),
22549
- /** Provider-specific extras that don't fit the structured slots.
22550
- * Whitelist what goes here per provider — do NOT dump entire HA
22551
- * attribute blobs (would explode the meta JSON column). */
22552
- raw: record(string(), unknown()).optional()
22553
- });
22554
- /**
22555
- * Build the synthetic SourceInfo every existing provider falls back to
22556
- * when no upstream value has been persisted. Keeps non-migrated providers
22557
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
22558
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
22559
- * id. Real upstream identity replaces this once a provider migrates and
22560
- * calls `updateSourceInfo()` with concrete values.
22561
- */
22562
- function synthesizeSourceInfo(input) {
22563
- return {
22564
- id: input.stableId,
22565
- system: input.addonId
22566
- };
22567
- }
22568
- /**
22569
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
22570
- * patch values are ignored (no field clobber); to clear a field
22571
- * explicitly, providers omit it from the patch and rebuild from scratch
22572
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
22573
- * bother with a `null`-means-delete sentinel.
22574
- *
22575
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
22576
- * merges over existing `raw` keys instead of replacing the whole bag.
22577
- */
22578
- function mergeSourceInfo(prev, patch) {
22579
- const next = { ...prev };
22580
- for (const [k, v] of Object.entries(patch)) {
22581
- if (v === void 0) continue;
22582
- if (k === "raw" && typeof v === "object" && v !== null) {
22583
- next.raw = {
22584
- ...prev.raw,
22585
- ...v
22586
- };
22587
- continue;
22588
- }
22589
- next[k] = v;
22590
- }
22591
- return next;
22592
- }
22593
- /**
22594
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
22595
- * blob is a free-form `Record<string, unknown>` shared with hardware-
22596
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
22597
- * lives under a single nested key to keep it composable with those.
22598
- */
22599
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
22600
- /**
22601
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
22602
- * with Zod. Returns `null` when the blob is missing or the nested
22603
- * `sourceInfo` key fails validation — callers fall back to the
22604
- * synthetic default in that case (see `synthesizeSourceInfo`).
22605
- */
22606
- function extractSourceInfoFromMetadata(metadata) {
22607
- if (!metadata) return null;
22608
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
22609
- if (!raw) return null;
22610
- const parsed = SourceInfoSchema.safeParse(raw);
22611
- if (!parsed.success) return null;
22612
- return parsed.data;
22613
- }
22614
- /**
22615
23614
  * device-discovery — device-scoped capability for parents that host /
22616
23615
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
22617
23616
  * integrations). The parent advertises a list of discoverable children,
@@ -23016,12 +24015,48 @@ var ExposeInputSchema = object({
23016
24015
  capabilities: array(string()).optional()
23017
24016
  });
23018
24017
  var UnexposeInputSchema = object({ deviceId: string() });
24018
+ /**
24019
+ * What an exporter may expose besides devices (D968). An action and a
24020
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
24021
+ * exporter's own selection, not the device registry.
24022
+ */
24023
+ var ExportEntityKindSchema = _enum([
24024
+ "device",
24025
+ "action",
24026
+ "notifier"
24027
+ ]);
24028
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
24029
+ object({
24030
+ kind: literal("device"),
24031
+ deviceId: string()
24032
+ }),
24033
+ object({
24034
+ kind: literal("action"),
24035
+ actionId: string().min(1)
24036
+ }),
24037
+ object({
24038
+ kind: literal("notifier"),
24039
+ targetId: string().min(1)
24040
+ })
24041
+ ]);
24042
+ var ExposedEntitySchema = object({
24043
+ entity: ExportEntityRefSchema,
24044
+ exposedAs: string().optional()
24045
+ });
24046
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
24047
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
23019
24048
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
23020
24049
  kind: "mutation",
23021
24050
  auth: "admin"
23022
24051
  }), method(UnexposeInputSchema, _void(), {
23023
24052
  kind: "mutation",
23024
24053
  auth: "admin"
24054
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
24055
+ kind: "mutation",
24056
+ auth: "admin"
24057
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
24058
+ kind: "mutation",
24059
+ auth: "admin"
23025
24060
  });
23026
24061
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
23027
24062
  var ICON_CATEGORY_IDS = [
@@ -44237,6 +45272,14 @@ var embeddingEncoderCapability = {
44237
45272
  })
44238
45273
  }
44239
45274
  };
45275
+ method(object({
45276
+ nodeId: string().min(1),
45277
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
45278
+ removedAt: number()
45279
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
45280
+ kind: "mutation",
45281
+ auth: "admin"
45282
+ });
44240
45283
  /**
44241
45284
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
44242
45285
  * so it can delete what it keeps for that device (D865).
@@ -45079,7 +46122,7 @@ var LlmProviderWidgetSchema = object({
45079
46122
  /** ConfigUISchema tree passed through untyped on the wire (the
45080
46123
  * notification-output `ConfigSchemaPassthrough` precedent at
45081
46124
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
45082
- var ConfigSchemaPassthrough$3 = unknown();
46125
+ var ConfigSchemaPassthrough$2 = unknown();
45083
46126
  var LlmProfileKindDescriptorSchema = object({
45084
46127
  kind: LlmProfileKindSchema,
45085
46128
  label: string(),
@@ -45088,9 +46131,9 @@ var LlmProfileKindDescriptorSchema = object({
45088
46131
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
45089
46132
  addonId: string(),
45090
46133
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
45091
- connectionSchema: ConfigSchemaPassthrough$3,
46134
+ connectionSchema: ConfigSchemaPassthrough$2,
45092
46135
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
45093
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
46136
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
45094
46137
  /** The kind's defaults; a model from `listModels` may override any of them. */
45095
46138
  features: LlmModelFeaturesSchema,
45096
46139
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -45616,10 +46659,10 @@ var CpuBreakdownSchema = object({
45616
46659
  cores: number()
45617
46660
  });
45618
46661
  var MemoryInfoSchema = object({
45619
- percent: number(),
46662
+ percent: number().nullable(),
45620
46663
  totalBytes: number(),
45621
- usedBytes: number(),
45622
- availableBytes: number(),
46664
+ usedBytes: number().nullable(),
46665
+ availableBytes: number().nullable(),
45623
46666
  swapUsedBytes: number(),
45624
46667
  swapTotalBytes: number()
45625
46668
  });
@@ -45923,8 +46966,9 @@ var GetLoadSeriesInputSchema = object({
45923
46966
  });
45924
46967
  var SystemMetricsSchema = object({
45925
46968
  cpuPercent: number(),
45926
- memoryPercent: number(),
45927
- memoryUsedMB: number(),
46969
+ /** `null` when the memory sample failed or was not taken yet (D393). */
46970
+ memoryPercent: number().nullable(),
46971
+ memoryUsedMB: number().nullable(),
45928
46972
  memoryTotalMB: number(),
45929
46973
  diskPercent: number().optional(),
45930
46974
  temperature: number().optional(),
@@ -46160,427 +47204,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
46160
47204
  sourcePort: number().optional()
46161
47205
  });
46162
47206
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
46163
- /**
46164
- * notification-output — canonical, capability-gated notification delivery.
46165
- *
46166
- * Apprise-derived model (see
46167
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
46168
- * callers emit ONE canonical `Notification`; each provider declares a
46169
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
46170
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
46171
- * message to what the kind supports — callers never special-case a service.
46172
- *
46173
- * DESIGN DECISIONS (locked):
46174
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
46175
- * `setTargetEnabled`), each provider persisting via the `settings-store`
46176
- * cap. Rationale: the admin UI needs one uniform surface across the
46177
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
46178
- * alternative would fork the UI per addon and cannot host the
46179
- * discovery→adopt flow.
46180
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
46181
- * the generated cap-mount auto-`concatCollection`-fans them across every
46182
- * registered provider (notifiers addon + HA addon) so one catalog is
46183
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
46184
- * `addonId` the generated collection router extracts from the call input.
46185
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
46186
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
46187
- * `storage` / `storage-provider` / `recording` caps over the same path. No
46188
- * base64 fallback needed.
46189
- *
46190
- * TODO (deferred, closed-set change — separate decision): add
46191
- * `providerKind: 'notify'` so notification providers surface on the unified
46192
- * admin "Integrations" page.
46193
- */
46194
- /**
46195
- * Zentik-derived typed-media enum — the superset across every kind. Each
46196
- * adapter picks what it supports and the degrade engine filters the rest.
46197
- */
46198
- var AttachmentMediaTypeSchema = _enum([
46199
- "image",
46200
- "video",
46201
- "gif",
46202
- "audio",
46203
- "icon"
46204
- ]);
46205
- /**
46206
- * A single attachment. Exactly one of `url` (remote source, most adapters
46207
- * prefer this) or `bytes` (inline source; required for Pushover-style
46208
- * bytes-only kinds) MUST be present — the degrade engine expresses a
46209
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
46210
- */
46211
- var AttachmentSchema = object({
46212
- mediaType: AttachmentMediaTypeSchema,
46213
- url: string().optional(),
46214
- bytes: _instanceof(Uint8Array).optional(),
46215
- mime: string().optional(),
46216
- name: string().optional()
46217
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
46218
- var NotificationFormatSchema = _enum([
46219
- "text",
46220
- "markdown",
46221
- "html"
46222
- ]);
46223
- /**
46224
- * The CLOSED icon vocabulary an action button may use.
46225
- *
46226
- * A closed set, not a free string, and that is the whole point: an arbitrary
46227
- * icon name is one that ntfy renders, zentik silently drops, and nobody
46228
- * notices — the same class of gap as a zone vocabulary nothing produced
46229
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
46230
- * declares `actionIcons: false` and the degrade engine strips the field.
46231
- *
46232
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
46233
- * renderer's icon set; "acknowledge" survives an adapter that draws it
46234
- * differently.
46235
- *
46236
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
46237
- *
46238
- * These names are for US. **No adapter may forward one verbatim.** Each maps
46239
- * the whole set onto its own renderer's vocabulary through a
46240
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
46241
- * fallback, so adding a member here fails every adapter's build until someone
46242
- * decides its glyph, which is the only place that decision can be made
46243
- * honestly.
46244
- *
46245
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
46246
- * `disarm` straight through; iOS feeds that string to
46247
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
46248
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
46249
- * mapping, and "the field is documented" is not "the value renders".
46250
- *
46251
- * Adding a member is TRAIN-BOUND. The enum lives in the published
46252
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
46253
- * so an addon that emits a token the running hub does not know does not lose an
46254
- * icon — its whole `send` fails Zod validation and the notification never
46255
- * arrives. Never emit a new token from an addon before the train carrying it.
46256
- */
46257
- var NotificationActionIconSchema = _enum([
46258
- "acknowledge",
46259
- "dismiss",
46260
- "silence",
46261
- "snooze",
46262
- "view",
46263
- "play",
46264
- "open",
46265
- "close",
46266
- "lock",
46267
- "unlock",
46268
- "arm",
46269
- "arm-home",
46270
- "arm-away",
46271
- "arm-night",
46272
- "disarm",
46273
- "light",
46274
- "alert",
46275
- "camera"
46276
- ]);
46277
- /** A single tap-through action button. */
46278
- var NotificationActionSchema = object({
46279
- id: string(),
46280
- label: string(),
46281
- url: string().optional(),
46282
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
46283
- icon: NotificationActionIconSchema.optional(),
46284
- /**
46285
- * Renders in a warning style where the notifier supports it.
46286
- *
46287
- * A HINT, never a gate. The callback's authority is its token and nothing
46288
- * else — see `notification-center/action-token.ts` for what that does and
46289
- * does not buy.
46290
- */
46291
- destructive: boolean().optional(),
46292
- /**
46293
- * How the tap should REACH the url.
46294
- *
46295
- * `navigate` (absent, and every button authored before this field) opens it:
46296
- * the phone leaves the notification and shows whatever the callback returns.
46297
- * That is right for a button whose answer the operator wants to read.
46298
- *
46299
- * `background` fires it as a POST and stays put. It exists for the buttons
46300
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
46301
- * an answer to the notification, and being thrown into a browser tab to
46302
- * confirm it costs more attention than the notification did. A backend that
46303
- * cannot do a background call renders it as an ordinary link (the adapters
46304
- * fall back rather than dropping the button), so this is a preference, never
46305
- * a requirement.
46306
- */
46307
- mode: _enum(["navigate", "background"]).optional()
46308
- });
46309
- /** A delivery parameter value: what a notifier declared it takes. */
46310
- var DeliveryParamValueSchema = union([
46311
- string(),
46312
- number(),
46313
- boolean()
46314
- ]);
46315
- /**
46316
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
46317
- * addresses a target of that kind may set it; a kind that declares nothing is
46318
- * never handed one. The admin UI derives its control from this descriptor —
46319
- * no editor knows a service by name.
46320
- *
46321
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
46322
- * run on the addon cap path).
46323
- */
46324
- var DeliveryParamSchema = object({
46325
- key: string().min(1),
46326
- label: string(),
46327
- /** i18n key of the label, resolved by the declaring addon's namespace. */
46328
- labelKey: string().optional(),
46329
- type: _enum([
46330
- "enum",
46331
- "number",
46332
- "boolean",
46333
- "string"
46334
- ]),
46335
- /** `enum` only: the closed set of accepted values. */
46336
- options: array(object({
46337
- value: string(),
46338
- label: string()
46339
- })).optional(),
46340
- /** `number` only. */
46341
- min: number().optional(),
46342
- max: number().optional(),
46343
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
46344
- default: DeliveryParamValueSchema.optional()
46345
- });
46346
- /**
46347
- * The canonical notification. `body` is the only hard field (Apprise model).
46348
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
46349
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
46350
- * the adapter maps this ordinal onto its native level. `level?` is an
46351
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
46352
- * `priority` for that one target.
46353
- */
46354
- var NotificationSchema = object({
46355
- body: string(),
46356
- title: string().optional(),
46357
- format: NotificationFormatSchema.default("text"),
46358
- priority: number().int().min(1).max(5).default(3),
46359
- level: string().optional(),
46360
- attachments: array(AttachmentSchema).optional(),
46361
- clickUrl: string().optional(),
46362
- actions: array(NotificationActionSchema).optional(),
46363
- sound: string().optional(),
46364
- ttl: number().optional(),
46365
- tag: string().optional(),
46366
- deviceId: number().optional(),
46367
- eventId: string().optional(),
46368
- metadata: record(string(), unknown()).optional(),
46369
- /**
46370
- * Values for the parameters the target's kind DECLARES in
46371
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
46372
- * declaration by the sender; a notifier reads only the keys it declared.
46373
- */
46374
- delivery: record(string(), DeliveryParamValueSchema).optional()
46375
- });
46376
- /** One declared native severity/priority level for a kind. */
46377
- var TargetKindLevelSchema = object({
46378
- id: string(),
46379
- label: string(),
46380
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
46381
- ordinal: number().int().min(1).max(5).nullable(),
46382
- flags: object({
46383
- critical: boolean().optional(),
46384
- silent: boolean().optional(),
46385
- noPush: boolean().optional()
46386
- }).optional(),
46387
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
46388
- requires: array(string()).optional(),
46389
- description: string().optional()
46390
- });
46391
- /** The full capability block consulted before dispatch. */
46392
- var TargetKindCapsSchema = object({
46393
- attachments: object({
46394
- mediaTypes: array(AttachmentMediaTypeSchema),
46395
- mode: _enum([
46396
- "url",
46397
- "bytes",
46398
- "both"
46399
- ]),
46400
- max: number().int().nonnegative(),
46401
- maxBytes: number().int().positive().optional()
46402
- }),
46403
- /** Max action buttons (0 = none). */
46404
- actions: number().int().nonnegative(),
46405
- /**
46406
- * Whether this kind renders a per-action ICON.
46407
- *
46408
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
46409
- * run on the addon cap path — three production failures in one day taught
46410
- * this repo that once. Absent is read as false by the degrade engine, which
46411
- * is the safe direction: an icon that is not rendered costs nothing, an icon
46412
- * assumed and dropped costs the operator's trust in the field.
46413
- */
46414
- actionIcons: boolean().optional(),
46415
- levels: array(TargetKindLevelSchema),
46416
- format: array(NotificationFormatSchema),
46417
- clickUrl: boolean(),
46418
- sound: boolean(),
46419
- ttl: boolean(),
46420
- bodyMaxLen: number().int().positive()
46421
- });
46422
- /**
46423
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
46424
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
46425
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
46426
- * the union is large and not meant for runtime validation here; the exported
46427
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
46428
- */
46429
- var ConfigSchemaPassthrough$2 = unknown();
46430
- var TargetKindSchema = object({
46431
- kind: string(),
46432
- label: string(),
46433
- icon: string(),
46434
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
46435
- addonId: string(),
46436
- /**
46437
- * URL of the kind's bundled BRAND icon, served by the providing addon over
46438
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
46439
- * when the addon bundles no icon for that kind — the client then falls back
46440
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
46441
- *
46442
- * Root-relative on purpose: it resolves against whatever origin serves a web
46443
- * client, and a native client joins it onto its own hub base.
46444
- *
46445
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
46446
- * field that survived only because the runtime cap-router forwards provider
46447
- * output verbatim — so every consumer had to re-declare it by hand to stop
46448
- * its own Zod parse from stripping it, and the whole arrangement would have
46449
- * broken silently the moment output validation was tightened anywhere.
46450
- */
46451
- iconUrl: string().optional(),
46452
- /**
46453
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
46454
- *
46455
- * The server knows this and therefore says it, because the client cannot
46456
- * safely guess: a React-Native client renders SVG and raster through two
46457
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
46458
- * not decode SVG on iOS/Android), so without this it silently fell back to a
46459
- * placeholder glyph for every vector icon while the web build looked fine.
46460
- *
46461
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
46462
- * not been updated — a client that cannot determine the type should prefer
46463
- * its raster path, which is the safe default for an unknown image.
46464
- */
46465
- iconMediaType: string().optional(),
46466
- configSchema: ConfigSchemaPassthrough$2,
46467
- /**
46468
- * The config keys that decide WHERE this kind's secrets are sent (server,
46469
- * base URL, instance, chat, recipient). A save that changes any of them
46470
- * while leaving a secret redacted is refused by the provider (D634), so an
46471
- * editor uses this to ask for the secret on exactly those edits instead of
46472
- * on every edit.
46473
- *
46474
- * Optional: absent from a provider that holds no secret (the Home Assistant
46475
- * kinds) and from a provider older than this field — an editor that finds
46476
- * it absent must assume ANY edit may need the secret.
46477
- */
46478
- destinationKeys: array(string()).optional(),
46479
- supportsDiscovery: boolean(),
46480
- /**
46481
- * Extra delivery parameters a rule may set for a target of this kind (D938).
46482
- * Absent = none; the rule editor then offers nothing.
46483
- */
46484
- deliveryParams: array(DeliveryParamSchema).optional(),
46485
- caps: TargetKindCapsSchema
46486
- });
46487
- /**
46488
- * A persisted target. `config` holds secrets; providers REDACT secret fields
46489
- * (return a presence marker only) when serving `listTargets` — never
46490
- * round-trip a stored secret to the UI.
46491
- */
46492
- var TargetSchema = object({
46493
- id: string(),
46494
- name: string(),
46495
- kind: string(),
46496
- addonId: string(),
46497
- enabled: boolean(),
46498
- config: record(string(), unknown())
46499
- });
46500
- /** A discovery-surfaced candidate (config is partial + non-secret). */
46501
- var DiscoveredTargetSchema = object({
46502
- kind: string(),
46503
- suggestedName: string(),
46504
- config: record(string(), unknown())
46505
- });
46506
- /** The degrade engine's report — what was resolved / dropped / degraded. */
46507
- var RenderedAsSchema = object({
46508
- level: string(),
46509
- format: NotificationFormatSchema,
46510
- attachmentsSent: number().int().nonnegative(),
46511
- actionsSent: number().int().nonnegative(),
46512
- truncated: boolean(),
46513
- dropped: array(string())
46514
- });
46515
- var SendResultSchema = object({
46516
- success: boolean(),
46517
- error: string().optional(),
46518
- renderedAs: RenderedAsSchema.optional()
46519
- });
46520
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
46521
- var TestResultSchema = SendResultSchema;
46522
- /**
46523
- * Pre-save credential probe against a partial (unsaved) target config.
46524
- * `resolvedUrl` never includes secrets (token stripped from path).
46525
- */
46526
- var ProbeTargetConfigResultSchema = object({
46527
- ok: boolean(),
46528
- resolvedUrl: string(),
46529
- httpStatus: number().int().optional(),
46530
- stateInstance: string().optional(),
46531
- error: string().optional()
46532
- });
46533
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
46534
- kind: string(),
46535
- config: record(string(), unknown()).optional(),
46536
- /**
46537
- * When editing / duplicating, the blank password field never round-trips
46538
- * the stored token. Pass the source target id so the provider can restore
46539
- * secrets server-side before dialling (same merge as upsert).
46540
- */
46541
- targetId: string().optional()
46542
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
46543
- kind: string(),
46544
- config: record(string(), unknown()),
46545
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
46546
- targetId: string().optional()
46547
- }), ProbeTargetConfigResultSchema, {
46548
- kind: "mutation",
46549
- auth: "admin"
46550
- }), method(object({
46551
- targetId: string(),
46552
- notification: NotificationSchema
46553
- }), SendResultSchema, {
46554
- kind: "mutation",
46555
- auth: "admin"
46556
- }), method(object({
46557
- targetId: string(),
46558
- sample: NotificationSchema.optional()
46559
- }), TestResultSchema, {
46560
- kind: "mutation",
46561
- auth: "admin"
46562
- }), method(object({
46563
- target: TargetSchema,
46564
- /**
46565
- * Duplicate flow: create a NEW target id while copying secrets from an
46566
- * existing row. The UI never holds the stored token; blank / redacted
46567
- * password fields are restored from this source (D634 still applies if
46568
- * destinations diverge without a re-typed secret).
46569
- */
46570
- cloneSecretsFrom: string().optional()
46571
- }), TargetSchema, {
46572
- kind: "mutation",
46573
- auth: "admin"
46574
- }), method(object({ targetId: string() }), _void(), {
46575
- kind: "mutation",
46576
- auth: "admin"
46577
- }), method(object({
46578
- targetId: string(),
46579
- enabled: boolean()
46580
- }), _void(), {
46581
- kind: "mutation",
46582
- auth: "admin"
46583
- });
46584
47207
  var AUDIO_MACRO_LABELS = [
46585
47208
  {
46586
47209
  id: "speech",
@@ -54825,14 +55448,6 @@ var EngineProvisioningSchema = object({
54825
55448
  */
54826
55449
  configIssues: array(string()).optional()
54827
55450
  });
54828
- var PipelineStepInputSchema = lazy(() => object({
54829
- addonId: string(),
54830
- modelId: string().optional(),
54831
- enabled: boolean().default(true),
54832
- children: array(PipelineStepInputSchema).optional(),
54833
- settings: record(string(), unknown()).optional(),
54834
- jumpDeviceKey: string().optional()
54835
- }));
54836
55451
  var ModelSubstitutionSchema = object({
54837
55452
  addonId: string(),
54838
55453
  chosen: string(),
@@ -54911,7 +55526,6 @@ var ConfigUISchemaBridge = custom();
54911
55526
  var ConfigUISchemaNullableBridge = custom();
54912
55527
  var InferenceCapabilitiesBridge = custom();
54913
55528
  var ModelAvailabilityListBridge = custom();
54914
- var PipelineRunResultBridge = custom();
54915
55529
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
54916
55530
  modelId: string(),
54917
55531
  settings: record(string(), unknown()).readonly()
@@ -57907,11 +58521,13 @@ var ImageContractSchema = object({
57907
58521
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
57908
58522
  * produced it.
57909
58523
  *
57910
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
57911
- * update is an ANNOUNCEMENT. The notification body carries the exact command
57912
- * for a `docker` shell and says the app restarts into the new build for
57913
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
57914
- * nothing reads is the defect this repo keeps paying for.
58524
+ * There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
58525
+ * ANNOUNCEMENT. The notification body carries the exact command for a `docker`
58526
+ * shell and says the app restarts into the new build for `electron`, and that
58527
+ * sentence is chosen from `kind` alone — a boolean nothing reads is the defect
58528
+ * this repo keeps paying for. The one shell that CAN be acted on is the
58529
+ * Electron app, and only through `installDesktopUpdate` (see
58530
+ * {@link DesktopAppStatusSchema} for what the node reads from the app first).
57915
58531
  */
57916
58532
  var WrapperIdentitySchema = object({
57917
58533
  kind: _enum([
@@ -57959,6 +58575,49 @@ var ServerRollbackInfoSchema = object({
57959
58575
  reason: string()
57960
58576
  });
57961
58577
  /**
58578
+ * What the native desktop app (Electron) that hosts this node says about
58579
+ * itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
58580
+ * Absent on the status = no desktop app, or it never wrote the file.
58581
+ *
58582
+ * `stale` is the honest answer to an app that stopped writing (closed, hung,
58583
+ * crashed): the update facts are then the LAST ones known, not current ones,
58584
+ * and a consumer must not read `update.available: false` from a stale record
58585
+ * as "no update". `stateAtMs` is when the app last wrote.
58586
+ */
58587
+ var DesktopAppStatusSchema = object({
58588
+ /** The Electron app's own version (not the node closure's). */
58589
+ version: string(),
58590
+ update: object({
58591
+ available: boolean(),
58592
+ /** The new build is downloaded and `installDesktopUpdate` can apply it. */
58593
+ downloaded: boolean(),
58594
+ version: string().nullable(),
58595
+ checking: boolean(),
58596
+ /** Why the app's last update check produced no answer; `null` when it did. */
58597
+ error: string().nullable()
58598
+ }),
58599
+ stateAtMs: number(),
58600
+ stale: boolean()
58601
+ });
58602
+ /**
58603
+ * Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
58604
+ * - `not-electron` — this node does not run inside the desktop app.
58605
+ * - `no-app-state` — the app never published its state (or the file is unreadable).
58606
+ * - `app-state-stale` — the app stopped writing; it is probably not running.
58607
+ * - `not-downloaded` — no update is downloaded yet; nothing to install.
58608
+ */
58609
+ var InstallDesktopUpdateReasonSchema = _enum([
58610
+ "not-electron",
58611
+ "no-app-state",
58612
+ "app-state-stale",
58613
+ "not-downloaded"
58614
+ ]);
58615
+ var InstallDesktopUpdateResultSchema = object({
58616
+ accepted: boolean(),
58617
+ /** Non-null exactly when `accepted` is false. */
58618
+ reason: InstallDesktopUpdateReasonSchema.nullable()
58619
+ });
58620
+ /**
57962
58621
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
57963
58622
  * `resolveRunnerId` over its forked addons; agent: the runners its process
57964
58623
  * service holds, because only the hub plans) against the states the process
@@ -58062,7 +58721,12 @@ var ServerPackageStatusSchema = object({
58062
58721
  * on a provider that predates it. Absent is "not checked yet", never "all
58063
58722
  * well": a consumer that needs the answer treats it as pending.
58064
58723
  */
58065
- runners: RunnerBootCheckSchema.optional()
58724
+ runners: RunnerBootCheckSchema.optional(),
58725
+ /**
58726
+ * The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
58727
+ * Optional for version skew AND because most nodes have no desktop app.
58728
+ */
58729
+ desktopApp: DesktopAppStatusSchema.optional()
58066
58730
  });
58067
58731
  var ServerUpdateCheckResultSchema = object({
58068
58732
  packageName: string(),
@@ -58145,6 +58809,12 @@ var ServerUpdateActionResultSchema = object({
58145
58809
  */
58146
58810
  blockers: array(UpdateBlockerSchema).optional()
58147
58811
  });
58812
+ var LaunchOverridesSchema = object({
58813
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
58814
+ heapMb: number().int().nullable() });
58815
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
58816
+ /** Always true: the override is read at launch, not by the running process. */
58817
+ restartRequired: boolean() });
58148
58818
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
58149
58819
  kind: "mutation",
58150
58820
  auth: "admin"
@@ -58161,12 +58831,18 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
58161
58831
  kind: "mutation",
58162
58832
  auth: "admin",
58163
58833
  timeoutMs: 16 * 6e4
58834
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
58835
+ kind: "mutation",
58836
+ auth: "admin"
58164
58837
  }), method(_void(), ServerUpdateActionResultSchema, {
58165
58838
  kind: "mutation",
58166
58839
  auth: "admin"
58167
58840
  }), method(_void(), ServerUpdateActionResultSchema, {
58168
58841
  kind: "mutation",
58169
58842
  auth: "admin"
58843
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
58844
+ kind: "mutation",
58845
+ auth: "admin"
58170
58846
  });
58171
58847
  /**
58172
58848
  * `smtp-provider` — pluggable email delivery surface.
@@ -61308,13 +61984,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
61308
61984
  }), method(ActionIdInput, _void(), {
61309
61985
  kind: "mutation",
61310
61986
  auth: "admin"
61311
- }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
61987
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
61988
+ action: ActionEntrySchema,
61989
+ warnings: array(UnitWarningSchema).optional()
61990
+ }), {
61312
61991
  kind: "mutation",
61313
61992
  auth: "admin"
61314
61993
  }), method(object({
61315
61994
  actionId: string().min(1),
61316
61995
  action: ActionInputSchema.partial()
61317
- }), object({ action: ActionEntrySchema }), {
61996
+ }), object({
61997
+ action: ActionEntrySchema,
61998
+ warnings: array(UnitWarningSchema).optional()
61999
+ }), {
61318
62000
  kind: "mutation",
61319
62001
  auth: "admin"
61320
62002
  }), method(ActionIdInput, _void(), {
@@ -61766,6 +62448,510 @@ onBrightnessChanged: { data: object({
61766
62448
  minIntervalMs: 0
61767
62449
  } } }
61768
62450
  };
62451
+ /**
62452
+ * The role vocabulary of a remote (D963): what a button MEANS, independent of
62453
+ * how it is wired. A layout template places buttons by role; a device binds a
62454
+ * role to an action; an exporter asks for a role by name. Roles are shared so
62455
+ * one layout fits every appliance and one voice command fits every binding.
62456
+ *
62457
+ * A role outside this list is `custom:<key>` — the operator's own button (a
62458
+ * SmartIR source, a channel preset). The key is a lowercase slug.
62459
+ */
62460
+ var REMOTE_KNOWN_ROLES = [
62461
+ "power",
62462
+ "power_on",
62463
+ "power_off",
62464
+ "volume_up",
62465
+ "volume_down",
62466
+ "mute",
62467
+ "channel_up",
62468
+ "channel_down",
62469
+ "up",
62470
+ "down",
62471
+ "left",
62472
+ "right",
62473
+ "ok",
62474
+ "back",
62475
+ "home",
62476
+ "menu",
62477
+ "info",
62478
+ "guide",
62479
+ "exit",
62480
+ "input",
62481
+ "play",
62482
+ "pause",
62483
+ "play_pause",
62484
+ "stop",
62485
+ "next",
62486
+ "previous",
62487
+ "fast_forward",
62488
+ "rewind",
62489
+ "digit_0",
62490
+ "digit_1",
62491
+ "digit_2",
62492
+ "digit_3",
62493
+ "digit_4",
62494
+ "digit_5",
62495
+ "digit_6",
62496
+ "digit_7",
62497
+ "digit_8",
62498
+ "digit_9"
62499
+ ];
62500
+ var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
62501
+ var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
62502
+ function isRemoteKnownRole(value) {
62503
+ return KNOWN_ROLES.has(value);
62504
+ }
62505
+ function isRemoteCustomKey(key) {
62506
+ return CUSTOM_KEY_RE.test(key);
62507
+ }
62508
+ /** The key of a `custom:<key>` role, or null for any other string. */
62509
+ function customKeyOfRole(role) {
62510
+ if (!role.startsWith("custom:")) return null;
62511
+ const key = role.slice(7);
62512
+ return isRemoteCustomKey(key) ? key : null;
62513
+ }
62514
+ function isRemoteRole(value) {
62515
+ return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
62516
+ }
62517
+ var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
62518
+ var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
62519
+ var RemoteCustomButtonSchema = object({
62520
+ /** The slug after `custom:` in the role. */
62521
+ key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
62522
+ /** What the operator calls it, and what a voice command may name. */
62523
+ name: string().min(1).max(60)
62524
+ });
62525
+ var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
62526
+ var RemoteConfigSchema = object({
62527
+ layoutTemplateId: string().min(1).nullable(),
62528
+ /**
62529
+ * The transmitter device new bindings and an import send through. A binding
62530
+ * names its own device, so one command may override it.
62531
+ */
62532
+ defaultTransmitterId: number().int().nonnegative().nullable().default(null),
62533
+ /** Keyed by role. A role with no entry falls back to the native cap. */
62534
+ bindings: record(string(), RemoteBindingSchema),
62535
+ customButtons: array(RemoteCustomButtonSchema).max(100),
62536
+ power: object({ mode: RemotePowerModeSchema })
62537
+ }).superRefine((config, ctx) => {
62538
+ const roles = Object.keys(config.bindings);
62539
+ if (roles.length > 200) ctx.addIssue({
62540
+ code: "custom",
62541
+ message: "too many bindings",
62542
+ path: ["bindings"]
62543
+ });
62544
+ for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
62545
+ code: "custom",
62546
+ message: `"${role}" is not a role`,
62547
+ path: ["bindings", role]
62548
+ });
62549
+ const keys = /* @__PURE__ */ new Set();
62550
+ config.customButtons.forEach((button, index) => {
62551
+ if (keys.has(button.key)) ctx.addIssue({
62552
+ code: "custom",
62553
+ message: `custom button "${button.key}" is listed twice`,
62554
+ path: [
62555
+ "customButtons",
62556
+ index,
62557
+ "key"
62558
+ ]
62559
+ });
62560
+ keys.add(button.key);
62561
+ });
62562
+ for (const role of roles) {
62563
+ const key = customKeyOfRole(role);
62564
+ if (key !== null && !keys.has(key)) ctx.addIssue({
62565
+ code: "custom",
62566
+ message: `${role} is bound but has no entry in customButtons`,
62567
+ path: ["bindings", role]
62568
+ });
62569
+ }
62570
+ });
62571
+ object({
62572
+ power: object({
62573
+ state: _enum(["on", "off"]),
62574
+ at: number()
62575
+ }).nullable(),
62576
+ muted: object({
62577
+ value: boolean(),
62578
+ at: number()
62579
+ }).nullable()
62580
+ });
62581
+ var RemoteButtonShapeSchema = _enum([
62582
+ "rect",
62583
+ "rounded",
62584
+ "round",
62585
+ "pill"
62586
+ ]);
62587
+ var RemoteButtonToneSchema = _enum([
62588
+ "neutral",
62589
+ "accent",
62590
+ "danger",
62591
+ "red",
62592
+ "green",
62593
+ "yellow",
62594
+ "blue"
62595
+ ]);
62596
+ var RemoteGroupFrameSchema = _enum([
62597
+ "none",
62598
+ "panel",
62599
+ "divider",
62600
+ "dpad",
62601
+ "rocker-v",
62602
+ "rocker-h",
62603
+ "keypad"
62604
+ ]);
62605
+ var RemoteLayoutButtonSchema = object({
62606
+ id: string().min(1).max(40),
62607
+ /** 0-based grid position. */
62608
+ row: number().int().min(0).max(24),
62609
+ col: number().int().min(0).max(24),
62610
+ rowSpan: number().int().min(1).max(24),
62611
+ colSpan: number().int().min(1).max(24),
62612
+ icon: string().min(1).max(60).optional(),
62613
+ label: string().min(1).max(40).optional(),
62614
+ role: RemoteRoleSchema,
62615
+ /** Absent = the plain rendering every layout had before shapes existed. */
62616
+ shape: RemoteButtonShapeSchema.optional(),
62617
+ tone: RemoteButtonToneSchema.optional(),
62618
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
62619
+ groupId: string().min(1).max(40).optional()
62620
+ });
62621
+ /**
62622
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
62623
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
62624
+ * entry it came from (informational: the layout is self-contained).
62625
+ */
62626
+ var RemoteLayoutGroupSchema = object({
62627
+ id: string().min(1).max(40),
62628
+ componentId: string().min(1).max(80),
62629
+ frame: RemoteGroupFrameSchema,
62630
+ row: number().int().min(0).max(24),
62631
+ col: number().int().min(0).max(24),
62632
+ rowSpan: number().int().min(1).max(24),
62633
+ colSpan: number().int().min(1).max(24),
62634
+ title: string().min(1).max(40).optional()
62635
+ });
62636
+ function rectsOverlap(a, b) {
62637
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
62638
+ }
62639
+ function rectContains(outer, inner) {
62640
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
62641
+ }
62642
+ /**
62643
+ * The group rules of a layout: inside the grid, no two groups overlap, a
62644
+ * member lies inside its group, a divider holds nothing, and a button that is
62645
+ * not a member never sits inside someone else's rectangle (a group moves as
62646
+ * one thing, so a stray key under it would be left behind).
62647
+ */
62648
+ function refineGroups(grid, ctx) {
62649
+ const groups = grid.groups ?? [];
62650
+ const byId = /* @__PURE__ */ new Map();
62651
+ const fail = (message, path) => {
62652
+ ctx.addIssue({
62653
+ code: "custom",
62654
+ message,
62655
+ path
62656
+ });
62657
+ };
62658
+ groups.forEach((group, index) => {
62659
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
62660
+ "groups",
62661
+ index,
62662
+ "id"
62663
+ ]);
62664
+ byId.set(group.id, group);
62665
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
62666
+ for (let other = 0; other < index; other += 1) {
62667
+ const earlier = groups[other];
62668
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
62669
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
62670
+ return;
62671
+ }
62672
+ }
62673
+ });
62674
+ grid.buttons.forEach((button, index) => {
62675
+ if (button.groupId !== void 0) {
62676
+ const group = byId.get(button.groupId);
62677
+ if (group === void 0) {
62678
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
62679
+ "buttons",
62680
+ index,
62681
+ "groupId"
62682
+ ]);
62683
+ return;
62684
+ }
62685
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
62686
+ "buttons",
62687
+ index,
62688
+ "groupId"
62689
+ ]);
62690
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
62691
+ return;
62692
+ }
62693
+ const foreign = groups.find((group) => rectsOverlap(group, button));
62694
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
62695
+ });
62696
+ }
62697
+ object({
62698
+ columns: number().int().min(1).max(24),
62699
+ rows: number().int().min(1).max(24),
62700
+ buttons: array(RemoteLayoutButtonSchema).max(200),
62701
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
62702
+ }).superRefine((grid, ctx) => {
62703
+ const ids = /* @__PURE__ */ new Set();
62704
+ const taken = /* @__PURE__ */ new Map();
62705
+ grid.buttons.forEach((button, index) => {
62706
+ if (ids.has(button.id)) ctx.addIssue({
62707
+ code: "custom",
62708
+ message: `button id "${button.id}" is used twice`,
62709
+ path: [
62710
+ "buttons",
62711
+ index,
62712
+ "id"
62713
+ ]
62714
+ });
62715
+ ids.add(button.id);
62716
+ if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
62717
+ ctx.addIssue({
62718
+ code: "custom",
62719
+ message: `button "${button.id}" leaves the grid`,
62720
+ path: ["buttons", index]
62721
+ });
62722
+ return;
62723
+ }
62724
+ for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
62725
+ const cell = `${r}:${c}`;
62726
+ const other = taken.get(cell);
62727
+ if (other !== void 0) {
62728
+ ctx.addIssue({
62729
+ code: "custom",
62730
+ message: `buttons "${other}" and "${button.id}" overlap`,
62731
+ path: ["buttons", index]
62732
+ });
62733
+ return;
62734
+ }
62735
+ taken.set(cell, button.id);
62736
+ }
62737
+ });
62738
+ refineGroups(grid, ctx);
62739
+ });
62740
+ var RemoteBoundCommandSchema = object({
62741
+ role: RemoteRoleSchema,
62742
+ /** The operator's name for a custom button; absent for a known role. */
62743
+ label: string().optional(),
62744
+ custom: boolean()
62745
+ });
62746
+ var RemoteCommandListSchema = object({
62747
+ layoutTemplateId: string().nullable(),
62748
+ power: object({ mode: RemotePowerModeSchema }),
62749
+ /** Only the roles that are BOUND — the ones a press can reach. */
62750
+ commands: array(RemoteBoundCommandSchema)
62751
+ });
62752
+ var RemoteRefusalReasonSchema = _enum([
62753
+ "no-remote",
62754
+ "unbound-role",
62755
+ "action-failed"
62756
+ ]);
62757
+ var RemoteSendResultSchema = discriminatedUnion("status", [
62758
+ object({
62759
+ status: literal("sent"),
62760
+ /** How many times the action ran. */
62761
+ sends: number().int().min(1)
62762
+ }),
62763
+ object({
62764
+ status: literal("skipped"),
62765
+ reason: literal("already-in-state")
62766
+ }),
62767
+ object({
62768
+ status: literal("refused"),
62769
+ reason: RemoteRefusalReasonSchema,
62770
+ detail: string().optional()
62771
+ })
62772
+ ]);
62773
+ var RemoteMediaCommandSchema = _enum([
62774
+ "play",
62775
+ "pause",
62776
+ "stop",
62777
+ "next",
62778
+ "previous",
62779
+ "setMute",
62780
+ "selectSource"
62781
+ ]);
62782
+ /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
62783
+ var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
62784
+ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
62785
+ status: literal("refused"),
62786
+ reason: _enum(["unknown-template", "not-a-media-player"]),
62787
+ detail: string()
62788
+ })]);
62789
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
62790
+ DeviceType.MediaPlayer;
62791
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
62792
+ kind: "mutation",
62793
+ auth: "admin"
62794
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
62795
+ role: RemoteRoleSchema,
62796
+ /** Sends in a row (volume steps). */
62797
+ repeat: number().int().min(1).max(50).default(1)
62798
+ }), RemoteSendResultSchema, {
62799
+ kind: "mutation",
62800
+ auth: "protected"
62801
+ }), method(deviceInput.extend({
62802
+ command: RemoteMediaCommandSchema,
62803
+ muted: boolean().optional(),
62804
+ source: string().optional(),
62805
+ repeat: number().int().min(1).max(50).default(1)
62806
+ }), RemoteMediaCommandResultSchema, {
62807
+ kind: "mutation",
62808
+ auth: "protected"
62809
+ });
62810
+ var TemplateKindSchema = _enum(["remote"]);
62811
+ /**
62812
+ * template — the generic store of reusable, named operator data (D963). A
62813
+ * record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
62814
+ * button grid of a remote) and the schema each payload must satisfy are
62815
+ * declared by the system in `template-kinds.ts`. Nothing here is specific to a
62816
+ * kind: the engine validates a payload against its kind's schema and, on a
62817
+ * delete, asks the kind's owner whether anything still points at the record.
62818
+ *
62819
+ * Every refusal is a typed result — a UI says "unknown kind" or names the
62820
+ * devices that still use a template, without parsing text.
62821
+ */
62822
+ var TemplateRecordSchema = object({
62823
+ id: string().min(1),
62824
+ kind: TemplateKindSchema,
62825
+ name: string().min(1).max(120),
62826
+ /** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
62827
+ payload: unknown(),
62828
+ createdAt: number(),
62829
+ updatedAt: number()
62830
+ });
62831
+ var TemplateWriteResultSchema = discriminatedUnion("status", [object({
62832
+ status: literal("saved"),
62833
+ template: TemplateRecordSchema
62834
+ }), object({
62835
+ status: literal("refused"),
62836
+ reason: _enum(["invalid-payload", "not-found"]),
62837
+ detail: string()
62838
+ })]);
62839
+ /** A device still pointing at a template. */
62840
+ var TemplateUserSchema = object({
62841
+ deviceId: number().int().nonnegative(),
62842
+ name: string()
62843
+ });
62844
+ var TemplateDeleteResultSchema = discriminatedUnion("status", [
62845
+ object({ status: literal("deleted") }),
62846
+ object({ status: literal("not-found") }),
62847
+ object({
62848
+ status: literal("in-use"),
62849
+ devices: array(TemplateUserSchema).min(1)
62850
+ })
62851
+ ]);
62852
+ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
62853
+ kind: TemplateKindSchema,
62854
+ name: string().min(1).max(120),
62855
+ payload: unknown()
62856
+ }), TemplateWriteResultSchema, {
62857
+ kind: "mutation",
62858
+ auth: "admin"
62859
+ }), method(object({
62860
+ id: string().min(1),
62861
+ name: string().min(1).max(120).optional(),
62862
+ payload: unknown().optional()
62863
+ }), TemplateWriteResultSchema, {
62864
+ kind: "mutation",
62865
+ auth: "admin"
62866
+ }), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
62867
+ kind: "mutation",
62868
+ auth: "admin"
62869
+ });
62870
+ var RemoteComponentCategorySchema = _enum([
62871
+ "power",
62872
+ "navigation",
62873
+ "volume",
62874
+ "numbers",
62875
+ "media",
62876
+ "colour",
62877
+ "apps",
62878
+ "layout"
62879
+ ]);
62880
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
62881
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
62882
+ var RemoteComponentButtonSchema = object({
62883
+ /** Joined to the instance id when the component is inserted. */
62884
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
62885
+ row: number().int().min(0).max(24),
62886
+ col: number().int().min(0).max(24),
62887
+ rowSpan: number().int().min(1).max(24),
62888
+ colSpan: number().int().min(1).max(24),
62889
+ role: RemoteRoleSchema,
62890
+ icon: string().min(1).max(60).optional(),
62891
+ label: string().min(1).max(40).optional(),
62892
+ shape: RemoteButtonShapeSchema.optional(),
62893
+ tone: RemoteButtonToneSchema.optional()
62894
+ });
62895
+ var RemoteLayoutComponentSchema = object({
62896
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
62897
+ name: string().min(1).max(60),
62898
+ nameI18n: object({
62899
+ en: string().min(1).max(60),
62900
+ it: string().min(1).max(60)
62901
+ }).optional(),
62902
+ /** A display icon key, resolved by the UI like a button icon. */
62903
+ icon: string().min(1).max(60),
62904
+ category: RemoteComponentCategorySchema,
62905
+ size: object({
62906
+ rows: number().int().min(1).max(24),
62907
+ cols: number().int().min(1).max(24)
62908
+ }),
62909
+ /** The frame drawn around the whole component, or null for loose buttons. */
62910
+ group: object({
62911
+ frame: RemoteGroupFrameSchema,
62912
+ title: string().min(1).max(40).optional()
62913
+ }).nullable(),
62914
+ buttons: array(RemoteComponentButtonSchema).max(40)
62915
+ }).superRefine((component, ctx) => {
62916
+ const suffixes = /* @__PURE__ */ new Set();
62917
+ component.buttons.forEach((button, index) => {
62918
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
62919
+ code: "custom",
62920
+ message: `button "${button.idSuffix}" is listed twice`,
62921
+ path: [
62922
+ "buttons",
62923
+ index,
62924
+ "idSuffix"
62925
+ ]
62926
+ });
62927
+ suffixes.add(button.idSuffix);
62928
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
62929
+ ctx.addIssue({
62930
+ code: "custom",
62931
+ message: `button "${button.idSuffix}" leaves the component`,
62932
+ path: ["buttons", index]
62933
+ });
62934
+ return;
62935
+ }
62936
+ for (let other = 0; other < index; other += 1) {
62937
+ const earlier = component.buttons[other];
62938
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
62939
+ ctx.addIssue({
62940
+ code: "custom",
62941
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
62942
+ path: ["buttons", index]
62943
+ });
62944
+ return;
62945
+ }
62946
+ }
62947
+ });
62948
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
62949
+ code: "custom",
62950
+ message: "a divider component holds no buttons",
62951
+ path: ["buttons"]
62952
+ });
62953
+ });
62954
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
61769
62955
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
61770
62956
  kind: "mutation",
61771
62957
  auth: "protected"
@@ -62821,14 +64007,13 @@ reset: method(object({
62821
64007
  auth: "admin"
62822
64008
  }) },
62823
64009
  /**
62824
- * A composed consumable (item added to a device with no native consumables
62825
- * provider) refuses `reset` by name: "Only meaningful when the item's
62826
- * `resettable` is true" — a composed item is never resettable until
62827
- * commands (`forward`) arrive in slice 3 (D663).
64010
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
64011
+ * name: only a composed device of a NEW type forwards an item's reset to a
64012
+ * source button, and marks that item `resettable` (D967).
62828
64013
  */
62829
64014
  composedMethods: { reset: {
62830
64015
  kind: "refuse",
62831
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
64016
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
62832
64017
  } },
62833
64018
  status: {
62834
64019
  schema: ConsumablesStatusSchema,
@@ -63444,7 +64629,7 @@ var DishwasherStatusSchema = object({
63444
64629
  /** Ms epoch of the last value received from the appliance; 0 = never. */
63445
64630
  lastFetchedAt: number()
63446
64631
  });
63447
- var DeviceIdSchema = number().int().nonnegative();
64632
+ var DeviceIdSchema$1 = number().int().nonnegative();
63448
64633
  /** A value a program option or setting can take. */
63449
64634
  var DishwasherValueSchema = union([
63450
64635
  boolean(),
@@ -63519,77 +64704,77 @@ var dishwasherCapability = {
63519
64704
  * option feature keys; each is checked against the program's constraints.
63520
64705
  */
63521
64706
  start: method(object({
63522
- deviceId: DeviceIdSchema,
64707
+ deviceId: DeviceIdSchema$1,
63523
64708
  program: string().min(1).optional(),
63524
64709
  options: record(string(), DishwasherValueSchema).optional()
63525
64710
  }), _void(), {
63526
64711
  kind: "mutation",
63527
64712
  auth: "protected"
63528
64713
  }),
63529
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
64714
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63530
64715
  kind: "mutation",
63531
64716
  auth: "protected"
63532
64717
  }),
63533
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
64718
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63534
64719
  kind: "mutation",
63535
64720
  auth: "protected"
63536
64721
  }),
63537
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
64722
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
63538
64723
  kind: "mutation",
63539
64724
  auth: "protected"
63540
64725
  }),
63541
64726
  /** Select (not start) a program; refused when the appliance does not offer it. */
63542
64727
  setProgram: method(object({
63543
- deviceId: DeviceIdSchema,
64728
+ deviceId: DeviceIdSchema$1,
63544
64729
  program: string().min(1)
63545
64730
  }), _void(), {
63546
64731
  kind: "mutation",
63547
64732
  auth: "protected"
63548
64733
  }),
63549
64734
  setPower: method(object({
63550
- deviceId: DeviceIdSchema,
64735
+ deviceId: DeviceIdSchema$1,
63551
64736
  on: boolean()
63552
64737
  }), _void(), {
63553
64738
  kind: "mutation",
63554
64739
  auth: "protected"
63555
64740
  }),
63556
64741
  setHalfLoad: method(object({
63557
- deviceId: DeviceIdSchema,
64742
+ deviceId: DeviceIdSchema$1,
63558
64743
  on: boolean()
63559
64744
  }), _void(), {
63560
64745
  kind: "mutation",
63561
64746
  auth: "protected"
63562
64747
  }),
63563
64748
  setHygienePlus: method(object({
63564
- deviceId: DeviceIdSchema,
64749
+ deviceId: DeviceIdSchema$1,
63565
64750
  on: boolean()
63566
64751
  }), _void(), {
63567
64752
  kind: "mutation",
63568
64753
  auth: "protected"
63569
64754
  }),
63570
64755
  setExtraDry: method(object({
63571
- deviceId: DeviceIdSchema,
64756
+ deviceId: DeviceIdSchema$1,
63572
64757
  on: boolean()
63573
64758
  }), _void(), {
63574
64759
  kind: "mutation",
63575
64760
  auth: "protected"
63576
64761
  }),
63577
64762
  setIntensiveZone: method(object({
63578
- deviceId: DeviceIdSchema,
64763
+ deviceId: DeviceIdSchema$1,
63579
64764
  on: boolean()
63580
64765
  }), _void(), {
63581
64766
  kind: "mutation",
63582
64767
  auth: "protected"
63583
64768
  }),
63584
64769
  setVarioSpeedPlus: method(object({
63585
- deviceId: DeviceIdSchema,
64770
+ deviceId: DeviceIdSchema$1,
63586
64771
  on: boolean()
63587
64772
  }), _void(), {
63588
64773
  kind: "mutation",
63589
64774
  auth: "protected"
63590
64775
  }),
63591
64776
  setChildLock: method(object({
63592
- deviceId: DeviceIdSchema,
64777
+ deviceId: DeviceIdSchema$1,
63593
64778
  on: boolean()
63594
64779
  }), _void(), {
63595
64780
  kind: "mutation",
@@ -63601,7 +64786,7 @@ var dishwasherCapability = {
63601
64786
  * its declared range / members.
63602
64787
  */
63603
64788
  setOption: method(object({
63604
- deviceId: DeviceIdSchema,
64789
+ deviceId: DeviceIdSchema$1,
63605
64790
  key: string().min(1),
63606
64791
  value: DishwasherValueSchema
63607
64792
  }), _void(), {
@@ -63609,7 +64794,7 @@ var dishwasherCapability = {
63609
64794
  auth: "protected"
63610
64795
  }),
63611
64796
  /** What this appliance can do, from its own description — the UI is derived from it. */
63612
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
64797
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
63613
64798
  },
63614
64799
  status: {
63615
64800
  schema: DishwasherStatusSchema,
@@ -66041,9 +67226,35 @@ var MediaInfoSchema = object({
66041
67226
  /** Optional cover-art / thumbnail URL. */
66042
67227
  imageUrl: string().optional()
66043
67228
  });
67229
+ /**
67230
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
67231
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
67232
+ * tracks what it last SENT; it moves only after a send was accepted, and a
67233
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
67234
+ * guess, never as a reading.
67235
+ */
67236
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
67237
+ /** The fields of a media-player status a driver can only guess. */
67238
+ var MediaPlayerAssumedFieldSchema = _enum([
67239
+ "state",
67240
+ "volumeLevel",
67241
+ "isMuted",
67242
+ "source"
67243
+ ]);
66044
67244
  var MediaPlayerStatusSchema = object({
66045
67245
  /** Playback lifecycle state. */
66046
67246
  state: MediaPlayerStateSchema,
67247
+ /**
67248
+ * Whether the state fields are readings or the driver's own bookkeeping.
67249
+ * A slice written before this field existed parses as `reported`.
67250
+ */
67251
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
67252
+ /**
67253
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
67254
+ * this is not empty; a field not listed here is a reading even then (power
67255
+ * from a running-state source, mute guessed from the last send).
67256
+ */
67257
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
66047
67258
  /** Volume as 0..100 inclusive. Null when the device has no volume
66048
67259
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
66049
67260
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -67360,7 +68571,8 @@ var TopologyNodeSchema = object({
67360
68571
  isHub: boolean(),
67361
68572
  isOnline: boolean(),
67362
68573
  cpuPercent: number(),
67363
- memoryPercent: number(),
68574
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
68575
+ memoryPercent: number().nullable(),
67364
68576
  uptime: number(),
67365
68577
  lastSeen: string(),
67366
68578
  localIps: array(string()).readonly(),
@@ -67971,6 +69183,14 @@ var PetFeederDeviceStatusSchema = _enum([
67971
69183
  "on_batteries"
67972
69184
  ]);
67973
69185
  var gramsPortion = number().int().min(4).max(200);
69186
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
69187
+ * hardware's own unit). The bounds are grams; a caller entering another unit
69188
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
69189
+ * plain grams — they are not readings in the kilogram base. */
69190
+ var gramsParam = gramsPortion.meta(paramMeta({
69191
+ quantity: "mass",
69192
+ unit: "g"
69193
+ }));
67974
69194
  var PetFeederStatusSchema = object({
67975
69195
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
67976
69196
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -67978,9 +69198,10 @@ var PetFeederStatusSchema = object({
67978
69198
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
67979
69199
  * the device has not reported yet. */
67980
69200
  foodLevel: number().nullable().meta(readingMeta("none")),
67981
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
67982
- * feeder's own units. Null when the model reports none. */
67983
- bowlFood: number().nullable().meta(readingMeta("none")),
69201
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
69202
+ * feeder with a bowl scale; null when the model has none or its reading is
69203
+ * of an unproven unit. */
69204
+ bowlFood: number().nullable().meta(readingMeta("mass")),
67984
69205
  /** True while the pet is eating (`state.eating`). */
67985
69206
  eating: boolean(),
67986
69207
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -68031,6 +69252,14 @@ var PetFeederStatusSchema = object({
68031
69252
  feedSound: boolean(),
68032
69253
  /** Speaker / prompt volume level (device-scaled integer). */
68033
69254
  volume: number().meta(readingMeta("none")),
69255
+ /** Portion (grams) of the last manual feed sent from a single-hopper
69256
+ * stepper — the default the next manual feed starts from. Persisted per
69257
+ * device by the provider; null until a manual feed has been sent. */
69258
+ lastFeedGrams: gramsPortion.nullable(),
69259
+ /** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
69260
+ lastFeedHopper1: gramsPortion.nullable(),
69261
+ /** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
69262
+ lastFeedHopper2: gramsPortion.nullable(),
68034
69263
  /** Ms epoch when the slice was last refreshed from the cloud. */
68035
69264
  lastFetchedAt: number()
68036
69265
  });
@@ -68050,9 +69279,9 @@ var petFeederCapability = {
68050
69279
  */
68051
69280
  feed: method(object({
68052
69281
  deviceId: number().int().nonnegative(),
68053
- grams: gramsPortion.optional(),
68054
- hopper1: gramsPortion.optional(),
68055
- hopper2: gramsPortion.optional()
69282
+ grams: gramsParam.optional(),
69283
+ hopper1: gramsParam.optional(),
69284
+ hopper2: gramsParam.optional()
68056
69285
  }), _void(), {
68057
69286
  kind: "mutation",
68058
69287
  auth: "protected"
@@ -68156,6 +69385,137 @@ var petFeederCapability = {
68156
69385
  }
68157
69386
  } }
68158
69387
  };
69388
+ /**
69389
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
69390
+ * circulating drinking fountain: how much water is in the tank, how much the
69391
+ * pet drank, whether the pump runs and on what duty cycle.
69392
+ *
69393
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
69394
+ * a container of loose entities (a pump switch, two number boxes, a "filter
69395
+ * changed" button), and an operator BUILDS the device from them — a composed
69396
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
69397
+ * method of ONE source entity, and every reading is a field derived from a
69398
+ * source entity; the provider-shaped interface below exists so the composed
69399
+ * device and any future native provider answer the same contract.
69400
+ *
69401
+ * Quantities are stored in the base unit of their kind (D944): volumes in
69402
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
69403
+ * reports millilitres or minutes is converted in the composition's expression,
69404
+ * never here. Every reading may be `null`: a source that is offline or does
69405
+ * not report is unknown, never zero.
69406
+ *
69407
+ * The filter is not a field of this cap: it is an item of the `consumables`
69408
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
69409
+ */
69410
+ var PetFountainStatusSchema = object({
69411
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
69412
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69413
+ /** Water in the tank, in litres. Null while the source has not reported. */
69414
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
69415
+ /** True when the tank is empty. Null while unknown. */
69416
+ empty: boolean().nullable(),
69417
+ /** True while the pump is running. Null while unknown. */
69418
+ pumpRunning: boolean().nullable(),
69419
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
69420
+ pumpIntervalEnabled: boolean().nullable(),
69421
+ /** How long the pump runs per cycle, in seconds. */
69422
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
69423
+ /** How long the pump rests per cycle, in seconds. */
69424
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
69425
+ /** Water drunk in the last drink, in litres. */
69426
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
69427
+ /** Water drunk in the current hour, in litres. */
69428
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
69429
+ /** Water drunk over the last 24 hours (rolling), in litres. */
69430
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
69431
+ /** Water drunk since local midnight, in litres. */
69432
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
69433
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
69434
+ error: string().nullable(),
69435
+ /** Ms epoch when the slice was last refreshed. */
69436
+ lastFetchedAt: number()
69437
+ });
69438
+ var DeviceIdSchema = number().int().nonnegative();
69439
+ var MinutesSchema = number().int().min(0);
69440
+ var petFountainCapability = {
69441
+ name: "pet-fountain",
69442
+ scope: "device",
69443
+ deviceNative: true,
69444
+ mode: "singleton",
69445
+ deviceTypes: [DeviceType.PetFountain],
69446
+ methods: {
69447
+ /** Start or stop the pump. */
69448
+ setPump: method(object({
69449
+ deviceId: DeviceIdSchema,
69450
+ on: boolean()
69451
+ }), _void(), {
69452
+ kind: "mutation",
69453
+ auth: "protected"
69454
+ }),
69455
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
69456
+ setPumpInterval: method(object({
69457
+ deviceId: DeviceIdSchema,
69458
+ on: boolean()
69459
+ }), _void(), {
69460
+ kind: "mutation",
69461
+ auth: "protected"
69462
+ }),
69463
+ /** Set how many minutes the pump runs per cycle. */
69464
+ setPumpOnMinutes: method(object({
69465
+ deviceId: DeviceIdSchema,
69466
+ minutes: MinutesSchema
69467
+ }), _void(), {
69468
+ kind: "mutation",
69469
+ auth: "protected"
69470
+ }),
69471
+ /** Set how many minutes the pump rests per cycle. */
69472
+ setPumpOffMinutes: method(object({
69473
+ deviceId: DeviceIdSchema,
69474
+ minutes: MinutesSchema
69475
+ }), _void(), {
69476
+ kind: "mutation",
69477
+ auth: "protected"
69478
+ }),
69479
+ /** Clear the rolling daily consumption counters on the device. */
69480
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69481
+ kind: "mutation",
69482
+ auth: "protected"
69483
+ })
69484
+ },
69485
+ status: {
69486
+ schema: PetFountainStatusSchema,
69487
+ kind: "push"
69488
+ },
69489
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
69490
+ runtimeState: PetFountainStatusSchema,
69491
+ /**
69492
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
69493
+ *
69494
+ * See `RuntimeStateDurability`. Enforced by
69495
+ * `scripts/check-runtime-state-durability.ts`.
69496
+ */
69497
+ durability: "session",
69498
+ /** History (D850): recorded series. */
69499
+ history: { fields: {
69500
+ waterLevel: {
69501
+ kind: "gauge",
69502
+ unit: "%",
69503
+ deadband: 1
69504
+ },
69505
+ waterVolume: {
69506
+ kind: "gauge",
69507
+ unit: "L"
69508
+ },
69509
+ todayDrink: {
69510
+ kind: "gauge",
69511
+ unit: "L"
69512
+ },
69513
+ last24hDrink: {
69514
+ kind: "gauge",
69515
+ unit: "L"
69516
+ }
69517
+ } }
69518
+ };
68159
69519
  var VehicleSchema = object({
68160
69520
  id: string(),
68161
69521
  name: string(),
@@ -74153,9 +75513,98 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
74153
75513
  kind: "mutation",
74154
75514
  auth: "admin"
74155
75515
  });
75516
+ var btn = (idSuffix, row, col, init) => ({
75517
+ idSuffix,
75518
+ row,
75519
+ col,
75520
+ rowSpan: 1,
75521
+ colSpan: 1,
75522
+ ...init
75523
+ });
75524
+ function rocker(id, name, it, frame, title, buttons, size) {
75525
+ return {
75526
+ id,
75527
+ name,
75528
+ nameI18n: {
75529
+ en: name,
75530
+ it
75531
+ },
75532
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
75533
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
75534
+ size,
75535
+ group: title === null ? { frame } : {
75536
+ frame,
75537
+ title
75538
+ },
75539
+ buttons
75540
+ };
75541
+ }
75542
+ btn("power", 0, 0, {
75543
+ role: "power",
75544
+ shape: "round",
75545
+ tone: "danger"
75546
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
75547
+ role: "ok",
75548
+ shape: "round",
75549
+ tone: "accent"
75550
+ }), btn("right", 1, 2, { role: "right" }), btn("down", 2, 1, { role: "down" }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
75551
+ role: "volume_up",
75552
+ shape: "pill"
75553
+ }), btn("down", 1, 0, {
75554
+ role: "volume_down",
75555
+ shape: "pill"
75556
+ })], {
75557
+ rows: 2,
75558
+ cols: 1
75559
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
75560
+ role: "channel_up",
75561
+ shape: "pill"
75562
+ }), btn("down", 1, 0, {
75563
+ role: "channel_down",
75564
+ shape: "pill"
75565
+ })], {
75566
+ rows: 2,
75567
+ cols: 1
75568
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
75569
+ role: "custom:minus",
75570
+ label: "−",
75571
+ shape: "pill"
75572
+ }), btn("plus", 0, 1, {
75573
+ role: "custom:plus",
75574
+ label: "+",
75575
+ shape: "pill"
75576
+ })], {
75577
+ rows: 1,
75578
+ cols: 2
75579
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
75580
+ role: "custom:color_red",
75581
+ tone: "red"
75582
+ }), btn("green", 0, 1, {
75583
+ role: "custom:color_green",
75584
+ tone: "green"
75585
+ }), btn("yellow", 0, 2, {
75586
+ role: "custom:color_yellow",
75587
+ tone: "yellow"
75588
+ }), btn("blue", 0, 3, {
75589
+ role: "custom:color_blue",
75590
+ tone: "blue"
75591
+ }), btn("rewind", 0, 0, { role: "rewind" }), btn("play", 0, 1, { role: "play" }), btn("pause", 0, 2, { role: "pause" }), btn("forward", 0, 3, { role: "fast_forward" }), btn("stop", 0, 4, { role: "stop" }), [
75592
+ 1,
75593
+ 2,
75594
+ 3,
75595
+ 4
75596
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
75597
+ discriminatedUnion("kind", [object({
75598
+ kind: literal("raw"),
75599
+ timings: array(number().int()),
75600
+ settings: TransmitSettingsSchema.optional()
75601
+ }), object({
75602
+ kind: literal("pronto"),
75603
+ code: string()
75604
+ })]);
74156
75605
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
74157
75606
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
74158
- 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;
75607
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
74159
75608
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
74160
75609
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
74161
75610
  new Set(CAP_NAMES);
@@ -74559,6 +76008,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
74559
76008
  notifier: notifierCapability,
74560
76009
  numericSensor: numericSensorCapability,
74561
76010
  petFeeder: petFeederCapability,
76011
+ petFountain: petFountainCapability,
74562
76012
  powerMeter: powerMeterCapability,
74563
76013
  presence: presenceCapability,
74564
76014
  pressureSensor: pressureSensorCapability,
@@ -75326,7 +76776,7 @@ var DeclaredDevices = class {
75326
76776
  return removed;
75327
76777
  }
75328
76778
  };
75329
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
76779
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
75330
76780
  new Set(Object.values(DeviceType));
75331
76781
  Object.freeze({
75332
76782
  "accessories.getStatus": {
@@ -77261,6 +78711,13 @@ Object.freeze({
77261
78711
  access: "create",
77262
78712
  role: "admin"
77263
78713
  },
78714
+ "deviceExport.exposeEntity": {
78715
+ capName: "device-export",
78716
+ capScope: "system",
78717
+ addonId: null,
78718
+ access: "create",
78719
+ role: "admin"
78720
+ },
77264
78721
  "deviceExport.getDeviceLiveContribution": {
77265
78722
  capName: "device-export",
77266
78723
  capScope: "system",
@@ -77289,6 +78746,13 @@ Object.freeze({
77289
78746
  access: "view",
77290
78747
  role: "admin"
77291
78748
  },
78749
+ "deviceExport.listExposedEntities": {
78750
+ capName: "device-export",
78751
+ capScope: "system",
78752
+ addonId: null,
78753
+ access: "view",
78754
+ role: "admin"
78755
+ },
77292
78756
  "deviceExport.listSupportedDeviceKinds": {
77293
78757
  capName: "device-export",
77294
78758
  capScope: "system",
@@ -77296,6 +78760,13 @@ Object.freeze({
77296
78760
  access: "view",
77297
78761
  role: "admin"
77298
78762
  },
78763
+ "deviceExport.listSupportedEntityKinds": {
78764
+ capName: "device-export",
78765
+ capScope: "system",
78766
+ addonId: null,
78767
+ access: "view",
78768
+ role: "admin"
78769
+ },
77299
78770
  "deviceExport.unexposeDevice": {
77300
78771
  capName: "device-export",
77301
78772
  capScope: "system",
@@ -77303,6 +78774,13 @@ Object.freeze({
77303
78774
  access: "create",
77304
78775
  role: "admin"
77305
78776
  },
78777
+ "deviceExport.unexposeEntity": {
78778
+ capName: "device-export",
78779
+ capScope: "system",
78780
+ addonId: null,
78781
+ access: "create",
78782
+ role: "admin"
78783
+ },
77306
78784
  "deviceExtension.listCandidates": {
77307
78785
  capName: "device-extension",
77308
78786
  capScope: "device",
@@ -80124,6 +81602,13 @@ Object.freeze({
80124
81602
  access: "create",
80125
81603
  role: "admin"
80126
81604
  },
81605
+ "nodeRemovalHook.onNodeRemoved": {
81606
+ capName: "node-removal-hook",
81607
+ capScope: "system",
81608
+ addonId: null,
81609
+ access: "create",
81610
+ role: "admin"
81611
+ },
80127
81612
  "nodes.clusterAddonStatus": {
80128
81613
  capName: "nodes",
80129
81614
  capScope: "system",
@@ -80761,6 +82246,48 @@ Object.freeze({
80761
82246
  access: "create",
80762
82247
  role: "viewer"
80763
82248
  },
82249
+ "petFountain.getStatus": {
82250
+ capName: "pet-fountain",
82251
+ capScope: "device",
82252
+ addonId: null,
82253
+ access: "view",
82254
+ role: "viewer"
82255
+ },
82256
+ "petFountain.resetDailyConsumption": {
82257
+ capName: "pet-fountain",
82258
+ capScope: "device",
82259
+ addonId: null,
82260
+ access: "delete",
82261
+ role: "viewer"
82262
+ },
82263
+ "petFountain.setPump": {
82264
+ capName: "pet-fountain",
82265
+ capScope: "device",
82266
+ addonId: null,
82267
+ access: "create",
82268
+ role: "viewer"
82269
+ },
82270
+ "petFountain.setPumpInterval": {
82271
+ capName: "pet-fountain",
82272
+ capScope: "device",
82273
+ addonId: null,
82274
+ access: "create",
82275
+ role: "viewer"
82276
+ },
82277
+ "petFountain.setPumpOffMinutes": {
82278
+ capName: "pet-fountain",
82279
+ capScope: "device",
82280
+ addonId: null,
82281
+ access: "create",
82282
+ role: "viewer"
82283
+ },
82284
+ "petFountain.setPumpOnMinutes": {
82285
+ capName: "pet-fountain",
82286
+ capScope: "device",
82287
+ addonId: null,
82288
+ access: "create",
82289
+ role: "viewer"
82290
+ },
80764
82291
  "pipelineAnalytics.addRetrainFrameFromRecording": {
80765
82292
  capName: "pipeline-analytics",
80766
82293
  capScope: "device",
@@ -83015,6 +84542,48 @@ Object.freeze({
83015
84542
  access: "create",
83016
84543
  role: "admin"
83017
84544
  },
84545
+ "remote.getConfig": {
84546
+ capName: "remote",
84547
+ capScope: "system",
84548
+ addonId: null,
84549
+ access: "view",
84550
+ role: "admin"
84551
+ },
84552
+ "remote.listCommands": {
84553
+ capName: "remote",
84554
+ capScope: "system",
84555
+ addonId: null,
84556
+ access: "view",
84557
+ role: "admin"
84558
+ },
84559
+ "remote.sendCommand": {
84560
+ capName: "remote",
84561
+ capScope: "system",
84562
+ addonId: null,
84563
+ access: "create",
84564
+ role: "admin"
84565
+ },
84566
+ "remote.sendMediaCommand": {
84567
+ capName: "remote",
84568
+ capScope: "system",
84569
+ addonId: null,
84570
+ access: "create",
84571
+ role: "admin"
84572
+ },
84573
+ "remote.setConfig": {
84574
+ capName: "remote",
84575
+ capScope: "system",
84576
+ addonId: null,
84577
+ access: "create",
84578
+ role: "admin"
84579
+ },
84580
+ "remoteLayoutComponents.listComponents": {
84581
+ capName: "remote-layout-components",
84582
+ capScope: "system",
84583
+ addonId: null,
84584
+ access: "view",
84585
+ role: "admin"
84586
+ },
83018
84587
  "runningState.getStatus": {
83019
84588
  capName: "running-state",
83020
84589
  capScope: "device",
@@ -83127,6 +84696,13 @@ Object.freeze({
83127
84696
  access: "create",
83128
84697
  role: "admin"
83129
84698
  },
84699
+ "serverManagement.getLaunchOverrides": {
84700
+ capName: "server-management",
84701
+ capScope: "system",
84702
+ addonId: null,
84703
+ access: "view",
84704
+ role: "admin"
84705
+ },
83130
84706
  "serverManagement.getServerPackageStatus": {
83131
84707
  capName: "server-management",
83132
84708
  capScope: "system",
@@ -83134,6 +84710,13 @@ Object.freeze({
83134
84710
  access: "view",
83135
84711
  role: "admin"
83136
84712
  },
84713
+ "serverManagement.installDesktopUpdate": {
84714
+ capName: "server-management",
84715
+ capScope: "system",
84716
+ addonId: null,
84717
+ access: "create",
84718
+ role: "admin"
84719
+ },
83137
84720
  "serverManagement.restartServer": {
83138
84721
  capName: "server-management",
83139
84722
  capScope: "system",
@@ -83148,6 +84731,13 @@ Object.freeze({
83148
84731
  access: "create",
83149
84732
  role: "admin"
83150
84733
  },
84734
+ "serverManagement.setLaunchOverrides": {
84735
+ capName: "server-management",
84736
+ capScope: "system",
84737
+ addonId: null,
84738
+ access: "create",
84739
+ role: "admin"
84740
+ },
83151
84741
  "settingsStore.aggregate": {
83152
84742
  capName: "settings-store",
83153
84743
  capScope: "system",
@@ -84205,6 +85795,41 @@ Object.freeze({
84205
85795
  access: "view",
84206
85796
  role: "viewer"
84207
85797
  },
85798
+ "template.create": {
85799
+ capName: "template",
85800
+ capScope: "system",
85801
+ addonId: null,
85802
+ access: "create",
85803
+ role: "admin"
85804
+ },
85805
+ "template.delete": {
85806
+ capName: "template",
85807
+ capScope: "system",
85808
+ addonId: null,
85809
+ access: "delete",
85810
+ role: "admin"
85811
+ },
85812
+ "template.get": {
85813
+ capName: "template",
85814
+ capScope: "system",
85815
+ addonId: null,
85816
+ access: "view",
85817
+ role: "admin"
85818
+ },
85819
+ "template.list": {
85820
+ capName: "template",
85821
+ capScope: "system",
85822
+ addonId: null,
85823
+ access: "view",
85824
+ role: "admin"
85825
+ },
85826
+ "template.update": {
85827
+ capName: "template",
85828
+ capScope: "system",
85829
+ addonId: null,
85830
+ access: "create",
85831
+ role: "admin"
85832
+ },
84208
85833
  "terminalSession.adoptLegacyMonitor": {
84209
85834
  capName: "terminal-session",
84210
85835
  capScope: "system",
@@ -87002,7 +88627,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87002
88627
  min: 4,
87003
88628
  max: 200,
87004
88629
  step: 1,
87005
- unit: "g"
88630
+ unit: "g",
88631
+ quantity: "mass"
87006
88632
  },
87007
88633
  optional: true
87008
88634
  }]
@@ -87021,7 +88647,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87021
88647
  min: 4,
87022
88648
  max: 200,
87023
88649
  step: 1,
87024
- unit: "g"
88650
+ unit: "g",
88651
+ quantity: "mass"
87025
88652
  },
87026
88653
  optional: true
87027
88654
  }, {
@@ -87032,7 +88659,8 @@ var APPLIANCE_CAP_VOCABULARY = {
87032
88659
  min: 4,
87033
88660
  max: 200,
87034
88661
  step: 1,
87035
- unit: "g"
88662
+ unit: "g",
88663
+ quantity: "mass"
87036
88664
  },
87037
88665
  optional: true
87038
88666
  }]
@@ -87173,6 +88801,100 @@ var APPLIANCE_CAP_VOCABULARY = {
87173
88801
  }
87174
88802
  ]
87175
88803
  },
88804
+ "pet-fountain": {
88805
+ scope: "device",
88806
+ noun: t("Pet fountain", "Fontana per animali"),
88807
+ actions: [
88808
+ {
88809
+ id: "petFountain.pumpOn",
88810
+ method: "setPump",
88811
+ label: t("Pump on", "Pompa accesa"),
88812
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
88813
+ icon: "play",
88814
+ args: { on: true }
88815
+ },
88816
+ {
88817
+ id: "petFountain.pumpOff",
88818
+ method: "setPump",
88819
+ label: t("Pump off", "Pompa spenta"),
88820
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
88821
+ icon: "square",
88822
+ args: { on: false }
88823
+ },
88824
+ {
88825
+ id: "petFountain.intervalOn",
88826
+ method: "setPumpInterval",
88827
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
88828
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
88829
+ icon: "timer",
88830
+ args: { on: true }
88831
+ },
88832
+ {
88833
+ id: "petFountain.intervalOff",
88834
+ method: "setPumpInterval",
88835
+ label: t("Pump cycle off", "Ciclo pompa spento"),
88836
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
88837
+ icon: "timer-off",
88838
+ args: { on: false }
88839
+ },
88840
+ {
88841
+ id: "petFountain.pumpOnMinutes",
88842
+ method: "setPumpOnMinutes",
88843
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
88844
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
88845
+ icon: "timer",
88846
+ openArgs: [{
88847
+ key: "minutes",
88848
+ label: t("Minutes", "Minuti"),
88849
+ offer: {
88850
+ kind: "number",
88851
+ min: 0,
88852
+ step: 1,
88853
+ unit: "min"
88854
+ }
88855
+ }]
88856
+ },
88857
+ {
88858
+ id: "petFountain.pumpOffMinutes",
88859
+ method: "setPumpOffMinutes",
88860
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
88861
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
88862
+ icon: "timer-off",
88863
+ openArgs: [{
88864
+ key: "minutes",
88865
+ label: t("Minutes", "Minuti"),
88866
+ offer: {
88867
+ kind: "number",
88868
+ min: 0,
88869
+ step: 1,
88870
+ unit: "min"
88871
+ }
88872
+ }]
88873
+ },
88874
+ {
88875
+ id: "petFountain.resetDailyConsumption",
88876
+ method: "resetDailyConsumption",
88877
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
88878
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
88879
+ icon: "rotate-ccw"
88880
+ }
88881
+ ],
88882
+ state: [{
88883
+ kind: "boolean",
88884
+ field: "empty",
88885
+ label: t("Tank empty", "Serbatoio vuoto"),
88886
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
88887
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
88888
+ icon: "droplet"
88889
+ }, {
88890
+ kind: "numeric",
88891
+ field: "waterLevel",
88892
+ quantity: t("the water level", "il livello dell’acqua"),
88893
+ label: t("Water level", "Livello dell’acqua"),
88894
+ unit: "%",
88895
+ icon: "droplets"
88896
+ }]
88897
+ },
87176
88898
  "media-player": {
87177
88899
  scope: "device",
87178
88900
  noun: t("Media player", "Lettore multimediale"),
@@ -88997,6 +90719,7 @@ var SYSTEM_CAP_VOCABULARY = {
88997
90719
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
88998
90720
  "device-provider": system(INTERNAL),
88999
90721
  "device-removal-hook": system(INTERNAL),
90722
+ "node-removal-hook": system(INTERNAL),
89000
90723
  "device-state": system("the runtime-state mirror every trigger already reads through"),
89001
90724
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
89002
90725
  "embedding-encoder": system(ML),
@@ -89036,6 +90759,8 @@ var SYSTEM_CAP_VOCABULARY = {
89036
90759
  "recording-archive": system(STORAGE),
89037
90760
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
89038
90761
  records: system(INTERNAL),
90762
+ remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
90763
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
89039
90764
  "server-management": system(CLUSTER),
89040
90765
  "settings-store": system(INTERNAL),
89041
90766
  "smtp-provider": system(INTERNAL),
@@ -89048,6 +90773,7 @@ var SYSTEM_CAP_VOCABULARY = {
89048
90773
  "storage-provider": system(STORAGE),
89049
90774
  "stream-broker": system(PIPELINE),
89050
90775
  system: system(CLUSTER),
90776
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
89051
90777
  "terminal-session": system(CLUSTER),
89052
90778
  toast: system(UI_PLUMBING),
89053
90779
  training: system(ML),
@@ -89107,7 +90833,7 @@ function projectAction(cap, action) {
89107
90833
  };
89108
90834
  }
89109
90835
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
89110
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
90836
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
89111
90837
  DeviceFeature.BatteryOperated;
89112
90838
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
89113
90839
  var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
@@ -90658,6 +92384,36 @@ Object.freeze({
90658
92384
  form: "single",
90659
92385
  optional: false
90660
92386
  }],
92387
+ "petFountain.getStatus": [{
92388
+ name: "deviceId",
92389
+ form: "single",
92390
+ optional: false
92391
+ }],
92392
+ "petFountain.resetDailyConsumption": [{
92393
+ name: "deviceId",
92394
+ form: "single",
92395
+ optional: false
92396
+ }],
92397
+ "petFountain.setPump": [{
92398
+ name: "deviceId",
92399
+ form: "single",
92400
+ optional: false
92401
+ }],
92402
+ "petFountain.setPumpInterval": [{
92403
+ name: "deviceId",
92404
+ form: "single",
92405
+ optional: false
92406
+ }],
92407
+ "petFountain.setPumpOffMinutes": [{
92408
+ name: "deviceId",
92409
+ form: "single",
92410
+ optional: false
92411
+ }],
92412
+ "petFountain.setPumpOnMinutes": [{
92413
+ name: "deviceId",
92414
+ form: "single",
92415
+ optional: false
92416
+ }],
90661
92417
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
90662
92418
  name: "deviceId",
90663
92419
  form: "single",
@@ -91474,6 +93230,31 @@ Object.freeze({
91474
93230
  form: "single",
91475
93231
  optional: true
91476
93232
  }],
93233
+ "remote.getConfig": [{
93234
+ name: "deviceId",
93235
+ form: "single",
93236
+ optional: false
93237
+ }],
93238
+ "remote.listCommands": [{
93239
+ name: "deviceId",
93240
+ form: "single",
93241
+ optional: false
93242
+ }],
93243
+ "remote.sendCommand": [{
93244
+ name: "deviceId",
93245
+ form: "single",
93246
+ optional: false
93247
+ }],
93248
+ "remote.sendMediaCommand": [{
93249
+ name: "deviceId",
93250
+ form: "single",
93251
+ optional: false
93252
+ }],
93253
+ "remote.setConfig": [{
93254
+ name: "deviceId",
93255
+ form: "single",
93256
+ optional: false
93257
+ }],
91477
93258
  "runningState.getStatus": [{
91478
93259
  name: "deviceId",
91479
93260
  form: "single",
@@ -95029,8 +96810,6 @@ var NC_TEMPLATE_VARS = [
95029
96810
  * delete it from here then, and the served catalog names it again.
95030
96811
  */
95031
96812
  var NC_TEMPLATE_HELD_BACK_SYSTEM_EVENT_KINDS = [
95032
- "node-restarting",
95033
- "node-updating",
95034
96813
  "alert-critical",
95035
96814
  "alert-error",
95036
96815
  "alert-critical-resolved",
@@ -96843,14 +98622,6 @@ function armRemovalReconcile(opts) {
96843
98622
  timer.unref?.();
96844
98623
  return () => clearInterval(timer);
96845
98624
  }
96846
- discriminatedUnion("kind", [object({
96847
- kind: literal("raw"),
96848
- timings: array(number().int()),
96849
- settings: TransmitSettingsSchema.optional()
96850
- }), object({
96851
- kind: literal("pronto"),
96852
- code: string()
96853
- })]);
96854
98625
  object({
96855
98626
  manufacturer: string().optional(),
96856
98627
  supportedModels: array(string()).optional(),