@camstack/types 1.2.432 → 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 +14 -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/client.js +35 -1
- package/dist/client.mjs +35 -1
- 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/i18n/humanize-id.d.ts +7 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +540 -318
- package/dist/index.mjs +540 -319
- 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",
|
|
@@ -113507,7 +113682,41 @@ function packagePickerItems(packages) {
|
|
|
113507
113682
|
return [...pinned, ...addons];
|
|
113508
113683
|
}
|
|
113509
113684
|
//#endregion
|
|
113685
|
+
//#region src/i18n/humanize-id.ts
|
|
113686
|
+
/**
|
|
113687
|
+
* Make a raw wire id readable when no dictionary knows it: `delivery_truck`
|
|
113688
|
+
* becomes "Delivery truck", `open-door` becomes "Open door". It invents no
|
|
113689
|
+
* translation — the words stay the id's own, only separators and the first
|
|
113690
|
+
* letter change. An empty id stays empty.
|
|
113691
|
+
*/
|
|
113692
|
+
function humanizeId(id) {
|
|
113693
|
+
const spaced = id.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
113694
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
113695
|
+
}
|
|
113696
|
+
//#endregion
|
|
113510
113697
|
//#region src/i18n/state-labels.ts
|
|
113698
|
+
/**
|
|
113699
|
+
* ONE dictionary of human labels for the state words the rule editors show
|
|
113700
|
+
* (R7, D690).
|
|
113701
|
+
*
|
|
113702
|
+
* A device-state gate offers the words the engine compares against —
|
|
113703
|
+
* `armed_away`, `not_home`, `unlocking` — and both rule editors printed them
|
|
113704
|
+
* verbatim. The words are the WIRE vocabulary and must stay what they are (a
|
|
113705
|
+
* rule stores them); what the operator reads is a label, and two editors that
|
|
113706
|
+
* each translate on their own are two editors that disagree. Both the admin UI
|
|
113707
|
+
* and the viewer resolve through {@link stateLabel}.
|
|
113708
|
+
*
|
|
113709
|
+
* Keys are the raw values themselves, flat: the vocabularies are disjoint where
|
|
113710
|
+
* they differ and agree where they overlap (`open` is the same word on a cover
|
|
113711
|
+
* and on a contact). An OPEN vocabulary (a presence zone the operator named,
|
|
113712
|
+
* a cover state a new integration invents) falls back to the raw word — never
|
|
113713
|
+
* an exception, never an empty chip.
|
|
113714
|
+
*
|
|
113715
|
+
* Covered, and held to their sources by the spec: the alarm panel's states,
|
|
113716
|
+
* the cover's, the lock's, the boolean readers' words
|
|
113717
|
+
* (`DEVICE_STATE_READERS`), presence's canonical `home`/`not_home`, and the
|
|
113718
|
+
* scene gate's verdicts and read modes.
|
|
113719
|
+
*/
|
|
113511
113720
|
/** Every key the dictionary answers for, in a stable order. */
|
|
113512
113721
|
var STATE_LABEL_KEYS = [
|
|
113513
113722
|
"disarmed",
|
|
@@ -113668,11 +113877,23 @@ function isStateLabelKey(key) {
|
|
|
113668
113877
|
* dictionary does not know it. Never throws.
|
|
113669
113878
|
*/
|
|
113670
113879
|
function stateLabel(key, locale) {
|
|
113671
|
-
return isStateLabelKey(key) ? STATE_LABELS[key][locale] : key;
|
|
113880
|
+
return isStateLabelKey(key) ? STATE_LABELS[key][locale] : humanizeId(key);
|
|
113672
113881
|
}
|
|
113673
113882
|
//#endregion
|
|
113674
113883
|
//#region src/i18n/event-kind-labels.ts
|
|
113675
113884
|
/**
|
|
113885
|
+
* ONE dictionary of human labels for event kinds (detection macros/subs,
|
|
113886
|
+
* sensors, audio macros, …) — the same vocabulary the viewer filters and the
|
|
113887
|
+
* notification centre speak (R7).
|
|
113888
|
+
*
|
|
113889
|
+
* Wire ids stay raw (`cat`, `animal`, `audio-dog`). What a human reads is a
|
|
113890
|
+
* label, and a surface that translates on its own is a surface that disagrees
|
|
113891
|
+
* with the next one. Admin UI, viewer and NC resolve through {@link eventKindLabel}.
|
|
113892
|
+
*
|
|
113893
|
+
* Keys match {@link EVENT_TAXONOMY}. An OPEN kind (a custom model class) falls
|
|
113894
|
+
* back to the raw word — never an exception, never an empty chip.
|
|
113895
|
+
*/
|
|
113896
|
+
/**
|
|
113676
113897
|
* Every kind the dictionary answers for. Order is stable; the set is held to
|
|
113677
113898
|
* {@link EVENT_TAXONOMY} by the spec.
|
|
113678
113899
|
*/
|
|
@@ -114042,7 +114263,7 @@ function isEventKindLabelKey(key) {
|
|
|
114042
114263
|
*/
|
|
114043
114264
|
function eventKindLabel(kind, locale) {
|
|
114044
114265
|
const key = kind.trim().toLowerCase();
|
|
114045
|
-
return isEventKindLabelKey(key) ? EVENT_KIND_LABELS[key][locale] : kind;
|
|
114266
|
+
return isEventKindLabelKey(key) ? EVENT_KIND_LABELS[key][locale] : humanizeId(kind);
|
|
114046
114267
|
}
|
|
114047
114268
|
//#endregion
|
|
114048
114269
|
//#region src/i18n/translation.ts
|
|
@@ -121775,6 +121996,7 @@ exports.evaluateRose = require_composition.evaluateRose;
|
|
|
121775
121996
|
exports.evaluateSensorEdge = require_composition.evaluateSensorEdge;
|
|
121776
121997
|
exports.evaluateSensorLevelEdge = require_composition.evaluateSensorLevelEdge;
|
|
121777
121998
|
exports.evaluateStatefulExpressionCall = require_composition.evaluateStatefulExpressionCall;
|
|
121999
|
+
exports.evaluateStationaryTrigger = evaluateStationaryTrigger;
|
|
121778
122000
|
exports.evaluateStatistic = require_composition.evaluateStatistic;
|
|
121779
122001
|
exports.evaluateZoneRules = evaluateZoneRules;
|
|
121780
122002
|
exports.event = require_sleep.event;
|