iobroker.govee-smart 2.5.3 → 2.5.4

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
@@ -124,6 +124,10 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
124
  ---
125
125
 
126
126
  ## Changelog
127
+ ### 2.5.4 (2026-05-04)
128
+
129
+ - Test-Coverage erweitert für den MQTT-Login-Pfad: `mqtt.connect` ist jetzt als optionaler Constructor-Parameter injizierbar (analog `httpsRequest` in v2.5.1) und 7 neue Mock-Tests decken die getIotKey-Authentifizierung und den persistierten-Credentials-Reuse-Pfad ab.
130
+
127
131
  ### 2.5.3 (2026-05-04)
128
132
 
129
133
  - Segment-Erkennungs-Wizard: kein „has no existing object"-Spam mehr für Indizes oberhalb der echten Strip-Länge — Echo-Pakete werden defensiv gegen `segmentCount` gefiltert (Issue #8).
@@ -142,10 +146,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
142
146
 
143
147
  - F4 final: `onMessage`-Handler (sendTo aus dem Admin-UI) ist jetzt eine eigene Klasse mit Host-Interface. main.ts deutlich kleiner, Login-Test/2FA-Code-Anforderung isoliert testbar. Verhalten identisch.
144
148
 
145
- ### 2.4.1 (2026-05-04)
146
-
147
- - Group-Fan-Out-Pfad (Mitglieder-Steuerung beim Schalten der Gruppe) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` nochmal kleiner. Verhalten identisch.
148
-
149
149
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
150
150
 
151
151
  ## Support
@@ -66,6 +66,7 @@ class GoveeMqttClient {
66
66
  log;
67
67
  timers;
68
68
  httpsRequestImpl;
69
+ mqttConnectImpl;
69
70
  client = null;
70
71
  accountTopic = "";
71
72
  _bearerToken = "";
@@ -109,13 +110,15 @@ class GoveeMqttClient {
109
110
  * @param log ioBroker logger
110
111
  * @param timers Timer adapter
111
112
  * @param httpsRequestImpl optional DI für Tests — Default ist die echte httpsRequest
113
+ * @param mqttConnectImpl optional DI für Tests — Default ist die echte mqtt.connect
112
114
  */
113
- constructor(email, password, log, timers, httpsRequestImpl = import_http_client.httpsRequest) {
115
+ constructor(email, password, log, timers, httpsRequestImpl = import_http_client.httpsRequest, mqttConnectImpl = mqtt.connect) {
114
116
  this.email = email;
115
117
  this.password = password;
116
118
  this.log = log;
117
119
  this.timers = timers;
118
120
  this.httpsRequestImpl = httpsRequestImpl;
121
+ this.mqttConnectImpl = mqttConnectImpl;
119
122
  this.clientId = (0, import_govee_constants.deriveGoveeClientId)(email);
120
123
  }
121
124
  /**
@@ -288,7 +291,7 @@ class GoveeMqttClient {
288
291
  });
289
292
  this.scheduleProactiveRefresh(expiresAt);
290
293
  const clientId = `AP/${this.accountId}/${this.sessionUuid}`;
291
- this.client = mqtt.connect(`mqtts://${endpoint}:8883`, {
294
+ this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {
292
295
  clientId,
293
296
  key,
294
297
  cert,
@@ -463,7 +466,7 @@ class GoveeMqttClient {
463
466
  const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;
464
467
  this.log.debug("MQTT: trying cached credentials (no fresh login)");
465
468
  this.persistedAttemptInFlight = true;
466
- this.client = mqtt.connect(`mqtts://${creds.iotEndpoint}:8883`, {
469
+ this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {
467
470
  clientId,
468
471
  key: extracted.key,
469
472
  cert: extracted.cert,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/govee-mqtt-client.ts"],
4
- "sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to clear it\n if (codeWasSent) {\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = mqtt.connect(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings\");\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: verification code rejected \u2014 request a fresh code\");\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\"MQTT not connected: login rejected \u2014 check email/password\");\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = mqtt.connect(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to clear it\n if (codeWasSent) {\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings\");\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: verification code rejected \u2014 request a fresh code\");\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\"MQTT not connected: login rejected \u2014 check email/password\");\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.5.3",
4
+ "version": "2.5.4",
5
5
  "news": {
6
+ "2.5.4": {
7
+ "en": "Test-coverage expansion: mqtt.connect is now an injectable constructor parameter (like httpsRequest in v2.5.1), and 7 new mock tests cover the getIotKey path and persisted-credentials reuse.",
8
+ "de": "Test-Coverage erweitert: mqtt.connect ist jetzt als Konstruktor-Parameter injizierbar (wie httpsRequest in v2.5.1), 7 neue Mock-Tests decken getIotKey und Persisted-Credentials-Reuse ab.",
9
+ "ru": "Расширено покрытие тестами: mqtt.connect теперь принимается как параметр конструктора (как httpsRequest в v2.5.1), 7 новых mock-тестов покрывают getIotKey и persisted-credentials reuse.",
10
+ "pt": "Mais cobertura de testes: mqtt.connect agora e parametro injetavel do construtor (como httpsRequest em v2.5.1), 7 novos mock tests cobrem getIotKey e reuse das credenciais persistidas.",
11
+ "nl": "Test-dekking uitgebreid: mqtt.connect is nu een injecteerbare constructor-parameter (zoals httpsRequest in v2.5.1), 7 nieuwe mock-tests dekken getIotKey en persisted-credentials reuse.",
12
+ "fr": "Couverture de tests etendue : mqtt.connect est maintenant un parametre injectable du constructeur (comme httpsRequest en v2.5.1), 7 nouveaux mock tests couvrent getIotKey et la reutilisation des credentials persistes.",
13
+ "it": "Coverage di test ampliato: mqtt.connect ora e parametro iniettabile del costruttore (come httpsRequest in v2.5.1), 7 nuovi mock test coprono getIotKey e il riuso delle credenziali persistite.",
14
+ "es": "Cobertura de tests ampliada: mqtt.connect ahora es parametro inyectable del constructor (como httpsRequest en v2.5.1), 7 nuevos mock tests cubren getIotKey y la reutilizacion de credenciales persistidas.",
15
+ "pl": "Rozszerzone pokrycie testami: mqtt.connect jest teraz wstrzykiwanym parametrem konstruktora (jak httpsRequest w v2.5.1), 7 nowych mock-testow obejmuje getIotKey i reuse przechowanych poswiadczen.",
16
+ "uk": "Розширене покриття тестами: mqtt.connect тепер інжектована параметром конструктора (як httpsRequest у v2.5.1), 7 нових mock-тестів покривають getIotKey і повторне використання збережених credentials.",
17
+ "zh-cn": "测试覆盖扩展:mqtt.connect 现可作为构造器参数注入(与 v2.5.1 的 httpsRequest 一致),7 个新 mock 测试覆盖 getIotKey 路径和持久化凭据复用。"
18
+ },
6
19
  "2.5.3": {
7
20
  "en": "Segment-detection wizard no longer spams 'has no existing object' WARN for indices above the real strip length. Plus: command before cloud-init is silent now (not 'No channel available').",
8
21
  "de": "Segment-Erkennungs-Wizard spammt nicht mehr 'has no existing object' fuer Indizes oberhalb der echten Strip-Laenge. Plus: Befehl vor Cloud-Init ist still (nicht 'No channel available').",
@@ -80,19 +93,6 @@
80
93
  "pl": "Higiena kodu: lokalny snapshot-manager zyskal wlasna klase z host-interface. main.ts maleje; zachowanie bez zmian.",
81
94
  "uk": "Гігієна коду: локальний snapshot-менеджер тепер живе у власному класі з host-інтерфейсом. main.ts зменшується; поведінка без змін.",
82
95
  "zh-cn": "代码卫生:本地 snapshot 管理器现已拆分为独立类,使用 host 接口。main.ts 缩小;行为不变。"
83
- },
84
- "2.3.1": {
85
- "en": "Test coverage: smoke tests for GoveeCloudClient + GoveeMqttClient. Initial-state and setter checks now in place; full mock-based test paths come in a later release.",
86
- "de": "Test-Coverage: Smoke-Tests für GoveeCloudClient + GoveeMqttClient. Initial-State + Setter-Checks drin; volle Mock-basierte Test-Pfade kommen separat.",
87
- "ru": "Покрытие тестами: smoke-тесты для GoveeCloudClient + GoveeMqttClient. Initial-state и setter-проверки добавлены; полные mock-пути позже.",
88
- "pt": "Cobertura de testes: smoke tests para GoveeCloudClient + GoveeMqttClient. Initial-state e setter-checks adicionados; caminhos completos com mock serao em release separado.",
89
- "nl": "Test-coverage: smoke tests voor GoveeCloudClient + GoveeMqttClient. Initial-state en setter-checks toegevoegd; volle mock-paths komen apart.",
90
- "fr": "Couverture de tests : smoke tests pour GoveeCloudClient + GoveeMqttClient. Initial-state et setter-checks ajoutes; chemins complets avec mocks plus tard.",
91
- "it": "Test coverage: smoke tests per GoveeCloudClient + GoveeMqttClient. Controlli initial-state e setter aggiunti; percorsi completi con mock separati.",
92
- "es": "Cobertura de tests: smoke tests para GoveeCloudClient + GoveeMqttClient. Initial-state y setter-checks agregados; rutas completas con mocks por separado.",
93
- "pl": "Pokrycie testami: smoke testy dla GoveeCloudClient + GoveeMqttClient. Initial-state i setter-checks dodane; pelne sciezki mock w osobnym release.",
94
- "uk": "Покриття тестами: smoke-тести для GoveeCloudClient + GoveeMqttClient. Initial-state та setter-перевірки додані; повні mock-шляхи окремо.",
95
- "zh-cn": "测试覆盖:GoveeCloudClient + GoveeMqttClient 的 smoke 测试。初始状态和 setter 检查已加入;完整 mock 路径将在后续发布。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.govee-smart",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",