@camstack/types 1.2.433 → 1.2.435

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/addon.js +2 -2
  3. package/dist/addon.mjs +2 -2
  4. package/dist/automation/index.d.ts +3 -1
  5. package/dist/automation/recipe.d.ts +33 -3
  6. package/dist/automation/stationary-trigger.d.ts +48 -0
  7. package/dist/automation/trace.d.ts +4 -1
  8. package/dist/capabilities/actions.cap.d.ts +1 -0
  9. package/dist/capabilities/air-quality-sensor.cap.d.ts +8 -8
  10. package/dist/capabilities/ambient-light-sensor.cap.d.ts +4 -4
  11. package/dist/capabilities/battery.cap.d.ts +14 -14
  12. package/dist/capabilities/brightness.cap.d.ts +7 -7
  13. package/dist/capabilities/capability-definition.d.ts +3 -3
  14. package/dist/capabilities/color.cap.d.ts +6 -6
  15. package/dist/capabilities/core-blocks.cap.d.ts +156 -0
  16. package/dist/capabilities/device-history.cap.d.ts +2 -2
  17. package/dist/capabilities/dishwasher.cap.d.ts +11 -11
  18. package/dist/capabilities/fan-control.cap.d.ts +6 -6
  19. package/dist/capabilities/humidity-sensor.cap.d.ts +4 -4
  20. package/dist/capabilities/lawn-mower-control.cap.d.ts +3 -3
  21. package/dist/capabilities/media-player.cap.d.ts +8 -8
  22. package/dist/capabilities/meter.cap.d.ts +1 -1
  23. package/dist/capabilities/network-link.cap.d.ts +13 -13
  24. package/dist/capabilities/power-meter.cap.d.ts +22 -22
  25. package/dist/capabilities/presence.cap.d.ts +1 -15
  26. package/dist/capabilities/pressure-sensor.cap.d.ts +4 -4
  27. package/dist/capabilities/temperature-sensor.cap.d.ts +4 -4
  28. package/dist/capabilities/vacuum-control.cap.d.ts +3 -3
  29. package/dist/client.js +1 -1
  30. package/dist/client.mjs +1 -1
  31. package/dist/composition/composition-report.d.ts +1 -1
  32. package/dist/composition/examples.d.ts +1 -1
  33. package/dist/composition/helpers/appliance.d.ts +1 -1
  34. package/dist/{composition-CpAc0YfQ.js → composition-BZf_gqG7.js} +2 -2
  35. package/dist/{composition-Bb8iwHEJ.mjs → composition-CZImLpOI.mjs} +2 -2
  36. package/dist/device/base-device.d.ts +2 -2
  37. package/dist/device/device-runtime-state.d.ts +1 -1
  38. package/dist/device/device-state-handle.d.ts +1 -1
  39. package/dist/device/system-mirror.d.ts +1 -1
  40. package/dist/enums/event-category.d.ts +1 -1
  41. package/dist/enums.js +1 -1
  42. package/dist/enums.mjs +1 -1
  43. package/dist/{event-category-hdMR7CTW.mjs → event-category-DmZDPDf_.mjs} +1 -1
  44. package/dist/{event-category-BhzwWQCH.js → event-category-E17zaHjN.js} +1 -1
  45. package/dist/generated/addon-cap-facts.d.ts +1 -1
  46. package/dist/generated/customize-catalog.d.ts +15 -15
  47. package/dist/generated/history-catalog.d.ts +1 -1
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +673 -518
  50. package/dist/index.mjs +673 -519
  51. package/dist/interfaces/event-bus.d.ts +24 -3
  52. package/dist/node.js +1 -1
  53. package/dist/node.mjs +1 -1
  54. package/dist/{sleep-BHjNez61.mjs → sleep-Crlmu_tO.mjs} +27 -31
  55. package/dist/{sleep-5zRztaHa.js → sleep-DHitF_e4.js} +27 -31
  56. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-5zRztaHa.js");
3
- const require_event_category = require("./event-category-BhzwWQCH.js");
4
- const require_composition = require("./composition-CpAc0YfQ.js");
2
+ const require_sleep = require("./sleep-DHitF_e4.js");
3
+ const require_event_category = require("./event-category-E17zaHjN.js");
4
+ const require_composition = require("./composition-BZf_gqG7.js");
5
5
  const require_node_color = require("./node-color.js");
6
6
  const require_err_msg = require("./err-msg-U7lLUMNz.js");
7
7
  const require_enums = require("./enums.js");
@@ -8074,10 +8074,10 @@ var ExpressionSourceSchema = zod.z.object({
8074
8074
  *
8075
8075
  * Three vocabularies, each salvaged rather than invented:
8076
8076
  *
8077
- * - **Triggers** — the only genuinely new surface. Six members, none of which
8077
+ * - **Triggers** — the only genuinely new surface. Seven members, none of which
8078
8078
  * touches notification delivery. The NC keeps its four *pipeline* triggers;
8079
8079
  * these are device/time triggers and the two sets do not overlap.
8080
- * - **Conditions** — `ALL`/`ANY`/`NOT` over two leaf kinds, bounded. The
8080
+ * - **Conditions** — `ALL`/`ANY`/`NOT` over three leaf kinds, bounded. The
8081
8081
  * expression leaf is the bounded engine at `../expression/`, which is where
8082
8082
  * arithmetic between two device fields lives and the only place it lives.
8083
8083
  * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
@@ -8124,6 +8124,14 @@ var AutomationConditionLeafSchema = zod.z.object({
8124
8124
  zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
8125
8125
  ])
8126
8126
  });
8127
+ var AutomationConditionStationaryPresentSchema = zod.z.object({
8128
+ kind: zod.z.literal("stationary-present"),
8129
+ deviceId: zod.z.number().int().nonnegative(),
8130
+ /** Class names; a macro class matches every raw class that maps to it. */
8131
+ classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8132
+ /** The parked object must be in at least one of these zones. */
8133
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional()
8134
+ });
8127
8135
  /**
8128
8136
  * The expression leaf, declared as a plain object rather than an intersection
8129
8137
  * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
@@ -8157,7 +8165,8 @@ var AutomationConditionSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind"
8157
8165
  child: AutomationConditionSchema
8158
8166
  }),
8159
8167
  AutomationConditionLeafSchema,
8160
- AutomationConditionExpressionSchema
8168
+ AutomationConditionExpressionSchema,
8169
+ AutomationConditionStationaryPresentSchema
8161
8170
  ]));
8162
8171
  /**
8163
8172
  * What starts a run.
@@ -8211,6 +8220,17 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
8211
8220
  /** Best-so-far confidence the track must reach. */
8212
8221
  minScore: zod.z.number().min(0).max(1).optional()
8213
8222
  }),
8223
+ zod.z.object({
8224
+ kind: zod.z.literal("stationary"),
8225
+ deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
8226
+ /** Class names; a macro class matches like the detection trigger. */
8227
+ classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8228
+ /** The parked object must be in at least one of these zones. */
8229
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8230
+ /** `appeared`: it became parked. `departed`: the registry retired it.
8231
+ * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
8232
+ phase: zod.z.enum(["appeared", "departed"])
8233
+ }),
8214
8234
  zod.z.object({
8215
8235
  kind: zod.z.literal("interval"),
8216
8236
  minutes: zod.z.number().int().min(1).max(1440)
@@ -8323,9 +8343,12 @@ var AutomationRecipeSchema = zod.z.object({
8323
8343
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
8324
8344
  keepTraces: zod.z.number().int().min(1).max(100).optional()
8325
8345
  });
8346
+ function isLeaf(cond) {
8347
+ return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
8348
+ }
8326
8349
  /** Depth of a tree, root container = 1. A bare leaf is depth 1. */
8327
8350
  function conditionDepth(cond) {
8328
- if (cond.kind === "condition" || cond.kind === "expression") return 1;
8351
+ if (isLeaf(cond)) return 1;
8329
8352
  if (cond.kind === "not") return 1 + conditionDepth(cond.child);
8330
8353
  const children = cond.children;
8331
8354
  if (children.length === 0) return 1;
@@ -8338,14 +8361,14 @@ function conditionDepth(cond) {
8338
8361
  }
8339
8362
  /** Leaves only — containers do not count toward the leaf budget. */
8340
8363
  function countConditionLeaves(cond) {
8341
- if (cond.kind === "condition" || cond.kind === "expression") return 1;
8364
+ if (isLeaf(cond)) return 1;
8342
8365
  if (cond.kind === "not") return countConditionLeaves(cond.child);
8343
8366
  let total = 0;
8344
8367
  for (const child of cond.children) total += countConditionLeaves(child);
8345
8368
  return total;
8346
8369
  }
8347
8370
  function findEmptyContainer(cond) {
8348
- if (cond.kind === "condition" || cond.kind === "expression") return false;
8371
+ if (isLeaf(cond)) return false;
8349
8372
  if (cond.kind === "not") return findEmptyContainer(cond.child);
8350
8373
  if (cond.children.length === 0) return true;
8351
8374
  return cond.children.some(findEmptyContainer);
@@ -8504,6 +8527,32 @@ function emitTrigger(trigger, index) {
8504
8527
  },
8505
8528
  ),
8506
8529
  )
8530
+ }`;
8531
+ case "stationary": return ` // trigger ${index}: stationary — a parked object appeared / departed. Stateless:
8532
+ // the registry emits one event per transition. A dropped event only misses a fire;
8533
+ // the stationary-present CONDITION reads the registry, not this event.
8534
+ {
8535
+ const trigger = ${j(trigger)} as const
8536
+ disposers.push(
8537
+ ctx.eventBus.subscribe(
8538
+ { category: EventCategory.PipelineAnalyticsStationaryChanged },
8539
+ (event) => {
8540
+ const decision = evaluateStationaryTrigger(trigger, event.data)
8541
+ if (decision.kind === 'ignore') return
8542
+ if (decision.kind === 'reject') {
8543
+ ctx.logger.debug('stationary trigger rejected an event', {
8544
+ tags: { deviceId: decision.vars.deviceId },
8545
+ reason: decision.reason,
8546
+ })
8547
+ }
8548
+ void run(
8549
+ { kind: 'stationary', at: Date.now(), ...decision.vars },
8550
+ decision.reason,
8551
+ decision.kind === 'fire',
8552
+ )
8553
+ },
8554
+ ),
8555
+ )
8507
8556
  }`;
8508
8557
  }
8509
8558
  }
@@ -8570,6 +8619,7 @@ import {
8570
8619
  collectMirrorSources,
8571
8620
  coreBlockIdFromAddonId,
8572
8621
  evaluateConditionTree,
8622
+ evaluateStationaryTrigger,
8573
8623
  isSkippedTrace,
8574
8624
  mergeSkipTraces,
8575
8625
  resolveDeviceTarget,
@@ -9064,310 +9114,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
9064
9114
  `;
9065
9115
  }
9066
9116
  //#endregion
