@camstack/system 1.2.456 → 1.2.457

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 (117) hide show
  1. package/dist/addon-runner.js +96 -10
  2. package/dist/addon-runner.mjs +96 -10
  3. package/dist/{agent-https-fm4AR8XL.mjs → agent-https-CK9Z4sSR.mjs} +2 -2
  4. package/dist/{agent-https-yjxdfJDA.js → agent-https-CoyOZ3pC.js} +2 -2
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +2 -2
  10. package/dist/builtins/alerts/alerts.addon.mjs +2 -2
  11. package/dist/builtins/autotrack/index.js +1 -1
  12. package/dist/builtins/autotrack/index.mjs +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  15. package/dist/builtins/calendars/calendars.addon.js +1 -1
  16. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  17. package/dist/builtins/camera-grid/index.js +1 -1
  18. package/dist/builtins/camera-grid/index.mjs +1 -1
  19. package/dist/builtins/composer/composer.addon.js +2 -2
  20. package/dist/builtins/composer/composer.addon.mjs +2 -2
  21. package/dist/builtins/console-logging/console-destination.d.ts +1 -2
  22. package/dist/builtins/console-logging/index.js +1 -4
  23. package/dist/builtins/console-logging/index.mjs +1 -4
  24. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  25. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  26. package/dist/builtins/device-history/capture/capture-engine.d.ts +6 -0
  27. package/dist/builtins/device-history/device-history.addon.js +441 -83
  28. package/dist/builtins/device-history/device-history.addon.mjs +441 -83
  29. package/dist/builtins/device-history/query/history-query.d.ts +35 -9
  30. package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
  31. package/dist/builtins/device-history/retention/retention.d.ts +1 -1
  32. package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
  33. package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
  34. package/dist/builtins/device-history/store/history-store.d.ts +7 -0
  35. package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
  36. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  37. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  38. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
  39. package/dist/builtins/hub-forwarder/index.js +1 -4
  40. package/dist/builtins/hub-forwarder/index.mjs +1 -4
  41. package/dist/builtins/keychain/index.js +4 -4
  42. package/dist/builtins/keychain/index.mjs +4 -4
  43. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  44. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  45. package/dist/builtins/local-auth/local-auth.addon.js +3 -3
  46. package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
  47. package/dist/builtins/local-network/local-network.addon.js +2 -2
  48. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  49. package/dist/builtins/loki-logging/index.js +323 -7
  50. package/dist/builtins/loki-logging/index.mjs +323 -7
  51. package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
  52. package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
  53. package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
  54. package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
  55. package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
  56. package/dist/builtins/platform-probe/index.js +1 -1
  57. package/dist/builtins/platform-probe/index.mjs +1 -1
  58. package/dist/builtins/record-entries/index.js +1 -1
  59. package/dist/builtins/record-entries/index.mjs +1 -1
  60. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  61. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/snapshot/index.js +1 -1
  63. package/dist/builtins/snapshot/index.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  65. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  66. package/dist/builtins/sqlite-storage/index.js +1 -1
  67. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  68. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  69. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  70. package/dist/builtins/storage-orchestrator/location-history-recorder.d.ts +76 -0
  71. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
  72. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
  73. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -1
  74. package/dist/builtins/system-config/system-config.addon.js +3 -3
  75. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  76. package/dist/builtins/winston-logging/index.js +165 -3
  77. package/dist/builtins/winston-logging/index.mjs +165 -3
  78. package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
  79. package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
  80. package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
  81. package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
  82. package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
  83. package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
  84. package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
  85. package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
  86. package/dist/{composition-sources-Ds0Vr6eD.mjs → composition-sources-B_jeZDRJ.mjs} +1 -1
  87. package/dist/{composition-sources-gB4coDJR.js → composition-sources-qKyvMAvc.js} +1 -1
  88. package/dist/{constants-CavyKfHZ.js → constants-B_X9cWEK.js} +1 -1
  89. package/dist/{constants-i5oD_gLB.mjs → constants-cxKcZRMi.mjs} +1 -1
  90. package/dist/{device-ca-qz1Pmeu1.mjs → device-ca-BB6nHAL5.mjs} +1 -1
  91. package/dist/{device-ca-bpoEr0zG.js → device-ca-CVK3YcR_.js} +1 -1
  92. package/dist/{dist-BpqA2CT0.js → dist-C89nqNnn.js} +471 -47
  93. package/dist/{dist-BSF_C8W2.mjs → dist-Dhux90yB.mjs} +454 -48
  94. package/dist/file-host-worker.js +1 -1
  95. package/dist/file-host-worker.mjs +1 -1
  96. package/dist/{hub-mdns-ciao-CHaGDzyz.mjs → hub-mdns-ciao-B8olZqqd.mjs} +2 -2
  97. package/dist/{hub-mdns-ciao-CZlZRxxA.js → hub-mdns-ciao-DB1Y9agt.js} +2 -2
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.js +135 -20
  100. package/dist/index.mjs +129 -21
  101. package/dist/kernel/config-schema.d.ts +5 -0
  102. package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
  103. package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
  104. package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
  105. package/dist/log-file-read-worker.js +64 -0
  106. package/dist/log-file-read-worker.mjs +64 -0
  107. package/dist/log-read-filter-Bi066jwU.js +134 -0
  108. package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
  109. package/dist/logging/log-manager.d.ts +10 -6
  110. package/dist/logging/log-read-filter.d.ts +43 -0
  111. package/dist/{phase6-declare-gate-n-IZRklX.js → phase6-declare-gate-BO-_jLh_.js} +1 -1
  112. package/dist/{phase6-declare-gate-B4yfsf2n.mjs → phase6-declare-gate-x9ZCbQJP.mjs} +1 -1
  113. package/dist/{restore-archive-B4QG7XmX.js → restore-archive-4DOUR5Gu.js} +1 -1
  114. package/dist/{restore-archive-BGUM4FKf.mjs → restore-archive-DyOaPeAr.mjs} +1 -1
  115. package/dist/{wal-maintenance-AerF4XE6.mjs → wal-maintenance-BMiX6UIa.mjs} +0 -0
  116. package/dist/{wal-maintenance-CNEs_I94.js → wal-maintenance-DVN-DiVP.js} +0 -0
  117. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BpqA2CT0.js");
