iobroker.ai-usage 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -104
- package/build/lib/poll-engine.js +58 -12
- 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/lib/snapshot-tree.js.map +2 -2
- package/build/main.js +75 -6
- package/build/main.js.map +2 -2
- package/io-package.json +27 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# <img src="https://cdn.jsdelivr.net/gh/krobipd/ioBroker.ai-usage@main/admin/ai-usage.svg?v=
|
|
1
|
+
# <img src="https://cdn.jsdelivr.net/gh/krobipd/ioBroker.ai-usage@main/admin/ai-usage.svg?v=4" width="48" align="top" /> ioBroker.ai-usage
|
|
2
2
|
|
|
3
3
|
**Release:** [](https://www.npmjs.com/package/iobroker.ai-usage)   [](https://www.npmjs.com/package/iobroker.ai-usage)
|
|
4
4
|
|
|
@@ -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,14 +72,14 @@ 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
|
|
111
79
|
├── claude / chatgpt / gemini — one node per subscription
|
|
112
80
|
│ ├── warning — this account is above its warn threshold (bool)
|
|
113
81
|
│ ├── limitReached — a plan-wide window of this account is full (bool)
|
|
114
|
-
│ ├── info.unreach —
|
|
82
|
+
│ ├── info.unreach — account is not delivering (bool) — drives the connection icon
|
|
115
83
|
│ ├── info.error — why there is no data, in plain text; empty while all is well
|
|
116
84
|
│ ├── info.lastUpdate — time of the last successful read
|
|
117
85
|
│ ├── limits.<window>.* — percent + reset time (session, week, per model, …)
|
|
@@ -125,54 +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
|
-
on only when the AI service itself did not answer: a fault reported by the service switches it
|
|
133
|
-
on immediately, an unreachable host after three attempts in a row, so a single hiccup does not
|
|
134
|
-
make it flap.
|
|
135
|
-
|
|
136
|
-
**`info.error`** says what is wrong, in a sentence you can read:
|
|
137
|
-
|
|
138
|
-
| Situation | `info.unreach` | `info.error` |
|
|
139
|
-
|-----------|----------------|--------------|
|
|
140
|
-
| Everything works | off | empty |
|
|
141
|
-
| Sign-in or key rejected | **off** — the service answered, only your access is broken | `Sign-in rejected — …` |
|
|
142
|
-
| Throttled by the provider | **off** — the service answered; last values are kept | `Throttled by the provider — retrying in 10 min …` |
|
|
143
|
-
| The service reports a fault | **on** | `The AI service reports a fault — HTTP 503` |
|
|
144
|
-
| Not reachable at all | **on** | `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.
|
|
145
100
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
sit at 100 % forever, and an alarm that never clears is worse than none. Google is the exception,
|
|
152
|
-
because there every bucket belongs to a model — those are your quota, so they do count.
|
|
153
|
-
|
|
154
|
-
Want a specific model watched anyway? Build the automation on its own datapoint,
|
|
155
|
-
`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`.
|
|
156
106
|
|
|
157
107
|
---
|
|
158
108
|
|
|
159
109
|
## Troubleshooting
|
|
160
110
|
|
|
161
111
|
### An account delivers no data
|
|
162
|
-
Read `info.error`
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
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.
|
|
166
115
|
|
|
167
116
|
### A subscription says "not signed in" although you just signed in
|
|
168
117
|
Save the settings first, then sign in — the row needs a saved account to attach the sign-in to.
|
|
169
|
-
After a successful sign-in the
|
|
170
|
-
seconds.
|
|
171
|
-
|
|
172
|
-
### The object tree changed after updating to 0.3.0
|
|
173
|
-
Accounts now live under fixed ids (`claude`, `chatgpt`, `gemini`, `<name>-api`) and the separate
|
|
174
|
-
`auth` branch is gone — the sign-in state moved into `<account>.info.signedIn`. Old nodes are
|
|
175
|
-
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.
|
|
176
119
|
|
|
177
120
|
---
|
|
178
121
|
|
|
@@ -182,9 +125,19 @@ removed automatically, and an existing Claude sign-in is carried over.
|
|
|
182
125
|
Placeholder for the next version (at the beginning of the line):
|
|
183
126
|
### **WORK IN PROGRESS**
|
|
184
127
|
-->
|
|
128
|
+
### 0.7.0 (2026-08-27)
|
|
129
|
+
|
|
130
|
+
- 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
|
|
131
|
+
- 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
|
|
132
|
+
- Fixed: Datapoints that only repeat their previous value are no longer rewritten every cycle, which kept flooding the history of anyone recording them
|
|
133
|
+
|
|
134
|
+
### 0.6.0 (2026-08-26)
|
|
135
|
+
|
|
136
|
+
- 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
|
|
137
|
+
|
|
185
138
|
### 0.5.0 (2026-08-26)
|
|
186
139
|
|
|
187
|
-
- Changed: Each account now has two status datapoints instead of six — an offline marker
|
|
140
|
+
- Changed: Each account now has two status datapoints instead of six — an offline marker and the reason in plain text. The retired ones are deleted on start
|
|
188
141
|
- New: The settings page shows every switched-on account as online, limited or offline at a glance, with the full reason in plain text when you hover the badge
|
|
189
142
|
- Changed: The names of the total and per-account limit datapoints now say "plan-wide", matching what they have actually counted since 0.4.0
|
|
190
143
|
|
|
@@ -203,15 +156,6 @@ removed automatically, and an existing Claude sign-in is carried over.
|
|
|
203
156
|
- Fixed: After signing in, the account is queried immediately instead of waiting for the next poll — no restart needed
|
|
204
157
|
- Fixed: OpenAI and Anthropic rows now state that they need an organisation admin key, instead of failing with an unexplained rejection
|
|
205
158
|
|
|
206
|
-
### 0.2.0 (2026-08-26)
|
|
207
|
-
|
|
208
|
-
- 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
|
|
209
|
-
- 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
|
|
210
|
-
|
|
211
|
-
### 0.1.0 (2026-08-25)
|
|
212
|
-
|
|
213
|
-
- New: First release — reads usage limits, credits and costs of your Claude, OpenAI, Anthropic, OpenRouter and DeepSeek accounts into datapoints, with one warning at your chosen threshold
|
|
214
|
-
|
|
215
159
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
216
160
|
|
|
217
161
|
## Support
|
package/build/lib/poll-engine.js
CHANGED
|
@@ -56,7 +56,8 @@ class PollEngine {
|
|
|
56
56
|
state: "no-connection",
|
|
57
57
|
error: "waiting for the first query",
|
|
58
58
|
createdObjects: /* @__PURE__ */ new Set(),
|
|
59
|
-
written: /* @__PURE__ */ new Map()
|
|
59
|
+
written: /* @__PURE__ */ new Map(),
|
|
60
|
+
firstPollDone: false
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -65,6 +66,9 @@ class PollEngine {
|
|
|
65
66
|
runtimes = [];
|
|
66
67
|
handles = [];
|
|
67
68
|
stopped = false;
|
|
69
|
+
firstRoundReported = false;
|
|
70
|
+
/** Last written value of the adapter-wide indicators — keeps history free of repeats. */
|
|
71
|
+
writtenTotals = /* @__PURE__ */ new Map();
|
|
68
72
|
/** Create the static per-account and totals objects, then arm the poll cycles. */
|
|
69
73
|
async start() {
|
|
70
74
|
for (const runtime of this.runtimes) {
|
|
@@ -72,6 +76,10 @@ class PollEngine {
|
|
|
72
76
|
}
|
|
73
77
|
await this.createTotalsSkeleton();
|
|
74
78
|
await this.writeTotals();
|
|
79
|
+
if (this.runtimes.length === 0) {
|
|
80
|
+
this.reportFirstRoundOnce();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
75
83
|
this.runtimes.forEach((runtime, index) => {
|
|
76
84
|
this.handles.push(
|
|
77
85
|
this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),
|
|
@@ -117,6 +125,8 @@ class PollEngine {
|
|
|
117
125
|
const { config } = runtime;
|
|
118
126
|
if (this.deps.now() < runtime.skipUntil) {
|
|
119
127
|
this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);
|
|
128
|
+
runtime.firstPollDone = true;
|
|
129
|
+
this.reportFirstRoundOnce();
|
|
120
130
|
return;
|
|
121
131
|
}
|
|
122
132
|
try {
|
|
@@ -135,6 +145,17 @@ class PollEngine {
|
|
|
135
145
|
}
|
|
136
146
|
this.writeAccountInfo(runtime);
|
|
137
147
|
await this.writeTotals();
|
|
148
|
+
runtime.firstPollDone = true;
|
|
149
|
+
this.reportFirstRoundOnce();
|
|
150
|
+
}
|
|
151
|
+
/** Fire the first-round hook exactly once, when no account is still pending. */
|
|
152
|
+
reportFirstRoundOnce() {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
if (this.firstRoundReported || this.runtimes.some((runtime) => !runtime.firstPollDone)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.firstRoundReported = true;
|
|
158
|
+
(_b = (_a = this.deps).afterFirstRound) == null ? void 0 : _b.call(_a);
|
|
138
159
|
}
|
|
139
160
|
/**
|
|
140
161
|
* Write a successful snapshot: upsert new objects (create-once cache), write the
|
|
@@ -160,8 +181,8 @@ class PollEngine {
|
|
|
160
181
|
const percent = (_a = driver == null ? void 0 : driver.percent) != null ? _a : 0;
|
|
161
182
|
const wasWarning = runtime.status.warning;
|
|
162
183
|
runtime.status.warning = percent >= config.warnThreshold;
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
184
|
+
this.setIfChanged(runtime, `${config.id}.warning`, runtime.status.warning);
|
|
185
|
+
this.setIfChanged(runtime, `${config.id}.limitReached`, percent >= 100);
|
|
165
186
|
if (runtime.status.warning && !wasWarning) {
|
|
166
187
|
const window = driver ? `${driver.label} ` : "";
|
|
167
188
|
const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;
|
|
@@ -234,7 +255,7 @@ class PollEngine {
|
|
|
234
255
|
}
|
|
235
256
|
}
|
|
236
257
|
/**
|
|
237
|
-
* Write one account's info states (
|
|
258
|
+
* Write one account's info states (offline marker, error text, last update).
|
|
238
259
|
*
|
|
239
260
|
* @param runtime the account's runtime
|
|
240
261
|
*/
|
|
@@ -248,16 +269,19 @@ class PollEngine {
|
|
|
248
269
|
/**
|
|
249
270
|
* Write the two status states.
|
|
250
271
|
*
|
|
251
|
-
* `unreach`
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
272
|
+
* `unreach` drives the connection icon the admin draws next to the account, so it
|
|
273
|
+
* has to mean what a user reads into that icon: green while the account delivers.
|
|
274
|
+
* A throttle keeps the last values and the service is fine, so it stays green and
|
|
275
|
+
* only fills the error text; a dead sign-in, a broken service or no connection at
|
|
276
|
+
* all turn it off. Both states are written on change only — an indicator rewritten
|
|
277
|
+
* every cycle floods the history and hides the real transition.
|
|
255
278
|
*
|
|
256
279
|
* @param runtime the account's runtime
|
|
257
280
|
*/
|
|
258
281
|
writeAccountStatus(runtime) {
|
|
259
282
|
const { config } = runtime;
|
|
260
|
-
|
|
283
|
+
const delivering = runtime.state === "ok" || runtime.state === "rate-limited";
|
|
284
|
+
this.setIfChanged(runtime, `${config.id}.info.unreach`, !delivering);
|
|
261
285
|
this.setIfChanged(runtime, `${config.id}.info.error`, runtime.error);
|
|
262
286
|
}
|
|
263
287
|
/**
|
|
@@ -267,6 +291,19 @@ class PollEngine {
|
|
|
267
291
|
* @param id the state id
|
|
268
292
|
* @param value the value
|
|
269
293
|
*/
|
|
294
|
+
/**
|
|
295
|
+
* Write an adapter-wide indicator only when it changed.
|
|
296
|
+
*
|
|
297
|
+
* @param id the state id
|
|
298
|
+
* @param value the value
|
|
299
|
+
*/
|
|
300
|
+
setTotalIfChanged(id, value) {
|
|
301
|
+
if (this.writtenTotals.get(id) === value) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.writtenTotals.set(id, value);
|
|
305
|
+
this.deps.setState(id, value);
|
|
306
|
+
}
|
|
270
307
|
setIfChanged(runtime, id, value) {
|
|
271
308
|
if (runtime.written.get(id) === value) {
|
|
272
309
|
return;
|
|
@@ -282,10 +319,10 @@ class PollEngine {
|
|
|
282
319
|
this.deps.setState("total.costs.projectedMonth", totals.costsProjectedMonth);
|
|
283
320
|
this.deps.setState("total.maxLimitPercent", totals.maxLimitPercent);
|
|
284
321
|
this.deps.setState("total.warningsActive", totals.warningsActive);
|
|
285
|
-
this.
|
|
322
|
+
this.setTotalIfChanged("total.limitReached", totals.limitReached);
|
|
286
323
|
this.deps.setState("total.accountsReachable", totals.accountsReachable);
|
|
287
324
|
this.deps.setState("total.accounts", totals.accounts);
|
|
288
|
-
this.
|
|
325
|
+
this.setTotalIfChanged("info.connection", totals.accountsReachable > 0);
|
|
289
326
|
return Promise.resolve();
|
|
290
327
|
}
|
|
291
328
|
/**
|
|
@@ -296,7 +333,16 @@ class PollEngine {
|
|
|
296
333
|
async createAccountSkeleton(runtime) {
|
|
297
334
|
const { config } = runtime;
|
|
298
335
|
const defs = [
|
|
299
|
-
{
|
|
336
|
+
{
|
|
337
|
+
id: config.id,
|
|
338
|
+
type: "device",
|
|
339
|
+
common: {
|
|
340
|
+
name: `${config.name} (${config.provider})`,
|
|
341
|
+
// The admin's object tree draws its connection icon from this link and
|
|
342
|
+
// from nothing else — govee, beszel, homewizard and nut2 all do the same.
|
|
343
|
+
statusStates: { offlineId: "info.unreach" }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
300
346
|
{ id: `${config.id}.info`, type: "channel", common: { name: "Info" } },
|
|
301
347
|
{
|
|
302
348
|
// The two slots ioBroker itself provides for this — measured against
|
|
@@ -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` says whether the AI SERVICE answered at all \u2014 a rejected sign-in or a\n * throttle both mean it is up, so those leave the marker off and only fill the\n * error text. Both are written on change only: an indicator rewritten every cycle\n * 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 this.setIfChanged(runtime, `${config.id}.info.unreach`, !runtime.serviceOnline);\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 { id: config.id, type: \"device\", common: { name: `${config.name} (${config.provider})` } },\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. */\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 * 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 /** Last written value per state id \u2014 keeps unchanged values out of the history. */\n written: Map<string, boolean | number | 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 /** Last written value of the adapter-wide indicators \u2014 keeps history free of repeats. */\n private readonly writtenTotals = new Map<string, boolean | number | string>();\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 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 on change only \u2014 the same reason as for the status states.\n this.setIfChanged(runtime, `${config.id}.warning`, runtime.status.warning);\n this.setIfChanged(runtime, `${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 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 /**\n * Write an adapter-wide indicator only when it changed.\n *\n * @param id the state id\n * @param value the value\n */\n private setTotalIfChanged(id: string, value: boolean | number | string): void {\n if (this.writtenTotals.get(id) === value) {\n return;\n }\n this.writtenTotals.set(id, value);\n this.deps.setState(id, value);\n }\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.setTotalIfChanged(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n this.deps.setState(\"total.accounts\", totals.accounts);\n this.setTotalIfChanged(\"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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/snapshot-tree.ts"],
|
|
4
|
-
"sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: string | Record<string, string>;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: string,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created,\n * so the tree mirrors exactly what the provider's source delivers.\n *\n * @param accountId the id-safe account id (device node)\n * @param accountName the display name for the device object\n * @param provider the provider kind (device object name suffix)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(\n accountId: string,\n accountName: string,\n provider: string,\n snapshot: UsageSnapshot,\n): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: string): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n\n objects.push({ id: accountId, type: \"device\", common: { name: `${accountName} (${provider})` } });\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, \"Limit windows\");\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n channel(`${accountId}.limits.${windowId}`, limit.label);\n add(\n state(`${accountId}.limits.${windowId}.percent`, `${limit.label} used`, \"number\", \"value\", limit.percent, \"%\"),\n );\n if (limit.resetAt !== undefined) {\n add(\n state(`${accountId}.limits.${windowId}.resetAt`, `${limit.label} resets at`, \"string\", \"date\", limit.resetAt),\n );\n }\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, \"Credits\");\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, \"Credits used\", \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, \"Credits limit\", \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(state(`${accountId}.credits.remaining`, \"Credits remaining\", \"number\", \"value\", credits.remaining, unit));\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, \"Credits used (percent)\", \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(state(`${accountId}.credits.granted`, \"Granted balance\", \"number\", \"value\", credits.granted, unit));\n }\n if (credits.toppedUp !== undefined) {\n add(state(`${accountId}.credits.toppedUp`, \"Topped-up balance\", \"number\", \"value\", credits.toppedUp, unit));\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, \"Costs\");\n if (costs.today !== undefined) {\n add(state(`${accountId}.costs.today`, \"Costs today\", \"number\", \"value\", costs.today, costs.currency));\n }\n if (costs.month !== undefined) {\n add(state(`${accountId}.costs.month`, \"Costs this month\", \"number\", \"value\", costs.month, costs.currency));\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, \"Costs total (lifetime)\", \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n \"Costs projected month-end (computed)\",\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, \"Tokens\");\n if (tokens.inputToday !== undefined) {\n add(state(`${accountId}.tokens.inputToday`, \"Input tokens today\", \"number\", \"value\", tokens.inputToday));\n }\n if (tokens.outputToday !== undefined) {\n add(state(`${accountId}.tokens.outputToday`, \"Output tokens today\", \"number\", \"value\", tokens.outputToday));\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, \"Per-model usage\");\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n channel(`${accountId}.models.${modelId}`, model.model);\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n `${model.model} tokens today`,\n \"number\",\n \"value\",\n model.tokens,\n ),\n );\n }\n if (model.cost !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.costToday`,\n `${model.model} costs today`,\n \"number\",\n \"value\",\n model.cost,\n snapshot.costs?.currency ?? \"USD\",\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n add(state(`${accountId}.available`, \"Balance sufficient for calls\", \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * The window that decides how full an account is: the highest PLAN-WIDE limit, or\n * the granted budget when the account has no windows at all.\n *\n * Windows marked `scoped` are left out on purpose \u2014 they cover a single model and\n * may sit at 100 % permanently for a model the user never touches, which would\n * pin the account's warning on forever. Their datapoints still exist; they just\n * do not speak for the account.\n *\n * @param snapshot the snapshot\n * @returns percent plus the label that produced it, or undefined when nothing applies\n */\nexport function limitingWindow(snapshot: UsageSnapshot): { percent: number; label: string } | undefined {\n let best: { percent: number; label: string } | undefined;\n for (const limit of snapshot.limits ?? []) {\n if (limit.scoped) {\n continue;\n }\n if (!best || limit.percent > best.percent) {\n best = { percent: limit.percent, label: limit.label };\n }\n }\n const credits = snapshot.credits?.percent;\n if (credits !== undefined && (!best || credits > best.percent)) {\n best = { percent: credits, label: \"Credits\" };\n }\n return best;\n}\n\n/**\n * The highest plan-wide limit percent in a snapshot, or undefined when it has none.\n *\n * @param snapshot the snapshot\n * @returns the maximum percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n return limitingWindow(snapshot)?.percent;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,0BAA2B;
|
|
4
|
+
"sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: string | Record<string, string>;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n /**\n * Links a device to the state that says whether it is up. THIS is what puts the\n * connection icon next to the device in the admin's object tree \u2014 the object\n * browser reads `common.statusStates` and nothing else (verified in\n * adapter-react-v5 `renderLeaf.tsx`). A relative id is expanded to\n * `<device>.<id>`. `offlineId` is inverted: true means offline.\n */\n statusStates?: { onlineId?: string; offlineId?: string; errorId?: string };\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: string,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created,\n * so the tree mirrors exactly what the provider's source delivers.\n *\n * @param accountId the id-safe account id (device node)\n * @param accountName the display name for the device object\n * @param provider the provider kind (device object name suffix)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(\n accountId: string,\n accountName: string,\n provider: string,\n snapshot: UsageSnapshot,\n): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: string): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n\n objects.push({ id: accountId, type: \"device\", common: { name: `${accountName} (${provider})` } });\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, \"Limit windows\");\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n channel(`${accountId}.limits.${windowId}`, limit.label);\n add(\n state(`${accountId}.limits.${windowId}.percent`, `${limit.label} used`, \"number\", \"value\", limit.percent, \"%\"),\n );\n if (limit.resetAt !== undefined) {\n add(\n state(`${accountId}.limits.${windowId}.resetAt`, `${limit.label} resets at`, \"string\", \"date\", limit.resetAt),\n );\n }\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, \"Credits\");\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, \"Credits used\", \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, \"Credits limit\", \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(state(`${accountId}.credits.remaining`, \"Credits remaining\", \"number\", \"value\", credits.remaining, unit));\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, \"Credits used (percent)\", \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(state(`${accountId}.credits.granted`, \"Granted balance\", \"number\", \"value\", credits.granted, unit));\n }\n if (credits.toppedUp !== undefined) {\n add(state(`${accountId}.credits.toppedUp`, \"Topped-up balance\", \"number\", \"value\", credits.toppedUp, unit));\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, \"Costs\");\n if (costs.today !== undefined) {\n add(state(`${accountId}.costs.today`, \"Costs today\", \"number\", \"value\", costs.today, costs.currency));\n }\n if (costs.month !== undefined) {\n add(state(`${accountId}.costs.month`, \"Costs this month\", \"number\", \"value\", costs.month, costs.currency));\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, \"Costs total (lifetime)\", \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n \"Costs projected month-end (computed)\",\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, \"Tokens\");\n if (tokens.inputToday !== undefined) {\n add(state(`${accountId}.tokens.inputToday`, \"Input tokens today\", \"number\", \"value\", tokens.inputToday));\n }\n if (tokens.outputToday !== undefined) {\n add(state(`${accountId}.tokens.outputToday`, \"Output tokens today\", \"number\", \"value\", tokens.outputToday));\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, \"Per-model usage\");\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n channel(`${accountId}.models.${modelId}`, model.model);\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n `${model.model} tokens today`,\n \"number\",\n \"value\",\n model.tokens,\n ),\n );\n }\n if (model.cost !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.costToday`,\n `${model.model} costs today`,\n \"number\",\n \"value\",\n model.cost,\n snapshot.costs?.currency ?? \"USD\",\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n add(state(`${accountId}.available`, \"Balance sufficient for calls\", \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * The window that decides how full an account is: the highest PLAN-WIDE limit, or\n * the granted budget when the account has no windows at all.\n *\n * Windows marked `scoped` are left out on purpose \u2014 they cover a single model and\n * may sit at 100 % permanently for a model the user never touches, which would\n * pin the account's warning on forever. Their datapoints still exist; they just\n * do not speak for the account.\n *\n * @param snapshot the snapshot\n * @returns percent plus the label that produced it, or undefined when nothing applies\n */\nexport function limitingWindow(snapshot: UsageSnapshot): { percent: number; label: string } | undefined {\n let best: { percent: number; label: string } | undefined;\n for (const limit of snapshot.limits ?? []) {\n if (limit.scoped) {\n continue;\n }\n if (!best || limit.percent > best.percent) {\n best = { percent: limit.percent, label: limit.label };\n }\n }\n const credits = snapshot.credits?.percent;\n if (credits !== undefined && (!best || credits > best.percent)) {\n best = { percent: credits, label: \"Credits\" };\n }\n return best;\n}\n\n/**\n * The highest plan-wide limit percent in a snapshot, or undefined when it has none.\n *\n * @param snapshot the snapshot\n * @returns the maximum percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n return limitingWindow(snapshot)?.percent;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
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,6 +431,10 @@ 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(() => {
|
|
@@ -392,15 +457,12 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
392
457
|
warn: (m) => this.log.warn(m),
|
|
393
458
|
error: (m) => this.log.error(m)
|
|
394
459
|
},
|
|
460
|
+
afterFirstRound: () => this.logDatapointBalance(),
|
|
395
461
|
notify: this.config.notifications ? (_account, message) => void this.registerNotification("ai-usage", "userActionRequired", message).catch(
|
|
396
462
|
(e) => this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`)
|
|
397
463
|
) : void 0
|
|
398
464
|
});
|
|
399
465
|
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
466
|
this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);
|
|
405
467
|
} catch (e) {
|
|
406
468
|
this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -497,6 +559,7 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
497
559
|
if (await this.getObjectAsync(id)) {
|
|
498
560
|
await this.delObjectAsync(id);
|
|
499
561
|
removed++;
|
|
562
|
+
this.knownStateIds.delete(id);
|
|
500
563
|
}
|
|
501
564
|
} catch (e) {
|
|
502
565
|
this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -529,6 +592,12 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
529
592
|
this.log.info(
|
|
530
593
|
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
594
|
);
|
|
595
|
+
for (const id of [...this.knownStateIds]) {
|
|
596
|
+
if (id === root || id.startsWith(`${root}.`)) {
|
|
597
|
+
this.knownStateIds.delete(id);
|
|
598
|
+
this.removedStates++;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
532
601
|
await this.delObjectAsync(root, { recursive: true });
|
|
533
602
|
}
|
|
534
603
|
} 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 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.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.7.0": {
|
|
7
|
+
"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",
|
|
8
|
+
"de": "Nach einer Änderung sagt das Protokoll in einer Zeile, wie viele Datenpunkte dazugekommen und weggefallen sind — kein Durchklicken des Objektbaums nötig",
|
|
9
|
+
"ru": "После изменения журнал одной строкой сообщает, сколько точек данных добавилось и исчезло — не нужно просматривать дерево объектов",
|
|
10
|
+
"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",
|
|
11
|
+
"nl": "Na een wijziging meldt het logboek in één regel hoeveel datapunten erbij kwamen en verdwenen — zonder de objectboom door te klikken",
|
|
12
|
+
"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",
|
|
13
|
+
"it": "Dopo una modifica il log indica in una riga quanti punti dati sono stati creati e rimossi — senza scorrere l'albero degli oggetti",
|
|
14
|
+
"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",
|
|
15
|
+
"pl": "Po zmianie dziennik podaje w jednej linii, ile punktów danych przybyło i ubyło — bez przeglądania drzewa obiektów",
|
|
16
|
+
"uk": "Після зміни журнал одним рядком повідомляє, скільки точок даних додалося та зникло — без перегляду дерева об'єктів",
|
|
17
|
+
"zh-cn": "更改后,日志用一行说明对象树新增和移除了多少数据点,无需逐层点开查看"
|
|
18
|
+
},
|
|
19
|
+
"0.6.0": {
|
|
20
|
+
"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",
|
|
21
|
+
"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",
|
|
22
|
+
"ru": "Каждый аккаунт теперь показывает значок соединения в дереве объектов — зелёный, пока данные идут, перечёркнутый, когда нет, как у любого устройства ioBroker",
|
|
23
|
+
"pt": "Cada conta mostra agora o ícone de ligação na árvore de objetos — verde enquanto entrega dados, cortado quando não, como qualquer dispositivo ioBroker",
|
|
24
|
+
"nl": "Elk account toont nu het verbindingspictogram in de objectboom — groen zolang er data komt, doorgestreept als dat niet zo is, net als elk ioBroker-apparaat",
|
|
25
|
+
"fr": "Chaque compte affiche désormais l'icône de connexion dans l'arbre des objets — verte tant qu'il fournit des données, barrée sinon, comme tout appareil ioBroker",
|
|
26
|
+
"it": "Ogni account mostra ora l'icona di connessione nell'albero degli oggetti — verde finché fornisce dati, barrata quando no, come qualsiasi dispositivo ioBroker",
|
|
27
|
+
"es": "Cada cuenta muestra ahora el icono de conexión en el árbol de objetos — verde mientras entrega datos, tachado cuando no, como cualquier dispositivo de ioBroker",
|
|
28
|
+
"pl": "Każde konto pokazuje teraz ikonę połączenia w drzewie obiektów — zieloną, dopóki dostarcza dane, przekreśloną gdy nie, jak każde urządzenie ioBroker",
|
|
29
|
+
"uk": "Кожен акаунт тепер має піктограму з’єднання в дереві об’єктів — зелену, поки дані надходять, перекреслену, коли ні, як у будь-якого пристрою ioBroker",
|
|
30
|
+
"zh-cn": "每个账户现在都会在对象树中显示连接图标——有数据时为绿色,无数据时带斜杠,与其他 ioBroker 设备一致"
|
|
31
|
+
},
|
|
6
32
|
"0.5.0": {
|
|
7
33
|
"en": "Two status datapoints per account instead of six: an offline marker ioBroker actually shows plus the reason in plain text; the retired ones are removed automatically",
|
|
8
34
|
"de": "Zwei Status-Datenpunkte je Konto statt sechs: ein Offline-Kennzeichen, das ioBroker wirklich anzeigt, plus den Grund im Klartext; die alten werden automatisch entfernt",
|