9067
- //#region src/automation/condition-eval.ts
9068
- /**
9069
- * Evaluating an automation's condition tree against the ConditionMirror.
9070
- *
9071
- * D49: a trigger is a one-shot event, so its gate must never hang on a read
9072
- * that can fail. Every field a condition leaf names is therefore kept in
9073
- * memory — filled at start, updated off device state-changed events, reconciled
9074
- * on a timer — and evaluation reads that memory ONLY. A value the mirror does
9075
- * not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
9076
- * `pass`. A failed refresh changes nothing: the previous value stays.
9077
- *
9078
- * The evaluator returns a node per tree position (containers, leaves, and the
9079
- * ones a short-circuit never reached) with a stable path and a human reason, so
9080
- * the editor can paint each block.
9081
- */
9082
- function collectInto(cond, out) {
9083
- switch (cond.kind) {
9084
- case "all":
9085
- case "any":
9086
- for (const child of cond.children) collectInto(child, out);
9087
- return;
9088
- case "not":
9089
- collectInto(cond.child, out);
9090
- return;
9091
- case "condition":
9092
- out.push({
9093
- kind: "device",
9094
- deviceId: cond.deviceId,
9095
- cap: cond.cap
9096
- });
9097
- return;
9098
- case "expression":
9099
- for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
9100
- kind: "stable",
9101
- stableId: binding.sourceStableId,
9102
- cap: binding.cap
9103
- });
9104
- return;
9105
- }
9106
- }
9107
- /** Every (device, cap) a tree reads, deduplicated. */
9108
- function collectMirrorSources(cond) {
9109
- if (cond === void 0) return [];
9110
- const all = [];
9111
- collectInto(cond, all);
9112
- const seen = /* @__PURE__ */ new Set();
9113
- return all.filter((s) => {
9114
- const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
9115
- if (seen.has(key)) return false;
9116
- seen.add(key);
9117
- return true;
9118
- });
9119
- }
9120
- function sliceKey(deviceId, cap) {
9121
- return `${deviceId}|${cap}`;
9122
- }
9123
- function pick$1(slice, fieldPath) {
9124
- let cursor = slice;
9125
- for (const part of fieldPath.split(".")) {
9126
- if (cursor === null || typeof cursor !== "object") return void 0;
9127
- cursor = Reflect.get(cursor, part);
9128
- }
9129
- return cursor;
9130
- }
9131
- function isSliceObject(value) {
9132
- return typeof value === "object" && value !== null;
9133
- }
9134
- var ConditionMirror = class {
9135
- sources;
9136
- slices = /* @__PURE__ */ new Map();
9137
- stableIds = /* @__PURE__ */ new Map();
9138
- watched;
9139
- constructor(sources) {
9140
- this.sources = sources;
9141
- this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
9142
- }
9143
- /** A `device.state-changed` event. Slices nobody reads are ignored. */
9144
- apply(deviceId, cap, slice) {
9145
- const key = sliceKey(deviceId, cap);
9146
- if (!isSliceObject(slice)) return;
9147
- if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
9148
- this.slices.set(key, slice);
9149
- }
9150
- isStableTarget(deviceId, cap) {
9151
- for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
9152
- return false;
9153
- }
9154
- read(deviceId, cap, fieldPath) {
9155
- const slice = this.slices.get(sliceKey(deviceId, cap));
9156
- return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
9157
- }
9158
- readStable(stableId, cap, fieldPath) {
9159
- const deviceId = this.stableIds.get(stableId);
9160
- return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
9161
- }
9162
- /** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
9163
- async refresh(fetchers) {
9164
- let updated = 0;
9165
- let kept = 0;
9166
- if (this.sources.some((s) => s.kind === "stable")) try {
9167
- this.stableIds = await fetchers.fetchStableIds();
9168
- } catch {}
9169
- const targets = /* @__PURE__ */ new Map();
9170
- for (const s of this.sources) {
9171
- const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
9172
- if (deviceId === void 0) {
9173
- kept += 1;
9174
- continue;
9175
- }
9176
- targets.set(sliceKey(deviceId, s.cap), {
9177
- deviceId,
9178
- cap: s.cap
9179
- });
9180
- }
9181
- for (const [key, target] of targets) try {
9182
- const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
9183
- if (isSliceObject(slice)) {
9184
- this.slices.set(key, slice);
9185
- updated += 1;
9186
- } else kept += 1;
9187
- } catch {
9188
- kept += 1;
9189
- }
9190
- return {
9191
- updated,
9192
- kept
9193
- };
9194
- }
9195
- };
9196
- var OPERATOR_SYMBOL = {
9197
- eq: "=",
9198
- ne: "≠",
9199
- gt: ">",
9200
- gte: "≥",
9201
- lt: "<",
9202
- lte: "≤",
9203
- contains: "contains",
9204
- in: "in"
9205
- };
9206
- function compare(actual, operator, expected) {
9207
- switch (operator) {
9208
- case "eq": return actual === expected;
9209
- case "ne": return actual !== expected;
9210
- case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
9211
- case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
9212
- case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
9213
- case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
9214
- case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
9215
- case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
9216
- }
9217
- }
9218
- function toTraceValue(value) {
9219
- if (value === void 0 || value === null) return null;
9220
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9221
- if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
9222
- return JSON.stringify(value);
9223
- }
9224
- function show(value) {
9225
- return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
9226
- }
9227
- function markSkipped(cond, path, why, out) {
9228
- out.push({
9229
- path,
9230
- kind: cond.kind,
9231
- outcome: "not-evaluated",
9232
- reason: why
9233
- });
9234
- if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
9235
- markSkipped(c, `${path}.${i}`, why, out);
9236
- });
9237
- else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
9238
- }
9239
- function evalNode(cond, path, reader, now, out) {
9240
- switch (cond.kind) {
9241
- case "condition": {
9242
- const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
9243
- const base = {
9244
- path,
9245
- kind: cond.kind,
9246
- deviceId: cond.deviceId,
9247
- cap: cond.cap,
9248
- fieldPath: cond.fieldPath,
9249
- operator: cond.operator,
9250
- expected: toTraceValue(cond.value),
9251
- observed: toTraceValue(observed)
9252
- };
9253
- if (observed === void 0) {
9254
- out.push({
9255
- ...base,
9256
- outcome: "fail",
9257
- reason: `${cond.fieldPath} value unknown → fail`
9258
- });
9259
- return false;
9260
- }
9261
- const ok = compare(observed, cond.operator, cond.value);
9262
- out.push({
9263
- ...base,
9264
- outcome: ok ? "pass" : "fail",
9265
- reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
9266
- });
9267
- return ok;
9268
- }
9269
- case "expression": {
9270
- const values = {};
9271
- for (const [alias, binding] of Object.entries(cond.bindings)) {
9272
- if (binding.kind === "literal") {
9273
- values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
9274
- continue;
9275
- }
9276
- const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
9277
- if (raw === void 0) {
9278
- out.push({
9279
- path,
9280
- kind: cond.kind,
9281
- outcome: "fail",
9282
- reason: `${alias} value unknown → fail`
9283
- });
9284
- return false;
9285
- }
9286
- values[alias] = require_composition.toExpressionValue(raw) ?? null;
9287
- }
9288
- const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
9289
- if (!result.ok) {
9290
- out.push({
9291
- path,
9292
- kind: cond.kind,
9293
- outcome: "fail",
9294
- reason: `${cond.expr}: ${result.error} → fail`
9295
- });
9296
- return false;
9297
- }
9298
- const ok = result.value === true;
9299
- out.push({
9300
- path,
9301
- kind: cond.kind,
9302
- outcome: ok ? "pass" : "fail",
9303
- reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
9304
- });
9305
- return ok;
9306
- }
9307
- case "not": {
9308
- const at = out.length;
9309
- out.push({
9310
- path,
9311
- kind: "not",
9312
- outcome: "not-evaluated",
9313
- reason: ""
9314
- });
9315
- const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
9316
- const ok = !inner;
9317
- out.splice(at, 1, {
9318
- path,
9319
- kind: "not",
9320
- outcome: ok ? "pass" : "fail",
9321
- reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
9322
- });
9323
- return ok;
9324
- }
9325
- case "all":
9326
- case "any": {
9327
- const isAll = cond.kind === "all";
9328
- const at = out.length;
9329
- out.push({
9330
- path,
9331
- kind: cond.kind,
9332
- outcome: "not-evaluated",
9333
- reason: ""
9334
- });
9335
- let decided = null;
9336
- for (let i = 0; i < cond.children.length; i += 1) {
9337
- const child = cond.children[i];
9338
- if (child === void 0) continue;
9339
- if (decided !== null) {
9340
- markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
9341
- continue;
9342
- }
9343
- const ok = evalNode(child, `${path}.${i}`, reader, now, out);
9344
- if (isAll ? !ok : ok) decided = i;
9345
- }
9346
- const ok = isAll ? decided === null : decided !== null;
9347
- const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
9348
- out.splice(at, 1, {
9349
- path,
9350
- kind: cond.kind,
9351
- outcome: ok ? "pass" : "fail",
9352
- reason
9353
- });
9354
- return ok;
9355
- }
9356
- }
9357
- }
9358
- /** No condition tree is a pass with no nodes. */
9359
- function evaluateConditionTree(cond, reader, now) {
9360
- if (cond === void 0) return {
9361
- pass: true,
9362
- nodes: []
9363
- };
9364
- const nodes = [];
9365
- return {
9366
- pass: evalNode(cond, "conditions", reader, now, nodes),
9367
- nodes
9368
- };
9369
- }
9370
- //#endregion
9371
9117
  //#region src/catalogs/coco-classmap.ts
