camstack 1.2.210 → 1.2.212

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 = {
@@ -1724,6 +1725,9 @@ var READING_QUANTITIES = [
1724
1725
  ];
1725
1726
  var ReadingQuantitySchema = z2.enum(READING_QUANTITIES);
1726
1727
  var QUANTITY_SET = new Set(READING_QUANTITIES);
1728
+ function isReadingQuantity(value) {
1729
+ return typeof value === "string" && QUANTITY_SET.has(value);
1730
+ }
1727
1731
  var QUANTITY_BASE = {
1728
1732
  temperature: "\xB0C",
1729
1733
  pressure: "hPa",
@@ -6747,6 +6751,63 @@ var ExpressionSourceSchema = z4.object({
6747
6751
  path: ["expr"]
6748
6752
  });
6749
6753
  });
6754
+ function unwrapSchema(schema) {
6755
+ let s = schema;
6756
+ while (s instanceof z4.ZodNullable || s instanceof z4.ZodOptional || s instanceof z4.ZodDefault) s = s.unwrap();
6757
+ return s;
6758
+ }
6759
+ function isMetaBag(value) {
6760
+ return typeof value === "object" && value !== null;
6761
+ }
6762
+ function metaQuantity(schema) {
6763
+ const bag = z4.globalRegistry.get(schema);
6764
+ if (isMetaBag(bag) && isReadingQuantity(bag.quantity)) return bag.quantity;
6765
+ }
6766
+ function quantityOfField(schema) {
6767
+ return metaQuantity(schema) ?? metaQuantity(unwrapSchema(schema));
6768
+ }
6769
+ function leafKind(schema) {
6770
+ const s = unwrapSchema(schema);
6771
+ if (s instanceof z4.ZodEnum) return {
6772
+ kind: "enum",
6773
+ enumValues: s.options
6774
+ };
6775
+ if (s instanceof z4.ZodNumber) return { kind: "number" };
6776
+ if (s instanceof z4.ZodString) return { kind: "string" };
6777
+ if (s instanceof z4.ZodBoolean) return { kind: "boolean" };
6778
+ return null;
6779
+ }
6780
+ function enumerateSchemaFields(schema, prefix = "") {
6781
+ const root = unwrapSchema(schema);
6782
+ if (!(root instanceof z4.ZodObject)) return [];
6783
+ const out = [];
6784
+ const shape = root.shape;
6785
+ for (const [key, field] of Object.entries(shape)) {
6786
+ const path2 = prefix ? `${prefix}.${key}` : key;
6787
+ const inner = unwrapSchema(field);
6788
+ if (inner instanceof z4.ZodObject) {
6789
+ out.push(...enumerateSchemaFields(inner, path2));
6790
+ continue;
6791
+ }
6792
+ const leaf = leafKind(field);
6793
+ if (leaf) {
6794
+ const quantity = quantityOfField(field);
6795
+ out.push({
6796
+ path: path2,
6797
+ kind: leaf.kind,
6798
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {},
6799
+ ...quantity !== void 0 ? { quantity } : {}
6800
+ });
6801
+ }
6802
+ }
6803
+ return out;
6804
+ }
6805
+ function enumerateItemArrayFields(itemArray) {
6806
+ return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
6807
+ ...f,
6808
+ item: true
6809
+ }));
6810
+ }
6750
6811
  var SourceInfoSchema = z4.object({
6751
6812
  /** Live dispatch key — mutable when the source system allows rename. */
6752
6813
  id: z4.string(),
@@ -6768,6 +6829,69 @@ var SourceInfoSchema = z4.object({
6768
6829
  * attribute blobs (would explode the meta JSON column). */
6769
6830
  raw: z4.record(z4.string(), z4.unknown()).optional()
6770
6831
  });
6832
+ function normalizeUnit(raw) {
6833
+ if (raw == null) return void 0;
6834
+ const trimmed = raw.trim();
6835
+ if (trimmed === "") return void 0;
6836
+ const lower = trimmed.toLowerCase();
6837
+ if (lower === "\xB0c" || lower === "c" || lower === "celsius") return "\xB0C";
6838
+ if (lower === "\xB0f" || lower === "f" || lower === "fahrenheit") return "\xB0F";
6839
+ if (lower === "k" || lower === "kelvin") return "K";
6840
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
6841
+ if (lower === "lx" || lower === "lux") return "lx";
6842
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
6843
+ if (lower === "kpa") return "kPa";
6844
+ if (lower === "pa") return "Pa";
6845
+ if (lower === "mbar" || lower === "millibar") return "mbar";
6846
+ if (lower === "w" || lower === "watt") return "W";
6847
+ if (lower === "kw" || lower === "kilowatt") return "kW";
6848
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
6849
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
6850
+ if (lower === "v" || lower === "volt") return "V";
6851
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
6852
+ if (lower === "ma" || lower === "milliampere") return "mA";
6853
+ if (lower === "va") return "VA";
6854
+ if (lower === "ppm") return "ppm";
6855
+ if (lower === "ppb") return "ppb";
6856
+ if (lower === "\xB5g/m\xB3" || lower === "ug/m3" || lower === "\xB5g/m3") return "\xB5g/m\xB3";
6857
+ if (lower === "km/h" || lower === "kph") return "km/h";
6858
+ if (lower === "mph") return "mph";
6859
+ if (lower === "m/s") return "m/s";
6860
+ if (lower === "m") return "m";
6861
+ if (lower === "m\xB2" || lower === "m2") return "m\xB2";
6862
+ if (lower === "mm") return "mm";
6863
+ if (lower === "cm") return "cm";
6864
+ if (lower === "km") return "km";
6865
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
6866
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
6867
+ if (lower === "kg") return "kg";
6868
+ if (lower === "g") return "g";
6869
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
6870
+ if (lower === "ml") return "mL";
6871
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
6872
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
6873
+ if (lower === "ms") return "ms";
6874
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
6875
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
6876
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
6877
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
6878
+ if (lower === "kib") return "KiB";
6879
+ if (lower === "mib") return "MiB";
6880
+ if (lower === "gib") return "GiB";
6881
+ if (lower === "tib") return "TiB";
6882
+ if (lower === "kb") return "kB";
6883
+ if (lower === "mb") return "MB";
6884
+ if (lower === "gb") return "GB";
6885
+ if (lower === "dbm") return "dBm";
6886
+ if (lower === "dbfs") return "dBFS";
6887
+ if (lower === "inhg") return "inHg";
6888
+ if (lower === "mm/h") return "mm/h";
6889
+ if (lower === "w/m\xB2" || lower === "w/m2") return "W/m\xB2";
6890
+ if (lower === "\xB0" || lower === "deg" || lower === "degree" || lower === "degrees") return "\xB0";
6891
+ if (lower === "kbps") return "kbps";
6892
+ if (lower === "fps") return "fps";
6893
+ return trimmed;
6894
+ }
6771
6895
  var UNIT_TABLE = {
6772
6896
  "\xB0C": {
6773
6897
  dimension: "temperature",
@@ -7118,6 +7242,9 @@ var UNIT_TABLE = {
7118
7242
  function specFor(unit) {
7119
7243
  return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
7120
7244
  }
7245
+ function unitDimension(unit) {
7246
+ return specFor(unit)?.dimension;
7247
+ }
7121
7248
  function toCanonical(value, spec) {
7122
7249
  return value * spec.factor + spec.offset;
7123
7250
  }
@@ -7133,6 +7260,11 @@ function tryConvertUnit(value, from, to) {
7133
7260
  if (fromSpec.dimension !== toSpec.dimension) return void 0;
7134
7261
  return fromCanonical(toCanonical(value, fromSpec), toSpec);
7135
7262
  }
7263
+ function unitsForDimension(dimension) {
7264
+ const out = [];
7265
+ for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
7266
+ return out;
7267
+ }
7136
7268
  var QuantityUnitError = class extends Error {
7137
7269
  code;
7138
7270
  constructor(code, message) {
@@ -7145,6 +7277,17 @@ var QuantityValueSchema = z4.object({
7145
7277
  value: z4.number(),
7146
7278
  unit: z4.string().min(1)
7147
7279
  });
7280
+ function isQuantityValue(candidate) {
7281
+ return QuantityValueSchema.safeParse(candidate).success;
7282
+ }
7283
+ function convertQuantityInput(input) {
7284
+ if (!Number.isFinite(input.value)) throw new QuantityUnitError("non-finite", `${String(input.value)} is not a finite number`);
7285
+ const unit = normalizeUnit(input.unit);
7286
+ if (unit === void 0 || unitDimension(unit) === void 0) throw new QuantityUnitError("unknown-unit", `unknown unit "${input.unit}"`);
7287
+ const converted = tryConvertUnit(input.value, unit, input.target);
7288
+ if (converted === void 0) throw new QuantityUnitError("cross-dimension", `"${input.unit}" is not a unit of ${input.target}'s quantity`);
7289
+ return Number(converted.toPrecision(12));
7290
+ }
7148
7291
  function paramMeta(declared) {
7149
7292
  const base = baseUnitOf(declared.quantity);
7150
7293
  if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
@@ -7158,6 +7301,13 @@ var ParamMetaBagSchema = z4.object({
7158
7301
  quantity: ReadingQuantitySchema,
7159
7302
  unit: z4.string().min(1)
7160
7303
  });
7304
+ function metaOf(schema) {
7305
+ const parsed = ParamMetaBagSchema.safeParse(z4.globalRegistry.get(schema));
7306
+ return parsed.success ? parsed.data : void 0;
7307
+ }
7308
+ function paramMetaOf(field) {
7309
+ return metaOf(field) ?? metaOf(unwrapSchema(field));
7310
+ }
7161
7311
  var AutomationConditionOperatorSchema = z4.enum([
7162
7312
  "eq",
7163
7313
  "ne",
@@ -7353,6 +7503,770 @@ var AutomationRecipeSchema = z4.object({
7353
7503
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
7354
7504
  keepTraces: z4.number().int().min(1).max(100).optional()
7355
7505
  });
7506
+ function isLeaf(cond) {
7507
+ return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
7508
+ }
7509
+ function conditionDepth(cond) {
7510
+ if (isLeaf(cond)) return 1;
7511
+ if (cond.kind === "not") return 1 + conditionDepth(cond.child);
7512
+ const children = cond.children;
7513
+ if (children.length === 0) return 1;
7514
+ let deepest = 0;
7515
+ for (const child of children) {
7516
+ const d = conditionDepth(child);
7517
+ if (d > deepest) deepest = d;
7518
+ }
7519
+ return 1 + deepest;
7520
+ }
7521
+ function countConditionLeaves(cond) {
7522
+ if (isLeaf(cond)) return 1;
7523
+ if (cond.kind === "not") return countConditionLeaves(cond.child);
7524
+ let total = 0;
7525
+ for (const child of cond.children) total += countConditionLeaves(child);
7526
+ return total;
7527
+ }
7528
+ function findEmptyContainer(cond) {
7529
+ if (isLeaf(cond)) return false;
7530
+ if (cond.kind === "not") return findEmptyContainer(cond.child);
7531
+ if (cond.children.length === 0) return true;
7532
+ return cond.children.some(findEmptyContainer);
7533
+ }
7534
+ function validateRecipeBounds(recipe) {
7535
+ if (recipe.triggers.length === 0) return "an automation needs at least one trigger \u2014 without one nothing can start a run";
7536
+ if (recipe.actions.length === 0) return "an automation needs at least one action \u2014 a rule that does nothing is not a rule";
7537
+ const cond = recipe.conditions;
7538
+ if (cond !== void 0) {
7539
+ if (findEmptyContainer(cond)) return "a condition group is empty \u2014 group existing rows instead of leaving a container to fill";
7540
+ const depth = conditionDepth(cond);
7541
+ 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`;
7542
+ const leaves = countConditionLeaves(cond);
7543
+ if (leaves > 32) return `conditions use ${leaves} leaves \u2014 the limit is 32`;
7544
+ }
7545
+ return null;
7546
+ }
7547
+ var RECONCILE_MS = 15e3;
7548
+ function stepMetaOf(action) {
7549
+ switch (action.kind) {
7550
+ case "wait":
7551
+ return { kind: "wait" };
7552
+ case "code":
7553
+ return { kind: "code" };
7554
+ case "action":
7555
+ return { kind: "action" };
7556
+ case "cap":
7557
+ return {
7558
+ kind: "cap",
7559
+ cap: action.cap,
7560
+ method: action.method,
7561
+ ...typeof action.deviceId === "number" ? { deviceId: action.deviceId } : {}
7562
+ };
7563
+ }
7564
+ }
7565
+ function j(value) {
7566
+ return JSON.stringify(value);
7567
+ }
7568
+ function emitTrigger(trigger, index) {
7569
+ switch (trigger.kind) {
7570
+ case "schedule":
7571
+ return ` // trigger ${index}: schedule \u2014 armed to the next minute boundary and
7572
+ // RE-ARMED from the wall clock every tick, so it cannot drift the way a
7573
+ // setInterval(24h) does. No cron library: a block cannot resolve one.
7574
+ {
7575
+ let timer: ReturnType<typeof setTimeout> | undefined
7576
+ const arm = (): void => {
7577
+ timer = setTimeout(() => {
7578
+ if (cronMatches(${j(trigger.cron)}, new Date())) {
7579
+ void run({ kind: 'schedule', at: Date.now() })
7580
+ }
7581
+ arm()
7582
+ }, nextMinuteBoundary())
7583
+ }
7584
+ arm()
7585
+ disposers.push(() => { if (timer !== undefined) clearTimeout(timer) })
7586
+ }`;
7587
+ case "device-event":
7588
+ return ` // trigger ${index}: device event
7589
+ disposers.push(
7590
+ ctx.eventBus.subscribe(
7591
+ { category: ${j(trigger.category)}${trigger.deviceId === void 0 ? "" : `, deviceId: ${j(trigger.deviceId)}`} },
7592
+ (event) => {
7593
+ void run({
7594
+ kind: 'device-event',
7595
+ at: Date.now(),
7596
+ deviceId: event.deviceId,
7597
+ category: ${j(trigger.category)},
7598
+ })
7599
+ },
7600
+ ),
7601
+ )`;
7602
+ case "device-state": {
7603
+ const becomes = trigger.becomes === void 0 ? "undefined" : j(trigger.becomes);
7604
+ return ` // trigger ${index}: device state \u2014 the mirror is the authority, the
7605
+ // event only WAKES the evaluation, so a dropped event delays and never loses.
7606
+ {
7607
+ let previous: unknown = undefined
7608
+ let debounce: ReturnType<typeof setTimeout> | undefined
7609
+ let heldSince: number | undefined
7610
+ const check = async (): Promise<void> => {
7611
+ const value = await readField(${j(trigger.deviceId)}, ${j(trigger.cap)}, ${j(trigger.fieldPath)})
7612
+ const want = ${becomes}
7613
+ const matches = want === undefined ? value !== previous : value === want
7614
+ const changed = value !== previous
7615
+ previous = value
7616
+ if (!matches) { heldSince = undefined; return }
7617
+ if (${trigger.edge === true ? "true" : "false"} && !changed) return
7618
+ const forMs = ${j(trigger.forMs ?? 0)}
7619
+ if (forMs > 0) {
7620
+ if (heldSince === undefined) { heldSince = Date.now(); return }
7621
+ if (Date.now() - heldSince < forMs) return
7622
+ }
7623
+ heldSince = undefined
7624
+ void run({
7625
+ kind: 'device-state',
7626
+ at: Date.now(),
7627
+ deviceId: ${j(trigger.deviceId)},
7628
+ cap: ${j(trigger.cap)},
7629
+ field: ${j(trigger.fieldPath)},
7630
+ to: value,
7631
+ })
7632
+ }
7633
+ const wake = (): void => {
7634
+ if (debounce !== undefined) clearTimeout(debounce)
7635
+ debounce = setTimeout(() => { void check() }, ${j(trigger.debounceMs ?? 0)})
7636
+ }
7637
+ disposers.push(
7638
+ ctx.eventBus.subscribe({ category: 'device.state-changed', deviceId: ${j(trigger.deviceId)} }, wake),
7639
+ )
7640
+ const reconcile = setInterval(() => { void check() }, RECONCILE_MS)
7641
+ disposers.push(() => clearInterval(reconcile))
7642
+ }`;
7643
+ }
7644
+ case "manual":
7645
+ return ` // trigger ${index}: manual \u2014 fired by automation-control.trigger
7646
+ // ("Run actions"), which runs the THEN half with an EMPTY vars bag. A manual
7647
+ // run bypasses only the cooldown wait, never the single/queued limits.`;
7648
+ case "interval":
7649
+ return ` // trigger ${index}: interval \u2014 every ${trigger.minutes} min, first run jittered so
7650
+ // blocks restarted together do not all fire on the same second.
7651
+ {
7652
+ let interval: ReturnType<typeof setInterval> | undefined
7653
+ const first = setTimeout(() => {
7654
+ void run({ kind: 'interval', at: Date.now() })
7655
+ interval = setInterval(() => { void run({ kind: 'interval', at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
7656
+ }, Math.floor(Math.random() * Math.min(${j(trigger.minutes * 6e4)}, 30_000)) + 1)
7657
+ disposers.push(() => {
7658
+ clearTimeout(first)
7659
+ if (interval !== undefined) clearInterval(interval)
7660
+ })
7661
+ }`;
7662
+ case "detection":
7663
+ return ` // trigger ${index}: detection \u2014 ONE fire per (device, track); a rejected event is
7664
+ // traced with its reason and never fires.
7665
+ {
7666
+ const filter = new DetectionTriggerFilter(${j(trigger)})
7667
+ disposers.push(
7668
+ ctx.eventBus.subscribe(
7669
+ { category: EventCategory.PipelineAnalyticsTrackLifecycle },
7670
+ (event) => {
7671
+ const decision = filter.evaluate(event.data, Date.now())
7672
+ if (decision.kind === 'ignore') return
7673
+ if (decision.kind === 'reject') {
7674
+ ctx.logger.debug('detection trigger rejected an event', {
7675
+ tags: { deviceId: decision.vars.deviceId },
7676
+ reason: decision.reason,
7677
+ })
7678
+ }
7679
+ void run(
7680
+ { kind: 'detection', at: Date.now(), ...decision.vars },
7681
+ decision.reason,
7682
+ decision.kind === 'fire',
7683
+ )
7684
+ },
7685
+ ),
7686
+ )
7687
+ }`;
7688
+ case "stationary":
7689
+ return ` // trigger ${index}: stationary \u2014 a parked object appeared / departed. Stateless:
7690
+ // the registry emits one event per transition. A dropped event only misses a fire;
7691
+ // the stationary-present CONDITION reads the registry, not this event.
7692
+ {
7693
+ const trigger = ${j(trigger)} as const
7694
+ disposers.push(
7695
+ ctx.eventBus.subscribe(
7696
+ { category: EventCategory.PipelineAnalyticsStationaryChanged },
7697
+ (event) => {
7698
+ const decision = evaluateStationaryTrigger(trigger, event.data)
7699
+ if (decision.kind === 'ignore') return
7700
+ if (decision.kind === 'reject') {
7701
+ ctx.logger.debug('stationary trigger rejected an event', {
7702
+ tags: { deviceId: decision.vars.deviceId },
7703
+ reason: decision.reason,
7704
+ })
7705
+ }
7706
+ void run(
7707
+ { kind: 'stationary', at: Date.now(), ...decision.vars },
7708
+ decision.reason,
7709
+ decision.kind === 'fire',
7710
+ )
7711
+ },
7712
+ ),
7713
+ )
7714
+ }`;
7715
+ }
7716
+ }
7717
+ function emitAction(action, index) {
7718
+ const step = (body) => ` {
7719
+ meta: STEP_META[${index}]!,
7720
+ run: async (scope) => {
7721
+ ${body}
7722
+ },
7723
+ },`;
7724
+ switch (action.kind) {
7725
+ case "wait":
7726
+ return step(` await sleep(${j(action.seconds * 1e3)})`);
7727
+ case "cap":
7728
+ if (Array.isArray(action.deviceId)) return step(` for (const deviceId of ${j(action.deviceId)}) {
7729
+ scope.setDeviceId(deviceId)
7730
+ await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
7731
+ }`);
7732
+ return step(` const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
7733
+ scope.setDeviceId(deviceId)
7734
+ await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
7735
+ case "action":
7736
+ return step(` // an Action run, awaited: the step takes THAT run's outcome
7737
+ const result = await ctx.api.actions.run.mutate({
7738
+ actionId: ${j(action.actionId)},
7739
+ variables: substituteVariables(${j(action.variables ?? {})}, vars),
7740
+ wait: true,
7741
+ origin: { blockId: BLOCK_ID ?? undefined },
7742
+ })
7743
+ if (result.outcome !== 'ran') {
7744
+ throw new Error('action ' + ${j(action.actionId)} + ' ' + result.outcome + (result.message === undefined ? '' : ': ' + result.message))
7745
+ }`);
7746
+ case "code":
7747
+ return step(` // inline code action \u2014 a fragment of THIS block, bundled by esbuild
7748
+ await (async () => {
7749
+ ${action.code.split("\n").map((line) => ` ${line}`).join("\n")}
7750
+ })()`);
7751
+ }
7752
+ }
7753
+ function generateAutomationBlock(input) {
7754
+ const boundsError = validateRecipeBounds(input.recipe);
7755
+ if (boundsError !== null) throw new Error(`cannot generate an automation from an invalid recipe: ${boundsError}`);
7756
+ const { name, recipe } = input;
7757
+ const runMode = recipe.runMode ?? { mode: "single" };
7758
+ const stepMeta = recipe.actions.map(stepMetaOf);
7759
+ return `import type {
7760
+ AddonContext,
7761
+ AutomationConditionTraceNode,
7762
+ AutomationRunTrace,
7763
+ AutomationRunVerdict,
7764
+ AutomationStepTrace,
7765
+ RunnableStep,
7766
+ } from '@camstack/types'
7767
+ import {
7768
+ ConditionMirror,
7769
+ DetectionTriggerFilter,
7770
+ EventCategory,
7771
+ RunGate,
7772
+ buildTriggerTrace,
7773
+ collectMirrorSources,
7774
+ coreBlockIdFromAddonId,
7775
+ evaluateConditionTree,
7776
+ evaluateStationaryTrigger,
7777
+ isSkippedTrace,
7778
+ mergeSkipTraces,
7779
+ resolveDeviceTarget,
7780
+ resolveStepArgs,
7781
+ runSteps,
7782
+ } from '@camstack/types'
7783
+
7784
+ /**
7785
+ * ${name}
7786
+ *
7787
+ * GENERATED FROM A RECIPE \u2014 do not edit by hand. Editing this file makes it a
7788
+ * script: the visual editor reads AUTOMATION_RECIPE below, and hand edits are
7789
+ * lost the next time the recipe is saved. Use "Eject to code" to take ownership
7790
+ * of it permanently (one way).
7791
+ */
7792
+
7793
+ const AUTOMATION_RECIPE = ${JSON.stringify(recipe, null, 2)} as const
7794
+
7795
+ const RECONCILE_MS = ${RECONCILE_MS}
7796
+
7797
+ /** How triggers that arrive while busy (or cooling down) are handled. */
7798
+ const RUN_MODE = ${j(runMode)} as const
7799
+
7800
+ /** Run traces kept (a ring, held in the hub's runs ledger). */
7801
+ const KEEP_TRACES = ${recipe.keepTraces ?? 20}
7802
+
7803
+ const CONDITIONS = ${recipe.conditions === void 0 ? "undefined" : "AUTOMATION_RECIPE.conditions"}
7804
+
7805
+ /** Skips that repeat are folded into one trace; it goes to the hub after this. */
7806
+ const SKIP_FLUSH_MS = 5_000
7807
+
7808
+ /** Static facts about each action step, for its trace. */
7809
+ const STEP_META: ReadonlyArray<{ kind: string; cap?: string; method?: string; deviceId?: number }> = ${j(stepMeta)}
7810
+
7811
+ /** A cap is named \`alarm-panel\`; \`ctx.api\` is keyed by the ROUTER name,
7812
+ * \`alarmPanel\`. Indexing ctx.api with the cap name yields undefined and every
7813
+ * action throws, so the conversion is not cosmetic. */
7814
+ function capToRouterKey(cap: string): string {
7815
+ return cap.replace(/-([a-z0-9])/g, (_m, c: string) => c.toUpperCase())
7816
+ }
7817
+
7818
+ /** Ms until the top of the next minute. Recomputed every tick so a long-lived
7819
+ * schedule cannot accumulate drift. */
7820
+ function nextMinuteBoundary(): number {
7821
+ const now = Date.now()
7822
+ return 60_000 - (now % 60_000)
7823
+ }
7824
+
7825
+ /** Does \`when\` satisfy a 5-field cron expression (min hour dom mon dow)?
7826
+ * Supports wildcards, step syntax, ranges, comma lists and plain numbers \u2014
7827
+ * the subset an operator can author, and nothing that needs a library. */
7828
+ function cronMatches(expr: string, when: Date): boolean {
7829
+ const fields = expr.trim().split(/\\s+/)
7830
+ if (fields.length !== 5) return false
7831
+ const actual = [
7832
+ when.getMinutes(),
7833
+ when.getHours(),
7834
+ when.getDate(),
7835
+ when.getMonth() + 1,
7836
+ when.getDay(),
7837
+ ]
7838
+ return fields.every((field, i) => matchField(field, actual[i]!))
7839
+ }
7840
+
7841
+ function matchField(field: string, value: number): boolean {
7842
+ for (const part of field.split(',')) {
7843
+ const [range, stepRaw] = part.split('/')
7844
+ const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10)
7845
+ if (!Number.isFinite(step) || step < 1) continue
7846
+ if (range === '*') {
7847
+ if (value % step === 0) return true
7848
+ continue
7849
+ }
7850
+ const bounds = range!.split('-')
7851
+ const lo = Number.parseInt(bounds[0]!, 10)
7852
+ const hi = bounds[1] === undefined ? lo : Number.parseInt(bounds[1], 10)
7853
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue
7854
+ if (value >= lo && value <= hi && (value - lo) % step === 0) return true
7855
+ }
7856
+ return false
7857
+ }
7858
+
7859
+ export default function block(ctx: AddonContext) {
7860
+ const disposers: Array<() => void> = []
7861
+ const gate = new RunGate(RUN_MODE)
7862
+ const mirror = new ConditionMirror(collectMirrorSources(CONDITIONS))
7863
+ const BLOCK_ID = coreBlockIdFromAddonId(ctx.id)
7864
+ /** The cooldown stamp: memory is the authority at fire time (D49); the hub ledger is read once at start. */
7865
+ let lastRunStartedAt: number | null = null
7866
+ /** True when the stamp could not be read at start \u2014 the first run is let through and says so. */
7867
+ let stampUnknown = false
7868
+ let sendChain: Promise<void> = Promise.resolve()
7869
+ let pendingSkip: AutomationRunTrace | undefined
7870
+ let skipTimer: ReturnType<typeof setTimeout> | undefined
7871
+ let stopped = false
7872
+ let runCounter = 0
7873
+ const runningTokens = new Set<RunToken>()
7874
+ const queue: QueuedRun[] = []
7875
+
7876
+ interface RunToken {
7877
+ aborted: boolean
7878
+ wake?: () => void
7879
+ }
7880
+ interface QueuedRun {
7881
+ readonly trigger: Record<string, unknown>
7882
+ readonly reason: string
7883
+ readonly queuedAt: number
7884
+ }
7885
+
7886
+ /** Sleep that a restart can cut short. */
7887
+ const abortableSleep = (ms: number, token: RunToken): Promise<void> =>
7888
+ new Promise((resolve) => {
7889
+ const timer = setTimeout(resolve, ms)
7890
+ token.wake = () => {
7891
+ clearTimeout(timer)
7892
+ resolve()
7893
+ }
7894
+ })
7895
+
7896
+ // \u2500\u2500 the hub's runs ledger: the stamp is read ONCE here, the ring is written through RPC \u2500\u2500
7897
+ async function loadState(): Promise<void> {
7898
+ if (BLOCK_ID === null) {
7899
+ ctx.logger.warn('this is not a core-block addon id \u2014 no run ledger, cooldown and traces will not survive a restart', { addonId: ctx.id })
7900
+ return
7901
+ }
7902
+ try {
7903
+ const doc = await ctx.api.coreBlocks.getAutomationRunState.query({ blockId: BLOCK_ID })
7904
+ lastRunStartedAt = doc.lastRunStartedAt
7905
+ } catch (err: unknown) {
7906
+ stampUnknown = true
7907
+ ctx.logger.warn('could not read the stored cooldown stamp \u2014 the first run is let through', {
7908
+ blockId: BLOCK_ID,
7909
+ error: err instanceof Error ? err.message : String(err),
7910
+ })
7911
+ }
7912
+ }
7913
+
7914
+ function send(payload: { trace?: AutomationRunTrace; runStartedAt?: number }): Promise<void> {
7915
+ if (BLOCK_ID === null) return Promise.resolve()
7916
+ const blockId = BLOCK_ID
7917
+ sendChain = sendChain.then(async () => {
7918
+ try {
7919
+ await ctx.api.coreBlocks.reportAutomationRun.mutate({ blockId, keepTraces: KEEP_TRACES, ...payload })
7920
+ } catch (err: unknown) {
7921
+ ctx.logger.warn('could not report the run to the hub ledger', {
7922
+ blockId,
7923
+ error: err instanceof Error ? err.message : String(err),
7924
+ })
7925
+ }
7926
+ })
7927
+ return sendChain
7928
+ }
7929
+
7930
+ function flushSkip(): void {
7931
+ if (skipTimer !== undefined) {
7932
+ clearTimeout(skipTimer)
7933
+ skipTimer = undefined
7934
+ }
7935
+ if (pendingSkip === undefined) return
7936
+ const trace = pendingSkip
7937
+ pendingSkip = undefined
7938
+ void send({ trace })
7939
+ }
7940
+
7941
+ /** A skip identical to the one waiting is COUNTED on it; anything else flushes it first. */
7942
+ function record(trace: AutomationRunTrace): void {
7943
+ if (isSkippedTrace(trace)) {
7944
+ if (pendingSkip !== undefined) {
7945
+ const merged = mergeSkipTraces(pendingSkip, trace)
7946
+ if (merged !== null) {
7947
+ pendingSkip = merged
7948
+ return
7949
+ }
7950
+ flushSkip()
7951
+ }
7952
+ pendingSkip = trace
7953
+ skipTimer = setTimeout(flushSkip, SKIP_FLUSH_MS)
7954
+ return
7955
+ }
7956
+ flushSkip()
7957
+ void send({ trace })
7958
+ }
7959
+
7960
+ // \u2500\u2500 the condition mirror (D49): memory only at fire time \u2500\u2500
7961
+ async function refreshMirror(): Promise<void> {
7962
+ const report = await mirror.refresh({
7963
+ fetchSlice: (deviceId, cap) => ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap }),
7964
+ fetchStableIds: async () => {
7965
+ const all = await ctx.api.deviceManager.listAll.query({})
7966
+ return new Map(all.devices.flatMap((d: { stableId?: string; id: number }) =>
7967
+ d.stableId === undefined ? [] : [[d.stableId, d.id] as [string, number]]))
7968
+ },
7969
+ })
7970
+ if (report.kept > 0) {
7971
+ ctx.logger.debug('condition mirror kept stale values for sources it could not read', report)
7972
+ }
7973
+ }
7974
+
7975
+ /** Read one cap field off the state mirror. Returns undefined when the device,
7976
+ * the cap or the field cannot be resolved \u2014 the caller decides what that
7977
+ * means, and it always logs. (Used by the device-state TRIGGER, which the
7978
+ * reconcile backs; conditions read the ConditionMirror instead.) */
7979
+ async function readField(
7980
+ deviceId: number,
7981
+ cap: string,
7982
+ fieldPath: string,
7983
+ ): Promise<unknown> {
7984
+ try {
7985
+ const slice = await ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap })
7986
+ let cursor: unknown = slice
7987
+ for (const part of fieldPath.split('.')) {
7988
+ if (cursor === null || typeof cursor !== 'object') return undefined
7989
+ cursor = (cursor as Record<string, unknown>)[part]
7990
+ }
7991
+ return cursor
7992
+ } catch {
7993
+ return undefined
7994
+ }
7995
+ }
7996
+
7997
+ /** A {$var} action target, resolved from the run's bag BEFORE dispatch \u2014 the
7998
+ * action runner only ever sees a number. */
7999
+ function resolveTarget(vars: Record<string, unknown>, name: string): number {
8000
+ return resolveDeviceTarget(vars, name)
8001
+ }
8002
+
8003
+ function warnMissingVar(name: string): void {
8004
+ ctx.logger.warn('template referenced a var that was never set', { var: name })
8005
+ }
8006
+
8007
+ /** Substitute {{vars.x}} slots and { $var } values. Substitution only \u2014 never evaluation. */
8008
+ function substitute(value: unknown, vars: Record<string, unknown>): unknown {
8009
+ return resolveStepArgs(value, vars, warnMissingVar)
8010
+ }
8011
+
8012
+ /** The variables handed to an Action step, substituted and kept to primitives. */
8013
+ function substituteVariables(
8014
+ raw: Record<string, string | number | boolean>,
8015
+ vars: Record<string, unknown>,
8016
+ ): Record<string, string | number | boolean> {
8017
+ const out: Record<string, string | number | boolean> = {}
8018
+ for (const [key, value] of Object.entries(raw)) {
8019
+ const resolved = substitute(value, vars)
8020
+ if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') {
8021
+ out[key] = resolved
8022
+ }
8023
+ }
8024
+ return out
8025
+ }
8026
+
8027
+ async function actuate(
8028
+ deviceId: number,
8029
+ cap: string,
8030
+ method: string,
8031
+ args: Record<string, unknown>,
8032
+ vars: Record<string, unknown>,
8033
+ ): Promise<void> {
8034
+ const substituted = substitute(args, vars)
8035
+ const resolved: Record<string, unknown> =
8036
+ substituted !== null && typeof substituted === 'object' ? { ...substituted } : {}
8037
+ // Forked-addon ctx.api namespaces are tRPC Proxies over a noop function
8038
+ // (typeof === 'function'), not plain objects \u2014 reject only null/undefined.
8039
+ const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
8040
+ const proc: unknown =
8041
+ client !== null && client !== undefined ? Reflect.get(client as object, method) : undefined
8042
+ const mutate: unknown =
8043
+ proc !== null && proc !== undefined ? Reflect.get(proc as object, 'mutate') : undefined
8044
+ if (typeof mutate !== 'function') {
8045
+ throw new Error('no such device method: ' + cap + '.' + method)
8046
+ }
8047
+ await mutate({ ...resolved, deviceId })
8048
+ }
8049
+
8050
+ /** The THEN half, through the ONE shared step runner: stops at the first
8051
+ * failure (continuing past a failed step executes an intent the operator
8052
+ * never expressed); every step is traced, the ones after a failure (or an
8053
+ * abort) are marked not-run. */
8054
+ async function runActions(
8055
+ vars: Record<string, unknown>,
8056
+ token: RunToken,
8057
+ ): Promise<{ steps: readonly AutomationStepTrace[]; failed: boolean }> {
8058
+ const sleep = (ms: number): Promise<void> => abortableSleep(ms, token)
8059
+ const steps: RunnableStep[] = [
8060
+ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
8061
+ ]
8062
+ return runSteps(steps, {
8063
+ pathPrefix: 'actions',
8064
+ isAborted: () => token.aborted,
8065
+ abortReason: 'aborted by a restart',
8066
+ now: () => Date.now(),
8067
+ onFailure: (failure) => {
8068
+ ctx.logger.warn('action failed \u2014 stopping the sequence', {
8069
+ ...(failure.deviceId === undefined ? {} : { tags: { deviceId: failure.deviceId } }),
8070
+ step: failure.index,
8071
+ error: failure.message,
8072
+ })
8073
+ },
8074
+ })
8075
+ }
8076
+
8077
+ function deviceTags(trigger: Record<string, unknown>): { tags?: { deviceId: number } } {
8078
+ const deviceId = trigger['deviceId']
8079
+ return typeof deviceId === 'number' ? { tags: { deviceId } } : {}
8080
+ }
8081
+
8082
+ function newRunId(at: number): string {
8083
+ runCounter += 1
8084
+ return at.toString(36) + '-' + runCounter.toString(36)
8085
+ }
8086
+
8087
+ /** A trigger that never became a run (rejected, or dropped by the run mode). */
8088
+ function skipTrace(
8089
+ trigger: Record<string, unknown>,
8090
+ reason: string,
8091
+ outcome: AutomationRunTrace['outcome'],
8092
+ verdict: AutomationRunVerdict | undefined,
8093
+ accepted: boolean,
8094
+ ): AutomationRunTrace {
8095
+ const now = Date.now()
8096
+ return {
8097
+ runId: newRunId(now),
8098
+ startedAt: now,
8099
+ endedAt: now,
8100
+ trigger: buildTriggerTrace(trigger, accepted ? 'pass' : 'fail', reason),
8101
+ conditions: [],
8102
+ ...(verdict === undefined ? {} : { verdict }),
8103
+ steps: [],
8104
+ outcome,
8105
+ repeat: 1,
8106
+ lastAt: now,
8107
+ }
8108
+ }
8109
+
8110
+ /** One admitted run: conditions (memory only), the cooldown, the actions. */
8111
+ async function execute(
8112
+ trigger: Record<string, unknown>,
8113
+ reason: string,
8114
+ admission: AutomationRunVerdict,
8115
+ ): Promise<void> {
8116
+ const token: RunToken = { aborted: false }
8117
+ runningTokens.add(token)
8118
+ const startedAt = Date.now()
8119
+ const runId = newRunId(startedAt)
8120
+ let verdict = admission
8121
+ let outcome: AutomationRunTrace['outcome'] = 'ran'
8122
+ let nodes: AutomationConditionTraceNode[] = []
8123
+ let steps: AutomationStepTrace[] = []
8124
+ try {
8125
+ const evaluation = evaluateConditionTree(CONDITIONS, mirror, Date.now())
8126
+ nodes = [...evaluation.nodes]
8127
+ for (const node of nodes) {
8128
+ if (node.kind === 'condition' && node.observed === null && node.deviceId !== undefined) {
8129
+ ctx.logger.warn('condition leaf value unknown \u2014 treated as false', {
8130
+ tags: { deviceId: node.deviceId },
8131
+ path: node.path,
8132
+ cap: node.cap,
8133
+ fieldPath: node.fieldPath,
8134
+ })
8135
+ }
8136
+ }
8137
+ if (!evaluation.pass) {
8138
+ outcome = 'skipped-conditions'
8139
+ ctx.logger.debug('automation did not run \u2014 conditions were false', { trigger, ...deviceTags(trigger) })
8140
+ return
8141
+ }
8142
+ const letThrough = stampUnknown && RUN_MODE.mode === 'cooldown'
8143
+ const cooldown = letThrough
8144
+ ? { ok: true, bypassed: true, reason: 'cooldown stamp unreadable at start \u2014 run let through' }
8145
+ : gate.cooldown({
8146
+ manual: trigger['kind'] === 'manual',
8147
+ now: Date.now(),
8148
+ lastRunStartedAt,
8149
+ })
8150
+ if (!cooldown.ok) {
8151
+ outcome = 'skipped-cooldown'
8152
+ verdict = { mode: RUN_MODE, decision: 'cooldown', reason: cooldown.reason }
8153
+ ctx.logger.info('automation did not run \u2014 ' + cooldown.reason, deviceTags(trigger))
8154
+ return
8155
+ }
8156
+ if (cooldown.bypassed) verdict = { mode: RUN_MODE, decision: 'bypass', reason: cooldown.reason }
8157
+ // Stamped when the actions START, and reported to the hub before they run,
8158
+ // so a restart mid-run cannot fire again inside the window.
8159
+ lastRunStartedAt = startedAt
8160
+ stampUnknown = false
8161
+ await send({ runStartedAt: startedAt })
8162
+ ctx.logger.info('automation running', { trigger, ...deviceTags(trigger) })
8163
+ const vars: Record<string, unknown> = { ...trigger }
8164
+ const result = await runActions(vars, token)
8165
+ steps = result.steps
8166
+ outcome = token.aborted ? 'aborted' : result.failed ? 'failed' : 'ran'
8167
+ } catch (err: unknown) {
8168
+ outcome = 'failed'
8169
+ ctx.logger.warn('automation run failed', { error: err instanceof Error ? err.message : String(err), ...deviceTags(trigger) })
8170
+ } finally {
8171
+ runningTokens.delete(token)
8172
+ const endedAt = Date.now()
8173
+ record({
8174
+ runId,
8175
+ startedAt,
8176
+ endedAt,
8177
+ trigger: buildTriggerTrace(trigger, 'pass', reason),
8178
+ conditions: nodes,
8179
+ verdict,
8180
+ steps,
8181
+ outcome,
8182
+ repeat: 1,
8183
+ lastAt: endedAt,
8184
+ })
8185
+ if (gate.release()) {
8186
+ const next = queue.shift()
8187
+ if (next !== undefined && !stopped) {
8188
+ void execute(next.trigger, next.reason, {
8189
+ mode: RUN_MODE,
8190
+ decision: 'queued',
8191
+ reason: 'queued, then ran after waiting ' + (Date.now() - next.queuedAt) + ' ms',
8192
+ })
8193
+ }
8194
+ }
8195
+ }
8196
+ }
8197
+
8198
+ /** Every trigger lands here. \`accepted\` = the trigger itself was satisfied
8199
+ * (a rejected detection event is only traced). */
8200
+ async function run(
8201
+ trigger: Record<string, unknown>,
8202
+ reason = 'fired',
8203
+ accepted = true,
8204
+ ): Promise<void> {
8205
+ await ready
8206
+ if (stopped) return
8207
+ if (!accepted) {
8208
+ record(skipTrace(trigger, reason, 'skipped-trigger', undefined, false))
8209
+ return
8210
+ }
8211
+ const admission = gate.admit()
8212
+ const verdict: AutomationRunVerdict = { mode: RUN_MODE, decision: admission.decision, reason: admission.reason }
8213
+ switch (admission.decision) {
8214
+ case 'drop':
8215
+ ctx.logger.info('automation trigger dropped \u2014 ' + admission.reason, deviceTags(trigger))
8216
+ record(skipTrace(trigger, reason, 'skipped-overlap', verdict, true))
8217
+ return
8218
+ case 'queued':
8219
+ ctx.logger.info('automation trigger queued \u2014 ' + admission.reason, deviceTags(trigger))
8220
+ queue.push({ trigger, reason, queuedAt: Date.now() })
8221
+ return
8222
+ case 'restart':
8223
+ ctx.logger.info('automation restarting \u2014 ' + admission.reason, deviceTags(trigger))
8224
+ for (const running of runningTokens) {
8225
+ running.aborted = true
8226
+ running.wake?.()
8227
+ }
8228
+ break
8229
+ case 'run':
8230
+ break
8231
+ }
8232
+ await execute(trigger, reason, verdict)
8233
+ }
8234
+
8235
+ const ready: Promise<void> = (async () => {
8236
+ await loadState()
8237
+ await refreshMirror()
8238
+ })()
8239
+
8240
+ if (CONDITIONS !== undefined) {
8241
+ disposers.push(
8242
+ ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
8243
+ const data = event.data
8244
+ const deviceId = data['deviceId']
8245
+ const capName = data['capName']
8246
+ if (typeof deviceId === 'number' && typeof capName === 'string') {
8247
+ mirror.apply(deviceId, capName, data['slice'])
8248
+ }
8249
+ }),
8250
+ )
8251
+ const mirrorReconcile = setInterval(() => { void refreshMirror() }, RECONCILE_MS)
8252
+ disposers.push(() => clearInterval(mirrorReconcile))
8253
+ }
8254
+
8255
+ ${recipe.triggers.map((t2, i) => emitTrigger(t2, i)).join("\n\n")}
8256
+
8257
+ return () => {
8258
+ stopped = true
8259
+ queue.length = 0
8260
+ flushSkip()
8261
+ for (const running of runningTokens) {
8262
+ running.aborted = true
8263
+ running.wake?.()
8264
+ }
8265
+ for (const dispose of disposers) dispose()
8266
+ }
8267
+ }
8268
+ `;
8269
+ }
7356
8270
  var MACRO_LABELS = [
7357
8271
  {
7358
8272
  id: "person",
@@ -7628,6 +8542,16 @@ var ActionRunChangedSchema = z4.object({
7628
8542
  lastRunAt: z4.number().nullable(),
7629
8543
  lastError: z4.string().nullable()
7630
8544
  });
8545
+ function baseUnitForQuantity(quantity) {
8546
+ return baseUnitOf(quantity);
8547
+ }
8548
+ function unitsForQuantity(quantity) {
8549
+ const base = baseUnitForQuantity(quantity);
8550
+ if (base === void 0) return [];
8551
+ const dimension = unitDimension(base);
8552
+ if (dimension === void 0) return [base];
8553
+ return [base, ...unitsForDimension(dimension).filter((unit) => unit !== base)];
8554
+ }
7631
8555
  var UnitWarningSchema = z4.object({
7632
8556
  /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
7633
8557
  path: z4.string(),
@@ -7637,6 +8561,260 @@ var UnitWarningSchema = z4.object({
7637
8561
  assumedUnit: z4.string(),
7638
8562
  message: z4.string()
7639
8563
  });
8564
+ function objectInput(schema) {
8565
+ const inner = unwrapSchema(schema);
8566
+ return inner instanceof z4.ZodObject ? inner : void 0;
8567
+ }
8568
+ function capUnitResolver(defs) {
8569
+ const byName = new Map(defs.map((def) => [def.name, def]));
8570
+ return {
8571
+ fieldQuantity(cap, fieldPath) {
8572
+ const status = byName.get(cap)?.status;
8573
+ if (status === void 0) return void 0;
8574
+ return [...enumerateSchemaFields(status.schema), ...status.itemArray === void 0 ? [] : enumerateItemArrayFields(status.itemArray)].find((f) => f.path === fieldPath)?.quantity;
8575
+ },
8576
+ paramMeta(cap, method2, arg) {
8577
+ const input = byName.get(cap)?.methods[method2]?.input;
8578
+ if (input === void 0) return void 0;
8579
+ const field = objectInput(input)?.shape[arg];
8580
+ return field === void 0 ? void 0 : paramMetaOf(field);
8581
+ }
8582
+ };
8583
+ }
8584
+ function pickedUnit(raw) {
8585
+ return normalizeUnit(raw) ?? raw;
8586
+ }
8587
+ function hasUnitChoice(quantity) {
8588
+ return unitsForQuantity(quantity).length > 1;
8589
+ }
8590
+ function bareWarning(input) {
8591
+ return {
8592
+ ...input,
8593
+ message: `${input.field}: the bare number ${input.value} was taken as ${input.assumedUnit} \u2014 send { value, unit } to state the unit`
8594
+ };
8595
+ }
8596
+ function checkPickedUnit(unit, target) {
8597
+ convertQuantityInput({
8598
+ value: 1,
8599
+ unit,
8600
+ target
8601
+ });
8602
+ }
8603
+ function normalizeLeaf(leaf, resolver, path2) {
8604
+ const quantity = resolver.fieldQuantity(leaf.cap, leaf.fieldPath);
8605
+ const base = quantity === void 0 ? void 0 : baseUnitForQuantity(quantity);
8606
+ const wantsUnit = isQuantityValue(leaf.value) || leaf.unit !== void 0;
8607
+ if (base === void 0 || quantity === void 0) {
8608
+ if (wantsUnit) throw new QuantityUnitError("no-quantity", `${leaf.cap}.${leaf.fieldPath} is not a quantity \u2014 it takes no unit`);
8609
+ return {
8610
+ leaf,
8611
+ warnings: []
8612
+ };
8613
+ }
8614
+ if (isQuantityValue(leaf.value)) {
8615
+ const converted = convertQuantityInput({
8616
+ value: leaf.value.value,
8617
+ unit: leaf.value.unit,
8618
+ target: base
8619
+ });
8620
+ return {
8621
+ leaf: {
8622
+ ...leaf,
8623
+ value: converted,
8624
+ unit: pickedUnit(leaf.value.unit)
8625
+ },
8626
+ warnings: []
8627
+ };
8628
+ }
8629
+ if (typeof leaf.value !== "number") return {
8630
+ leaf,
8631
+ warnings: []
8632
+ };
8633
+ if (leaf.unit !== void 0) {
8634
+ checkPickedUnit(leaf.unit, base);
8635
+ return {
8636
+ leaf,
8637
+ warnings: []
8638
+ };
8639
+ }
8640
+ if (!hasUnitChoice(quantity)) return {
8641
+ leaf,
8642
+ warnings: []
8643
+ };
8644
+ return {
8645
+ leaf,
8646
+ warnings: [bareWarning({
8647
+ path: path2,
8648
+ deviceId: leaf.deviceId,
8649
+ field: leaf.fieldPath,
8650
+ value: leaf.value,
8651
+ assumedUnit: base
8652
+ })]
8653
+ };
8654
+ }
8655
+ function normalizeConditionUnits(condition, resolver, path2 = "conditions") {
8656
+ switch (condition.kind) {
8657
+ case "condition": {
8658
+ const { leaf, warnings } = normalizeLeaf(condition, resolver, path2);
8659
+ return {
8660
+ value: leaf,
8661
+ warnings
8662
+ };
8663
+ }
8664
+ case "not": {
8665
+ const inner = normalizeConditionUnits(condition.child, resolver, `${path2}.child`);
8666
+ return {
8667
+ value: {
8668
+ ...condition,
8669
+ child: inner.value
8670
+ },
8671
+ warnings: inner.warnings
8672
+ };
8673
+ }
8674
+ case "all":
8675
+ case "any": {
8676
+ const children = condition.children.map((child, i) => normalizeConditionUnits(child, resolver, `${path2}.children.${i}`));
8677
+ return {
8678
+ value: {
8679
+ ...condition,
8680
+ children: children.map((c) => c.value)
8681
+ },
8682
+ warnings: children.flatMap((c) => c.warnings)
8683
+ };
8684
+ }
8685
+ default:
8686
+ return {
8687
+ value: condition,
8688
+ warnings: []
8689
+ };
8690
+ }
8691
+ }
8692
+ function normalizeCapStep(step, resolver, path2) {
8693
+ const deviceId = typeof step.deviceId === "number" ? step.deviceId : void 0;
8694
+ const args = { ...step.args };
8695
+ const argUnits = { ...step.argUnits };
8696
+ const warnings = [];
8697
+ for (const [arg, picked] of Object.entries(step.argUnits ?? {})) {
8698
+ const meta = resolver.paramMeta(step.cap, step.method, arg);
8699
+ if (meta === void 0) throw new QuantityUnitError("no-quantity", `${step.cap}.${step.method}(${arg}) is not a quantity \u2014 it takes no unit`);
8700
+ checkPickedUnit(picked, meta.unit);
8701
+ }
8702
+ for (const [arg, given] of Object.entries(step.args ?? {})) {
8703
+ const meta = resolver.paramMeta(step.cap, step.method, arg);
8704
+ if (meta === void 0) continue;
8705
+ if (isQuantityValue(given)) {
8706
+ args[arg] = convertQuantityInput({
8707
+ value: given.value,
8708
+ unit: given.unit,
8709
+ target: meta.unit
8710
+ });
8711
+ argUnits[arg] = pickedUnit(given.unit);
8712
+ } else if (typeof given === "number" && argUnits[arg] === void 0 && hasUnitChoice(meta.quantity)) warnings.push(bareWarning({
8713
+ path: `${path2}.args.${arg}`,
8714
+ ...deviceId === void 0 ? {} : { deviceId },
8715
+ field: arg,
8716
+ value: given,
8717
+ assumedUnit: meta.unit
8718
+ }));
8719
+ }
8720
+ const { argUnits: _dropped, ...rest } = step;
8721
+ return {
8722
+ value: {
8723
+ ...rest,
8724
+ ...step.args === void 0 ? {} : { args },
8725
+ ...Object.keys(argUnits).length > 0 ? { argUnits } : {}
8726
+ },
8727
+ warnings
8728
+ };
8729
+ }
8730
+ function normalizeActionUnits(actions, resolver, path2 = "steps") {
8731
+ const out = [];
8732
+ const warnings = [];
8733
+ actions.forEach((action, i) => {
8734
+ if (action.kind !== "cap") {
8735
+ out.push(action);
8736
+ return;
8737
+ }
8738
+ const normalized = normalizeCapStep(action, resolver, `${path2}.${i}`);
8739
+ out.push(normalized.value);
8740
+ warnings.push(...normalized.warnings);
8741
+ });
8742
+ return {
8743
+ value: out,
8744
+ warnings
8745
+ };
8746
+ }
8747
+ function normalizeRecipeUnits(recipe, resolver) {
8748
+ const conditions = recipe.conditions === void 0 ? void 0 : normalizeConditionUnits(recipe.conditions, resolver, "conditions");
8749
+ const actions = normalizeActionUnits(recipe.actions, resolver, "actions");
8750
+ return {
8751
+ value: {
8752
+ ...recipe,
8753
+ ...conditions === void 0 ? {} : { conditions: conditions.value },
8754
+ actions: actions.value
8755
+ },
8756
+ warnings: [...conditions?.warnings ?? [], ...actions.warnings]
8757
+ };
8758
+ }
8759
+ function stampCondition(condition, resolver) {
8760
+ switch (condition.kind) {
8761
+ case "condition": {
8762
+ if (typeof condition.value !== "number" || condition.unit !== void 0) return condition;
8763
+ const quantity = resolver.fieldQuantity(condition.cap, condition.fieldPath);
8764
+ if (quantity === void 0 || unitsForQuantity(quantity).length < 2) return condition;
8765
+ const base = baseUnitForQuantity(quantity);
8766
+ return base === void 0 ? condition : {
8767
+ ...condition,
8768
+ unit: base
8769
+ };
8770
+ }
8771
+ case "not":
8772
+ return {
8773
+ ...condition,
8774
+ child: stampCondition(condition.child, resolver)
8775
+ };
8776
+ case "all":
8777
+ case "any":
8778
+ return {
8779
+ ...condition,
8780
+ children: condition.children.map((child) => stampCondition(child, resolver))
8781
+ };
8782
+ default:
8783
+ return condition;
8784
+ }
8785
+ }
8786
+ function stampAction(action, resolver) {
8787
+ if (action.kind !== "cap" || action.args === void 0) return action;
8788
+ const argUnits = { ...action.argUnits };
8789
+ for (const [arg, value] of Object.entries(action.args)) {
8790
+ if (typeof value !== "number" || argUnits[arg] !== void 0) continue;
8791
+ const meta = resolver.paramMeta(action.cap, action.method, arg);
8792
+ if (meta !== void 0 && unitsForQuantity(meta.quantity).length > 1) argUnits[arg] = meta.unit;
8793
+ }
8794
+ return Object.keys(argUnits).length === 0 ? action : {
8795
+ ...action,
8796
+ argUnits
8797
+ };
8798
+ }
8799
+ function prepareRecipeUnits(recipe, resolver) {
8800
+ const stamped = {
8801
+ ...recipe,
8802
+ ...recipe.conditions === void 0 ? {} : { conditions: stampCondition(recipe.conditions, resolver) },
8803
+ actions: recipe.actions.map((action) => stampAction(action, resolver))
8804
+ };
8805
+ try {
8806
+ return {
8807
+ ok: true,
8808
+ recipe: normalizeRecipeUnits(stamped, resolver).value
8809
+ };
8810
+ } catch (error) {
8811
+ if (error instanceof QuantityUnitError) return {
8812
+ ok: false,
8813
+ error: error.message
8814
+ };
8815
+ throw error;
8816
+ }
8817
+ }
7640
8818
  var AttachmentMediaTypeSchema = z4.enum([
7641
8819
  "image",
7642
8820
  "video",
@@ -9669,8 +10847,21 @@ var BrokerProviderInfoSchema = z4.object({
9669
10847
  * (`{ sessionId }`) drops the session. The dialog that edits a broker
9670
10848
  * offers it as the default path; the fields the session fills are
9671
10849
  * marked `filledBySignIn` in the kind's settings schema.
10850
+ *
10851
+ * `methods` lists the ways the session can sign in, recommended first:
10852
+ * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
10853
+ * settings) answering `{ sessionId, userCode, verificationUrl,
10854
+ * expiresAt }` — the operator types `userCode` at `verificationUrl` on
10855
+ * any device, and the same `status` / `commit` / `cancel` routes follow
10856
+ * (status then carries `method`, `userCode`, `verificationUrl`,
10857
+ * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
10858
+ * `'proxy'` is the `{basePath}/start` flow above. Absent means
10859
+ * `['proxy']`.
9672
10860
  */
9673
- signIn: z4.object({ basePath: z4.string().min(1) }).optional()
10861
+ signIn: z4.object({
10862
+ basePath: z4.string().min(1),
10863
+ methods: z4.array(z4.enum(["device-code", "proxy"])).optional()
10864
+ }).optional()
9674
10865
  }))
9675
10866
  });
9676
10867
  var ListInputSchema = z4.object({
@@ -58990,6 +60181,71 @@ var powerMeterCustomize = {
58990
60181
  powerTarget("current", "A")
58991
60182
  ]
58992
60183
  };
60184
+ var powerMeterCapability = {
60185
+ name: "power-meter",
60186
+ scope: "device",
60187
+ deviceNative: true,
60188
+ mode: "singleton",
60189
+ deviceTypes: POWER_METER_DEVICE_TYPES,
60190
+ methods: {},
60191
+ status: {
60192
+ schema: PowerMeterStatusSchema,
60193
+ kind: "push"
60194
+ },
60195
+ runtimeState: PowerMeterStatusSchema,
60196
+ /**
60197
+ * Runtime-state durability: **restored** — as `numeric-sensor`; `energy` is an accumulator whose restored value is the baseline.
60198
+ *
60199
+ * See `RuntimeStateDurability`. Enforced by
60200
+ * `scripts/check-runtime-state-durability.ts`.
60201
+ */
60202
+ durability: "restored",
60203
+ /**
60204
+ * History (D850): recorded series. Gauges are floored at 30 s; `energy` is the one monotonic meter (counter).
60205
+ */
60206
+ history: { fields: {
60207
+ power: {
60208
+ kind: "gauge",
60209
+ unit: "W",
60210
+ deadband: 1,
60211
+ minIntervalMs: 3e4
60212
+ },
60213
+ energy: {
60214
+ kind: "counter",
60215
+ unit: "kWh",
60216
+ deadband: 0.01,
60217
+ minIntervalMs: 6e4
60218
+ },
60219
+ voltage: {
60220
+ kind: "gauge",
60221
+ unit: "V",
60222
+ deadband: 1,
60223
+ minIntervalMs: 3e4
60224
+ },
60225
+ current: {
60226
+ kind: "gauge",
60227
+ unit: "A",
60228
+ deadband: 0.05,
60229
+ minIntervalMs: 3e4
60230
+ },
60231
+ apparentPower: {
60232
+ kind: "gauge",
60233
+ unit: "VA",
60234
+ deadband: 1,
60235
+ minIntervalMs: 3e4
60236
+ },
60237
+ powerFactor: {
60238
+ kind: "gauge",
60239
+ unit: "ratio",
60240
+ deadband: 0.01,
60241
+ minIntervalMs: 6e4
60242
+ }
60243
+ } },
60244
+ /** Clock fields: written, but excluded from the compare that decides
60245
+ * whether persisting is worth a SQLite commit. */
60246
+ volatileStateFields: ["lastFetchedAt"],
60247
+ customize: powerMeterCustomize
60248
+ };
58993
60249
  var GpsLocationSchema = z4.object({
58994
60250
  /** Latitude in decimal degrees, -90..90. */
58995
60251
  latitude: z4.number().min(-90).max(90).meta(readingMeta("angle")),
@@ -59653,6 +60909,27 @@ var runningCustomize = {
59653
60909
  advancedExpression: true
59654
60910
  }]
59655
60911
  };
60912
+ var runningStateCapability = {
60913
+ name: "running-state",
60914
+ scope: "device",
60915
+ deviceNative: true,
60916
+ mode: "singleton",
60917
+ deviceTypes: RUNNING_DEVICE_TYPES,
60918
+ methods: {},
60919
+ events: {
60920
+ onStarted: { data: RunningFlipEventDataSchema },
60921
+ onFinished: { data: RunningFlipEventDataSchema }
60922
+ },
60923
+ status: {
60924
+ schema: RunningStateStatusSchema,
60925
+ kind: "push"
60926
+ },
60927
+ runtimeState: RunningStateStatusSchema,
60928
+ durability: "restored",
60929
+ history: { fields: { running: { kind: "boolean" } } },
60930
+ volatileStateFields: ["lastFetchedAt"],
60931
+ customize: runningCustomize
60932
+ };
59656
60933
  var rebootCapability = {
59657
60934
  name: "reboot",
59658
60935
  scope: "device",
@@ -59661,7 +60938,8 @@ var rebootCapability = {
59661
60938
  deviceTypes: [
59662
60939
  DeviceType.Camera,
59663
60940
  DeviceType.Sensor,
59664
- DeviceType.Switch
60941
+ DeviceType.Switch,
60942
+ DeviceType.Gateway
59665
60943
  ],
59666
60944
  methods: { reboot: method(z4.object({ deviceId: z4.number().int().nonnegative() }), z4.object({ success: z4.literal(true) }), {
59667
60945
  kind: "mutation",
@@ -64187,7 +65465,8 @@ var CAP_CUSTOMIZE_CATALOG = {
64187
65465
  DeviceType.PetFountain,
64188
65466
  DeviceType.Dishwasher,
64189
65467
  DeviceType.Washer,
64190
- DeviceType.Dryer
65468
+ DeviceType.Dryer,
65469
+ DeviceType.Gateway
64191
65470
  ],
64192
65471
  apply: "add",
64193
65472
  vocabularyCap: "consumables",
@@ -64688,7 +65967,8 @@ var DEVICE_TYPE_CONTROL_KIND = {
64688
65967
  [DeviceType.PetFountain]: "pet-fountain",
64689
65968
  [DeviceType.Dishwasher]: "dishwasher",
64690
65969
  [DeviceType.Washer]: "dummy",
64691
- [DeviceType.Dryer]: "dummy"
65970
+ [DeviceType.Dryer]: "dummy",
65971
+ [DeviceType.Gateway]: "dummy"
64692
65972
  };
64693
65973
  var DEVICE_TYPE_VALUES = new Set(Object.values(DeviceType));
64694
65974
  var METHOD_ACCESS_MAP = Object.freeze({
@@ -78750,7 +80030,8 @@ var DEVICE_TYPE_DO_CAPS = {
78750
80030
  [DeviceType.PetFountain]: ["pet-fountain"],
78751
80031
  [DeviceType.Dishwasher]: ["dishwasher"],
78752
80032
  [DeviceType.Washer]: [],
78753
- [DeviceType.Dryer]: []
80033
+ [DeviceType.Dryer]: [],
80034
+ [DeviceType.Gateway]: []
78754
80035
  };
78755
80036
  var NUMERIC_ABOVE = t("When {quantity} of {device} rises above {threshold}", "Quando {quantity} di {device} sale sopra {threshold}");
78756
80037
  var NUMERIC_BELOW = t("When {quantity} of {device} falls below {threshold}", "Quando {quantity} di {device} scende sotto {threshold}");
@@ -78817,6 +80098,201 @@ var APPLIANCE_EXPORT = {
78817
80098
  matter: withheld(MATTER_REASON)
78818
80099
  };
78819
80100
  var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
80101
+ var ALL_CAPABILITY_DEFINITIONS = [
80102
+ accessoriesCapability,
80103
+ actionSourceCapability,
80104
+ actionsCapability,
80105
+ addonPagesCapability,
80106
+ addonPagesSourceCapability,
80107
+ addonRoutesCapability,
80108
+ addonSettingsCapability,
80109
+ addonWidgetsCapability,
80110
+ addonWidgetsSourceCapability,
80111
+ addonsCapability,
80112
+ adminUiCapability,
80113
+ airQualitySensorCapability,
80114
+ alarmPanelCapability,
80115
+ alertsCapability,
80116
+ ambientLightSensorCapability,
80117
+ audioAnalysisCapability,
80118
+ audioAnalyzerCapability,
80119
+ audioCodecCapability,
80120
+ audioMetricsCapability,
80121
+ authProviderCapability,
80122
+ automationControlCapability,
80123
+ backupCapability,
80124
+ batteryCapability,
80125
+ binaryCapability,
80126
+ brightnessCapability,
80127
+ brokerCapability,
80128
+ buttonCapability,
80129
+ calendarSourceCapability,
80130
+ calendarsCapability,
80131
+ cameraCredentialsCapability,
80132
+ cameraGridLayoutCapability,
80133
+ cameraPipelineConfigCapability,
80134
+ cameraStreamsCapability,
80135
+ carbonMonoxideCapability,
80136
+ climateControlCapability,
80137
+ colorCapability,
80138
+ composerCapability,
80139
+ connectionTestCapability,
80140
+ connectivityCapability,
80141
+ consumablesCapability,
80142
+ contactCapability,
80143
+ controlCapability,
80144
+ coreBlocksCapability,
80145
+ coverCapability,
80146
+ customModelRegistryCapability,
80147
+ dataStoreProviderCapability,
80148
+ databaseSnapshotCapability,
80149
+ dayNightCapability,
80150
+ decoderCapability,
80151
+ detectionPipelineCapability,
80152
+ deviceAdminLinkCapability,
80153
+ deviceAdoptionCapability,
80154
+ deviceCredentialsCapability,
80155
+ deviceDiscoveryCapability,
80156
+ deviceExportCapability,
80157
+ deviceExtensionCapability,
80158
+ deviceHistoryCapability,
80159
+ deviceManagerCapability,
80160
+ deviceOpsCapability,
80161
+ deviceProviderCapability,
80162
+ deviceRemovalHookCapability,
80163
+ deviceStateCapability,
80164
+ deviceStatusCapability,
80165
+ dishwasherCapability,
80166
+ displayPaletteCapability,
80167
+ doorbellCapability,
80168
+ embeddingEncoderCapability,
80169
+ enumSensorCapability,
80170
+ eventEmitterCapability,
80171
+ faceGalleryCapability,
80172
+ failureContributionCapability,
80173
+ fanControlCapability,
80174
+ featureProbeCapability,
80175
+ filesystemBrowseCapability,
80176
+ floodCapability,
80177
+ gasCapability,
80178
+ groupActiveCapability,
80179
+ groupCommandsCapability,
80180
+ groupStatusCapability,
80181
+ heatingValveCapability,
80182
+ humidifierCapability,
80183
+ humiditySensorCapability,
80184
+ imageCapability,
80185
+ imageSettingsCapability,
80186
+ integrationsCapability,
80187
+ intercomCapability,
80188
+ irTransmitterCapability,
80189
+ keychainCapability,
80190
+ keychainAdminCapability,
80191
+ lawnMowerControlCapability,
80192
+ llmCapability,
80193
+ llmRuntimeCapability,
80194
+ loadContributionCapability,
80195
+ localNetworkCapability,
80196
+ lockControlCapability,
80197
+ logChannelsCapability,
80198
+ logDestinationCapability,
80199
+ loginMethodCapability,
80200
+ mediaPlayerCapability,
80201
+ meshNetworkCapability,
80202
+ meterCapability,
80203
+ metricsProviderCapability,
80204
+ modelConvertCapability,
80205
+ modelDistributorCapability,
80206
+ motionCapability,
80207
+ motionDetectionCapability,
80208
+ motionTriggerCapability,
80209
+ motionZonesCapability,
80210
+ mqttBrokerCapability,
80211
+ nativeObjectDetectionCapability,
80212
+ navigationCapability,
80213
+ networkAccessCapability,
80214
+ networkLinkCapability,
80215
+ networkQualityCapability,
80216
+ nodeCertificatesCapability,
80217
+ nodeRemovalHookCapability,
80218
+ nodesCapability,
80219
+ notificationOutputCapability,
80220
+ notificationRulesCapability,
80221
+ notifierCapability,
80222
+ numericSensorCapability,
80223
+ oauthIntegrationCapability,
80224
+ oidcConnectionsCapability,
80225
+ osdCapability,
80226
+ osdManagerCapability,
80227
+ petFeederCapability,
80228
+ petFountainCapability,
80229
+ pipelineAnalyticsCapability,
80230
+ pipelineExecutorCapability,
80231
+ pipelineOrchestratorCapability,
80232
+ pipelineRunnerCapability,
80233
+ plateGalleryCapability,
80234
+ platformProbeCapability,
80235
+ powerMeterCapability,
80236
+ presenceCapability,
80237
+ pressureSensorCapability,
80238
+ privacyMaskCapability,
80239
+ ptzCapability,
80240
+ ptzAutotrackCapability,
80241
+ rebootCapability,
80242
+ recordEntriesCapability,
80243
+ recordingCapability,
80244
+ recordingArchiveCapability,
80245
+ recordingExportCapability,
80246
+ recordingOnboardCapability,
80247
+ recordingSignalCapability,
80248
+ recordsCapability,
80249
+ remoteCapability,
80250
+ remoteLayoutComponentsCapability,
80251
+ runningStateCapability,
80252
+ sceneMonitorCapability,
80253
+ scriptRunnerCapability,
80254
+ serverManagementCapability,
80255
+ settingsStoreCapability,
80256
+ smokeCapability,
80257
+ smtpProviderCapability,
80258
+ snapshotCapability,
80259
+ ssoBridgeCapability,
80260
+ storageCapability,
80261
+ storageAdminCapability,
80262
+ storageEvictableCapability,
80263
+ storageMigrationCapability,
80264
+ storageOccupancyCapability,
80265
+ storageProviderCapability,
80266
+ streamBrokerCapability,
80267
+ streamCatalogCapability,
80268
+ streamParamsCapability,
80269
+ streamSignalsCapability,
80270
+ switchCapability,
80271
+ systemCapability,
80272
+ tamperCapability,
80273
+ temperatureSensorCapability,
80274
+ templateCapability,
80275
+ terminalSessionCapability,
80276
+ toastCapability,
80277
+ trainingCapability,
80278
+ trainingProviderCapability,
80279
+ turnProviderCapability,
80280
+ updateCapability,
80281
+ userManagementCapability,
80282
+ userPasskeysCapability,
80283
+ vacuumControlCapability,
80284
+ valveCapability,
80285
+ vectorStoreCapability,
80286
+ vibrationCapability,
80287
+ videoclipsCapability,
80288
+ viewerUiCapability,
80289
+ waterHeaterCapability,
80290
+ weatherCapability,
80291
+ webrtcSessionCapability,
80292
+ zoneAnalyticsCapability,
80293
+ zoneRulesCapability,
80294
+ zonesCapability
80295
+ ];
78820
80296
  var METHOD_DEVICE_SELECTORS = Object.freeze({
78821
80297
  "accessories.getStatus": [{
78822
80298
  name: "deviceId",
@@ -83863,9 +85339,21 @@ function describeHub(hub) {
83863
85339
  }
83864
85340
 
83865
85341
  export {
85342
+ isReadingQuantity,
83866
85343
  FFMPEG_PATH_ENV,
85344
+ AutomationRecipeSchema,
85345
+ validateRecipeBounds,
85346
+ generateAutomationBlock,
85347
+ MACRO_LABELS,
85348
+ unitsForQuantity,
85349
+ capUnitResolver,
85350
+ prepareRecipeUnits,
83867
85351
  buildViewerLink,
85352
+ AlarmArmModeSchema,
85353
+ NcRuleInputSchema,
85354
+ ZoneKindEnum,
83868
85355
  DETECTOR_PERF,
85356
+ ALL_CAPABILITY_DEFINITIONS,
83869
85357
  classifyInferenceHardware,
83870
85358
  acceleratorKindOf,
83871
85359
  validateManualHubUrl,