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.
- package/CHANGELOG.md +14 -0
- package/dist/{chunk-EMTOU2RA.js → chunk-MLWXP2SI.js} +1495 -7
- package/dist/cli.js +6 -6
- package/dist/{launcher-FMNINS4U.js → launcher-EACOVUIT.js} +193 -93
- package/dist/server-2GOCBDEY.js +8132 -0
- package/package.json +1 -1
- package/dist/server-LHBD6ATG.js +0 -2243
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// ../types/dist/err-msg-
|
|
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-
|
|
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({
|
|
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,
|