9372
9118
  var COCO_80_LABELS = [
9373
9119
  {
@@ -9770,7 +9516,7 @@ function detectionClassMatches(wanted, actual) {
9770
9516
  if (w === a) return true;
9771
9517
  return MACRO_NAMES.has(w) && macroOf(a) === w;
9772
9518
  }
9773
- function isPhase(value) {
9519
+ function isPhase$1(value) {
9774
9520
  return value === "start" || value === "update" || value === "end";
9775
9521
  }
9776
9522
  var DetectionTriggerFilter = class {
@@ -9785,7 +9531,7 @@ var DetectionTriggerFilter = class {
9785
9531
  }
9786
9532
  evaluate(event, now) {
9787
9533
  const { deviceId, trackId, phase } = event;
9788
- if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase(phase)) return {
9534
+ if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase$1(phase)) return {
9789
9535
  kind: "ignore",
9790
9536
  why: "malformed track-lifecycle event"
9791
9537
  };
@@ -9856,6 +9602,433 @@ var DetectionTriggerFilter = class {
9856
9602
  }
9857
9603
  };
9858
9604
  //#endregion
9605
+ //#region src/automation/condition-eval.ts
9606
+ /**
9607
+ * Evaluating an automation's condition tree against the ConditionMirror.
9608
+ *
9609
+ * D49: a trigger is a one-shot event, so its gate must never hang on a read
9610
+ * that can fail. Every field a condition leaf names is therefore kept in
9611
+ * memory — filled at start, updated off device state-changed events, reconciled
9612
+ * on a timer — and evaluation reads that memory ONLY. A value the mirror does
9613
+ * not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
9614
+ * `pass`. A failed refresh changes nothing: the previous value stays.
9615
+ *
9616
+ * The evaluator returns a node per tree position (containers, leaves, and the
9617
+ * ones a short-circuit never reached) with a stable path and a human reason, so
9618
+ * the editor can paint each block.
9619
+ */
9620
+ /** The cap whose runtime-state slice IS the occupancy snapshot, parked objects included. */
9621
+ var ZONE_ANALYTICS_CAP = "zone-analytics";
9622
+ function collectInto(cond, out) {
9623
+ switch (cond.kind) {
9624
+ case "all":
9625
+ case "any":
9626
+ for (const child of cond.children) collectInto(child, out);
9627
+ return;
9628
+ case "not":
9629
+ collectInto(cond.child, out);
9630
+ return;
9631
+ case "condition":
9632
+ out.push({
9633
+ kind: "device",
9634
+ deviceId: cond.deviceId,
9635
+ cap: cond.cap
9636
+ });
9637
+ return;
9638
+ case "stationary-present":
9639
+ out.push({
9640
+ kind: "device",
9641
+ deviceId: cond.deviceId,
9642
+ cap: ZONE_ANALYTICS_CAP
9643
+ });
9644
+ return;
9645
+ case "expression":
9646
+ for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
9647
+ kind: "stable",
9648
+ stableId: binding.sourceStableId,
9649
+ cap: binding.cap
9650
+ });
9651
+ return;
9652
+ }
9653
+ }
9654
+ /** Every (device, cap) a tree reads, deduplicated. */
9655
+ function collectMirrorSources(cond) {
9656
+ if (cond === void 0) return [];
9657
+ const all = [];
9658
+ collectInto(cond, all);
9659
+ const seen = /* @__PURE__ */ new Set();
9660
+ return all.filter((s) => {
9661
+ const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
9662
+ if (seen.has(key)) return false;
9663
+ seen.add(key);
9664
+ return true;
9665
+ });
9666
+ }
9667
+ function sliceKey(deviceId, cap) {
9668
+ return `${deviceId}|${cap}`;
9669
+ }
9670
+ function pick$1(slice, fieldPath) {
9671
+ let cursor = slice;
9672
+ for (const part of fieldPath.split(".")) {
9673
+ if (cursor === null || typeof cursor !== "object") return void 0;
9674
+ cursor = Reflect.get(cursor, part);
9675
+ }
9676
+ return cursor;
9677
+ }
9678
+ function isSliceObject(value) {
9679
+ return typeof value === "object" && value !== null;
9680
+ }
9681
+ var ConditionMirror = class {
9682
+ sources;
9683
+ slices = /* @__PURE__ */ new Map();
9684
+ stableIds = /* @__PURE__ */ new Map();
9685
+ watched;
9686
+ constructor(sources) {
9687
+ this.sources = sources;
9688
+ this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
9689
+ }
9690
+ /** A `device.state-changed` event. Slices nobody reads are ignored. */
9691
+ apply(deviceId, cap, slice) {
9692
+ const key = sliceKey(deviceId, cap);
9693
+ if (!isSliceObject(slice)) return;
9694
+ if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
9695
+ this.slices.set(key, slice);
9696
+ }
9697
+ isStableTarget(deviceId, cap) {
9698
+ for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
9699
+ return false;
9700
+ }
9701
+ read(deviceId, cap, fieldPath) {
9702
+ const slice = this.slices.get(sliceKey(deviceId, cap));
9703
+ return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
9704
+ }
9705
+ readStable(stableId, cap, fieldPath) {
9706
+ const deviceId = this.stableIds.get(stableId);
9707
+ return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
9708
+ }
9709
+ /** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
9710
+ async refresh(fetchers) {
9711
+ let updated = 0;
9712
+ let kept = 0;
9713
+ if (this.sources.some((s) => s.kind === "stable")) try {
9714
+ this.stableIds = await fetchers.fetchStableIds();
9715
+ } catch {}
9716
+ const targets = /* @__PURE__ */ new Map();
9717
+ for (const s of this.sources) {
9718
+ const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
9719
+ if (deviceId === void 0) {
9720
+ kept += 1;
9721
+ continue;
9722
+ }
9723
+ targets.set(sliceKey(deviceId, s.cap), {
9724
+ deviceId,
9725
+ cap: s.cap
9726
+ });
9727
+ }
9728
+ for (const [key, target] of targets) try {
9729
+ const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
9730
+ if (isSliceObject(slice)) {
9731
+ this.slices.set(key, slice);
9732
+ updated += 1;
9733
+ } else kept += 1;
9734
+ } catch {
9735
+ kept += 1;
9736
+ }
9737
+ return {
9738
+ updated,
9739
+ kept
9740
+ };
9741
+ }
9742
+ };
9743
+ var OPERATOR_SYMBOL = {
9744
+ eq: "=",
9745
+ ne: "≠",
9746
+ gt: ">",
9747
+ gte: "≥",
9748
+ lt: "<",
9749
+ lte: "≤",
9750
+ contains: "contains",
9751
+ in: "in"
9752
+ };
9753
+ function compare(actual, operator, expected) {
9754
+ switch (operator) {
9755
+ case "eq": return actual === expected;
9756
+ case "ne": return actual !== expected;
9757
+ case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
9758
+ case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
9759
+ case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
9760
+ case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
9761
+ case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
9762
+ case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
9763
+ }
9764
+ }
9765
+ function toTraceValue(value) {
9766
+ if (value === void 0 || value === null) return null;
9767
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9768
+ if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
9769
+ return JSON.stringify(value);
9770
+ }
9771
+ function show(value) {
9772
+ return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
9773
+ }
9774
+ function markSkipped(cond, path, why, out) {
9775
+ out.push({
9776
+ path,
9777
+ kind: cond.kind,
9778
+ outcome: "not-evaluated",
9779
+ reason: why
9780
+ });
9781
+ if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
9782
+ markSkipped(c, `${path}.${i}`, why, out);
9783
+ });
9784
+ else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
9785
+ }
9786
+ function parkedObjectsOf(value) {
9787
+ if (!Array.isArray(value)) return [];
9788
+ const rows = [];
9789
+ for (const item of value) {
9790
+ const id = Reflect.get(Object(item), "id");
9791
+ const className = Reflect.get(Object(item), "className");
9792
+ if (typeof id === "string" && typeof className === "string") rows.push({
9793
+ id,
9794
+ className
9795
+ });
9796
+ }
9797
+ return rows;
9798
+ }
9799
+ /** `zoneId`s whose `trackIds` list the parked object (`stationary:<id>`, as the snapshot builds it). */
9800
+ function zonesOfParked(zones, parkedId) {
9801
+ if (!Array.isArray(zones)) return [];
9802
+ const out = [];
9803
+ for (const zone of zones) {
9804
+ const zoneId = Reflect.get(Object(zone), "zoneId");
9805
+ const trackIds = Reflect.get(Object(zone), "trackIds");
9806
+ if (typeof zoneId === "string" && Array.isArray(trackIds) && trackIds.includes(`stationary:${parkedId}`)) out.push(zoneId);
9807
+ }
9808
+ return out;
9809
+ }
9810
+ /**
9811
+ * Is a matching parked object there NOW? Read off the mirrored zone-analytics
9812
+ * snapshot: `frame` is always present in a snapshot, so its absence means the
9813
+ * slice was never read (UNKNOWN, a fail that says so), while a snapshot without
9814
+ * `stationaryObjects` is a definite "nothing parked" (the provider omits an
9815
+ * empty list).
9816
+ */
9817
+ function judgeStationaryPresent(cond, reader) {
9818
+ if (reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "frame") === void 0) return {
9819
+ pass: false,
9820
+ why: `device ${cond.deviceId} occupancy snapshot unknown`
9821
+ };
9822
+ const parked = parkedObjectsOf(reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "stationaryObjects"));
9823
+ if (parked.length === 0) return {
9824
+ pass: false,
9825
+ why: `no parked objects on device ${cond.deviceId}`
9826
+ };
9827
+ const zones = reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "zones");
9828
+ for (const obj of parked) {
9829
+ if (cond.classes !== void 0 && !cond.classes.some((c) => detectionClassMatches(c, obj.className))) continue;
9830
+ if (cond.zones !== void 0) {
9831
+ const inZones = zonesOfParked(zones, obj.id);
9832
+ if (!cond.zones.some((z) => inZones.includes(z))) continue;
9833
+ }
9834
+ return {
9835
+ pass: true,
9836
+ why: `parked ${obj.className} (${obj.id}) on device ${cond.deviceId}`
9837
+ };
9838
+ }
9839
+ return {
9840
+ pass: false,
9841
+ why: `${parked.length} parked on device ${cond.deviceId}, none matching the class/zone filter`
9842
+ };
9843
+ }
9844
+ function evalNode(cond, path, reader, now, out) {
9845
+ switch (cond.kind) {
9846
+ case "condition": {
9847
+ const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
9848
+ const base = {
9849
+ path,
9850
+ kind: cond.kind,
9851
+ deviceId: cond.deviceId,
9852
+ cap: cond.cap,
9853
+ fieldPath: cond.fieldPath,
9854
+ operator: cond.operator,
9855
+ expected: toTraceValue(cond.value),
9856
+ observed: toTraceValue(observed)
9857
+ };
9858
+ if (observed === void 0) {
9859
+ out.push({
9860
+ ...base,
9861
+ outcome: "fail",
9862
+ reason: `${cond.fieldPath} value unknown → fail`
9863
+ });
9864
+ return false;
9865
+ }
9866
+ const ok = compare(observed, cond.operator, cond.value);
9867
+ out.push({
9868
+ ...base,
9869
+ outcome: ok ? "pass" : "fail",
9870
+ reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
9871
+ });
9872
+ return ok;
9873
+ }
9874
+ case "stationary-present": {
9875
+ const verdict = judgeStationaryPresent(cond, reader);
9876
+ out.push({
9877
+ path,
9878
+ kind: cond.kind,
9879
+ outcome: verdict.pass ? "pass" : "fail",
9880
+ reason: `${verdict.why} → ${verdict.pass ? "pass" : "fail"}`,
9881
+ deviceId: cond.deviceId,
9882
+ cap: ZONE_ANALYTICS_CAP
9883
+ });
9884
+ return verdict.pass;
9885
+ }
9886
+ case "expression": {
9887
+ const values = {};
9888
+ for (const [alias, binding] of Object.entries(cond.bindings)) {
9889
+ if (binding.kind === "literal") {
9890
+ values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
9891
+ continue;
9892
+ }
9893
+ const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
9894
+ if (raw === void 0) {
9895
+ out.push({
9896
+ path,
9897
+ kind: cond.kind,
9898
+ outcome: "fail",
9899
+ reason: `${alias} value unknown → fail`
9900
+ });
9901
+ return false;
9902
+ }
9903
+ values[alias] = require_composition.toExpressionValue(raw) ?? null;
9904
+ }
9905
+ const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
9906
+ if (!result.ok) {
9907
+ out.push({
9908
+ path,
9909
+ kind: cond.kind,
9910
+ outcome: "fail",
9911
+ reason: `${cond.expr}: ${result.error} → fail`
9912
+ });
9913
+ return false;
9914
+ }
9915
+ const ok = result.value === true;
9916
+ out.push({
9917
+ path,
9918
+ kind: cond.kind,
9919
+ outcome: ok ? "pass" : "fail",
9920
+ reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
9921
+ });
9922
+ return ok;
9923
+ }
9924
+ case "not": {
9925
+ const at = out.length;
9926
+ out.push({
9927
+ path,
9928
+ kind: "not",
9929
+ outcome: "not-evaluated",
9930
+ reason: ""
9931
+ });
9932
+ const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
9933
+ const ok = !inner;
9934
+ out.splice(at, 1, {
9935
+ path,
9936
+ kind: "not",
9937
+ outcome: ok ? "pass" : "fail",
9938
+ reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
9939
+ });
9940
+ return ok;
9941
+ }
9942
+ case "all":
9943
+ case "any": {
9944
+ const isAll = cond.kind === "all";
9945
+ const at = out.length;
9946
+ out.push({
9947
+ path,
9948
+ kind: cond.kind,
9949
+ outcome: "not-evaluated",
9950
+ reason: ""
9951
+ });
9952
+ let decided = null;
9953
+ for (let i = 0; i < cond.children.length; i += 1) {
9954
+ const child = cond.children[i];
9955
+ if (child === void 0) continue;
9956
+ if (decided !== null) {
9957
+ markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
9958
+ continue;
9959
+ }
9960
+ const ok = evalNode(child, `${path}.${i}`, reader, now, out);
9961
+ if (isAll ? !ok : ok) decided = i;
9962
+ }
9963
+ const ok = isAll ? decided === null : decided !== null;
9964
+ const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
9965
+ out.splice(at, 1, {
9966
+ path,
9967
+ kind: cond.kind,
9968
+ outcome: ok ? "pass" : "fail",
9969
+ reason
9970
+ });
9971
+ return ok;
9972
+ }
9973
+ }
9974
+ }
9975
+ /** No condition tree is a pass with no nodes. */
9976
+ function evaluateConditionTree(cond, reader, now) {
9977
+ if (cond === void 0) return {
9978
+ pass: true,
9979
+ nodes: []
9980
+ };
9981
+ const nodes = [];
9982
+ return {
9983
+ pass: evalNode(cond, "conditions", reader, now, nodes),
9984
+ nodes
9985
+ };
9986
+ }
9987
+ //#endregion
9988
+ //#region src/automation/stationary-trigger.ts
9989
+ function isPhase(value) {
9990
+ return value === "appeared" || value === "departed";
9991
+ }
9992
+ function evaluateStationaryTrigger(trigger, event) {
9993
+ const { deviceId, entryId, phase } = event;
9994
+ if (typeof deviceId !== "number" || typeof entryId !== "string" || !isPhase(phase)) return {
9995
+ kind: "ignore",
9996
+ why: "malformed stationary-changed event"
9997
+ };
9998
+ if (!trigger.deviceIds.includes(deviceId)) return {
9999
+ kind: "ignore",
10000
+ why: `device ${deviceId} is not one of the trigger's devices`
10001
+ };
10002
+ if (phase !== trigger.phase) return {
10003
+ kind: "ignore",
10004
+ why: `phase ${phase}, the trigger wants ${trigger.phase}`
10005
+ };
10006
+ const className = typeof event.className === "string" ? event.className : "";
10007
+ const zones = Array.isArray(event.zones) ? event.zones.filter((z) => typeof z === "string") : [];
10008
+ const vars = {
10009
+ deviceId,
10010
+ className,
10011
+ zones,
10012
+ phase,
10013
+ entryId
10014
+ };
10015
+ if (trigger.classes !== void 0 && !trigger.classes.some((c) => detectionClassMatches(c, className))) return {
10016
+ kind: "reject",
10017
+ reason: `class ${className || "unknown"} not in [${trigger.classes.join(", ")}]`,
10018
+ vars
10019
+ };
10020
+ if (trigger.zones !== void 0 && !trigger.zones.some((z) => zones.includes(z))) return {
10021
+ kind: "reject",
10022
+ reason: `zone mismatch: in [${zones.join(", ")}], wanted [${trigger.zones.join(", ")}]`,
10023
+ vars
10024
+ };
10025
+ return {
10026
+ kind: "fire",
10027
+ reason: `${className} ${phase} on device ${deviceId}`,
10028
+ vars
10029
+ };
10030
+ }
10031
+ //#endregion
9859
10032
  //#region src/automation/run-gate.ts
9860
10033
  var MINUTE_MS$1 = 6e4;
