@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.
- package/dist/addon-runner.js +96 -10
- package/dist/addon-runner.mjs +96 -10
- package/dist/{agent-https-fm4AR8XL.mjs → agent-https-CK9Z4sSR.mjs} +2 -2
- package/dist/{agent-https-yjxdfJDA.js → agent-https-CoyOZ3pC.js} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +2 -2
- package/dist/builtins/alerts/alerts.addon.mjs +2 -2
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/console-destination.d.ts +1 -2
- package/dist/builtins/console-logging/index.js +1 -4
- package/dist/builtins/console-logging/index.mjs +1 -4
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/capture/capture-engine.d.ts +6 -0
- package/dist/builtins/device-history/device-history.addon.js +441 -83
- package/dist/builtins/device-history/device-history.addon.mjs +441 -83
- package/dist/builtins/device-history/query/history-query.d.ts +35 -9
- package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
- package/dist/builtins/device-history/retention/retention.d.ts +1 -1
- package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
- package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
- package/dist/builtins/device-history/store/history-store.d.ts +7 -0
- package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
- package/dist/builtins/hub-forwarder/index.js +1 -4
- package/dist/builtins/hub-forwarder/index.mjs +1 -4
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +323 -7
- package/dist/builtins/loki-logging/index.mjs +323 -7
- package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
- package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
- package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/location-history-recorder.d.ts +76 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +165 -3
- package/dist/builtins/winston-logging/index.mjs +165 -3
- package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
- package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
- package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
- package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
- package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
- package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
- package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
- package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
- package/dist/{composition-sources-Ds0Vr6eD.mjs → composition-sources-B_jeZDRJ.mjs} +1 -1
- package/dist/{composition-sources-gB4coDJR.js → composition-sources-qKyvMAvc.js} +1 -1
- package/dist/{constants-CavyKfHZ.js → constants-B_X9cWEK.js} +1 -1
- package/dist/{constants-i5oD_gLB.mjs → constants-cxKcZRMi.mjs} +1 -1
- package/dist/{device-ca-qz1Pmeu1.mjs → device-ca-BB6nHAL5.mjs} +1 -1
- package/dist/{device-ca-bpoEr0zG.js → device-ca-CVK3YcR_.js} +1 -1
- package/dist/{dist-BpqA2CT0.js → dist-C89nqNnn.js} +471 -47
- package/dist/{dist-BSF_C8W2.mjs → dist-Dhux90yB.mjs} +454 -48
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CHaGDzyz.mjs → hub-mdns-ciao-B8olZqqd.mjs} +2 -2
- package/dist/{hub-mdns-ciao-CZlZRxxA.js → hub-mdns-ciao-DB1Y9agt.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +135 -20
- package/dist/index.mjs +129 -21
- package/dist/kernel/config-schema.d.ts +5 -0
- package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
- package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
- package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
- package/dist/log-file-read-worker.js +64 -0
- package/dist/log-file-read-worker.mjs +64 -0
- package/dist/log-read-filter-Bi066jwU.js +134 -0
- package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
- package/dist/logging/log-manager.d.ts +10 -6
- package/dist/logging/log-read-filter.d.ts +43 -0
- package/dist/{phase6-declare-gate-n-IZRklX.js → phase6-declare-gate-BO-_jLh_.js} +1 -1
- package/dist/{phase6-declare-gate-B4yfsf2n.mjs → phase6-declare-gate-x9ZCbQJP.mjs} +1 -1
- package/dist/{restore-archive-B4QG7XmX.js → restore-archive-4DOUR5Gu.js} +1 -1
- package/dist/{restore-archive-BGUM4FKf.mjs → restore-archive-DyOaPeAr.mjs} +1 -1
- package/dist/{wal-maintenance-AerF4XE6.mjs → wal-maintenance-BMiX6UIa.mjs} +0 -0
- package/dist/{wal-maintenance-CNEs_I94.js → wal-maintenance-DVN-DiVP.js} +0 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as HISTORY_CATALOG, O as DEVICE_HISTORY_ENERGY_KEY, Oa as EventCategory, Tt as armRemovalReconcile, di as HISTORY_DEFAULT_MIN_INTERVAL_MS, ei as DeviceType, in as deviceRemovalHookCapability, ni as BaseAddon, nn as deviceHistoryCapability } from "../../dist-Dhux90yB.mjs";
|
|
2
2
|
//#region src/builtins/device-history/store/series-registry.ts
|
|
3
3
|
function subjectIdOf(subject) {
|
|
4
4
|
return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
|
|
@@ -6,16 +6,29 @@ function subjectIdOf(subject) {
|
|
|
6
6
|
function deviceIdOf(subject) {
|
|
7
7
|
return subject.kind === "device" ? subject.deviceId : null;
|
|
8
8
|
}
|
|
9
|
+
/** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
|
|
10
|
+
function isMetricKey(key) {
|
|
11
|
+
return key.startsWith("cap:") || key.startsWith("addon:");
|
|
12
|
+
}
|
|
9
13
|
function identityOf(key, subjectId, scope) {
|
|
10
14
|
return `${key}\u0000${subjectId}\u0000${scope}`;
|
|
11
15
|
}
|
|
12
16
|
var LOAD_PAGE = 5e3;
|
|
17
|
+
/** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
|
|
18
|
+
function mergeEnumValues(stored, next) {
|
|
19
|
+
if (stored === null) return next;
|
|
20
|
+
if (next === null) return stored;
|
|
21
|
+
const missing = next.filter((v) => !stored.includes(v));
|
|
22
|
+
return missing.length === 0 ? stored : [...stored, ...missing];
|
|
23
|
+
}
|
|
13
24
|
var SeriesRegistry = class {
|
|
14
25
|
store;
|
|
15
26
|
metrics = /* @__PURE__ */ new Map();
|
|
16
27
|
byIdentity = /* @__PURE__ */ new Map();
|
|
17
28
|
byId = /* @__PURE__ */ new Map();
|
|
18
29
|
inflight = /* @__PURE__ */ new Map();
|
|
30
|
+
/** Enum metrics whose value list grew in memory and is not stored yet. */
|
|
31
|
+
dirtyMetrics = /* @__PURE__ */ new Set();
|
|
19
32
|
removeListeners = [];
|
|
20
33
|
constructor(store) {
|
|
21
34
|
this.store = store;
|
|
@@ -24,7 +37,10 @@ var SeriesRegistry = class {
|
|
|
24
37
|
this.metrics.clear();
|
|
25
38
|
this.byIdentity.clear();
|
|
26
39
|
this.byId.clear();
|
|
27
|
-
|
|
40
|
+
const stray = [];
|
|
41
|
+
for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
|
|
42
|
+
else stray.push(m.key);
|
|
43
|
+
if (stray.length > 0) await this.store.deleteMetrics(stray);
|
|
28
44
|
let afterId = 0;
|
|
29
45
|
for (;;) {
|
|
30
46
|
const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
|
|
@@ -44,12 +60,48 @@ var SeriesRegistry = class {
|
|
|
44
60
|
allMetrics() {
|
|
45
61
|
return [...this.metrics.values()];
|
|
46
62
|
}
|
|
47
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Persist and remember a metric; idempotent. An `enum` value list is MERGED
|
|
65
|
+
* append-only with the stored one, so reordering a `z.enum` never changes
|
|
66
|
+
* what a stored ordinal means.
|
|
67
|
+
*/
|
|
48
68
|
async declareMetric(metric) {
|
|
49
69
|
const existing = this.metrics.get(metric.key);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const merged = {
|
|
71
|
+
...metric,
|
|
72
|
+
values: mergeEnumValues(existing?.values ?? null, metric.values)
|
|
73
|
+
};
|
|
74
|
+
if (existing !== void 0 && sameMetric(existing, merged)) return;
|
|
75
|
+
await this.store.upsertMetric(merged);
|
|
76
|
+
this.metrics.set(metric.key, merged);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The ordinal of `value` in an `enum` metric's list, learning (appending) a
|
|
80
|
+
* value seen for the first time. `null` when the key is not an enum metric or
|
|
81
|
+
* its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
|
|
82
|
+
* {@link persistDirtyMetrics} BEFORE any point that uses it.
|
|
83
|
+
*/
|
|
84
|
+
ordinalOf(key, value) {
|
|
85
|
+
const metric = this.metrics.get(key);
|
|
86
|
+
if (metric === void 0 || metric.kind !== "enum") return null;
|
|
87
|
+
const values = metric.values ?? [];
|
|
88
|
+
const known = values.indexOf(value);
|
|
89
|
+
if (known >= 0) return known;
|
|
90
|
+
if (values.length >= 1024) return null;
|
|
91
|
+
this.metrics.set(key, {
|
|
92
|
+
...metric,
|
|
93
|
+
values: [...values, value]
|
|
94
|
+
});
|
|
95
|
+
this.dirtyMetrics.add(key);
|
|
96
|
+
return values.length;
|
|
97
|
+
}
|
|
98
|
+
/** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
|
|
99
|
+
async persistDirtyMetrics() {
|
|
100
|
+
for (const key of this.dirtyMetrics) {
|
|
101
|
+
const metric = this.metrics.get(key);
|
|
102
|
+
if (metric !== void 0) await this.store.upsertMetric(metric);
|
|
103
|
+
this.dirtyMetrics.delete(key);
|
|
104
|
+
}
|
|
53
105
|
}
|
|
54
106
|
find(key, subjectId, scope) {
|
|
55
107
|
return this.byIdentity.get(identityOf(key, subjectId, scope));
|
|
@@ -114,7 +166,7 @@ var SeriesRegistry = class {
|
|
|
114
166
|
}
|
|
115
167
|
};
|
|
116
168
|
function sameMetric(a, b) {
|
|
117
|
-
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;
|
|
169
|
+
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);
|
|
118
170
|
}
|
|
119
171
|
//#endregion
|
|
120
172
|
//#region src/builtins/device-history/capture/capture-filter.ts
|
|
@@ -138,10 +190,10 @@ var DEADBAND_EPSILON = 1e-9;
|
|
|
138
190
|
/** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
|
|
139
191
|
function minIntervalOf(spec) {
|
|
140
192
|
if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
|
|
141
|
-
return spec.kind === "boolean" ? 0 : HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
193
|
+
return spec.kind === "boolean" || spec.kind === "enum" ? 0 : HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
142
194
|
}
|
|
143
195
|
function clearsDeadband(spec, last, value) {
|
|
144
|
-
if (spec.kind === "boolean") return true;
|
|
196
|
+
if (spec.kind === "boolean" || spec.kind === "enum") return true;
|
|
145
197
|
const delta = Math.abs(value - last);
|
|
146
198
|
if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
|
|
147
199
|
if (spec.deadbandPct !== void 0) {
|
|
@@ -326,7 +378,8 @@ function catalogMetrics(catalog, now) {
|
|
|
326
378
|
unit: spec.unit ?? null,
|
|
327
379
|
displayName: spec.displayName ?? humanize(capName, path),
|
|
328
380
|
ingest: "points",
|
|
329
|
-
declaredAt: now
|
|
381
|
+
declaredAt: now,
|
|
382
|
+
values: spec.kind === "enum" ? spec.values ?? [] : null
|
|
330
383
|
})));
|
|
331
384
|
}
|
|
332
385
|
/** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
|
|
@@ -375,11 +428,28 @@ var CaptureEngine = class {
|
|
|
375
428
|
counters.skip("excluded-device-type");
|
|
376
429
|
continue;
|
|
377
430
|
}
|
|
378
|
-
const value =
|
|
431
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
379
432
|
if (value === null) continue;
|
|
380
433
|
this.consider(deviceId, capName, path, spec, value, at, false);
|
|
381
434
|
}
|
|
382
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* The number a field contributes, or `null` (not a reading — never 0). An
|
|
438
|
+
* `enum` state becomes the ordinal of its value in the metric's list; a value
|
|
439
|
+
* the full list cannot take is refused and counted (`enum-overflow`).
|
|
440
|
+
*/
|
|
441
|
+
readValue(slice, capName, path, spec) {
|
|
442
|
+
if (spec.skipWhenSet !== void 0) {
|
|
443
|
+
const gate = readPath(slice, spec.skipWhenSet);
|
|
444
|
+
if (gate !== void 0 && gate !== null) return null;
|
|
445
|
+
}
|
|
446
|
+
if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
|
|
447
|
+
const raw = readPath(slice, path);
|
|
448
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
449
|
+
const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
|
|
450
|
+
if (ordinal === null) this.deps.counters.skip("enum-overflow");
|
|
451
|
+
return ordinal;
|
|
452
|
+
}
|
|
383
453
|
consider(deviceId, capName, path, spec, value, at, reconciling) {
|
|
384
454
|
const key = capSeriesKey(capName, path);
|
|
385
455
|
const subject = {
|
|
@@ -485,7 +555,7 @@ var CaptureEngine = class {
|
|
|
485
555
|
if (slice === void 0) continue;
|
|
486
556
|
for (const [path, spec] of Object.entries(fields)) {
|
|
487
557
|
if (spec.excludeDeviceTypes?.includes(type) === true) continue;
|
|
488
|
-
const value =
|
|
558
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
489
559
|
if (value === null) continue;
|
|
490
560
|
this.consider(deviceId, capName, path, spec, value, now, true);
|
|
491
561
|
}
|
|
@@ -568,6 +638,7 @@ var PointWriter = class {
|
|
|
568
638
|
this.queue = [];
|
|
569
639
|
const { registry, store, counters, logger, clock } = this.deps;
|
|
570
640
|
try {
|
|
641
|
+
await registry.persistDirtyMetrics();
|
|
571
642
|
const bySeries = /* @__PURE__ */ new Map();
|
|
572
643
|
for (const point of batch) {
|
|
573
644
|
const series = await registry.ensure(point.key, point.subject, point.scope, point.ts);
|
|
@@ -743,7 +814,8 @@ var ProducerIngest = class {
|
|
|
743
814
|
unit: input.unit ?? null,
|
|
744
815
|
displayName: input.displayName,
|
|
745
816
|
ingest: input.ingest,
|
|
746
|
-
declaredAt: this.deps.clock()
|
|
817
|
+
declaredAt: this.deps.clock(),
|
|
818
|
+
values: null
|
|
747
819
|
};
|
|
748
820
|
await this.deps.registry.declareMetric(metric);
|
|
749
821
|
return { key };
|
|
@@ -830,7 +902,8 @@ var ProducerIngest = class {
|
|
|
830
902
|
last: bucket.last,
|
|
831
903
|
lastTs: bucket.lastTs,
|
|
832
904
|
twSum: null,
|
|
833
|
-
twMs: null
|
|
905
|
+
twMs: null,
|
|
906
|
+
states: null
|
|
834
907
|
});
|
|
835
908
|
}
|
|
836
909
|
await store.upsertRollups("5m", rows);
|
|
@@ -869,7 +942,109 @@ var ProducerIngest = class {
|
|
|
869
942
|
}
|
|
870
943
|
};
|
|
871
944
|
//#endregion
|
|
945
|
+
//#region src/builtins/device-history/query/point-shape.ts
|
|
946
|
+
/** kWh in one W·ms: 1 kWh = 1 000 W × 3 600 000 ms. */
|
|
947
|
+
var KWH_PER_WATT_MS = 1 / 36e8;
|
|
948
|
+
/** Growth from `prev` to `cur`; a drop is a reset counted from zero. `fallback` when `prev` is unknown. */
|
|
949
|
+
function counterDelta(prev, cur, fallback) {
|
|
950
|
+
if (cur === null) return null;
|
|
951
|
+
if (prev === null) return fallback;
|
|
952
|
+
return cur >= prev ? cur - prev : cur;
|
|
953
|
+
}
|
|
954
|
+
/** The name of an ordinal, or `null` when the series is not an enum or the ordinal is unknown. */
|
|
955
|
+
function stateName(meaning, ordinal) {
|
|
956
|
+
if (meaning.kind !== "enum" || meaning.values === null || ordinal === null) return null;
|
|
957
|
+
return meaning.values[ordinal] ?? null;
|
|
958
|
+
}
|
|
959
|
+
function namedStates(meaning, states) {
|
|
960
|
+
if (meaning.kind !== "enum" || states === null) return null;
|
|
961
|
+
const out = {};
|
|
962
|
+
for (const [ordinal, ms] of Object.entries(states)) {
|
|
963
|
+
const name = stateName(meaning, Number(ordinal));
|
|
964
|
+
if (name !== null) out[name] = (out[name] ?? 0) + ms;
|
|
965
|
+
}
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
968
|
+
/** Raw points, oldest first; `prev` is the value of the point before the first one. */
|
|
969
|
+
function rawPoints(rows, meaning, prev) {
|
|
970
|
+
const isEnum = meaning.kind === "enum";
|
|
971
|
+
let held = prev;
|
|
972
|
+
return rows.map((r) => {
|
|
973
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, r.value, null) : null;
|
|
974
|
+
held = r.value;
|
|
975
|
+
return {
|
|
976
|
+
t: r.ts,
|
|
977
|
+
samples: 1,
|
|
978
|
+
mean: isEnum ? null : r.value,
|
|
979
|
+
twMean: null,
|
|
980
|
+
min: r.value,
|
|
981
|
+
max: r.value,
|
|
982
|
+
last: r.value,
|
|
983
|
+
delta,
|
|
984
|
+
state: stateName(meaning, r.value),
|
|
985
|
+
states: null
|
|
986
|
+
};
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/** Rollup rows, oldest first; `prevLast` is the `last` of the bucket before the first one. */
|
|
990
|
+
function rollupPoints(rows, meaning, prevLast) {
|
|
991
|
+
const isEnum = meaning.kind === "enum";
|
|
992
|
+
let held = prevLast;
|
|
993
|
+
return rows.map((row) => {
|
|
994
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, row.last, row.last === null ? null : row.last - row.min) : null;
|
|
995
|
+
if (row.last !== null) held = row.last;
|
|
996
|
+
return {
|
|
997
|
+
t: row.bucketStart,
|
|
998
|
+
samples: row.samples,
|
|
999
|
+
mean: isEnum || row.samples <= 0 ? null : row.sum / row.samples,
|
|
1000
|
+
twMean: !isEnum && row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
1001
|
+
min: row.min,
|
|
1002
|
+
max: row.max,
|
|
1003
|
+
last: row.last,
|
|
1004
|
+
delta,
|
|
1005
|
+
state: stateName(meaning, row.last),
|
|
1006
|
+
states: namedStates(meaning, row.states)
|
|
1007
|
+
};
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/** Energy used in each bucket of a `watts` gauge: its time-weighted integral, in kWh. */
|
|
1011
|
+
function integratedEnergyPoints(rows) {
|
|
1012
|
+
return rows.map((row) => ({
|
|
1013
|
+
t: row.bucketStart,
|
|
1014
|
+
samples: row.samples,
|
|
1015
|
+
mean: null,
|
|
1016
|
+
twMean: null,
|
|
1017
|
+
min: row.min,
|
|
1018
|
+
max: row.max,
|
|
1019
|
+
last: null,
|
|
1020
|
+
delta: row.twSum === null ? null : row.twSum * KWH_PER_WATT_MS,
|
|
1021
|
+
state: null,
|
|
1022
|
+
states: null
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
1025
|
+
//#endregion
|
|
872
1026
|
//#region src/builtins/device-history/query/history-query.ts
|
|
1027
|
+
/**
|
|
1028
|
+
* Reads: `listSeries`, `query` and `queryMany` (plan §5.1).
|
|
1029
|
+
*
|
|
1030
|
+
* `auto` resolution picks the finest tier whose retention covers `from` and
|
|
1031
|
+
* whose point count fits `maxPoints`: a rollup tier fits when
|
|
1032
|
+
* `ceil(range / bucket) <= maxPoints`, raw fits when a read of `maxPoints + 1`
|
|
1033
|
+
* rows comes back short. The coarsest covering tier is the last resort and is
|
|
1034
|
+
* truncated to its newest `maxPoints` points (`truncated: true`) rather than
|
|
1035
|
+
* answering nothing. A series fed by closed buckets has NO raw rows, so raw is
|
|
1036
|
+
* never a candidate for it: an empty raw read "fits" every window.
|
|
1037
|
+
*
|
|
1038
|
+
* `before` is the last point strictly before the window in the chosen tier, so a
|
|
1039
|
+
* step chart can start at the value in force when the window opens — a quiet
|
|
1040
|
+
* bucket has no row, and the held value is otherwise invisible.
|
|
1041
|
+
*
|
|
1042
|
+
* {@link DEVICE_HISTORY_ENERGY_KEY} is computed here at read time, never stored:
|
|
1043
|
+
* the device's `kwhTotal` meter when it has one (per-bucket growth), else the
|
|
1044
|
+
* time-weighted integral of its `watts` (`tw_sum`, already in every rollup).
|
|
1045
|
+
* One place makes that choice, so two charts of one device cannot disagree, and
|
|
1046
|
+
* it covers every hour of history recorded before the key existed.
|
|
1047
|
+
*/
|
|
873
1048
|
var TIER_ORDER = [
|
|
874
1049
|
"raw",
|
|
875
1050
|
"5m",
|
|
@@ -883,33 +1058,25 @@ var BUCKET_MS = {
|
|
|
883
1058
|
};
|
|
884
1059
|
/** A key with more scopes than this is cut (a zone x class matrix, not a chart). */
|
|
885
1060
|
var MAX_SCOPES = 50;
|
|
1061
|
+
/** The power-meter series the energy key is derived from, meter first. */
|
|
1062
|
+
var ENERGY_METER_KEY = capSeriesKey("power-meter", "kwhTotal");
|
|
1063
|
+
var ENERGY_POWER_KEY = capSeriesKey("power-meter", "watts");
|
|
886
1064
|
function retentionMsOf(tier, policy) {
|
|
887
1065
|
if (tier === "raw") return policy.rawDays * DAY_MS;
|
|
888
1066
|
if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
|
|
889
1067
|
if (tier === "1h") return policy.hourDays * DAY_MS;
|
|
890
1068
|
return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
|
|
891
1069
|
}
|
|
892
|
-
|
|
1070
|
+
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
1071
|
+
function bounded(newestFirst, maxPoints) {
|
|
1072
|
+
const overflow = newestFirst.length > maxPoints;
|
|
893
1073
|
return {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
mean: value,
|
|
897
|
-
twMean: null,
|
|
898
|
-
min: value,
|
|
899
|
-
max: value,
|
|
900
|
-
last: value
|
|
1074
|
+
rows: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
|
|
1075
|
+
overflow
|
|
901
1076
|
};
|
|
902
1077
|
}
|
|
903
|
-
function
|
|
904
|
-
return
|
|
905
|
-
t: row.bucketStart,
|
|
906
|
-
samples: row.samples,
|
|
907
|
-
mean: row.samples > 0 ? row.sum / row.samples : null,
|
|
908
|
-
twMean: row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
909
|
-
min: row.min,
|
|
910
|
-
max: row.max,
|
|
911
|
-
last: row.last
|
|
912
|
-
};
|
|
1078
|
+
function errorText(err) {
|
|
1079
|
+
return err instanceof Error ? err.message : String(err);
|
|
913
1080
|
}
|
|
914
1081
|
var HistoryQuery = class {
|
|
915
1082
|
deps;
|
|
@@ -917,12 +1084,75 @@ var HistoryQuery = class {
|
|
|
917
1084
|
this.deps = deps;
|
|
918
1085
|
}
|
|
919
1086
|
async query(input) {
|
|
1087
|
+
if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
|
|
920
1088
|
const { registry } = this.deps;
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
const
|
|
1089
|
+
const wanted = this.seriesOf(input);
|
|
1090
|
+
const metric = registry.metric(input.key);
|
|
1091
|
+
const meaning = {
|
|
1092
|
+
kind: metric?.kind ?? "gauge",
|
|
1093
|
+
values: metric?.values ?? null
|
|
1094
|
+
};
|
|
1095
|
+
const { chosen, reads } = await this.choose(input, wanted, metric?.ingest === "buckets");
|
|
1096
|
+
const series = [];
|
|
1097
|
+
for (const [s, read] of reads) {
|
|
1098
|
+
const before = await this.before(chosen, s, input.from, meaning);
|
|
1099
|
+
const points = read.tier === "raw" ? rawPoints(read.rows, meaning, before?.value ?? null) : rollupPoints(read.rows, meaning, before?.value ?? null);
|
|
1100
|
+
series.push({
|
|
1101
|
+
scope: s.scope,
|
|
1102
|
+
before,
|
|
1103
|
+
points,
|
|
1104
|
+
derivedFrom: null
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
resolution: chosen,
|
|
1109
|
+
series,
|
|
1110
|
+
truncated: reads.some(([, r]) => r.overflow)
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
/** Several reads over one window; one failing item answers its error, not the batch's. */
|
|
1114
|
+
async queryMany(input) {
|
|
1115
|
+
const out = [];
|
|
1116
|
+
for (const item of input.items) {
|
|
1117
|
+
const subject = "deviceId" in item ? {
|
|
1118
|
+
kind: "device",
|
|
1119
|
+
deviceId: item.deviceId
|
|
1120
|
+
} : {
|
|
1121
|
+
kind: "node",
|
|
1122
|
+
nodeId: item.forNodeId
|
|
1123
|
+
};
|
|
1124
|
+
try {
|
|
1125
|
+
const result = await this.query({
|
|
1126
|
+
key: item.key,
|
|
1127
|
+
subject,
|
|
1128
|
+
...item.scope !== void 0 ? { scope: item.scope } : {},
|
|
1129
|
+
from: input.from,
|
|
1130
|
+
to: input.to,
|
|
1131
|
+
resolution: input.resolution,
|
|
1132
|
+
maxPoints: input.maxPoints
|
|
1133
|
+
});
|
|
1134
|
+
out.push({
|
|
1135
|
+
ok: true,
|
|
1136
|
+
result
|
|
1137
|
+
});
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
out.push({
|
|
1140
|
+
ok: false,
|
|
1141
|
+
error: errorText(err)
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
seriesOf(input) {
|
|
1148
|
+
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);
|
|
1149
|
+
}
|
|
1150
|
+
/** The tier to answer from and its reads. `noRaw`: the series has no raw rows to read. */
|
|
1151
|
+
async choose(input, wanted, noRaw) {
|
|
1152
|
+
const covering = this.coveringTiers(input.from);
|
|
1153
|
+
const candidates = noRaw ? withoutRaw(covering) : covering;
|
|
924
1154
|
const forced = input.resolution === "auto" ? null : input.resolution;
|
|
925
|
-
const order = forced === null ? candidates : [forced];
|
|
1155
|
+
const order = forced === null ? candidates : [noRaw && forced === "raw" ? "5m" : forced];
|
|
926
1156
|
let chosen = order[order.length - 1] ?? "1d";
|
|
927
1157
|
let reads = [];
|
|
928
1158
|
for (const [index, tier] of order.entries()) {
|
|
@@ -933,16 +1163,9 @@ var HistoryQuery = class {
|
|
|
933
1163
|
if (reads.some(([, r]) => r.overflow) && !last) continue;
|
|
934
1164
|
break;
|
|
935
1165
|
}
|
|
936
|
-
const series = [];
|
|
937
|
-
for (const [s, read] of reads) series.push({
|
|
938
|
-
scope: s.scope,
|
|
939
|
-
before: await this.before(chosen, s, input.from),
|
|
940
|
-
points: read.points
|
|
941
|
-
});
|
|
942
1166
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
truncated: reads.some(([, r]) => r.overflow)
|
|
1167
|
+
chosen,
|
|
1168
|
+
reads
|
|
946
1169
|
};
|
|
947
1170
|
}
|
|
948
1171
|
/** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
|
|
@@ -964,35 +1187,79 @@ var HistoryQuery = class {
|
|
|
964
1187
|
async readTier(tier, series, input) {
|
|
965
1188
|
const { store } = this.deps;
|
|
966
1189
|
const limit = input.maxPoints + 1;
|
|
967
|
-
if (tier === "raw") {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
1190
|
+
if (tier === "raw") return {
|
|
1191
|
+
tier,
|
|
1192
|
+
...bounded(await store.readRaw(series.id, input.from, input.to, limit, "desc"), input.maxPoints)
|
|
1193
|
+
};
|
|
971
1194
|
const lower = bucketStartOf(tier, input.from);
|
|
972
|
-
const rows = await store.readRollups(tier, series.id, lower, input.to, limit, "desc");
|
|
973
|
-
return this.shape(rows.map(rollupPoint), input.maxPoints);
|
|
974
|
-
}
|
|
975
|
-
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
976
|
-
shape(newestFirst, maxPoints) {
|
|
977
|
-
const overflow = newestFirst.length > maxPoints;
|
|
978
1195
|
return {
|
|
979
|
-
|
|
980
|
-
|
|
1196
|
+
tier,
|
|
1197
|
+
...bounded(await store.readRollups(tier, series.id, lower, input.to, limit, "desc"), input.maxPoints)
|
|
981
1198
|
};
|
|
982
1199
|
}
|
|
983
|
-
async before(tier, series, from) {
|
|
1200
|
+
async before(tier, series, from, meaning) {
|
|
984
1201
|
const { store } = this.deps;
|
|
985
1202
|
if (tier === "raw") {
|
|
986
1203
|
const p = await store.rawBefore(series.id, from);
|
|
987
1204
|
return p === null ? null : {
|
|
988
1205
|
t: p.ts,
|
|
989
|
-
value: p.value
|
|
1206
|
+
value: p.value,
|
|
1207
|
+
state: stateName(meaning, p.value)
|
|
990
1208
|
};
|
|
991
1209
|
}
|
|
992
1210
|
const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
|
|
993
1211
|
return row === null ? null : {
|
|
994
1212
|
t: row.bucketStart,
|
|
995
|
-
value: row.last
|
|
1213
|
+
value: row.last,
|
|
1214
|
+
state: stateName(meaning, row.last)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** The series energy is derived from for a device: the meter when it has one, else its power. */
|
|
1218
|
+
energySource(subject) {
|
|
1219
|
+
if (subject.kind !== "device") return null;
|
|
1220
|
+
const subjectId = subjectIdOf(subject);
|
|
1221
|
+
const meter = this.deps.registry.find(ENERGY_METER_KEY, subjectId, "");
|
|
1222
|
+
if (meter !== void 0) return {
|
|
1223
|
+
series: meter,
|
|
1224
|
+
from: "meter"
|
|
1225
|
+
};
|
|
1226
|
+
const power = this.deps.registry.find(ENERGY_POWER_KEY, subjectId, "");
|
|
1227
|
+
return power === void 0 ? null : {
|
|
1228
|
+
series: power,
|
|
1229
|
+
from: "power"
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async queryEnergy(input) {
|
|
1233
|
+
const source = this.energySource(input.subject);
|
|
1234
|
+
if (source === null || input.scope !== void 0 && input.scope !== "") return {
|
|
1235
|
+
resolution: "5m",
|
|
1236
|
+
series: [],
|
|
1237
|
+
truncated: false
|
|
1238
|
+
};
|
|
1239
|
+
const { chosen, reads } = await this.choose(input, [source.series], true);
|
|
1240
|
+
const read = reads[0]?.[1];
|
|
1241
|
+
if (read === void 0 || read.tier === "raw") return {
|
|
1242
|
+
resolution: chosen,
|
|
1243
|
+
series: [],
|
|
1244
|
+
truncated: false
|
|
1245
|
+
};
|
|
1246
|
+
let points;
|
|
1247
|
+
if (source.from === "meter") {
|
|
1248
|
+
const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, bucketStartOf(read.tier, input.from));
|
|
1249
|
+
points = rollupPoints(read.rows, {
|
|
1250
|
+
kind: "counter",
|
|
1251
|
+
values: null
|
|
1252
|
+
}, prev?.last ?? null);
|
|
1253
|
+
} else points = integratedEnergyPoints(read.rows);
|
|
1254
|
+
return {
|
|
1255
|
+
resolution: chosen,
|
|
1256
|
+
series: [{
|
|
1257
|
+
scope: "",
|
|
1258
|
+
before: null,
|
|
1259
|
+
points,
|
|
1260
|
+
derivedFrom: source.series.key
|
|
1261
|
+
}],
|
|
1262
|
+
truncated: read.overflow
|
|
996
1263
|
};
|
|
997
1264
|
}
|
|
998
1265
|
async listSeries(subject) {
|
|
@@ -1009,10 +1276,23 @@ var HistoryQuery = class {
|
|
|
1009
1276
|
unit: await this.unitOf(subject, s, metric.unit, slices),
|
|
1010
1277
|
displayName: metric.displayName,
|
|
1011
1278
|
source: metric.source,
|
|
1279
|
+
values: metric.kind === "enum" ? metric.values ?? [] : null,
|
|
1012
1280
|
firstTs: s.firstTs,
|
|
1013
1281
|
lastTs: s.lastTs
|
|
1014
1282
|
});
|
|
1015
1283
|
}
|
|
1284
|
+
const energy = this.energySource(subject);
|
|
1285
|
+
if (energy !== null) infos.push({
|
|
1286
|
+
key: DEVICE_HISTORY_ENERGY_KEY,
|
|
1287
|
+
scope: "",
|
|
1288
|
+
kind: "counter",
|
|
1289
|
+
unit: "kWh",
|
|
1290
|
+
displayName: "Energy",
|
|
1291
|
+
source: "derived",
|
|
1292
|
+
values: null,
|
|
1293
|
+
firstTs: energy.series.firstTs,
|
|
1294
|
+
lastTs: energy.series.lastTs
|
|
1295
|
+
});
|
|
1016
1296
|
return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
|
|
1017
1297
|
}
|
|
1018
1298
|
/** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
|
|
@@ -1033,6 +1313,11 @@ var HistoryQuery = class {
|
|
|
1033
1313
|
return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
|
|
1034
1314
|
}
|
|
1035
1315
|
};
|
|
1316
|
+
/** Every covering tier but raw; the coarsest stays when it is the only one. */
|
|
1317
|
+
function withoutRaw(tiers) {
|
|
1318
|
+
const rollups = tiers.filter((t) => t !== "raw");
|
|
1319
|
+
return rollups.length > 0 ? rollups : ["5m"];
|
|
1320
|
+
}
|
|
1036
1321
|
//#endregion
|
|
1037
1322
|
//#region src/builtins/device-history/retention/departure-tracker.ts
|
|
1038
1323
|
var DEPARTED_ABSENCE_MS = 7 * DAY_MS;
|
|
@@ -1081,7 +1366,7 @@ var DepartureTracker = class {
|
|
|
1081
1366
|
//#region src/builtins/device-history/retention/retention.ts
|
|
1082
1367
|
var DEFAULT_RETENTION = {
|
|
1083
1368
|
rawDays: 7,
|
|
1084
|
-
fiveMinuteDays:
|
|
1369
|
+
fiveMinuteDays: 30,
|
|
1085
1370
|
hourDays: 365,
|
|
1086
1371
|
dayDays: null
|
|
1087
1372
|
};
|
|
@@ -1287,6 +1572,10 @@ function rollupDeclaration(collection) {
|
|
|
1287
1572
|
{
|
|
1288
1573
|
name: "tw_ms",
|
|
1289
1574
|
type: "INTEGER"
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: "state_ms",
|
|
1578
|
+
type: "TEXT"
|
|
1290
1579
|
}
|
|
1291
1580
|
]
|
|
1292
1581
|
};
|
|
@@ -1341,6 +1630,10 @@ var HISTORY_DECLARATIONS = [
|
|
|
1341
1630
|
name: "declared_at",
|
|
1342
1631
|
type: "INTEGER",
|
|
1343
1632
|
notNull: true
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
name: "enum_values",
|
|
1636
|
+
type: "TEXT"
|
|
1344
1637
|
}
|
|
1345
1638
|
]
|
|
1346
1639
|
},
|
|
@@ -1514,9 +1807,34 @@ function chunks(items, size) {
|
|
|
1514
1807
|
return out;
|
|
1515
1808
|
}
|
|
1516
1809
|
function parseKind(value) {
|
|
1517
|
-
if (value === "gauge" || value === "counter" || value === "boolean") return value;
|
|
1810
|
+
if (value === "gauge" || value === "counter" || value === "boolean" || value === "enum") return value;
|
|
1518
1811
|
throw new Error(`history row: unknown kind "${value}"`);
|
|
1519
1812
|
}
|
|
1813
|
+
/** A JSON TEXT column: the engine hands back the decoded value, or the text itself. */
|
|
1814
|
+
function jsonColumn(data, column) {
|
|
1815
|
+
const raw = data[column];
|
|
1816
|
+
if (raw === null || raw === void 0) return null;
|
|
1817
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1818
|
+
}
|
|
1819
|
+
/** A JSON column that must hold a string array, or nothing. */
|
|
1820
|
+
function parseValues(data) {
|
|
1821
|
+
const parsed = jsonColumn(data, "enum_values");
|
|
1822
|
+
if (parsed === null) return null;
|
|
1823
|
+
if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) return parsed;
|
|
1824
|
+
throw new Error("history row: column \"enum_values\" is not a JSON string array");
|
|
1825
|
+
}
|
|
1826
|
+
/** A JSON column that must hold `{ "<ordinal>": ms }`, or nothing. */
|
|
1827
|
+
function parseStates(data) {
|
|
1828
|
+
const parsed = jsonColumn(data, "state_ms");
|
|
1829
|
+
if (parsed === null) return null;
|
|
1830
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("history row: column \"state_ms\" is not a JSON object");
|
|
1831
|
+
const out = {};
|
|
1832
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1833
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error("history row: column \"state_ms\" holds a non-number");
|
|
1834
|
+
out[k] = v;
|
|
1835
|
+
}
|
|
1836
|
+
return out;
|
|
1837
|
+
}
|
|
1520
1838
|
function parseMetric(data) {
|
|
1521
1839
|
const source = readText(data, "source");
|
|
1522
1840
|
const subjectKind = readText(data, "subject_kind");
|
|
@@ -1533,7 +1851,8 @@ function parseMetric(data) {
|
|
|
1533
1851
|
unit: readTextOrNull(data, "unit"),
|
|
1534
1852
|
displayName: readText(data, "display_name"),
|
|
1535
1853
|
ingest,
|
|
1536
|
-
declaredAt: readNumber(data, "declared_at")
|
|
1854
|
+
declaredAt: readNumber(data, "declared_at"),
|
|
1855
|
+
values: parseValues(data)
|
|
1537
1856
|
};
|
|
1538
1857
|
}
|
|
1539
1858
|
function parseSeries(data) {
|
|
@@ -1558,7 +1877,8 @@ function parseRollup(data) {
|
|
|
1558
1877
|
last: readNumberOrNull(data, "last"),
|
|
1559
1878
|
lastTs: readNumberOrNull(data, "last_ts"),
|
|
1560
1879
|
twSum: readNumberOrNull(data, "tw_sum"),
|
|
1561
|
-
twMs: readNumberOrNull(data, "tw_ms")
|
|
1880
|
+
twMs: readNumberOrNull(data, "tw_ms"),
|
|
1881
|
+
states: parseStates(data)
|
|
1562
1882
|
};
|
|
1563
1883
|
}
|
|
1564
1884
|
function rollupData(row) {
|
|
@@ -1572,7 +1892,8 @@ function rollupData(row) {
|
|
|
1572
1892
|
last: row.last,
|
|
1573
1893
|
last_ts: row.lastTs,
|
|
1574
1894
|
tw_sum: row.twSum,
|
|
1575
|
-
tw_ms: row.twMs
|
|
1895
|
+
tw_ms: row.twMs,
|
|
1896
|
+
state_ms: row.states === null ? null : JSON.stringify(row.states)
|
|
1576
1897
|
};
|
|
1577
1898
|
}
|
|
1578
1899
|
function collectionOf(tier) {
|
|
@@ -1596,22 +1917,37 @@ var HistoryStore = class {
|
|
|
1596
1917
|
filter: { limit: 2e4 }
|
|
1597
1918
|
})).map((r) => parseMetric(r.data));
|
|
1598
1919
|
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Delete metric rows no lookup can reach: rows an earlier build wrote under an
|
|
1922
|
+
* engine-minted random key (every metric key is `cap:` / `addon:` prefixed).
|
|
1923
|
+
*/
|
|
1924
|
+
async deleteMetrics(keys) {
|
|
1925
|
+
for (const batch of chunks(keys, WRITE_CHUNK)) await this.door.deleteWhere.mutate({
|
|
1926
|
+
namespace: HISTORY_NAMESPACE,
|
|
1927
|
+
collection: COLLECTIONS.metrics,
|
|
1928
|
+
filter: { whereIn: { key: [...batch] } }
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1599
1931
|
async upsertMetric(metric) {
|
|
1600
1932
|
await this.door.upsertMany.mutate({
|
|
1601
1933
|
namespace: HISTORY_NAMESPACE,
|
|
1602
1934
|
collection: COLLECTIONS.metrics,
|
|
1603
1935
|
conflict: ["key"],
|
|
1604
|
-
records: [{
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1936
|
+
records: [{
|
|
1937
|
+
id: metric.key,
|
|
1938
|
+
data: {
|
|
1939
|
+
key: metric.key,
|
|
1940
|
+
source: metric.source,
|
|
1941
|
+
owner_addon: metric.ownerAddon,
|
|
1942
|
+
subject_kind: metric.subjectKind,
|
|
1943
|
+
kind: metric.kind,
|
|
1944
|
+
unit: metric.unit,
|
|
1945
|
+
display_name: metric.displayName,
|
|
1946
|
+
ingest: metric.ingest,
|
|
1947
|
+
declared_at: metric.declaredAt,
|
|
1948
|
+
enum_values: metric.values === null ? null : JSON.stringify(metric.values)
|
|
1949
|
+
}
|
|
1950
|
+
}]
|
|
1615
1951
|
});
|
|
1616
1952
|
}
|
|
1617
1953
|
/** Series with `id > afterId`, ascending — the boot load pages with this. */
|
|
@@ -1858,6 +2194,11 @@ var HistoryStore = class {
|
|
|
1858
2194
|
* The held value also counts toward `min`/`max` when it was in force at the
|
|
1859
2195
|
* start of a bucket, because it really was the series' value then.
|
|
1860
2196
|
*/
|
|
2197
|
+
function credit(states, value, ms) {
|
|
2198
|
+
if (states === null || ms <= 0) return;
|
|
2199
|
+
const key = String(value);
|
|
2200
|
+
states[key] = (states[key] ?? 0) + ms;
|
|
2201
|
+
}
|
|
1861
2202
|
/** One row per 5-minute bucket that has at least one point. */
|
|
1862
2203
|
function rawToFiveMinuteRows(input) {
|
|
1863
2204
|
const rows = [];
|
|
@@ -1876,6 +2217,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1876
2217
|
let max = Number.NEGATIVE_INFINITY;
|
|
1877
2218
|
let twSum = 0;
|
|
1878
2219
|
let twMs = 0;
|
|
2220
|
+
const states = input.trackStates === true ? {} : null;
|
|
1879
2221
|
let cursor = bucketStart;
|
|
1880
2222
|
let value = held;
|
|
1881
2223
|
if (value !== null) {
|
|
@@ -1890,6 +2232,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1890
2232
|
const dt = p.ts - cursor;
|
|
1891
2233
|
twSum += value * dt;
|
|
1892
2234
|
twMs += dt;
|
|
2235
|
+
credit(states, value, dt);
|
|
1893
2236
|
}
|
|
1894
2237
|
cursor = p.ts;
|
|
1895
2238
|
value = p.value;
|
|
@@ -1903,6 +2246,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1903
2246
|
if (value !== null && coverageEnd > cursor) {
|
|
1904
2247
|
twSum += value * (coverageEnd - cursor);
|
|
1905
2248
|
twMs += coverageEnd - cursor;
|
|
2249
|
+
credit(states, value, coverageEnd - cursor);
|
|
1906
2250
|
}
|
|
1907
2251
|
held = last.value;
|
|
1908
2252
|
rows.push({
|
|
@@ -1915,7 +2259,8 @@ function rawToFiveMinuteRows(input) {
|
|
|
1915
2259
|
last: last.value,
|
|
1916
2260
|
lastTs: last.ts,
|
|
1917
2261
|
twSum,
|
|
1918
|
-
twMs
|
|
2262
|
+
twMs,
|
|
2263
|
+
states
|
|
1919
2264
|
});
|
|
1920
2265
|
}
|
|
1921
2266
|
return rows;
|
|
@@ -1932,6 +2277,7 @@ function combineRows(input) {
|
|
|
1932
2277
|
let max = Number.NEGATIVE_INFINITY;
|
|
1933
2278
|
let twSum = 0;
|
|
1934
2279
|
let twMs = 0;
|
|
2280
|
+
const states = input.trackStates === true ? {} : null;
|
|
1935
2281
|
let held = input.carryIn;
|
|
1936
2282
|
let cursor = input.windowStart;
|
|
1937
2283
|
for (const row of rows) {
|
|
@@ -1943,6 +2289,7 @@ function combineRows(input) {
|
|
|
1943
2289
|
if (hasTw && gap > 0 && held !== null) {
|
|
1944
2290
|
twSum += held * gap;
|
|
1945
2291
|
twMs += gap;
|
|
2292
|
+
credit(states, held, gap);
|
|
1946
2293
|
if (held < min) min = held;
|
|
1947
2294
|
if (held > max) max = held;
|
|
1948
2295
|
}
|
|
@@ -1950,6 +2297,7 @@ function combineRows(input) {
|
|
|
1950
2297
|
twSum += row.twSum;
|
|
1951
2298
|
twMs += row.twMs;
|
|
1952
2299
|
}
|
|
2300
|
+
if (states !== null && row.states !== null) for (const [state, ms] of Object.entries(row.states)) states[state] = (states[state] ?? 0) + ms;
|
|
1953
2301
|
cursor = row.bucketStart + input.childWidthMs;
|
|
1954
2302
|
held = row.last;
|
|
1955
2303
|
}
|
|
@@ -1957,6 +2305,7 @@ function combineRows(input) {
|
|
|
1957
2305
|
if (hasTw && held !== null && coverageEnd > cursor) {
|
|
1958
2306
|
twSum += held * (coverageEnd - cursor);
|
|
1959
2307
|
twMs += coverageEnd - cursor;
|
|
2308
|
+
credit(states, held, coverageEnd - cursor);
|
|
1960
2309
|
}
|
|
1961
2310
|
return {
|
|
1962
2311
|
seriesId: input.seriesId,
|
|
@@ -1968,7 +2317,8 @@ function combineRows(input) {
|
|
|
1968
2317
|
last: lastRow.last,
|
|
1969
2318
|
lastTs: lastRow.lastTs,
|
|
1970
2319
|
twSum: hasTw ? twSum : null,
|
|
1971
|
-
twMs: hasTw ? twMs : null
|
|
2320
|
+
twMs: hasTw ? twMs : null,
|
|
2321
|
+
states
|
|
1972
2322
|
};
|
|
1973
2323
|
}
|
|
1974
2324
|
/** How far back a boot rebuilds from raw and re-marks the upper tiers. */
|
|
@@ -2109,6 +2459,11 @@ var RollupEngine = class {
|
|
|
2109
2459
|
}
|
|
2110
2460
|
if (handled > 0) this.deps.counters.last1dAt = now;
|
|
2111
2461
|
}
|
|
2462
|
+
/** An `enum` series keeps the time spent in each state in its rollups. */
|
|
2463
|
+
isEnum(seriesId) {
|
|
2464
|
+
const series = this.deps.registry.byIdOf(seriesId);
|
|
2465
|
+
return series !== void 0 && this.deps.registry.metric(series.key)?.kind === "enum";
|
|
2466
|
+
}
|
|
2112
2467
|
/** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
|
|
2113
2468
|
async recomputeFiveMinute(seriesId, from, toExclusive, horizon) {
|
|
2114
2469
|
const { store } = this.deps;
|
|
@@ -2125,7 +2480,8 @@ var RollupEngine = class {
|
|
|
2125
2480
|
seriesId,
|
|
2126
2481
|
points,
|
|
2127
2482
|
carryIn: await store.rawBefore(seriesId, from),
|
|
2128
|
-
horizon
|
|
2483
|
+
horizon,
|
|
2484
|
+
trackStates: this.isEnum(seriesId)
|
|
2129
2485
|
});
|
|
2130
2486
|
if (rows.length === 0) return 0;
|
|
2131
2487
|
await store.upsertRollups("5m", rows);
|
|
@@ -2149,7 +2505,8 @@ var RollupEngine = class {
|
|
|
2149
2505
|
childWidthMs,
|
|
2150
2506
|
rows: await store.readRollups(child, seriesId, windowStart, windowEnd - 1, READ_PAGE),
|
|
2151
2507
|
carryIn: (await store.rollupBefore(child, seriesId, windowStart))?.last ?? null,
|
|
2152
|
-
horizon: now
|
|
2508
|
+
horizon: now,
|
|
2509
|
+
trackStates: this.isEnum(seriesId)
|
|
2153
2510
|
});
|
|
2154
2511
|
if (combined === null) return;
|
|
2155
2512
|
await store.upsertRollups(parent, [combined]);
|
|
@@ -2656,7 +3013,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2656
3013
|
}),
|
|
2657
3014
|
listNodeSeries: async (input) => this.requireRuntime().query.listSeries({
|
|
2658
3015
|
kind: "node",
|
|
2659
|
-
nodeId: input.
|
|
3016
|
+
nodeId: input.forNodeId
|
|
2660
3017
|
}),
|
|
2661
3018
|
query: async ({ deviceId, ...window }) => this.requireRuntime().query.query({
|
|
2662
3019
|
...window,
|
|
@@ -2665,13 +3022,14 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2665
3022
|
deviceId
|
|
2666
3023
|
}
|
|
2667
3024
|
}),
|
|
2668
|
-
queryNode: async ({
|
|
3025
|
+
queryNode: async ({ forNodeId, ...window }) => this.requireRuntime().query.query({
|
|
2669
3026
|
...window,
|
|
2670
3027
|
subject: {
|
|
2671
3028
|
kind: "node",
|
|
2672
|
-
nodeId
|
|
3029
|
+
nodeId: forNodeId
|
|
2673
3030
|
}
|
|
2674
3031
|
}),
|
|
3032
|
+
queryMany: async (input) => ({ results: await this.requireRuntime().query.queryMany(input) }),
|
|
2675
3033
|
declareSeries: async (input) => this.requireRuntime().ingest.declareSeries(callerOf(input), input),
|
|
2676
3034
|
record: async (input) => this.requireRuntime().ingest.record(callerOf(input), input.points),
|
|
2677
3035
|
recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
|