iobroker.anomaly-detection 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/admin/anomaly-detection-icon.png +0 -0
  4. package/admin/anomaly-detection.png +0 -0
  5. package/admin/i18n/de.json +32 -0
  6. package/admin/i18n/en.json +32 -0
  7. package/admin/i18n/es.json +32 -0
  8. package/admin/i18n/fr.json +32 -0
  9. package/admin/i18n/it.json +32 -0
  10. package/admin/i18n/nl.json +32 -0
  11. package/admin/i18n/pl.json +32 -0
  12. package/admin/i18n/pt.json +32 -0
  13. package/admin/i18n/ru.json +32 -0
  14. package/admin/i18n/uk.json +32 -0
  15. package/admin/i18n/zh-cn.json +32 -0
  16. package/admin/jsonConfig.json +220 -0
  17. package/build/lib/advanced-detectors.js +149 -0
  18. package/build/lib/advanced-detectors.js.map +7 -0
  19. package/build/lib/context-value.js +42 -0
  20. package/build/lib/context-value.js.map +7 -0
  21. package/build/lib/detectors.js +76 -0
  22. package/build/lib/detectors.js.map +7 -0
  23. package/build/lib/history-provider.js +58 -0
  24. package/build/lib/history-provider.js.map +7 -0
  25. package/build/lib/history-source-resolver.js +109 -0
  26. package/build/lib/history-source-resolver.js.map +7 -0
  27. package/build/lib/model/contextual-model.js +98 -0
  28. package/build/lib/model/contextual-model.js.map +7 -0
  29. package/build/lib/model/statistics.js +78 -0
  30. package/build/lib/model/statistics.js.map +7 -0
  31. package/build/lib/model/temporal-model.js +98 -0
  32. package/build/lib/model/temporal-model.js.map +7 -0
  33. package/build/lib/scoring.js +57 -0
  34. package/build/lib/scoring.js.map +7 -0
  35. package/build/lib/source-cleanup.js +58 -0
  36. package/build/lib/source-cleanup.js.map +7 -0
  37. package/build/lib/source-monitor.js +348 -0
  38. package/build/lib/source-monitor.js.map +7 -0
  39. package/build/main.js +589 -0
  40. package/build/main.js.map +7 -0
  41. package/io-package.json +95 -0
  42. package/package.json +105 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/detectors.ts"],