6
+ const require_dist = require("../../dist-C89nqNnn.js");
7
7
  //#region src/builtins/device-history/store/series-registry.ts
8
8
  function subjectIdOf(subject) {
9
9
  return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
@@ -11,16 +11,29 @@ function subjectIdOf(subject) {
11
11
  function deviceIdOf(subject) {
12
12
  return subject.kind === "device" ? subject.deviceId : null;
13
13
  }
14
+ /** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
15
+ function isMetricKey(key) {
16
+ return key.startsWith("cap:") || key.startsWith("addon:");
17
+ }
14
18
  function identityOf(key, subjectId, scope) {
15
19
  return `${key}\u0000${subjectId}\u0000${scope}`;
16
20
  }
17
21
  var LOAD_PAGE = 5e3;
22
+ /** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
23
+ function mergeEnumValues(stored, next) {
24
+ if (stored === null) return next;
25
+ if (next === null) return stored;
26
+ const missing = next.filter((v) => !stored.includes(v));
27
+ return missing.length === 0 ? stored : [...stored, ...missing];
28
+ }
18
29
  var SeriesRegistry = class {
19
30
  store;
20
31
  metrics = /* @__PURE__ */ new Map();
21
32
  byIdentity = /* @__PURE__ */ new Map();
22
33
  byId = /* @__PURE__ */ new Map();
23
34
  inflight = /* @__PURE__ */ new Map();
35
+ /** Enum metrics whose value list grew in memory and is not stored yet. */
36
+ dirtyMetrics = /* @__PURE__ */ new Set();
24
37
  removeListeners = [];
25
38
  constructor(store) {
26
39
  this.store = store;
@@ -29,7 +42,10 @@ var SeriesRegistry = class {
29
42
  this.metrics.clear();
30
43
  this.byIdentity.clear();
31
44
  this.byId.clear();
32
- for (const m of await this.store.loadMetrics()) this.metrics.set(m.key, m);
45
+ const stray = [];
46
+ for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
47
+ else stray.push(m.key);
48
+ if (stray.length > 0) await this.store.deleteMetrics(stray);
33
49
  let afterId = 0;
34
50
  for (;;) {
35
51
  const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
@@ -49,12 +65,48 @@ var SeriesRegistry = class {
49
65
  allMetrics() {
50
66
  return [...this.metrics.values()];
51
67
  }
52
- /** Persist and remember a metric; idempotent. */
68
+ /**
69
+ * Persist and remember a metric; idempotent. An `enum` value list is MERGED
70
+ * append-only with the stored one, so reordering a `z.enum` never changes
71
+ * what a stored ordinal means.
72
+ */
53
73
  async declareMetric(metric) {
54
74
  const existing = this.metrics.get(metric.key);
55
- if (existing !== void 0 && sameMetric(existing, metric)) return;
56
- await this.store.upsertMetric(metric);
57
- this.metrics.set(metric.key, metric);
75
+ const merged = {
76
+ ...metric,
77
+ values: mergeEnumValues(existing?.values ?? null, metric.values)
78
+ };
79
+ if (existing !== void 0 && sameMetric(existing, merged)) return;
80
+ await this.store.upsertMetric(merged);
81
+ this.metrics.set(metric.key, merged);
82
+ }
83
+ /**
84
+ * The ordinal of `value` in an `enum` metric's list, learning (appending) a
85
+ * value seen for the first time. `null` when the key is not an enum metric or
86
+ * its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
87
+ * {@link persistDirtyMetrics} BEFORE any point that uses it.
88
+ */
89
+ ordinalOf(key, value) {
90
+ const metric = this.metrics.get(key);
91
+ if (metric === void 0 || metric.kind !== "enum") return null;
92
+ const values = metric.values ?? [];
93
+ const known = values.indexOf(value);
94
+ if (known >= 0) return known;
95
+ if (values.length >= 1024) return null;
96
+ this.metrics.set(key, {
97
+ ...metric,
98
+ values: [...values, value]
99
+ });
100
+ this.dirtyMetrics.add(key);
101
+ return values.length;
102
+ }
103
+ /** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
104
+ async persistDirtyMetrics() {
105
+ for (const key of this.dirtyMetrics) {
106
+ const metric = this.metrics.get(key);
107
+ if (metric !== void 0) await this.store.upsertMetric(metric);
108
+ this.dirtyMetrics.delete(key);
109
+ }
58
110
  }
59
111
  find(key, subjectId, scope) {
60
112
  return this.byIdentity.get(identityOf(key, subjectId, scope));
@@ -119,7 +171,7 @@ var SeriesRegistry = class {
119
171
  }
120
172
  };
121
173
  function sameMetric(a, b) {
122
- return a.source === b.source && a.ownerAddon === b.ownerAddon && a.subjectKind === b.subjectKind && a.kind === b.kind && a.unit === b.unit && a.displayName === b.displayName && a.ingest === b.ingest;
174
+ return a.source === b.source && a.ownerAddon === b.ownerAddon && a.subjectKind === b.subjectKind && a.kind === b.kind && a.unit === b.unit && a.displayName === b.displayName && a.ingest === b.ingest && JSON.stringify(a.values) === JSON.stringify(b.values);
123
175
  }
124
176
  //#endregion
125
177
  //#region src/builtins/device-history/capture/capture-filter.ts
@@ -143,10 +195,10 @@ var DEADBAND_EPSILON = 1e-9;
143
195
  /** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
144
196
  function minIntervalOf(spec) {
145
197
  if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
146
- return spec.kind === "boolean" ? 0 : require_dist.HISTORY_DEFAULT_MIN_INTERVAL_MS;
198
+ return spec.kind === "boolean" || spec.kind === "enum" ? 0 : require_dist.HISTORY_DEFAULT_MIN_INTERVAL_MS;
147
199
  }
148
200
  function clearsDeadband(spec, last, value) {
149
- if (spec.kind === "boolean") return true;
201
+ if (spec.kind === "boolean" || spec.kind === "enum") return true;
150
202
  const delta = Math.abs(value - last);
151
203
  if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
152
204
  if (spec.deadbandPct !== void 0) {
@@ -331,7 +383,8 @@ function catalogMetrics(catalog, now) {
331
383
  unit: spec.unit ?? null,
332
384
  displayName: spec.displayName ?? humanize(capName, path),
333
385
  ingest: "points",
334
- declaredAt: now
386
+ declaredAt: now,
387
+ values: spec.kind === "enum" ? spec.values ?? [] : null
335
388
  })));
336
389
  }
337
390
  /** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
@@ -380,11 +433,28 @@ var CaptureEngine = class {
380
433
  counters.skip("excluded-device-type");
381
434
  continue;
382
435
  }
383
- const value = readNumeric(slice, path, spec.kind);
436
+ const value = this.readValue(slice, capName, path, spec);
384
437
  if (value === null) continue;
385
438
  this.consider(deviceId, capName, path, spec, value, at, false);
386
439
  }
387
440
  }
441
+ /**
442
+ * The number a field contributes, or `null` (not a reading — never 0). An
443
+ * `enum` state becomes the ordinal of its value in the metric's list; a value
444
+ * the full list cannot take is refused and counted (`enum-overflow`).
445
+ */
446
+ readValue(slice, capName, path, spec) {
447
+ if (spec.skipWhenSet !== void 0) {
448
+ const gate = readPath(slice, spec.skipWhenSet);
449
+ if (gate !== void 0 && gate !== null) return null;
450
+ }
451
+ if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
452
+ const raw = readPath(slice, path);
453
+ if (typeof raw !== "string" || raw.length === 0) return null;
454
+ const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
455
+ if (ordinal === null) this.deps.counters.skip("enum-overflow");
456
+ return ordinal;
457
+ }
388
458
  consider(deviceId, capName, path, spec, value, at, reconciling) {
389
459
  const key = capSeriesKey(capName, path);
390
460
  const subject = {
@@ -490,7 +560,7 @@ var CaptureEngine = class {
490
560
  if (slice === void 0) continue;
491
561
  for (const [path, spec] of Object.entries(fields)) {
492
562
  if (spec.excludeDeviceTypes?.includes(type) === true) continue;
493
- const value = readNumeric(slice, path, spec.kind);
563
+ const value = this.readValue(slice, capName, path, spec);
494
564
  if (value === null) continue;
495
565
  this.consider(deviceId, capName, path, spec, value, now, true);
496
566
  }
@@ -573,6 +643,7 @@ var PointWriter = class {
573
643
  this.queue = [];
574
644
  const { registry, store, counters, logger, clock } = this.deps;
575
645
  try {
646
+ await registry.persistDirtyMetrics();
576
647
  const bySeries = /* @__PURE__ */ new Map();
577
648
  for (const point of batch) {
578
649
  const series = await registry.ensure(point.key, point.subject, point.scope, point.ts);
@@ -748,7 +819,8 @@ var ProducerIngest = class {
748
819
  unit: input.unit ?? null,
749
820
  displayName: input.displayName,
750
821
  ingest: input.ingest,
751
- declaredAt: this.deps.clock()
822
+ declaredAt: this.deps.clock(),
823
+ values: null
752
824
  };
753
825
  await this.deps.registry.declareMetric(metric);
754
826
  return { key };
@@ -835,7 +907,8 @@ var ProducerIngest = class {
835
907
  last: bucket.last,
836
908
  lastTs: bucket.lastTs,
837
909
  twSum: null,
838
- twMs: null
910
+ twMs: null,
911
+ states: null
839
912
  });
840
913
  }
841
914
  await store.upsertRollups("5m", rows);
@@ -874,7 +947,109 @@ var ProducerIngest = class {
874
947
  }
875
948
  };
876
949
  //#endregion
950
+ //#region src/builtins/device-history/query/point-shape.ts
951
+ /** kWh in one W·ms: 1 kWh = 1 000 W × 3 600 000 ms. */
952
+ var KWH_PER_WATT_MS = 1 / 36e8;
953
+ /** Growth from `prev` to `cur`; a drop is a reset counted from zero. `fallback` when `prev` is unknown. */
954
+ function counterDelta(prev, cur, fallback) {
955
+ if (cur === null) return null;
956
+ if (prev === null) return fallback;
957
+ return cur >= prev ? cur - prev : cur;
958
+ }
959
+ /** The name of an ordinal, or `null` when the series is not an enum or the ordinal is unknown. */
960
+ function stateName(meaning, ordinal) {
961
+ if (meaning.kind !== "enum" || meaning.values === null || ordinal === null) return null;
962
+ return meaning.values[ordinal] ?? null;
963
+ }
964
+ function namedStates(meaning, states) {
965
+ if (meaning.kind !== "enum" || states === null) return null;
966
+ const out = {};
967
+ for (const [ordinal, ms] of Object.entries(states)) {
968
+ const name = stateName(meaning, Number(ordinal));
969
+ if (name !== null) out[name] = (out[name] ?? 0) + ms;
970
+ }
971
+ return out;
972
+ }
973
+ /** Raw points, oldest first; `prev` is the value of the point before the first one. */
974
+ function rawPoints(rows, meaning, prev) {
975
+ const isEnum = meaning.kind === "enum";
976
+ let held = prev;
977
+ return rows.map((r) => {
978
+ const delta = meaning.kind === "counter" ? counterDelta(held, r.value, null) : null;
979
+ held = r.value;
980
+ return {
981
+ t: r.ts,
982
+ samples: 1,
983
+ mean: isEnum ? null : r.value,
984
+ twMean: null,
985
+ min: r.value,
986
+ max: r.value,
987
+ last: r.value,
988
+ delta,
989
+ state: stateName(meaning, r.value),
990
+ states: null
991
+ };
992
+ });
993
+ }
994
+ /** Rollup rows, oldest first; `prevLast` is the `last` of the bucket before the first one. */
995
+ function rollupPoints(rows, meaning, prevLast) {
996
+ const isEnum = meaning.kind === "enum";
997
+ let held = prevLast;
998
+ return rows.map((row) => {
999
+ const delta = meaning.kind === "counter" ? counterDelta(held, row.last, row.last === null ? null : row.last - row.min) : null;
1000
+ if (row.last !== null) held = row.last;
1001
+ return {
1002
+ t: row.bucketStart,
1003
+ samples: row.samples,
1004
+ mean: isEnum || row.samples <= 0 ? null : row.sum / row.samples,
1005
+ twMean: !isEnum && row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
1006
+ min: row.min,
1007
+ max: row.max,
1008
+ last: row.last,
1009
+ delta,
1010
+ state: stateName(meaning, row.last),
1011
+ states: namedStates(meaning, row.states)
1012
+ };
1013
+ });
1014
+ }
1015
+ /** Energy used in each bucket of a `watts` gauge: its time-weighted integral, in kWh. */
1016
+ function integratedEnergyPoints(rows) {
1017
+ return rows.map((row) => ({
1018
+ t: row.bucketStart,
1019
+ samples: row.samples,
1020
+ mean: null,
1021
+ twMean: null,
1022
+ min: row.min,
1023
+ max: row.max,
1024
+ last: null,
1025
+ delta: row.twSum === null ? null : row.twSum * KWH_PER_WATT_MS,
1026
+ state: null,
1027
+ states: null
1028
+ }));
1029
+ }
1030
+ //#endregion
877
1031
  //#region src/builtins/device-history/query/history-query.ts
1032
+ /**
1033
+ * Reads: `listSeries`, `query` and `queryMany` (plan §5.1).
1034
+ *
1035
+ * `auto` resolution picks the finest tier whose retention covers `from` and
1036
+ * whose point count fits `maxPoints`: a rollup tier fits when
1037
+ * `ceil(range / bucket) <= maxPoints`, raw fits when a read of `maxPoints + 1`
1038
+ * rows comes back short. The coarsest covering tier is the last resort and is
1039
+ * truncated to its newest `maxPoints` points (`truncated: true`) rather than
1040
+ * answering nothing. A series fed by closed buckets has NO raw rows, so raw is
1041
+ * never a candidate for it: an empty raw read "fits" every window.
1042
+ *
1043
+ * `before` is the last point strictly before the window in the chosen tier, so a
1044
+ * step chart can start at the value in force when the window opens — a quiet
1045
+ * bucket has no row, and the held value is otherwise invisible.
1046
+ *
1047
+ * {@link DEVICE_HISTORY_ENERGY_KEY} is computed here at read time, never stored:
1048
+ * the device's `kwhTotal` meter when it has one (per-bucket growth), else the
1049
+ * time-weighted integral of its `watts` (`tw_sum`, already in every rollup).
1050
+ * One place makes that choice, so two charts of one device cannot disagree, and
1051
+ * it covers every hour of history recorded before the key existed.
1052
+ */
878
1053
  var TIER_ORDER = [
879
1054
  "raw",
880
1055
  "5m",
@@ -888,33 +1063,25 @@ var BUCKET_MS = {
888
1063
  };
889
1064
  /** A key with more scopes than this is cut (a zone x class matrix, not a chart). */
890
1065
  var MAX_SCOPES = 50;
1066
+ /** The power-meter series the energy key is derived from, meter first. */
1067
+ var ENERGY_METER_KEY = capSeriesKey("power-meter", "kwhTotal");
1068
+ var ENERGY_POWER_KEY = capSeriesKey("power-meter", "watts");
891
1069
  function retentionMsOf(tier, policy) {
892
1070
  if (tier === "raw") return policy.rawDays * DAY_MS;
893
1071
  if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
894
1072
  if (tier === "1h") return policy.hourDays * DAY_MS;
895
1073
  return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
896
1074
  }
897
- function rawPoint(ts, value) {
1075
+ /** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
1076
+ function bounded(newestFirst, maxPoints) {
1077
+ const overflow = newestFirst.length > maxPoints;
898
1078
  return {
899
- t: ts,
900
- samples: 1,
901
- mean: value,
902
- twMean: null,
903
- min: value,
904
- max: value,
905
- last: value
1079
+ rows: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
1080
+ overflow
906
1081
  };
907
1082
  }
908
- function rollupPoint(row) {
909
- return {
910
- t: row.bucketStart,
911
- samples: row.samples,
912
- mean: row.samples > 0 ? row.sum / row.samples : null,
913
- twMean: row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
914
- min: row.min,
915
- max: row.max,
916
- last: row.last
917
- };
1083
+ function errorText(err) {
1084
+ return err instanceof Error ? err.message : String(err);
918
1085
  }
919
1086
  var HistoryQuery = class {
920
1087
  deps;
@@ -922,12 +1089,75 @@ var HistoryQuery = class {
922
1089
  this.deps = deps;
923
1090
  }
924
1091
  async query(input) {
1092
+ if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
925
1093
  const { registry } = this.deps;
926
- const subjectId = subjectIdOf(input.subject);
927
- const wanted = registry.forKeyAndSubject(input.key, subjectId).filter((s) => input.scope === void 0 || s.scope === input.scope).toSorted((a, b) => a.scope.localeCompare(b.scope)).slice(0, MAX_SCOPES);
928
- const candidates = this.coveringTiers(input.from);
1094
+ const wanted = this.seriesOf(input);
1095
+ const metric = registry.metric(input.key);
1096
+ const meaning = {
1097
+ kind: metric?.kind ?? "gauge",
1098
+ values: metric?.values ?? null
1099
+ };
1100
+ const { chosen, reads } = await this.choose(input, wanted, metric?.ingest === "buckets");
1101
+ const series = [];
1102
+ for (const [s, read] of reads) {
1103
+ const before = await this.before(chosen, s, input.from, meaning);
1104
+ const points = read.tier === "raw" ? rawPoints(read.rows, meaning, before?.value ?? null) : rollupPoints(read.rows, meaning, before?.value ?? null);
1105
+ series.push({
1106
+ scope: s.scope,
1107
+ before,
1108
+ points,
1109
+ derivedFrom: null
1110
+ });
1111
+ }
1112
+ return {
1113
+ resolution: chosen,
1114
+ series,
1115
+ truncated: reads.some(([, r]) => r.overflow)
1116
+ };
1117
+ }
1118
+ /** Several reads over one window; one failing item answers its error, not the batch's. */
1119
+ async queryMany(input) {
1120
+ const out = [];
1121
+ for (const item of input.items) {
1122
+ const subject = "deviceId" in item ? {
1123
+ kind: "device",
1124
+ deviceId: item.deviceId
1125
+ } : {
1126
+ kind: "node",
1127
+ nodeId: item.forNodeId
1128
+ };
1129
+ try {
1130
+ const result = await this.query({
1131
+ key: item.key,
1132
+ subject,
1133
+ ...item.scope !== void 0 ? { scope: item.scope } : {},
1134
+ from: input.from,
1135
+ to: input.to,
1136
+ resolution: input.resolution,
1137
+ maxPoints: input.maxPoints
1138
+ });
1139
+ out.push({
1140
+ ok: true,
1141
+ result
1142
+ });
1143
+ } catch (err) {
1144
+ out.push({
1145
+ ok: false,
1146
+ error: errorText(err)
1147
+ });
1148
+ }
1149
+ }
1150
+ return out;
1151
+ }
1152
+ seriesOf(input) {
1153
+ return this.deps.registry.forKeyAndSubject(input.key, subjectIdOf(input.subject)).filter((s) => input.scope === void 0 || s.scope === input.scope).toSorted((a, b) => a.scope.localeCompare(b.scope)).slice(0, MAX_SCOPES);
1154
+ }
1155
+ /** The tier to answer from and its reads. `noRaw`: the series has no raw rows to read. */
1156
+ async choose(input, wanted, noRaw) {
1157
+ const covering = this.coveringTiers(input.from);
1158
+ const candidates = noRaw ? withoutRaw(covering) : covering;
929
1159
  const forced = input.resolution === "auto" ? null : input.resolution;
930
- const order = forced === null ? candidates : [forced];
1160
+ const order = forced === null ? candidates : [noRaw && forced === "raw" ? "5m" : forced];
931
1161
  let chosen = order[order.length - 1] ?? "1d";
932
1162
  let reads = [];
933
1163
  for (const [index, tier] of order.entries()) {
@@ -938,16 +1168,9 @@ var HistoryQuery = class {
938
1168
  if (reads.some(([, r]) => r.overflow) && !last) continue;
939
1169
  break;
940
1170
  }
941
- const series = [];
942
- for (const [s, read] of reads) series.push({
943
- scope: s.scope,
944
- before: await this.before(chosen, s, input.from),
945
- points: read.points
946
- });
947
1171
  return {
948
- resolution: chosen,
949
- series,
950
- truncated: reads.some(([, r]) => r.overflow)
1172
+ chosen,
1173
+ reads
951
1174
  };
952
1175
  }
953
1176
  /** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
@@ -969,35 +1192,79 @@ var HistoryQuery = class {
969
1192
  async readTier(tier, series, input) {
970
1193
  const { store } = this.deps;
971
1194
  const limit = input.maxPoints + 1;
972
- if (tier === "raw") {
973
- const rows = await store.readRaw(series.id, input.from, input.to, limit, "desc");
974
- return this.shape(rows.map((r) => rawPoint(r.ts, r.value)), input.maxPoints);
975
- }
1195
+ if (tier === "raw") return {
1196
+ tier,
1197
+ ...bounded(await store.readRaw(series.id, input.from, input.to, limit, "desc"), input.maxPoints)
1198
+ };
976
1199
  const lower = bucketStartOf(tier, input.from);
977
- const rows = await store.readRollups(tier, series.id, lower, input.to, limit, "desc");
978
- return this.shape(rows.map(rollupPoint), input.maxPoints);
979
- }
980
- /** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
981
- shape(newestFirst, maxPoints) {
982
- const overflow = newestFirst.length > maxPoints;
983
1200
  return {
984
- points: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
985
- overflow
1201
+ tier,
1202
+ ...bounded(await store.readRollups(tier, series.id, lower, input.to, limit, "desc"), input.maxPoints)
986
1203
  };
987
1204
  }
988
- async before(tier, series, from) {
1205
+ async before(tier, series, from, meaning) {
989
1206
  const { store } = this.deps;
990
1207
  if (tier === "raw") {
991
1208
  const p = await store.rawBefore(series.id, from);
992
1209
  return p === null ? null : {
993
1210
  t: p.ts,
994
- value: p.value
1211
+ value: p.value,
1212
+ state: stateName(meaning, p.value)
995
1213
  };
996
1214
  }
997
1215
  const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
998
1216
  return row === null ? null : {
999
1217
  t: row.bucketStart,
1000
- value: row.last
1218
+ value: row.last,
1219
+ state: stateName(meaning, row.last)
1220
+ };
1221
+ }
1222
+ /** The series energy is derived from for a device: the meter when it has one, else its power. */
1223
+ energySource(subject) {
1224
+ if (subject.kind !== "device") return null;
1225
+ const subjectId = subjectIdOf(subject);
1226
+ const meter = this.deps.registry.find(ENERGY_METER_KEY, subjectId, "");
1227
+ if (meter !== void 0) return {
1228
+ series: meter,
1229
+ from: "meter"
1230
+ };
1231
+ const power = this.deps.registry.find(ENERGY_POWER_KEY, subjectId, "");
1232
+ return power === void 0 ? null : {
1233
+ series: power,
1234
+ from: "power"
1235
+ };
1236
+ }
1237
+ async queryEnergy(input) {
1238
+ const source = this.energySource(input.subject);
1239
+ if (source === null || input.scope !== void 0 && input.scope !== "") return {
1240
+ resolution: "5m",
1241
+ series: [],
1242
+ truncated: false
1243
+ };
1244
+ const { chosen, reads } = await this.choose(input, [source.series], true);
1245
+ const read = reads[0]?.[1];
1246
+ if (read === void 0 || read.tier === "raw") return {
1247
+ resolution: chosen,
1248
+ series: [],
1249
+ truncated: false
1250
+ };
1251
+ let points;
1252
+ if (source.from === "meter") {
1253
+ const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, bucketStartOf(read.tier, input.from));
1254
+ points = rollupPoints(read.rows, {
1255
+ kind: "counter",
1256
+ values: null
1257
+ }, prev?.last ?? null);
1258
+ } else points = integratedEnergyPoints(read.rows);
1259
+ return {
1260
+ resolution: chosen,
1261
+ series: [{
1262
+ scope: "",
1263
+ before: null,
1264
+ points,
1265
+ derivedFrom: source.series.key
1266
+ }],
1267
+ truncated: read.overflow
1001
1268
  };
1002
1269
  }
1003
1270
  async listSeries(subject) {
@@ -1014,10 +1281,23 @@ var HistoryQuery = class {
1014
1281
  unit: await this.unitOf(subject, s, metric.unit, slices),
1015
1282
  displayName: metric.displayName,
1016
1283
  source: metric.source,
1284
+ values: metric.kind === "enum" ? metric.values ?? [] : null,
1017
1285
  firstTs: s.firstTs,
1018
1286
  lastTs: s.lastTs
1019
1287
  });
1020
1288
  }
1289
+ const energy = this.energySource(subject);
1290
+ if (energy !== null) infos.push({
1291
+ key: require_dist.DEVICE_HISTORY_ENERGY_KEY,
1292
+ scope: "",
1293
+ kind: "counter",
1294
+ unit: "kWh",
1295
+ displayName: "Energy",
1296
+ source: "derived",
1297
+ values: null,
1298
+ firstTs: energy.series.firstTs,
1299
+ lastTs: energy.series.lastTs
1300
+ });
1021
1301
  return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
1022
1302
  }
1023
1303
  /** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
@@ -1038,6 +1318,11 @@ var HistoryQuery = class {
1038
1318
  return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
1039
1319
  }
1040
1320
  };
1321
+ /** Every covering tier but raw; the coarsest stays when it is the only one. */
1322
+ function withoutRaw(tiers) {
1323
+ const rollups = tiers.filter((t) => t !== "raw");
1324
+ return rollups.length > 0 ? rollups : ["5m"];
1325
+ }
1041
1326
  //#endregion
1042
1327
  //#region src/builtins/device-history/retention/departure-tracker.ts
1043
1328
  var DEPARTED_ABSENCE_MS = 7 * DAY_MS;
@@ -1086,7 +1371,7 @@ var DepartureTracker = class {
1086
1371
  //#region src/builtins/device-history/retention/retention.ts
1087
1372
  var DEFAULT_RETENTION = {
1088
1373
  rawDays: 7,
1089
- fiveMinuteDays: 14,
1374
+ fiveMinuteDays: 30,
1090
1375
  hourDays: 365,
1091
1376
  dayDays: null
1092
1377
  };
@@ -1292,6 +1577,10 @@ function rollupDeclaration(collection) {
1292
1577
  {
1293
1578
  name: "tw_ms",
1294
1579
  type: "INTEGER"
1580
+ },
1581
+ {
1582
+ name: "state_ms",
1583
+ type: "TEXT"
1295
1584
  }
1296
1585
  ]
1297
1586
  };
@@ -1346,6 +1635,10 @@ var HISTORY_DECLARATIONS = [
1346
1635
  name: "declared_at",
1347
1636
  type: "INTEGER",
1348
1637
  notNull: true
1638
+ },
1639
+ {
1640
+ name: "enum_values",
1641
+ type: "TEXT"
1349
1642
  }
1350
1643
  ]
1351
1644
  },
@@ -1519,9 +1812,34 @@ function chunks(items, size) {
1519
1812
  return out;
1520
1813
  }
1521
1814
  function parseKind(value) {
1522
- if (value === "gauge" || value === "counter" || value === "boolean") return value;
1815
+ if (value === "gauge" || value === "counter" || value === "boolean" || value === "enum") return value;
1523
1816
  throw new Error(`history row: unknown kind "${value}"`);
1524
1817
  }
1818
+ /** A JSON TEXT column: the engine hands back the decoded value, or the text itself. */
1819
+ function jsonColumn(data, column) {
1820
+ const raw = data[column];
1821
+ if (raw === null || raw === void 0) return null;
1822
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
1823
+ }
1824
+ /** A JSON column that must hold a string array, or nothing. */
1825
+ function parseValues(data) {
1826
+ const parsed = jsonColumn(data, "enum_values");
1827
+ if (parsed === null) return null;
1828
+ if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) return parsed;
1829
+ throw new Error("history row: column \"enum_values\" is not a JSON string array");
1830
+ }
1831
+ /** A JSON column that must hold `{ "<ordinal>": ms }`, or nothing. */
1832
+ function parseStates(data) {
1833
+ const parsed = jsonColumn(data, "state_ms");
1834
+ if (parsed === null) return null;
1835
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("history row: column \"state_ms\" is not a JSON object");
1836
+ const out = {};
1837
+ for (const [k, v] of Object.entries(parsed)) {
1838
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new Error("history row: column \"state_ms\" holds a non-number");
1839
+ out[k] = v;
1840
+ }
1841
+ return out;
1842
+ }
1525
1843
  function parseMetric(data) {
1526
1844
  const source = readText(data, "source");
1527
1845
  const subjectKind = readText(data, "subject_kind");
@@ -1538,7 +1856,8 @@ function parseMetric(data) {
1538
1856
  unit: readTextOrNull(data, "unit"),
1539
1857
  displayName: readText(data, "display_name"),
1540
1858
  ingest,
1541
- declaredAt: readNumber(data, "declared_at")
1859
+ declaredAt: readNumber(data, "declared_at"),
1860
+ values: parseValues(data)
1542
1861
  };
1543
1862
  }
1544
1863
  function parseSeries(data) {
@@ -1563,7 +1882,8 @@ function parseRollup(data) {
1563
1882
  last: readNumberOrNull(data, "last"),
1564
1883
  lastTs: readNumberOrNull(data, "last_ts"),
1565
1884
  twSum: readNumberOrNull(data, "tw_sum"),
1566
- twMs: readNumberOrNull(data, "tw_ms")
1885
+ twMs: readNumberOrNull(data, "tw_ms"),
1886
+ states: parseStates(data)
1567
1887
  };
1568
1888
  }
1569
1889
  function rollupData(row) {
@@ -1577,7 +1897,8 @@ function rollupData(row) {
1577
1897
  last: row.last,
1578
1898
  last_ts: row.lastTs,
1579
1899
  tw_sum: row.twSum,
1580
- tw_ms: row.twMs
1900
+ tw_ms: row.twMs,
1901
+ state_ms: row.states === null ? null : JSON.stringify(row.states)
1581
1902
  };
1582
1903
  }
1583
1904
  function collectionOf(tier) {
@@ -1601,22 +1922,37 @@ var HistoryStore = class {
1601
1922
  filter: { limit: 2e4 }
1602
1923
  })).map((r) => parseMetric(r.data));
1603
1924
  }
1925
+ /**
1926
+ * Delete metric rows no lookup can reach: rows an earlier build wrote under an
1927
+ * engine-minted random key (every metric key is `cap:` / `addon:` prefixed).
1928
+ */
1929
+ async deleteMetrics(keys) {
1930
+ for (const batch of chunks(keys, WRITE_CHUNK)) await this.door.deleteWhere.mutate({
1931
+ namespace: HISTORY_NAMESPACE,
1932
+ collection: COLLECTIONS.metrics,
1933
+ filter: { whereIn: { key: [...batch] } }
1934
+ });
1935
+ }
1604
1936
  async upsertMetric(metric) {
1605
1937
  await this.door.upsertMany.mutate({
1606
1938
  namespace: HISTORY_NAMESPACE,
1607
1939
  collection: COLLECTIONS.metrics,
1608
1940
  conflict: ["key"],
1609
- records: [{ data: {
1610
- key: metric.key,
1611
- source: metric.source,
1612
- owner_addon: metric.ownerAddon,
1613
- subject_kind: metric.subjectKind,
1614
- kind: metric.kind,
1615
- unit: metric.unit,
1616
- display_name: metric.displayName,
1617
- ingest: metric.ingest,
1618
- declared_at: metric.declaredAt
1619
- } }]
1941
+ records: [{
1942
+ id: metric.key,
1943
+ data: {
1944
+ key: metric.key,
1945
+ source: metric.source,
1946
+ owner_addon: metric.ownerAddon,
1947
+ subject_kind: metric.subjectKind,
1948
+ kind: metric.kind,
1949
+ unit: metric.unit,
1950
+ display_name: metric.displayName,
1951
+ ingest: metric.ingest,
1952
+ declared_at: metric.declaredAt,
1953
+ enum_values: metric.values === null ? null : JSON.stringify(metric.values)
1954
+ }
1955
+ }]
1620
1956
  });
1621
1957
  }
1622
1958
  /** Series with `id > afterId`, ascending — the boot load pages with this. */
@@ -1863,6 +2199,11 @@ var HistoryStore = class {
1863
2199
  * The held value also counts toward `min`/`max` when it was in force at the
1864
2200
  * start of a bucket, because it really was the series' value then.
1865
2201
  */
2202
+ function credit(states, value, ms) {
2203
+ if (states === null || ms <= 0) return;
2204
+ const key = String(value);
2205
+ states[key] = (states[key] ?? 0) + ms;
2206
+ }
1866
2207
  /** One row per 5-minute bucket that has at least one point. */
1867
2208
  function rawToFiveMinuteRows(input) {
1868
2209
  const rows = [];
@@ -1881,6 +2222,7 @@ function rawToFiveMinuteRows(input) {
1881
2222
  let max = Number.NEGATIVE_INFINITY;
1882
2223
  let twSum = 0;
1883
2224
  let twMs = 0;
2225
+ const states = input.trackStates === true ? {} : null;
1884
2226
  let cursor = bucketStart;
1885
2227
  let value = held;
1886
2228
  if (value !== null) {
@@ -1895,6 +2237,7 @@ function rawToFiveMinuteRows(input) {
1895
2237
  const dt = p.ts - cursor;
1896
2238
  twSum += value * dt;
1897
2239
  twMs += dt;
2240
+ credit(states, value, dt);
1898
2241
  }
1899
2242
  cursor = p.ts;
1900
2243
  value = p.value;
@@ -1908,6 +2251,7 @@ function rawToFiveMinuteRows(input) {
1908
2251
  if (value !== null && coverageEnd > cursor) {
1909
2252
  twSum += value * (coverageEnd - cursor);
1910
2253
  twMs += coverageEnd - cursor;
2254
+ credit(states, value, coverageEnd - cursor);
1911
2255
  }
1912
2256
  held = last.value;
1913
2257
  rows.push({
@@ -1920,7 +2264,8 @@ function rawToFiveMinuteRows(input) {
1920
2264
  last: last.value,
1921
2265
  lastTs: last.ts,
1922
2266
  twSum,
1923
- twMs
2267
+ twMs,
2268
+ states
1924
2269
  });
1925
2270
  }
1926
2271
  return rows;
@@ -1937,6 +2282,7 @@ function combineRows(input) {
1937
2282
  let max = Number.NEGATIVE_INFINITY;
1938
2283
  let twSum = 0;
1939
2284
  let twMs = 0;
2285
+ const states = input.trackStates === true ? {} : null;
1940
2286
  let held = input.carryIn;
1941
2287
  let cursor = input.windowStart;
1942
2288
  for (const row of rows) {
@@ -1948,6 +2294,7 @@ function combineRows(input) {
1948
2294
  if (hasTw && gap > 0 && held !== null) {
1949
2295
  twSum += held * gap;
1950
2296
  twMs += gap;
2297
+ credit(states, held, gap);
1951
2298
  if (held < min) min = held;
1952
2299
  if (held > max) max = held;
1953
2300
  }
@@ -1955,6 +2302,7 @@ function combineRows(input) {
1955
2302
  twSum += row.twSum;
1956
2303
  twMs += row.twMs;
1957
2304
  }
2305
+ if (states !== null && row.states !== null) for (const [state, ms] of Object.entries(row.states)) states[state] = (states[state] ?? 0) + ms;
1958
2306
  cursor = row.bucketStart + input.childWidthMs;
1959
2307
  held = row.last;
1960
2308
  }
@@ -1962,6 +2310,7 @@ function combineRows(input) {
1962
2310
  if (hasTw && held !== null && coverageEnd > cursor) {
1963
2311
  twSum += held * (coverageEnd - cursor);
1964
2312
  twMs += coverageEnd - cursor;
2313
+ credit(states, held, coverageEnd - cursor);
1965
2314
  }
1966
2315
  return {
1967
2316
  seriesId: input.seriesId,
@@ -1973,7 +2322,8 @@ function combineRows(input) {
1973
2322
  last: lastRow.last,
1974
2323
  lastTs: lastRow.lastTs,
1975
2324
  twSum: hasTw ? twSum : null,
1976
- twMs: hasTw ? twMs : null
2325
+ twMs: hasTw ? twMs : null,
2326
+ states
1977
2327
  };
1978
2328
  }
1979
2329
  /** How far back a boot rebuilds from raw and re-marks the upper tiers. */
@@ -2114,6 +2464,11 @@ var RollupEngine = class {
2114
2464
  }
2115
2465
  if (handled > 0) this.deps.counters.last1dAt = now;
2116
2466
  }
2467
+ /** An `enum` series keeps the time spent in each state in its rollups. */
2468
+ isEnum(seriesId) {
2469
+ const series = this.deps.registry.byIdOf(seriesId);
2470
+ return series !== void 0 && this.deps.registry.metric(series.key)?.kind === "enum";
2471
+ }
2117
2472
  /** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
2118
2473
  async recomputeFiveMinute(seriesId, from, toExclusive, horizon) {
2119
2474
  const { store } = this.deps;
@@ -2130,7 +2485,8 @@ var RollupEngine = class {
2130
2485
  seriesId,
2131
2486
  points,
2132
2487
  carryIn: await store.rawBefore(seriesId, from),
2133
- horizon
2488
+ horizon,
2489
+ trackStates: this.isEnum(seriesId)
2134
2490
  });
2135
2491
  if (rows.length === 0) return 0;
2136
2492
  await store.upsertRollups("5m", rows);
@@ -2154,7 +2510,8 @@ var RollupEngine = class {
2154
2510
  childWidthMs,
2155
2511
  rows: await store.readRollups(child, seriesId, windowStart, windowEnd - 1, READ_PAGE),
2156
2512
  carryIn: (await store.rollupBefore(child, seriesId, windowStart))?.last ?? null,
2157
- horizon: now
2513
+ horizon: now,
2514
+ trackStates: this.isEnum(seriesId)
2158
2515
  });
2159
2516
  if (combined === null) return;
2160
2517
  await store.upsertRollups(parent, [combined]);
@@ -2661,7 +3018,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
2661
3018
  }),
2662
3019
  listNodeSeries: async (input) => this.requireRuntime().query.listSeries({
2663
3020
  kind: "node",
2664
- nodeId: input.nodeId
3021
+ nodeId: input.forNodeId
2665
3022
  }),
2666
3023
  query: async ({ deviceId, ...window }) => this.requireRuntime().query.query({
2667
3024
  ...window,
@@ -2670,13 +3027,14 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
2670
3027
  deviceId
2671
3028
  }
2672
3029
  }),
2673
- queryNode: async ({ nodeId, ...window }) => this.requireRuntime().query.query({
3030
+ queryNode: async ({ forNodeId, ...window }) => this.requireRuntime().query.query({
2674
3031
  ...window,
2675
3032
  subject: {
2676
3033
  kind: "node",
2677
- nodeId
3034
+ nodeId: forNodeId
2678
3035
  }
2679
3036
  }),
3037
+ queryMany: async (input) => ({ results: await this.requireRuntime().query.queryMany(input) }),
2680
3038
  declareSeries: async (input) => this.requireRuntime().ingest.declareSeries(callerOf(input), input),
2681
3039
  record: async (input) => this.requireRuntime().ingest.record(callerOf(input), input.points),
2682
3040
  recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),