@camstack/types 1.2.428 → 1.2.430
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/session-health.d.ts +124 -0
- package/dist/addon.js +4 -3
- package/dist/addon.mjs +4 -3
- package/dist/automation/action.d.ts +210 -0
- package/dist/automation/condition-eval.d.ts +64 -0
- package/dist/automation/detection-trigger.d.ts +66 -0
- package/dist/automation/index.d.ts +12 -2
- package/dist/automation/recipe.d.ts +103 -1
- package/dist/automation/run-gate.d.ts +43 -0
- package/dist/automation/step-runner.d.ts +82 -0
- package/dist/automation/trace.d.ts +325 -0
- package/dist/capabilities/action-source.cap.d.ts +74 -0
- package/dist/capabilities/actions.cap.d.ts +459 -0
- package/dist/capabilities/core-blocks.cap.d.ts +761 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/pet-feeder.cap.d.ts +15 -1
- package/dist/client.js +73 -49
- package/dist/client.mjs +73 -49
- package/dist/enums/event-category.d.ts +5 -0
- package/dist/enums.js +1 -1
- package/dist/enums.mjs +1 -1
- package/dist/{event-category-Ave2VSxY.js → event-category-BhzwWQCH.js} +5 -0
- package/dist/{event-category-CT-hz5Rb.mjs → event-category-hdMR7CTW.mjs} +5 -0
- package/dist/generated/addon-api.d.ts +135 -0
- package/dist/generated/addon-cap-facts.d.ts +1 -1
- package/dist/generated/capability-router-map.d.ts +10 -4
- package/dist/generated/collection-array-methods.d.ts +1 -1
- package/dist/generated/device-proxy.d.ts +2 -0
- package/dist/generated/history-catalog.d.ts +1 -1
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/method-device-selectors.d.ts +2 -2
- package/dist/generated/system-proxy.d.ts +5 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3211 -1093
- package/dist/index.mjs +3153 -1094
- package/dist/{sleep-edKiOXLo.js → sleep-5zRztaHa.js} +24 -2
- package/dist/{sleep--jXS9hWo.mjs → sleep-BHjNez61.mjs} +24 -2
- 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-5zRztaHa.js");
|
|
3
|
+
const require_event_category = require("./event-category-BhzwWQCH.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");
|
|
@@ -289,6 +289,254 @@ function buildCapturingReply(envelope) {
|
|
|
289
289
|
return wrapper;
|
|
290
290
|
}
|
|
291
291
|
//#endregion
|
|
292
|
+
//#region src/addon/session-health.ts
|
|
293
|
+
var SESSION_LOST_ALERT_CATEGORY = "integration.session-lost";
|
|
294
|
+
var SESSION_EPISODE_KEY = "session-lost";
|
|
295
|
+
/** A transient reconnect is quiet for this long before it is worth an alert. */
|
|
296
|
+
var DEFAULT_SESSION_LOST_DEBOUNCE_MS = 2 * 6e4;
|
|
297
|
+
var REASON_TEXT = {
|
|
298
|
+
kicked: "the account was signed in on another device, which ended this session",
|
|
299
|
+
"auth-rejected": "the cloud rejected the sign-in (check the credentials)",
|
|
300
|
+
"token-expired": "the session token expired and could not be renewed",
|
|
301
|
+
"connection-lost": "the connection to the cloud was lost"
|
|
302
|
+
};
|
|
303
|
+
function sessionLostAlertId(kind, integrationId) {
|
|
304
|
+
return `session:${kind}:${integrationId}`;
|
|
305
|
+
}
|
|
306
|
+
/** Pure builder, exported so an addon with its own flow can still emit the standard shape. */
|
|
307
|
+
function buildSessionLostAlert(input) {
|
|
308
|
+
const now = input.now ?? Date.now();
|
|
309
|
+
return {
|
|
310
|
+
id: sessionLostAlertId(input.kind, input.integrationId),
|
|
311
|
+
category: SESSION_LOST_ALERT_CATEGORY,
|
|
312
|
+
severity: input.severity ?? "error",
|
|
313
|
+
title: `${input.providerName} connection lost`,
|
|
314
|
+
message: `${input.integrationName} lost its ${input.providerName} session: ${REASON_TEXT[input.reason]}.`,
|
|
315
|
+
action: input.action ?? {
|
|
316
|
+
label: "Open integration",
|
|
317
|
+
href: `/integrations/${encodeURIComponent(input.integrationId)}`
|
|
318
|
+
},
|
|
319
|
+
status: "active",
|
|
320
|
+
read: false,
|
|
321
|
+
createdAt: now,
|
|
322
|
+
updatedAt: now,
|
|
323
|
+
source: {
|
|
324
|
+
type: input.kind,
|
|
325
|
+
id: input.integrationId
|
|
326
|
+
},
|
|
327
|
+
metadata: {
|
|
328
|
+
episodeKey: SESSION_EPISODE_KEY,
|
|
329
|
+
reason: input.reason
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
var SessionHealthTracker = class {
|
|
334
|
+
#opts;
|
|
335
|
+
#debounceMs;
|
|
336
|
+
#entries = /* @__PURE__ */ new Map();
|
|
337
|
+
constructor(opts) {
|
|
338
|
+
this.#opts = opts;
|
|
339
|
+
this.#debounceMs = opts.debounceMs ?? 12e4;
|
|
340
|
+
}
|
|
341
|
+
/** The session is gone. Idempotent: call it on every failed poll. */
|
|
342
|
+
lost(integrationId, info) {
|
|
343
|
+
const tags = { integrationId };
|
|
344
|
+
const entry = this.#entries.get(integrationId);
|
|
345
|
+
if (entry === void 0 || entry.phase === "ok") {
|
|
346
|
+
const timer = setTimeout(() => void this.#fire(integrationId), this.#debounceMs);
|
|
347
|
+
this.#entries.set(integrationId, {
|
|
348
|
+
phase: "pending",
|
|
349
|
+
info,
|
|
350
|
+
timer
|
|
351
|
+
});
|
|
352
|
+
this.#opts.logger.info(`${this.#opts.providerName} session lost — alert after ${this.#debounceMs}ms unless it recovers`, {
|
|
353
|
+
tags,
|
|
354
|
+
meta: { reason: info.reason }
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
entry.info = info;
|
|
359
|
+
if (entry.phase === "pending") {
|
|
360
|
+
this.#opts.logger.debug(`${this.#opts.providerName} session still lost — alert pending`, {
|
|
361
|
+
tags,
|
|
362
|
+
meta: { reason: info.reason }
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (entry.phase === "unsent") {
|
|
367
|
+
this.#emit(integrationId, entry);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.#opts.logger.debug(`${this.#opts.providerName} session still lost — alert already active`, {
|
|
371
|
+
tags,
|
|
372
|
+
meta: { reason: info.reason }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/** A call went through. Idempotent: call it on every successful poll. */
|
|
376
|
+
async restored(integrationId) {
|
|
377
|
+
const tags = { integrationId };
|
|
378
|
+
const entry = this.#entries.get(integrationId);
|
|
379
|
+
if (entry === void 0) {
|
|
380
|
+
this.#entries.set(integrationId, {
|
|
381
|
+
phase: "ok",
|
|
382
|
+
info: EMPTY_INFO,
|
|
383
|
+
timer: null
|
|
384
|
+
});
|
|
385
|
+
if (!await this.#resolve(integrationId, "startup reconcile")) this.#entries.delete(integrationId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (entry.phase === "ok") return;
|
|
389
|
+
this.#clearTimer(entry);
|
|
390
|
+
const previous = entry.phase;
|
|
391
|
+
entry.phase = "ok";
|
|
392
|
+
if (previous === "pending" || previous === "unsent") {
|
|
393
|
+
this.#opts.logger.info(`${this.#opts.providerName} session recovered inside the debounce — no alert`, { tags });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
await this.#resolve(integrationId, "session restored");
|
|
397
|
+
}
|
|
398
|
+
/** The integration is gone (deleted / disabled): drop its state and its alert. */
|
|
399
|
+
async forget(integrationId) {
|
|
400
|
+
const entry = this.#entries.get(integrationId);
|
|
401
|
+
if (entry === void 0) return;
|
|
402
|
+
this.#clearTimer(entry);
|
|
403
|
+
this.#entries.delete(integrationId);
|
|
404
|
+
if (entry.phase === "alerted") await this.#resolve(integrationId, "integration removed");
|
|
405
|
+
}
|
|
406
|
+
/** Stop every pending timer (addon shutdown). */
|
|
407
|
+
dispose() {
|
|
408
|
+
for (const entry of this.#entries.values()) this.#clearTimer(entry);
|
|
409
|
+
this.#entries.clear();
|
|
410
|
+
}
|
|
411
|
+
#clearTimer(entry) {
|
|
412
|
+
if (entry.timer !== null) clearTimeout(entry.timer);
|
|
413
|
+
entry.timer = null;
|
|
414
|
+
}
|
|
415
|
+
async #fire(integrationId) {
|
|
416
|
+
const entry = this.#entries.get(integrationId);
|
|
417
|
+
if (entry === void 0 || entry.phase !== "pending") return;
|
|
418
|
+
entry.timer = null;
|
|
419
|
+
await this.#emit(integrationId, entry);
|
|
420
|
+
}
|
|
421
|
+
async #emit(integrationId, entry) {
|
|
422
|
+
const tags = { integrationId };
|
|
423
|
+
const emit = this.#opts.getAlerts()?.emit;
|
|
424
|
+
if (emit === void 0) {
|
|
425
|
+
entry.phase = "unsent";
|
|
426
|
+
this.#opts.logger.warn(`${this.#opts.providerName} session alert skipped — alerts capability missing`, { tags });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
await emit.mutate(buildSessionLostAlert({
|
|
431
|
+
kind: this.#opts.kind,
|
|
432
|
+
providerName: this.#opts.providerName,
|
|
433
|
+
integrationId,
|
|
434
|
+
integrationName: entry.info.integrationName,
|
|
435
|
+
reason: entry.info.reason,
|
|
436
|
+
...entry.info.action !== void 0 ? { action: entry.info.action } : {}
|
|
437
|
+
}));
|
|
438
|
+
entry.phase = "alerted";
|
|
439
|
+
this.#opts.logger.warn(`${this.#opts.providerName} session lost — alert raised`, {
|
|
440
|
+
tags,
|
|
441
|
+
meta: { reason: entry.info.reason }
|
|
442
|
+
});
|
|
443
|
+
} catch (err) {
|
|
444
|
+
entry.phase = "unsent";
|
|
445
|
+
this.#opts.logger.warn(`${this.#opts.providerName} session alert emit failed`, {
|
|
446
|
+
tags,
|
|
447
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async #resolve(integrationId, why) {
|
|
452
|
+
const tags = { integrationId };
|
|
453
|
+
const update = this.#opts.getAlerts()?.update;
|
|
454
|
+
if (update === void 0) {
|
|
455
|
+
this.#opts.logger.debug(`${this.#opts.providerName} session alert not resolved — no alerts cap`, { tags });
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
await update.mutate({
|
|
460
|
+
alertId: sessionLostAlertId(this.#opts.kind, integrationId),
|
|
461
|
+
patch: {
|
|
462
|
+
status: "completed",
|
|
463
|
+
read: true
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
this.#opts.logger.info(`${this.#opts.providerName} session alert resolved`, {
|
|
467
|
+
tags,
|
|
468
|
+
meta: { why }
|
|
469
|
+
});
|
|
470
|
+
} catch (err) {
|
|
471
|
+
this.#opts.logger.debug(`${this.#opts.providerName} session alert resolve skipped`, {
|
|
472
|
+
tags,
|
|
473
|
+
meta: {
|
|
474
|
+
why,
|
|
475
|
+
error: err instanceof Error ? err.message : String(err)
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var EMPTY_INFO = {
|
|
483
|
+
integrationName: "",
|
|
484
|
+
reason: "connection-lost"
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* The addon-facing face of {@link SessionHealthTracker}: a process-wide object an
|
|
488
|
+
* integration manager AND its device classes can reach (the kernel builds a
|
|
489
|
+
* device from a `DeviceContext` alone, so neither can be handed a tracker).
|
|
490
|
+
* The addon `bind()`s it in `onInitialize`, `unbind()`s it on shutdown and
|
|
491
|
+
* `forget()`s an integration that vanished; everything before `bind` is a no-op.
|
|
492
|
+
*/
|
|
493
|
+
var IntegrationSessionReporter = class {
|
|
494
|
+
#opts;
|
|
495
|
+
#tracker = null;
|
|
496
|
+
#names = /* @__PURE__ */ new Map();
|
|
497
|
+
constructor(opts) {
|
|
498
|
+
this.#opts = opts;
|
|
499
|
+
}
|
|
500
|
+
bind(binding) {
|
|
501
|
+
this.#tracker?.dispose();
|
|
502
|
+
this.#tracker = new SessionHealthTracker({
|
|
503
|
+
kind: this.#opts.kind,
|
|
504
|
+
providerName: this.#opts.providerName,
|
|
505
|
+
logger: binding.logger,
|
|
506
|
+
getAlerts: binding.getAlerts,
|
|
507
|
+
...binding.debounceMs !== void 0 ? { debounceMs: binding.debounceMs } : {}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
unbind() {
|
|
511
|
+
this.#tracker?.dispose();
|
|
512
|
+
this.#tracker = null;
|
|
513
|
+
}
|
|
514
|
+
/** The manager tells us the display name once, so a device can report by id alone. */
|
|
515
|
+
register(integrationId, integrationName) {
|
|
516
|
+
this.#names.set(integrationId, integrationName);
|
|
517
|
+
}
|
|
518
|
+
/** A call failed. Only a session-ending failure counts; anything else is ignored. */
|
|
519
|
+
failure(integrationId, err) {
|
|
520
|
+
const reason = this.#opts.classify(err);
|
|
521
|
+
if (reason === null) return;
|
|
522
|
+
const action = this.#opts.action?.(integrationId);
|
|
523
|
+
this.#tracker?.lost(integrationId, {
|
|
524
|
+
integrationName: this.#names.get(integrationId) ?? integrationId,
|
|
525
|
+
reason,
|
|
526
|
+
...action !== void 0 ? { action } : {}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/** A call went through. */
|
|
530
|
+
async ok(integrationId) {
|
|
531
|
+
await this.#tracker?.restored(integrationId);
|
|
532
|
+
}
|
|
533
|
+
/** The integration was removed. */
|
|
534
|
+
async forget(integrationId) {
|
|
535
|
+
this.#names.delete(integrationId);
|
|
536
|
+
await this.#tracker?.forget(integrationId);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
//#endregion
|
|
292
540
|
//#region src/encode-profile.ts
|
|
293
541
|
/**
|
|
294
542
|
* Shared video / audio encode profile schemas used by every consumer
|
|
@@ -7826,7 +8074,7 @@ var ExpressionSourceSchema = zod.z.object({
|
|
|
7826
8074
|
*
|
|
7827
8075
|
* Three vocabularies, each salvaged rather than invented:
|
|
7828
8076
|
*
|
|
7829
|
-
* - **Triggers** — the only genuinely new surface.
|
|
8077
|
+
* - **Triggers** — the only genuinely new surface. Six members, none of which
|
|
7830
8078
|
* touches notification delivery. The NC keeps its four *pipeline* triggers;
|
|
7831
8079
|
* these are device/time triggers and the two sets do not overlap.
|
|
7832
8080
|
* - **Conditions** — `ALL`/`ANY`/`NOT` over two leaf kinds, bounded. The
|
|
@@ -7950,7 +8198,23 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
|
|
|
7950
8198
|
kind: zod.z.literal("schedule"),
|
|
7951
8199
|
cron: zod.z.string().min(1).max(120)
|
|
7952
8200
|
}),
|
|
7953
|
-
zod.z.object({ kind: zod.z.literal("manual") })
|
|
8201
|
+
zod.z.object({ kind: zod.z.literal("manual") }),
|
|
8202
|
+
zod.z.object({
|
|
8203
|
+
kind: zod.z.literal("detection"),
|
|
8204
|
+
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
8205
|
+
* matches every raw class that maps to it; any other name matches
|
|
8206
|
+
* exactly. */
|
|
8207
|
+
classes: zod.z.array(zod.z.string().min(1)).min(1),
|
|
8208
|
+
deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
|
|
8209
|
+
/** The track must have visited at least one of these zones. */
|
|
8210
|
+
zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
|
|
8211
|
+
/** Best-so-far confidence the track must reach. */
|
|
8212
|
+
minScore: zod.z.number().min(0).max(1).optional()
|
|
8213
|
+
}),
|
|
8214
|
+
zod.z.object({
|
|
8215
|
+
kind: zod.z.literal("interval"),
|
|
8216
|
+
minutes: zod.z.number().int().min(1).max(1440)
|
|
8217
|
+
})
|
|
7954
8218
|
]);
|
|
7955
8219
|
/**
|
|
7956
8220
|
* One action step.
|
|
@@ -7970,28 +8234,47 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
|
|
|
7970
8234
|
* is called. The runner still receives a number and is untouched; the
|
|
7971
8235
|
* resolution is the recipe's job, not the runner's.
|
|
7972
8236
|
*/
|
|
8237
|
+
var AutomationWaitStepSchema = zod.z.object({
|
|
8238
|
+
kind: zod.z.literal("wait"),
|
|
8239
|
+
seconds: zod.z.number().min(0).max(300)
|
|
8240
|
+
});
|
|
8241
|
+
var AutomationCapStepSchema = zod.z.object({
|
|
8242
|
+
kind: zod.z.literal("cap"),
|
|
8243
|
+
/**
|
|
8244
|
+
* One device, several same-family devices (locks / climate family / …),
|
|
8245
|
+
* or `{ $var }` for the device that triggered the run. An array is
|
|
8246
|
+
* fan-out at codegen — each id gets the same cap.method call.
|
|
8247
|
+
*/
|
|
8248
|
+
deviceId: zod.z.union([
|
|
8249
|
+
zod.z.number().int(),
|
|
8250
|
+
zod.z.array(zod.z.number().int()).min(1),
|
|
8251
|
+
zod.z.object({ $var: zod.z.string().min(1) })
|
|
8252
|
+
]),
|
|
8253
|
+
cap: zod.z.string().min(1),
|
|
8254
|
+
method: zod.z.string().min(1),
|
|
8255
|
+
/** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
|
|
8256
|
+
* SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
|
|
8257
|
+
* the expression leaf. */
|
|
8258
|
+
args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
8259
|
+
});
|
|
8260
|
+
/**
|
|
8261
|
+
* The steps an internal Action may contain: cap and wait, and nothing that can
|
|
8262
|
+
* call back into an Action — so an Action can never reach itself (no cycles).
|
|
8263
|
+
*/
|
|
8264
|
+
var ActionStepSchema = zod.z.discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
|
|
7973
8265
|
var AutomationActionSchema = zod.z.discriminatedUnion("kind", [
|
|
8266
|
+
AutomationWaitStepSchema,
|
|
8267
|
+
AutomationCapStepSchema,
|
|
7974
8268
|
zod.z.object({
|
|
7975
|
-
kind: zod.z.literal("
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
*/
|
|
7985
|
-
deviceId: zod.z.union([
|
|
7986
|
-
zod.z.number().int(),
|
|
7987
|
-
zod.z.array(zod.z.number().int()).min(1),
|
|
7988
|
-
zod.z.object({ $var: zod.z.string().min(1) })
|
|
7989
|
-
]),
|
|
7990
|
-
cap: zod.z.string().min(1),
|
|
7991
|
-
method: zod.z.string().min(1),
|
|
7992
|
-
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
7993
|
-
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
7994
|
-
args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
8269
|
+
kind: zod.z.literal("action"),
|
|
8270
|
+
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
8271
|
+
actionId: zod.z.string().min(1),
|
|
8272
|
+
/** Values for the action's declared variables; the others take their default. */
|
|
8273
|
+
variables: zod.z.record(zod.z.string(), zod.z.union([
|
|
8274
|
+
zod.z.string(),
|
|
8275
|
+
zod.z.number(),
|
|
8276
|
+
zod.z.boolean()
|
|
8277
|
+
])).optional()
|
|
7995
8278
|
}),
|
|
7996
8279
|
zod.z.object({
|
|
7997
8280
|
kind: zod.z.literal("code"),
|
|
@@ -8000,10 +8283,45 @@ var AutomationActionSchema = zod.z.discriminatedUnion("kind", [
|
|
|
8000
8283
|
code: zod.z.string().min(1).max(2e4)
|
|
8001
8284
|
})
|
|
8002
8285
|
]);
|
|
8286
|
+
/**
|
|
8287
|
+
* How a trigger that arrives while the automation is busy (or cooling down) is
|
|
8288
|
+
* handled. One of the automation's execution modes, never a property of its
|
|
8289
|
+
* actions. Absent means `single`.
|
|
8290
|
+
*
|
|
8291
|
+
* - `single` — drop the trigger while a run is in progress (traced + logged).
|
|
8292
|
+
* - `restart` — abort the run in progress (at its next step boundary), start fresh.
|
|
8293
|
+
* - `queued` — queue up to `max` triggers, drop the rest (traced).
|
|
8294
|
+
* - `parallel` — up to `max` runs at once, drop the rest (traced).
|
|
8295
|
+
* - `cooldown` — `single`, and no new run until `minutes` after the last run
|
|
8296
|
+
* STARTED; persisted across a restart. A manual trigger bypasses only this
|
|
8297
|
+
* wait, never the single/queued limits.
|
|
8298
|
+
*/
|
|
8299
|
+
var AutomationRunModeSchema = zod.z.discriminatedUnion("mode", [
|
|
8300
|
+
zod.z.object({ mode: zod.z.literal("single") }),
|
|
8301
|
+
zod.z.object({ mode: zod.z.literal("restart") }),
|
|
8302
|
+
zod.z.object({
|
|
8303
|
+
mode: zod.z.literal("queued"),
|
|
8304
|
+
max: zod.z.number().int().min(1).max(10)
|
|
8305
|
+
}),
|
|
8306
|
+
zod.z.object({
|
|
8307
|
+
mode: zod.z.literal("parallel"),
|
|
8308
|
+
max: zod.z.number().int().min(1).max(10)
|
|
8309
|
+
}),
|
|
8310
|
+
zod.z.object({
|
|
8311
|
+
mode: zod.z.literal("cooldown"),
|
|
8312
|
+
minutes: zod.z.number().int().min(1).max(1440)
|
|
8313
|
+
})
|
|
8314
|
+
]);
|
|
8315
|
+
/** Run traces kept per automation when the recipe does not say. */
|
|
8316
|
+
var DEFAULT_TRACE_KEEP = 20;
|
|
8317
|
+
var MAX_TRACE_KEEP = 100;
|
|
8003
8318
|
var AutomationRecipeSchema = zod.z.object({
|
|
8004
8319
|
triggers: zod.z.array(AutomationTriggerSchema),
|
|
8005
8320
|
conditions: AutomationConditionSchema.optional(),
|
|
8006
|
-
actions: zod.z.array(AutomationActionSchema)
|
|
8321
|
+
actions: zod.z.array(AutomationActionSchema),
|
|
8322
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
8323
|
+
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
8324
|
+
keepTraces: zod.z.number().int().min(1).max(100).optional()
|
|
8007
8325
|
});
|
|
8008
8326
|
/** Depth of a tree, root container = 1. A bare leaf is depth 1. */
|
|
8009
8327
|
function conditionDepth(cond) {
|
|
@@ -8055,19 +8373,23 @@ function validateRecipeBounds(recipe) {
|
|
|
8055
8373
|
/** How often the state mirror is re-read as the backstop for a dropped event
|
|
8056
8374
|
* (D8: an event WAKES the evaluation, the reconcile guarantees it). */
|
|
8057
8375
|
var RECONCILE_MS = 15e3;
|
|
8376
|
+
/** What a step trace shows before the step runs. */
|
|
8377
|
+
function stepMetaOf(action) {
|
|
8378
|
+
switch (action.kind) {
|
|
8379
|
+
case "wait": return { kind: "wait" };
|
|
8380
|
+
case "code": return { kind: "code" };
|
|
8381
|
+
case "action": return { kind: "action" };
|
|
8382
|
+
case "cap": return {
|
|
8383
|
+
kind: "cap",
|
|
8384
|
+
cap: action.cap,
|
|
8385
|
+
method: action.method,
|
|
8386
|
+
...typeof action.deviceId === "number" ? { deviceId: action.deviceId } : {}
|
|
8387
|
+
};
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8058
8390
|
function j(value) {
|
|
8059
8391
|
return JSON.stringify(value);
|
|
8060
8392
|
}
|
|
8061
|
-
/** Emit the boolean tree as calls into the generated combinators. */
|
|
8062
|
-
function emitCondition(cond, indent) {
|
|
8063
|
-
switch (cond.kind) {
|
|
8064
|
-
case "all": return `allOf([\n${cond.children.map((c) => `${indent} () => ${emitCondition(c, `${indent} `)}`).join(",\n")}\n${indent}])`;
|
|
8065
|
-
case "any": return `anyOf([\n${cond.children.map((c) => `${indent} () => ${emitCondition(c, `${indent} `)}`).join(",\n")}\n${indent}])`;
|
|
8066
|
-
case "not": return `notOf(() => ${emitCondition(cond.child, indent)})`;
|
|
8067
|
-
case "condition": return `leaf(${j(cond.deviceId)}, ${j(cond.cap)}, ${j(cond.fieldPath)}, ${j(cond.operator)}, ${j(cond.value)})`;
|
|
8068
|
-
case "expression": return `exprLeaf(${j(cond.expr)}, ${j(cond.bindings)})`;
|
|
8069
|
-
}
|
|
8070
|
-
}
|
|
8071
8393
|
function emitTrigger(trigger, index) {
|
|
8072
8394
|
switch (trigger.kind) {
|
|
8073
8395
|
case "schedule": return ` // trigger ${index}: schedule — armed to the next minute boundary and
|
|
@@ -8143,31 +8465,79 @@ function emitTrigger(trigger, index) {
|
|
|
8143
8465
|
}`;
|
|
8144
8466
|
}
|
|
8145
8467
|
case "manual": return ` // trigger ${index}: manual — fired by automation-control.trigger
|
|
8146
|
-
// ("Run actions"), which runs the THEN half with an EMPTY vars bag
|
|
8468
|
+
// ("Run actions"), which runs the THEN half with an EMPTY vars bag. A manual
|
|
8469
|
+
// run bypasses only the cooldown wait, never the single/queued limits.`;
|
|
8470
|
+
case "interval": return ` // trigger ${index}: interval — every ${trigger.minutes} min, first run jittered so
|
|
8471
|
+
// blocks restarted together do not all fire on the same second.
|
|
8472
|
+
{
|
|
8473
|
+
let interval: ReturnType<typeof setInterval> | undefined
|
|
8474
|
+
const first = setTimeout(() => {
|
|
8475
|
+
void run({ kind: 'interval', at: Date.now() })
|
|
8476
|
+
interval = setInterval(() => { void run({ kind: 'interval', at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
|
|
8477
|
+
}, Math.floor(Math.random() * Math.min(${j(trigger.minutes * 6e4)}, 30_000)) + 1)
|
|
8478
|
+
disposers.push(() => {
|
|
8479
|
+
clearTimeout(first)
|
|
8480
|
+
if (interval !== undefined) clearInterval(interval)
|
|
8481
|
+
})
|
|
8482
|
+
}`;
|
|
8483
|
+
case "detection": return ` // trigger ${index}: detection — ONE fire per (device, track); a rejected event is
|
|
8484
|
+
// traced with its reason and never fires.
|
|
8485
|
+
{
|
|
8486
|
+
const filter = new DetectionTriggerFilter(${j(trigger)})
|
|
8487
|
+
disposers.push(
|
|
8488
|
+
ctx.eventBus.subscribe(
|
|
8489
|
+
{ category: EventCategory.PipelineAnalyticsTrackLifecycle },
|
|
8490
|
+
(event) => {
|
|
8491
|
+
const decision = filter.evaluate(event.data, Date.now())
|
|
8492
|
+
if (decision.kind === 'ignore') return
|
|
8493
|
+
if (decision.kind === 'reject') {
|
|
8494
|
+
ctx.logger.debug('detection trigger rejected an event', {
|
|
8495
|
+
tags: { deviceId: decision.vars.deviceId },
|
|
8496
|
+
reason: decision.reason,
|
|
8497
|
+
})
|
|
8498
|
+
}
|
|
8499
|
+
void run(
|
|
8500
|
+
{ kind: 'detection', at: Date.now(), ...decision.vars },
|
|
8501
|
+
decision.reason,
|
|
8502
|
+
decision.kind === 'fire',
|
|
8503
|
+
)
|
|
8504
|
+
},
|
|
8505
|
+
),
|
|
8506
|
+
)
|
|
8507
|
+
}`;
|
|
8147
8508
|
}
|
|
8148
8509
|
}
|
|
8149
8510
|
function emitAction(action, index) {
|
|
8511
|
+
const step = (body) => ` {
|
|
8512
|
+
meta: STEP_META[${index}]!,
|
|
8513
|
+
run: async (scope) => {
|
|
8514
|
+
${body}
|
|
8515
|
+
},
|
|
8516
|
+
},`;
|
|
8150
8517
|
switch (action.kind) {
|
|
8151
|
-
case "wait": return `
|
|
8518
|
+
case "wait": return step(` await sleep(${j(action.seconds * 1e3)})`);
|
|
8152
8519
|
case "cap":
|
|
8153
|
-
if (Array.isArray(action.deviceId)) return `
|
|
8154
|
-
|
|
8520
|
+
if (Array.isArray(action.deviceId)) return step(` for (const deviceId of ${j(action.deviceId)}) {
|
|
8521
|
+
scope.setDeviceId(deviceId)
|
|
8155
8522
|
await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
|
|
8156
|
-
}
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8523
|
+
}`);
|
|
8524
|
+
return step(` const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
|
|
8525
|
+
scope.setDeviceId(deviceId)
|
|
8526
|
+
await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
|
|
8527
|
+
case "action": return step(` // an Action run, awaited: the step takes THAT run's outcome
|
|
8528
|
+
const result = await ctx.api.actions.run.mutate({
|
|
8529
|
+
actionId: ${j(action.actionId)},
|
|
8530
|
+
variables: substituteVariables(${j(action.variables ?? {})}, vars),
|
|
8531
|
+
wait: true,
|
|
8532
|
+
origin: { blockId: BLOCK_ID ?? undefined },
|
|
8533
|
+
})
|
|
8534
|
+
if (result.outcome !== 'ran') {
|
|
8535
|
+
throw new Error('action ' + ${j(action.actionId)} + ' ' + result.outcome + (result.message === undefined ? '' : ': ' + result.message))
|
|
8536
|
+
}`);
|
|
8537
|
+
case "code": return step(` // inline code action — a fragment of THIS block, bundled by esbuild
|
|
8166
8538
|
await (async () => {
|
|
8167
8539
|
${action.code.split("\n").map((line) => ` ${line}`).join("\n")}
|
|
8168
|
-
})()
|
|
8169
|
-
break
|
|
8170
|
-
}`;
|
|
8540
|
+
})()`);
|
|
8171
8541
|
}
|
|
8172
8542
|
}
|
|
8173
8543
|
/**
|
|
@@ -8181,9 +8551,31 @@ function generateAutomationBlock(input) {
|
|
|
8181
8551
|
const boundsError = validateRecipeBounds(input.recipe);
|
|
8182
8552
|
if (boundsError !== null) throw new Error(`cannot generate an automation from an invalid recipe: ${boundsError}`);
|
|
8183
8553
|
const { name, recipe } = input;
|
|
8184
|
-
const
|
|
8185
|
-
|
|
8186
|
-
import {
|
|
8554
|
+
const runMode = recipe.runMode ?? { mode: "single" };
|
|
8555
|
+
const stepMeta = recipe.actions.map(stepMetaOf);
|
|
8556
|
+
return `import type {
|
|
8557
|
+
AddonContext,
|
|
8558
|
+
AutomationConditionTraceNode,
|
|
8559
|
+
AutomationRunTrace,
|
|
8560
|
+
AutomationRunVerdict,
|
|
8561
|
+
AutomationStepTrace,
|
|
8562
|
+
RunnableStep,
|
|
8563
|
+
} from '@camstack/types'
|
|
8564
|
+
import {
|
|
8565
|
+
ConditionMirror,
|
|
8566
|
+
DetectionTriggerFilter,
|
|
8567
|
+
EventCategory,
|
|
8568
|
+
RunGate,
|
|
8569
|
+
buildTriggerTrace,
|
|
8570
|
+
collectMirrorSources,
|
|
8571
|
+
coreBlockIdFromAddonId,
|
|
8572
|
+
evaluateConditionTree,
|
|
8573
|
+
isSkippedTrace,
|
|
8574
|
+
mergeSkipTraces,
|
|
8575
|
+
resolveDeviceTarget,
|
|
8576
|
+
resolveStepArgs,
|
|
8577
|
+
runSteps,
|
|
8578
|
+
} from '@camstack/types'
|
|
8187
8579
|
|
|
8188
8580
|
/**
|
|
8189
8581
|
* ${name}
|
|
@@ -8198,6 +8590,20 @@ const AUTOMATION_RECIPE = ${JSON.stringify(recipe, null, 2)} as const
|
|
|
8198
8590
|
|
|
8199
8591
|
const RECONCILE_MS = ${RECONCILE_MS}
|
|
8200
8592
|
|
|
8593
|
+
/** How triggers that arrive while busy (or cooling down) are handled. */
|
|
8594
|
+
const RUN_MODE = ${j(runMode)} as const
|
|
8595
|
+
|
|
8596
|
+
/** Run traces kept (a ring, held in the hub's runs ledger). */
|
|
8597
|
+
const KEEP_TRACES = ${recipe.keepTraces ?? 20}
|
|
8598
|
+
|
|
8599
|
+
const CONDITIONS = ${recipe.conditions === void 0 ? "undefined" : "AUTOMATION_RECIPE.conditions"}
|
|
8600
|
+
|
|
8601
|
+
/** Skips that repeat are folded into one trace; it goes to the hub after this. */
|
|
8602
|
+
const SKIP_FLUSH_MS = 5_000
|
|
8603
|
+
|
|
8604
|
+
/** Static facts about each action step, for its trace. */
|
|
8605
|
+
const STEP_META: ReadonlyArray<{ kind: string; cap?: string; method?: string; deviceId?: number }> = ${j(stepMeta)}
|
|
8606
|
+
|
|
8201
8607
|
/** A cap is named \`alarm-panel\`; \`ctx.api\` is keyed by the ROUTER name,
|
|
8202
8608
|
* \`alarmPanel\`. Indexing ctx.api with the cap name yields undefined and every
|
|
8203
8609
|
* action throws, so the conversion is not cosmetic. */
|
|
@@ -8248,137 +8654,170 @@ function matchField(field: string, value: number): boolean {
|
|
|
8248
8654
|
|
|
8249
8655
|
export default function block(ctx: AddonContext) {
|
|
8250
8656
|
const disposers: Array<() => void> = []
|
|
8251
|
-
const
|
|
8657
|
+
const gate = new RunGate(RUN_MODE)
|
|
8658
|
+
const mirror = new ConditionMirror(collectMirrorSources(CONDITIONS))
|
|
8659
|
+
const BLOCK_ID = coreBlockIdFromAddonId(ctx.id)
|
|
8660
|
+
/** The cooldown stamp: memory is the authority at fire time (D49); the hub ledger is read once at start. */
|
|
8661
|
+
let lastRunStartedAt: number | null = null
|
|
8662
|
+
/** True when the stamp could not be read at start — the first run is let through and says so. */
|
|
8663
|
+
let stampUnknown = false
|
|
8664
|
+
let sendChain: Promise<void> = Promise.resolve()
|
|
8665
|
+
let pendingSkip: AutomationRunTrace | undefined
|
|
8666
|
+
let skipTimer: ReturnType<typeof setTimeout> | undefined
|
|
8667
|
+
let stopped = false
|
|
8668
|
+
let runCounter = 0
|
|
8669
|
+
const runningTokens = new Set<RunToken>()
|
|
8670
|
+
const queue: QueuedRun[] = []
|
|
8252
8671
|
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8672
|
+
interface RunToken {
|
|
8673
|
+
aborted: boolean
|
|
8674
|
+
wake?: () => void
|
|
8675
|
+
}
|
|
8676
|
+
interface QueuedRun {
|
|
8677
|
+
readonly trigger: Record<string, unknown>
|
|
8678
|
+
readonly reason: string
|
|
8679
|
+
readonly queuedAt: number
|
|
8680
|
+
}
|
|
8681
|
+
|
|
8682
|
+
/** Sleep that a restart can cut short. */
|
|
8683
|
+
const abortableSleep = (ms: number, token: RunToken): Promise<void> =>
|
|
8684
|
+
new Promise((resolve) => {
|
|
8685
|
+
const timer = setTimeout(resolve, ms)
|
|
8686
|
+
token.wake = () => {
|
|
8687
|
+
clearTimeout(timer)
|
|
8688
|
+
resolve()
|
|
8267
8689
|
}
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8690
|
+
})
|
|
8691
|
+
|
|
8692
|
+
// ── the hub's runs ledger: the stamp is read ONCE here, the ring is written through RPC ──
|
|
8693
|
+
async function loadState(): Promise<void> {
|
|
8694
|
+
if (BLOCK_ID === null) {
|
|
8695
|
+
ctx.logger.warn('this is not a core-block addon id — no run ledger, cooldown and traces will not survive a restart', { addonId: ctx.id })
|
|
8696
|
+
return
|
|
8697
|
+
}
|
|
8698
|
+
try {
|
|
8699
|
+
const doc = await ctx.api.coreBlocks.getAutomationRunState.query({ blockId: BLOCK_ID })
|
|
8700
|
+
lastRunStartedAt = doc.lastRunStartedAt
|
|
8701
|
+
} catch (err: unknown) {
|
|
8702
|
+
stampUnknown = true
|
|
8703
|
+
ctx.logger.warn('could not read the stored cooldown stamp — the first run is let through', {
|
|
8704
|
+
blockId: BLOCK_ID,
|
|
8705
|
+
error: err instanceof Error ? err.message : String(err),
|
|
8706
|
+
})
|
|
8271
8707
|
}
|
|
8272
8708
|
}
|
|
8273
8709
|
|
|
8274
|
-
function
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8710
|
+
function send(payload: { trace?: AutomationRunTrace; runStartedAt?: number }): Promise<void> {
|
|
8711
|
+
if (BLOCK_ID === null) return Promise.resolve()
|
|
8712
|
+
const blockId = BLOCK_ID
|
|
8713
|
+
sendChain = sendChain.then(async () => {
|
|
8714
|
+
try {
|
|
8715
|
+
await ctx.api.coreBlocks.reportAutomationRun.mutate({ blockId, keepTraces: KEEP_TRACES, ...payload })
|
|
8716
|
+
} catch (err: unknown) {
|
|
8717
|
+
ctx.logger.warn('could not report the run to the hub ledger', {
|
|
8718
|
+
blockId,
|
|
8719
|
+
error: err instanceof Error ? err.message : String(err),
|
|
8720
|
+
})
|
|
8721
|
+
}
|
|
8722
|
+
})
|
|
8723
|
+
return sendChain
|
|
8286
8724
|
}
|
|
8287
8725
|
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
cap: string,
|
|
8293
|
-
fieldPath: string,
|
|
8294
|
-
operator: string,
|
|
8295
|
-
expected: unknown,
|
|
8296
|
-
): Promise<boolean> {
|
|
8297
|
-
const actual = await readField(deviceId, cap, fieldPath)
|
|
8298
|
-
if (actual === undefined) {
|
|
8299
|
-
ctx.logger.warn('condition leaf unresolvable — treated as false', {
|
|
8300
|
-
tags: { deviceId },
|
|
8301
|
-
cap,
|
|
8302
|
-
fieldPath,
|
|
8303
|
-
})
|
|
8304
|
-
return false
|
|
8726
|
+
function flushSkip(): void {
|
|
8727
|
+
if (skipTimer !== undefined) {
|
|
8728
|
+
clearTimeout(skipTimer)
|
|
8729
|
+
skipTimer = undefined
|
|
8305
8730
|
}
|
|
8306
|
-
|
|
8731
|
+
if (pendingSkip === undefined) return
|
|
8732
|
+
const trace = pendingSkip
|
|
8733
|
+
pendingSkip = undefined
|
|
8734
|
+
void send({ trace })
|
|
8307
8735
|
}
|
|
8308
8736
|
|
|
8309
|
-
/**
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
continue
|
|
8320
|
-
}
|
|
8321
|
-
const deviceId = await stableIdToDeviceId(binding.sourceStableId)
|
|
8322
|
-
if (deviceId === undefined) {
|
|
8323
|
-
ctx.logger.warn('condition leaf unresolvable — treated as false', {
|
|
8324
|
-
tags: { deviceId: -1 },
|
|
8325
|
-
binding: alias,
|
|
8326
|
-
})
|
|
8327
|
-
return false
|
|
8737
|
+
/** A skip identical to the one waiting is COUNTED on it; anything else flushes it first. */
|
|
8738
|
+
function record(trace: AutomationRunTrace): void {
|
|
8739
|
+
if (isSkippedTrace(trace)) {
|
|
8740
|
+
if (pendingSkip !== undefined) {
|
|
8741
|
+
const merged = mergeSkipTraces(pendingSkip, trace)
|
|
8742
|
+
if (merged !== null) {
|
|
8743
|
+
pendingSkip = merged
|
|
8744
|
+
return
|
|
8745
|
+
}
|
|
8746
|
+
flushSkip()
|
|
8328
8747
|
}
|
|
8329
|
-
|
|
8330
|
-
|
|
8748
|
+
pendingSkip = trace
|
|
8749
|
+
skipTimer = setTimeout(flushSkip, SKIP_FLUSH_MS)
|
|
8750
|
+
return
|
|
8331
8751
|
}
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8752
|
+
flushSkip()
|
|
8753
|
+
void send({ trace })
|
|
8754
|
+
}
|
|
8755
|
+
|
|
8756
|
+
// ── the condition mirror (D49): memory only at fire time ──
|
|
8757
|
+
async function refreshMirror(): Promise<void> {
|
|
8758
|
+
const report = await mirror.refresh({
|
|
8759
|
+
fetchSlice: (deviceId, cap) => ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap }),
|
|
8760
|
+
fetchStableIds: async () => {
|
|
8761
|
+
const all = await ctx.api.deviceManager.listAll.query({})
|
|
8762
|
+
return new Map(all.devices.flatMap((d: { stableId?: string; id: number }) =>
|
|
8763
|
+
d.stableId === undefined ? [] : [[d.stableId, d.id] as [string, number]]))
|
|
8764
|
+
},
|
|
8765
|
+
})
|
|
8766
|
+
if (report.kept > 0) {
|
|
8767
|
+
ctx.logger.debug('condition mirror kept stale values for sources it could not read', report)
|
|
8336
8768
|
}
|
|
8337
|
-
return result.value === true
|
|
8338
8769
|
}
|
|
8339
8770
|
|
|
8340
|
-
|
|
8341
|
-
|
|
8771
|
+
/** Read one cap field off the state mirror. Returns undefined when the device,
|
|
8772
|
+
* the cap or the field cannot be resolved — the caller decides what that
|
|
8773
|
+
* means, and it always logs. (Used by the device-state TRIGGER, which the
|
|
8774
|
+
* reconcile backs; conditions read the ConditionMirror instead.) */
|
|
8775
|
+
async function readField(
|
|
8776
|
+
deviceId: number,
|
|
8777
|
+
cap: string,
|
|
8778
|
+
fieldPath: string,
|
|
8779
|
+
): Promise<unknown> {
|
|
8342
8780
|
try {
|
|
8343
|
-
const
|
|
8344
|
-
|
|
8345
|
-
|
|
8781
|
+
const slice = await ctx.api.deviceState.getCapSlice.query({ deviceId, capName: cap })
|
|
8782
|
+
let cursor: unknown = slice
|
|
8783
|
+
for (const part of fieldPath.split('.')) {
|
|
8784
|
+
if (cursor === null || typeof cursor !== 'object') return undefined
|
|
8785
|
+
cursor = (cursor as Record<string, unknown>)[part]
|
|
8786
|
+
}
|
|
8787
|
+
return cursor
|
|
8346
8788
|
} catch {
|
|
8347
8789
|
return undefined
|
|
8348
8790
|
}
|
|
8349
8791
|
}
|
|
8350
8792
|
|
|
8351
|
-
async function allOf(children: Array<() => Promise<boolean>>): Promise<boolean> {
|
|
8352
|
-
for (const c of children) { if (!(await c())) return false }
|
|
8353
|
-
return true
|
|
8354
|
-
}
|
|
8355
|
-
async function anyOf(children: Array<() => Promise<boolean>>): Promise<boolean> {
|
|
8356
|
-
for (const c of children) { if (await c()) return true }
|
|
8357
|
-
return false
|
|
8358
|
-
}
|
|
8359
|
-
async function notOf(child: () => Promise<boolean>): Promise<boolean> {
|
|
8360
|
-
return !(await child())
|
|
8361
|
-
}
|
|
8362
|
-
|
|
8363
8793
|
/** A {$var} action target, resolved from the run's bag BEFORE dispatch — the
|
|
8364
8794
|
* action runner only ever sees a number. */
|
|
8365
8795
|
function resolveTarget(vars: Record<string, unknown>, name: string): number {
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8796
|
+
return resolveDeviceTarget(vars, name)
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
function warnMissingVar(name: string): void {
|
|
8800
|
+
ctx.logger.warn('template referenced a var that was never set', { var: name })
|
|
8369
8801
|
}
|
|
8370
8802
|
|
|
8371
|
-
/** Substitute {{vars.x}} slots. Substitution only — never evaluation. */
|
|
8803
|
+
/** Substitute {{vars.x}} slots and { $var } values. Substitution only — never evaluation. */
|
|
8372
8804
|
function substitute(value: unknown, vars: Record<string, unknown>): unknown {
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8805
|
+
return resolveStepArgs(value, vars, warnMissingVar)
|
|
8806
|
+
}
|
|
8807
|
+
|
|
8808
|
+
/** The variables handed to an Action step, substituted and kept to primitives. */
|
|
8809
|
+
function substituteVariables(
|
|
8810
|
+
raw: Record<string, string | number | boolean>,
|
|
8811
|
+
vars: Record<string, unknown>,
|
|
8812
|
+
): Record<string, string | number | boolean> {
|
|
8813
|
+
const out: Record<string, string | number | boolean> = {}
|
|
8814
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
8815
|
+
const resolved = substitute(value, vars)
|
|
8816
|
+
if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') {
|
|
8817
|
+
out[key] = resolved
|
|
8379
8818
|
}
|
|
8380
|
-
|
|
8381
|
-
|
|
8819
|
+
}
|
|
8820
|
+
return out
|
|
8382
8821
|
}
|
|
8383
8822
|
|
|
8384
8823
|
async function actuate(
|
|
@@ -8388,8 +8827,9 @@ export default function block(ctx: AddonContext) {
|
|
|
8388
8827
|
args: Record<string, unknown>,
|
|
8389
8828
|
vars: Record<string, unknown>,
|
|
8390
8829
|
): Promise<void> {
|
|
8391
|
-
const
|
|
8392
|
-
|
|
8830
|
+
const substituted = substitute(args, vars)
|
|
8831
|
+
const resolved: Record<string, unknown> =
|
|
8832
|
+
substituted !== null && typeof substituted === 'object' ? { ...substituted } : {}
|
|
8393
8833
|
// Forked-addon ctx.api namespaces are tRPC Proxies over a noop function
|
|
8394
8834
|
// (typeof === 'function'), not plain objects — reject only null/undefined.
|
|
8395
8835
|
const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
|
|
@@ -8403,292 +8843,1937 @@ export default function block(ctx: AddonContext) {
|
|
|
8403
8843
|
await mutate({ ...resolved, deviceId })
|
|
8404
8844
|
}
|
|
8405
8845
|
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8846
|
+
/** The THEN half, through the ONE shared step runner: stops at the first
|
|
8847
|
+
* failure (continuing past a failed step executes an intent the operator
|
|
8848
|
+
* never expressed); every step is traced, the ones after a failure (or an
|
|
8849
|
+
* abort) are marked not-run. */
|
|
8850
|
+
async function runActions(
|
|
8851
|
+
vars: Record<string, unknown>,
|
|
8852
|
+
token: RunToken,
|
|
8853
|
+
): Promise<{ steps: readonly AutomationStepTrace[]; failed: boolean }> {
|
|
8854
|
+
const sleep = (ms: number): Promise<void> => abortableSleep(ms, token)
|
|
8855
|
+
const steps: RunnableStep[] = [
|
|
8416
8856
|
${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
|
|
8417
|
-
|
|
8418
|
-
|
|
8857
|
+
]
|
|
8858
|
+
return runSteps(steps, {
|
|
8859
|
+
pathPrefix: 'actions',
|
|
8860
|
+
isAborted: () => token.aborted,
|
|
8861
|
+
abortReason: 'aborted by a restart',
|
|
8862
|
+
now: () => Date.now(),
|
|
8863
|
+
onFailure: (failure) => {
|
|
8419
8864
|
ctx.logger.warn('action failed — stopping the sequence', {
|
|
8420
|
-
|
|
8421
|
-
|
|
8865
|
+
...(failure.deviceId === undefined ? {} : { tags: { deviceId: failure.deviceId } }),
|
|
8866
|
+
step: failure.index,
|
|
8867
|
+
error: failure.message,
|
|
8422
8868
|
})
|
|
8869
|
+
},
|
|
8870
|
+
})
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8873
|
+
function deviceTags(trigger: Record<string, unknown>): { tags?: { deviceId: number } } {
|
|
8874
|
+
const deviceId = trigger['deviceId']
|
|
8875
|
+
return typeof deviceId === 'number' ? { tags: { deviceId } } : {}
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8878
|
+
function newRunId(at: number): string {
|
|
8879
|
+
runCounter += 1
|
|
8880
|
+
return at.toString(36) + '-' + runCounter.toString(36)
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
/** A trigger that never became a run (rejected, or dropped by the run mode). */
|
|
8884
|
+
function skipTrace(
|
|
8885
|
+
trigger: Record<string, unknown>,
|
|
8886
|
+
reason: string,
|
|
8887
|
+
outcome: AutomationRunTrace['outcome'],
|
|
8888
|
+
verdict: AutomationRunVerdict | undefined,
|
|
8889
|
+
accepted: boolean,
|
|
8890
|
+
): AutomationRunTrace {
|
|
8891
|
+
const now = Date.now()
|
|
8892
|
+
return {
|
|
8893
|
+
runId: newRunId(now),
|
|
8894
|
+
startedAt: now,
|
|
8895
|
+
endedAt: now,
|
|
8896
|
+
trigger: buildTriggerTrace(trigger, accepted ? 'pass' : 'fail', reason),
|
|
8897
|
+
conditions: [],
|
|
8898
|
+
...(verdict === undefined ? {} : { verdict }),
|
|
8899
|
+
steps: [],
|
|
8900
|
+
outcome,
|
|
8901
|
+
repeat: 1,
|
|
8902
|
+
lastAt: now,
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
/** One admitted run: conditions (memory only), the cooldown, the actions. */
|
|
8907
|
+
async function execute(
|
|
8908
|
+
trigger: Record<string, unknown>,
|
|
8909
|
+
reason: string,
|
|
8910
|
+
admission: AutomationRunVerdict,
|
|
8911
|
+
): Promise<void> {
|
|
8912
|
+
const token: RunToken = { aborted: false }
|
|
8913
|
+
runningTokens.add(token)
|
|
8914
|
+
const startedAt = Date.now()
|
|
8915
|
+
const runId = newRunId(startedAt)
|
|
8916
|
+
let verdict = admission
|
|
8917
|
+
let outcome: AutomationRunTrace['outcome'] = 'ran'
|
|
8918
|
+
let nodes: AutomationConditionTraceNode[] = []
|
|
8919
|
+
let steps: AutomationStepTrace[] = []
|
|
8920
|
+
try {
|
|
8921
|
+
const evaluation = evaluateConditionTree(CONDITIONS, mirror, Date.now())
|
|
8922
|
+
nodes = [...evaluation.nodes]
|
|
8923
|
+
for (const node of nodes) {
|
|
8924
|
+
if (node.kind === 'condition' && node.observed === null && node.deviceId !== undefined) {
|
|
8925
|
+
ctx.logger.warn('condition leaf value unknown — treated as false', {
|
|
8926
|
+
tags: { deviceId: node.deviceId },
|
|
8927
|
+
path: node.path,
|
|
8928
|
+
cap: node.cap,
|
|
8929
|
+
fieldPath: node.fieldPath,
|
|
8930
|
+
})
|
|
8931
|
+
}
|
|
8932
|
+
}
|
|
8933
|
+
if (!evaluation.pass) {
|
|
8934
|
+
outcome = 'skipped-conditions'
|
|
8935
|
+
ctx.logger.debug('automation did not run — conditions were false', { trigger, ...deviceTags(trigger) })
|
|
8936
|
+
return
|
|
8937
|
+
}
|
|
8938
|
+
const letThrough = stampUnknown && RUN_MODE.mode === 'cooldown'
|
|
8939
|
+
const cooldown = letThrough
|
|
8940
|
+
? { ok: true, bypassed: true, reason: 'cooldown stamp unreadable at start — run let through' }
|
|
8941
|
+
: gate.cooldown({
|
|
8942
|
+
manual: trigger['kind'] === 'manual',
|
|
8943
|
+
now: Date.now(),
|
|
8944
|
+
lastRunStartedAt,
|
|
8945
|
+
})
|
|
8946
|
+
if (!cooldown.ok) {
|
|
8947
|
+
outcome = 'skipped-cooldown'
|
|
8948
|
+
verdict = { mode: RUN_MODE, decision: 'cooldown', reason: cooldown.reason }
|
|
8949
|
+
ctx.logger.info('automation did not run — ' + cooldown.reason, deviceTags(trigger))
|
|
8423
8950
|
return
|
|
8424
8951
|
}
|
|
8952
|
+
if (cooldown.bypassed) verdict = { mode: RUN_MODE, decision: 'bypass', reason: cooldown.reason }
|
|
8953
|
+
// Stamped when the actions START, and reported to the hub before they run,
|
|
8954
|
+
// so a restart mid-run cannot fire again inside the window.
|
|
8955
|
+
lastRunStartedAt = startedAt
|
|
8956
|
+
stampUnknown = false
|
|
8957
|
+
await send({ runStartedAt: startedAt })
|
|
8958
|
+
ctx.logger.info('automation running', { trigger, ...deviceTags(trigger) })
|
|
8959
|
+
const vars: Record<string, unknown> = { ...trigger }
|
|
8960
|
+
const result = await runActions(vars, token)
|
|
8961
|
+
steps = result.steps
|
|
8962
|
+
outcome = token.aborted ? 'aborted' : result.failed ? 'failed' : 'ran'
|
|
8963
|
+
} catch (err: unknown) {
|
|
8964
|
+
outcome = 'failed'
|
|
8965
|
+
ctx.logger.warn('automation run failed', { error: err instanceof Error ? err.message : String(err), ...deviceTags(trigger) })
|
|
8966
|
+
} finally {
|
|
8967
|
+
runningTokens.delete(token)
|
|
8968
|
+
const endedAt = Date.now()
|
|
8969
|
+
record({
|
|
8970
|
+
runId,
|
|
8971
|
+
startedAt,
|
|
8972
|
+
endedAt,
|
|
8973
|
+
trigger: buildTriggerTrace(trigger, 'pass', reason),
|
|
8974
|
+
conditions: nodes,
|
|
8975
|
+
verdict,
|
|
8976
|
+
steps,
|
|
8977
|
+
outcome,
|
|
8978
|
+
repeat: 1,
|
|
8979
|
+
lastAt: endedAt,
|
|
8980
|
+
})
|
|
8981
|
+
if (gate.release()) {
|
|
8982
|
+
const next = queue.shift()
|
|
8983
|
+
if (next !== undefined && !stopped) {
|
|
8984
|
+
void execute(next.trigger, next.reason, {
|
|
8985
|
+
mode: RUN_MODE,
|
|
8986
|
+
decision: 'queued',
|
|
8987
|
+
reason: 'queued, then ran after waiting ' + (Date.now() - next.queuedAt) + ' ms',
|
|
8988
|
+
})
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8425
8991
|
}
|
|
8426
8992
|
}
|
|
8427
8993
|
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8994
|
+
/** Every trigger lands here. \`accepted\` = the trigger itself was satisfied
|
|
8995
|
+
* (a rejected detection event is only traced). */
|
|
8996
|
+
async function run(
|
|
8997
|
+
trigger: Record<string, unknown>,
|
|
8998
|
+
reason = 'fired',
|
|
8999
|
+
accepted = true,
|
|
9000
|
+
): Promise<void> {
|
|
9001
|
+
await ready
|
|
9002
|
+
if (stopped) return
|
|
9003
|
+
if (!accepted) {
|
|
9004
|
+
record(skipTrace(trigger, reason, 'skipped-trigger', undefined, false))
|
|
8433
9005
|
return
|
|
8434
9006
|
}
|
|
8435
|
-
|
|
8436
|
-
|
|
9007
|
+
const admission = gate.admit()
|
|
9008
|
+
const verdict: AutomationRunVerdict = { mode: RUN_MODE, decision: admission.decision, reason: admission.reason }
|
|
9009
|
+
switch (admission.decision) {
|
|
9010
|
+
case 'drop':
|
|
9011
|
+
ctx.logger.info('automation trigger dropped — ' + admission.reason, deviceTags(trigger))
|
|
9012
|
+
record(skipTrace(trigger, reason, 'skipped-overlap', verdict, true))
|
|
9013
|
+
return
|
|
9014
|
+
case 'queued':
|
|
9015
|
+
ctx.logger.info('automation trigger queued — ' + admission.reason, deviceTags(trigger))
|
|
9016
|
+
queue.push({ trigger, reason, queuedAt: Date.now() })
|
|
9017
|
+
return
|
|
9018
|
+
case 'restart':
|
|
9019
|
+
ctx.logger.info('automation restarting — ' + admission.reason, deviceTags(trigger))
|
|
9020
|
+
for (const running of runningTokens) {
|
|
9021
|
+
running.aborted = true
|
|
9022
|
+
running.wake?.()
|
|
9023
|
+
}
|
|
9024
|
+
break
|
|
9025
|
+
case 'run':
|
|
9026
|
+
break
|
|
9027
|
+
}
|
|
9028
|
+
await execute(trigger, reason, verdict)
|
|
9029
|
+
}
|
|
9030
|
+
|
|
9031
|
+
const ready: Promise<void> = (async () => {
|
|
9032
|
+
await loadState()
|
|
9033
|
+
await refreshMirror()
|
|
9034
|
+
})()
|
|
9035
|
+
|
|
9036
|
+
if (CONDITIONS !== undefined) {
|
|
9037
|
+
disposers.push(
|
|
9038
|
+
ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
|
|
9039
|
+
const data = event.data
|
|
9040
|
+
const deviceId = data['deviceId']
|
|
9041
|
+
const capName = data['capName']
|
|
9042
|
+
if (typeof deviceId === 'number' && typeof capName === 'string') {
|
|
9043
|
+
mirror.apply(deviceId, capName, data['slice'])
|
|
9044
|
+
}
|
|
9045
|
+
}),
|
|
9046
|
+
)
|
|
9047
|
+
const mirrorReconcile = setInterval(() => { void refreshMirror() }, RECONCILE_MS)
|
|
9048
|
+
disposers.push(() => clearInterval(mirrorReconcile))
|
|
8437
9049
|
}
|
|
8438
9050
|
|
|
8439
9051
|
${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
8440
9052
|
|
|
8441
9053
|
return () => {
|
|
9054
|
+
stopped = true
|
|
9055
|
+
queue.length = 0
|
|
9056
|
+
flushSkip()
|
|
9057
|
+
for (const running of runningTokens) {
|
|
9058
|
+
running.aborted = true
|
|
9059
|
+
running.wake?.()
|
|
9060
|
+
}
|
|
8442
9061
|
for (const dispose of disposers) dispose()
|
|
8443
9062
|
}
|
|
8444
9063
|
}
|
|
8445
9064
|
`;
|
|
8446
9065
|
}
|
|
8447
9066
|
//#endregion
|
|
8448
|
-
//#region src/
|
|
9067
|
+
//#region src/automation/condition-eval.ts
|
|
8449
9068
|
/**
|
|
8450
|
-
*
|
|
8451
|
-
* surface that admin-ui consumes through `useAddonPagesListPages()`.
|
|
9069
|
+
* Evaluating an automation's condition tree against the ConditionMirror.
|
|
8452
9070
|
*
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
*
|
|
8456
|
-
*
|
|
8457
|
-
*
|
|
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.
|
|
8458
9077
|
*
|
|
8459
|
-
* The
|
|
8460
|
-
*
|
|
8461
|
-
*
|
|
8462
|
-
* keeps both ends in codegen — there's no hand-written
|
|
8463
|
-
* `addon-pages.router.ts` wrapper anymore.
|
|
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.
|
|
8464
9081
|
*/
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
});
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
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
|
+
}
|
|
8485
9195
|
};
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
*
|
|
8496
|
-
* The split exists because the public listing has a different output
|
|
8497
|
-
* shape than the per-provider raw declarations, and we want both ends
|
|
8498
|
-
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
8499
|
-
*/
|
|
8500
|
-
var AddonPageDeclarationSchema = zod.z.object({
|
|
8501
|
-
id: zod.z.string(),
|
|
8502
|
-
label: zod.z.string(),
|
|
8503
|
-
icon: zod.z.string(),
|
|
8504
|
-
path: zod.z.string(),
|
|
8505
|
-
/**
|
|
8506
|
-
* Module Federation remote name — must match the `name` field on the
|
|
8507
|
-
* page addon's `federation()` plugin config. Used by admin-ui's
|
|
8508
|
-
* `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
|
|
8509
|
-
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
8510
|
-
* contain hyphens).
|
|
8511
|
-
*/
|
|
8512
|
-
remoteName: zod.z.string(),
|
|
8513
|
-
/**
|
|
8514
|
-
* Bundle filename inside the addon's `dist/` dir served at
|
|
8515
|
-
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
8516
|
-
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
8517
|
-
* the static-file route can compute an mtime-based cache-buster URL
|
|
8518
|
-
* without a separate filesystem stat.
|
|
8519
|
-
*/
|
|
8520
|
-
bundle: zod.z.string(),
|
|
8521
|
-
/**
|
|
8522
|
-
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
8523
|
-
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
8524
|
-
* Any OTHER string creates (or joins) a custom section rendered after
|
|
8525
|
-
* the built-in groups; its label comes from `sectionLabel` (first
|
|
8526
|
-
* declaration wins), falling back to the id. Absent → the legacy
|
|
8527
|
-
* "Addon Pages" group.
|
|
8528
|
-
*/
|
|
8529
|
-
section: zod.z.string().optional(),
|
|
8530
|
-
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
8531
|
-
sectionLabel: zod.z.string().optional()
|
|
8532
|
-
});
|
|
8533
|
-
var addonPagesSourceCapability = {
|
|
8534
|
-
name: "addon-pages-source",
|
|
8535
|
-
scope: "system",
|
|
8536
|
-
mode: "collection",
|
|
8537
|
-
internal: true,
|
|
8538
|
-
methods: {
|
|
8539
|
-
/**
|
|
8540
|
-
* `internal: true` did not gate the mount (see the 2026-08-26 note on
|
|
8541
|
-
* `data-store-provider`) — this was reachable on the AppRouter by ANY
|
|
8542
|
-
* authenticated session at the default `auth: 'protected'`. The only
|
|
8543
|
-
* consumer, the `addon-pages-aggregator` builtin, reads it via
|
|
8544
|
-
* `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
|
|
8545
|
-
* only, never tRPC (per `ctx.capabilities` seeing only the local
|
|
8546
|
-
* process). The public, enriched listing admin-ui actually consumes is
|
|
8547
|
-
* the separate `addon-pages` cap, unaffected by this change.
|
|
8548
|
-
*/
|
|
8549
|
-
listPages: require_sleep.method(zod.z.void(), zod.z.array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
|
|
9196
|
+
var OPERATOR_SYMBOL = {
|
|
9197
|
+
eq: "=",
|
|
9198
|
+
ne: "≠",
|
|
9199
|
+
gt: ">",
|
|
9200
|
+
gte: "≥",
|
|
9201
|
+
lt: "<",
|
|
9202
|
+
lte: "≤",
|
|
9203
|
+
contains: "contains",
|
|
9204
|
+
in: "in"
|
|
8550
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
|
+
}
|
|
8551
9370
|
//#endregion
|
|
8552
|
-
//#region src/
|
|
8553
|
-
var
|
|
8554
|
-
|
|
8555
|
-
"
|
|
8556
|
-
"
|
|
8557
|
-
"PUT",
|
|
8558
|
-
"DELETE",
|
|
8559
|
-
"PATCH"
|
|
8560
|
-
]),
|
|
8561
|
-
path: zod.z.string(),
|
|
8562
|
-
access: zod.z.enum([
|
|
8563
|
-
"public",
|
|
8564
|
-
"authenticated",
|
|
8565
|
-
"admin"
|
|
8566
|
-
]).optional(),
|
|
8567
|
-
description: zod.z.string().optional(),
|
|
8568
|
-
/** The hub limits this route per client address before the handler runs. */
|
|
8569
|
-
rateLimit: zod.z.enum(["auth"]).optional()
|
|
8570
|
-
});
|
|
8571
|
-
/**
|
|
8572
|
-
* Cross-process route invocation envelope. The hub captures the
|
|
8573
|
-
* request as plain data, ships it to the worker via Moleculer, and
|
|
8574
|
-
* the worker runs the local handler against a capturing reply. The
|
|
8575
|
-
* envelope returned describes what the handler intended (status,
|
|
8576
|
-
* headers, body, or a redirect) so the hub can translate it back to
|
|
8577
|
-
* the Fastify reply that's actually wired to the socket.
|
|
8578
|
-
*/
|
|
8579
|
-
var InvokeRequestSchema = zod.z.object({
|
|
8580
|
-
method: zod.z.string(),
|
|
8581
|
-
path: zod.z.string(),
|
|
8582
|
-
params: zod.z.record(zod.z.string(), zod.z.string()),
|
|
8583
|
-
query: zod.z.record(zod.z.string(), zod.z.string()),
|
|
8584
|
-
body: zod.z.unknown(),
|
|
8585
|
-
headers: zod.z.record(zod.z.string(), zod.z.string()),
|
|
8586
|
-
user: zod.z.object({
|
|
8587
|
-
id: zod.z.string(),
|
|
8588
|
-
username: zod.z.string(),
|
|
8589
|
-
isAdmin: zod.z.boolean()
|
|
8590
|
-
}).optional(),
|
|
8591
|
-
scopedToken: zod.z.unknown().optional()
|
|
8592
|
-
});
|
|
8593
|
-
var InvokeReplyEnvelopeSchema = zod.z.object({
|
|
8594
|
-
status: zod.z.number().int(),
|
|
8595
|
-
headers: zod.z.record(zod.z.string(), zod.z.string()),
|
|
8596
|
-
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
8597
|
-
* sending `body`. Status defaults to 302 when this is set unless
|
|
8598
|
-
* the handler called `reply.code(...)` explicitly. */
|
|
8599
|
-
redirectUrl: zod.z.string().nullable(),
|
|
8600
|
-
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
8601
|
-
body: zod.z.unknown().optional(),
|
|
8602
|
-
/** Set when the handler called `reply.type(mime)`. */
|
|
8603
|
-
contentType: zod.z.string().optional()
|
|
8604
|
-
});
|
|
8605
|
-
var addonRoutesCapability = {
|
|
8606
|
-
name: "addon-routes",
|
|
8607
|
-
scope: "system",
|
|
8608
|
-
mode: "collection",
|
|
8609
|
-
internal: true,
|
|
8610
|
-
methods: {
|
|
8611
|
-
getRoutes: require_sleep.method(zod.z.void(), zod.z.array(AddonHttpRouteSchema)),
|
|
8612
|
-
/**
|
|
8613
|
-
* Cross-process dispatch entry point. Forked addons implement this
|
|
8614
|
-
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
8615
|
-
* can route through Moleculer when the handler lives in a worker.
|
|
8616
|
-
*
|
|
8617
|
-
* Local addons can implement it for free with the same helper;
|
|
8618
|
-
* the hub bypasses the wire on co-located addons.
|
|
8619
|
-
*/
|
|
8620
|
-
invoke: require_sleep.method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
9371
|
+
//#region src/catalogs/coco-classmap.ts
|
|
9372
|
+
var COCO_80_LABELS = [
|
|
9373
|
+
{
|
|
9374
|
+
id: "person",
|
|
9375
|
+
name: "Person"
|
|
8621
9376
|
},
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
"
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
9377
|
+
{
|
|
9378
|
+
id: "bicycle",
|
|
9379
|
+
name: "Bicycle"
|
|
9380
|
+
},
|
|
9381
|
+
{
|
|
9382
|
+
id: "car",
|
|
9383
|
+
name: "Car"
|
|
9384
|
+
},
|
|
9385
|
+
{
|
|
9386
|
+
id: "motorcycle",
|
|
9387
|
+
name: "Motorcycle"
|
|
9388
|
+
},
|
|
9389
|
+
{
|
|
9390
|
+
id: "airplane",
|
|
9391
|
+
name: "Airplane"
|
|
9392
|
+
},
|
|
9393
|
+
{
|
|
9394
|
+
id: "bus",
|
|
9395
|
+
name: "Bus"
|
|
9396
|
+
},
|
|
9397
|
+
{
|
|
9398
|
+
id: "train",
|
|
9399
|
+
name: "Train"
|
|
9400
|
+
},
|
|
9401
|
+
{
|
|
9402
|
+
id: "truck",
|
|
9403
|
+
name: "Truck"
|
|
9404
|
+
},
|
|
9405
|
+
{
|
|
9406
|
+
id: "boat",
|
|
9407
|
+
name: "Boat"
|
|
9408
|
+
},
|
|
9409
|
+
{
|
|
9410
|
+
id: "traffic light",
|
|
9411
|
+
name: "Traffic Light"
|
|
9412
|
+
},
|
|
9413
|
+
{
|
|
9414
|
+
id: "fire hydrant",
|
|
9415
|
+
name: "Fire Hydrant"
|
|
9416
|
+
},
|
|
9417
|
+
{
|
|
9418
|
+
id: "stop sign",
|
|
9419
|
+
name: "Stop Sign"
|
|
9420
|
+
},
|
|
9421
|
+
{
|
|
9422
|
+
id: "parking meter",
|
|
9423
|
+
name: "Parking Meter"
|
|
9424
|
+
},
|
|
9425
|
+
{
|
|
9426
|
+
id: "bench",
|
|
9427
|
+
name: "Bench"
|
|
9428
|
+
},
|
|
9429
|
+
{
|
|
9430
|
+
id: "bird",
|
|
9431
|
+
name: "Bird"
|
|
9432
|
+
},
|
|
9433
|
+
{
|
|
9434
|
+
id: "cat",
|
|
9435
|
+
name: "Cat"
|
|
9436
|
+
},
|
|
9437
|
+
{
|
|
9438
|
+
id: "dog",
|
|
9439
|
+
name: "Dog"
|
|
9440
|
+
},
|
|
9441
|
+
{
|
|
9442
|
+
id: "horse",
|
|
9443
|
+
name: "Horse"
|
|
9444
|
+
},
|
|
9445
|
+
{
|
|
9446
|
+
id: "sheep",
|
|
9447
|
+
name: "Sheep"
|
|
9448
|
+
},
|
|
9449
|
+
{
|
|
9450
|
+
id: "cow",
|
|
9451
|
+
name: "Cow"
|
|
9452
|
+
},
|
|
9453
|
+
{
|
|
9454
|
+
id: "elephant",
|
|
9455
|
+
name: "Elephant"
|
|
9456
|
+
},
|
|
9457
|
+
{
|
|
9458
|
+
id: "bear",
|
|
9459
|
+
name: "Bear"
|
|
9460
|
+
},
|
|
9461
|
+
{
|
|
9462
|
+
id: "zebra",
|
|
9463
|
+
name: "Zebra"
|
|
9464
|
+
},
|
|
9465
|
+
{
|
|
9466
|
+
id: "giraffe",
|
|
9467
|
+
name: "Giraffe"
|
|
9468
|
+
},
|
|
9469
|
+
{
|
|
9470
|
+
id: "backpack",
|
|
9471
|
+
name: "Backpack"
|
|
9472
|
+
},
|
|
9473
|
+
{
|
|
9474
|
+
id: "umbrella",
|
|
9475
|
+
name: "Umbrella"
|
|
9476
|
+
},
|
|
9477
|
+
{
|
|
9478
|
+
id: "handbag",
|
|
9479
|
+
name: "Handbag"
|
|
9480
|
+
},
|
|
9481
|
+
{
|
|
9482
|
+
id: "tie",
|
|
9483
|
+
name: "Tie"
|
|
9484
|
+
},
|
|
9485
|
+
{
|
|
9486
|
+
id: "suitcase",
|
|
9487
|
+
name: "Suitcase"
|
|
9488
|
+
},
|
|
9489
|
+
{
|
|
9490
|
+
id: "frisbee",
|
|
9491
|
+
name: "Frisbee"
|
|
9492
|
+
},
|
|
9493
|
+
{
|
|
9494
|
+
id: "skis",
|
|
9495
|
+
name: "Skis"
|
|
9496
|
+
},
|
|
9497
|
+
{
|
|
9498
|
+
id: "snowboard",
|
|
9499
|
+
name: "Snowboard"
|
|
9500
|
+
},
|
|
9501
|
+
{
|
|
9502
|
+
id: "sports ball",
|
|
9503
|
+
name: "Sports Ball"
|
|
9504
|
+
},
|
|
9505
|
+
{
|
|
9506
|
+
id: "kite",
|
|
9507
|
+
name: "Kite"
|
|
9508
|
+
},
|
|
9509
|
+
{
|
|
9510
|
+
id: "baseball bat",
|
|
9511
|
+
name: "Baseball Bat"
|
|
9512
|
+
},
|
|
9513
|
+
{
|
|
9514
|
+
id: "baseball glove",
|
|
9515
|
+
name: "Baseball Glove"
|
|
9516
|
+
},
|
|
9517
|
+
{
|
|
9518
|
+
id: "skateboard",
|
|
9519
|
+
name: "Skateboard"
|
|
9520
|
+
},
|
|
9521
|
+
{
|
|
9522
|
+
id: "surfboard",
|
|
9523
|
+
name: "Surfboard"
|
|
9524
|
+
},
|
|
9525
|
+
{
|
|
9526
|
+
id: "tennis racket",
|
|
9527
|
+
name: "Tennis Racket"
|
|
9528
|
+
},
|
|
9529
|
+
{
|
|
9530
|
+
id: "bottle",
|
|
9531
|
+
name: "Bottle"
|
|
9532
|
+
},
|
|
9533
|
+
{
|
|
9534
|
+
id: "wine glass",
|
|
9535
|
+
name: "Wine Glass"
|
|
9536
|
+
},
|
|
9537
|
+
{
|
|
9538
|
+
id: "cup",
|
|
9539
|
+
name: "Cup"
|
|
9540
|
+
},
|
|
9541
|
+
{
|
|
9542
|
+
id: "fork",
|
|
9543
|
+
name: "Fork"
|
|
9544
|
+
},
|
|
9545
|
+
{
|
|
9546
|
+
id: "knife",
|
|
9547
|
+
name: "Knife"
|
|
9548
|
+
},
|
|
9549
|
+
{
|
|
9550
|
+
id: "spoon",
|
|
9551
|
+
name: "Spoon"
|
|
9552
|
+
},
|
|
9553
|
+
{
|
|
9554
|
+
id: "bowl",
|
|
9555
|
+
name: "Bowl"
|
|
9556
|
+
},
|
|
9557
|
+
{
|
|
9558
|
+
id: "banana",
|
|
9559
|
+
name: "Banana"
|
|
9560
|
+
},
|
|
9561
|
+
{
|
|
9562
|
+
id: "apple",
|
|
9563
|
+
name: "Apple"
|
|
9564
|
+
},
|
|
9565
|
+
{
|
|
9566
|
+
id: "sandwich",
|
|
9567
|
+
name: "Sandwich"
|
|
9568
|
+
},
|
|
9569
|
+
{
|
|
9570
|
+
id: "orange",
|
|
9571
|
+
name: "Orange"
|
|
9572
|
+
},
|
|
9573
|
+
{
|
|
9574
|
+
id: "broccoli",
|
|
9575
|
+
name: "Broccoli"
|
|
9576
|
+
},
|
|
9577
|
+
{
|
|
9578
|
+
id: "carrot",
|
|
9579
|
+
name: "Carrot"
|
|
9580
|
+
},
|
|
9581
|
+
{
|
|
9582
|
+
id: "hot dog",
|
|
9583
|
+
name: "Hot Dog"
|
|
9584
|
+
},
|
|
9585
|
+
{
|
|
9586
|
+
id: "pizza",
|
|
9587
|
+
name: "Pizza"
|
|
9588
|
+
},
|
|
9589
|
+
{
|
|
9590
|
+
id: "donut",
|
|
9591
|
+
name: "Donut"
|
|
9592
|
+
},
|
|
9593
|
+
{
|
|
9594
|
+
id: "cake",
|
|
9595
|
+
name: "Cake"
|
|
9596
|
+
},
|
|
9597
|
+
{
|
|
9598
|
+
id: "chair",
|
|
9599
|
+
name: "Chair"
|
|
9600
|
+
},
|
|
9601
|
+
{
|
|
9602
|
+
id: "couch",
|
|
9603
|
+
name: "Couch"
|
|
9604
|
+
},
|
|
9605
|
+
{
|
|
9606
|
+
id: "potted plant",
|
|
9607
|
+
name: "Potted Plant"
|
|
9608
|
+
},
|
|
9609
|
+
{
|
|
9610
|
+
id: "bed",
|
|
9611
|
+
name: "Bed"
|
|
9612
|
+
},
|
|
9613
|
+
{
|
|
9614
|
+
id: "dining table",
|
|
9615
|
+
name: "Dining Table"
|
|
9616
|
+
},
|
|
9617
|
+
{
|
|
9618
|
+
id: "toilet",
|
|
9619
|
+
name: "Toilet"
|
|
9620
|
+
},
|
|
9621
|
+
{
|
|
9622
|
+
id: "tv",
|
|
9623
|
+
name: "TV"
|
|
9624
|
+
},
|
|
9625
|
+
{
|
|
9626
|
+
id: "laptop",
|
|
9627
|
+
name: "Laptop"
|
|
9628
|
+
},
|
|
9629
|
+
{
|
|
9630
|
+
id: "mouse",
|
|
9631
|
+
name: "Mouse"
|
|
9632
|
+
},
|
|
9633
|
+
{
|
|
9634
|
+
id: "remote",
|
|
9635
|
+
name: "Remote"
|
|
9636
|
+
},
|
|
9637
|
+
{
|
|
9638
|
+
id: "keyboard",
|
|
9639
|
+
name: "Keyboard"
|
|
9640
|
+
},
|
|
9641
|
+
{
|
|
9642
|
+
id: "cell phone",
|
|
9643
|
+
name: "Cell Phone"
|
|
9644
|
+
},
|
|
9645
|
+
{
|
|
9646
|
+
id: "microwave",
|
|
9647
|
+
name: "Microwave"
|
|
9648
|
+
},
|
|
9649
|
+
{
|
|
9650
|
+
id: "oven",
|
|
9651
|
+
name: "Oven"
|
|
9652
|
+
},
|
|
9653
|
+
{
|
|
9654
|
+
id: "toaster",
|
|
9655
|
+
name: "Toaster"
|
|
9656
|
+
},
|
|
9657
|
+
{
|
|
9658
|
+
id: "sink",
|
|
9659
|
+
name: "Sink"
|
|
9660
|
+
},
|
|
9661
|
+
{
|
|
9662
|
+
id: "refrigerator",
|
|
9663
|
+
name: "Refrigerator"
|
|
9664
|
+
},
|
|
9665
|
+
{
|
|
9666
|
+
id: "book",
|
|
9667
|
+
name: "Book"
|
|
9668
|
+
},
|
|
9669
|
+
{
|
|
9670
|
+
id: "clock",
|
|
9671
|
+
name: "Clock"
|
|
9672
|
+
},
|
|
9673
|
+
{
|
|
9674
|
+
id: "vase",
|
|
9675
|
+
name: "Vase"
|
|
9676
|
+
},
|
|
9677
|
+
{
|
|
9678
|
+
id: "scissors",
|
|
9679
|
+
name: "Scissors"
|
|
9680
|
+
},
|
|
9681
|
+
{
|
|
9682
|
+
id: "teddy bear",
|
|
9683
|
+
name: "Teddy Bear"
|
|
9684
|
+
},
|
|
9685
|
+
{
|
|
9686
|
+
id: "hair drier",
|
|
9687
|
+
name: "Hair Drier"
|
|
9688
|
+
},
|
|
9689
|
+
{
|
|
9690
|
+
id: "toothbrush",
|
|
9691
|
+
name: "Toothbrush"
|
|
9692
|
+
}
|
|
9693
|
+
];
|
|
9694
|
+
var MACRO_LABELS = [
|
|
9695
|
+
{
|
|
9696
|
+
id: "person",
|
|
9697
|
+
name: "Person"
|
|
9698
|
+
},
|
|
9699
|
+
{
|
|
9700
|
+
id: "vehicle",
|
|
9701
|
+
name: "Vehicle"
|
|
9702
|
+
},
|
|
9703
|
+
{
|
|
9704
|
+
id: "animal",
|
|
9705
|
+
name: "Animal"
|
|
9706
|
+
},
|
|
9707
|
+
{
|
|
9708
|
+
id: "package",
|
|
9709
|
+
name: "Package"
|
|
9710
|
+
}
|
|
9711
|
+
];
|
|
8664
9712
|
/**
|
|
8665
|
-
*
|
|
9713
|
+
* The class names a per-class COUNT can ever be keyed on — the macro ids, and
|
|
9714
|
+
* only those.
|
|
8666
9715
|
*
|
|
8667
|
-
*
|
|
8668
|
-
*
|
|
8669
|
-
*
|
|
8670
|
-
*
|
|
9716
|
+
* `preserveOriginal: false` below is the whole reason this set exists: the
|
|
9717
|
+
* executor maps each raw COCO label onto its macro and DROPS everything else,
|
|
9718
|
+
* so a tracked detection's `className` is always one of {@link MACRO_LABELS}.
|
|
9719
|
+
* Any surface that indexes counts by class name — the ZoneAnalytics occupancy
|
|
9720
|
+
* snapshot's `byClass`, and therefore every occupancy rule — can match nothing
|
|
9721
|
+
* else. Derived from the catalog, never hand-listed, so a new macro joins it by
|
|
9722
|
+
* being declared once.
|
|
8671
9723
|
*
|
|
8672
|
-
*
|
|
8673
|
-
*
|
|
8674
|
-
*
|
|
9724
|
+
* Cost of not having it, 2026-08-12: the notification rule editor offered the
|
|
9725
|
+
* full taxonomy for an occupancy class (occupancy is the one condition that
|
|
9726
|
+
* does not go through the engine's macro-expanding `expandClassSelector`), two
|
|
9727
|
+
* live rules were authored on `car`, and both counters read 0 forever.
|
|
9728
|
+
*/
|
|
9729
|
+
var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
|
|
9730
|
+
/** True when `className` is a macro the detector can actually emit (and hence a
|
|
9731
|
+
* class a count can be keyed on) — see {@link DETECTION_MACRO_CLASSES}. */
|
|
9732
|
+
function isDetectionMacroClass(className) {
|
|
9733
|
+
return DETECTION_MACRO_CLASSES.has(className.trim().toLowerCase());
|
|
9734
|
+
}
|
|
9735
|
+
var COCO_TO_MACRO = {
|
|
9736
|
+
mapping: {
|
|
9737
|
+
person: "person",
|
|
9738
|
+
bicycle: "vehicle",
|
|
9739
|
+
car: "vehicle",
|
|
9740
|
+
motorcycle: "vehicle",
|
|
9741
|
+
airplane: "vehicle",
|
|
9742
|
+
bus: "vehicle",
|
|
9743
|
+
train: "vehicle",
|
|
9744
|
+
truck: "vehicle",
|
|
9745
|
+
boat: "vehicle",
|
|
9746
|
+
bird: "animal",
|
|
9747
|
+
cat: "animal",
|
|
9748
|
+
dog: "animal",
|
|
9749
|
+
horse: "animal",
|
|
9750
|
+
sheep: "animal",
|
|
9751
|
+
cow: "animal",
|
|
9752
|
+
elephant: "animal",
|
|
9753
|
+
bear: "animal",
|
|
9754
|
+
zebra: "animal",
|
|
9755
|
+
giraffe: "animal",
|
|
9756
|
+
suitcase: "package",
|
|
9757
|
+
backpack: "package",
|
|
9758
|
+
handbag: "package"
|
|
9759
|
+
},
|
|
9760
|
+
preserveOriginal: false
|
|
9761
|
+
};
|
|
9762
|
+
var MACRO_NAMES = new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
9763
|
+
function macroOf(className) {
|
|
9764
|
+
return COCO_TO_MACRO.mapping[className] ?? className;
|
|
9765
|
+
}
|
|
9766
|
+
/** `animal` matches a dog; `dog` matches only a dog (a cat is also an animal). */
|
|
9767
|
+
function detectionClassMatches(wanted, actual) {
|
|
9768
|
+
const w = wanted.toLowerCase();
|
|
9769
|
+
const a = actual.toLowerCase();
|
|
9770
|
+
if (w === a) return true;
|
|
9771
|
+
return MACRO_NAMES.has(w) && macroOf(a) === w;
|
|
9772
|
+
}
|
|
9773
|
+
function isPhase(value) {
|
|
9774
|
+
return value === "start" || value === "update" || value === "end";
|
|
9775
|
+
}
|
|
9776
|
+
var DetectionTriggerFilter = class {
|
|
9777
|
+
trigger;
|
|
9778
|
+
/** `${deviceId}|${trackId}` → fired at. Insertion-ordered, so the oldest is first. */
|
|
9779
|
+
fired = /* @__PURE__ */ new Map();
|
|
9780
|
+
constructor(trigger) {
|
|
9781
|
+
this.trigger = trigger;
|
|
9782
|
+
}
|
|
9783
|
+
get firedCount() {
|
|
9784
|
+
return this.fired.size;
|
|
9785
|
+
}
|
|
9786
|
+
evaluate(event, now) {
|
|
9787
|
+
const { deviceId, trackId, phase } = event;
|
|
9788
|
+
if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase(phase)) return {
|
|
9789
|
+
kind: "ignore",
|
|
9790
|
+
why: "malformed track-lifecycle event"
|
|
9791
|
+
};
|
|
9792
|
+
if (!this.trigger.deviceIds.includes(deviceId)) return {
|
|
9793
|
+
kind: "ignore",
|
|
9794
|
+
why: `device ${deviceId} is not one of the trigger's devices`
|
|
9795
|
+
};
|
|
9796
|
+
const key = `${deviceId}|${trackId}`;
|
|
9797
|
+
if (phase === "end") {
|
|
9798
|
+
this.fired.delete(key);
|
|
9799
|
+
return {
|
|
9800
|
+
kind: "ignore",
|
|
9801
|
+
why: "track ended"
|
|
9802
|
+
};
|
|
9803
|
+
}
|
|
9804
|
+
this.expire(now);
|
|
9805
|
+
const className = typeof event.bestClassName === "string" ? event.bestClassName : "";
|
|
9806
|
+
const score = typeof event.bestConfidence === "number" ? event.bestConfidence : 0;
|
|
9807
|
+
const vars = {
|
|
9808
|
+
deviceId,
|
|
9809
|
+
className,
|
|
9810
|
+
trackId,
|
|
9811
|
+
score
|
|
9812
|
+
};
|
|
9813
|
+
if (this.fired.has(key)) return {
|
|
9814
|
+
kind: "reject",
|
|
9815
|
+
reason: `already fired for track ${trackId}`,
|
|
9816
|
+
vars
|
|
9817
|
+
};
|
|
9818
|
+
if (!this.trigger.classes.some((c) => detectionClassMatches(c, className))) return {
|
|
9819
|
+
kind: "reject",
|
|
9820
|
+
reason: `class ${className || "unknown"} not in [${this.trigger.classes.join(", ")}]`,
|
|
9821
|
+
vars
|
|
9822
|
+
};
|
|
9823
|
+
if (this.trigger.minScore !== void 0 && score < this.trigger.minScore) return {
|
|
9824
|
+
kind: "reject",
|
|
9825
|
+
reason: `score ${score} < ${this.trigger.minScore}`,
|
|
9826
|
+
vars
|
|
9827
|
+
};
|
|
9828
|
+
if (this.trigger.zones !== void 0) {
|
|
9829
|
+
const visited = Array.isArray(event.zonesVisited) ? event.zonesVisited.filter((z) => typeof z === "string") : [];
|
|
9830
|
+
if (!this.trigger.zones.some((z) => visited.includes(z))) return {
|
|
9831
|
+
kind: "reject",
|
|
9832
|
+
reason: `zone mismatch: visited [${visited.join(", ")}], wanted [${this.trigger.zones.join(", ")}]`,
|
|
9833
|
+
vars
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
this.remember(key, now);
|
|
9837
|
+
return {
|
|
9838
|
+
kind: "fire",
|
|
9839
|
+
reason: `${className} ${score} on device ${deviceId}`,
|
|
9840
|
+
vars
|
|
9841
|
+
};
|
|
9842
|
+
}
|
|
9843
|
+
remember(key, now) {
|
|
9844
|
+
this.fired.set(key, now);
|
|
9845
|
+
while (this.fired.size > 512) {
|
|
9846
|
+
const oldest = this.fired.keys().next();
|
|
9847
|
+
if (oldest.done === true) break;
|
|
9848
|
+
this.fired.delete(oldest.value);
|
|
9849
|
+
}
|
|
9850
|
+
}
|
|
9851
|
+
expire(now) {
|
|
9852
|
+
for (const [key, at] of this.fired) {
|
|
9853
|
+
if (now - at < 9e5) break;
|
|
9854
|
+
this.fired.delete(key);
|
|
9855
|
+
}
|
|
9856
|
+
}
|
|
9857
|
+
};
|
|
9858
|
+
//#endregion
|
|
9859
|
+
//#region src/automation/run-gate.ts
|
|
9860
|
+
var MINUTE_MS$1 = 6e4;
|
|
9861
|
+
function minutes(ms) {
|
|
9862
|
+
return (ms / MINUTE_MS$1).toFixed(1);
|
|
9863
|
+
}
|
|
9864
|
+
var RunGate = class {
|
|
9865
|
+
mode;
|
|
9866
|
+
active = 0;
|
|
9867
|
+
pending = 0;
|
|
9868
|
+
constructor(mode) {
|
|
9869
|
+
this.mode = mode;
|
|
9870
|
+
}
|
|
9871
|
+
get activeRuns() {
|
|
9872
|
+
return this.active;
|
|
9873
|
+
}
|
|
9874
|
+
get queuedTriggers() {
|
|
9875
|
+
return this.pending;
|
|
9876
|
+
}
|
|
9877
|
+
admit() {
|
|
9878
|
+
const mode = this.mode;
|
|
9879
|
+
switch (mode.mode) {
|
|
9880
|
+
case "single":
|
|
9881
|
+
case "cooldown":
|
|
9882
|
+
if (this.active > 0) return {
|
|
9883
|
+
decision: "drop",
|
|
9884
|
+
reason: `${mode.mode}: a run is already in progress`
|
|
9885
|
+
};
|
|
9886
|
+
this.active += 1;
|
|
9887
|
+
return {
|
|
9888
|
+
decision: "run",
|
|
9889
|
+
reason: `${mode.mode}: no run in progress`
|
|
9890
|
+
};
|
|
9891
|
+
case "restart":
|
|
9892
|
+
if (this.active > 0) {
|
|
9893
|
+
this.active += 1;
|
|
9894
|
+
return {
|
|
9895
|
+
decision: "restart",
|
|
9896
|
+
reason: `restart: aborting ${this.active - 1} run(s) in progress`
|
|
9897
|
+
};
|
|
9898
|
+
}
|
|
9899
|
+
this.active += 1;
|
|
9900
|
+
return {
|
|
9901
|
+
decision: "run",
|
|
9902
|
+
reason: "restart: no run in progress"
|
|
9903
|
+
};
|
|
9904
|
+
case "queued":
|
|
9905
|
+
if (this.active === 0) {
|
|
9906
|
+
this.active += 1;
|
|
9907
|
+
return {
|
|
9908
|
+
decision: "run",
|
|
9909
|
+
reason: "queued: no run in progress"
|
|
9910
|
+
};
|
|
9911
|
+
}
|
|
9912
|
+
if (this.pending < mode.max) {
|
|
9913
|
+
this.pending += 1;
|
|
9914
|
+
return {
|
|
9915
|
+
decision: "queued",
|
|
9916
|
+
reason: `queued: a run is in progress, waiting ${this.pending} of ${mode.max}`
|
|
9917
|
+
};
|
|
9918
|
+
}
|
|
9919
|
+
return {
|
|
9920
|
+
decision: "drop",
|
|
9921
|
+
reason: `queued: a run is in progress and the queue is full (${mode.max})`
|
|
9922
|
+
};
|
|
9923
|
+
case "parallel":
|
|
9924
|
+
if (this.active < mode.max) {
|
|
9925
|
+
this.active += 1;
|
|
9926
|
+
return {
|
|
9927
|
+
decision: "run",
|
|
9928
|
+
reason: `parallel: ${this.active} of ${mode.max} running`
|
|
9929
|
+
};
|
|
9930
|
+
}
|
|
9931
|
+
return {
|
|
9932
|
+
decision: "drop",
|
|
9933
|
+
reason: `parallel: ${mode.max} runs already in progress`
|
|
9934
|
+
};
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
/** The run holding a slot ended (any outcome). True = a queued trigger took the slot. */
|
|
9938
|
+
release() {
|
|
9939
|
+
if (this.active > 0) this.active -= 1;
|
|
9940
|
+
if (this.mode.mode === "queued" && this.pending > 0) {
|
|
9941
|
+
this.pending -= 1;
|
|
9942
|
+
this.active += 1;
|
|
9943
|
+
return true;
|
|
9944
|
+
}
|
|
9945
|
+
return false;
|
|
9946
|
+
}
|
|
9947
|
+
/** Checked AFTER the conditions pass. Always `ok` outside `cooldown` mode. */
|
|
9948
|
+
cooldown(input) {
|
|
9949
|
+
if (this.mode.mode !== "cooldown") return {
|
|
9950
|
+
ok: true,
|
|
9951
|
+
bypassed: false,
|
|
9952
|
+
reason: ""
|
|
9953
|
+
};
|
|
9954
|
+
const waitMs = this.mode.minutes * MINUTE_MS$1;
|
|
9955
|
+
if (input.lastRunStartedAt === null) return {
|
|
9956
|
+
ok: true,
|
|
9957
|
+
bypassed: false,
|
|
9958
|
+
reason: `cooldown ${this.mode.minutes} min: no earlier run`
|
|
9959
|
+
};
|
|
9960
|
+
const elapsed = input.now - input.lastRunStartedAt;
|
|
9961
|
+
if (elapsed >= waitMs) return {
|
|
9962
|
+
ok: true,
|
|
9963
|
+
bypassed: false,
|
|
9964
|
+
reason: `cooldown ${this.mode.minutes} min: last run started ${minutes(elapsed)} min ago`
|
|
9965
|
+
};
|
|
9966
|
+
const left = `${minutes(waitMs - elapsed)} min left`;
|
|
9967
|
+
if (input.manual) return {
|
|
9968
|
+
ok: true,
|
|
9969
|
+
bypassed: true,
|
|
9970
|
+
reason: `cooldown ${this.mode.minutes} min bypassed by a manual trigger (${left})`
|
|
9971
|
+
};
|
|
9972
|
+
return {
|
|
9973
|
+
ok: false,
|
|
9974
|
+
bypassed: false,
|
|
9975
|
+
reason: `cooldown ${this.mode.minutes} min: last run started ${minutes(elapsed)} min ago, ${left}`
|
|
9976
|
+
};
|
|
9977
|
+
}
|
|
9978
|
+
};
|
|
9979
|
+
//#endregion
|
|
9980
|
+
//#region src/automation/trace.ts
|
|
9981
|
+
/**
|
|
9982
|
+
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
9983
|
+
* so the editor can paint every block green or red with its reason.
|
|
8675
9984
|
*
|
|
8676
|
-
*
|
|
8677
|
-
*
|
|
8678
|
-
*
|
|
8679
|
-
*
|
|
9985
|
+
* A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
|
|
9986
|
+
* every condition node (containers and leaves, including the ones a
|
|
9987
|
+
* short-circuit never reached), the run-mode verdict and every action step —
|
|
9988
|
+
* each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
|
|
9989
|
+
* for an Action run). Skipped
|
|
9990
|
+
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
9991
|
+
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
8680
9992
|
*
|
|
8681
|
-
*
|
|
8682
|
-
*
|
|
9993
|
+
* {@link AutomationStepTraceSchema} is deliberately free of automation
|
|
9994
|
+
* vocabulary so an Action run records the same step shape.
|
|
8683
9995
|
*/
|
|
9996
|
+
/** A value a trace may show: JSON primitives and short lists, nothing deeper. */
|
|
9997
|
+
var TraceValueSchema = zod.z.union([
|
|
9998
|
+
zod.z.string(),
|
|
9999
|
+
zod.z.number(),
|
|
10000
|
+
zod.z.boolean(),
|
|
10001
|
+
zod.z.null(),
|
|
10002
|
+
zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
|
|
10003
|
+
]);
|
|
10004
|
+
var AutomationRunOutcomeSchema = zod.z.enum([
|
|
10005
|
+
"ran",
|
|
10006
|
+
"skipped-trigger",
|
|
10007
|
+
"skipped-conditions",
|
|
10008
|
+
"skipped-cooldown",
|
|
10009
|
+
"skipped-overlap",
|
|
10010
|
+
"failed",
|
|
10011
|
+
"aborted"
|
|
10012
|
+
]);
|
|
10013
|
+
/** `not-evaluated` = a short-circuit never reached the node. */
|
|
10014
|
+
var TraceNodeOutcomeSchema = zod.z.enum([
|
|
10015
|
+
"pass",
|
|
10016
|
+
"fail",
|
|
10017
|
+
"not-evaluated"
|
|
10018
|
+
]);
|
|
10019
|
+
var AutomationTriggerTraceSchema = zod.z.object({
|
|
10020
|
+
kind: zod.z.string().min(1),
|
|
10021
|
+
outcome: zod.z.enum(["pass", "fail"]),
|
|
10022
|
+
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
10023
|
+
reason: zod.z.string(),
|
|
10024
|
+
/** deviceId, className, trackId, score … — only what the trigger exposes. */
|
|
10025
|
+
vars: zod.z.record(zod.z.string(), TraceValueSchema)
|
|
10026
|
+
});
|
|
10027
|
+
var AutomationConditionTraceNodeSchema = zod.z.object({
|
|
10028
|
+
/** `conditions` for the root, `conditions.<i>` for its i-th child, … */
|
|
10029
|
+
path: zod.z.string().min(1),
|
|
10030
|
+
kind: zod.z.enum([
|
|
10031
|
+
"all",
|
|
10032
|
+
"any",
|
|
10033
|
+
"not",
|
|
10034
|
+
"condition",
|
|
10035
|
+
"expression"
|
|
10036
|
+
]),
|
|
10037
|
+
outcome: TraceNodeOutcomeSchema,
|
|
10038
|
+
reason: zod.z.string(),
|
|
10039
|
+
deviceId: zod.z.number().int().optional(),
|
|
10040
|
+
cap: zod.z.string().optional(),
|
|
10041
|
+
fieldPath: zod.z.string().optional(),
|
|
10042
|
+
operator: zod.z.string().optional(),
|
|
10043
|
+
expected: TraceValueSchema.optional(),
|
|
10044
|
+
/** What the mirror held; `null` when it held nothing (unknown). */
|
|
10045
|
+
observed: TraceValueSchema.optional()
|
|
10046
|
+
});
|
|
10047
|
+
var AutomationRunVerdictSchema = zod.z.object({
|
|
10048
|
+
mode: AutomationRunModeSchema,
|
|
10049
|
+
decision: zod.z.enum([
|
|
10050
|
+
"run",
|
|
10051
|
+
"queued",
|
|
10052
|
+
"restart",
|
|
10053
|
+
"drop",
|
|
10054
|
+
"cooldown",
|
|
10055
|
+
"bypass"
|
|
10056
|
+
]),
|
|
10057
|
+
reason: zod.z.string()
|
|
10058
|
+
});
|
|
10059
|
+
/** One executed (or not-run) step — shared by automations and Action runs. */
|
|
10060
|
+
var AutomationStepTraceSchema = zod.z.object({
|
|
10061
|
+
/** `actions.<i>` */
|
|
10062
|
+
path: zod.z.string().min(1),
|
|
10063
|
+
kind: zod.z.string().min(1),
|
|
10064
|
+
cap: zod.z.string().optional(),
|
|
10065
|
+
method: zod.z.string().optional(),
|
|
10066
|
+
deviceId: zod.z.number().int().optional(),
|
|
10067
|
+
startedAt: zod.z.number().optional(),
|
|
10068
|
+
endedAt: zod.z.number().optional(),
|
|
10069
|
+
outcome: zod.z.enum([
|
|
10070
|
+
"ok",
|
|
10071
|
+
"error",
|
|
10072
|
+
"not-run"
|
|
10073
|
+
]),
|
|
10074
|
+
message: zod.z.string().optional()
|
|
10075
|
+
});
|
|
10076
|
+
var AutomationRunTraceSchema = zod.z.object({
|
|
10077
|
+
runId: zod.z.string().min(1),
|
|
10078
|
+
startedAt: zod.z.number(),
|
|
10079
|
+
endedAt: zod.z.number(),
|
|
10080
|
+
trigger: AutomationTriggerTraceSchema,
|
|
10081
|
+
conditions: zod.z.array(AutomationConditionTraceNodeSchema),
|
|
10082
|
+
verdict: AutomationRunVerdictSchema.optional(),
|
|
10083
|
+
steps: zod.z.array(AutomationStepTraceSchema),
|
|
10084
|
+
outcome: AutomationRunOutcomeSchema,
|
|
10085
|
+
/** How many identical consecutive skips this trace stands for (>= 1). */
|
|
10086
|
+
repeat: zod.z.number().int().min(1),
|
|
10087
|
+
/** When the latest of those repeats happened. */
|
|
10088
|
+
lastAt: zod.z.number()
|
|
10089
|
+
});
|
|
8684
10090
|
/**
|
|
8685
|
-
* `
|
|
8686
|
-
*
|
|
8687
|
-
* internal dispatcher, not a routing hint for the cap-router. See
|
|
8688
|
-
* `CapabilityDefinition.nodeIdMode` for the contract.
|
|
10091
|
+
* What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
|
|
10092
|
+
* `action:<actionId>`): the cooldown stamp and the trace ring.
|
|
8689
10093
|
*/
|
|
8690
|
-
var
|
|
8691
|
-
|
|
10094
|
+
var AutomationStateDocSchema = zod.z.object({
|
|
10095
|
+
version: zod.z.literal(1),
|
|
10096
|
+
lastRunStartedAt: zod.z.number().nullable(),
|
|
10097
|
+
traces: zod.z.array(AutomationRunTraceSchema)
|
|
10098
|
+
});
|
|
10099
|
+
/** The state of an owner that never reported. */
|
|
10100
|
+
var EMPTY_AUTOMATION_STATE = {
|
|
10101
|
+
version: 1,
|
|
10102
|
+
lastRunStartedAt: null,
|
|
10103
|
+
traces: []
|
|
10104
|
+
};
|
|
10105
|
+
function isSkippedTrace(trace) {
|
|
10106
|
+
return trace.outcome.startsWith("skipped-");
|
|
10107
|
+
}
|
|
10108
|
+
/** Numbers vary between repeats of the same cause ("0.41 < 0.6" / "0.43 < 0.6"). */
|
|
10109
|
+
function normaliseReason(reason) {
|
|
10110
|
+
return reason.replace(/\d+(\.\d+)?/g, "#");
|
|
10111
|
+
}
|
|
10112
|
+
/** Two skips with the same signature are the same event repeating. */
|
|
10113
|
+
function skipSignature(trace) {
|
|
10114
|
+
return JSON.stringify([
|
|
10115
|
+
trace.outcome,
|
|
10116
|
+
trace.trigger.kind,
|
|
10117
|
+
trace.trigger.outcome,
|
|
10118
|
+
normaliseReason(trace.trigger.reason),
|
|
10119
|
+
trace.verdict === void 0 ? null : [trace.verdict.decision, normaliseReason(trace.verdict.reason)],
|
|
10120
|
+
trace.conditions.map((n) => [n.path, n.outcome])
|
|
10121
|
+
]);
|
|
10122
|
+
}
|
|
10123
|
+
/**
|
|
10124
|
+
* `next` folded into `prev` when both are skips of the same signature (the
|
|
10125
|
+
* repeat count and the latest time move on), else null. Used by the block
|
|
10126
|
+
* BEFORE it sends, so a trigger that fails every few seconds costs one RPC per
|
|
10127
|
+
* flush, not one per skip; the ledger applies the same rule through
|
|
10128
|
+
* {@link appendTrace}.
|
|
10129
|
+
*/
|
|
10130
|
+
function mergeSkipTraces(prev, next) {
|
|
10131
|
+
if (!isSkippedTrace(prev) || !isSkippedTrace(next)) return null;
|
|
10132
|
+
if (skipSignature(prev) !== skipSignature(next)) return null;
|
|
10133
|
+
return {
|
|
10134
|
+
...prev,
|
|
10135
|
+
repeat: prev.repeat + next.repeat,
|
|
10136
|
+
lastAt: next.lastAt,
|
|
10137
|
+
endedAt: next.endedAt
|
|
10138
|
+
};
|
|
10139
|
+
}
|
|
10140
|
+
/**
|
|
10141
|
+
* Append a trace to a ring of at most `keep` entries, without mutating it.
|
|
10142
|
+
*
|
|
10143
|
+
* - A skip identical to the previous trace is counted on that trace
|
|
10144
|
+
* (`repeat`, `lastAt`), not appended.
|
|
10145
|
+
* - When the ring is full, the oldest SKIPPED trace goes first; a real run is
|
|
10146
|
+
* evicted only when every trace is a run. A noisy skip stream therefore
|
|
10147
|
+
* cannot push the runs the operator is looking for out of the ring.
|
|
10148
|
+
*/
|
|
10149
|
+
function appendTrace(ring, trace, keep) {
|
|
10150
|
+
const last = ring[ring.length - 1];
|
|
10151
|
+
const merged = last === void 0 ? null : mergeSkipTraces(last, trace);
|
|
10152
|
+
if (merged !== null) return [...ring.slice(0, -1), merged];
|
|
10153
|
+
const next = [...ring, trace];
|
|
10154
|
+
while (next.length > keep) {
|
|
10155
|
+
const skipAt = next.findIndex((t, i) => i < next.length - 1 && isSkippedTrace(t));
|
|
10156
|
+
next.splice(skipAt === -1 ? 0 : skipAt, 1);
|
|
10157
|
+
}
|
|
10158
|
+
return next;
|
|
10159
|
+
}
|
|
10160
|
+
/**
|
|
10161
|
+
* The trigger stage of a trace from the bag a trigger handed to `run()`:
|
|
10162
|
+
* primitives only (a device-state `to` that is an object is left out), minus the
|
|
10163
|
+
* bookkeeping keys `kind` and `at`.
|
|
10164
|
+
*/
|
|
10165
|
+
function buildTriggerTrace(trigger, outcome, reason) {
|
|
10166
|
+
const vars = {};
|
|
10167
|
+
for (const [key, value] of Object.entries(trigger)) {
|
|
10168
|
+
if (key === "kind" || key === "at") continue;
|
|
10169
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
|
|
10170
|
+
}
|
|
10171
|
+
return {
|
|
10172
|
+
kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
|
|
10173
|
+
outcome,
|
|
10174
|
+
reason,
|
|
10175
|
+
vars
|
|
10176
|
+
};
|
|
10177
|
+
}
|
|
10178
|
+
//#endregion
|
|
10179
|
+
//#region src/automation/action.ts
|
|
10180
|
+
/**
|
|
10181
|
+
* Actions — named, runnable things an operator (or an automation) can start,
|
|
10182
|
+
* declared as LIST ENTRIES, never as devices.
|
|
10183
|
+
*
|
|
10184
|
+
* An entry is either **internal** (a short list of cap/wait steps kept in the
|
|
10185
|
+
* hub's `actions` record kind) or declared by an addon through the
|
|
10186
|
+
* `action-source` collection (a Home Assistant script, a scene …). The one
|
|
10187
|
+
* surface the UI and automations talk to is the `actions` cap; it keeps no
|
|
10188
|
+
* catalog of its own (D3) — every `list` asks the providers live.
|
|
10189
|
+
*
|
|
10190
|
+
* This file is the data half: the entry schema, the variables an entry may
|
|
10191
|
+
* declare, and the pure resolution of the values a run was given. It holds no
|
|
10192
|
+
* runtime.
|
|
10193
|
+
*/
|
|
10194
|
+
/** What a variable's value is. `select` takes one of `options`. */
|
|
10195
|
+
var ActionVariableTypeSchema = zod.z.enum([
|
|
10196
|
+
"string",
|
|
10197
|
+
"number",
|
|
10198
|
+
"boolean",
|
|
10199
|
+
"select"
|
|
10200
|
+
]);
|
|
10201
|
+
var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10202
|
+
var ActionVariableValueSchema = zod.z.union([
|
|
10203
|
+
zod.z.string(),
|
|
10204
|
+
zod.z.number(),
|
|
10205
|
+
zod.z.boolean()
|
|
10206
|
+
]);
|
|
10207
|
+
/**
|
|
10208
|
+
* One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
|
|
10209
|
+
* value) or `{{vars.name}}` (inside a string) in their args.
|
|
10210
|
+
*/
|
|
10211
|
+
var ActionVariableSchema = zod.z.object({
|
|
10212
|
+
name: zod.z.string().regex(ACTION_VARIABLE_NAME_RE),
|
|
10213
|
+
label: zod.z.string().min(1).max(120).optional(),
|
|
10214
|
+
type: ActionVariableTypeSchema,
|
|
10215
|
+
required: zod.z.boolean().optional(),
|
|
10216
|
+
default: ActionVariableValueSchema.optional(),
|
|
10217
|
+
/** The allowed values of a `select`. */
|
|
10218
|
+
options: zod.z.array(zod.z.string().min(1)).min(1).optional()
|
|
10219
|
+
}).superRefine((v, ctx) => {
|
|
10220
|
+
if (v.type === "select" && v.options === void 0) ctx.addIssue({
|
|
10221
|
+
code: "custom",
|
|
10222
|
+
message: "a select variable needs options",
|
|
10223
|
+
path: ["options"]
|
|
10224
|
+
});
|
|
10225
|
+
if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
|
|
10226
|
+
code: "custom",
|
|
10227
|
+
message: variableValueError(v, v.default) ?? "invalid default",
|
|
10228
|
+
path: ["default"]
|
|
10229
|
+
});
|
|
10230
|
+
});
|
|
10231
|
+
/** Why `value` is not a legal value for the variable, or null when it is. */
|
|
10232
|
+
function variableValueError(variable, value) {
|
|
10233
|
+
switch (variable.type) {
|
|
10234
|
+
case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
|
|
10235
|
+
case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
|
|
10236
|
+
case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
|
|
10237
|
+
case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
|
|
10238
|
+
}
|
|
10239
|
+
}
|
|
10240
|
+
/**
|
|
10241
|
+
* The values a run starts with: what the caller gave, checked against the
|
|
10242
|
+
* declaration, with defaults filled. A value the entry did not declare is
|
|
10243
|
+
* REFUSED — a typo in a variable name must not silently run with the default.
|
|
10244
|
+
*/
|
|
10245
|
+
function resolveActionVariables(declared, given) {
|
|
10246
|
+
const decls = declared ?? [];
|
|
10247
|
+
const provided = given ?? {};
|
|
10248
|
+
for (const name of Object.keys(provided)) if (!decls.some((d) => d.name === name)) return {
|
|
10249
|
+
ok: false,
|
|
10250
|
+
error: `unknown variable "${name}"`
|
|
10251
|
+
};
|
|
10252
|
+
const values = {};
|
|
10253
|
+
for (const decl of decls) {
|
|
10254
|
+
const value = provided[decl.name] ?? decl.default;
|
|
10255
|
+
if (value === void 0) {
|
|
10256
|
+
if (decl.required === true) return {
|
|
10257
|
+
ok: false,
|
|
10258
|
+
error: `variable "${decl.name}" is required`
|
|
10259
|
+
};
|
|
10260
|
+
continue;
|
|
10261
|
+
}
|
|
10262
|
+
const error = variableValueError(decl, value);
|
|
10263
|
+
if (error !== null) return {
|
|
10264
|
+
ok: false,
|
|
10265
|
+
error
|
|
10266
|
+
};
|
|
10267
|
+
values[decl.name] = value;
|
|
10268
|
+
}
|
|
10269
|
+
return {
|
|
10270
|
+
ok: true,
|
|
10271
|
+
values
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
var ActionSourceKindSchema = zod.z.enum(["internal", "addon"]);
|
|
10275
|
+
/** Prefix of an internal action's id. */
|
|
10276
|
+
var INTERNAL_ACTION_PREFIX = "internal:";
|
|
10277
|
+
function internalActionId(uuid) {
|
|
10278
|
+
return `${INTERNAL_ACTION_PREFIX}${uuid}`;
|
|
10279
|
+
}
|
|
10280
|
+
function isInternalActionId(actionId) {
|
|
10281
|
+
return actionId.startsWith("internal:") && actionId.length > 9;
|
|
10282
|
+
}
|
|
10283
|
+
/** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
|
|
10284
|
+
function addonIdOfActionId(actionId) {
|
|
10285
|
+
if (isInternalActionId(actionId)) return null;
|
|
10286
|
+
const at = actionId.indexOf(":");
|
|
10287
|
+
return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
|
|
10288
|
+
}
|
|
10289
|
+
var ActionStatusSchema = zod.z.object({
|
|
10290
|
+
isRunning: zod.z.boolean(),
|
|
10291
|
+
/** Ms epoch the latest run STARTED; null when it never ran. */
|
|
10292
|
+
lastRunAt: zod.z.number().nullable(),
|
|
10293
|
+
/** The latest run's failure; null on success or when it never ran. */
|
|
10294
|
+
lastError: zod.z.string().nullable()
|
|
10295
|
+
});
|
|
10296
|
+
var IDLE_ACTION_STATUS = {
|
|
10297
|
+
isRunning: false,
|
|
10298
|
+
lastRunAt: null,
|
|
10299
|
+
lastError: null
|
|
10300
|
+
};
|
|
10301
|
+
var ActionEntrySchema = zod.z.object({
|
|
10302
|
+
/** `internal:<uuid>` or `<addonId>:<localId>`. */
|
|
10303
|
+
id: zod.z.string().min(3),
|
|
10304
|
+
name: zod.z.string().min(1).max(120),
|
|
10305
|
+
/** The addon that declared it; `core-blocks` for an internal one. */
|
|
10306
|
+
addonId: zod.z.string().min(1),
|
|
10307
|
+
source: ActionSourceKindSchema,
|
|
10308
|
+
/** An addon-declared entry is edited where it was declared. */
|
|
10309
|
+
readOnly: zod.z.boolean(),
|
|
10310
|
+
/** Internal only: cap and wait steps. No action-in-action, so no cycles. */
|
|
10311
|
+
steps: zod.z.array(ActionStepSchema).min(1).max(50).optional(),
|
|
10312
|
+
variables: zod.z.array(ActionVariableSchema).max(20).optional(),
|
|
10313
|
+
status: ActionStatusSchema
|
|
10314
|
+
}).superRefine((entry, ctx) => {
|
|
10315
|
+
if (entry.source === "internal") {
|
|
10316
|
+
if (!isInternalActionId(entry.id)) ctx.addIssue({
|
|
10317
|
+
code: "custom",
|
|
10318
|
+
message: "an internal action id is internal:<uuid>",
|
|
10319
|
+
path: ["id"]
|
|
10320
|
+
});
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
if (entry.steps !== void 0) ctx.addIssue({
|
|
10324
|
+
code: "custom",
|
|
10325
|
+
message: "only an internal action carries steps",
|
|
10326
|
+
path: ["steps"]
|
|
10327
|
+
});
|
|
10328
|
+
if (!entry.readOnly) ctx.addIssue({
|
|
10329
|
+
code: "custom",
|
|
10330
|
+
message: "an addon action is read-only here",
|
|
10331
|
+
path: ["readOnly"]
|
|
10332
|
+
});
|
|
10333
|
+
if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
|
|
10334
|
+
code: "custom",
|
|
10335
|
+
message: "an addon action id is <addonId>:<localId>",
|
|
10336
|
+
path: ["id"]
|
|
10337
|
+
});
|
|
10338
|
+
});
|
|
10339
|
+
/** What an operator writes for an internal action. */
|
|
10340
|
+
var ActionInputSchema = zod.z.object({
|
|
10341
|
+
name: zod.z.string().min(1).max(120),
|
|
10342
|
+
steps: zod.z.array(ActionStepSchema).min(1).max(50),
|
|
10343
|
+
variables: zod.z.array(ActionVariableSchema).max(20).optional()
|
|
10344
|
+
});
|
|
10345
|
+
/** Who started a run. Absent = the operator pressed it. */
|
|
10346
|
+
var ActionRunOriginSchema = zod.z.object({
|
|
10347
|
+
/** The automation block whose step started it. */
|
|
10348
|
+
blockId: zod.z.string().min(1).optional() });
|
|
10349
|
+
/** `started` = the caller did not ask to wait. */
|
|
10350
|
+
var ActionRunOutcomeSchema = zod.z.union([AutomationRunOutcomeSchema, zod.z.literal("started")]);
|
|
10351
|
+
var ActionRunResultSchema = zod.z.object({
|
|
10352
|
+
runId: zod.z.string().min(1),
|
|
10353
|
+
outcome: ActionRunOutcomeSchema,
|
|
10354
|
+
/** Why it did not run, or what failed. */
|
|
10355
|
+
message: zod.z.string().optional()
|
|
10356
|
+
});
|
|
10357
|
+
var ActionErrorCodeSchema = zod.z.enum([
|
|
10358
|
+
"not-found",
|
|
10359
|
+
"read-only",
|
|
10360
|
+
"invalid-variables",
|
|
10361
|
+
"invalid-steps",
|
|
10362
|
+
"not-linked",
|
|
10363
|
+
"busy"
|
|
10364
|
+
]);
|
|
10365
|
+
/**
|
|
10366
|
+
* A refusal with a reason a caller can branch on. The code leads the message
|
|
10367
|
+
* (`[read-only] …`) because a cap call flattens an error to its text.
|
|
10368
|
+
*/
|
|
10369
|
+
var ActionError = class extends Error {
|
|
10370
|
+
code;
|
|
10371
|
+
constructor(code, message) {
|
|
10372
|
+
super(`[${code}] ${message}`);
|
|
10373
|
+
this.name = "ActionError";
|
|
10374
|
+
this.code = code;
|
|
10375
|
+
}
|
|
10376
|
+
};
|
|
10377
|
+
/** The run-state change `actions.onRunChanged` carries. */
|
|
10378
|
+
var ActionRunChangedSchema = zod.z.object({
|
|
10379
|
+
actionId: zod.z.string(),
|
|
10380
|
+
runId: zod.z.string(),
|
|
10381
|
+
isRunning: zod.z.boolean(),
|
|
10382
|
+
/** Set when the run ended. */
|
|
10383
|
+
outcome: AutomationRunOutcomeSchema.optional(),
|
|
10384
|
+
lastRunAt: zod.z.number().nullable(),
|
|
10385
|
+
lastError: zod.z.string().nullable()
|
|
10386
|
+
});
|
|
10387
|
+
//#endregion
|
|
10388
|
+
//#region src/automation/step-runner.ts
|
|
10389
|
+
function errorMessage(err) {
|
|
10390
|
+
return err instanceof Error ? err.message : String(err);
|
|
10391
|
+
}
|
|
10392
|
+
async function runSteps(steps, options) {
|
|
10393
|
+
const traces = [];
|
|
10394
|
+
let failed = false;
|
|
10395
|
+
let aborted = false;
|
|
10396
|
+
for (const [index, step] of steps.entries()) {
|
|
10397
|
+
const path = `${options.pathPrefix}.${index}`;
|
|
10398
|
+
if (failed || aborted || options.isAborted()) {
|
|
10399
|
+
if (!failed) aborted = true;
|
|
10400
|
+
traces.push({
|
|
10401
|
+
...step.meta,
|
|
10402
|
+
path,
|
|
10403
|
+
outcome: "not-run",
|
|
10404
|
+
message: failed ? "an earlier step failed" : options.abortReason
|
|
10405
|
+
});
|
|
10406
|
+
continue;
|
|
10407
|
+
}
|
|
10408
|
+
const startedAt = options.now();
|
|
10409
|
+
let deviceId = step.meta.deviceId;
|
|
10410
|
+
const scope = { setDeviceId: (id) => {
|
|
10411
|
+
deviceId = id;
|
|
10412
|
+
} };
|
|
10413
|
+
try {
|
|
10414
|
+
await step.run(scope);
|
|
10415
|
+
traces.push({
|
|
10416
|
+
...step.meta,
|
|
10417
|
+
...deviceId === void 0 ? {} : { deviceId },
|
|
10418
|
+
path,
|
|
10419
|
+
startedAt,
|
|
10420
|
+
endedAt: options.now(),
|
|
10421
|
+
outcome: "ok"
|
|
10422
|
+
});
|
|
10423
|
+
} catch (err) {
|
|
10424
|
+
failed = true;
|
|
10425
|
+
const message = errorMessage(err);
|
|
10426
|
+
options.onFailure({
|
|
10427
|
+
index,
|
|
10428
|
+
path,
|
|
10429
|
+
message,
|
|
10430
|
+
deviceId
|
|
10431
|
+
});
|
|
10432
|
+
traces.push({
|
|
10433
|
+
...step.meta,
|
|
10434
|
+
...deviceId === void 0 ? {} : { deviceId },
|
|
10435
|
+
path,
|
|
10436
|
+
startedAt,
|
|
10437
|
+
endedAt: options.now(),
|
|
10438
|
+
outcome: "error",
|
|
10439
|
+
message
|
|
10440
|
+
});
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
10443
|
+
return {
|
|
10444
|
+
steps: traces,
|
|
10445
|
+
failed,
|
|
10446
|
+
aborted: aborted && !failed
|
|
10447
|
+
};
|
|
10448
|
+
}
|
|
10449
|
+
function isVarRef(value) {
|
|
10450
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10451
|
+
const keys = Object.keys(value);
|
|
10452
|
+
return keys.length === 1 && keys[0] === "$var" && typeof Reflect.get(value, "$var") === "string";
|
|
10453
|
+
}
|
|
10454
|
+
var TEMPLATE_RE = /\{\{\s*vars\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
10455
|
+
/**
|
|
10456
|
+
* Substitute a step's args from the run's variables. SUBSTITUTION only, never
|
|
10457
|
+
* evaluation:
|
|
10458
|
+
* - a value that is exactly `{ $var: 'name' }` becomes the variable's value,
|
|
10459
|
+
* keeping its type (a number stays a number);
|
|
10460
|
+
* - `{{vars.name}}` inside a string becomes its text.
|
|
10461
|
+
* A variable that is not set THROWS — a step must not run with a hole where
|
|
10462
|
+
* the operator meant a value. (`{{vars.x}}` keeps the automation behaviour:
|
|
10463
|
+
* the hole is reported through `onMissing` and rendered empty.)
|
|
10464
|
+
*/
|
|
10465
|
+
function resolveStepArgs(value, vars, onMissing) {
|
|
10466
|
+
if (isVarRef(value)) {
|
|
10467
|
+
const found = vars[value.$var];
|
|
10468
|
+
if (found === void 0) throw new Error(`variable "${value.$var}" is not set`);
|
|
10469
|
+
return found;
|
|
10470
|
+
}
|
|
10471
|
+
if (typeof value === "string") return value.replace(TEMPLATE_RE, (_m, key) => {
|
|
10472
|
+
const v = vars[key];
|
|
10473
|
+
if (v === void 0) {
|
|
10474
|
+
onMissing(key);
|
|
10475
|
+
return "";
|
|
10476
|
+
}
|
|
10477
|
+
return String(v);
|
|
10478
|
+
});
|
|
10479
|
+
if (Array.isArray(value)) return value.map((v) => resolveStepArgs(v, vars, onMissing));
|
|
10480
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveStepArgs(v, vars, onMissing)]));
|
|
10481
|
+
return value;
|
|
10482
|
+
}
|
|
10483
|
+
/** A `{ $var }` device target, resolved from the bag BEFORE dispatch. */
|
|
10484
|
+
function resolveDeviceTarget(vars, name) {
|
|
10485
|
+
const raw = vars[name];
|
|
10486
|
+
if (typeof raw === "number") return raw;
|
|
10487
|
+
throw new Error(`action target var "${name}" is not a device id`);
|
|
10488
|
+
}
|
|
10489
|
+
function stepMetaOfAction(step) {
|
|
10490
|
+
if (step.kind === "wait") return { kind: "wait" };
|
|
10491
|
+
return {
|
|
10492
|
+
kind: "cap",
|
|
10493
|
+
cap: step.cap,
|
|
10494
|
+
method: step.method,
|
|
10495
|
+
...typeof step.deviceId === "number" ? { deviceId: step.deviceId } : {}
|
|
10496
|
+
};
|
|
10497
|
+
}
|
|
10498
|
+
function isPlainRecord(value) {
|
|
10499
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10500
|
+
}
|
|
10501
|
+
/**
|
|
10502
|
+
* The runnable form of an internal Action's steps, bound to one run's
|
|
10503
|
+
* variables. Variables are substituted per step, at the moment it runs, so a
|
|
10504
|
+
* missing one fails THAT step (traced) and leaves the later ones not-run.
|
|
10505
|
+
*/
|
|
10506
|
+
function buildActionSteps(steps, vars, deps) {
|
|
10507
|
+
return steps.map((step) => {
|
|
10508
|
+
const meta = stepMetaOfAction(step);
|
|
10509
|
+
if (step.kind === "wait") return {
|
|
10510
|
+
meta,
|
|
10511
|
+
run: () => deps.sleep(step.seconds * 1e3)
|
|
10512
|
+
};
|
|
10513
|
+
return {
|
|
10514
|
+
meta,
|
|
10515
|
+
run: async (scope) => {
|
|
10516
|
+
const resolved = resolveStepArgs(step.args ?? {}, vars, deps.onMissingVar);
|
|
10517
|
+
const args = isPlainRecord(resolved) ? resolved : {};
|
|
10518
|
+
const targets = typeof step.deviceId === "number" ? [step.deviceId] : Array.isArray(step.deviceId) ? step.deviceId : [resolveDeviceTarget(vars, step.deviceId.$var)];
|
|
10519
|
+
for (const deviceId of targets) {
|
|
10520
|
+
scope.setDeviceId(deviceId);
|
|
10521
|
+
await deps.invokeCap({
|
|
10522
|
+
deviceId,
|
|
10523
|
+
cap: step.cap,
|
|
10524
|
+
method: step.method,
|
|
10525
|
+
args
|
|
10526
|
+
});
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
};
|
|
10530
|
+
});
|
|
10531
|
+
}
|
|
10532
|
+
//#endregion
|
|
10533
|
+
//#region src/capabilities/addon-pages.cap.ts
|
|
10534
|
+
/**
|
|
10535
|
+
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
10536
|
+
* surface that admin-ui consumes through `useAddonPagesListPages()`.
|
|
10537
|
+
*
|
|
10538
|
+
* The provider iterates every `addon-pages-source` (collection) provider
|
|
10539
|
+
* and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
|
|
10540
|
+
* pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
|
|
10541
|
+
* filesystem `mtime` cache-buster lets the browser pick up addon
|
|
10542
|
+
* rebuilds without manual reload.
|
|
10543
|
+
*
|
|
10544
|
+
* The hub-local builtin `addon-pages-aggregator` (see
|
|
10545
|
+
* `@camstack/system/builtins/addon-pages-aggregator`) registers the
|
|
10546
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
10547
|
+
* keeps both ends in codegen — there's no hand-written
|
|
10548
|
+
* `addon-pages.router.ts` wrapper anymore.
|
|
10549
|
+
*/
|
|
10550
|
+
var AddonPageDeclarationSchema$1 = zod.z.object({
|
|
10551
|
+
id: zod.z.string(),
|
|
10552
|
+
label: zod.z.string(),
|
|
10553
|
+
icon: zod.z.string(),
|
|
10554
|
+
path: zod.z.string(),
|
|
10555
|
+
remoteName: zod.z.string(),
|
|
10556
|
+
bundle: zod.z.string(),
|
|
10557
|
+
section: zod.z.string().optional(),
|
|
10558
|
+
sectionLabel: zod.z.string().optional()
|
|
10559
|
+
});
|
|
10560
|
+
var AddonPageInfoSchema = zod.z.object({
|
|
10561
|
+
addonId: zod.z.string(),
|
|
10562
|
+
page: AddonPageDeclarationSchema$1,
|
|
10563
|
+
bundleUrl: zod.z.string()
|
|
10564
|
+
});
|
|
10565
|
+
var addonPagesCapability = {
|
|
10566
|
+
name: "addon-pages",
|
|
10567
|
+
scope: "system",
|
|
10568
|
+
mode: "singleton",
|
|
10569
|
+
methods: { listPages: require_sleep.method(zod.z.void(), zod.z.array(AddonPageInfoSchema).readonly()) }
|
|
10570
|
+
};
|
|
10571
|
+
//#endregion
|
|
10572
|
+
//#region src/capabilities/addon-pages-source.cap.ts
|
|
10573
|
+
/**
|
|
10574
|
+
* `addon-pages-source` — collection cap exposing per-provider raw page
|
|
10575
|
+
* declarations. Every addon that contributes a UI page registers a
|
|
10576
|
+
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
10577
|
+
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
10578
|
+
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
10579
|
+
* that admin-ui consumes.
|
|
10580
|
+
*
|
|
10581
|
+
* The split exists because the public listing has a different output
|
|
10582
|
+
* shape than the per-provider raw declarations, and we want both ends
|
|
10583
|
+
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
10584
|
+
*/
|
|
10585
|
+
var AddonPageDeclarationSchema = zod.z.object({
|
|
10586
|
+
id: zod.z.string(),
|
|
10587
|
+
label: zod.z.string(),
|
|
10588
|
+
icon: zod.z.string(),
|
|
10589
|
+
path: zod.z.string(),
|
|
10590
|
+
/**
|
|
10591
|
+
* Module Federation remote name — must match the `name` field on the
|
|
10592
|
+
* page addon's `federation()` plugin config. Used by admin-ui's
|
|
10593
|
+
* `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
|
|
10594
|
+
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
10595
|
+
* contain hyphens).
|
|
10596
|
+
*/
|
|
10597
|
+
remoteName: zod.z.string(),
|
|
10598
|
+
/**
|
|
10599
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
10600
|
+
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
10601
|
+
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
10602
|
+
* the static-file route can compute an mtime-based cache-buster URL
|
|
10603
|
+
* without a separate filesystem stat.
|
|
10604
|
+
*/
|
|
10605
|
+
bundle: zod.z.string(),
|
|
10606
|
+
/**
|
|
10607
|
+
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
10608
|
+
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
10609
|
+
* Any OTHER string creates (or joins) a custom section rendered after
|
|
10610
|
+
* the built-in groups; its label comes from `sectionLabel` (first
|
|
10611
|
+
* declaration wins), falling back to the id. Absent → the legacy
|
|
10612
|
+
* "Addon Pages" group.
|
|
10613
|
+
*/
|
|
10614
|
+
section: zod.z.string().optional(),
|
|
10615
|
+
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
10616
|
+
sectionLabel: zod.z.string().optional()
|
|
10617
|
+
});
|
|
10618
|
+
var addonPagesSourceCapability = {
|
|
10619
|
+
name: "addon-pages-source",
|
|
10620
|
+
scope: "system",
|
|
10621
|
+
mode: "collection",
|
|
10622
|
+
internal: true,
|
|
10623
|
+
methods: {
|
|
10624
|
+
/**
|
|
10625
|
+
* `internal: true` did not gate the mount (see the 2026-08-26 note on
|
|
10626
|
+
* `data-store-provider`) — this was reachable on the AppRouter by ANY
|
|
10627
|
+
* authenticated session at the default `auth: 'protected'`. The only
|
|
10628
|
+
* consumer, the `addon-pages-aggregator` builtin, reads it via
|
|
10629
|
+
* `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
|
|
10630
|
+
* only, never tRPC (per `ctx.capabilities` seeing only the local
|
|
10631
|
+
* process). The public, enriched listing admin-ui actually consumes is
|
|
10632
|
+
* the separate `addon-pages` cap, unaffected by this change.
|
|
10633
|
+
*/
|
|
10634
|
+
listPages: require_sleep.method(zod.z.void(), zod.z.array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
|
|
10635
|
+
};
|
|
10636
|
+
//#endregion
|
|
10637
|
+
//#region src/capabilities/addon-routes.cap.ts
|
|
10638
|
+
var AddonHttpRouteSchema = zod.z.object({
|
|
10639
|
+
method: zod.z.enum([
|
|
10640
|
+
"GET",
|
|
10641
|
+
"POST",
|
|
10642
|
+
"PUT",
|
|
10643
|
+
"DELETE",
|
|
10644
|
+
"PATCH"
|
|
10645
|
+
]),
|
|
10646
|
+
path: zod.z.string(),
|
|
10647
|
+
access: zod.z.enum([
|
|
10648
|
+
"public",
|
|
10649
|
+
"authenticated",
|
|
10650
|
+
"admin"
|
|
10651
|
+
]).optional(),
|
|
10652
|
+
description: zod.z.string().optional(),
|
|
10653
|
+
/** The hub limits this route per client address before the handler runs. */
|
|
10654
|
+
rateLimit: zod.z.enum(["auth"]).optional()
|
|
10655
|
+
});
|
|
10656
|
+
/**
|
|
10657
|
+
* Cross-process route invocation envelope. The hub captures the
|
|
10658
|
+
* request as plain data, ships it to the worker via Moleculer, and
|
|
10659
|
+
* the worker runs the local handler against a capturing reply. The
|
|
10660
|
+
* envelope returned describes what the handler intended (status,
|
|
10661
|
+
* headers, body, or a redirect) so the hub can translate it back to
|
|
10662
|
+
* the Fastify reply that's actually wired to the socket.
|
|
10663
|
+
*/
|
|
10664
|
+
var InvokeRequestSchema = zod.z.object({
|
|
10665
|
+
method: zod.z.string(),
|
|
10666
|
+
path: zod.z.string(),
|
|
10667
|
+
params: zod.z.record(zod.z.string(), zod.z.string()),
|
|
10668
|
+
query: zod.z.record(zod.z.string(), zod.z.string()),
|
|
10669
|
+
body: zod.z.unknown(),
|
|
10670
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()),
|
|
10671
|
+
user: zod.z.object({
|
|
10672
|
+
id: zod.z.string(),
|
|
10673
|
+
username: zod.z.string(),
|
|
10674
|
+
isAdmin: zod.z.boolean()
|
|
10675
|
+
}).optional(),
|
|
10676
|
+
scopedToken: zod.z.unknown().optional()
|
|
10677
|
+
});
|
|
10678
|
+
var InvokeReplyEnvelopeSchema = zod.z.object({
|
|
10679
|
+
status: zod.z.number().int(),
|
|
10680
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()),
|
|
10681
|
+
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
10682
|
+
* sending `body`. Status defaults to 302 when this is set unless
|
|
10683
|
+
* the handler called `reply.code(...)` explicitly. */
|
|
10684
|
+
redirectUrl: zod.z.string().nullable(),
|
|
10685
|
+
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
10686
|
+
body: zod.z.unknown().optional(),
|
|
10687
|
+
/** Set when the handler called `reply.type(mime)`. */
|
|
10688
|
+
contentType: zod.z.string().optional()
|
|
10689
|
+
});
|
|
10690
|
+
var addonRoutesCapability = {
|
|
10691
|
+
name: "addon-routes",
|
|
10692
|
+
scope: "system",
|
|
10693
|
+
mode: "collection",
|
|
10694
|
+
internal: true,
|
|
10695
|
+
methods: {
|
|
10696
|
+
getRoutes: require_sleep.method(zod.z.void(), zod.z.array(AddonHttpRouteSchema)),
|
|
10697
|
+
/**
|
|
10698
|
+
* Cross-process dispatch entry point. Forked addons implement this
|
|
10699
|
+
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
10700
|
+
* can route through Moleculer when the handler lives in a worker.
|
|
10701
|
+
*
|
|
10702
|
+
* Local addons can implement it for free with the same helper;
|
|
10703
|
+
* the hub bypasses the wire on co-located addons.
|
|
10704
|
+
*/
|
|
10705
|
+
invoke: require_sleep.method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
10706
|
+
},
|
|
10707
|
+
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
10708
|
+
mount: { kind: "skip" }
|
|
10709
|
+
};
|
|
10710
|
+
//#endregion
|
|
10711
|
+
//#region src/capabilities/addon-settings.cap.ts
|
|
10712
|
+
var ConfigTabDeclarationSchema = zod.z.object({
|
|
10713
|
+
id: zod.z.string(),
|
|
10714
|
+
label: zod.z.string(),
|
|
10715
|
+
icon: zod.z.string(),
|
|
10716
|
+
order: zod.z.number().optional()
|
|
10717
|
+
});
|
|
10718
|
+
var ConfigSectionWithValuesSchema = zod.z.object({
|
|
10719
|
+
id: zod.z.string(),
|
|
10720
|
+
title: zod.z.string(),
|
|
10721
|
+
titleKey: zod.z.string().optional(),
|
|
10722
|
+
description: zod.z.string().optional(),
|
|
10723
|
+
descriptionKey: zod.z.string().optional(),
|
|
10724
|
+
style: zod.z.enum(["card", "accordion"]).optional(),
|
|
10725
|
+
defaultCollapsed: zod.z.boolean().optional(),
|
|
10726
|
+
columns: zod.z.union([
|
|
10727
|
+
zod.z.literal(1),
|
|
10728
|
+
zod.z.literal(2),
|
|
10729
|
+
zod.z.literal(3),
|
|
10730
|
+
zod.z.literal(4)
|
|
10731
|
+
]).optional(),
|
|
10732
|
+
tab: zod.z.string().optional(),
|
|
10733
|
+
location: zod.z.enum([
|
|
10734
|
+
"settings",
|
|
10735
|
+
"top-tab",
|
|
10736
|
+
"hero"
|
|
10737
|
+
]).optional(),
|
|
10738
|
+
order: zod.z.number().optional(),
|
|
10739
|
+
fields: zod.z.array(zod.z.any())
|
|
10740
|
+
});
|
|
10741
|
+
var SettingsSchemaWithValuesSchema = zod.z.object({
|
|
10742
|
+
tabs: zod.z.array(ConfigTabDeclarationSchema).optional(),
|
|
10743
|
+
sections: zod.z.array(ConfigSectionWithValuesSchema)
|
|
10744
|
+
});
|
|
10745
|
+
/** Patch object — keys are field names, values are the new field values. */
|
|
10746
|
+
var SettingsPatchSchema = zod.z.record(zod.z.string(), zod.z.unknown());
|
|
10747
|
+
/** Standard success response for update operations. */
|
|
10748
|
+
var SettingsUpdateResultSchema = zod.z.object({ success: zod.z.literal(true) });
|
|
10749
|
+
/**
|
|
10750
|
+
* addon-settings — singleton gateway for three-level addon settings.
|
|
10751
|
+
*
|
|
10752
|
+
* Works like `device-manager`: a single hub-side provider that resolves
|
|
10753
|
+
* `addonId` to the target addon and delegates the call. For hub-local
|
|
10754
|
+
* addons the call is direct; for remote agents it proxies via the
|
|
10755
|
+
* per-addon Moleculer service.
|
|
10756
|
+
*
|
|
10757
|
+
* Replaces the `$addonHost` Moleculer service. Transport transparency
|
|
10758
|
+
* is handled by the provider implementation — callers (admin UI, other
|
|
10759
|
+
* addons via `ctx.api`) never know which node hosts the target addon.
|
|
10760
|
+
*
|
|
10761
|
+
* Three levels:
|
|
10762
|
+
* - **addon**: addon-scoped settings (installation config, API keys, …)
|
|
10763
|
+
* - **global**: settings applied to all devices by default
|
|
10764
|
+
* - **device**: per-device overrides
|
|
10765
|
+
*
|
|
10766
|
+
* Optional `nodeId` allows explicit node targeting. When absent the
|
|
10767
|
+
* provider resolves the addon's host node automatically.
|
|
10768
|
+
*/
|
|
10769
|
+
/**
|
|
10770
|
+
* `addon-settings` is a **hub-centric** cap: the hub hosts the single
|
|
10771
|
+
* provider, and `nodeId` in the input is data for the hub provider's
|
|
10772
|
+
* internal dispatcher, not a routing hint for the cap-router. See
|
|
10773
|
+
* `CapabilityDefinition.nodeIdMode` for the contract.
|
|
10774
|
+
*/
|
|
10775
|
+
var addonSettingsCapability = {
|
|
10776
|
+
name: "addon-settings",
|
|
8692
10777
|
scope: "system",
|
|
8693
10778
|
mode: "singleton",
|
|
8694
10779
|
nodeIdMode: "data",
|
|
@@ -13871,7 +15956,7 @@ async function runFanOut(calls, invoke) {
|
|
|
13871
15956
|
//#region src/generated/history-catalog.ts
|
|
13872
15957
|
/**
|
|
13873
15958
|
* Cap history catalog, projected from every `*.cap.ts` `history` block.
|
|
13874
|
-
* 42 caps with series (
|
|
15959
|
+
* 42 caps with series (77 series), 33 caps answered `'none'`.
|
|
13875
15960
|
* Field keys are `runtimeState` paths (`a.b` into fixed objects only).
|
|
13876
15961
|
*/
|
|
13877
15962
|
var HISTORY_CATALOG = {
|
|
@@ -14098,6 +16183,7 @@ var HISTORY_CATALOG = {
|
|
|
14098
16183
|
} },
|
|
14099
16184
|
"pet-feeder": {
|
|
14100
16185
|
"foodLevel": { kind: "gauge" },
|
|
16186
|
+
"bowlFood": { kind: "gauge" },
|
|
14101
16187
|
"food1": { kind: "gauge" },
|
|
14102
16188
|
"food2": { kind: "gauge" },
|
|
14103
16189
|
"batteryPower": {
|
|
@@ -15989,7 +18075,29 @@ var coreBlocksCapability = {
|
|
|
15989
18075
|
kind: "mutation",
|
|
15990
18076
|
auth: "admin",
|
|
15991
18077
|
access: "view"
|
|
15992
|
-
})
|
|
18078
|
+
}),
|
|
18079
|
+
/**
|
|
18080
|
+
* An automation block reports one run (or one coalesced skip) to the hub's
|
|
18081
|
+
* `runs` ledger. Only the block itself may: the transport stamps
|
|
18082
|
+
* `callerAddonId`, and anything but `core-block-<blockId>` is refused.
|
|
18083
|
+
* `keepTraces` bounds the ring; `runStartedAt` moves the cooldown stamp.
|
|
18084
|
+
*/
|
|
18085
|
+
reportAutomationRun: require_sleep.method(zod.z.object({
|
|
18086
|
+
blockId: zod.z.string().min(1),
|
|
18087
|
+
trace: AutomationRunTraceSchema.optional(),
|
|
18088
|
+
runStartedAt: zod.z.number().optional(),
|
|
18089
|
+
keepTraces: zod.z.number().int().min(1).max(100).optional()
|
|
18090
|
+
}), zod.z.void(), {
|
|
18091
|
+
kind: "mutation",
|
|
18092
|
+
auth: "admin"
|
|
18093
|
+
}),
|
|
18094
|
+
/** The cooldown stamp and trace ring a block reads ONCE, at start. Owner-only. */
|
|
18095
|
+
getAutomationRunState: require_sleep.method(zod.z.object({ blockId: zod.z.string().min(1) }), AutomationStateDocSchema, { auth: "admin" }),
|
|
18096
|
+
/** An automation's run traces, newest last. The operator's read. */
|
|
18097
|
+
getTraces: require_sleep.method(zod.z.object({ blockId: zod.z.string().min(1) }), zod.z.object({
|
|
18098
|
+
traces: zod.z.array(AutomationRunTraceSchema),
|
|
18099
|
+
lastError: zod.z.string().nullable()
|
|
18100
|
+
}), { auth: "admin" })
|
|
15993
18101
|
}
|
|
15994
18102
|
};
|
|
15995
18103
|
//#endregion
|
|
@@ -44006,683 +46114,291 @@ var NotificationActionIconSchema = zod.z.enum([
|
|
|
44006
46114
|
"arm-night",
|
|
44007
46115
|
"disarm",
|
|
44008
46116
|
"light",
|
|
44009
|
-
"alert",
|
|
44010
|
-
"camera"
|
|
44011
|
-
]);
|
|
44012
|
-
/** A single tap-through action button. */
|
|
44013
|
-
var NotificationActionSchema = zod.z.object({
|
|
44014
|
-
id: zod.z.string(),
|
|
44015
|
-
label: zod.z.string(),
|
|
44016
|
-
url: zod.z.string().optional(),
|
|
44017
|
-
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
44018
|
-
icon: NotificationActionIconSchema.optional(),
|
|
44019
|
-
/**
|
|
44020
|
-
* Renders in a warning style where the notifier supports it.
|
|
44021
|
-
*
|
|
44022
|
-
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
44023
|
-
* else — see `notification-center/action-token.ts` for what that does and
|
|
44024
|
-
* does not buy.
|
|
44025
|
-
*/
|
|
44026
|
-
destructive: zod.z.boolean().optional(),
|
|
44027
|
-
/**
|
|
44028
|
-
* How the tap should REACH the url.
|
|
44029
|
-
*
|
|
44030
|
-
* `navigate` (absent, and every button authored before this field) opens it:
|
|
44031
|
-
* the phone leaves the notification and shows whatever the callback returns.
|
|
44032
|
-
* That is right for a button whose answer the operator wants to read.
|
|
44033
|
-
*
|
|
44034
|
-
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
44035
|
-
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
44036
|
-
* an answer to the notification, and being thrown into a browser tab to
|
|
44037
|
-
* confirm it costs more attention than the notification did. A backend that
|
|
44038
|
-
* cannot do a background call renders it as an ordinary link (the adapters
|
|
44039
|
-
* fall back rather than dropping the button), so this is a preference, never
|
|
44040
|
-
* a requirement.
|
|
44041
|
-
*/
|
|
44042
|
-
mode: zod.z.enum(["navigate", "background"]).optional()
|
|
44043
|
-
});
|
|
44044
|
-
/**
|
|
44045
|
-
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
44046
|
-
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
44047
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
44048
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
44049
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
44050
|
-
* `priority` for that one target.
|
|
44051
|
-
*/
|
|
44052
|
-
var NotificationSchema = zod.z.object({
|
|
44053
|
-
body: zod.z.string(),
|
|
44054
|
-
title: zod.z.string().optional(),
|
|
44055
|
-
format: NotificationFormatSchema.default("text"),
|
|
44056
|
-
priority: zod.z.number().int().min(1).max(5).default(3),
|
|
44057
|
-
level: zod.z.string().optional(),
|
|
44058
|
-
attachments: zod.z.array(AttachmentSchema).optional(),
|
|
44059
|
-
clickUrl: zod.z.string().optional(),
|
|
44060
|
-
actions: zod.z.array(NotificationActionSchema).optional(),
|
|
44061
|
-
sound: zod.z.string().optional(),
|
|
44062
|
-
ttl: zod.z.number().optional(),
|
|
44063
|
-
tag: zod.z.string().optional(),
|
|
44064
|
-
deviceId: zod.z.number().optional(),
|
|
44065
|
-
eventId: zod.z.string().optional(),
|
|
44066
|
-
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
44067
|
-
});
|
|
44068
|
-
/** One declared native severity/priority level for a kind. */
|
|
44069
|
-
var TargetKindLevelSchema = zod.z.object({
|
|
44070
|
-
id: zod.z.string(),
|
|
44071
|
-
label: zod.z.string(),
|
|
44072
|
-
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
44073
|
-
ordinal: zod.z.number().int().min(1).max(5).nullable(),
|
|
44074
|
-
flags: zod.z.object({
|
|
44075
|
-
critical: zod.z.boolean().optional(),
|
|
44076
|
-
silent: zod.z.boolean().optional(),
|
|
44077
|
-
noPush: zod.z.boolean().optional()
|
|
44078
|
-
}).optional(),
|
|
44079
|
-
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
44080
|
-
requires: zod.z.array(zod.z.string()).optional(),
|
|
44081
|
-
description: zod.z.string().optional()
|
|
44082
|
-
});
|
|
44083
|
-
/** Attachment capabilities for a kind (drives the degrade engine + test panel). */
|
|
44084
|
-
var TargetKindAttachmentsCapsSchema = zod.z.object({
|
|
44085
|
-
mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
|
|
44086
|
-
mode: zod.z.enum([
|
|
44087
|
-
"url",
|
|
44088
|
-
"bytes",
|
|
44089
|
-
"both"
|
|
44090
|
-
]),
|
|
44091
|
-
max: zod.z.number().int().nonnegative(),
|
|
44092
|
-
maxBytes: zod.z.number().int().positive().optional()
|
|
44093
|
-
});
|
|
44094
|
-
/** The full capability block consulted before dispatch. */
|
|
44095
|
-
var TargetKindCapsSchema = zod.z.object({
|
|
44096
|
-
attachments: TargetKindAttachmentsCapsSchema,
|
|
44097
|
-
/** Max action buttons (0 = none). */
|
|
44098
|
-
actions: zod.z.number().int().nonnegative(),
|
|
44099
|
-
/**
|
|
44100
|
-
* Whether this kind renders a per-action ICON.
|
|
44101
|
-
*
|
|
44102
|
-
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
44103
|
-
* run on the addon cap path — three production failures in one day taught
|
|
44104
|
-
* this repo that once. Absent is read as false by the degrade engine, which
|
|
44105
|
-
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
44106
|
-
* assumed and dropped costs the operator's trust in the field.
|
|
44107
|
-
*/
|
|
44108
|
-
actionIcons: zod.z.boolean().optional(),
|
|
44109
|
-
levels: zod.z.array(TargetKindLevelSchema),
|
|
44110
|
-
format: zod.z.array(NotificationFormatSchema),
|
|
44111
|
-
clickUrl: zod.z.boolean(),
|
|
44112
|
-
sound: zod.z.boolean(),
|
|
44113
|
-
ttl: zod.z.boolean(),
|
|
44114
|
-
bodyMaxLen: zod.z.number().int().positive()
|
|
44115
|
-
});
|
|
44116
|
-
/**
|
|
44117
|
-
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
44118
|
-
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
44119
|
-
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
44120
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
44121
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
44122
|
-
*/
|
|
44123
|
-
var ConfigSchemaPassthrough$2 = zod.z.unknown();
|
|
44124
|
-
var TargetKindSchema = zod.z.object({
|
|
44125
|
-
kind: zod.z.string(),
|
|
44126
|
-
label: zod.z.string(),
|
|
44127
|
-
icon: zod.z.string(),
|
|
44128
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
44129
|
-
addonId: zod.z.string(),
|
|
44130
|
-
/**
|
|
44131
|
-
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
44132
|
-
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
44133
|
-
* when the addon bundles no icon for that kind — the client then falls back
|
|
44134
|
-
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
44135
|
-
*
|
|
44136
|
-
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
44137
|
-
* client, and a native client joins it onto its own hub base.
|
|
44138
|
-
*
|
|
44139
|
-
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
44140
|
-
* field that survived only because the runtime cap-router forwards provider
|
|
44141
|
-
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
44142
|
-
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
44143
|
-
* broken silently the moment output validation was tightened anywhere.
|
|
44144
|
-
*/
|
|
44145
|
-
iconUrl: zod.z.string().optional(),
|
|
44146
|
-
/**
|
|
44147
|
-
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
44148
|
-
*
|
|
44149
|
-
* The server knows this and therefore says it, because the client cannot
|
|
44150
|
-
* safely guess: a React-Native client renders SVG and raster through two
|
|
44151
|
-
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
44152
|
-
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
44153
|
-
* placeholder glyph for every vector icon while the web build looked fine.
|
|
44154
|
-
*
|
|
44155
|
-
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
44156
|
-
* not been updated — a client that cannot determine the type should prefer
|
|
44157
|
-
* its raster path, which is the safe default for an unknown image.
|
|
44158
|
-
*/
|
|
44159
|
-
iconMediaType: zod.z.string().optional(),
|
|
44160
|
-
configSchema: ConfigSchemaPassthrough$2,
|
|
44161
|
-
/**
|
|
44162
|
-
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
44163
|
-
* base URL, instance, chat, recipient). A save that changes any of them
|
|
44164
|
-
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
44165
|
-
* editor uses this to ask for the secret on exactly those edits instead of
|
|
44166
|
-
* on every edit.
|
|
44167
|
-
*
|
|
44168
|
-
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
44169
|
-
* kinds) and from a provider older than this field — an editor that finds
|
|
44170
|
-
* it absent must assume ANY edit may need the secret.
|
|
44171
|
-
*/
|
|
44172
|
-
destinationKeys: zod.z.array(zod.z.string()).optional(),
|
|
44173
|
-
supportsDiscovery: zod.z.boolean(),
|
|
44174
|
-
caps: TargetKindCapsSchema
|
|
44175
|
-
});
|
|
44176
|
-
/**
|
|
44177
|
-
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
44178
|
-
* (return a presence marker only) when serving `listTargets` — never
|
|
44179
|
-
* round-trip a stored secret to the UI.
|
|
44180
|
-
*/
|
|
44181
|
-
var TargetSchema = zod.z.object({
|
|
44182
|
-
id: zod.z.string(),
|
|
44183
|
-
name: zod.z.string(),
|
|
44184
|
-
kind: zod.z.string(),
|
|
44185
|
-
addonId: zod.z.string(),
|
|
44186
|
-
enabled: zod.z.boolean(),
|
|
44187
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
44188
|
-
});
|
|
44189
|
-
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
44190
|
-
var DiscoveredTargetSchema = zod.z.object({
|
|
44191
|
-
kind: zod.z.string(),
|
|
44192
|
-
suggestedName: zod.z.string(),
|
|
44193
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
44194
|
-
});
|
|
44195
|
-
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
44196
|
-
var RenderedAsSchema = zod.z.object({
|
|
44197
|
-
level: zod.z.string(),
|
|
44198
|
-
format: NotificationFormatSchema,
|
|
44199
|
-
attachmentsSent: zod.z.number().int().nonnegative(),
|
|
44200
|
-
actionsSent: zod.z.number().int().nonnegative(),
|
|
44201
|
-
truncated: zod.z.boolean(),
|
|
44202
|
-
dropped: zod.z.array(zod.z.string())
|
|
44203
|
-
});
|
|
44204
|
-
var SendResultSchema = zod.z.object({
|
|
44205
|
-
success: zod.z.boolean(),
|
|
44206
|
-
error: zod.z.string().optional(),
|
|
44207
|
-
renderedAs: RenderedAsSchema.optional()
|
|
44208
|
-
});
|
|
44209
|
-
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
44210
|
-
var TestResultSchema = SendResultSchema;
|
|
44211
|
-
/**
|
|
44212
|
-
* Pre-save credential probe against a partial (unsaved) target config.
|
|
44213
|
-
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
44214
|
-
*/
|
|
44215
|
-
var ProbeTargetConfigResultSchema = zod.z.object({
|
|
44216
|
-
ok: zod.z.boolean(),
|
|
44217
|
-
resolvedUrl: zod.z.string(),
|
|
44218
|
-
httpStatus: zod.z.number().int().optional(),
|
|
44219
|
-
stateInstance: zod.z.string().optional(),
|
|
44220
|
-
error: zod.z.string().optional()
|
|
44221
|
-
});
|
|
44222
|
-
var notificationOutputCapability = {
|
|
44223
|
-
name: "notification-output",
|
|
44224
|
-
scope: "system",
|
|
44225
|
-
mode: "collection",
|
|
44226
|
-
methods: {
|
|
44227
|
-
listTargetKinds: require_sleep.method(zod.z.object({}), zod.z.array(TargetKindSchema)),
|
|
44228
|
-
listTargets: require_sleep.method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }),
|
|
44229
|
-
/**
|
|
44230
|
-
* One target WITH secrets — admin edit / duplicate hydration only.
|
|
44231
|
-
* `listTargets` stays redacted; the editor loads this so password fields
|
|
44232
|
-
* are filled (masked in the input) and a save can round-trip the token
|
|
44233
|
-
* when a destination changes (Duplicate with a new chatId, etc.).
|
|
44234
|
-
*/
|
|
44235
|
-
getTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }),
|
|
44236
|
-
discoverTargets: require_sleep.method(zod.z.object({
|
|
44237
|
-
kind: zod.z.string(),
|
|
44238
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
44239
|
-
/**
|
|
44240
|
-
* When editing / duplicating, the blank password field never round-trips
|
|
44241
|
-
* the stored token. Pass the source target id so the provider can restore
|
|
44242
|
-
* secrets server-side before dialling (same merge as upsert).
|
|
44243
|
-
*/
|
|
44244
|
-
targetId: zod.z.string().optional()
|
|
44245
|
-
}), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }),
|
|
44246
|
-
probeTargetConfig: require_sleep.method(zod.z.object({
|
|
44247
|
-
kind: zod.z.string(),
|
|
44248
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown()),
|
|
44249
|
-
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
44250
|
-
targetId: zod.z.string().optional()
|
|
44251
|
-
}), ProbeTargetConfigResultSchema, {
|
|
44252
|
-
kind: "mutation",
|
|
44253
|
-
auth: "admin"
|
|
44254
|
-
}),
|
|
44255
|
-
send: require_sleep.method(zod.z.object({
|
|
44256
|
-
targetId: zod.z.string(),
|
|
44257
|
-
notification: NotificationSchema
|
|
44258
|
-
}), SendResultSchema, {
|
|
44259
|
-
kind: "mutation",
|
|
44260
|
-
auth: "admin"
|
|
44261
|
-
}),
|
|
44262
|
-
testTarget: require_sleep.method(zod.z.object({
|
|
44263
|
-
targetId: zod.z.string(),
|
|
44264
|
-
sample: NotificationSchema.optional()
|
|
44265
|
-
}), TestResultSchema, {
|
|
44266
|
-
kind: "mutation",
|
|
44267
|
-
auth: "admin"
|
|
44268
|
-
}),
|
|
44269
|
-
upsertTarget: require_sleep.method(zod.z.object({
|
|
44270
|
-
target: TargetSchema,
|
|
44271
|
-
/**
|
|
44272
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
44273
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
44274
|
-
* password fields are restored from this source (D634 still applies if
|
|
44275
|
-
* destinations diverge without a re-typed secret).
|
|
44276
|
-
*/
|
|
44277
|
-
cloneSecretsFrom: zod.z.string().optional()
|
|
44278
|
-
}), TargetSchema, {
|
|
44279
|
-
kind: "mutation",
|
|
44280
|
-
auth: "admin"
|
|
44281
|
-
}),
|
|
44282
|
-
deleteTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
|
|
44283
|
-
kind: "mutation",
|
|
44284
|
-
auth: "admin"
|
|
44285
|
-
}),
|
|
44286
|
-
setTargetEnabled: require_sleep.method(zod.z.object({
|
|
44287
|
-
targetId: zod.z.string(),
|
|
44288
|
-
enabled: zod.z.boolean()
|
|
44289
|
-
}), zod.z.void(), {
|
|
44290
|
-
kind: "mutation",
|
|
44291
|
-
auth: "admin"
|
|
44292
|
-
})
|
|
44293
|
-
}
|
|
44294
|
-
};
|
|
44295
|
-
//#endregion
|
|
44296
|
-
//#region src/catalogs/coco-classmap.ts
|
|
44297
|
-
var COCO_80_LABELS = [
|
|
44298
|
-
{
|
|
44299
|
-
id: "person",
|
|
44300
|
-
name: "Person"
|
|
44301
|
-
},
|
|
44302
|
-
{
|
|
44303
|
-
id: "bicycle",
|
|
44304
|
-
name: "Bicycle"
|
|
44305
|
-
},
|
|
44306
|
-
{
|
|
44307
|
-
id: "car",
|
|
44308
|
-
name: "Car"
|
|
44309
|
-
},
|
|
44310
|
-
{
|
|
44311
|
-
id: "motorcycle",
|
|
44312
|
-
name: "Motorcycle"
|
|
44313
|
-
},
|
|
44314
|
-
{
|
|
44315
|
-
id: "airplane",
|
|
44316
|
-
name: "Airplane"
|
|
44317
|
-
},
|
|
44318
|
-
{
|
|
44319
|
-
id: "bus",
|
|
44320
|
-
name: "Bus"
|
|
44321
|
-
},
|
|
44322
|
-
{
|
|
44323
|
-
id: "train",
|
|
44324
|
-
name: "Train"
|
|
44325
|
-
},
|
|
44326
|
-
{
|
|
44327
|
-
id: "truck",
|
|
44328
|
-
name: "Truck"
|
|
44329
|
-
},
|
|
44330
|
-
{
|
|
44331
|
-
id: "boat",
|
|
44332
|
-
name: "Boat"
|
|
44333
|
-
},
|
|
44334
|
-
{
|
|
44335
|
-
id: "traffic light",
|
|
44336
|
-
name: "Traffic Light"
|
|
44337
|
-
},
|
|
44338
|
-
{
|
|
44339
|
-
id: "fire hydrant",
|
|
44340
|
-
name: "Fire Hydrant"
|
|
44341
|
-
},
|
|
44342
|
-
{
|
|
44343
|
-
id: "stop sign",
|
|
44344
|
-
name: "Stop Sign"
|
|
44345
|
-
},
|
|
44346
|
-
{
|
|
44347
|
-
id: "parking meter",
|
|
44348
|
-
name: "Parking Meter"
|
|
44349
|
-
},
|
|
44350
|
-
{
|
|
44351
|
-
id: "bench",
|
|
44352
|
-
name: "Bench"
|
|
44353
|
-
},
|
|
44354
|
-
{
|
|
44355
|
-
id: "bird",
|
|
44356
|
-
name: "Bird"
|
|
44357
|
-
},
|
|
44358
|
-
{
|
|
44359
|
-
id: "cat",
|
|
44360
|
-
name: "Cat"
|
|
44361
|
-
},
|
|
44362
|
-
{
|
|
44363
|
-
id: "dog",
|
|
44364
|
-
name: "Dog"
|
|
44365
|
-
},
|
|
44366
|
-
{
|
|
44367
|
-
id: "horse",
|
|
44368
|
-
name: "Horse"
|
|
44369
|
-
},
|
|
44370
|
-
{
|
|
44371
|
-
id: "sheep",
|
|
44372
|
-
name: "Sheep"
|
|
44373
|
-
},
|
|
44374
|
-
{
|
|
44375
|
-
id: "cow",
|
|
44376
|
-
name: "Cow"
|
|
44377
|
-
},
|
|
44378
|
-
{
|
|
44379
|
-
id: "elephant",
|
|
44380
|
-
name: "Elephant"
|
|
44381
|
-
},
|
|
44382
|
-
{
|
|
44383
|
-
id: "bear",
|
|
44384
|
-
name: "Bear"
|
|
44385
|
-
},
|
|
44386
|
-
{
|
|
44387
|
-
id: "zebra",
|
|
44388
|
-
name: "Zebra"
|
|
44389
|
-
},
|
|
44390
|
-
{
|
|
44391
|
-
id: "giraffe",
|
|
44392
|
-
name: "Giraffe"
|
|
44393
|
-
},
|
|
44394
|
-
{
|
|
44395
|
-
id: "backpack",
|
|
44396
|
-
name: "Backpack"
|
|
44397
|
-
},
|
|
44398
|
-
{
|
|
44399
|
-
id: "umbrella",
|
|
44400
|
-
name: "Umbrella"
|
|
44401
|
-
},
|
|
44402
|
-
{
|
|
44403
|
-
id: "handbag",
|
|
44404
|
-
name: "Handbag"
|
|
44405
|
-
},
|
|
44406
|
-
{
|
|
44407
|
-
id: "tie",
|
|
44408
|
-
name: "Tie"
|
|
44409
|
-
},
|
|
44410
|
-
{
|
|
44411
|
-
id: "suitcase",
|
|
44412
|
-
name: "Suitcase"
|
|
44413
|
-
},
|
|
44414
|
-
{
|
|
44415
|
-
id: "frisbee",
|
|
44416
|
-
name: "Frisbee"
|
|
44417
|
-
},
|
|
44418
|
-
{
|
|
44419
|
-
id: "skis",
|
|
44420
|
-
name: "Skis"
|
|
44421
|
-
},
|
|
44422
|
-
{
|
|
44423
|
-
id: "snowboard",
|
|
44424
|
-
name: "Snowboard"
|
|
44425
|
-
},
|
|
44426
|
-
{
|
|
44427
|
-
id: "sports ball",
|
|
44428
|
-
name: "Sports Ball"
|
|
44429
|
-
},
|
|
44430
|
-
{
|
|
44431
|
-
id: "kite",
|
|
44432
|
-
name: "Kite"
|
|
44433
|
-
},
|
|
44434
|
-
{
|
|
44435
|
-
id: "baseball bat",
|
|
44436
|
-
name: "Baseball Bat"
|
|
44437
|
-
},
|
|
44438
|
-
{
|
|
44439
|
-
id: "baseball glove",
|
|
44440
|
-
name: "Baseball Glove"
|
|
44441
|
-
},
|
|
44442
|
-
{
|
|
44443
|
-
id: "skateboard",
|
|
44444
|
-
name: "Skateboard"
|
|
44445
|
-
},
|
|
44446
|
-
{
|
|
44447
|
-
id: "surfboard",
|
|
44448
|
-
name: "Surfboard"
|
|
44449
|
-
},
|
|
44450
|
-
{
|
|
44451
|
-
id: "tennis racket",
|
|
44452
|
-
name: "Tennis Racket"
|
|
44453
|
-
},
|
|
44454
|
-
{
|
|
44455
|
-
id: "bottle",
|
|
44456
|
-
name: "Bottle"
|
|
44457
|
-
},
|
|
44458
|
-
{
|
|
44459
|
-
id: "wine glass",
|
|
44460
|
-
name: "Wine Glass"
|
|
44461
|
-
},
|
|
44462
|
-
{
|
|
44463
|
-
id: "cup",
|
|
44464
|
-
name: "Cup"
|
|
44465
|
-
},
|
|
44466
|
-
{
|
|
44467
|
-
id: "fork",
|
|
44468
|
-
name: "Fork"
|
|
44469
|
-
},
|
|
44470
|
-
{
|
|
44471
|
-
id: "knife",
|
|
44472
|
-
name: "Knife"
|
|
44473
|
-
},
|
|
44474
|
-
{
|
|
44475
|
-
id: "spoon",
|
|
44476
|
-
name: "Spoon"
|
|
44477
|
-
},
|
|
44478
|
-
{
|
|
44479
|
-
id: "bowl",
|
|
44480
|
-
name: "Bowl"
|
|
44481
|
-
},
|
|
44482
|
-
{
|
|
44483
|
-
id: "banana",
|
|
44484
|
-
name: "Banana"
|
|
44485
|
-
},
|
|
44486
|
-
{
|
|
44487
|
-
id: "apple",
|
|
44488
|
-
name: "Apple"
|
|
44489
|
-
},
|
|
44490
|
-
{
|
|
44491
|
-
id: "sandwich",
|
|
44492
|
-
name: "Sandwich"
|
|
44493
|
-
},
|
|
44494
|
-
{
|
|
44495
|
-
id: "orange",
|
|
44496
|
-
name: "Orange"
|
|
44497
|
-
},
|
|
44498
|
-
{
|
|
44499
|
-
id: "broccoli",
|
|
44500
|
-
name: "Broccoli"
|
|
44501
|
-
},
|
|
44502
|
-
{
|
|
44503
|
-
id: "carrot",
|
|
44504
|
-
name: "Carrot"
|
|
44505
|
-
},
|
|
44506
|
-
{
|
|
44507
|
-
id: "hot dog",
|
|
44508
|
-
name: "Hot Dog"
|
|
44509
|
-
},
|
|
44510
|
-
{
|
|
44511
|
-
id: "pizza",
|
|
44512
|
-
name: "Pizza"
|
|
44513
|
-
},
|
|
44514
|
-
{
|
|
44515
|
-
id: "donut",
|
|
44516
|
-
name: "Donut"
|
|
44517
|
-
},
|
|
44518
|
-
{
|
|
44519
|
-
id: "cake",
|
|
44520
|
-
name: "Cake"
|
|
44521
|
-
},
|
|
44522
|
-
{
|
|
44523
|
-
id: "chair",
|
|
44524
|
-
name: "Chair"
|
|
44525
|
-
},
|
|
44526
|
-
{
|
|
44527
|
-
id: "couch",
|
|
44528
|
-
name: "Couch"
|
|
44529
|
-
},
|
|
44530
|
-
{
|
|
44531
|
-
id: "potted plant",
|
|
44532
|
-
name: "Potted Plant"
|
|
44533
|
-
},
|
|
44534
|
-
{
|
|
44535
|
-
id: "bed",
|
|
44536
|
-
name: "Bed"
|
|
44537
|
-
},
|
|
44538
|
-
{
|
|
44539
|
-
id: "dining table",
|
|
44540
|
-
name: "Dining Table"
|
|
44541
|
-
},
|
|
44542
|
-
{
|
|
44543
|
-
id: "toilet",
|
|
44544
|
-
name: "Toilet"
|
|
44545
|
-
},
|
|
44546
|
-
{
|
|
44547
|
-
id: "tv",
|
|
44548
|
-
name: "TV"
|
|
44549
|
-
},
|
|
44550
|
-
{
|
|
44551
|
-
id: "laptop",
|
|
44552
|
-
name: "Laptop"
|
|
44553
|
-
},
|
|
44554
|
-
{
|
|
44555
|
-
id: "mouse",
|
|
44556
|
-
name: "Mouse"
|
|
44557
|
-
},
|
|
44558
|
-
{
|
|
44559
|
-
id: "remote",
|
|
44560
|
-
name: "Remote"
|
|
44561
|
-
},
|
|
44562
|
-
{
|
|
44563
|
-
id: "keyboard",
|
|
44564
|
-
name: "Keyboard"
|
|
44565
|
-
},
|
|
44566
|
-
{
|
|
44567
|
-
id: "cell phone",
|
|
44568
|
-
name: "Cell Phone"
|
|
44569
|
-
},
|
|
44570
|
-
{
|
|
44571
|
-
id: "microwave",
|
|
44572
|
-
name: "Microwave"
|
|
44573
|
-
},
|
|
44574
|
-
{
|
|
44575
|
-
id: "oven",
|
|
44576
|
-
name: "Oven"
|
|
44577
|
-
},
|
|
44578
|
-
{
|
|
44579
|
-
id: "toaster",
|
|
44580
|
-
name: "Toaster"
|
|
44581
|
-
},
|
|
44582
|
-
{
|
|
44583
|
-
id: "sink",
|
|
44584
|
-
name: "Sink"
|
|
44585
|
-
},
|
|
44586
|
-
{
|
|
44587
|
-
id: "refrigerator",
|
|
44588
|
-
name: "Refrigerator"
|
|
44589
|
-
},
|
|
44590
|
-
{
|
|
44591
|
-
id: "book",
|
|
44592
|
-
name: "Book"
|
|
44593
|
-
},
|
|
44594
|
-
{
|
|
44595
|
-
id: "clock",
|
|
44596
|
-
name: "Clock"
|
|
44597
|
-
},
|
|
44598
|
-
{
|
|
44599
|
-
id: "vase",
|
|
44600
|
-
name: "Vase"
|
|
44601
|
-
},
|
|
44602
|
-
{
|
|
44603
|
-
id: "scissors",
|
|
44604
|
-
name: "Scissors"
|
|
44605
|
-
},
|
|
44606
|
-
{
|
|
44607
|
-
id: "teddy bear",
|
|
44608
|
-
name: "Teddy Bear"
|
|
44609
|
-
},
|
|
44610
|
-
{
|
|
44611
|
-
id: "hair drier",
|
|
44612
|
-
name: "Hair Drier"
|
|
44613
|
-
},
|
|
44614
|
-
{
|
|
44615
|
-
id: "toothbrush",
|
|
44616
|
-
name: "Toothbrush"
|
|
44617
|
-
}
|
|
44618
|
-
];
|
|
44619
|
-
var MACRO_LABELS = [
|
|
44620
|
-
{
|
|
44621
|
-
id: "person",
|
|
44622
|
-
name: "Person"
|
|
44623
|
-
},
|
|
44624
|
-
{
|
|
44625
|
-
id: "vehicle",
|
|
44626
|
-
name: "Vehicle"
|
|
44627
|
-
},
|
|
44628
|
-
{
|
|
44629
|
-
id: "animal",
|
|
44630
|
-
name: "Animal"
|
|
44631
|
-
},
|
|
44632
|
-
{
|
|
44633
|
-
id: "package",
|
|
44634
|
-
name: "Package"
|
|
44635
|
-
}
|
|
44636
|
-
];
|
|
46117
|
+
"alert",
|
|
46118
|
+
"camera"
|
|
46119
|
+
]);
|
|
46120
|
+
/** A single tap-through action button. */
|
|
46121
|
+
var NotificationActionSchema = zod.z.object({
|
|
46122
|
+
id: zod.z.string(),
|
|
46123
|
+
label: zod.z.string(),
|
|
46124
|
+
url: zod.z.string().optional(),
|
|
46125
|
+
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
46126
|
+
icon: NotificationActionIconSchema.optional(),
|
|
46127
|
+
/**
|
|
46128
|
+
* Renders in a warning style where the notifier supports it.
|
|
46129
|
+
*
|
|
46130
|
+
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
46131
|
+
* else — see `notification-center/action-token.ts` for what that does and
|
|
46132
|
+
* does not buy.
|
|
46133
|
+
*/
|
|
46134
|
+
destructive: zod.z.boolean().optional(),
|
|
46135
|
+
/**
|
|
46136
|
+
* How the tap should REACH the url.
|
|
46137
|
+
*
|
|
46138
|
+
* `navigate` (absent, and every button authored before this field) opens it:
|
|
46139
|
+
* the phone leaves the notification and shows whatever the callback returns.
|
|
46140
|
+
* That is right for a button whose answer the operator wants to read.
|
|
46141
|
+
*
|
|
46142
|
+
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
46143
|
+
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
46144
|
+
* an answer to the notification, and being thrown into a browser tab to
|
|
46145
|
+
* confirm it costs more attention than the notification did. A backend that
|
|
46146
|
+
* cannot do a background call renders it as an ordinary link (the adapters
|
|
46147
|
+
* fall back rather than dropping the button), so this is a preference, never
|
|
46148
|
+
* a requirement.
|
|
46149
|
+
*/
|
|
46150
|
+
mode: zod.z.enum(["navigate", "background"]).optional()
|
|
46151
|
+
});
|
|
44637
46152
|
/**
|
|
44638
|
-
* The
|
|
44639
|
-
*
|
|
44640
|
-
*
|
|
44641
|
-
*
|
|
44642
|
-
*
|
|
44643
|
-
*
|
|
44644
|
-
* Any surface that indexes counts by class name — the ZoneAnalytics occupancy
|
|
44645
|
-
* snapshot's `byClass`, and therefore every occupancy rule — can match nothing
|
|
44646
|
-
* else. Derived from the catalog, never hand-listed, so a new macro joins it by
|
|
44647
|
-
* being declared once.
|
|
44648
|
-
*
|
|
44649
|
-
* Cost of not having it, 2026-08-12: the notification rule editor offered the
|
|
44650
|
-
* full taxonomy for an occupancy class (occupancy is the one condition that
|
|
44651
|
-
* does not go through the engine's macro-expanding `expandClassSelector`), two
|
|
44652
|
-
* live rules were authored on `car`, and both counters read 0 forever.
|
|
46153
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
46154
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
46155
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
46156
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
46157
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
46158
|
+
* `priority` for that one target.
|
|
44653
46159
|
*/
|
|
44654
|
-
var
|
|
44655
|
-
|
|
44656
|
-
|
|
44657
|
-
|
|
44658
|
-
|
|
44659
|
-
|
|
44660
|
-
|
|
44661
|
-
|
|
44662
|
-
|
|
44663
|
-
|
|
44664
|
-
|
|
44665
|
-
|
|
44666
|
-
|
|
44667
|
-
|
|
44668
|
-
|
|
44669
|
-
|
|
44670
|
-
|
|
44671
|
-
|
|
44672
|
-
|
|
44673
|
-
|
|
44674
|
-
|
|
44675
|
-
|
|
44676
|
-
|
|
44677
|
-
|
|
44678
|
-
|
|
44679
|
-
|
|
44680
|
-
|
|
44681
|
-
|
|
44682
|
-
|
|
44683
|
-
|
|
44684
|
-
|
|
44685
|
-
|
|
46160
|
+
var NotificationSchema = zod.z.object({
|
|
46161
|
+
body: zod.z.string(),
|
|
46162
|
+
title: zod.z.string().optional(),
|
|
46163
|
+
format: NotificationFormatSchema.default("text"),
|
|
46164
|
+
priority: zod.z.number().int().min(1).max(5).default(3),
|
|
46165
|
+
level: zod.z.string().optional(),
|
|
46166
|
+
attachments: zod.z.array(AttachmentSchema).optional(),
|
|
46167
|
+
clickUrl: zod.z.string().optional(),
|
|
46168
|
+
actions: zod.z.array(NotificationActionSchema).optional(),
|
|
46169
|
+
sound: zod.z.string().optional(),
|
|
46170
|
+
ttl: zod.z.number().optional(),
|
|
46171
|
+
tag: zod.z.string().optional(),
|
|
46172
|
+
deviceId: zod.z.number().optional(),
|
|
46173
|
+
eventId: zod.z.string().optional(),
|
|
46174
|
+
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
46175
|
+
});
|
|
46176
|
+
/** One declared native severity/priority level for a kind. */
|
|
46177
|
+
var TargetKindLevelSchema = zod.z.object({
|
|
46178
|
+
id: zod.z.string(),
|
|
46179
|
+
label: zod.z.string(),
|
|
46180
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
46181
|
+
ordinal: zod.z.number().int().min(1).max(5).nullable(),
|
|
46182
|
+
flags: zod.z.object({
|
|
46183
|
+
critical: zod.z.boolean().optional(),
|
|
46184
|
+
silent: zod.z.boolean().optional(),
|
|
46185
|
+
noPush: zod.z.boolean().optional()
|
|
46186
|
+
}).optional(),
|
|
46187
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
46188
|
+
requires: zod.z.array(zod.z.string()).optional(),
|
|
46189
|
+
description: zod.z.string().optional()
|
|
46190
|
+
});
|
|
46191
|
+
/** Attachment capabilities for a kind (drives the degrade engine + test panel). */
|
|
46192
|
+
var TargetKindAttachmentsCapsSchema = zod.z.object({
|
|
46193
|
+
mediaTypes: zod.z.array(AttachmentMediaTypeSchema),
|
|
46194
|
+
mode: zod.z.enum([
|
|
46195
|
+
"url",
|
|
46196
|
+
"bytes",
|
|
46197
|
+
"both"
|
|
46198
|
+
]),
|
|
46199
|
+
max: zod.z.number().int().nonnegative(),
|
|
46200
|
+
maxBytes: zod.z.number().int().positive().optional()
|
|
46201
|
+
});
|
|
46202
|
+
/** The full capability block consulted before dispatch. */
|
|
46203
|
+
var TargetKindCapsSchema = zod.z.object({
|
|
46204
|
+
attachments: TargetKindAttachmentsCapsSchema,
|
|
46205
|
+
/** Max action buttons (0 = none). */
|
|
46206
|
+
actions: zod.z.number().int().nonnegative(),
|
|
46207
|
+
/**
|
|
46208
|
+
* Whether this kind renders a per-action ICON.
|
|
46209
|
+
*
|
|
46210
|
+
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
46211
|
+
* run on the addon cap path — three production failures in one day taught
|
|
46212
|
+
* this repo that once. Absent is read as false by the degrade engine, which
|
|
46213
|
+
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
46214
|
+
* assumed and dropped costs the operator's trust in the field.
|
|
46215
|
+
*/
|
|
46216
|
+
actionIcons: zod.z.boolean().optional(),
|
|
46217
|
+
levels: zod.z.array(TargetKindLevelSchema),
|
|
46218
|
+
format: zod.z.array(NotificationFormatSchema),
|
|
46219
|
+
clickUrl: zod.z.boolean(),
|
|
46220
|
+
sound: zod.z.boolean(),
|
|
46221
|
+
ttl: zod.z.boolean(),
|
|
46222
|
+
bodyMaxLen: zod.z.number().int().positive()
|
|
46223
|
+
});
|
|
46224
|
+
/**
|
|
46225
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
46226
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
46227
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
46228
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
46229
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
46230
|
+
*/
|
|
46231
|
+
var ConfigSchemaPassthrough$2 = zod.z.unknown();
|
|
46232
|
+
var TargetKindSchema = zod.z.object({
|
|
46233
|
+
kind: zod.z.string(),
|
|
46234
|
+
label: zod.z.string(),
|
|
46235
|
+
icon: zod.z.string(),
|
|
46236
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
46237
|
+
addonId: zod.z.string(),
|
|
46238
|
+
/**
|
|
46239
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
46240
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
46241
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
46242
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
46243
|
+
*
|
|
46244
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
46245
|
+
* client, and a native client joins it onto its own hub base.
|
|
46246
|
+
*
|
|
46247
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
46248
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
46249
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
46250
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
46251
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
46252
|
+
*/
|
|
46253
|
+
iconUrl: zod.z.string().optional(),
|
|
46254
|
+
/**
|
|
46255
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
46256
|
+
*
|
|
46257
|
+
* The server knows this and therefore says it, because the client cannot
|
|
46258
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
46259
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
46260
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
46261
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
46262
|
+
*
|
|
46263
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
46264
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
46265
|
+
* its raster path, which is the safe default for an unknown image.
|
|
46266
|
+
*/
|
|
46267
|
+
iconMediaType: zod.z.string().optional(),
|
|
46268
|
+
configSchema: ConfigSchemaPassthrough$2,
|
|
46269
|
+
/**
|
|
46270
|
+
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
46271
|
+
* base URL, instance, chat, recipient). A save that changes any of them
|
|
46272
|
+
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
46273
|
+
* editor uses this to ask for the secret on exactly those edits instead of
|
|
46274
|
+
* on every edit.
|
|
46275
|
+
*
|
|
46276
|
+
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
46277
|
+
* kinds) and from a provider older than this field — an editor that finds
|
|
46278
|
+
* it absent must assume ANY edit may need the secret.
|
|
46279
|
+
*/
|
|
46280
|
+
destinationKeys: zod.z.array(zod.z.string()).optional(),
|
|
46281
|
+
supportsDiscovery: zod.z.boolean(),
|
|
46282
|
+
caps: TargetKindCapsSchema
|
|
46283
|
+
});
|
|
46284
|
+
/**
|
|
46285
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
46286
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
46287
|
+
* round-trip a stored secret to the UI.
|
|
46288
|
+
*/
|
|
46289
|
+
var TargetSchema = zod.z.object({
|
|
46290
|
+
id: zod.z.string(),
|
|
46291
|
+
name: zod.z.string(),
|
|
46292
|
+
kind: zod.z.string(),
|
|
46293
|
+
addonId: zod.z.string(),
|
|
46294
|
+
enabled: zod.z.boolean(),
|
|
46295
|
+
config: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
46296
|
+
});
|
|
46297
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
46298
|
+
var DiscoveredTargetSchema = zod.z.object({
|
|
46299
|
+
kind: zod.z.string(),
|
|
46300
|
+
suggestedName: zod.z.string(),
|
|
46301
|
+
config: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
46302
|
+
});
|
|
46303
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
46304
|
+
var RenderedAsSchema = zod.z.object({
|
|
46305
|
+
level: zod.z.string(),
|
|
46306
|
+
format: NotificationFormatSchema,
|
|
46307
|
+
attachmentsSent: zod.z.number().int().nonnegative(),
|
|
46308
|
+
actionsSent: zod.z.number().int().nonnegative(),
|
|
46309
|
+
truncated: zod.z.boolean(),
|
|
46310
|
+
dropped: zod.z.array(zod.z.string())
|
|
46311
|
+
});
|
|
46312
|
+
var SendResultSchema = zod.z.object({
|
|
46313
|
+
success: zod.z.boolean(),
|
|
46314
|
+
error: zod.z.string().optional(),
|
|
46315
|
+
renderedAs: RenderedAsSchema.optional()
|
|
46316
|
+
});
|
|
46317
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
46318
|
+
var TestResultSchema = SendResultSchema;
|
|
46319
|
+
/**
|
|
46320
|
+
* Pre-save credential probe against a partial (unsaved) target config.
|
|
46321
|
+
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
46322
|
+
*/
|
|
46323
|
+
var ProbeTargetConfigResultSchema = zod.z.object({
|
|
46324
|
+
ok: zod.z.boolean(),
|
|
46325
|
+
resolvedUrl: zod.z.string(),
|
|
46326
|
+
httpStatus: zod.z.number().int().optional(),
|
|
46327
|
+
stateInstance: zod.z.string().optional(),
|
|
46328
|
+
error: zod.z.string().optional()
|
|
46329
|
+
});
|
|
46330
|
+
var notificationOutputCapability = {
|
|
46331
|
+
name: "notification-output",
|
|
46332
|
+
scope: "system",
|
|
46333
|
+
mode: "collection",
|
|
46334
|
+
methods: {
|
|
46335
|
+
listTargetKinds: require_sleep.method(zod.z.object({}), zod.z.array(TargetKindSchema)),
|
|
46336
|
+
listTargets: require_sleep.method(zod.z.object({}), zod.z.array(TargetSchema), { auth: "admin" }),
|
|
46337
|
+
/**
|
|
46338
|
+
* One target WITH secrets — admin edit / duplicate hydration only.
|
|
46339
|
+
* `listTargets` stays redacted; the editor loads this so password fields
|
|
46340
|
+
* are filled (masked in the input) and a save can round-trip the token
|
|
46341
|
+
* when a destination changes (Duplicate with a new chatId, etc.).
|
|
46342
|
+
*/
|
|
46343
|
+
getTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), TargetSchema, { auth: "admin" }),
|
|
46344
|
+
discoverTargets: require_sleep.method(zod.z.object({
|
|
46345
|
+
kind: zod.z.string(),
|
|
46346
|
+
config: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
46347
|
+
/**
|
|
46348
|
+
* When editing / duplicating, the blank password field never round-trips
|
|
46349
|
+
* the stored token. Pass the source target id so the provider can restore
|
|
46350
|
+
* secrets server-side before dialling (same merge as upsert).
|
|
46351
|
+
*/
|
|
46352
|
+
targetId: zod.z.string().optional()
|
|
46353
|
+
}), zod.z.array(DiscoveredTargetSchema), { auth: "admin" }),
|
|
46354
|
+
probeTargetConfig: require_sleep.method(zod.z.object({
|
|
46355
|
+
kind: zod.z.string(),
|
|
46356
|
+
config: zod.z.record(zod.z.string(), zod.z.unknown()),
|
|
46357
|
+
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
46358
|
+
targetId: zod.z.string().optional()
|
|
46359
|
+
}), ProbeTargetConfigResultSchema, {
|
|
46360
|
+
kind: "mutation",
|
|
46361
|
+
auth: "admin"
|
|
46362
|
+
}),
|
|
46363
|
+
send: require_sleep.method(zod.z.object({
|
|
46364
|
+
targetId: zod.z.string(),
|
|
46365
|
+
notification: NotificationSchema
|
|
46366
|
+
}), SendResultSchema, {
|
|
46367
|
+
kind: "mutation",
|
|
46368
|
+
auth: "admin"
|
|
46369
|
+
}),
|
|
46370
|
+
testTarget: require_sleep.method(zod.z.object({
|
|
46371
|
+
targetId: zod.z.string(),
|
|
46372
|
+
sample: NotificationSchema.optional()
|
|
46373
|
+
}), TestResultSchema, {
|
|
46374
|
+
kind: "mutation",
|
|
46375
|
+
auth: "admin"
|
|
46376
|
+
}),
|
|
46377
|
+
upsertTarget: require_sleep.method(zod.z.object({
|
|
46378
|
+
target: TargetSchema,
|
|
46379
|
+
/**
|
|
46380
|
+
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
46381
|
+
* existing row. The UI never holds the stored token; blank / redacted
|
|
46382
|
+
* password fields are restored from this source (D634 still applies if
|
|
46383
|
+
* destinations diverge without a re-typed secret).
|
|
46384
|
+
*/
|
|
46385
|
+
cloneSecretsFrom: zod.z.string().optional()
|
|
46386
|
+
}), TargetSchema, {
|
|
46387
|
+
kind: "mutation",
|
|
46388
|
+
auth: "admin"
|
|
46389
|
+
}),
|
|
46390
|
+
deleteTarget: require_sleep.method(zod.z.object({ targetId: zod.z.string() }), zod.z.void(), {
|
|
46391
|
+
kind: "mutation",
|
|
46392
|
+
auth: "admin"
|
|
46393
|
+
}),
|
|
46394
|
+
setTargetEnabled: require_sleep.method(zod.z.object({
|
|
46395
|
+
targetId: zod.z.string(),
|
|
46396
|
+
enabled: zod.z.boolean()
|
|
46397
|
+
}), zod.z.void(), {
|
|
46398
|
+
kind: "mutation",
|
|
46399
|
+
auth: "admin"
|
|
46400
|
+
})
|
|
46401
|
+
}
|
|
44686
46402
|
};
|
|
44687
46403
|
//#endregion
|
|
44688
46404
|
//#region src/catalogs/audio-classmap.ts
|
|
@@ -62628,6 +64344,152 @@ var audioMetricsCapability = {
|
|
|
62628
64344
|
history: "none"
|
|
62629
64345
|
};
|
|
62630
64346
|
//#endregion
|
|
64347
|
+
//#region src/capabilities/action-source.cap.ts
|
|
64348
|
+
/**
|
|
64349
|
+
* action-source — an addon DECLARES runnable actions (a Home Assistant script,
|
|
64350
|
+
* a scene, a vendor routine) as list entries. Never as devices.
|
|
64351
|
+
*
|
|
64352
|
+
* It mirrors `notification-output`: `listActions` returns an array, so the
|
|
64353
|
+
* generated cap mount concat-fans it across every provider; `runAction` and
|
|
64354
|
+
* `stopAction` route to ONE provider by the `addonId` the generated collection
|
|
64355
|
+
* router extracts from the call input.
|
|
64356
|
+
*
|
|
64357
|
+
* Nobody but the `actions` surface (the core-blocks builtin) calls this cap.
|
|
64358
|
+
* The UI and automations talk to `actions`, which fans out here LIVE on every
|
|
64359
|
+
* `list` and keeps no catalog of its own (D3: no shadow registry).
|
|
64360
|
+
*
|
|
64361
|
+
* `listActions` entries carry `source: 'addon'`, `readOnly: true`, an id of
|
|
64362
|
+
* `<addonId>:<localId>` and an idle `status` (the provider may fill what it
|
|
64363
|
+
* knows). `actionId` on `runAction` / `stopAction` is the full entry id.
|
|
64364
|
+
*/
|
|
64365
|
+
var actionSourceCapability = {
|
|
64366
|
+
name: "action-source",
|
|
64367
|
+
scope: "system",
|
|
64368
|
+
mode: "collection",
|
|
64369
|
+
methods: {
|
|
64370
|
+
listActions: require_sleep.method(zod.z.object({}), zod.z.array(ActionEntrySchema), { auth: "admin" }),
|
|
64371
|
+
runAction: require_sleep.method(zod.z.object({
|
|
64372
|
+
actionId: zod.z.string().min(1),
|
|
64373
|
+
variables: zod.z.record(zod.z.string(), ActionVariableValueSchema).optional()
|
|
64374
|
+
}), zod.z.void(), {
|
|
64375
|
+
kind: "mutation",
|
|
64376
|
+
auth: "admin"
|
|
64377
|
+
}),
|
|
64378
|
+
stopAction: require_sleep.method(zod.z.object({ actionId: zod.z.string().min(1) }), zod.z.void(), {
|
|
64379
|
+
kind: "mutation",
|
|
64380
|
+
auth: "admin"
|
|
64381
|
+
})
|
|
64382
|
+
}
|
|
64383
|
+
};
|
|
64384
|
+
//#endregion
|
|
64385
|
+
//#region src/capabilities/actions.cap.ts
|
|
64386
|
+
/**
|
|
64387
|
+
* actions — the ONE surface for runnable actions, for the UI and for
|
|
64388
|
+
* automations. Provided by the core-blocks builtin on the hub.
|
|
64389
|
+
*
|
|
64390
|
+
* An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
|
|
64391
|
+
* steps, kept in the hub's `actions` record kind) or declared by an addon over
|
|
64392
|
+
* `action-source`. `list` asks every `action-source` provider LIVE and overlays
|
|
64393
|
+
* the run status from the hub's `runs` ledger; there is no catalog here to
|
|
64394
|
+
* drift (D3).
|
|
64395
|
+
*
|
|
64396
|
+
* **Permissions follow the camera, as linked devices do.** An action is linked
|
|
64397
|
+
* to cameras by the operator (`setLinks`, D356: nothing links itself). A
|
|
64398
|
+
* non-admin may `get` / `run` an action only through a camera they may see that
|
|
64399
|
+
* links it: they name the camera (`deviceId`, a top-level field so the scope
|
|
64400
|
+
* gate sees it) and the provider refuses an action that camera does not link.
|
|
64401
|
+
* An admin, and an in-process caller (an automation block), may run any action.
|
|
64402
|
+
* Every refusal is typed (`[not-linked] …`) and logged.
|
|
64403
|
+
*
|
|
64404
|
+
* A second `run` of an action that is already running is REFUSED — traced and
|
|
64405
|
+
* logged — never queued.
|
|
64406
|
+
*/
|
|
64407
|
+
var ActionIdInput = zod.z.object({ actionId: zod.z.string().min(1) });
|
|
64408
|
+
var ActionLinksSchema = zod.z.object({
|
|
64409
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
64410
|
+
actionIds: zod.z.array(zod.z.string().min(1)).max(100)
|
|
64411
|
+
});
|
|
64412
|
+
var actionsCapability = {
|
|
64413
|
+
name: "actions",
|
|
64414
|
+
scope: "system",
|
|
64415
|
+
mode: "singleton",
|
|
64416
|
+
methods: {
|
|
64417
|
+
/** Every action, live from the providers, with its run status. Admin. */
|
|
64418
|
+
list: require_sleep.method(zod.z.object({}), zod.z.array(ActionEntrySchema), { auth: "admin" }),
|
|
64419
|
+
/**
|
|
64420
|
+
* One action. A non-admin names the camera (`deviceId`) that links it.
|
|
64421
|
+
* `null` when it does not exist.
|
|
64422
|
+
*/
|
|
64423
|
+
get: require_sleep.method(zod.z.object({
|
|
64424
|
+
actionId: zod.z.string().min(1),
|
|
64425
|
+
deviceId: zod.z.number().int().nonnegative().optional()
|
|
64426
|
+
}), zod.z.object({ action: ActionEntrySchema.nullable() }), {
|
|
64427
|
+
role: "viewer",
|
|
64428
|
+
auth: "protected",
|
|
64429
|
+
kind: "query",
|
|
64430
|
+
caller: "required"
|
|
64431
|
+
}),
|
|
64432
|
+
/**
|
|
64433
|
+
* Start an action. `wait` holds the call until the run ended (an automation
|
|
64434
|
+
* step does); without it the call returns `started`. A non-admin names the
|
|
64435
|
+
* camera (`deviceId`) that links the action.
|
|
64436
|
+
*/
|
|
64437
|
+
run: require_sleep.method(zod.z.object({
|
|
64438
|
+
actionId: zod.z.string().min(1),
|
|
64439
|
+
variables: zod.z.record(zod.z.string(), ActionVariableValueSchema).optional(),
|
|
64440
|
+
wait: zod.z.boolean().optional(),
|
|
64441
|
+
origin: ActionRunOriginSchema.optional(),
|
|
64442
|
+
deviceId: zod.z.number().int().nonnegative().optional()
|
|
64443
|
+
}), ActionRunResultSchema, {
|
|
64444
|
+
role: "viewer",
|
|
64445
|
+
auth: "protected",
|
|
64446
|
+
kind: "mutation",
|
|
64447
|
+
caller: "required"
|
|
64448
|
+
}),
|
|
64449
|
+
stop: require_sleep.method(ActionIdInput, zod.z.void(), {
|
|
64450
|
+
kind: "mutation",
|
|
64451
|
+
auth: "admin"
|
|
64452
|
+
}),
|
|
64453
|
+
getTraces: require_sleep.method(ActionIdInput, zod.z.object({ traces: zod.z.array(AutomationRunTraceSchema) }), { auth: "admin" }),
|
|
64454
|
+
/** Internal actions only. A read-only (addon) entry is refused with `[read-only]`. */
|
|
64455
|
+
create: require_sleep.method(zod.z.object({ action: ActionInputSchema }), zod.z.object({ action: ActionEntrySchema }), {
|
|
64456
|
+
kind: "mutation",
|
|
64457
|
+
auth: "admin"
|
|
64458
|
+
}),
|
|
64459
|
+
update: require_sleep.method(zod.z.object({
|
|
64460
|
+
actionId: zod.z.string().min(1),
|
|
64461
|
+
action: ActionInputSchema.partial()
|
|
64462
|
+
}), zod.z.object({ action: ActionEntrySchema }), {
|
|
64463
|
+
kind: "mutation",
|
|
64464
|
+
auth: "admin"
|
|
64465
|
+
}),
|
|
64466
|
+
delete: require_sleep.method(ActionIdInput, zod.z.void(), {
|
|
64467
|
+
kind: "mutation",
|
|
64468
|
+
auth: "admin"
|
|
64469
|
+
}),
|
|
64470
|
+
/**
|
|
64471
|
+
* What a viewer sees under a camera: the actions this camera LINKS, with
|
|
64472
|
+
* their variables, and only when the caller may see the camera. The scope
|
|
64473
|
+
* gate admits the camera; the answer is cut to the linked ones.
|
|
64474
|
+
*/
|
|
64475
|
+
listForCamera: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.array(ActionEntrySchema), {
|
|
64476
|
+
role: "viewer",
|
|
64477
|
+
auth: "protected",
|
|
64478
|
+
kind: "query"
|
|
64479
|
+
}),
|
|
64480
|
+
/** The actions a camera links. Admin (the picker). */
|
|
64481
|
+
getLinks: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }),
|
|
64482
|
+
/** Replace a camera's linked actions with what the operator picked. */
|
|
64483
|
+
setLinks: require_sleep.method(ActionLinksSchema, ActionLinksSchema, {
|
|
64484
|
+
kind: "mutation",
|
|
64485
|
+
auth: "admin"
|
|
64486
|
+
})
|
|
64487
|
+
},
|
|
64488
|
+
events: {
|
|
64489
|
+
/** A run started or ended. Telemetry (D8): `list` is the authority. */
|
|
64490
|
+
onRunChanged: { data: ActionRunChangedSchema } }
|
|
64491
|
+
};
|
|
64492
|
+
//#endregion
|
|
62631
64493
|
//#region src/capabilities/automation-control.cap.ts
|
|
62632
64494
|
/**
|
|
62633
64495
|
* Automation-control cap. Models HA `automation.*` entities on
|
|
@@ -70222,10 +72084,20 @@ var PetFeederDeviceStatusSchema = zod.z.enum([
|
|
|
70222
72084
|
]);
|
|
70223
72085
|
var gramsPortion = zod.z.number().int().min(4).max(200);
|
|
70224
72086
|
var PetFeederStatusSchema = zod.z.object({
|
|
70225
|
-
/** Food
|
|
70226
|
-
*
|
|
70227
|
-
* bowl
|
|
72087
|
+
/** Food in the TANK (hopper), as the feeder's own sensor reports it
|
|
72088
|
+
* (`state.food`). A coarse level code, not grams — `0` is empty. The
|
|
72089
|
+
* amount in the bowl is `bowlFood`. On dual-hopper models this is the
|
|
72090
|
+
* combined reading; per-hopper levels live in `food1`/`food2`. Null when
|
|
72091
|
+
* the device has not reported yet. */
|
|
70228
72092
|
foodLevel: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
|
|
72093
|
+
/** Food in the BOWL as the feeder reports it (`state.bowl`), in the
|
|
72094
|
+
* feeder's own units. Null when the model reports none. */
|
|
72095
|
+
bowlFood: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
|
|
72096
|
+
/** True while the pet is eating (`state.eating`). */
|
|
72097
|
+
eating: zod.z.boolean(),
|
|
72098
|
+
/** Whether `markFoodReplenished` does anything on this model. When false
|
|
72099
|
+
* the client must not offer it. */
|
|
72100
|
+
supportsMarkRefilled: zod.z.boolean(),
|
|
70229
72101
|
/** Hopper-1 food level (grams) on dual-hopper feeders; null on
|
|
70230
72102
|
* single-hopper models. */
|
|
70231
72103
|
food1: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
|
|
@@ -70233,8 +72105,8 @@ var PetFeederStatusSchema = zod.z.object({
|
|
|
70233
72105
|
* single-hopper models. */
|
|
70234
72106
|
food2: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
|
|
70235
72107
|
/** Derived low-food flag — mirrors the HA `food_level` binary_sensor
|
|
70236
|
-
* (`device_class: problem`, on = low). True when the
|
|
70237
|
-
*
|
|
72108
|
+
* (`device_class: problem`, on = low). True when the tank reports empty,
|
|
72109
|
+
* or, on a dual-hopper model, when EITHER hopper is empty. */
|
|
70238
72110
|
lowFood: zod.z.boolean(),
|
|
70239
72111
|
/** Battery charge 0..100 (%). Null on mains-powered models or when the
|
|
70240
72112
|
* device has no battery reading. */
|
|
@@ -70302,7 +72174,9 @@ var petFeederCapability = {
|
|
|
70302
72174
|
kind: "mutation",
|
|
70303
72175
|
auth: "protected"
|
|
70304
72176
|
}),
|
|
70305
|
-
/**
|
|
72177
|
+
/** Tell the PetKit cloud a hopper was refilled so it stops its low-food
|
|
72178
|
+
* reminders (D4H/D4S/D4SH only — see `supportsMarkRefilled`). It does not
|
|
72179
|
+
* change any sensor reading. */
|
|
70306
72180
|
markFoodReplenished: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
|
|
70307
72181
|
kind: "mutation",
|
|
70308
72182
|
auth: "protected"
|
|
@@ -70375,6 +72249,7 @@ var petFeederCapability = {
|
|
|
70375
72249
|
*/
|
|
70376
72250
|
history: { fields: {
|
|
70377
72251
|
foodLevel: { kind: "gauge" },
|
|
72252
|
+
bowlFood: { kind: "gauge" },
|
|
70378
72253
|
food1: { kind: "gauge" },
|
|
70379
72254
|
food2: { kind: "gauge" },
|
|
70380
72255
|
batteryPower: {
|
|
@@ -85207,6 +87082,104 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
85207
87082
|
access: "create",
|
|
85208
87083
|
role: "admin"
|
|
85209
87084
|
},
|
|
87085
|
+
"actions.create": {
|
|
87086
|
+
capName: "actions",
|
|
87087
|
+
capScope: "system",
|
|
87088
|
+
addonId: null,
|
|
87089
|
+
access: "create",
|
|
87090
|
+
role: "admin"
|
|
87091
|
+
},
|
|
87092
|
+
"actions.delete": {
|
|
87093
|
+
capName: "actions",
|
|
87094
|
+
capScope: "system",
|
|
87095
|
+
addonId: null,
|
|
87096
|
+
access: "delete",
|
|
87097
|
+
role: "admin"
|
|
87098
|
+
},
|
|
87099
|
+
"actions.get": {
|
|
87100
|
+
capName: "actions",
|
|
87101
|
+
capScope: "system",
|
|
87102
|
+
addonId: null,
|
|
87103
|
+
access: "view",
|
|
87104
|
+
role: "viewer"
|
|
87105
|
+
},
|
|
87106
|
+
"actions.getLinks": {
|
|
87107
|
+
capName: "actions",
|
|
87108
|
+
capScope: "system",
|
|
87109
|
+
addonId: null,
|
|
87110
|
+
access: "view",
|
|
87111
|
+
role: "admin"
|
|
87112
|
+
},
|
|
87113
|
+
"actions.getTraces": {
|
|
87114
|
+
capName: "actions",
|
|
87115
|
+
capScope: "system",
|
|
87116
|
+
addonId: null,
|
|
87117
|
+
access: "view",
|
|
87118
|
+
role: "admin"
|
|
87119
|
+
},
|
|
87120
|
+
"actions.list": {
|
|
87121
|
+
capName: "actions",
|
|
87122
|
+
capScope: "system",
|
|
87123
|
+
addonId: null,
|
|
87124
|
+
access: "view",
|
|
87125
|
+
role: "admin"
|
|
87126
|
+
},
|
|
87127
|
+
"actions.listForCamera": {
|
|
87128
|
+
capName: "actions",
|
|
87129
|
+
capScope: "system",
|
|
87130
|
+
addonId: null,
|
|
87131
|
+
access: "view",
|
|
87132
|
+
role: "viewer"
|
|
87133
|
+
},
|
|
87134
|
+
"actions.run": {
|
|
87135
|
+
capName: "actions",
|
|
87136
|
+
capScope: "system",
|
|
87137
|
+
addonId: null,
|
|
87138
|
+
access: "create",
|
|
87139
|
+
role: "viewer"
|
|
87140
|
+
},
|
|
87141
|
+
"actions.setLinks": {
|
|
87142
|
+
capName: "actions",
|
|
87143
|
+
capScope: "system",
|
|
87144
|
+
addonId: null,
|
|
87145
|
+
access: "create",
|
|
87146
|
+
role: "admin"
|
|
87147
|
+
},
|
|
87148
|
+
"actions.stop": {
|
|
87149
|
+
capName: "actions",
|
|
87150
|
+
capScope: "system",
|
|
87151
|
+
addonId: null,
|
|
87152
|
+
access: "create",
|
|
87153
|
+
role: "admin"
|
|
87154
|
+
},
|
|
87155
|
+
"actions.update": {
|
|
87156
|
+
capName: "actions",
|
|
87157
|
+
capScope: "system",
|
|
87158
|
+
addonId: null,
|
|
87159
|
+
access: "create",
|
|
87160
|
+
role: "admin"
|
|
87161
|
+
},
|
|
87162
|
+
"actionSource.listActions": {
|
|
87163
|
+
capName: "action-source",
|
|
87164
|
+
capScope: "system",
|
|
87165
|
+
addonId: null,
|
|
87166
|
+
access: "view",
|
|
87167
|
+
role: "admin"
|
|
87168
|
+
},
|
|
87169
|
+
"actionSource.runAction": {
|
|
87170
|
+
capName: "action-source",
|
|
87171
|
+
capScope: "system",
|
|
87172
|
+
addonId: null,
|
|
87173
|
+
access: "create",
|
|
87174
|
+
role: "admin"
|
|
87175
|
+
},
|
|
87176
|
+
"actionSource.stopAction": {
|
|
87177
|
+
capName: "action-source",
|
|
87178
|
+
capScope: "system",
|
|
87179
|
+
addonId: null,
|
|
87180
|
+
access: "create",
|
|
87181
|
+
role: "admin"
|
|
87182
|
+
},
|
|
85210
87183
|
"addonPages.listPages": {
|
|
85211
87184
|
capName: "addon-pages",
|
|
85212
87185
|
capScope: "system",
|
|
@@ -86488,6 +88461,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
86488
88461
|
access: "view",
|
|
86489
88462
|
role: "admin"
|
|
86490
88463
|
},
|
|
88464
|
+
"coreBlocks.getAutomationRunState": {
|
|
88465
|
+
capName: "core-blocks",
|
|
88466
|
+
capScope: "system",
|
|
88467
|
+
addonId: null,
|
|
88468
|
+
access: "view",
|
|
88469
|
+
role: "admin"
|
|
88470
|
+
},
|
|
86491
88471
|
"coreBlocks.getBlockSettings": {
|
|
86492
88472
|
capName: "core-blocks",
|
|
86493
88473
|
capScope: "system",
|
|
@@ -86509,6 +88489,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
86509
88489
|
access: "view",
|
|
86510
88490
|
role: "admin"
|
|
86511
88491
|
},
|
|
88492
|
+
"coreBlocks.getTraces": {
|
|
88493
|
+
capName: "core-blocks",
|
|
88494
|
+
capScope: "system",
|
|
88495
|
+
addonId: null,
|
|
88496
|
+
access: "view",
|
|
88497
|
+
role: "admin"
|
|
88498
|
+
},
|
|
86512
88499
|
"coreBlocks.getTypeDefs": {
|
|
86513
88500
|
capName: "core-blocks",
|
|
86514
88501
|
capScope: "system",
|
|
@@ -86544,6 +88531,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
86544
88531
|
access: "create",
|
|
86545
88532
|
role: "admin"
|
|
86546
88533
|
},
|
|
88534
|
+
"coreBlocks.reportAutomationRun": {
|
|
88535
|
+
capName: "core-blocks",
|
|
88536
|
+
capScope: "system",
|
|
88537
|
+
addonId: null,
|
|
88538
|
+
access: "create",
|
|
88539
|
+
role: "admin"
|
|
88540
|
+
},
|
|
86547
88541
|
"coreBlocks.restart": {
|
|
86548
88542
|
capName: "core-blocks",
|
|
86549
88543
|
capScope: "system",
|
|
@@ -94906,6 +96900,8 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
94906
96900
|
/** Set of all known cap names — handy for the scope picker. */
|
|
94907
96901
|
var KNOWN_CAP_NAMES = [
|
|
94908
96902
|
"accessories",
|
|
96903
|
+
"action-source",
|
|
96904
|
+
"actions",
|
|
94909
96905
|
"addon-pages",
|
|
94910
96906
|
"addon-pages-source",
|
|
94911
96907
|
"addon-routes",
|
|
@@ -95191,6 +97187,8 @@ var DEVICE_CAP_NAMES = [
|
|
|
95191
97187
|
];
|
|
95192
97188
|
/** Every cap declared as `scope: 'system'` — drives the `category:system` matcher. */
|
|
95193
97189
|
var SYSTEM_CAP_NAMES = [
|
|
97190
|
+
"action-source",
|
|
97191
|
+
"actions",
|
|
95194
97192
|
"addon-pages",
|
|
95195
97193
|
"addon-pages-source",
|
|
95196
97194
|
"addon-routes",
|
|
@@ -99089,6 +101087,8 @@ var ML = "model and training machinery — no device action";
|
|
|
99089
101087
|
var INTERNAL = "internal plumbing between addons — never operator-facing";
|
|
99090
101088
|
/** Every system-scoped cap, with why it is not a device's vocabulary. */
|
|
99091
101089
|
var SYSTEM_CAP_VOCABULARY = {
|
|
101090
|
+
"action-source": system(INTERNAL),
|
|
101091
|
+
actions: system("declared actions — run from the Actions page and from the cameras that link them"),
|
|
99092
101092
|
"addon-pages": system(UI_PLUMBING),
|
|
99093
101093
|
"addon-pages-source": system(UI_PLUMBING),
|
|
99094
101094
|
"addon-routes": system(UI_PLUMBING),
|
|
@@ -100995,6 +102995,8 @@ var CAP_NAMES_WITH_STATUS = [
|
|
|
100995
102995
|
//#region src/generated/capability-router-map.ts
|
|
100996
102996
|
var CAPABILITY_NAMES = {
|
|
100997
102997
|
accessories: "accessories",
|
|
102998
|
+
actionSource: "action-source",
|
|
102999
|
+
actions: "actions",
|
|
100998
103000
|
addonPages: "addon-pages",
|
|
100999
103001
|
addonPagesSource: "addon-pages-source",
|
|
101000
103002
|
addonRoutes: "addon-routes",
|
|
@@ -101186,6 +103188,14 @@ var CAPABILITY_ROUTER_KEYS = [
|
|
|
101186
103188
|
key: "accessories",
|
|
101187
103189
|
name: "accessories"
|
|
101188
103190
|
},
|
|
103191
|
+
{
|
|
103192
|
+
key: "actionSource",
|
|
103193
|
+
name: "action-source"
|
|
103194
|
+
},
|
|
103195
|
+
{
|
|
103196
|
+
key: "actions",
|
|
103197
|
+
name: "actions"
|
|
103198
|
+
},
|
|
101189
103199
|
{
|
|
101190
103200
|
key: "addonPages",
|
|
101191
103201
|
name: "addon-pages"
|
|
@@ -101934,6 +103944,8 @@ var CAPABILITY_ROUTER_KEYS = [
|
|
|
101934
103944
|
*/
|
|
101935
103945
|
var ALL_CAPABILITY_DEFINITIONS = [
|
|
101936
103946
|
accessoriesCapability,
|
|
103947
|
+
actionSourceCapability,
|
|
103948
|
+
actionsCapability,
|
|
101937
103949
|
addonPagesCapability,
|
|
101938
103950
|
addonPagesSourceCapability,
|
|
101939
103951
|
addonRoutesCapability,
|
|
@@ -102136,6 +104148,31 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
|
|
|
102136
104148
|
form: "single",
|
|
102137
104149
|
optional: false
|
|
102138
104150
|
}],
|
|
104151
|
+
"actions.get": [{
|
|
104152
|
+
name: "deviceId",
|
|
104153
|
+
form: "single",
|
|
104154
|
+
optional: true
|
|
104155
|
+
}],
|
|
104156
|
+
"actions.getLinks": [{
|
|
104157
|
+
name: "deviceId",
|
|
104158
|
+
form: "single",
|
|
104159
|
+
optional: false
|
|
104160
|
+
}],
|
|
104161
|
+
"actions.listForCamera": [{
|
|
104162
|
+
name: "deviceId",
|
|
104163
|
+
form: "single",
|
|
104164
|
+
optional: false
|
|
104165
|
+
}],
|
|
104166
|
+
"actions.run": [{
|
|
104167
|
+
name: "deviceId",
|
|
104168
|
+
form: "single",
|
|
104169
|
+
optional: true
|
|
104170
|
+
}],
|
|
104171
|
+
"actions.setLinks": [{
|
|
104172
|
+
name: "deviceId",
|
|
104173
|
+
form: "single",
|
|
104174
|
+
optional: false
|
|
104175
|
+
}],
|
|
102139
104176
|
"addonSettings.getDeviceSettings": [{
|
|
102140
104177
|
name: "deviceId",
|
|
102141
104178
|
form: "single",
|
|
@@ -104907,6 +106944,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
|
|
|
104907
106944
|
/** Cap-method paths that carry a device reference AND live on a system-scope
|
|
104908
106945
|
* cap — the set the v3 device check newly reaches. */
|
|
104909
106946
|
var SYSTEM_SCOPE_DEVICE_METHODS = [
|
|
106947
|
+
"actions.get",
|
|
106948
|
+
"actions.getLinks",
|
|
106949
|
+
"actions.listForCamera",
|
|
106950
|
+
"actions.run",
|
|
106951
|
+
"actions.setLinks",
|
|
104910
106952
|
"addonSettings.getDeviceSettings",
|
|
104911
106953
|
"addonSettings.updateDeviceSettings",
|
|
104912
106954
|
"audioAnalyzer.attachDevice",
|
|
@@ -106121,6 +108163,20 @@ function createSystemProxy(api) {
|
|
|
106121
108163
|
return require_sleep.invokeTrpcLeaf(api, capProp, method, kind, input, push);
|
|
106122
108164
|
}
|
|
106123
108165
|
return {
|
|
108166
|
+
actions: {
|
|
108167
|
+
list: (input) => dispatch("actions", "list", "query", input),
|
|
108168
|
+
stop: (input) => dispatch("actions", "stop", "mutation", input),
|
|
108169
|
+
getTraces: (input) => dispatch("actions", "getTraces", "query", input),
|
|
108170
|
+
create: (input) => dispatch("actions", "create", "mutation", input),
|
|
108171
|
+
update: (input) => dispatch("actions", "update", "mutation", input),
|
|
108172
|
+
delete: (input) => dispatch("actions", "delete", "mutation", input),
|
|
108173
|
+
setLinks: (input) => dispatch("actions", "setLinks", "mutation", input)
|
|
108174
|
+
},
|
|
108175
|
+
actionSource: {
|
|
108176
|
+
listActions: (input) => dispatch("actionSource", "listActions", "query", input),
|
|
108177
|
+
runAction: (input) => dispatch("actionSource", "runAction", "mutation", input),
|
|
108178
|
+
stopAction: (input) => dispatch("actionSource", "stopAction", "mutation", input)
|
|
108179
|
+
},
|
|
106124
108180
|
addonPages: { listPages: (input) => dispatch("addonPages", "listPages", "query", input) },
|
|
106125
108181
|
addons: {
|
|
106126
108182
|
list: (input) => dispatch("addons", "list", "query", input),
|
|
@@ -106264,7 +108320,10 @@ function createSystemProxy(api) {
|
|
|
106264
108320
|
listCompositions: (input) => dispatch("coreBlocks", "listCompositions", "query", input),
|
|
106265
108321
|
getCustomization: (input) => dispatch("coreBlocks", "getCustomization", "query", input),
|
|
106266
108322
|
listComposableCaps: (input) => dispatch("coreBlocks", "listComposableCaps", "query", input),
|
|
106267
|
-
validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input)
|
|
108323
|
+
validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input),
|
|
108324
|
+
reportAutomationRun: (input) => dispatch("coreBlocks", "reportAutomationRun", "mutation", input),
|
|
108325
|
+
getAutomationRunState: (input) => dispatch("coreBlocks", "getAutomationRunState", "query", input),
|
|
108326
|
+
getTraces: (input) => dispatch("coreBlocks", "getTraces", "query", input)
|
|
106268
108327
|
},
|
|
106269
108328
|
decoder: {
|
|
106270
108329
|
getInfo: (input) => dispatch("decoder", "getInfo", "query", input),
|
|
@@ -116887,6 +118946,21 @@ exports.AcceleratorComputeSchema = AcceleratorComputeSchema;
|
|
|
116887
118946
|
exports.AcceleratorKindSchema = AcceleratorKindSchema;
|
|
116888
118947
|
exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
|
|
116889
118948
|
exports.AccessoryKind = AccessoryKind;
|
|
118949
|
+
exports.ActionEntrySchema = ActionEntrySchema;
|
|
118950
|
+
exports.ActionError = ActionError;
|
|
118951
|
+
exports.ActionErrorCodeSchema = ActionErrorCodeSchema;
|
|
118952
|
+
exports.ActionInputSchema = ActionInputSchema;
|
|
118953
|
+
exports.ActionLinksSchema = ActionLinksSchema;
|
|
118954
|
+
exports.ActionRunChangedSchema = ActionRunChangedSchema;
|
|
118955
|
+
exports.ActionRunOriginSchema = ActionRunOriginSchema;
|
|
118956
|
+
exports.ActionRunOutcomeSchema = ActionRunOutcomeSchema;
|
|
118957
|
+
exports.ActionRunResultSchema = ActionRunResultSchema;
|
|
118958
|
+
exports.ActionSourceKindSchema = ActionSourceKindSchema;
|
|
118959
|
+
exports.ActionStatusSchema = ActionStatusSchema;
|
|
118960
|
+
exports.ActionStepSchema = ActionStepSchema;
|
|
118961
|
+
exports.ActionVariableSchema = ActionVariableSchema;
|
|
118962
|
+
exports.ActionVariableTypeSchema = ActionVariableTypeSchema;
|
|
118963
|
+
exports.ActionVariableValueSchema = ActionVariableValueSchema;
|
|
116890
118964
|
exports.ActiveCriterionSchema = require_composition.ActiveCriterionSchema;
|
|
116891
118965
|
exports.AddBrokerInputSchema = AddBrokerInputSchema;
|
|
116892
118966
|
exports.AddRetrainFrameFromRecordingResultSchema = AddRetrainFrameFromRecordingResultSchema;
|
|
@@ -116967,11 +119041,21 @@ exports.AutoTunePlanSchema = AutoTunePlanSchema;
|
|
|
116967
119041
|
exports.AutoTuneWarningSchema = AutoTuneWarningSchema;
|
|
116968
119042
|
exports.AutoUpdateSettingsSchema = AutoUpdateSettingsSchema;
|
|
116969
119043
|
exports.AutomationActionSchema = AutomationActionSchema;
|
|
119044
|
+
exports.AutomationCapStepSchema = AutomationCapStepSchema;
|
|
116970
119045
|
exports.AutomationConditionOperatorSchema = AutomationConditionOperatorSchema;
|
|
116971
119046
|
exports.AutomationConditionSchema = AutomationConditionSchema;
|
|
119047
|
+
exports.AutomationConditionTraceNodeSchema = AutomationConditionTraceNodeSchema;
|
|
116972
119048
|
exports.AutomationControlStatusSchema = AutomationControlStatusSchema;
|
|
116973
119049
|
exports.AutomationRecipeSchema = AutomationRecipeSchema;
|
|
119050
|
+
exports.AutomationRunModeSchema = AutomationRunModeSchema;
|
|
119051
|
+
exports.AutomationRunOutcomeSchema = AutomationRunOutcomeSchema;
|
|
119052
|
+
exports.AutomationRunTraceSchema = AutomationRunTraceSchema;
|
|
119053
|
+
exports.AutomationRunVerdictSchema = AutomationRunVerdictSchema;
|
|
119054
|
+
exports.AutomationStateDocSchema = AutomationStateDocSchema;
|
|
119055
|
+
exports.AutomationStepTraceSchema = AutomationStepTraceSchema;
|
|
116974
119056
|
exports.AutomationTriggerSchema = AutomationTriggerSchema;
|
|
119057
|
+
exports.AutomationTriggerTraceSchema = AutomationTriggerTraceSchema;
|
|
119058
|
+
exports.AutomationWaitStepSchema = AutomationWaitStepSchema;
|
|
116975
119059
|
exports.AvailableIntegrationTypeSchema = AvailableIntegrationTypeSchema;
|
|
116976
119060
|
exports.BASE_DEVICE_CAPS = BASE_DEVICE_CAPS;
|
|
116977
119061
|
exports.BASE_LIVE_EGRESS_PROFILE = BASE_LIVE_EGRESS_PROFILE;
|
|
@@ -117233,6 +119317,7 @@ exports.CompositionTargetSchema = require_composition.CompositionTargetSchema;
|
|
|
117233
119317
|
exports.CompositionUnavailableModeSchema = CompositionUnavailableModeSchema;
|
|
117234
119318
|
exports.CompositionValidationSchema = CompositionValidationSchema;
|
|
117235
119319
|
exports.CompositionViewSchema = CompositionViewSchema;
|
|
119320
|
+
exports.ConditionMirror = ConditionMirror;
|
|
117236
119321
|
exports.ConfigEntrySchema = ConfigEntrySchema;
|
|
117237
119322
|
exports.ConfigSectionWithValuesSchema = ConfigSectionWithValuesSchema;
|
|
117238
119323
|
exports.ConfigTabDeclarationSchema = ConfigTabDeclarationSchema;
|
|
@@ -117307,10 +119392,12 @@ exports.DEFAULT_POOL_MEMORY_POLICY = DEFAULT_POOL_MEMORY_POLICY;
|
|
|
117307
119392
|
exports.DEFAULT_RECORDING_PROFILES = DEFAULT_RECORDING_PROFILES;
|
|
117308
119393
|
exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
|
|
117309
119394
|
exports.DEFAULT_RUNTIME_STATE_DURABILITY = require_sleep.DEFAULT_RUNTIME_STATE_DURABILITY;
|
|
119395
|
+
exports.DEFAULT_SESSION_LOST_DEBOUNCE_MS = DEFAULT_SESSION_LOST_DEBOUNCE_MS;
|
|
117310
119396
|
exports.DEFAULT_SESSION_PLACEMENT_POLICY = DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
117311
119397
|
exports.DEFAULT_SHARE_SOURCE = DEFAULT_SHARE_SOURCE;
|
|
117312
119398
|
exports.DEFAULT_TIMELAPSE_PREVIEW_TEXT = DEFAULT_TIMELAPSE_PREVIEW_TEXT;
|
|
117313
119399
|
exports.DEFAULT_TOKEN_EXPIRY = require_sleep.DEFAULT_TOKEN_EXPIRY;
|
|
119400
|
+
exports.DEFAULT_TRACE_KEEP = DEFAULT_TRACE_KEEP;
|
|
117314
119401
|
exports.DEFAULT_VIEWER_SCOPE = DEFAULT_VIEWER_SCOPE;
|
|
117315
119402
|
exports.DEFINITIVE_DEVICE_REFUSALS = DEFINITIVE_DEVICE_REFUSALS;
|
|
117316
119403
|
exports.DERIVATIVE_UNITS = require_composition.DERIVATIVE_UNITS;
|
|
@@ -117378,6 +119465,7 @@ exports.DetailCropConventionSchema = DetailCropConventionSchema;
|
|
|
117378
119465
|
exports.DetectionCatalogClassMapSchema = DetectionCatalogClassMapSchema;
|
|
117379
119466
|
exports.DetectionSourceSchema = DetectionSourceSchema;
|
|
117380
119467
|
exports.DetectionSourcesSchema = DetectionSourcesSchema;
|
|
119468
|
+
exports.DetectionTriggerFilter = DetectionTriggerFilter;
|
|
117381
119469
|
exports.DeviceAdminLinkSchema = DeviceAdminLinkSchema;
|
|
117382
119470
|
exports.DeviceCaStatusSchema = DeviceCaStatusSchema;
|
|
117383
119471
|
exports.DeviceCertificateLoginMethodSchema = DeviceCertificateLoginMethodSchema;
|
|
@@ -117444,6 +119532,7 @@ exports.DoorbellStatusSchema = DoorbellStatusSchema;
|
|
|
117444
119532
|
exports.EMBEDDING_MODEL_FAILED = EMBEDDING_MODEL_FAILED;
|
|
117445
119533
|
exports.EMBEDDING_REQUIREMENTS_FAILED = EMBEDDING_REQUIREMENTS_FAILED;
|
|
117446
119534
|
exports.EMBEDDING_REQUIREMENTS_UNAVAILABLE = EMBEDDING_REQUIREMENTS_UNAVAILABLE;
|
|
119535
|
+
exports.EMPTY_AUTOMATION_STATE = EMPTY_AUTOMATION_STATE;
|
|
117447
119536
|
exports.ESTIMATE_COMPUTE_RATIO_MAX = ESTIMATE_COMPUTE_RATIO_MAX;
|
|
117448
119537
|
exports.ESTIMATE_COMPUTE_RATIO_MIN = ESTIMATE_COMPUTE_RATIO_MIN;
|
|
117449
119538
|
exports.EVENTFUL_CAP_NAMES = EVENTFUL_CAP_NAMES;
|
|
@@ -117632,10 +119721,12 @@ exports.ICON_CATEGORIES = ICON_CATEGORIES;
|
|
|
117632
119721
|
exports.ICON_ENTRIES = ICON_ENTRIES;
|
|
117633
119722
|
exports.ICON_KEYS = ICON_KEYS;
|
|
117634
119723
|
exports.ICON_VIEWER_MODULES = ICON_VIEWER_MODULES;
|
|
119724
|
+
exports.IDLE_ACTION_STATUS = IDLE_ACTION_STATUS;
|
|
117635
119725
|
exports.INFERENCE_DEVICE_EXCLUSION_REASONS = INFERENCE_DEVICE_EXCLUSION_REASONS;
|
|
117636
119726
|
exports.INITIAL_PULSE = INITIAL_PULSE;
|
|
117637
119727
|
exports.INITIAL_SPAN = INITIAL_SPAN;
|
|
117638
119728
|
exports.INTEGRAL_OUTPUTS = require_composition.INTEGRAL_OUTPUTS;
|
|
119729
|
+
exports.INTERNAL_ACTION_PREFIX = INTERNAL_ACTION_PREFIX;
|
|
117639
119730
|
exports.INVERTIBLE_CAPS = INVERTIBLE_CAPS;
|
|
117640
119731
|
exports.IconKeySchema = IconKeySchema;
|
|
117641
119732
|
exports.ImageContractSchema = ImageContractSchema;
|
|
@@ -117652,6 +119743,7 @@ exports.IngestOwnerSchema = IngestOwnerSchema;
|
|
|
117652
119743
|
exports.InstalledPackageSchema = InstalledPackageSchema;
|
|
117653
119744
|
exports.IntegralHelperSpecSchema = require_composition.IntegralHelperSpecSchema;
|
|
117654
119745
|
exports.IntegrationLiteSchema = IntegrationLiteSchema;
|
|
119746
|
+
exports.IntegrationSessionReporter = IntegrationSessionReporter;
|
|
117655
119747
|
exports.IntegrationWithStateSchema = IntegrationWithStateSchema;
|
|
117656
119748
|
exports.IntercomAbilitySchema = IntercomAbilitySchema;
|
|
117657
119749
|
exports.IntercomStatusSchema = IntercomStatusSchema;
|
|
@@ -117802,6 +119894,7 @@ exports.MAX_KEYS = MAX_KEYS;
|
|
|
117802
119894
|
exports.MAX_REASONS_PER_KEY = MAX_REASONS_PER_KEY;
|
|
117803
119895
|
exports.MAX_REQUEUED_BUCKETS_PER_SERIES = MAX_REQUEUED_BUCKETS_PER_SERIES;
|
|
117804
119896
|
exports.MAX_SENSOR_TRIGGER_DEVICES = MAX_SENSOR_TRIGGER_DEVICES;
|
|
119897
|
+
exports.MAX_TRACE_KEEP = MAX_TRACE_KEEP;
|
|
117805
119898
|
exports.MAX_TRACK_DEBUG_NOTE_LEN = MAX_TRACK_DEBUG_NOTE_LEN;
|
|
117806
119899
|
exports.MAX_TRACK_PURITIES = MAX_TRACK_PURITIES;
|
|
117807
119900
|
exports.MAX_TRACK_RELATIONS = MAX_TRACK_RELATIONS;
|
|
@@ -118525,6 +120618,7 @@ exports.RollingWindowSchema = require_composition.RollingWindowSchema;
|
|
|
118525
120618
|
exports.RootStepSupplySchema = RootStepSupplySchema;
|
|
118526
120619
|
exports.RtpSourceSchema = RtpSourceSchema;
|
|
118527
120620
|
exports.RtspRestreamEntrySchema = RtspRestreamEntrySchema;
|
|
120621
|
+
exports.RunGate = RunGate;
|
|
118528
120622
|
exports.RunnerBootCheckSchema = RunnerBootCheckSchema;
|
|
118529
120623
|
exports.RunnerCameraConfigSchema = RunnerCameraConfigSchema;
|
|
118530
120624
|
exports.RunnerCameraDeviceUIFields = RunnerCameraDeviceUIFields;
|
|
@@ -118556,6 +120650,7 @@ exports.SERIAL_CAPACITY_CAMERA_FPS = SERIAL_CAPACITY_CAMERA_FPS;
|
|
|
118556
120650
|
exports.SERIES_KINDS = require_composition.SERIES_KINDS;
|
|
118557
120651
|
exports.SERVER_NAME_MAX_LENGTH = SERVER_NAME_MAX_LENGTH;
|
|
118558
120652
|
exports.SERVER_NAME_SETTING_KEY = SERVER_NAME_SETTING_KEY;
|
|
120653
|
+
exports.SESSION_LOST_ALERT_CATEGORY = SESSION_LOST_ALERT_CATEGORY;
|
|
118559
120654
|
exports.SESSION_PLACEMENT_SETTING_KEY = SESSION_PLACEMENT_SETTING_KEY;
|
|
118560
120655
|
exports.SHARE_AFFINITY_TOLERANCE_SESSIONS = SHARE_AFFINITY_TOLERANCE_SESSIONS;
|
|
118561
120656
|
exports.SHARE_SOURCE_SETTING_KEY = SHARE_SOURCE_SETTING_KEY;
|
|
@@ -118612,6 +120707,7 @@ exports.ServerUpdateActionResultSchema = ServerUpdateActionResultSchema;
|
|
|
118612
120707
|
exports.ServerUpdateCheckResultSchema = ServerUpdateCheckResultSchema;
|
|
118613
120708
|
exports.ServerUpdateOutcomeSchema = ServerUpdateOutcomeSchema;
|
|
118614
120709
|
exports.ServerUpdateStateSchema = ServerUpdateStateSchema;
|
|
120710
|
+
exports.SessionHealthTracker = SessionHealthTracker;
|
|
118615
120711
|
exports.SessionPlacementPolicySchema = SessionPlacementPolicySchema;
|
|
118616
120712
|
exports.SessionTriggerSchema = SessionTriggerSchema;
|
|
118617
120713
|
exports.SetCameraSensorsInputSchema = SetCameraSensorsInputSchema;
|
|
@@ -118748,6 +120844,8 @@ exports.TokenScopeSchema = TokenScopeSchema;
|
|
|
118748
120844
|
exports.TopologyNodeSchema = TopologyNodeSchema;
|
|
118749
120845
|
exports.TopologyProcessSchema = TopologyProcessSchema;
|
|
118750
120846
|
exports.TopologyServiceSchema = TopologyServiceSchema;
|
|
120847
|
+
exports.TraceNodeOutcomeSchema = TraceNodeOutcomeSchema;
|
|
120848
|
+
exports.TraceValueSchema = TraceValueSchema;
|
|
118751
120849
|
exports.TrackCascadeCountsSchema = TrackCascadeCountsSchema;
|
|
118752
120850
|
exports.TrackEnvelopeSchema = TrackEnvelopeSchema;
|
|
118753
120851
|
exports.TrackFlagsPatchSchema = TrackFlagsPatchSchema;
|
|
@@ -118899,6 +120997,8 @@ exports.accessoriesCapability = accessoriesCapability;
|
|
|
118899
120997
|
exports.accessoryChildrenComplete = accessoryChildrenComplete;
|
|
118900
120998
|
exports.accessoryChildrenUnknown = accessoryChildrenUnknown;
|
|
118901
120999
|
exports.accessoryStableId = accessoryStableId;
|
|
121000
|
+
exports.actionSourceCapability = actionSourceCapability;
|
|
121001
|
+
exports.actionsCapability = actionsCapability;
|
|
118902
121002
|
exports.actionsForBoundCaps = actionsForBoundCaps;
|
|
118903
121003
|
exports.actionsForDevice = actionsForDevice;
|
|
118904
121004
|
exports.actionsForDeviceType = actionsForDeviceType;
|
|
@@ -118907,6 +121007,7 @@ exports.activateAlarmProfile = activateAlarmProfile;
|
|
|
118907
121007
|
exports.activeForReducer = activeForReducer;
|
|
118908
121008
|
exports.actuatorMethodRow = actuatorMethodRow;
|
|
118909
121009
|
exports.addonCatalogHref = addonCatalogHref;
|
|
121010
|
+
exports.addonIdOfActionId = addonIdOfActionId;
|
|
118910
121011
|
exports.addonPagesCapability = addonPagesCapability;
|
|
118911
121012
|
exports.addonPagesSourceCapability = addonPagesSourceCapability;
|
|
118912
121013
|
exports.addonRoutesCapability = addonRoutesCapability;
|
|
@@ -118933,6 +121034,7 @@ exports.allowedSuppliedEffect = allowedSuppliedEffect;
|
|
|
118933
121034
|
exports.ambientLightSensorCapability = ambientLightSensorCapability;
|
|
118934
121035
|
exports.annotateClipsWithEventLabels = annotateClipsWithEventLabels;
|
|
118935
121036
|
exports.appFlowLocation = appFlowLocation;
|
|
121037
|
+
exports.appendTrace = appendTrace;
|
|
118936
121038
|
exports.applianceDeviceType = require_composition.applianceDeviceType;
|
|
118937
121039
|
exports.applianceExport = applianceExport;
|
|
118938
121040
|
exports.applyBlockingEdit = applyBlockingEdit;
|
|
@@ -118980,6 +121082,7 @@ exports.bindAddonActions = bindAddonActions;
|
|
|
118980
121082
|
exports.brightnessCapability = brightnessCapability;
|
|
118981
121083
|
exports.brokerCapability = brokerCapability;
|
|
118982
121084
|
exports.bucketStartOf = bucketStartOf;
|
|
121085
|
+
exports.buildActionSteps = buildActionSteps;
|
|
118983
121086
|
exports.buildAddonRouteProvider = buildAddonRouteProvider;
|
|
118984
121087
|
exports.buildAudioArgs = require_composition.buildAudioArgs;
|
|
118985
121088
|
exports.buildBenchStepTree = buildBenchStepTree;
|
|
@@ -118995,8 +121098,10 @@ exports.buildNcTaxonomy = buildNcTaxonomy;
|
|
|
118995
121098
|
exports.buildPlusCatalogClassMap = buildPlusCatalogClassMap;
|
|
118996
121099
|
exports.buildRoleScopes = buildRoleScopes;
|
|
118997
121100
|
exports.buildSecretRef = require_sleep.buildSecretRef;
|
|
121101
|
+
exports.buildSessionLostAlert = buildSessionLostAlert;
|
|
118998
121102
|
exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
|
|
118999
121103
|
exports.buildSubjectPath = buildSubjectPath;
|
|
121104
|
+
exports.buildTriggerTrace = buildTriggerTrace;
|
|
119000
121105
|
exports.buildVideoArgs = require_composition.buildVideoArgs;
|
|
119001
121106
|
exports.buildViewerLink = buildViewerLink;
|
|
119002
121107
|
exports.builtCapabilityBlockers = builtCapabilityBlockers;
|
|
@@ -119051,6 +121156,7 @@ exports.codecIsAccepted = codecIsAccepted;
|
|
|
119051
121156
|
exports.collectHydratedFieldEntries = require_sleep.collectHydratedFieldEntries;
|
|
119052
121157
|
exports.collectHydratedFieldValues = require_sleep.collectHydratedFieldValues;
|
|
119053
121158
|
exports.collectKeychainStoredKeys = require_sleep.collectKeychainStoredKeys;
|
|
121159
|
+
exports.collectMirrorSources = collectMirrorSources;
|
|
119054
121160
|
exports.collectModelBoundKeys = collectModelBoundKeys;
|
|
119055
121161
|
exports.collectSecretConfigKeys = require_sleep.collectSecretConfigKeys;
|
|
119056
121162
|
exports.collectSecretLeaves = require_sleep.collectSecretLeaves;
|
|
@@ -119171,6 +121277,7 @@ exports.describeSystemEventDeliveryProblem = describeSystemEventDeliveryProblem;
|
|
|
119171
121277
|
exports.describeSystemEventFamilyProblem = describeSystemEventFamilyProblem;
|
|
119172
121278
|
exports.describeTopLevelStateFields = describeTopLevelStateFields;
|
|
119173
121279
|
exports.detectAccessRole = detectAccessRole;
|
|
121280
|
+
exports.detectionClassMatches = detectionClassMatches;
|
|
119174
121281
|
exports.detectionPipelineCapability = detectionPipelineCapability;
|
|
119175
121282
|
exports.deviceAdminLinkCapability = deviceAdminLinkCapability;
|
|
119176
121283
|
exports.deviceAdoptionCapability = deviceAdoptionCapability;
|
|
@@ -119231,6 +121338,7 @@ exports.estimateNodeBenchmarkDurationMs = estimateNodeBenchmarkDurationMs;
|
|
|
119231
121338
|
exports.evaluateAggregate = evaluateAggregate;
|
|
119232
121339
|
exports.evaluateAst = require_composition.evaluateAst;
|
|
119233
121340
|
exports.evaluateCompositionField = evaluateCompositionField;
|
|
121341
|
+
exports.evaluateConditionTree = evaluateConditionTree;
|
|
119234
121342
|
exports.evaluateCount = require_composition.evaluateCount;
|
|
119235
121343
|
exports.evaluateDerivative = require_composition.evaluateDerivative;
|
|
119236
121344
|
exports.evaluateExpressionSource = require_composition.evaluateExpressionSource;
|
|
@@ -119341,6 +121449,7 @@ exports.integralDraft = integralDraft;
|
|
|
119341
121449
|
exports.integrateReading = require_composition.integrateReading;
|
|
119342
121450
|
exports.integrationsCapability = integrationsCapability;
|
|
119343
121451
|
exports.intercomCapability = intercomCapability;
|
|
121452
|
+
exports.internalActionId = internalActionId;
|
|
119344
121453
|
exports.interpolate = interpolate;
|
|
119345
121454
|
exports.interpretStoredClusterModel = interpretStoredClusterModel;
|
|
119346
121455
|
exports.invalidClusterStepModelEntries = invalidClusterStepModelEntries;
|
|
@@ -119382,6 +121491,7 @@ exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
|
|
|
119382
121491
|
exports.isHeldBackTemplateVar = isHeldBackTemplateVar;
|
|
119383
121492
|
exports.isHistoryHelper = require_composition.isHistoryHelper;
|
|
119384
121493
|
exports.isIconKey = isIconKey;
|
|
121494
|
+
exports.isInternalActionId = isInternalActionId;
|
|
119385
121495
|
exports.isIsolatedBuiltin = isIsolatedBuiltin;
|
|
119386
121496
|
exports.isKeychainStoredField = require_sleep.isKeychainStoredField;
|
|
119387
121497
|
exports.isKnownTemplateVar = isKnownTemplateVar;
|
|
@@ -119410,6 +121520,7 @@ exports.isSecretMarkerLike = require_sleep.isSecretMarkerLike;
|
|
|
119410
121520
|
exports.isSecretRef = require_sleep.isSecretRef;
|
|
119411
121521
|
exports.isSecretRefOwnedBy = require_sleep.isSecretRefOwnedBy;
|
|
119412
121522
|
exports.isSingleHubAddressRole = isSingleHubAddressRole;
|
|
121523
|
+
exports.isSkippedTrace = isSkippedTrace;
|
|
119413
121524
|
exports.isSoftwareDecode = require_composition.isSoftwareDecode;
|
|
119414
121525
|
exports.isSourceCap = require_composition.isSourceCap;
|
|
119415
121526
|
exports.isStateLabelKey = isStateLabelKey;
|
|
@@ -119482,6 +121593,7 @@ exports.mergeOverrides = mergeOverrides;
|
|
|
119482
121593
|
exports.mergeScalarTarget = mergeScalarTarget;
|
|
119483
121594
|
exports.mergeSecretStatePaths = require_sleep.mergeSecretStatePaths;
|
|
119484
121595
|
exports.mergeSettingsBags = mergeSettingsBags;
|
|
121596
|
+
exports.mergeSkipTraces = mergeSkipTraces;
|
|
119485
121597
|
exports.mergeSourceInfo = mergeSourceInfo;
|
|
119486
121598
|
exports.meshNetworkCapability = meshNetworkCapability;
|
|
119487
121599
|
exports.meterCapability = meterCapability;
|
|
@@ -119687,6 +121799,7 @@ exports.requiredTopLevelKeys = requiredTopLevelKeys;
|
|
|
119687
121799
|
exports.requiresPython = requiresPython;
|
|
119688
121800
|
exports.reservedSourceOwner = reservedSourceOwner;
|
|
119689
121801
|
exports.resetPoolBaseline = resetPoolBaseline;
|
|
121802
|
+
exports.resolveActionVariables = resolveActionVariables;
|
|
119690
121803
|
exports.resolveAddonExecution = resolveAddonExecution;
|
|
119691
121804
|
exports.resolveAddonGroup = resolveAddonGroup;
|
|
119692
121805
|
exports.resolveAddonPlacement = resolveAddonPlacement;
|
|
@@ -119701,6 +121814,7 @@ exports.resolveCoreBlockFiles = resolveCoreBlockFiles;
|
|
|
119701
121814
|
exports.resolveCreateExportSubject = resolveCreateExportSubject;
|
|
119702
121815
|
exports.resolveDeviceControlKind = resolveDeviceControlKind;
|
|
119703
121816
|
exports.resolveDeviceProfile = resolveDeviceProfile;
|
|
121817
|
+
exports.resolveDeviceTarget = resolveDeviceTarget;
|
|
119704
121818
|
exports.resolveEffectiveShares = resolveEffectiveShares;
|
|
119705
121819
|
exports.resolveEgressDecodeHwAccel = resolveEgressDecodeHwAccel;
|
|
119706
121820
|
exports.resolveExportProfiles = resolveExportProfiles;
|
|
@@ -119727,6 +121841,7 @@ exports.resolveRunnerId = resolveRunnerId;
|
|
|
119727
121841
|
exports.resolveSecretMarkers = require_sleep.resolveSecretMarkers;
|
|
119728
121842
|
exports.resolveSecretViewsIn = require_sleep.resolveSecretViewsIn;
|
|
119729
121843
|
exports.resolveServerName = resolveServerName;
|
|
121844
|
+
exports.resolveStepArgs = resolveStepArgs;
|
|
119730
121845
|
exports.resolveStreamMaps = require_composition.resolveStreamMaps;
|
|
119731
121846
|
exports.resolveVariantModelId = resolveVariantModelId;
|
|
119732
121847
|
exports.resolveViewableDeviceIds = resolveViewableDeviceIds;
|
|
@@ -119755,6 +121870,7 @@ exports.ruleSeedForSection = ruleSeedForSection;
|
|
|
119755
121870
|
exports.ruleTriggerZoneIds = ruleTriggerZoneIds;
|
|
119756
121871
|
exports.runFanOut = runFanOut;
|
|
119757
121872
|
exports.runInferenceStep = runInferenceStep;
|
|
121873
|
+
exports.runSteps = runSteps;
|
|
119758
121874
|
exports.runningFromDishwasherOperation = runningFromDishwasherOperation;
|
|
119759
121875
|
exports.runningFromLawnMower = runningFromLawnMower;
|
|
119760
121876
|
exports.runningFromMediaPlayer = runningFromMediaPlayer;
|
|
@@ -119795,6 +121911,7 @@ exports.sensorTransitionNeverFires = sensorTransitionNeverFires;
|
|
|
119795
121911
|
exports.sensorTransitionOffered = sensorTransitionOffered;
|
|
119796
121912
|
exports.servedTemplateVars = servedTemplateVars;
|
|
119797
121913
|
exports.serverManagementCapability = serverManagementCapability;
|
|
121914
|
+
exports.sessionLostAlertId = sessionLostAlertId;
|
|
119798
121915
|
exports.sessionsOfDeviceCell = sessionsOfDeviceCell;
|
|
119799
121916
|
exports.setByPath = setByPath;
|
|
119800
121917
|
exports.setFormulaExpr = setFormulaExpr;
|
|
@@ -119812,6 +121929,7 @@ exports.shortSecretRefHash = require_sleep.shortSecretRefHash;
|
|
|
119812
121929
|
exports.signalPercentFromBars = signalPercentFromBars;
|
|
119813
121930
|
exports.signalPercentFromRssi = signalPercentFromRssi;
|
|
119814
121931
|
exports.siteZoneKey = siteZoneKey;
|
|
121932
|
+
exports.skipSignature = skipSignature;
|
|
119815
121933
|
exports.sleep = require_sleep.sleep;
|
|
119816
121934
|
exports.sleepCancellable = require_sleep.sleepCancellable;
|
|
119817
121935
|
exports.sliceActiveValue = require_composition.sliceActiveValue;
|