4
+ "sourcesContent": ["import { clamp, type SampleSeries } from \"./model/statistics\";\n\nexport interface DetectorResult {\n\tname: \"value\" | \"context\" | \"rate\" | \"stuck\" | \"changePoint\" | \"trend\";\n\tscore: number;\n\treason: string;\n}\n\nexport function detectMadDeviation(\n\tvalue: number,\n\tseries: SampleSeries,\n\tminimumSamples: number,\n\tsensitivity: number,\n): DetectorResult | undefined {\n\tif (series.count < minimumSamples) {\n\t\treturn undefined;\n\t}\n\tconst median = series.median();\n\tconst mad = series.mad();\n\tif (median === undefined || mad === undefined) {\n\t\treturn undefined;\n\t}\n\tconst deviation = Math.abs(value - median);\n\tconst robustZ = mad === 0 ? (deviation === 0 ? 0 : Number.POSITIVE_INFINITY) : (0.6745 * deviation) / mad;\n\tconst score = robustZ === Number.POSITIVE_INFINITY ? 100 : clamp((robustZ / sensitivity) * 100, 0, 100);\n\treturn { name: \"value\", score, reason: \"Value is significantly outside the normal range for this time period\" };\n}\n\nexport function detectRateDeviation(\n\trate: number | undefined,\n\tseries: SampleSeries,\n\tminimumSamples: number,\n\tsensitivity: number,\n): DetectorResult | undefined {\n\tif (rate === undefined || !Number.isFinite(rate) || series.count < minimumSamples) {\n\t\treturn undefined;\n\t}\n\tconst median = series.median();\n\tconst mad = series.mad();\n\tif (median === undefined || mad === undefined) {\n\t\treturn undefined;\n\t}\n\tconst deviation = Math.abs(rate - median);\n\tconst robustZ = mad === 0 ? (deviation === 0 ? 0 : Number.POSITIVE_INFINITY) : (0.6745 * deviation) / mad;\n\tconst score = robustZ === Number.POSITIVE_INFINITY ? 100 : clamp((robustZ / sensitivity) * 100, 0, 100);\n\treturn { name: \"rate\", score, reason: \"Rate of change is unusually high\" };\n}\n\nexport function detectStuck(\n\trepeatedSince: number | undefined,\n\ttimestamp: number,\n\tstuckDurationMinutes: number,\n): DetectorResult | undefined {\n\tif (repeatedSince === undefined || timestamp < repeatedSince) {\n\t\treturn undefined;\n\t}\n\tconst duration = timestamp - repeatedSince;\n\tconst limit = stuckDurationMinutes * 60_000;\n\tif (limit <= 0 || duration < limit) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tname: \"stuck\",\n\t\tscore: clamp((duration / limit) * 50, 50, 100),\n\t\treason: \"Value has remained unchanged significantly longer than configured\",\n\t};\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAyC;AAQlC,SAAS,mBACf,OACA,QACA,gBACA,aAC6B;AAC7B,MAAI,OAAO,QAAQ,gBAAgB;AAClC,WAAO;AAAA,EACR;AACA,QAAM,SAAS,OAAO,OAAO;AAC7B,QAAM,MAAM,OAAO,IAAI;AACvB,MAAI,WAAW,UAAa,QAAQ,QAAW;AAC9C,WAAO;AAAA,EACR;AACA,QAAM,YAAY,KAAK,IAAI,QAAQ,MAAM;AACzC,QAAM,UAAU,QAAQ,IAAK,cAAc,IAAI,IAAI,OAAO,oBAAsB,SAAS,YAAa;AACtG,QAAM,QAAQ,YAAY,OAAO,oBAAoB,UAAM,yBAAO,UAAU,cAAe,KAAK,GAAG,GAAG;AACtG,SAAO,EAAE,MAAM,SAAS,OAAO,QAAQ,uEAAuE;AAC/G;AAEO,SAAS,oBACf,MACA,QACA,gBACA,aAC6B;AAC7B,MAAI,SAAS,UAAa,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,QAAQ,gBAAgB;AAClF,WAAO;AAAA,EACR;AACA,QAAM,SAAS,OAAO,OAAO;AAC7B,QAAM,MAAM,OAAO,IAAI;AACvB,MAAI,WAAW,UAAa,QAAQ,QAAW;AAC9C,WAAO;AAAA,EACR;AACA,QAAM,YAAY,KAAK,IAAI,OAAO,MAAM;AACxC,QAAM,UAAU,QAAQ,IAAK,cAAc,IAAI,IAAI,OAAO,oBAAsB,SAAS,YAAa;AACtG,QAAM,QAAQ,YAAY,OAAO,oBAAoB,UAAM,yBAAO,UAAU,cAAe,KAAK,GAAG,GAAG;AACtG,SAAO,EAAE,MAAM,QAAQ,OAAO,QAAQ,mCAAmC;AAC1E;AAEO,SAAS,YACf,eACA,WACA,sBAC6B;AAC7B,MAAI,kBAAkB,UAAa,YAAY,eAAe;AAC7D,WAAO;AAAA,EACR;AACA,QAAM,WAAW,YAAY;AAC7B,QAAM,QAAQ,uBAAuB;AACrC,MAAI,SAAS,KAAK,WAAW,OAAO;AACnC,WAAO;AAAA,EACR;AACA,SAAO;AAAA,IACN,MAAM;AAAA,IACN,WAAO,yBAAO,WAAW,QAAS,IAAI,IAAI,GAAG;AAAA,IAC7C,QAAQ;AAAA,EACT;AACD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var history_provider_exports = {};
20
+ __export(history_provider_exports, {
21
+ sanitizeHistory: () => sanitizeHistory
22
+ });
23
+ module.exports = __toCommonJS(history_provider_exports);
24
+ function sanitizeHistory(raw, limit) {
25
+ if (!Number.isInteger(limit) || limit < 1) {
26
+ return [];
27
+ }
28
+ const entries = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.result) ? raw.result : [];
29
+ const byTimestamp = /* @__PURE__ */ new Map();
30
+ for (const entry of entries) {
31
+ if (!entry || typeof entry !== "object") {
32
+ continue;
33
+ }
34
+ const candidate = entry;
35
+ const value = typeof candidate.val === "number" ? candidate.val : Number(candidate.val);
36
+ const timestamp = typeof candidate.ts === "number" ? candidate.ts : Number(candidate.ts);
37
+ if (Number.isFinite(value) && Number.isFinite(timestamp) && timestamp > 0) {
38
+ byTimestamp.set(timestamp, { value, timestamp });
39
+ }
40
+ }
41
+ const sorted = [...byTimestamp.values()].sort((left, right) => left.timestamp - right.timestamp);
42
+ if (sorted.length <= limit) {
43
+ return sorted;
44
+ }
45
+ if (limit === 1) {
46
+ return [sorted[0]];
47
+ }
48
+ const result = [];
49
+ for (let index = 0; index < limit; index++) {
50
+ result.push(sorted[Math.round(index * (sorted.length - 1) / (limit - 1))]);
51
+ }
52
+ return result;
53
+ }
54
+ // Annotate the CommonJS export names for ESM import in node:
55
+ 0 && (module.exports = {
56
+ sanitizeHistory
57
+ });
58
+ //# sourceMappingURL=history-provider.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/history-provider.ts"],
4
+ "sourcesContent": ["/** A numerical state sample returned by a history provider. */\nexport interface HistorySample {\n\tvalue: number;\n\ttimestamp: number;\n}\n\n/** Generic access contract for an ioBroker history adapter. */\nexport interface HistoryProvider {\n\tgetHistory(sourceId: string, start: number, end: number, limit: number): Promise<unknown>;\n}\n\n/**\n * Normalizes the result shapes commonly returned by ioBroker history adapters.\n * Duplicate timestamps keep the final value; evenly spaced downsampling preserves whole-range coverage.\n *\n * @param raw History response to normalize.\n * @param limit Maximum number of representative samples.\n */\nexport function sanitizeHistory(raw: unknown, limit: number): HistorySample[] {\n\tif (!Number.isInteger(limit) || limit < 1) {\n\t\treturn [];\n\t}\n\tconst entries = Array.isArray(raw)\n\t\t? raw\n\t\t: raw && typeof raw === \"object\" && Array.isArray((raw as { result?: unknown }).result)\n\t\t\t? (raw as { result: unknown[] }).result\n\t\t\t: [];\n\tconst byTimestamp = new Map<number, HistorySample>();\n\tfor (const entry of entries) {\n\t\tif (!entry || typeof entry !== \"object\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst candidate = entry as { val?: unknown; ts?: unknown };\n\t\tconst value = typeof candidate.val === \"number\" ? candidate.val : Number(candidate.val);\n\t\tconst timestamp = typeof candidate.ts === \"number\" ? candidate.ts : Number(candidate.ts);\n\t\tif (Number.isFinite(value) && Number.isFinite(timestamp) && timestamp > 0) {\n\t\t\tbyTimestamp.set(timestamp, { value, timestamp });\n\t\t}\n\t}\n\tconst sorted = [...byTimestamp.values()].sort((left, right) => left.timestamp - right.timestamp);\n\tif (sorted.length <= limit) {\n\t\treturn sorted;\n\t}\n\tif (limit === 1) {\n\t\treturn [sorted[0]];\n\t}\n\tconst result: HistorySample[] = [];\n\tfor (let index = 0; index < limit; index++) {\n\t\tresult.push(sorted[Math.round((index * (sorted.length - 1)) / (limit - 1))]);\n\t}\n\treturn result;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBO,SAAS,gBAAgB,KAAc,OAAgC;AAC7E,MAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AAC1C,WAAO,CAAC;AAAA,EACT;AACA,QAAM,UAAU,MAAM,QAAQ,GAAG,IAC9B,MACA,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAS,IAA6B,MAAM,IAClF,IAA8B,SAC/B,CAAC;AACL,QAAM,cAAc,oBAAI,IAA2B;AACnD,aAAW,SAAS,SAAS;AAC5B,QAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACxC;AAAA,IACD;AACA,UAAM,YAAY;AAClB,UAAM,QAAQ,OAAO,UAAU,QAAQ,WAAW,UAAU,MAAM,OAAO,UAAU,GAAG;AACtF,UAAM,YAAY,OAAO,UAAU,OAAO,WAAW,UAAU,KAAK,OAAO,UAAU,EAAE;AACvF,QAAI,OAAO,SAAS,KAAK,KAAK,OAAO,SAAS,SAAS,KAAK,YAAY,GAAG;AAC1E,kBAAY,IAAI,WAAW,EAAE,OAAO,UAAU,CAAC;AAAA,IAChD;AAAA,EACD;AACA,QAAM,SAAS,CAAC,GAAG,YAAY,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,YAAY,MAAM,SAAS;AAC/F,MAAI,OAAO,UAAU,OAAO;AAC3B,WAAO;AAAA,EACR;AACA,MAAI,UAAU,GAAG;AAChB,WAAO,CAAC,OAAO,CAAC,CAAC;AAAA,EAClB;AACA,QAAM,SAA0B,CAAC;AACjC,WAAS,QAAQ,GAAG,QAAQ,OAAO,SAAS;AAC3C,WAAO,KAAK,OAAO,KAAK,MAAO,SAAS,OAAO,SAAS,MAAO,QAAQ,EAAE,CAAC,CAAC;AAAA,EAC5E;AACA,SAAO;AACR;",
6
+ "names": []
7
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var history_source_resolver_exports = {};
20
+ __export(history_source_resolver_exports, {
21
+ HistorySourceResolver: () => HistorySourceResolver,
22
+ isCompleteInstanceId: () => isCompleteInstanceId,
23
+ selectHistorySource: () => selectHistorySource
24
+ });
25
+ module.exports = __toCommonJS(history_source_resolver_exports);
26
+ class HistorySourceResolver {
27
+ constructor(objects) {
28
+ this.objects = objects;
29
+ }
30
+ async resolve(configuredSourceId) {
31
+ const configuredObject = await this.objects.getForeignObjectAsync(configuredSourceId);
32
+ if (!isStateObject(configuredObject)) {
33
+ return [];
34
+ }
35
+ const targetId = getAliasReadTarget(configuredObject);
36
+ const sources = [
37
+ { id: configuredSourceId, object: configuredObject }
38
+ ];
39
+ if (targetId && targetId !== configuredSourceId) {
40
+ const targetObject = await this.objects.getForeignObjectAsync(targetId);
41
+ if (isStateObject(targetObject)) {
42
+ sources.push({ id: targetId, object: targetObject });
43
+ }
44
+ }
45
+ const resolved = /* @__PURE__ */ new Map();
46
+ for (const source of sources) {
47
+ for (const instanceId of enabledHistoryInstances(source.object)) {
48
+ if (resolved.has(instanceId) || !await this.isUsableHistoryInstance(instanceId)) {
49
+ continue;
50
+ }
51
+ resolved.set(instanceId, { instanceId, sourceId: source.id });
52
+ }
53
+ }
54
+ return [...resolved.values()].sort((left, right) => left.instanceId.localeCompare(right.instanceId));
55
+ }
56
+ async isUsableHistoryInstance(instanceId) {
57
+ var _a;
58
+ if (!isCompleteInstanceId(instanceId)) {
59
+ return false;
60
+ }
61
+ const instance = await this.objects.getForeignObjectAsync(`system.adapter.${instanceId}`);
62
+ if (!instance || instance.type !== "instance" || instance.common.enabled !== true) {
63
+ return false;
64
+ }
65
+ return instance.common.getHistory === true || ((_a = instance.common.supportedMessages) == null ? void 0 : _a.getHistory) === true;
66
+ }
67
+ }
68
+ function isCompleteInstanceId(value) {
69
+ return typeof value === "string" && /^[A-Za-z0-9_-]+\.\d+$/.test(value);
70
+ }
71
+ function selectHistorySource(configuredSourceId, instanceId, candidates) {
72
+ if (!isCompleteInstanceId(instanceId)) {
73
+ throw new Error(
74
+ `Invalid history instance '${instanceId}' configured for ${configuredSourceId}. Select a complete instance ID such as 'influxdb.0'.`
75
+ );
76
+ }
77
+ const source = candidates.find((candidate) => candidate.instanceId === instanceId);
78
+ if (!source) {
79
+ throw new Error(
80
+ `History instance '${instanceId}' is not enabled for ${configuredSourceId} or its alias target. Select a current history source.`
81
+ );
82
+ }
83
+ return source;
84
+ }
85
+ function isStateObject(object) {
86
+ return (object == null ? void 0 : object.type) === "state";
87
+ }
88
+ function getAliasReadTarget(object) {
89
+ var _a;
90
+ const aliasId = (_a = object.common.alias) == null ? void 0 : _a.id;
91
+ if (typeof aliasId === "string") {
92
+ return aliasId;
93
+ }
94
+ if (aliasId && typeof aliasId.read === "string" && aliasId.read.trim()) {
95
+ return aliasId.read;
96
+ }
97
+ return void 0;
98
+ }
99
+ function enabledHistoryInstances(object) {
100
+ var _a;
101
+ return Object.entries((_a = object.common.custom) != null ? _a : {}).filter(([instanceId, settings]) => isCompleteInstanceId(instanceId) && (settings == null ? void 0 : settings.enabled) === true).map(([instanceId]) => instanceId);
102
+ }
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ HistorySourceResolver,
106
+ isCompleteInstanceId,
107
+ selectHistorySource
108
+ });
109
+ //# sourceMappingURL=history-source-resolver.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/history-source-resolver.ts"],
4
+ "sourcesContent": ["export interface HistorySource {\n\tinstanceId: string;\n\tsourceId: string;\n}\n\nexport interface HistoryObjectStore {\n\tgetForeignObjectAsync(id: string): Promise<ioBroker.Object | null | undefined>;\n}\n\n/** Resolves enabled per-state history settings without querying a history database. */\nexport class HistorySourceResolver {\n\tpublic constructor(private readonly objects: HistoryObjectStore) {}\n\n\tpublic async resolve(configuredSourceId: string): Promise<HistorySource[]> {\n\t\tconst configuredObject = await this.objects.getForeignObjectAsync(configuredSourceId);\n\t\tif (!isStateObject(configuredObject)) {\n\t\t\treturn [];\n\t\t}\n\t\tconst targetId = getAliasReadTarget(configuredObject);\n\t\tconst sources: Array<{ id: string; object: ioBroker.StateObject }> = [\n\t\t\t{ id: configuredSourceId, object: configuredObject },\n\t\t];\n\t\tif (targetId && targetId !== configuredSourceId) {\n\t\t\tconst targetObject = await this.objects.getForeignObjectAsync(targetId);\n\t\t\tif (isStateObject(targetObject)) {\n\t\t\t\tsources.push({ id: targetId, object: targetObject });\n\t\t\t}\n\t\t}\n\n\t\tconst resolved = new Map<string, HistorySource>();\n\t\tfor (const source of sources) {\n\t\t\tfor (const instanceId of enabledHistoryInstances(source.object)) {\n\t\t\t\tif (resolved.has(instanceId) || !(await this.isUsableHistoryInstance(instanceId))) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// The configured alias is visited first, so its own history wins over its target's history.\n\t\t\t\tresolved.set(instanceId, { instanceId, sourceId: source.id });\n\t\t\t}\n\t\t}\n\t\treturn [...resolved.values()].sort((left, right) => left.instanceId.localeCompare(right.instanceId));\n\t}\n\n\tprivate async isUsableHistoryInstance(instanceId: string): Promise<boolean> {\n\t\tif (!isCompleteInstanceId(instanceId)) {\n\t\t\treturn false;\n\t\t}\n\t\tconst instance = await this.objects.getForeignObjectAsync(`system.adapter.${instanceId}`);\n\t\tif (!instance || instance.type !== \"instance\" || instance.common.enabled !== true) {\n\t\t\treturn false;\n\t\t}\n\t\treturn instance.common.getHistory === true || instance.common.supportedMessages?.getHistory === true;\n\t}\n}\n\n/** True only for a complete ioBroker adapter instance ID such as `influxdb.0`. */\nexport function isCompleteInstanceId(value: unknown): boolean {\n\treturn typeof value === \"string\" && /^[A-Za-z0-9_-]+\\.\\d+$/.test(value);\n}\n\n/** Validates a persisted selection against the currently enabled history sources. */\nexport function selectHistorySource(\n\tconfiguredSourceId: string,\n\tinstanceId: string,\n\tcandidates: readonly HistorySource[],\n): HistorySource {\n\tif (!isCompleteInstanceId(instanceId)) {\n\t\tthrow new Error(\n\t\t\t`Invalid history instance '${instanceId}' configured for ${configuredSourceId}. Select a complete instance ID such as 'influxdb.0'.`,\n\t\t);\n\t}\n\tconst source = candidates.find(candidate => candidate.instanceId === instanceId);\n\tif (!source) {\n\t\tthrow new Error(\n\t\t\t`History instance '${instanceId}' is not enabled for ${configuredSourceId} or its alias target. Select a current history source.`,\n\t\t);\n\t}\n\treturn source;\n}\n\nfunction isStateObject(object: ioBroker.Object | null | undefined): object is ioBroker.StateObject {\n\treturn object?.type === \"state\";\n}\n\nfunction getAliasReadTarget(object: ioBroker.StateObject): string | undefined {\n\tconst aliasId = object.common.alias?.id;\n\tif (typeof aliasId === \"string\") {\n\t\treturn aliasId;\n\t}\n\tif (aliasId && typeof aliasId.read === \"string\" && aliasId.read.trim()) {\n\t\treturn aliasId.read;\n\t}\n\treturn undefined;\n}\n\nfunction enabledHistoryInstances(object: ioBroker.StateObject): string[] {\n\treturn Object.entries(object.common.custom ?? {})\n\t\t.filter(([instanceId, settings]) => isCompleteInstanceId(instanceId) && settings?.enabled === true)\n\t\t.map(([instanceId]) => instanceId);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,sBAAsB;AAAA,EAC3B,YAA6B,SAA6B;AAA7B;AAAA,EAA8B;AAAA,EAElE,MAAa,QAAQ,oBAAsD;AAC1E,UAAM,mBAAmB,MAAM,KAAK,QAAQ,sBAAsB,kBAAkB;AACpF,QAAI,CAAC,cAAc,gBAAgB,GAAG;AACrC,aAAO,CAAC;AAAA,IACT;AACA,UAAM,WAAW,mBAAmB,gBAAgB;AACpD,UAAM,UAA+D;AAAA,MACpE,EAAE,IAAI,oBAAoB,QAAQ,iBAAiB;AAAA,IACpD;AACA,QAAI,YAAY,aAAa,oBAAoB;AAChD,YAAM,eAAe,MAAM,KAAK,QAAQ,sBAAsB,QAAQ;AACtE,UAAI,cAAc,YAAY,GAAG;AAChC,gBAAQ,KAAK,EAAE,IAAI,UAAU,QAAQ,aAAa,CAAC;AAAA,MACpD;AAAA,IACD;AAEA,UAAM,WAAW,oBAAI,IAA2B;AAChD,eAAW,UAAU,SAAS;AAC7B,iBAAW,cAAc,wBAAwB,OAAO,MAAM,GAAG;AAChE,YAAI,SAAS,IAAI,UAAU,KAAK,CAAE,MAAM,KAAK,wBAAwB,UAAU,GAAI;AAClF;AAAA,QACD;AAEA,iBAAS,IAAI,YAAY,EAAE,YAAY,UAAU,OAAO,GAAG,CAAC;AAAA,MAC7D;AAAA,IACD;AACA,WAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,WAAW,cAAc,MAAM,UAAU,CAAC;AAAA,EACpG;AAAA,EAEA,MAAc,wBAAwB,YAAsC;AA1C7E;AA2CE,QAAI,CAAC,qBAAqB,UAAU,GAAG;AACtC,aAAO;AAAA,IACR;AACA,UAAM,WAAW,MAAM,KAAK,QAAQ,sBAAsB,kBAAkB,UAAU,EAAE;AACxF,QAAI,CAAC,YAAY,SAAS,SAAS,cAAc,SAAS,OAAO,YAAY,MAAM;AAClF,aAAO;AAAA,IACR;AACA,WAAO,SAAS,OAAO,eAAe,UAAQ,cAAS,OAAO,sBAAhB,mBAAmC,gBAAe;AAAA,EACjG;AACD;AAGO,SAAS,qBAAqB,OAAyB;AAC7D,SAAO,OAAO,UAAU,YAAY,wBAAwB,KAAK,KAAK;AACvE;AAGO,SAAS,oBACf,oBACA,YACA,YACgB;AAChB,MAAI,CAAC,qBAAqB,UAAU,GAAG;AACtC,UAAM,IAAI;AAAA,MACT,6BAA6B,UAAU,oBAAoB,kBAAkB;AAAA,IAC9E;AAAA,EACD;AACA,QAAM,SAAS,WAAW,KAAK,eAAa,UAAU,eAAe,UAAU;AAC/E,MAAI,CAAC,QAAQ;AACZ,UAAM,IAAI;AAAA,MACT,qBAAqB,UAAU,wBAAwB,kBAAkB;AAAA,IAC1E;AAAA,EACD;AACA,SAAO;AACR;AAEA,SAAS,cAAc,QAA4E;AAClG,UAAO,iCAAQ,UAAS;AACzB;AAEA,SAAS,mBAAmB,QAAkD;AAnF9E;AAoFC,QAAM,WAAU,YAAO,OAAO,UAAd,mBAAqB;AACrC,MAAI,OAAO,YAAY,UAAU;AAChC,WAAO;AAAA,EACR;AACA,MAAI,WAAW,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,KAAK,GAAG;AACvE,WAAO,QAAQ;AAAA,EAChB;AACA,SAAO;AACR;AAEA,SAAS,wBAAwB,QAAwC;AA9FzE;AA+FC,SAAO,OAAO,SAAQ,YAAO,OAAO,WAAd,YAAwB,CAAC,CAAC,EAC9C,OAAO,CAAC,CAAC,YAAY,QAAQ,MAAM,qBAAqB,UAAU,MAAK,qCAAU,aAAY,IAAI,EACjG,IAAI,CAAC,CAAC,UAAU,MAAM,UAAU;AACnC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var contextual_model_exports = {};
20
+ __export(contextual_model_exports, {
21
+ ContextualModel: () => ContextualModel
22
+ });
23
+ module.exports = __toCommonJS(contextual_model_exports);
24
+ var import_statistics = require("./statistics");
25
+ var import_temporal_model = require("./temporal-model");
26
+ class ContextualModel {
27
+ constructor(bucketMinutes, maxContexts, maxSamples = import_statistics.DEFAULT_MAX_SAMPLES, data) {
28
+ this.bucketMinutes = bucketMinutes;
29
+ this.maxContexts = maxContexts;
30
+ this.maxSamples = maxSamples;
31
+ var _a;
32
+ for (const [key, entry] of Object.entries((_a = data == null ? void 0 : data.contexts) != null ? _a : {})) {
33
+ if (this.contexts.size >= maxContexts) {
34
+ break;
35
+ }
36
+ this.contexts.set(key, {
37
+ model: new import_temporal_model.TemporalModel(bucketMinutes, maxSamples, entry.model),
38
+ lastUsed: Number.isFinite(entry.lastUsed) ? entry.lastUsed : 0
39
+ });
40
+ }
41
+ }
42
+ contexts = /* @__PURE__ */ new Map();
43
+ baseline(key, timestamp, minimumSamples, useTimeContext, useWeekdayContext) {
44
+ if (!key) {
45
+ return void 0;
46
+ }
47
+ const entry = this.contexts.get(key);
48
+ if (!entry || entry.model.sampleCount < minimumSamples) {
49
+ return void 0;
50
+ }
51
+ entry.lastUsed = timestamp;
52
+ return entry.model.baseline(timestamp, minimumSamples, useTimeContext, useWeekdayContext);
53
+ }
54
+ /**
55
+ * Returns the retained global sample count for one exact context combination.
56
+ *
57
+ * @param key Normalized context combination key.
58
+ */
59
+ sampleCount(key) {
60
+ var _a, _b;
61
+ return key ? (_b = (_a = this.contexts.get(key)) == null ? void 0 : _a.model.sampleCount) != null ? _b : 0 : 0;
62
+ }
63
+ add(key, value, timestamp, useTimeContext, useWeekdayContext) {
64
+ if (!key) {
65
+ return;
66
+ }
67
+ let entry = this.contexts.get(key);
68
+ if (!entry) {
69
+ if (this.contexts.size >= this.maxContexts) {
70
+ const oldest = [...this.contexts.entries()].sort(
71
+ ([leftKey, left], [rightKey, right]) => left.lastUsed - right.lastUsed || leftKey.localeCompare(rightKey)
72
+ )[0];
73
+ if (oldest) {
74
+ this.contexts.delete(oldest[0]);
75
+ }
76
+ }
77
+ entry = { model: new import_temporal_model.TemporalModel(this.bucketMinutes, this.maxSamples), lastUsed: timestamp };
78
+ this.contexts.set(key, entry);
79
+ }
80
+ entry.lastUsed = timestamp;
81
+ entry.model.add(value, timestamp, useTimeContext, useWeekdayContext);
82
+ }
83
+ toJSON() {
84
+ return {
85
+ contexts: Object.fromEntries(
86
+ [...this.contexts].map(([key, entry]) => [
87
+ key,
88
+ { model: entry.model.toJSON(), lastUsed: entry.lastUsed }
89
+ ])
90
+ )
91
+ };
92
+ }
93
+ }
94
+ // Annotate the CommonJS export names for ESM import in node:
95
+ 0 && (module.exports = {
96
+ ContextualModel
97
+ });
98
+ //# sourceMappingURL=contextual-model.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/model/contextual-model.ts"],
4
+ "sourcesContent": ["import { DEFAULT_MAX_SAMPLES } from \"./statistics\";\nimport { TemporalModel, type Baseline, type TemporalModelData } from \"./temporal-model\";\n\nexport interface ContextualModelEntryData {\n\tmodel: TemporalModelData;\n\tlastUsed: number;\n}\n\nexport interface ContextualModelData {\n\tcontexts: Record<string, ContextualModelEntryData>;\n}\n\n/** Bounded context-specific temporal baselines with deterministic least-recently-used eviction. */\nexport class ContextualModel {\n\tprivate readonly contexts = new Map<string, { model: TemporalModel; lastUsed: number }>();\n\n\tpublic constructor(\n\t\tprivate readonly bucketMinutes: number,\n\t\tprivate readonly maxContexts: number,\n\t\tprivate readonly maxSamples = DEFAULT_MAX_SAMPLES,\n\t\tdata?: ContextualModelData,\n\t) {\n\t\tfor (const [key, entry] of Object.entries(data?.contexts ?? {})) {\n\t\t\tif (this.contexts.size >= maxContexts) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tthis.contexts.set(key, {\n\t\t\t\tmodel: new TemporalModel(bucketMinutes, maxSamples, entry.model),\n\t\t\t\tlastUsed: Number.isFinite(entry.lastUsed) ? entry.lastUsed : 0,\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic baseline(\n\t\tkey: string | undefined,\n\t\ttimestamp: number,\n\t\tminimumSamples: number,\n\t\tuseTimeContext: boolean,\n\t\tuseWeekdayContext: boolean,\n\t): Baseline | undefined {\n\t\tif (!key) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst entry = this.contexts.get(key);\n\t\tif (!entry || entry.model.sampleCount < minimumSamples) {\n\t\t\treturn undefined;\n\t\t}\n\t\tentry.lastUsed = timestamp;\n\t\treturn entry.model.baseline(timestamp, minimumSamples, useTimeContext, useWeekdayContext);\n\t}\n\n\t/**\n\t * Returns the retained global sample count for one exact context combination.\n\t *\n\t * @param key Normalized context combination key.\n\t */\n\tpublic sampleCount(key: string | undefined): number {\n\t\treturn key ? (this.contexts.get(key)?.model.sampleCount ?? 0) : 0;\n\t}\n\n\tpublic add(\n\t\tkey: string | undefined,\n\t\tvalue: number,\n\t\ttimestamp: number,\n\t\tuseTimeContext: boolean,\n\t\tuseWeekdayContext: boolean,\n\t): void {\n\t\tif (!key) {\n\t\t\treturn;\n\t\t}\n\t\tlet entry = this.contexts.get(key);\n\t\tif (!entry) {\n\t\t\tif (this.contexts.size >= this.maxContexts) {\n\t\t\t\tconst oldest = [...this.contexts.entries()].sort(\n\t\t\t\t\t([leftKey, left], [rightKey, right]) =>\n\t\t\t\t\t\tleft.lastUsed - right.lastUsed || leftKey.localeCompare(rightKey),\n\t\t\t\t)[0];\n\t\t\t\tif (oldest) {\n\t\t\t\t\tthis.contexts.delete(oldest[0]);\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry = { model: new TemporalModel(this.bucketMinutes, this.maxSamples), lastUsed: timestamp };\n\t\t\tthis.contexts.set(key, entry);\n\t\t}\n\t\tentry.lastUsed = timestamp;\n\t\tentry.model.add(value, timestamp, useTimeContext, useWeekdayContext);\n\t}\n\n\tpublic toJSON(): ContextualModelData {\n\t\treturn {\n\t\t\tcontexts: Object.fromEntries(\n\t\t\t\t[...this.contexts].map(([key, entry]) => [\n\t\t\t\t\tkey,\n\t\t\t\t\t{ model: entry.model.toJSON(), lastUsed: entry.lastUsed },\n\t\t\t\t]),\n\t\t\t),\n\t\t};\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAoC;AACpC,4BAAqE;AAY9D,MAAM,gBAAgB;AAAA,EAGrB,YACW,eACA,aACA,aAAa,uCAC9B,MACC;AAJgB;AACA;AACA;AAnBnB;AAsBE,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,SAAQ,kCAAM,aAAN,YAAkB,CAAC,CAAC,GAAG;AAChE,UAAI,KAAK,SAAS,QAAQ,aAAa;AACtC;AAAA,MACD;AACA,WAAK,SAAS,IAAI,KAAK;AAAA,QACtB,OAAO,IAAI,oCAAc,eAAe,YAAY,MAAM,KAAK;AAAA,QAC/D,UAAU,OAAO,SAAS,MAAM,QAAQ,IAAI,MAAM,WAAW;AAAA,MAC9D,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAjBiB,WAAW,oBAAI,IAAwD;AAAA,EAmBjF,SACN,KACA,WACA,gBACA,gBACA,mBACuB;AACvB,QAAI,CAAC,KAAK;AACT,aAAO;AAAA,IACR;AACA,UAAM,QAAQ,KAAK,SAAS,IAAI,GAAG;AACnC,QAAI,CAAC,SAAS,MAAM,MAAM,cAAc,gBAAgB;AACvD,aAAO;AAAA,IACR;AACA,UAAM,WAAW;AACjB,WAAO,MAAM,MAAM,SAAS,WAAW,gBAAgB,gBAAgB,iBAAiB;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOO,YAAY,KAAiC;AAxDrD;AAyDE,WAAO,OAAO,gBAAK,SAAS,IAAI,GAAG,MAArB,mBAAwB,MAAM,gBAA9B,YAA6C,IAAK;AAAA,EACjE;AAAA,EAEO,IACN,KACA,OACA,WACA,gBACA,mBACO;AACP,QAAI,CAAC,KAAK;AACT;AAAA,IACD;AACA,QAAI,QAAQ,KAAK,SAAS,IAAI,GAAG;AACjC,QAAI,CAAC,OAAO;AACX,UAAI,KAAK,SAAS,QAAQ,KAAK,aAAa;AAC3C,cAAM,SAAS,CAAC,GAAG,KAAK,SAAS,QAAQ,CAAC,EAAE;AAAA,UAC3C,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,KAAK,MACjC,KAAK,WAAW,MAAM,YAAY,QAAQ,cAAc,QAAQ;AAAA,QAClE,EAAE,CAAC;AACH,YAAI,QAAQ;AACX,eAAK,SAAS,OAAO,OAAO,CAAC,CAAC;AAAA,QAC/B;AAAA,MACD;AACA,cAAQ,EAAE,OAAO,IAAI,oCAAc,KAAK,eAAe,KAAK,UAAU,GAAG,UAAU,UAAU;AAC7F,WAAK,SAAS,IAAI,KAAK,KAAK;AAAA,IAC7B;AACA,UAAM,WAAW;AACjB,UAAM,MAAM,IAAI,OAAO,WAAW,gBAAgB,iBAAiB;AAAA,EACpE;AAAA,EAEO,SAA8B;AACpC,WAAO;AAAA,MACN,UAAU,OAAO;AAAA,QAChB,CAAC,GAAG,KAAK,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAAA,UACxC;AAAA,UACA,EAAE,OAAO,MAAM,MAAM,OAAO,GAAG,UAAU,MAAM,SAAS;AAAA,QACzD,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AACD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var statistics_exports = {};
20
+ __export(statistics_exports, {
21
+ DEFAULT_MAX_SAMPLES: () => DEFAULT_MAX_SAMPLES,
22
+ MODEL_SCHEMA_VERSION: () => MODEL_SCHEMA_VERSION,
23
+ SampleSeries: () => SampleSeries,
24
+ clamp: () => clamp,
25
+ median: () => median
26
+ });
27
+ module.exports = __toCommonJS(statistics_exports);
28
+ const MODEL_SCHEMA_VERSION = 2;
29
+ const DEFAULT_MAX_SAMPLES = 240;
30
+ class SampleSeries {
31
+ constructor(maxSamples = DEFAULT_MAX_SAMPLES, values = []) {
32
+ this.maxSamples = maxSamples;
33
+ this.values = values.filter(Number.isFinite).slice(-maxSamples);
34
+ }
35
+ values;
36
+ add(value) {
37
+ if (!Number.isFinite(value)) {
38
+ return;
39
+ }
40
+ this.values.push(value);
41
+ if (this.values.length > this.maxSamples) {
42
+ this.values.splice(0, this.values.length - this.maxSamples);
43
+ }
44
+ }
45
+ get count() {
46
+ return this.values.length;
47
+ }
48
+ median() {
49
+ return median(this.values);
50
+ }
51
+ mad() {
52
+ const center = this.median();
53
+ return center === void 0 ? void 0 : median(this.values.map((value) => Math.abs(value - center)));
54
+ }
55
+ toJSON() {
56
+ return { values: [...this.values] };
57
+ }
58
+ }
59
+ function median(values) {
60
+ const finite = values.filter(Number.isFinite).sort((left, right) => left - right);
61
+ if (finite.length === 0) {
62
+ return void 0;
63
+ }
64
+ const middle = Math.floor(finite.length / 2);
65
+ return finite.length % 2 === 0 ? (finite[middle - 1] + finite[middle]) / 2 : finite[middle];
66
+ }
67
+ function clamp(value, min, max) {
68
+ return Math.min(max, Math.max(min, value));
69
+ }
70
+ // Annotate the CommonJS export names for ESM import in node:
71
+ 0 && (module.exports = {
72
+ DEFAULT_MAX_SAMPLES,
73
+ MODEL_SCHEMA_VERSION,
74
+ SampleSeries,
75
+ clamp,
76
+ median
77
+ });
78
+ //# sourceMappingURL=statistics.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/model/statistics.ts"],
4
+ "sourcesContent": ["export const MODEL_SCHEMA_VERSION = 2;\nexport const DEFAULT_MAX_SAMPLES = 240;\n\nexport interface SampleSeriesData {\n\tvalues: number[];\n}\n\n/** A bounded sample reservoir. Keeping samples rather than raw history makes median and MAD robust and restart-safe. */\nexport class SampleSeries {\n\tprivate readonly values: number[];\n\n\tpublic constructor(\n\t\tprivate readonly maxSamples = DEFAULT_MAX_SAMPLES,\n\t\tvalues: number[] = [],\n\t) {\n\t\tthis.values = values.filter(Number.isFinite).slice(-maxSamples);\n\t}\n\n\tpublic add(value: number): void {\n\t\tif (!Number.isFinite(value)) {\n\t\t\treturn;\n\t\t}\n\t\tthis.values.push(value);\n\t\tif (this.values.length > this.maxSamples) {\n\t\t\tthis.values.splice(0, this.values.length - this.maxSamples);\n\t\t}\n\t}\n\n\tpublic get count(): number {\n\t\treturn this.values.length;\n\t}\n\n\tpublic median(): number | undefined {\n\t\treturn median(this.values);\n\t}\n\n\tpublic mad(): number | undefined {\n\t\tconst center = this.median();\n\t\treturn center === undefined ? undefined : median(this.values.map(value => Math.abs(value - center)));\n\t}\n\n\tpublic toJSON(): SampleSeriesData {\n\t\treturn { values: [...this.values] };\n\t}\n}\n\nexport function median(values: readonly number[]): number | undefined {\n\tconst finite = values.filter(Number.isFinite).sort((left, right) => left - right);\n\tif (finite.length === 0) {\n\t\treturn undefined;\n\t}\n\tconst middle = Math.floor(finite.length / 2);\n\treturn finite.length % 2 === 0 ? (finite[middle - 1] + finite[middle]) / 2 : finite[middle];\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAO5B,MAAM,aAAa;AAAA,EAGlB,YACW,aAAa,qBAC9B,SAAmB,CAAC,GACnB;AAFgB;AAGjB,SAAK,SAAS,OAAO,OAAO,OAAO,QAAQ,EAAE,MAAM,CAAC,UAAU;AAAA,EAC/D;AAAA,EAPiB;AAAA,EASV,IAAI,OAAqB;AAC/B,QAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC5B;AAAA,IACD;AACA,SAAK,OAAO,KAAK,KAAK;AACtB,QAAI,KAAK,OAAO,SAAS,KAAK,YAAY;AACzC,WAAK,OAAO,OAAO,GAAG,KAAK,OAAO,SAAS,KAAK,UAAU;AAAA,IAC3D;AAAA,EACD;AAAA,EAEA,IAAW,QAAgB;AAC1B,WAAO,KAAK,OAAO;AAAA,EACpB;AAAA,EAEO,SAA6B;AACnC,WAAO,OAAO,KAAK,MAAM;AAAA,EAC1B;AAAA,EAEO,MAA0B;AAChC,UAAM,SAAS,KAAK,OAAO;AAC3B,WAAO,WAAW,SAAY,SAAY,OAAO,KAAK,OAAO,IAAI,WAAS,KAAK,IAAI,QAAQ,MAAM,CAAC,CAAC;AAAA,EACpG;AAAA,EAEO,SAA2B;AACjC,WAAO,EAAE,QAAQ,CAAC,GAAG,KAAK,MAAM,EAAE;AAAA,EACnC;AACD;AAEO,SAAS,OAAO,QAA+C;AACrE,QAAM,SAAS,OAAO,OAAO,OAAO,QAAQ,EAAE,KAAK,CAAC,MAAM,UAAU,OAAO,KAAK;AAChF,MAAI,OAAO,WAAW,GAAG;AACxB,WAAO;AAAA,EACR;AACA,QAAM,SAAS,KAAK,MAAM,OAAO,SAAS,CAAC;AAC3C,SAAO,OAAO,SAAS,MAAM,KAAK,OAAO,SAAS,CAAC,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM;AAC3F;AAEO,SAAS,MAAM,OAAe,KAAa,KAAqB;AACtE,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var temporal_model_exports = {};
20
+ __export(temporal_model_exports, {
21
+ TemporalModel: () => TemporalModel
22
+ });
23
+ module.exports = __toCommonJS(temporal_model_exports);
24
+ var import_statistics = require("./statistics");
25
+ class TemporalModel {
26
+ constructor(bucketMinutes, maxSamples = import_statistics.DEFAULT_MAX_SAMPLES, data) {
27
+ this.bucketMinutes = bucketMinutes;
28
+ this.maxSamples = maxSamples;
29
+ var _a, _b;
30
+ this.global = new import_statistics.SampleSeries(maxSamples, data == null ? void 0 : data.global.values);
31
+ for (const [key, series] of Object.entries((_a = data == null ? void 0 : data.timeBuckets) != null ? _a : {})) {
32
+ this.timeBuckets.set(key, new import_statistics.SampleSeries(maxSamples, series.values));
33
+ }
34
+ for (const [key, series] of Object.entries((_b = data == null ? void 0 : data.contextBuckets) != null ? _b : {})) {
35
+ this.contextBuckets.set(key, new import_statistics.SampleSeries(maxSamples, series.values));
36
+ }
37
+ }
38
+ global;
39
+ timeBuckets = /* @__PURE__ */ new Map();
40
+ contextBuckets = /* @__PURE__ */ new Map();
41
+ add(value, timestamp, useTimeContext, useWeekdayContext) {
42
+ this.global.add(value);
43
+ if (!useTimeContext) {
44
+ return;
45
+ }
46
+ const timeKey = this.getTimeKey(timestamp);
47
+ this.getOrCreate(this.timeBuckets, timeKey).add(value);
48
+ if (useWeekdayContext) {
49
+ this.getOrCreate(this.contextBuckets, `${this.getDayKey(timestamp)}:${timeKey}`).add(value);
50
+ }
51
+ }
52
+ baseline(timestamp, minSamples, useTimeContext, useWeekdayContext) {
53
+ if (useTimeContext) {
54
+ const timeKey = this.getTimeKey(timestamp);
55
+ if (useWeekdayContext) {
56
+ const contextual = this.contextBuckets.get(`${this.getDayKey(timestamp)}:${timeKey}`);
57
+ if (contextual && contextual.count >= minSamples) {
58
+ return { series: contextual, scope: "context" };
59
+ }
60
+ }
61
+ const time = this.timeBuckets.get(timeKey);
62
+ if (time && time.count >= minSamples) {
63
+ return { series: time, scope: "time" };
64
+ }
65
+ }
66
+ return { series: this.global, scope: "global" };
67
+ }
68
+ get sampleCount() {
69
+ return this.global.count;
70
+ }
71
+ toJSON() {
72
+ return {
73
+ global: this.global.toJSON(),
74
+ timeBuckets: Object.fromEntries([...this.timeBuckets].map(([key, value]) => [key, value.toJSON()])),
75
+ contextBuckets: Object.fromEntries([...this.contextBuckets].map(([key, value]) => [key, value.toJSON()]))
76
+ };
77
+ }
78
+ getOrCreate(map, key) {
79
+ let series = map.get(key);
80
+ if (!series) {
81
+ series = new import_statistics.SampleSeries(this.maxSamples);
82
+ map.set(key, series);
83
+ }
84
+ return series;
85
+ }
86
+ getTimeKey(timestamp) {
87
+ const date = new Date(timestamp);
88
+ return String(Math.floor((date.getHours() * 60 + date.getMinutes()) / this.bucketMinutes));
89
+ }
90
+ getDayKey(timestamp) {
91
+ return String(new Date(timestamp).getDay());
92
+ }
93
+ }
94
+ // Annotate the CommonJS export names for ESM import in node:
95
+ 0 && (module.exports = {
96
+ TemporalModel
97
+ });
98
+ //# sourceMappingURL=temporal-model.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/model/temporal-model.ts"],
4
+ "sourcesContent": ["import { DEFAULT_MAX_SAMPLES, SampleSeries, type SampleSeriesData } from \"./statistics\";\n\nexport interface TemporalModelData {\n\tglobal: SampleSeriesData;\n\ttimeBuckets: Record<string, SampleSeriesData>;\n\tcontextBuckets: Record<string, SampleSeriesData>;\n}\n\nexport interface Baseline {\n\tseries: SampleSeries;\n\tscope: \"context\" | \"time\" | \"global\";\n}\n\n/**\n * Stores only bounded samples per broad baseline, time bucket and optional day context.\n * The selection cascade avoids treating a sparse context as a reliable baseline.\n */\nexport class TemporalModel {\n\tprivate readonly global: SampleSeries;\n\tprivate readonly timeBuckets = new Map<string, SampleSeries>();\n\tprivate readonly contextBuckets = new Map<string, SampleSeries>();\n\n\tpublic constructor(\n\t\tprivate readonly bucketMinutes: number,\n\t\tprivate readonly maxSamples = DEFAULT_MAX_SAMPLES,\n\t\tdata?: TemporalModelData,\n\t) {\n\t\tthis.global = new SampleSeries(maxSamples, data?.global.values);\n\t\tfor (const [key, series] of Object.entries(data?.timeBuckets ?? {})) {\n\t\t\tthis.timeBuckets.set(key, new SampleSeries(maxSamples, series.values));\n\t\t}\n\t\tfor (const [key, series] of Object.entries(data?.contextBuckets ?? {})) {\n\t\t\tthis.contextBuckets.set(key, new SampleSeries(maxSamples, series.values));\n\t\t}\n\t}\n\n\tpublic add(value: number, timestamp: number, useTimeContext: boolean, useWeekdayContext: boolean): void {\n\t\tthis.global.add(value);\n\t\tif (!useTimeContext) {\n\t\t\treturn;\n\t\t}\n\t\tconst timeKey = this.getTimeKey(timestamp);\n\t\tthis.getOrCreate(this.timeBuckets, timeKey).add(value);\n\t\tif (useWeekdayContext) {\n\t\t\tthis.getOrCreate(this.contextBuckets, `${this.getDayKey(timestamp)}:${timeKey}`).add(value);\n\t\t}\n\t}\n\n\tpublic baseline(\n\t\ttimestamp: number,\n\t\tminSamples: number,\n\t\tuseTimeContext: boolean,\n\t\tuseWeekdayContext: boolean,\n\t): Baseline {\n\t\tif (useTimeContext) {\n\t\t\tconst timeKey = this.getTimeKey(timestamp);\n\t\t\tif (useWeekdayContext) {\n\t\t\t\tconst contextual = this.contextBuckets.get(`${this.getDayKey(timestamp)}:${timeKey}`);\n\t\t\t\tif (contextual && contextual.count >= minSamples) {\n\t\t\t\t\treturn { series: contextual, scope: \"context\" };\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst time = this.timeBuckets.get(timeKey);\n\t\t\tif (time && time.count >= minSamples) {\n\t\t\t\treturn { series: time, scope: \"time\" };\n\t\t\t}\n\t\t}\n\t\treturn { series: this.global, scope: \"global\" };\n\t}\n\n\tpublic get sampleCount(): number {\n\t\treturn this.global.count;\n\t}\n\n\tpublic toJSON(): TemporalModelData {\n\t\treturn {\n\t\t\tglobal: this.global.toJSON(),\n\t\t\ttimeBuckets: Object.fromEntries([...this.timeBuckets].map(([key, value]) => [key, value.toJSON()])),\n\t\t\tcontextBuckets: Object.fromEntries([...this.contextBuckets].map(([key, value]) => [key, value.toJSON()])),\n\t\t};\n\t}\n\n\tprivate getOrCreate(map: Map<string, SampleSeries>, key: string): SampleSeries {\n\t\tlet series = map.get(key);\n\t\tif (!series) {\n\t\t\tseries = new SampleSeries(this.maxSamples);\n\t\t\tmap.set(key, series);\n\t\t}\n\t\treturn series;\n\t}\n\n\tprivate getTimeKey(timestamp: number): string {\n\t\tconst date = new Date(timestamp);\n\t\treturn String(Math.floor((date.getHours() * 60 + date.getMinutes()) / this.bucketMinutes));\n\t}\n\n\tprivate getDayKey(timestamp: number): string {\n\t\treturn String(new Date(timestamp).getDay());\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAyE;AAiBlE,MAAM,cAAc;AAAA,EAKnB,YACW,eACA,aAAa,uCAC9B,MACC;AAHgB;AACA;AAxBnB;AA2BE,SAAK,SAAS,IAAI,+BAAa,YAAY,6BAAM,OAAO,MAAM;AAC9D,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,SAAQ,kCAAM,gBAAN,YAAqB,CAAC,CAAC,GAAG;AACpE,WAAK,YAAY,IAAI,KAAK,IAAI,+BAAa,YAAY,OAAO,MAAM,CAAC;AAAA,IACtE;AACA,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,SAAQ,kCAAM,mBAAN,YAAwB,CAAC,CAAC,GAAG;AACvE,WAAK,eAAe,IAAI,KAAK,IAAI,+BAAa,YAAY,OAAO,MAAM,CAAC;AAAA,IACzE;AAAA,EACD;AAAA,EAhBiB;AAAA,EACA,cAAc,oBAAI,IAA0B;AAAA,EAC5C,iBAAiB,oBAAI,IAA0B;AAAA,EAgBzD,IAAI,OAAe,WAAmB,gBAAyB,mBAAkC;AACvG,SAAK,OAAO,IAAI,KAAK;AACrB,QAAI,CAAC,gBAAgB;AACpB;AAAA,IACD;AACA,UAAM,UAAU,KAAK,WAAW,SAAS;AACzC,SAAK,YAAY,KAAK,aAAa,OAAO,EAAE,IAAI,KAAK;AACrD,QAAI,mBAAmB;AACtB,WAAK,YAAY,KAAK,gBAAgB,GAAG,KAAK,UAAU,SAAS,CAAC,IAAI,OAAO,EAAE,EAAE,IAAI,KAAK;AAAA,IAC3F;AAAA,EACD;AAAA,EAEO,SACN,WACA,YACA,gBACA,mBACW;AACX,QAAI,gBAAgB;AACnB,YAAM,UAAU,KAAK,WAAW,SAAS;AACzC,UAAI,mBAAmB;AACtB,cAAM,aAAa,KAAK,eAAe,IAAI,GAAG,KAAK,UAAU,SAAS,CAAC,IAAI,OAAO,EAAE;AACpF,YAAI,cAAc,WAAW,SAAS,YAAY;AACjD,iBAAO,EAAE,QAAQ,YAAY,OAAO,UAAU;AAAA,QAC/C;AAAA,MACD;AACA,YAAM,OAAO,KAAK,YAAY,IAAI,OAAO;AACzC,UAAI,QAAQ,KAAK,SAAS,YAAY;AACrC,eAAO,EAAE,QAAQ,MAAM,OAAO,OAAO;AAAA,MACtC;AAAA,IACD;AACA,WAAO,EAAE,QAAQ,KAAK,QAAQ,OAAO,SAAS;AAAA,EAC/C;AAAA,EAEA,IAAW,cAAsB;AAChC,WAAO,KAAK,OAAO;AAAA,EACpB;AAAA,EAEO,SAA4B;AAClC,WAAO;AAAA,MACN,QAAQ,KAAK,OAAO,OAAO;AAAA,MAC3B,aAAa,OAAO,YAAY,CAAC,GAAG,KAAK,WAAW,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC;AAAA,MAClG,gBAAgB,OAAO,YAAY,CAAC,GAAG,KAAK,cAAc,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC;AAAA,IACzG;AAAA,EACD;AAAA,EAEQ,YAAY,KAAgC,KAA2B;AAC9E,QAAI,SAAS,IAAI,IAAI,GAAG;AACxB,QAAI,CAAC,QAAQ;AACZ,eAAS,IAAI,+BAAa,KAAK,UAAU;AACzC,UAAI,IAAI,KAAK,MAAM;AAAA,IACpB;AACA,WAAO;AAAA,EACR;AAAA,EAEQ,WAAW,WAA2B;AAC7C,UAAM,OAAO,IAAI,KAAK,SAAS;AAC/B,WAAO,OAAO,KAAK,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,WAAW,KAAK,KAAK,aAAa,CAAC;AAAA,EAC1F;AAAA,EAEQ,UAAU,WAA2B;AAC5C,WAAO,OAAO,IAAI,KAAK,SAAS,EAAE,OAAO,CAAC;AAAA,EAC3C;AACD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var scoring_exports = {};
20
+ __export(scoring_exports, {
21
+ scoreDetectors: () => scoreDetectors
22
+ });
23
+ module.exports = __toCommonJS(scoring_exports);
24
+ var import_statistics = require("./model/statistics");
25
+ const WEIGHTS = {
26
+ value: 0.5,
27
+ context: 0.5,
28
+ rate: 0.3,
29
+ stuck: 0.2,
30
+ changePoint: 0.45,
31
+ trend: 0.35
32
+ };
33
+ const BEHAVIOR_DETECTORS = /* @__PURE__ */ new Set(["value", "context", "changePoint", "trend"]);
34
+ function scoreDetectors(results) {
35
+ if (results.length === 0) {
36
+ return { score: 0, reason: "Insufficient data for anomaly detection" };
37
+ }
38
+ const behavior = results.filter((result) => BEHAVIOR_DETECTORS.has(result.name));
39
+ const selected = [
40
+ ...behavior.length ? [behavior.reduce((best, result) => result.score > best.score ? result : best)] : [],
41
+ ...results.filter((result) => !BEHAVIOR_DETECTORS.has(result.name))
42
+ ];
43
+ const weight = selected.reduce((total, result) => total + WEIGHTS[result.name], 0);
44
+ const weightedScore = selected.reduce((total, result) => total + result.score * WEIGHTS[result.name], 0) / weight;
45
+ const strong = selected.filter((result) => result.score >= 50);
46
+ const score = (0, import_statistics.clamp)(weightedScore + (strong.length >= 2 ? 10 : 0), 0, 100);
47
+ if (score === 0) {
48
+ return { score, reason: "Normal" };
49
+ }
50
+ const reason = strong.length >= 2 ? "Multiple anomaly detectors agree" : selected.reduce((best, result) => result.score > best.score ? result : best).reason;
51
+ return { score, reason };
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ scoreDetectors
56
+ });
57
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/scoring.ts"],
4
+ "sourcesContent": ["import { clamp } from \"./model/statistics\";\nimport type { DetectorResult } from \"./detectors\";\n\nexport interface ScoringResult {\n\tscore: number;\n\treason: string;\n}\n\nconst WEIGHTS: Record<DetectorResult[\"name\"], number> = {\n\tvalue: 0.5,\n\tcontext: 0.5,\n\trate: 0.3,\n\tstuck: 0.2,\n\tchangePoint: 0.45,\n\ttrend: 0.35,\n};\nconst BEHAVIOR_DETECTORS = new Set<DetectorResult[\"name\"]>([\"value\", \"context\", \"changePoint\", \"trend\"]);\n\nexport function scoreDetectors(results: readonly DetectorResult[]): ScoringResult {\n\tif (results.length === 0) {\n\t\treturn { score: 0, reason: \"Insufficient data for anomaly detection\" };\n\t}\n\tconst behavior = results.filter(result => BEHAVIOR_DETECTORS.has(result.name));\n\tconst selected = [\n\t\t...(behavior.length ? [behavior.reduce((best, result) => (result.score > best.score ? result : best))] : []),\n\t\t...results.filter(result => !BEHAVIOR_DETECTORS.has(result.name)),\n\t];\n\tconst weight = selected.reduce((total, result) => total + WEIGHTS[result.name], 0);\n\tconst weightedScore = selected.reduce((total, result) => total + result.score * WEIGHTS[result.name], 0) / weight;\n\tconst strong = selected.filter(result => result.score >= 50);\n\tconst score = clamp(weightedScore + (strong.length >= 2 ? 10 : 0), 0, 100);\n\tif (score === 0) {\n\t\treturn { score, reason: \"Normal\" };\n\t}\n\tconst reason =\n\t\tstrong.length >= 2\n\t\t\t? \"Multiple anomaly detectors agree\"\n\t\t\t: selected.reduce((best, result) => (result.score > best.score ? result : best)).reason;\n\treturn { score, reason };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAsB;AAQtB,MAAM,UAAkD;AAAA,EACvD,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AACR;AACA,MAAM,qBAAqB,oBAAI,IAA4B,CAAC,SAAS,WAAW,eAAe,OAAO,CAAC;AAEhG,SAAS,eAAe,SAAmD;AACjF,MAAI,QAAQ,WAAW,GAAG;AACzB,WAAO,EAAE,OAAO,GAAG,QAAQ,0CAA0C;AAAA,EACtE;AACA,QAAM,WAAW,QAAQ,OAAO,YAAU,mBAAmB,IAAI,OAAO,IAAI,CAAC;AAC7E,QAAM,WAAW;AAAA,IAChB,GAAI,SAAS,SAAS,CAAC,SAAS,OAAO,CAAC,MAAM,WAAY,OAAO,QAAQ,KAAK,QAAQ,SAAS,IAAK,CAAC,IAAI,CAAC;AAAA,IAC1G,GAAG,QAAQ,OAAO,YAAU,CAAC,mBAAmB,IAAI,OAAO,IAAI,CAAC;AAAA,EACjE;AACA,QAAM,SAAS,SAAS,OAAO,CAAC,OAAO,WAAW,QAAQ,QAAQ,OAAO,IAAI,GAAG,CAAC;AACjF,QAAM,gBAAgB,SAAS,OAAO,CAAC,OAAO,WAAW,QAAQ,OAAO,QAAQ,QAAQ,OAAO,IAAI,GAAG,CAAC,IAAI;AAC3G,QAAM,SAAS,SAAS,OAAO,YAAU,OAAO,SAAS,EAAE;AAC3D,QAAM,YAAQ,yBAAM,iBAAiB,OAAO,UAAU,IAAI,KAAK,IAAI,GAAG,GAAG;AACzE,MAAI,UAAU,GAAG;AAChB,WAAO,EAAE,OAAO,QAAQ,SAAS;AAAA,EAClC;AACA,QAAM,SACL,OAAO,UAAU,IACd,qCACA,SAAS,OAAO,CAAC,MAAM,WAAY,OAAO,QAAQ,KAAK,QAAQ,SAAS,IAAK,EAAE;AACnF,SAAO,EAAE,OAAO,OAAO;AACxB;",
6
+ "names": []
7
+ }