@camstack/types 1.2.433 → 1.2.434
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 +7 -0
- package/dist/addon.js +2 -2
- package/dist/addon.mjs +2 -2
- package/dist/automation/index.d.ts +3 -1
- package/dist/automation/recipe.d.ts +33 -3
- package/dist/automation/stationary-trigger.d.ts +48 -0
- package/dist/automation/trace.d.ts +4 -1
- package/dist/capabilities/actions.cap.d.ts +1 -0
- package/dist/capabilities/core-blocks.cap.d.ts +156 -0
- package/dist/enums/event-category.d.ts +1 -1
- package/dist/enums.js +1 -1
- package/dist/enums.mjs +1 -1
- package/dist/{event-category-hdMR7CTW.mjs → event-category-DmZDPDf_.mjs} +1 -1
- package/dist/{event-category-BhzwWQCH.js → event-category-E17zaHjN.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +492 -316
- package/dist/index.mjs +492 -317
- package/dist/interfaces/event-bus.d.ts +21 -0
- package/dist/{sleep-5zRztaHa.js → sleep-CO61AfwW.js} +1 -1
- package/dist/{sleep-BHjNez61.mjs → sleep-DFaVPxfm.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_sleep = require("./sleep-
|
|
3
|
-
const require_event_category = require("./event-category-
|
|
2
|
+
const require_sleep = require("./sleep-CO61AfwW.js");
|
|
3
|
+
const require_event_category = require("./event-category-E17zaHjN.js");
|
|
4
4
|
const require_composition = require("./composition-CpAc0YfQ.js");
|
|
5
5
|
const require_node_color = require("./node-color.js");
|
|
6
6
|
const require_err_msg = require("./err-msg-U7lLUMNz.js");
|
|
@@ -8074,10 +8074,10 @@ var ExpressionSourceSchema = zod.z.object({
|
|
|
8074
8074
|
*
|
|
8075
8075
|
* Three vocabularies, each salvaged rather than invented:
|
|
8076
8076
|
*
|
|
8077
|
-
* - **Triggers** — the only genuinely new surface.
|
|
8077
|
+
* - **Triggers** — the only genuinely new surface. Seven members, none of which
|
|
8078
8078
|
* touches notification delivery. The NC keeps its four *pipeline* triggers;
|
|
8079
8079
|
* these are device/time triggers and the two sets do not overlap.
|
|
8080
|
-
* - **Conditions** — `ALL`/`ANY`/`NOT` over
|
|
8080
|
+
* - **Conditions** — `ALL`/`ANY`/`NOT` over three leaf kinds, bounded. The
|
|
8081
8081
|
* expression leaf is the bounded engine at `../expression/`, which is where
|
|
8082
8082
|
* arithmetic between two device fields lives and the only place it lives.
|
|
8083
8083
|
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
@@ -8124,6 +8124,14 @@ var AutomationConditionLeafSchema = zod.z.object({
|
|
|
8124
8124
|
zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
|
|
8125
8125
|
])
|
|
8126
8126
|
});
|
|
8127
|
+
var AutomationConditionStationaryPresentSchema = zod.z.object({
|
|
8128
|
+
kind: zod.z.literal("stationary-present"),
|
|
8129
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
8130
|
+
/** Class names; a macro class matches every raw class that maps to it. */
|
|
8131
|
+
classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
|
|
8132
|
+
/** The parked object must be in at least one of these zones. */
|
|
8133
|
+
zones: zod.z.array(zod.z.string().min(1)).min(1).optional()
|
|
8134
|
+
});
|
|
8127
8135
|
/**
|
|
8128
8136
|
* The expression leaf, declared as a plain object rather than an intersection
|
|
8129
8137
|
* with {@link ExpressionSourceSchema}: a discriminated union has to be able to
|
|
@@ -8157,7 +8165,8 @@ var AutomationConditionSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind"
|
|
|
8157
8165
|
child: AutomationConditionSchema
|
|
8158
8166
|
}),
|
|
8159
8167
|
AutomationConditionLeafSchema,
|
|
8160
|
-
AutomationConditionExpressionSchema
|
|
8168
|
+
AutomationConditionExpressionSchema,
|
|
8169
|
+
AutomationConditionStationaryPresentSchema
|
|
8161
8170
|
]));
|
|
8162
8171
|
/**
|
|
8163
8172
|
* What starts a run.
|
|
@@ -8211,6 +8220,17 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
|
|
|
8211
8220
|
/** Best-so-far confidence the track must reach. */
|
|
8212
8221
|
minScore: zod.z.number().min(0).max(1).optional()
|
|
8213
8222
|
}),
|
|
8223
|
+
zod.z.object({
|
|
8224
|
+
kind: zod.z.literal("stationary"),
|
|
8225
|
+
deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
|
|
8226
|
+
/** Class names; a macro class matches like the detection trigger. */
|
|
8227
|
+
classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
|
|
8228
|
+
/** The parked object must be in at least one of these zones. */
|
|
8229
|
+
zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
|
|
8230
|
+
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
8231
|
+
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
8232
|
+
phase: zod.z.enum(["appeared", "departed"])
|
|
8233
|
+
}),
|
|
8214
8234
|
zod.z.object({
|
|
8215
8235
|
kind: zod.z.literal("interval"),
|
|
8216
8236
|
minutes: zod.z.number().int().min(1).max(1440)
|
|
@@ -8323,9 +8343,12 @@ var AutomationRecipeSchema = zod.z.object({
|
|
|
8323
8343
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
8324
8344
|
keepTraces: zod.z.number().int().min(1).max(100).optional()
|
|
8325
8345
|
});
|
|
8346
|
+
function isLeaf(cond) {
|
|
8347
|
+
return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
|
|
8348
|
+
}
|
|
8326
8349
|
/** Depth of a tree, root container = 1. A bare leaf is depth 1. */
|
|
8327
8350
|
function conditionDepth(cond) {
|
|
8328
|
-
if (cond
|
|
8351
|
+
if (isLeaf(cond)) return 1;
|
|
8329
8352
|
if (cond.kind === "not") return 1 + conditionDepth(cond.child);
|
|
8330
8353
|
const children = cond.children;
|
|
8331
8354
|
if (children.length === 0) return 1;
|
|
@@ -8338,14 +8361,14 @@ function conditionDepth(cond) {
|
|
|
8338
8361
|
}
|
|
8339
8362
|
/** Leaves only — containers do not count toward the leaf budget. */
|
|
8340
8363
|
function countConditionLeaves(cond) {
|
|
8341
|
-
if (cond
|
|
8364
|
+
if (isLeaf(cond)) return 1;
|
|
8342
8365
|
if (cond.kind === "not") return countConditionLeaves(cond.child);
|
|
8343
8366
|
let total = 0;
|
|
8344
8367
|
for (const child of cond.children) total += countConditionLeaves(child);
|
|
8345
8368
|
return total;
|
|
8346
8369
|
}
|
|
8347
8370
|
function findEmptyContainer(cond) {
|
|
8348
|
-
if (cond
|
|
8371
|
+
if (isLeaf(cond)) return false;
|
|
8349
8372
|
if (cond.kind === "not") return findEmptyContainer(cond.child);
|
|
8350
8373
|
if (cond.children.length === 0) return true;
|
|
8351
8374
|
return cond.children.some(findEmptyContainer);
|
|
@@ -8504,6 +8527,32 @@ function emitTrigger(trigger, index) {
|
|
|
8504
8527
|
},
|
|
8505
8528
|
),
|
|
8506
8529
|
)
|
|
8530
|
+
}`;
|
|
8531
|
+
case "stationary": return ` // trigger ${index}: stationary — a parked object appeared / departed. Stateless:
|
|
8532
|
+
// the registry emits one event per transition. A dropped event only misses a fire;
|
|
8533
|
+
// the stationary-present CONDITION reads the registry, not this event.
|
|
8534
|
+
{
|
|
8535
|
+
const trigger = ${j(trigger)} as const
|
|
8536
|
+
disposers.push(
|
|
8537
|
+
ctx.eventBus.subscribe(
|
|
8538
|
+
{ category: EventCategory.PipelineAnalyticsStationaryChanged },
|
|
8539
|
+
(event) => {
|
|
8540
|
+
const decision = evaluateStationaryTrigger(trigger, event.data)
|
|
8541
|
+
if (decision.kind === 'ignore') return
|
|
8542
|
+
if (decision.kind === 'reject') {
|
|
8543
|
+
ctx.logger.debug('stationary trigger rejected an event', {
|
|
8544
|
+
tags: { deviceId: decision.vars.deviceId },
|
|
8545
|
+
reason: decision.reason,
|
|
8546
|
+
})
|
|
8547
|
+
}
|
|
8548
|
+
void run(
|
|
8549
|
+
{ kind: 'stationary', at: Date.now(), ...decision.vars },
|
|
8550
|
+
decision.reason,
|
|
8551
|
+
decision.kind === 'fire',
|
|
8552
|
+
)
|
|
8553
|
+
},
|
|
8554
|
+
),
|
|
8555
|
+
)
|
|
8507
8556
|
}`;
|
|
8508
8557
|
}
|
|
8509
8558
|
}
|
|
@@ -8570,6 +8619,7 @@ import {
|
|
|
8570
8619
|
collectMirrorSources,
|
|
8571
8620
|
coreBlockIdFromAddonId,
|
|
8572
8621
|
evaluateConditionTree,
|
|
8622
|
+
evaluateStationaryTrigger,
|
|
8573
8623
|
isSkippedTrace,
|
|
8574
8624
|
mergeSkipTraces,
|
|
8575
8625
|
resolveDeviceTarget,
|
|
@@ -9064,310 +9114,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
9064
9114
|
`;
|
|
9065
9115
|
}
|
|
9066
9116
|
//#endregion
|
|
9067
|
-
//#region src/automation/condition-eval.ts
|
|
9068
|
-
/**
|
|
9069
|
-
* Evaluating an automation's condition tree against the ConditionMirror.
|
|
9070
|
-
*
|
|
9071
|
-
* D49: a trigger is a one-shot event, so its gate must never hang on a read
|
|
9072
|
-
* that can fail. Every field a condition leaf names is therefore kept in
|
|
9073
|
-
* memory — filled at start, updated off device state-changed events, reconciled
|
|
9074
|
-
* on a timer — and evaluation reads that memory ONLY. A value the mirror does
|
|
9075
|
-
* not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
|
|
9076
|
-
* `pass`. A failed refresh changes nothing: the previous value stays.
|
|
9077
|
-
*
|
|
9078
|
-
* The evaluator returns a node per tree position (containers, leaves, and the
|
|
9079
|
-
* ones a short-circuit never reached) with a stable path and a human reason, so
|
|
9080
|
-
* the editor can paint each block.
|
|
9081
|
-
*/
|
|
9082
|
-
function collectInto(cond, out) {
|
|
9083
|
-
switch (cond.kind) {
|
|
9084
|
-
case "all":
|
|
9085
|
-
case "any":
|
|
9086
|
-
for (const child of cond.children) collectInto(child, out);
|
|
9087
|
-
return;
|
|
9088
|
-
case "not":
|
|
9089
|
-
collectInto(cond.child, out);
|
|
9090
|
-
return;
|
|
9091
|
-
case "condition":
|
|
9092
|
-
out.push({
|
|
9093
|
-
kind: "device",
|
|
9094
|
-
deviceId: cond.deviceId,
|
|
9095
|
-
cap: cond.cap
|
|
9096
|
-
});
|
|
9097
|
-
return;
|
|
9098
|
-
case "expression":
|
|
9099
|
-
for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
|
|
9100
|
-
kind: "stable",
|
|
9101
|
-
stableId: binding.sourceStableId,
|
|
9102
|
-
cap: binding.cap
|
|
9103
|
-
});
|
|
9104
|
-
return;
|
|
9105
|
-
}
|
|
9106
|
-
}
|
|
9107
|
-
/** Every (device, cap) a tree reads, deduplicated. */
|
|
9108
|
-
function collectMirrorSources(cond) {
|
|
9109
|
-
if (cond === void 0) return [];
|
|
9110
|
-
const all = [];
|
|
9111
|
-
collectInto(cond, all);
|
|
9112
|
-
const seen = /* @__PURE__ */ new Set();
|
|
9113
|
-
return all.filter((s) => {
|
|
9114
|
-
const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
|
|
9115
|
-
if (seen.has(key)) return false;
|
|
9116
|
-
seen.add(key);
|
|
9117
|
-
return true;
|
|
9118
|
-
});
|
|
9119
|
-
}
|
|
9120
|
-
function sliceKey(deviceId, cap) {
|
|
9121
|
-
return `${deviceId}|${cap}`;
|
|
9122
|
-
}
|
|
9123
|
-
function pick$1(slice, fieldPath) {
|
|
9124
|
-
let cursor = slice;
|
|
9125
|
-
for (const part of fieldPath.split(".")) {
|
|
9126
|
-
if (cursor === null || typeof cursor !== "object") return void 0;
|
|
9127
|
-
cursor = Reflect.get(cursor, part);
|
|
9128
|
-
}
|
|
9129
|
-
return cursor;
|
|
9130
|
-
}
|
|
9131
|
-
function isSliceObject(value) {
|
|
9132
|
-
return typeof value === "object" && value !== null;
|
|
9133
|
-
}
|
|
9134
|
-
var ConditionMirror = class {
|
|
9135
|
-
sources;
|
|
9136
|
-
slices = /* @__PURE__ */ new Map();
|
|
9137
|
-
stableIds = /* @__PURE__ */ new Map();
|
|
9138
|
-
watched;
|
|
9139
|
-
constructor(sources) {
|
|
9140
|
-
this.sources = sources;
|
|
9141
|
-
this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
|
|
9142
|
-
}
|
|
9143
|
-
/** A `device.state-changed` event. Slices nobody reads are ignored. */
|
|
9144
|
-
apply(deviceId, cap, slice) {
|
|
9145
|
-
const key = sliceKey(deviceId, cap);
|
|
9146
|
-
if (!isSliceObject(slice)) return;
|
|
9147
|
-
if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
|
|
9148
|
-
this.slices.set(key, slice);
|
|
9149
|
-
}
|
|
9150
|
-
isStableTarget(deviceId, cap) {
|
|
9151
|
-
for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
|
|
9152
|
-
return false;
|
|
9153
|
-
}
|
|
9154
|
-
read(deviceId, cap, fieldPath) {
|
|
9155
|
-
const slice = this.slices.get(sliceKey(deviceId, cap));
|
|
9156
|
-
return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
|
|
9157
|
-
}
|
|
9158
|
-
readStable(stableId, cap, fieldPath) {
|
|
9159
|
-
const deviceId = this.stableIds.get(stableId);
|
|
9160
|
-
return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
|
|
9161
|
-
}
|
|
9162
|
-
/** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
|
|
9163
|
-
async refresh(fetchers) {
|
|
9164
|
-
let updated = 0;
|
|
9165
|
-
let kept = 0;
|
|
9166
|
-
if (this.sources.some((s) => s.kind === "stable")) try {
|
|
9167
|
-
this.stableIds = await fetchers.fetchStableIds();
|
|
9168
|
-
} catch {}
|
|
9169
|
-
const targets = /* @__PURE__ */ new Map();
|
|
9170
|
-
for (const s of this.sources) {
|
|
9171
|
-
const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
|
|
9172
|
-
if (deviceId === void 0) {
|
|
9173
|
-
kept += 1;
|
|
9174
|
-
continue;
|
|
9175
|
-
}
|
|
9176
|
-
targets.set(sliceKey(deviceId, s.cap), {
|
|
9177
|
-
deviceId,
|
|
9178
|
-
cap: s.cap
|
|
9179
|
-
});
|
|
9180
|
-
}
|
|
9181
|
-
for (const [key, target] of targets) try {
|
|
9182
|
-
const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
|
|
9183
|
-
if (isSliceObject(slice)) {
|
|
9184
|
-
this.slices.set(key, slice);
|
|
9185
|
-
updated += 1;
|
|
9186
|
-
} else kept += 1;
|
|
9187
|
-
} catch {
|
|
9188
|
-
kept += 1;
|
|
9189
|
-
}
|
|
9190
|
-
return {
|
|
9191
|
-
updated,
|
|
9192
|
-
kept
|
|
9193
|
-
};
|
|
9194
|
-
}
|
|
9195
|
-
};
|
|
9196
|
-
var OPERATOR_SYMBOL = {
|
|
9197
|
-
eq: "=",
|
|
9198
|
-
ne: "≠",
|
|
9199
|
-
gt: ">",
|
|
9200
|
-
gte: "≥",
|
|
9201
|
-
lt: "<",
|
|
9202
|
-
lte: "≤",
|
|
9203
|
-
contains: "contains",
|
|
9204
|
-
in: "in"
|
|
9205
|
-
};
|
|
9206
|
-
function compare(actual, operator, expected) {
|
|
9207
|
-
switch (operator) {
|
|
9208
|
-
case "eq": return actual === expected;
|
|
9209
|
-
case "ne": return actual !== expected;
|
|
9210
|
-
case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
|
9211
|
-
case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
|
9212
|
-
case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
|
9213
|
-
case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
|
9214
|
-
case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
|
|
9215
|
-
case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
|
|
9216
|
-
}
|
|
9217
|
-
}
|
|
9218
|
-
function toTraceValue(value) {
|
|
9219
|
-
if (value === void 0 || value === null) return null;
|
|
9220
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
9221
|
-
if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
|
|
9222
|
-
return JSON.stringify(value);
|
|
9223
|
-
}
|
|
9224
|
-
function show(value) {
|
|
9225
|
-
return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
|
|
9226
|
-
}
|
|
9227
|
-
function markSkipped(cond, path, why, out) {
|
|
9228
|
-
out.push({
|
|
9229
|
-
path,
|
|
9230
|
-
kind: cond.kind,
|
|
9231
|
-
outcome: "not-evaluated",
|
|
9232
|
-
reason: why
|
|
9233
|
-
});
|
|
9234
|
-
if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
|
|
9235
|
-
markSkipped(c, `${path}.${i}`, why, out);
|
|
9236
|
-
});
|
|
9237
|
-
else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
|
|
9238
|
-
}
|
|
9239
|
-
function evalNode(cond, path, reader, now, out) {
|
|
9240
|
-
switch (cond.kind) {
|
|
9241
|
-
case "condition": {
|
|
9242
|
-
const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
|
|
9243
|
-
const base = {
|
|
9244
|
-
path,
|
|
9245
|
-
kind: cond.kind,
|
|
9246
|
-
deviceId: cond.deviceId,
|
|
9247
|
-
cap: cond.cap,
|
|
9248
|
-
fieldPath: cond.fieldPath,
|
|
9249
|
-
operator: cond.operator,
|
|
9250
|
-
expected: toTraceValue(cond.value),
|
|
9251
|
-
observed: toTraceValue(observed)
|
|
9252
|
-
};
|
|
9253
|
-
if (observed === void 0) {
|
|
9254
|
-
out.push({
|
|
9255
|
-
...base,
|
|
9256
|
-
outcome: "fail",
|
|
9257
|
-
reason: `${cond.fieldPath} value unknown → fail`
|
|
9258
|
-
});
|
|
9259
|
-
return false;
|
|
9260
|
-
}
|
|
9261
|
-
const ok = compare(observed, cond.operator, cond.value);
|
|
9262
|
-
out.push({
|
|
9263
|
-
...base,
|
|
9264
|
-
outcome: ok ? "pass" : "fail",
|
|
9265
|
-
reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
|
|
9266
|
-
});
|
|
9267
|
-
return ok;
|
|
9268
|
-
}
|
|
9269
|
-
case "expression": {
|
|
9270
|
-
const values = {};
|
|
9271
|
-
for (const [alias, binding] of Object.entries(cond.bindings)) {
|
|
9272
|
-
if (binding.kind === "literal") {
|
|
9273
|
-
values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
|
|
9274
|
-
continue;
|
|
9275
|
-
}
|
|
9276
|
-
const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
|
|
9277
|
-
if (raw === void 0) {
|
|
9278
|
-
out.push({
|
|
9279
|
-
path,
|
|
9280
|
-
kind: cond.kind,
|
|
9281
|
-
outcome: "fail",
|
|
9282
|
-
reason: `${alias} value unknown → fail`
|
|
9283
|
-
});
|
|
9284
|
-
return false;
|
|
9285
|
-
}
|
|
9286
|
-
values[alias] = require_composition.toExpressionValue(raw) ?? null;
|
|
9287
|
-
}
|
|
9288
|
-
const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
|
|
9289
|
-
if (!result.ok) {
|
|
9290
|
-
out.push({
|
|
9291
|
-
path,
|
|
9292
|
-
kind: cond.kind,
|
|
9293
|
-
outcome: "fail",
|
|
9294
|
-
reason: `${cond.expr}: ${result.error} → fail`
|
|
9295
|
-
});
|
|
9296
|
-
return false;
|
|
9297
|
-
}
|
|
9298
|
-
const ok = result.value === true;
|
|
9299
|
-
out.push({
|
|
9300
|
-
path,
|
|
9301
|
-
kind: cond.kind,
|
|
9302
|
-
outcome: ok ? "pass" : "fail",
|
|
9303
|
-
reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
|
|
9304
|
-
});
|
|
9305
|
-
return ok;
|
|
9306
|
-
}
|
|
9307
|
-
case "not": {
|
|
9308
|
-
const at = out.length;
|
|
9309
|
-
out.push({
|
|
9310
|
-
path,
|
|
9311
|
-
kind: "not",
|
|
9312
|
-
outcome: "not-evaluated",
|
|
9313
|
-
reason: ""
|
|
9314
|
-
});
|
|
9315
|
-
const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
|
|
9316
|
-
const ok = !inner;
|
|
9317
|
-
out.splice(at, 1, {
|
|
9318
|
-
path,
|
|
9319
|
-
kind: "not",
|
|
9320
|
-
outcome: ok ? "pass" : "fail",
|
|
9321
|
-
reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
|
|
9322
|
-
});
|
|
9323
|
-
return ok;
|
|
9324
|
-
}
|
|
9325
|
-
case "all":
|
|
9326
|
-
case "any": {
|
|
9327
|
-
const isAll = cond.kind === "all";
|
|
9328
|
-
const at = out.length;
|
|
9329
|
-
out.push({
|
|
9330
|
-
path,
|
|
9331
|
-
kind: cond.kind,
|
|
9332
|
-
outcome: "not-evaluated",
|
|
9333
|
-
reason: ""
|
|
9334
|
-
});
|
|
9335
|
-
let decided = null;
|
|
9336
|
-
for (let i = 0; i < cond.children.length; i += 1) {
|
|
9337
|
-
const child = cond.children[i];
|
|
9338
|
-
if (child === void 0) continue;
|
|
9339
|
-
if (decided !== null) {
|
|
9340
|
-
markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
|
|
9341
|
-
continue;
|
|
9342
|
-
}
|
|
9343
|
-
const ok = evalNode(child, `${path}.${i}`, reader, now, out);
|
|
9344
|
-
if (isAll ? !ok : ok) decided = i;
|
|
9345
|
-
}
|
|
9346
|
-
const ok = isAll ? decided === null : decided !== null;
|
|
9347
|
-
const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
|
|
9348
|
-
out.splice(at, 1, {
|
|
9349
|
-
path,
|
|
9350
|
-
kind: cond.kind,
|
|
9351
|
-
outcome: ok ? "pass" : "fail",
|
|
9352
|
-
reason
|
|
9353
|
-
});
|
|
9354
|
-
return ok;
|
|
9355
|
-
}
|
|
9356
|
-
}
|
|
9357
|
-
}
|
|
9358
|
-
/** No condition tree is a pass with no nodes. */
|
|
9359
|
-
function evaluateConditionTree(cond, reader, now) {
|
|
9360
|
-
if (cond === void 0) return {
|
|
9361
|
-
pass: true,
|
|
9362
|
-
nodes: []
|
|
9363
|
-
};
|
|
9364
|
-
const nodes = [];
|
|
9365
|
-
return {
|
|
9366
|
-
pass: evalNode(cond, "conditions", reader, now, nodes),
|
|
9367
|
-
nodes
|
|
9368
|
-
};
|
|
9369
|
-
}
|
|
9370
|
-
//#endregion
|
|
9371
9117
|
//#region src/catalogs/coco-classmap.ts
|
|
9372
9118
|
var COCO_80_LABELS = [
|
|
9373
9119
|
{
|
|
@@ -9770,7 +9516,7 @@ function detectionClassMatches(wanted, actual) {
|
|
|
9770
9516
|
if (w === a) return true;
|
|
9771
9517
|
return MACRO_NAMES.has(w) && macroOf(a) === w;
|
|
9772
9518
|
}
|
|
9773
|
-
function isPhase(value) {
|
|
9519
|
+
function isPhase$1(value) {
|
|
9774
9520
|
return value === "start" || value === "update" || value === "end";
|
|
9775
9521
|
}
|
|
9776
9522
|
var DetectionTriggerFilter = class {
|
|
@@ -9785,7 +9531,7 @@ var DetectionTriggerFilter = class {
|
|
|
9785
9531
|
}
|
|
9786
9532
|
evaluate(event, now) {
|
|
9787
9533
|
const { deviceId, trackId, phase } = event;
|
|
9788
|
-
if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase(phase)) return {
|
|
9534
|
+
if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase$1(phase)) return {
|
|
9789
9535
|
kind: "ignore",
|
|
9790
9536
|
why: "malformed track-lifecycle event"
|
|
9791
9537
|
};
|
|
@@ -9856,6 +9602,433 @@ var DetectionTriggerFilter = class {
|
|
|
9856
9602
|
}
|
|
9857
9603
|
};
|
|
9858
9604
|
//#endregion
|
|
9605
|
+
//#region src/automation/condition-eval.ts
|
|
9606
|
+
/**
|
|
9607
|
+
* Evaluating an automation's condition tree against the ConditionMirror.
|
|
9608
|
+
*
|
|
9609
|
+
* D49: a trigger is a one-shot event, so its gate must never hang on a read
|
|
9610
|
+
* that can fail. Every field a condition leaf names is therefore kept in
|
|
9611
|
+
* memory — filled at start, updated off device state-changed events, reconciled
|
|
9612
|
+
* on a timer — and evaluation reads that memory ONLY. A value the mirror does
|
|
9613
|
+
* not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
|
|
9614
|
+
* `pass`. A failed refresh changes nothing: the previous value stays.
|
|
9615
|
+
*
|
|
9616
|
+
* The evaluator returns a node per tree position (containers, leaves, and the
|
|
9617
|
+
* ones a short-circuit never reached) with a stable path and a human reason, so
|
|
9618
|
+
* the editor can paint each block.
|
|
9619
|
+
*/
|
|
9620
|
+
/** The cap whose runtime-state slice IS the occupancy snapshot, parked objects included. */
|
|
9621
|
+
var ZONE_ANALYTICS_CAP = "zone-analytics";
|
|
9622
|
+
function collectInto(cond, out) {
|
|
9623
|
+
switch (cond.kind) {
|
|
9624
|
+
case "all":
|
|
9625
|
+
case "any":
|
|
9626
|
+
for (const child of cond.children) collectInto(child, out);
|
|
9627
|
+
return;
|
|
9628
|
+
case "not":
|
|
9629
|
+
collectInto(cond.child, out);
|
|
9630
|
+
return;
|
|
9631
|
+
case "condition":
|
|
9632
|
+
out.push({
|
|
9633
|
+
kind: "device",
|
|
9634
|
+
deviceId: cond.deviceId,
|
|
9635
|
+
cap: cond.cap
|
|
9636
|
+
});
|
|
9637
|
+
return;
|
|
9638
|
+
case "stationary-present":
|
|
9639
|
+
out.push({
|
|
9640
|
+
kind: "device",
|
|
9641
|
+
deviceId: cond.deviceId,
|
|
9642
|
+
cap: ZONE_ANALYTICS_CAP
|
|
9643
|
+
});
|
|
9644
|
+
return;
|
|
9645
|
+
case "expression":
|
|
9646
|
+
for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
|
|
9647
|
+
kind: "stable",
|
|
9648
|
+
stableId: binding.sourceStableId,
|
|
9649
|
+
cap: binding.cap
|
|
9650
|
+
});
|
|
9651
|
+
return;
|
|
9652
|
+
}
|
|
9653
|
+
}
|
|
9654
|
+
/** Every (device, cap) a tree reads, deduplicated. */
|
|
9655
|
+
function collectMirrorSources(cond) {
|
|
9656
|
+
if (cond === void 0) return [];
|
|
9657
|
+
const all = [];
|
|
9658
|
+
collectInto(cond, all);
|
|
9659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9660
|
+
return all.filter((s) => {
|
|
9661
|
+
const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
|
|
9662
|
+
if (seen.has(key)) return false;
|
|
9663
|
+
seen.add(key);
|
|
9664
|
+
return true;
|
|
9665
|
+
});
|
|
9666
|
+
}
|
|
9667
|
+
function sliceKey(deviceId, cap) {
|
|
9668
|
+
return `${deviceId}|${cap}`;
|
|
9669
|
+
}
|
|
9670
|
+
function pick$1(slice, fieldPath) {
|
|
9671
|
+
let cursor = slice;
|
|
9672
|
+
for (const part of fieldPath.split(".")) {
|
|
9673
|
+
if (cursor === null || typeof cursor !== "object") return void 0;
|
|
9674
|
+
cursor = Reflect.get(cursor, part);
|
|
9675
|
+
}
|
|
9676
|
+
return cursor;
|
|
9677
|
+
}
|
|
9678
|
+
function isSliceObject(value) {
|
|
9679
|
+
return typeof value === "object" && value !== null;
|
|
9680
|
+
}
|
|
9681
|
+
var ConditionMirror = class {
|
|
9682
|
+
sources;
|
|
9683
|
+
slices = /* @__PURE__ */ new Map();
|
|
9684
|
+
stableIds = /* @__PURE__ */ new Map();
|
|
9685
|
+
watched;
|
|
9686
|
+
constructor(sources) {
|
|
9687
|
+
this.sources = sources;
|
|
9688
|
+
this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
|
|
9689
|
+
}
|
|
9690
|
+
/** A `device.state-changed` event. Slices nobody reads are ignored. */
|
|
9691
|
+
apply(deviceId, cap, slice) {
|
|
9692
|
+
const key = sliceKey(deviceId, cap);
|
|
9693
|
+
if (!isSliceObject(slice)) return;
|
|
9694
|
+
if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
|
|
9695
|
+
this.slices.set(key, slice);
|
|
9696
|
+
}
|
|
9697
|
+
isStableTarget(deviceId, cap) {
|
|
9698
|
+
for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
|
|
9699
|
+
return false;
|
|
9700
|
+
}
|
|
9701
|
+
read(deviceId, cap, fieldPath) {
|
|
9702
|
+
const slice = this.slices.get(sliceKey(deviceId, cap));
|
|
9703
|
+
return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
|
|
9704
|
+
}
|
|
9705
|
+
readStable(stableId, cap, fieldPath) {
|
|
9706
|
+
const deviceId = this.stableIds.get(stableId);
|
|
9707
|
+
return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
|
|
9708
|
+
}
|
|
9709
|
+
/** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
|
|
9710
|
+
async refresh(fetchers) {
|
|
9711
|
+
let updated = 0;
|
|
9712
|
+
let kept = 0;
|
|
9713
|
+
if (this.sources.some((s) => s.kind === "stable")) try {
|
|
9714
|
+
this.stableIds = await fetchers.fetchStableIds();
|
|
9715
|
+
} catch {}
|
|
9716
|
+
const targets = /* @__PURE__ */ new Map();
|
|
9717
|
+
for (const s of this.sources) {
|
|
9718
|
+
const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
|
|
9719
|
+
if (deviceId === void 0) {
|
|
9720
|
+
kept += 1;
|
|
9721
|
+
continue;
|
|
9722
|
+
}
|
|
9723
|
+
targets.set(sliceKey(deviceId, s.cap), {
|
|
9724
|
+
deviceId,
|
|
9725
|
+
cap: s.cap
|
|
9726
|
+
});
|
|
9727
|
+
}
|
|
9728
|
+
for (const [key, target] of targets) try {
|
|
9729
|
+
const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
|
|
9730
|
+
if (isSliceObject(slice)) {
|
|
9731
|
+
this.slices.set(key, slice);
|
|
9732
|
+
updated += 1;
|
|
9733
|
+
} else kept += 1;
|
|
9734
|
+
} catch {
|
|
9735
|
+
kept += 1;
|
|
9736
|
+
}
|
|
9737
|
+
return {
|
|
9738
|
+
updated,
|
|
9739
|
+
kept
|
|
9740
|
+
};
|
|
9741
|
+
}
|
|
9742
|
+
};
|
|
9743
|
+
var OPERATOR_SYMBOL = {
|
|
9744
|
+
eq: "=",
|
|
9745
|
+
ne: "≠",
|
|
9746
|
+
gt: ">",
|
|
9747
|
+
gte: "≥",
|
|
9748
|
+
lt: "<",
|
|
9749
|
+
lte: "≤",
|
|
9750
|
+
contains: "contains",
|
|
9751
|
+
in: "in"
|
|
9752
|
+
};
|
|
9753
|
+
function compare(actual, operator, expected) {
|
|
9754
|
+
switch (operator) {
|
|
9755
|
+
case "eq": return actual === expected;
|
|
9756
|
+
case "ne": return actual !== expected;
|
|
9757
|
+
case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
|
9758
|
+
case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
|
9759
|
+
case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
|
9760
|
+
case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
|
9761
|
+
case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
|
|
9762
|
+
case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9765
|
+
function toTraceValue(value) {
|
|
9766
|
+
if (value === void 0 || value === null) return null;
|
|
9767
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
9768
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
|
|
9769
|
+
return JSON.stringify(value);
|
|
9770
|
+
}
|
|
9771
|
+
function show(value) {
|
|
9772
|
+
return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
|
|
9773
|
+
}
|
|
9774
|
+
function markSkipped(cond, path, why, out) {
|
|
9775
|
+
out.push({
|
|
9776
|
+
path,
|
|
9777
|
+
kind: cond.kind,
|
|
9778
|
+
outcome: "not-evaluated",
|
|
9779
|
+
reason: why
|
|
9780
|
+
});
|
|
9781
|
+
if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
|
|
9782
|
+
markSkipped(c, `${path}.${i}`, why, out);
|
|
9783
|
+
});
|
|
9784
|
+
else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
|
|
9785
|
+
}
|
|
9786
|
+
function parkedObjectsOf(value) {
|
|
9787
|
+
if (!Array.isArray(value)) return [];
|
|
9788
|
+
const rows = [];
|
|
9789
|
+
for (const item of value) {
|
|
9790
|
+
const id = Reflect.get(Object(item), "id");
|
|
9791
|
+
const className = Reflect.get(Object(item), "className");
|
|
9792
|
+
if (typeof id === "string" && typeof className === "string") rows.push({
|
|
9793
|
+
id,
|
|
9794
|
+
className
|
|
9795
|
+
});
|
|
9796
|
+
}
|
|
9797
|
+
return rows;
|
|
9798
|
+
}
|
|
9799
|
+
/** `zoneId`s whose `trackIds` list the parked object (`stationary:<id>`, as the snapshot builds it). */
|
|
9800
|
+
function zonesOfParked(zones, parkedId) {
|
|
9801
|
+
if (!Array.isArray(zones)) return [];
|
|
9802
|
+
const out = [];
|
|
9803
|
+
for (const zone of zones) {
|
|
9804
|
+
const zoneId = Reflect.get(Object(zone), "zoneId");
|
|
9805
|
+
const trackIds = Reflect.get(Object(zone), "trackIds");
|
|
9806
|
+
if (typeof zoneId === "string" && Array.isArray(trackIds) && trackIds.includes(`stationary:${parkedId}`)) out.push(zoneId);
|
|
9807
|
+
}
|
|
9808
|
+
return out;
|
|
9809
|
+
}
|
|
9810
|
+
/**
|
|
9811
|
+
* Is a matching parked object there NOW? Read off the mirrored zone-analytics
|
|
9812
|
+
* snapshot: `frame` is always present in a snapshot, so its absence means the
|
|
9813
|
+
* slice was never read (UNKNOWN, a fail that says so), while a snapshot without
|
|
9814
|
+
* `stationaryObjects` is a definite "nothing parked" (the provider omits an
|
|
9815
|
+
* empty list).
|
|
9816
|
+
*/
|
|
9817
|
+
function judgeStationaryPresent(cond, reader) {
|
|
9818
|
+
if (reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "frame") === void 0) return {
|
|
9819
|
+
pass: false,
|
|
9820
|
+
why: `device ${cond.deviceId} occupancy snapshot unknown`
|
|
9821
|
+
};
|
|
9822
|
+
const parked = parkedObjectsOf(reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "stationaryObjects"));
|
|
9823
|
+
if (parked.length === 0) return {
|
|
9824
|
+
pass: false,
|
|
9825
|
+
why: `no parked objects on device ${cond.deviceId}`
|
|
9826
|
+
};
|
|
9827
|
+
const zones = reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "zones");
|
|
9828
|
+
for (const obj of parked) {
|
|
9829
|
+
if (cond.classes !== void 0 && !cond.classes.some((c) => detectionClassMatches(c, obj.className))) continue;
|
|
9830
|
+
if (cond.zones !== void 0) {
|
|
9831
|
+
const inZones = zonesOfParked(zones, obj.id);
|
|
9832
|
+
if (!cond.zones.some((z) => inZones.includes(z))) continue;
|
|
9833
|
+
}
|
|
9834
|
+
return {
|
|
9835
|
+
pass: true,
|
|
9836
|
+
why: `parked ${obj.className} (${obj.id}) on device ${cond.deviceId}`
|
|
9837
|
+
};
|
|
9838
|
+
}
|
|
9839
|
+
return {
|
|
9840
|
+
pass: false,
|
|
9841
|
+
why: `${parked.length} parked on device ${cond.deviceId}, none matching the class/zone filter`
|
|
9842
|
+
};
|
|
9843
|
+
}
|
|
9844
|
+
function evalNode(cond, path, reader, now, out) {
|
|
9845
|
+
switch (cond.kind) {
|
|
9846
|
+
case "condition": {
|
|
9847
|
+
const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
|
|
9848
|
+
const base = {
|
|
9849
|
+
path,
|
|
9850
|
+
kind: cond.kind,
|
|
9851
|
+
deviceId: cond.deviceId,
|
|
9852
|
+
cap: cond.cap,
|
|
9853
|
+
fieldPath: cond.fieldPath,
|
|
9854
|
+
operator: cond.operator,
|
|
9855
|
+
expected: toTraceValue(cond.value),
|
|
9856
|
+
observed: toTraceValue(observed)
|
|
9857
|
+
};
|
|
9858
|
+
if (observed === void 0) {
|
|
9859
|
+
out.push({
|
|
9860
|
+
...base,
|
|
9861
|
+
outcome: "fail",
|
|
9862
|
+
reason: `${cond.fieldPath} value unknown → fail`
|
|
9863
|
+
});
|
|
9864
|
+
return false;
|
|
9865
|
+
}
|
|
9866
|
+
const ok = compare(observed, cond.operator, cond.value);
|
|
9867
|
+
out.push({
|
|
9868
|
+
...base,
|
|
9869
|
+
outcome: ok ? "pass" : "fail",
|
|
9870
|
+
reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
|
|
9871
|
+
});
|
|
9872
|
+
return ok;
|
|
9873
|
+
}
|
|
9874
|
+
case "stationary-present": {
|
|
9875
|
+
const verdict = judgeStationaryPresent(cond, reader);
|
|
9876
|
+
out.push({
|
|
9877
|
+
path,
|
|
9878
|
+
kind: cond.kind,
|
|
9879
|
+
outcome: verdict.pass ? "pass" : "fail",
|
|
9880
|
+
reason: `${verdict.why} → ${verdict.pass ? "pass" : "fail"}`,
|
|
9881
|
+
deviceId: cond.deviceId,
|
|
9882
|
+
cap: ZONE_ANALYTICS_CAP
|
|
9883
|
+
});
|
|
9884
|
+
return verdict.pass;
|
|
9885
|
+
}
|
|
9886
|
+
case "expression": {
|
|
9887
|
+
const values = {};
|
|
9888
|
+
for (const [alias, binding] of Object.entries(cond.bindings)) {
|
|
9889
|
+
if (binding.kind === "literal") {
|
|
9890
|
+
values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
|
|
9891
|
+
continue;
|
|
9892
|
+
}
|
|
9893
|
+
const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
|
|
9894
|
+
if (raw === void 0) {
|
|
9895
|
+
out.push({
|
|
9896
|
+
path,
|
|
9897
|
+
kind: cond.kind,
|
|
9898
|
+
outcome: "fail",
|
|
9899
|
+
reason: `${alias} value unknown → fail`
|
|
9900
|
+
});
|
|
9901
|
+
return false;
|
|
9902
|
+
}
|
|
9903
|
+
values[alias] = require_composition.toExpressionValue(raw) ?? null;
|
|
9904
|
+
}
|
|
9905
|
+
const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
|
|
9906
|
+
if (!result.ok) {
|
|
9907
|
+
out.push({
|
|
9908
|
+
path,
|
|
9909
|
+
kind: cond.kind,
|
|
9910
|
+
outcome: "fail",
|
|
9911
|
+
reason: `${cond.expr}: ${result.error} → fail`
|
|
9912
|
+
});
|
|
9913
|
+
return false;
|
|
9914
|
+
}
|
|
9915
|
+
const ok = result.value === true;
|
|
9916
|
+
out.push({
|
|
9917
|
+
path,
|
|
9918
|
+
kind: cond.kind,
|
|
9919
|
+
outcome: ok ? "pass" : "fail",
|
|
9920
|
+
reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
|
|
9921
|
+
});
|
|
9922
|
+
return ok;
|
|
9923
|
+
}
|
|
9924
|
+
case "not": {
|
|
9925
|
+
const at = out.length;
|
|
9926
|
+
out.push({
|
|
9927
|
+
path,
|
|
9928
|
+
kind: "not",
|
|
9929
|
+
outcome: "not-evaluated",
|
|
9930
|
+
reason: ""
|
|
9931
|
+
});
|
|
9932
|
+
const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
|
|
9933
|
+
const ok = !inner;
|
|
9934
|
+
out.splice(at, 1, {
|
|
9935
|
+
path,
|
|
9936
|
+
kind: "not",
|
|
9937
|
+
outcome: ok ? "pass" : "fail",
|
|
9938
|
+
reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
|
|
9939
|
+
});
|
|
9940
|
+
return ok;
|
|
9941
|
+
}
|
|
9942
|
+
case "all":
|
|
9943
|
+
case "any": {
|
|
9944
|
+
const isAll = cond.kind === "all";
|
|
9945
|
+
const at = out.length;
|
|
9946
|
+
out.push({
|
|
9947
|
+
path,
|
|
9948
|
+
kind: cond.kind,
|
|
9949
|
+
outcome: "not-evaluated",
|
|
9950
|
+
reason: ""
|
|
9951
|
+
});
|
|
9952
|
+
let decided = null;
|
|
9953
|
+
for (let i = 0; i < cond.children.length; i += 1) {
|
|
9954
|
+
const child = cond.children[i];
|
|
9955
|
+
if (child === void 0) continue;
|
|
9956
|
+
if (decided !== null) {
|
|
9957
|
+
markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
|
|
9958
|
+
continue;
|
|
9959
|
+
}
|
|
9960
|
+
const ok = evalNode(child, `${path}.${i}`, reader, now, out);
|
|
9961
|
+
if (isAll ? !ok : ok) decided = i;
|
|
9962
|
+
}
|
|
9963
|
+
const ok = isAll ? decided === null : decided !== null;
|
|
9964
|
+
const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
|
|
9965
|
+
out.splice(at, 1, {
|
|
9966
|
+
path,
|
|
9967
|
+
kind: cond.kind,
|
|
9968
|
+
outcome: ok ? "pass" : "fail",
|
|
9969
|
+
reason
|
|
9970
|
+
});
|
|
9971
|
+
return ok;
|
|
9972
|
+
}
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9975
|
+
/** No condition tree is a pass with no nodes. */
|
|
9976
|
+
function evaluateConditionTree(cond, reader, now) {
|
|
9977
|
+
if (cond === void 0) return {
|
|
9978
|
+
pass: true,
|
|
9979
|
+
nodes: []
|
|
9980
|
+
};
|
|
9981
|
+
const nodes = [];
|
|
9982
|
+
return {
|
|
9983
|
+
pass: evalNode(cond, "conditions", reader, now, nodes),
|
|
9984
|
+
nodes
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9987
|
+
//#endregion
|
|
9988
|
+
//#region src/automation/stationary-trigger.ts
|
|
9989
|
+
function isPhase(value) {
|
|
9990
|
+
return value === "appeared" || value === "departed";
|
|
9991
|
+
}
|
|
9992
|
+
function evaluateStationaryTrigger(trigger, event) {
|
|
9993
|
+
const { deviceId, entryId, phase } = event;
|
|
9994
|
+
if (typeof deviceId !== "number" || typeof entryId !== "string" || !isPhase(phase)) return {
|
|
9995
|
+
kind: "ignore",
|
|
9996
|
+
why: "malformed stationary-changed event"
|
|
9997
|
+
};
|
|
9998
|
+
if (!trigger.deviceIds.includes(deviceId)) return {
|
|
9999
|
+
kind: "ignore",
|
|
10000
|
+
why: `device ${deviceId} is not one of the trigger's devices`
|
|
10001
|
+
};
|
|
10002
|
+
if (phase !== trigger.phase) return {
|
|
10003
|
+
kind: "ignore",
|
|
10004
|
+
why: `phase ${phase}, the trigger wants ${trigger.phase}`
|
|
10005
|
+
};
|
|
10006
|
+
const className = typeof event.className === "string" ? event.className : "";
|
|
10007
|
+
const zones = Array.isArray(event.zones) ? event.zones.filter((z) => typeof z === "string") : [];
|
|
10008
|
+
const vars = {
|
|
10009
|
+
deviceId,
|
|
10010
|
+
className,
|
|
10011
|
+
zones,
|
|
10012
|
+
phase,
|
|
10013
|
+
entryId
|
|
10014
|
+
};
|
|
10015
|
+
if (trigger.classes !== void 0 && !trigger.classes.some((c) => detectionClassMatches(c, className))) return {
|
|
10016
|
+
kind: "reject",
|
|
10017
|
+
reason: `class ${className || "unknown"} not in [${trigger.classes.join(", ")}]`,
|
|
10018
|
+
vars
|
|
10019
|
+
};
|
|
10020
|
+
if (trigger.zones !== void 0 && !trigger.zones.some((z) => zones.includes(z))) return {
|
|
10021
|
+
kind: "reject",
|
|
10022
|
+
reason: `zone mismatch: in [${zones.join(", ")}], wanted [${trigger.zones.join(", ")}]`,
|
|
10023
|
+
vars
|
|
10024
|
+
};
|
|
10025
|
+
return {
|
|
10026
|
+
kind: "fire",
|
|
10027
|
+
reason: `${className} ${phase} on device ${deviceId}`,
|
|
10028
|
+
vars
|
|
10029
|
+
};
|
|
10030
|
+
}
|
|
10031
|
+
//#endregion
|
|
9859
10032
|
//#region src/automation/run-gate.ts
|
|
9860
10033
|
var MINUTE_MS$1 = 6e4;
|
|
9861
10034
|
function minutes(ms) {
|
|
@@ -10032,7 +10205,8 @@ var AutomationConditionTraceNodeSchema = zod.z.object({
|
|
|
10032
10205
|
"any",
|
|
10033
10206
|
"not",
|
|
10034
10207
|
"condition",
|
|
10035
|
-
"expression"
|
|
10208
|
+
"expression",
|
|
10209
|
+
"stationary-present"
|
|
10036
10210
|
]),
|
|
10037
10211
|
outcome: TraceNodeOutcomeSchema,
|
|
10038
10212
|
reason: zod.z.string(),
|
|
@@ -10159,7 +10333,7 @@ function appendTrace(ring, trace, keep) {
|
|
|
10159
10333
|
}
|
|
10160
10334
|
/**
|
|
10161
10335
|
* The trigger stage of a trace from the bag a trigger handed to `run()`:
|
|
10162
|
-
* primitives
|
|
10336
|
+
* primitives and short lists of them (a device-state `to` that is an object is left out), minus the
|
|
10163
10337
|
* bookkeeping keys `kind` and `at`.
|
|
10164
10338
|
*/
|
|
10165
10339
|
function buildTriggerTrace(trigger, outcome, reason) {
|
|
@@ -10167,6 +10341,7 @@ function buildTriggerTrace(trigger, outcome, reason) {
|
|
|
10167
10341
|
for (const [key, value] of Object.entries(trigger)) {
|
|
10168
10342
|
if (key === "kind" || key === "at") continue;
|
|
10169
10343
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
|
|
10344
|
+
else if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) vars[key] = value;
|
|
10170
10345
|
}
|
|
10171
10346
|
return {
|
|
10172
10347
|
kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
|
|
@@ -121821,6 +121996,7 @@ exports.evaluateRose = require_composition.evaluateRose;
|
|
|
121821
121996
|
exports.evaluateSensorEdge = require_composition.evaluateSensorEdge;
|
|
121822
121997
|
exports.evaluateSensorLevelEdge = require_composition.evaluateSensorLevelEdge;
|
|
121823
121998
|
exports.evaluateStatefulExpressionCall = require_composition.evaluateStatefulExpressionCall;
|
|
121999
|
+
exports.evaluateStationaryTrigger = evaluateStationaryTrigger;
|
|
121824
122000
|
exports.evaluateStatistic = require_composition.evaluateStatistic;
|
|
121825
122001
|
exports.evaluateZoneRules = evaluateZoneRules;
|
|
121826
122002
|
exports.event = require_sleep.event;
|