iobroker.ai-usage 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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/admin/ai-usage.svg +19 -0
  4. package/admin/i18n/de.json +29 -0
  5. package/admin/i18n/en.json +29 -0
  6. package/admin/i18n/es.json +29 -0
  7. package/admin/i18n/fr.json +29 -0
  8. package/admin/i18n/it.json +29 -0
  9. package/admin/i18n/nl.json +29 -0
  10. package/admin/i18n/pl.json +29 -0
  11. package/admin/i18n/pt.json +29 -0
  12. package/admin/i18n/ru.json +29 -0
  13. package/admin/i18n/uk.json +29 -0
  14. package/admin/i18n/zh-cn.json +29 -0
  15. package/admin/jsonConfig.json +252 -0
  16. package/build/lib/http.js +81 -0
  17. package/build/lib/http.js.map +7 -0
  18. package/build/lib/poll-engine.js +341 -0
  19. package/build/lib/poll-engine.js.map +7 -0
  20. package/build/lib/provider.js +40 -0
  21. package/build/lib/provider.js.map +7 -0
  22. package/build/lib/providers/anthropic-api.js +120 -0
  23. package/build/lib/providers/anthropic-api.js.map +7 -0
  24. package/build/lib/providers/claude-auth.js +109 -0
  25. package/build/lib/providers/claude-auth.js.map +7 -0
  26. package/build/lib/providers/claude-sub.js +165 -0
  27. package/build/lib/providers/claude-sub.js.map +7 -0
  28. package/build/lib/providers/deepseek.js +67 -0
  29. package/build/lib/providers/deepseek.js.map +7 -0
  30. package/build/lib/providers/openai.js +136 -0
  31. package/build/lib/providers/openai.js.map +7 -0
  32. package/build/lib/providers/openrouter.js +71 -0
  33. package/build/lib/providers/openrouter.js.map +7 -0
  34. package/build/lib/providers/report-utils.js +59 -0
  35. package/build/lib/providers/report-utils.js.map +7 -0
  36. package/build/lib/pure-helpers.js +96 -0
  37. package/build/lib/pure-helpers.js.map +7 -0
  38. package/build/lib/snapshot-tree.js +175 -0
  39. package/build/lib/snapshot-tree.js.map +7 -0
  40. package/build/lib/totals.js +79 -0
  41. package/build/lib/totals.js.map +7 -0
  42. package/build/main.js +336 -0
  43. package/build/main.js.map +7 -0
  44. package/io-package.json +222 -0
  45. package/package.json +91 -0
