iobroker.ai-usage 0.6.0 → 0.7.1
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/README.md +45 -103
- package/build/lib/poll-engine.js +28 -24
- package/build/lib/poll-engine.js.map +2 -2
- package/build/lib/pure-helpers.js +12 -0
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/sign-in.js +7 -0
- package/build/lib/sign-in.js.map +2 -2
- package/build/main.js +79 -6
- package/build/main.js.map +2 -2
- package/io-package.json +27 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
**Support:** [](https://ko-fi.com/krobipd) [](https://paypal.me/krobipd)
|
|
8
8
|
|
|
9
|
-
Monitors your AI accounts
|
|
10
|
-
|
|
11
|
-
accounts — as clean datapoints for dashboards, history and automations ("send a message at
|
|
12
|
-
90 % of the weekly limit").
|
|
9
|
+
Monitors usage, limits and costs of your AI accounts — the Claude, ChatGPT and Google
|
|
10
|
+
subscriptions plus OpenRouter, DeepSeek, OpenAI and Anthropic API accounts. Needs ioBroker Admin 8.
|
|
13
11
|
|
|
14
12
|
---
|
|
15
13
|
|
|
16
14
|
## Features
|
|
17
15
|
|
|
18
|
-
- **
|
|
19
|
-
- **Totals** — summed costs, the highest
|
|
20
|
-
- **Warn threshold per account** —
|
|
21
|
-
- **Three subscriptions** — Claude, ChatGPT and Google
|
|
22
|
-
- **Central credentials** — API keys
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **Throttle-safe
|
|
16
|
+
- **One node per account** — limit windows with percent and reset time, credits, costs and tokens, named the same way for every provider
|
|
17
|
+
- **Totals** — summed costs, the highest utilisation of any account, and one trigger for automations
|
|
18
|
+
- **Warn threshold per account** — one notification when an account crosses it
|
|
19
|
+
- **Three subscriptions** — Claude, ChatGPT and Google, signed in with your own account; the settings page walks you through each step
|
|
20
|
+
- **Central credentials** — API keys come from the admin's credential storage, shared with the admin AI assistant
|
|
21
|
+
- **Online status** — the connection icon you know from every device, plus the reason in plain text
|
|
22
|
+
- **Read-only** — the adapter only reads; it never calls or configures an AI service
|
|
23
|
+
- **Throttle-safe** — a minimum interval and automatic backoff keep the provider from locking your account
|
|
26
24
|
|
|
27
25
|
---
|
|
28
26
|
|
|
@@ -44,54 +42,24 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
|
|
|
44
42
|
|
|
45
43
|
## Configuration
|
|
46
44
|
|
|
47
|
-
The instance settings show one list of AI accounts. Switch on what you want to monitor
|
|
45
|
+
The instance settings show one list of AI accounts. Switch on what you want to monitor.
|
|
48
46
|
|
|
49
47
|
| Account | How it is connected |
|
|
50
48
|
|---------|---------------------|
|
|
51
|
-
| **Claude subscription** |
|
|
52
|
-
| **ChatGPT subscription** |
|
|
53
|
-
| **Google
|
|
54
|
-
| **OpenRouter, DeepSeek** | Pick the stored key from the admin's
|
|
55
|
-
| **OpenAI, Anthropic
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **Claude** — open the sign-in page, log in, copy the code shown there and paste it back.
|
|
62
|
-
- **ChatGPT** — the adapter shows a short code; type it on the OpenAI page it links to. The
|
|
63
|
-
settings page notices by itself once you confirmed. The adapter signs in separately from the
|
|
64
|
-
Codex CLI and does not disturb its session.
|
|
65
|
-
- **Google/Gemini** — open the sign-in page and log in. Google then sends the result to
|
|
66
|
-
`localhost`, so **your browser shows an error page ("site cannot be reached"). That is expected**
|
|
67
|
-
— the address bar still carries the sign-in result. Copy the **whole address** and paste it back.
|
|
68
|
-
|
|
69
|
-
> The three subscription endpoints are **not officially documented**. They are the same ones the
|
|
70
|
-
> providers' own tools use, but they can change without notice. If a subscription stops delivering
|
|
71
|
-
> data, that is the likely reason.
|
|
72
|
-
>
|
|
73
|
-
> **Verification status:** the Claude sign-in and read-out were tested against a live subscription.
|
|
74
|
-
> ChatGPT and Google/Gemini were built from the requests those providers' own tools make, but no
|
|
75
|
-
> live subscription of either was available to test them on — please open an issue if a value looks
|
|
76
|
-
> wrong or a sign-in step behaves differently than described.
|
|
77
|
-
|
|
78
|
-
### API accounts with a key
|
|
79
|
-
|
|
80
|
-
Keys come from the admin's central credential storage (Settings → Credentials) and are shared with
|
|
81
|
-
the admin AI assistant. Two of them need a **different** key than the assistant uses:
|
|
82
|
-
|
|
83
|
-
- **OpenAI** — the usage and cost reports are organisation endpoints and need an **admin key**
|
|
84
|
-
created in the organisation settings.
|
|
85
|
-
- **Anthropic** — same, an **admin key** (`sk-ant-admin…`), which only an organisation
|
|
86
|
-
administrator can create. Personal accounts without an organisation cannot deliver these reports
|
|
87
|
-
at all; use the Claude subscription row instead.
|
|
88
|
-
|
|
89
|
-
Add such a key as its own entry in the credential storage (template "Key") and pick it in the row.
|
|
49
|
+
| **[Claude](https://claude.ai) subscription** | Open the sign-in page, log in, copy the code shown there and paste it back |
|
|
50
|
+
| **[ChatGPT](https://chatgpt.com) subscription** | The adapter shows a short code; type it on the OpenAI page it links to. The settings page notices by itself. Your Codex CLI session is not touched |
|
|
51
|
+
| **[Google Gemini](https://gemini.google.com) subscription** | Open the sign-in page and log in. Google sends the result to `localhost`, so **your browser shows an error page — that is expected**. Copy the **whole address** from the address bar and paste it back |
|
|
52
|
+
| **[OpenRouter](https://openrouter.ai), [DeepSeek](https://www.deepseek.com)** | Pick the stored key from the admin's credential storage |
|
|
53
|
+
| **[OpenAI](https://openai.com), [Anthropic](https://www.anthropic.com)** | Needs an **admin key** of your organisation, not the key the admin assistant uses. A personal account without an organisation cannot deliver these reports at all — use the Claude subscription instead |
|
|
54
|
+
|
|
55
|
+
The three subscription endpoints are **not officially documented**; they are the ones those
|
|
56
|
+
providers' own tools use and can change without notice. Claude was tested against a live
|
|
57
|
+
subscription, ChatGPT and Google could not be — please open an issue if something looks wrong.
|
|
90
58
|
|
|
91
59
|
| Option | Description | Default |
|
|
92
60
|
|--------|-------------|---------|
|
|
93
61
|
| **Warn at %** | Per account: one notification when a plan-wide limit window crosses this utilisation | 80 |
|
|
94
|
-
| **Poll interval** | How often each account is queried, 60–3600 seconds. The floor
|
|
62
|
+
| **Poll interval** | How often each account is queried, 60–3600 seconds. The floor keeps the provider from throttling you | 300 |
|
|
95
63
|
| **Notifications** | One notification on threshold crossing or broken credentials | on |
|
|
96
64
|
|
|
97
65
|
---
|
|
@@ -104,7 +72,7 @@ ai-usage.0.
|
|
|
104
72
|
├── total. — totals across all accounts
|
|
105
73
|
│ ├── costs.today/month/… — summed real money (same currency only)
|
|
106
74
|
│ ├── maxLimitPercent — highest plan-wide utilisation of any account
|
|
107
|
-
│ ├── warningsActive —
|
|
75
|
+
│ ├── warningsActive — accounts above their threshold
|
|
108
76
|
│ ├── limitReached — a plan-wide window is full (automation trigger)
|
|
109
77
|
│ ├── accountsReachable — accounts currently delivering data
|
|
110
78
|
│ └── accounts — configured accounts
|
|
@@ -125,52 +93,29 @@ ai-usage.0.
|
|
|
125
93
|
|
|
126
94
|
Only what an account's source actually delivers is created.
|
|
127
95
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
the icon shows up in the object tree without you configuring anything.
|
|
133
|
-
|
|
134
|
-
**`info.unreach`** is that marker. **`info.error`** says what is wrong, in a sentence you can read:
|
|
135
|
-
|
|
136
|
-
| Situation | Icon | `info.error` |
|
|
137
|
-
|-----------|------|--------------|
|
|
138
|
-
| Everything works | connected | empty |
|
|
139
|
-
| Throttled by the provider | **connected** — the last values stay valid while the adapter waits | `Throttled by the provider — retrying in 10 min …` |
|
|
140
|
-
| Sign-in or key rejected | disconnected | `Sign-in rejected — …` |
|
|
141
|
-
| The service reports a fault | disconnected, immediately | `The AI service reports a fault — HTTP 503` |
|
|
142
|
-
| Not reachable at all | disconnected after three attempts in a row, so a hiccup does not make it flap | `Not reachable after 3 attempts — …` |
|
|
96
|
+
**The connection icon** sits next to each account, green while it delivers. A throttle keeps it
|
|
97
|
+
green — the last values stay valid while the adapter waits. A rejected sign-in or a broken service
|
|
98
|
+
switch it off at once, an unreachable service after three attempts, so a hiccup does not make it
|
|
99
|
+
flap. `info.error` always names the cause.
|
|
143
100
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
sit at 100 % forever, and an alarm that never clears is worse than none. Google is the exception,
|
|
150
|
-
because there every bucket belongs to a model — those are your quota, so they do count.
|
|
151
|
-
|
|
152
|
-
Want a specific model watched anyway? Build the automation on its own datapoint,
|
|
153
|
-
`limits.<window>.percent`.
|
|
101
|
+
**Only plan-wide windows raise the warning** — your session and your week — and the message names
|
|
102
|
+
the window it came from. A window belonging to a single model keeps its own datapoints but stays
|
|
103
|
+
out of it: a model you never use can sit at 100 % forever, and an alarm that never clears is worse
|
|
104
|
+
than none. Google is the exception, because there every bucket belongs to a model. To watch one
|
|
105
|
+
model anyway, build the automation on its own `limits.<window>.percent`.
|
|
154
106
|
|
|
155
107
|
---
|
|
156
108
|
|
|
157
109
|
## Troubleshooting
|
|
158
110
|
|
|
159
111
|
### An account delivers no data
|
|
160
|
-
Read `info.error`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
the same reason once and raises a single notification.
|
|
112
|
+
Read `info.error` — it names the cause. A rejected sign-in means signing in again in the settings,
|
|
113
|
+
or that the key is not an organisation admin key. A service fault or a missing connection is
|
|
114
|
+
outside your instance and clears up by itself. The log states the same reason once.
|
|
164
115
|
|
|
165
116
|
### A subscription says "not signed in" although you just signed in
|
|
166
117
|
Save the settings first, then sign in — the row needs a saved account to attach the sign-in to.
|
|
167
|
-
After a successful sign-in the
|
|
168
|
-
seconds.
|
|
169
|
-
|
|
170
|
-
### The object tree changed after updating to 0.3.0
|
|
171
|
-
Accounts now live under fixed ids (`claude`, `chatgpt`, `gemini`, `<name>-api`) and the separate
|
|
172
|
-
`auth` branch is gone — the sign-in state moved into `<account>.info.signedIn`. Old nodes are
|
|
173
|
-
removed automatically, and an existing Claude sign-in is carried over.
|
|
118
|
+
After a successful sign-in the account is queried immediately, so values appear within seconds.
|
|
174
119
|
|
|
175
120
|
---
|
|
176
121
|
|
|
@@ -180,6 +125,16 @@ removed automatically, and an existing Claude sign-in is carried over.
|
|
|
180
125
|
Placeholder for the next version (at the beginning of the line):
|
|
181
126
|
### **WORK IN PROGRESS**
|
|
182
127
|
-->
|
|
128
|
+
### 0.7.1 (2026-08-27)
|
|
129
|
+
|
|
130
|
+
- Fixed: Restarting the adapter no longer writes one unchanged value into every status datapoint, so a recorded history stays free of restart noise
|
|
131
|
+
|
|
132
|
+
### 0.7.0 (2026-08-27)
|
|
133
|
+
|
|
134
|
+
- New: One log line after a change tells you how many datapoints the object tree gained and lost, instead of leaving you to click through the tree
|
|
135
|
+
- Fixed: A started sign-in that sat unused for a quarter of an hour now says so plainly instead of failing later with the provider's own cryptic answer
|
|
136
|
+
- Fixed: Datapoints that only repeat their previous value are no longer rewritten every cycle, which kept flooding the history of anyone recording them
|
|
137
|
+
|
|
183
138
|
### 0.6.0 (2026-08-26)
|
|
184
139
|
|
|
185
140
|
- New: Each account now shows the connection icon in the object tree — green while it delivers, struck through when it does not, exactly like every other ioBroker device
|
|
@@ -197,19 +152,6 @@ removed automatically, and an existing Claude sign-in is carried over.
|
|
|
197
152
|
- New: Error reporting via Sentry — crashes reach the developer automatically, but only if you enabled diagnostics and error reporting in the ioBroker system settings
|
|
198
153
|
- Changed: New adapter icon — a network of nodes instead of the dark tile, so it reads as AI at a glance and sits cleanly in both the light and the dark admin
|
|
199
154
|
|
|
200
|
-
### 0.3.0 (2026-08-26)
|
|
201
|
-
|
|
202
|
-
- New: ChatGPT and Google/Gemini subscriptions can now be monitored like the Claude one — each with its own guided sign-in that the settings page walks you through step by step
|
|
203
|
-
- New: The ChatGPT and Gemini readouts use the same endpoints the providers' own tools use, but no live subscription was available to test them on — please report anything that looks wrong
|
|
204
|
-
- Changed: Each account now owns exactly one node in the object tree (`claude`, `chatgpt`, `gemini`, `<name>-api`); the separate sign-in branch is gone and old nodes are cleaned up automatically
|
|
205
|
-
- Fixed: After signing in, the account is queried immediately instead of waiting for the next poll — no restart needed
|
|
206
|
-
- Fixed: OpenAI and Anthropic rows now state that they need an organisation admin key, instead of failing with an unexplained rejection
|
|
207
|
-
|
|
208
|
-
### 0.2.0 (2026-08-26)
|
|
209
|
-
|
|
210
|
-
- Changed: Completely new settings page — your stored AI credentials appear as simple on/off switches instead of a table, and new keys are picked up straight from the admin credential storage
|
|
211
|
-
- Fixed: The Claude subscription sign-in works reliably now — a guided card with live status, and the sign-in link stays valid until it is used instead of regenerating while you type
|
|
212
|
-
|
|
213
155
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
214
156
|
|
|
215
157
|
## Support
|
package/build/lib/poll-engine.js
CHANGED
|
@@ -56,7 +56,7 @@ class PollEngine {
|
|
|
56
56
|
state: "no-connection",
|
|
57
57
|
error: "waiting for the first query",
|
|
58
58
|
createdObjects: /* @__PURE__ */ new Set(),
|
|
59
|
-
|
|
59
|
+
firstPollDone: false
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -65,6 +65,7 @@ class PollEngine {
|
|
|
65
65
|
runtimes = [];
|
|
66
66
|
handles = [];
|
|
67
67
|
stopped = false;
|
|
68
|
+
firstRoundReported = false;
|
|
68
69
|
/** Create the static per-account and totals objects, then arm the poll cycles. */
|
|
69
70
|
async start() {
|
|
70
71
|
for (const runtime of this.runtimes) {
|
|
@@ -72,6 +73,10 @@ class PollEngine {
|
|
|
72
73
|
}
|
|
73
74
|
await this.createTotalsSkeleton();
|
|
74
75
|
await this.writeTotals();
|
|
76
|
+
if (this.runtimes.length === 0) {
|
|
77
|
+
this.reportFirstRoundOnce();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
75
80
|
this.runtimes.forEach((runtime, index) => {
|
|
76
81
|
this.handles.push(
|
|
77
82
|
this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),
|
|
@@ -117,6 +122,8 @@ class PollEngine {
|
|
|
117
122
|
const { config } = runtime;
|
|
118
123
|
if (this.deps.now() < runtime.skipUntil) {
|
|
119
124
|
this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);
|
|
125
|
+
runtime.firstPollDone = true;
|
|
126
|
+
this.reportFirstRoundOnce();
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
try {
|
|
@@ -135,6 +142,17 @@ class PollEngine {
|
|
|
135
142
|
}
|
|
136
143
|
this.writeAccountInfo(runtime);
|
|
137
144
|
await this.writeTotals();
|
|
145
|
+
runtime.firstPollDone = true;
|
|
146
|
+
this.reportFirstRoundOnce();
|
|
147
|
+
}
|
|
148
|
+
/** Fire the first-round hook exactly once, when no account is still pending. */
|
|
149
|
+
reportFirstRoundOnce() {
|
|
150
|
+
var _a, _b;
|
|
151
|
+
if (this.firstRoundReported || this.runtimes.some((runtime) => !runtime.firstPollDone)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.firstRoundReported = true;
|
|
155
|
+
(_b = (_a = this.deps).afterFirstRound) == null ? void 0 : _b.call(_a);
|
|
138
156
|
}
|
|
139
157
|
/**
|
|
140
158
|
* Write a successful snapshot: upsert new objects (create-once cache), write the
|
|
@@ -160,8 +178,8 @@ class PollEngine {
|
|
|
160
178
|
const percent = (_a = driver == null ? void 0 : driver.percent) != null ? _a : 0;
|
|
161
179
|
const wasWarning = runtime.status.warning;
|
|
162
180
|
runtime.status.warning = percent >= config.warnThreshold;
|
|
163
|
-
this.deps.
|
|
164
|
-
this.deps.
|
|
181
|
+
this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);
|
|
182
|
+
this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100);
|
|
165
183
|
if (runtime.status.warning && !wasWarning) {
|
|
166
184
|
const window = driver ? `${driver.label} ` : "";
|
|
167
185
|
const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;
|
|
@@ -234,7 +252,7 @@ class PollEngine {
|
|
|
234
252
|
}
|
|
235
253
|
}
|
|
236
254
|
/**
|
|
237
|
-
* Write one account's info states (
|
|
255
|
+
* Write one account's info states (offline marker, error text, last update).
|
|
238
256
|
*
|
|
239
257
|
* @param runtime the account's runtime
|
|
240
258
|
*/
|
|
@@ -252,30 +270,16 @@ class PollEngine {
|
|
|
252
270
|
* has to mean what a user reads into that icon: green while the account delivers.
|
|
253
271
|
* A throttle keeps the last values and the service is fine, so it stays green and
|
|
254
272
|
* only fills the error text; a dead sign-in, a broken service or no connection at
|
|
255
|
-
* all turn it off. Both
|
|
256
|
-
*
|
|
273
|
+
* all turn it off. Both go through the changed-write: an indicator rewritten every
|
|
274
|
+
* cycle floods the history and hides the real transition.
|
|
257
275
|
*
|
|
258
276
|
* @param runtime the account's runtime
|
|
259
277
|
*/
|
|
260
278
|
writeAccountStatus(runtime) {
|
|
261
279
|
const { config } = runtime;
|
|
262
280
|
const delivering = runtime.state === "ok" || runtime.state === "rate-limited";
|
|
263
|
-
this.
|
|
264
|
-
this.
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Write a state only when its value actually changed since the last write.
|
|
268
|
-
*
|
|
269
|
-
* @param runtime the account's runtime (holds the last-written cache)
|
|
270
|
-
* @param id the state id
|
|
271
|
-
* @param value the value
|
|
272
|
-
*/
|
|
273
|
-
setIfChanged(runtime, id, value) {
|
|
274
|
-
if (runtime.written.get(id) === value) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
runtime.written.set(id, value);
|
|
278
|
-
this.deps.setState(id, value);
|
|
281
|
+
this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering);
|
|
282
|
+
this.deps.setStateChanged(`${config.id}.info.error`, runtime.error);
|
|
279
283
|
}
|
|
280
284
|
/** Recompute and write the totals + info.connection. */
|
|
281
285
|
async writeTotals() {
|
|
@@ -285,10 +289,10 @@ class PollEngine {
|
|
|
285
289
|
this.deps.setState("total.costs.projectedMonth", totals.costsProjectedMonth);
|
|
286
290
|
this.deps.setState("total.maxLimitPercent", totals.maxLimitPercent);
|
|
287
291
|
this.deps.setState("total.warningsActive", totals.warningsActive);
|
|
288
|
-
this.deps.
|
|
292
|
+
this.deps.setStateChanged("total.limitReached", totals.limitReached);
|
|
289
293
|
this.deps.setState("total.accountsReachable", totals.accountsReachable);
|
|
290
294
|
this.deps.setState("total.accounts", totals.accounts);
|
|
291
|
-
this.deps.
|
|
295
|
+
this.deps.setStateChanged("info.connection", totals.accountsReachable > 0);
|
|
292
296
|
return Promise.resolve();
|
|
293
297
|
}
|
|
294
298
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/poll-engine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AccountConfig } from \"./pure-helpers\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { limitingWindow, mapSnapshot, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/**\n * What the adapter currently knows about one account, in one word.\n *\n * `ok` and `rate-limited` mean the AI service is up and talking to us,\n * `unauthorized` means it is up but rejects our sign-in, `service-down` means the\n * service itself answered with a fault, `no-connection` means we never reached it.\n */\nexport type AccountState = \"ok\" | \"unauthorized\" | \"rate-limited\" | \"service-down\" | \"no-connection\";\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Write a state value with ack. */\n setState(id: string, value: boolean | number | string): void;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Last written value per state id \u2014 keeps unchanged values out of the history. */\n written: Map<string, boolean | number | string>;\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, service = reported down at once,\n * network = tolerated three times), warn-threshold transitions on the PLAN-WIDE\n * windows only, and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n for (const config of accounts) {\n const provider = providers.get(config.id);\n if (!provider) {\n deps.log.warn(`${config.name}: provider \"${config.provider}\" is not available \u2014 account skipped`);\n continue;\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n serviceOnline: false,\n state: \"no-connection\",\n error: \"waiting for the first query\",\n createdObjects: new Set(),\n written: new Map(),\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n await this.writeTotals();\n this.runtimes.forEach((runtime, index) => {\n this.handles.push(\n this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),\n this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /** The account ids the engine drives (for the stale-object cleanup). */\n public get accountIds(): string[] {\n return this.runtimes.map(runtime => runtime.config.id);\n }\n\n /**\n * Poll one account immediately, by id. Used after a successful sign-in: waiting\n * up to a full interval there reads as \"the sign-in did not work\".\n *\n * @param accountId the account's object id\n */\n public async pollNow(accountId: string): Promise<void> {\n const runtime = this.runtimes.find(entry => entry.config.id === accountId);\n if (runtime) {\n // A fresh sign-in clears a previous auth failure and any backoff.\n runtime.authNotified = false;\n runtime.skipUntil = 0;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped) {\n return;\n }\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n return;\n }\n try {\n const snapshot = await runtime.provider.fetch();\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.status.reachable = true;\n runtime.serviceOnline = true;\n runtime.state = \"ok\";\n runtime.error = \"\";\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n this.handleFailure(runtime, e);\n }\n this.writeAccountInfo(runtime);\n await this.writeTotals();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, config.name, config.provider, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26).\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n this.deps.setState(`${config.id}.warning`, runtime.status.warning);\n this.deps.setState(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Classify a fetch failure.\n *\n * The split matters for the online indicator: with `auth` and `rate-limit` the AI\n * service ANSWERED \u2014 it is online, it just said no \u2014 so only our own access is\n * broken. `service` means the service answered with a fault of its own and is\n * reported as down at once (it told us, that is not a flake). A `network` failure\n * is tolerated MAX_NETWORK_FAILURES times before we call the connection gone, so\n * a single hiccup does not make the indicator flap.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.status.reachable = false;\n runtime.serviceOnline = true;\n runtime.state = \"unauthorized\";\n runtime.error = `Sign-in rejected \u2014 ${message}`;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.serviceOnline = true;\n runtime.state = \"rate-limited\";\n runtime.error = `Throttled by the provider \u2014 retrying in ${Math.round(runtime.backoffMs / 60000)} min, last values kept`;\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n if (error instanceof FetchError && error.kind === \"service\") {\n runtime.status.reachable = false;\n runtime.failCount = 0;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: the service reports a fault (${message}) \u2014 values kept`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"service-down\";\n runtime.error = `The AI service reports a fault \u2014 ${message}`;\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n runtime.status.reachable = false;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: not reachable after ${runtime.failCount} attempts (${message})`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"no-connection\";\n runtime.error = `Not reachable after ${runtime.failCount} attempts \u2014 ${message}`;\n }\n }\n\n /**\n * Write one account's info states (data received, service online, state, last update).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n this.writeAccountStatus(runtime);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both states are written on change only \u2014 an indicator rewritten\n * every cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private writeAccountStatus(runtime: AccountRuntime): void {\n const { config } = runtime;\n const delivering = runtime.state === \"ok\" || runtime.state === \"rate-limited\";\n this.setIfChanged(runtime, `${config.id}.info.unreach`, !delivering);\n this.setIfChanged(runtime, `${config.id}.info.error`, runtime.error);\n }\n\n /**\n * Write a state only when its value actually changed since the last write.\n *\n * @param runtime the account's runtime (holds the last-written cache)\n * @param id the state id\n * @param value the value\n */\n private setIfChanged(runtime: AccountRuntime, id: string, value: boolean | number | string): void {\n if (runtime.written.get(id) === value) {\n return;\n }\n runtime.written.set(id, value);\n this.deps.setState(id, value);\n }\n\n /** Recompute and write the totals + info.connection. */\n private async writeTotals(): Promise<void> {\n const totals = computeTotals(this.runtimes.map(runtime => runtime.status));\n this.deps.setState(\"total.costs.today\", totals.costsToday);\n this.deps.setState(\"total.costs.month\", totals.costsMonth);\n this.deps.setState(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n this.deps.setState(\"total.maxLimitPercent\", totals.maxLimitPercent);\n this.deps.setState(\"total.warningsActive\", totals.warningsActive);\n this.deps.setState(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n this.deps.setState(\"total.accounts\", totals.accounts);\n this.deps.setState(\"info.connection\", totals.accountsReachable > 0);\n return Promise.resolve();\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n {\n id: config.id,\n type: \"device\",\n common: {\n name: `${config.name} (${config.provider})`,\n // The admin's object tree draws its connection icon from this link and\n // from nothing else \u2014 govee, beszel, homewizard and nut2 all do the same.\n statusStates: { offlineId: \"info.unreach\" },\n },\n },\n { id: `${config.id}.info`, type: \"channel\", common: { name: \"Info\" } },\n {\n // The two slots ioBroker itself provides for this \u2014 measured against\n // @iobroker/type-detector 6.0.0: `unreach` is the offline marker every\n // device type carries (`indicator.reachable` is deprecated there), and\n // `indicator.error` is the standard home for the reason.\n id: `${config.id}.info.unreach`,\n type: \"state\",\n common: {\n name: \"AI service not reachable\",\n type: \"boolean\",\n role: \"indicator.maintenance.unreach\",\n read: true,\n write: false,\n },\n },\n {\n // NOT `indicator.error`: the two official sources disagree \u2014 the type-detector\n // lists that role as a String, the repochecker's validity whitelist allows\n // boolean only (E1009). Validity wins, so the message rides on `text`.\n id: `${config.id}.info.error`,\n type: \"state\",\n common: { name: \"Last error\", type: \"string\", role: \"text\", read: true, write: false },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: { name: \"Last successful update\", type: \"string\", role: \"date\", read: true, write: false },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: { name: \"Above warn threshold\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: {\n name: \"A plan-wide limit window is full\",\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n this.writeAccountStatus(runtime);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: \"Costs (USD accounts)\" } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: \"Costs today (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: \"Costs this month (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: \"Costs projected month-end (computed)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n name: \"Highest plan-wide utilisation of any account\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: \"Accounts above their warn threshold\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: {\n name: \"Any plan-wide limit window full\",\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: { name: \"Reachable accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: { name: \"Configured accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,sBAAmE;AACnE,2BAA4D;AAC5D,oBAAkD;AAYlD,MAAM,uBAAuB;AAE7B,MAAM,mBAAmB,KAAK,KAAK;AAEnC,MAAM,iBAAiB,KAAK,KAAK;AAEjC,MAAM,aAAa;
|
|
4
|
+
"sourcesContent": ["import type { AccountConfig } from \"./pure-helpers\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { limitingWindow, mapSnapshot, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/**\n * What the adapter currently knows about one account, in one word.\n *\n * `ok` and `rate-limited` mean the AI service is up and talking to us,\n * `unauthorized` means it is up but rejects our sign-in, `service-down` means the\n * service itself answered with a fault, `no-connection` means we never reached it.\n */\nexport type AccountState = \"ok\" | \"unauthorized\" | \"rate-limited\" | \"service-down\" | \"no-connection\";\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Write a state value with ack \u2014 for MEASUREMENTS, where every cycle carries information. */\n setState(id: string, value: boolean | number | string): void;\n /**\n * Write a state only when the value differs from what the database holds \u2014 for\n * INDICATORS. js-controller does the comparison (`setStateChangedAsync`), which is\n * what the rest of the fleet uses; a hand-rolled cache would only know what this\n * process wrote and would still write blindly after a restart.\n */\n setStateChanged(id: string, value: boolean | number | string): void;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n /**\n * Called once, when every account has finished its FIRST poll.\n *\n * The first round is staggered on purpose, so the adapter cannot report what the\n * object tree gained until the last account has been through. A config change\n * restarts the instance, so this is also the only moment a user needs the report.\n */\n afterFirstRound?(): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Whether this account has been through its first poll. */\n firstPollDone: boolean;\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, service = reported down at once,\n * network = tolerated three times), warn-threshold transitions on the PLAN-WIDE\n * windows only, and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n private firstRoundReported = false;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n for (const config of accounts) {\n const provider = providers.get(config.id);\n if (!provider) {\n deps.log.warn(`${config.name}: provider \"${config.provider}\" is not available \u2014 account skipped`);\n continue;\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n serviceOnline: false,\n state: \"no-connection\",\n error: \"waiting for the first query\",\n createdObjects: new Set(),\n firstPollDone: false,\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n await this.writeTotals();\n if (this.runtimes.length === 0) {\n // Nothing will ever poll \u2014 report right away, the cleanup may still have changed something.\n this.reportFirstRoundOnce();\n return;\n }\n this.runtimes.forEach((runtime, index) => {\n this.handles.push(\n this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),\n this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /** The account ids the engine drives (for the stale-object cleanup). */\n public get accountIds(): string[] {\n return this.runtimes.map(runtime => runtime.config.id);\n }\n\n /**\n * Poll one account immediately, by id. Used after a successful sign-in: waiting\n * up to a full interval there reads as \"the sign-in did not work\".\n *\n * @param accountId the account's object id\n */\n public async pollNow(accountId: string): Promise<void> {\n const runtime = this.runtimes.find(entry => entry.config.id === accountId);\n if (runtime) {\n // A fresh sign-in clears a previous auth failure and any backoff.\n runtime.authNotified = false;\n runtime.skipUntil = 0;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped) {\n return;\n }\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n // Still counts as \"been through\": otherwise an account that starts inside a\n // backoff would hold the first-round report back forever.\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n return;\n }\n try {\n const snapshot = await runtime.provider.fetch();\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.status.reachable = true;\n runtime.serviceOnline = true;\n runtime.state = \"ok\";\n runtime.error = \"\";\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n this.handleFailure(runtime, e);\n }\n this.writeAccountInfo(runtime);\n await this.writeTotals();\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n }\n\n /** Fire the first-round hook exactly once, when no account is still pending. */\n private reportFirstRoundOnce(): void {\n if (this.firstRoundReported || this.runtimes.some(runtime => !runtime.firstPollDone)) {\n return;\n }\n this.firstRoundReported = true;\n this.deps.afterFirstRound?.();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, config.name, config.provider, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26).\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n // Indicators go through the changed-write, measurements through the normal one.\n this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);\n this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Classify a fetch failure.\n *\n * The split matters for the online indicator: with `auth` and `rate-limit` the AI\n * service ANSWERED \u2014 it is online, it just said no \u2014 so only our own access is\n * broken. `service` means the service answered with a fault of its own and is\n * reported as down at once (it told us, that is not a flake). A `network` failure\n * is tolerated MAX_NETWORK_FAILURES times before we call the connection gone, so\n * a single hiccup does not make the indicator flap.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.status.reachable = false;\n runtime.serviceOnline = true;\n runtime.state = \"unauthorized\";\n runtime.error = `Sign-in rejected \u2014 ${message}`;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.serviceOnline = true;\n runtime.state = \"rate-limited\";\n runtime.error = `Throttled by the provider \u2014 retrying in ${Math.round(runtime.backoffMs / 60000)} min, last values kept`;\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n if (error instanceof FetchError && error.kind === \"service\") {\n runtime.status.reachable = false;\n runtime.failCount = 0;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: the service reports a fault (${message}) \u2014 values kept`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"service-down\";\n runtime.error = `The AI service reports a fault \u2014 ${message}`;\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n runtime.status.reachable = false;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: not reachable after ${runtime.failCount} attempts (${message})`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"no-connection\";\n runtime.error = `Not reachable after ${runtime.failCount} attempts \u2014 ${message}`;\n }\n }\n\n /**\n * Write one account's info states (offline marker, error text, last update).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n this.writeAccountStatus(runtime);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both go through the changed-write: an indicator rewritten every\n * cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private writeAccountStatus(runtime: AccountRuntime): void {\n const { config } = runtime;\n const delivering = runtime.state === \"ok\" || runtime.state === \"rate-limited\";\n this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering);\n this.deps.setStateChanged(`${config.id}.info.error`, runtime.error);\n }\n\n /** Recompute and write the totals + info.connection. */\n private async writeTotals(): Promise<void> {\n const totals = computeTotals(this.runtimes.map(runtime => runtime.status));\n this.deps.setState(\"total.costs.today\", totals.costsToday);\n this.deps.setState(\"total.costs.month\", totals.costsMonth);\n this.deps.setState(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n this.deps.setState(\"total.maxLimitPercent\", totals.maxLimitPercent);\n this.deps.setState(\"total.warningsActive\", totals.warningsActive);\n this.deps.setStateChanged(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n this.deps.setState(\"total.accounts\", totals.accounts);\n this.deps.setStateChanged(\"info.connection\", totals.accountsReachable > 0);\n return Promise.resolve();\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n {\n id: config.id,\n type: \"device\",\n common: {\n name: `${config.name} (${config.provider})`,\n // The admin's object tree draws its connection icon from this link and\n // from nothing else \u2014 govee, beszel, homewizard and nut2 all do the same.\n statusStates: { offlineId: \"info.unreach\" },\n },\n },\n { id: `${config.id}.info`, type: \"channel\", common: { name: \"Info\" } },\n {\n // The two slots ioBroker itself provides for this \u2014 measured against\n // @iobroker/type-detector 6.0.0: `unreach` is the offline marker every\n // device type carries (`indicator.reachable` is deprecated there), and\n // `indicator.error` is the standard home for the reason.\n id: `${config.id}.info.unreach`,\n type: \"state\",\n common: {\n name: \"AI service not reachable\",\n type: \"boolean\",\n role: \"indicator.maintenance.unreach\",\n read: true,\n write: false,\n },\n },\n {\n // NOT `indicator.error`: the two official sources disagree \u2014 the type-detector\n // lists that role as a String, the repochecker's validity whitelist allows\n // boolean only (E1009). Validity wins, so the message rides on `text`.\n id: `${config.id}.info.error`,\n type: \"state\",\n common: { name: \"Last error\", type: \"string\", role: \"text\", read: true, write: false },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: { name: \"Last successful update\", type: \"string\", role: \"date\", read: true, write: false },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: { name: \"Above warn threshold\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: {\n name: \"A plan-wide limit window is full\",\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n this.writeAccountStatus(runtime);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: \"Costs (USD accounts)\" } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: \"Costs today (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: \"Costs this month (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: \"Costs projected month-end (computed)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n name: \"Highest plan-wide utilisation of any account\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: \"Accounts above their warn threshold\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: {\n name: \"Any plan-wide limit window full\",\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: { name: \"Reachable accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: { name: \"Configured accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -24,6 +24,7 @@ __export(pure_helpers_exports, {
|
|
|
24
24
|
SUBSCRIPTION_KINDS: () => SUBSCRIPTION_KINDS,
|
|
25
25
|
accountId: () => accountId,
|
|
26
26
|
clampPollInterval: () => clampPollInterval,
|
|
27
|
+
datapointBalanceLine: () => datapointBalanceLine,
|
|
27
28
|
parseAccounts: () => parseAccounts,
|
|
28
29
|
sanitizeId: () => sanitizeId,
|
|
29
30
|
validAccountIds: () => validAccountIds
|
|
@@ -116,6 +117,16 @@ function clampPollInterval(raw) {
|
|
|
116
117
|
}
|
|
117
118
|
return Math.min(3600, Math.max(60, Math.round(value)));
|
|
118
119
|
}
|
|
120
|
+
function datapointBalanceLine(created, removed) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (created > 0) {
|
|
123
|
+
parts.push(`created ${created} datapoint(s)`);
|
|
124
|
+
}
|
|
125
|
+
if (removed > 0) {
|
|
126
|
+
parts.push(`removed ${removed} datapoint(s)`);
|
|
127
|
+
}
|
|
128
|
+
return parts.length ? `Object tree updated: ${parts.join(", ")}` : null;
|
|
129
|
+
}
|
|
119
130
|
// Annotate the CommonJS export names for ESM import in node:
|
|
120
131
|
0 && (module.exports = {
|
|
121
132
|
PROVIDER_KINDS,
|
|
@@ -124,6 +135,7 @@ function clampPollInterval(raw) {
|
|
|
124
135
|
SUBSCRIPTION_KINDS,
|
|
125
136
|
accountId,
|
|
126
137
|
clampPollInterval,
|
|
138
|
+
datapointBalanceLine,
|
|
127
139
|
parseAccounts,
|
|
128
140
|
sanitizeId,
|
|
129
141
|
validAccountIds
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/pure-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["/** One configured AI account, validated from the admin table (API boundary). */\nexport interface AccountConfig {\n /** Display name from the table. */\n name: string;\n /** Id-safe object id derived from the name. */\n id: string;\n /** Provider kind. */\n provider: string;\n /** Central credential id (system.credentials.*); empty for claude-sub. */\n credentialId: string;\n /** Warn threshold in percent. */\n warnThreshold: number;\n}\n\n/**\n * Make a string safe as an ioBroker object-id segment.\n *\n * @param name the raw name\n * @returns the sanitized id segment\n */\nexport function sanitizeId(name: string): string {\n return name\n .trim()\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_{2,}/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** The provider kinds the adapter knows. */\nexport const PROVIDER_KINDS = [\n \"claude-sub\",\n \"chatgpt-sub\",\n \"gemini-sub\",\n \"openrouter\",\n \"deepseek\",\n \"openai\",\n \"anthropic-api\",\n];\n\n/** The subscription kinds: signed in with the user's account, no key involved. */\nexport const SUBSCRIPTION_KINDS = [\"claude-sub\", \"chatgpt-sub\", \"gemini-sub\"];\n\n/** Fixed object id per subscription \u2014 adapter-owned, never derived from a display name. */\nexport const SUBSCRIPTION_IDS: Record<string, string> = {\n \"claude-sub\": \"claude\",\n \"chatgpt-sub\": \"chatgpt\",\n \"gemini-sub\": \"gemini\",\n};\n\n/** Top-level object roots the adapter owns \u2014 no account may use them as id. */\nexport const RESERVED_ROOT_IDS = [\"info\", \"total\"];\n\n/**\n * The object id of one account. Deterministic and stable: a subscription always owns\n * its provider id, a key-based account always carries the \"-api\" suffix. Neither\n * depends on a display name, so an id never moves when the user renames something\n * or adds an unrelated credential later.\n *\n * @param provider the provider kind\n * @param credentialId the central credential id (key-based accounts)\n * @returns the id-safe object id, or \"\" when it cannot be formed\n */\nexport function accountId(provider: string, credentialId: string): string {\n const fixed = SUBSCRIPTION_IDS[provider];\n if (fixed) {\n return fixed;\n }\n const suffix = sanitizeId(credentialId.replace(/^system\\.credentials\\./, \"\"));\n return suffix ? `${suffix}-api` : \"\";\n}\n\n/**\n * Parse and validate the admin accounts table. Rows without a usable name or with an\n * unknown provider are skipped (type-guarded \u2014 the table is external input); disabled\n * rows are skipped too. Duplicate ids keep the first row.\n *\n * @param raw the native.accounts value\n * @returns the validated, enabled accounts\n */\nexport function parseAccounts(raw: unknown): AccountConfig[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const accounts: AccountConfig[] = [];\n const seen = new Set<string>();\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n if (row.enabled === false) {\n continue;\n }\n const provider = typeof row.provider === \"string\" ? row.provider : \"\";\n const credentialId = typeof row.credentialId === \"string\" ? row.credentialId : \"\";\n const id = accountId(provider, credentialId);\n const name = (typeof row.name === \"string\" ? row.name.trim() : \"\") || id;\n if (!id || RESERVED_ROOT_IDS.includes(id) || !PROVIDER_KINDS.includes(provider) || seen.has(id)) {\n continue;\n }\n seen.add(id);\n const threshold = Number(row.warnThreshold);\n accounts.push({\n name,\n id,\n provider,\n credentialId,\n warnThreshold: Number.isFinite(threshold) && threshold >= 10 && threshold <= 100 ? threshold : 80,\n });\n }\n return accounts;\n}\n\n/**\n * The ids of ALL valid table rows \u2014 including disabled ones. Used by the stale-object\n * cleanup: a disabled account is paused, not deleted; only rows removed from the\n * table lose their tree.\n *\n * @param raw the native.accounts value\n * @returns the id-safe ids of every valid row\n */\nexport function validAccountIds(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const ids: string[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n const id = accountId(\n typeof row.provider === \"string\" ? row.provider : \"\",\n typeof row.credentialId === \"string\" ? row.credentialId : \"\",\n );\n if (id && !RESERVED_ROOT_IDS.includes(id) && !ids.includes(id)) {\n ids.push(id);\n }\n }\n return ids;\n}\n\n/**\n * Clamp the poll interval to the safe range: minimum 60 s (provider throttling locks\n * whole accounts), default 300 s for anything unusable.\n *\n * @param raw the configured value\n * @returns the effective interval in seconds\n */\nexport function clampPollInterval(raw: unknown): number {\n const value = Number(raw);\n if (!Number.isFinite(value) || value <= 0) {\n return 300;\n }\n return Math.min(3600, Math.max(60, Math.round(value)));\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KACJ,KAAK,EACL,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,UAAU,GAAG,EACrB,QAAQ,YAAY,EAAE;AAC3B;AAGO,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,qBAAqB,CAAC,cAAc,eAAe,YAAY;AAGrE,MAAM,mBAA2C;AAAA,EACtD,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAChB;AAGO,MAAM,oBAAoB,CAAC,QAAQ,OAAO;AAY1C,SAAS,UAAU,UAAkB,cAA8B;AACxE,QAAM,QAAQ,iBAAiB,QAAQ;AACvC,MAAI,OAAO;AACT,WAAO;AAAA,EACT;AACA,QAAM,SAAS,WAAW,aAAa,QAAQ,0BAA0B,EAAE,CAAC;AAC5E,SAAO,SAAS,GAAG,MAAM,SAAS;AACpC;AAUO,SAAS,cAAc,KAA+B;AAC3D,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,WAA4B,CAAC;AACnC,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,KAAK;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,IACF;AACA,UAAM,MAAM;AACZ,QAAI,IAAI,YAAY,OAAO;AACzB;AAAA,IACF;AACA,UAAM,WAAW,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;AACnE,UAAM,eAAe,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAC/E,UAAM,KAAK,UAAU,UAAU,YAAY;AAC3C,UAAM,QAAQ,OAAO,IAAI,SAAS,WAAW,IAAI,KAAK,KAAK,IAAI,OAAO;AACtE,QAAI,CAAC,MAAM,kBAAkB,SAAS,EAAE,KAAK,CAAC,eAAe,SAAS,QAAQ,KAAK,KAAK,IAAI,EAAE,GAAG;AAC/F;AAAA,IACF;AACA,SAAK,IAAI,EAAE;AACX,UAAM,YAAY,OAAO,IAAI,aAAa;AAC1C,aAAS,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe,OAAO,SAAS,SAAS,KAAK,aAAa,MAAM,aAAa,MAAM,YAAY;AAAA,IACjG,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAUO,SAAS,gBAAgB,KAAwB;AACtD,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,KAAK;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,IACF;AACA,UAAM,MAAM;AACZ,UAAM,KAAK;AAAA,MACT,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;AAAA,MAClD,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAAA,IAC5D;AACA,QAAI,MAAM,CAAC,kBAAkB,SAAS,EAAE,KAAK,CAAC,IAAI,SAAS,EAAE,GAAG;AAC9D,UAAI,KAAK,EAAE;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,KAAsB;AACtD,QAAM,QAAQ,OAAO,GAAG;AACxB,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG;AACzC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC;AACvD;",
|
|
4
|
+
"sourcesContent": ["/** One configured AI account, validated from the admin table (API boundary). */\nexport interface AccountConfig {\n /** Display name from the table. */\n name: string;\n /** Id-safe object id derived from the name. */\n id: string;\n /** Provider kind. */\n provider: string;\n /** Central credential id (system.credentials.*); empty for claude-sub. */\n credentialId: string;\n /** Warn threshold in percent. */\n warnThreshold: number;\n}\n\n/**\n * Make a string safe as an ioBroker object-id segment.\n *\n * @param name the raw name\n * @returns the sanitized id segment\n */\nexport function sanitizeId(name: string): string {\n return name\n .trim()\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_{2,}/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** The provider kinds the adapter knows. */\nexport const PROVIDER_KINDS = [\n \"claude-sub\",\n \"chatgpt-sub\",\n \"gemini-sub\",\n \"openrouter\",\n \"deepseek\",\n \"openai\",\n \"anthropic-api\",\n];\n\n/** The subscription kinds: signed in with the user's account, no key involved. */\nexport const SUBSCRIPTION_KINDS = [\"claude-sub\", \"chatgpt-sub\", \"gemini-sub\"];\n\n/** Fixed object id per subscription \u2014 adapter-owned, never derived from a display name. */\nexport const SUBSCRIPTION_IDS: Record<string, string> = {\n \"claude-sub\": \"claude\",\n \"chatgpt-sub\": \"chatgpt\",\n \"gemini-sub\": \"gemini\",\n};\n\n/** Top-level object roots the adapter owns \u2014 no account may use them as id. */\nexport const RESERVED_ROOT_IDS = [\"info\", \"total\"];\n\n/**\n * The object id of one account. Deterministic and stable: a subscription always owns\n * its provider id, a key-based account always carries the \"-api\" suffix. Neither\n * depends on a display name, so an id never moves when the user renames something\n * or adds an unrelated credential later.\n *\n * @param provider the provider kind\n * @param credentialId the central credential id (key-based accounts)\n * @returns the id-safe object id, or \"\" when it cannot be formed\n */\nexport function accountId(provider: string, credentialId: string): string {\n const fixed = SUBSCRIPTION_IDS[provider];\n if (fixed) {\n return fixed;\n }\n const suffix = sanitizeId(credentialId.replace(/^system\\.credentials\\./, \"\"));\n return suffix ? `${suffix}-api` : \"\";\n}\n\n/**\n * Parse and validate the admin accounts table. Rows without a usable name or with an\n * unknown provider are skipped (type-guarded \u2014 the table is external input); disabled\n * rows are skipped too. Duplicate ids keep the first row.\n *\n * @param raw the native.accounts value\n * @returns the validated, enabled accounts\n */\nexport function parseAccounts(raw: unknown): AccountConfig[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const accounts: AccountConfig[] = [];\n const seen = new Set<string>();\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n if (row.enabled === false) {\n continue;\n }\n const provider = typeof row.provider === \"string\" ? row.provider : \"\";\n const credentialId = typeof row.credentialId === \"string\" ? row.credentialId : \"\";\n const id = accountId(provider, credentialId);\n const name = (typeof row.name === \"string\" ? row.name.trim() : \"\") || id;\n if (!id || RESERVED_ROOT_IDS.includes(id) || !PROVIDER_KINDS.includes(provider) || seen.has(id)) {\n continue;\n }\n seen.add(id);\n const threshold = Number(row.warnThreshold);\n accounts.push({\n name,\n id,\n provider,\n credentialId,\n warnThreshold: Number.isFinite(threshold) && threshold >= 10 && threshold <= 100 ? threshold : 80,\n });\n }\n return accounts;\n}\n\n/**\n * The ids of ALL valid table rows \u2014 including disabled ones. Used by the stale-object\n * cleanup: a disabled account is paused, not deleted; only rows removed from the\n * table lose their tree.\n *\n * @param raw the native.accounts value\n * @returns the id-safe ids of every valid row\n */\nexport function validAccountIds(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const ids: string[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n const id = accountId(\n typeof row.provider === \"string\" ? row.provider : \"\",\n typeof row.credentialId === \"string\" ? row.credentialId : \"\",\n );\n if (id && !RESERVED_ROOT_IDS.includes(id) && !ids.includes(id)) {\n ids.push(id);\n }\n }\n return ids;\n}\n\n/**\n * Clamp the poll interval to the safe range: minimum 60 s (provider throttling locks\n * whole accounts), default 300 s for anything unusable.\n *\n * @param raw the configured value\n * @returns the effective interval in seconds\n */\nexport function clampPollInterval(raw: unknown): number {\n const value = Number(raw);\n if (!Number.isFinite(value) || value <= 0) {\n return 300;\n }\n return Math.min(3600, Math.max(60, Math.round(value)));\n}\n\n/**\n * The one-line balance of what the object tree gained and lost, or null when\n * nothing changed.\n *\n * Silence at 0/0 is the point: a normal restart must not write anything, otherwise\n * the line becomes noise and stops being read (fleet standard, beszel).\n *\n * @param created datapoints added since the snapshot\n * @param removed datapoints deleted since the snapshot\n * @returns the log line, or null when there is nothing to report\n */\nexport function datapointBalanceLine(created: number, removed: number): string | null {\n const parts: string[] = [];\n if (created > 0) {\n parts.push(`created ${created} datapoint(s)`);\n }\n if (removed > 0) {\n parts.push(`removed ${removed} datapoint(s)`);\n }\n return parts.length ? `Object tree updated: ${parts.join(\", \")}` : null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/sign-in.js
CHANGED
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var sign_in_exports = {};
|
|
20
20
|
__export(sign_in_exports, {
|
|
21
21
|
SIGN_IN_FLOWS: () => SIGN_IN_FLOWS,
|
|
22
|
+
SIGN_IN_LABELS: () => SIGN_IN_LABELS,
|
|
22
23
|
attemptExpired: () => attemptExpired
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(sign_in_exports);
|
|
@@ -27,12 +28,18 @@ const SIGN_IN_FLOWS = {
|
|
|
27
28
|
"chatgpt-sub": "device-code",
|
|
28
29
|
"gemini-sub": "paste-url"
|
|
29
30
|
};
|
|
31
|
+
const SIGN_IN_LABELS = {
|
|
32
|
+
"claude-sub": "Claude",
|
|
33
|
+
"chatgpt-sub": "ChatGPT",
|
|
34
|
+
"gemini-sub": "Gemini"
|
|
35
|
+
};
|
|
30
36
|
function attemptExpired(expiresAt, now) {
|
|
31
37
|
return now >= expiresAt;
|
|
32
38
|
}
|
|
33
39
|
// Annotate the CommonJS export names for ESM import in node:
|
|
34
40
|
0 && (module.exports = {
|
|
35
41
|
SIGN_IN_FLOWS,
|
|
42
|
+
SIGN_IN_LABELS,
|
|
36
43
|
attemptExpired
|
|
37
44
|
});
|
|
38
45
|
//# sourceMappingURL=sign-in.js.map
|
package/build/lib/sign-in.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/sign-in.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SignInFlow } from \"./provider\";\n\n/** Which sign-in flow a subscription uses \u2014 see {@link SignInFlow} for why they differ. */\nexport const SIGN_IN_FLOWS: Record<string, SignInFlow> = {\n \"claude-sub\": \"paste-code\",\n \"chatgpt-sub\": \"device-code\",\n \"gemini-sub\": \"paste-url\",\n};\n\n/** The live state of one subscription row, as the admin panel renders it. */\nexport type SignInState =\n /** Signed in, tokens usable. */\n | { status: \"signed-in\" }\n /** Nothing started yet. */\n | { status: \"signed-out\" }\n /** Claude/Gemini: the user has to open the link and paste something back. */\n | { status: \"awaiting-paste\"; url: string; flow: SignInFlow }\n /** ChatGPT: the user types this code on the provider's page; the adapter waits. */\n | { status: \"awaiting-device\"; userCode: string; verificationUrl: string; expiresAt: number }\n /** The last attempt failed \u2014 reason is user-facing. */\n | { status: \"failed\"; reason: string };\n\n/**\n * Whether a running sign-in attempt is over.\n *\n * @param expiresAt end of the attempt window (ms since epoch)\n * @param now current time (ms)\n * @returns true when the attempt can no longer succeed\n */\nexport function attemptExpired(expiresAt: number, now: number): boolean {\n return now >= expiresAt;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,gBAA4C;AAAA,EACvD,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAChB;
|
|
4
|
+
"sourcesContent": ["import type { SignInFlow } from \"./provider\";\n\n/** Which sign-in flow a subscription uses \u2014 see {@link SignInFlow} for why they differ. */\nexport const SIGN_IN_FLOWS: Record<string, SignInFlow> = {\n \"claude-sub\": \"paste-code\",\n \"chatgpt-sub\": \"device-code\",\n \"gemini-sub\": \"paste-url\",\n};\n\n/** The live state of one subscription row, as the admin panel renders it. */\nexport type SignInState =\n /** Signed in, tokens usable. */\n | { status: \"signed-in\" }\n /** Nothing started yet. */\n | { status: \"signed-out\" }\n /** Claude/Gemini: the user has to open the link and paste something back. */\n | { status: \"awaiting-paste\"; url: string; flow: SignInFlow }\n /** ChatGPT: the user types this code on the provider's page; the adapter waits. */\n | { status: \"awaiting-device\"; userCode: string; verificationUrl: string; expiresAt: number }\n /** The last attempt failed \u2014 reason is user-facing. */\n | { status: \"failed\"; reason: string };\n\n/** Readable name per subscription \u2014 log lines must not show the internal kind. */\nexport const SIGN_IN_LABELS: Record<string, string> = {\n \"claude-sub\": \"Claude\",\n \"chatgpt-sub\": \"ChatGPT\",\n \"gemini-sub\": \"Gemini\",\n};\n\n/**\n * Whether a running sign-in attempt is over.\n *\n * @param expiresAt end of the attempt window (ms since epoch)\n * @param now current time (ms)\n * @returns true when the attempt can no longer succeed\n */\nexport function attemptExpired(expiresAt: number, now: number): boolean {\n return now >= expiresAt;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,gBAA4C;AAAA,EACvD,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAChB;AAgBO,MAAM,iBAAyC;AAAA,EACpD,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAChB;AASO,SAAS,eAAe,WAAmB,KAAsB;AACtE,SAAO,OAAO;AAChB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -57,6 +57,54 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
57
57
|
signInErrors = /* @__PURE__ */ new Map();
|
|
58
58
|
/** Device-code pollers, so they can be stopped on unload. */
|
|
59
59
|
devicePollers = /* @__PURE__ */ new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Every state id that already existed when this process started.
|
|
62
|
+
*
|
|
63
|
+
* The create path runs `extendObject` for every state once per process — also for
|
|
64
|
+
* states that were already in the database — so "the create path touched it" would
|
|
65
|
+
* report every datapoint as new after each restart. Only what is missing from this
|
|
66
|
+
* snapshot is a real addition (beszel pattern).
|
|
67
|
+
*/
|
|
68
|
+
knownStateIds = /* @__PURE__ */ new Set();
|
|
69
|
+
/** Datapoints created since the snapshot. */
|
|
70
|
+
createdStates = 0;
|
|
71
|
+
/** Datapoints removed since the snapshot — excluding the one-shot migration, which reports itself. */
|
|
72
|
+
removedStates = 0;
|
|
73
|
+
/** Whether the startup balance was already logged. */
|
|
74
|
+
balanceLogged = false;
|
|
75
|
+
/**
|
|
76
|
+
* Read every existing state id once, before anything creates or deletes.
|
|
77
|
+
*
|
|
78
|
+
* @returns nothing; fills {@link knownStateIds}
|
|
79
|
+
*/
|
|
80
|
+
async snapshotExistingStates() {
|
|
81
|
+
var _a;
|
|
82
|
+
try {
|
|
83
|
+
const view = await this.getObjectViewAsync("system", "state", {
|
|
84
|
+
startkey: `${this.namespace}.`,
|
|
85
|
+
endkey: `${this.namespace}.\uFFFF`
|
|
86
|
+
});
|
|
87
|
+
for (const row of (_a = view == null ? void 0 : view.rows) != null ? _a : []) {
|
|
88
|
+
this.knownStateIds.add(row.id.substring(this.namespace.length + 1));
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
this.log.debug(`Could not snapshot existing states: ${e instanceof Error ? e.message : String(e)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Report what the object tree gained and lost in this startup — one line, both
|
|
96
|
+
* sides, silent when nothing changed. A normal restart must stay quiet.
|
|
97
|
+
*/
|
|
98
|
+
logDatapointBalance() {
|
|
99
|
+
if (this.balanceLogged) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.balanceLogged = true;
|
|
103
|
+
const line = (0, import_pure_helpers.datapointBalanceLine)(this.createdStates, this.removedStates);
|
|
104
|
+
if (line) {
|
|
105
|
+
this.log.info(line);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
60
108
|
/**
|
|
61
109
|
* @param options the adapter options
|
|
62
110
|
*/
|
|
@@ -169,6 +217,10 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
169
217
|
if (!attempt || attempt.flow === "device-code") {
|
|
170
218
|
return { status: "failed", reason: "start the sign-in first" };
|
|
171
219
|
}
|
|
220
|
+
if ((0, import_sign_in.attemptExpired)(attempt.expiresAt, Date.now())) {
|
|
221
|
+
this.attempts.delete(provider);
|
|
222
|
+
return { status: "failed", reason: "the sign-in window expired \u2014 start the sign-in again" };
|
|
223
|
+
}
|
|
172
224
|
if (!value) {
|
|
173
225
|
return { status: "failed", reason: "nothing pasted" };
|
|
174
226
|
}
|
|
@@ -191,7 +243,7 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
191
243
|
* @param tokens the token set
|
|
192
244
|
*/
|
|
193
245
|
async finishSignIn(provider, tokens) {
|
|
194
|
-
var _a;
|
|
246
|
+
var _a, _b;
|
|
195
247
|
await this.tokenStore(provider).save(tokens);
|
|
196
248
|
this.attempts.delete(provider);
|
|
197
249
|
this.signInErrors.delete(provider);
|
|
@@ -200,7 +252,7 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
200
252
|
if (id) {
|
|
201
253
|
await ((_a = this.engine) == null ? void 0 : _a.pollNow(id));
|
|
202
254
|
}
|
|
203
|
-
this.log.info(`${provider}: signed in`);
|
|
255
|
+
this.log.info(`${(_b = import_sign_in.SIGN_IN_LABELS[provider]) != null ? _b : provider}: signed in`);
|
|
204
256
|
}
|
|
205
257
|
/**
|
|
206
258
|
* Poll the device-code endpoint until the user confirmed, the window closed or
|
|
@@ -266,6 +318,10 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
266
318
|
};
|
|
267
319
|
}
|
|
268
320
|
if (attempt) {
|
|
321
|
+
if ((0, import_sign_in.attemptExpired)(attempt.expiresAt, Date.now())) {
|
|
322
|
+
this.attempts.delete(provider);
|
|
323
|
+
return { status: "failed", reason: "the sign-in window expired \u2014 start the sign-in again" };
|
|
324
|
+
}
|
|
269
325
|
return { status: "awaiting-paste", url: attempt.url, flow: attempt.flow };
|
|
270
326
|
}
|
|
271
327
|
if (failure) {
|
|
@@ -354,7 +410,12 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
354
410
|
const accounts = (0, import_pure_helpers.parseAccounts)(this.config.accounts);
|
|
355
411
|
const interval = (0, import_pure_helpers.clampPollInterval)(this.config.pollInterval);
|
|
356
412
|
await this.migrateTokenFiles();
|
|
413
|
+
await this.snapshotExistingStates();
|
|
357
414
|
await this.cleanupStaleObjects();
|
|
415
|
+
const retired = await this.removeRetiredStates(accounts);
|
|
416
|
+
if (retired > 0) {
|
|
417
|
+
this.log.info(`Object tree updated: removed ${retired} obsolete status datapoint(s)`);
|
|
418
|
+
}
|
|
358
419
|
if (accounts.length === 0) {
|
|
359
420
|
this.log.info("No AI accounts configured \u2014 add accounts in the instance settings");
|
|
360
421
|
await this.setState("info.connection", { val: false, ack: true });
|
|
@@ -370,11 +431,19 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
370
431
|
this.engine = new import_poll_engine.PollEngine(accounts, providers, interval, {
|
|
371
432
|
upsertObject: async (def) => {
|
|
372
433
|
await this.extendObject(def.id, { type: def.type, common: def.common, native: {} });
|
|
434
|
+
if (def.type === "state" && !this.knownStateIds.has(def.id)) {
|
|
435
|
+
this.knownStateIds.add(def.id);
|
|
436
|
+
this.createdStates++;
|
|
437
|
+
}
|
|
373
438
|
},
|
|
374
439
|
setState: (id, value) => {
|
|
375
440
|
void this.setState(id, { val: value, ack: true }).catch(() => {
|
|
376
441
|
});
|
|
377
442
|
},
|
|
443
|
+
setStateChanged: (id, value) => {
|
|
444
|
+
void this.setStateChangedAsync(id, { val: value, ack: true }).catch(() => {
|
|
445
|
+
});
|
|
446
|
+
},
|
|
378
447
|
schedule: (cb, ms) => ({ kind: "interval", handle: this.setInterval(cb, ms) }),
|
|
379
448
|
scheduleOnce: (cb, ms) => ({ kind: "timeout", handle: this.setTimeout(cb, ms) }),
|
|
380
449
|
cancel: (handle) => {
|
|
@@ -392,15 +461,12 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
392
461
|
warn: (m) => this.log.warn(m),
|
|
393
462
|
error: (m) => this.log.error(m)
|
|
394
463
|
},
|
|
464
|
+
afterFirstRound: () => this.logDatapointBalance(),
|
|
395
465
|
notify: this.config.notifications ? (_account, message) => void this.registerNotification("ai-usage", "userActionRequired", message).catch(
|
|
396
466
|
(e) => this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`)
|
|
397
467
|
) : void 0
|
|
398
468
|
});
|
|
399
469
|
await this.engine.start();
|
|
400
|
-
const removed = await this.removeRetiredStates(accounts);
|
|
401
|
-
if (removed > 0) {
|
|
402
|
-
this.log.info(`Object tree updated: removed ${removed} obsolete status datapoint(s)`);
|
|
403
|
-
}
|
|
404
470
|
this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);
|
|
405
471
|
} catch (e) {
|
|
406
472
|
this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -497,6 +563,7 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
497
563
|
if (await this.getObjectAsync(id)) {
|
|
498
564
|
await this.delObjectAsync(id);
|
|
499
565
|
removed++;
|
|
566
|
+
this.knownStateIds.delete(id);
|
|
500
567
|
}
|
|
501
568
|
} catch (e) {
|
|
502
569
|
this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -529,6 +596,12 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
529
596
|
this.log.info(
|
|
530
597
|
root === "auth" ? "Removing the old sign-in branch \u2014 the sign-in state now lives inside each account" : `Removing objects of no longer configured account "${root}"`
|
|
531
598
|
);
|
|
599
|
+
for (const id of [...this.knownStateIds]) {
|
|
600
|
+
if (id === root || id.startsWith(`${root}.`)) {
|
|
601
|
+
this.knownStateIds.delete(id);
|
|
602
|
+
this.removedStates++;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
532
605
|
await this.delObjectAsync(root, { recursive: true });
|
|
533
606
|
}
|
|
534
607
|
} catch (e) {
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { Credentials } from \"@iobroker/adapter-core\";\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { postForm, postJson } from \"./lib/http\";\nimport { PollEngine } from \"./lib/poll-engine\";\nimport {\n clampPollInterval,\n parseAccounts,\n SUBSCRIPTION_IDS,\n validAccountIds,\n type AccountConfig,\n} from \"./lib/pure-helpers\";\nimport type { TokenSet, TokenStore, UsageProvider } from \"./lib/provider\";\nimport { SIGN_IN_FLOWS, attemptExpired, type SignInState } from \"./lib/sign-in\";\nimport { buildAuthorizeUrl, exchangeCode, generatePkce, type PkcePair } from \"./lib/providers/claude-auth\";\nimport { claudeSubProvider } from \"./lib/providers/claude-sub\";\nimport {\n exchangeDeviceCode,\n pollDeviceCode,\n startDeviceCode,\n type DeviceCodeStart,\n} from \"./lib/providers/chatgpt-auth\";\nimport { chatgptSubProvider } from \"./lib/providers/chatgpt-sub\";\nimport {\n buildGeminiAuthorizeUrl,\n exchangeGeminiCode,\n extractGeminiCode,\n generateGeminiPkce,\n type GeminiPkce,\n} from \"./lib/providers/gemini-auth\";\nimport { geminiSubProvider } from \"./lib/providers/gemini-sub\";\nimport { anthropicApiProvider } from \"./lib/providers/anthropic-api\";\nimport { deepSeekProvider } from \"./lib/providers/deepseek\";\nimport { openAiProvider } from \"./lib/providers/openai\";\nimport { openRouterProvider } from \"./lib/providers/openrouter\";\n\n/** A cancellable handle: interval or timeout \u2014 the engine treats them uniformly. */\ntype TimerHandle =\n | { kind: \"interval\"; handle: ioBroker.Interval | undefined }\n | { kind: \"timeout\"; handle: ioBroker.Timeout | undefined };\n\n/** One running sign-in attempt (secrets live in memory only, never on disk). */\ntype Attempt =\n | { flow: \"paste-code\"; pkce: PkcePair; url: string; expiresAt: number }\n | { flow: \"paste-url\"; pkce: GeminiPkce; url: string; expiresAt: number }\n | { flow: \"device-code\"; start: DeviceCodeStart };\n\n/**\n * AI Usage adapter \u2014 reads usage windows, credits and costs of AI accounts into\n * read-only states. Three subscriptions sign in with the user's own account\n * (Claude, ChatGPT, Google), the other accounts use a key from the admin's central\n * credential storage. Orchestration lives in the unit-tested {@link PollEngine};\n * this class wires ioBroker IO, the sign-in flows and the token files to it.\n */\nexport class AiUsageAdapter extends utils.Adapter {\n private engine: PollEngine | null = null;\n /** Running sign-in attempts, keyed by provider kind. */\n private readonly attempts = new Map<string, Attempt>();\n /** Last failure reason per provider, shown in the admin row. */\n private readonly signInErrors = new Map<string, string>();\n /** Device-code pollers, so they can be stopped on unload. */\n private readonly devicePollers = new Map<string, TimerHandle>();\n\n /**\n * @param options the adapter options\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"ai-usage\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n }\n\n // ---------------------------------------------------------------- sign-in\n\n /**\n * Handle admin messages: the three sign-in flows plus their status.\n *\n * @param obj the message\n */\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n const provider = this.providerFrom(obj.message);\n switch (obj.command) {\n case \"signInStart\":\n this.respond(obj, provider ? await this.startSignIn(provider) : { error: \"unknown provider\" });\n return;\n case \"signInSubmit\":\n this.respond(obj, provider ? await this.submitSignIn(provider, obj.message) : { error: \"unknown provider\" });\n return;\n case \"signInStatus\":\n this.respond(obj, provider ? await this.signInState(provider) : { error: \"unknown provider\" });\n return;\n case \"signOut\":\n this.respond(obj, provider ? await this.signOut(provider) : { error: \"unknown provider\" });\n return;\n default:\n // Always answer, or the caller's callback dangles until timeout.\n this.respond(obj, { error: `Unknown command: ${obj.command}` });\n }\n } catch (e) {\n this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);\n this.respond(obj, { error: \"internal error \u2014 see log\" });\n }\n }\n\n /**\n * Send a message response, when the caller expects one.\n *\n * @param obj the request message\n * @param response the response payload\n */\n private respond(obj: ioBroker.Message, response: unknown): void {\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, response, obj.callback);\n }\n }\n\n /**\n * The provider kind named in a message, if it is a subscription we know.\n *\n * @param message the message payload ({ provider })\n * @returns the provider kind, or undefined\n */\n private providerFrom(message: unknown): string | undefined {\n const value =\n typeof (message as { provider?: unknown })?.provider === \"string\"\n ? (message as { provider: string }).provider\n : \"\";\n return SIGN_IN_FLOWS[value] ? value : undefined;\n }\n\n /**\n * Begin a sign-in: build the link (Claude/Google) or fetch a device code (ChatGPT).\n *\n * @param provider the subscription kind\n * @returns what the admin panel has to show\n */\n private async startSignIn(provider: string): Promise<SignInState | { error: string }> {\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const flow = SIGN_IN_FLOWS[provider];\n const now = Date.now();\n try {\n if (flow === \"paste-code\") {\n const pkce = generatePkce();\n const url = buildAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n if (flow === \"paste-url\") {\n const pkce = generateGeminiPkce();\n const url = buildGeminiAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n const start = await startDeviceCode(postJson, now);\n this.attempts.set(provider, { flow: \"device-code\", start });\n this.armDevicePoller(provider, start);\n return {\n status: \"awaiting-device\",\n userCode: start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: start.expiresAt,\n };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Finish a paste-based sign-in (Claude code, Google address).\n *\n * @param provider the subscription kind\n * @param message the message payload ({ value })\n * @returns the resulting state\n */\n private async submitSignIn(provider: string, message: unknown): Promise<SignInState> {\n const attempt = this.attempts.get(provider);\n const value =\n typeof (message as { value?: unknown })?.value === \"string\" ? (message as { value: string }).value.trim() : \"\";\n if (!attempt || attempt.flow === \"device-code\") {\n return { status: \"failed\", reason: \"start the sign-in first\" };\n }\n if (!value) {\n return { status: \"failed\", reason: \"nothing pasted\" };\n }\n try {\n const now = Date.now();\n const tokens =\n attempt.flow === \"paste-code\"\n ? await exchangeCode(value, attempt.pkce, postJson, now)\n : await exchangeGeminiCode(extractGeminiCode(value, attempt.pkce.state), attempt.pkce, postForm, now);\n await this.finishSignIn(provider, tokens);\n return { status: \"signed-in\" };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Store fresh tokens, mark the account signed in and poll it right away \u2014 waiting\n * up to a full interval after a successful sign-in reads as \"it did not work\".\n *\n * @param provider the subscription kind\n * @param tokens the token set\n */\n private async finishSignIn(provider: string, tokens: TokenSet): Promise<void> {\n await this.tokenStore(provider).save(tokens);\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const id = SUBSCRIPTION_IDS[provider];\n if (id) {\n await this.engine?.pollNow(id);\n }\n this.log.info(`${provider}: signed in`);\n }\n\n /**\n * Poll the device-code endpoint until the user confirmed, the window closed or\n * the adapter stops. The handle lives in memory only \u2014 a restart mid-flow just\n * means the user starts again, which is cheaper than persisting a 15-minute secret.\n *\n * @param provider the subscription kind\n * @param start the device-code handle\n */\n private armDevicePoller(provider: string, start: DeviceCodeStart): void {\n const tick = async (): Promise<void> => {\n try {\n if (attemptExpired(start.expiresAt, Date.now())) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, \"the code expired \u2014 start the sign-in again\");\n return;\n }\n const result = await pollDeviceCode(start, postJson);\n if (result.status === \"ready\") {\n this.stopDevicePoller(provider);\n const tokens = await exchangeDeviceCode(result.code, result.codeVerifier, postForm, Date.now());\n await this.finishSignIn(provider, tokens);\n }\n } catch (e) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, e instanceof Error ? e.message : String(e));\n }\n };\n this.devicePollers.set(provider, {\n kind: \"interval\",\n handle: this.setInterval(() => void tick(), Math.max(start.intervalSec, 1) * 1000),\n });\n }\n\n /**\n * Stop a running device-code poller.\n *\n * @param provider the subscription kind\n */\n private stopDevicePoller(provider: string): void {\n const handle = this.devicePollers.get(provider);\n if (handle?.kind === \"interval\") {\n this.clearInterval(handle.handle);\n }\n this.devicePollers.delete(provider);\n }\n\n /**\n * The current sign-in state of one subscription, for the admin row.\n *\n * @param provider the subscription kind\n * @returns the state\n */\n private async signInState(provider: string): Promise<SignInState> {\n const failure = this.signInErrors.get(provider);\n const attempt = this.attempts.get(provider);\n if (attempt?.flow === \"device-code\") {\n return {\n status: \"awaiting-device\",\n userCode: attempt.start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: attempt.start.expiresAt,\n };\n }\n if (attempt) {\n return { status: \"awaiting-paste\", url: attempt.url, flow: attempt.flow };\n }\n if (failure) {\n return { status: \"failed\", reason: failure };\n }\n return (await this.tokenStore(provider).load()) ? { status: \"signed-in\" } : { status: \"signed-out\" };\n }\n\n /**\n * Forget the tokens of one subscription.\n *\n * @param provider the subscription kind\n * @returns the resulting state\n */\n private async signOut(provider: string): Promise<SignInState> {\n await this.tokenStore(provider).clear();\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n return { status: \"signed-out\" };\n }\n\n // ------------------------------------------------------------ token files\n\n /**\n * Where one subscription's tokens live: an encrypted file in the instance data\n * directory, named after the PROVIDER. Keying by provider (not by account name)\n * keeps a sign-in alive when an account is renamed \u2014 the previous scheme lost it.\n *\n * @param provider the subscription kind\n * @returns the store\n */\n private tokenStore(provider: string): TokenStore {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const file = join(dir, `tokens-${provider}.json`);\n return {\n load: async (): Promise<TokenSet | null> => {\n try {\n const parsed = JSON.parse(this.decrypt(await readFile(file, \"utf8\"))) as Partial<TokenSet>;\n if (typeof parsed.accessToken !== \"string\" || typeof parsed.refreshToken !== \"string\") {\n return null;\n }\n return {\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken,\n expiresAt: Number(parsed.expiresAt) || 0,\n accountRef: typeof parsed.accountRef === \"string\" ? parsed.accountRef : undefined,\n };\n } catch {\n return null; // never signed in (or unreadable) \u2014 the provider reports auth-required\n }\n },\n save: async (tokens: TokenSet): Promise<void> => {\n await mkdir(dir, { recursive: true });\n await writeFile(file, this.encrypt(JSON.stringify(tokens)), \"utf8\");\n },\n clear: async (): Promise<void> => {\n await unlink(file).catch(() => {\n /* already gone */\n });\n },\n };\n }\n\n /**\n * Carry a sign-in from the pre-0.3.0 layout over: tokens used to be stored per\n * ACCOUNT NAME (`claude-tokens-<name>.json`). Without this the rename to fixed\n * account ids would silently sign the user out.\n */\n private async migrateTokenFiles(): Promise<void> {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const target = join(dir, \"tokens-claude-sub.json\");\n try {\n await readFile(target, \"utf8\");\n return; // already migrated\n } catch {\n // not there yet \u2014 look for an old file\n }\n for (const legacy of [\"claude-tokens-Claude.json\", \"claude-tokens-claude.json\"]) {\n try {\n await rename(join(dir, legacy), target);\n this.log.info(\"Carried the existing Claude sign-in over to the new layout\");\n return;\n } catch {\n // try the next candidate\n }\n }\n }\n\n // ------------------------------------------------------------- life cycle\n\n /** Validate the configuration, clean up stale objects and start the engine. */\n private async onReady(): Promise<void> {\n try {\n const accounts = parseAccounts(this.config.accounts);\n const interval = clampPollInterval(this.config.pollInterval);\n await this.migrateTokenFiles();\n await this.cleanupStaleObjects();\n if (accounts.length === 0) {\n this.log.info(\"No AI accounts configured \u2014 add accounts in the instance settings\");\n await this.setState(\"info.connection\", { val: false, ack: true });\n return;\n }\n const providers = new Map<string, UsageProvider>();\n for (const account of accounts) {\n const provider = await this.makeProvider(account);\n if (provider) {\n providers.set(account.id, provider);\n }\n }\n this.engine = new PollEngine(accounts, providers, interval, {\n upsertObject: async def => {\n await this.extendObject(def.id, { type: def.type, common: def.common as ioBroker.ObjectCommon, native: {} });\n },\n setState: (id, value) => {\n void this.setState(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n schedule: (cb, ms): TimerHandle => ({ kind: \"interval\", handle: this.setInterval(cb, ms) }),\n scheduleOnce: (cb, ms): TimerHandle => ({ kind: \"timeout\", handle: this.setTimeout(cb, ms) }),\n cancel: handle => {\n const timer = handle as TimerHandle;\n if (timer.kind === \"interval\") {\n this.clearInterval(timer.handle);\n } else {\n this.clearTimeout(timer.handle);\n }\n },\n now: () => Date.now(),\n log: {\n debug: m => this.log.debug(m),\n info: m => this.log.info(m),\n warn: m => this.log.warn(m),\n error: m => this.log.error(m),\n },\n notify: this.config.notifications\n ? (_account, message) =>\n void this.registerNotification(\"ai-usage\", \"userActionRequired\", message).catch(e =>\n this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`),\n )\n : undefined,\n });\n await this.engine.start();\n const removed = await this.removeRetiredStates(accounts);\n if (removed > 0) {\n this.log.info(`Object tree updated: removed ${removed} obsolete status datapoint(s)`);\n }\n this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);\n } catch (e) {\n this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Build the provider for one account.\n *\n * @param account the validated account config\n * @returns the provider, or undefined to skip the account\n */\n private async makeProvider(account: AccountConfig): Promise<UsageProvider | undefined> {\n switch (account.provider) {\n case \"claude-sub\":\n return claudeSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"chatgpt-sub\":\n return chatgptSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"gemini-sub\":\n return geminiSubProvider(this.tokenStore(account.provider), postJson, postForm);\n case \"openrouter\": {\n const key = await this.resolveKey(account);\n return key ? openRouterProvider(key) : undefined;\n }\n case \"deepseek\": {\n const key = await this.resolveKey(account);\n return key ? deepSeekProvider(key) : undefined;\n }\n case \"openai\": {\n const key = await this.resolveKey(account);\n return key ? openAiProvider(key) : undefined;\n }\n case \"anthropic-api\": {\n const key = await this.resolveKey(account);\n return key ? anthropicApiProvider(key) : undefined;\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Read and decrypt a key-form credential from the central credential storage.\n *\n * @param account the account whose credential to resolve\n * @returns the key, or undefined (with a log line) when it cannot be read\n */\n private async resolveKey(account: AccountConfig): Promise<string | undefined> {\n if (!account.credentialId) {\n this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);\n return undefined;\n }\n try {\n const credential = await Credentials.getCredentials(this, account.credentialId);\n const values = credential.values as { key?: unknown };\n const key = typeof values.key === \"string\" && values.key ? values.key : undefined;\n if (!key) {\n this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);\n }\n return key;\n } catch (e) {\n this.log.warn(\n `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`,\n );\n return undefined;\n }\n }\n\n /**\n * Status states retired in 0.5.0 \u2014 `provider` (visible in the node name anyway),\n * `reachable`/`serviceOnline`/`state` (three spellings of one fact) and `signedIn`\n * (the settings page asks the adapter directly).\n *\n * ioBroker never garbage-collects an object whose id the adapter stopped writing:\n * it would sit there frozen on its last value and keep lying. So the old ids are\n * deleted from a fixed list on every start \u2014 no state reading, no heuristics.\n */\n private static readonly RETIRED_INFO_STATES = [\n \"info.provider\",\n \"info.reachable\",\n \"info.serviceOnline\",\n \"info.state\",\n \"info.signedIn\",\n ];\n\n /**\n * Remove the retired status states of every configured account.\n *\n * @param accounts the configured accounts\n * @returns how many objects were actually deleted\n */\n private async removeRetiredStates(accounts: readonly AccountConfig[]): Promise<number> {\n let removed = 0;\n for (const account of accounts) {\n for (const suffix of AiUsageAdapter.RETIRED_INFO_STATES) {\n const id = `${account.id}.${suffix}`;\n try {\n // Existence-checked so the log line can report a real number; these are\n // stable dead orphans, never touched concurrently.\n if (await this.getObjectAsync(id)) {\n await this.delObjectAsync(id);\n removed++;\n }\n } catch (e) {\n this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n }\n return removed;\n }\n\n /**\n * Delete object trees that no longer belong to a configured account, plus the\n * `auth` branch of the pre-0.3.0 layout. An EMPTY table deletes nothing \u2014 the\n * guard against wiping everything through an accidental clear.\n */\n private async cleanupStaleObjects(): Promise<void> {\n const keepIds = validAccountIds(this.config.accounts);\n if (keepIds.length === 0) {\n return;\n }\n const keep = new Set([...keepIds, \"info\", \"total\"]);\n try {\n const objects = await this.getAdapterObjectsAsync();\n const roots = new Set<string>();\n for (const id of Object.keys(objects)) {\n const root = id.substring(this.namespace.length + 1).split(\".\")[0];\n if (root && !keep.has(root)) {\n roots.add(root);\n }\n }\n for (const root of roots) {\n this.log.info(\n root === \"auth\"\n ? \"Removing the old sign-in branch \u2014 the sign-in state now lives inside each account\"\n : `Removing objects of no longer configured account \"${root}\"`,\n );\n await this.delObjectAsync(root, { recursive: true });\n }\n } catch (e) {\n this.log.warn(`Cleanup of stale objects failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Tear down synchronously \u2014 no async/await here, else the controller kills the\n * process before cleanup finishes.\n *\n * @param callback invoked when cleanup is done\n */\n private onUnload(callback: () => void): void {\n try {\n for (const provider of [...this.devicePollers.keys()]) {\n this.stopDevicePoller(provider);\n }\n this.engine?.stop();\n this.engine = null;\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // never block shutdown\n }\n callback();\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AiUsageAdapter(options);\n} else {\n (() => new AiUsageAdapter())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { Credentials } from \"@iobroker/adapter-core\";\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { postForm, postJson } from \"./lib/http\";\nimport { PollEngine } from \"./lib/poll-engine\";\nimport {\n clampPollInterval,\n datapointBalanceLine,\n parseAccounts,\n SUBSCRIPTION_IDS,\n validAccountIds,\n type AccountConfig,\n} from \"./lib/pure-helpers\";\nimport type { TokenSet, TokenStore, UsageProvider } from \"./lib/provider\";\nimport { SIGN_IN_FLOWS, SIGN_IN_LABELS, attemptExpired, type SignInState } from \"./lib/sign-in\";\nimport { buildAuthorizeUrl, exchangeCode, generatePkce, type PkcePair } from \"./lib/providers/claude-auth\";\nimport { claudeSubProvider } from \"./lib/providers/claude-sub\";\nimport {\n exchangeDeviceCode,\n pollDeviceCode,\n startDeviceCode,\n type DeviceCodeStart,\n} from \"./lib/providers/chatgpt-auth\";\nimport { chatgptSubProvider } from \"./lib/providers/chatgpt-sub\";\nimport {\n buildGeminiAuthorizeUrl,\n exchangeGeminiCode,\n extractGeminiCode,\n generateGeminiPkce,\n type GeminiPkce,\n} from \"./lib/providers/gemini-auth\";\nimport { geminiSubProvider } from \"./lib/providers/gemini-sub\";\nimport { anthropicApiProvider } from \"./lib/providers/anthropic-api\";\nimport { deepSeekProvider } from \"./lib/providers/deepseek\";\nimport { openAiProvider } from \"./lib/providers/openai\";\nimport { openRouterProvider } from \"./lib/providers/openrouter\";\n\n/** A cancellable handle: interval or timeout \u2014 the engine treats them uniformly. */\ntype TimerHandle =\n | { kind: \"interval\"; handle: ioBroker.Interval | undefined }\n | { kind: \"timeout\"; handle: ioBroker.Timeout | undefined };\n\n/** One running sign-in attempt (secrets live in memory only, never on disk). */\ntype Attempt =\n | { flow: \"paste-code\"; pkce: PkcePair; url: string; expiresAt: number }\n | { flow: \"paste-url\"; pkce: GeminiPkce; url: string; expiresAt: number }\n | { flow: \"device-code\"; start: DeviceCodeStart };\n\n/**\n * AI Usage adapter \u2014 reads usage windows, credits and costs of AI accounts into\n * read-only states. Three subscriptions sign in with the user's own account\n * (Claude, ChatGPT, Google), the other accounts use a key from the admin's central\n * credential storage. Orchestration lives in the unit-tested {@link PollEngine};\n * this class wires ioBroker IO, the sign-in flows and the token files to it.\n */\nexport class AiUsageAdapter extends utils.Adapter {\n private engine: PollEngine | null = null;\n /** Running sign-in attempts, keyed by provider kind. */\n private readonly attempts = new Map<string, Attempt>();\n /** Last failure reason per provider, shown in the admin row. */\n private readonly signInErrors = new Map<string, string>();\n /** Device-code pollers, so they can be stopped on unload. */\n private readonly devicePollers = new Map<string, TimerHandle>();\n\n /**\n * Every state id that already existed when this process started.\n *\n * The create path runs `extendObject` for every state once per process \u2014 also for\n * states that were already in the database \u2014 so \"the create path touched it\" would\n * report every datapoint as new after each restart. Only what is missing from this\n * snapshot is a real addition (beszel pattern).\n */\n private knownStateIds = new Set<string>();\n /** Datapoints created since the snapshot. */\n private createdStates = 0;\n /** Datapoints removed since the snapshot \u2014 excluding the one-shot migration, which reports itself. */\n private removedStates = 0;\n /** Whether the startup balance was already logged. */\n private balanceLogged = false;\n\n /**\n * Read every existing state id once, before anything creates or deletes.\n *\n * @returns nothing; fills {@link knownStateIds}\n */\n private async snapshotExistingStates(): Promise<void> {\n try {\n const view = await this.getObjectViewAsync(\"system\", \"state\", {\n startkey: `${this.namespace}.`,\n endkey: `${this.namespace}.\\uFFFF`,\n });\n for (const row of view?.rows ?? []) {\n this.knownStateIds.add(row.id.substring(this.namespace.length + 1));\n }\n } catch (e) {\n this.log.debug(`Could not snapshot existing states: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Report what the object tree gained and lost in this startup \u2014 one line, both\n * sides, silent when nothing changed. A normal restart must stay quiet.\n */\n private logDatapointBalance(): void {\n if (this.balanceLogged) {\n return;\n }\n this.balanceLogged = true;\n const line = datapointBalanceLine(this.createdStates, this.removedStates);\n if (line) {\n this.log.info(line);\n }\n }\n\n /**\n * @param options the adapter options\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"ai-usage\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n }\n\n // ---------------------------------------------------------------- sign-in\n\n /**\n * Handle admin messages: the three sign-in flows plus their status.\n *\n * @param obj the message\n */\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n const provider = this.providerFrom(obj.message);\n switch (obj.command) {\n case \"signInStart\":\n this.respond(obj, provider ? await this.startSignIn(provider) : { error: \"unknown provider\" });\n return;\n case \"signInSubmit\":\n this.respond(obj, provider ? await this.submitSignIn(provider, obj.message) : { error: \"unknown provider\" });\n return;\n case \"signInStatus\":\n this.respond(obj, provider ? await this.signInState(provider) : { error: \"unknown provider\" });\n return;\n case \"signOut\":\n this.respond(obj, provider ? await this.signOut(provider) : { error: \"unknown provider\" });\n return;\n default:\n // Always answer, or the caller's callback dangles until timeout.\n this.respond(obj, { error: `Unknown command: ${obj.command}` });\n }\n } catch (e) {\n this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);\n this.respond(obj, { error: \"internal error \u2014 see log\" });\n }\n }\n\n /**\n * Send a message response, when the caller expects one.\n *\n * @param obj the request message\n * @param response the response payload\n */\n private respond(obj: ioBroker.Message, response: unknown): void {\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, response, obj.callback);\n }\n }\n\n /**\n * The provider kind named in a message, if it is a subscription we know.\n *\n * @param message the message payload ({ provider })\n * @returns the provider kind, or undefined\n */\n private providerFrom(message: unknown): string | undefined {\n const value =\n typeof (message as { provider?: unknown })?.provider === \"string\"\n ? (message as { provider: string }).provider\n : \"\";\n return SIGN_IN_FLOWS[value] ? value : undefined;\n }\n\n /**\n * Begin a sign-in: build the link (Claude/Google) or fetch a device code (ChatGPT).\n *\n * @param provider the subscription kind\n * @returns what the admin panel has to show\n */\n private async startSignIn(provider: string): Promise<SignInState | { error: string }> {\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const flow = SIGN_IN_FLOWS[provider];\n const now = Date.now();\n try {\n if (flow === \"paste-code\") {\n const pkce = generatePkce();\n const url = buildAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n if (flow === \"paste-url\") {\n const pkce = generateGeminiPkce();\n const url = buildGeminiAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n const start = await startDeviceCode(postJson, now);\n this.attempts.set(provider, { flow: \"device-code\", start });\n this.armDevicePoller(provider, start);\n return {\n status: \"awaiting-device\",\n userCode: start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: start.expiresAt,\n };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Finish a paste-based sign-in (Claude code, Google address).\n *\n * @param provider the subscription kind\n * @param message the message payload ({ value })\n * @returns the resulting state\n */\n private async submitSignIn(provider: string, message: unknown): Promise<SignInState> {\n const attempt = this.attempts.get(provider);\n const value =\n typeof (message as { value?: unknown })?.value === \"string\" ? (message as { value: string }).value.trim() : \"\";\n if (!attempt || attempt.flow === \"device-code\") {\n return { status: \"failed\", reason: \"start the sign-in first\" };\n }\n // The 15-minute window was stored but never enforced \u2014 a stale attempt used to\n // fail with the provider's own cryptic answer instead of a clear instruction.\n if (attemptExpired(attempt.expiresAt, Date.now())) {\n this.attempts.delete(provider);\n return { status: \"failed\", reason: \"the sign-in window expired \u2014 start the sign-in again\" };\n }\n if (!value) {\n return { status: \"failed\", reason: \"nothing pasted\" };\n }\n try {\n const now = Date.now();\n const tokens =\n attempt.flow === \"paste-code\"\n ? await exchangeCode(value, attempt.pkce, postJson, now)\n : await exchangeGeminiCode(extractGeminiCode(value, attempt.pkce.state), attempt.pkce, postForm, now);\n await this.finishSignIn(provider, tokens);\n return { status: \"signed-in\" };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Store fresh tokens, mark the account signed in and poll it right away \u2014 waiting\n * up to a full interval after a successful sign-in reads as \"it did not work\".\n *\n * @param provider the subscription kind\n * @param tokens the token set\n */\n private async finishSignIn(provider: string, tokens: TokenSet): Promise<void> {\n await this.tokenStore(provider).save(tokens);\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const id = SUBSCRIPTION_IDS[provider];\n if (id) {\n await this.engine?.pollNow(id);\n }\n this.log.info(`${SIGN_IN_LABELS[provider] ?? provider}: signed in`);\n }\n\n /**\n * Poll the device-code endpoint until the user confirmed, the window closed or\n * the adapter stops. The handle lives in memory only \u2014 a restart mid-flow just\n * means the user starts again, which is cheaper than persisting a 15-minute secret.\n *\n * @param provider the subscription kind\n * @param start the device-code handle\n */\n private armDevicePoller(provider: string, start: DeviceCodeStart): void {\n const tick = async (): Promise<void> => {\n try {\n if (attemptExpired(start.expiresAt, Date.now())) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, \"the code expired \u2014 start the sign-in again\");\n return;\n }\n const result = await pollDeviceCode(start, postJson);\n if (result.status === \"ready\") {\n this.stopDevicePoller(provider);\n const tokens = await exchangeDeviceCode(result.code, result.codeVerifier, postForm, Date.now());\n await this.finishSignIn(provider, tokens);\n }\n } catch (e) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, e instanceof Error ? e.message : String(e));\n }\n };\n this.devicePollers.set(provider, {\n kind: \"interval\",\n handle: this.setInterval(() => void tick(), Math.max(start.intervalSec, 1) * 1000),\n });\n }\n\n /**\n * Stop a running device-code poller.\n *\n * @param provider the subscription kind\n */\n private stopDevicePoller(provider: string): void {\n const handle = this.devicePollers.get(provider);\n if (handle?.kind === \"interval\") {\n this.clearInterval(handle.handle);\n }\n this.devicePollers.delete(provider);\n }\n\n /**\n * The current sign-in state of one subscription, for the admin row.\n *\n * @param provider the subscription kind\n * @returns the state\n */\n private async signInState(provider: string): Promise<SignInState> {\n const failure = this.signInErrors.get(provider);\n const attempt = this.attempts.get(provider);\n if (attempt?.flow === \"device-code\") {\n return {\n status: \"awaiting-device\",\n userCode: attempt.start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: attempt.start.expiresAt,\n };\n }\n if (attempt) {\n if (attemptExpired(attempt.expiresAt, Date.now())) {\n this.attempts.delete(provider);\n return { status: \"failed\", reason: \"the sign-in window expired \u2014 start the sign-in again\" };\n }\n return { status: \"awaiting-paste\", url: attempt.url, flow: attempt.flow };\n }\n if (failure) {\n return { status: \"failed\", reason: failure };\n }\n return (await this.tokenStore(provider).load()) ? { status: \"signed-in\" } : { status: \"signed-out\" };\n }\n\n /**\n * Forget the tokens of one subscription.\n *\n * @param provider the subscription kind\n * @returns the resulting state\n */\n private async signOut(provider: string): Promise<SignInState> {\n await this.tokenStore(provider).clear();\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n return { status: \"signed-out\" };\n }\n\n // ------------------------------------------------------------ token files\n\n /**\n * Where one subscription's tokens live: an encrypted file in the instance data\n * directory, named after the PROVIDER. Keying by provider (not by account name)\n * keeps a sign-in alive when an account is renamed \u2014 the previous scheme lost it.\n *\n * @param provider the subscription kind\n * @returns the store\n */\n private tokenStore(provider: string): TokenStore {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const file = join(dir, `tokens-${provider}.json`);\n return {\n load: async (): Promise<TokenSet | null> => {\n try {\n const parsed = JSON.parse(this.decrypt(await readFile(file, \"utf8\"))) as Partial<TokenSet>;\n if (typeof parsed.accessToken !== \"string\" || typeof parsed.refreshToken !== \"string\") {\n return null;\n }\n return {\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken,\n expiresAt: Number(parsed.expiresAt) || 0,\n accountRef: typeof parsed.accountRef === \"string\" ? parsed.accountRef : undefined,\n };\n } catch {\n return null; // never signed in (or unreadable) \u2014 the provider reports auth-required\n }\n },\n save: async (tokens: TokenSet): Promise<void> => {\n await mkdir(dir, { recursive: true });\n await writeFile(file, this.encrypt(JSON.stringify(tokens)), \"utf8\");\n },\n clear: async (): Promise<void> => {\n await unlink(file).catch(() => {\n /* already gone */\n });\n },\n };\n }\n\n /**\n * Carry a sign-in from the pre-0.3.0 layout over: tokens used to be stored per\n * ACCOUNT NAME (`claude-tokens-<name>.json`). Without this the rename to fixed\n * account ids would silently sign the user out.\n */\n private async migrateTokenFiles(): Promise<void> {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const target = join(dir, \"tokens-claude-sub.json\");\n try {\n await readFile(target, \"utf8\");\n return; // already migrated\n } catch {\n // not there yet \u2014 look for an old file\n }\n for (const legacy of [\"claude-tokens-Claude.json\", \"claude-tokens-claude.json\"]) {\n try {\n await rename(join(dir, legacy), target);\n this.log.info(\"Carried the existing Claude sign-in over to the new layout\");\n return;\n } catch {\n // try the next candidate\n }\n }\n }\n\n // ------------------------------------------------------------- life cycle\n\n /** Validate the configuration, clean up stale objects and start the engine. */\n private async onReady(): Promise<void> {\n try {\n const accounts = parseAccounts(this.config.accounts);\n const interval = clampPollInterval(this.config.pollInterval);\n await this.migrateTokenFiles();\n // Baseline first: the cleanup deletes and the engine creates, both are counted\n // against this snapshot.\n await this.snapshotExistingStates();\n await this.cleanupStaleObjects();\n const retired = await this.removeRetiredStates(accounts);\n if (retired > 0) {\n this.log.info(`Object tree updated: removed ${retired} obsolete status datapoint(s)`);\n }\n if (accounts.length === 0) {\n this.log.info(\"No AI accounts configured \u2014 add accounts in the instance settings\");\n await this.setState(\"info.connection\", { val: false, ack: true });\n return;\n }\n const providers = new Map<string, UsageProvider>();\n for (const account of accounts) {\n const provider = await this.makeProvider(account);\n if (provider) {\n providers.set(account.id, provider);\n }\n }\n this.engine = new PollEngine(accounts, providers, interval, {\n upsertObject: async def => {\n await this.extendObject(def.id, { type: def.type, common: def.common as ioBroker.ObjectCommon, native: {} });\n if (def.type === \"state\" && !this.knownStateIds.has(def.id)) {\n this.knownStateIds.add(def.id);\n this.createdStates++;\n }\n },\n setState: (id, value) => {\n void this.setState(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n setStateChanged: (id, value) => {\n void this.setStateChangedAsync(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n schedule: (cb, ms): TimerHandle => ({ kind: \"interval\", handle: this.setInterval(cb, ms) }),\n scheduleOnce: (cb, ms): TimerHandle => ({ kind: \"timeout\", handle: this.setTimeout(cb, ms) }),\n cancel: handle => {\n const timer = handle as TimerHandle;\n if (timer.kind === \"interval\") {\n this.clearInterval(timer.handle);\n } else {\n this.clearTimeout(timer.handle);\n }\n },\n now: () => Date.now(),\n log: {\n debug: m => this.log.debug(m),\n info: m => this.log.info(m),\n warn: m => this.log.warn(m),\n error: m => this.log.error(m),\n },\n afterFirstRound: () => this.logDatapointBalance(),\n notify: this.config.notifications\n ? (_account, message) =>\n void this.registerNotification(\"ai-usage\", \"userActionRequired\", message).catch(e =>\n this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`),\n )\n : undefined,\n });\n await this.engine.start();\n this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);\n } catch (e) {\n this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Build the provider for one account.\n *\n * @param account the validated account config\n * @returns the provider, or undefined to skip the account\n */\n private async makeProvider(account: AccountConfig): Promise<UsageProvider | undefined> {\n switch (account.provider) {\n case \"claude-sub\":\n return claudeSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"chatgpt-sub\":\n return chatgptSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"gemini-sub\":\n return geminiSubProvider(this.tokenStore(account.provider), postJson, postForm);\n case \"openrouter\": {\n const key = await this.resolveKey(account);\n return key ? openRouterProvider(key) : undefined;\n }\n case \"deepseek\": {\n const key = await this.resolveKey(account);\n return key ? deepSeekProvider(key) : undefined;\n }\n case \"openai\": {\n const key = await this.resolveKey(account);\n return key ? openAiProvider(key) : undefined;\n }\n case \"anthropic-api\": {\n const key = await this.resolveKey(account);\n return key ? anthropicApiProvider(key) : undefined;\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Read and decrypt a key-form credential from the central credential storage.\n *\n * @param account the account whose credential to resolve\n * @returns the key, or undefined (with a log line) when it cannot be read\n */\n private async resolveKey(account: AccountConfig): Promise<string | undefined> {\n if (!account.credentialId) {\n this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);\n return undefined;\n }\n try {\n const credential = await Credentials.getCredentials(this, account.credentialId);\n const values = credential.values as { key?: unknown };\n const key = typeof values.key === \"string\" && values.key ? values.key : undefined;\n if (!key) {\n this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);\n }\n return key;\n } catch (e) {\n this.log.warn(\n `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`,\n );\n return undefined;\n }\n }\n\n /**\n * Status states retired in 0.5.0 \u2014 `provider` (visible in the node name anyway),\n * `reachable`/`serviceOnline`/`state` (three spellings of one fact) and `signedIn`\n * (the settings page asks the adapter directly).\n *\n * ioBroker never garbage-collects an object whose id the adapter stopped writing:\n * it would sit there frozen on its last value and keep lying. So the old ids are\n * deleted from a fixed list on every start \u2014 no state reading, no heuristics.\n */\n private static readonly RETIRED_INFO_STATES = [\n \"info.provider\",\n \"info.reachable\",\n \"info.serviceOnline\",\n \"info.state\",\n \"info.signedIn\",\n ];\n\n /**\n * Remove the retired status states of every configured account.\n *\n * @param accounts the configured accounts\n * @returns how many objects were actually deleted\n */\n private async removeRetiredStates(accounts: readonly AccountConfig[]): Promise<number> {\n let removed = 0;\n for (const account of accounts) {\n for (const suffix of AiUsageAdapter.RETIRED_INFO_STATES) {\n const id = `${account.id}.${suffix}`;\n try {\n // Existence-checked so the log line can report a real number; these are\n // stable dead orphans, never touched concurrently.\n if (await this.getObjectAsync(id)) {\n await this.delObjectAsync(id);\n removed++;\n // Deliberately NOT counted in the startup balance: this one-shot migration\n // reports its own sum, and the same deletion must not appear in two lines\n // that mean different things.\n this.knownStateIds.delete(id);\n }\n } catch (e) {\n this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n }\n return removed;\n }\n\n /**\n * Delete object trees that no longer belong to a configured account, plus the\n * `auth` branch of the pre-0.3.0 layout. An EMPTY table deletes nothing \u2014 the\n * guard against wiping everything through an accidental clear.\n */\n private async cleanupStaleObjects(): Promise<void> {\n const keepIds = validAccountIds(this.config.accounts);\n if (keepIds.length === 0) {\n return;\n }\n const keep = new Set([...keepIds, \"info\", \"total\"]);\n try {\n const objects = await this.getAdapterObjectsAsync();\n const roots = new Set<string>();\n for (const id of Object.keys(objects)) {\n const root = id.substring(this.namespace.length + 1).split(\".\")[0];\n if (root && !keep.has(root)) {\n roots.add(root);\n }\n }\n for (const root of roots) {\n this.log.info(\n root === \"auth\"\n ? \"Removing the old sign-in branch \u2014 the sign-in state now lives inside each account\"\n : `Removing objects of no longer configured account \"${root}\"`,\n );\n // Count the datapoints BEFORE they are gone \u2014 afterwards there is nothing to count.\n for (const id of [...this.knownStateIds]) {\n if (id === root || id.startsWith(`${root}.`)) {\n this.knownStateIds.delete(id);\n this.removedStates++;\n }\n }\n await this.delObjectAsync(root, { recursive: true });\n }\n } catch (e) {\n this.log.warn(`Cleanup of stale objects failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Tear down synchronously \u2014 no async/await here, else the controller kills the\n * process before cleanup finishes.\n *\n * @param callback invoked when cleanup is done\n */\n private onUnload(callback: () => void): void {\n try {\n for (const provider of [...this.devicePollers.keys()]) {\n this.stopDevicePoller(provider);\n }\n this.engine?.stop();\n this.engine = null;\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // never block shutdown\n }\n callback();\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AiUsageAdapter(options);\n} else {\n (() => new AiUsageAdapter())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "ai-usage",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.7.1": {
|
|
7
|
+
"en": "Restarting the adapter no longer writes one unchanged value into every status datapoint — a recorded history now stays free of restart noise",
|
|
8
|
+
"de": "Ein Neustart des Adapters schreibt nicht mehr einmalig einen unveränderten Wert in jeden Status-Datenpunkt — eine Aufzeichnung bleibt frei von Neustart-Rauschen",
|
|
9
|
+
"ru": "Перезапуск адаптера больше не записывает однократно неизменённое значение в каждую точку состояния — история остаётся без шума от перезапусков",
|
|
10
|
+
"pt": "Reiniciar o adaptador já não escreve uma vez um valor inalterado em cada ponto de estado — o histórico fica livre do ruído dos reinícios",
|
|
11
|
+
"nl": "Een herstart van de adapter schrijft niet meer eenmalig een ongewijzigde waarde naar elk statusdatapunt — een opname blijft vrij van herstartruis",
|
|
12
|
+
"fr": "Un redémarrage de l'adaptateur n'écrit plus une fois une valeur inchangée dans chaque point d'état — l'historique reste exempt de bruit de redémarrage",
|
|
13
|
+
"it": "Il riavvio dell'adapter non scrive più una volta un valore invariato in ogni punto di stato — la cronologia resta libera dal rumore dei riavvii",
|
|
14
|
+
"es": "Reiniciar el adaptador ya no escribe una vez un valor sin cambios en cada punto de estado — el historial queda libre del ruido de los reinicios",
|
|
15
|
+
"pl": "Ponowne uruchomienie adaptera nie zapisuje już jednorazowo niezmienionej wartości w każdym punkcie stanu — historia pozostaje wolna od szumu restartów",
|
|
16
|
+
"uk": "Перезапуск адаптера більше не записує одноразово незмінене значення в кожну точку стану — історія лишається без шуму від перезапусків",
|
|
17
|
+
"zh-cn": "重启适配器不再向每个状态数据点写入一次未变化的值,记录的历史不再出现重启噪声"
|
|
18
|
+
},
|
|
19
|
+
"0.7.0": {
|
|
20
|
+
"en": "After a change the log says in one line how many datapoints the object tree gained and lost — no clicking through the tree to find out",
|
|
21
|
+
"de": "Nach einer Änderung sagt das Protokoll in einer Zeile, wie viele Datenpunkte dazugekommen und weggefallen sind — kein Durchklicken des Objektbaums nötig",
|
|
22
|
+
"ru": "После изменения журнал одной строкой сообщает, сколько точек данных добавилось и исчезло — не нужно просматривать дерево объектов",
|
|
23
|
+
"pt": "Após uma alteração, o registo indica numa linha quantos pontos de dados foram criados e removidos — sem percorrer a árvore de objetos",
|
|
24
|
+
"nl": "Na een wijziging meldt het logboek in één regel hoeveel datapunten erbij kwamen en verdwenen — zonder de objectboom door te klikken",
|
|
25
|
+
"fr": "Après une modification, le journal indique en une ligne combien de points de données ont été créés et supprimés — sans parcourir l'arbre des objets",
|
|
26
|
+
"it": "Dopo una modifica il log indica in una riga quanti punti dati sono stati creati e rimossi — senza scorrere l'albero degli oggetti",
|
|
27
|
+
"es": "Tras un cambio, el registro indica en una línea cuántos puntos de datos se crearon y eliminaron — sin recorrer el árbol de objetos",
|
|
28
|
+
"pl": "Po zmianie dziennik podaje w jednej linii, ile punktów danych przybyło i ubyło — bez przeglądania drzewa obiektów",
|
|
29
|
+
"uk": "Після зміни журнал одним рядком повідомляє, скільки точок даних додалося та зникло — без перегляду дерева об'єктів",
|
|
30
|
+
"zh-cn": "更改后,日志用一行说明对象树新增和移除了多少数据点,无需逐层点开查看"
|
|
31
|
+
},
|
|
6
32
|
"0.6.0": {
|
|
7
33
|
"en": "Every account now carries the connection icon in the object tree — green while it delivers, struck through when it does not, like any other ioBroker device",
|
|
8
34
|
"de": "Jedes Konto trägt jetzt das Verbindungs-Symbol im Objektbaum — grün solange es liefert, durchgestrichen wenn nicht, wie bei jedem anderen ioBroker-Gerät",
|
|
@@ -67,19 +93,6 @@
|
|
|
67
93
|
"pl": "Całkowicie nowa strona ustawień: zapisane dane dostępu AI jako proste przełączniki i niezawodne prowadzone logowanie Claude",
|
|
68
94
|
"uk": "Повністю нова сторінка налаштувань: збережені дані доступу ШІ як прості перемикачі та надійний покроковий вхід Claude",
|
|
69
95
|
"zh-cn": "全新的设置页面:已保存的 AI 凭据变为简单的开关,并提供可靠的引导式 Claude 登录"
|
|
70
|
-
},
|
|
71
|
-
"0.1.0": {
|
|
72
|
-
"en": "First release: usage limits, credits and costs of your Claude, OpenAI, Anthropic, OpenRouter and DeepSeek accounts as datapoints, with a warning at your chosen threshold",
|
|
73
|
-
"de": "Erste Version: Auslastungs-Limits, Guthaben und Kosten Ihrer Claude-, OpenAI-, Anthropic-, OpenRouter- und DeepSeek-Konten als Datenpunkte, mit Warnung an Ihrer Schwelle",
|
|
74
|
-
"ru": "Первый выпуск: лимиты, кредиты и расходы аккаунтов Claude, OpenAI, Anthropic, OpenRouter и DeepSeek как точки данных, с предупреждением на заданном пороге",
|
|
75
|
-
"pt": "Primeira versão: limites de uso, créditos e custos das contas Claude, OpenAI, Anthropic, OpenRouter e DeepSeek como pontos de dados, com aviso no limite escolhido",
|
|
76
|
-
"nl": "Eerste release: gebruikslimieten, tegoeden en kosten van uw Claude-, OpenAI-, Anthropic-, OpenRouter- en DeepSeek-accounts als datapunten, met waarschuwing bij uw drempel",
|
|
77
|
-
"fr": "Première version : limites d'utilisation, crédits et coûts de vos comptes Claude, OpenAI, Anthropic, OpenRouter et DeepSeek comme points de données, avec alerte au seuil choisi",
|
|
78
|
-
"it": "Prima versione: limiti di utilizzo, crediti e costi degli account Claude, OpenAI, Anthropic, OpenRouter e DeepSeek come punti dati, con avviso alla soglia scelta",
|
|
79
|
-
"es": "Primera versión: límites de uso, créditos y costes de sus cuentas Claude, OpenAI, Anthropic, OpenRouter y DeepSeek como puntos de datos, con aviso en el umbral elegido",
|
|
80
|
-
"pl": "Pierwsze wydanie: limity użycia, środki i koszty kont Claude, OpenAI, Anthropic, OpenRouter i DeepSeek jako punkty danych, z ostrzeżeniem przy wybranym progu",
|
|
81
|
-
"uk": "Перший випуск: ліміти використання, кредити та витрати акаунтів Claude, OpenAI, Anthropic, OpenRouter і DeepSeek як точки даних, з попередженням на обраному порозі",
|
|
82
|
-
"zh-cn": "首个版本:将 Claude、OpenAI、Anthropic、OpenRouter 和 DeepSeek 账户的使用限额、余额与费用作为数据点,并在达到所选阈值时发出警告"
|
|
83
96
|
}
|
|
84
97
|
},
|
|
85
98
|
"plugins": {
|