@camstack/addon-provider-rademacher 0.2.149 → 0.2.151

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.js CHANGED
@@ -5458,7 +5458,7 @@ var ZodIssueCode = {
5458
5458
  var ZodFirstPartyTypeKind;
5459
5459
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5460
5460
  //#endregion
5461
- //#region ../types/dist/sleep-63gM_1JR.mjs
5461
+ //#region ../types/dist/sleep-CeyhrcOk.mjs
5462
5462
  /**
5463
5463
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5464
5464
  * window to float samples (D455).
@@ -7525,6 +7525,7 @@ var BaseAddon = class {
7525
7525
  * a warning and falls back rather than crashing boot.
7526
7526
  */
7527
7527
  state(key, schema, fallback) {
7528
+ this._declaredStateKeys.add(key);
7528
7529
  return createDurableState({
7529
7530
  key,
7530
7531
  schema,
@@ -7540,6 +7541,96 @@ var BaseAddon = class {
7540
7541
  } })
7541
7542
  });
7542
7543
  }
7544
+ /** Durable-state keys this process registered through {@link state}. */
7545
+ _declaredStateKeys = /* @__PURE__ */ new Set();
7546
+ /**
7547
+ * Stale-data audit: every global-settings key this addon still declares —
7548
+ * constructor defaults, settings-schema fields (layout containers included),
7549
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
7550
+ * here, in the addon's own process, so the hub keeps no copy of it.
7551
+ */
7552
+ async getDeclaredSettingsKeys() {
7553
+ return { keys: [...this.declaredKeySet()].toSorted() };
7554
+ }
7555
+ declaredKeySet() {
7556
+ const keys = new Set(Object.keys(this.defaults));
7557
+ const schema = this.globalSettingsSchema();
7558
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
7559
+ for (const key of this._declaredStateKeys) keys.add(key);
7560
+ for (const path of this.secretStatePaths) {
7561
+ const root = path.split(/[.[]/, 1)[0];
7562
+ if (root) keys.add(root);
7563
+ }
7564
+ return keys;
7565
+ }
7566
+ /**
7567
+ * Stale-data audit: remove stored global-settings keys the addon no longer
7568
+ * declares, and clear the keychain values their markers held — except a value
7569
+ * a surviving key still points at. A declared or absent key is refused.
7570
+ */
7571
+ async unsetStoredSettingsKeys(keys) {
7572
+ const settings = this._ctx?.settings;
7573
+ const raw = this.rawSettings();
7574
+ if (!settings || !raw) return {
7575
+ removed: [],
7576
+ refused: keys.map((key) => ({
7577
+ key,
7578
+ reason: "no settings store"
7579
+ })),
7580
+ secretsCleared: 0
7581
+ };
7582
+ let stored;
7583
+ try {
7584
+ stored = await raw.readAddonStore();
7585
+ } catch (error) {
7586
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
7587
+ return {
7588
+ removed: [],
7589
+ refused: keys.map((key) => ({
7590
+ key,
7591
+ reason
7592
+ })),
7593
+ secretsCleared: 0
7594
+ };
7595
+ }
7596
+ const declared = this.declaredKeySet();
7597
+ const removed = [];
7598
+ const refused = [];
7599
+ for (const key of keys) {
7600
+ const base = key.split("@", 1)[0] ?? key;
7601
+ if (declared.has(base)) refused.push({
7602
+ key,
7603
+ reason: "still declared by the addon"
7604
+ });
7605
+ else if (!(key in stored)) refused.push({
7606
+ key,
7607
+ reason: "not stored"
7608
+ });
7609
+ else removed.push(key);
7610
+ }
7611
+ if (removed.length === 0) return {
7612
+ removed,
7613
+ refused,
7614
+ secretsCleared: 0
7615
+ };
7616
+ const kept = /* @__PURE__ */ new Set();
7617
+ const held = /* @__PURE__ */ new Set();
7618
+ for (const [key, value] of Object.entries(stored)) {
7619
+ const target = removed.includes(key) ? held : kept;
7620
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
7621
+ }
7622
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
7623
+ const patch = {};
7624
+ for (const key of removed) patch[key] = void 0;
7625
+ await settings.writeAddonStore(patch);
7626
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
7627
+ await this.resolveConfig();
7628
+ return {
7629
+ removed,
7630
+ refused,
7631
+ secretsCleared
7632
+ };
7633
+ }
7543
7634
  /** Per-device variant of {@link state}, backed by the per-device store. */
7544
7635
  deviceState(deviceId, key, schema, fallback) {
7545
7636
  return createDurableState({
@@ -7810,6 +7901,21 @@ function dropPerNodeFields(fields) {
7810
7901
  }
7811
7902
  return kept;
7812
7903
  }
7904
+ function collectFieldKeys(fields) {
7905
+ const collected = [];
7906
+ for (const field of fields) {
7907
+ if (field.type === "group") {
7908
+ collected.push(...collectFieldKeys(field.fields));
7909
+ continue;
7910
+ }
7911
+ if (field.type === "sub-tabs") {
7912
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
7913
+ continue;
7914
+ }
7915
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
7916
+ }
7917
+ return collected;
7918
+ }
7813
7919
  function collectPerNodeFieldKeys(fields) {
7814
7920
  const collected = [];
7815
7921
  for (const field of fields) {
@@ -16081,8 +16187,172 @@ object({
16081
16187
  quantity: ReadingQuantitySchema,
16082
16188
  unit: string().min(1)
16083
16189
  });
16084
- /** How a leaf compares a device field to a value. Derived from the field's
16085
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16190
+ new Set([
16191
+ {
16192
+ id: "person",
16193
+ name: "Person"
16194
+ },
16195
+ {
16196
+ id: "vehicle",
16197
+ name: "Vehicle"
16198
+ },
16199
+ {
16200
+ id: "animal",
16201
+ name: "Animal"
16202
+ },
16203
+ {
16204
+ id: "package",
16205
+ name: "Package"
16206
+ }
16207
+ ].map((l) => l.id));
16208
+ var COCO_TO_MACRO = {
16209
+ mapping: {
16210
+ person: "person",
16211
+ bicycle: "vehicle",
16212
+ car: "vehicle",
16213
+ motorcycle: "vehicle",
16214
+ airplane: "vehicle",
16215
+ bus: "vehicle",
16216
+ train: "vehicle",
16217
+ truck: "vehicle",
16218
+ boat: "vehicle",
16219
+ bird: "animal",
16220
+ cat: "animal",
16221
+ dog: "animal",
16222
+ horse: "animal",
16223
+ sheep: "animal",
16224
+ cow: "animal",
16225
+ elephant: "animal",
16226
+ bear: "animal",
16227
+ zebra: "animal",
16228
+ giraffe: "animal",
16229
+ suitcase: "package",
16230
+ backpack: "package",
16231
+ handbag: "package"
16232
+ },
16233
+ preserveOriginal: false
16234
+ };
16235
+ /**
16236
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
16237
+ * animal, package) plus an OPTIONAL label that narrows it.
16238
+ *
16239
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
16240
+ * second list. The label that fits depends on the macro class:
16241
+ *
16242
+ * - `animal` → a sub-class (`dog`, `cat`, …);
16243
+ * - `person` → a known face (an identity of the `face-gallery`);
16244
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
16245
+ * - `package` → a sub-class.
16246
+ *
16247
+ * `other` exists only so a class the taxonomy does not know (a custom model's
16248
+ * `forklift`) survives the conversion of a stored recipe without widening.
16249
+ *
16250
+ * A label is decided LATE (D683): face recognition and LPR land after the
16251
+ * detection starts. The track-lifecycle `update` event carries the newly
16252
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
16253
+ * the subject GETS it — not at the first detection.
16254
+ */
16255
+ var SubClassLabelSchema = object({
16256
+ kind: literal("sub-class"),
16257
+ className: string().min(1)
16258
+ });
16259
+ var IdentityLabelSchema = object({
16260
+ kind: literal("identity"),
16261
+ /** The `face-gallery` identity id (stable across a rename). */
16262
+ identityId: string().min(1),
16263
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
16264
+ name: string().min(1)
16265
+ });
16266
+ var PlateLabelSchema = object({
16267
+ kind: literal("plate"),
16268
+ text: string().min(1)
16269
+ });
16270
+ function macroOf$1(className) {
16271
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
16272
+ }
16273
+ /** A sub-class label must belong to the macro it is under. */
16274
+ function subClassBelongs(macro, label) {
16275
+ return label === void 0 || macroOf$1(label.className) === macro;
16276
+ }
16277
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
16278
+ var DetectionSubjectSchema = discriminatedUnion("macro", [
16279
+ object({
16280
+ macro: literal("animal"),
16281
+ label: subClassOf("animal").optional()
16282
+ }),
16283
+ object({
16284
+ macro: literal("person"),
16285
+ label: IdentityLabelSchema.optional()
16286
+ }),
16287
+ object({
16288
+ macro: literal("vehicle"),
16289
+ label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
16290
+ }),
16291
+ object({
16292
+ macro: literal("package"),
16293
+ label: subClassOf("package").optional()
16294
+ }),
16295
+ object({
16296
+ macro: literal("other"),
16297
+ className: string().min(1)
16298
+ })
16299
+ ]);
16300
+ /**
16301
+ * The subject of a PARKED object. The stationary registry's event carries a
16302
+ * class and a zone, no face and no plate — an identity or plate label there
16303
+ * could never match, so the schema refuses it instead of letting it sit dead.
16304
+ */
16305
+ 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" });
16306
+ /**
16307
+ * A stored class string as a subject: a macro name stays a macro; a raw class
16308
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
16309
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
16310
+ */
16311
+ function subjectFromClassName(className) {
16312
+ const name = className.trim().toLowerCase();
16313
+ const macro = COCO_TO_MACRO.mapping[name];
16314
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
16315
+ if (macro === void 0) return {
16316
+ macro: "other",
16317
+ className
16318
+ };
16319
+ const label = {
16320
+ kind: "sub-class",
16321
+ className: name
16322
+ };
16323
+ switch (macro) {
16324
+ case "animal": return {
16325
+ macro: "animal",
16326
+ label
16327
+ };
16328
+ case "vehicle": return {
16329
+ macro: "vehicle",
16330
+ label
16331
+ };
16332
+ case "package": return {
16333
+ macro: "package",
16334
+ label
16335
+ };
16336
+ default: return {
16337
+ macro: "other",
16338
+ className
16339
+ };
16340
+ }
16341
+ }
16342
+ function macroOnly(macro) {
16343
+ switch (macro) {
16344
+ case "animal": return { macro: "animal" };
16345
+ case "person": return { macro: "person" };
16346
+ case "vehicle": return { macro: "vehicle" };
16347
+ case "package": return { macro: "package" };
16348
+ default: return {
16349
+ macro: "other",
16350
+ className: macro
16351
+ };
16352
+ }
16353
+ }
16354
+ /** How a state leaf compares a device field to a value. Derived from the
16355
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16086
16356
  var AutomationConditionOperatorSchema = _enum([
16087
16357
  "eq",
16088
16358
  "ne",
@@ -16093,12 +16363,76 @@ var AutomationConditionOperatorSchema = _enum([
16093
16363
  "contains",
16094
16364
  "in"
16095
16365
  ]);
16096
- var AutomationConditionLeafSchema = object({
16097
- kind: literal("condition"),
16366
+ /**
16367
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
16368
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
16369
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
16370
+ * fire; it does not lose it.
16371
+ */
16372
+ var DeviceBecomesLeafSchema = object({
16373
+ kind: literal("device-becomes"),
16374
+ deviceId: number().int().nonnegative(),
16375
+ cap: string().min(1),
16376
+ fieldPath: string().min(1),
16377
+ /** Fire when the field takes this value. Omit to fire on any change. */
16378
+ becomes: union([
16379
+ string(),
16380
+ number(),
16381
+ boolean()
16382
+ ]).optional(),
16383
+ /** Only on a CHANGE of value, not on every re-report. */
16384
+ edge: boolean().optional(),
16385
+ /** The new value must hold this long before it counts. */
16386
+ forMs: number().int().min(0).max(864e5).optional(),
16387
+ /** Collapse a burst into one occurrence. */
16388
+ debounceMs: number().int().min(0).max(6e5).optional()
16389
+ });
16390
+ var DeviceEventLeafSchema = object({
16391
+ kind: literal("device-event"),
16392
+ /** An `EventCategory` value. */
16393
+ category: string().min(1),
16394
+ deviceId: number().int().nonnegative().optional()
16395
+ });
16396
+ var ScheduleLeafSchema = object({
16397
+ kind: literal("schedule"),
16398
+ cron: string().min(1).max(120)
16399
+ });
16400
+ var ManualLeafSchema = object({ kind: literal("manual") });
16401
+ var IntervalLeafSchema = object({
16402
+ kind: literal("interval"),
16403
+ minutes: number().int().min(1).max(1440)
16404
+ });
16405
+ var DetectionLeafSchema = object({
16406
+ kind: literal("detection"),
16407
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
16408
+ subjects: array(DetectionSubjectSchema).min(1),
16409
+ deviceIds: array(number().int().nonnegative()).min(1),
16410
+ /** The track must have visited at least one of these zones. */
16411
+ zones: array(string().min(1)).min(1).optional(),
16412
+ /** Best-so-far confidence the track must reach. */
16413
+ minScore: number().min(0).max(1).optional()
16414
+ });
16415
+ var StationaryLeafSchema = object({
16416
+ kind: literal("stationary"),
16417
+ deviceIds: array(number().int().nonnegative()).min(1),
16418
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16419
+ zones: array(string().min(1)).min(1).optional(),
16420
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
16421
+ phase: _enum(["appeared", "departed"])
16422
+ });
16423
+ /** "is": one device field compared to one value, NOW. */
16424
+ var DeviceIsLeafSchema = object({
16425
+ kind: literal("device-is"),
16098
16426
  deviceId: number().int().nonnegative(),
16099
16427
  cap: string().min(1),
16100
16428
  fieldPath: string().min(1),
16101
16429
  operator: AutomationConditionOperatorSchema,
16430
+ /**
16431
+ * The value. A number on a quantity field is stored in the field's base unit
16432
+ * (D944); a quantity value is the WRITE form, which the save boundary
16433
+ * replaces with the converted number and `unit`. A leaf that still carries
16434
+ * one is never evaluated (it fails).
16435
+ */
16102
16436
  value: union([
16103
16437
  string(),
16104
16438
  number(),
@@ -16106,24 +16440,26 @@ var AutomationConditionLeafSchema = object({
16106
16440
  array(union([string(), number()])),
16107
16441
  QuantityValueSchema
16108
16442
  ]),
16443
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
16109
16444
  unit: string().min(1).optional()
16110
16445
  });
16111
- var AutomationConditionStationaryPresentSchema = object({
16446
+ /**
16447
+ * True when the camera currently has a PARKED object matching `subjects` in at
16448
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
16449
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
16450
+ */
16451
+ var StationaryPresentLeafSchema = object({
16112
16452
  kind: literal("stationary-present"),
16113
16453
  deviceId: number().int().nonnegative(),
16114
- /** Class names; a macro class matches every raw class that maps to it. */
16115
- classes: array(string().min(1)).min(1).optional(),
16116
- /** The parked object must be in at least one of these zones. */
16454
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16117
16455
  zones: array(string().min(1)).min(1).optional()
16118
16456
  });
16119
16457
  /**
16120
- * The expression leaf, declared as a plain object rather than an intersection
16121
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
16122
- * read `kind` off each option, and an intersection hides it. The author-time
16123
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
16124
- * cannot drift — an expression that one accepts, the other accepts.
16458
+ * The escape hatch: the bounded expression engine over named device bindings.
16459
+ * Declared as a plain object so a discriminated union can read `kind`; the
16460
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
16125
16461
  */
16126
- var AutomationConditionExpressionSchema = object({
16462
+ var ExpressionLeafSchema = object({
16127
16463
  kind: literal("expression"),
16128
16464
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16129
16465
  bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -16135,55 +16471,62 @@ var AutomationConditionExpressionSchema = object({
16135
16471
  path: ["expr"]
16136
16472
  });
16137
16473
  });
16138
- var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
16474
+ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
16139
16475
  object({
16140
16476
  kind: literal("all"),
16141
- children: array(AutomationConditionSchema)
16477
+ children: array(AutomationWhenSchema)
16142
16478
  }),
16143
16479
  object({
16144
16480
  kind: literal("any"),
16145
- children: array(AutomationConditionSchema)
16481
+ children: array(AutomationWhenSchema)
16146
16482
  }),
16147
16483
  object({
16148
16484
  kind: literal("not"),
16149
- child: AutomationConditionSchema
16485
+ child: AutomationWhenSchema
16150
16486
  }),
16151
- AutomationConditionLeafSchema,
16152
- AutomationConditionExpressionSchema,
16153
- AutomationConditionStationaryPresentSchema
16487
+ DeviceBecomesLeafSchema,
16488
+ DeviceEventLeafSchema,
16489
+ ScheduleLeafSchema,
16490
+ ManualLeafSchema,
16491
+ IntervalLeafSchema,
16492
+ DetectionLeafSchema,
16493
+ StationaryLeafSchema,
16494
+ DeviceIsLeafSchema,
16495
+ StationaryPresentLeafSchema,
16496
+ ExpressionLeafSchema
16154
16497
  ]));
16155
16498
  /**
16156
- * What starts a run.
16157
- *
16158
- * D8 compliance, and it is the reason `device-state` is not merely an event
16159
- * subscription: the trigger evaluates against the **state mirror**, which is
16160
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
16161
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
16162
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
16163
- * not.
16164
- */
16165
- var AutomationTriggerSchema = discriminatedUnion("kind", [
16499
+ * The recipe shape before the single `when` tree: a flat list of triggers
16500
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
16501
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
16502
+ * older client still posts — are in this shape, so it stays READABLE here, and
16503
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
16504
+ * way. Nothing writes this shape any more.
16505
+ *
16506
+ * `{ triggers: [t1…tn], conditions?: C }`
16507
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
16508
+ * a single trigger is not wrapped in `any`.
16509
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
16510
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
16511
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
16512
+ */
16513
+ var LegacyTriggerSchema = discriminatedUnion("kind", [
16166
16514
  object({
16167
16515
  kind: literal("device-state"),
16168
16516
  deviceId: number().int().nonnegative(),
16169
16517
  cap: string().min(1),
16170
16518
  fieldPath: string().min(1),
16171
- /** Fire when the field takes this value. Omit to fire on any change. */
16172
16519
  becomes: union([
16173
16520
  string(),
16174
16521
  number(),
16175
16522
  boolean()
16176
16523
  ]).optional(),
16177
- /** Only on a CHANGE of value, not on every re-report. */
16178
16524
  edge: boolean().optional(),
16179
- /** The condition must hold this long before the run starts. */
16180
16525
  forMs: number().int().min(0).max(864e5).optional(),
16181
- /** Collapse a burst into one run. */
16182
16526
  debounceMs: number().int().min(0).max(6e5).optional()
16183
16527
  }),
16184
16528
  object({
16185
16529
  kind: literal("device-event"),
16186
- /** An `EventCategory` value. */
16187
16530
  category: string().min(1),
16188
16531
  deviceId: number().int().nonnegative().optional()
16189
16532
  }),
@@ -16194,25 +16537,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16194
16537
  object({ kind: literal("manual") }),
16195
16538
  object({
16196
16539
  kind: literal("detection"),
16197
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
16198
- * matches every raw class that maps to it; any other name matches
16199
- * exactly. */
16200
16540
  classes: array(string().min(1)).min(1),
16201
16541
  deviceIds: array(number().int().nonnegative()).min(1),
16202
- /** The track must have visited at least one of these zones. */
16203
16542
  zones: array(string().min(1)).min(1).optional(),
16204
- /** Best-so-far confidence the track must reach. */
16205
16543
  minScore: number().min(0).max(1).optional()
16206
16544
  }),
16207
16545
  object({
16208
16546
  kind: literal("stationary"),
16209
16547
  deviceIds: array(number().int().nonnegative()).min(1),
16210
- /** Class names; a macro class matches like the detection trigger. */
16211
16548
  classes: array(string().min(1)).min(1).optional(),
16212
- /** The parked object must be in at least one of these zones. */
16213
16549
  zones: array(string().min(1)).min(1).optional(),
16214
- /** `appeared`: it became parked. `departed`: the registry retired it.
16215
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
16216
16550
  phase: _enum(["appeared", "departed"])
16217
16551
  }),
16218
16552
  object({
@@ -16220,6 +16554,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16220
16554
  minutes: number().int().min(1).max(1440)
16221
16555
  })
16222
16556
  ]);
16557
+ var LegacyConditionLeafSchema = object({
16558
+ kind: literal("condition"),
16559
+ deviceId: number().int().nonnegative(),
16560
+ cap: string().min(1),
16561
+ fieldPath: string().min(1),
16562
+ operator: AutomationConditionOperatorSchema,
16563
+ value: union([
16564
+ string(),
16565
+ number(),
16566
+ boolean(),
16567
+ array(union([string(), number()])),
16568
+ QuantityValueSchema
16569
+ ]),
16570
+ unit: string().min(1).optional()
16571
+ });
16572
+ var LegacyStationaryPresentSchema = object({
16573
+ kind: literal("stationary-present"),
16574
+ deviceId: number().int().nonnegative(),
16575
+ classes: array(string().min(1)).min(1).optional(),
16576
+ zones: array(string().min(1)).min(1).optional()
16577
+ });
16578
+ var LegacyExpressionSchema = object({
16579
+ kind: literal("expression"),
16580
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16581
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
16582
+ }).superRefine((src, ctx) => {
16583
+ const err = validateExpressionSource(src);
16584
+ if (err !== null) ctx.addIssue({
16585
+ code: "custom",
16586
+ message: err,
16587
+ path: ["expr"]
16588
+ });
16589
+ });
16590
+ var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
16591
+ object({
16592
+ kind: literal("all"),
16593
+ children: array(LegacyConditionSchema)
16594
+ }),
16595
+ object({
16596
+ kind: literal("any"),
16597
+ children: array(LegacyConditionSchema)
16598
+ }),
16599
+ object({
16600
+ kind: literal("not"),
16601
+ child: LegacyConditionSchema
16602
+ }),
16603
+ LegacyConditionLeafSchema,
16604
+ LegacyExpressionSchema,
16605
+ LegacyStationaryPresentSchema
16606
+ ]));
16607
+ function convertTrigger(trigger) {
16608
+ switch (trigger.kind) {
16609
+ case "device-state": return {
16610
+ ...trigger,
16611
+ kind: "device-becomes"
16612
+ };
16613
+ case "detection": {
16614
+ const { classes, ...rest } = trigger;
16615
+ return {
16616
+ ...rest,
16617
+ subjects: classes.map(subjectFromClassName)
16618
+ };
16619
+ }
16620
+ case "stationary": {
16621
+ const { classes, ...rest } = trigger;
16622
+ return {
16623
+ ...rest,
16624
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
16625
+ };
16626
+ }
16627
+ default: return trigger;
16628
+ }
16629
+ }
16630
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
16631
+ function convertLegacyCondition(condition) {
16632
+ switch (condition.kind) {
16633
+ case "condition": return {
16634
+ ...condition,
16635
+ kind: "device-is"
16636
+ };
16637
+ case "stationary-present": {
16638
+ const { classes, ...rest } = condition;
16639
+ return {
16640
+ ...rest,
16641
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
16642
+ };
16643
+ }
16644
+ case "expression": return condition;
16645
+ case "not": return {
16646
+ kind: "not",
16647
+ child: convertLegacyCondition(condition.child)
16648
+ };
16649
+ case "all":
16650
+ case "any": return {
16651
+ kind: condition.kind,
16652
+ children: condition.children.map(convertLegacyCondition)
16653
+ };
16654
+ }
16655
+ }
16656
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
16657
+ function convertLegacyWhen(half) {
16658
+ const leaves = half.triggers.map(convertTrigger);
16659
+ const first = leaves[0];
16660
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
16661
+ kind: "any",
16662
+ children: leaves
16663
+ };
16664
+ if (half.conditions === void 0) return triggers;
16665
+ return {
16666
+ kind: "all",
16667
+ children: [triggers, convertLegacyCondition(half.conditions)]
16668
+ };
16669
+ }
16670
+ /**
16671
+ * The automation RECIPE — what a visually-authored automation is, before it
16672
+ * becomes code.
16673
+ *
16674
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
16675
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
16676
+ * declarative description that {@link generateAutomationBlock} compiles to the
16677
+ * TypeScript of an ordinary core block. There is therefore no second rule
16678
+ * engine, no second store and no second runtime — the block runner is the
16679
+ * runtime, `core_blocks` is the store (§2.1).
16680
+ *
16681
+ * Two parts:
16682
+ *
16683
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
16684
+ * (instants) and state leaves (durations). It replaces the flat trigger list
16685
+ * plus the separate condition tree; the firing rule is in that file and in
16686
+ * ADR "one tree".
16687
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
16688
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
16689
+ * this had to diverge from "verbatim", and why.
16690
+ *
16691
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
16692
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
16693
+ */
16223
16694
  /**
16224
16695
  * One action step.
16225
16696
  *
@@ -16322,69 +16793,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
16322
16793
  minutes: number().int().min(1).max(1440)
16323
16794
  })
16324
16795
  ]);
16325
- var AutomationRecipeSchema = object({
16326
- triggers: array(AutomationTriggerSchema),
16327
- conditions: AutomationConditionSchema.optional(),
16796
+ var CurrentAutomationRecipeSchema = object({
16797
+ when: AutomationWhenSchema,
16328
16798
  actions: array(AutomationActionSchema),
16329
16799
  runMode: AutomationRunModeSchema.optional(),
16330
16800
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
16331
16801
  keepTraces: number().int().min(1).max(100).optional()
16332
16802
  });
16333
- new Set([
16334
- {
16335
- id: "person",
16336
- name: "Person"
16337
- },
16338
- {
16339
- id: "vehicle",
16340
- name: "Vehicle"
16341
- },
16342
- {
16343
- id: "animal",
16344
- name: "Animal"
16345
- },
16346
- {
16347
- id: "package",
16348
- name: "Package"
16349
- }
16350
- ].map((l) => l.id));
16351
- var COCO_TO_MACRO = {
16352
- mapping: {
16353
- person: "person",
16354
- bicycle: "vehicle",
16355
- car: "vehicle",
16356
- motorcycle: "vehicle",
16357
- airplane: "vehicle",
16358
- bus: "vehicle",
16359
- train: "vehicle",
16360
- truck: "vehicle",
16361
- boat: "vehicle",
16362
- bird: "animal",
16363
- cat: "animal",
16364
- dog: "animal",
16365
- horse: "animal",
16366
- sheep: "animal",
16367
- cow: "animal",
16368
- elephant: "animal",
16369
- bear: "animal",
16370
- zebra: "animal",
16371
- giraffe: "animal",
16372
- suitcase: "package",
16373
- backpack: "package",
16374
- handbag: "package"
16375
- },
16376
- preserveOriginal: false
16377
- };
16803
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
16804
+ var LegacyAutomationRecipeSchema = object({
16805
+ triggers: array(LegacyTriggerSchema),
16806
+ conditions: LegacyConditionSchema.optional(),
16807
+ actions: array(AutomationActionSchema),
16808
+ runMode: AutomationRunModeSchema.optional(),
16809
+ keepTraces: number().int().min(1).max(100).optional()
16810
+ });
16811
+ /** The old shape as the new one, lossless. */
16812
+ function convertLegacyRecipe(legacy) {
16813
+ const { triggers, conditions, ...rest } = legacy;
16814
+ return {
16815
+ when: convertLegacyWhen({
16816
+ triggers,
16817
+ conditions
16818
+ }),
16819
+ ...rest
16820
+ };
16821
+ }
16822
+ /**
16823
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
16824
+ * saved block carries): the new shape as it is, or the old shape converted to
16825
+ * it. The output type is always the new one.
16826
+ */
16827
+ var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
16378
16828
  new Set(Object.values(COCO_TO_MACRO.mapping));
16379
16829
  /**
16380
16830
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
16381
16831
  * so the editor can paint every block green or red with its reason.
16382
16832
  *
16383
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
16384
- * every condition node (containers and leaves, including the ones a
16385
- * short-circuit never reached), the run-mode verdict and every action step —
16386
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
16387
- * for an Action run). Skipped
16833
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
16834
+ * fired, every node of the `when` tree (containers and leaves, including the
16835
+ * ones a short-circuit never reached), the run-mode verdict and every action
16836
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
16837
+ * for an Action run). Traces written before the single tree carry
16838
+ * `conditions.*` paths and still parse. Skipped
16388
16839
  * evaluations are traced too, and identical consecutive skips collapse into a
16389
16840
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
16390
16841
  *
@@ -16416,6 +16867,8 @@ var TraceNodeOutcomeSchema = _enum([
16416
16867
  ]);
16417
16868
  var AutomationTriggerTraceSchema = object({
16418
16869
  kind: string().min(1),
16870
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
16871
+ path: string().min(1).optional(),
16419
16872
  outcome: _enum(["pass", "fail"]),
16420
16873
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
16421
16874
  reason: string(),
@@ -16423,15 +16876,23 @@ var AutomationTriggerTraceSchema = object({
16423
16876
  vars: record(string(), TraceValueSchema)
16424
16877
  });
16425
16878
  var AutomationConditionTraceNodeSchema = object({
16426
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
16879
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
16427
16880
  path: string().min(1),
16428
16881
  kind: _enum([
16429
16882
  "all",
16430
16883
  "any",
16431
16884
  "not",
16432
16885
  "condition",
16886
+ "device-is",
16433
16887
  "expression",
16434
- "stationary-present"
16888
+ "stationary-present",
16889
+ "device-becomes",
16890
+ "device-event",
16891
+ "schedule",
16892
+ "interval",
16893
+ "manual",
16894
+ "detection",
16895
+ "stationary"
16435
16896
  ]),
16436
16897
  outcome: TraceNodeOutcomeSchema,
16437
16898
  reason: string(),
@@ -16465,6 +16926,12 @@ var AutomationStepTraceSchema = object({
16465
16926
  deviceId: number().int().optional(),
16466
16927
  startedAt: number().optional(),
16467
16928
  endedAt: number().optional(),
16929
+ /**
16930
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
16931
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
16932
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
16933
+ */
16934
+ args: record(string(), TraceValueSchema).optional(),
16468
16935
  outcome: _enum([
16469
16936
  "ok",
16470
16937
  "error",
@@ -17214,6 +17681,23 @@ method(object({
17214
17681
  addonId: string(),
17215
17682
  nodeId: string().optional()
17216
17683
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
17684
+ addonId: string(),
17685
+ nodeId: string().optional()
17686
+ }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
17687
+ addonId: string(),
17688
+ nodeId: string().optional(),
17689
+ keys: array(string())
17690
+ }), object({
17691
+ removed: array(string()),
17692
+ refused: array(object({
17693
+ key: string(),
17694
+ reason: string()
17695
+ })),
17696
+ secretsCleared: number()
17697
+ }), {
17698
+ kind: "mutation",
17699
+ auth: "admin"
17700
+ }), method(object({
17217
17701
  addonId: string(),
17218
17702
  deviceId: number(),
17219
17703
  nodeId: string().optional()
@@ -21243,6 +21727,8 @@ var StorageKindSchema = object({
21243
21727
  "orphaned",
21244
21728
  "dropped"
21245
21729
  ]),
21730
+ /** An owner declared this kind since the engine started. */
21731
+ declaredThisBoot: boolean(),
21246
21732
  /** `null` when the caller asked for no counts (`counts: false`). */
21247
21733
  rows: number().int().nonnegative().nullable(),
21248
21734
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -21254,6 +21740,51 @@ var StorageKindSchema = object({
21254
21740
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
21255
21741
  declaration: RecordKindDeclarationSchema
21256
21742
  });
21743
+ _enum([
21744
+ "orphaned",
21745
+ "stale",
21746
+ "unknown"
21747
+ ]);
21748
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
21749
+ var StaleKindFindingSchema = object({
21750
+ owner: string(),
21751
+ kind: string(),
21752
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
21753
+ verdict: _enum(["orphaned", "unknown"]),
21754
+ reason: string(),
21755
+ rows: number().int().nonnegative().nullable(),
21756
+ /** local-auth's tables are never dropped by this audit. */
21757
+ protected: boolean()
21758
+ });
21759
+ /** A stored global-settings key the running addon no longer declares. */
21760
+ var StaleSettingsKeyFindingSchema = object({
21761
+ addonId: string(),
21762
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
21763
+ key: string(),
21764
+ /**
21765
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
21766
+ * not orphans yet. Removing the key releases them.
21767
+ */
21768
+ secretRefs: array(string())
21769
+ });
21770
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
21771
+ var StaleUnknownSchema = object({
21772
+ subject: string(),
21773
+ reason: string()
21774
+ });
21775
+ var StaleDataReportSchema = object({
21776
+ /** Epoch ms of the last finished scan; `null` before the first. */
21777
+ scannedAt: number().nullable(),
21778
+ scanning: boolean(),
21779
+ trigger: _enum([
21780
+ "boot",
21781
+ "interval",
21782
+ "manual"
21783
+ ]).nullable(),
21784
+ kinds: array(StaleKindFindingSchema),
21785
+ settingsKeys: array(StaleSettingsKeyFindingSchema),
21786
+ unknown: array(StaleUnknownSchema)
21787
+ });
21257
21788
  method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
21258
21789
  owner: string().min(1).optional(),
21259
21790
  /** `false` skips the per-kind row count (a catalog read only). Default true. */
@@ -21261,7 +21792,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
21261
21792
  }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
21262
21793
  owner: string().min(1),
21263
21794
  kind: RecordKindNameSchema
21264
- }), RecordQueryResultSchema, { auth: "admin" });
21795
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
21796
+ kind: "mutation",
21797
+ auth: "admin"
21798
+ }), method(object({
21799
+ owner: string().min(1),
21800
+ kind: RecordKindNameSchema
21801
+ }), object({
21802
+ deleted: number().int().nonnegative(),
21803
+ secretsReleased: number().int().nonnegative()
21804
+ }), {
21805
+ kind: "mutation",
21806
+ auth: "admin",
21807
+ caller: "required"
21808
+ }), method(object({
21809
+ addonId: string().min(1),
21810
+ key: string().min(1)
21811
+ }), object({
21812
+ removed: boolean(),
21813
+ secretsCleared: number().int().nonnegative()
21814
+ }), {
21815
+ kind: "mutation",
21816
+ auth: "admin",
21817
+ caller: "required"
21818
+ });
21265
21819
  /**
21266
21820
  * record-entries - the ONE admin surface for every list of user-created entries
21267
21821
  * (D881): notification targets, brokers, LLM profiles, anything an addon
@@ -21400,6 +21954,12 @@ method(object({
21400
21954
  }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
21401
21955
  kind: "mutation",
21402
21956
  auth: "admin"
21957
+ }), method(object({
21958
+ refs: RefListSchema,
21959
+ onBehalfOf: string().min(1)
21960
+ }), object({ released: number().int() }), {
21961
+ kind: "mutation",
21962
+ auth: "admin"
21403
21963
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
21404
21964
  /**
21405
21965
  * device-history — the ONE mechanism for numeric history (D850).
@@ -22302,7 +22862,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
22302
22862
  /** Upstream-system identity + rendering hints for the candidate. */
22303
22863
  sourceInfo: SourceInfoSchema.optional(),
22304
22864
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
22305
- children: array(DiscoveredChildDeviceSchema).readonly().optional()
22865
+ children: array(DiscoveredChildDeviceSchema).readonly().optional(),
22866
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
22867
+ parentStableId: string().optional()
22306
22868
  }));
22307
22869
  /**
22308
22870
  * Status block — populated by the integration after each
@@ -42685,6 +43247,79 @@ object({
42685
43247
  metadata: record(string(), unknown()).optional()
42686
43248
  });
42687
43249
  /**
43250
+ * HOW a provider can be found on the network. The provider only DECLARES the
43251
+ * mode; the `local-network` scan engine runs it once for every provider that
43252
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
43253
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
43254
+ * except through a `custom` mode.
43255
+ */
43256
+ var DiscoveryModeSchema = discriminatedUnion("kind", [
43257
+ object({
43258
+ kind: literal("mdns"),
43259
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
43260
+ serviceType: string().min(1),
43261
+ /** Regular expression an instance name must match (case-insensitive). */
43262
+ instancePattern: string().optional()
43263
+ }),
43264
+ object({
43265
+ kind: literal("ws-discovery"),
43266
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
43267
+ types: array(string()).optional()
43268
+ }),
43269
+ object({
43270
+ kind: literal("ssdp"),
43271
+ searchTarget: string().min(1)
43272
+ }),
43273
+ object({
43274
+ kind: literal("udp-broadcast"),
43275
+ port: number().int().min(1).max(65535),
43276
+ payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
43277
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
43278
+ listenPort: number().int().min(1).max(65535).optional()
43279
+ }),
43280
+ object({
43281
+ kind: literal("http-probe"),
43282
+ port: number().int().min(1).max(65535),
43283
+ path: string().startsWith("/"),
43284
+ method: _enum(["GET", "POST"]).default("GET"),
43285
+ bodyText: string().optional(),
43286
+ contentType: string().optional(),
43287
+ tls: boolean().default(false),
43288
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
43289
+ acceptStatus: array(number().int()).optional()
43290
+ }),
43291
+ object({
43292
+ kind: literal("custom"),
43293
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
43294
+ reason: string().min(1)
43295
+ })
43296
+ ]);
43297
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
43298
+ var DiscoveryHitSchema = object({
43299
+ /** Index into the provider's own `getDiscoveryModes()` array. */
43300
+ modeIndex: number().int().min(0),
43301
+ host: string(),
43302
+ port: number().int(),
43303
+ http: object({
43304
+ status: number().int(),
43305
+ headers: record(string(), string()),
43306
+ /** At most 16 KiB. */
43307
+ bodyText: string()
43308
+ }).optional(),
43309
+ mdns: object({
43310
+ instance: string(),
43311
+ host: string(),
43312
+ port: number().int(),
43313
+ txt: record(string(), string())
43314
+ }).optional(),
43315
+ udp: object({ payloadHex: string() }).optional(),
43316
+ wsd: object({
43317
+ xaddrs: array(string()),
43318
+ types: array(string()),
43319
+ scopes: array(string())
43320
+ }).optional()
43321
+ });
43322
+ /**
42688
43323
  * Candidate handed back from discovery and accepted by
42689
43324
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
42690
43325
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -42719,7 +43354,17 @@ var DiscoveryCandidateSchema = object({
42719
43354
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
42720
43355
  onboardedDeviceId: number().optional(),
42721
43356
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
42722
- onboardedName: string().optional()
43357
+ onboardedName: string().optional(),
43358
+ /**
43359
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
43360
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
43361
+ * which is still read for one release.
43362
+ */
43363
+ setupIntegration: boolean().optional(),
43364
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
43365
+ parentStableId: string().optional(),
43366
+ /** `false` = shown for context but not pickable. Default true. */
43367
+ selectable: boolean().optional()
42723
43368
  });
42724
43369
  /**
42725
43370
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -42816,6 +43461,24 @@ var deviceProviderCapability = {
42816
43461
  auth: "admin"
42817
43462
  }),
42818
43463
  /**
43464
+ * How this provider is found on the network. `[]` = the provider has no
43465
+ * declarative mode and keeps answering `discoverDevices` itself.
43466
+ */
43467
+ getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
43468
+ /**
43469
+ * Turn the engine's raw hits into candidates. The provider may make
43470
+ * follow-up UNICAST requests to a hit host (an authenticated call, an
43471
+ * identity check) and never sweeps a subnet. `params` is the per-scan form
43472
+ * from `getDiscoveryParamsSchema`.
43473
+ */
43474
+ classifyDiscoveryHits: method(object({
43475
+ hits: array(DiscoveryHitSchema),
43476
+ params: record(string(), unknown()).optional()
43477
+ }), array(DiscoveryCandidateSchema), {
43478
+ kind: "mutation",
43479
+ auth: "admin"
43480
+ }),
43481
+ /**
42819
43482
  * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
42820
43483
  * provider accepts (e.g. Gree's broadcast address for a different subnet).
42821
43484
  * `null` when the provider takes no extra scan params — the generic
@@ -43580,17 +44243,23 @@ method(object({
43580
44243
  }), method(object({}), object({ providers: array(object({
43581
44244
  addonId: string(),
43582
44245
  label: string()
43583
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
43584
- addonId: string(),
43585
- label: string(),
43586
- candidates: array(DiscoveryCandidateSchema).readonly(),
43587
- error: string().nullable()
43588
- })).readonly() }), {
43589
- kind: "mutation",
43590
- auth: "admin"
43591
- }), method(object({
44246
+ })).readonly() }), { auth: "admin" }), method(
44247
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
44248
+ object({ subnets: array(string()).optional() }),
44249
+ object({ groups: array(object({
44250
+ addonId: string(),
44251
+ label: string(),
44252
+ candidates: array(DiscoveryCandidateSchema).readonly(),
44253
+ error: string().nullable()
44254
+ })).readonly() }),
44255
+ {
44256
+ kind: "mutation",
44257
+ auth: "admin"
44258
+ }
44259
+ ), method(object({
43592
44260
  addonId: string(),
43593
- params: record(string(), unknown()).optional()
44261
+ params: record(string(), unknown()).optional(),
44262
+ subnets: array(string()).optional()
43594
44263
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
43595
44264
  kind: "mutation",
43596
44265
  auth: "admin"
@@ -56257,6 +56926,12 @@ var TerminalInstanceInfoSchema = object({
56257
56926
  instanceId: string(),
56258
56927
  cameraStableId: string(),
56259
56928
  nodeId: string(),
56929
+ /**
56930
+ * False while the node the instance is bound to is not in the cluster
56931
+ * topology (forgotten, renamed, re-paired). The instance is kept; the
56932
+ * operator re-points it with `updateInstance({ targetNodeId })`.
56933
+ */
56934
+ nodeInCluster: boolean(),
56260
56935
  profileId: string(),
56261
56936
  profileLabel: string(),
56262
56937
  name: string(),
@@ -56306,6 +56981,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
56306
56981
  auth: "admin"
56307
56982
  }), method(object({
56308
56983
  instanceId: string().min(1),
56984
+ /** Re-point the instance to another node; the profile must exist there. */
56985
+ targetNodeId: string().min(1).max(256).optional(),
56309
56986
  name: string().trim().min(1).max(160).optional(),
56310
56987
  executable: string().max(1024).optional(),
56311
56988
  args: array(string().max(2048)).max(64).optional(),
@@ -62932,7 +63609,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
62932
63609
  id: string(),
62933
63610
  settings: record(string(), unknown()),
62934
63611
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
62935
- secretKeys: array(string()).optional()
63612
+ secretKeys: array(string()).optional(),
63613
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
63614
+ skipRestart: boolean().optional()
62936
63615
  }), object({ success: boolean() }), {
62937
63616
  kind: "mutation",
62938
63617
  auth: "admin"
@@ -63560,6 +64239,39 @@ var TlsStatusSchema = object({
63560
64239
  hasCa: boolean()
63561
64240
  }).nullable()
63562
64241
  });
64242
+ /** One provider's ask of the scan engine: the modes it declared. */
64243
+ var ScanRequestSchema = object({
64244
+ providerId: string().min(1),
64245
+ modes: array(DiscoveryModeSchema)
64246
+ });
64247
+ var ScanErrorSchema = object({
64248
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
64249
+ providerId: string().optional(),
64250
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
64251
+ mode: string().optional(),
64252
+ message: string()
64253
+ });
64254
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
64255
+ var ScanStatsSchema = object({
64256
+ hostsProbed: number().int(),
64257
+ hits: number().int(),
64258
+ timeouts: number().int(),
64259
+ byMode: record(string(), object({
64260
+ probed: number().int(),
64261
+ hits: number().int(),
64262
+ timeouts: number().int()
64263
+ }))
64264
+ });
64265
+ var ScanNetworkResultSchema = object({
64266
+ hits: array(object({
64267
+ providerId: string(),
64268
+ hit: DiscoveryHitSchema
64269
+ })),
64270
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
64271
+ subnetsScanned: array(string()),
64272
+ errors: array(ScanErrorSchema),
64273
+ stats: ScanStatsSchema
64274
+ });
63563
64275
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
63564
64276
  /**
63565
64277
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -63614,6 +64326,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
63614
64326
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
63615
64327
  kind: "mutation",
63616
64328
  auth: "admin"
64329
+ }), method(object({
64330
+ requests: array(ScanRequestSchema),
64331
+ subnets: array(string()).optional()
64332
+ }), ScanNetworkResultSchema, {
64333
+ kind: "mutation",
64334
+ auth: "admin",
64335
+ timeoutMs: 9e4
64336
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
64337
+ kind: "mutation",
64338
+ auth: "admin"
63617
64339
  }), method(_void(), TlsStatusSchema, {
63618
64340
  kind: "mutation",
63619
64341
  auth: "admin"
@@ -73196,6 +73918,17 @@ var BaseDeviceProvider = class extends BaseAddon {
73196
73918
  async discoverDevices(_input) {
73197
73919
  return [];
73198
73920
  }
73921
+ /**
73922
+ * How the scan engine finds this provider. Default `[]`: the provider is
73923
+ * not migrated and keeps answering `discoverDevices` itself.
73924
+ */
73925
+ async getDiscoveryModes() {
73926
+ return [];
73927
+ }
73928
+ /** Turn the scan engine's raw hits into candidates. Default: none. */
73929
+ async classifyDiscoveryHits(_input) {
73930
+ return [];
73931
+ }
73199
73932
  /** Extra per-scan input form (e.g. a broadcast address for another subnet).
73200
73933
  * Null = no extra params. Override in providers that support scoped scans. */
73201
73934
  async getDiscoveryParamsSchema() {
@@ -74064,6 +74797,13 @@ Object.freeze({
74064
74797
  access: "create",
74065
74798
  role: "admin"
74066
74799
  },
74800
+ "addonSettings.getDeclaredSettingsKeys": {
74801
+ capName: "addon-settings",
74802
+ capScope: "system",
74803
+ addonId: null,
74804
+ access: "view",
74805
+ role: "admin"
74806
+ },
74067
74807
  "addonSettings.getDeviceSettings": {
74068
74808
  capName: "addon-settings",
74069
74809
  capScope: "system",
@@ -74085,6 +74825,13 @@ Object.freeze({
74085
74825
  access: "view",
74086
74826
  role: "admin"
74087
74827
  },
74828
+ "addonSettings.unsetStoredSettingsKeys": {
74829
+ capName: "addon-settings",
74830
+ capScope: "system",
74831
+ addonId: null,
74832
+ access: "create",
74833
+ role: "admin"
74834
+ },
74088
74835
  "addonSettings.updateDeviceSettings": {
74089
74836
  capName: "addon-settings",
74090
74837
  capScope: "system",
@@ -76500,6 +77247,13 @@ Object.freeze({
76500
77247
  access: "create",
76501
77248
  role: "admin"
76502
77249
  },
77250
+ "deviceProvider.classifyDiscoveryHits": {
77251
+ capName: "device-provider",
77252
+ capScope: "system",
77253
+ addonId: null,
77254
+ access: "create",
77255
+ role: "admin"
77256
+ },
76503
77257
  "deviceProvider.createDevice": {
76504
77258
  capName: "device-provider",
76505
77259
  capScope: "system",
@@ -76528,6 +77282,13 @@ Object.freeze({
76528
77282
  access: "view",
76529
77283
  role: "admin"
76530
77284
  },
77285
+ "deviceProvider.getDiscoveryModes": {
77286
+ capName: "device-provider",
77287
+ capScope: "system",
77288
+ addonId: null,
77289
+ access: "view",
77290
+ role: "admin"
77291
+ },
76531
77292
  "deviceProvider.getDiscoveryParamsSchema": {
76532
77293
  capName: "device-provider",
76533
77294
  capScope: "system",
@@ -77305,6 +78066,13 @@ Object.freeze({
77305
78066
  access: "create",
77306
78067
  role: "admin"
77307
78068
  },
78069
+ "keychain.releaseRecordRefs": {
78070
+ capName: "keychain",
78071
+ capScope: "system",
78072
+ addonId: null,
78073
+ access: "create",
78074
+ role: "admin"
78075
+ },
77308
78076
  "keychain.resolve": {
77309
78077
  capName: "keychain",
77310
78078
  capScope: "system",
@@ -77725,6 +78493,13 @@ Object.freeze({
77725
78493
  access: "view",
77726
78494
  role: "admin"
77727
78495
  },
78496
+ "localNetwork.getScanSubnets": {
78497
+ capName: "local-network",
78498
+ capScope: "system",
78499
+ addonId: null,
78500
+ access: "view",
78501
+ role: "admin"
78502
+ },
77728
78503
  "localNetwork.getTlsStatus": {
77729
78504
  capName: "local-network",
77730
78505
  capScope: "system",
@@ -77774,6 +78549,13 @@ Object.freeze({
77774
78549
  access: "create",
77775
78550
  role: "admin"
77776
78551
  },
78552
+ "localNetwork.scanNetwork": {
78553
+ capName: "local-network",
78554
+ capScope: "system",
78555
+ addonId: null,
78556
+ access: "create",
78557
+ role: "admin"
78558
+ },
77777
78559
  "localNetwork.setAddressRole": {
77778
78560
  capName: "local-network",
77779
78561
  capScope: "system",
@@ -77795,6 +78577,13 @@ Object.freeze({
77795
78577
  access: "create",
77796
78578
  role: "admin"
77797
78579
  },
78580
+ "localNetwork.setScanSubnets": {
78581
+ capName: "local-network",
78582
+ capScope: "system",
78583
+ addonId: null,
78584
+ access: "create",
78585
+ role: "admin"
78586
+ },
77798
78587
  "localNetwork.setViewerEndpoints": {
77799
78588
  capName: "local-network",
77800
78589
  capScope: "system",
@@ -82016,6 +82805,13 @@ Object.freeze({
82016
82805
  access: "create",
82017
82806
  role: "admin"
82018
82807
  },
82808
+ "storageAdmin.dropOrphanedKind": {
82809
+ capName: "storage-admin",
82810
+ capScope: "system",
82811
+ addonId: null,
82812
+ access: "delete",
82813
+ role: "admin"
82814
+ },
82019
82815
  "storageAdmin.listKinds": {
82020
82816
  capName: "storage-admin",
82021
82817
  capScope: "system",
@@ -82037,6 +82833,27 @@ Object.freeze({
82037
82833
  access: "view",
82038
82834
  role: "admin"
82039
82835
  },
82836
+ "storageAdmin.scanStaleData": {
82837
+ capName: "storage-admin",
82838
+ capScope: "system",
82839
+ addonId: null,
82840
+ access: "create",
82841
+ role: "admin"
82842
+ },
82843
+ "storageAdmin.staleDataReport": {
82844
+ capName: "storage-admin",
82845
+ capScope: "system",
82846
+ addonId: null,
82847
+ access: "view",
82848
+ role: "admin"
82849
+ },
82850
+ "storageAdmin.unsetStaleSettingsKey": {
82851
+ capName: "storage-admin",
82852
+ capScope: "system",
82853
+ addonId: null,
82854
+ access: "create",
82855
+ role: "admin"
82856
+ },
82040
82857
  "storageEvictable.evict": {
82041
82858
  capName: "storage-evictable",
82042
82859
  capScope: "system",
@@ -92506,7 +93323,8 @@ function resolveNativeId(integrationId, covers, childNativeId) {
92506
93323
  /**
92507
93324
  * The adoption candidates of one integration, pure and in a fixed order: the hub
92508
93325
  * box ({@link DeviceType.Gateway}), then one {@link DeviceType.Cover} per roller
92509
- * shutter. None is a child of the gateway.
93326
+ * shutter. Each cover names the gateway through `parentStableId`, so the
93327
+ * adoption modal nests it under the hub.
92510
93328
  */
92511
93329
  function buildRademacherCandidates(input) {
92512
93330
  const gatewayId = gatewayNativeId(input.integrationId);
@@ -92538,7 +93356,8 @@ function buildRademacherCandidates(input) {
92538
93356
  },
92539
93357
  alreadyAdopted: adoptedId !== null,
92540
93358
  adoptedDeviceId: adoptedId,
92541
- capabilities: [coverCapability.name]
93359
+ capabilities: [coverCapability.name],
93360
+ parentStableId: gatewayId
92542
93361
  });
92543
93362
  }
92544
93363
  return out;
@@ -93225,7 +94044,7 @@ var HubDevice = class extends BaseDevice {
93225
94044
  var VERSION_PATH = "/service/system-update-image/version";
93226
94045
  var NODENAME_PATH = "/service/system/networkmgr/v1/nodename";
93227
94046
  var INTERFACES_PATH = "/service/system/networkmgr/v1/interfaces";
93228
- var SALT_PATH = "/authentication/password_salt";
94047
+ var SALT_PATH$1 = "/authentication/password_salt";
93229
94048
  async function tryJson(fetchImpl, url, timeoutMs) {
93230
94049
  try {
93231
94050
  const res = await fetchImpl(url, {
@@ -93264,7 +94083,7 @@ async function probeHub(fetchImpl, host, timeoutMs = 1500) {
93264
94083
  }
93265
94084
  async function saltStatus(fetchImpl, host, basePath, timeoutMs) {
93266
94085
  try {
93267
- return (await fetchImpl(`http://${host}${basePath}${SALT_PATH}`, {
94086
+ return (await fetchImpl(`http://${host}${basePath}${SALT_PATH$1}`, {
93268
94087
  method: "POST",
93269
94088
  timeoutMs
93270
94089
  })).statusCode;
@@ -94142,8 +94961,28 @@ function slugifyHost(value) {
94142
94961
  }
94143
94962
  //#endregion
94144
94963
  //#region src/rademacher-hub-discovery.ts
94145
- /** `prefilledConfig` flag: the scan opens the integration form instead of adopting a device. */
94146
- var RADEMACHER_SETUP_INTEGRATION_KEY = "setupIntegration";
94964
+ /**
94965
+ * The request the library's probe opens with: a POST on the password-salt
94966
+ * endpoint, which a HomePilot / Start2Smart hub answers 200 (protected) or 500
94967
+ * (open). The api base path is `''` on v1 and `/hp` on v2, so there are two modes.
94968
+ */
94969
+ var SALT_PATH = "/authentication/password_salt";
94970
+ var SALT_STATUSES = [200, 500];
94971
+ var RADEMACHER_DISCOVERY_MODES = [{
94972
+ kind: "http-probe",
94973
+ port: 80,
94974
+ path: SALT_PATH,
94975
+ method: "POST",
94976
+ tls: false,
94977
+ acceptStatus: [...SALT_STATUSES]
94978
+ }, {
94979
+ kind: "http-probe",
94980
+ port: 80,
94981
+ path: `/hp${SALT_PATH}`,
94982
+ method: "POST",
94983
+ tls: false,
94984
+ acceptStatus: [...SALT_STATUSES]
94985
+ }];
94147
94986
  /** Project the library's probe result onto the shape the form and the candidates share. */
94148
94987
  function hubFromIdentity(identity) {
94149
94988
  const uid = identity.uid?.trim().toLowerCase() ?? "";
@@ -94156,14 +94995,35 @@ function hubFromIdentity(identity) {
94156
94995
  basePath: identity.basePath
94157
94996
  };
94158
94997
  }
94159
- /** Sweep the local /24 networks for HomePilot / Start2Smart hubs (the library's HTTP probe). */
94160
- async function browseRademacherHubs(discover = () => Rademacher.discover()) {
94161
- const seen = /* @__PURE__ */ new Set();
94998
+ var probeFetch = async (url, options) => {
94999
+ const res = await fetch(url, {
95000
+ method: options?.method ?? "GET",
95001
+ ...options?.body !== void 0 ? { body: options.body } : {},
95002
+ ...options?.headers !== void 0 ? { headers: { ...options.headers } } : {},
95003
+ signal: AbortSignal.timeout(options?.timeoutMs ?? 1500)
95004
+ });
95005
+ return {
95006
+ statusCode: res.status,
95007
+ headers: Object.fromEntries(res.headers.entries()),
95008
+ json: () => res.json(),
95009
+ text: () => res.text()
95010
+ };
95011
+ };
95012
+ var probeRademacherHost = (host) => probeHub(probeFetch, host);
95013
+ /**
95014
+ * Hubs among the engine's http-probe hits. A hit only says the salt endpoint
95015
+ * answered; the library's identity check (a unicast follow-up to that host)
95016
+ * decides whether it really is a hub and reads its name, model and MAC.
95017
+ */
95018
+ async function hubsFromHits(hits, probe = probeRademacherHost) {
94162
95019
  const out = [];
94163
- for (const identity of await discover()) {
94164
- if (seen.has(identity.host)) continue;
94165
- seen.add(identity.host);
94166
- out.push(hubFromIdentity(identity));
95020
+ const seen = /* @__PURE__ */ new Set();
95021
+ for (const hit of hits) {
95022
+ if (hit.http === void 0 || !SALT_STATUSES.includes(hit.http.status)) continue;
95023
+ if (seen.has(hit.host)) continue;
95024
+ seen.add(hit.host);
95025
+ const identity = await probe(hit.host).catch(() => null);
95026
+ if (identity !== null) out.push(hubFromIdentity(identity));
94167
95027
  }
94168
95028
  return out;
94169
95029
  }
@@ -94178,12 +95038,13 @@ function proposeRademacherHubs(found, existingHosts) {
94178
95038
  function rademacherDiscoveryCandidates(proposals) {
94179
95039
  return proposals.map((found) => ({
94180
95040
  stableId: `rademacher:${found.id}`,
95041
+ type: DeviceType.Gateway,
94181
95042
  suggestedName: found.name,
94182
95043
  prefilledConfig: {
94183
95044
  host: found.host,
94184
- ...found.basePath.length > 0 ? { basePath: found.basePath } : {},
94185
- [RADEMACHER_SETUP_INTEGRATION_KEY]: true
94186
- }
95045
+ ...found.basePath.length > 0 ? { basePath: found.basePath } : {}
95046
+ },
95047
+ setupIntegration: true
94187
95048
  }));
94188
95049
  }
94189
95050
  function rademacherDiscoverySummary(foundCount, proposedCount) {
@@ -94239,14 +95100,15 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
94239
95100
  ],
94240
95101
  customActions: rademacherActions,
94241
95102
  actionHandlers: { discoverHub: async () => {
94242
- const { proposals, summary } = await this.discoverHubs();
95103
+ const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
95104
+ const hosts = candidates.map((candidate) => ({
95105
+ host: String(candidate.prefilledConfig["host"] ?? ""),
95106
+ name: candidate.suggestedName,
95107
+ id: candidate.stableId.replace(/^rademacher:/, "")
95108
+ }));
94243
95109
  return {
94244
- hosts: proposals.map((found) => ({
94245
- host: found.host,
94246
- name: found.name,
94247
- id: found.id
94248
- })),
94249
- summary
95110
+ hosts,
95111
+ summary: rademacherDiscoverySummary(hosts.length, hosts.length)
94250
95112
  };
94251
95113
  } }
94252
95114
  };
@@ -94266,21 +95128,21 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
94266
95128
  async supportsManualCreation() {
94267
95129
  return false;
94268
95130
  }
94269
- /**
94270
- * LAN scan for HomePilot / Start2Smart hubs: the library's HTTP probe over the
94271
- * local /24 networks of this (hub-only) addon's host.
94272
- */
95131
+ /** A hub is found by the scan engine's http-probe; this addon classifies the hits. */
94273
95132
  async supportsDiscovery() {
94274
95133
  return true;
94275
95134
  }
94276
- async discoverDevices() {
94277
- const { proposals } = await this.discoverHubs();
94278
- return rademacherDiscoveryCandidates(proposals).map((candidate) => ({
94279
- stableId: candidate.stableId,
94280
- type: DeviceType.Gateway,
94281
- suggestedName: candidate.suggestedName,
94282
- prefilledConfig: { ...candidate.prefilledConfig }
94283
- }));
95135
+ async getDiscoveryModes() {
95136
+ return RADEMACHER_DISCOVERY_MODES;
95137
+ }
95138
+ async classifyDiscoveryHits(input) {
95139
+ const found = await hubsFromHits(input.hits);
95140
+ const proposals = proposeRademacherHubs(found, await this.knownHubHosts());
95141
+ if (found.length > proposals.length) this.ctx.logger.info("Rademacher discovery skipped hubs already added", { meta: {
95142
+ found: found.length,
95143
+ proposed: proposals.length
95144
+ } });
95145
+ return rademacherDiscoveryCandidates(proposals);
94284
95146
  }
94285
95147
  async adoptDiscoveredDevice(_input) {
94286
95148
  throw new Error("A Rademacher hub is added from the integration form");
@@ -94297,23 +95159,6 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
94297
95159
  if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("provider-rademacher generateStableId: nativeId required");
94298
95160
  return `rademacher:${nativeId}`;
94299
95161
  }
94300
- async discoverHubs() {
94301
- let found = [];
94302
- try {
94303
- found = await browseRademacherHubs();
94304
- } catch (err) {
94305
- this.ctx.logger.warn("Rademacher hub discovery failed", { meta: { error: errMsg(err) } });
94306
- }
94307
- const proposals = proposeRademacherHubs(found, await this.knownHubHosts());
94308
- if (found.length > proposals.length) this.ctx.logger.info("Rademacher discovery skipped hubs already added", { meta: {
94309
- found: found.length,
94310
- proposed: proposals.length
94311
- } });
94312
- return {
94313
- proposals,
94314
- summary: rademacherDiscoverySummary(found.length, proposals.length)
94315
- };
94316
- }
94317
95162
  async knownHubHosts() {
94318
95163
  try {
94319
95164
  const all = await this.ctx.api.integrations.list.query();