@camstack/addon-pipeline-orchestrator 1.2.640 → 1.2.641

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/index.mjs CHANGED
@@ -5591,7 +5591,7 @@ var ZodIssueCode = {
5591
5591
  var ZodFirstPartyTypeKind;
5592
5592
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5593
5593
  //#endregion
5594
- //#region ../types/dist/sleep-6eG9C8Xl.mjs
5594
+ //#region ../types/dist/sleep-CeyhrcOk.mjs
5595
5595
  /**
5596
5596
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5597
5597
  * window to float samples (D455).
@@ -7674,6 +7674,7 @@ var BaseAddon = class {
7674
7674
  * a warning and falls back rather than crashing boot.
7675
7675
  */
7676
7676
  state(key, schema, fallback) {
7677
+ this._declaredStateKeys.add(key);
7677
7678
  return createDurableState({
7678
7679
  key,
7679
7680
  schema,
@@ -7689,6 +7690,96 @@ var BaseAddon = class {
7689
7690
  } })
7690
7691
  });
7691
7692
  }
7693
+ /** Durable-state keys this process registered through {@link state}. */
7694
+ _declaredStateKeys = /* @__PURE__ */ new Set();
7695
+ /**
7696
+ * Stale-data audit: every global-settings key this addon still declares —
7697
+ * constructor defaults, settings-schema fields (layout containers included),
7698
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
7699
+ * here, in the addon's own process, so the hub keeps no copy of it.
7700
+ */
7701
+ async getDeclaredSettingsKeys() {
7702
+ return { keys: [...this.declaredKeySet()].toSorted() };
7703
+ }
7704
+ declaredKeySet() {
7705
+ const keys = new Set(Object.keys(this.defaults));
7706
+ const schema = this.globalSettingsSchema();
7707
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
7708
+ for (const key of this._declaredStateKeys) keys.add(key);
7709
+ for (const path of this.secretStatePaths) {
7710
+ const root = path.split(/[.[]/, 1)[0];
7711
+ if (root) keys.add(root);
7712
+ }
7713
+ return keys;
7714
+ }
7715
+ /**
7716
+ * Stale-data audit: remove stored global-settings keys the addon no longer
7717
+ * declares, and clear the keychain values their markers held — except a value
7718
+ * a surviving key still points at. A declared or absent key is refused.
7719
+ */
7720
+ async unsetStoredSettingsKeys(keys) {
7721
+ const settings = this._ctx?.settings;
7722
+ const raw = this.rawSettings();
7723
+ if (!settings || !raw) return {
7724
+ removed: [],
7725
+ refused: keys.map((key) => ({
7726
+ key,
7727
+ reason: "no settings store"
7728
+ })),
7729
+ secretsCleared: 0
7730
+ };
7731
+ let stored;
7732
+ try {
7733
+ stored = await raw.readAddonStore();
7734
+ } catch (error) {
7735
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
7736
+ return {
7737
+ removed: [],
7738
+ refused: keys.map((key) => ({
7739
+ key,
7740
+ reason
7741
+ })),
7742
+ secretsCleared: 0
7743
+ };
7744
+ }
7745
+ const declared = this.declaredKeySet();
7746
+ const removed = [];
7747
+ const refused = [];
7748
+ for (const key of keys) {
7749
+ const base = key.split("@", 1)[0] ?? key;
7750
+ if (declared.has(base)) refused.push({
7751
+ key,
7752
+ reason: "still declared by the addon"
7753
+ });
7754
+ else if (!(key in stored)) refused.push({
7755
+ key,
7756
+ reason: "not stored"
7757
+ });
7758
+ else removed.push(key);
7759
+ }
7760
+ if (removed.length === 0) return {
7761
+ removed,
7762
+ refused,
7763
+ secretsCleared: 0
7764
+ };
7765
+ const kept = /* @__PURE__ */ new Set();
7766
+ const held = /* @__PURE__ */ new Set();
7767
+ for (const [key, value] of Object.entries(stored)) {
7768
+ const target = removed.includes(key) ? held : kept;
7769
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
7770
+ }
7771
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
7772
+ const patch = {};
7773
+ for (const key of removed) patch[key] = void 0;
7774
+ await settings.writeAddonStore(patch);
7775
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
7776
+ await this.resolveConfig();
7777
+ return {
7778
+ removed,
7779
+ refused,
7780
+ secretsCleared
7781
+ };
7782
+ }
7692
7783
  /** Per-device variant of {@link state}, backed by the per-device store. */
7693
7784
  deviceState(deviceId, key, schema, fallback) {
7694
7785
  return createDurableState({
@@ -7959,6 +8050,21 @@ function dropPerNodeFields(fields) {
7959
8050
  }
7960
8051
  return kept;
7961
8052
  }
8053
+ function collectFieldKeys(fields) {
8054
+ const collected = [];
8055
+ for (const field of fields) {
8056
+ if (field.type === "group") {
8057
+ collected.push(...collectFieldKeys(field.fields));
8058
+ continue;
8059
+ }
8060
+ if (field.type === "sub-tabs") {
8061
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
8062
+ continue;
8063
+ }
8064
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8065
+ }
8066
+ return collected;
8067
+ }
7962
8068
  function collectPerNodeFieldKeys(fields) {
7963
8069
  const collected = [];
7964
8070
  for (const field of fields) {
@@ -9639,6 +9745,7 @@ var CAP_NAMES = Object.freeze([
9639
9745
  "recording-signal",
9640
9746
  "records",
9641
9747
  "remote",
9748
+ "remote-layout-components",
9642
9749
  "running-state",
9643
9750
  "scene-monitor",
9644
9751
  "script-runner",
@@ -11508,7 +11615,7 @@ function sleep(ms) {
11508
11615
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11509
11616
  }
11510
11617
  //#endregion
11511
- //#region ../types/dist/err-msg-DWXWc0Di.mjs
11618
+ //#region ../types/dist/err-msg-BD5buxvT.mjs
11512
11619
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11513
11620
  DeviceType["Camera"] = "camera";
11514
11621
  DeviceType["Hub"] = "hub";
@@ -11614,6 +11721,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11614
11721
  DeviceType["Washer"] = "washer";
11615
11722
  /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11616
11723
  DeviceType["Dryer"] = "dryer";
11724
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
11725
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
11726
+ * address, connection state) and the actions the box really supports
11727
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
11728
+ * adopted beside it — and its connection settings stay on the integration. */
11729
+ DeviceType["Gateway"] = "gateway";
11617
11730
  return DeviceType;
11618
11731
  }({});
11619
11732
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11874,7 +11987,7 @@ function errMsg$2(err) {
11874
11987
  return String(err);
11875
11988
  }
11876
11989
  //#endregion
11877
- //#region ../types/dist/composition-kSuOifuG.mjs
11990
+ //#region ../types/dist/composition-C7WdThmP.mjs
11878
11991
  /**
11879
11992
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11880
11993
  *
@@ -17343,8 +17456,172 @@ object({
17343
17456
  quantity: ReadingQuantitySchema,
17344
17457
  unit: string().min(1)
17345
17458
  });
17346
- /** How a leaf compares a device field to a value. Derived from the field's
17347
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
17459
+ new Set([
17460
+ {
17461
+ id: "person",
17462
+ name: "Person"
17463
+ },
17464
+ {
17465
+ id: "vehicle",
17466
+ name: "Vehicle"
17467
+ },
17468
+ {
17469
+ id: "animal",
17470
+ name: "Animal"
17471
+ },
17472
+ {
17473
+ id: "package",
17474
+ name: "Package"
17475
+ }
17476
+ ].map((l) => l.id));
17477
+ var COCO_TO_MACRO = {
17478
+ mapping: {
17479
+ person: "person",
17480
+ bicycle: "vehicle",
17481
+ car: "vehicle",
17482
+ motorcycle: "vehicle",
17483
+ airplane: "vehicle",
17484
+ bus: "vehicle",
17485
+ train: "vehicle",
17486
+ truck: "vehicle",
17487
+ boat: "vehicle",
17488
+ bird: "animal",
17489
+ cat: "animal",
17490
+ dog: "animal",
17491
+ horse: "animal",
17492
+ sheep: "animal",
17493
+ cow: "animal",
17494
+ elephant: "animal",
17495
+ bear: "animal",
17496
+ zebra: "animal",
17497
+ giraffe: "animal",
17498
+ suitcase: "package",
17499
+ backpack: "package",
17500
+ handbag: "package"
17501
+ },
17502
+ preserveOriginal: false
17503
+ };
17504
+ /**
17505
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
17506
+ * animal, package) plus an OPTIONAL label that narrows it.
17507
+ *
17508
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
17509
+ * second list. The label that fits depends on the macro class:
17510
+ *
17511
+ * - `animal` → a sub-class (`dog`, `cat`, …);
17512
+ * - `person` → a known face (an identity of the `face-gallery`);
17513
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
17514
+ * - `package` → a sub-class.
17515
+ *
17516
+ * `other` exists only so a class the taxonomy does not know (a custom model's
17517
+ * `forklift`) survives the conversion of a stored recipe without widening.
17518
+ *
17519
+ * A label is decided LATE (D683): face recognition and LPR land after the
17520
+ * detection starts. The track-lifecycle `update` event carries the newly
17521
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
17522
+ * the subject GETS it — not at the first detection.
17523
+ */
17524
+ var SubClassLabelSchema = object({
17525
+ kind: literal("sub-class"),
17526
+ className: string().min(1)
17527
+ });
17528
+ var IdentityLabelSchema = object({
17529
+ kind: literal("identity"),
17530
+ /** The `face-gallery` identity id (stable across a rename). */
17531
+ identityId: string().min(1),
17532
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
17533
+ name: string().min(1)
17534
+ });
17535
+ var PlateLabelSchema = object({
17536
+ kind: literal("plate"),
17537
+ text: string().min(1)
17538
+ });
17539
+ function macroOf$1(className) {
17540
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
17541
+ }
17542
+ /** A sub-class label must belong to the macro it is under. */
17543
+ function subClassBelongs(macro, label) {
17544
+ return label === void 0 || macroOf$1(label.className) === macro;
17545
+ }
17546
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
17547
+ var DetectionSubjectSchema = discriminatedUnion("macro", [
17548
+ object({
17549
+ macro: literal("animal"),
17550
+ label: subClassOf("animal").optional()
17551
+ }),
17552
+ object({
17553
+ macro: literal("person"),
17554
+ label: IdentityLabelSchema.optional()
17555
+ }),
17556
+ object({
17557
+ macro: literal("vehicle"),
17558
+ label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
17559
+ }),
17560
+ object({
17561
+ macro: literal("package"),
17562
+ label: subClassOf("package").optional()
17563
+ }),
17564
+ object({
17565
+ macro: literal("other"),
17566
+ className: string().min(1)
17567
+ })
17568
+ ]);
17569
+ /**
17570
+ * The subject of a PARKED object. The stationary registry's event carries a
17571
+ * class and a zone, no face and no plate — an identity or plate label there
17572
+ * could never match, so the schema refuses it instead of letting it sit dead.
17573
+ */
17574
+ var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
17575
+ /**
17576
+ * A stored class string as a subject: a macro name stays a macro; a raw class
17577
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
17578
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
17579
+ */
17580
+ function subjectFromClassName(className) {
17581
+ const name = className.trim().toLowerCase();
17582
+ const macro = COCO_TO_MACRO.mapping[name];
17583
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
17584
+ if (macro === void 0) return {
17585
+ macro: "other",
17586
+ className
17587
+ };
17588
+ const label = {
17589
+ kind: "sub-class",
17590
+ className: name
17591
+ };
17592
+ switch (macro) {
17593
+ case "animal": return {
17594
+ macro: "animal",
17595
+ label
17596
+ };
17597
+ case "vehicle": return {
17598
+ macro: "vehicle",
17599
+ label
17600
+ };
17601
+ case "package": return {
17602
+ macro: "package",
17603
+ label
17604
+ };
17605
+ default: return {
17606
+ macro: "other",
17607
+ className
17608
+ };
17609
+ }
17610
+ }
17611
+ function macroOnly(macro) {
17612
+ switch (macro) {
17613
+ case "animal": return { macro: "animal" };
17614
+ case "person": return { macro: "person" };
17615
+ case "vehicle": return { macro: "vehicle" };
17616
+ case "package": return { macro: "package" };
17617
+ default: return {
17618
+ macro: "other",
17619
+ className: macro
17620
+ };
17621
+ }
17622
+ }
17623
+ /** How a state leaf compares a device field to a value. Derived from the
17624
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
17348
17625
  var AutomationConditionOperatorSchema = _enum([
17349
17626
  "eq",
17350
17627
  "ne",
@@ -17355,12 +17632,76 @@ var AutomationConditionOperatorSchema = _enum([
17355
17632
  "contains",
17356
17633
  "in"
17357
17634
  ]);
17358
- var AutomationConditionLeafSchema = object({
17359
- kind: literal("condition"),
17635
+ /**
17636
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
17637
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
17638
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
17639
+ * fire; it does not lose it.
17640
+ */
17641
+ var DeviceBecomesLeafSchema = object({
17642
+ kind: literal("device-becomes"),
17643
+ deviceId: number().int().nonnegative(),
17644
+ cap: string().min(1),
17645
+ fieldPath: string().min(1),
17646
+ /** Fire when the field takes this value. Omit to fire on any change. */
17647
+ becomes: union([
17648
+ string(),
17649
+ number(),
17650
+ boolean()
17651
+ ]).optional(),
17652
+ /** Only on a CHANGE of value, not on every re-report. */
17653
+ edge: boolean().optional(),
17654
+ /** The new value must hold this long before it counts. */
17655
+ forMs: number().int().min(0).max(864e5).optional(),
17656
+ /** Collapse a burst into one occurrence. */
17657
+ debounceMs: number().int().min(0).max(6e5).optional()
17658
+ });
17659
+ var DeviceEventLeafSchema = object({
17660
+ kind: literal("device-event"),
17661
+ /** An `EventCategory` value. */
17662
+ category: string().min(1),
17663
+ deviceId: number().int().nonnegative().optional()
17664
+ });
17665
+ var ScheduleLeafSchema = object({
17666
+ kind: literal("schedule"),
17667
+ cron: string().min(1).max(120)
17668
+ });
17669
+ var ManualLeafSchema = object({ kind: literal("manual") });
17670
+ var IntervalLeafSchema = object({
17671
+ kind: literal("interval"),
17672
+ minutes: number().int().min(1).max(1440)
17673
+ });
17674
+ var DetectionLeafSchema = object({
17675
+ kind: literal("detection"),
17676
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
17677
+ subjects: array(DetectionSubjectSchema).min(1),
17678
+ deviceIds: array(number().int().nonnegative()).min(1),
17679
+ /** The track must have visited at least one of these zones. */
17680
+ zones: array(string().min(1)).min(1).optional(),
17681
+ /** Best-so-far confidence the track must reach. */
17682
+ minScore: number().min(0).max(1).optional()
17683
+ });
17684
+ var StationaryLeafSchema = object({
17685
+ kind: literal("stationary"),
17686
+ deviceIds: array(number().int().nonnegative()).min(1),
17687
+ subjects: array(StationarySubjectSchema).min(1).optional(),
17688
+ zones: array(string().min(1)).min(1).optional(),
17689
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
17690
+ phase: _enum(["appeared", "departed"])
17691
+ });
17692
+ /** "is": one device field compared to one value, NOW. */
17693
+ var DeviceIsLeafSchema = object({
17694
+ kind: literal("device-is"),
17360
17695
  deviceId: number().int().nonnegative(),
17361
17696
  cap: string().min(1),
17362
17697
  fieldPath: string().min(1),
17363
17698
  operator: AutomationConditionOperatorSchema,
17699
+ /**
17700
+ * The value. A number on a quantity field is stored in the field's base unit
17701
+ * (D944); a quantity value is the WRITE form, which the save boundary
17702
+ * replaces with the converted number and `unit`. A leaf that still carries
17703
+ * one is never evaluated (it fails).
17704
+ */
17364
17705
  value: union([
17365
17706
  string(),
17366
17707
  number(),
@@ -17368,24 +17709,26 @@ var AutomationConditionLeafSchema = object({
17368
17709
  array(union([string(), number()])),
17369
17710
  QuantityValueSchema
17370
17711
  ]),
17712
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
17371
17713
  unit: string().min(1).optional()
17372
17714
  });
17373
- var AutomationConditionStationaryPresentSchema = object({
17715
+ /**
17716
+ * True when the camera currently has a PARKED object matching `subjects` in at
17717
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
17718
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
17719
+ */
17720
+ var StationaryPresentLeafSchema = object({
17374
17721
  kind: literal("stationary-present"),
17375
17722
  deviceId: number().int().nonnegative(),
17376
- /** Class names; a macro class matches every raw class that maps to it. */
17377
- classes: array(string().min(1)).min(1).optional(),
17378
- /** The parked object must be in at least one of these zones. */
17723
+ subjects: array(StationarySubjectSchema).min(1).optional(),
17379
17724
  zones: array(string().min(1)).min(1).optional()
17380
17725
  });
17381
17726
  /**
17382
- * The expression leaf, declared as a plain object rather than an intersection
17383
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
17384
- * read `kind` off each option, and an intersection hides it. The author-time
17385
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
17386
- * cannot drift — an expression that one accepts, the other accepts.
17727
+ * The escape hatch: the bounded expression engine over named device bindings.
17728
+ * Declared as a plain object so a discriminated union can read `kind`; the
17729
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
17387
17730
  */
17388
- var AutomationConditionExpressionSchema = object({
17731
+ var ExpressionLeafSchema = object({
17389
17732
  kind: literal("expression"),
17390
17733
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17391
17734
  bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -17397,55 +17740,62 @@ var AutomationConditionExpressionSchema = object({
17397
17740
  path: ["expr"]
17398
17741
  });
17399
17742
  });
17400
- var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
17743
+ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
17401
17744
  object({
17402
17745
  kind: literal("all"),
17403
- children: array(AutomationConditionSchema)
17746
+ children: array(AutomationWhenSchema)
17404
17747
  }),
17405
17748
  object({
17406
17749
  kind: literal("any"),
17407
- children: array(AutomationConditionSchema)
17750
+ children: array(AutomationWhenSchema)
17408
17751
  }),
17409
17752
  object({
17410
17753
  kind: literal("not"),
17411
- child: AutomationConditionSchema
17754
+ child: AutomationWhenSchema
17412
17755
  }),
17413
- AutomationConditionLeafSchema,
17414
- AutomationConditionExpressionSchema,
17415
- AutomationConditionStationaryPresentSchema
17756
+ DeviceBecomesLeafSchema,
17757
+ DeviceEventLeafSchema,
17758
+ ScheduleLeafSchema,
17759
+ ManualLeafSchema,
17760
+ IntervalLeafSchema,
17761
+ DetectionLeafSchema,
17762
+ StationaryLeafSchema,
17763
+ DeviceIsLeafSchema,
17764
+ StationaryPresentLeafSchema,
17765
+ ExpressionLeafSchema
17416
17766
  ]));
17417
17767
  /**
17418
- * What starts a run.
17419
- *
17420
- * D8 compliance, and it is the reason `device-state` is not merely an event
17421
- * subscription: the trigger evaluates against the **state mirror**, which is
17422
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
17423
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
17424
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
17425
- * not.
17426
- */
17427
- var AutomationTriggerSchema = discriminatedUnion("kind", [
17768
+ * The recipe shape before the single `when` tree: a flat list of triggers
17769
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
17770
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
17771
+ * older client still posts — are in this shape, so it stays READABLE here, and
17772
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
17773
+ * way. Nothing writes this shape any more.
17774
+ *
17775
+ * `{ triggers: [t1…tn], conditions?: C }`
17776
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
17777
+ * a single trigger is not wrapped in `any`.
17778
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
17779
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
17780
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
17781
+ */
17782
+ var LegacyTriggerSchema = discriminatedUnion("kind", [
17428
17783
  object({
17429
17784
  kind: literal("device-state"),
17430
17785
  deviceId: number().int().nonnegative(),
17431
17786
  cap: string().min(1),
17432
17787
  fieldPath: string().min(1),
17433
- /** Fire when the field takes this value. Omit to fire on any change. */
17434
17788
  becomes: union([
17435
17789
  string(),
17436
17790
  number(),
17437
17791
  boolean()
17438
17792
  ]).optional(),
17439
- /** Only on a CHANGE of value, not on every re-report. */
17440
17793
  edge: boolean().optional(),
17441
- /** The condition must hold this long before the run starts. */
17442
17794
  forMs: number().int().min(0).max(864e5).optional(),
17443
- /** Collapse a burst into one run. */
17444
17795
  debounceMs: number().int().min(0).max(6e5).optional()
17445
17796
  }),
17446
17797
  object({
17447
17798
  kind: literal("device-event"),
17448
- /** An `EventCategory` value. */
17449
17799
  category: string().min(1),
17450
17800
  deviceId: number().int().nonnegative().optional()
17451
17801
  }),
@@ -17456,25 +17806,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
17456
17806
  object({ kind: literal("manual") }),
17457
17807
  object({
17458
17808
  kind: literal("detection"),
17459
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
17460
- * matches every raw class that maps to it; any other name matches
17461
- * exactly. */
17462
17809
  classes: array(string().min(1)).min(1),
17463
17810
  deviceIds: array(number().int().nonnegative()).min(1),
17464
- /** The track must have visited at least one of these zones. */
17465
17811
  zones: array(string().min(1)).min(1).optional(),
17466
- /** Best-so-far confidence the track must reach. */
17467
17812
  minScore: number().min(0).max(1).optional()
17468
17813
  }),
17469
17814
  object({
17470
17815
  kind: literal("stationary"),
17471
17816
  deviceIds: array(number().int().nonnegative()).min(1),
17472
- /** Class names; a macro class matches like the detection trigger. */
17473
17817
  classes: array(string().min(1)).min(1).optional(),
17474
- /** The parked object must be in at least one of these zones. */
17475
17818
  zones: array(string().min(1)).min(1).optional(),
17476
- /** `appeared`: it became parked. `departed`: the registry retired it.
17477
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
17478
17819
  phase: _enum(["appeared", "departed"])
17479
17820
  }),
17480
17821
  object({
@@ -17482,6 +17823,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
17482
17823
  minutes: number().int().min(1).max(1440)
17483
17824
  })
17484
17825
  ]);
17826
+ var LegacyConditionLeafSchema = object({
17827
+ kind: literal("condition"),
17828
+ deviceId: number().int().nonnegative(),
17829
+ cap: string().min(1),
17830
+ fieldPath: string().min(1),
17831
+ operator: AutomationConditionOperatorSchema,
17832
+ value: union([
17833
+ string(),
17834
+ number(),
17835
+ boolean(),
17836
+ array(union([string(), number()])),
17837
+ QuantityValueSchema
17838
+ ]),
17839
+ unit: string().min(1).optional()
17840
+ });
17841
+ var LegacyStationaryPresentSchema = object({
17842
+ kind: literal("stationary-present"),
17843
+ deviceId: number().int().nonnegative(),
17844
+ classes: array(string().min(1)).min(1).optional(),
17845
+ zones: array(string().min(1)).min(1).optional()
17846
+ });
17847
+ var LegacyExpressionSchema = object({
17848
+ kind: literal("expression"),
17849
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17850
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
17851
+ }).superRefine((src, ctx) => {
17852
+ const err = validateExpressionSource(src);
17853
+ if (err !== null) ctx.addIssue({
17854
+ code: "custom",
17855
+ message: err,
17856
+ path: ["expr"]
17857
+ });
17858
+ });
17859
+ var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
17860
+ object({
17861
+ kind: literal("all"),
17862
+ children: array(LegacyConditionSchema)
17863
+ }),
17864
+ object({
17865
+ kind: literal("any"),
17866
+ children: array(LegacyConditionSchema)
17867
+ }),
17868
+ object({
17869
+ kind: literal("not"),
17870
+ child: LegacyConditionSchema
17871
+ }),
17872
+ LegacyConditionLeafSchema,
17873
+ LegacyExpressionSchema,
17874
+ LegacyStationaryPresentSchema
17875
+ ]));
17876
+ function convertTrigger(trigger) {
17877
+ switch (trigger.kind) {
17878
+ case "device-state": return {
17879
+ ...trigger,
17880
+ kind: "device-becomes"
17881
+ };
17882
+ case "detection": {
17883
+ const { classes, ...rest } = trigger;
17884
+ return {
17885
+ ...rest,
17886
+ subjects: classes.map(subjectFromClassName)
17887
+ };
17888
+ }
17889
+ case "stationary": {
17890
+ const { classes, ...rest } = trigger;
17891
+ return {
17892
+ ...rest,
17893
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17894
+ };
17895
+ }
17896
+ default: return trigger;
17897
+ }
17898
+ }
17899
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
17900
+ function convertLegacyCondition(condition) {
17901
+ switch (condition.kind) {
17902
+ case "condition": return {
17903
+ ...condition,
17904
+ kind: "device-is"
17905
+ };
17906
+ case "stationary-present": {
17907
+ const { classes, ...rest } = condition;
17908
+ return {
17909
+ ...rest,
17910
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17911
+ };
17912
+ }
17913
+ case "expression": return condition;
17914
+ case "not": return {
17915
+ kind: "not",
17916
+ child: convertLegacyCondition(condition.child)
17917
+ };
17918
+ case "all":
17919
+ case "any": return {
17920
+ kind: condition.kind,
17921
+ children: condition.children.map(convertLegacyCondition)
17922
+ };
17923
+ }
17924
+ }
17925
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
17926
+ function convertLegacyWhen(half) {
17927
+ const leaves = half.triggers.map(convertTrigger);
17928
+ const first = leaves[0];
17929
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
17930
+ kind: "any",
17931
+ children: leaves
17932
+ };
17933
+ if (half.conditions === void 0) return triggers;
17934
+ return {
17935
+ kind: "all",
17936
+ children: [triggers, convertLegacyCondition(half.conditions)]
17937
+ };
17938
+ }
17939
+ /**
17940
+ * The automation RECIPE — what a visually-authored automation is, before it
17941
+ * becomes code.
17942
+ *
17943
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
17944
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
17945
+ * declarative description that {@link generateAutomationBlock} compiles to the
17946
+ * TypeScript of an ordinary core block. There is therefore no second rule
17947
+ * engine, no second store and no second runtime — the block runner is the
17948
+ * runtime, `core_blocks` is the store (§2.1).
17949
+ *
17950
+ * Two parts:
17951
+ *
17952
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
17953
+ * (instants) and state leaves (durations). It replaces the flat trigger list
17954
+ * plus the separate condition tree; the firing rule is in that file and in
17955
+ * ADR "one tree".
17956
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
17957
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
17958
+ * this had to diverge from "verbatim", and why.
17959
+ *
17960
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
17961
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
17962
+ */
17485
17963
  /**
17486
17964
  * One action step.
17487
17965
  *
@@ -17584,69 +18062,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
17584
18062
  minutes: number().int().min(1).max(1440)
17585
18063
  })
17586
18064
  ]);
17587
- var AutomationRecipeSchema = object({
17588
- triggers: array(AutomationTriggerSchema),
17589
- conditions: AutomationConditionSchema.optional(),
18065
+ var CurrentAutomationRecipeSchema = object({
18066
+ when: AutomationWhenSchema,
17590
18067
  actions: array(AutomationActionSchema),
17591
18068
  runMode: AutomationRunModeSchema.optional(),
17592
18069
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
17593
18070
  keepTraces: number().int().min(1).max(100).optional()
17594
18071
  });
17595
- new Set([
17596
- {
17597
- id: "person",
17598
- name: "Person"
17599
- },
17600
- {
17601
- id: "vehicle",
17602
- name: "Vehicle"
17603
- },
17604
- {
17605
- id: "animal",
17606
- name: "Animal"
17607
- },
17608
- {
17609
- id: "package",
17610
- name: "Package"
17611
- }
17612
- ].map((l) => l.id));
17613
- var COCO_TO_MACRO = {
17614
- mapping: {
17615
- person: "person",
17616
- bicycle: "vehicle",
17617
- car: "vehicle",
17618
- motorcycle: "vehicle",
17619
- airplane: "vehicle",
17620
- bus: "vehicle",
17621
- train: "vehicle",
17622
- truck: "vehicle",
17623
- boat: "vehicle",
17624
- bird: "animal",
17625
- cat: "animal",
17626
- dog: "animal",
17627
- horse: "animal",
17628
- sheep: "animal",
17629
- cow: "animal",
17630
- elephant: "animal",
17631
- bear: "animal",
17632
- zebra: "animal",
17633
- giraffe: "animal",
17634
- suitcase: "package",
17635
- backpack: "package",
17636
- handbag: "package"
17637
- },
17638
- preserveOriginal: false
17639
- };
18072
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
18073
+ var LegacyAutomationRecipeSchema = object({
18074
+ triggers: array(LegacyTriggerSchema),
18075
+ conditions: LegacyConditionSchema.optional(),
18076
+ actions: array(AutomationActionSchema),
18077
+ runMode: AutomationRunModeSchema.optional(),
18078
+ keepTraces: number().int().min(1).max(100).optional()
18079
+ });
18080
+ /** The old shape as the new one, lossless. */
18081
+ function convertLegacyRecipe(legacy) {
18082
+ const { triggers, conditions, ...rest } = legacy;
18083
+ return {
18084
+ when: convertLegacyWhen({
18085
+ triggers,
18086
+ conditions
18087
+ }),
18088
+ ...rest
18089
+ };
18090
+ }
18091
+ /**
18092
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
18093
+ * saved block carries): the new shape as it is, or the old shape converted to
18094
+ * it. The output type is always the new one.
18095
+ */
18096
+ var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
17640
18097
  new Set(Object.values(COCO_TO_MACRO.mapping));
17641
18098
  /**
17642
18099
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
17643
18100
  * so the editor can paint every block green or red with its reason.
17644
18101
  *
17645
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
17646
- * every condition node (containers and leaves, including the ones a
17647
- * short-circuit never reached), the run-mode verdict and every action step —
17648
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
17649
- * for an Action run). Skipped
18102
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
18103
+ * fired, every node of the `when` tree (containers and leaves, including the
18104
+ * ones a short-circuit never reached), the run-mode verdict and every action
18105
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
18106
+ * for an Action run). Traces written before the single tree carry
18107
+ * `conditions.*` paths and still parse. Skipped
17650
18108
  * evaluations are traced too, and identical consecutive skips collapse into a
17651
18109
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
17652
18110
  *
@@ -17678,6 +18136,8 @@ var TraceNodeOutcomeSchema = _enum([
17678
18136
  ]);
17679
18137
  var AutomationTriggerTraceSchema = object({
17680
18138
  kind: string().min(1),
18139
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
18140
+ path: string().min(1).optional(),
17681
18141
  outcome: _enum(["pass", "fail"]),
17682
18142
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
17683
18143
  reason: string(),
@@ -17685,15 +18145,23 @@ var AutomationTriggerTraceSchema = object({
17685
18145
  vars: record(string(), TraceValueSchema)
17686
18146
  });
17687
18147
  var AutomationConditionTraceNodeSchema = object({
17688
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
18148
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
17689
18149
  path: string().min(1),
17690
18150
  kind: _enum([
17691
18151
  "all",
17692
18152
  "any",
17693
18153
  "not",
17694
18154
  "condition",
18155
+ "device-is",
17695
18156
  "expression",
17696
- "stationary-present"
18157
+ "stationary-present",
18158
+ "device-becomes",
18159
+ "device-event",
18160
+ "schedule",
18161
+ "interval",
18162
+ "manual",
18163
+ "detection",
18164
+ "stationary"
17697
18165
  ]),
17698
18166
  outcome: TraceNodeOutcomeSchema,
17699
18167
  reason: string(),
@@ -17727,6 +18195,12 @@ var AutomationStepTraceSchema = object({
17727
18195
  deviceId: number().int().optional(),
17728
18196
  startedAt: number().optional(),
17729
18197
  endedAt: number().optional(),
18198
+ /**
18199
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
18200
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
18201
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
18202
+ */
18203
+ args: record(string(), TraceValueSchema).optional(),
17730
18204
  outcome: _enum([
17731
18205
  "ok",
17732
18206
  "error",
@@ -18476,6 +18950,23 @@ method(object({
18476
18950
  addonId: string(),
18477
18951
  nodeId: string().optional()
18478
18952
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
18953
+ addonId: string(),
18954
+ nodeId: string().optional()
18955
+ }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
18956
+ addonId: string(),
18957
+ nodeId: string().optional(),
18958
+ keys: array(string())
18959
+ }), object({
18960
+ removed: array(string()),
18961
+ refused: array(object({
18962
+ key: string(),
18963
+ reason: string()
18964
+ })),
18965
+ secretsCleared: number()
18966
+ }), {
18967
+ kind: "mutation",
18968
+ auth: "admin"
18969
+ }), method(object({
18479
18970
  addonId: string(),
18480
18971
  deviceId: number(),
18481
18972
  nodeId: string().optional()
@@ -19930,8 +20421,21 @@ var BrokerProviderInfoSchema = object({
19930
20421
  * (`{ sessionId }`) drops the session. The dialog that edits a broker
19931
20422
  * offers it as the default path; the fields the session fills are
19932
20423
  * marked `filledBySignIn` in the kind's settings schema.
20424
+ *
20425
+ * `methods` lists the ways the session can sign in, recommended first:
20426
+ * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
20427
+ * settings) answering `{ sessionId, userCode, verificationUrl,
20428
+ * expiresAt }` — the operator types `userCode` at `verificationUrl` on
20429
+ * any device, and the same `status` / `commit` / `cancel` routes follow
20430
+ * (status then carries `method`, `userCode`, `verificationUrl`,
20431
+ * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
20432
+ * `'proxy'` is the `{basePath}/start` flow above. Absent means
20433
+ * `['proxy']`.
19933
20434
  */
19934
- signIn: object({ basePath: string().min(1) }).optional()
20435
+ signIn: object({
20436
+ basePath: string().min(1),
20437
+ methods: array(_enum(["device-code", "proxy"])).optional()
20438
+ }).optional()
19935
20439
  }))
19936
20440
  });
19937
20441
  var ListInputSchema = object({
@@ -22443,6 +22947,8 @@ var StorageKindSchema = object({
22443
22947
  "orphaned",
22444
22948
  "dropped"
22445
22949
  ]),
22950
+ /** An owner declared this kind since the engine started. */
22951
+ declaredThisBoot: boolean(),
22446
22952
  /** `null` when the caller asked for no counts (`counts: false`). */
22447
22953
  rows: number().int().nonnegative().nullable(),
22448
22954
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -22454,6 +22960,51 @@ var StorageKindSchema = object({
22454
22960
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
22455
22961
  declaration: RecordKindDeclarationSchema
22456
22962
  });
22963
+ _enum([
22964
+ "orphaned",
22965
+ "stale",
22966
+ "unknown"
22967
+ ]);
22968
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
22969
+ var StaleKindFindingSchema = object({
22970
+ owner: string(),
22971
+ kind: string(),
22972
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
22973
+ verdict: _enum(["orphaned", "unknown"]),
22974
+ reason: string(),
22975
+ rows: number().int().nonnegative().nullable(),
22976
+ /** local-auth's tables are never dropped by this audit. */
22977
+ protected: boolean()
22978
+ });
22979
+ /** A stored global-settings key the running addon no longer declares. */
22980
+ var StaleSettingsKeyFindingSchema = object({
22981
+ addonId: string(),
22982
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
22983
+ key: string(),
22984
+ /**
22985
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
22986
+ * not orphans yet. Removing the key releases them.
22987
+ */
22988
+ secretRefs: array(string())
22989
+ });
22990
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
22991
+ var StaleUnknownSchema = object({
22992
+ subject: string(),
22993
+ reason: string()
22994
+ });
22995
+ var StaleDataReportSchema = object({
22996
+ /** Epoch ms of the last finished scan; `null` before the first. */
22997
+ scannedAt: number().nullable(),
22998
+ scanning: boolean(),
22999
+ trigger: _enum([
23000
+ "boot",
23001
+ "interval",
23002
+ "manual"
23003
+ ]).nullable(),
23004
+ kinds: array(StaleKindFindingSchema),
23005
+ settingsKeys: array(StaleSettingsKeyFindingSchema),
23006
+ unknown: array(StaleUnknownSchema)
23007
+ });
22457
23008
  method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
22458
23009
  owner: string().min(1).optional(),
22459
23010
  /** `false` skips the per-kind row count (a catalog read only). Default true. */
@@ -22461,7 +23012,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
22461
23012
  }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
22462
23013
  owner: string().min(1),
22463
23014
  kind: RecordKindNameSchema
22464
- }), RecordQueryResultSchema, { auth: "admin" });
23015
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
23016
+ kind: "mutation",
23017
+ auth: "admin"
23018
+ }), method(object({
23019
+ owner: string().min(1),
23020
+ kind: RecordKindNameSchema
23021
+ }), object({
23022
+ deleted: number().int().nonnegative(),
23023
+ secretsReleased: number().int().nonnegative()
23024
+ }), {
23025
+ kind: "mutation",
23026
+ auth: "admin",
23027
+ caller: "required"
23028
+ }), method(object({
23029
+ addonId: string().min(1),
23030
+ key: string().min(1)
23031
+ }), object({
23032
+ removed: boolean(),
23033
+ secretsCleared: number().int().nonnegative()
23034
+ }), {
23035
+ kind: "mutation",
23036
+ auth: "admin",
23037
+ caller: "required"
23038
+ });
22465
23039
  /**
22466
23040
  * record-entries - the ONE admin surface for every list of user-created entries
22467
23041
  * (D881): notification targets, brokers, LLM profiles, anything an addon
@@ -22600,6 +23174,12 @@ method(object({
22600
23174
  }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
22601
23175
  kind: "mutation",
22602
23176
  auth: "admin"
23177
+ }), method(object({
23178
+ refs: RefListSchema,
23179
+ onBehalfOf: string().min(1)
23180
+ }), object({ released: number().int() }), {
23181
+ kind: "mutation",
23182
+ auth: "admin"
22603
23183
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
22604
23184
  /**
22605
23185
  * device-history — the ONE mechanism for numeric history (D850).
@@ -23389,7 +23969,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
23389
23969
  /** Upstream-system identity + rendering hints for the candidate. */
23390
23970
  sourceInfo: SourceInfoSchema.optional(),
23391
23971
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
23392
- children: array(DiscoveredChildDeviceSchema).readonly().optional()
23972
+ children: array(DiscoveredChildDeviceSchema).readonly().optional(),
23973
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
23974
+ parentStableId: string().optional()
23393
23975
  }));
23394
23976
  /**
23395
23977
  * Status block — populated by the integration after each
@@ -43704,6 +44286,79 @@ object({
43704
44286
  metadata: record(string(), unknown()).optional()
43705
44287
  });
43706
44288
  /**
44289
+ * HOW a provider can be found on the network. The provider only DECLARES the
44290
+ * mode; the `local-network` scan engine runs it once for every provider that
44291
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
44292
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
44293
+ * except through a `custom` mode.
44294
+ */
44295
+ var DiscoveryModeSchema = discriminatedUnion("kind", [
44296
+ object({
44297
+ kind: literal("mdns"),
44298
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
44299
+ serviceType: string().min(1),
44300
+ /** Regular expression an instance name must match (case-insensitive). */
44301
+ instancePattern: string().optional()
44302
+ }),
44303
+ object({
44304
+ kind: literal("ws-discovery"),
44305
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
44306
+ types: array(string()).optional()
44307
+ }),
44308
+ object({
44309
+ kind: literal("ssdp"),
44310
+ searchTarget: string().min(1)
44311
+ }),
44312
+ object({
44313
+ kind: literal("udp-broadcast"),
44314
+ port: number().int().min(1).max(65535),
44315
+ payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
44316
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
44317
+ listenPort: number().int().min(1).max(65535).optional()
44318
+ }),
44319
+ object({
44320
+ kind: literal("http-probe"),
44321
+ port: number().int().min(1).max(65535),
44322
+ path: string().startsWith("/"),
44323
+ method: _enum(["GET", "POST"]).default("GET"),
44324
+ bodyText: string().optional(),
44325
+ contentType: string().optional(),
44326
+ tls: boolean().default(false),
44327
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
44328
+ acceptStatus: array(number().int()).optional()
44329
+ }),
44330
+ object({
44331
+ kind: literal("custom"),
44332
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
44333
+ reason: string().min(1)
44334
+ })
44335
+ ]);
44336
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
44337
+ var DiscoveryHitSchema = object({
44338
+ /** Index into the provider's own `getDiscoveryModes()` array. */
44339
+ modeIndex: number().int().min(0),
44340
+ host: string(),
44341
+ port: number().int(),
44342
+ http: object({
44343
+ status: number().int(),
44344
+ headers: record(string(), string()),
44345
+ /** At most 16 KiB. */
44346
+ bodyText: string()
44347
+ }).optional(),
44348
+ mdns: object({
44349
+ instance: string(),
44350
+ host: string(),
44351
+ port: number().int(),
44352
+ txt: record(string(), string())
44353
+ }).optional(),
44354
+ udp: object({ payloadHex: string() }).optional(),
44355
+ wsd: object({
44356
+ xaddrs: array(string()),
44357
+ types: array(string()),
44358
+ scopes: array(string())
44359
+ }).optional()
44360
+ });
44361
+ /**
43707
44362
  * Candidate handed back from discovery and accepted by
43708
44363
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
43709
44364
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -43738,7 +44393,17 @@ var DiscoveryCandidateSchema = object({
43738
44393
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
43739
44394
  onboardedDeviceId: number().optional(),
43740
44395
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
43741
- onboardedName: string().optional()
44396
+ onboardedName: string().optional(),
44397
+ /**
44398
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
44399
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
44400
+ * which is still read for one release.
44401
+ */
44402
+ setupIntegration: boolean().optional(),
44403
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
44404
+ parentStableId: string().optional(),
44405
+ /** `false` = shown for context but not pickable. Default true. */
44406
+ selectable: boolean().optional()
43742
44407
  });
43743
44408
  /**
43744
44409
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -43791,6 +44456,12 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
43791
44456
  }), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
43792
44457
  kind: "mutation",
43793
44458
  auth: "admin"
44459
+ }), method(object({}), array(DiscoveryModeSchema)), method(object({
44460
+ hits: array(DiscoveryHitSchema),
44461
+ params: record(string(), unknown()).optional()
44462
+ }), array(DiscoveryCandidateSchema), {
44463
+ kind: "mutation",
44464
+ auth: "admin"
43794
44465
  }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
43795
44466
  kind: "mutation",
43796
44467
  auth: "admin"
@@ -44518,17 +45189,23 @@ method(object({
44518
45189
  }), method(object({}), object({ providers: array(object({
44519
45190
  addonId: string(),
44520
45191
  label: string()
44521
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
44522
- addonId: string(),
44523
- label: string(),
44524
- candidates: array(DiscoveryCandidateSchema).readonly(),
44525
- error: string().nullable()
44526
- })).readonly() }), {
44527
- kind: "mutation",
44528
- auth: "admin"
44529
- }), method(object({
45192
+ })).readonly() }), { auth: "admin" }), method(
45193
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
45194
+ object({ subnets: array(string()).optional() }),
45195
+ object({ groups: array(object({
45196
+ addonId: string(),
45197
+ label: string(),
45198
+ candidates: array(DiscoveryCandidateSchema).readonly(),
45199
+ error: string().nullable()
45200
+ })).readonly() }),
45201
+ {
45202
+ kind: "mutation",
45203
+ auth: "admin"
45204
+ }
45205
+ ), method(object({
44530
45206
  addonId: string(),
44531
- params: record(string(), unknown()).optional()
45207
+ params: record(string(), unknown()).optional(),
45208
+ subnets: array(string()).optional()
44532
45209
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
44533
45210
  kind: "mutation",
44534
45211
  auth: "admin"
@@ -58264,6 +58941,12 @@ var TerminalInstanceInfoSchema = object({
58264
58941
  instanceId: string(),
58265
58942
  cameraStableId: string(),
58266
58943
  nodeId: string(),
58944
+ /**
58945
+ * False while the node the instance is bound to is not in the cluster
58946
+ * topology (forgotten, renamed, re-paired). The instance is kept; the
58947
+ * operator re-points it with `updateInstance({ targetNodeId })`.
58948
+ */
58949
+ nodeInCluster: boolean(),
58267
58950
  profileId: string(),
58268
58951
  profileLabel: string(),
58269
58952
  name: string(),
@@ -58313,6 +58996,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
58313
58996
  auth: "admin"
58314
58997
  }), method(object({
58315
58998
  instanceId: string().min(1),
58999
+ /** Re-point the instance to another node; the profile must exist there. */
59000
+ targetNodeId: string().min(1).max(256).optional(),
58316
59001
  name: string().trim().min(1).max(160).optional(),
58317
59002
  executable: string().max(1024).optional(),
58318
59003
  args: array(string().max(2048)).max(64).optional(),
@@ -60832,6 +61517,30 @@ object({
60832
61517
  at: number()
60833
61518
  }).nullable()
60834
61519
  });
61520
+ var RemoteButtonShapeSchema = _enum([
61521
+ "rect",
61522
+ "rounded",
61523
+ "round",
61524
+ "pill"
61525
+ ]);
61526
+ var RemoteButtonToneSchema = _enum([
61527
+ "neutral",
61528
+ "accent",
61529
+ "danger",
61530
+ "red",
61531
+ "green",
61532
+ "yellow",
61533
+ "blue"
61534
+ ]);
61535
+ var RemoteGroupFrameSchema = _enum([
61536
+ "none",
61537
+ "panel",
61538
+ "divider",
61539
+ "dpad",
61540
+ "rocker-v",
61541
+ "rocker-h",
61542
+ "keypad"
61543
+ ]);
60835
61544
  var RemoteLayoutButtonSchema = object({
60836
61545
  id: string().min(1).max(40),
60837
61546
  /** 0-based grid position. */
@@ -60841,12 +61550,94 @@ var RemoteLayoutButtonSchema = object({
60841
61550
  colSpan: number().int().min(1).max(24),
60842
61551
  icon: string().min(1).max(60).optional(),
60843
61552
  label: string().min(1).max(40).optional(),
60844
- role: RemoteRoleSchema
61553
+ role: RemoteRoleSchema,
61554
+ /** Absent = the plain rendering every layout had before shapes existed. */
61555
+ shape: RemoteButtonShapeSchema.optional(),
61556
+ tone: RemoteButtonToneSchema.optional(),
61557
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
61558
+ groupId: string().min(1).max(40).optional()
61559
+ });
61560
+ /**
61561
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
61562
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
61563
+ * entry it came from (informational: the layout is self-contained).
61564
+ */
61565
+ var RemoteLayoutGroupSchema = object({
61566
+ id: string().min(1).max(40),
61567
+ componentId: string().min(1).max(80),
61568
+ frame: RemoteGroupFrameSchema,
61569
+ row: number().int().min(0).max(24),
61570
+ col: number().int().min(0).max(24),
61571
+ rowSpan: number().int().min(1).max(24),
61572
+ colSpan: number().int().min(1).max(24),
61573
+ title: string().min(1).max(40).optional()
60845
61574
  });
61575
+ function rectsOverlap(a, b) {
61576
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
61577
+ }
61578
+ function rectContains(outer, inner) {
61579
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
61580
+ }
61581
+ /**
61582
+ * The group rules of a layout: inside the grid, no two groups overlap, a
61583
+ * member lies inside its group, a divider holds nothing, and a button that is
61584
+ * not a member never sits inside someone else's rectangle (a group moves as
61585
+ * one thing, so a stray key under it would be left behind).
61586
+ */
61587
+ function refineGroups(grid, ctx) {
61588
+ const groups = grid.groups ?? [];
61589
+ const byId = /* @__PURE__ */ new Map();
61590
+ const fail = (message, path) => {
61591
+ ctx.addIssue({
61592
+ code: "custom",
61593
+ message,
61594
+ path
61595
+ });
61596
+ };
61597
+ groups.forEach((group, index) => {
61598
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
61599
+ "groups",
61600
+ index,
61601
+ "id"
61602
+ ]);
61603
+ byId.set(group.id, group);
61604
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
61605
+ for (let other = 0; other < index; other += 1) {
61606
+ const earlier = groups[other];
61607
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
61608
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
61609
+ return;
61610
+ }
61611
+ }
61612
+ });
61613
+ grid.buttons.forEach((button, index) => {
61614
+ if (button.groupId !== void 0) {
61615
+ const group = byId.get(button.groupId);
61616
+ if (group === void 0) {
61617
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
61618
+ "buttons",
61619
+ index,
61620
+ "groupId"
61621
+ ]);
61622
+ return;
61623
+ }
61624
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
61625
+ "buttons",
61626
+ index,
61627
+ "groupId"
61628
+ ]);
61629
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
61630
+ return;
61631
+ }
61632
+ const foreign = groups.find((group) => rectsOverlap(group, button));
61633
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
61634
+ });
61635
+ }
60846
61636
  object({
60847
61637
  columns: number().int().min(1).max(24),
60848
61638
  rows: number().int().min(1).max(24),
60849
- buttons: array(RemoteLayoutButtonSchema).max(200)
61639
+ buttons: array(RemoteLayoutButtonSchema).max(200),
61640
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
60850
61641
  }).superRefine((grid, ctx) => {
60851
61642
  const ids = /* @__PURE__ */ new Set();
60852
61643
  const taken = /* @__PURE__ */ new Map();
@@ -60883,6 +61674,7 @@ object({
60883
61674
  taken.set(cell, button.id);
60884
61675
  }
60885
61676
  });
61677
+ refineGroups(grid, ctx);
60886
61678
  });
