@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
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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
|
|
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
|
|
927
|
-
const
|
|
928
|
-
const
|
|
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
|
-
|
|
949
|
-
|
|
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
|
-
|
|
974
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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:
|
|
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: [{
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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.
|
|
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 ({
|
|
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),
|