camstack 1.2.211 → 1.2.213

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../types/dist/err-msg-DWXWc0Di.mjs
3
+ // ../types/dist/err-msg-BD5buxvT.mjs
4
4
  var DeviceType = /* @__PURE__ */ (function(DeviceType2) {
5
5
  DeviceType2["Camera"] = "camera";
6
6
  DeviceType2["Hub"] = "hub";
@@ -37,6 +37,7 @@ var DeviceType = /* @__PURE__ */ (function(DeviceType2) {
37
37
  DeviceType2["Dishwasher"] = "dishwasher";
38
38
  DeviceType2["Washer"] = "washer";
39
39
  DeviceType2["Dryer"] = "dryer";
40
+ DeviceType2["Gateway"] = "gateway";
40
41
  return DeviceType2;
41
42
  })({});
42
43
  var DeviceFeature = /* @__PURE__ */ (function(DeviceFeature2) {
@@ -154,7 +155,7 @@ var DeviceRole = /* @__PURE__ */ (function(DeviceRole2) {
154
155
  return DeviceRole2;
155
156
  })({});
156
157
 
157
- // ../types/dist/composition-kSuOifuG.mjs
158
+ // ../types/dist/composition-C7WdThmP.mjs
158
159
  import { z } from "zod";
159
160
  import { createHash } from "crypto";
160
161
  var RESTREAM_INPUT_PROBE = {
@@ -1403,7 +1404,7 @@ var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
1403
1404
  return EventCategory2;
1404
1405
  })({});
1405
1406
 
1406
- // ../types/dist/sleep-63gM_1JR.mjs
1407
+ // ../types/dist/sleep-CeyhrcOk.mjs
1407
1408
  import { z as z2 } from "zod";
1408
1409
  var AUDIO_CHUNK_FORMATS = [
1409
1410
  "f32le",
@@ -7307,6 +7308,149 @@ function metaOf(schema) {
7307
7308
  function paramMetaOf(field) {
7308
7309
  return metaOf(field) ?? metaOf(unwrapSchema(field));
7309
7310
  }
7311
+ var MACRO_LABELS = [
7312
+ {
7313
+ id: "person",
7314
+ name: "Person"
7315
+ },
7316
+ {
7317
+ id: "vehicle",
7318
+ name: "Vehicle"
7319
+ },
7320
+ {
7321
+ id: "animal",
7322
+ name: "Animal"
7323
+ },
7324
+ {
7325
+ id: "package",
7326
+ name: "Package"
7327
+ }
7328
+ ];
7329
+ var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
7330
+ var COCO_TO_MACRO = {
7331
+ mapping: {
7332
+ person: "person",
7333
+ bicycle: "vehicle",
7334
+ car: "vehicle",
7335
+ motorcycle: "vehicle",
7336
+ airplane: "vehicle",
7337
+ bus: "vehicle",
7338
+ train: "vehicle",
7339
+ truck: "vehicle",
7340
+ boat: "vehicle",
7341
+ bird: "animal",
7342
+ cat: "animal",
7343
+ dog: "animal",
7344
+ horse: "animal",
7345
+ sheep: "animal",
7346
+ cow: "animal",
7347
+ elephant: "animal",
7348
+ bear: "animal",
7349
+ zebra: "animal",
7350
+ giraffe: "animal",
7351
+ suitcase: "package",
7352
+ backpack: "package",
7353
+ handbag: "package"
7354
+ },
7355
+ preserveOriginal: false
7356
+ };
7357
+ var SubClassLabelSchema = z4.object({
7358
+ kind: z4.literal("sub-class"),
7359
+ className: z4.string().min(1)
7360
+ });
7361
+ var IdentityLabelSchema = z4.object({
7362
+ kind: z4.literal("identity"),
7363
+ /** The `face-gallery` identity id (stable across a rename). */
7364
+ identityId: z4.string().min(1),
7365
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
7366
+ name: z4.string().min(1)
7367
+ });
7368
+ var PlateLabelSchema = z4.object({
7369
+ kind: z4.literal("plate"),
7370
+ text: z4.string().min(1)
7371
+ });
7372
+ function macroOf$1(className) {
7373
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
7374
+ }
7375
+ function subClassBelongs(macro2, label) {
7376
+ return label === void 0 || macroOf$1(label.className) === macro2;
7377
+ }
7378
+ var subClassOf = (macro2) => SubClassLabelSchema.refine((l) => subClassBelongs(macro2, l), { message: `that class is not a kind of ${macro2} in the detection taxonomy` });
7379
+ var DetectionSubjectSchema = z4.discriminatedUnion("macro", [
7380
+ z4.object({
7381
+ macro: z4.literal("animal"),
7382
+ label: subClassOf("animal").optional()
7383
+ }),
7384
+ z4.object({
7385
+ macro: z4.literal("person"),
7386
+ label: IdentityLabelSchema.optional()
7387
+ }),
7388
+ z4.object({
7389
+ macro: z4.literal("vehicle"),
7390
+ label: z4.discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
7391
+ }),
7392
+ z4.object({
7393
+ macro: z4.literal("package"),
7394
+ label: subClassOf("package").optional()
7395
+ }),
7396
+ z4.object({
7397
+ macro: z4.literal("other"),
7398
+ className: z4.string().min(1)
7399
+ })
7400
+ ]);
7401
+ 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 \u2014 only a class can narrow it" });
7402
+ function subjectFromClassName(className) {
7403
+ const name = className.trim().toLowerCase();
7404
+ const macro2 = COCO_TO_MACRO.mapping[name];
7405
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
7406
+ if (macro2 === void 0) return {
7407
+ macro: "other",
7408
+ className
7409
+ };
7410
+ const label = {
7411
+ kind: "sub-class",
7412
+ className: name
7413
+ };
7414
+ switch (macro2) {
7415
+ case "animal":
7416
+ return {
7417
+ macro: "animal",
7418
+ label
7419
+ };
7420
+ case "vehicle":
7421
+ return {
7422
+ macro: "vehicle",
7423
+ label
7424
+ };
7425
+ case "package":
7426
+ return {
7427
+ macro: "package",
7428
+ label
7429
+ };
7430
+ default:
7431
+ return {
7432
+ macro: "other",
7433
+ className
7434
+ };
7435
+ }
7436
+ }
7437
+ function macroOnly(macro2) {
7438
+ switch (macro2) {
7439
+ case "animal":
7440
+ return { macro: "animal" };
7441
+ case "person":
7442
+ return { macro: "person" };
7443
+ case "vehicle":
7444
+ return { macro: "vehicle" };
7445
+ case "package":
7446
+ return { macro: "package" };
7447
+ default:
7448
+ return {
7449
+ macro: "other",
7450
+ className: macro2
7451
+ };
7452
+ }
7453
+ }
7310
7454
  var AutomationConditionOperatorSchema = z4.enum([
7311
7455
  "eq",
7312
7456
  "ne",
@@ -7317,12 +7461,69 @@ var AutomationConditionOperatorSchema = z4.enum([
7317
7461
  "contains",
7318
7462
  "in"
7319
7463
  ]);
7320
- var AutomationConditionLeafSchema = z4.object({
7321
- kind: z4.literal("condition"),
7464
+ var DeviceBecomesLeafSchema = z4.object({
7465
+ kind: z4.literal("device-becomes"),
7466
+ deviceId: z4.number().int().nonnegative(),
7467
+ cap: z4.string().min(1),
7468
+ fieldPath: z4.string().min(1),
7469
+ /** Fire when the field takes this value. Omit to fire on any change. */
7470
+ becomes: z4.union([
7471
+ z4.string(),
7472
+ z4.number(),
7473
+ z4.boolean()
7474
+ ]).optional(),
7475
+ /** Only on a CHANGE of value, not on every re-report. */
7476
+ edge: z4.boolean().optional(),
7477
+ /** The new value must hold this long before it counts. */
7478
+ forMs: z4.number().int().min(0).max(864e5).optional(),
7479
+ /** Collapse a burst into one occurrence. */
7480
+ debounceMs: z4.number().int().min(0).max(6e5).optional()
7481
+ });
7482
+ var DeviceEventLeafSchema = z4.object({
7483
+ kind: z4.literal("device-event"),
7484
+ /** An `EventCategory` value. */
7485
+ category: z4.string().min(1),
7486
+ deviceId: z4.number().int().nonnegative().optional()
7487
+ });
7488
+ var ScheduleLeafSchema = z4.object({
7489
+ kind: z4.literal("schedule"),
7490
+ cron: z4.string().min(1).max(120)
7491
+ });
7492
+ var ManualLeafSchema = z4.object({ kind: z4.literal("manual") });
7493
+ var IntervalLeafSchema = z4.object({
7494
+ kind: z4.literal("interval"),
7495
+ minutes: z4.number().int().min(1).max(1440)
7496
+ });
7497
+ var DetectionLeafSchema = z4.object({
7498
+ kind: z4.literal("detection"),
7499
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
7500
+ subjects: z4.array(DetectionSubjectSchema).min(1),
7501
+ deviceIds: z4.array(z4.number().int().nonnegative()).min(1),
7502
+ /** The track must have visited at least one of these zones. */
7503
+ zones: z4.array(z4.string().min(1)).min(1).optional(),
7504
+ /** Best-so-far confidence the track must reach. */
7505
+ minScore: z4.number().min(0).max(1).optional()
7506
+ });
7507
+ var StationaryLeafSchema = z4.object({
7508
+ kind: z4.literal("stationary"),
7509
+ deviceIds: z4.array(z4.number().int().nonnegative()).min(1),
7510
+ subjects: z4.array(StationarySubjectSchema).min(1).optional(),
7511
+ zones: z4.array(z4.string().min(1)).min(1).optional(),
7512
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
7513
+ phase: z4.enum(["appeared", "departed"])
7514
+ });
7515
+ var DeviceIsLeafSchema = z4.object({
7516
+ kind: z4.literal("device-is"),
7322
7517
  deviceId: z4.number().int().nonnegative(),
7323
7518
  cap: z4.string().min(1),
7324
7519
  fieldPath: z4.string().min(1),
7325
7520
  operator: AutomationConditionOperatorSchema,
7521
+ /**
7522
+ * The value. A number on a quantity field is stored in the field's base unit
7523
+ * (D944); a quantity value is the WRITE form, which the save boundary
7524
+ * replaces with the converted number and `unit`. A leaf that still carries
7525
+ * one is never evaluated (it fails).
7526
+ */
7326
7527
  value: z4.union([
7327
7528
  z4.string(),
7328
7529
  z4.number(),
@@ -7330,17 +7531,16 @@ var AutomationConditionLeafSchema = z4.object({
7330
7531
  z4.array(z4.union([z4.string(), z4.number()])),
7331
7532
  QuantityValueSchema
7332
7533
  ]),
7534
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
7333
7535
  unit: z4.string().min(1).optional()
7334
7536
  });
7335
- var AutomationConditionStationaryPresentSchema = z4.object({
7537
+ var StationaryPresentLeafSchema = z4.object({
7336
7538
  kind: z4.literal("stationary-present"),
7337
7539
  deviceId: z4.number().int().nonnegative(),
7338
- /** Class names; a macro class matches every raw class that maps to it. */
7339
- classes: z4.array(z4.string().min(1)).min(1).optional(),
7340
- /** The parked object must be in at least one of these zones. */
7540
+ subjects: z4.array(StationarySubjectSchema).min(1).optional(),
7341
7541
  zones: z4.array(z4.string().min(1)).min(1).optional()
7342
7542
  });
7343
- var AutomationConditionExpressionSchema = z4.object({
7543
+ var ExpressionLeafSchema = z4.object({
7344
7544
  kind: z4.literal("expression"),
7345
7545
  expr: z4.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
7346
7546
  bindings: z4.record(z4.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -7352,45 +7552,150 @@ var AutomationConditionExpressionSchema = z4.object({
7352
7552
  path: ["expr"]
7353
7553
  });
7354
7554
  });
7355
- var AutomationConditionSchema = z4.lazy(() => z4.discriminatedUnion("kind", [
7555
+ var AutomationWhenSchema = z4.lazy(() => z4.discriminatedUnion("kind", [
7356
7556
  z4.object({
7357
7557
  kind: z4.literal("all"),
7358
- children: z4.array(AutomationConditionSchema)
7558
+ children: z4.array(AutomationWhenSchema)
7359
7559
  }),
7360
7560
  z4.object({
7361
7561
  kind: z4.literal("any"),
7362
- children: z4.array(AutomationConditionSchema)
7562
+ children: z4.array(AutomationWhenSchema)
7363
7563
  }),
7364
7564
  z4.object({
7365
7565
  kind: z4.literal("not"),
7366
- child: AutomationConditionSchema
7566
+ child: AutomationWhenSchema
7367
7567
  }),
7368
- AutomationConditionLeafSchema,
7369
- AutomationConditionExpressionSchema,
7370
- AutomationConditionStationaryPresentSchema
7568
+ DeviceBecomesLeafSchema,
7569
+ DeviceEventLeafSchema,
7570
+ ScheduleLeafSchema,
7571
+ ManualLeafSchema,
7572
+ IntervalLeafSchema,
7573
+ DetectionLeafSchema,
7574
+ StationaryLeafSchema,
7575
+ DeviceIsLeafSchema,
7576
+ StationaryPresentLeafSchema,
7577
+ ExpressionLeafSchema
7371
7578
  ]));
7372
- var AutomationTriggerSchema = z4.discriminatedUnion("kind", [
7579
+ var EVENT_KINDS = /* @__PURE__ */ new Set([
7580
+ "device-becomes",
7581
+ "device-event",
7582
+ "schedule",
7583
+ "manual",
7584
+ "interval",
7585
+ "detection",
7586
+ "stationary"
7587
+ ]);
7588
+ function isEventLeaf(node) {
7589
+ return EVENT_KINDS.has(node.kind);
7590
+ }
7591
+ function isLeaf(node) {
7592
+ return node.kind !== "all" && node.kind !== "any" && node.kind !== "not";
7593
+ }
7594
+ function whenDepth(node) {
7595
+ if (isLeaf(node)) return 1;
7596
+ if (node.kind === "not") return 1 + whenDepth(node.child);
7597
+ let deepest = 0;
7598
+ for (const child of node.children) deepest = Math.max(deepest, whenDepth(child));
7599
+ return 1 + deepest;
7600
+ }
7601
+ function countWhenLeaves(node) {
7602
+ if (isLeaf(node)) return 1;
7603
+ if (node.kind === "not") return countWhenLeaves(node.child);
7604
+ let total = 0;
7605
+ for (const child of node.children) total += countWhenLeaves(child);
7606
+ return total;
7607
+ }
7608
+ function forEachLeaf(node, visit, path2 = "when") {
7609
+ if (isLeaf(node)) {
7610
+ visit(node, path2);
7611
+ return;
7612
+ }
7613
+ if (node.kind === "not") {
7614
+ forEachLeaf(node.child, visit, `${path2}.0`);
7615
+ return;
7616
+ }
7617
+ node.children.forEach((child, i) => {
7618
+ forEachLeaf(child, visit, `${path2}.${i}`);
7619
+ });
7620
+ }
7621
+ function hasEventLeaves(node) {
7622
+ let found = false;
7623
+ forEachLeaf(node, (leaf) => {
7624
+ if (isEventLeaf(leaf)) found = true;
7625
+ });
7626
+ return found;
7627
+ }
7628
+ function hasEmptyContainer(node) {
7629
+ if (isLeaf(node)) return false;
7630
+ if (node.kind === "not") return hasEmptyContainer(node.child);
7631
+ if (node.children.length === 0) return true;
7632
+ return node.children.some(hasEmptyContainer);
7633
+ }
7634
+ var LEAF_WORDS = {
7635
+ "device-becomes": "a device value that becomes",
7636
+ "device-event": "a device event",
7637
+ schedule: "a schedule",
7638
+ manual: "a manual run",
7639
+ interval: "an interval",
7640
+ detection: "a detection",
7641
+ stationary: "a parked object appearing or leaving"
7642
+ };
7643
+ function say(kind) {
7644
+ return LEAF_WORDS[kind] ?? kind;
7645
+ }
7646
+ function eventNeed(node, path2, negated) {
7647
+ if (isLeaf(node)) {
7648
+ if (!isEventLeaf(node)) return { paths: [] };
7649
+ if (negated) return { error: `"not" is applied to ${say(node.kind)} (${path2}) \u2014 an event is an instant, so "it did not happen" is true at every other moment and can never start a run. Negate a state ("is") instead, or remove the "not"` };
7650
+ return { paths: [{
7651
+ path: path2,
7652
+ kind: node.kind
7653
+ }] };
7654
+ }
7655
+ if (node.kind === "not") return eventNeed(node.child, `${path2}.0`, !negated);
7656
+ const conjunctive = node.kind === "all" !== negated;
7657
+ const parts = [];
7658
+ for (let i = 0; i < node.children.length; i += 1) {
7659
+ const child = node.children[i];
7660
+ if (child === void 0) continue;
7661
+ const need = eventNeed(child, `${path2}.${i}`, negated);
7662
+ if ("error" in need) return need;
7663
+ parts.push(need);
7664
+ }
7665
+ if (conjunctive) return { paths: parts.flatMap((p) => p.paths) };
7666
+ let best;
7667
+ for (const part of parts) if (best === void 0 || part.paths.length < best.paths.length) best = part;
7668
+ return best ?? { paths: [] };
7669
+ }
7670
+ function validateWhenTree(when) {
7671
+ if (hasEmptyContainer(when)) return "a group is empty \u2014 group existing rows instead of leaving a container to fill";
7672
+ const depth = whenDepth(when);
7673
+ if (depth > 4) return `the rules nest ${depth} levels deep \u2014 the limit is 4. "not (any of \u2026)" is the same as "all of (not \u2026)", which is one level shallower`;
7674
+ const leaves = countWhenLeaves(when);
7675
+ if (leaves > 32) return `the rules use ${leaves} rows \u2014 the limit is 32`;
7676
+ const need = eventNeed(when, "when", false);
7677
+ if ("error" in need) return need.error;
7678
+ const [first, second] = need.paths;
7679
+ if (first !== void 0 && second !== void 0) return `${say(first.kind)} (${first.path}) and ${say(second.kind)} (${second.path}) would both have to happen at the same instant, and an event happens at one moment \u2014 this can never run. Use "any of" to start on either, or turn one of them into a state ("is") that must already hold`;
7680
+ return null;
7681
+ }
7682
+ var LegacyTriggerSchema = z4.discriminatedUnion("kind", [
7373
7683
  z4.object({
7374
7684
  kind: z4.literal("device-state"),
7375
7685
  deviceId: z4.number().int().nonnegative(),
7376
7686
  cap: z4.string().min(1),
7377
7687
  fieldPath: z4.string().min(1),
7378
- /** Fire when the field takes this value. Omit to fire on any change. */
7379
7688
  becomes: z4.union([
7380
7689
  z4.string(),
7381
7690
  z4.number(),
7382
7691
  z4.boolean()
7383
7692
  ]).optional(),
7384
- /** Only on a CHANGE of value, not on every re-report. */
7385
7693
  edge: z4.boolean().optional(),
7386
- /** The condition must hold this long before the run starts. */
7387
7694
  forMs: z4.number().int().min(0).max(864e5).optional(),
7388
- /** Collapse a burst into one run. */
7389
7695
  debounceMs: z4.number().int().min(0).max(6e5).optional()
7390
7696
  }),
7391
7697
  z4.object({
7392
7698
  kind: z4.literal("device-event"),
7393
- /** An `EventCategory` value. */
7394
7699
  category: z4.string().min(1),
7395
7700
  deviceId: z4.number().int().nonnegative().optional()
7396
7701
  }),
@@ -7401,25 +7706,16 @@ var AutomationTriggerSchema = z4.discriminatedUnion("kind", [
7401
7706
  z4.object({ kind: z4.literal("manual") }),
7402
7707
  z4.object({
7403
7708
  kind: z4.literal("detection"),
7404
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
7405
- * matches every raw class that maps to it; any other name matches
7406
- * exactly. */
7407
7709
  classes: z4.array(z4.string().min(1)).min(1),
7408
7710
  deviceIds: z4.array(z4.number().int().nonnegative()).min(1),
7409
- /** The track must have visited at least one of these zones. */
7410
7711
  zones: z4.array(z4.string().min(1)).min(1).optional(),
7411
- /** Best-so-far confidence the track must reach. */
7412
7712
  minScore: z4.number().min(0).max(1).optional()
7413
7713
  }),
7414
7714
  z4.object({
7415
7715
  kind: z4.literal("stationary"),
7416
7716
  deviceIds: z4.array(z4.number().int().nonnegative()).min(1),
7417
- /** Class names; a macro class matches like the detection trigger. */
7418
7717
  classes: z4.array(z4.string().min(1)).min(1).optional(),
7419
- /** The parked object must be in at least one of these zones. */
7420
7718
  zones: z4.array(z4.string().min(1)).min(1).optional(),
7421
- /** `appeared`: it became parked. `departed`: the registry retired it.
7422
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
7423
7719
  phase: z4.enum(["appeared", "departed"])
7424
7720
  }),
7425
7721
  z4.object({
@@ -7427,6 +7723,123 @@ var AutomationTriggerSchema = z4.discriminatedUnion("kind", [
7427
7723
  minutes: z4.number().int().min(1).max(1440)
7428
7724
  })
7429
7725
  ]);
7726
+ var LegacyConditionLeafSchema = z4.object({
7727
+ kind: z4.literal("condition"),
7728
+ deviceId: z4.number().int().nonnegative(),
7729
+ cap: z4.string().min(1),
7730
+ fieldPath: z4.string().min(1),
7731
+ operator: AutomationConditionOperatorSchema,
7732
+ value: z4.union([
7733
+ z4.string(),
7734
+ z4.number(),
7735
+ z4.boolean(),
7736
+ z4.array(z4.union([z4.string(), z4.number()])),
7737
+ QuantityValueSchema
7738
+ ]),
7739
+ unit: z4.string().min(1).optional()
7740
+ });
7741
+ var LegacyStationaryPresentSchema = z4.object({
7742
+ kind: z4.literal("stationary-present"),
7743
+ deviceId: z4.number().int().nonnegative(),
7744
+ classes: z4.array(z4.string().min(1)).min(1).optional(),
7745
+ zones: z4.array(z4.string().min(1)).min(1).optional()
7746
+ });
7747
+ var LegacyExpressionSchema = z4.object({
7748
+ kind: z4.literal("expression"),
7749
+ expr: z4.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
7750
+ bindings: z4.record(z4.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
7751
+ }).superRefine((src, ctx) => {
7752
+ const err = validateExpressionSource(src);
7753
+ if (err !== null) ctx.addIssue({
7754
+ code: "custom",
7755
+ message: err,
7756
+ path: ["expr"]
7757
+ });
7758
+ });
7759
+ var LegacyConditionSchema = z4.lazy(() => z4.discriminatedUnion("kind", [
7760
+ z4.object({
7761
+ kind: z4.literal("all"),
7762
+ children: z4.array(LegacyConditionSchema)
7763
+ }),
7764
+ z4.object({
7765
+ kind: z4.literal("any"),
7766
+ children: z4.array(LegacyConditionSchema)
7767
+ }),
7768
+ z4.object({
7769
+ kind: z4.literal("not"),
7770
+ child: LegacyConditionSchema
7771
+ }),
7772
+ LegacyConditionLeafSchema,
7773
+ LegacyExpressionSchema,
7774
+ LegacyStationaryPresentSchema
7775
+ ]));
7776
+ function convertTrigger(trigger) {
7777
+ switch (trigger.kind) {
7778
+ case "device-state":
7779
+ return {
7780
+ ...trigger,
7781
+ kind: "device-becomes"
7782
+ };
7783
+ case "detection": {
7784
+ const { classes, ...rest } = trigger;
7785
+ return {
7786
+ ...rest,
7787
+ subjects: classes.map(subjectFromClassName)
7788
+ };
7789
+ }
7790
+ case "stationary": {
7791
+ const { classes, ...rest } = trigger;
7792
+ return {
7793
+ ...rest,
7794
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
7795
+ };
7796
+ }
7797
+ default:
7798
+ return trigger;
7799
+ }
7800
+ }
7801
+ function convertLegacyCondition(condition) {
7802
+ switch (condition.kind) {
7803
+ case "condition":
7804
+ return {
7805
+ ...condition,
7806
+ kind: "device-is"
7807
+ };
7808
+ case "stationary-present": {
7809
+ const { classes, ...rest } = condition;
7810
+ return {
7811
+ ...rest,
7812
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
7813
+ };
7814
+ }
7815
+ case "expression":
7816
+ return condition;
7817
+ case "not":
7818
+ return {
7819
+ kind: "not",
7820
+ child: convertLegacyCondition(condition.child)
7821
+ };
7822
+ case "all":
7823
+ case "any":
7824
+ return {
7825
+ kind: condition.kind,
7826
+ children: condition.children.map(convertLegacyCondition)
7827
+ };
7828
+ }
7829
+ }
7830
+ function convertLegacyWhen(half) {
7831
+ const leaves = half.triggers.map(convertTrigger);
7832
+ const first = leaves[0];
7833
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
7834
+ kind: "any",
7835
+ children: leaves
7836
+ };
7837
+ if (half.conditions === void 0) return triggers;
7838
+ return {
7839
+ kind: "all",
7840
+ children: [triggers, convertLegacyCondition(half.conditions)]
7841
+ };
7842
+ }
7430
7843
  var AutomationWaitStepSchema = z4.object({
7431
7844
  kind: z4.literal("wait"),
7432
7845
  seconds: z4.number().min(0).max(300)
@@ -7494,54 +7907,34 @@ var AutomationRunModeSchema = z4.discriminatedUnion("mode", [
7494
7907
  minutes: z4.number().int().min(1).max(1440)
7495
7908
  })
7496
7909
  ]);
7497
- var AutomationRecipeSchema = z4.object({
7498
- triggers: z4.array(AutomationTriggerSchema),
7499
- conditions: AutomationConditionSchema.optional(),
7910
+ var CurrentAutomationRecipeSchema = z4.object({
7911
+ when: AutomationWhenSchema,
7500
7912
  actions: z4.array(AutomationActionSchema),
7501
7913
  runMode: AutomationRunModeSchema.optional(),
7502
7914
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
7503
7915
  keepTraces: z4.number().int().min(1).max(100).optional()
7504
7916
  });
7505
- function isLeaf(cond) {
7506
- return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
7507
- }
7508
- function conditionDepth(cond) {
7509
- if (isLeaf(cond)) return 1;
7510
- if (cond.kind === "not") return 1 + conditionDepth(cond.child);
7511
- const children = cond.children;
7512
- if (children.length === 0) return 1;
7513
- let deepest = 0;
7514
- for (const child of children) {
7515
- const d = conditionDepth(child);
7516
- if (d > deepest) deepest = d;
7517
- }
7518
- return 1 + deepest;
7519
- }
7520
- function countConditionLeaves(cond) {
7521
- if (isLeaf(cond)) return 1;
7522
- if (cond.kind === "not") return countConditionLeaves(cond.child);
7523
- let total = 0;
7524
- for (const child of cond.children) total += countConditionLeaves(child);
7525
- return total;
7526
- }
7527
- function findEmptyContainer(cond) {
7528
- if (isLeaf(cond)) return false;
7529
- if (cond.kind === "not") return findEmptyContainer(cond.child);
7530
- if (cond.children.length === 0) return true;
7531
- return cond.children.some(findEmptyContainer);
7917
+ var LegacyAutomationRecipeSchema = z4.object({
7918
+ triggers: z4.array(LegacyTriggerSchema),
7919
+ conditions: LegacyConditionSchema.optional(),
7920
+ actions: z4.array(AutomationActionSchema),
7921
+ runMode: AutomationRunModeSchema.optional(),
7922
+ keepTraces: z4.number().int().min(1).max(100).optional()
7923
+ });
7924
+ function convertLegacyRecipe(legacy) {
7925
+ const { triggers, conditions, ...rest } = legacy;
7926
+ return {
7927
+ when: convertLegacyWhen({
7928
+ triggers,
7929
+ conditions
7930
+ }),
7931
+ ...rest
7932
+ };
7532
7933
  }
7934
+ var AutomationRecipeSchema = z4.union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
7533
7935
  function validateRecipeBounds(recipe) {
7534
- if (recipe.triggers.length === 0) return "an automation needs at least one trigger \u2014 without one nothing can start a run";
7535
7936
  if (recipe.actions.length === 0) return "an automation needs at least one action \u2014 a rule that does nothing is not a rule";
7536
- const cond = recipe.conditions;
7537
- if (cond !== void 0) {
7538
- if (findEmptyContainer(cond)) return "a condition group is empty \u2014 group existing rows instead of leaving a container to fill";
7539
- const depth = conditionDepth(cond);
7540
- if (depth > 3) return `conditions nest ${depth} levels deep \u2014 the limit is 3. "not (any of \u2026)" is the same as "all of (not \u2026)", which is one level shallower`;
7541
- const leaves = countConditionLeaves(cond);
7542
- if (leaves > 32) return `conditions use ${leaves} leaves \u2014 the limit is 32`;
7543
- }
7544
- return null;
7937
+ return validateWhenTree(recipe.when);
7545
7938
  }
7546
7939
  var RECONCILE_MS = 15e3;
7547
7940
  function stepMetaOf(action) {
@@ -7564,10 +7957,11 @@ function stepMetaOf(action) {
7564
7957
  function j(value) {
7565
7958
  return JSON.stringify(value);
7566
7959
  }
7567
- function emitTrigger(trigger, index) {
7960
+ function emitLeaf(trigger, path2) {
7961
+ const at = j(path2);
7568
7962
  switch (trigger.kind) {
7569
7963
  case "schedule":
7570
- return ` // trigger ${index}: schedule \u2014 armed to the next minute boundary and
7964
+ return ` // event leaf ${path2}: schedule \u2014 armed to the next minute boundary and
7571
7965
  // RE-ARMED from the wall clock every tick, so it cannot drift the way a
7572
7966
  // setInterval(24h) does. No cron library: a block cannot resolve one.
7573
7967
  {
@@ -7575,7 +7969,7 @@ function emitTrigger(trigger, index) {
7575
7969
  const arm = (): void => {
7576
7970
  timer = setTimeout(() => {
7577
7971
  if (cronMatches(${j(trigger.cron)}, new Date())) {
7578
- void run({ kind: 'schedule', at: Date.now() })
7972
+ void run({ kind: 'schedule', leaf: ${at}, at: Date.now() })
7579
7973
  }
7580
7974
  arm()
7581
7975
  }, nextMinuteBoundary())
@@ -7584,13 +7978,14 @@ function emitTrigger(trigger, index) {
7584
7978
  disposers.push(() => { if (timer !== undefined) clearTimeout(timer) })
7585
7979
  }`;
7586
7980
  case "device-event":
7587
- return ` // trigger ${index}: device event
7981
+ return ` // event leaf ${path2}: device event
7588
7982
  disposers.push(
7589
7983
  ctx.eventBus.subscribe(
7590
7984
  { category: ${j(trigger.category)}${trigger.deviceId === void 0 ? "" : `, deviceId: ${j(trigger.deviceId)}`} },
7591
7985
  (event) => {
7592
7986
  void run({
7593
7987
  kind: 'device-event',
7988
+ leaf: ${at},
7594
7989
  at: Date.now(),
7595
7990
  deviceId: event.deviceId,
7596
7991
  category: ${j(trigger.category)},
@@ -7598,9 +7993,9 @@ function emitTrigger(trigger, index) {
7598
7993
  },
7599
7994
  ),
7600
7995
  )`;
7601
- case "device-state": {
7996
+ case "device-becomes": {
7602
7997
  const becomes = trigger.becomes === void 0 ? "undefined" : j(trigger.becomes);
7603
- return ` // trigger ${index}: device state \u2014 the mirror is the authority, the
7998
+ return ` // event leaf ${path2}: device state \u2014 the mirror is the authority, the
7604
7999
  // event only WAKES the evaluation, so a dropped event delays and never loses.
7605
8000
  {
7606
8001
  let previous: unknown = undefined
@@ -7621,7 +8016,8 @@ function emitTrigger(trigger, index) {
7621
8016
  }
7622
8017
  heldSince = undefined
7623
8018
  void run({
7624
- kind: 'device-state',
8019
+ kind: 'device-becomes',
8020
+ leaf: ${at},
7625
8021
  at: Date.now(),
7626
8022
  deviceId: ${j(trigger.deviceId)},
7627
8023
  cap: ${j(trigger.cap)},
@@ -7641,17 +8037,17 @@ function emitTrigger(trigger, index) {
7641
8037
  }`;
7642
8038
  }
7643
8039
  case "manual":
7644
- return ` // trigger ${index}: manual \u2014 fired by automation-control.trigger
8040
+ return ` // event leaf ${path2}: manual \u2014 fired by automation-control.trigger
7645
8041
  // ("Run actions"), which runs the THEN half with an EMPTY vars bag. A manual
7646
8042
  // run bypasses only the cooldown wait, never the single/queued limits.`;
7647
8043
  case "interval":
7648
- return ` // trigger ${index}: interval \u2014 every ${trigger.minutes} min, first run jittered so
8044
+ return ` // event leaf ${path2}: interval \u2014 every ${trigger.minutes} min, first run jittered so
7649
8045
  // blocks restarted together do not all fire on the same second.
7650
8046
  {
7651
8047
  let interval: ReturnType<typeof setInterval> | undefined
7652
8048
  const first = setTimeout(() => {
7653
- void run({ kind: 'interval', at: Date.now() })
7654
- interval = setInterval(() => { void run({ kind: 'interval', at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
8049
+ void run({ kind: 'interval', leaf: ${at}, at: Date.now() })
8050
+ interval = setInterval(() => { void run({ kind: 'interval', leaf: ${at}, at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
7655
8051
  }, Math.floor(Math.random() * Math.min(${j(trigger.minutes * 6e4)}, 30_000)) + 1)
7656
8052
  disposers.push(() => {
7657
8053
  clearTimeout(first)
@@ -7659,7 +8055,7 @@ function emitTrigger(trigger, index) {
7659
8055
  })
7660
8056
  }`;
7661
8057
  case "detection":
7662
- return ` // trigger ${index}: detection \u2014 ONE fire per (device, track); a rejected event is
8058
+ return ` // event leaf ${path2}: detection \u2014 ONE fire per (device, track); a rejected event is
7663
8059
  // traced with its reason and never fires.
7664
8060
  {
7665
8061
  const filter = new DetectionTriggerFilter(${j(trigger)})
@@ -7676,7 +8072,7 @@ function emitTrigger(trigger, index) {
7676
8072
  })
7677
8073
  }
7678
8074
  void run(
7679
- { kind: 'detection', at: Date.now(), ...decision.vars },
8075
+ { kind: 'detection', leaf: ${at}, at: Date.now(), ...decision.vars },
7680
8076
  decision.reason,
7681
8077
  decision.kind === 'fire',
7682
8078
  )
@@ -7685,9 +8081,9 @@ function emitTrigger(trigger, index) {
7685
8081
  )
7686
8082
  }`;
7687
8083
  case "stationary":
7688
- return ` // trigger ${index}: stationary \u2014 a parked object appeared / departed. Stateless:
8084
+ return ` // event leaf ${path2}: stationary \u2014 a parked object appeared / departed. Stateless:
7689
8085
  // the registry emits one event per transition. A dropped event only misses a fire;
7690
- // the stationary-present CONDITION reads the registry, not this event.
8086
+ // the stationary-present STATE leaf reads the registry, not this event.
7691
8087
  {
7692
8088
  const trigger = ${j(trigger)} as const
7693
8089
  disposers.push(
@@ -7703,7 +8099,7 @@ function emitTrigger(trigger, index) {
7703
8099
  })
7704
8100
  }
7705
8101
  void run(
7706
- { kind: 'stationary', at: Date.now(), ...decision.vars },
8102
+ { kind: 'stationary', leaf: ${at}, at: Date.now(), ...decision.vars },
7707
8103
  decision.reason,
7708
8104
  decision.kind === 'fire',
7709
8105
  )
@@ -7726,11 +8122,11 @@ ${body}
7726
8122
  case "cap":
7727
8123
  if (Array.isArray(action.deviceId)) return step(` for (const deviceId of ${j(action.deviceId)}) {
7728
8124
  scope.setDeviceId(deviceId)
7729
- await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
8125
+ await actuate(scope, deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
7730
8126
  }`);
7731
8127
  return step(` const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
7732
8128
  scope.setDeviceId(deviceId)
7733
- await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
8129
+ await actuate(scope, deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
7734
8130
  case "action":
7735
8131
  return step(` // an Action run, awaited: the step takes THAT run's outcome
7736
8132
  const result = await ctx.api.actions.run.mutate({
@@ -7755,6 +8151,13 @@ function generateAutomationBlock(input) {
7755
8151
  const { name, recipe } = input;
7756
8152
  const runMode = recipe.runMode ?? { mode: "single" };
7757
8153
  const stepMeta = recipe.actions.map(stepMetaOf);
8154
+ const eventLeaves = [];
8155
+ forEachLeaf(recipe.when, (leaf, path2) => {
8156
+ if (isEventLeaf(leaf)) eventLeaves.push({
8157
+ leaf,
8158
+ path: path2
8159
+ });
8160
+ });
7758
8161
  return `import type {
7759
8162
  AddonContext,
7760
8163
  AutomationConditionTraceNode,
@@ -7762,6 +8165,7 @@ function generateAutomationBlock(input) {
7762
8165
  AutomationRunVerdict,
7763
8166
  AutomationStepTrace,
7764
8167
  RunnableStep,
8168
+ StepScope,
7765
8169
  } from '@camstack/types'
7766
8170
  import {
7767
8171
  ConditionMirror,
@@ -7769,9 +8173,9 @@ import {
7769
8173
  EventCategory,
7770
8174
  RunGate,
7771
8175
  buildTriggerTrace,
7772
- collectMirrorSources,
8176
+ collectWhenMirrorSources,
7773
8177
  coreBlockIdFromAddonId,
7774
- evaluateConditionTree,
8178
+ evaluateWhenTree,
7775
8179
  evaluateStationaryTrigger,
7776
8180
  isSkippedTrace,
7777
8181
  mergeSkipTraces,
@@ -7799,7 +8203,11 @@ const RUN_MODE = ${j(runMode)} as const
7799
8203
  /** Run traces kept (a ring, held in the hub's runs ledger). */
7800
8204
  const KEEP_TRACES = ${recipe.keepTraces ?? 20}
7801
8205
 
7802
- const CONDITIONS = ${recipe.conditions === void 0 ? "undefined" : "AUTOMATION_RECIPE.conditions"}
8206
+ /** The ONE tree: event leaves (instants) and state leaves (durations). */
8207
+ const WHEN = AUTOMATION_RECIPE.when
8208
+
8209
+ /** No event leaf: the automation fires when the tree goes false \u2192 true, never because it was already true. */
8210
+ const STATE_ONLY = ${!hasEventLeaves(recipe.when)}
7803
8211
 
7804
8212
  /** Skips that repeat are folded into one trace; it goes to the hub after this. */
7805
8213
  const SKIP_FLUSH_MS = 5_000
@@ -7858,7 +8266,8 @@ function matchField(field: string, value: number): boolean {
7858
8266
  export default function block(ctx: AddonContext) {
7859
8267
  const disposers: Array<() => void> = []
7860
8268
  const gate = new RunGate(RUN_MODE)
7861
- const mirror = new ConditionMirror(collectMirrorSources(CONDITIONS))
8269
+ const MIRROR_SOURCES = collectWhenMirrorSources(WHEN)
8270
+ const mirror = new ConditionMirror(MIRROR_SOURCES)
7862
8271
  const BLOCK_ID = coreBlockIdFromAddonId(ctx.id)
7863
8272
  /** The cooldown stamp: memory is the authority at fire time (D49); the hub ledger is read once at start. */
7864
8273
  let lastRunStartedAt: number | null = null
@@ -8024,6 +8433,7 @@ export default function block(ctx: AddonContext) {
8024
8433
  }
8025
8434
 
8026
8435
  async function actuate(
8436
+ scope: StepScope,
8027
8437
  deviceId: number,
8028
8438
  cap: string,
8029
8439
  method: string,
@@ -8033,6 +8443,7 @@ export default function block(ctx: AddonContext) {
8033
8443
  const substituted = substitute(args, vars)
8034
8444
  const resolved: Record<string, unknown> =
8035
8445
  substituted !== null && typeof substituted === 'object' ? { ...substituted } : {}
8446
+ scope.setArgs(resolved)
8036
8447
  // Forked-addon ctx.api namespaces are tRPC Proxies over a noop function
8037
8448
  // (typeof === 'function'), not plain objects \u2014 reject only null/undefined.
8038
8449
  const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
@@ -8121,10 +8532,13 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8121
8532
  let nodes: AutomationConditionTraceNode[] = []
8122
8533
  let steps: AutomationStepTrace[] = []
8123
8534
  try {
8124
- const evaluation = evaluateConditionTree(CONDITIONS, mirror, Date.now())
8535
+ const firedLeaf = trigger['leaf']
8536
+ const evaluation = evaluateWhenTree(WHEN, mirror, Date.now(), {
8537
+ ...(typeof firedLeaf === 'string' ? { firedPath: firedLeaf } : {}),
8538
+ })
8125
8539
  nodes = [...evaluation.nodes]
8126
8540
  for (const node of nodes) {
8127
- if (node.kind === 'condition' && node.observed === null && node.deviceId !== undefined) {
8541
+ if (node.kind === 'device-is' && node.observed === null && node.deviceId !== undefined) {
8128
8542
  ctx.logger.warn('condition leaf value unknown \u2014 treated as false', {
8129
8543
  tags: { deviceId: node.deviceId },
8130
8544
  path: node.path,
@@ -8135,7 +8549,7 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8135
8549
  }
8136
8550
  if (!evaluation.pass) {
8137
8551
  outcome = 'skipped-conditions'
8138
- ctx.logger.debug('automation did not run \u2014 conditions were false', { trigger, ...deviceTags(trigger) })
8552
+ ctx.logger.debug('automation did not run \u2014 the rule tree was false at that instant', { trigger, ...deviceTags(trigger) })
8139
8553
  return
8140
8554
  }
8141
8555
  const letThrough = stampUnknown && RUN_MODE.mode === 'cooldown'
@@ -8234,9 +8648,30 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8234
8648
  const ready: Promise<void> = (async () => {
8235
8649
  await loadState()
8236
8650
  await refreshMirror()
8651
+ stateWatching = true
8652
+ evaluateStateTree()
8237
8653
  })()
8238
8654
 
8239
- if (CONDITIONS !== undefined) {
8655
+ /**
8656
+ * Rule 2 \u2014 a tree of states only. It fires when the tree goes false \u2192 true.
8657
+ * The FIRST evaluation only seeds \`treeWasTrue\` (a tree that is already true
8658
+ * when the hub or runner starts must not fire), and an evaluation that read an
8659
+ * unknown value neither seeds nor moves it: unknown is not false.
8660
+ */
8661
+ let treeWasTrue: boolean | undefined
8662
+ let stateWatching = false
8663
+ function evaluateStateTree(): void {
8664
+ if (!STATE_ONLY || !stateWatching || stopped) return
8665
+ const evaluation = evaluateWhenTree(WHEN, mirror, Date.now())
8666
+ if (evaluation.unknown) return
8667
+ const was = treeWasTrue
8668
+ treeWasTrue = evaluation.pass
8669
+ if (was === false && evaluation.pass) {
8670
+ void run({ kind: 'state', leaf: 'when', at: Date.now() }, 'the rules became true', true)
8671
+ }
8672
+ }
8673
+
8674
+ if (MIRROR_SOURCES.length > 0) {
8240
8675
  disposers.push(
8241
8676
  ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
8242
8677
  const data = event.data
@@ -8244,14 +8679,17 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8244
8679
  const capName = data['capName']
8245
8680
  if (typeof deviceId === 'number' && typeof capName === 'string') {
8246
8681
  mirror.apply(deviceId, capName, data['slice'])
8682
+ evaluateStateTree()
8247
8683
  }
8248
8684
  }),
8249
8685
  )
8250
- const mirrorReconcile = setInterval(() => { void refreshMirror() }, RECONCILE_MS)
8686
+ const mirrorReconcile = setInterval(() => {
8687
+ void refreshMirror().then(evaluateStateTree)
8688
+ }, RECONCILE_MS)
8251
8689
  disposers.push(() => clearInterval(mirrorReconcile))
8252
8690
  }
8253
8691
 
8254
- ${recipe.triggers.map((t2, i) => emitTrigger(t2, i)).join("\n\n")}
8692
+ ${eventLeaves.map(({ leaf, path: path2 }) => emitLeaf(leaf, path2)).join("\n\n")}
8255
8693
 
8256
8694
  return () => {
8257
8695
  stopped = true
@@ -8266,52 +8704,6 @@ ${recipe.triggers.map((t2, i) => emitTrigger(t2, i)).join("\n\n")}
8266
8704
  }
8267
8705
  `;
8268
8706
  }
8269
- var MACRO_LABELS = [
8270
- {
8271
- id: "person",
8272
- name: "Person"
8273
- },
8274
- {
8275
- id: "vehicle",
8276
- name: "Vehicle"
8277
- },
8278
- {
8279
- id: "animal",
8280
- name: "Animal"
8281
- },
8282
- {
8283
- id: "package",
8284
- name: "Package"
8285
- }
8286
- ];
8287
- var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
8288
- var COCO_TO_MACRO = {
8289
- mapping: {
8290
- person: "person",
8291
- bicycle: "vehicle",
8292
- car: "vehicle",
8293
- motorcycle: "vehicle",
8294
- airplane: "vehicle",
8295
- bus: "vehicle",
8296
- train: "vehicle",
8297
- truck: "vehicle",
8298
- boat: "vehicle",
8299
- bird: "animal",
8300
- cat: "animal",
8301
- dog: "animal",
8302
- horse: "animal",
8303
- sheep: "animal",
8304
- cow: "animal",
8305
- elephant: "animal",
8306
- bear: "animal",
8307
- zebra: "animal",
8308
- giraffe: "animal",
8309
- suitcase: "package",
8310
- backpack: "package",
8311
- handbag: "package"
8312
- },
8313
- preserveOriginal: false
8314
- };
8315
8707
  var MACRO_NAMES = new Set(Object.values(COCO_TO_MACRO.mapping));
8316
8708
  var TraceValueSchema = z4.union([
8317
8709
  z4.string(),
@@ -8336,6 +8728,8 @@ var TraceNodeOutcomeSchema = z4.enum([
8336
8728
  ]);
8337
8729
  var AutomationTriggerTraceSchema = z4.object({
8338
8730
  kind: z4.string().min(1),
8731
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
8732
+ path: z4.string().min(1).optional(),
8339
8733
  outcome: z4.enum(["pass", "fail"]),
8340
8734
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
8341
8735
  reason: z4.string(),
@@ -8343,15 +8737,23 @@ var AutomationTriggerTraceSchema = z4.object({
8343
8737
  vars: z4.record(z4.string(), TraceValueSchema)
8344
8738
  });
8345
8739
  var AutomationConditionTraceNodeSchema = z4.object({
8346
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
8740
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
8347
8741
  path: z4.string().min(1),
8348
8742
  kind: z4.enum([
8349
8743
  "all",
8350
8744
  "any",
8351
8745
  "not",
8352
8746
  "condition",
8747
+ "device-is",
8353
8748
  "expression",
8354
- "stationary-present"
8749
+ "stationary-present",
8750
+ "device-becomes",
8751
+ "device-event",
8752
+ "schedule",
8753
+ "interval",
8754
+ "manual",
8755
+ "detection",
8756
+ "stationary"
8355
8757
  ]),
8356
8758
  outcome: TraceNodeOutcomeSchema,
8357
8759
  reason: z4.string(),
@@ -8384,6 +8786,12 @@ var AutomationStepTraceSchema = z4.object({
8384
8786
  deviceId: z4.number().int().optional(),
8385
8787
  startedAt: z4.number().optional(),
8386
8788
  endedAt: z4.number().optional(),
8789
+ /**
8790
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
8791
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
8792
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
8793
+ */
8794
+ args: z4.record(z4.string(), TraceValueSchema).optional(),
8387
8795
  outcome: z4.enum([
8388
8796
  "ok",
8389
8797
  "error",
@@ -8651,9 +9059,9 @@ function normalizeLeaf(leaf, resolver, path2) {
8651
9059
  })]
8652
9060
  };
8653
9061
  }
8654
- function normalizeConditionUnits(condition, resolver, path2 = "conditions") {
9062
+ function normalizeConditionUnits(condition, resolver, path2 = "when") {
8655
9063
  switch (condition.kind) {
8656
- case "condition": {
9064
+ case "device-is": {
8657
9065
  const { leaf, warnings } = normalizeLeaf(condition, resolver, path2);
8658
9066
  return {
8659
9067
  value: leaf,
@@ -8744,20 +9152,20 @@ function normalizeActionUnits(actions, resolver, path2 = "steps") {
8744
9152
  };
8745
9153
  }
8746
9154
  function normalizeRecipeUnits(recipe, resolver) {
8747
- const conditions = recipe.conditions === void 0 ? void 0 : normalizeConditionUnits(recipe.conditions, resolver, "conditions");
9155
+ const when = normalizeConditionUnits(recipe.when, resolver, "when");
8748
9156
  const actions = normalizeActionUnits(recipe.actions, resolver, "actions");
8749
9157
  return {
8750
9158
  value: {
8751
9159
  ...recipe,
8752
- ...conditions === void 0 ? {} : { conditions: conditions.value },
9160
+ when: when.value,
8753
9161
  actions: actions.value
8754
9162
  },
8755
- warnings: [...conditions?.warnings ?? [], ...actions.warnings]
9163
+ warnings: [...when.warnings, ...actions.warnings]
8756
9164
  };
8757
9165
  }
8758
9166
  function stampCondition(condition, resolver) {
8759
9167
  switch (condition.kind) {
8760
- case "condition": {
9168
+ case "device-is": {
8761
9169
  if (typeof condition.value !== "number" || condition.unit !== void 0) return condition;
8762
9170
  const quantity = resolver.fieldQuantity(condition.cap, condition.fieldPath);
8763
9171
  if (quantity === void 0 || unitsForQuantity(quantity).length < 2) return condition;
@@ -8798,7 +9206,7 @@ function stampAction(action, resolver) {
8798
9206
  function prepareRecipeUnits(recipe, resolver) {
8799
9207
  const stamped = {
8800
9208
  ...recipe,
8801
- ...recipe.conditions === void 0 ? {} : { conditions: stampCondition(recipe.conditions, resolver) },
9209
+ when: stampCondition(recipe.when, resolver),
8802
9210
  actions: recipe.actions.map((action) => stampAction(action, resolver))
8803
9211
  };
8804
9212
  try {
@@ -9287,6 +9695,25 @@ var addonSettingsCapability = {
9287
9695
  addonId: z4.string(),
9288
9696
  nodeId: z4.string().optional()
9289
9697
  }), SettingsSchemaWithValuesSchema.nullable()),
9698
+ getDeclaredSettingsKeys: method(z4.object({
9699
+ addonId: z4.string(),
9700
+ nodeId: z4.string().optional()
9701
+ }), z4.object({ keys: z4.array(z4.string()) }).nullable(), { auth: "admin" }),
9702
+ unsetStoredSettingsKeys: method(z4.object({
9703
+ addonId: z4.string(),
9704
+ nodeId: z4.string().optional(),
9705
+ keys: z4.array(z4.string())
9706
+ }), z4.object({
9707
+ removed: z4.array(z4.string()),
9708
+ refused: z4.array(z4.object({
9709
+ key: z4.string(),
9710
+ reason: z4.string()
9711
+ })),
9712
+ secretsCleared: z4.number()
9713
+ }), {
9714
+ kind: "mutation",
9715
+ auth: "admin"
9716
+ }),
9290
9717
  getDeviceSettings: method(z4.object({
9291
9718
  addonId: z4.string(),
9292
9719
  deviceId: z4.number(),
@@ -10846,8 +11273,21 @@ var BrokerProviderInfoSchema = z4.object({
10846
11273
  * (`{ sessionId }`) drops the session. The dialog that edits a broker
10847
11274
  * offers it as the default path; the fields the session fills are
10848
11275
  * marked `filledBySignIn` in the kind's settings schema.
11276
+ *
11277
+ * `methods` lists the ways the session can sign in, recommended first:
11278
+ * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
11279
+ * settings) answering `{ sessionId, userCode, verificationUrl,
11280
+ * expiresAt }` — the operator types `userCode` at `verificationUrl` on
11281
+ * any device, and the same `status` / `commit` / `cancel` routes follow
11282
+ * (status then carries `method`, `userCode`, `verificationUrl`,
11283
+ * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
11284
+ * `'proxy'` is the `{basePath}/start` flow above. Absent means
11285
+ * `['proxy']`.
10849
11286
  */
10850
- signIn: z4.object({ basePath: z4.string().min(1) }).optional()
11287
+ signIn: z4.object({
11288
+ basePath: z4.string().min(1),
11289
+ methods: z4.array(z4.enum(["device-code", "proxy"])).optional()
11290
+ }).optional()
10851
11291
  }))
10852
11292
  });
10853
11293
  var ListInputSchema = z4.object({
@@ -14140,6 +14580,8 @@ var StorageKindSchema = z4.object({
14140
14580
  "orphaned",
14141
14581
  "dropped"
14142
14582
  ]),
14583
+ /** An owner declared this kind since the engine started. */
14584
+ declaredThisBoot: z4.boolean(),
14143
14585
  /** `null` when the caller asked for no counts (`counts: false`). */
14144
14586
  rows: z4.number().int().nonnegative().nullable(),
14145
14587
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -14151,6 +14593,48 @@ var StorageKindSchema = z4.object({
14151
14593
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
14152
14594
  declaration: RecordKindDeclarationSchema
14153
14595
  });
14596
+ z4.enum([
14597
+ "orphaned",
14598
+ "stale",
14599
+ "unknown"
14600
+ ]);
14601
+ var StaleKindFindingSchema = z4.object({
14602
+ owner: z4.string(),
14603
+ kind: z4.string(),
14604
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
14605
+ verdict: z4.enum(["orphaned", "unknown"]),
14606
+ reason: z4.string(),
14607
+ rows: z4.number().int().nonnegative().nullable(),
14608
+ /** local-auth's tables are never dropped by this audit. */
14609
+ protected: z4.boolean()
14610
+ });
14611
+ var StaleSettingsKeyFindingSchema = z4.object({
14612
+ addonId: z4.string(),
14613
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
14614
+ key: z4.string(),
14615
+ /**
14616
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
14617
+ * not orphans yet. Removing the key releases them.
14618
+ */
14619
+ secretRefs: z4.array(z4.string())
14620
+ });
14621
+ var StaleUnknownSchema = z4.object({
14622
+ subject: z4.string(),
14623
+ reason: z4.string()
14624
+ });
14625
+ var StaleDataReportSchema = z4.object({
14626
+ /** Epoch ms of the last finished scan; `null` before the first. */
14627
+ scannedAt: z4.number().nullable(),
14628
+ scanning: z4.boolean(),
14629
+ trigger: z4.enum([
14630
+ "boot",
14631
+ "interval",
14632
+ "manual"
14633
+ ]).nullable(),
14634
+ kinds: z4.array(StaleKindFindingSchema),
14635
+ settingsKeys: z4.array(StaleSettingsKeyFindingSchema),
14636
+ unknown: z4.array(StaleUnknownSchema)
14637
+ });
14154
14638
  var storageAdminCapability = {
14155
14639
  name: "storage-admin",
14156
14640
  scope: "system",
@@ -14175,7 +14659,49 @@ var storageAdminCapability = {
14175
14659
  readRecords: method(RecordQueryInputSchema.extend({
14176
14660
  owner: z4.string().min(1),
14177
14661
  kind: RecordKindNameSchema
14178
- }), RecordQueryResultSchema, { auth: "admin" })
14662
+ }), RecordQueryResultSchema, { auth: "admin" }),
14663
+ /**
14664
+ * The last stale-data scan (read-only detection): record kinds left behind
14665
+ * and settings keys no running addon declares. Held in memory; a scan runs
14666
+ * at hub boot, every 24 h and on `scanStaleData`.
14667
+ */
14668
+ staleDataReport: method(z4.object({}), StaleDataReportSchema, { auth: "admin" }),
14669
+ /** Scan now. Detection only: it logs findings (owner and key, never values) and changes nothing. */
14670
+ scanStaleData: method(z4.object({}), StaleDataReportSchema, {
14671
+ kind: "mutation",
14672
+ auth: "admin"
14673
+ }),
14674
+ /**
14675
+ * Drop an ORPHANED kind: its rows, their keychain values, then mark it
14676
+ * dropped. The verdict is re-checked at call time; anything not still
14677
+ * orphaned (or a protected owner) is refused.
14678
+ */
14679
+ dropOrphanedKind: method(z4.object({
14680
+ owner: z4.string().min(1),
14681
+ kind: RecordKindNameSchema
14682
+ }), z4.object({
14683
+ deleted: z4.number().int().nonnegative(),
14684
+ secretsReleased: z4.number().int().nonnegative()
14685
+ }), {
14686
+ kind: "mutation",
14687
+ auth: "admin",
14688
+ caller: "required"
14689
+ }),
14690
+ /**
14691
+ * Unset one stale settings key on a running addon (an owner-side write that
14692
+ * also clears the keychain values the key held). Re-checked at call time.
14693
+ */
14694
+ unsetStaleSettingsKey: method(z4.object({
14695
+ addonId: z4.string().min(1),
14696
+ key: z4.string().min(1)
14697
+ }), z4.object({
14698
+ removed: z4.boolean(),
14699
+ secretsCleared: z4.number().int().nonnegative()
14700
+ }), {
14701
+ kind: "mutation",
14702
+ auth: "admin",
14703
+ caller: "required"
14704
+ })
14179
14705
  }
14180
14706
  };
14181
14707
  var EntryKindInfoSchema = z4.object({
@@ -14307,6 +14833,19 @@ var keychainCapability = {
14307
14833
  kind: "mutation",
14308
14834
  auth: "admin"
14309
14835
  }),
14836
+ /**
14837
+ * Release the values rows of a DROPPED record kind held, whatever the kind's
14838
+ * owner (stale-data audit). Reserved to `sqlite-settings`, the engine that
14839
+ * just deleted the rows: only refs scoped `row/records:<kind>/...` are
14840
+ * accepted, and the audit names the operator it acted for. Answers a count.
14841
+ */
14842
+ releaseRecordRefs: method(z4.object({
14843
+ refs: RefListSchema,
14844
+ onBehalfOf: z4.string().min(1)
14845
+ }), z4.object({ released: z4.number().int() }), {
14846
+ kind: "mutation",
14847
+ auth: "admin"
14848
+ }),
14310
14849
  /** The caller's own refs, metadata only. */
14311
14850
  listMeta: method(z4.object({}), z4.array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }),
14312
14851
  /** Whether values can be served — the save path refuses a write while locked. */
@@ -15201,7 +15740,9 @@ var DiscoveredChildDeviceSchema = z4.lazy(() => z4.object({
15201
15740
  /** Upstream-system identity + rendering hints for the candidate. */
15202
15741
  sourceInfo: SourceInfoSchema.optional(),
15203
15742
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
15204
- children: z4.array(DiscoveredChildDeviceSchema).readonly().optional()
15743
+ children: z4.array(DiscoveredChildDeviceSchema).readonly().optional(),
15744
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
15745
+ parentStableId: z4.string().optional()
15205
15746
  }));
15206
15747
  var DeviceDiscoveryStatusSchema = z4.object({
15207
15748
  discovered: z4.array(DiscoveredChildDeviceSchema),
@@ -35419,6 +35960,71 @@ var DiscoveredDeviceSchema = z4.object({
35419
35960
  type: z4.string(),
35420
35961
  metadata: z4.record(z4.string(), z4.unknown()).optional()
35421
35962
  });
35963
+ var DiscoveryModeSchema = z4.discriminatedUnion("kind", [
35964
+ z4.object({
35965
+ kind: z4.literal("mdns"),
35966
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
35967
+ serviceType: z4.string().min(1),
35968
+ /** Regular expression an instance name must match (case-insensitive). */
35969
+ instancePattern: z4.string().optional()
35970
+ }),
35971
+ z4.object({
35972
+ kind: z4.literal("ws-discovery"),
35973
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
35974
+ types: z4.array(z4.string()).optional()
35975
+ }),
35976
+ z4.object({
35977
+ kind: z4.literal("ssdp"),
35978
+ searchTarget: z4.string().min(1)
35979
+ }),
35980
+ z4.object({
35981
+ kind: z4.literal("udp-broadcast"),
35982
+ port: z4.number().int().min(1).max(65535),
35983
+ payloadHex: z4.string().regex(/^([0-9a-fA-F]{2})*$/),
35984
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
35985
+ listenPort: z4.number().int().min(1).max(65535).optional()
35986
+ }),
35987
+ z4.object({
35988
+ kind: z4.literal("http-probe"),
35989
+ port: z4.number().int().min(1).max(65535),
35990
+ path: z4.string().startsWith("/"),
35991
+ method: z4.enum(["GET", "POST"]).default("GET"),
35992
+ bodyText: z4.string().optional(),
35993
+ contentType: z4.string().optional(),
35994
+ tls: z4.boolean().default(false),
35995
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
35996
+ acceptStatus: z4.array(z4.number().int()).optional()
35997
+ }),
35998
+ z4.object({
35999
+ kind: z4.literal("custom"),
36000
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
36001
+ reason: z4.string().min(1)
36002
+ })
36003
+ ]);
36004
+ var DiscoveryHitSchema = z4.object({
36005
+ /** Index into the provider's own `getDiscoveryModes()` array. */
36006
+ modeIndex: z4.number().int().min(0),
36007
+ host: z4.string(),
36008
+ port: z4.number().int(),
36009
+ http: z4.object({
36010
+ status: z4.number().int(),
36011
+ headers: z4.record(z4.string(), z4.string()),
36012
+ /** At most 16 KiB. */
36013
+ bodyText: z4.string()
36014
+ }).optional(),
36015
+ mdns: z4.object({
36016
+ instance: z4.string(),
36017
+ host: z4.string(),
36018
+ port: z4.number().int(),
36019
+ txt: z4.record(z4.string(), z4.string())
36020
+ }).optional(),
36021
+ udp: z4.object({ payloadHex: z4.string() }).optional(),
36022
+ wsd: z4.object({
36023
+ xaddrs: z4.array(z4.string()),
36024
+ types: z4.array(z4.string()),
36025
+ scopes: z4.array(z4.string())
36026
+ }).optional()
36027
+ });
35422
36028
  var DiscoveryCandidateSchema = z4.object({
35423
36029
  stableId: z4.string(),
35424
36030
  type: z4.enum(DeviceType),
@@ -35449,7 +36055,17 @@ var DiscoveryCandidateSchema = z4.object({
35449
36055
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
35450
36056
  onboardedDeviceId: z4.number().optional(),
35451
36057
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
35452
- onboardedName: z4.string().optional()
36058
+ onboardedName: z4.string().optional(),
36059
+ /**
36060
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
36061
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
36062
+ * which is still read for one release.
36063
+ */
36064
+ setupIntegration: z4.boolean().optional(),
36065
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
36066
+ parentStableId: z4.string().optional(),
36067
+ /** `false` = shown for context but not pickable. Default true. */
36068
+ selectable: z4.boolean().optional()
35453
36069
  });
35454
36070
  var DeviceSummarySchema = z4.object({
35455
36071
  id: z4.number(),
@@ -35527,6 +36143,24 @@ var deviceProviderCapability = {
35527
36143
  auth: "admin"
35528
36144
  }),
35529
36145
  /**
36146
+ * How this provider is found on the network. `[]` = the provider has no
36147
+ * declarative mode and keeps answering `discoverDevices` itself.
36148
+ */
36149
+ getDiscoveryModes: method(z4.object({}), z4.array(DiscoveryModeSchema)),
36150
+ /**
36151
+ * Turn the engine's raw hits into candidates. The provider may make
36152
+ * follow-up UNICAST requests to a hit host (an authenticated call, an
36153
+ * identity check) and never sweeps a subnet. `params` is the per-scan form
36154
+ * from `getDiscoveryParamsSchema`.
36155
+ */
36156
+ classifyDiscoveryHits: method(z4.object({
36157
+ hits: z4.array(DiscoveryHitSchema),
36158
+ params: z4.record(z4.string(), z4.unknown()).optional()
36159
+ }), z4.array(DiscoveryCandidateSchema), {
36160
+ kind: "mutation",
36161
+ auth: "admin"
36162
+ }),
36163
+ /**
35530
36164
  * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
35531
36165
  * provider accepts (e.g. Gree's broadcast address for a different subnet).
35532
36166
  * `null` when the provider takes no extra scan params — the generic
@@ -36782,20 +37416,26 @@ var deviceManagerCapability = {
36782
37416
  label: z4.string()
36783
37417
  })).readonly() }), { auth: "admin" }),
36784
37418
  /** Scan EVERY discovery-capable provider; returns candidates grouped by addon. */
36785
- discoverAllProviders: method(z4.object({}), z4.object({ groups: z4.array(z4.object({
36786
- addonId: z4.string(),
36787
- label: z4.string(),
36788
- candidates: z4.array(DiscoveryCandidateSchema).readonly(),
36789
- error: z4.string().nullable()
36790
- })).readonly() }), {
36791
- kind: "mutation",
36792
- auth: "admin"
36793
- }),
37419
+ discoverAllProviders: method(
37420
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
37421
+ z4.object({ subnets: z4.array(z4.string()).optional() }),
37422
+ z4.object({ groups: z4.array(z4.object({
37423
+ addonId: z4.string(),
37424
+ label: z4.string(),
37425
+ candidates: z4.array(DiscoveryCandidateSchema).readonly(),
37426
+ error: z4.string().nullable()
37427
+ })).readonly() }),
37428
+ {
37429
+ kind: "mutation",
37430
+ auth: "admin"
37431
+ }
37432
+ ),
36794
37433
  /** Scan ONE provider with optional provider-specific params (subnet /
36795
37434
  * broadcast addr) — the per-integration "Scan network" surface. */
36796
37435
  discoverProvider: method(z4.object({
36797
37436
  addonId: z4.string(),
36798
- params: z4.record(z4.string(), z4.unknown()).optional()
37437
+ params: z4.record(z4.string(), z4.unknown()).optional(),
37438
+ subnets: z4.array(z4.string()).optional()
36799
37439
  }), z4.object({ candidates: z4.array(DiscoveryCandidateSchema).readonly() }), {
36800
37440
  kind: "mutation",
36801
37441
  auth: "admin"
@@ -56163,7 +56803,9 @@ var integrationsCapability = {
56163
56803
  id: z4.string(),
56164
56804
  settings: z4.record(z4.string(), z4.unknown()),
56165
56805
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56166
- secretKeys: z4.array(z4.string()).optional()
56806
+ secretKeys: z4.array(z4.string()).optional(),
56807
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56808
+ skipRestart: z4.boolean().optional()
56167
56809
  }), z4.object({ success: z4.boolean() }), {
56168
56810
  kind: "mutation",
56169
56811
  auth: "admin"
@@ -56699,6 +57341,37 @@ var TlsStatusSchema = z4.object({
56699
57341
  hasCa: z4.boolean()
56700
57342
  }).nullable()
56701
57343
  });
57344
+ var ScanRequestSchema = z4.object({
57345
+ providerId: z4.string().min(1),
57346
+ modes: z4.array(DiscoveryModeSchema)
57347
+ });
57348
+ var ScanErrorSchema = z4.object({
57349
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
57350
+ providerId: z4.string().optional(),
57351
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
57352
+ mode: z4.string().optional(),
57353
+ message: z4.string()
57354
+ });
57355
+ var ScanStatsSchema = z4.object({
57356
+ hostsProbed: z4.number().int(),
57357
+ hits: z4.number().int(),
57358
+ timeouts: z4.number().int(),
57359
+ byMode: z4.record(z4.string(), z4.object({
57360
+ probed: z4.number().int(),
57361
+ hits: z4.number().int(),
57362
+ timeouts: z4.number().int()
57363
+ }))
57364
+ });
57365
+ var ScanNetworkResultSchema = z4.object({
57366
+ hits: z4.array(z4.object({
57367
+ providerId: z4.string(),
57368
+ hit: DiscoveryHitSchema
57369
+ })),
57370
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
57371
+ subnetsScanned: z4.array(z4.string()),
57372
+ errors: z4.array(ScanErrorSchema),
57373
+ stats: ScanStatsSchema
57374
+ });
56702
57375
  var localNetworkCapability = {
56703
57376
  name: "local-network",
56704
57377
  scope: "system",
@@ -56912,6 +57585,34 @@ var localNetworkCapability = {
56912
57585
  kind: "mutation",
56913
57586
  auth: "admin"
56914
57587
  }),
57588
+ /**
57589
+ * The LAN discovery engine. Every provider that declared discovery modes
57590
+ * (`device-provider.getDiscoveryModes`) is scanned in ONE pass: a single
57591
+ * mDNS browse for the union of service types, one WS-Discovery probe, one
57592
+ * SSDP search per target and one sweep that visits each host once and runs
57593
+ * every http-probe of every provider. Subnets scanned = the hub's own
57594
+ * non-internal IPv4 /24s ∪ the saved list (`getScanSubnets`) ∪ `subnets`.
57595
+ * A bad CIDR, or more than 4096 hosts in total, refuses the scan.
57596
+ */
57597
+ scanNetwork: method(z4.object({
57598
+ requests: z4.array(ScanRequestSchema),
57599
+ subnets: z4.array(z4.string()).optional()
57600
+ }), ScanNetworkResultSchema, {
57601
+ kind: "mutation",
57602
+ auth: "admin",
57603
+ timeoutMs: 9e4
57604
+ }),
57605
+ /** Extra subnets every scan covers besides the hub's own, as CIDR strings. */
57606
+ getScanSubnets: method(z4.void(), z4.object({ subnets: z4.array(z4.string()) }), { auth: "admin" }),
57607
+ /**
57608
+ * Replace the saved extra subnets (a cluster setting). Each entry is IPv4
57609
+ * CIDR, at most a /22; the list is refused when it is invalid or its hosts
57610
+ * would exceed the scan cap.
57611
+ */
57612
+ setScanSubnets: method(z4.object({ subnets: z4.array(z4.string()) }), z4.object({ subnets: z4.array(z4.string()) }), {
57613
+ kind: "mutation",
57614
+ auth: "admin"
57615
+ }),
56915
57616
  /** Return to the generated local CA. The uploaded certificate stays stored (D873). */
56916
57617
  revertToGeneratedCertificate: method(z4.void(), TlsStatusSchema, {
56917
57618
  kind: "mutation",
@@ -60924,7 +61625,8 @@ var rebootCapability = {
60924
61625
  deviceTypes: [
60925
61626
  DeviceType.Camera,
60926
61627
  DeviceType.Sensor,
60927
- DeviceType.Switch
61628
+ DeviceType.Switch,
61629
+ DeviceType.Gateway
60928
61630
  ],
60929
61631
  methods: { reboot: method(z4.object({ deviceId: z4.number().int().nonnegative() }), z4.object({ success: z4.literal(true) }), {
60930
61632
  kind: "mutation",
@@ -65450,7 +66152,8 @@ var CAP_CUSTOMIZE_CATALOG = {
65450
66152
  DeviceType.PetFountain,
65451
66153
  DeviceType.Dishwasher,
65452
66154
  DeviceType.Washer,
65453
- DeviceType.Dryer
66155
+ DeviceType.Dryer,
66156
+ DeviceType.Gateway
65454
66157
  ],
65455
66158
  apply: "add",
65456
66159
  vocabularyCap: "consumables",
@@ -65951,7 +66654,8 @@ var DEVICE_TYPE_CONTROL_KIND = {
65951
66654
  [DeviceType.PetFountain]: "pet-fountain",
65952
66655
  [DeviceType.Dishwasher]: "dishwasher",
65953
66656
  [DeviceType.Washer]: "dummy",
65954
- [DeviceType.Dryer]: "dummy"
66657
+ [DeviceType.Dryer]: "dummy",
66658
+ [DeviceType.Gateway]: "dummy"
65955
66659
  };
65956
66660
  var DEVICE_TYPE_VALUES = new Set(Object.values(DeviceType));
65957
66661
  var METHOD_ACCESS_MAP = Object.freeze({
@@ -66340,6 +67044,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
66340
67044
  access: "create",
66341
67045
  role: "admin"
66342
67046
  },
67047
+ "addonSettings.getDeclaredSettingsKeys": {
67048
+ capName: "addon-settings",
67049
+ capScope: "system",
67050
+ addonId: null,
67051
+ access: "view",
67052
+ role: "admin"
67053
+ },
66343
67054
  "addonSettings.getDeviceSettings": {
66344
67055
  capName: "addon-settings",
66345
67056
  capScope: "system",
@@ -66361,6 +67072,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
66361
67072
  access: "view",
66362
67073
  role: "admin"
66363
67074
  },
67075
+ "addonSettings.unsetStoredSettingsKeys": {
67076
+ capName: "addon-settings",
67077
+ capScope: "system",
67078
+ addonId: null,
67079
+ access: "create",
67080
+ role: "admin"
67081
+ },
66364
67082
  "addonSettings.updateDeviceSettings": {
66365
67083
  capName: "addon-settings",
66366
67084
  capScope: "system",
@@ -68776,6 +69494,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
68776
69494
  access: "create",
68777
69495
  role: "admin"
68778
69496
  },
69497
+ "deviceProvider.classifyDiscoveryHits": {
69498
+ capName: "device-provider",
69499
+ capScope: "system",
69500
+ addonId: null,
69501
+ access: "create",
69502
+ role: "admin"
69503
+ },
68779
69504
  "deviceProvider.createDevice": {
68780
69505
  capName: "device-provider",
68781
69506
  capScope: "system",
@@ -68804,6 +69529,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
68804
69529
  access: "view",
68805
69530
  role: "admin"
68806
69531
  },
69532
+ "deviceProvider.getDiscoveryModes": {
69533
+ capName: "device-provider",
69534
+ capScope: "system",
69535
+ addonId: null,
69536
+ access: "view",
69537
+ role: "admin"
69538
+ },
68807
69539
  "deviceProvider.getDiscoveryParamsSchema": {
68808
69540
  capName: "device-provider",
68809
69541
  capScope: "system",
@@ -69581,6 +70313,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
69581
70313
  access: "create",
69582
70314
  role: "admin"
69583
70315
  },
70316
+ "keychain.releaseRecordRefs": {
70317
+ capName: "keychain",
70318
+ capScope: "system",
70319
+ addonId: null,
70320
+ access: "create",
70321
+ role: "admin"
70322
+ },
69584
70323
  "keychain.resolve": {
69585
70324
  capName: "keychain",
69586
70325
  capScope: "system",
@@ -70001,6 +70740,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
70001
70740
  access: "view",
70002
70741
  role: "admin"
70003
70742
  },
70743
+ "localNetwork.getScanSubnets": {
70744
+ capName: "local-network",
70745
+ capScope: "system",
70746
+ addonId: null,
70747
+ access: "view",
70748
+ role: "admin"
70749
+ },
70004
70750
  "localNetwork.getTlsStatus": {
70005
70751
  capName: "local-network",
70006
70752
  capScope: "system",
@@ -70050,6 +70796,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
70050
70796
  access: "create",
70051
70797
  role: "admin"
70052
70798
  },
70799
+ "localNetwork.scanNetwork": {
70800
+ capName: "local-network",
70801
+ capScope: "system",
70802
+ addonId: null,
70803
+ access: "create",
70804
+ role: "admin"
70805
+ },
70053
70806
  "localNetwork.setAddressRole": {
70054
70807
  capName: "local-network",
70055
70808
  capScope: "system",
@@ -70071,6 +70824,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
70071
70824
  access: "create",
70072
70825
  role: "admin"
70073
70826
  },
70827
+ "localNetwork.setScanSubnets": {
70828
+ capName: "local-network",
70829
+ capScope: "system",
70830
+ addonId: null,
70831
+ access: "create",
70832
+ role: "admin"
70833
+ },
70074
70834
  "localNetwork.setViewerEndpoints": {
70075
70835
  capName: "local-network",
70076
70836
  capScope: "system",
@@ -74292,6 +75052,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
74292
75052
  access: "create",
74293
75053
  role: "admin"
74294
75054
  },
75055
+ "storageAdmin.dropOrphanedKind": {
75056
+ capName: "storage-admin",
75057
+ capScope: "system",
75058
+ addonId: null,
75059
+ access: "delete",
75060
+ role: "admin"
75061
+ },
74295
75062
  "storageAdmin.listKinds": {
74296
75063
  capName: "storage-admin",
74297
75064
  capScope: "system",
@@ -74313,6 +75080,27 @@ var METHOD_ACCESS_MAP = Object.freeze({
74313
75080
  access: "view",
74314
75081
  role: "admin"
74315
75082
  },
75083
+ "storageAdmin.scanStaleData": {
75084
+ capName: "storage-admin",
75085
+ capScope: "system",
75086
+ addonId: null,
75087
+ access: "create",
75088
+ role: "admin"
75089
+ },
75090
+ "storageAdmin.staleDataReport": {
75091
+ capName: "storage-admin",
75092
+ capScope: "system",
75093
+ addonId: null,
75094
+ access: "view",
75095
+ role: "admin"
75096
+ },
75097
+ "storageAdmin.unsetStaleSettingsKey": {
75098
+ capName: "storage-admin",
75099
+ capScope: "system",
75100
+ addonId: null,
75101
+ access: "create",
75102
+ role: "admin"
75103
+ },
74316
75104
  "storageEvictable.evict": {
74317
75105
  capName: "storage-evictable",
74318
75106
  capScope: "system",
@@ -80013,7 +80801,8 @@ var DEVICE_TYPE_DO_CAPS = {
80013
80801
  [DeviceType.PetFountain]: ["pet-fountain"],
80014
80802
  [DeviceType.Dishwasher]: ["dishwasher"],
80015
80803
  [DeviceType.Washer]: [],
80016
- [DeviceType.Dryer]: []
80804
+ [DeviceType.Dryer]: [],
80805
+ [DeviceType.Gateway]: []
80017
80806
  };
80018
80807
  var NUMERIC_ABOVE = t("When {quantity} of {device} rises above {threshold}", "Quando {quantity} di {device} sale sopra {threshold}");
80019
80808
  var NUMERIC_BELOW = t("When {quantity} of {device} falls below {threshold}", "Quando {quantity} di {device} scende sotto {threshold}");
@@ -85323,10 +86112,12 @@ function describeHub(hub) {
85323
86112
  export {
85324
86113
  isReadingQuantity,
85325
86114
  FFMPEG_PATH_ENV,
86115
+ MACRO_LABELS,
86116
+ countWhenLeaves,
86117
+ convertLegacyWhen,
85326
86118
  AutomationRecipeSchema,
85327
86119
  validateRecipeBounds,
85328
86120
  generateAutomationBlock,
85329
- MACRO_LABELS,
85330
86121
  unitsForQuantity,
85331
86122
  capUnitResolver,
85332
86123
  prepareRecipeUnits,