60887
61679
  var RemoteBoundCommandSchema = object({
60888
61680
  role: RemoteRoleSchema,
@@ -61014,6 +61806,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
61014
61806
  kind: "mutation",
61015
61807
  auth: "admin"
61016
61808
  });
61809
+ var RemoteComponentCategorySchema = _enum([
61810
+ "power",
61811
+ "navigation",
61812
+ "volume",
61813
+ "numbers",
61814
+ "media",
61815
+ "colour",
61816
+ "apps",
61817
+ "layout"
61818
+ ]);
61819
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
61820
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
61821
+ var RemoteComponentButtonSchema = object({
61822
+ /** Joined to the instance id when the component is inserted. */
61823
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
61824
+ row: number().int().min(0).max(24),
61825
+ col: number().int().min(0).max(24),
61826
+ rowSpan: number().int().min(1).max(24),
61827
+ colSpan: number().int().min(1).max(24),
61828
+ role: RemoteRoleSchema,
61829
+ icon: string().min(1).max(60).optional(),
61830
+ label: string().min(1).max(40).optional(),
61831
+ shape: RemoteButtonShapeSchema.optional(),
61832
+ tone: RemoteButtonToneSchema.optional()
61833
+ });
61834
+ var RemoteLayoutComponentSchema = object({
61835
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
61836
+ name: string().min(1).max(60),
61837
+ nameI18n: object({
61838
+ en: string().min(1).max(60),
61839
+ it: string().min(1).max(60)
61840
+ }).optional(),
61841
+ /** A display icon key, resolved by the UI like a button icon. */
61842
+ icon: string().min(1).max(60),
61843
+ category: RemoteComponentCategorySchema,
61844
+ size: object({
61845
+ rows: number().int().min(1).max(24),
61846
+ cols: number().int().min(1).max(24)
61847
+ }),
61848
+ /** The frame drawn around the whole component, or null for loose buttons. */
61849
+ group: object({
61850
+ frame: RemoteGroupFrameSchema,
61851
+ title: string().min(1).max(40).optional()
61852
+ }).nullable(),
61853
+ buttons: array(RemoteComponentButtonSchema).max(40)
61854
+ }).superRefine((component, ctx) => {
61855
+ const suffixes = /* @__PURE__ */ new Set();
61856
+ component.buttons.forEach((button, index) => {
61857
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
61858
+ code: "custom",
61859
+ message: `button "${button.idSuffix}" is listed twice`,
61860
+ path: [
61861
+ "buttons",
61862
+ index,
61863
+ "idSuffix"
61864
+ ]
61865
+ });
61866
+ suffixes.add(button.idSuffix);
61867
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
61868
+ ctx.addIssue({
61869
+ code: "custom",
61870
+ message: `button "${button.idSuffix}" leaves the component`,
61871
+ path: ["buttons", index]
61872
+ });
61873
+ return;
61874
+ }
61875
+ for (let other = 0; other < index; other += 1) {
61876
+ const earlier = component.buttons[other];
61877
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
61878
+ ctx.addIssue({
61879
+ code: "custom",
61880
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
61881
+ path: ["buttons", index]
61882
+ });
61883
+ return;
61884
+ }
61885
+ }
61886
+ });
61887
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
61888
+ code: "custom",
61889
+ message: "a divider component holds no buttons",
61890
+ path: ["buttons"]
61891
+ });
61892
+ });
61893
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
61017
61894
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
61018
61895
  kind: "mutation",
