iobroker.ai-usage 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -126,6 +126,10 @@ After a successful sign-in the account is queried immediately, so values appear
126
126
  Placeholder for the next version (at the beginning of the line):
127
127
  ### **WORK IN PROGRESS**
128
128
  -->
129
+ ### 0.9.1 (2026-08-27)
130
+
131
+ - Changed: While an account has nothing to report — the adapter switched off, or started and not asked yet — the reason now reads "Unknown" instead of a sentence about the adapter
132
+
129
133
  ### 0.9.0 (2026-08-27)
130
134
 
131
135
  - Fixed: Switching the instance off now shows every account as offline in the object tree and the settings, instead of leaving them green for as long as the adapter is not running
@@ -153,10 +157,6 @@ After a successful sign-in the account is queried immediately, so values appear
153
157
  - 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
154
158
  - Fixed: Datapoints that only repeat their previous value are no longer rewritten every cycle, which kept flooding the history of anyone recording them
155
159
 
156
- ### 0.6.0 (2026-08-26)
157
-
158
- - 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
159
-
160
160
  [Older changelogs can be found there](CHANGELOG_OLD.md)
161
161
 
162
162
  ## Support
@@ -28,6 +28,7 @@ const MAX_NETWORK_FAILURES = 3;
28
28
  const BACKOFF_START_MS = 10 * 60 * 1e3;
29
29
  const BACKOFF_MAX_MS = 60 * 60 * 1e3;
30
30
  const STAGGER_MS = 3e3;
