@camstack/addon-import-alexa 0.2.165 → 0.2.167

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.
package/dist/addon.mjs CHANGED
@@ -5247,6 +5247,14 @@ function object(shape, params) {
5247
5247
  ...normalizeParams(params)
5248
5248
  });
5249
5249
  }
5250
+ function strictObject(shape, params) {
5251
+ return new ZodObject({
5252
+ type: "object",
5253
+ shape,
5254
+ catchall: never(),
5255
+ ...normalizeParams(params)
5256
+ });
5257
+ }
5250
5258
  function looseObject(shape, params) {
5251
5259
  return new ZodObject({
5252
5260
  type: "object",
@@ -5626,7 +5634,7 @@ var ZodIssueCode = {
5626
5634
  var ZodFirstPartyTypeKind;
5627
5635
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5628
5636
  //#endregion
5629
- //#region ../types/dist/sleep-Crlmu_tO.mjs
5637
+ //#region ../types/dist/sleep-6eG9C8Xl.mjs
5630
5638
  /**
5631
5639
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5632
5640
  * window to float samples (D455).
@@ -8861,9 +8869,76 @@ function optionalMethod(input, output, options) {
8861
8869
  function event(data) {
8862
8870
  return { data };
8863
8871
  }
8872
+ /**
8873
+ * `addon-pages-source` — collection cap exposing per-provider raw page
8874
+ * declarations. Every addon that contributes a UI page registers a
8875
+ * provider here. The hub-side singleton aggregator (`addon-pages` cap,
8876
+ * see `addon-pages.cap.ts`) walks this collection, stamps versioned
8877
+ * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
8878
+ * that admin-ui consumes.
8879
+ *
8880
+ * The split exists because the public listing has a different output
8881
+ * shape than the per-provider raw declarations, and we want both ends
8882
+ * to flow through codegen instead of relying on a hand-written wrapper.
8883
+ */
8884
+ var AddonPageDeclarationSchema = object({
8885
+ id: string(),
8886
+ label: string(),
8887
+ icon: string(),
8888
+ path: string(),
8889
+ /**
8890
+ * Module Federation remote name — must match the `name` field on the
8891
+ * page addon's `federation()` plugin config. Used by admin-ui's
8892
+ * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
8893
+ * Conventionally `addon_<id>_page` (snake_case; MF names cannot
8894
+ * contain hyphens).
8895
+ */
8896
+ remoteName: string(),
8897
+ /**
8898
+ * Bundle filename inside the addon's `dist/` dir served at
8899
+ * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
8900
+ * is always `'remoteEntry.js'`; the value is kept on the metadata so
8901
+ * the static-file route can compute an mtime-based cache-buster URL
8902
+ * without a separate filesystem stat.
8903
+ */
8904
+ bundle: string(),
8905
+ /**
8906
+ * Sidebar section this page docks into. Well-known ids: `'detection'`,
8907
+ * `'cluster'`, `'administration'` — the page renders inside that group.
8908
+ * Any OTHER string creates (or joins) a custom section rendered after
8909
+ * the built-in groups; its label comes from `sectionLabel` (first
8910
+ * declaration wins), falling back to the id. Absent → the legacy
8911
+ * "Addon Pages" group.
8912
+ */
8913
+ section: string().optional(),
8914
+ /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
8915
+ sectionLabel: string().optional()
8916
+ });
8917
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
8864
8918
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8865
8919
  var VersionOutputSchema$1 = object({ version: string() });
8866
8920
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8921
+ /**
8922
+ * The pipeline step-input shape and the run-result bridge, in a module that
8923
+ * imports nothing but zod and types.
8924
+ *
8925
+ * WHY A SEPARATE FILE (addon footprint, D28): both were declared in
8926
+ * `pipeline-executor.cap.ts`, whose import evaluates the whole executor schema
8927
+ * graph. An addon that only needs these two (the benchmark's `runPipelineTest`
8928
+ * action) paid ~130 MB of heap at load. `pipeline-executor.cap.ts` re-exports
8929
+ * them, so every existing import keeps working; addon server code takes them
8930
+ * from `@camstack/types/addon`.
8931
+ */
8932
+ var PipelineStepInputSchema = lazy(() => object({
8933
+ addonId: string(),
8934
+ modelId: string().optional(),
8935
+ enabled: boolean().default(true),
8936
+ children: array(PipelineStepInputSchema).optional(),
8937
+ settings: record(string(), unknown()).optional(),
8938
+ jumpDeviceKey: string().optional()
8939
+ }));
8940
+ /** Typed bridge to the recursive `FrameResult` graph; validation is structural. */
8941
+ var PipelineRunResultBridge = custom();
8867
8942
  var StaticDirOutputSchema = object({ staticDir: string() });
8868
8943
  var VersionOutputSchema = object({ version: string() });
8869
8944
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9019,6 +9094,7 @@ var CAP_NAMES = Object.freeze([
9019
9094
  "image-settings",
9020
9095
  "integrations",
9021
9096
  "intercom",
9097
+ "ir-transmitter",
9022
9098
  "keychain",
9023
9099
  "keychain-admin",
9024
9100
  "lawn-mower-control",
@@ -9047,6 +9123,7 @@ var CAP_NAMES = Object.freeze([
9047
9123
  "network-link",
9048
9124
  "network-quality",
9049
9125
  "node-certificates",
9126
+ "node-removal-hook",
9050
9127
  "nodes",
9051
9128
  "notification-output",
9052
9129
  "notification-rules",
@@ -9057,6 +9134,7 @@ var CAP_NAMES = Object.freeze([
9057
9134
  "osd",
9058
9135
  "osd-manager",
9059
9136
  "pet-feeder",
9137
+ "pet-fountain",
9060
9138
  "pipeline-analytics",
9061
9139
  "pipeline-executor",
9062
9140
  "pipeline-orchestrator",
@@ -9077,6 +9155,7 @@ var CAP_NAMES = Object.freeze([
9077
9155
  "recording-onboard",
9078
9156
  "recording-signal",
9079
9157
  "records",
9158
+ "remote",
9080
9159
  "running-state",
9081
9160
  "scene-monitor",
9082
9161
  "script-runner",
@@ -9100,6 +9179,7 @@ var CAP_NAMES = Object.freeze([
9100
9179
  "system",
9101
9180
  "tamper",
9102
9181
  "temperature-sensor",
9182
+ "template",
9103
9183
  "terminal-session",
9104
9184
  "toast",
9105
9185
  "training",
@@ -10040,6 +10120,11 @@ Object.freeze({
10040
10120
  "standby"
10041
10121
  ]
10042
10122
  },
10123
+ {
10124
+ "path": "stateSource",
10125
+ "valueType": "enum",
10126
+ "enumValues": ["reported", "assumed"]
10127
+ },
10043
10128
  {
10044
10129
  "path": "volumeLevel",
10045
10130
  "valueType": "number"
@@ -10266,6 +10351,11 @@ Object.freeze({
10266
10351
  "path": "batteryPower",
10267
10352
  "valueType": "number"
10268
10353
  },
10354
+ {
10355
+ "path": "powerSource",
10356
+ "valueType": "enum",
10357
+ "enumValues": ["mains", "battery"]
10358
+ },
10269
10359
  {
10270
10360
  "path": "desiccantLeftDays",
10271
10361
  "valueType": "number"
@@ -10310,6 +10400,68 @@ Object.freeze({
10310
10400
  {
10311
10401
  "path": "volume",
10312
10402
  "valueType": "number"
10403
+ },
10404
+ {
10405
+ "path": "lastFeedGrams",
10406
+ "valueType": "number"
10407
+ },
10408
+ {
10409
+ "path": "lastFeedHopper1",
10410
+ "valueType": "number"
10411
+ },
10412
+ {
10413
+ "path": "lastFeedHopper2",
10414
+ "valueType": "number"
10415
+ }
10416
+ ],
10417
+ "pet-fountain": [
10418
+ {
10419
+ "path": "waterLevel",
10420
+ "valueType": "number"
10421
+ },
10422
+ {
10423
+ "path": "waterVolume",
10424
+ "valueType": "number"
10425
+ },
10426
+ {
10427
+ "path": "empty",
10428
+ "valueType": "boolean"
10429
+ },
10430
+ {
10431
+ "path": "pumpRunning",
10432
+ "valueType": "boolean"
10433
+ },
10434
+ {
10435
+ "path": "pumpIntervalEnabled",
10436
+ "valueType": "boolean"
10437
+ },
10438
+ {
10439
+ "path": "pumpOnTime",
10440
+ "valueType": "number"
10441
+ },
10442
+ {
10443
+ "path": "pumpOffTime",
10444
+ "valueType": "number"
10445
+ },
10446
+ {
10447
+ "path": "lastDrink",
10448
+ "valueType": "number"
10449
+ },
10450
+ {
10451
+ "path": "currentHourDrink",
10452
+ "valueType": "number"
10453
+ },
10454
+ {
10455
+ "path": "last24hDrink",
10456
+ "valueType": "number"
10457
+ },
10458
+ {
10459
+ "path": "todayDrink",
10460
+ "valueType": "number"
10461
+ },
10462
+ {
10463
+ "path": "error",
10464
+ "valueType": "string"
10313
10465
  }
10314
10466
  ],
10315
10467
  "power-meter": [
@@ -10849,7 +11001,7 @@ object({
10849
11001
  params: record(string(), union([string(), number()])).optional()
10850
11002
  });
10851
11003
  //#endregion
10852
- //#region ../types/dist/err-msg-DIorGYeC.mjs
11004
+ //#region ../types/dist/err-msg-DWXWc0Di.mjs
10853
11005
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10854
11006
  DeviceType["Camera"] = "camera";
10855
11007
  DeviceType["Hub"] = "hub";
@@ -10941,6 +11093,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10941
11093
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
10942
11094
  * integrations sharing the same food/desiccant/hopper surface. */
10943
11095
  DeviceType["PetFeeder"] = "pet-feeder";
11096
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
11097
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
11098
+ * of a container and is BUILT as a composed device (D967). */
11099
+ DeviceType["PetFountain"] = "pet-fountain";
10944
11100
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10945
11101
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10946
11102
  * Sources: native Bosch Home Connect, reusable by similar integrations.
@@ -11195,6 +11351,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11195
11351
  /** Fallback when the notifier service name doesn't match a known
11196
11352
  * pattern. */
11197
11353
  DeviceRole["GenericNotifier"] = "generic-notifier";
11354
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
11355
+ * cap. A `DeviceType.Generic` device. */
11356
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
11198
11357
  return DeviceRole;
11199
11358
  }({});
11200
11359
  /**
@@ -11208,7 +11367,7 @@ function errMsg(err) {
11208
11367
  return String(err);
11209
11368
  }
11210
11369
  //#endregion
11211
- //#region ../types/dist/composition-DwA0uVW5.mjs
11370
+ //#region ../types/dist/composition-kSuOifuG.mjs
11212
11371
  /**
11213
11372
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11214
11373
  *
@@ -11256,11 +11415,11 @@ function errMsg(err) {
11256
11415
  */
11257
11416
  /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
11258
11417
  var FFMPEG_RELEASE = "8.1.2-5";
11259
- var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11418
+ var BASE$1 = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
11260
11419
  function portable(target) {
11261
- return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11420
+ return `${BASE$1}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
11262
11421
  }
11263
- portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
11422
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE$1}${FFMPEG_RELEASE}`;
11264
11423
  /**
11265
11424
  * THE table "which boolean of this device type counts as HIGH", and the
11266
11425
  * pure evaluator of its EDGE.
@@ -12293,12 +12452,28 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12293
12452
  ])
12294
12453
  })
12295
12454
  ]);
12296
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12455
+ var forwardArgUnion = union([
12456
+ string(),
12457
+ number(),
12458
+ boolean(),
12459
+ strictObject({ ["$input"]: string().min(1) }),
12460
+ record(string().min(1), lazy(() => forwardArgUnion))
12461
+ ]);
12462
+ var CompositionForwardArgSchema = forwardArgUnion;
12463
+ /**
12464
+ * `forward` (D659, amended by D967): one method of a BUILT capability is one
12465
+ * call of `cap.method` on `source`. Without `args` the built method's input
12466
+ * passes through minus `deviceId`; with `args` the template IS the source's
12467
+ * whole input. `deviceId` is always the source's own, never an argument.
12468
+ */
12469
+ var CompositionForwardCommandSchema = object({
12297
12470
  kind: literal("forward"),
12298
12471
  source: CompositionSourceRefSchema,
12299
12472
  cap: string().min(1),
12300
- method: string().min(1)
12301
- }), CompositionCodeSourceSchema]);
12473
+ method: string().min(1),
12474
+ args: record(string().min(1), CompositionForwardArgSchema).optional()
12475
+ });
12476
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12302
12477
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12303
12478
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12304
12479
  /**
@@ -12308,7 +12483,13 @@ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12308
12483
  */
12309
12484
  var CompositionItemEntrySchema = object({
12310
12485
  label: string().min(1).max(80),
12311
- fields: record(string().min(1), CompositionFieldSourceSchema)
12486
+ fields: record(string().min(1), CompositionFieldSourceSchema),
12487
+ /**
12488
+ * What the cap's `reset` does for THIS item (consumables). Present = the item
12489
+ * is resettable and a `reset` call naming its key forwards here. The item key
12490
+ * selects the command, so it is never an argument: there is no pass-through.
12491
+ */
12492
+ reset: CompositionForwardCommandSchema.optional()
12312
12493
  });
12313
12494
  var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12314
12495
  kind: literal("fields"),
@@ -12320,7 +12501,7 @@ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12320
12501
  * owns the WHOLE array field: the composed value holds exactly these items.
12321
12502
  */
12322
12503
  items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
12323
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12504
+ /** Method name → what serves it. Only a BUILT capability takes commands (D967). */
12324
12505
  commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12325
12506
  }), object({
12326
12507
  kind: literal("passthrough"),
@@ -16377,6 +16558,449 @@ object({
16377
16558
  lastError: string().nullable()
16378
16559
  });
16379
16560
  /**
16561
+ * notification-output — canonical, capability-gated notification delivery.
16562
+ *
16563
+ * Apprise-derived model (see
16564
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16565
+ * callers emit ONE canonical `Notification`; each provider declares a
16566
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16567
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16568
+ * message to what the kind supports — callers never special-case a service.
16569
+ *
16570
+ * DESIGN DECISIONS (locked):
16571
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16572
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16573
+ * cap. Rationale: the admin UI needs one uniform surface across the
16574
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16575
+ * alternative would fork the UI per addon and cannot host the
16576
+ * discovery→adopt flow.
16577
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16578
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16579
+ * registered provider (notifiers addon + HA addon) so one catalog is
16580
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16581
+ * `addonId` the generated collection router extracts from the call input.
16582
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16583
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16584
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16585
+ * base64 fallback needed.
16586
+ *
16587
+ * TODO (deferred, closed-set change — separate decision): add
16588
+ * `providerKind: 'notify'` so notification providers surface on the unified
16589
+ * admin "Integrations" page.
16590
+ */
16591
+ /**
16592
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16593
+ * adapter picks what it supports and the degrade engine filters the rest.
16594
+ */
16595
+ var AttachmentMediaTypeSchema = _enum([
16596
+ "image",
16597
+ "video",
16598
+ "gif",
16599
+ "audio",
16600
+ "icon"
16601
+ ]);
16602
+ /**
16603
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16604
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16605
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16606
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16607
+ */
16608
+ var AttachmentSchema = object({
16609
+ mediaType: AttachmentMediaTypeSchema,
16610
+ url: string().optional(),
16611
+ bytes: _instanceof(Uint8Array).optional(),
16612
+ mime: string().optional(),
16613
+ name: string().optional()
16614
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16615
+ var NotificationFormatSchema = _enum([
16616
+ "text",
16617
+ "markdown",
16618
+ "html"
16619
+ ]);
16620
+ /**
16621
+ * The CLOSED icon vocabulary an action button may use.
16622
+ *
16623
+ * A closed set, not a free string, and that is the whole point: an arbitrary
16624
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
16625
+ * notices — the same class of gap as a zone vocabulary nothing produced
16626
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
16627
+ * declares `actionIcons: false` and the degrade engine strips the field.
16628
+ *
16629
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
16630
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
16631
+ * differently.
16632
+ *
16633
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
16634
+ *
16635
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
16636
+ * the whole set onto its own renderer's vocabulary through a
16637
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
16638
+ * fallback, so adding a member here fails every adapter's build until someone
16639
+ * decides its glyph, which is the only place that decision can be made
16640
+ * honestly.
16641
+ *
16642
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
16643
+ * `disarm` straight through; iOS feeds that string to
16644
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
16645
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
16646
+ * mapping, and "the field is documented" is not "the value renders".
16647
+ *
16648
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
16649
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
16650
+ * so an addon that emits a token the running hub does not know does not lose an
16651
+ * icon — its whole `send` fails Zod validation and the notification never
16652
+ * arrives. Never emit a new token from an addon before the train carrying it.
16653
+ */
16654
+ var NotificationActionIconSchema = _enum([
16655
+ "acknowledge",
16656
+ "dismiss",
16657
+ "silence",
16658
+ "snooze",
16659
+ "view",
16660
+ "play",
16661
+ "open",
16662
+ "close",
16663
+ "lock",
16664
+ "unlock",
16665
+ "arm",
16666
+ "arm-home",
16667
+ "arm-away",
16668
+ "arm-night",
16669
+ "disarm",
16670
+ "light",
16671
+ "alert",
16672
+ "camera"
16673
+ ]);
16674
+ /** A single tap-through action button. */
16675
+ var NotificationActionSchema = object({
16676
+ id: string(),
16677
+ label: string(),
16678
+ url: string().optional(),
16679
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
16680
+ icon: NotificationActionIconSchema.optional(),
16681
+ /**
16682
+ * Renders in a warning style where the notifier supports it.
16683
+ *
16684
+ * A HINT, never a gate. The callback's authority is its token and nothing
16685
+ * else — see `notification-center/action-token.ts` for what that does and
16686
+ * does not buy.
16687
+ */
16688
+ destructive: boolean().optional(),
16689
+ /**
16690
+ * How the tap should REACH the url.
16691
+ *
16692
+ * `navigate` (absent, and every button authored before this field) opens it:
16693
+ * the phone leaves the notification and shows whatever the callback returns.
16694
+ * That is right for a button whose answer the operator wants to read.
16695
+ *
16696
+ * `background` fires it as a POST and stays put. It exists for the buttons
16697
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
16698
+ * an answer to the notification, and being thrown into a browser tab to
16699
+ * confirm it costs more attention than the notification did. A backend that
16700
+ * cannot do a background call renders it as an ordinary link (the adapters
16701
+ * fall back rather than dropping the button), so this is a preference, never
16702
+ * a requirement.
16703
+ */
16704
+ mode: _enum(["navigate", "background"]).optional()
16705
+ });
16706
+ /** A delivery parameter value: what a notifier declared it takes. */
16707
+ var DeliveryParamValueSchema = union([
16708
+ string(),
16709
+ number(),
16710
+ boolean()
16711
+ ]);
16712
+ /**
16713
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
16714
+ * addresses a target of that kind may set it; a kind that declares nothing is
16715
+ * never handed one. The admin UI derives its control from this descriptor —
16716
+ * no editor knows a service by name.
16717
+ *
16718
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
16719
+ * run on the addon cap path).
16720
+ */
16721
+ var DeliveryParamSchema = object({
16722
+ key: string().min(1),
16723
+ label: string(),
16724
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
16725
+ labelKey: string().optional(),
16726
+ type: _enum([
16727
+ "enum",
16728
+ "number",
16729
+ "boolean",
16730
+ "string"
16731
+ ]),
16732
+ /** `enum` only: the closed set of accepted values. */
16733
+ options: array(object({
16734
+ value: string(),
16735
+ label: string()
16736
+ })).optional(),
16737
+ /** `number` only. */
16738
+ min: number().optional(),
16739
+ max: number().optional(),
16740
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
16741
+ default: DeliveryParamValueSchema.optional()
16742
+ });
16743
+ /**
16744
+ * The canonical notification. `body` is the only hard field (Apprise model).
16745
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16746
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16747
+ * the adapter maps this ordinal onto its native level. `level?` is an
16748
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16749
+ * `priority` for that one target.
16750
+ */
16751
+ var NotificationSchema = object({
16752
+ body: string(),
16753
+ title: string().optional(),
16754
+ format: NotificationFormatSchema.default("text"),
16755
+ priority: number().int().min(1).max(5).default(3),
16756
+ level: string().optional(),
16757
+ attachments: array(AttachmentSchema).optional(),
16758
+ clickUrl: string().optional(),
16759
+ actions: array(NotificationActionSchema).optional(),
16760
+ sound: string().optional(),
16761
+ ttl: number().optional(),
16762
+ tag: string().optional(),
16763
+ deviceId: number().optional(),
16764
+ eventId: string().optional(),
16765
+ metadata: record(string(), unknown()).optional(),
16766
+ /**
16767
+ * Values for the parameters the target's kind DECLARES in
16768
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
16769
+ * declaration by the sender; a notifier reads only the keys it declared.
16770
+ */
16771
+ delivery: record(string(), DeliveryParamValueSchema).optional()
16772
+ });
16773
+ /** One declared native severity/priority level for a kind. */
16774
+ var TargetKindLevelSchema = object({
16775
+ id: string(),
16776
+ label: string(),
16777
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16778
+ ordinal: number().int().min(1).max(5).nullable(),
16779
+ flags: object({
16780
+ critical: boolean().optional(),
16781
+ silent: boolean().optional(),
16782
+ noPush: boolean().optional()
16783
+ }).optional(),
16784
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16785
+ requires: array(string()).optional(),
16786
+ description: string().optional()
16787
+ });
16788
+ /** The full capability block consulted before dispatch. */
16789
+ var TargetKindCapsSchema = object({
16790
+ attachments: object({
16791
+ mediaTypes: array(AttachmentMediaTypeSchema),
16792
+ mode: _enum([
16793
+ "url",
16794
+ "bytes",
16795
+ "both"
16796
+ ]),
16797
+ max: number().int().nonnegative(),
16798
+ maxBytes: number().int().positive().optional()
16799
+ }),
16800
+ /** Max action buttons (0 = none). */
16801
+ actions: number().int().nonnegative(),
16802
+ /**
16803
+ * Whether this kind renders a per-action ICON.
16804
+ *
16805
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
16806
+ * run on the addon cap path — three production failures in one day taught
16807
+ * this repo that once. Absent is read as false by the degrade engine, which
16808
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
16809
+ * assumed and dropped costs the operator's trust in the field.
16810
+ */
16811
+ actionIcons: boolean().optional(),
16812
+ levels: array(TargetKindLevelSchema),
16813
+ format: array(NotificationFormatSchema),
16814
+ clickUrl: boolean(),
16815
+ sound: boolean(),
16816
+ ttl: boolean(),
16817
+ bodyMaxLen: number().int().positive()
16818
+ });
16819
+ /**
16820
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16821
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16822
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16823
+ * the union is large and not meant for runtime validation here; the exported
16824
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16825
+ */
16826
+ var ConfigSchemaPassthrough$3 = unknown();
16827
+ var TargetKindSchema = object({
16828
+ kind: string(),
16829
+ label: string(),
16830
+ icon: string(),
16831
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16832
+ addonId: string(),
16833
+ /**
16834
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
16835
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
16836
+ * when the addon bundles no icon for that kind — the client then falls back
16837
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
16838
+ *
16839
+ * Root-relative on purpose: it resolves against whatever origin serves a web
16840
+ * client, and a native client joins it onto its own hub base.
16841
+ *
16842
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
16843
+ * field that survived only because the runtime cap-router forwards provider
16844
+ * output verbatim — so every consumer had to re-declare it by hand to stop
16845
+ * its own Zod parse from stripping it, and the whole arrangement would have
16846
+ * broken silently the moment output validation was tightened anywhere.
16847
+ */
16848
+ iconUrl: string().optional(),
16849
+ /**
16850
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
16851
+ *
16852
+ * The server knows this and therefore says it, because the client cannot
16853
+ * safely guess: a React-Native client renders SVG and raster through two
16854
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
16855
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
16856
+ * placeholder glyph for every vector icon while the web build looked fine.
16857
+ *
16858
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
16859
+ * not been updated — a client that cannot determine the type should prefer
16860
+ * its raster path, which is the safe default for an unknown image.
16861
+ */
16862
+ iconMediaType: string().optional(),
16863
+ configSchema: ConfigSchemaPassthrough$3,
16864
+ /**
16865
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16866
+ * base URL, instance, chat, recipient). A save that changes any of them
16867
+ * while leaving a secret redacted is refused by the provider (D634), so an
16868
+ * editor uses this to ask for the secret on exactly those edits instead of
16869
+ * on every edit.
16870
+ *
16871
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16872
+ * kinds) and from a provider older than this field — an editor that finds
16873
+ * it absent must assume ANY edit may need the secret.
16874
+ */
16875
+ destinationKeys: array(string()).optional(),
16876
+ supportsDiscovery: boolean(),
16877
+ /**
16878
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
16879
+ * Absent = none; the rule editor then offers nothing.
16880
+ */
16881
+ deliveryParams: array(DeliveryParamSchema).optional(),
16882
+ caps: TargetKindCapsSchema
16883
+ });
16884
+ /**
16885
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16886
+ * (return a presence marker only) when serving `listTargets` — never
16887
+ * round-trip a stored secret to the UI.
16888
+ */
16889
+ var TargetSchema = object({
16890
+ id: string(),
16891
+ name: string(),
16892
+ kind: string(),
16893
+ addonId: string(),
16894
+ enabled: boolean(),
16895
+ config: record(string(), unknown())
16896
+ });
16897
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16898
+ var DiscoveredTargetSchema = object({
16899
+ kind: string(),
16900
+ suggestedName: string(),
16901
+ config: record(string(), unknown())
16902
+ });
16903
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16904
+ var RenderedAsSchema = object({
16905
+ level: string(),
16906
+ format: NotificationFormatSchema,
16907
+ attachmentsSent: number().int().nonnegative(),
16908
+ actionsSent: number().int().nonnegative(),
16909
+ truncated: boolean(),
16910
+ dropped: array(string())
16911
+ });
16912
+ var SendResultSchema = object({
16913
+ success: boolean(),
16914
+ error: string().optional(),
16915
+ renderedAs: RenderedAsSchema.optional()
16916
+ });
16917
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16918
+ var TestResultSchema = SendResultSchema;
16919
+ /**
16920
+ * Pre-save credential probe against a partial (unsaved) target config.
16921
+ * `resolvedUrl` never includes secrets (token stripped from path).
16922
+ */
16923
+ var ProbeTargetConfigResultSchema = object({
16924
+ ok: boolean(),
16925
+ resolvedUrl: string(),
16926
+ httpStatus: number().int().optional(),
16927
+ stateInstance: string().optional(),
16928
+ error: string().optional()
16929
+ });
16930
+ var notificationOutputCapability = {
16931
+ name: "notification-output",
16932
+ scope: "system",
16933
+ mode: "collection",
16934
+ methods: {
16935
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
16936
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
16937
+ /**
16938
+ * One target WITH secrets — admin edit / duplicate hydration only.
16939
+ * `listTargets` stays redacted; the editor loads this so password fields
16940
+ * are filled (masked in the input) and a save can round-trip the token
16941
+ * when a destination changes (Duplicate with a new chatId, etc.).
16942
+ */
16943
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
16944
+ discoverTargets: method(object({
16945
+ kind: string(),
16946
+ config: record(string(), unknown()).optional(),
16947
+ /**
16948
+ * When editing / duplicating, the blank password field never round-trips
16949
+ * the stored token. Pass the source target id so the provider can restore
16950
+ * secrets server-side before dialling (same merge as upsert).
16951
+ */
16952
+ targetId: string().optional()
16953
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
16954
+ probeTargetConfig: method(object({
16955
+ kind: string(),
16956
+ config: record(string(), unknown()),
16957
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
16958
+ targetId: string().optional()
16959
+ }), ProbeTargetConfigResultSchema, {
16960
+ kind: "mutation",
16961
+ auth: "admin"
16962
+ }),
16963
+ send: method(object({
16964
+ targetId: string(),
16965
+ notification: NotificationSchema
16966
+ }), SendResultSchema, {
16967
+ kind: "mutation",
16968
+ auth: "admin"
16969
+ }),
16970
+ testTarget: method(object({
16971
+ targetId: string(),
16972
+ sample: NotificationSchema.optional()
16973
+ }), TestResultSchema, {
16974
+ kind: "mutation",
16975
+ auth: "admin"
16976
+ }),
16977
+ upsertTarget: method(object({
16978
+ target: TargetSchema,
16979
+ /**
16980
+ * Duplicate flow: create a NEW target id while copying secrets from an
16981
+ * existing row. The UI never holds the stored token; blank / redacted
16982
+ * password fields are restored from this source (D634 still applies if
16983
+ * destinations diverge without a re-typed secret).
16984
+ */
16985
+ cloneSecretsFrom: string().optional()
16986
+ }), TargetSchema, {
16987
+ kind: "mutation",
16988
+ auth: "admin"
16989
+ }),
16990
+ deleteTarget: method(object({ targetId: string() }), _void(), {
16991
+ kind: "mutation",
16992
+ auth: "admin"
16993
+ }),
16994
+ setTargetEnabled: method(object({
16995
+ targetId: string(),
16996
+ enabled: boolean()
16997
+ }), _void(), {
16998
+ kind: "mutation",
16999
+ auth: "admin"
17000
+ })
17001
+ }
17002
+ };
17003
+ /**
16380
17004
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
16381
17005
  * surface that admin-ui consumes through `useAddonPagesListPages()`.
16382
17006
  *
@@ -16408,52 +17032,6 @@ var AddonPageInfoSchema = object({
16408
17032
  bundleUrl: string()
16409
17033
  });
16410
17034
  method(_void(), array(AddonPageInfoSchema).readonly());
16411
- /**
16412
- * `addon-pages-source` — collection cap exposing per-provider raw page
16413
- * declarations. Every addon that contributes a UI page registers a
16414
- * provider here. The hub-side singleton aggregator (`addon-pages` cap,
16415
- * see `addon-pages.cap.ts`) walks this collection, stamps versioned
16416
- * `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
16417
- * that admin-ui consumes.
16418
- *
16419
- * The split exists because the public listing has a different output
16420
- * shape than the per-provider raw declarations, and we want both ends
16421
- * to flow through codegen instead of relying on a hand-written wrapper.
16422
- */
16423
- var AddonPageDeclarationSchema = object({
16424
- id: string(),
16425
- label: string(),
16426
- icon: string(),
16427
- path: string(),
16428
- /**
16429
- * Module Federation remote name — must match the `name` field on the
16430
- * page addon's `federation()` plugin config. Used by admin-ui's
16431
- * `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
16432
- * Conventionally `addon_<id>_page` (snake_case; MF names cannot
16433
- * contain hyphens).
16434
- */
16435
- remoteName: string(),
16436
- /**
16437
- * Bundle filename inside the addon's `dist/` dir served at
16438
- * `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
16439
- * is always `'remoteEntry.js'`; the value is kept on the metadata so
16440
- * the static-file route can compute an mtime-based cache-buster URL
16441
- * without a separate filesystem stat.
16442
- */
16443
- bundle: string(),
16444
- /**
16445
- * Sidebar section this page docks into. Well-known ids: `'detection'`,
16446
- * `'cluster'`, `'administration'` — the page renders inside that group.
16447
- * Any OTHER string creates (or joins) a custom section rendered after
16448
- * the built-in groups; its label comes from `sectionLabel` (first
16449
- * declaration wins), falling back to the id. Absent → the legacy
16450
- * "Addon Pages" group.
16451
- */
16452
- section: string().optional(),
16453
- /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16454
- sectionLabel: string().optional()
16455
- });
16456
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16457
17035
  var AddonHttpRouteSchema = object({
16458
17036
  method: _enum([
16459
17037
  "GET",
@@ -16754,9 +17332,21 @@ var AlertSeveritySchema = _enum([
16754
17332
  "error",
16755
17333
  "critical"
16756
17334
  ]);
17335
+ /**
17336
+ * An action that RUNS something instead of going somewhere: the admin UI calls
17337
+ * the addon's custom action (`addons.custom`) with exactly these fields. The
17338
+ * producer owns the meaning; the UI knows nothing about the addon or the kind.
17339
+ */
17340
+ var AlertActionInvokeSchema = object({
17341
+ addonId: string().min(1),
17342
+ action: string().min(1),
17343
+ input: record(string(), unknown()).optional()
17344
+ });
17345
+ /** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
16757
17346
  var AlertActionSchema = object({
16758
17347
  label: string(),
16759
17348
  href: string().optional(),
17349
+ invoke: AlertActionInvokeSchema.optional(),
16760
17350
  /**
16761
17351
  * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
16762
17352
  * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
@@ -19158,7 +19748,11 @@ var CompositionProblemSchema = object({
19158
19748
  "mixed-quantity",
19159
19749
  "helper-mismatch",
19160
19750
  "member-constraint",
19161
- "retention"
19751
+ "retention",
19752
+ "command-method-unknown",
19753
+ "command-source-unknown",
19754
+ "command-input-unknown",
19755
+ "command-input-missing"
19162
19756
  ]),
19163
19757
  path: string(),
19164
19758
  message: string()
@@ -19197,7 +19791,9 @@ var CompositionFieldRoleSchema = _enum([
19197
19791
  var CompositionFieldItemSchema = object({
19198
19792
  arrayPath: string(),
19199
19793
  key: string(),
19200
- label: string()
19794
+ label: string(),
19795
+ /** True when the item carries a `reset` forward: the composer writes `resettable` from this (D967). */
19796
+ resettable: boolean().optional()
19201
19797
  });
19202
19798
  var CompositionFieldPlanSchema = object({
19203
19799
  cap: string(),
@@ -19402,7 +19998,7 @@ Object.freeze({
19402
19998
  * One catalog row per base reading a group helper can build (D944 §3).
19403
19999
  * Data, not a form default written into the block.
19404
20000
  */
19405
- var NOW$2 = {
20001
+ var NOW$3 = {
19406
20002
  fieldPath: "lastFetchedAt",
19407
20003
  expr: "now"
19408
20004
  };
@@ -19454,7 +20050,7 @@ var GROUP_TEMPLATES = [
19454
20050
  quantity: "temperature",
19455
20051
  defaultAggregate: "mean",
19456
20052
  empty: "null",
19457
- companions: [NOW$2],
20053
+ companions: [NOW$3],
19458
20054
  alsoFrom: {
19459
20055
  deviceTypes: CLIMATE_DEVICES,
19460
20056
  accept: {
@@ -19472,7 +20068,7 @@ var GROUP_TEMPLATES = [
19472
20068
  quantity: "humidity",
19473
20069
  defaultAggregate: "mean",
19474
20070
  empty: "null",
19475
- companions: [NOW$2],
20071
+ companions: [NOW$3],
19476
20072
  alsoFrom: {
19477
20073
  deviceTypes: CLIMATE_DEVICES,
19478
20074
  accept: {
@@ -19490,7 +20086,7 @@ var GROUP_TEMPLATES = [
19490
20086
  quantity: "pressure",
19491
20087
  defaultAggregate: "mean",
19492
20088
  empty: "null",
19493
- companions: [NOW$2]
20089
+ companions: [NOW$3]
19494
20090
  }),
19495
20091
  numeric({
19496
20092
  id: "illuminance",
@@ -19501,7 +20097,7 @@ var GROUP_TEMPLATES = [
19501
20097
  quantity: "illuminance",
19502
20098
  defaultAggregate: "mean",
19503
20099
  empty: "null",
19504
- companions: [NOW$2]
20100
+ companions: [NOW$3]
19505
20101
  }),
19506
20102
  numeric({
19507
20103
  id: "power",
@@ -19512,7 +20108,7 @@ var GROUP_TEMPLATES = [
19512
20108
  quantity: "power",
19513
20109
  defaultAggregate: "sum",
19514
20110
  empty: "omit",
19515
- companions: [NOW$2]
20111
+ companions: [NOW$3]
19516
20112
  }),
19517
20113
  numeric({
19518
20114
  id: "energy",
@@ -19523,7 +20119,7 @@ var GROUP_TEMPLATES = [
19523
20119
  quantity: "energy",
19524
20120
  defaultAggregate: "sum",
19525
20121
  empty: "omit",
19526
- companions: [NOW$2]
20122
+ companions: [NOW$3]
19527
20123
  }),
19528
20124
  numeric({
19529
20125
  id: "battery",
@@ -19558,7 +20154,7 @@ var GROUP_TEMPLATES = [
19558
20154
  quantity: "device",
19559
20155
  defaultAggregate: "mean",
19560
20156
  empty: "null",
19561
- companions: [NOW$2]
20157
+ companions: [NOW$3]
19562
20158
  }),
19563
20159
  binary({
19564
20160
  id: "motion",
@@ -22032,12 +22628,48 @@ var ExposeInputSchema = object({
22032
22628
  capabilities: array(string()).optional()
22033
22629
  });
22034
22630
  var UnexposeInputSchema = object({ deviceId: string() });
22631
+ /**
22632
+ * What an exporter may expose besides devices (D968). An action and a
22633
+ * notification target are LIST ENTRIES, never devices (D954): they enter an
22634
+ * exporter's own selection, not the device registry.
22635
+ */
22636
+ var ExportEntityKindSchema = _enum([
22637
+ "device",
22638
+ "action",
22639
+ "notifier"
22640
+ ]);
22641
+ var ExportEntityRefSchema = discriminatedUnion("kind", [
22642
+ object({
22643
+ kind: literal("device"),
22644
+ deviceId: string()
22645
+ }),
22646
+ object({
22647
+ kind: literal("action"),
22648
+ actionId: string().min(1)
22649
+ }),
22650
+ object({
22651
+ kind: literal("notifier"),
22652
+ targetId: string().min(1)
22653
+ })
22654
+ ]);
22655
+ var ExposedEntitySchema = object({
22656
+ entity: ExportEntityRefSchema,
22657
+ exposedAs: string().optional()
22658
+ });
22659
+ var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22660
+ var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
22035
22661
  method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
22036
22662
  kind: "mutation",
22037
22663
  auth: "admin"
22038
22664
  }), method(UnexposeInputSchema, _void(), {
22039
22665
  kind: "mutation",
22040
22666
  auth: "admin"
22667
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
22668
+ kind: "mutation",
22669
+ auth: "admin"
22670
+ }), optionalMethod(UnexposeEntityInputSchema, _void(), {
22671
+ kind: "mutation",
22672
+ auth: "admin"
22041
22673
  });
22042
22674
  /** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
22043
22675
  var ICON_CATEGORY_IDS = [
@@ -43259,6 +43891,14 @@ method(object({
43259
43891
  kind: "mutation",
43260
43892
  auth: "admin"
43261
43893
  });
43894
+ method(object({
43895
+ nodeId: string().min(1),
43896
+ /** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
43897
+ removedAt: number()
43898
+ }), object({ counts: record(string(), number().int().nonnegative()) }), {
43899
+ kind: "mutation",
43900
+ auth: "admin"
43901
+ });
43262
43902
  /**
43263
43903
  * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
43264
43904
  * so it can delete what it keeps for that device (D865).
@@ -43979,7 +44619,7 @@ var LlmProviderWidgetSchema = object({
43979
44619
  /** ConfigUISchema tree passed through untyped on the wire (the
43980
44620
  * notification-output `ConfigSchemaPassthrough` precedent at
43981
44621
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
43982
- var ConfigSchemaPassthrough$3 = unknown();
44622
+ var ConfigSchemaPassthrough$2 = unknown();
43983
44623
  var LlmProfileKindDescriptorSchema = object({
43984
44624
  kind: LlmProfileKindSchema,
43985
44625
  label: string(),
@@ -43988,9 +44628,9 @@ var LlmProfileKindDescriptorSchema = object({
43988
44628
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
43989
44629
  addonId: string(),
43990
44630
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
43991
- connectionSchema: ConfigSchemaPassthrough$3,
44631
+ connectionSchema: ConfigSchemaPassthrough$2,
43992
44632
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
43993
- advancedSchema: ConfigSchemaPassthrough$3.optional(),
44633
+ advancedSchema: ConfigSchemaPassthrough$2.optional(),
43994
44634
  /** The kind's defaults; a model from `listModels` may override any of them. */
43995
44635
  features: LlmModelFeaturesSchema,
43996
44636
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -44516,10 +45156,10 @@ var CpuBreakdownSchema = object({
44516
45156
  cores: number()
44517
45157
  });
44518
45158
  var MemoryInfoSchema = object({
44519
- percent: number(),
45159
+ percent: number().nullable(),
44520
45160
  totalBytes: number(),
44521
- usedBytes: number(),
44522
- availableBytes: number(),
45161
+ usedBytes: number().nullable(),
45162
+ availableBytes: number().nullable(),
44523
45163
  swapUsedBytes: number(),
44524
45164
  swapTotalBytes: number()
44525
45165
  });
@@ -44823,8 +45463,9 @@ var GetLoadSeriesInputSchema = object({
44823
45463
  });
44824
45464
  var SystemMetricsSchema = object({
44825
45465
  cpuPercent: number(),
44826
- memoryPercent: number(),
44827
- memoryUsedMB: number(),
45466
+ /** `null` when the memory sample failed or was not taken yet (D393). */
45467
+ memoryPercent: number().nullable(),
45468
+ memoryUsedMB: number().nullable(),
44828
45469
  memoryTotalMB: number(),
44829
45470
  diskPercent: number().optional(),
44830
45471
  temperature: number().optional(),
@@ -44944,565 +45585,122 @@ var BrokerInfoSchema$1 = object({
44944
45585
  error: string().optional(),
44945
45586
  /** Embedded brokers only: number of MQTT clients currently connected. */
44946
45587
  connectedClients: number().int().nonnegative().optional(),
44947
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
44948
- lastCheckedAt: number().optional()
44949
- });
44950
- /**
44951
- * Connection details — what a consumer needs to call
44952
- * `mqtt.connect(url, options)`. We split URL + credentials so the
44953
- * consumer can pass them as `mqtt.connect(url, { username, password })`
44954
- * instead of stuffing creds into the URL (which leaks them into logs).
44955
- */
44956
- var BrokerConnectionDetailsSchema = object({
44957
- url: string(),
44958
- username: string().optional(),
44959
- password: string().optional(),
44960
- /**
44961
- * Suggested prefix for `clientId`. Each consumer should suffix this
44962
- * with its own discriminator (addon id, instance id) so reconnects
44963
- * don't kick each other off (MQTT spec: clientId must be unique per
44964
- * broker).
44965
- */
44966
- clientIdPrefix: string().optional()
44967
- });
44968
- var AddBrokerInputSchema = object({
44969
- name: string().min(1),
44970
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
44971
- username: string().optional(),
44972
- password: string().optional(),
44973
- clientIdPrefix: string().optional()
44974
- });
44975
- var AddBrokerResultSchema = object({ id: string() });
44976
- var IdInputSchema = object({ id: string() });
44977
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
44978
- ok: literal(true),
44979
- latencyMs: number()
44980
- }), object({
44981
- ok: literal(false),
44982
- error: string()
44983
- })]);
44984
- var StartEmbeddedInputSchema = object({
44985
- port: number().int().min(1).max(65535).default(1883),
44986
- /** Allow anonymous connect (no username/password). Default: false. */
44987
- allowAnonymous: boolean().default(false),
44988
- /** Optional shared username/password for clients. */
44989
- username: string().optional(),
44990
- password: string().optional()
44991
- });
44992
- var StartEmbeddedResultSchema = object({
44993
- id: string(),
44994
- url: string()
44995
- });
44996
- var StatusSchema = object({
44997
- brokerCount: number(),
44998
- embeddedRunning: boolean()
44999
- });
45000
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45001
- kind: "mutation",
45002
- auth: "admin"
45003
- }), method(IdInputSchema, _void(), {
45004
- kind: "mutation",
45005
- auth: "admin"
45006
- }), method(IdInputSchema, TestResultSchema$1, {
45007
- kind: "mutation",
45008
- auth: "admin"
45009
- }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45010
- kind: "mutation",
45011
- auth: "admin"
45012
- }), method(IdInputSchema, _void(), {
45013
- kind: "mutation",
45014
- auth: "admin"
45015
- }), method(_void(), StatusSchema);
45016
- var NetworkEndpointSchema = object({
45017
- url: string(),
45018
- hostname: string(),
45019
- port: number(),
45020
- protocol: _enum(["http", "https"]),
45021
- /**
45022
- * Whether this address survives a restart of its provider. `false` = it is
45023
- * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45024
- * hub never selects such an address AUTOMATICALLY for a role (D860), though
45025
- * it stays visible and an operator may still flag it. Absent = stable: every
45026
- * provider except a Quick tunnel owns a name that does not change.
45027
- */
45028
- stable: boolean().optional()
45029
- });
45030
- object({
45031
- url: string(),
45032
- /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45033
- stable: boolean().optional(),
45034
- updated: boolean().optional()
45035
- });
45036
- var NetworkAccessStatusSchema = object({
45037
- connected: boolean(),
45038
- endpoint: NetworkEndpointSchema.nullable(),
45039
- error: string().optional()
45040
- });
45041
- /**
45042
- * Optional, richer endpoint shape returned by providers that expose
45043
- * MORE than one ingress concurrently (Tailscale Funnel with mixed
45044
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45045
- * the originating provider config (mode + sourcePort) so the
45046
- * orchestrator UI can label rows distinctly. Providers that expose only
45047
- * one endpoint just omit `listEndpoints` from their provider impl.
45048
- */
45049
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45050
- /**
45051
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
45052
- * the orchestrator can dedupe across `listEndpoints` polls.
45053
- */
45054
- id: string(),
45055
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45056
- label: string(),
45057
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45058
- mode: string().optional(),
45059
- /** Originating local port the ingress fronts (informational). */
45060
- sourcePort: number().optional()
45061
- });
45062
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45063
- /**
45064
- * notification-output — canonical, capability-gated notification delivery.
45065
- *
45066
- * Apprise-derived model (see
45067
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
45068
- * callers emit ONE canonical `Notification`; each provider declares a
45069
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
45070
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
45071
- * message to what the kind supports — callers never special-case a service.
45072
- *
45073
- * DESIGN DECISIONS (locked):
45074
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
45075
- * `setTargetEnabled`), each provider persisting via the `settings-store`
45076
- * cap. Rationale: the admin UI needs one uniform surface across the
45077
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
45078
- * alternative would fork the UI per addon and cannot host the
45079
- * discovery→adopt flow.
45080
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
45081
- * the generated cap-mount auto-`concatCollection`-fans them across every
45082
- * registered provider (notifiers addon + HA addon) so one catalog is
45083
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
45084
- * `addonId` the generated collection router extracts from the call input.
45085
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
45086
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
45087
- * `storage` / `storage-provider` / `recording` caps over the same path. No
45088
- * base64 fallback needed.
45089
- *
45090
- * TODO (deferred, closed-set change — separate decision): add
45091
- * `providerKind: 'notify'` so notification providers surface on the unified
45092
- * admin "Integrations" page.
45093
- */
45094
- /**
45095
- * Zentik-derived typed-media enum — the superset across every kind. Each
45096
- * adapter picks what it supports and the degrade engine filters the rest.
45097
- */
45098
- var AttachmentMediaTypeSchema = _enum([
45099
- "image",
45100
- "video",
45101
- "gif",
45102
- "audio",
45103
- "icon"
45104
- ]);
45105
- /**
45106
- * A single attachment. Exactly one of `url` (remote source, most adapters
45107
- * prefer this) or `bytes` (inline source; required for Pushover-style
45108
- * bytes-only kinds) MUST be present — the degrade engine expresses a
45109
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
45110
- */
45111
- var AttachmentSchema = object({
45112
- mediaType: AttachmentMediaTypeSchema,
45113
- url: string().optional(),
45114
- bytes: _instanceof(Uint8Array).optional(),
45115
- mime: string().optional(),
45116
- name: string().optional()
45117
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
45118
- var NotificationFormatSchema = _enum([
45119
- "text",
45120
- "markdown",
45121
- "html"
45122
- ]);
45123
- /**
45124
- * The CLOSED icon vocabulary an action button may use.
45125
- *
45126
- * A closed set, not a free string, and that is the whole point: an arbitrary
45127
- * icon name is one that ntfy renders, zentik silently drops, and nobody
45128
- * notices — the same class of gap as a zone vocabulary nothing produced
45129
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
45130
- * declares `actionIcons: false` and the degrade engine strips the field.
45131
- *
45132
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
45133
- * renderer's icon set; "acknowledge" survives an adapter that draws it
45134
- * differently.
45135
- *
45136
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
45137
- *
45138
- * These names are for US. **No adapter may forward one verbatim.** Each maps
45139
- * the whole set onto its own renderer's vocabulary through a
45140
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
45141
- * fallback, so adding a member here fails every adapter's build until someone
45142
- * decides its glyph, which is the only place that decision can be made
45143
- * honestly.
45144
- *
45145
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
45146
- * `disarm` straight through; iOS feeds that string to
45147
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
45148
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
45149
- * mapping, and "the field is documented" is not "the value renders".
45150
- *
45151
- * Adding a member is TRAIN-BOUND. The enum lives in the published
45152
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
45153
- * so an addon that emits a token the running hub does not know does not lose an
45154
- * icon — its whole `send` fails Zod validation and the notification never
45155
- * arrives. Never emit a new token from an addon before the train carrying it.
45156
- */
45157
- var NotificationActionIconSchema = _enum([
45158
- "acknowledge",
45159
- "dismiss",
45160
- "silence",
45161
- "snooze",
45162
- "view",
45163
- "play",
45164
- "open",
45165
- "close",
45166
- "lock",
45167
- "unlock",
45168
- "arm",
45169
- "arm-home",
45170
- "arm-away",
45171
- "arm-night",
45172
- "disarm",
45173
- "light",
45174
- "alert",
45175
- "camera"
45176
- ]);
45177
- /** A single tap-through action button. */
45178
- var NotificationActionSchema = object({
45179
- id: string(),
45180
- label: string(),
45181
- url: string().optional(),
45182
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
45183
- icon: NotificationActionIconSchema.optional(),
45184
- /**
45185
- * Renders in a warning style where the notifier supports it.
45186
- *
45187
- * A HINT, never a gate. The callback's authority is its token and nothing
45188
- * else — see `notification-center/action-token.ts` for what that does and
45189
- * does not buy.
45190
- */
45191
- destructive: boolean().optional(),
45192
- /**
45193
- * How the tap should REACH the url.
45194
- *
45195
- * `navigate` (absent, and every button authored before this field) opens it:
45196
- * the phone leaves the notification and shows whatever the callback returns.
45197
- * That is right for a button whose answer the operator wants to read.
45198
- *
45199
- * `background` fires it as a POST and stays put. It exists for the buttons
45200
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
45201
- * an answer to the notification, and being thrown into a browser tab to
45202
- * confirm it costs more attention than the notification did. A backend that
45203
- * cannot do a background call renders it as an ordinary link (the adapters
45204
- * fall back rather than dropping the button), so this is a preference, never
45205
- * a requirement.
45206
- */
45207
- mode: _enum(["navigate", "background"]).optional()
45208
- });
45209
- /** A delivery parameter value: what a notifier declared it takes. */
45210
- var DeliveryParamValueSchema = union([
45211
- string(),
45212
- number(),
45213
- boolean()
45214
- ]);
45215
- /**
45216
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
45217
- * addresses a target of that kind may set it; a kind that declares nothing is
45218
- * never handed one. The admin UI derives its control from this descriptor —
45219
- * no editor knows a service by name.
45220
- *
45221
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45222
- * run on the addon cap path).
45223
- */
45224
- var DeliveryParamSchema = object({
45225
- key: string().min(1),
45226
- label: string(),
45227
- /** i18n key of the label, resolved by the declaring addon's namespace. */
45228
- labelKey: string().optional(),
45229
- type: _enum([
45230
- "enum",
45231
- "number",
45232
- "boolean",
45233
- "string"
45234
- ]),
45235
- /** `enum` only: the closed set of accepted values. */
45236
- options: array(object({
45237
- value: string(),
45238
- label: string()
45239
- })).optional(),
45240
- /** `number` only. */
45241
- min: number().optional(),
45242
- max: number().optional(),
45243
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45244
- default: DeliveryParamValueSchema.optional()
45245
- });
45246
- /**
45247
- * The canonical notification. `body` is the only hard field (Apprise model).
45248
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
45249
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
45250
- * the adapter maps this ordinal onto its native level. `level?` is an
45251
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
45252
- * `priority` for that one target.
45253
- */
45254
- var NotificationSchema = object({
45255
- body: string(),
45256
- title: string().optional(),
45257
- format: NotificationFormatSchema.default("text"),
45258
- priority: number().int().min(1).max(5).default(3),
45259
- level: string().optional(),
45260
- attachments: array(AttachmentSchema).optional(),
45261
- clickUrl: string().optional(),
45262
- actions: array(NotificationActionSchema).optional(),
45263
- sound: string().optional(),
45264
- ttl: number().optional(),
45265
- tag: string().optional(),
45266
- deviceId: number().optional(),
45267
- eventId: string().optional(),
45268
- metadata: record(string(), unknown()).optional(),
45269
- /**
45270
- * Values for the parameters the target's kind DECLARES in
45271
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45272
- * declaration by the sender; a notifier reads only the keys it declared.
45273
- */
45274
- delivery: record(string(), DeliveryParamValueSchema).optional()
45275
- });
45276
- /** One declared native severity/priority level for a kind. */
45277
- var TargetKindLevelSchema = object({
45278
- id: string(),
45279
- label: string(),
45280
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
45281
- ordinal: number().int().min(1).max(5).nullable(),
45282
- flags: object({
45283
- critical: boolean().optional(),
45284
- silent: boolean().optional(),
45285
- noPush: boolean().optional()
45286
- }).optional(),
45287
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
45288
- requires: array(string()).optional(),
45289
- description: string().optional()
45290
- });
45291
- /** The full capability block consulted before dispatch. */
45292
- var TargetKindCapsSchema = object({
45293
- attachments: object({
45294
- mediaTypes: array(AttachmentMediaTypeSchema),
45295
- mode: _enum([
45296
- "url",
45297
- "bytes",
45298
- "both"
45299
- ]),
45300
- max: number().int().nonnegative(),
45301
- maxBytes: number().int().positive().optional()
45302
- }),
45303
- /** Max action buttons (0 = none). */
45304
- actions: number().int().nonnegative(),
45305
- /**
45306
- * Whether this kind renders a per-action ICON.
45307
- *
45308
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
45309
- * run on the addon cap path — three production failures in one day taught
45310
- * this repo that once. Absent is read as false by the degrade engine, which
45311
- * is the safe direction: an icon that is not rendered costs nothing, an icon
45312
- * assumed and dropped costs the operator's trust in the field.
45313
- */
45314
- actionIcons: boolean().optional(),
45315
- levels: array(TargetKindLevelSchema),
45316
- format: array(NotificationFormatSchema),
45317
- clickUrl: boolean(),
45318
- sound: boolean(),
45319
- ttl: boolean(),
45320
- bodyMaxLen: number().int().positive()
45588
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
45589
+ lastCheckedAt: number().optional()
45321
45590
  });
45322
45591
  /**
45323
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
45324
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
45325
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
45326
- * the union is large and not meant for runtime validation here; the exported
45327
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
45592
+ * Connection details — what a consumer needs to call
45593
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
45594
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
45595
+ * instead of stuffing creds into the URL (which leaks them into logs).
45328
45596
  */
45329
- var ConfigSchemaPassthrough$2 = unknown();
45330
- var TargetKindSchema = object({
45331
- kind: string(),
45332
- label: string(),
45333
- icon: string(),
45334
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
45335
- addonId: string(),
45336
- /**
45337
- * URL of the kind's bundled BRAND icon, served by the providing addon over
45338
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
45339
- * when the addon bundles no icon for that kind — the client then falls back
45340
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
45341
- *
45342
- * Root-relative on purpose: it resolves against whatever origin serves a web
45343
- * client, and a native client joins it onto its own hub base.
45344
- *
45345
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
45346
- * field that survived only because the runtime cap-router forwards provider
45347
- * output verbatim — so every consumer had to re-declare it by hand to stop
45348
- * its own Zod parse from stripping it, and the whole arrangement would have
45349
- * broken silently the moment output validation was tightened anywhere.
45350
- */
45351
- iconUrl: string().optional(),
45352
- /**
45353
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
45354
- *
45355
- * The server knows this and therefore says it, because the client cannot
45356
- * safely guess: a React-Native client renders SVG and raster through two
45357
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
45358
- * not decode SVG on iOS/Android), so without this it silently fell back to a
45359
- * placeholder glyph for every vector icon while the web build looked fine.
45360
- *
45361
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
45362
- * not been updated — a client that cannot determine the type should prefer
45363
- * its raster path, which is the safe default for an unknown image.
45364
- */
45365
- iconMediaType: string().optional(),
45366
- configSchema: ConfigSchemaPassthrough$2,
45367
- /**
45368
- * The config keys that decide WHERE this kind's secrets are sent (server,
45369
- * base URL, instance, chat, recipient). A save that changes any of them
45370
- * while leaving a secret redacted is refused by the provider (D634), so an
45371
- * editor uses this to ask for the secret on exactly those edits instead of
45372
- * on every edit.
45373
- *
45374
- * Optional: absent from a provider that holds no secret (the Home Assistant
45375
- * kinds) and from a provider older than this field — an editor that finds
45376
- * it absent must assume ANY edit may need the secret.
45377
- */
45378
- destinationKeys: array(string()).optional(),
45379
- supportsDiscovery: boolean(),
45597
+ var BrokerConnectionDetailsSchema = object({
45598
+ url: string(),
45599
+ username: string().optional(),
45600
+ password: string().optional(),
45380
45601
  /**
45381
- * Extra delivery parameters a rule may set for a target of this kind (D938).
45382
- * Absent = none; the rule editor then offers nothing.
45602
+ * Suggested prefix for `clientId`. Each consumer should suffix this
45603
+ * with its own discriminator (addon id, instance id) so reconnects
45604
+ * don't kick each other off (MQTT spec: clientId must be unique per
45605
+ * broker).
45383
45606
  */
45384
- deliveryParams: array(DeliveryParamSchema).optional(),
45385
- caps: TargetKindCapsSchema
45607
+ clientIdPrefix: string().optional()
45386
45608
  });
45387
- /**
45388
- * A persisted target. `config` holds secrets; providers REDACT secret fields
45389
- * (return a presence marker only) when serving `listTargets` — never
45390
- * round-trip a stored secret to the UI.
45391
- */
45392
- var TargetSchema = object({
45609
+ var AddBrokerInputSchema = object({
45610
+ name: string().min(1),
45611
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
45612
+ username: string().optional(),
45613
+ password: string().optional(),
45614
+ clientIdPrefix: string().optional()
45615
+ });
45616
+ var AddBrokerResultSchema = object({ id: string() });
45617
+ var IdInputSchema = object({ id: string() });
45618
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
45619
+ ok: literal(true),
45620
+ latencyMs: number()
45621
+ }), object({
45622
+ ok: literal(false),
45623
+ error: string()
45624
+ })]);
45625
+ var StartEmbeddedInputSchema = object({
45626
+ port: number().int().min(1).max(65535).default(1883),
45627
+ /** Allow anonymous connect (no username/password). Default: false. */
45628
+ allowAnonymous: boolean().default(false),
45629
+ /** Optional shared username/password for clients. */
45630
+ username: string().optional(),
45631
+ password: string().optional()
45632
+ });
45633
+ var StartEmbeddedResultSchema = object({
45393
45634
  id: string(),
45394
- name: string(),
45395
- kind: string(),
45396
- addonId: string(),
45397
- enabled: boolean(),
45398
- config: record(string(), unknown())
45635
+ url: string()
45399
45636
  });
45400
- /** A discovery-surfaced candidate (config is partial + non-secret). */
45401
- var DiscoveredTargetSchema = object({
45402
- kind: string(),
45403
- suggestedName: string(),
45404
- config: record(string(), unknown())
45637
+ var StatusSchema = object({
45638
+ brokerCount: number(),
45639
+ embeddedRunning: boolean()
45405
45640
  });
45406
- /** The degrade engine's report — what was resolved / dropped / degraded. */
45407
- var RenderedAsSchema = object({
45408
- level: string(),
45409
- format: NotificationFormatSchema,
45410
- attachmentsSent: number().int().nonnegative(),
45411
- actionsSent: number().int().nonnegative(),
45412
- truncated: boolean(),
45413
- dropped: array(string())
45641
+ method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
45642
+ kind: "mutation",
45643
+ auth: "admin"
45644
+ }), method(IdInputSchema, _void(), {
45645
+ kind: "mutation",
45646
+ auth: "admin"
45647
+ }), method(IdInputSchema, TestResultSchema$1, {
45648
+ kind: "mutation",
45649
+ auth: "admin"
45650
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
45651
+ kind: "mutation",
45652
+ auth: "admin"
45653
+ }), method(IdInputSchema, _void(), {
45654
+ kind: "mutation",
45655
+ auth: "admin"
45656
+ }), method(_void(), StatusSchema);
45657
+ var NetworkEndpointSchema = object({
45658
+ url: string(),
45659
+ hostname: string(),
45660
+ port: number(),
45661
+ protocol: _enum(["http", "https"]),
45662
+ /**
45663
+ * Whether this address survives a restart of its provider. `false` = it is
45664
+ * re-issued every time (a Quick tunnel's random `*.trycloudflare.com`); the
45665
+ * hub never selects such an address AUTOMATICALLY for a role (D860), though
45666
+ * it stays visible and an operator may still flag it. Absent = stable: every
45667
+ * provider except a Quick tunnel owns a name that does not change.
45668
+ */
45669
+ stable: boolean().optional()
45414
45670
  });
45415
- var SendResultSchema = object({
45416
- success: boolean(),
45417
- error: string().optional(),
45418
- renderedAs: RenderedAsSchema.optional()
45671
+ object({
45672
+ url: string(),
45673
+ /** Mirrors {@link NetworkEndpointSchema} `stable`; absent = stable. */
45674
+ stable: boolean().optional(),
45675
+ updated: boolean().optional()
45676
+ });
45677
+ var NetworkAccessStatusSchema = object({
45678
+ connected: boolean(),
45679
+ endpoint: NetworkEndpointSchema.nullable(),
45680
+ error: string().optional()
45419
45681
  });
45420
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
45421
- var TestResultSchema = SendResultSchema;
45422
45682
  /**
45423
- * Pre-save credential probe against a partial (unsaved) target config.
45424
- * `resolvedUrl` never includes secrets (token stripped from path).
45683
+ * Optional, richer endpoint shape returned by providers that expose
45684
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
45685
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
45686
+ * the originating provider config (mode + sourcePort) so the
45687
+ * orchestrator UI can label rows distinctly. Providers that expose only
45688
+ * one endpoint just omit `listEndpoints` from their provider impl.
45425
45689
  */
45426
- var ProbeTargetConfigResultSchema = object({
45427
- ok: boolean(),
45428
- resolvedUrl: string(),
45429
- httpStatus: number().int().optional(),
45430
- stateInstance: string().optional(),
45431
- error: string().optional()
45690
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
45691
+ /**
45692
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
45693
+ * the orchestrator can dedupe across `listEndpoints` polls.
45694
+ */
45695
+ id: string(),
45696
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
45697
+ label: string(),
45698
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
45699
+ mode: string().optional(),
45700
+ /** Originating local port the ingress fronts (informational). */
45701
+ sourcePort: number().optional()
45432
45702
  });
45433
- var notificationOutputCapability = {
45434
- name: "notification-output",
45435
- scope: "system",
45436
- mode: "collection",
45437
- methods: {
45438
- listTargetKinds: method(object({}), array(TargetKindSchema)),
45439
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
45440
- /**
45441
- * One target WITH secrets — admin edit / duplicate hydration only.
45442
- * `listTargets` stays redacted; the editor loads this so password fields
45443
- * are filled (masked in the input) and a save can round-trip the token
45444
- * when a destination changes (Duplicate with a new chatId, etc.).
45445
- */
45446
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
45447
- discoverTargets: method(object({
45448
- kind: string(),
45449
- config: record(string(), unknown()).optional(),
45450
- /**
45451
- * When editing / duplicating, the blank password field never round-trips
45452
- * the stored token. Pass the source target id so the provider can restore
45453
- * secrets server-side before dialling (same merge as upsert).
45454
- */
45455
- targetId: string().optional()
45456
- }), array(DiscoveredTargetSchema), { auth: "admin" }),
45457
- probeTargetConfig: method(object({
45458
- kind: string(),
45459
- config: record(string(), unknown()),
45460
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
45461
- targetId: string().optional()
45462
- }), ProbeTargetConfigResultSchema, {
45463
- kind: "mutation",
45464
- auth: "admin"
45465
- }),
45466
- send: method(object({
45467
- targetId: string(),
45468
- notification: NotificationSchema
45469
- }), SendResultSchema, {
45470
- kind: "mutation",
45471
- auth: "admin"
45472
- }),
45473
- testTarget: method(object({
45474
- targetId: string(),
45475
- sample: NotificationSchema.optional()
45476
- }), TestResultSchema, {
45477
- kind: "mutation",
45478
- auth: "admin"
45479
- }),
45480
- upsertTarget: method(object({
45481
- target: TargetSchema,
45482
- /**
45483
- * Duplicate flow: create a NEW target id while copying secrets from an
45484
- * existing row. The UI never holds the stored token; blank / redacted
45485
- * password fields are restored from this source (D634 still applies if
45486
- * destinations diverge without a re-typed secret).
45487
- */
45488
- cloneSecretsFrom: string().optional()
45489
- }), TargetSchema, {
45490
- kind: "mutation",
45491
- auth: "admin"
45492
- }),
45493
- deleteTarget: method(object({ targetId: string() }), _void(), {
45494
- kind: "mutation",
45495
- auth: "admin"
45496
- }),
45497
- setTargetEnabled: method(object({
45498
- targetId: string(),
45499
- enabled: boolean()
45500
- }), _void(), {
45501
- kind: "mutation",
45502
- auth: "admin"
45503
- })
45504
- }
45505
- };
45703
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
45506
45704
  var AUDIO_MACRO_LABELS = [
45507
45705
  {
45508
45706
  id: "speech",
@@ -51947,14 +52145,6 @@ var EngineProvisioningSchema = object({
51947
52145
  */
51948
52146
  configIssues: array(string()).optional()
51949
52147
  });
51950
- var PipelineStepInputSchema = lazy(() => object({
51951
- addonId: string(),
51952
- modelId: string().optional(),
51953
- enabled: boolean().default(true),
51954
- children: array(PipelineStepInputSchema).optional(),
51955
- settings: record(string(), unknown()).optional(),
51956
- jumpDeviceKey: string().optional()
51957
- }));
51958
52148
  var ModelSubstitutionSchema = object({
51959
52149
  addonId: string(),
51960
52150
  chosen: string(),
@@ -52033,7 +52223,6 @@ var ConfigUISchemaBridge = custom();
52033
52223
  var ConfigUISchemaNullableBridge = custom();
52034
52224
  var InferenceCapabilitiesBridge = custom();
52035
52225
  var ModelAvailabilityListBridge = custom();
52036
- var PipelineRunResultBridge = custom();
52037
52226
  method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngineChoiceSchema), method(object({ nodeId: string() }), EngineProvisioningSchema), method(_void(), record(string(), object({
52038
52227
  modelId: string(),
52039
52228
  settings: record(string(), unknown()).readonly()
@@ -55239,6 +55428,12 @@ var ServerUpdateActionResultSchema = object({
55239
55428
  */
55240
55429
  blockers: array(UpdateBlockerSchema).optional()
55241
55430
  });
55431
+ var LaunchOverridesSchema = object({
55432
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
55433
+ heapMb: number().int().nullable() });
55434
+ var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
55435
+ /** Always true: the override is read at launch, not by the running process. */
55436
+ restartRequired: boolean() });
55242
55437
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
55243
55438
  kind: "mutation",
55244
55439
  auth: "admin"
@@ -55261,6 +55456,9 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
55261
55456
  }), method(_void(), ServerUpdateActionResultSchema, {
55262
55457
  kind: "mutation",
55263
55458
  auth: "admin"
55459
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
55460
+ kind: "mutation",
55461
+ auth: "admin"
55264
55462
  });
55265
55463
  /**
55266
55464
  * `smtp-provider` — pluggable email delivery surface.
@@ -58827,6 +59025,318 @@ onBrightnessChanged: { data: object({
58827
59025
  } } }
58828
59026
  };
58829
59027
  /**
59028
+ * The role vocabulary of a remote (D963): what a button MEANS, independent of
59029
+ * how it is wired. A layout template places buttons by role; a device binds a
59030
+ * role to an action; an exporter asks for a role by name. Roles are shared so
59031
+ * one layout fits every appliance and one voice command fits every binding.
59032
+ *
59033
+ * A role outside this list is `custom:<key>` — the operator's own button (a
59034
+ * SmartIR source, a channel preset). The key is a lowercase slug.
59035
+ */
59036
+ var REMOTE_KNOWN_ROLES = [
59037
+ "power",
59038
+ "power_on",
59039
+ "power_off",
59040
+ "volume_up",
59041
+ "volume_down",
59042
+ "mute",
59043
+ "channel_up",
59044
+ "channel_down",
59045
+ "up",
59046
+ "down",
59047
+ "left",
59048
+ "right",
59049
+ "ok",
59050
+ "back",
59051
+ "home",
59052
+ "menu",
59053
+ "info",
59054
+ "guide",
59055
+ "exit",
59056
+ "input",
59057
+ "play",
59058
+ "pause",
59059
+ "play_pause",
59060
+ "stop",
59061
+ "next",
59062
+ "previous",
59063
+ "fast_forward",
59064
+ "rewind",
59065
+ "digit_0",
59066
+ "digit_1",
59067
+ "digit_2",
59068
+ "digit_3",
59069
+ "digit_4",
59070
+ "digit_5",
59071
+ "digit_6",
59072
+ "digit_7",
59073
+ "digit_8",
59074
+ "digit_9"
59075
+ ];
59076
+ var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
59077
+ var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
59078
+ function isRemoteKnownRole(value) {
59079
+ return KNOWN_ROLES.has(value);
59080
+ }
59081
+ function isRemoteCustomKey(key) {
59082
+ return CUSTOM_KEY_RE.test(key);
59083
+ }
59084
+ /** The key of a `custom:<key>` role, or null for any other string. */
59085
+ function customKeyOfRole(role) {
59086
+ if (!role.startsWith("custom:")) return null;
59087
+ const key = role.slice(7);
59088
+ return isRemoteCustomKey(key) ? key : null;
59089
+ }
59090
+ function isRemoteRole(value) {
59091
+ return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
59092
+ }
59093
+ var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
59094
+ var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
59095
+ var RemoteCustomButtonSchema = object({
59096
+ /** The slug after `custom:` in the role. */
59097
+ key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
59098
+ /** What the operator calls it, and what a voice command may name. */
59099
+ name: string().min(1).max(60)
59100
+ });
59101
+ var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
59102
+ var RemoteConfigSchema = object({
59103
+ layoutTemplateId: string().min(1).nullable(),
59104
+ /**
59105
+ * The transmitter device new bindings and an import send through. A binding
59106
+ * names its own device, so one command may override it.
59107
+ */
59108
+ defaultTransmitterId: number().int().nonnegative().nullable().default(null),
59109
+ /** Keyed by role. A role with no entry falls back to the native cap. */
59110
+ bindings: record(string(), RemoteBindingSchema),
59111
+ customButtons: array(RemoteCustomButtonSchema).max(100),
59112
+ power: object({ mode: RemotePowerModeSchema })
59113
+ }).superRefine((config, ctx) => {
59114
+ const roles = Object.keys(config.bindings);
59115
+ if (roles.length > 200) ctx.addIssue({
59116
+ code: "custom",
59117
+ message: "too many bindings",
59118
+ path: ["bindings"]
59119
+ });
59120
+ for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
59121
+ code: "custom",
59122
+ message: `"${role}" is not a role`,
59123
+ path: ["bindings", role]
59124
+ });
59125
+ const keys = /* @__PURE__ */ new Set();
59126
+ config.customButtons.forEach((button, index) => {
59127
+ if (keys.has(button.key)) ctx.addIssue({
59128
+ code: "custom",
59129
+ message: `custom button "${button.key}" is listed twice`,
59130
+ path: [
59131
+ "customButtons",
59132
+ index,
59133
+ "key"
59134
+ ]
59135
+ });
59136
+ keys.add(button.key);
59137
+ });
59138
+ for (const role of roles) {
59139
+ const key = customKeyOfRole(role);
59140
+ if (key !== null && !keys.has(key)) ctx.addIssue({
59141
+ code: "custom",
59142
+ message: `${role} is bound but has no entry in customButtons`,
59143
+ path: ["bindings", role]
59144
+ });
59145
+ }
59146
+ });
59147
+ object({
59148
+ power: object({
59149
+ state: _enum(["on", "off"]),
59150
+ at: number()
59151
+ }).nullable(),
59152
+ muted: object({
59153
+ value: boolean(),
59154
+ at: number()
59155
+ }).nullable()
59156
+ });
59157
+ var RemoteLayoutButtonSchema = object({
59158
+ id: string().min(1).max(40),
59159
+ /** 0-based grid position. */
59160
+ row: number().int().min(0).max(24),
59161
+ col: number().int().min(0).max(24),
59162
+ rowSpan: number().int().min(1).max(24),
59163
+ colSpan: number().int().min(1).max(24),
59164
+ icon: string().min(1).max(60).optional(),
59165
+ label: string().min(1).max(40).optional(),
59166
+ role: RemoteRoleSchema
59167
+ });
59168
+ object({
59169
+ columns: number().int().min(1).max(24),
59170
+ rows: number().int().min(1).max(24),
59171
+ buttons: array(RemoteLayoutButtonSchema).max(200)
59172
+ }).superRefine((grid, ctx) => {
59173
+ const ids = /* @__PURE__ */ new Set();
59174
+ const taken = /* @__PURE__ */ new Map();
59175
+ grid.buttons.forEach((button, index) => {
59176
+ if (ids.has(button.id)) ctx.addIssue({
59177
+ code: "custom",
59178
+ message: `button id "${button.id}" is used twice`,
59179
+ path: [
59180
+ "buttons",
59181
+ index,
59182
+ "id"
59183
+ ]
59184
+ });
59185
+ ids.add(button.id);
59186
+ if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
59187
+ ctx.addIssue({
59188
+ code: "custom",
59189
+ message: `button "${button.id}" leaves the grid`,
59190
+ path: ["buttons", index]
59191
+ });
59192
+ return;
59193
+ }
59194
+ for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
59195
+ const cell = `${r}:${c}`;
59196
+ const other = taken.get(cell);
59197
+ if (other !== void 0) {
59198
+ ctx.addIssue({
59199
+ code: "custom",
59200
+ message: `buttons "${other}" and "${button.id}" overlap`,
59201
+ path: ["buttons", index]
59202
+ });
59203
+ return;
59204
+ }
59205
+ taken.set(cell, button.id);
59206
+ }
59207
+ });
59208
+ });
59209
+ var RemoteBoundCommandSchema = object({
59210
+ role: RemoteRoleSchema,
59211
+ /** The operator's name for a custom button; absent for a known role. */
59212
+ label: string().optional(),
59213
+ custom: boolean()
59214
+ });
59215
+ var RemoteCommandListSchema = object({
59216
+ layoutTemplateId: string().nullable(),
59217
+ power: object({ mode: RemotePowerModeSchema }),
59218
+ /** Only the roles that are BOUND — the ones a press can reach. */
59219
+ commands: array(RemoteBoundCommandSchema)
59220
+ });
59221
+ var RemoteRefusalReasonSchema = _enum([
59222
+ "no-remote",
59223
+ "unbound-role",
59224
+ "action-failed"
59225
+ ]);
59226
+ var RemoteSendResultSchema = discriminatedUnion("status", [
59227
+ object({
59228
+ status: literal("sent"),
59229
+ /** How many times the action ran. */
59230
+ sends: number().int().min(1)
59231
+ }),
59232
+ object({
59233
+ status: literal("skipped"),
59234
+ reason: literal("already-in-state")
59235
+ }),
59236
+ object({
59237
+ status: literal("refused"),
59238
+ reason: RemoteRefusalReasonSchema,
59239
+ detail: string().optional()
59240
+ })
59241
+ ]);
59242
+ var RemoteMediaCommandSchema = _enum([
59243
+ "play",
59244
+ "pause",
59245
+ "stop",
59246
+ "next",
59247
+ "previous",
59248
+ "setMute",
59249
+ "selectSource"
59250
+ ]);
59251
+ /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
59252
+ var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
59253
+ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
59254
+ status: literal("refused"),
59255
+ reason: _enum(["unknown-template", "not-a-media-player"]),
59256
+ detail: string()
59257
+ })]);
59258
+ var deviceInput = object({ deviceId: number().int().nonnegative() });
59259
+ DeviceType.MediaPlayer;
59260
+ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
59261
+ kind: "mutation",
59262
+ auth: "admin"
59263
+ }), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
59264
+ role: RemoteRoleSchema,
59265
+ /** Sends in a row (volume steps). */
59266
+ repeat: number().int().min(1).max(50).default(1)
59267
+ }), RemoteSendResultSchema, {
59268
+ kind: "mutation",
59269
+ auth: "protected"
59270
+ }), method(deviceInput.extend({
59271
+ command: RemoteMediaCommandSchema,
59272
+ muted: boolean().optional(),
59273
+ source: string().optional(),
59274
+ repeat: number().int().min(1).max(50).default(1)
59275
+ }), RemoteMediaCommandResultSchema, {
59276
+ kind: "mutation",
59277
+ auth: "protected"
59278
+ });
59279
+ var TemplateKindSchema = _enum(["remote"]);
59280
+ /**
59281
+ * template — the generic store of reusable, named operator data (D963). A
59282
+ * record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
59283
+ * button grid of a remote) and the schema each payload must satisfy are
59284
+ * declared by the system in `template-kinds.ts`. Nothing here is specific to a
59285
+ * kind: the engine validates a payload against its kind's schema and, on a
59286
+ * delete, asks the kind's owner whether anything still points at the record.
59287
+ *
59288
+ * Every refusal is a typed result — a UI says "unknown kind" or names the
59289
+ * devices that still use a template, without parsing text.
59290
+ */
59291
+ var TemplateRecordSchema = object({
59292
+ id: string().min(1),
59293
+ kind: TemplateKindSchema,
59294
+ name: string().min(1).max(120),
59295
+ /** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
59296
+ payload: unknown(),
59297
+ createdAt: number(),
59298
+ updatedAt: number()
59299
+ });
59300
+ var TemplateWriteResultSchema = discriminatedUnion("status", [object({
59301
+ status: literal("saved"),
59302
+ template: TemplateRecordSchema
59303
+ }), object({
59304
+ status: literal("refused"),
59305
+ reason: _enum(["invalid-payload", "not-found"]),
59306
+ detail: string()
59307
+ })]);
59308
+ /** A device still pointing at a template. */
59309
+ var TemplateUserSchema = object({
59310
+ deviceId: number().int().nonnegative(),
59311
+ name: string()
59312
+ });
59313
+ var TemplateDeleteResultSchema = discriminatedUnion("status", [
59314
+ object({ status: literal("deleted") }),
59315
+ object({ status: literal("not-found") }),
59316
+ object({
59317
+ status: literal("in-use"),
59318
+ devices: array(TemplateUserSchema).min(1)
59319
+ })
59320
+ ]);
59321
+ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
59322
+ kind: TemplateKindSchema,
59323
+ name: string().min(1).max(120),
59324
+ payload: unknown()
59325
+ }), TemplateWriteResultSchema, {
59326
+ kind: "mutation",
59327
+ auth: "admin"
59328
+ }), method(object({
59329
+ id: string().min(1),
59330
+ name: string().min(1).max(120).optional(),
59331
+ payload: unknown().optional()
59332
+ }), TemplateWriteResultSchema, {
59333
+ kind: "mutation",
59334
+ auth: "admin"
59335
+ }), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
59336
+ kind: "mutation",
59337
+ auth: "admin"
59338
+ });
59339
+ /**
58830
59340
  * button — device-scoped capability for HA `button.*` / `input_button.*`
58831
59341
  * fire-only actuators. A button entity has no readable state — pressing it
58832
59342
  * triggers an action on the HA side. There is no `status` / `runtimeState`:
@@ -58848,6 +59358,56 @@ var buttonCapability = {
58848
59358
  }) }
58849
59359
  };
58850
59360
  /**
59361
+ * A pulse train in signed microseconds: mark (+) first, then space (-), strictly
59362
+ * alternating, no zeros, 2..1024 entries.
59363
+ */
59364
+ var IrTimingsSchema = array(number().int()).min(2).max(1024).superRefine((timings, ctx) => {
59365
+ timings.forEach((value, index) => {
59366
+ const expectMark = index % 2 === 0;
59367
+ if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
59368
+ code: "custom",
59369
+ message: `timing ${index} must be a non-zero ${expectMark ? "mark (positive)" : "space (negative)"}`,
59370
+ path: [index]
59371
+ });
59372
+ });
59373
+ });
59374
+ /** Per-command transmit options. Every key is optional; a transmitter applies what it can. */
59375
+ var TransmitSettingsSchema = object({
59376
+ carrierHz: number().int().positive().optional(),
59377
+ dutyCyclePercent: number().int().min(1).max(100).optional(),
59378
+ repeat: number().int().min(0).max(20).optional(),
59379
+ repeatDelayMs: number().int().min(0).optional()
59380
+ });
59381
+ /** The setting keys a transmitter may declare as settable (`getInfo.settable`). */
59382
+ var TransmitSettingKeySchema = _enum([
59383
+ "carrierHz",
59384
+ "dutyCyclePercent",
59385
+ "repeat",
59386
+ "repeatDelayMs"
59387
+ ]);
59388
+ var IrMediumSchema = _enum(["ir", "rf"]);
59389
+ var IrSendResultSchema = object({
59390
+ ok: literal(true),
59391
+ /** Settings the transmitter could not apply; the train was still sent. */
59392
+ ignoredSettings: array(string())
59393
+ });
59394
+ var IrTransmitterInfoSchema = object({
59395
+ media: array(IrMediumSchema),
59396
+ settable: array(TransmitSettingKeySchema),
59397
+ maxTimings: number().int().positive()
59398
+ });
59399
+ DeviceType.Generic, method(object({
59400
+ deviceId: number().int().nonnegative(),
59401
+ timings: IrTimingsSchema,
59402
+ settings: TransmitSettingsSchema.optional()
59403
+ }), IrSendResultSchema, {
59404
+ kind: "mutation",
59405
+ auth: "protected"
59406
+ }), method(object({ deviceId: number().int().nonnegative() }), IrTransmitterInfoSchema, {
59407
+ kind: "query",
59408
+ auth: "protected"
59409
+ });
59410
+ /**
58851
59411
  * `calendar-source` — collection cap exposing per-provider calendars and
58852
59412
  * their events. Every addon that contributes calendars (Home Assistant,
58853
59413
  * a future Google Calendar addon, …) registers a provider here. The hub
@@ -59807,14 +60367,13 @@ reset: method(object({
59807
60367
  auth: "admin"
59808
60368
  }) },
59809
60369
  /**
59810
- * A composed consumable (item added to a device with no native consumables
59811
- * provider) refuses `reset` by name: "Only meaningful when the item's
59812
- * `resettable` is true" — a composed item is never resettable until
59813
- * commands (`forward`) arrive in slice 3 (D663).
60370
+ * A consumable ADDED to an existing device (a graft) refuses `reset` by
60371
+ * name: only a composed device of a NEW type forwards an item's reset to a
60372
+ * source button, and marks that item `resettable` (D967).
59814
60373
  */
59815
60374
  composedMethods: { reset: {
59816
60375
  kind: "refuse",
59817
- reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
60376
+ reason: "a consumable added to an existing device is not resettable — only a new composed device forwards a reset"
59818
60377
  } },
59819
60378
  status: {
59820
60379
  schema: ConsumablesStatusSchema,
@@ -60408,7 +60967,7 @@ var DishwasherStatusSchema = object({
60408
60967
  /** Ms epoch of the last value received from the appliance; 0 = never. */
60409
60968
  lastFetchedAt: number()
60410
60969
  });
60411
- var DeviceIdSchema = number().int().nonnegative();
60970
+ var DeviceIdSchema$1 = number().int().nonnegative();
60412
60971
  /** A value a program option or setting can take. */
60413
60972
  var DishwasherValueSchema = union([
60414
60973
  boolean(),
@@ -60483,77 +61042,77 @@ var dishwasherCapability = {
60483
61042
  * option feature keys; each is checked against the program's constraints.
60484
61043
  */
60485
61044
  start: method(object({
60486
- deviceId: DeviceIdSchema,
61045
+ deviceId: DeviceIdSchema$1,
60487
61046
  program: string().min(1).optional(),
60488
61047
  options: record(string(), DishwasherValueSchema).optional()
60489
61048
  }), _void(), {
60490
61049
  kind: "mutation",
60491
61050
  auth: "protected"
60492
61051
  }),
60493
- pause: method(object({ deviceId: DeviceIdSchema }), _void(), {
61052
+ pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60494
61053
  kind: "mutation",
60495
61054
  auth: "protected"
60496
61055
  }),
60497
- resume: method(object({ deviceId: DeviceIdSchema }), _void(), {
61056
+ resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60498
61057
  kind: "mutation",
60499
61058
  auth: "protected"
60500
61059
  }),
60501
- stop: method(object({ deviceId: DeviceIdSchema }), _void(), {
61060
+ stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
60502
61061
  kind: "mutation",
60503
61062
  auth: "protected"
60504
61063
  }),
60505
61064
  /** Select (not start) a program; refused when the appliance does not offer it. */
60506
61065
  setProgram: method(object({
60507
- deviceId: DeviceIdSchema,
61066
+ deviceId: DeviceIdSchema$1,
60508
61067
  program: string().min(1)
60509
61068
  }), _void(), {
60510
61069
  kind: "mutation",
60511
61070
  auth: "protected"
60512
61071
  }),
60513
61072
  setPower: method(object({
60514
- deviceId: DeviceIdSchema,
61073
+ deviceId: DeviceIdSchema$1,
60515
61074
  on: boolean()
60516
61075
  }), _void(), {
60517
61076
  kind: "mutation",
60518
61077
  auth: "protected"
60519
61078
  }),
60520
61079
  setHalfLoad: method(object({
60521
- deviceId: DeviceIdSchema,
61080
+ deviceId: DeviceIdSchema$1,
60522
61081
  on: boolean()
60523
61082
  }), _void(), {
60524
61083
  kind: "mutation",
60525
61084
  auth: "protected"
60526
61085
  }),
60527
61086
  setHygienePlus: method(object({
60528
- deviceId: DeviceIdSchema,
61087
+ deviceId: DeviceIdSchema$1,
60529
61088
  on: boolean()
60530
61089
  }), _void(), {
60531
61090
  kind: "mutation",
60532
61091
  auth: "protected"
60533
61092
  }),
60534
61093
  setExtraDry: method(object({
60535
- deviceId: DeviceIdSchema,
61094
+ deviceId: DeviceIdSchema$1,
60536
61095
  on: boolean()
60537
61096
  }), _void(), {
60538
61097
  kind: "mutation",
60539
61098
  auth: "protected"
60540
61099
  }),
60541
61100
  setIntensiveZone: method(object({
60542
- deviceId: DeviceIdSchema,
61101
+ deviceId: DeviceIdSchema$1,
60543
61102
  on: boolean()
60544
61103
  }), _void(), {
60545
61104
  kind: "mutation",
60546
61105
  auth: "protected"
60547
61106
  }),
60548
61107
  setVarioSpeedPlus: method(object({
60549
- deviceId: DeviceIdSchema,
61108
+ deviceId: DeviceIdSchema$1,
60550
61109
  on: boolean()
60551
61110
  }), _void(), {
60552
61111
  kind: "mutation",
60553
61112
  auth: "protected"
60554
61113
  }),
60555
61114
  setChildLock: method(object({
60556
- deviceId: DeviceIdSchema,
61115
+ deviceId: DeviceIdSchema$1,
60557
61116
  on: boolean()
60558
61117
  }), _void(), {
60559
61118
  kind: "mutation",
@@ -60565,7 +61124,7 @@ var dishwasherCapability = {
60565
61124
  * its declared range / members.
60566
61125
  */
60567
61126
  setOption: method(object({
60568
- deviceId: DeviceIdSchema,
61127
+ deviceId: DeviceIdSchema$1,
60569
61128
  key: string().min(1),
60570
61129
  value: DishwasherValueSchema
60571
61130
  }), _void(), {
@@ -60573,7 +61132,7 @@ var dishwasherCapability = {
60573
61132
  auth: "protected"
60574
61133
  }),
60575
61134
  /** What this appliance can do, from its own description — the UI is derived from it. */
60576
- getOptions: method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
61135
+ getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
60577
61136
  },
60578
61137
  status: {
60579
61138
  schema: DishwasherStatusSchema,
@@ -62944,9 +63503,35 @@ var MediaInfoSchema = object({
62944
63503
  /** Optional cover-art / thumbnail URL. */
62945
63504
  imageUrl: string().optional()
62946
63505
  });
63506
+ /**
63507
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
63508
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
63509
+ * tracks what it last SENT; it moves only after a send was accepted, and a
63510
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
63511
+ * guess, never as a reading.
63512
+ */
63513
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
63514
+ /** The fields of a media-player status a driver can only guess. */
63515
+ var MediaPlayerAssumedFieldSchema = _enum([
63516
+ "state",
63517
+ "volumeLevel",
63518
+ "isMuted",
63519
+ "source"
63520
+ ]);
62947
63521
  var MediaPlayerStatusSchema = object({
62948
63522
  /** Playback lifecycle state. */
62949
63523
  state: MediaPlayerStateSchema,
63524
+ /**
63525
+ * Whether the state fields are readings or the driver's own bookkeeping.
63526
+ * A slice written before this field existed parses as `reported`.
63527
+ */
63528
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
63529
+ /**
63530
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
63531
+ * this is not empty; a field not listed here is a reading even then (power
63532
+ * from a running-state source, mute guessed from the last send).
63533
+ */
63534
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
62950
63535
  /** Volume as 0..100 inclusive. Null when the device has no volume
62951
63536
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62952
63537
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -64238,7 +64823,8 @@ var TopologyNodeSchema = object({
64238
64823
  isHub: boolean(),
64239
64824
  isOnline: boolean(),
64240
64825
  cpuPercent: number(),
64241
- memoryPercent: number(),
64826
+ /** `null` when the node's memory read failed or it has not reported one (D393). */
64827
+ memoryPercent: number().nullable(),
64242
64828
  uptime: number(),
64243
64829
  lastSeen: string(),
64244
64830
  localIps: array(string()).readonly(),
@@ -64856,9 +65442,10 @@ var PetFeederStatusSchema = object({
64856
65442
  * combined reading; per-hopper levels live in `food1`/`food2`. Null when
64857
65443
  * the device has not reported yet. */
64858
65444
  foodLevel: number().nullable().meta(readingMeta("none")),
64859
- /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
64860
- * feeder's own units. Null when the model reports none. */
64861
- bowlFood: number().nullable().meta(readingMeta("none")),
65445
+ /** Food in the BOWL, a mass (stored in kilograms, D944). Reported only by a
65446
+ * feeder with a bowl scale; null when the model has none or its reading is
65447
+ * of an unproven unit. */
65448
+ bowlFood: number().nullable().meta(readingMeta("mass")),
64862
65449
  /** True while the pet is eating (`state.eating`). */
64863
65450
  eating: boolean(),
64864
65451
  /** Whether `markFoodReplenished` does anything on this model. When false
@@ -64874,9 +65461,14 @@ var PetFeederStatusSchema = object({
64874
65461
  * (`device_class: problem`, on = low). True when the tank reports empty,
64875
65462
  * or, on a dual-hopper model, when EITHER hopper is empty. */
64876
65463
  lowFood: boolean(),
64877
- /** Battery charge 0..100 (%). Null on mains-powered models or when the
64878
- * device has no battery reading. */
65464
+ /** Battery charge 0..100 (%). On a feeder running on `mains` this is the
65465
+ * BACKUP battery's charge, not the power source. Null when the device has
65466
+ * no battery reading. */
64879
65467
  batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65468
+ /** What the feeder is powered from right now: `mains` (plugged in, the
65469
+ * battery is only a backup) or `battery`. Null when the device does not
65470
+ * report it. */
65471
+ powerSource: _enum(["mains", "battery"]).nullable(),
64880
65472
  /**
64881
65473
  * Desiccant life remaining, stored in seconds (duration base, D944).
64882
65474
  * Providers convert from the days the feeder reports. Null when the
@@ -64904,6 +65496,14 @@ var PetFeederStatusSchema = object({
64904
65496
  feedSound: boolean(),
64905
65497
  /** Speaker / prompt volume level (device-scaled integer). */
64906
65498
  volume: number().meta(readingMeta("none")),
65499
+ /** Portion (grams) of the last manual feed sent from a single-hopper
65500
+ * stepper — the default the next manual feed starts from. Persisted per
65501
+ * device by the provider; null until a manual feed has been sent. */
65502
+ lastFeedGrams: gramsPortion.nullable(),
65503
+ /** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
65504
+ lastFeedHopper1: gramsPortion.nullable(),
65505
+ /** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
65506
+ lastFeedHopper2: gramsPortion.nullable(),
64907
65507
  /** Ms epoch when the slice was last refreshed from the cloud. */
64908
65508
  lastFetchedAt: number()
64909
65509
  });
@@ -65029,6 +65629,137 @@ var petFeederCapability = {
65029
65629
  }
65030
65630
  } }
65031
65631
  };
65632
+ /**
65633
+ * Pet water-fountain cap. Models the telemetry and the control surface of a
65634
+ * circulating drinking fountain: how much water is in the tank, how much the
65635
+ * pet drank, whether the pump runs and on what duty cycle.
65636
+ *
65637
+ * It has no native vendor provider of its own: a fountain reaches CamStack as
65638
+ * a container of loose entities (a pump switch, two number boxes, a "filter
65639
+ * changed" button), and an operator BUILDS the device from them — a composed
65640
+ * device of this type (D659, D967). Every method is therefore a forward to ONE
65641
+ * method of ONE source entity, and every reading is a field derived from a
65642
+ * source entity; the provider-shaped interface below exists so the composed
65643
+ * device and any future native provider answer the same contract.
65644
+ *
65645
+ * Quantities are stored in the base unit of their kind (D944): volumes in
65646
+ * LITRES, durations in SECONDS, the tank level in percent. A source that
65647
+ * reports millilitres or minutes is converted in the composition's expression,
65648
+ * never here. Every reading may be `null`: a source that is offline or does
65649
+ * not report is unknown, never zero.
65650
+ *
65651
+ * The filter is not a field of this cap: it is an item of the `consumables`
65652
+ * cap on the same device (kind `wear`), reset by the same forward mechanism.
65653
+ */
65654
+ var PetFountainStatusSchema = object({
65655
+ /** Water in the tank, 0..100 (%). Null while the source has not reported. */
65656
+ waterLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65657
+ /** Water in the tank, in litres. Null while the source has not reported. */
65658
+ waterVolume: number().min(0).nullable().meta(readingMeta("volume")),
65659
+ /** True when the tank is empty. Null while unknown. */
65660
+ empty: boolean().nullable(),
65661
+ /** True while the pump is running. Null while unknown. */
65662
+ pumpRunning: boolean().nullable(),
65663
+ /** True when the pump works on an on/off duty cycle rather than continuously. */
65664
+ pumpIntervalEnabled: boolean().nullable(),
65665
+ /** How long the pump runs per cycle, in seconds. */
65666
+ pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
65667
+ /** How long the pump rests per cycle, in seconds. */
65668
+ pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
65669
+ /** Water drunk in the last drink, in litres. */
65670
+ lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
65671
+ /** Water drunk in the current hour, in litres. */
65672
+ currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
65673
+ /** Water drunk over the last 24 hours (rolling), in litres. */
65674
+ last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
65675
+ /** Water drunk since local midnight, in litres. */
65676
+ todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
65677
+ /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
65678
+ error: string().nullable(),
65679
+ /** Ms epoch when the slice was last refreshed. */
65680
+ lastFetchedAt: number()
65681
+ });
65682
+ var DeviceIdSchema = number().int().nonnegative();
65683
+ var MinutesSchema = number().int().min(0);
65684
+ var petFountainCapability = {
65685
+ name: "pet-fountain",
65686
+ scope: "device",
65687
+ deviceNative: true,
65688
+ mode: "singleton",
65689
+ deviceTypes: [DeviceType.PetFountain],
65690
+ methods: {
65691
+ /** Start or stop the pump. */
65692
+ setPump: method(object({
65693
+ deviceId: DeviceIdSchema,
65694
+ on: boolean()
65695
+ }), _void(), {
65696
+ kind: "mutation",
65697
+ auth: "protected"
65698
+ }),
65699
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
65700
+ setPumpInterval: method(object({
65701
+ deviceId: DeviceIdSchema,
65702
+ on: boolean()
65703
+ }), _void(), {
65704
+ kind: "mutation",
65705
+ auth: "protected"
65706
+ }),
65707
+ /** Set how many minutes the pump runs per cycle. */
65708
+ setPumpOnMinutes: method(object({
65709
+ deviceId: DeviceIdSchema,
65710
+ minutes: MinutesSchema
65711
+ }), _void(), {
65712
+ kind: "mutation",
65713
+ auth: "protected"
65714
+ }),
65715
+ /** Set how many minutes the pump rests per cycle. */
65716
+ setPumpOffMinutes: method(object({
65717
+ deviceId: DeviceIdSchema,
65718
+ minutes: MinutesSchema
65719
+ }), _void(), {
65720
+ kind: "mutation",
65721
+ auth: "protected"
65722
+ }),
65723
+ /** Clear the rolling daily consumption counters on the device. */
65724
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
65725
+ kind: "mutation",
65726
+ auth: "protected"
65727
+ })
65728
+ },
65729
+ status: {
65730
+ schema: PetFountainStatusSchema,
65731
+ kind: "push"
65732
+ },
65733
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
65734
+ runtimeState: PetFountainStatusSchema,
65735
+ /**
65736
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
65737
+ *
65738
+ * See `RuntimeStateDurability`. Enforced by
65739
+ * `scripts/check-runtime-state-durability.ts`.
65740
+ */
65741
+ durability: "session",
65742
+ /** History (D850): recorded series. */
65743
+ history: { fields: {
65744
+ waterLevel: {
65745
+ kind: "gauge",
65746
+ unit: "%",
65747
+ deadband: 1
65748
+ },
65749
+ waterVolume: {
65750
+ kind: "gauge",
65751
+ unit: "L"
65752
+ },
65753
+ todayDrink: {
65754
+ kind: "gauge",
65755
+ unit: "L"
65756
+ },
65757
+ last24hDrink: {
65758
+ kind: "gauge",
65759
+ unit: "L"
65760
+ }
65761
+ } }
65762
+ };
65032
65763
  var VehicleSchema = object({
65033
65764
  id: string(),
65034
65765
  name: string(),
@@ -70941,9 +71672,17 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
70941
71672
  kind: "mutation",
70942
71673
  auth: "admin"
70943
71674
  });
71675
+ discriminatedUnion("kind", [object({
71676
+ kind: literal("raw"),
71677
+ timings: array(number().int()),
71678
+ settings: TransmitSettingsSchema.optional()
71679
+ }), object({
71680
+ kind: literal("pronto"),
71681
+ code: string()
71682
+ })]);
70944
71683
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70945
71684
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70946
- 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;
71685
+ 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;
70947
71686
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70948
71687
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70949
71688
  new Set(CAP_NAMES);
@@ -71294,6 +72033,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
71294
72033
  notifier: notifierCapability,
71295
72034
  numericSensor: numericSensorCapability,
71296
72035
  petFeeder: petFeederCapability,
72036
+ petFountain: petFountainCapability,
71297
72037
  powerMeter: powerMeterCapability,
71298
72038
  presence: presenceCapability,
71299
72039
  pressureSensor: pressureSensorCapability,
@@ -72544,7 +73284,7 @@ var BaseDeviceProvider = class extends BaseAddon {
72544
73284
  return toDeviceSummary(device, this.addonId);
72545
73285
  }
72546
73286
  };
72547
- 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;
73287
+ 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;
72548
73288
  new Set(Object.values(DeviceType));
72549
73289
  Object.freeze({
72550
73290
  "accessories.getStatus": {
@@ -74479,6 +75219,13 @@ Object.freeze({
74479
75219
  access: "create",
74480
75220
  role: "admin"
74481
75221
  },
75222
+ "deviceExport.exposeEntity": {
75223
+ capName: "device-export",
75224
+ capScope: "system",
75225
+ addonId: null,
75226
+ access: "create",
75227
+ role: "admin"
75228
+ },
74482
75229
  "deviceExport.getDeviceLiveContribution": {
74483
75230
  capName: "device-export",
74484
75231
  capScope: "system",
@@ -74507,6 +75254,13 @@ Object.freeze({
74507
75254
  access: "view",
74508
75255
  role: "admin"
74509
75256
  },
75257
+ "deviceExport.listExposedEntities": {
75258
+ capName: "device-export",
75259
+ capScope: "system",
75260
+ addonId: null,
75261
+ access: "view",
75262
+ role: "admin"
75263
+ },
74510
75264
  "deviceExport.listSupportedDeviceKinds": {
74511
75265
  capName: "device-export",
74512
75266
  capScope: "system",
@@ -74514,6 +75268,13 @@ Object.freeze({
74514
75268
  access: "view",
74515
75269
  role: "admin"
74516
75270
  },
75271
+ "deviceExport.listSupportedEntityKinds": {
75272
+ capName: "device-export",
75273
+ capScope: "system",
75274
+ addonId: null,
75275
+ access: "view",
75276
+ role: "admin"
75277
+ },
74517
75278
  "deviceExport.unexposeDevice": {
74518
75279
  capName: "device-export",
74519
75280
  capScope: "system",
@@ -74521,6 +75282,13 @@ Object.freeze({
74521
75282
  access: "create",
74522
75283
  role: "admin"
74523
75284
  },
75285
+ "deviceExport.unexposeEntity": {
75286
+ capName: "device-export",
75287
+ capScope: "system",
75288
+ addonId: null,
75289
+ access: "create",
75290
+ role: "admin"
75291
+ },
74524
75292
  "deviceExtension.listCandidates": {
74525
75293
  capName: "device-extension",
74526
75294
  capScope: "device",
@@ -76096,6 +76864,20 @@ Object.freeze({
76096
76864
  access: "create",
76097
76865
  role: "viewer"
76098
76866
  },
76867
+ "irTransmitter.getInfo": {
76868
+ capName: "ir-transmitter",
76869
+ capScope: "device",
76870
+ addonId: null,
76871
+ access: "view",
76872
+ role: "viewer"
76873
+ },
76874
+ "irTransmitter.send": {
76875
+ capName: "ir-transmitter",
76876
+ capScope: "device",
76877
+ addonId: null,
76878
+ access: "create",
76879
+ role: "viewer"
76880
+ },
76099
76881
  "keychain.delete": {
76100
76882
  capName: "keychain",
76101
76883
  capScope: "system",
@@ -77328,6 +78110,13 @@ Object.freeze({
77328
78110
  access: "create",
77329
78111
  role: "admin"
77330
78112
  },
78113
+ "nodeRemovalHook.onNodeRemoved": {
78114
+ capName: "node-removal-hook",
78115
+ capScope: "system",
78116
+ addonId: null,
78117
+ access: "create",
78118
+ role: "admin"
78119
+ },
77331
78120
  "nodes.clusterAddonStatus": {
77332
78121
  capName: "nodes",
77333
78122
  capScope: "system",
@@ -77965,6 +78754,48 @@ Object.freeze({
77965
78754
  access: "create",
77966
78755
  role: "viewer"
77967
78756
  },
78757
+ "petFountain.getStatus": {
78758
+ capName: "pet-fountain",
78759
+ capScope: "device",
78760
+ addonId: null,
78761
+ access: "view",
78762
+ role: "viewer"
78763
+ },
78764
+ "petFountain.resetDailyConsumption": {
78765
+ capName: "pet-fountain",
78766
+ capScope: "device",
78767
+ addonId: null,
78768
+ access: "delete",
78769
+ role: "viewer"
78770
+ },
78771
+ "petFountain.setPump": {
78772
+ capName: "pet-fountain",
78773
+ capScope: "device",
78774
+ addonId: null,
78775
+ access: "create",
78776
+ role: "viewer"
78777
+ },
78778
+ "petFountain.setPumpInterval": {
78779
+ capName: "pet-fountain",
78780
+ capScope: "device",
78781
+ addonId: null,
78782
+ access: "create",
78783
+ role: "viewer"
78784
+ },
78785
+ "petFountain.setPumpOffMinutes": {
78786
+ capName: "pet-fountain",
78787
+ capScope: "device",
78788
+ addonId: null,
78789
+ access: "create",
78790
+ role: "viewer"
78791
+ },
78792
+ "petFountain.setPumpOnMinutes": {
78793
+ capName: "pet-fountain",
78794
+ capScope: "device",
78795
+ addonId: null,
78796
+ access: "create",
78797
+ role: "viewer"
78798
+ },
77968
78799
  "pipelineAnalytics.addRetrainFrameFromRecording": {
77969
78800
  capName: "pipeline-analytics",
77970
78801
  capScope: "device",
@@ -80219,6 +81050,41 @@ Object.freeze({
80219
81050
  access: "create",
80220
81051
  role: "admin"
80221
81052
  },
81053
+ "remote.getConfig": {
81054
+ capName: "remote",
81055
+ capScope: "system",
81056
+ addonId: null,
81057
+ access: "view",
81058
+ role: "admin"
81059
+ },
81060
+ "remote.listCommands": {
81061
+ capName: "remote",
81062
+ capScope: "system",
81063
+ addonId: null,
81064
+ access: "view",
81065
+ role: "admin"
81066
+ },
81067
+ "remote.sendCommand": {
81068
+ capName: "remote",
81069
+ capScope: "system",
81070
+ addonId: null,
81071
+ access: "create",
81072
+ role: "admin"
81073
+ },
81074
+ "remote.sendMediaCommand": {
81075
+ capName: "remote",
81076
+ capScope: "system",
81077
+ addonId: null,
81078
+ access: "create",
81079
+ role: "admin"
81080
+ },
81081
+ "remote.setConfig": {
81082
+ capName: "remote",
81083
+ capScope: "system",
81084
+ addonId: null,
81085
+ access: "create",
81086
+ role: "admin"
81087
+ },
80222
81088
  "runningState.getStatus": {
80223
81089
  capName: "running-state",
80224
81090
  capScope: "device",
@@ -80331,6 +81197,13 @@ Object.freeze({
80331
81197
  access: "create",
80332
81198
  role: "admin"
80333
81199
  },
81200
+ "serverManagement.getLaunchOverrides": {
81201
+ capName: "server-management",
81202
+ capScope: "system",
81203
+ addonId: null,
81204
+ access: "view",
81205
+ role: "admin"
81206
+ },
80334
81207
  "serverManagement.getServerPackageStatus": {
80335
81208
  capName: "server-management",
80336
81209
  capScope: "system",
@@ -80352,6 +81225,13 @@ Object.freeze({
80352
81225
  access: "create",
80353
81226
  role: "admin"
80354
81227
  },
81228
+ "serverManagement.setLaunchOverrides": {
81229
+ capName: "server-management",
81230
+ capScope: "system",
81231
+ addonId: null,
81232
+ access: "create",
81233
+ role: "admin"
81234
+ },
80355
81235
  "settingsStore.aggregate": {
80356
81236
  capName: "settings-store",
80357
81237
  capScope: "system",
@@ -81409,6 +82289,41 @@ Object.freeze({
81409
82289
  access: "view",
81410
82290
  role: "viewer"
81411
82291
  },
82292
+ "template.create": {
82293
+ capName: "template",
82294
+ capScope: "system",
82295
+ addonId: null,
82296
+ access: "create",
82297
+ role: "admin"
82298
+ },
82299
+ "template.delete": {
82300
+ capName: "template",
82301
+ capScope: "system",
82302
+ addonId: null,
82303
+ access: "delete",
82304
+ role: "admin"
82305
+ },
82306
+ "template.get": {
82307
+ capName: "template",
82308
+ capScope: "system",
82309
+ addonId: null,
82310
+ access: "view",
82311
+ role: "admin"
82312
+ },
82313
+ "template.list": {
82314
+ capName: "template",
82315
+ capScope: "system",
82316
+ addonId: null,
82317
+ access: "view",
82318
+ role: "admin"
82319
+ },
82320
+ "template.update": {
82321
+ capName: "template",
82322
+ capScope: "system",
82323
+ addonId: null,
82324
+ access: "create",
82325
+ role: "admin"
82326
+ },
81412
82327
  "terminalSession.adoptLegacyMonitor": {
81413
82328
  capName: "terminal-session",
81414
82329
  capScope: "system",
@@ -83719,6 +84634,11 @@ var ACTUATOR_CAP_VOCABULARY = {
83719
84634
  icon: "circle-dot"
83720
84635
  }]
83721
84636
  },
84637
+ "ir-transmitter": {
84638
+ scope: "device",
84639
+ noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
84640
+ notOperatorFacing: { send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it" }
84641
+ },
83722
84642
  control: {
83723
84643
  scope: "device",
83724
84644
  noun: t("Control", "Controllo"),
@@ -84372,6 +85292,100 @@ var APPLIANCE_CAP_VOCABULARY = {
84372
85292
  }
84373
85293
  ]
84374
85294
  },
85295
+ "pet-fountain": {
85296
+ scope: "device",
85297
+ noun: t("Pet fountain", "Fontana per animali"),
85298
+ actions: [
85299
+ {
85300
+ id: "petFountain.pumpOn",
85301
+ method: "setPump",
85302
+ label: t("Pump on", "Pompa accesa"),
85303
+ phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
85304
+ icon: "play",
85305
+ args: { on: true }
85306
+ },
85307
+ {
85308
+ id: "petFountain.pumpOff",
85309
+ method: "setPump",
85310
+ label: t("Pump off", "Pompa spenta"),
85311
+ phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
85312
+ icon: "square",
85313
+ args: { on: false }
85314
+ },
85315
+ {
85316
+ id: "petFountain.intervalOn",
85317
+ method: "setPumpInterval",
85318
+ label: t("Pump cycle on", "Ciclo pompa attivo"),
85319
+ phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
85320
+ icon: "timer",
85321
+ args: { on: true }
85322
+ },
85323
+ {
85324
+ id: "petFountain.intervalOff",
85325
+ method: "setPumpInterval",
85326
+ label: t("Pump cycle off", "Ciclo pompa spento"),
85327
+ phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
85328
+ icon: "timer-off",
85329
+ args: { on: false }
85330
+ },
85331
+ {
85332
+ id: "petFountain.pumpOnMinutes",
85333
+ method: "setPumpOnMinutes",
85334
+ label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
85335
+ phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
85336
+ icon: "timer",
85337
+ openArgs: [{
85338
+ key: "minutes",
85339
+ label: t("Minutes", "Minuti"),
85340
+ offer: {
85341
+ kind: "number",
85342
+ min: 0,
85343
+ step: 1,
85344
+ unit: "min"
85345
+ }
85346
+ }]
85347
+ },
85348
+ {
85349
+ id: "petFountain.pumpOffMinutes",
85350
+ method: "setPumpOffMinutes",
85351
+ label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
85352
+ phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
85353
+ icon: "timer-off",
85354
+ openArgs: [{
85355
+ key: "minutes",
85356
+ label: t("Minutes", "Minuti"),
85357
+ offer: {
85358
+ kind: "number",
85359
+ min: 0,
85360
+ step: 1,
85361
+ unit: "min"
85362
+ }
85363
+ }]
85364
+ },
85365
+ {
85366
+ id: "petFountain.resetDailyConsumption",
85367
+ method: "resetDailyConsumption",
85368
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
85369
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
85370
+ icon: "rotate-ccw"
85371
+ }
85372
+ ],
85373
+ state: [{
85374
+ kind: "boolean",
85375
+ field: "empty",
85376
+ label: t("Tank empty", "Serbatoio vuoto"),
85377
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
85378
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
85379
+ icon: "droplet"
85380
+ }, {
85381
+ kind: "numeric",
85382
+ field: "waterLevel",
85383
+ quantity: t("the water level", "il livello dell’acqua"),
85384
+ label: t("Water level", "Livello dell’acqua"),
85385
+ unit: "%",
85386
+ icon: "droplets"
85387
+ }]
85388
+ },
84375
85389
  "media-player": {
84376
85390
  scope: "device",
84377
85391
  noun: t("Media player", "Lettore multimediale"),
@@ -86196,6 +87210,7 @@ var SYSTEM_CAP_VOCABULARY = {
86196
87210
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
86197
87211
  "device-provider": system(INTERNAL),
86198
87212
  "device-removal-hook": system(INTERNAL),
87213
+ "node-removal-hook": system(INTERNAL),
86199
87214
  "device-state": system("the runtime-state mirror every trigger already reads through"),
86200
87215
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
86201
87216
  "embedding-encoder": system(ML),
@@ -86235,6 +87250,7 @@ var SYSTEM_CAP_VOCABULARY = {
86235
87250
  "recording-archive": system(STORAGE),
86236
87251
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
86237
87252
  records: system(INTERNAL),
87253
+ remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
86238
87254
  "server-management": system(CLUSTER),
86239
87255
  "settings-store": system(INTERNAL),
86240
87256
  "smtp-provider": system(INTERNAL),
@@ -86247,6 +87263,7 @@ var SYSTEM_CAP_VOCABULARY = {
86247
87263
  "storage-provider": system(STORAGE),
86248
87264
  "stream-broker": system(PIPELINE),
86249
87265
  system: system(CLUSTER),
87266
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
86250
87267
  "terminal-session": system(CLUSTER),
86251
87268
  toast: system(UI_PLUMBING),
86252
87269
  training: system(ML),
@@ -86306,7 +87323,7 @@ function projectAction(cap, action) {
86306
87323
  };
86307
87324
  }
86308
87325
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
86309
- 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;
87326
+ 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;
86310
87327
  DeviceFeature.BatteryOperated;
86311
87328
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
86312
87329
  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.";
@@ -87438,6 +88455,16 @@ Object.freeze({
87438
88455
  form: "single",
87439
88456
  optional: false
87440
88457
  }],
88458
+ "irTransmitter.getInfo": [{
88459
+ name: "deviceId",
88460
+ form: "single",
88461
+ optional: false
88462
+ }],
88463
+ "irTransmitter.send": [{
88464
+ name: "deviceId",
88465
+ form: "single",
88466
+ optional: false
88467
+ }],
87441
88468
  "keychain.deleteDeviceScope": [{
87442
88469
  name: "deviceId",
87443
88470
  form: "single",
@@ -87847,6 +88874,36 @@ Object.freeze({
87847
88874
  form: "single",
87848
88875
  optional: false
87849
88876
  }],
88877
+ "petFountain.getStatus": [{
88878
+ name: "deviceId",
88879
+ form: "single",
88880
+ optional: false
88881
+ }],
88882
+ "petFountain.resetDailyConsumption": [{
88883
+ name: "deviceId",
88884
+ form: "single",
88885
+ optional: false
88886
+ }],
88887
+ "petFountain.setPump": [{
88888
+ name: "deviceId",
88889
+ form: "single",
88890
+ optional: false
88891
+ }],
88892
+ "petFountain.setPumpInterval": [{
88893
+ name: "deviceId",
88894
+ form: "single",
88895
+ optional: false
88896
+ }],
88897
+ "petFountain.setPumpOffMinutes": [{
88898
+ name: "deviceId",
88899
+ form: "single",
88900
+ optional: false
88901
+ }],
88902
+ "petFountain.setPumpOnMinutes": [{
88903
+ name: "deviceId",
88904
+ form: "single",
88905
+ optional: false
88906
+ }],
87850
88907
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
87851
88908
  name: "deviceId",
87852
88909
  form: "single",
@@ -88663,6 +89720,31 @@ Object.freeze({
88663
89720
  form: "single",
88664
89721
  optional: true
88665
89722
  }],
89723
+ "remote.getConfig": [{
89724
+ name: "deviceId",
89725
+ form: "single",
89726
+ optional: false
89727
+ }],
89728
+ "remote.listCommands": [{
89729
+ name: "deviceId",
89730
+ form: "single",
89731
+ optional: false
89732
+ }],
89733
+ "remote.sendCommand": [{
89734
+ name: "deviceId",
89735
+ form: "single",
89736
+ optional: false
89737
+ }],
89738
+ "remote.sendMediaCommand": [{
89739
+ name: "deviceId",
89740
+ form: "single",
89741
+ optional: false
89742
+ }],
89743
+ "remote.setConfig": [{
89744
+ name: "deviceId",
89745
+ form: "single",
89746
+ optional: false
89747
+ }],
88666
89748
  "runningState.getStatus": [{
88667
89749
  name: "deviceId",
88668
89750
  form: "single",
@@ -90897,6 +91979,81 @@ function rewriteFormBody(body, route) {
90897
91979
  function filterForwardedCookies(cookieHeader) {
90898
91980
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
90899
91981
  }
91982
+ /** A short stable label for the attempt: enough to group lines, useless as a capability. */
91983
+ function attemptLabel(attemptId) {
91984
+ let hash = 2166136261;
91985
+ for (let i = 0; i < attemptId.length; i += 1) hash = Math.imul(hash ^ attemptId.charCodeAt(i), 16777619) >>> 0;
91986
+ return hash.toString(16).padStart(8, "0");
91987
+ }
91988
+ /** Distinct parameter names of a `?a=1&b=2` string (or of a urlencoded body), in order. */
91989
+ function paramNames(query) {
91990
+ const names = [];
91991
+ const raw = query.startsWith("?") ? query.slice(1) : query;
91992
+ for (const pair of raw.split("&")) {
91993
+ if (pair.length === 0) continue;
91994
+ const eq = pair.indexOf("=");
91995
+ const rawName = eq === -1 ? pair : pair.slice(0, eq);
91996
+ let name;
91997
+ try {
91998
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
91999
+ } catch {
92000
+ name = rawName;
92001
+ }
92002
+ if (!names.includes(name)) names.push(name);
92003
+ }
92004
+ return names;
92005
+ }
92006
+ function hasOa2(names) {
92007
+ return names.some((name) => name.startsWith("openid.oa2.") || name === "openid.ns.oa2");
92008
+ }
92009
+ function cookieNamesOf(setCookies) {
92010
+ return setCookies.map((c) => c.split(";")[0]?.split("=")[0]?.trim() ?? "");
92011
+ }
92012
+ function mediaType(contentType) {
92013
+ return (contentType ?? "none").split(";")[0]?.trim().toLowerCase() || "none";
92014
+ }
92015
+ /** Host and path of a Location (no query), or 'unparsable'. */
92016
+ function locationShape(location, base) {
92017
+ try {
92018
+ const url = new URL(location, base);
92019
+ return {
92020
+ target: `${url.host}${url.pathname}`,
92021
+ names: paramNames(url.search)
92022
+ };
92023
+ } catch {
92024
+ return {
92025
+ target: "unparsable",
92026
+ names: []
92027
+ };
92028
+ }
92029
+ }
92030
+ /** The `meta` of one trace line. Values: never. */
92031
+ function flowTraceMeta(input) {
92032
+ const queryNames = paramNames(input.requestSearch);
92033
+ const formNames = input.formBody === null ? null : paramNames(input.formBody);
92034
+ const meta = {
92035
+ session: attemptLabel(input.attemptId),
92036
+ method: input.method,
92037
+ upstream: `${input.upstreamHost}${input.upstreamPath}`,
92038
+ queryNames: queryNames.join(",") || "none",
92039
+ oa2InQuery: hasOa2(queryNames),
92040
+ status: input.status,
92041
+ setCookieNames: cookieNamesOf(input.setCookies).join(",") || "none",
92042
+ cookiesSent: (input.cookieHeader ?? "").split(";").filter((p) => p.trim().length > 0).length
92043
+ };
92044
+ if (formNames !== null) {
92045
+ meta["contentType"] = mediaType(input.contentType);
92046
+ meta["formNames"] = formNames.join(",") || "none";
92047
+ meta["oa2InForm"] = hasOa2(formNames);
92048
+ }
92049
+ if (input.location !== null) {
92050
+ const loc = locationShape(input.location, `http://${input.upstreamHost}/`);
92051
+ meta["location"] = loc.target;
92052
+ meta["locationQueryNames"] = loc.names.join(",") || "none";
92053
+ meta["oa2InLocation"] = hasOa2(loc.names);
92054
+ }
92055
+ return meta;
92056
+ }
90900
92057
  var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
90901
92058
  var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
90902
92059
  /** Request headers that are never forwarded to the vendor. */
@@ -91001,6 +92158,8 @@ var VendorLoginProxy = class {
91001
92158
  #now;
91002
92159
  #fetch;
91003
92160
  #attempts = /* @__PURE__ */ new Map();
92161
+ /** Trace lines written per attempt (bounded by FLOW_TRACE_MAX_LINES). */
92162
+ #traced = /* @__PURE__ */ new Map();
91004
92163
  constructor(options) {
91005
92164
  this.#addonId = options.addonId;
91006
92165
  this.#logger = options.logger;
@@ -91027,6 +92186,7 @@ var VendorLoginProxy = class {
91027
92186
  }
91028
92187
  close(attemptId) {
91029
92188
  this.#attempts.delete(attemptId);
92189
+ this.#traced.delete(attemptId);
91030
92190
  }
91031
92191
  isOpen(attemptId) {
91032
92192
  const attempt = this.#attempts.get(attemptId);
@@ -91073,7 +92233,7 @@ var VendorLoginProxy = class {
91073
92233
  const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
91074
92234
  const attempt = this.#attempts.get(parsed.attemptId);
91075
92235
  if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
91076
- if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
92236
+ if (attempt !== void 0) this.close(parsed.attemptId);
91077
92237
  this.#logger.info("vendor login proxy: attempt refused", { meta: {
91078
92238
  addonId: this.#addonId,
91079
92239
  reason: attempt === void 0 ? "missing" : "expired"
@@ -91098,13 +92258,17 @@ var VendorLoginProxy = class {
91098
92258
  }
91099
92259
  const method = (req.method ?? "GET").toUpperCase();
91100
92260
  let body;
92261
+ let formBodyRaw = null;
91101
92262
  if (method !== "GET" && method !== "HEAD") {
91102
92263
  const read = await readRequestBody(req);
91103
92264
  if (read === "too-large") {
91104
92265
  sendPlain(res, 413, "Request too large");
91105
92266
  return;
91106
92267
  }
91107
- body = isFormEncoded(req.headers["content-type"]) ? Buffer.from(rewriteFormBody(read.toString("utf8"), route), "utf8") : read;
92268
+ if (isFormEncoded(req.headers["content-type"])) {
92269
+ formBodyRaw = read.toString("utf8");
92270
+ body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
92271
+ } else body = read;
91108
92272
  }
91109
92273
  const headers = this.#requestHeaders(req, route, upstream);
91110
92274
  const upstreamRes = await this.#fetch(target, {
@@ -91129,6 +92293,19 @@ var VendorLoginProxy = class {
91129
92293
  sentNames: cookieNames(headers["cookie"]),
91130
92294
  vendorSets: setCookies.map(setCookieShape).join(" | ")
91131
92295
  } });
92296
+ this.#trace(parsed.attemptId, {
92297
+ attemptId: parsed.attemptId,
92298
+ method,
92299
+ upstreamHost: upstream.host,
92300
+ upstreamPath: target.pathname,
92301
+ requestSearch: target.search,
92302
+ formBody: formBodyRaw,
92303
+ contentType: firstHeader(req.headers["content-type"]),
92304
+ status: upstreamRes.status,
92305
+ location,
92306
+ setCookies,
92307
+ cookieHeader: headers["cookie"]
92308
+ });
91132
92309
  const settled = await attempt.evaluate({
91133
92310
  status: upstreamRes.status,
91134
92311
  url: target.toString(),
@@ -91137,7 +92314,7 @@ var VendorLoginProxy = class {
91137
92314
  bodyText: text
91138
92315
  });
91139
92316
  if (settled.kind === "done") {
91140
- this.#attempts.delete(parsed.attemptId);
92317
+ this.close(parsed.attemptId);
91141
92318
  this.#logger.info("vendor login proxy: sign-in finished", { meta: {
91142
92319
  addonId: this.#addonId,
91143
92320
  status: settled.landing.status
@@ -91161,6 +92338,16 @@ var VendorLoginProxy = class {
91161
92338
  res.writeHead(upstreamRes.status, out);
91162
92339
  await this.#pipeBody(upstreamRes, res);
91163
92340
  }
92341
+ /** One names-only line per proxied request, at most FLOW_TRACE_MAX_LINES per attempt. */
92342
+ #trace(attemptId, input) {
92343
+ const written = this.#traced.get(attemptId) ?? 0;
92344
+ if (written >= 200) return;
92345
+ this.#traced.set(attemptId, written + 1);
92346
+ this.#logger.info("vendor login proxy: flow", { meta: {
92347
+ addonId: this.#addonId,
92348
+ ...flowTraceMeta(input)
92349
+ } });
92350
+ }
91164
92351
  #requestHeaders(req, route, upstream) {
91165
92352
  const headers = {};
91166
92353
  for (const [name, value] of Object.entries(req.headers)) {
@@ -91202,6 +92389,14 @@ var VendorLoginProxy = class {
91202
92389
  };
91203
92390
  var MB = 1024 * 1024;
91204
92391
  1024 * MB, 3072 * MB;
92392
+ object({
92393
+ manufacturer: string().optional(),
92394
+ supportedModels: array(string()).optional(),
92395
+ supportedController: string().optional(),
92396
+ commandsEncoding: string(),
92397
+ delay: number().nonnegative().optional(),
92398
+ commands: record(string(), unknown())
92399
+ });
91205
92400
  //#endregion
91206
92401
  //#region src/config.ts
91207
92402
  /**
@@ -132862,6 +134057,38 @@ function alexaRegionOptions(amazonDomain) {
132862
134057
  };
132863
134058
  }
132864
134059
  //#endregion
134060
+ //#region src/alexa-url-params.ts
134061
+ var BASE = "http://placeholder.invalid";
134062
+ function describeUrlParams(url) {
134063
+ let parsed;
134064
+ try {
134065
+ parsed = new URL(url, BASE);
134066
+ } catch {
134067
+ return {
134068
+ path: "",
134069
+ names: [],
134070
+ oa2: false,
134071
+ returnTo: false,
134072
+ assocHandle: false,
134073
+ mode: false
134074
+ };
134075
+ }
134076
+ const names = [...new Set(parsed.searchParams.keys())].sort();
134077
+ return {
134078
+ path: parsed.pathname,
134079
+ names,
134080
+ oa2: names.some((n) => n.startsWith("openid.oa2.")),
134081
+ returnTo: names.includes("openid.return_to"),
134082
+ assocHandle: names.includes("openid.assoc_handle"),
134083
+ mode: names.includes("openid.mode")
134084
+ };
134085
+ }
134086
+ /** One-line, value-free rendering of {@link describeUrlParams}. */
134087
+ function formatUrlParams(url) {
134088
+ const d = describeUrlParams(url);
134089
+ return `names=[${d.names.join(",")}] oa2=${d.oa2} return_to=${d.returnTo} assoc_handle=${d.assocHandle} mode=${d.mode}`;
134090
+ }
134091
+ //#endregion
132865
134092
  //#region src/alexa-proxy-trace.ts
132866
134093
  /**
132867
134094
  * Sanitised one-line trace of the alexa-cookie2 proxy's own log lines.
@@ -132882,17 +134109,21 @@ function stripQuery(target) {
132882
134109
  const cut = target.search(/[?#]/);
132883
134110
  return cut === -1 ? target : target.slice(0, cut);
132884
134111
  }
134112
+ /** The target without its query, plus the value-free parameter summary. */
134113
+ function withParams(target) {
134114
+ return `${stripQuery(target)} ${formatUrlParams(target)}`;
134115
+ }
132885
134116
  function sanitizeProxyTraceLine(line) {
132886
134117
  const router = ROUTER_RE.exec(line);
132887
- if (router !== null) return `router ${router[2]} ${stripQuery(router[1] ?? "")}`;
134118
+ if (router !== null) return `router ${router[2]} ${withParams(router[1] ?? "")}`;
132888
134119
  const request = PROXY_REQUEST_RE.exec(line);
132889
- if (request !== null) return `proxy-request ${request[1]} ${stripQuery(request[2] ?? "")}`;
134120
+ if (request !== null) return `proxy-request ${request[1]} ${withParams(request[2] ?? "")}`;
132890
134121
  const response = PROXY_RESPONSE_RE.exec(line);
132891
134122
  if (response !== null) return `proxy-response host=${response[1]}`;
132892
134123
  const original = REDIRECT_ORIGINAL_RE.exec(line);
132893
- if (original !== null) return `redirect-original ${stripQuery(original[1] ?? "")}`;
134124
+ if (original !== null) return `redirect-original ${withParams(original[1] ?? "")}`;
132894
134125
  const final = REDIRECT_FINAL_RE.exec(line);
132895
- if (final !== null) return `redirect-final ${stripQuery(final[1] ?? "")}`;
134126
+ if (final !== null) return `redirect-final ${withParams(final[1] ?? "")}`;
132896
134127
  return null;
132897
134128
  }
132898
134129
  //#endregion
@@ -133008,7 +134239,7 @@ var AlexaProxyLoginSession = class {
133008
134239
  const line = args.map(String).join(" ");
133009
134240
  const trace = sanitizeProxyTraceLine(line);
133010
134241
  if (trace !== null) {
133011
- this.#logger.info(`alexa proxy-login: ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
134242
+ this.#logger.info(`[alexa-login-trace] ${trace}`, { meta: { sessionId: this.#opts.sessionId } });
133012
134243
  return;
133013
134244
  }
133014
134245
  if (SECRET_LINE_RE.test(line)) return;
@@ -133140,20 +134371,36 @@ var ADDON_ID = "import-alexa";
133140
134371
  var ATTEMPT_TTL_MS = 600 * 1e3;
133141
134372
  /** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
133142
134373
  var CAPTURE_WAIT_MS = 6e4;
134374
+ /** The query parameter Amazon sets on `/ap/maplanding` when it honoured the oa2 request. */
134375
+ var AUTHORIZATION_CODE_PARAM = "openid.oa2.authorization_code";
133143
134376
  /**
133144
- * Terminal condition: alexa-cookie2 turns Amazon's final redirect into a 302 to its own
133145
- * `/cookie-success` page once it holds the login cookie. Matching on the path of the raw
133146
- * `Location` keeps this independent of the page's HTML.
134377
+ * Terminal condition. The login is over when alexa-cookie2 turns Amazon's final redirect into
134378
+ * a 302 to its own `/cookie-success` page (it only does so when the landing carries the oa2
134379
+ * authorization code), OR when Amazon's `/ap/maplanding` itself shows up as a `Location` or as
134380
+ * the request. A maplanding WITHOUT the authorization code is a dead end: alexa-cookie2 cannot
134381
+ * register a device from it and no cookie will ever come, so it ends at once as
134382
+ * `no_authorization_code` instead of after the capture wait. Only paths and parameter NAMES are
134383
+ * read, never a value (the query carries signed openid values).
133147
134384
  */
133148
134385
  function cookieSuccessTerminal(view) {
133149
- if (view.location === null) return { kind: "continue" };
133150
- try {
133151
- return new URL(view.location, `http://127.0.0.1`).pathname === "/cookie-success" ? {
134386
+ for (const raw of [view.url, view.location]) {
134387
+ if (raw === null) continue;
134388
+ const end = loginEnd(raw);
134389
+ if (end !== null) return {
133152
134390
  kind: "done",
133153
- payload: true
133154
- } : { kind: "continue" };
134391
+ payload: end
134392
+ };
134393
+ }
134394
+ return { kind: "continue" };
134395
+ }
134396
+ function loginEnd(raw) {
134397
+ try {
134398
+ const url = new URL(raw, `http://${ALEXA_PROXY_LOOPBACK_HOST}`);
134399
+ if (url.pathname === "/cookie-success") return "registering";
134400
+ if (!url.pathname.endsWith("/ap/maplanding")) return null;
134401
+ return url.searchParams.has(AUTHORIZATION_CODE_PARAM) ? "registering" : "no_authorization_code";
133155
134402
  } catch {
133156
- return { kind: "continue" };
134403
+ return null;
133157
134404
  }
133158
134405
  }
133159
134406
  var AlexaProxyLoginManager = class {
@@ -133170,6 +134417,13 @@ var AlexaProxyLoginManager = class {
133170
134417
  this.#getLoginProxy = deps.getLoginProxy;
133171
134418
  this.#captureWaitMs = deps.captureWaitMs ?? CAPTURE_WAIT_MS;
133172
134419
  }
134420
+ /** One value-free line per upstream hop the hub login proxy sees (names only). */
134421
+ #traceHop(sessionId, view) {
134422
+ const url = describeUrlParams(view.url);
134423
+ const landing = url.path.endsWith("/ap/maplanding") ? " maplanding" : "";
134424
+ const location = view.location === null ? "none" : `${describeUrlParams(view.location).path} ${formatUrlParams(view.location)}`;
134425
+ this.#logger.info(`[alexa-login-trace] hub-hop${landing} status=${view.status} url=${url.path} ${formatUrlParams(view.url)} location=${location}`, { meta: { sessionId } });
134426
+ }
133173
134427
  /**
133174
134428
  * Start a fresh proxy-login. Cancels any in-flight session first (single
133175
134429
  * fixed port). Returns the `loginUrl` for the onboarding UI to open.
@@ -133188,8 +134442,19 @@ var AlexaProxyLoginManager = class {
133188
134442
  host: upstreamHost,
133189
134443
  scheme: "http"
133190
134444
  }],
133191
- terminal: cookieSuccessTerminal,
133192
- onDone: async () => {
134445
+ terminal: (view) => {
134446
+ this.#traceHop(sessionId, view);
134447
+ return cookieSuccessTerminal(view);
134448
+ },
134449
+ onDone: async (end) => {
134450
+ if (end === "no_authorization_code") {
134451
+ this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });
134452
+ return {
134453
+ status: "error",
134454
+ addonId: ADDON_ID,
134455
+ reason: "no_authorization_code"
134456
+ };
134457
+ }
133193
134458
  return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
133194
134459
  status: "success",
133195
134460
  addonId: ADDON_ID