61019
61896
  auth: "protected"
@@ -63155,7 +64032,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
63155
64032
  id: string(),
63156
64033
  settings: record(string(), unknown()),
63157
64034
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
63158
- secretKeys: array(string()).optional()
64035
+ secretKeys: array(string()).optional(),
64036
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
64037
+ skipRestart: boolean().optional()
63159
64038
  }), object({ success: boolean() }), {
63160
64039
  kind: "mutation",
63161
64040
  auth: "admin"
@@ -63632,6 +64511,39 @@ var TlsStatusSchema = object({
63632
64511
  hasCa: boolean()
63633
64512
  }).nullable()
63634
64513
  });
64514
+ /** One provider's ask of the scan engine: the modes it declared. */
64515
+ var ScanRequestSchema = object({
64516
+ providerId: string().min(1),
64517
+ modes: array(DiscoveryModeSchema)
64518
+ });
64519
+ var ScanErrorSchema = object({
64520
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
64521
+ providerId: string().optional(),
64522
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
64523
+ mode: string().optional(),
64524
+ message: string()
64525
+ });
64526
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
64527
+ var ScanStatsSchema = object({
64528
+ hostsProbed: number().int(),
64529
+ hits: number().int(),
64530
+ timeouts: number().int(),
64531
+ byMode: record(string(), object({
64532
+ probed: number().int(),
64533
+ hits: number().int(),
64534
+ timeouts: number().int()
64535
+ }))
64536
+ });
64537
+ var ScanNetworkResultSchema = object({
64538
+ hits: array(object({
64539
+ providerId: string(),
64540
+ hit: DiscoveryHitSchema
64541
+ })),
64542
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
64543
+ subnetsScanned: array(string()),
64544
+ errors: array(ScanErrorSchema),
64545
+ stats: ScanStatsSchema
64546
+ });
63635
64547
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
63636
64548
  /**
63637
64549
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -63686,6 +64598,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
63686
64598
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
63687
64599
  kind: "mutation",
63688
64600
  auth: "admin"
64601
+ }), method(object({
64602
+ requests: array(ScanRequestSchema),
64603
+ subnets: array(string()).optional()
64604
+ }), ScanNetworkResultSchema, {
64605
+ kind: "mutation",
64606
+ auth: "admin",
64607
+ timeoutMs: 9e4
64608
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
64609
+ kind: "mutation",
64610
+ auth: "admin"
63689
64611
  }), method(_void(), TlsStatusSchema, {
63690
64612
  kind: "mutation",
63691
64613
  auth: "admin"
@@ -66969,7 +67891,7 @@ object({
66969
67891
  });
66970
67892
  Object.values(DeviceType);
66971
67893
  DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
66972
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
67894
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
66973
67895
  kind: "mutation",
66974
67896
  auth: "admin"
66975
67897
  });
@@ -70793,6 +71715,124 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
70793
71715
  kind: "mutation",
70794
71716
  auth: "admin"
70795
71717
  });
71718
+ var btn = (idSuffix, row, col, init) => ({
71719
+ idSuffix,
71720
+ row,
71721
+ col,
71722
+ rowSpan: 1,
71723
+ colSpan: 1,
71724
+ ...init
71725
+ });
71726
+ function rocker(id, name, it, frame, title, buttons, size) {
71727
+ return {
71728
+ id,
71729
+ name,
71730
+ nameI18n: {
71731
+ en: name,
71732
+ it
71733
+ },
71734
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
71735
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
71736
+ size,
71737
+ group: title === null ? { frame } : {
71738
+ frame,
71739
+ title
71740
+ },
71741
+ buttons
71742
+ };
71743
+ }
71744
+ btn("power", 0, 0, {
71745
+ role: "power",
71746
+ shape: "round",
71747
+ tone: "danger"
71748
+ }), btn("up", 0, 1, {
71749
+ role: "up",
71750
+ icon: "chevron-up"
71751
+ }), btn("left", 1, 0, {
71752
+ role: "left",
71753
+ icon: "chevron-left"
71754
+ }), btn("ok", 1, 1, {
71755
+ role: "ok",
71756
+ shape: "round",
71757
+ tone: "accent"
71758
+ }), btn("right", 1, 2, {
71759
+ role: "right",
71760
+ icon: "chevron-right"
71761
+ }), btn("down", 2, 1, {
71762
+ role: "down",
71763
+ icon: "chevron-down"
71764
+ }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
71765
+ role: "volume_up",
71766
+ icon: "plus",
71767
+ shape: "pill"
71768
+ }), btn("down", 1, 0, {
71769
+ role: "volume_down",
71770
+ icon: "minus",
71771
+ shape: "pill"
71772
+ })], {
71773
+ rows: 2,
71774
+ cols: 1
71775
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
71776
+ role: "channel_up",
71777
+ icon: "chevron-up",
71778
+ shape: "pill"
71779
+ }), btn("down", 1, 0, {
71780
+ role: "channel_down",
71781
+ icon: "chevron-down",
71782
+ shape: "pill"
71783
+ })], {
71784
+ rows: 2,
71785
+ cols: 1
71786
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
71787
+ role: "custom:minus",
71788
+ label: "Minus",
71789
+ icon: "minus",
71790
+ shape: "pill"
71791
+ }), btn("plus", 0, 1, {
71792
+ role: "custom:plus",
71793
+ label: "Plus",
71794
+ icon: "plus",
71795
+ shape: "pill"
71796
+ })], {
71797
+ rows: 1,
71798
+ cols: 2
71799
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
71800
+ role: "custom:color_red",
71801
+ label: "Red",
71802
+ tone: "red"
71803
+ }), btn("green", 0, 1, {
71804
+ role: "custom:color_green",
71805
+ label: "Green",
71806
+ tone: "green"
71807
+ }), btn("yellow", 0, 2, {
71808
+ role: "custom:color_yellow",
71809
+ label: "Yellow",
71810
+ tone: "yellow"
71811
+ }), btn("blue", 0, 3, {
71812
+ role: "custom:color_blue",
71813
+ label: "Blue",
71814
+ tone: "blue"
71815
+ }), btn("rewind", 0, 0, {
71816
+ role: "rewind",
71817
+ icon: "rewind"
71818
+ }), btn("play", 0, 1, {
71819
+ role: "play",
71820
+ icon: "play"
71821
+ }), btn("pause", 0, 2, {
71822
+ role: "pause",
71823
+ icon: "pause"
71824
+ }), btn("forward", 0, 3, {
71825
+ role: "fast_forward",
71826
+ icon: "fast-forward"
71827
+ }), btn("stop", 0, 4, {
71828
+ role: "stop",
71829
+ icon: "square"
71830
+ }), [
71831
+ 1,
71832
+ 2,
71833
+ 3,
71834
+ 4
71835
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
70796
71836
  discriminatedUnion("kind", [object({
70797
71837
  kind: literal("raw"),
70798
71838
  timings: array(number().int()),
@@ -70803,7 +71843,7 @@ discriminatedUnion("kind", [object({
70803
71843
  })]);
70804
71844
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70805
71845
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70806
- 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;
71846
+ 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.Gateway, 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;
70807
71847
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70808
71848
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70809
71849
  new Set(CAP_NAMES);
@@ -70856,7 +71896,7 @@ var AccessoryKind = {
70856
71896
  PrivacyMask: DeviceRole.PrivacyMask
70857
71897
  };
70858
71898
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
70859
- 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;
71899
+ 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, DeviceType.Gateway;
70860
71900
  new Set(Object.values(DeviceType));
70861
71901
  Object.freeze({
70862
71902
  "accessories.getStatus": {
@@ -71244,6 +72284,13 @@ Object.freeze({
71244
72284
  access: "create",
71245
72285
  role: "admin"
71246
72286
  },
72287
+ "addonSettings.getDeclaredSettingsKeys": {
72288
+ capName: "addon-settings",
72289
+ capScope: "system",
72290
+ addonId: null,
72291
+ access: "view",
72292
+ role: "admin"
72293
+ },
71247
72294
  "addonSettings.getDeviceSettings": {
71248
72295
  capName: "addon-settings",
71249
72296
  capScope: "system",
@@ -71265,6 +72312,13 @@ Object.freeze({
71265
72312
  access: "view",
71266
72313
  role: "admin"
71267
72314
  },
72315
+ "addonSettings.unsetStoredSettingsKeys": {
72316
+ capName: "addon-settings",
72317
+ capScope: "system",
72318
+ addonId: null,
72319
+ access: "create",
72320
+ role: "admin"
72321
+ },
71268
72322
  "addonSettings.updateDeviceSettings": {
71269
72323
  capName: "addon-settings",
71270
72324
  capScope: "system",
@@ -73680,6 +74734,13 @@ Object.freeze({
73680
74734
  access: "create",
73681
74735
  role: "admin"
73682
74736
  },
74737
+ "deviceProvider.classifyDiscoveryHits": {
74738
+ capName: "device-provider",
74739
+ capScope: "system",
74740
+ addonId: null,
74741
+ access: "create",
74742
+ role: "admin"
74743
+ },
73683
74744
  "deviceProvider.createDevice": {
73684
74745
  capName: "device-provider",
73685
74746
  capScope: "system",
@@ -73708,6 +74769,13 @@ Object.freeze({
73708
74769
  access: "view",
73709
74770
  role: "admin"
73710
74771
  },
74772
+ "deviceProvider.getDiscoveryModes": {
74773
+ capName: "device-provider",
74774
+ capScope: "system",
74775
+ addonId: null,
74776
+ access: "view",
74777
+ role: "admin"
74778
+ },
73711
74779
  "deviceProvider.getDiscoveryParamsSchema": {
73712
74780
  capName: "device-provider",
73713
74781
  capScope: "system",
@@ -74485,6 +75553,13 @@ Object.freeze({
74485
75553
  access: "create",
74486
75554
  role: "admin"
74487
75555
  },
75556
+ "keychain.releaseRecordRefs": {
75557
+ capName: "keychain",
75558
+ capScope: "system",
75559
+ addonId: null,
75560
+ access: "create",
75561
+ role: "admin"
75562
+ },
74488
75563
  "keychain.resolve": {
74489
75564
  capName: "keychain",
74490
75565
  capScope: "system",
@@ -74905,6 +75980,13 @@ Object.freeze({
74905
75980
  access: "view",
74906
75981
  role: "admin"
74907
75982
  },
75983
+ "localNetwork.getScanSubnets": {
75984
+ capName: "local-network",
75985
+ capScope: "system",
75986
+ addonId: null,
75987
+ access: "view",
75988
+ role: "admin"
75989
+ },
74908
75990
  "localNetwork.getTlsStatus": {
74909
75991
  capName: "local-network",
74910
75992
  capScope: "system",
@@ -74954,6 +76036,13 @@ Object.freeze({
74954
76036
  access: "create",
74955
76037
  role: "admin"
74956
76038
  },
76039
+ "localNetwork.scanNetwork": {
76040
+ capName: "local-network",
76041
+ capScope: "system",
76042
+ addonId: null,
76043
+ access: "create",
76044
+ role: "admin"
76045
+ },
74957
76046
  "localNetwork.setAddressRole": {
74958
76047
  capName: "local-network",
74959
76048
  capScope: "system",
@@ -74975,6 +76064,13 @@ Object.freeze({
74975
76064
  access: "create",
74976
76065
  role: "admin"
74977
76066
  },
76067
+ "localNetwork.setScanSubnets": {
76068
+ capName: "local-network",
76069
+ capScope: "system",
76070
+ addonId: null,
76071
+ access: "create",
76072
+ role: "admin"
76073
+ },
74978
76074
  "localNetwork.setViewerEndpoints": {
74979
76075
  capName: "local-network",
74980
76076
  capScope: "system",
@@ -78657,6 +79753,13 @@ Object.freeze({
78657
79753
  access: "create",
78658
79754
  role: "admin"
78659
79755
  },
79756
+ "remoteLayoutComponents.listComponents": {
79757
+ capName: "remote-layout-components",
79758
+ capScope: "system",
79759
+ addonId: null,
79760
+ access: "view",
79761
+ role: "admin"
79762
+ },
78660
79763
  "runningState.getStatus": {
78661
79764
  capName: "running-state",
78662
79765
  capScope: "device",
@@ -79189,6 +80292,13 @@ Object.freeze({
79189
80292
  access: "create",
79190
80293
  role: "admin"
79191
80294
  },
80295
+ "storageAdmin.dropOrphanedKind": {
80296
+ capName: "storage-admin",
80297
+ capScope: "system",
80298
+ addonId: null,
80299
+ access: "delete",
80300
+ role: "admin"
80301
+ },
79192
80302
  "storageAdmin.listKinds": {
79193
80303
  capName: "storage-admin",
79194
80304
  capScope: "system",
@@ -79210,6 +80320,27 @@ Object.freeze({
79210
80320
  access: "view",
79211
80321
  role: "admin"
79212
80322
  },
80323
+ "storageAdmin.scanStaleData": {
80324
+ capName: "storage-admin",
80325
+ capScope: "system",
80326
+ addonId: null,
80327
+ access: "create",
80328
+ role: "admin"
80329
+ },
80330
+ "storageAdmin.staleDataReport": {
80331
+ capName: "storage-admin",
80332
+ capScope: "system",
80333
+ addonId: null,
80334
+ access: "view",
80335
+ role: "admin"
80336
+ },
80337
+ "storageAdmin.unsetStaleSettingsKey": {
80338
+ capName: "storage-admin",
80339
+ capScope: "system",
80340
+ addonId: null,
80341
+ access: "create",
80342
+ role: "admin"
80343
+ },
79213
80344
  "storageEvictable.evict": {
79214
80345
  capName: "storage-evictable",
79215
80346
  capScope: "system",
@@ -84833,6 +85964,7 @@ var SYSTEM_CAP_VOCABULARY = {
84833
85964
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
84834
85965
  records: system(INTERNAL),
84835
85966
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
85967
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
84836
85968
  "server-management": system(CLUSTER),
84837
85969
  "settings-store": system(INTERNAL),
84838
85970
  "smtp-provider": system(INTERNAL),
@@ -84905,7 +86037,7 @@ function projectAction(cap, action) {
84905
86037
  };
84906
86038
  }
84907
86039
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84908
- 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;
86040
+ 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.Gateway;
84909
86041
  /**
84910
86042
  * Heuristic check — `device.features?.includes(profile.when.hasFeature)`.
84911
86043
  */