31
+ const REASON_UNKNOWN = "Unknown";
31
32
  class PollEngine {
32
33
  /**
33
34
  * @param accounts the validated account configs
@@ -55,7 +56,9 @@ class PollEngine {
55
56
  authNotified: false,
56
57
  serviceOnline: false,
57
58
  state: "no-connection",
58
- error: "waiting for the first query",
59
+ // Nothing known until the service says something; the skeleton writes
60
+ // REASON_UNKNOWN, and the first answer replaces it.
61
+ error: REASON_UNKNOWN,
59
62
  createdObjects: /* @__PURE__ */ new Set(),
60
63
  firstPollDone: false,
61
64
  polling: false,
@@ -116,6 +119,11 @@ class PollEngine {
116
119
  * and the badge in the settings, and on its last value it stays green for as long
117
120
  * as the instance is switched off.
118
121
  *
122
+ * `info.error` is deliberately NOT touched. That datapoint answers "what did the AI
123
+ * service say", and the adapter being switched off is not something the service
124
+ * said — a sentence about our own operating state in there reads as if the provider
125
+ * had reported it. Whatever the last real reason was stays readable.
126
+ *
119
127
  * The returned promise is what makes this WORK. Measured on the live server
120
128
  * 2026-08-27: issued fire-and-forget and followed by an immediate `callback()`,
121
129
  * not one of these writes ever reached the database — the process was gone first.
@@ -125,13 +133,10 @@ class PollEngine {
125
133
  * @returns resolves once every write has been acknowledged
126
134
  */
127
135
  async markAllOffline() {
128
- const writes = [];
129
- for (const runtime of this.runtimes) {
130
- writes.push(this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true));
131
- writes.push(
132
- this.deps.setStateChanged(`${runtime.config.id}.info.error`, "The adapter is stopped \u2014 nothing is being read")
133
- );
134
- }
136
+ const writes = this.runtimes.flatMap((runtime) => [
137
+ this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true),
138
+ this.deps.setStateChanged(`${runtime.config.id}.info.error`, REASON_UNKNOWN)
139
+ ]);
135
140
  writes.push(this.deps.setStateChanged("total.accountsReachable", 0));
136
141
  writes.push(this.deps.setStateChanged("info.connection", false));
137
142
  await Promise.all(writes);
@@ -458,7 +463,8 @@ class PollEngine {
458
463
  runtime.createdObjects.add(def.id);
459
464
  }
460
465
  runtime.staticIds = defs.filter((def) => def.type === "state").map((def) => def.id);
461
- void this.writeAccountStatus(runtime);
466
+ void this.deps.setStateChanged(`${config.id}.info.unreach`, true);
467
+ void this.deps.setStateChanged(`${config.id}.info.error`, REASON_UNKNOWN);
462
468
  }
463
469
  /** The totals skeleton (channel + states). */
464
470
  async createTotalsSkeleton() {
@@ -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, orphanObjectIds, 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 /** Delete an object and everything below it. */\n deleteObject(id: string): Promise<void>;\n /** Every state id that currently exists below `prefix` (relative to the instance). */\n listStateIds(prefix: string): Promise<string[]>;\n /** Write a state value with ack \u2014 for MEASUREMENTS, where every cycle carries information. */\n setState(id: string, value: boolean | number | string): void;\n /**\n * Write a state only when the value differs from what the database holds \u2014 for\n * INDICATORS. js-controller does the comparison (`setStateChangedAsync`), which is\n * what the rest of the fleet uses; a hand-rolled cache would only know what this\n * process wrote and would still write blindly after a restart.\n *\n * Returns a promise so the shutdown path can WAIT for its writes; everywhere else\n * it is deliberately ignored \u2014 a poll cycle must not be held up by the database.\n */\n setStateChanged(id: string, value: boolean | number | string): Promise<void>;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n /**\n * Called once, when every account has finished its FIRST poll.\n *\n * The first round is staggered on purpose, so the adapter cannot report what the\n * object tree gained until the last account has been through. A config change\n * restarts the instance, so this is also the only moment a user needs the report.\n */\n afterFirstRound?(): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Whether this account has been through its first poll. */\n firstPollDone: boolean;\n /** True while a poll of this account is in flight \u2014 a second one must not overlap. */\n polling: boolean;\n /** Set when a poll was requested while one was running; runs once the current one ends. */\n pollAgain: boolean;\n /**\n * The dynamic state ids the last snapshot delivered, or null until the first\n * reconcile \u2014 which reads the database, so a datapoint that vanished while the\n * adapter was stopped is caught too.\n */\n deliveredIds: string[] | null;\n /** The skeleton's own state ids \u2014 they never expire. */\n staticIds: 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 private firstRoundReported = false;\n /**\n * How many accounts the user switched on \u2014 including those the adapter cannot\n * poll because their credential is missing. `total.accounts` is what the user\n * configured, not what happened to work out.\n */\n private readonly configuredAccounts: number;\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 this.configuredAccounts = accounts.length;\n for (const config of accounts) {\n const provider = providers.get(config.id);\n if (!provider) {\n deps.log.warn(`${config.name}: provider \"${config.provider}\" is not available \u2014 account skipped`);\n continue;\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n serviceOnline: false,\n state: \"no-connection\",\n error: \"waiting for the first query\",\n createdObjects: new Set(),\n firstPollDone: false,\n polling: false,\n pollAgain: false,\n deliveredIds: null,\n staticIds: [],\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 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 // The repeating timer is armed INSIDE the staggered first poll, not next to\n // it: armed here it would start counting for every account in the same\n // instant, and from the second round on all of them would fire together \u2014\n // exactly the burst the stagger exists to prevent, against providers that\n // answer a burst by locking the whole account for a day.\n this.handles.push(\n this.deps.scheduleOnce(() => {\n if (this.stopped) {\n return;\n }\n this.handles.push(this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000));\n void this.pollAccount(runtime);\n }, index * STAGGER_MS),\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 /**\n * Say that no account is delivering any more \u2014 for shutdown.\n *\n * A stopped adapter reads nothing, so it must not leave every account claiming to\n * be online: `info.unreach` is what colours the account in the admin's object tree\n * and the badge in the settings, and on its last value it stays green for as long\n * as the instance is switched off.\n *\n * The returned promise is what makes this WORK. Measured on the live server\n * 2026-08-27: issued fire-and-forget and followed by an immediate `callback()`,\n * not one of these writes ever reached the database \u2014 the process was gone first.\n * The caller has to wait for this (with its own time limit) before saying it is\n * done.\n *\n * @returns resolves once every write has been acknowledged\n */\n public async markAllOffline(): Promise<void> {\n const writes: Promise<void>[] = [];\n for (const runtime of this.runtimes) {\n writes.push(this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true));\n writes.push(\n this.deps.setStateChanged(`${runtime.config.id}.info.error`, \"The adapter is stopped \u2014 nothing is being read\"),\n );\n }\n // The same lie one level up: \"accounts currently delivering data\" is zero.\n writes.push(this.deps.setStateChanged(\"total.accountsReachable\", 0));\n writes.push(this.deps.setStateChanged(\"info.connection\", false));\n await Promise.all(writes);\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 * Never two at once for the same account: a sign-in triggers an immediate poll,\n * which can land on top of a scheduled one \u2014 and two token refreshes in parallel\n * on a rotating refresh token sign each other out. A request that arrives while\n * one is running is remembered and runs right after, so nothing is lost.\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 if (runtime.polling) {\n runtime.pollAgain = true;\n return;\n }\n runtime.polling = true;\n try {\n await this.pollOnce(runtime);\n } finally {\n runtime.polling = false;\n }\n if (runtime.pollAgain && !this.stopped) {\n runtime.pollAgain = false;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * One poll of one account: fetch, classify, write.\n *\n * @param runtime the account's runtime\n */\n private async pollOnce(runtime: AccountRuntime): Promise<void> {\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 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, 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 await this.removeVanished(\n runtime,\n writes.map(write => write.id),\n );\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26).\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n // Indicators go through the changed-write, measurements through the normal one.\n void this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);\n void this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Delete what this account no longer delivers.\n *\n * The first round after a start compares against the DATABASE, so a window or\n * model that disappeared while the adapter was stopped is caught as well; every\n * round after that compares against the previous snapshot, which costs nothing.\n *\n * @param runtime the account's runtime\n * @param delivered the state ids this snapshot wrote\n */\n private async removeVanished(runtime: AccountRuntime, delivered: string[]): Promise<void> {\n const known = runtime.deliveredIds ?? (await this.deps.listStateIds(runtime.config.id));\n for (const id of orphanObjectIds(known, delivered, runtime.staticIds)) {\n await this.deps.deleteObject(id);\n runtime.createdObjects.delete(id);\n this.deps.log.info(`${runtime.config.name}: removed \"${id}\" \u2014 the provider no longer reports it`);\n }\n runtime.deliveredIds = delivered;\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 void this.writeAccountStatus(runtime);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both go through the changed-write: an indicator rewritten every\n * cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private async writeAccountStatus(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const delivering = runtime.state === \"ok\" || runtime.state === \"rate-limited\";\n await Promise.all([\n this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering),\n this.deps.setStateChanged(`${config.id}.info.error`, runtime.error),\n ]);\n }\n\n /** Recompute and write the totals + info.connection. */\n private writeTotals(): void {\n const totals = computeTotals(\n this.runtimes.map(runtime => runtime.status),\n this.configuredAccounts,\n );\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 void this.deps.setStateChanged(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n // The configured count only ever changes with the configuration, which restarts\n // the instance \u2014 rewriting it every cycle would be pure noise in a recording.\n void this.deps.setStateChanged(\"total.accounts\", totals.accounts);\n void this.deps.setStateChanged(\"info.connection\", totals.accountsReachable > 0);\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 runtime.staticIds = defs.filter(def => def.type === \"state\").map(def => def.id);\n // Mark it as not delivering right away, before anything has been asked.\n //\n // This looks pessimistic and is the honest state: nothing has been read yet. It\n // also carries the whole weight of \"the instance was off\". Whatever the previous\n // run left behind stands until someone overwrites it \u2014 after a hard kill, after\n // a crash, after an unclean shutdown the account would otherwise sit there green\n // and claim to deliver while no process exists at all. nut2 marks its devices\n // unreachable on start for exactly this reason.\n //\n // The window is short: the first poll follows within seconds and writes the real\n // state, success or failure.\n void 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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",
4
+ "sourcesContent": ["import type { AccountConfig } from \"./pure-helpers\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { limitingWindow, mapSnapshot, orphanObjectIds, 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/**\n * What `info.error` says while the adapter itself has nothing to report: switched\n * off, or started and not asked yet.\n *\n * ONE wording for the whole fleet (krobi 2026-08-27) \u2014 the datapoint otherwise ends\n * up saying something different in every adapter. It stays a single word: the field\n * names the reason, it does not explain itself.\n */\nconst REASON_UNKNOWN = \"Unknown\";\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 /** Delete an object and everything below it. */\n deleteObject(id: string): Promise<void>;\n /** Every state id that currently exists below `prefix` (relative to the instance). */\n listStateIds(prefix: string): Promise<string[]>;\n /** Write a state value with ack \u2014 for MEASUREMENTS, where every cycle carries information. */\n setState(id: string, value: boolean | number | string): void;\n /**\n * Write a state only when the value differs from what the database holds \u2014 for\n * INDICATORS. js-controller does the comparison (`setStateChangedAsync`), which is\n * what the rest of the fleet uses; a hand-rolled cache would only know what this\n * process wrote and would still write blindly after a restart.\n *\n * Returns a promise so the shutdown path can WAIT for its writes; everywhere else\n * it is deliberately ignored \u2014 a poll cycle must not be held up by the database.\n */\n setStateChanged(id: string, value: boolean | number | string): Promise<void>;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n /**\n * Called once, when every account has finished its FIRST poll.\n *\n * The first round is staggered on purpose, so the adapter cannot report what the\n * object tree gained until the last account has been through. A config change\n * restarts the instance, so this is also the only moment a user needs the report.\n */\n afterFirstRound?(): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Whether this account has been through its first poll. */\n firstPollDone: boolean;\n /** True while a poll of this account is in flight \u2014 a second one must not overlap. */\n polling: boolean;\n /** Set when a poll was requested while one was running; runs once the current one ends. */\n pollAgain: boolean;\n /**\n * The dynamic state ids the last snapshot delivered, or null until the first\n * reconcile \u2014 which reads the database, so a datapoint that vanished while the\n * adapter was stopped is caught too.\n */\n deliveredIds: string[] | null;\n /** The skeleton's own state ids \u2014 they never expire. */\n staticIds: 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 private firstRoundReported = false;\n /**\n * How many accounts the user switched on \u2014 including those the adapter cannot\n * poll because their credential is missing. `total.accounts` is what the user\n * configured, not what happened to work out.\n */\n private readonly configuredAccounts: number;\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 this.configuredAccounts = accounts.length;\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 // Nothing known until the service says something; the skeleton writes\n // REASON_UNKNOWN, and the first answer replaces it.\n error: REASON_UNKNOWN,\n createdObjects: new Set(),\n firstPollDone: false,\n polling: false,\n pollAgain: false,\n deliveredIds: null,\n staticIds: [],\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 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 // The repeating timer is armed INSIDE the staggered first poll, not next to\n // it: armed here it would start counting for every account in the same\n // instant, and from the second round on all of them would fire together \u2014\n // exactly the burst the stagger exists to prevent, against providers that\n // answer a burst by locking the whole account for a day.\n this.handles.push(\n this.deps.scheduleOnce(() => {\n if (this.stopped) {\n return;\n }\n this.handles.push(this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000));\n void this.pollAccount(runtime);\n }, index * STAGGER_MS),\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 /**\n * Say that no account is delivering any more \u2014 for shutdown.\n *\n * A stopped adapter reads nothing, so it must not leave every account claiming to\n * be online: `info.unreach` is what colours the account in the admin's object tree\n * and the badge in the settings, and on its last value it stays green for as long\n * as the instance is switched off.\n *\n * `info.error` is deliberately NOT touched. That datapoint answers \"what did the AI\n * service say\", and the adapter being switched off is not something the service\n * said \u2014 a sentence about our own operating state in there reads as if the provider\n * had reported it. Whatever the last real reason was stays readable.\n *\n * The returned promise is what makes this WORK. Measured on the live server\n * 2026-08-27: issued fire-and-forget and followed by an immediate `callback()`,\n * not one of these writes ever reached the database \u2014 the process was gone first.\n * The caller has to wait for this (with its own time limit) before saying it is\n * done.\n *\n * @returns resolves once every write has been acknowledged\n */\n public async markAllOffline(): Promise<void> {\n const writes = this.runtimes.flatMap(runtime => [\n this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true),\n this.deps.setStateChanged(`${runtime.config.id}.info.error`, REASON_UNKNOWN),\n ]);\n // The same lie one level up: \"accounts currently delivering data\" is zero.\n writes.push(this.deps.setStateChanged(\"total.accountsReachable\", 0));\n writes.push(this.deps.setStateChanged(\"info.connection\", false));\n await Promise.all(writes);\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 * Never two at once for the same account: a sign-in triggers an immediate poll,\n * which can land on top of a scheduled one \u2014 and two token refreshes in parallel\n * on a rotating refresh token sign each other out. A request that arrives while\n * one is running is remembered and runs right after, so nothing is lost.\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 if (runtime.polling) {\n runtime.pollAgain = true;\n return;\n }\n runtime.polling = true;\n try {\n await this.pollOnce(runtime);\n } finally {\n runtime.polling = false;\n }\n if (runtime.pollAgain && !this.stopped) {\n runtime.pollAgain = false;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * One poll of one account: fetch, classify, write.\n *\n * @param runtime the account's runtime\n */\n private async pollOnce(runtime: AccountRuntime): Promise<void> {\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 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, 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 await this.removeVanished(\n runtime,\n writes.map(write => write.id),\n );\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26).\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n // Indicators go through the changed-write, measurements through the normal one.\n void this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);\n void this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Delete what this account no longer delivers.\n *\n * The first round after a start compares against the DATABASE, so a window or\n * model that disappeared while the adapter was stopped is caught as well; every\n * round after that compares against the previous snapshot, which costs nothing.\n *\n * @param runtime the account's runtime\n * @param delivered the state ids this snapshot wrote\n */\n private async removeVanished(runtime: AccountRuntime, delivered: string[]): Promise<void> {\n const known = runtime.deliveredIds ?? (await this.deps.listStateIds(runtime.config.id));\n for (const id of orphanObjectIds(known, delivered, runtime.staticIds)) {\n await this.deps.deleteObject(id);\n runtime.createdObjects.delete(id);\n this.deps.log.info(`${runtime.config.name}: removed \"${id}\" \u2014 the provider no longer reports it`);\n }\n runtime.deliveredIds = delivered;\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 void this.writeAccountStatus(runtime);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both go through the changed-write: an indicator rewritten every\n * cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private async writeAccountStatus(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const delivering = runtime.state === \"ok\" || runtime.state === \"rate-limited\";\n await Promise.all([\n this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering),\n this.deps.setStateChanged(`${config.id}.info.error`, runtime.error),\n ]);\n }\n\n /** Recompute and write the totals + info.connection. */\n private writeTotals(): void {\n const totals = computeTotals(\n this.runtimes.map(runtime => runtime.status),\n this.configuredAccounts,\n );\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 void this.deps.setStateChanged(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n // The configured count only ever changes with the configuration, which restarts\n // the instance \u2014 rewriting it every cycle would be pure noise in a recording.\n void this.deps.setStateChanged(\"total.accounts\", totals.accounts);\n void this.deps.setStateChanged(\"info.connection\", totals.accountsReachable > 0);\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 runtime.staticIds = defs.filter(def => def.type === \"state\").map(def => def.id);\n // Mark it as not delivering right away, before anything has been asked.\n //\n // This looks pessimistic and is the honest state: nothing has been read yet. It\n // also carries the whole weight of \"the instance was off\". Whatever the previous\n // run left behind stands until someone overwrites it \u2014 after a hard kill, after\n // a crash, after an unclean shutdown the account would otherwise sit there green\n // and claim to deliver while no process exists at all. nut2 marks its devices\n // unreachable on start for exactly this reason.\n //\n // The window is short: the first poll follows within seconds and writes the real\n // state, success or failure. Only the marker \u2014 `info.error` belongs to the AI\n // service, and \"we have not asked yet\" is not something it said.\n void this.deps.setStateChanged(`${config.id}.info.unreach`, true);\n void this.deps.setStateChanged(`${config.id}.info.error`, REASON_UNKNOWN);\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
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "ai-usage",
4
- "version": "0.9.0",
4
+ "version": "0.9.1",
5
5
  "news": {
6
+ "0.9.1": {
7
+ "en": "While an account has nothing to report — adapter off, or started and not asked yet — the reason now reads \"Unknown\" instead of a sentence about the adapter",
8
+ "de": "Solange ein Konto nichts zu melden hat — Adapter aus oder gestartet und noch nichts gefragt — steht als Grund jetzt „Unknown\" statt eines Satzes über den Adapter",
9
+ "ru": "Пока по аккаунту нечего сообщить — адаптер выключен или запущен и ещё не опрошен — в причине теперь стоит «Unknown» вместо фразы про адаптер",
10
+ "pt": "Enquanto uma conta não tem nada a comunicar — adaptador desligado ou iniciado e ainda sem consulta — o motivo passa a ser \"Unknown\" em vez de uma frase sobre o adaptador",
11
+ "nl": "Zolang een account niets te melden heeft — adapter uit of gestart en nog niets opgevraagd — staat er als reden nu \"Unknown\" in plaats van een zin over de adapter",
12
+ "fr": "Tant qu'un compte n'a rien à signaler — adaptateur arrêté ou démarré sans interrogation — la raison affiche désormais « Unknown » au lieu d'une phrase sur l'adaptateur",
13
+ "it": "Finché un account non ha nulla da segnalare — adapter spento o avviato senza interrogazione — il motivo indica ora \"Unknown\" invece di una frase sull'adapter",
14
+ "es": "Mientras una cuenta no tenga nada que informar — adaptador apagado o iniciado sin consultar — el motivo muestra ahora \"Unknown\" en vez de una frase sobre el adaptador",
15
+ "pl": "Dopóki konto nie ma nic do zgłoszenia — adapter wyłączony lub uruchomiony i jeszcze nieodpytany — jako powód widnieje teraz „Unknown\" zamiast zdania o adapterze",
16
+ "uk": "Поки обліковий запис не має про що повідомити — адаптер вимкнено або запущено й ще не опитано — як причина тепер стоїть «Unknown» замість фрази про адаптер",
17
+ "zh-cn": "当某个账户暂无可报告的内容时——适配器已停止,或刚启动尚未查询——原因显示为“Unkn”,而不再是一句关于适配器的说明"
18
+ },
6
19
  "0.9.0": {
7
20
  "en": "Stopping the instance now really marks every account as offline, and an account no longer keeps claiming to deliver data after a crash",
8
21
  "de": "Das Stoppen der Instanz kennzeichnet jetzt wirklich jedes Konto als offline, und nach einem Absturz behauptet kein Konto mehr, es liefere Daten",
@@ -80,19 +93,6 @@
80
93
  "pl": "Dwa punkty stanu na konto zamiast sześciu: znacznik offline, który ioBroker naprawdę pokazuje, oraz powód tekstem; stare są usuwane automatycznie",
81
94
  "uk": "Дві точки стану на акаунт замість шести: позначка «не в мережі», яку ioBroker справді показує, і причина текстом; застарілі видаляються автоматично",
82
95
  "zh-cn": "每个账户从六个状态数据点减到两个:ioBroker 真正会显示的离线标记,加上纯文本的原因;旧的会被自动删除"
83
- },
84
- "0.4.0": {
85
- "en": "Limits of a single model no longer report the whole account as full, every account shows whether the AI service itself is online, plus Sentry error reporting and a new icon",
86
- "de": "Limits eines einzelnen Modells melden nicht mehr das ganze Konto als voll, jedes Konto zeigt ob der KI-Dienst selbst online ist, dazu Fehlerberichte über Sentry und ein neues Symbol",
87
- "ru": "Лимиты отдельной модели больше не отмечают весь аккаунт как исчерпанный, каждый аккаунт показывает, онлайн ли сам сервис ИИ, плюс отчёты об ошибках Sentry и новый значок",
88
- "pt": "Limites de um único modelo já não marcam a conta inteira como cheia, cada conta mostra se o próprio serviço de IA está online, mais relatórios de erros Sentry e um novo ícone",
89
- "nl": "Limieten van één model melden niet langer het hele account als vol, elk account toont of de AI-dienst zelf online is, plus foutrapportage via Sentry en een nieuw pictogram",
90
- "fr": "Les limites d'un seul modèle ne marquent plus tout le compte comme plein, chaque compte indique si le service IA est en ligne, plus les rapports d'erreurs Sentry et une nouvelle icône",
91
- "it": "I limiti di un singolo modello non segnalano più l'intero account come pieno, ogni account mostra se il servizio IA è online, più i rapporti di errore Sentry e una nuova icona",
92
- "es": "Los límites de un solo modelo ya no marcan toda la cuenta como llena, cada cuenta muestra si el servicio de IA está en línea, además de informes de errores Sentry y un icono nuevo",
93
- "pl": "Limity pojedynczego modelu nie oznaczają już całego konta jako pełnego, każde konto pokazuje, czy usługa AI jest online, do tego raporty błędów Sentry i nowa ikona",
94
- "uk": "Ліміти окремої моделі більше не позначають весь акаунт як заповнений, кожен акаунт показує, чи сервіс ШІ онлайн, а також звіти про помилки Sentry і нова піктограма",
95
- "zh-cn": "单个模型的限额不再把整个账户标记为已满,每个账户都会显示 AI 服务本身是否在线,另有 Sentry 错误报告和新图标"
96
96
  }
97
97
  },
98
98
  "plugins": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.ai-usage",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "ioBroker adapter monitoring usage, limits and costs of AI accounts (Claude, OpenAI, OpenRouter, DeepSeek)",
5
5
  "author": {
6
6
  "name": "krobi",