@@ -0,0 +1,252 @@
1
+ {
2
+ "type": "panel",
3
+ "i18n": true,
4
+ "items": {
5
+ "_accountsHeader": {
6
+ "type": "header",
7
+ "text": "accountsHeader",
8
+ "size": 5
9
+ },
10
+ "_accountsInfo": {
11
+ "type": "staticText",
12
+ "text": "accountsInfo",
13
+ "xs": 12,
14
+ "sm": 12,
15
+ "md": 12,
16
+ "lg": 12,
17
+ "xl": 12,
18
+ "style": {
19
+ "fontSize": 14,
20
+ "marginBottom": 8
21
+ }
22
+ },
23
+ "accounts": {
24
+ "type": "table",
25
+ "label": "label_accounts",
26
+ "xs": 12,
27
+ "sm": 12,
28
+ "md": 12,
29
+ "lg": 12,
30
+ "xl": 12,
31
+ "uniqueColumns": [
32
+ "name"
33
+ ],
34
+ "items": [
35
+ {
36
+ "type": "text",
37
+ "attr": "name",
38
+ "title": "col_name",
39
+ "width": "20%",
40
+ "filter": false,
41
+ "sort": false,
42
+ "default": ""
43
+ },
44
+ {
45
+ "type": "select",
46
+ "attr": "provider",
47
+ "title": "col_provider",
48
+ "width": "20%",
49
+ "filter": false,
50
+ "sort": false,
51
+ "noTranslation": true,
52
+ "default": "claude-sub",
53
+ "options": [
54
+ {
55
+ "label": "Claude subscription",
56
+ "value": "claude-sub"
57
+ },
58
+ {
59
+ "label": "OpenRouter",
60
+ "value": "openrouter"
61
+ },
62
+ {
63
+ "label": "DeepSeek",
64
+ "value": "deepseek"
65
+ },
66
+ {
67
+ "label": "OpenAI API",
68
+ "value": "openai"
69
+ },
70
+ {
71
+ "label": "Anthropic API",
72
+ "value": "anthropic-api"
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "type": "credential",
78
+ "attr": "credentialId",
79
+ "title": "col_credential",
80
+ "width": "30%",
81
+ "filter": false,
82
+ "sort": false,
83
+ "default": ""
84
+ },
85
+ {
86
+ "type": "number",
87
+ "attr": "warnThreshold",
88
+ "title": "col_warnThreshold",
89
+ "width": "15%",
90
+ "filter": false,
91
+ "sort": false,
92
+ "min": 10,
93
+ "max": 100,
94
+ "default": 80
95
+ },
96
+ {
97
+ "type": "checkbox",
98
+ "attr": "enabled",
99
+ "title": "col_enabled",
100
+ "width": "15%",
101
+ "filter": false,
102
+ "sort": false,
103
+ "default": true
104
+ }
105
+ ]
106
+ },
107
+ "_claudeHeader": {
108
+ "newLine": true,
109
+ "type": "header",
110
+ "text": "claudeHeader",
111
+ "size": 5
112
+ },
113
+ "_claudeInfo": {
114
+ "type": "staticText",
115
+ "text": "claudeInfo",
116
+ "xs": 12,
117
+ "sm": 12,
118
+ "md": 12,
119
+ "lg": 12,
120
+ "xl": 12,
121
+ "style": {
122
+ "fontSize": 14,
123
+ "marginBottom": 8
124
+ }
125
+ },
126
+ "_claudeAccount": {
127
+ "type": "text",
128
+ "label": "label_claudeAccount",
129
+ "help": "help_claudeAccount",
130
+ "doNotSave": true,
131
+ "xs": 12,
132
+ "sm": 6,
133
+ "md": 4,
134
+ "lg": 4,
135
+ "xl": 3
136
+ },
137
+ "_claudeUrl": {
138
+ "type": "textSendTo",
139
+ "command": "claudeAuthStart",
140
+ "jsonData": "{\"account\": \"${data._claudeAccount}\"}",
141
+ "alsoDependsOn": [
142
+ "_claudeAccount"
143
+ ],
144
+ "label": "label_claudeUrl",
145
+ "copyToClipboard": true,
146
+ "xs": 12,
147
+ "sm": 12,
148
+ "md": 8,
149
+ "lg": 8,
150
+ "xl": 8
151
+ },
152
+ "_claudeCode": {
153
+ "type": "text",
154
+ "label": "label_claudeCode",
155
+ "help": "help_claudeCode",
156
+ "doNotSave": true,
157
+ "xs": 12,
158
+ "sm": 6,
159
+ "md": 4,
160
+ "lg": 4,
161
+ "xl": 3
162
+ },
163
+ "_claudeRedeem": {
164
+ "type": "sendTo",
165
+ "command": "claudeAuthCode",
166
+ "jsonData": "{\"account\": \"${data._claudeAccount}\", \"code\": \"${data._claudeCode}\"}",
167
+ "label": "btn_claudeRedeem",
168
+ "variant": "contained",
169
+ "useNative": false,
170
+ "xs": 12,
171
+ "sm": 6,
172
+ "md": 4,
173
+ "lg": 4,
174
+ "xl": 3
175
+ },
176
+ "_settingsHeader": {
177
+ "newLine": true,
178
+ "type": "header",
179
+ "text": "settingsHeader",
180
+ "size": 5
181
+ },
182
+ "pollInterval": {
183
+ "type": "number",
184
+ "label": "label_pollInterval",
185
+ "help": "help_pollInterval",
186
+ "min": 60,
187
+ "max": 3600,
188
+ "default": 300,
189
+ "xs": 12,
190
+ "sm": 6,
191
+ "md": 3,
192
+ "lg": 3,
193
+ "xl": 3
194
+ },
195
+ "notifications": {
196
+ "type": "checkbox",
197
+ "label": "label_notifications",
198
+ "help": "help_notifications",
199
+ "default": true,
200
+ "xs": 12,
201
+ "sm": 6,
202
+ "md": 4,
203
+ "lg": 4,
204
+ "xl": 4
205
+ },
206
+ "_supportHeader": {
207
+ "newLine": true,
208
+ "type": "header",
209
+ "text": "supportHeader",
210
+ "size": 5
211
+ },
212
+ "_aboutInfo": {
213
+ "type": "staticText",
214
+ "text": "aboutInfo",
215
+ "xs": 12,
216
+ "sm": 12,
217
+ "md": 12,
218
+ "lg": 12,
219
+ "xl": 12,
220
+ "style": {
221
+ "fontSize": 14,
222
+ "marginBottom": 16
223
+ }
224
+ },
225
+ "_kofiLink": {
226
+ "type": "staticLink",
227
+ "href": "https://ko-fi.com/krobipd",
228
+ "label": "donateKofi",
229
+ "button": true,
230
+ "variant": "outlined",
231
+ "color": "primary",
232
+ "xs": 12,
233
+ "sm": 6,
234
+ "md": 4,
235
+ "lg": 4,
236
+ "xl": 4
237
+ },
238
+ "_paypalLink": {
239
+ "type": "staticLink",
240
+ "href": "https://paypal.me/krobipd",
241
+ "label": "donatePaypal",
242
+ "button": true,
243
+ "variant": "outlined",
244
+ "color": "primary",
245
+ "xs": 12,
246
+ "sm": 6,
247
+ "md": 4,
248
+ "lg": 4,
249
+ "xl": 4
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,81 @@
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 http_exports = {};
20
+ __export(http_exports, {
21
+ getJson: () => getJson,
22
+ postJson: () => postJson
23
+ });
24
+ module.exports = __toCommonJS(http_exports);
25
+ var import_provider = require("./provider");
26
+ const REQUEST_TIMEOUT_MS = 15e3;
27
+ async function getJson(url, headers) {
28
+ let response;
29
+ try {
30
+ response = await fetch(url, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
31
+ } catch (e) {
32
+ throw new import_provider.FetchError("network", e instanceof Error ? e.message : String(e));
33
+ }
34
+ if (response.status === 401 || response.status === 403) {
35
+ throw new import_provider.FetchError("auth", `HTTP ${response.status}`);
36
+ }
37
+ if (response.status === 429) {
38
+ throw new import_provider.FetchError("rate-limit", "HTTP 429");
39
+ }
40
+ if (!response.ok) {
41
+ throw new import_provider.FetchError("network", `HTTP ${response.status}`);
42
+ }
43
+ try {
44
+ return await response.json();
45
+ } catch {
46
+ throw new import_provider.FetchError("network", "invalid JSON response");
47
+ }
48
+ }
49
+ async function postJson(url, body) {
50
+ let response;
51
+ try {
52
+ response = await fetch(url, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify(body),
56
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
57
+ });
58
+ } catch (e) {
59
+ throw new import_provider.FetchError("network", e instanceof Error ? e.message : String(e));
60
+ }
61
+ if (response.status === 401 || response.status === 403 || response.status === 400) {
62
+ throw new import_provider.FetchError("auth", `HTTP ${response.status}`);
63
+ }
64
+ if (response.status === 429) {
65
+ throw new import_provider.FetchError("rate-limit", "HTTP 429");
66
+ }
67
+ if (!response.ok) {
68
+ throw new import_provider.FetchError("network", `HTTP ${response.status}`);
69
+ }
70
+ try {
71
+ return await response.json();
72
+ } catch {
73
+ throw new import_provider.FetchError("network", "invalid JSON response");
74
+ }
75
+ }
76
+ // Annotate the CommonJS export names for ESM import in node:
77
+ 0 && (module.exports = {
78
+ getJson,
79
+ postJson
80
+ });
81
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/http.ts"],
4
+ "sourcesContent": ["import { FetchError } from \"./provider\";\n\n/** Per-request timeout (ms). */\nconst REQUEST_TIMEOUT_MS = 15000;\n\n/** The JSON-GET seam the provider modules use \u2014 injectable for tests. */\nexport type JsonFetch = (url: string, headers: Record<string, string>) => Promise<unknown>;\n\n/**\n * GET a JSON document with the shared failure classification: 401/403 become an\n * auth error, 429 a rate-limit error, everything else (bad status, network throw,\n * timeout, invalid JSON) a network error \u2014 the classes the poll engine acts on.\n *\n * @param url the request URL\n * @param headers request headers (Authorization etc.)\n * @returns the parsed JSON body\n */\nexport async function getJson(url: string, headers: Record<string, string>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n\n/**\n * POST a JSON document with the same failure classification as {@link getJson}.\n *\n * @param url the request URL\n * @param body the JSON body\n * @returns the parsed JSON response\n */\nexport async function postJson(url: string, body: Record<string, unknown>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403 || response.status === 400) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAA2B;AAG3B,MAAM,qBAAqB;AAc3B,eAAsB,QAAQ,KAAa,SAAmD;AAC5F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK,EAAE,SAAS,QAAQ,YAAY,QAAQ,kBAAkB,EAAE,CAAC;AAAA,EAC1F,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;AASA,eAAsB,SAAS,KAAa,MAAiD;AAC3F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAAA,EACH,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACjF,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,341 @@
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 poll_engine_exports = {};
20
+ __export(poll_engine_exports, {
21
+ PollEngine: () => PollEngine
22
+ });
23
+ module.exports = __toCommonJS(poll_engine_exports);
24
+ var import_provider = require("./provider");
25
+ var import_snapshot_tree = require("./snapshot-tree");
26
+ var import_totals = require("./totals");
27
+ const MAX_NETWORK_FAILURES = 3;
28
+ const BACKOFF_START_MS = 10 * 60 * 1e3;
29
+ const BACKOFF_MAX_MS = 60 * 60 * 1e3;
30
+ const STAGGER_MS = 3e3;
31
+ class PollEngine {
32
+ /**
33
+ * @param accounts the validated account configs
34
+ * @param providers each account id's provider (accounts without one are skipped)
35
+ * @param intervalSec the poll interval in seconds
36
+ * @param deps the injected adapter callbacks
37
+ */
38
+ constructor(accounts, providers, intervalSec, deps) {
39
+ this.intervalSec = intervalSec;
40
+ this.deps = deps;
41
+ for (const config of accounts) {
42
+ const provider = providers.get(config.id);
43
+ if (!provider) {
44
+ deps.log.warn(`${config.name}: provider "${config.provider}" is not available \u2014 account skipped`);
45
+ continue;
46
+ }
47
+ this.runtimes.push({
48
+ config,
49
+ provider,
50
+ status: { reachable: false, warning: false },
51
+ failCount: 0,
52
+ skipUntil: 0,
53
+ backoffMs: BACKOFF_START_MS,
54
+ authNotified: false,
55
+ createdObjects: /* @__PURE__ */ new Set()
56
+ });
57
+ }
58
+ }
59
+ intervalSec;
60
+ deps;
61
+ runtimes = [];
62
+ handles = [];
63
+ stopped = false;
64
+ /** Create the static per-account and totals objects, then arm the poll cycles. */
65
+ async start() {
66
+ for (const runtime of this.runtimes) {
67
+ await this.createAccountSkeleton(runtime);
68
+ }
69
+ await this.createTotalsSkeleton();
70
+ await this.writeTotals();
71
+ this.runtimes.forEach((runtime, index) => {
72
+ this.handles.push(
73
+ this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),
74
+ this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1e3)
75
+ );
76
+ });
77
+ }
78
+ /** Cancel every timer. Synchronous — safe from onUnload. */
79
+ stop() {
80
+ this.stopped = true;
81
+ for (const handle of this.handles) {
82
+ this.deps.cancel(handle);
83
+ }
84
+ this.handles.length = 0;
85
+ }
86
+ /** The account ids the engine drives (for the stale-object cleanup). */
87
+ get accountIds() {
88
+ return this.runtimes.map((runtime) => runtime.config.id);
89
+ }
90
+ /**
91
+ * Poll one account now (also used by the staggered first run).
92
+ *
93
+ * @param runtime the account's runtime
94
+ */
95
+ async pollAccount(runtime) {
96
+ if (this.stopped) {
97
+ return;
98
+ }
99
+ const { config } = runtime;
100
+ if (this.deps.now() < runtime.skipUntil) {
101
+ this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);
102
+ return;
103
+ }
104
+ try {
105
+ const snapshot = await runtime.provider.fetch();
106
+ runtime.failCount = 0;
107
+ runtime.backoffMs = BACKOFF_START_MS;
108
+ runtime.authNotified = false;
109
+ runtime.status.snapshot = snapshot;
110
+ runtime.status.reachable = true;
111
+ await this.applySnapshot(runtime, snapshot);
112
+ } catch (e) {
113
+ this.handleFailure(runtime, e);
114
+ }
115
+ this.writeAccountInfo(runtime);
116
+ await this.writeTotals();
117
+ }
118
+ /**
119
+ * Write a successful snapshot: upsert new objects (create-once cache), write the
120
+ * values, and run the warn-threshold transition.
121
+ *
122
+ * @param runtime the account's runtime
123
+ * @param snapshot the fetched snapshot
124
+ */
125
+ async applySnapshot(runtime, snapshot) {
126
+ var _a, _b, _c;
127
+ const { config } = runtime;
128
+ const { objects, writes } = (0, import_snapshot_tree.mapSnapshot)(config.id, config.name, config.provider, snapshot);
129
+ for (const object of objects) {
130
+ if (!runtime.createdObjects.has(object.id)) {
131
+ await this.deps.upsertObject(object);
132
+ runtime.createdObjects.add(object.id);
133
+ }
134
+ }
135
+ for (const write of writes) {
136
+ this.deps.setState(write.id, write.value);
137
+ }
138
+ const percent = (_a = (0, import_snapshot_tree.maxLimitPercent)(snapshot)) != null ? _a : 0;
139
+ const wasWarning = runtime.status.warning;
140
+ runtime.status.warning = percent >= config.warnThreshold;
141
+ this.deps.setState(`${config.id}.warning`, runtime.status.warning);
142
+ this.deps.setState(`${config.id}.limitReached`, percent >= 100);
143
+ if (runtime.status.warning && !wasWarning) {
144
+ const message = `${config.name}: usage at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;
145
+ this.deps.log.warn(message);
146
+ (_c = (_b = this.deps).notify) == null ? void 0 : _c.call(_b, config.name, message);
147
+ }
148
+ }
149
+ /**
150
+ * Classify a fetch failure: auth = unreachable + ONE notification until it recovers;
151
+ * rate-limit = backoff, last values stay; network = tolerated MAX_NETWORK_FAILURES times.
152
+ *
153
+ * @param runtime the account's runtime
154
+ * @param error the thrown error
155
+ */
156
+ handleFailure(runtime, error) {
157
+ var _a, _b;
158
+ const { config } = runtime;
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ if (error instanceof import_provider.FetchError && error.kind === "auth") {
161
+ runtime.status.reachable = false;
162
+ if (!runtime.authNotified) {
163
+ runtime.authNotified = true;
164
+ const text = `${config.name}: credentials rejected \u2014 ${message}`;
165
+ this.deps.log.warn(text);
166
+ (_b = (_a = this.deps).notify) == null ? void 0 : _b.call(_a, config.name, text);
167
+ }
168
+ return;
169
+ }
170
+ if (error instanceof import_provider.FetchError && error.kind === "rate-limit") {
171
+ runtime.skipUntil = this.deps.now() + runtime.backoffMs;
172
+ this.deps.log.warn(
173
+ `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 6e4)} min, keeping last values`
174
+ );
175
+ runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);
176
+ return;
177
+ }
178
+ runtime.failCount++;
179
+ this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);
180
+ if (runtime.failCount >= MAX_NETWORK_FAILURES) {
181
+ runtime.status.reachable = false;
182
+ }
183
+ }
184
+ /**
185
+ * Write one account's info states (reachable, last update).
186
+ *
187
+ * @param runtime the account's runtime
188
+ */
189
+ writeAccountInfo(runtime) {
190
+ const { config } = runtime;
191
+ this.deps.setState(`${config.id}.info.reachable`, runtime.status.reachable);
192
+ if (runtime.status.reachable) {
193
+ this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());
194
+ }
195
+ }
196
+ /** Recompute and write the totals + info.connection. */
197
+ async writeTotals() {
198
+ const totals = (0, import_totals.computeTotals)(this.runtimes.map((runtime) => runtime.status));
199
+ this.deps.setState("total.costs.today", totals.costsToday);
200
+ this.deps.setState("total.costs.month", totals.costsMonth);
201
+ this.deps.setState("total.costs.projectedMonth", totals.costsProjectedMonth);
202
+ this.deps.setState("total.maxLimitPercent", totals.maxLimitPercent);
203
+ this.deps.setState("total.warningsActive", totals.warningsActive);
204
+ this.deps.setState("total.limitReached", totals.limitReached);
205
+ this.deps.setState("total.accountsReachable", totals.accountsReachable);
206
+ this.deps.setState("total.accounts", totals.accounts);
207
+ this.deps.setState("info.connection", totals.accountsReachable > 0);
208
+ return Promise.resolve();
209
+ }
210
+ /**
211
+ * The static per-account objects that exist regardless of what the source delivers.
212
+ *
213
+ * @param runtime the account's runtime
214
+ */
215
+ async createAccountSkeleton(runtime) {
216
+ const { config } = runtime;
217
+ const defs = [
218
+ { id: config.id, type: "device", common: { name: `${config.name} (${config.provider})` } },
219
+ { id: `${config.id}.info`, type: "channel", common: { name: "Info" } },
220
+ {
221
+ id: `${config.id}.info.provider`,
222
+ type: "state",
223
+ common: { name: "Provider", type: "string", role: "text", read: true, write: false }
224
+ },
225
+ {
226
+ id: `${config.id}.info.reachable`,
227
+ type: "state",
228
+ common: { name: "Reachable", type: "boolean", role: "indicator.reachable", read: true, write: false }
229
+ },
230
+ {
231
+ id: `${config.id}.info.lastUpdate`,
232
+ type: "state",
233
+ common: { name: "Last successful update", type: "string", role: "date", read: true, write: false }
234
+ },
235
+ {
236
+ id: `${config.id}.warning`,
237
+ type: "state",
238
+ common: { name: "Above warn threshold", type: "boolean", role: "indicator", read: true, write: false }
239
+ },
240
+ {
241
+ id: `${config.id}.limitReached`,
242
+ type: "state",
243
+ common: { name: "A limit window is full", type: "boolean", role: "indicator", read: true, write: false }
244
+ }
245
+ ];
246
+ for (const def of defs) {
247
+ await this.deps.upsertObject(def);
248
+ runtime.createdObjects.add(def.id);
249
+ }
250
+ this.deps.setState(`${config.id}.info.provider`, config.provider);
251
+ this.deps.setState(`${config.id}.info.reachable`, false);
252
+ }
253
+ /** The totals skeleton (channel + states). */
254
+ async createTotalsSkeleton() {
255
+ const defs = [
256
+ { id: "total.costs", type: "channel", common: { name: "Costs (USD accounts)" } },
257
+ {
258
+ id: "total.costs.today",
259
+ type: "state",
260
+ common: {
261
+ name: "Costs today (all accounts)",
262
+ type: "number",
263
+ role: "value",
264
+ read: true,
265
+ write: false,
266
+ unit: "USD"
267
+ }
268
+ },
269
+ {
270
+ id: "total.costs.month",
271
+ type: "state",
272
+ common: {
273
+ name: "Costs this month (all accounts)",
274
+ type: "number",
275
+ role: "value",
276
+ read: true,
277
+ write: false,
278
+ unit: "USD"
279
+ }
280
+ },
281
+ {
282
+ id: "total.costs.projectedMonth",
283
+ type: "state",
284
+ common: {
285
+ name: "Costs projected month-end (computed)",
286
+ type: "number",
287
+ role: "value",
288
+ read: true,
289
+ write: false,
290
+ unit: "USD"
291
+ }
292
+ },
293
+ {
294
+ id: "total.maxLimitPercent",
295
+ type: "state",
296
+ common: {
297
+ name: "Highest limit utilisation of any account",
298
+ type: "number",
299
+ role: "value",
300
+ read: true,
301
+ write: false,
302
+ unit: "%"
303
+ }
304
+ },
305
+ {
306
+ id: "total.warningsActive",
307
+ type: "state",
308
+ common: {
309
+ name: "Accounts above their warn threshold",
310
+ type: "number",
311
+ role: "value",
312
+ read: true,
313
+ write: false
314
+ }
315
+ },
316
+ {
317
+ id: "total.limitReached",
318
+ type: "state",
319
+ common: { name: "Any limit window full", type: "boolean", role: "indicator", read: true, write: false }
320
+ },
321
+ {
322
+ id: "total.accountsReachable",
323
+ type: "state",
324
+ common: { name: "Reachable accounts", type: "number", role: "value", read: true, write: false }
325
+ },
326
+ {
327
+ id: "total.accounts",
328
+ type: "state",
329
+ common: { name: "Configured accounts", type: "number", role: "value", read: true, write: false }
330
+ }
331
+ ];
332
+ for (const def of defs) {
333
+ await this.deps.upsertObject(def);
334
+ }
335
+ }
336
+ }
337
+ // Annotate the CommonJS export names for ESM import in node:
338
+ 0 && (module.exports = {
339
+ PollEngine
340
+ });
341
+ //# sourceMappingURL=poll-engine.js.map