9861
10034
  function minutes(ms) {
@@ -10032,7 +10205,8 @@ var AutomationConditionTraceNodeSchema = zod.z.object({
10032
10205
  "any",
10033
10206
  "not",
10034
10207
  "condition",
10035
- "expression"
10208
+ "expression",
10209
+ "stationary-present"
10036
10210
  ]),
10037
10211
  outcome: TraceNodeOutcomeSchema,
10038
10212
  reason: zod.z.string(),
@@ -10159,7 +10333,7 @@ function appendTrace(ring, trace, keep) {
10159
10333
  }
10160
10334
  /**
10161
10335
  * The trigger stage of a trace from the bag a trigger handed to `run()`:
10162
- * primitives only (a device-state `to` that is an object is left out), minus the
10336
+ * primitives and short lists of them (a device-state `to` that is an object is left out), minus the
10163
10337
  * bookkeeping keys `kind` and `at`.
10164
10338
  */
10165
10339
  function buildTriggerTrace(trigger, outcome, reason) {
@@ -10167,6 +10341,7 @@ function buildTriggerTrace(trigger, outcome, reason) {
10167
10341
  for (const [key, value] of Object.entries(trigger)) {
10168
10342
  if (key === "kind" || key === "at") continue;
10169
10343
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
10344
+ else if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) vars[key] = value;
10170
10345
  }
10171
10346
  return {
10172
10347
  kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
@@ -14730,7 +14905,7 @@ var CompositionProblemCodeSchema = zod.z.enum([
14730
14905
  "member-constraint",
14731
14906
  "retention"
14732
14907
  ]);
14733
- /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
14908
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.temperature`. */
14734
14909
  var CompositionProblemSchema = zod.z.object({
14735
14910
  code: CompositionProblemCodeSchema,
14736
14911
  path: zod.z.string(),
@@ -15436,7 +15611,7 @@ var GROUP_TEMPLATES = [
15436
15611
  summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
15437
15612
  role: require_err_msg.DeviceRole.TemperatureSensor,
15438
15613
  outputCap: "temperature-sensor",
15439
- outputField: "celsius",
15614
+ outputField: "temperature",
15440
15615
  quantity: "temperature",
15441
15616
  defaultAggregate: "mean",
15442
15617
  empty: "null",
@@ -15447,7 +15622,7 @@ var GROUP_TEMPLATES = [
15447
15622
  summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
15448
15623
  role: require_err_msg.DeviceRole.HumiditySensor,
15449
15624
  outputCap: "humidity-sensor",
15450
- outputField: "percent",
15625
+ outputField: "humidity",
15451
15626
  quantity: "humidity",
15452
15627
  defaultAggregate: "mean",
15453
15628
  empty: "null",
@@ -15458,7 +15633,7 @@ var GROUP_TEMPLATES = [
15458
15633
  summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
15459
15634
  role: require_err_msg.DeviceRole.PressureSensor,
15460
15635
  outputCap: "pressure-sensor",
15461
- outputField: "hpa",
15636
+ outputField: "pressure",
15462
15637
  quantity: "pressure",
15463
15638
  defaultAggregate: "mean",
15464
15639
  empty: "null",
@@ -15469,7 +15644,7 @@ var GROUP_TEMPLATES = [
15469
15644
  summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
15470
15645
  role: require_err_msg.DeviceRole.AmbientLightSensor,
15471
15646
  outputCap: "ambient-light-sensor",
15472
- outputField: "lux",
15647
+ outputField: "illuminance",
15473
15648
  quantity: "illuminance",
15474
15649
  defaultAggregate: "mean",
15475
15650
  empty: "null",
@@ -15480,7 +15655,7 @@ var GROUP_TEMPLATES = [
15480
15655
  summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
15481
15656
  role: require_err_msg.DeviceRole.PowerSensor,
15482
15657
  outputCap: "power-meter",
15483
- outputField: "watts",
15658
+ outputField: "power",
15484
15659
  quantity: "power",
15485
15660
  defaultAggregate: "sum",
15486
15661
  empty: "omit",
@@ -15491,7 +15666,7 @@ var GROUP_TEMPLATES = [
15491
15666
  summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
15492
15667
  role: require_err_msg.DeviceRole.EnergySensor,
15493
15668
  outputCap: "power-meter",
15494
- outputField: "kwhTotal",
15669
+ outputField: "energy",
15495
15670
  quantity: "energy",
15496
15671
  defaultAggregate: "sum",
15497
15672
  empty: "omit",
@@ -15502,7 +15677,7 @@ var GROUP_TEMPLATES = [
15502
15677
  summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
15503
15678
  role: require_err_msg.DeviceRole.BatterySensor,
15504
15679
  outputCap: "battery",
15505
- outputField: "percentage",
15680
+ outputField: "level",
15506
15681
  quantity: "percentage",
15507
15682
  defaultAggregate: "mean",
15508
15683
  empty: "null",
@@ -15956,17 +16131,17 @@ async function runFanOut(calls, invoke) {
15956
16131
  //#region src/generated/history-catalog.ts
15957
16132
  /**
15958
16133
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
15959
- * 42 caps with series (77 series), 33 caps answered `'none'`.
16134
+ * 41 caps with series (76 series), 34 caps answered `'none'`.
15960
16135
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
15961
16136
  */
15962
16137
  var HISTORY_CATALOG = {
15963
16138
  "air-quality-sensor": {
15964
- "co2Ppm": {
16139
+ "co2": {
15965
16140
  kind: "gauge",
15966
16141
  unit: "ppm",
15967
16142
  deadband: 5
15968
16143
  },
15969
- "vocPpb": {
16144
+ "voc": {
15970
16145
  kind: "gauge",
15971
16146
  unit: "ppb",
15972
16147
  deadband: 5
@@ -16002,24 +16177,24 @@ var HISTORY_CATALOG = {
16002
16177
  "triggered"
16003
16178
  ]
16004
16179
  } },
16005
- "ambient-light-sensor": { "lux": {
16180
+ "ambient-light-sensor": { "illuminance": {
16006
16181
  kind: "gauge",
16007
16182
  unit: "lx",
16008
16183
  deadbandPct: 10,
16009
16184
  minIntervalMs: 6e4
16010
16185
  } },
16011
16186
  "battery": {
16012
- "percentage": {
16187
+ "level": {
16013
16188
  kind: "gauge",
16014
16189
  unit: "%"
16015
16190
  },
16016
- "volts": {
16191
+ "voltage": {
16017
16192
  kind: "gauge",
16018
16193
  unit: "V"
16019
16194
  }
16020
16195
  },
16021
16196
  "binary": { "on": { kind: "boolean" } },
16022
- "brightness": { "percentage": {
16197
+ "brightness": { "level": {
16023
16198
  kind: "gauge",
16024
16199
  unit: "%",
16025
16200
  minIntervalMs: 0
@@ -16061,7 +16236,7 @@ var HISTORY_CATALOG = {
16061
16236
  } },
16062
16237
  "device-status": { "online": { kind: "boolean" } },
16063
16238
  "dishwasher": {
16064
- "progressPercent": {
16239
+ "progress": {
16065
16240
  kind: "gauge",
16066
16241
  unit: "%",
16067
16242
  deadband: 2
@@ -16091,7 +16266,7 @@ var HISTORY_CATALOG = {
16091
16266
  "Aborting"
16092
16267
  ]
16093
16268
  },
16094
- "remainingSeconds": {
16269
+ "remaining": {
16095
16270
  kind: "gauge",
16096
16271
  unit: "s",
16097
16272
  minIntervalMs: 6e4
@@ -16104,7 +16279,7 @@ var HISTORY_CATALOG = {
16104
16279
  skipWhenSet: "timestamp"
16105
16280
  } },
16106
16281
  "fan-control": {
16107
- "percentage": {
16282
+ "speed": {
16108
16283
  kind: "gauge",
16109
16284
  unit: "%",
16110
16285
  minIntervalMs: 0
@@ -16126,7 +16301,7 @@ var HISTORY_CATALOG = {
16126
16301
  },
16127
16302
  "on": { kind: "boolean" }
16128
16303
  },
16129
- "humidity-sensor": { "percent": {
16304
+ "humidity-sensor": { "humidity": {
16130
16305
  kind: "gauge",
16131
16306
  unit: "%",
16132
16307
  deadband: .5
@@ -16164,13 +16339,13 @@ var HISTORY_CATALOG = {
16164
16339
  excludeDeviceTypes: [require_err_msg.DeviceType.Camera]
16165
16340
  } },
16166
16341
  "network-link": {
16167
- "signalPercent": {
16342
+ "signal": {
16168
16343
  kind: "gauge",
16169
16344
  unit: "%",
16170
16345
  deadband: 3,
16171
16346
  minIntervalMs: 6e4
16172
16347
  },
16173
- "rssiDbm": {
16348
+ "rssi": {
16174
16349
  kind: "gauge",
16175
16350
  unit: "dBm",
16176
16351
  deadband: 2,
@@ -16197,31 +16372,31 @@ var HISTORY_CATALOG = {
16197
16372
  }
16198
16373
  },
16199
16374
  "power-meter": {
16200
- "watts": {
16375
+ "power": {
16201
16376
  kind: "gauge",
16202
16377
  unit: "W",
16203
16378
  deadband: 1,
16204
16379
  minIntervalMs: 3e4
16205
16380
  },
16206
- "kwhTotal": {
16381
+ "energy": {
16207
16382
  kind: "counter",
16208
16383
  unit: "kWh",
16209
16384
  deadband: .01,
16210
16385
  minIntervalMs: 6e4
16211
16386
  },
16212
- "volts": {
16387
+ "voltage": {
16213
16388
  kind: "gauge",
16214
16389
  unit: "V",
16215
16390
  deadband: 1,
16216
16391
  minIntervalMs: 3e4
16217
16392
  },
16218
- "amps": {
16393
+ "current": {
16219
16394
  kind: "gauge",
16220
16395
  unit: "A",
16221
16396
  deadband: .05,
16222
16397
  minIntervalMs: 3e4
16223
16398
  },
16224
- "va": {
16399
+ "apparentPower": {
16225
16400
  kind: "gauge",
16226
16401
  unit: "VA",
16227
16402
  deadband: 1,
@@ -16234,12 +16409,7 @@ var HISTORY_CATALOG = {
16234
16409
  minIntervalMs: 6e4
16235
16410
  }
16236
16411
  },
16237
- "presence": { "batteryPercent": {
16238
- kind: "gauge",
16239
- unit: "%",
16240
- deadband: 1
16241
- } },
16242
- "pressure-sensor": { "hpa": {
16412
+ "pressure-sensor": { "pressure": {
16243
16413
  kind: "gauge",
16244
16414
  unit: "hPa",
16245
16415
  deadband: .1
@@ -16266,7 +16436,7 @@ var HISTORY_CATALOG = {
16266
16436
  },
16267
16437
  "switch": { "on": { kind: "boolean" } },
16268
16438
  "tamper": { "tampered": { kind: "boolean" } },
16269
- "temperature-sensor": { "celsius": {
16439
+ "temperature-sensor": { "temperature": {
16270
16440
  kind: "gauge",
16271
16441
  unit: "°C",
16272
16442
  deadband: .1
@@ -16374,6 +16544,7 @@ var HISTORY_NONE_CAPS = [
16374
16544
  "native-object-detection",
16375
16545
  "navigation",
16376
16546
  "notifier",
16547
+ "presence",
16377
16548
  "privacy-mask",
16378
16549
  "ptz-autotrack",
16379
16550
  "recording-onboard",
@@ -16511,10 +16682,10 @@ function derivativeDraft(spec, retention = require_composition.DEFAULT_HISTORY_R
16511
16682
  function integralDraft(spec, retention = require_composition.DEFAULT_HISTORY_RETENTION) {
16512
16683
  const problems = [...historyProblems(spec.source)];
16513
16684
  const kind = declaredSeriesKind(spec.source);
16514
- const watts = spec.source.cap === "power-meter" && spec.source.fieldPath === "watts";
16685
+ const isPower = spec.source.cap === "power-meter" && spec.source.fieldPath === "power";
16515
16686
  if (kind === "counter" || kind === "boolean" || kind === "enum") problems.push(problem("kind-mismatch", "helper.source", "an integral reads an instantaneous gauge, not a counter or a state"));
16516
- if (spec.output === "energy" && !watts) problems.push(problem("kind-mismatch", "helper.output", "energy is the integral of power-meter.watts; another reading uses the meter"));
16517
- if (spec.output === "meter" && watts) problems.push(problem("kind-mismatch", "helper.output", "power integrates to power-meter.kwhTotal, which energy dashboards already read"));
16687
+ if (spec.output === "energy" && !isPower) problems.push(problem("kind-mismatch", "helper.output", "energy is the integral of power-meter.power; another reading uses the meter"));
16688
+ if (spec.output === "meter" && isPower) problems.push(problem("kind-mismatch", "helper.output", "power integrates to power-meter.energy, which energy dashboards already read"));
16518
16689
  const tier = require_composition.planResetTier(spec.reset, retention);
16519
16690
  if (!tier.ok) problems.push(problem("retention", "helper.reset", tier.reason));
16520
16691
  const energy = spec.output === "energy";
@@ -16523,7 +16694,7 @@ function integralDraft(spec, retention = require_composition.DEFAULT_HISTORY_RET
16523
16694
  draft: draft({
16524
16695
  role: energy ? require_err_msg.DeviceRole.EnergySensor : require_err_msg.DeviceRole.NumericSensor,
16525
16696
  cap: energy ? "power-meter" : "meter",
16526
- field: energy ? "kwhTotal" : "value",
16697
+ field: energy ? "energy" : "value",
16527
16698
  empty: "offline",
16528
16699
  spec,
16529
16700
  member: spec.source
@@ -16782,9 +16953,9 @@ function expressionSource(expr, bindings = {}) {
16782
16953
  }
16783
16954
  function compileAppliance(spec) {
16784
16955
  const member = spec.member;
16785
- if (member.cap !== "power-meter" || member.fieldPath !== "watts") return {
16956
+ if (member.cap !== "power-meter" || member.fieldPath !== "power") return {
16786
16957
  ok: false,
16787
- problems: [problem("member-constraint", "helper.member", "an appliance helper reads power-meter.watts")]
16958
+ problems: [problem("member-constraint", "helper.member", "an appliance helper reads power-meter.power")]
16788
16959
  };
16789
16960
  const threshold = unsignedToken(spec.thresholdWatts);
16790
16961
  const start = unsignedToken(spec.startDwellMs);
@@ -16825,7 +16996,7 @@ function compileAppliance(spec) {
16825
16996
  kind: "fields",
16826
16997
  cap: "power-meter",
16827
16998
  fields: {
16828
- watts: read,
16999
+ power: read,
16829
17000
  lastFetchedAt: clock
16830
17001
  }
16831
17002
  }]
@@ -19051,8 +19222,8 @@ var DeviceHistoryProducerKindSchema = zod.z.enum([
19051
19222
  /**
19052
19223
  * A key computed at READ time from stored series of the same device, never
19053
19224
  * stored itself. Energy (kWh per bucket) comes from the power meter's
19054
- * `kwhTotal` counter when the device has one, else from integrating its
19055
- * `watts` gauge over time.
19225
+ * `energy` counter when the device has one, else from integrating its
19226
+ * `power` gauge over time.
19056
19227
  */
19057
19228
  var DEVICE_HISTORY_ENERGY_KEY = "derived:energy.kwh";
19058
19229
  var DeviceHistoryResolutionSchema = zod.z.enum([
@@ -64091,9 +64262,9 @@ var addonsCapability = {
64091
64262
  */
64092
64263
  var AirQualitySensorStatusSchema = zod.z.object({
64093
64264
  /** Carbon dioxide concentration in ppm. */
64094
- co2Ppm: zod.z.number().min(0).optional().meta(require_sleep.readingMeta("concentration-volume")),
64265
+ co2: zod.z.number().min(0).optional().meta(require_sleep.readingMeta("concentration-volume")),
64095
64266
  /** Total volatile organic compounds in ppb. */
64096
- vocPpb: zod.z.number().min(0).optional().meta(require_sleep.readingMeta("concentration-ppb")),
64267
+ voc: zod.z.number().min(0).optional().meta(require_sleep.readingMeta("concentration-ppb")),
64097
64268
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
64098
64269
  pm25: zod.z.number().min(0).optional().meta(require_sleep.readingMeta("concentration-mass")),
64099
64270
  /** Particulate matter ≤ 10 μm in µg/m³. */
@@ -64126,12 +64297,12 @@ var airQualitySensorCapability = {
64126
64297
  * History (D850): recorded series.
64127
64298
  */
64128
64299
  history: { fields: {
64129
- co2Ppm: {
64300
+ co2: {
64130
64301
  kind: "gauge",
64131
64302
  unit: "ppm",
64132
64303
  deadband: 5
64133
64304
  },
64134
- vocPpb: {
64305
+ voc: {
64135
64306
  kind: "gauge",
64136
64307
  unit: "ppb",
64137
64308
  deadband: 5
@@ -64164,7 +64335,7 @@ var airQualitySensorCapability = {
64164
64335
  */
64165
64336
  var AmbientLightSensorStatusSchema = zod.z.object({
64166
64337
  /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
64167
- lux: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("illuminance", {
64338
+ illuminance: zod.z.number().min(0).nullable().meta(require_sleep.readingMeta("illuminance", {
64168
64339
  precision: 0,
64169
64340
  label: "Illuminance"
64170
64341
  })),
@@ -64193,7 +64364,7 @@ var ambientLightSensorCapability = {
64193
64364
  /**
64194
64365
  * History (D850): recorded series. Wide range, so a RELATIVE deadband (10 %).
64195
64366
  */
64196
- history: { fields: { lux: {
64367
+ history: { fields: { illuminance: {
64197
64368
  kind: "gauge",
64198
64369
  unit: "lx",
64199
64370
  deadbandPct: 10,
@@ -64589,7 +64760,7 @@ var BatteryStatusSchema = zod.z.object({
64589
64760
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
64590
64761
  * declines to band a non-finite reading, which is the correct shape.
64591
64762
  */
64592
- percentage: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage", { label: "Charge" })),
64763
+ level: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage", { label: "Charge" })),
64593
64764
  /**
64594
64765
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
64595
64766
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -64599,7 +64770,7 @@ var BatteryStatusSchema = zod.z.object({
64599
64770
  * `'unknown'` is the absence of an answer: the provider has not read the
64600
64771
  * power fields yet, or the firmware reported neither. Before it existed,
64601
64772
  * two unreadable fields produced `'none'`, and a camera nobody had reached
64602
- * was drawn "on battery alone" — the same D315 defect `percentage` closed
64773
+ * was drawn "on battery alone" — the same D315 defect `level` closed
64603
64774
  * by going nullable. Consumers SKIP it (no charger state published, no
64604
64775
  * glyph, no HomeKit ChargingState) rather than coerce it either way.
64605
64776
  */
@@ -64623,7 +64794,7 @@ var BatteryStatusSchema = zod.z.object({
64623
64794
  * that only report charge. A second reading on this cap, not a second
64624
64795
  * `numeric-sensor` on the same device.
64625
64796
  */
64626
- volts: zod.z.number().nullable().optional().meta(require_sleep.readingMeta("voltage", { label: "Voltage" })),
64797
+ voltage: zod.z.number().nullable().optional().meta(require_sleep.readingMeta("voltage", { label: "Voltage" })),
64627
64798
  /**
64628
64799
  * Ms epoch of the last time the device PROVED it was reachable — a
64629
64800
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -64657,7 +64828,7 @@ var BatteryStatusSchema = zod.z.object({
64657
64828
  /**
64658
64829
  * True when the source is a BINARY low-battery indicator (HA
64659
64830
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
64660
- * charge level — `percentage` is then a coarse stand-in (100 = normal,
64831
+ * charge level — `level` is then a coarse stand-in (100 = normal,
64661
64832
  * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
64662
64833
  * misleading exact percentage. Absent/false → genuine 0–100 % reading.
64663
64834
  */
@@ -64677,15 +64848,15 @@ var BATTERY_DEVICE_TYPES = [
64677
64848
  var batteryCustomize = {
64678
64849
  version: 1,
64679
64850
  targets: [{
64680
- id: "percentage",
64851
+ id: "level",
64681
64852
  cardinality: "scalar",
64682
64853
  vocabulary: {
64683
64854
  cap: "battery",
64684
- stateField: "percentage"
64855
+ stateField: "level"
64685
64856
  },
64686
64857
  deviceTypes: BATTERY_DEVICE_TYPES,
64687
64858
  apply: "add",
64688
- path: "percentage",
64859
+ path: "level",
64689
64860
  sources: [{
64690
64861
  kind: "field",
64691
64862
  valueKinds: ["number", "string"],
@@ -64779,7 +64950,7 @@ onStatusChanged: { data: zod.z.object({
64779
64950
  schema: BatteryStatusSchema,
64780
64951
  kind: "push",
64781
64952
  empty: {
64782
- percentage: null,
64953
+ level: null,
64783
64954
  charging: "unknown",
64784
64955
  sleeping: false,
64785
64956
  lastUpdated: 0
@@ -64806,11 +64977,11 @@ onStatusChanged: { data: zod.z.object({
64806
64977
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
64807
64978
  */
64808
64979
  history: { fields: {
64809
- percentage: {
64980
+ level: {
64810
64981
  kind: "gauge",
64811
64982
  unit: "%"
64812
64983
  },
64813
- volts: {
64984
+ voltage: {
64814
64985
  kind: "gauge",
64815
64986
  unit: "V"
64816
64987
  }
@@ -64874,14 +65045,14 @@ var binaryCapability = {
64874
65045
  * controls to this single-method surface.
64875
65046
  *
64876
65047
  * The cap is intentionally minimal: a single `setBrightness({deviceId,
64877
- * percentage})` mutation plus the auto-injected `getStatus`. Drivers
65048
+ * level})` mutation plus the auto-injected `getStatus`. Drivers
64878
65049
  * that expose richer controls (color temperature, scenes, schedules)
64879
65050
  * should surface those via the device's `getSettingsUISchema()`
64880
65051
  * instead of bloating this cap.
64881
65052
  */
64882
65053
  var BrightnessStatusSchema = zod.z.object({
64883
65054
  /** Current level as 0..100 inclusive. Firmware-reported. */
64884
- percentage: zod.z.number().min(0).max(100).meta(require_sleep.readingMeta("percentage")),
65055
+ level: zod.z.number().min(0).max(100).meta(require_sleep.readingMeta("percentage")),
64885
65056
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
64886
65057
  lastChangedAt: zod.z.number()
64887
65058
  });
@@ -64893,7 +65064,7 @@ var brightnessCapability = {
64893
65064
  deviceTypes: [require_err_msg.DeviceType.Light],
64894
65065
  methods: { setBrightness: require_sleep.method(zod.z.object({
64895
65066
  deviceId: zod.z.number().int().nonnegative(),
64896
- percentage: zod.z.number().min(0).max(100)
65067
+ level: zod.z.number().min(0).max(100)
64897
65068
  }), zod.z.void(), {
64898
65069
  kind: "mutation",
64899
65070
  auth: "protected"
@@ -64906,7 +65077,7 @@ var brightnessCapability = {
64906
65077
  */
64907
65078
  onBrightnessChanged: { data: zod.z.object({
64908
65079
  deviceId: zod.z.number(),
64909
- percentage: zod.z.number().min(0).max(100),
65080
+ level: zod.z.number().min(0).max(100),
64910
65081
  lastChangedAt: zod.z.number()
64911
65082
  }) } },
64912
65083
  status: {
@@ -64929,7 +65100,7 @@ onBrightnessChanged: { data: zod.z.object({
64929
65100
  /**
64930
65101
  * History (D850): recorded series. Actuator tier: level keeps every change.
64931
65102
  */
64932
- history: { fields: { percentage: {
65103
+ history: { fields: { level: {
64933
65104
  kind: "gauge",
64934
65105
  unit: "%",
64935
65106
  minIntervalMs: 0
@@ -65673,11 +65844,11 @@ var ColorInputSchema = zod.z.discriminatedUnion("mode", [
65673
65844
  }),
65674
65845
  zod.z.object({
65675
65846
  mode: zod.z.literal("mired"),
65676
- mireds: zod.z.number().int().min(50).max(1e3)
65847
+ colorTemperature: zod.z.number().int().min(50).max(1e3)
65677
65848
  })
65678
65849
  ]);
65679
65850
  var ColorStatusSchema = zod.z.object({
65680
- /** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
65851
+ /** Active color mode — which of `rgb` / `hsv` / `colorTemperature` reflects
65681
65852
  * the bulb's current state. */
65682
65853
  mode: zod.z.enum([
65683
65854
  "rgb",
@@ -65689,7 +65860,7 @@ var ColorStatusSchema = zod.z.object({
65689
65860
  /** Populated when `mode === 'hsv'`. */
65690
65861
  hsv: HsvTripletSchema.optional(),
65691
65862
  /** Populated when `mode === 'mired'`. */
65692
- mireds: zod.z.number().int().optional().meta(require_sleep.readingMeta("colour-temperature")),
65863
+ colorTemperature: zod.z.number().int().optional().meta(require_sleep.readingMeta("colour-temperature")),
65693
65864
  /** Ms epoch of the last operator-driven change. */
65694
65865
  lastChangedAt: zod.z.number()
65695
65866
  });
@@ -65725,7 +65896,7 @@ onColorChanged: { data: zod.z.object({
65725
65896
  ]),
65726
65897
  rgb: RgbTripletSchema.optional(),
65727
65898
  hsv: HsvTripletSchema.optional(),
65728
- mireds: zod.z.number().int().optional(),
65899
+ colorTemperature: zod.z.number().int().optional(),
65729
65900
  lastChangedAt: zod.z.number()
65730
65901
  }) } },
65731
65902
  status: {
@@ -66758,11 +66929,11 @@ var DishwasherStatusSchema = zod.z.object({
66758
66929
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
66759
66930
  availablePrograms: zod.z.array(zod.z.string()),
66760
66931
  /** Seconds remaining on the active cycle, or null when not running / not known. */
66761
- remainingSeconds: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration")),
66932
+ remaining: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration")),
66762
66933
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
66763
- delayedStartSeconds: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration")),
66934
+ delayedStart: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration")),
66764
66935
  /** Cycle completion 0..100, or null when not applicable. */
66765
- progressPercent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
66936
+ progress: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
66766
66937
  /** True when the door is reported open; null when the door state is not known. */
66767
66938
  doorOpen: zod.z.boolean().nullable(),
66768
66939
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -66981,14 +67152,14 @@ var dishwasherCapability = {
66981
67152
  runtimeState: DishwasherStatusSchema,
66982
67153
  durability: "session",
66983
67154
  history: { fields: {
66984
- progressPercent: {
67155
+ progress: {
66985
67156
  kind: "gauge",
66986
67157
  unit: "%",
66987
67158
  deadband: 2
66988
67159
  },
66989
67160
  state: { kind: "enum" },
66990
67161
  operationState: { kind: "enum" },
66991
- remainingSeconds: {
67162
+ remaining: {
66992
67163
  kind: "gauge",
66993
67164
  unit: "s",
66994
67165
  minIntervalMs: 6e4
@@ -67641,7 +67812,7 @@ var FanDirectionSchema = zod.z.enum(["forward", "reverse"]);
67641
67812
  var FanControlStatusSchema = zod.z.object({
67642
67813
  /** Active speed as 0..100 inclusive. Null when the device has no
67643
67814
  * speed surface (single-speed fan). */
67644
- percentage: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
67815
+ speed: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
67645
67816
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
67646
67817
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
67647
67818
  percentageStep: zod.z.number().positive().optional().meta(require_sleep.readingMeta("percentage")),
@@ -67668,7 +67839,7 @@ var fanControlCapability = {
67668
67839
  methods: {
67669
67840
  setPercentage: require_sleep.method(zod.z.object({
67670
67841
  deviceId: zod.z.number().int().nonnegative(),
67671
- percentage: zod.z.number().min(0).max(100)
67842
+ speed: zod.z.number().min(0).max(100)
67672
67843
  }), zod.z.void(), {
67673
67844
  kind: "mutation",
67674
67845
  auth: "protected"
@@ -67701,7 +67872,7 @@ var fanControlCapability = {
67701
67872
  },
67702
67873
  /**
67703
67874
  * Runtime-state slice — mirrored by the kernel. UI fan speed
67704
- * sliders read `percentage` for live updates.
67875
+ * sliders read `speed` for live updates.
67705
67876
  */
67706
67877
  runtimeState: FanControlStatusSchema,
67707
67878
  /**
@@ -67715,7 +67886,7 @@ var fanControlCapability = {
67715
67886
  * History (D850): recorded series. Actuator tier: speed keeps every change.
67716
67887
  */
67717
67888
  history: { fields: {
67718
- percentage: {
67889
+ speed: {
67719
67890
  kind: "gauge",
67720
67891
  unit: "%",
67721
67892
  minIntervalMs: 0
@@ -68185,7 +68356,7 @@ var humidifierCapability = {
68185
68356
  */
68186
68357
  var HumiditySensorStatusSchema = zod.z.object({
68187
68358
  /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
68188
- percent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("humidity", {
68359
+ humidity: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("humidity", {
68189
68360
  precision: 0,
68190
68361
  label: "Humidity"
68191
68362
  })),
@@ -68214,7 +68385,7 @@ var humiditySensorCapability = {
68214
68385
  /**
68215
68386
  * History (D850): recorded series.
68216
68387
  */
68217
- history: { fields: { percent: {
68388
+ history: { fields: { humidity: {
68218
68389
  kind: "gauge",
68219
68390
  unit: "%",
68220
68391
  deadband: .5
@@ -68882,7 +69053,7 @@ var LawnMowerControlStatusSchema = zod.z.object({
68882
69053
  batteryLevel: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
68883
69054
  /** 0..100 mowing-completion percentage of the current task, or null when no
68884
69055
  * task is active / progress is unavailable. */
68885
- progressPercent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
69056
+ progress: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
68886
69057
  /** Current device/event code (dynamic — mostly status, sometimes an error),
68887
69058
  * or null when unknown. */
68888
69059
  currentCode: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
@@ -69579,7 +69750,7 @@ var lockControlCapability = {
69579
69750
  //#endregion
69580
69751
  //#region src/capabilities/meter.cap.ts
69581
69752
  /**
69582
- * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
69753
+ * A monotonic total between resets (D944). Energy uses `power-meter.energy`
69583
69754
  * so dashboards keep one series; every other instantaneous reading lands here.
69584
69755
  * `value` is the time integral in the source quantity's accumulated base
69585
69756
  * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
@@ -69675,10 +69846,10 @@ var MediaPlayerStatusSchema = zod.z.object({
69675
69846
  currentMedia: MediaInfoSchema.nullable(),
69676
69847
  /** Current playback position in ms. Null when not seekable or
69677
69848
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
69678
- positionMs: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration-ms")),
69849
+ position: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration-ms")),
69679
69850
  /** Total duration of the current media in ms. Null when unknown
69680
69851
  * (live stream) or nothing is playing. */
69681
- durationMs: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration-ms")),
69852
+ duration: zod.z.number().int().nonnegative().nullable().meta(require_sleep.readingMeta("duration-ms")),
69682
69853
  /** Shuffle toggle. Null when no shuffle surface.
69683
69854
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
69684
69855
  shuffle: zod.z.boolean().nullable(),
@@ -69717,7 +69888,7 @@ var mediaPlayerCapability = {
69717
69888
  }),
69718
69889
  seek: require_sleep.method(zod.z.object({
69719
69890
  deviceId: zod.z.number().int().nonnegative(),
69720
- positionMs: zod.z.number().int().nonnegative()
69891
+ position: zod.z.number().int().nonnegative()
69721
69892
  }), zod.z.void(), {
69722
69893
  kind: "mutation",
69723
69894
  auth: "protected"
@@ -69789,7 +69960,7 @@ var mediaPlayerCapability = {
69789
69960
  /**
69790
69961
  * History (D850): volume (a level an operator or an automation sets; the
69791
69962
  * default 10 s floor folds a slider drag into its end value) and mute.
69792
- * `positionMs` is not recorded: it moves continuously while playing.
69963
+ * `position` is not recorded: it moves continuously while playing.
69793
69964
  */
69794
69965
  history: { fields: {
69795
69966
  volumeLevel: {
@@ -71014,9 +71185,9 @@ var NetworkLinkStatusSchema = zod.z.object({
71014
71185
  * one whose reading has not landed must not be drawn at 0 %. Consumers
71015
71186
  * SKIP a null rather than coerce it.
71016
71187
  */
71017
- signalPercent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
71188
+ signal: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
71018
71189
  /** Raw received signal strength in dBm, when the firmware reports one. */
71019
- rssiDbm: zod.z.number().optional().meta(require_sleep.readingMeta("signal-level")),
71190
+ rssi: zod.z.number().optional().meta(require_sleep.readingMeta("signal-level")),
71020
71191
  /** Network name of a wireless link, when the firmware reports it. */
71021
71192
  ssid: zod.z.string().optional(),
71022
71193
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
@@ -71025,7 +71196,7 @@ var NetworkLinkStatusSchema = zod.z.object({
71025
71196
  /** The slice a provider seeds before its first read: nothing is known yet. */
71026
71197
  var NETWORK_LINK_UNKNOWN = {
71027
71198
  type: "unknown",
71028
- signalPercent: null,
71199
+ signal: null,
71029
71200
  lastUpdated: 0
71030
71201
  };
71031
71202
  /**
@@ -71042,9 +71213,9 @@ function signalPercentFromBars(bars, maxBars) {
71042
71213
  * below is 0 %, -50 dBm and above is 100 %, linear between. Non-finite or
71043
71214
  * positive input is not an RSSI: `null`.
71044
71215
  */
71045
- function signalPercentFromRssi(rssiDbm) {
71046
- if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
71047
- return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
71216
+ function signalPercentFromRssi(rssi) {
71217
+ if (rssi === void 0 || !Number.isFinite(rssi) || rssi > 0) return null;
71218
+ return Math.round((Math.min(-50, Math.max(-100, rssi)) + 100) * 2);
71048
71219
  }
71049
71220
  var networkLinkCapability = {
71050
71221
  name: "network-link",
@@ -71095,13 +71266,13 @@ onStatusChanged: { data: zod.z.object({
71095
71266
  * History (D850): recorded series.
71096
71267
  */
71097
71268
  history: { fields: {
71098
- signalPercent: {
71269
+ signal: {
71099
71270
  kind: "gauge",
71100
71271
  unit: "%",
71101
71272
  deadband: 3,
71102
71273
  minIntervalMs: 6e4
71103
71274
  },
71104
- rssiDbm: {
71275
+ rssi: {
71105
71276
  kind: "gauge",
71106
71277
  unit: "dBm",
71107
71278
  deadband: 2,
@@ -73464,16 +73635,16 @@ var platformProbeCapability = {
73464
73635
  * in real-world devices.
73465
73636
  */
73466
73637
  var PowerMeterStatusSchema = zod.z.object({
73467
- /** Instantaneous power draw in watts. */
73468
- watts: zod.z.number().optional().meta(require_sleep.readingMeta("power", { label: "Power" })),
73638
+ /** Instantaneous power draw (watts). */
73639
+ power: zod.z.number().optional().meta(require_sleep.readingMeta("power", { label: "Power" })),
73469
73640
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
73470
- kwhTotal: zod.z.number().optional().meta(require_sleep.readingMeta("energy", { label: "Energy" })),
73471
- /** Voltage in volts. */
73472
- volts: zod.z.number().optional().meta(require_sleep.readingMeta("voltage", { label: "Voltage" })),
73641
+ energy: zod.z.number().optional().meta(require_sleep.readingMeta("energy", { label: "Energy" })),
73642
+ /** Voltage (volts). */
73643
+ voltage: zod.z.number().optional().meta(require_sleep.readingMeta("voltage", { label: "Voltage" })),
73473
73644
  /** Current in amperes. */
73474
- amps: zod.z.number().optional().meta(require_sleep.readingMeta("current", { label: "Current" })),
73645
+ current: zod.z.number().optional().meta(require_sleep.readingMeta("current", { label: "Current" })),
73475
73646
  /** Apparent power in volt-amperes. */
73476
- va: zod.z.number().optional().meta(require_sleep.readingMeta("apparent-power")),
73647
+ apparentPower: zod.z.number().optional().meta(require_sleep.readingMeta("apparent-power")),
73477
73648
  /** Power factor (0..1) if reported. */
73478
73649
  powerFactor: zod.z.number().min(0).max(1).optional().meta(require_sleep.readingMeta("ratio")),
73479
73650
  /** Ms epoch when the slice was last updated. */
@@ -73531,14 +73702,14 @@ function powerTarget(id, unit) {
73531
73702
  advancedExpression: true
73532
73703
  };
73533
73704
  }
73534
- /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
73705
+ /** "Power and energy" extension: power, energy, voltage and current from other devices (D951). */
73535
73706
  var powerMeterCustomize = {
73536
73707
  version: 1,
73537
73708
  targets: [
73538
- powerTarget("watts", "W"),
73539
- powerTarget("kwhTotal", "kWh"),
73540
- powerTarget("volts", "V"),
73541
- powerTarget("amps", "A")
73709
+ powerTarget("power", "W"),
73710
+ powerTarget("energy", "kWh"),
73711
+ powerTarget("voltage", "V"),
73712
+ powerTarget("current", "A")
73542
73713
  ]
73543
73714
  };
73544
73715
  var powerMeterCapability = {
@@ -73554,41 +73725,41 @@ var powerMeterCapability = {
73554
73725
  },
73555
73726
  runtimeState: PowerMeterStatusSchema,
73556
73727
  /**
73557
- * Runtime-state durability: **restored** — as `numeric-sensor`; `kwhTotal` is an accumulator whose restored value is the baseline.
73728
+ * Runtime-state durability: **restored** — as `numeric-sensor`; `energy` is an accumulator whose restored value is the baseline.
73558
73729
  *
73559
73730
  * See `RuntimeStateDurability`. Enforced by
73560
73731
  * `scripts/check-runtime-state-durability.ts`.
73561
73732
  */
73562
73733
  durability: "restored",
73563
73734
  /**
73564
- * History (D850): recorded series. Gauges are floored at 30 s; `kwhTotal` is the one monotonic meter (counter).
73735
+ * History (D850): recorded series. Gauges are floored at 30 s; `energy` is the one monotonic meter (counter).
73565
73736
  */
73566
73737
  history: { fields: {
73567
- watts: {
73738
+ power: {
73568
73739
  kind: "gauge",
73569
73740
  unit: "W",
73570
73741
  deadband: 1,
73571
73742
  minIntervalMs: 3e4
73572
73743
  },
73573
- kwhTotal: {
73744
+ energy: {
73574
73745
  kind: "counter",
73575
73746
  unit: "kWh",
73576
73747
  deadband: .01,
73577
73748
  minIntervalMs: 6e4
73578
73749
  },
73579
- volts: {
73750
+ voltage: {
73580
73751
  kind: "gauge",
73581
73752
  unit: "V",
73582
73753
  deadband: 1,
73583
73754
  minIntervalMs: 3e4
73584
73755
  },
73585
- amps: {
73756
+ current: {
73586
73757
  kind: "gauge",
73587
73758
  unit: "A",
73588
73759
  deadband: .05,
73589
73760
  minIntervalMs: 3e4
73590
73761
  },
73591
- va: {
73762
+ apparentPower: {
73592
73763
  kind: "gauge",
73593
73764
  unit: "VA",
73594
73765
  deadband: 1,
@@ -73640,9 +73811,6 @@ var PresenceStatusSchema = zod.z.object({
73640
73811
  location: zod.z.string().nullable(),
73641
73812
  /** GPS coordinates when available. Null otherwise. */
73642
73813
  gps: GpsLocationSchema.nullable(),
73643
- /** Optional battery level of the tracking device (0..100). Null
73644
- * when not reported. */
73645
- batteryPercent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
73646
73814
  /** Ms epoch when the slice was last updated. */
73647
73815
  lastChangedAt: zod.z.number()
73648
73816
  });
@@ -73671,14 +73839,7 @@ var presenceCapability = {
73671
73839
  * `scripts/check-runtime-state-durability.ts`.
73672
73840
  */
73673
73841
  durability: "restored",
73674
- /**
73675
- * History (D850): recorded series.
73676
- */
73677
- history: { fields: { batteryPercent: {
73678
- kind: "gauge",
73679
- unit: "%",
73680
- deadband: 1
73681
- } } },
73842
+ history: "none",
73682
73843
  /** Clock fields: written, but excluded from the compare that decides
73683
73844
  * whether persisting is worth a SQLite commit. */
73684
73845
  volatileStateFields: ["lastChangedAt"]
@@ -73695,7 +73856,7 @@ var presenceCapability = {
73695
73856
  */
73696
73857
  var PressureSensorStatusSchema = zod.z.object({
73697
73858
  /** Current pressure in hPa. `null` when the sample was refused (D393). */
73698
- hpa: zod.z.number().nullable().meta(require_sleep.readingMeta("pressure", { precision: 0 })),
73859
+ pressure: zod.z.number().nullable().meta(require_sleep.readingMeta("pressure", { precision: 0 })),
73699
73860
  /** Ms epoch when the slice was last updated. */
73700
73861
  lastFetchedAt: zod.z.number()
73701
73862
  });
@@ -73721,7 +73882,7 @@ var pressureSensorCapability = {
73721
73882
  /**
73722
73883
  * History (D850): recorded series.
73723
73884
  */
73724
- history: { fields: { hpa: {
73885
+ history: { fields: { pressure: {
73725
73886
  kind: "gauge",
73726
73887
  unit: "hPa",
73727
73888
  deadband: .1
@@ -78948,7 +79109,7 @@ var tamperCapability = {
78948
79109
  */
78949
79110
  var TemperatureSensorStatusSchema = zod.z.object({
78950
79111
  /** Current temperature in Celsius. `null` when the sample was refused (D393). */
78951
- celsius: zod.z.number().nullable().meta(require_sleep.readingMeta("temperature", {
79112
+ temperature: zod.z.number().nullable().meta(require_sleep.readingMeta("temperature", {
78952
79113
  precision: 1,
78953
79114
  label: "Temperature"
78954
79115
  })),
@@ -78977,7 +79138,7 @@ var temperatureSensorCapability = {
78977
79138
  /**
78978
79139
  * History (D850): recorded series.
78979
79140
  */
78980
- history: { fields: { celsius: {
79141
+ history: { fields: { temperature: {
78981
79142
  kind: "gauge",
78982
79143
  unit: "°C",
78983
79144
  deadband: .1
@@ -79345,7 +79506,7 @@ var VacuumControlStatusSchema = zod.z.object({
79345
79506
  /** Speed tokens the hardware accepts — drives the UI selector. */
79346
79507
  availableFanSpeeds: zod.z.array(zod.z.string()),
79347
79508
  /** 0..100 cleaning-completion percentage of the current task, or null. */
79348
- progressPercent: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
79509
+ progress: zod.z.number().min(0).max(100).nullable().meta(require_sleep.readingMeta("percentage")),
79349
79510
  /** Current error code (0 / null = no error). */
79350
79511
  errorCode: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
79351
79512
  /** Human label for {@link errorCode}, or null when none / undecodable. */
@@ -81995,7 +82156,7 @@ function named(prefix, sources, cap, fieldPath) {
81995
82156
  return Object.fromEntries(sources.map((s, i) => [`${prefix}${i + 1}`, from(s, cap, fieldPath)]));
81996
82157
  }
81997
82158
  function roomCompositionExample(input) {
81998
- const t = named("t", input.temperatureSources, "temperature-sensor", "celsius");
82159
+ const t = named("t", input.temperatureSources, "temperature-sensor", "temperature");
81999
82160
  const f = named("f", input.temperatureSources, "temperature-sensor", "lastFetchedAt");
82000
82161
  const d = named("d", input.motionSources, "motion", "detected");
82001
82162
  const m = named("m", input.motionSources, "motion", "lastDetectedAt");
@@ -82010,7 +82171,7 @@ function roomCompositionExample(input) {
82010
82171
  kind: "fields",
82011
82172
  cap: "temperature-sensor",
82012
82173
  fields: {
82013
- celsius: {
82174
+ temperature: {
82014
82175
  kind: "expression",
82015
82176
  expr: `round(avg(${Object.keys(t).join(", ")}), 1)`,
82016
82177
  bindings: t
@@ -82056,7 +82217,7 @@ function roomCompositionExample(input) {
82056
82217
  * an `enum-sensor` whose `value` is the raw STRING state — `number(d)` reads it,
82057
82218
  * and anything that is not a number is `null`, never 0 (D393). The battery
82058
82219
  * status sensor is an `enum-sensor` too (`unavailable` keeps its cold `''`, so
82059
- * `percentage` is `null`). `charging` maps `battery_charging` off to
82220
+ * `level` is `null`). `charging` maps `battery_charging` off to
82060
82221
  * `'unknown'`, never `'none'`: off does not prove "on battery alone" (the
82061
82222
  * operator confirms the real semantics live).
82062
82223
  */
@@ -82125,7 +82286,7 @@ function feederCustomizationExample(sources) {
82125
82286
  kind: "fields",
82126
82287
  cap: "battery",
82127
82288
  fields: {
82128
- percentage: {
82289
+ level: {
82129
82290
  kind: "expression",
82130
82291
  expr: "s == \"low\" ? 15 : (s == \"normal\" ? 100 : null)",
82131
82292
  bindings: { s: from(sources.batteryStatus, "enum-sensor", "value") }
@@ -82193,10 +82354,10 @@ function unitConstant(to) {
82193
82354
  /** Primary numeric field that also publishes the cap's `unit` string. */
82194
82355
  var UNIT_COMPANION_FIELD = {
82195
82356
  "numeric-sensor": "value",
82196
- "temperature-sensor": "celsius",
82197
- "humidity-sensor": "percent",
82198
- "pressure-sensor": "hpa",
82199
- "ambient-light-sensor": "lux"
82357
+ "temperature-sensor": "temperature",
82358
+ "humidity-sensor": "humidity",
82359
+ "pressure-sensor": "pressure",
82360
+ "ambient-light-sensor": "illuminance"
82200
82361
  };
82201
82362
  /** `unit` when converting that cap's primary numeric field; otherwise none. */
82202
82363
  function unitCompanionField(cap, fieldPath) {
@@ -82383,7 +82544,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82383
82544
  "battery": {
82384
82545
  version: 1,
82385
82546
  targets: [{
82386
- id: "percentage",
82547
+ id: "level",
82387
82548
  cardinality: "scalar",
82388
82549
  deviceTypes: [
82389
82550
  require_err_msg.DeviceType.Camera,
@@ -82397,7 +82558,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82397
82558
  ],
82398
82559
  apply: "add",
82399
82560
  vocabularyCap: "battery",
82400
- vocabularyStateField: "percentage",
82561
+ vocabularyStateField: "level",
82401
82562
  sources: [{
82402
82563
  kind: "field",
82403
82564
  valueKinds: ["number", "string"],
@@ -82412,7 +82573,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82412
82573
  }],
82413
82574
  sourceReport: { status: "assignment-status" },
82414
82575
  advancedExpression: true,
82415
- path: "percentage",
82576
+ path: "level",
82416
82577
  valueKind: "number",
82417
82578
  nullable: true,
82418
82579
  companions: [
@@ -82647,7 +82808,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82647
82808
  version: 1,
82648
82809
  targets: [
82649
82810
  {
82650
- id: "watts",
82811
+ id: "power",
82651
82812
  cardinality: "scalar",
82652
82813
  deviceTypes: [
82653
82814
  require_err_msg.DeviceType.Switch,
@@ -82667,7 +82828,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82667
82828
  ],
82668
82829
  apply: "add",
82669
82830
  vocabularyCap: "power-meter",
82670
- vocabularyStateField: "watts",
82831
+ vocabularyStateField: "power",
82671
82832
  sources: [{
82672
82833
  kind: "numeric",
82673
82834
  requiredUnit: "W",
@@ -82676,7 +82837,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82676
82837
  }],
82677
82838
  sourceReport: { status: "assignment-status" },
82678
82839
  advancedExpression: true,
82679
- path: "watts",
82840
+ path: "power",
82680
82841
  valueKind: "number",
82681
82842
  nullable: false,
82682
82843
  companions: [{
@@ -82686,7 +82847,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82686
82847
  }]
82687
82848
  },
82688
82849
  {
82689
- id: "kwhTotal",
82850
+ id: "energy",
82690
82851
  cardinality: "scalar",
82691
82852
  deviceTypes: [
82692
82853
  require_err_msg.DeviceType.Switch,
@@ -82706,7 +82867,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82706
82867
  ],
82707
82868
  apply: "add",
82708
82869
  vocabularyCap: "power-meter",
82709
- vocabularyStateField: "kwhTotal",
82870
+ vocabularyStateField: "energy",
82710
82871
  sources: [{
82711
82872
  kind: "numeric",
82712
82873
  requiredUnit: "kWh",
@@ -82715,7 +82876,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82715
82876
  }],
82716
82877
  sourceReport: { status: "assignment-status" },
82717
82878
  advancedExpression: true,
82718
- path: "kwhTotal",
82879
+ path: "energy",
82719
82880
  valueKind: "number",
82720
82881
  nullable: false,
82721
82882
  companions: [{
@@ -82725,7 +82886,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82725
82886
  }]
82726
82887
  },
82727
82888
  {
82728
- id: "volts",
82889
+ id: "voltage",
82729
82890
  cardinality: "scalar",
82730
82891
  deviceTypes: [
82731
82892
  require_err_msg.DeviceType.Switch,
@@ -82745,7 +82906,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82745
82906
  ],
82746
82907
  apply: "add",
82747
82908
  vocabularyCap: "power-meter",
82748
- vocabularyStateField: "volts",
82909
+ vocabularyStateField: "voltage",
82749
82910
  sources: [{
82750
82911
  kind: "numeric",
82751
82912
  requiredUnit: "V",
@@ -82754,7 +82915,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82754
82915
  }],
82755
82916
  sourceReport: { status: "assignment-status" },
82756
82917
  advancedExpression: true,
82757
- path: "volts",
82918
+ path: "voltage",
82758
82919
  valueKind: "number",
82759
82920
  nullable: false,
82760
82921
  companions: [{
@@ -82764,7 +82925,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82764
82925
  }]
82765
82926
  },
82766
82927
  {
82767
- id: "amps",
82928
+ id: "current",
82768
82929
  cardinality: "scalar",
82769
82930
  deviceTypes: [
82770
82931
  require_err_msg.DeviceType.Switch,
@@ -82784,7 +82945,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82784
82945
  ],
82785
82946
  apply: "add",
82786
82947
  vocabularyCap: "power-meter",
82787
- vocabularyStateField: "amps",
82948
+ vocabularyStateField: "current",
82788
82949
  sources: [{
82789
82950
  kind: "numeric",
82790
82951
  requiredUnit: "A",
@@ -82793,7 +82954,7 @@ var CAP_CUSTOMIZE_CATALOG = {
82793
82954
  }],
82794
82955
  sourceReport: { status: "assignment-status" },
82795
82956
  advancedExpression: true,
82796
- path: "amps",
82957
+ path: "current",
82797
82958
  valueKind: "number",
82798
82959
  nullable: false,
82799
82960
  companions: [{
@@ -82894,19 +83055,19 @@ var CAP_CUSTOMIZE_CATALOG = {
82894
83055
  * it is not an override list (D944).
82895
83056
  */
82896
83057
  var CAP_RUNTIME_FIELD_UNITS = {
82897
- "ambient-light-sensor": { lux: "lx" },
83058
+ "ambient-light-sensor": { illuminance: "lx" },
82898
83059
  "battery": {
82899
- percentage: "%",
82900
- volts: "V"
83060
+ level: "%",
83061
+ voltage: "V"
82901
83062
  },
82902
- "humidity-sensor": { percent: "%" },
83063
+ "humidity-sensor": { humidity: "%" },
82903
83064
  "power-meter": {
82904
- watts: "W",
82905
- kwhTotal: "kWh",
82906
- volts: "V",
82907
- amps: "A"
83065
+ power: "W",
83066
+ energy: "kWh",
83067
+ voltage: "V",
83068
+ current: "A"
82908
83069
  },
82909
- "temperature-sensor": { celsius: "°C" }
83070
+ "temperature-sensor": { temperature: "°C" }
82910
83071
  };
82911
83072
  //#endregion
82912
83073
  //#region src/composition/customize/field-source.ts
@@ -85025,7 +85186,7 @@ var BaseDevice = class {
85025
85186
  * `device-local-state.ts`. Drivers access via:
85026
85187
  *
85027
85188
  * `this.state.battery.sleeping = true` // patches the battery slice
85028
- * `const pct = this.state.battery.percentage` // reads the battery slice
85189
+ * `const pct = this.state.battery.level` // reads the battery slice
85029
85190
  * `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
85030
85191
  *
85031
85192
  * Adding a new cap with `runtimeState:` automatically extends this
@@ -85065,7 +85226,7 @@ var BaseDevice = class {
85065
85226
  * shape; drivers don't redeclare it, they just write through.
85066
85227
  *
85067
85228
  * Read: `this.runtimeState.getCapState('battery')` →
85068
- * `{percentage, charging, sleeping, lastUpdated}` for any
85229
+ * `{level, charging, sleeping, lastUpdated}` for any
85069
85230
  * provider that registers `batteryCapability`.
85070
85231
  * Write: `this.runtimeState.setCapState('battery', { … })`.
85071
85232
  *
@@ -87030,17 +87191,17 @@ function pickAccessoryControl(device) {
87030
87191
  var SENSOR_MAP = {
87031
87192
  "temperature-sensor": {
87032
87193
  capKey: "temperatureSensor",
87033
- field: "celsius",
87194
+ field: "temperature",
87034
87195
  fallbackUnit: "°C"
87035
87196
  },
87036
87197
  "humidity-sensor": {
87037
87198
  capKey: "humiditySensor",
87038
- field: "percent",
87199
+ field: "humidity",
87039
87200
  fallbackUnit: "%"
87040
87201
  },
87041
87202
  "ambient-light-sensor": {
87042
87203
  capKey: "ambientLightSensor",
87043
- field: "lux",
87204
+ field: "illuminance",
87044
87205
  fallbackUnit: "lx"
87045
87206
  },
87046
87207
  "numeric-sensor": {
@@ -97498,7 +97659,7 @@ var ACTUATOR_CAP_VOCABULARY = {
97498
97659
  label: t("Full brightness", "Luminosità massima"),
97499
97660
  phrase: t("Set {device} to full brightness", "Porta {device} alla massima luminosità"),
97500
97661
  icon: "sun",
97501
- args: { percentage: 100 }
97662
+ args: { level: 100 }
97502
97663
  },
97503
97664
  {
97504
97665
  id: "brightness.half",
@@ -97506,7 +97667,7 @@ var ACTUATOR_CAP_VOCABULARY = {
97506
97667
  label: t("Half brightness", "Luminosità a metà"),
97507
97668
  phrase: t("Dim {device} to half", "Abbassa {device} a metà luminosità"),
97508
97669
  icon: "sun-dim",
97509
- args: { percentage: 50 }
97670
+ args: { level: 50 }
97510
97671
  },
97511
97672
  {
97512
97673
  id: "brightness.off",
@@ -97514,16 +97675,16 @@ var ACTUATOR_CAP_VOCABULARY = {
97514
97675
  label: t("Brightness off", "Luminosità a zero"),
97515
97676
  phrase: t("Dim {device} to zero", "Abbassa {device} a zero"),
97516
97677
  icon: "lightbulb-off",
97517
- args: { percentage: 0 }
97678
+ args: { level: 0 }
97518
97679
  },
97519
97680
  {
97520
97681
  id: "brightness.set",
97521
97682
  method: "setBrightness",
97522
97683
  label: t("Set brightness", "Imposta la luminosità"),
97523
- phrase: t("Set the brightness of {device} to {percentage}", "Imposta la luminosità di {device} a {percentage}"),
97684
+ phrase: t("Set the brightness of {device} to {level}", "Imposta la luminosità di {device} a {level}"),
97524
97685
  icon: "sun-medium",
97525
97686
  openArgs: [{
97526
- key: "percentage",
97687
+ key: "level",
97527
97688
  label: PERCENT,
97528
97689
  offer: {
97529
97690
  kind: "number",
@@ -97537,13 +97698,13 @@ var ACTUATOR_CAP_VOCABULARY = {
97537
97698
  ],
97538
97699
  state: [{
97539
97700
  kind: "numeric",
97540
- field: "percentage",
97701
+ field: "level",
97541
97702
  quantity: t("the brightness", "la luminosità"),
97542
97703
  label: t("Brightness", "Luminosità"),
97543
97704
  unit: "%",
97544
97705
  icon: "sun"
97545
97706
  }],
97546
- events: { onBrightnessChanged: { notOperatorFacing: "the `percentage` state trigger reads the same change" } }
97707
+ events: { onBrightnessChanged: { notOperatorFacing: "the `level` state trigger reads the same change" } }
97547
97708
  },
97548
97709
  color: {
97549
97710
  scope: "device",
@@ -97557,7 +97718,7 @@ var ACTUATOR_CAP_VOCABULARY = {
97557
97718
  icon: "sun",
97558
97719
  args: { color: {
97559
97720
  mode: "mired",
97560
- mireds: 370
97721
+ colorTemperature: 370
97561
97722
  } }
97562
97723
  },
97563
97724
  {
@@ -97568,7 +97729,7 @@ var ACTUATOR_CAP_VOCABULARY = {
97568
97729
  icon: "snowflake",
97569
97730
  args: { color: {
97570
97731
  mode: "mired",
97571
- mireds: 200
97732
+ colorTemperature: 200
97572
97733
  } }
97573
97734
  },
97574
97735
  {
@@ -98182,7 +98343,7 @@ var ACTUATOR_CAP_VOCABULARY = {
98182
98343
  label: t("Fan off", "Ventilatore spento"),
98183
98344
  phrase: t("Stop {device}", "Ferma {device}"),
98184
98345
  icon: "power-off",
98185
- args: { percentage: 0 }
98346
+ args: { speed: 0 }
98186
98347
  },
98187
98348
  {
98188
98349
  id: "fan.full",
@@ -98190,16 +98351,16 @@ var ACTUATOR_CAP_VOCABULARY = {
98190
98351
  label: t("Fan full", "Ventilatore al massimo"),
98191
98352
  phrase: t("Run {device} at full speed", "Porta {device} alla massima velocità"),
98192
98353
  icon: "fan",
98193
- args: { percentage: 100 }
98354
+ args: { speed: 100 }
98194
98355
  },
98195
98356
  {
98196
98357
  id: "fan.speed",
98197
98358
  method: "setPercentage",
98198
98359
  label: t("Set speed", "Imposta la velocità"),
98199
- phrase: t("Set the speed of {device} to {percentage}", "Imposta la velocità di {device} al {percentage}"),
98360
+ phrase: t("Set the speed of {device} to {speed}", "Imposta la velocità di {device} al {speed}"),
98200
98361
  icon: "fan",
98201
98362
  openArgs: [{
98202
- key: "percentage",
98363
+ key: "speed",
98203
98364
  label: t("Speed", "Velocità"),
98204
98365
  offer: {
98205
98366
  kind: "number",
@@ -98261,7 +98422,7 @@ var ACTUATOR_CAP_VOCABULARY = {
98261
98422
  ],
98262
98423
  state: [{
98263
98424
  kind: "numeric",
98264
- field: "percentage",
98425
+ field: "speed",
98265
98426
  quantity: t("the speed", "la velocità"),
98266
98427
  label: t("Speed", "Velocità"),
98267
98428
  unit: "%",
@@ -99061,7 +99222,7 @@ var APPLIANCE_CAP_VOCABULARY = {
99061
99222
  },
99062
99223
  {
99063
99224
  kind: "numeric",
99064
- field: "progressPercent",
99225
+ field: "progress",
99065
99226
  quantity: t("the cycle progress", "l’avanzamento del lavaggio"),
99066
99227
  label: t("Progress", "Avanzamento"),
99067
99228
  unit: "%",
@@ -99419,10 +99580,10 @@ var APPLIANCE_CAP_VOCABULARY = {
99419
99580
  id: "media.seek",
99420
99581
  method: "seek",
99421
99582
  label: t("Seek", "Vai al punto"),
99422
- phrase: t("Move {device} to {positionMs}", "Porta {device} al punto {positionMs}"),
99583
+ phrase: t("Move {device} to {position}", "Porta {device} al punto {position}"),
99423
99584
  icon: "fast-forward",
99424
99585
  openArgs: [{
99425
- key: "positionMs",
99586
+ key: "position",
99426
99587
  label: t("Position", "Posizione"),
99427
99588
  offer: {
99428
99589
  kind: "number",
@@ -100475,7 +100636,7 @@ var SENSOR_CAP_VOCABULARY = {
100475
100636
  state: [
100476
100637
  {
100477
100638
  kind: "numeric",
100478
- field: "co2Ppm",
100639
+ field: "co2",
100479
100640
  quantity: t("the CO₂", "la CO₂"),
100480
100641
  label: t("CO₂", "CO₂"),
100481
100642
  unit: "ppm",
@@ -100483,7 +100644,7 @@ var SENSOR_CAP_VOCABULARY = {
100483
100644
  },
100484
100645
  {
100485
100646
  kind: "numeric",
100486
- field: "vocPpb",
100647
+ field: "voc",
100487
100648
  quantity: t("the VOC level", "il livello di COV"),
100488
100649
  label: t("VOC", "COV"),
100489
100650
  unit: "ppb",
@@ -100519,7 +100680,7 @@ var SENSOR_CAP_VOCABULARY = {
100519
100680
  noun: t("Light level", "Luminosità ambiente"),
100520
100681
  state: [{
100521
100682
  kind: "numeric",
100522
- field: "lux",
100683
+ field: "illuminance",
100523
100684
  quantity: t("the light level", "la luminosità"),
100524
100685
  label: t("Illuminance", "Illuminamento"),
100525
100686
  unit: "lx",
@@ -100722,7 +100883,7 @@ var SENSOR_CAP_VOCABULARY = {
100722
100883
  noun: t("Humidity", "Umidità"),
100723
100884
  state: [{
100724
100885
  kind: "numeric",
100725
- field: "percent",
100886
+ field: "humidity",
100726
100887
  quantity: t("the humidity", "l’umidità"),
100727
100888
  label: t("Humidity", "Umidità"),
100728
100889
  unit: "%",
@@ -100770,7 +100931,7 @@ var SENSOR_CAP_VOCABULARY = {
100770
100931
  state: [
100771
100932
  {
100772
100933
  kind: "numeric",
100773
- field: "watts",
100934
+ field: "power",
100774
100935
  quantity: t("the power draw", "il consumo"),
100775
100936
  label: t("Power", "Potenza"),
100776
100937
  unit: "W",
@@ -100778,7 +100939,7 @@ var SENSOR_CAP_VOCABULARY = {
100778
100939
  },
100779
100940
  {
100780
100941
  kind: "numeric",
100781
- field: "kwhTotal",
100942
+ field: "energy",
100782
100943
  quantity: t("the energy used", "l’energia consumata"),
100783
100944
  label: t("Energy", "Energia"),
100784
100945
  unit: "kWh",
@@ -100786,7 +100947,7 @@ var SENSOR_CAP_VOCABULARY = {
100786
100947
  },
100787
100948
  {
100788
100949
  kind: "numeric",
100789
- field: "volts",
100950
+ field: "voltage",
100790
100951
  quantity: t("the voltage", "la tensione"),
100791
100952
  label: t("Voltage", "Tensione"),
100792
100953
  unit: "V",
@@ -100794,7 +100955,7 @@ var SENSOR_CAP_VOCABULARY = {
100794
100955
  },
100795
100956
  {
100796
100957
  kind: "numeric",
100797
- field: "amps",
100958
+ field: "current",
100798
100959
  quantity: t("the current", "la corrente"),
100799
100960
  label: t("Current", "Corrente"),
100800
100961
  unit: "A",
@@ -100811,13 +100972,6 @@ var SENSOR_CAP_VOCABULARY = {
100811
100972
  label: t("Where", "Posizione"),
100812
100973
  phrase: t("When {device} is at {value}", "Quando {device} è in {value}"),
100813
100974
  icon: "map-pin"
100814
- }, {
100815
- kind: "numeric",
100816
- field: "batteryPercent",
100817
- quantity: t("the phone battery", "la batteria del telefono"),
100818
- label: t("Battery", "Batteria"),
100819
- unit: "%",
100820
- icon: "battery"
100821
100975
  }]
100822
100976
  },
100823
100977
  "pressure-sensor": {
@@ -100825,7 +100979,7 @@ var SENSOR_CAP_VOCABULARY = {
100825
100979
  noun: t("Pressure", "Pressione"),
100826
100980
  state: [{
100827
100981
  kind: "numeric",
100828
- field: "hpa",
100982
+ field: "pressure",
100829
100983
  quantity: t("the pressure", "la pressione"),
100830
100984
  label: t("Pressure", "Pressione"),
100831
100985
  unit: "hPa",
@@ -100861,7 +101015,7 @@ var SENSOR_CAP_VOCABULARY = {
100861
101015
  noun: t("Temperature", "Temperatura"),
100862
101016
  state: [{
100863
101017
  kind: "numeric",
100864
- field: "celsius",
101018
+ field: "temperature",
100865
101019
  quantity: t("the temperature", "la temperatura"),
100866
101020
  label: t("Temperature", "Temperatura"),
100867
101021
  unit: "°C",
@@ -100932,7 +101086,7 @@ var SENSOR_CAP_VOCABULARY = {
100932
101086
  state: [
100933
101087
  {
100934
101088
  kind: "numeric",
100935
- field: "percentage",
101089
+ field: "level",
100936
101090
  quantity: t("the battery level", "il livello della batteria"),
100937
101091
  label: t("Battery", "Batteria"),
100938
101092
  unit: "%",
@@ -100974,7 +101128,7 @@ var SENSOR_CAP_VOCABULARY = {
100974
101128
  icon: "battery-charging"
100975
101129
  }
100976
101130
  ],
100977
- events: { onStatusChanged: { notOperatorFacing: "the percentage / sleeping / charging state triggers each read one field of this change" } },
101131
+ events: { onStatusChanged: { notOperatorFacing: "the level / sleeping / charging state triggers each read one field of this change" } },
100978
101132
  extraEvents: [{
100979
101133
  category: require_event_category.EventCategory.DeviceAwake,
100980
101134
  label: t("Woke up", "Risvegliato"),
@@ -100992,7 +101146,7 @@ var SENSOR_CAP_VOCABULARY = {
100992
101146
  noun: t("Network link", "Collegamento di rete"),
100993
101147
  state: [{
100994
101148
  kind: "numeric",
100995
- field: "signalPercent",
101149
+ field: "signal",
100996
101150
  quantity: t("the signal strength", "l’intensità del segnale"),
100997
101151
  label: t("Signal", "Segnale"),
100998
101152
  unit: "%",
@@ -121821,6 +121975,7 @@ exports.evaluateRose = require_composition.evaluateRose;
121821
121975
  exports.evaluateSensorEdge = require_composition.evaluateSensorEdge;
121822
121976
  exports.evaluateSensorLevelEdge = require_composition.evaluateSensorLevelEdge;
121823
121977
  exports.evaluateStatefulExpressionCall = require_composition.evaluateStatefulExpressionCall;
121978
+ exports.evaluateStationaryTrigger = evaluateStationaryTrigger;
121824
121979
  exports.evaluateStatistic = require_composition.evaluateStatistic;
121825
121980
  exports.evaluateZoneRules = evaluateZoneRules;
121826
121981
  exports.event = require_sleep.event;