iobroker.govee-smart 2.1.1 → 2.1.2

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,12 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
124
  ---
125
125
 
126
126
  ## Changelog
127
+ ### 2.1.2 (2026-05-02)
128
+
129
+ - The verification message no longer claims your account has 2FA when it doesn't. Govee asks for a one-time check the first time it sees this client — same dialog, but the wording matches reality now.
130
+ - Adapters upgrading from v2.1.0 had stored MQTT credentials as plain text by mistake. The corrupted leftover bytes are now cleared on first start, so the verification flow only runs once.
131
+ - New device added to the catalogue: H61D5 (LED Strip).
132
+
127
133
  ### 2.1.1 (2026-05-02)
128
134
 
129
135
  - Security fix: in v2.1.0 your saved MQTT login (token + certificate) was accidentally stored unencrypted. Now actually encrypted at rest as intended.
@@ -152,13 +158,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
152
158
  - Sensor and appliance events (lack-of-water, ice-bucket-full, etc.) now arrive reliably across reconnects. Govee used to treat each reconnect as a new connection and drop the subscription.
153
159
  - Min js-controller `>=7.0.23`.
154
160
 
155
- ### 2.0.1 (2026-04-26)
156
-
157
- - Sensor values and events now land under `sensor/` and `events/` (were both under `control/` in v2.0.0). Removes `no existing object` warnings in the log on first start.
158
- - Snapshots and scenes only attach to lights now — thermometers, heaters and kettles no longer get `snapshot_local` / `snapshot_save` / `snapshot_delete`.
159
- - The `N experimental device(s) detected` boot-time log line is gone. The hint now fires once per lifetime per SKU, only when that SKU actually shows up.
160
- - Less info-level log noise on startup (the routine `OpenAPI MQTT connected` line was removed; recovery messages stay).
161
-
162
161
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
163
162
 
164
163
  ## Support
@@ -264,7 +264,7 @@ class GoveeMqttClient {
264
264
  this.lastErrorCategory = category;
265
265
  if (isNew) {
266
266
  this.log.warn(
267
- "Govee requires 2-Factor verification \u2014 request a code via the adapter settings, paste it into 'mqttVerificationCode' and save"
267
+ 'Govee asked for one-time client verification (HTTP 454). Open adapter settings, click "Request verification code", paste the code from the email into the field, save. Govee remembers this client afterwards. Account-level 2FA is not required.'
268
268
  );
269
269
  } else {
270
270
  this.log.debug("MQTT verification still pending (Govee returned 454 again)");
@@ -279,7 +279,7 @@ class GoveeMqttClient {
279
279
  this.lastErrorCategory = category;
280
280
  if (isNew) {
281
281
  this.log.warn(
282
- "Govee 2-Factor verification code is invalid or expired \u2014 request a fresh code via the adapter settings"
282
+ "Govee rejected the verification code (HTTP 455) \u2014 request a fresh code via the adapter settings."
283
283
  );
284
284
  } else {
285
285
  this.log.debug("MQTT verification code rejected again (Govee returned 455)");
@@ -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 } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\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 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 /** 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 */\n constructor(email: string, password: string, log: ioBroker.Logger, timers: TimerAdapter) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\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 /** 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 this._bearerToken = loginResp.client.token;\n this.accountId = String(loginResp.client.accountId);\n this.accountTopic = loginResp.client.topic;\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: ${err instanceof Error ? err.message : String(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // 2FA pending/failed: pause reconnect until the user submits a code\n // via Settings (which triggers an adapter restart). Don't increment\n // auth-failure counter \u2014 these are not credential errors.\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 \u2014 the warn would otherwise fire on\n // every reconnect-backoff cycle and look like a hard failure even\n // when MQTT settles after the next attempt.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\n \"Govee requires 2-Factor verification \u2014 request a code via the adapter settings, paste it into 'mqttVerificationCode' and save\",\n );\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(\n \"Govee 2-Factor verification code is invalid or expired \u2014 request a fresh code via the adapter settings\",\n );\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 login failed ${this.authFailCount} times \u2014 check email/password in adapter settings`);\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 if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = 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 const delay = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\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(\n `Persisted P12 cert unusable: ${e instanceof Error ? e.message : String(e)} \u2014 falling back to fresh login`,\n );\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.info(\"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 to AWS IoT\");\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 this.log.debug(`MQTT error: ${err.message}`);\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.info(\"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 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: ${e instanceof Error ? e.message : String(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(\n `Silent bearer refresh failed: ${e instanceof Error ? e.message : String(e)} \u2014 current session kept`,\n );\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 httpsRequest<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 httpsRequest<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 httpsRequest<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 } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\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 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 /** 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 */\n constructor(email: string, password: string, log: ioBroker.Logger, timers: TimerAdapter) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\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 /** 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 this._bearerToken = loginResp.client.token;\n this.accountId = String(loginResp.client.accountId);\n this.accountTopic = loginResp.client.topic;\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: ${err instanceof Error ? err.message : String(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(\n 'Govee asked for one-time client verification (HTTP 454). Open adapter settings, click \"Request verification code\", paste the code from the email into the field, save. Govee remembers this client afterwards. Account-level 2FA is not required.',\n );\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(\n \"Govee rejected the verification code (HTTP 455) \u2014 request a fresh code via the adapter settings.\",\n );\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 login failed ${this.authFailCount} times \u2014 check email/password in adapter settings`);\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 if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = 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 const delay = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\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(\n `Persisted P12 cert unusable: ${e instanceof Error ? e.message : String(e)} \u2014 falling back to fresh login`,\n );\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.info(\"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 to AWS IoT\");\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 this.log.debug(`MQTT error: ${err.message}`);\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.info(\"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 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: ${e instanceof Error ? e.message : String(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(\n `Silent bearer refresh failed: ${e instanceof Error ? e.message : String(e)} \u2014 current session kept`,\n );\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 httpsRequest<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 httpsRequest<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 httpsRequest<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/build/main.js CHANGED
@@ -1334,6 +1334,14 @@ class GoveeAdapter extends utils.Adapter {
1334
1334
  * Read the persisted MQTT bundle out of adapter native. Returns null if
1335
1335
  * any required field is missing — the caller falls back to a fresh login.
1336
1336
  *
1337
+ * v2.1.0 had `encryptedNative` / `protectedNative` listed under
1338
+ * `/common` (schema-rejected) so the new MQTT bundle was written as
1339
+ * plaintext. v2.1.1 moved them to root, so js-controller now tries to
1340
+ * decrypt the previously plaintext-stored bytes — yielding garbage.
1341
+ * Detect that here by trying to base64-decode the P12 cert: if it
1342
+ * fails or the decoded length looks wrong, the bundle is unusable.
1343
+ * `null` return triggers fresh login + the caller wipes native.
1344
+ *
1337
1345
  * @param config Adapter native settings
1338
1346
  */
1339
1347
  buildPersistedCredsFromConfig(config) {
@@ -1348,6 +1356,35 @@ class GoveeAdapter extends utils.Adapter {
1348
1356
  if (!bearer || !endpoint || !p12 || !accountId || !accountTopic || !expiresAt) {
1349
1357
  return null;
1350
1358
  }
1359
+ try {
1360
+ const decoded = Buffer.from(p12, "base64");
1361
+ if (decoded.length < 100 || decoded[0] !== 48) {
1362
+ this.log.info(
1363
+ "MQTT: stored credentials look corrupted (likely a v2.1.0 plaintext-storage leftover) \u2014 clearing and forcing fresh login."
1364
+ );
1365
+ void this.persistMqttCredentials({
1366
+ bearerToken: "",
1367
+ iotEndpoint: "",
1368
+ p12Cert: "",
1369
+ p12Pass: "",
1370
+ accountId: "",
1371
+ accountTopic: "",
1372
+ tokenExpiresAt: 0
1373
+ });
1374
+ return null;
1375
+ }
1376
+ } catch {
1377
+ void this.persistMqttCredentials({
1378
+ bearerToken: "",
1379
+ iotEndpoint: "",
1380
+ p12Cert: "",
1381
+ p12Pass: "",
1382
+ accountId: "",
1383
+ accountTopic: "",
1384
+ tokenExpiresAt: 0
1385
+ });
1386
+ return null;
1387
+ }
1351
1388
  return {
1352
1389
  bearerToken: bearer,
1353
1390
  iotEndpoint: endpoint,
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport {\n buildDeviceStateDefs,\n getDefaultLanStates,\n mapCloudStateValue,\n planCloudCapabilityWrites,\n} from \"./lib/capability-mapper\";\nimport { getDeviceTier, initDeviceRegistry } from \"./lib/device-registry\";\nimport { DeviceManager, resolveSegmentCount, SEGMENT_HARD_MAX } from \"./lib/device-manager\";\nimport { GoveeApiClient } from \"./lib/govee-api-client\";\nimport { GoveeCloudClient } from \"./lib/govee-cloud-client\";\nimport { GoveeLanClient } from \"./lib/govee-lan-client\";\nimport { GoveeMqttClient } from \"./lib/govee-mqtt-client\";\nimport { GoveeOpenapiMqttClient } from \"./lib/govee-openapi-mqtt-client\";\nimport { LocalSnapshotStore, type LocalSnapshot, type SnapshotSegment } from \"./lib/local-snapshots\";\nimport { CloudRetryLoop, type CloudRetryHost } from \"./lib/cloud-retry\";\nimport { RateLimiter } from \"./lib/rate-limiter\";\nimport { SegmentWizard, wizardIdleText, type WizardHost, type WizardResult } from \"./lib/segment-wizard\";\nimport { SkuCache } from \"./lib/sku-cache\";\nimport { StateManager } from \"./lib/state-manager\";\nimport {\n hexToRgb,\n parseSegmentList,\n resolveStatesValue,\n rgbIntToHex,\n rgbToHex,\n type AdapterConfig,\n type CloudLoadResult,\n type CloudStateCapability,\n type DeviceState,\n type GoveeDevice,\n type PersistedMqttCredentials,\n} from \"./lib/types\";\n\n/**\n * Rate limit defaults \u2014 full Cloud API budget (8/min, 9000/day). v2 no\n * longer halves this with govee-appliances because that adapter is\n * deprecated and won't run alongside govee-smart.\n */\nconst FULL_LIMITS = { perMinute: 8, perDay: 9000 };\n\n/**\n * Minimum gap between two `mqttAuth: requestCode` calls. Govee returns 200\n * for every request and queues another email \u2014 without a throttle, double-clicking\n * the button in admin would spam the user's inbox. 30 s is short enough that a\n * legitimate retry after a failed delivery still feels responsive.\n */\nconst VERIFICATION_REQUEST_THROTTLE_MS = 30_000;\n\n/**\n * State-suffix \u2192 command-name lookup for writable states. Segment indices\n * are dynamic and handled by regex in stateToCommand \u2014 everything else is\n * a straight string mapping.\n */\nconst STATE_TO_COMMAND: Readonly<Record<string, string>> = {\n \"control.power\": \"power\",\n \"control.brightness\": \"brightness\",\n \"control.colorRgb\": \"colorRgb\",\n \"control.colorTemperature\": \"colorTemperature\",\n \"control.scene\": \"scene\",\n \"control.gradient_toggle\": \"gradientToggle\",\n \"scenes.light_scene\": \"lightScene\",\n \"scenes.diy_scene\": \"diyScene\",\n \"scenes.scene_speed\": \"sceneSpeed\",\n \"music.music_mode\": \"music\",\n \"music.music_sensitivity\": \"music\",\n \"music.music_auto_color\": \"music\",\n \"snapshots.snapshot_cloud\": \"snapshot\",\n \"segments.command\": \"segmentBatch\",\n};\n\nclass GoveeAdapter extends utils.Adapter {\n private deviceManager: DeviceManager | null = null;\n private stateManager: StateManager | null = null;\n private lanClient: GoveeLanClient | null = null;\n private mqttClient: GoveeMqttClient | null = null;\n private openapiMqttClient: GoveeOpenapiMqttClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /** Repeating timer for the App-API poll (sensor-state pull). */\n private appApiPollTimer: ioBroker.Interval | undefined;\n private skuCache: SkuCache | null = null;\n private localSnapshots: LocalSnapshotStore | null = null;\n private cloudWasConnected = false;\n private readyLogged = false;\n private cloudInitDone = false;\n private lanScanDone = false;\n private statesReady = false;\n private stateCreationQueue: Promise<void>[] = [];\n private lanScanTimer: ioBroker.Timeout | undefined;\n private cleanupTimer: ioBroker.Timeout | undefined;\n private readyTimer: ioBroker.Timeout | undefined;\n private cloudRetry: CloudRetryLoop | null = null;\n private segmentWizard: SegmentWizard | null = null;\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** Per-device timestamp of the last diagnostics export \u2014 throttle gate */\n private diagnosticsLastRun = new Map<string, number>();\n /** Cached admin language from system.config \u2014 used for wizard UI text */\n private adminLanguage = \"en\";\n /** Last time `requestCode` was triggered via onMessage \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"govee-smart\" });\n // Per ioBroker rule: async handlers registered on events MUST .catch,\n // otherwise rejections become unhandled \u2192 SIGKILL code 6 \u2192 restart loop.\n this.on(\"ready\", () =>\n this.onReady().catch(e =>\n this.log.error(`onReady crashed: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`),\n ),\n );\n this.on(\"stateChange\", (id, state) =>\n this.onStateChange(id, state).catch(e =>\n this.log.warn(`onStateChange crashed for ${id}: ${e instanceof Error ? e.message : String(e)}`),\n ),\n );\n this.on(\"message\", obj => this.onMessage(obj));\n this.on(\"unload\", callback => this.onUnload(callback));\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler .catch() wrappers cover the\n // direct entry points; this catches whatever slips past them so the\n // adapter logs the cause instead of triggering js-controller SIGKILL.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(\n `Unhandled rejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`,\n );\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.stack ?? err.message}`);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n /** Adapter started \u2014 initialize all channels */\n private async onReady(): Promise<void> {\n const config = this.config as unknown as AdapterConfig;\n\n // info channel + states are declared as instanceObjects in\n // io-package.json, so js-controller materialises them on install /\n // upgrade. We only initialise the runtime values here.\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n await this.setStateAsync(\"info.mqttConnected\", { val: false, ack: true });\n await this.setStateAsync(\"info.cloudConnected\", { val: false, ack: true });\n await this.setStateAsync(\"info.openapiMqttConnected\", {\n val: false,\n ack: true,\n });\n await this.setStateAsync(\"info.refresh_cloud_data\", {\n val: false,\n ack: true,\n });\n // Load admin language from system.config so wizard prose matches the\n // user's Admin UI. Falls back to English on any lookup failure.\n try {\n const sysConf = await this.getForeignObjectAsync(\"system.config\");\n const lang = (sysConf?.common as { language?: string } | undefined)?.language;\n if (typeof lang === \"string\" && lang.length > 0) {\n this.adminLanguage = lang;\n }\n } catch {\n // Keep default \"en\"\n }\n await this.setStateAsync(\"info.wizardStatus\", {\n val: wizardIdleText(this.adminLanguage),\n ack: true,\n });\n\n this.stateManager = new StateManager(this);\n // General groups online state (reflects Cloud connection)\n await this.stateManager.createGroupsOnlineState(false);\n this.deviceManager = new DeviceManager(this.log, this);\n const dataDir = utils.getAbsoluteInstanceDataDir(this);\n\n // Load device registry from devices.json in the adapter package root.\n // Status filter: verified+reported active by default; seed-status entries\n // require the experimentalQuirks config toggle.\n initDeviceRegistry({\n experimental: config.experimentalQuirks === true,\n log: this.log,\n });\n this.skuCache = new SkuCache(dataDir, this.log);\n this.localSnapshots = new LocalSnapshotStore(dataDir, this.log);\n this.deviceManager.setSkuCache(this.skuCache);\n\n // API client for undocumented scene/music/DIY libraries (always available)\n const apiClient = new GoveeApiClient();\n apiClient.setEmail(config.goveeEmail);\n this.deviceManager.setApiClient(apiClient);\n\n this.deviceManager.setCallbacks(\n (device, state) => this.onDeviceStateUpdate(device, state),\n devices => this.onDeviceListChanged(devices),\n );\n\n // Update info.ip when LAN IP changes\n this.deviceManager.onLanIpChanged = (device, ip) => {\n const prefix = this.stateManager!.devicePrefix(device);\n this.setStateAsync(`${prefix}.info.ip`, { val: ip, ack: true }).catch(() => {});\n };\n\n // Sync individual segment states after batch command\n this.deviceManager.onSegmentBatchUpdate = (device, batch) => {\n const prefix = this.stateManager!.devicePrefix(device);\n for (const idx of batch.segments) {\n if (batch.color !== undefined) {\n const hex = rgbIntToHex(batch.color);\n this.setStateAsync(`${prefix}.segments.${idx}.color`, {\n val: hex,\n ack: true,\n }).catch(() => {});\n }\n if (batch.brightness !== undefined) {\n this.setStateAsync(`${prefix}.segments.${idx}.brightness`, {\n val: batch.brightness,\n ack: true,\n }).catch(() => {});\n }\n }\n };\n\n // Sync per-segment states from MQTT BLE status push (AA A5 packets)\n this.deviceManager.onMqttSegmentUpdate = (device, segments) => {\n const prefix = this.stateManager!.devicePrefix(device);\n for (const seg of segments) {\n this.setStateAsync(`${prefix}.segments.${seg.index}.color`, {\n val: rgbToHex(seg.r, seg.g, seg.b),\n ack: true,\n }).catch(() => {});\n this.setStateAsync(`${prefix}.segments.${seg.index}.brightness`, {\n val: seg.brightness,\n ack: true,\n }).catch(() => {});\n }\n };\n\n // When MQTT reveals more segments than the Cloud advertised, rebuild\n // the device's state tree so the extra segments get their datapoints.\n this.deviceManager.onSegmentCountGrown = device => {\n if (!this.stateManager) {\n return;\n }\n this.stateManager.createSegmentStates(device).catch(e => {\n this.log.warn(\n `Failed to rebuild segment tree for ${device.name} after count growth: ${e instanceof Error ? e.message : String(e)}`,\n );\n });\n };\n\n // Log startup with configured channels\n const startChannels: string[] = [\"LAN\"];\n if (config.apiKey) {\n startChannels.push(\"Cloud\");\n }\n if (config.goveeEmail && config.goveePassword) {\n startChannels.push(\"MQTT\");\n }\n this.log.info(`Starting with channels: ${startChannels.join(\", \")} \u2014 please wait...`);\n\n // --- LAN (always active) ---\n this.lanClient = new GoveeLanClient(this.log, this);\n this.deviceManager.setLanClient(this.lanClient);\n\n this.lanClient.start(\n lanDevice => {\n this.deviceManager!.handleLanDiscovery(lanDevice);\n // Poll status only when MQTT is unavailable. With an active MQTT\n // subscription Govee pushes state changes authoritatively, so the\n // LAN devStatus request would be duplicate traffic.\n if (!this.mqttClient?.connected) {\n this.lanClient!.requestStatus(lanDevice.ip);\n }\n },\n (sourceIp, status) => {\n this.deviceManager!.handleLanStatus(sourceIp, status);\n },\n 30_000,\n config.networkInterface || \"\",\n );\n\n // Wait for first LAN scan responses (UDP multicast, devices respond within 1-2s)\n this.lanScanTimer = this.setTimeout(() => {\n this.lanScanDone = true;\n this.checkAllReady();\n }, 3_000);\n\n // --- MQTT (if account credentials provided) ---\n // Initialize MQTT before Cloud so scene library can load on first cycle\n if (config.goveeEmail && config.goveePassword) {\n this.mqttClient = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n\n // Forward every parsed MQTT op.command into the diagnostics ring buffer\n // so diag.export contains the recent packets per device.\n this.mqttClient.setPacketHook((deviceId, topic, hex) => {\n this.deviceManager?.getDiagnostics().addMqttPacket(deviceId, topic, hex);\n });\n\n // 2FA: forward optional code from settings into the next login attempt;\n // clear the field automatically once Govee has accepted it.\n this.mqttClient.setVerificationCode(config.mqttVerificationCode ?? \"\");\n this.mqttClient.setOnVerificationConsumed(() => {\n this.clearVerificationCodeSetting().catch(e => {\n this.log.warn(`Could not clear mqttVerificationCode: ${e instanceof Error ? e.message : String(e)}`);\n });\n });\n this.mqttClient.setOnVerificationFailed(reason => {\n // On 'failed' (455 / 454+code-was-sent) blank the code so the user\n // doesn't keep retrying with a stale value. On 'pending' (454 + no\n // code) we leave the field as-is \u2014 the user is about to fill it.\n if (reason === \"failed\") {\n this.clearVerificationCodeSetting().catch(() => {});\n }\n });\n\n // Re-use cached MQTT credentials across restarts: P12 cert + bearer\n // are encrypted in adapter native. If they're still valid we skip the\n // login flow entirely (and avoid spamming the user with 2FA emails).\n const cachedCreds = this.buildPersistedCredsFromConfig(config);\n if (cachedCreds) {\n this.mqttClient.setPersistedCredentials(cachedCreds);\n }\n this.mqttClient.setOnCredentialsRefresh(creds => {\n this.persistMqttCredentials(creds).catch(e => {\n this.log.warn(`Could not persist MQTT credentials: ${e instanceof Error ? e.message : String(e)}`);\n });\n });\n\n await this.mqttClient.connect(\n update => this.deviceManager!.handleMqttStatus(update),\n connected => {\n this.setStateAsync(\"info.mqttConnected\", {\n val: connected,\n ack: true,\n }).catch(() => {});\n if (connected) {\n this.checkAllReady();\n }\n this.updateConnectionState();\n },\n // Forward every fresh bearer token \u2014 fires on initial login and on\n // each reconnect-login, so the API client never runs with a stale one.\n token => apiClient.setBearerToken(token),\n );\n }\n\n // --- Device data: Cache first, Cloud only on cache miss ---\n const cachedOk = this.deviceManager.loadFromCache();\n\n if (config.apiKey) {\n this.cloudClient = new GoveeCloudClient(config.apiKey, this.log);\n // Capture the most recent Cloud response per (deviceId, endpoint) for\n // diagnostics \u2014 bounded by the DiagnosticsCollector's response slot cap.\n this.cloudClient.setResponseHook((deviceId, endpoint, body) => {\n this.deviceManager?.getDiagnostics().setApiResponse(deviceId, endpoint, body);\n });\n this.deviceManager.setCloudClient(this.cloudClient);\n\n // Bridge synthetic capabilities (App-API, OpenAPI-MQTT events) into the\n // same setState pipeline as polled Cloud state. Keeps mapCloudStateValue\n // as the single source of truth for value coercion + state-id resolution.\n this.deviceManager.setOnCloudCapabilities((device, caps) => {\n this.applyCloudCapabilities(device, caps).catch(e =>\n this.log.warn(\n `applyCloudCapabilities failed for ${device.sku}: ${e instanceof Error ? e.message : String(e)}`,\n ),\n );\n });\n\n this.rateLimiter = new RateLimiter(this.log, this, FULL_LIMITS.perMinute, FULL_LIMITS.perDay);\n this.rateLimiter.start();\n this.deviceManager.setRateLimiter(this.rateLimiter);\n\n // OpenAPI-MQTT \u2014 push channel for appliance/sensor events\n // (lackWater, iceFull, bodyAppeared etc.). API key is enough; no\n // separate credentials required. Connection runs in parallel to\n // the AWS-IoT MQTT used for status push of regular devices.\n this.openapiMqttClient = new GoveeOpenapiMqttClient(config.apiKey, this.log, this);\n this.openapiMqttClient.connect(\n event => this.deviceManager?.handleOpenApiEvent(event),\n connected => {\n this.setStateAsync(\"info.openapiMqttConnected\", {\n val: connected,\n ack: true,\n }).catch(() => {});\n },\n );\n\n // App-API poll \u2014 every 2 minutes, pulls state for sensors like H5179\n // where OpenAPI v2 /device/state returns empty. Bearer token comes\n // from the AWS-IoT MQTT login, so a no-op until that succeeds.\n this.appApiPollTimer = this.setInterval(\n () => {\n this.deviceManager\n ?.pollAppApi()\n .catch(e => this.log.debug(`pollAppApi failed: ${e instanceof Error ? e.message : String(e)}`));\n },\n 2 * 60 * 1000,\n );\n\n if (!cachedOk) {\n // No cache \u2014 first start, fetch from Cloud with 60s hard-timeout.\n // If Cloud hangs/fails, we don't want to block adapter startup indefinitely.\n const result = await this.cloudInitWithTimeout();\n this.cloudWasConnected = result.ok;\n this.ensureCloudRetry().setConnected(result.ok);\n this.setStateAsync(\"info.cloudConnected\", {\n val: result.ok,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(result.ok).catch(() => {});\n\n if (result.ok) {\n await this.loadCloudStates();\n } else {\n this.handleCloudFailure(result);\n }\n } else {\n this.log.info(\"Using cached device data \u2014 no Cloud calls needed\");\n this.cloudWasConnected = true;\n this.ensureCloudRetry().setConnected(true);\n this.setStateAsync(\"info.cloudConnected\", {\n val: true,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(true).catch(() => {});\n }\n // Load group membership from undocumented API (needs bearer token + device map)\n await this.deviceManager.loadGroupMembers();\n\n this.cloudInitDone = true;\n }\n\n // Wait for all state creation from cache/cloud load to complete.\n // Drain-loop: a callback that fires during the await (e.g. a late LAN\n // discovery) can push fresh promises into the queue \u2014 we need to await\n // those too before flipping statesReady, otherwise the initial state\n // tree would be incomplete on very fast startups.\n while (this.stateCreationQueue.length > 0) {\n const pending = this.stateCreationQueue;\n this.stateCreationQueue = [];\n await Promise.all(pending);\n }\n this.statesReady = true;\n\n // Subscribe to all writable device and group states\n await this.subscribeStatesAsync(\"devices.*\");\n await this.subscribeStatesAsync(\"groups.*\");\n await this.subscribeStatesAsync(\"info.refresh_cloud_data\");\n\n // Cleanup stale devices after initial discovery (30s delay for LAN scan).\n // Reaps devices from every adapter-level map that was keyed on them so the\n // process doesn't leak memory across Cloud-side device turnover.\n this.cleanupTimer = this.setTimeout(() => {\n this.reapStaleDevices().catch(e =>\n this.log.debug(`Device cleanup failed: ${e instanceof Error ? e.message : String(e)}`),\n );\n }, 30_000);\n\n this.updateConnectionState();\n\n // Check if all channels are ready \u2014 may already be true if MQTT connected fast\n this.checkAllReady();\n // Safety timeout: log ready even if a channel takes too long.\n // 60s deckt normalen MQTT-Connect + 1 Reconnect-Attempt ab.\n this.readyTimer = this.setTimeout(() => {\n if (!this.readyLogged) {\n this.readyLogged = true;\n this.logDeviceSummary();\n }\n }, 60_000);\n }\n\n /**\n * Initial Cloud-Load mit 60-Sekunden-Hardtimeout.\n * Blockiert nicht l\u00E4nger \u2014 wenn Cloud h\u00E4ngt, geht Adapter mit LAN+MQTT weiter,\n * und der Retry-Loop probiert's passend zum Fehlergrund erneut.\n */\n private async cloudInitWithTimeout(): Promise<CloudLoadResult> {\n if (!this.deviceManager) {\n return { ok: false, reason: \"transient\" };\n }\n const loadPromise = this.deviceManager.loadFromCloud();\n const timeoutPromise = new Promise<CloudLoadResult>(resolve => {\n this.setTimeout(() => resolve({ ok: false, reason: \"transient\" }), 60_000);\n });\n try {\n return await Promise.race([loadPromise, timeoutPromise]);\n } catch {\n return { ok: false, reason: \"transient\" };\n }\n }\n\n /** Build the host object for {@link CloudRetryLoop}. */\n private buildCloudRetryHost(): CloudRetryHost {\n return {\n log: this.log,\n setTimeout: (cb, ms) => this.setTimeout(cb, ms),\n clearTimeout: h => this.clearTimeout(h as ioBroker.Timeout),\n loadFromCloud: () => this.cloudInitWithTimeout(),\n onCloudRestored: async () => {\n this.cloudWasConnected = true;\n this.setStateAsync(\"info.cloudConnected\", {\n val: true,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(true).catch(() => {});\n await this.loadCloudStates();\n },\n };\n }\n\n /** Lazy-initialise the retry loop on first use. */\n private ensureCloudRetry(): CloudRetryLoop {\n if (!this.cloudRetry) {\n this.cloudRetry = new CloudRetryLoop(this.buildCloudRetryHost());\n this.cloudRetry.setConnected(this.cloudWasConnected);\n }\n return this.cloudRetry;\n }\n\n /**\n * React to a Cloud-load outcome \u2014 delegates to {@link CloudRetryLoop}.\n *\n * @param result CloudLoadResult from initial load or retry attempt\n */\n private handleCloudFailure(result: CloudLoadResult): void {\n this.ensureCloudRetry().handleResult(result);\n }\n\n /**\n * React to the user writing `info.refresh_cloud_data = true`. Performs one\n * full Cloud reload cycle so newly created scenes/snapshots from the Govee\n * Home app show up without an adapter restart.\n */\n private async handleManualCloudRefresh(): Promise<void> {\n if (!this.deviceManager || !this.cloudClient) {\n this.log.info(\"Refresh cloud data: no Cloud client configured (API key missing) \u2014 nothing to do\");\n return;\n }\n this.log.info(\"Refresh cloud data: re-fetching scenes and snapshots for all devices\");\n try {\n const changed = await this.deviceManager.refreshSceneData();\n if (changed) {\n await this.loadCloudStates();\n }\n this.log.info(\"Refresh cloud data: done\");\n } catch (e) {\n this.log.warn(`Refresh cloud data failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous.\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n try {\n if (this.lanScanTimer) {\n this.clearTimeout(this.lanScanTimer);\n }\n if (this.cleanupTimer) {\n this.clearTimeout(this.cleanupTimer);\n }\n if (this.readyTimer) {\n this.clearTimeout(this.readyTimer);\n }\n if (this.appApiPollTimer) {\n this.clearInterval(this.appApiPollTimer);\n this.appApiPollTimer = undefined;\n }\n this.cloudRetry?.dispose();\n this.segmentWizard?.dispose();\n this.lanClient?.stop();\n this.mqttClient?.disconnect();\n this.openapiMqttClient?.disconnect();\n this.rateLimiter?.stop();\n // Remove process-level handlers so an adapter restart doesn't stack them.\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n // onUnload MUST be synchronous \u2014 don't await, but silence potential\n // promise rejection during teardown to avoid \"unhandled rejection\" warnings.\n this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {});\n this.setState(\"info.mqttConnected\", { val: false, ack: true }).catch(() => {});\n this.setState(\"info.openapiMqttConnected\", {\n val: false,\n ack: true,\n }).catch(() => {});\n this.setState(\"info.cloudConnected\", { val: false, ack: true }).catch(() => {});\n } catch {\n // ignore\n }\n callback();\n }\n\n /**\n * Handle state changes from user (write operations).\n *\n * @param id State ID\n * @param state New state value\n */\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack || !this.deviceManager || !this.stateManager) {\n return;\n }\n\n // Global refresh button \u2014 triggers one fresh cloud fetch across all\n // devices and re-builds the state tree. Handy after creating a new\n // snapshot in the Govee Home app without restarting the adapter.\n if (id === `${this.namespace}.info.refresh_cloud_data` && state.val) {\n await this.handleManualCloudRefresh();\n await this.setStateAsync(id, { val: false, ack: true });\n return;\n }\n\n // Find which device this state belongs to\n const localId = id.replace(`${this.namespace}.`, \"\");\n if (!localId.startsWith(\"devices.\") && !localId.startsWith(\"groups.\")) {\n return;\n }\n\n const device = this.findDeviceForState(localId);\n if (!device) {\n return;\n }\n\n // Determine command from state suffix after device prefix\n const prefix = this.stateManager.devicePrefix(device);\n const stateSuffix = localId.slice(prefix.length + 1);\n\n // Resolve dropdown input \u2014 accept Number, numeric String, or label\n // (case-insensitive) against the state's common.states map. Returns\n // the canonical key as String so the rest of the handler sees the\n // same shape it always saw (e.g. \"1\"). Non-dropdown states are\n // passed through unchanged.\n const resolved = await this.resolveDropdownInput(id, state.val);\n if (!resolved.ok) {\n this.log.warn(`Unknown dropdown value for ${id}: ${String(state.val)} \u2014 ignoring`);\n return;\n }\n const val = resolved.val;\n\n // Group fan-out: route commands to each member device\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n await this.handleGroupFanOut(device, stateSuffix, val);\n await this.setStateAsync(id, { val, ack: true });\n if (stateSuffix === \"scenes.light_scene\" || stateSuffix === \"music.music_mode\") {\n await this.resetRelatedDropdowns(prefix, stateSuffix === \"scenes.light_scene\" ? \"lightScene\" : \"music\");\n }\n return;\n }\n\n // Handle local snapshot commands (no Cloud/MQTT needed)\n if (stateSuffix === \"snapshots.snapshot_save\" && typeof val === \"string\" && val.trim()) {\n await this.handleSnapshotSave(device, val.trim());\n await this.setStateAsync(id, { val: \"\", ack: true });\n return;\n }\n if (stateSuffix === \"snapshots.snapshot_local\") {\n if (val !== \"0\" && val !== 0) {\n await this.handleSnapshotRestore(device, val);\n await this.resetRelatedDropdowns(prefix, \"snapshotLocal\");\n }\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n if (stateSuffix === \"snapshots.snapshot_delete\" && typeof val === \"string\" && val.trim()) {\n this.handleSnapshotDelete(device, val.trim());\n await this.setStateAsync(id, { val: \"\", ack: true });\n return;\n }\n\n // Manual segments toggle/list \u2014 handler owns the ack because a parse\n // error rewrites manual_mode to false, and an outer ack with the\n // raw value would resurrect the rejected entry.\n if (stateSuffix === \"segments.manual_mode\" || stateSuffix === \"segments.manual_list\") {\n await this.handleManualSegmentsChange(device, stateSuffix, val);\n return;\n }\n\n // Diagnostics export button \u2014 throttled to 2 s per device so a repeated\n // or scripted trigger can't produce a burst of JSON serialisations.\n if (stateSuffix === \"diag.export\" && val) {\n const deviceKey = `${device.sku}:${device.deviceId}`;\n const now = Date.now();\n const last = this.diagnosticsLastRun.get(deviceKey) ?? 0;\n if (now - last < 2000) {\n this.log.debug(`Diagnostics export throttled for ${device.name} \u2014 last run ${now - last}ms ago`);\n await this.setStateAsync(id, { val: false, ack: true });\n return;\n }\n this.diagnosticsLastRun.set(deviceKey, now);\n const diag = this.deviceManager.generateDiagnostics(device, this.version ?? \"unknown\");\n const resultId = `${this.namespace}.${prefix}.diag.result`;\n await this.setStateAsync(resultId, {\n val: JSON.stringify(diag, null, 2),\n ack: true,\n });\n await this.setStateAsync(id, { val: false, ack: true });\n this.log.info(`Diagnostics exported for ${device.name} (${device.sku})`);\n return;\n }\n\n const command = this.stateToCommand(stateSuffix);\n\n if (!command) {\n // Try generic capability routing via state object metadata\n const obj = await this.getObjectAsync(id);\n const capType = obj?.native?.capabilityType;\n const capInstance = obj?.native?.capabilityInstance;\n if (typeof capType === \"string\" && typeof capInstance === \"string\") {\n try {\n await this.deviceManager.sendCapabilityCommand(device, capType, capInstance, val);\n await this.setStateAsync(id, { val, ack: true });\n } catch (err) {\n this.log.warn(`Command failed for ${device.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n } else {\n this.log.debug(`Unknown writable state: ${stateSuffix}`);\n }\n return;\n }\n\n // Dropdown reset to \"---\" (value 0) \u2014 acknowledge without sending command\n if ((command === \"lightScene\" || command === \"diyScene\" || command === \"snapshot\") && (val === \"0\" || val === 0)) {\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n\n // Scene speed: store on device, applied on next scene activation.\n // Persist to SKU cache so the user's choice survives a restart.\n if (command === \"sceneSpeed\") {\n const level = typeof val === \"number\" ? val : parseInt(String(val), 10);\n if (!isNaN(level)) {\n device.sceneSpeed = level;\n this.deviceManager?.persistDeviceToCache(device);\n }\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n\n try {\n // Music mode: combine all music states into one STRUCT command\n if (command === \"music\") {\n // music_mode \"---\" (value 0) \u2014 acknowledge without sending command\n if (stateSuffix === \"music.music_mode\" && (val === \"0\" || val === 0)) {\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n await this.sendMusicCommand(device, prefix, stateSuffix, val);\n await this.setStateAsync(id, { val, ack: true });\n // Reset scene/snapshot dropdowns when activating music mode\n if (stateSuffix === \"music.music_mode\") {\n await this.resetRelatedDropdowns(prefix, \"music\");\n }\n return;\n }\n\n await this.deviceManager.sendCommand(device, command, val);\n // Optimistic ack\n await this.setStateAsync(id, { val, ack: true });\n // Reset related dropdowns when switching modes.\n // Power-off is a special case \u2014 the device is off, so no mode is\n // active anymore; reset every mode dropdown so the UI reflects the\n // reality (scene/music/snapshot selections are now just history).\n if (command === \"power\" && val === false) {\n await this.resetModeDropdowns(prefix, \"\");\n } else {\n await this.resetRelatedDropdowns(prefix, command);\n }\n } catch (err) {\n this.log.warn(`Command failed for ${device.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n /**\n * Resolve a dropdown-state input value against the state's common.states\n * map. Returns the canonical key (always String form) so a user can write\n * either the index (\"1\"), the index as a number (1) or the label name\n * (\"Aurora\", case-insensitive) \u2014 all three land at the same canonical\n * value for the rest of the handler.\n *\n * Non-dropdown states (no common.states), reset sentinels (0/\"0\"/\"\") and\n * non-string/number inputs are passed through unchanged. A dropdown input\n * that doesn't match any key or label returns ok=false so the caller can\n * warn and skip the command.\n *\n * @param id Full state id\n * @param raw Raw input value as provided by the user/script\n */\n private async resolveDropdownInput(\n id: string,\n raw: ioBroker.StateValue,\n ): Promise<{ val: ioBroker.StateValue; ok: boolean }> {\n if (raw === null || raw === undefined) {\n return { val: raw, ok: true };\n }\n // Reset sentinels \u2014 let the existing branch handle them.\n if (raw === 0 || raw === \"0\" || raw === \"\") {\n return { val: raw, ok: true };\n }\n // Only dropdown candidates have common.states; non-dropdown inputs\n // can't be resolved here so they pass through.\n if (typeof raw !== \"number\" && typeof raw !== \"string\") {\n return { val: raw, ok: true };\n }\n const obj = await this.getObjectAsync(id);\n const states = obj?.common?.states;\n if (!states || typeof states !== \"object\") {\n return { val: raw, ok: true };\n }\n const resolved = resolveStatesValue(raw, states as Record<string, string>);\n if (resolved) {\n return { val: resolved.key, ok: true };\n }\n return { val: raw, ok: false };\n }\n\n /**\n * Build and send a music_setting STRUCT command.\n * Reads sibling music state values and combines them into one API call.\n *\n * @param device Target device\n * @param prefix Device state prefix\n * @param changedSuffix Which music state was changed\n * @param newValue New value for the changed state\n */\n private async sendMusicCommand(\n device: GoveeDevice,\n prefix: string,\n changedSuffix: string,\n newValue: ioBroker.StateValue,\n ): Promise<void> {\n const musicBase = `${this.namespace}.${prefix}.music`;\n\n // Read current sibling values\n const modeState = await this.getStateAsync(`${musicBase}.music_mode`);\n const sensState = await this.getStateAsync(`${musicBase}.music_sensitivity`);\n const autoState = await this.getStateAsync(`${musicBase}.music_auto_color`);\n\n // Apply the changed value, use siblings for the rest\n const musicMode =\n changedSuffix === \"music.music_mode\" ? parseInt(String(newValue), 10) : parseInt(String(modeState?.val ?? 0), 10);\n const sensitivity =\n changedSuffix === \"music.music_sensitivity\" ? (newValue as number) : ((sensState?.val as number) ?? 100);\n const autoColor = changedSuffix === \"music.music_auto_color\" ? (newValue ? 1 : 0) : autoState?.val ? 1 : 0;\n\n if (!musicMode || musicMode === 0) {\n this.log.debug(\"Music mode not selected, skipping command\");\n return;\n }\n\n // LAN first: send via ptReal BLE if device is on LAN\n if (device.lanIp && this.lanClient) {\n // Read current color for RGB-modes (Spectrum=1, Rolling=2)\n let r = 0,\n g = 0,\n b = 0;\n if (musicMode === 1 || musicMode === 2) {\n const colorState = await this.getStateAsync(`${this.namespace}.${prefix}.control.colorRgb`);\n if (colorState?.val && typeof colorState.val === \"string\") {\n ({ r, g, b } = hexToRgb(colorState.val));\n }\n }\n this.lanClient.setMusicMode(device.lanIp, musicMode, r, g, b);\n return;\n }\n\n // Cloud fallback\n const structValue: Record<string, unknown> = {\n musicMode,\n sensitivity,\n autoColor,\n };\n\n await this.deviceManager!.sendCapabilityCommand(\n device,\n \"devices.capabilities.music_setting\",\n \"musicMode\",\n structValue,\n );\n }\n\n /**\n * Called by device-manager when a device state changes\n *\n * @param device Updated device\n * @param state Changed state values\n */\n private onDeviceStateUpdate(device: GoveeDevice, state: Partial<DeviceState>): void {\n if (this.stateManager) {\n this.stateManager.updateDeviceState(device, state).catch(() => {});\n }\n this.updateConnectionState();\n\n // Update group reachability when member online status changes\n if (state.online !== undefined) {\n this.updateGroupReachability();\n }\n\n // Mirror power-off to mode-dropdown reset. Covers MQTT/LAN-initiated\n // power changes (Govee app or physical remote) so the UI stays honest:\n // a device that's off can't be \"playing Aurora-A\" anymore.\n if (state.power === false && this.stateManager) {\n const prefix = this.stateManager.devicePrefix(device);\n this.resetModeDropdowns(prefix, \"\").catch(() => undefined);\n }\n }\n\n /**\n * Fan out a group command to all member devices.\n * Basic controls (power, brightness, color) are sent directly.\n * Scenes/music are matched by name across members.\n *\n * @param group BaseGroup device\n * @param stateSuffix State path suffix (e.g. \"control.power\")\n * @param value Command value\n */\n private async handleGroupFanOut(group: GoveeDevice, stateSuffix: string, value: ioBroker.StateValue): Promise<void> {\n if (!this.deviceManager || !group.groupMembers) {\n return;\n }\n\n const devices = this.deviceManager.getDevices();\n const members = this.resolveGroupMembers(group, devices).filter(d => d.state.online);\n\n if (members.length === 0) {\n this.log.debug(`Group \"${group.name}\": no reachable members for fan-out`);\n return;\n }\n\n const command = this.stateToCommand(stateSuffix);\n if (!command) {\n return;\n }\n\n // Dropdown reset \u2014 no command needed\n if ((command === \"lightScene\" || command === \"music\") && (value === \"0\" || value === 0)) {\n return;\n }\n\n for (const member of members) {\n try {\n if (command === \"lightScene\") {\n await this.fanOutScene(group, member, value);\n } else if (command === \"music\") {\n await this.fanOutMusic(group, member, stateSuffix, value);\n } else {\n await this.deviceManager.sendCommand(member, command, value);\n }\n } catch (err) {\n this.log.debug(`Group fan-out to ${member.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n }\n\n /**\n * Fan out a scene command: match group scene name to member scene index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param value Dropdown index value\n */\n private async fanOutScene(group: GoveeDevice, member: GoveeDevice, value: ioBroker.StateValue): Promise<void> {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n\n // Get group scene name from dropdown value (1-based index)\n const groupPrefix = this.stateManager.devicePrefix(group);\n const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.scenes.light_scene`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const sceneName = groupStates?.[String(value)];\n if (!sceneName) {\n return;\n }\n\n // Find the same scene name in the member's scene list (1-based)\n const memberIdx = member.scenes.findIndex(s => s.name === sceneName);\n if (memberIdx >= 0) {\n await this.deviceManager.sendCommand(member, \"lightScene\", memberIdx + 1);\n }\n }\n\n /**\n * Fan out a music command: match group music name to member music index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param stateSuffix Music state path suffix\n * @param value Command value\n */\n private async fanOutMusic(\n group: GoveeDevice,\n member: GoveeDevice,\n stateSuffix: string,\n value: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n\n // For sensitivity/auto_color, forward directly \u2014 these are numeric values\n if (stateSuffix !== \"music.music_mode\") {\n await this.sendMusicCommand(member, this.stateManager.devicePrefix(member), stateSuffix, value);\n return;\n }\n\n // Get group music name from dropdown value (1-based index)\n const groupPrefix = this.stateManager.devicePrefix(group);\n const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.music.music_mode`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const musicName = groupStates?.[String(value)];\n if (!musicName) {\n return;\n }\n\n // Find the same music name in the member's music library (1-based)\n const memberIdx = member.musicLibrary.findIndex(m => m.name === musicName);\n if (memberIdx >= 0) {\n // Build the music command struct for the member\n const memberPrefix = this.stateManager.devicePrefix(member);\n // Temporarily write the music mode value to trigger the member's music command\n await this.sendMusicCommand(member, memberPrefix, \"music.music_mode\", memberIdx + 1);\n }\n }\n\n /**\n * Resolve group member references to actual device objects.\n *\n * @param group BaseGroup device with groupMembers\n * @param devices Full device list to search\n */\n private resolveGroupMembers(group: GoveeDevice, devices: GoveeDevice[]): GoveeDevice[] {\n if (!group.groupMembers) {\n return [];\n }\n return group.groupMembers\n .map(m => devices.find(d => d.sku === m.sku && d.deviceId === m.deviceId))\n .filter((d): d is GoveeDevice => d !== undefined);\n }\n\n /**\n * Recalculate info.membersUnreachable for all groups.\n * Called when any device's online status changes.\n */\n private updateGroupReachability(): void {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n const devices = this.deviceManager.getDevices();\n for (const group of devices) {\n if (group.sku !== \"BaseGroup\" || !group.groupMembers) {\n continue;\n }\n const memberDevices = this.resolveGroupMembers(group, devices);\n this.stateManager.updateGroupMembersUnreachable(group, memberDevices).catch(() => {});\n }\n }\n\n /**\n * Rebuild state definitions for one device and feed them into StateManager.\n * Used both from the full-list callback and from targeted refreshes\n * (e.g. after a local snapshot was added or removed \u2014 no reason to rebuild\n * the entire tree for every device then).\n *\n * @param device Target device\n * @param allDevices Full device list (needed to resolve group members)\n */\n private refreshDeviceStates(device: GoveeDevice, allDevices: GoveeDevice[]): void {\n if (!this.stateManager) {\n return;\n }\n const localSnaps = this.localSnapshots?.getSnapshots(device.sku, device.deviceId);\n let memberDevices: GoveeDevice[] | undefined;\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n memberDevices = this.resolveGroupMembers(device, allDevices);\n }\n const stateDefs = buildDeviceStateDefs(device, localSnaps, memberDevices);\n const p = this.stateManager\n .createDeviceStates(device, stateDefs)\n .then(async () => {\n // v2.1.0 \u2192 v2.1.1 layout migration: drop legacy info.diagnostics_*\n // before publishing the new diag.tier value. Idempotent.\n await this.stateManager?.migrateLegacyDiagnostics(device);\n await this.stateManager?.updateDeviceTier(device, getDeviceTier(device.sku));\n })\n .catch(e => {\n this.log.error(`createDeviceStates failed for ${device.name}: ${e instanceof Error ? e.message : String(e)}`);\n });\n // Until ready, collect so onReady can await the whole initial batch.\n // After ready, fire-and-forget \u2014 the queue would otherwise keep growing\n // with resolved promises for the lifetime of the adapter.\n if (!this.statesReady) {\n this.stateCreationQueue.push(p);\n } else {\n void p;\n }\n }\n\n /**\n * Called by device-manager when the device list changes\n *\n * @param devices Current list of all devices\n */\n private onDeviceListChanged(devices: GoveeDevice[]): void {\n if (!this.stateManager) {\n return;\n }\n\n for (const device of devices) {\n this.refreshDeviceStates(device, devices);\n }\n\n this.updateConnectionState();\n // Cache sync happens once after the initial setup completes (see\n // checkAllReady) \u2014 triggering here would fire on every device update\n // and spam the log.\n\n // Keep adapter-level per-device maps (diagnosticsLastRun, ...) aligned\n // with the new device list so removed devices don't leave orphan keys.\n // Skip during the initial boot phase \u2014 the startup cleanupTimer handles\n // that pass with proper LAN-scan-settled timing.\n if (this.statesReady) {\n this.reapStaleDevices().catch(() => undefined);\n }\n }\n\n /** Update global info.connection */\n private updateConnectionState(): void {\n const devices = this.deviceManager?.getDevices() ?? [];\n const hasDevices = devices.length > 0;\n const anyOnline = devices.some(d => d.state.online);\n const lanRunning = this.lanClient !== null;\n const connected = hasDevices ? anyOnline : lanRunning;\n this.setStateAsync(\"info.connection\", { val: connected, ack: true }).catch(() => {});\n }\n\n /**\n * Delete ioBroker objects for devices no longer present and drop the same\n * devices from adapter-level maps. Called after the initial-discovery\n * window and every time the device list changes.\n *\n * Scope of \"stale\" today: cleanupDevices compares the ioBroker object tree\n * against the live device-manager registry \u2014 it deletes objects that\n * outlive their entry in `DeviceManager.devices`. In v2.0 that registry is\n * monotonically growing within a single adapter lifetime (entries only\n * leave via cache pruning across restarts), so this primarily catches\n * tree leftovers from a previous adapter version after upgrade. The\n * adapter-level `diagnosticsLastRun` map is also reaped so it can't outlive\n * its devices either.\n *\n * A future stale-pruning step that explicitly retires devices from the\n * device-manager registry should also drop the device from\n * `deviceManager.devices` and call `getDiagnostics().forget(deviceId)` for\n * each retired device \u2014 those reaping APIs come in with the pruning patch,\n * not before (Memory `feedback_kein_phantom_schema`).\n */\n private async reapStaleDevices(): Promise<void> {\n if (!this.stateManager || !this.deviceManager) {\n return;\n }\n const currentDevices = this.deviceManager.getDevices();\n await this.stateManager.cleanupDevices(currentDevices);\n\n const liveKeys = new Set(currentDevices.map(d => `${d.sku}:${d.deviceId}`));\n for (const key of this.diagnosticsLastRun.keys()) {\n if (!liveKeys.has(key)) {\n this.diagnosticsLastRun.delete(key);\n }\n }\n }\n\n /**\n * Check if all configured channels are initialized and log ready message.\n * Called from MQTT onConnection callback and end of onReady.\n */\n private checkAllReady(): void {\n if (this.readyLogged) {\n return;\n }\n // Wait for first LAN scan (always active)\n if (!this.lanScanDone) {\n return;\n }\n // Wait for initial state creation to complete\n if (!this.statesReady) {\n return;\n }\n // Wait for Cloud init if configured\n if (this.cloudClient && !this.cloudInitDone) {\n return;\n }\n // Wait for MQTT connection if configured\n if (this.mqttClient && !this.mqttClient.connected) {\n return;\n }\n this.readyLogged = true;\n this.logDeviceSummary();\n // Persist any learned changes from the initial load (e.g. resolveSegmentCount\n // collapsing Cloud's 15 to the real 10 on H70D1). One-shot on first ready;\n // subsequent mutations persist themselves (MQTT bumps, wizard, manual-mode).\n this.deviceManager?.saveDevicesToCache();\n }\n\n /**\n * Log final ready message with device/group/channel summary.\n */\n private logDeviceSummary(): void {\n if (!this.deviceManager) {\n return;\n }\n const all = this.deviceManager.getDevices();\n const devices = all.filter(d => d.sku !== \"BaseGroup\");\n const groups = all.filter(d => d.sku === \"BaseGroup\");\n\n const channels: string[] = [\"LAN\"];\n if (this.cloudWasConnected) {\n channels.push(\"Cloud\");\n }\n if (this.mqttClient?.connected) {\n channels.push(\"MQTT\");\n }\n\n // If a channel is configured but not connected, note it honestly \u2014\n // background reconnect will continue and log \"restored\" on success.\n const pending: string[] = [];\n if (this.cloudClient && !this.cloudWasConnected) {\n pending.push(\"Cloud\");\n }\n if (this.mqttClient && !this.mqttClient.connected) {\n pending.push(\"MQTT\");\n }\n const pendingNote =\n pending.length > 0 ? `, ${pending.join(\"+\")} noch im Aufbau \u2014 wird im Hintergrund fortgesetzt` : \"\";\n\n if (devices.length === 0 && groups.length === 0) {\n this.log.info(`Govee adapter ready \u2014 no devices found (channels: ${channels.join(\"+\")}${pendingNote})`);\n return;\n }\n\n // Summary line\n const parts: string[] = [];\n if (devices.length > 0) {\n parts.push(`${devices.length} device${devices.length > 1 ? \"s\" : \"\"}`);\n }\n if (groups.length > 0) {\n parts.push(`${groups.length} group${groups.length > 1 ? \"s\" : \"\"}`);\n }\n this.log.info(`Govee adapter ready \u2014 ${parts.join(\", \")} (channels: ${channels.join(\"+\")}${pendingNote})`);\n }\n\n /**\n * Load current state for all Cloud devices and populate state values.\n * Called once after initial Cloud device list load.\n */\n private async loadCloudStates(): Promise<void> {\n if (!this.cloudClient || !this.deviceManager || !this.stateManager) {\n return;\n }\n\n const devices = this.deviceManager.getDevices();\n // LAN-first: never overwrite LAN states with Cloud values\n const lanStateIds = new Set(getDefaultLanStates().map(s => s.id));\n let loaded = 0;\n\n for (const device of devices) {\n if (!device.channels.cloud || device.capabilities.length === 0) {\n continue;\n }\n\n try {\n const caps = await this.cloudClient.getDeviceState(device.sku, device.deviceId);\n const prefix = this.stateManager.devicePrefix(device);\n\n const writes: Promise<unknown>[] = [];\n for (const cap of caps) {\n const mapped = mapCloudStateValue(cap);\n if (!mapped) {\n continue;\n }\n // Skip LAN-covered states for LAN-capable devices\n if (device.lanIp && lanStateIds.has(mapped.stateId)) {\n continue;\n }\n const statePath = this.stateManager.resolveStatePath(prefix, mapped.stateId);\n // Fire-and-forget \u2014 States are created before loadCloudStates runs;\n // a rejection here means the state was deleted out-of-band and\n // can be safely ignored.\n writes.push(\n this.setStateAsync(statePath, {\n val: mapped.value,\n ack: true,\n }).catch(() => undefined),\n );\n }\n await Promise.all(writes);\n loaded++;\n } catch {\n this.log.debug(`Could not load Cloud state for ${device.name} (${device.sku})`);\n }\n }\n\n if (loaded > 0) {\n this.log.debug(`Cloud states loaded for ${loaded} devices`);\n }\n }\n\n /**\n * Apply a list of synthesized Cloud-state capabilities to a single\n * device \u2014 the App-API poll and OpenAPI-MQTT events both use this path\n * so their values flow through the same `mapCloudStateValue` pipeline\n * that polled Cloud states use.\n *\n * @param device Target Govee device\n * @param caps Capabilities to apply\n */\n private async applyCloudCapabilities(device: GoveeDevice, caps: CloudStateCapability[]): Promise<void> {\n if (!this.stateManager) {\n return;\n }\n const lanStateIds = new Set(getDefaultLanStates().map(s => s.id));\n const prefix = this.stateManager.devicePrefix(device);\n const planned = planCloudCapabilityWrites(caps, Boolean(device.lanIp), lanStateIds);\n // App-API and OpenAPI-MQTT deliver state IDs (battery, temperature,\n // humidity, lackWater, \u2026) that the Cloud-capability pipeline doesn't\n // declare for sensor/appliance SKUs \u2014 the state objects therefore\n // don't exist yet on first write. ensureSyntheticStateObject creates\n // them lazily with the right channel + role + unit.\n for (const mapped of planned) {\n await this.stateManager.ensureSyntheticStateObject(prefix, mapped.stateId);\n }\n const writes = planned.map(mapped => {\n const statePath = this.stateManager!.resolveStatePath(prefix, mapped.stateId);\n return this.setStateAsync(statePath, {\n val: mapped.value,\n ack: true,\n }).catch(() => undefined);\n });\n await Promise.all(writes);\n }\n\n /**\n * Find device for a state ID\n *\n * @param localId Local state ID without namespace prefix\n */\n private findDeviceForState(localId: string): GoveeDevice | undefined {\n if (!this.deviceManager || !this.stateManager) {\n return undefined;\n }\n\n for (const device of this.deviceManager.getDevices()) {\n const prefix = this.stateManager.devicePrefix(device);\n if (localId.startsWith(`${prefix}.`)) {\n return device;\n }\n }\n return undefined;\n }\n\n /**\n * Map state suffix to command name.\n *\n * Simple suffixes live in a lookup table, segment indices need regex\n * extraction because they're dynamic. The three music states all route\n * to the same \"music\" command \u2014 the handler reads sibling values.\n *\n * @param suffix State ID suffix (e.g. \"power\", \"brightness\")\n */\n private stateToCommand(suffix: string): string | null {\n const direct = STATE_TO_COMMAND[suffix];\n if (direct) {\n return direct;\n }\n const segColorMatch = /^segments\\.(\\d+)\\.color$/.exec(suffix);\n if (segColorMatch) {\n return `segmentColor:${segColorMatch[1]}`;\n }\n const segBrightMatch = /^segments\\.(\\d+)\\.brightness$/.exec(suffix);\n if (segBrightMatch) {\n return `segmentBrightness:${segBrightMatch[1]}`;\n }\n return null;\n }\n\n /**\n * Central entry point for manual-segment updates. Sets the device flags,\n * rebuilds the segment tree (which writes manual_mode + manual_list with\n * ack=true), and persists to cache. Both the user state-change handler\n * and the wizard route their final decisions here.\n *\n * @param device Target device\n * @param mode Whether manual mode should be active\n * @param indices Physical indices when mode=true, ignored otherwise\n */\n private async applyManualSegments(device: GoveeDevice, mode: boolean, indices?: number[]): Promise<void> {\n if (!this.stateManager) {\n return;\n }\n device.manualMode = mode;\n device.manualSegments = mode && Array.isArray(indices) && indices.length > 0 ? indices.slice() : undefined;\n await this.stateManager.createSegmentStates(device);\n this.deviceManager?.persistDeviceToCache(device);\n }\n\n /**\n * React to manual-segments state changes \u2014 parses list, forwards to\n * {@link applyManualSegments}. On parse error disables manual mode so the\n * rejected value doesn't survive in the state tree.\n *\n * @param device Target device\n * @param suffix State suffix (either \"segments.manual_mode\" or \"segments.manual_list\")\n * @param newValue Written value\n */\n private async handleManualSegmentsChange(device: GoveeDevice, suffix: string, newValue: unknown): Promise<void> {\n // Peer value that wasn't just written comes from the device object\n // (kept in sync via createSegmentStates), not a separate state read.\n const modeVal = suffix === \"segments.manual_mode\" ? Boolean(newValue) : device.manualMode === true;\n const listVal =\n suffix === \"segments.manual_list\"\n ? typeof newValue === \"string\"\n ? newValue\n : \"\"\n : Array.isArray(device.manualSegments)\n ? device.manualSegments.join(\",\")\n : \"\";\n\n if (!modeVal) {\n this.log.info(`${device.name}: manual segments disabled \u2014 strip treated as contiguous`);\n await this.applyManualSegments(device, false);\n return;\n }\n\n // Upper bound: cap at the real length if known, otherwise the protocol limit.\n // Real length can still grow via MQTT discovery, so SEGMENT_HARD_MAX is the\n // absolute safety net.\n const maxIndex =\n typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount - 1 : SEGMENT_HARD_MAX;\n const parsed = parseSegmentList(listVal, maxIndex);\n if (parsed.error) {\n this.log.warn(`${device.name}: manual_list invalid (${parsed.error}) \u2014 disabling manual mode`);\n await this.applyManualSegments(device, false);\n return;\n }\n\n this.log.info(`${device.name}: manual segments active \u2014 ${parsed.indices.length} physical indices (${listVal})`);\n await this.applyManualSegments(device, true, parsed.indices);\n }\n\n // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Segment-Detection-Wizard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Handle incoming sendTo messages (from jsonConfig).\n *\n * @param obj ioBroker message object\n */\n private onMessage(obj: ioBroker.Message): void {\n if (!obj?.command) {\n return;\n }\n // Never let a rejection bubble up from the event handler \u2014 the ioBroker\n // event emitter doesn't catch it, which would crash the adapter.\n this.handleMessage(obj).catch(e => {\n this.log.warn(`onMessage handler crashed for ${obj.command}: ${e instanceof Error ? e.message : String(e)}`);\n this.sendMessageResponse(obj, {\n error: e instanceof Error ? e.message : String(e),\n });\n });\n }\n\n private async handleMessage(obj: ioBroker.Message): Promise<void> {\n try {\n if (obj.command === \"getSegmentDevices\") {\n const devices = this.deviceManager?.getDevices() ?? [];\n const list = devices\n .filter(d => d.sku !== \"BaseGroup\" && d.state?.online === true && resolveSegmentCount(d) > 0)\n .map(d => {\n const count = resolveSegmentCount(d);\n return {\n value: this.deviceKeyFor(d),\n label: `${d.name} (${d.sku}, bisher ${count} Segmente)`,\n };\n });\n // selectSendTo expects the array directly, not wrapped in an object\n this.sendMessageResponse(obj, list);\n return;\n }\n if (obj.command === \"segmentWizard\") {\n const payload = (obj.message ?? {}) as {\n action?: string;\n device?: string;\n };\n const response = await this.runWizardStep(payload.action ?? \"\", payload.device ?? \"\");\n this.sendMessageResponse(obj, response);\n return;\n }\n if (obj.command === \"mqttAuth\") {\n const payload = (obj.message ?? {}) as { action?: string };\n const response = await this.runMqttAuthAction(payload.action ?? \"\");\n this.sendMessageResponse(obj, response);\n return;\n }\n } catch (e) {\n this.log.warn(`onMessage failed for ${obj.command}: ${e instanceof Error ? e.message : String(e)}`);\n this.sendMessageResponse(obj, {\n error: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n /**\n * Handle the `mqttAuth` onMessage commands triggered by the admin UI.\n *\n * Two actions:\n * - `test` \u2014 try a one-shot login with the current settings (incl. any code) and\n * report a single-line plaintext result the admin can show as a toast.\n * - `requestCode` \u2014 POST to /account/rest/account/v1/verification so Govee emails a fresh\n * code. 30-second in-memory throttle prevents double-click email spam.\n *\n * @param action Action name from the jsonConfig sendTo button\n */\n private async runMqttAuthAction(action: string): Promise<{ result: string }> {\n const config = this.config as unknown as AdapterConfig;\n if (!config.goveeEmail || !config.goveePassword) {\n return { result: \"Email + Passwort in den Adapter-Einstellungen n\u00F6tig.\" };\n }\n\n if (action === \"test\") {\n // One-shot client: don't reuse this.mqttClient \u2014 we don't want this\n // probe to take over its reconnect timer or auth-failure counter.\n const probe = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n probe.setVerificationCode(config.mqttVerificationCode ?? \"\");\n try {\n let connected = false;\n await probe.connect(\n () => {},\n isConnected => {\n connected = isConnected;\n },\n );\n probe.disconnect();\n return {\n result: connected\n ? \"Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv.\"\n : \"Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten.\",\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n if (/Verification required/i.test(msg)) {\n return {\n result:\n \"Govee verlangt 2-Faktor-Best\u00E4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern.\",\n };\n }\n if (/Verification code invalid/i.test(msg)) {\n return {\n result: \"2-Faktor-Code ung\u00FCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern.\",\n };\n }\n if (/email not registered/i.test(msg)) {\n return { result: \"Diese E-Mail ist bei Govee nicht registriert.\" };\n }\n if (/Login failed/i.test(msg)) {\n return { result: \"Passwort wurde von Govee abgelehnt.\" };\n }\n if (/Rate limited/i.test(msg)) {\n return { result: \"Govee meldet Rate-Limit \u2014 bitte sp\u00E4ter erneut versuchen.\" };\n }\n if (/Account temporarily locked/i.test(msg)) {\n return { result: \"Govee-Account vor\u00FCbergehend gesperrt \u2014 Govee Home App \u00F6ffnen und Status pr\u00FCfen.\" };\n }\n return { result: `Login fehlgeschlagen: ${msg}` };\n }\n }\n\n if (action === \"requestCode\") {\n const now = Date.now();\n if (now - this.lastVerificationRequestMs < VERIFICATION_REQUEST_THROTTLE_MS) {\n const wait = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1000);\n return { result: `Bitte ${wait}s warten \u2014 gerade wurde schon ein Code angefordert.` };\n }\n this.lastVerificationRequestMs = now;\n const probe = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n try {\n await probe.requestVerificationCode();\n return { result: \"Code wurde an deine Govee-E-Mail-Adresse gesendet (Spam-Ordner pr\u00FCfen).\" };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return { result: `Govee hat den Code-Versand abgelehnt: ${msg}` };\n }\n }\n\n return { result: `Unbekannte Aktion '${action}'.` };\n }\n\n /**\n * Helper: clear `mqttVerificationCode` in adapter native after a successful\n * login or a 455-fail. Triggers a settings-reload by the admin UI but the\n * adapter restart is gentle (only the field changed, not the connection).\n */\n private async clearVerificationCodeSetting(): Promise<void> {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { mqttVerificationCode: \"\" },\n });\n }\n\n /**\n * Read the persisted MQTT bundle out of adapter native. Returns null if\n * any required field is missing \u2014 the caller falls back to a fresh login.\n *\n * @param config Adapter native settings\n */\n private buildPersistedCredsFromConfig(config: AdapterConfig): PersistedMqttCredentials | null {\n const bearer = config.mqttBearerToken ?? \"\";\n const endpoint = config.mqttIotEndpoint ?? \"\";\n const p12 = config.mqttP12Cert ?? \"\";\n const p12Pass = config.mqttP12Pass ?? \"\";\n const accountId = config.mqttAccountId ?? \"\";\n const accountTopic = config.mqttAccountTopic ?? \"\";\n const expiresAt = Number(config.mqttTokenExpiresAt ?? 0);\n if (!bearer || !endpoint || !p12 || !accountId || !accountTopic || !expiresAt) {\n return null;\n }\n return {\n bearerToken: bearer,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId,\n accountTopic,\n tokenExpiresAt: expiresAt,\n };\n }\n\n /**\n * Persist freshly-issued MQTT credentials back into adapter native. The\n * P12 cert + bearer are listed in `encryptedNative` / `protectedNative`\n * (io-package.json) so js-controller transparently encrypts them at rest.\n *\n * @param creds The freshly-issued MQTT bundle from a successful login\n */\n private async persistMqttCredentials(creds: PersistedMqttCredentials): Promise<void> {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: {\n mqttBearerToken: creds.bearerToken,\n mqttIotEndpoint: creds.iotEndpoint,\n mqttP12Cert: creds.p12Cert,\n mqttP12Pass: creds.p12Pass,\n mqttAccountId: creds.accountId,\n mqttAccountTopic: creds.accountTopic,\n mqttTokenExpiresAt: creds.tokenExpiresAt,\n },\n });\n }\n\n private sendMessageResponse(obj: ioBroker.Message, data: unknown): void {\n if (obj.callback && obj.from) {\n this.sendTo(obj.from, obj.command, data as Record<string, unknown>, obj.callback);\n }\n }\n\n /**\n * Stable device key for wizard session tracking.\n *\n * @param device Target device\n */\n private deviceKeyFor(device: GoveeDevice): string {\n return `${device.sku}:${device.deviceId}`;\n }\n\n private findDeviceByKey(key: string): GoveeDevice | undefined {\n const devices = this.deviceManager?.getDevices() ?? [];\n return devices.find(d => this.deviceKeyFor(d) === key);\n }\n\n /** Construct the host object passed into SegmentWizard. */\n private buildWizardHost(): WizardHost {\n return {\n log: this.log,\n getState: id => this.getStateAsync(id),\n sendCommand: async (device, command, value) => {\n await this.deviceManager?.sendCommand(device, command, value);\n },\n flashSegmentAtomic: (device, idx) => {\n if (!device.lanIp || !this.lanClient) {\n return Promise.resolve(false);\n }\n this.lanClient.flashSingleSegment(device.lanIp, idx);\n return Promise.resolve(true);\n },\n restoreStripAtomic: (device, total, color, brightness) => {\n if (!device.lanIp || !this.lanClient) {\n return Promise.resolve(false);\n }\n const r = (color >> 16) & 0xff;\n const g = (color >> 8) & 0xff;\n const b = color & 0xff;\n this.lanClient.restoreAllSegments(device.lanIp, total, r, g, b, brightness);\n return Promise.resolve(true);\n },\n findDevice: key => this.findDeviceByKey(key),\n namespace: this.namespace,\n devicePrefix: device => this.stateManager?.devicePrefix(device) ?? \"\",\n setTimeout: (cb, ms) => this.setTimeout(cb, ms),\n clearTimeout: h => this.clearTimeout(h as ioBroker.Timeout),\n applyWizardResult: (device, result) => this.applyWizardResult(device, result),\n getLanguage: () => this.adminLanguage,\n };\n }\n\n /**\n * Apply a finished wizard's measurement: set the real segment count, then\n * route through {@link applyManualSegments} so the same state-tree rebuild\n * and cache-persist path runs for both wizard results and user edits.\n *\n * @param device Target device\n * @param result Wizard's measurement\n */\n private async applyWizardResult(device: GoveeDevice, result: WizardResult): Promise<void> {\n device.segmentCount = result.segmentCount;\n if (result.hasGaps) {\n const parsed = parseSegmentList(result.manualList, result.segmentCount - 1);\n await this.applyManualSegments(device, true, parsed.error ? undefined : parsed.indices);\n } else {\n await this.applyManualSegments(device, false);\n }\n this.log.debug(\n `applyWizardResult: ${device.sku} \u2192 segmentCount=${result.segmentCount}, ` +\n `manualMode=${device.manualMode}, list=\"${result.manualList}\"`,\n );\n }\n\n /**\n * Execute one wizard step (start/yes/no/abort). Delegates to\n * {@link SegmentWizard} \u2014 see `lib/segment-wizard.ts`.\n *\n * @param action \"start\" | \"yes\" | \"no\" | \"abort\"\n * @param deviceKey device identifier (only required for \"start\")\n */\n private async runWizardStep(action: string, deviceKey: string): Promise<Record<string, unknown>> {\n if (!this.segmentWizard) {\n this.segmentWizard = new SegmentWizard(this.buildWizardHost());\n }\n const response = await this.segmentWizard.runStep(action, deviceKey);\n // Mirror the current wizard status into a plain state so admin's\n // `type: \"state\"` component can show it live via state subscription.\n const statusText = this.segmentWizard.getStatusText();\n await this.setStateAsync(\"info.wizardStatus\", {\n val: statusText,\n ack: true,\n });\n return response;\n }\n\n /**\n * Save current device state as a local snapshot.\n *\n * @param device Target device\n * @param name Snapshot name\n */\n private async handleSnapshotSave(device: GoveeDevice, name: string): Promise<void> {\n if (!this.localSnapshots || !this.stateManager) {\n return;\n }\n\n const prefix = this.stateManager.devicePrefix(device);\n const ns = this.namespace;\n\n // Read device-level state in parallel\n const [powerState, brightState, colorState, ctState] = await Promise.all([\n this.getStateAsync(`${ns}.${prefix}.control.power`),\n this.getStateAsync(`${ns}.${prefix}.control.brightness`),\n this.getStateAsync(`${ns}.${prefix}.control.colorRgb`),\n this.getStateAsync(`${ns}.${prefix}.control.colorTemperature`),\n ]);\n\n // Read per-segment states in parallel \u2014 20 segments \u00D7 2 reads used to run\n // sequentially (~80ms), parallel completes in a single round-trip.\n let segments: SnapshotSegment[] | undefined;\n const segCount = device.segmentCount ?? 0;\n if (segCount > 0) {\n const segReads: Promise<[ioBroker.State | null | undefined, ioBroker.State | null | undefined]>[] = [];\n for (let i = 0; i < segCount; i++) {\n segReads.push(\n Promise.all([\n this.getStateAsync(`${ns}.${prefix}.segments.${i}.color`),\n this.getStateAsync(`${ns}.${prefix}.segments.${i}.brightness`),\n ]),\n );\n }\n const segResults = await Promise.all(segReads);\n segments = segResults.map(([segColor, segBright]) => ({\n color: typeof segColor?.val === \"string\" ? segColor.val : \"#000000\",\n brightness: typeof segBright?.val === \"number\" ? segBright.val : 100,\n }));\n }\n\n const snapshot: LocalSnapshot = {\n name,\n power: powerState?.val === true,\n brightness: typeof brightState?.val === \"number\" ? brightState.val : 0,\n colorRgb: typeof colorState?.val === \"string\" ? colorState.val : \"#000000\",\n colorTemperature: typeof ctState?.val === \"number\" ? ctState.val : 0,\n segments,\n savedAt: Date.now(),\n };\n\n this.localSnapshots.saveSnapshot(device.sku, device.deviceId, snapshot);\n this.log.info(`Local snapshot saved: \"${name}\" for ${device.name}`);\n\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.refreshDeviceStates(device, this.deviceManager!.getDevices());\n }\n\n /**\n * Restore a local snapshot by index.\n *\n * @param device Target device\n * @param val Dropdown index value\n */\n private async handleSnapshotRestore(device: GoveeDevice, val: ioBroker.StateValue): Promise<void> {\n if (!this.localSnapshots || !this.deviceManager) {\n return;\n }\n\n const idx = parseInt(String(val), 10);\n if (idx < 1) {\n return;\n }\n\n const snaps = this.localSnapshots.getSnapshots(device.sku, device.deviceId);\n const snap = snaps[idx - 1];\n if (!snap) {\n this.log.warn(`Local snapshot index ${idx} not found for ${device.name}`);\n return;\n }\n\n this.log.info(`Restoring local snapshot \"${snap.name}\" for ${device.name}`);\n\n // Send each state via LAN \u2192 Cloud routing\n await this.deviceManager.sendCommand(device, \"power\", snap.power);\n if (snap.power) {\n await this.deviceManager.sendCommand(device, \"brightness\", snap.brightness);\n if (snap.colorTemperature > 0) {\n await this.deviceManager.sendCommand(device, \"colorTemperature\", snap.colorTemperature);\n } else {\n await this.deviceManager.sendCommand(device, \"colorRgb\", snap.colorRgb);\n }\n\n // Restore per-segment states via ptReal\n if (snap.segments && snap.segments.length > 0) {\n for (let i = 0; i < snap.segments.length; i++) {\n const seg = snap.segments[i];\n await this.deviceManager.sendCommand(device, `segmentColor:${i}`, seg.color);\n await this.deviceManager.sendCommand(device, `segmentBrightness:${i}`, seg.brightness);\n }\n }\n }\n }\n\n /**\n * Delete a local snapshot by name.\n *\n * @param device Target device\n * @param name Snapshot name to delete\n */\n private handleSnapshotDelete(device: GoveeDevice, name: string): void {\n if (!this.localSnapshots) {\n return;\n }\n\n if (this.localSnapshots.deleteSnapshot(device.sku, device.deviceId, name)) {\n this.log.info(`Local snapshot deleted: \"${name}\" for ${device.name}`);\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.refreshDeviceStates(device, this.deviceManager!.getDevices());\n } else {\n this.log.warn(`Local snapshot \"${name}\" not found for ${device.name}`);\n }\n }\n\n /** Dropdowns whose value is a mode-selection \u2014 reset to \"---\" (0) when the mode stops. */\n private static readonly MODE_DROPDOWNS = [\n \"scenes.light_scene\",\n \"scenes.diy_scene\",\n \"snapshots.snapshot_cloud\",\n \"snapshots.snapshot_local\",\n \"music.music_mode\",\n ];\n\n /** Map command \u2192 its own dropdown path (excluded from reset when that mode is the one that was just activated). */\n private static readonly COMMAND_DROPDOWN: Record<string, string> = {\n lightScene: \"scenes.light_scene\",\n diyScene: \"scenes.diy_scene\",\n snapshot: \"snapshots.snapshot_cloud\",\n snapshotLocal: \"snapshots.snapshot_local\",\n music: \"music.music_mode\",\n colorRgb: \"\",\n colorTemperature: \"\",\n };\n\n /**\n * Reset related dropdown states when switching between scenes/snapshots/colors.\n * Each mode-switch resets all OTHER mode dropdowns to \"---\" (0).\n *\n * @param prefix Device state prefix\n * @param activeCommand The command that was just executed\n */\n private async resetRelatedDropdowns(prefix: string, activeCommand: string): Promise<void> {\n if (!(activeCommand in GoveeAdapter.COMMAND_DROPDOWN)) {\n return;\n }\n const ownDropdown = GoveeAdapter.COMMAND_DROPDOWN[activeCommand];\n await this.resetModeDropdowns(prefix, ownDropdown);\n }\n\n /**\n * Reset every mode dropdown except `keep` (empty = reset all). Used both for\n * mode-switches (keep the new mode's own dropdown) and for power-off\n * (reset everything \u2014 a device that's off has no active mode).\n *\n * @param prefix Device state prefix\n * @param keep Dropdown path to leave untouched (e.g. \"music.music_mode\"), or \"\" to reset all\n */\n private async resetModeDropdowns(prefix: string, keep: string): Promise<void> {\n await Promise.all(\n GoveeAdapter.MODE_DROPDOWNS.filter(d => d !== keep).map(async dropdown => {\n const stateId = `${this.namespace}.${prefix}.${dropdown}`;\n const current = await this.getStateAsync(stateId);\n if (current?.val && current.val !== \"0\" && current.val !== 0) {\n await this.setStateAsync(stateId, { val: \"0\", ack: true });\n }\n }),\n );\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new GoveeAdapter(options);\n} else {\n (() => new GoveeAdapter())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport {\n buildDeviceStateDefs,\n getDefaultLanStates,\n mapCloudStateValue,\n planCloudCapabilityWrites,\n} from \"./lib/capability-mapper\";\nimport { getDeviceTier, initDeviceRegistry } from \"./lib/device-registry\";\nimport { DeviceManager, resolveSegmentCount, SEGMENT_HARD_MAX } from \"./lib/device-manager\";\nimport { GoveeApiClient } from \"./lib/govee-api-client\";\nimport { GoveeCloudClient } from \"./lib/govee-cloud-client\";\nimport { GoveeLanClient } from \"./lib/govee-lan-client\";\nimport { GoveeMqttClient } from \"./lib/govee-mqtt-client\";\nimport { GoveeOpenapiMqttClient } from \"./lib/govee-openapi-mqtt-client\";\nimport { LocalSnapshotStore, type LocalSnapshot, type SnapshotSegment } from \"./lib/local-snapshots\";\nimport { CloudRetryLoop, type CloudRetryHost } from \"./lib/cloud-retry\";\nimport { RateLimiter } from \"./lib/rate-limiter\";\nimport { SegmentWizard, wizardIdleText, type WizardHost, type WizardResult } from \"./lib/segment-wizard\";\nimport { SkuCache } from \"./lib/sku-cache\";\nimport { StateManager } from \"./lib/state-manager\";\nimport {\n hexToRgb,\n parseSegmentList,\n resolveStatesValue,\n rgbIntToHex,\n rgbToHex,\n type AdapterConfig,\n type CloudLoadResult,\n type CloudStateCapability,\n type DeviceState,\n type GoveeDevice,\n type PersistedMqttCredentials,\n} from \"./lib/types\";\n\n/**\n * Rate limit defaults \u2014 full Cloud API budget (8/min, 9000/day). v2 no\n * longer halves this with govee-appliances because that adapter is\n * deprecated and won't run alongside govee-smart.\n */\nconst FULL_LIMITS = { perMinute: 8, perDay: 9000 };\n\n/**\n * Minimum gap between two `mqttAuth: requestCode` calls. Govee returns 200\n * for every request and queues another email \u2014 without a throttle, double-clicking\n * the button in admin would spam the user's inbox. 30 s is short enough that a\n * legitimate retry after a failed delivery still feels responsive.\n */\nconst VERIFICATION_REQUEST_THROTTLE_MS = 30_000;\n\n/**\n * State-suffix \u2192 command-name lookup for writable states. Segment indices\n * are dynamic and handled by regex in stateToCommand \u2014 everything else is\n * a straight string mapping.\n */\nconst STATE_TO_COMMAND: Readonly<Record<string, string>> = {\n \"control.power\": \"power\",\n \"control.brightness\": \"brightness\",\n \"control.colorRgb\": \"colorRgb\",\n \"control.colorTemperature\": \"colorTemperature\",\n \"control.scene\": \"scene\",\n \"control.gradient_toggle\": \"gradientToggle\",\n \"scenes.light_scene\": \"lightScene\",\n \"scenes.diy_scene\": \"diyScene\",\n \"scenes.scene_speed\": \"sceneSpeed\",\n \"music.music_mode\": \"music\",\n \"music.music_sensitivity\": \"music\",\n \"music.music_auto_color\": \"music\",\n \"snapshots.snapshot_cloud\": \"snapshot\",\n \"segments.command\": \"segmentBatch\",\n};\n\nclass GoveeAdapter extends utils.Adapter {\n private deviceManager: DeviceManager | null = null;\n private stateManager: StateManager | null = null;\n private lanClient: GoveeLanClient | null = null;\n private mqttClient: GoveeMqttClient | null = null;\n private openapiMqttClient: GoveeOpenapiMqttClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /** Repeating timer for the App-API poll (sensor-state pull). */\n private appApiPollTimer: ioBroker.Interval | undefined;\n private skuCache: SkuCache | null = null;\n private localSnapshots: LocalSnapshotStore | null = null;\n private cloudWasConnected = false;\n private readyLogged = false;\n private cloudInitDone = false;\n private lanScanDone = false;\n private statesReady = false;\n private stateCreationQueue: Promise<void>[] = [];\n private lanScanTimer: ioBroker.Timeout | undefined;\n private cleanupTimer: ioBroker.Timeout | undefined;\n private readyTimer: ioBroker.Timeout | undefined;\n private cloudRetry: CloudRetryLoop | null = null;\n private segmentWizard: SegmentWizard | null = null;\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** Per-device timestamp of the last diagnostics export \u2014 throttle gate */\n private diagnosticsLastRun = new Map<string, number>();\n /** Cached admin language from system.config \u2014 used for wizard UI text */\n private adminLanguage = \"en\";\n /** Last time `requestCode` was triggered via onMessage \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"govee-smart\" });\n // Per ioBroker rule: async handlers registered on events MUST .catch,\n // otherwise rejections become unhandled \u2192 SIGKILL code 6 \u2192 restart loop.\n this.on(\"ready\", () =>\n this.onReady().catch(e =>\n this.log.error(`onReady crashed: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`),\n ),\n );\n this.on(\"stateChange\", (id, state) =>\n this.onStateChange(id, state).catch(e =>\n this.log.warn(`onStateChange crashed for ${id}: ${e instanceof Error ? e.message : String(e)}`),\n ),\n );\n this.on(\"message\", obj => this.onMessage(obj));\n this.on(\"unload\", callback => this.onUnload(callback));\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler .catch() wrappers cover the\n // direct entry points; this catches whatever slips past them so the\n // adapter logs the cause instead of triggering js-controller SIGKILL.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(\n `Unhandled rejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`,\n );\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.stack ?? err.message}`);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n /** Adapter started \u2014 initialize all channels */\n private async onReady(): Promise<void> {\n const config = this.config as unknown as AdapterConfig;\n\n // info channel + states are declared as instanceObjects in\n // io-package.json, so js-controller materialises them on install /\n // upgrade. We only initialise the runtime values here.\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n await this.setStateAsync(\"info.mqttConnected\", { val: false, ack: true });\n await this.setStateAsync(\"info.cloudConnected\", { val: false, ack: true });\n await this.setStateAsync(\"info.openapiMqttConnected\", {\n val: false,\n ack: true,\n });\n await this.setStateAsync(\"info.refresh_cloud_data\", {\n val: false,\n ack: true,\n });\n // Load admin language from system.config so wizard prose matches the\n // user's Admin UI. Falls back to English on any lookup failure.\n try {\n const sysConf = await this.getForeignObjectAsync(\"system.config\");\n const lang = (sysConf?.common as { language?: string } | undefined)?.language;\n if (typeof lang === \"string\" && lang.length > 0) {\n this.adminLanguage = lang;\n }\n } catch {\n // Keep default \"en\"\n }\n await this.setStateAsync(\"info.wizardStatus\", {\n val: wizardIdleText(this.adminLanguage),\n ack: true,\n });\n\n this.stateManager = new StateManager(this);\n // General groups online state (reflects Cloud connection)\n await this.stateManager.createGroupsOnlineState(false);\n this.deviceManager = new DeviceManager(this.log, this);\n const dataDir = utils.getAbsoluteInstanceDataDir(this);\n\n // Load device registry from devices.json in the adapter package root.\n // Status filter: verified+reported active by default; seed-status entries\n // require the experimentalQuirks config toggle.\n initDeviceRegistry({\n experimental: config.experimentalQuirks === true,\n log: this.log,\n });\n this.skuCache = new SkuCache(dataDir, this.log);\n this.localSnapshots = new LocalSnapshotStore(dataDir, this.log);\n this.deviceManager.setSkuCache(this.skuCache);\n\n // API client for undocumented scene/music/DIY libraries (always available)\n const apiClient = new GoveeApiClient();\n apiClient.setEmail(config.goveeEmail);\n this.deviceManager.setApiClient(apiClient);\n\n this.deviceManager.setCallbacks(\n (device, state) => this.onDeviceStateUpdate(device, state),\n devices => this.onDeviceListChanged(devices),\n );\n\n // Update info.ip when LAN IP changes\n this.deviceManager.onLanIpChanged = (device, ip) => {\n const prefix = this.stateManager!.devicePrefix(device);\n this.setStateAsync(`${prefix}.info.ip`, { val: ip, ack: true }).catch(() => {});\n };\n\n // Sync individual segment states after batch command\n this.deviceManager.onSegmentBatchUpdate = (device, batch) => {\n const prefix = this.stateManager!.devicePrefix(device);\n for (const idx of batch.segments) {\n if (batch.color !== undefined) {\n const hex = rgbIntToHex(batch.color);\n this.setStateAsync(`${prefix}.segments.${idx}.color`, {\n val: hex,\n ack: true,\n }).catch(() => {});\n }\n if (batch.brightness !== undefined) {\n this.setStateAsync(`${prefix}.segments.${idx}.brightness`, {\n val: batch.brightness,\n ack: true,\n }).catch(() => {});\n }\n }\n };\n\n // Sync per-segment states from MQTT BLE status push (AA A5 packets)\n this.deviceManager.onMqttSegmentUpdate = (device, segments) => {\n const prefix = this.stateManager!.devicePrefix(device);\n for (const seg of segments) {\n this.setStateAsync(`${prefix}.segments.${seg.index}.color`, {\n val: rgbToHex(seg.r, seg.g, seg.b),\n ack: true,\n }).catch(() => {});\n this.setStateAsync(`${prefix}.segments.${seg.index}.brightness`, {\n val: seg.brightness,\n ack: true,\n }).catch(() => {});\n }\n };\n\n // When MQTT reveals more segments than the Cloud advertised, rebuild\n // the device's state tree so the extra segments get their datapoints.\n this.deviceManager.onSegmentCountGrown = device => {\n if (!this.stateManager) {\n return;\n }\n this.stateManager.createSegmentStates(device).catch(e => {\n this.log.warn(\n `Failed to rebuild segment tree for ${device.name} after count growth: ${e instanceof Error ? e.message : String(e)}`,\n );\n });\n };\n\n // Log startup with configured channels\n const startChannels: string[] = [\"LAN\"];\n if (config.apiKey) {\n startChannels.push(\"Cloud\");\n }\n if (config.goveeEmail && config.goveePassword) {\n startChannels.push(\"MQTT\");\n }\n this.log.info(`Starting with channels: ${startChannels.join(\", \")} \u2014 please wait...`);\n\n // --- LAN (always active) ---\n this.lanClient = new GoveeLanClient(this.log, this);\n this.deviceManager.setLanClient(this.lanClient);\n\n this.lanClient.start(\n lanDevice => {\n this.deviceManager!.handleLanDiscovery(lanDevice);\n // Poll status only when MQTT is unavailable. With an active MQTT\n // subscription Govee pushes state changes authoritatively, so the\n // LAN devStatus request would be duplicate traffic.\n if (!this.mqttClient?.connected) {\n this.lanClient!.requestStatus(lanDevice.ip);\n }\n },\n (sourceIp, status) => {\n this.deviceManager!.handleLanStatus(sourceIp, status);\n },\n 30_000,\n config.networkInterface || \"\",\n );\n\n // Wait for first LAN scan responses (UDP multicast, devices respond within 1-2s)\n this.lanScanTimer = this.setTimeout(() => {\n this.lanScanDone = true;\n this.checkAllReady();\n }, 3_000);\n\n // --- MQTT (if account credentials provided) ---\n // Initialize MQTT before Cloud so scene library can load on first cycle\n if (config.goveeEmail && config.goveePassword) {\n this.mqttClient = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n\n // Forward every parsed MQTT op.command into the diagnostics ring buffer\n // so diag.export contains the recent packets per device.\n this.mqttClient.setPacketHook((deviceId, topic, hex) => {\n this.deviceManager?.getDiagnostics().addMqttPacket(deviceId, topic, hex);\n });\n\n // 2FA: forward optional code from settings into the next login attempt;\n // clear the field automatically once Govee has accepted it.\n this.mqttClient.setVerificationCode(config.mqttVerificationCode ?? \"\");\n this.mqttClient.setOnVerificationConsumed(() => {\n this.clearVerificationCodeSetting().catch(e => {\n this.log.warn(`Could not clear mqttVerificationCode: ${e instanceof Error ? e.message : String(e)}`);\n });\n });\n this.mqttClient.setOnVerificationFailed(reason => {\n // On 'failed' (455 / 454+code-was-sent) blank the code so the user\n // doesn't keep retrying with a stale value. On 'pending' (454 + no\n // code) we leave the field as-is \u2014 the user is about to fill it.\n if (reason === \"failed\") {\n this.clearVerificationCodeSetting().catch(() => {});\n }\n });\n\n // Re-use cached MQTT credentials across restarts: P12 cert + bearer\n // are encrypted in adapter native. If they're still valid we skip the\n // login flow entirely (and avoid spamming the user with 2FA emails).\n const cachedCreds = this.buildPersistedCredsFromConfig(config);\n if (cachedCreds) {\n this.mqttClient.setPersistedCredentials(cachedCreds);\n }\n this.mqttClient.setOnCredentialsRefresh(creds => {\n this.persistMqttCredentials(creds).catch(e => {\n this.log.warn(`Could not persist MQTT credentials: ${e instanceof Error ? e.message : String(e)}`);\n });\n });\n\n await this.mqttClient.connect(\n update => this.deviceManager!.handleMqttStatus(update),\n connected => {\n this.setStateAsync(\"info.mqttConnected\", {\n val: connected,\n ack: true,\n }).catch(() => {});\n if (connected) {\n this.checkAllReady();\n }\n this.updateConnectionState();\n },\n // Forward every fresh bearer token \u2014 fires on initial login and on\n // each reconnect-login, so the API client never runs with a stale one.\n token => apiClient.setBearerToken(token),\n );\n }\n\n // --- Device data: Cache first, Cloud only on cache miss ---\n const cachedOk = this.deviceManager.loadFromCache();\n\n if (config.apiKey) {\n this.cloudClient = new GoveeCloudClient(config.apiKey, this.log);\n // Capture the most recent Cloud response per (deviceId, endpoint) for\n // diagnostics \u2014 bounded by the DiagnosticsCollector's response slot cap.\n this.cloudClient.setResponseHook((deviceId, endpoint, body) => {\n this.deviceManager?.getDiagnostics().setApiResponse(deviceId, endpoint, body);\n });\n this.deviceManager.setCloudClient(this.cloudClient);\n\n // Bridge synthetic capabilities (App-API, OpenAPI-MQTT events) into the\n // same setState pipeline as polled Cloud state. Keeps mapCloudStateValue\n // as the single source of truth for value coercion + state-id resolution.\n this.deviceManager.setOnCloudCapabilities((device, caps) => {\n this.applyCloudCapabilities(device, caps).catch(e =>\n this.log.warn(\n `applyCloudCapabilities failed for ${device.sku}: ${e instanceof Error ? e.message : String(e)}`,\n ),\n );\n });\n\n this.rateLimiter = new RateLimiter(this.log, this, FULL_LIMITS.perMinute, FULL_LIMITS.perDay);\n this.rateLimiter.start();\n this.deviceManager.setRateLimiter(this.rateLimiter);\n\n // OpenAPI-MQTT \u2014 push channel for appliance/sensor events\n // (lackWater, iceFull, bodyAppeared etc.). API key is enough; no\n // separate credentials required. Connection runs in parallel to\n // the AWS-IoT MQTT used for status push of regular devices.\n this.openapiMqttClient = new GoveeOpenapiMqttClient(config.apiKey, this.log, this);\n this.openapiMqttClient.connect(\n event => this.deviceManager?.handleOpenApiEvent(event),\n connected => {\n this.setStateAsync(\"info.openapiMqttConnected\", {\n val: connected,\n ack: true,\n }).catch(() => {});\n },\n );\n\n // App-API poll \u2014 every 2 minutes, pulls state for sensors like H5179\n // where OpenAPI v2 /device/state returns empty. Bearer token comes\n // from the AWS-IoT MQTT login, so a no-op until that succeeds.\n this.appApiPollTimer = this.setInterval(\n () => {\n this.deviceManager\n ?.pollAppApi()\n .catch(e => this.log.debug(`pollAppApi failed: ${e instanceof Error ? e.message : String(e)}`));\n },\n 2 * 60 * 1000,\n );\n\n if (!cachedOk) {\n // No cache \u2014 first start, fetch from Cloud with 60s hard-timeout.\n // If Cloud hangs/fails, we don't want to block adapter startup indefinitely.\n const result = await this.cloudInitWithTimeout();\n this.cloudWasConnected = result.ok;\n this.ensureCloudRetry().setConnected(result.ok);\n this.setStateAsync(\"info.cloudConnected\", {\n val: result.ok,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(result.ok).catch(() => {});\n\n if (result.ok) {\n await this.loadCloudStates();\n } else {\n this.handleCloudFailure(result);\n }\n } else {\n this.log.info(\"Using cached device data \u2014 no Cloud calls needed\");\n this.cloudWasConnected = true;\n this.ensureCloudRetry().setConnected(true);\n this.setStateAsync(\"info.cloudConnected\", {\n val: true,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(true).catch(() => {});\n }\n // Load group membership from undocumented API (needs bearer token + device map)\n await this.deviceManager.loadGroupMembers();\n\n this.cloudInitDone = true;\n }\n\n // Wait for all state creation from cache/cloud load to complete.\n // Drain-loop: a callback that fires during the await (e.g. a late LAN\n // discovery) can push fresh promises into the queue \u2014 we need to await\n // those too before flipping statesReady, otherwise the initial state\n // tree would be incomplete on very fast startups.\n while (this.stateCreationQueue.length > 0) {\n const pending = this.stateCreationQueue;\n this.stateCreationQueue = [];\n await Promise.all(pending);\n }\n this.statesReady = true;\n\n // Subscribe to all writable device and group states\n await this.subscribeStatesAsync(\"devices.*\");\n await this.subscribeStatesAsync(\"groups.*\");\n await this.subscribeStatesAsync(\"info.refresh_cloud_data\");\n\n // Cleanup stale devices after initial discovery (30s delay for LAN scan).\n // Reaps devices from every adapter-level map that was keyed on them so the\n // process doesn't leak memory across Cloud-side device turnover.\n this.cleanupTimer = this.setTimeout(() => {\n this.reapStaleDevices().catch(e =>\n this.log.debug(`Device cleanup failed: ${e instanceof Error ? e.message : String(e)}`),\n );\n }, 30_000);\n\n this.updateConnectionState();\n\n // Check if all channels are ready \u2014 may already be true if MQTT connected fast\n this.checkAllReady();\n // Safety timeout: log ready even if a channel takes too long.\n // 60s deckt normalen MQTT-Connect + 1 Reconnect-Attempt ab.\n this.readyTimer = this.setTimeout(() => {\n if (!this.readyLogged) {\n this.readyLogged = true;\n this.logDeviceSummary();\n }\n }, 60_000);\n }\n\n /**\n * Initial Cloud-Load mit 60-Sekunden-Hardtimeout.\n * Blockiert nicht l\u00E4nger \u2014 wenn Cloud h\u00E4ngt, geht Adapter mit LAN+MQTT weiter,\n * und der Retry-Loop probiert's passend zum Fehlergrund erneut.\n */\n private async cloudInitWithTimeout(): Promise<CloudLoadResult> {\n if (!this.deviceManager) {\n return { ok: false, reason: \"transient\" };\n }\n const loadPromise = this.deviceManager.loadFromCloud();\n const timeoutPromise = new Promise<CloudLoadResult>(resolve => {\n this.setTimeout(() => resolve({ ok: false, reason: \"transient\" }), 60_000);\n });\n try {\n return await Promise.race([loadPromise, timeoutPromise]);\n } catch {\n return { ok: false, reason: \"transient\" };\n }\n }\n\n /** Build the host object for {@link CloudRetryLoop}. */\n private buildCloudRetryHost(): CloudRetryHost {\n return {\n log: this.log,\n setTimeout: (cb, ms) => this.setTimeout(cb, ms),\n clearTimeout: h => this.clearTimeout(h as ioBroker.Timeout),\n loadFromCloud: () => this.cloudInitWithTimeout(),\n onCloudRestored: async () => {\n this.cloudWasConnected = true;\n this.setStateAsync(\"info.cloudConnected\", {\n val: true,\n ack: true,\n }).catch(() => {});\n this.stateManager?.updateGroupsOnline(true).catch(() => {});\n await this.loadCloudStates();\n },\n };\n }\n\n /** Lazy-initialise the retry loop on first use. */\n private ensureCloudRetry(): CloudRetryLoop {\n if (!this.cloudRetry) {\n this.cloudRetry = new CloudRetryLoop(this.buildCloudRetryHost());\n this.cloudRetry.setConnected(this.cloudWasConnected);\n }\n return this.cloudRetry;\n }\n\n /**\n * React to a Cloud-load outcome \u2014 delegates to {@link CloudRetryLoop}.\n *\n * @param result CloudLoadResult from initial load or retry attempt\n */\n private handleCloudFailure(result: CloudLoadResult): void {\n this.ensureCloudRetry().handleResult(result);\n }\n\n /**\n * React to the user writing `info.refresh_cloud_data = true`. Performs one\n * full Cloud reload cycle so newly created scenes/snapshots from the Govee\n * Home app show up without an adapter restart.\n */\n private async handleManualCloudRefresh(): Promise<void> {\n if (!this.deviceManager || !this.cloudClient) {\n this.log.info(\"Refresh cloud data: no Cloud client configured (API key missing) \u2014 nothing to do\");\n return;\n }\n this.log.info(\"Refresh cloud data: re-fetching scenes and snapshots for all devices\");\n try {\n const changed = await this.deviceManager.refreshSceneData();\n if (changed) {\n await this.loadCloudStates();\n }\n this.log.info(\"Refresh cloud data: done\");\n } catch (e) {\n this.log.warn(`Refresh cloud data failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous.\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n try {\n if (this.lanScanTimer) {\n this.clearTimeout(this.lanScanTimer);\n }\n if (this.cleanupTimer) {\n this.clearTimeout(this.cleanupTimer);\n }\n if (this.readyTimer) {\n this.clearTimeout(this.readyTimer);\n }\n if (this.appApiPollTimer) {\n this.clearInterval(this.appApiPollTimer);\n this.appApiPollTimer = undefined;\n }\n this.cloudRetry?.dispose();\n this.segmentWizard?.dispose();\n this.lanClient?.stop();\n this.mqttClient?.disconnect();\n this.openapiMqttClient?.disconnect();\n this.rateLimiter?.stop();\n // Remove process-level handlers so an adapter restart doesn't stack them.\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n // onUnload MUST be synchronous \u2014 don't await, but silence potential\n // promise rejection during teardown to avoid \"unhandled rejection\" warnings.\n this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {});\n this.setState(\"info.mqttConnected\", { val: false, ack: true }).catch(() => {});\n this.setState(\"info.openapiMqttConnected\", {\n val: false,\n ack: true,\n }).catch(() => {});\n this.setState(\"info.cloudConnected\", { val: false, ack: true }).catch(() => {});\n } catch {\n // ignore\n }\n callback();\n }\n\n /**\n * Handle state changes from user (write operations).\n *\n * @param id State ID\n * @param state New state value\n */\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack || !this.deviceManager || !this.stateManager) {\n return;\n }\n\n // Global refresh button \u2014 triggers one fresh cloud fetch across all\n // devices and re-builds the state tree. Handy after creating a new\n // snapshot in the Govee Home app without restarting the adapter.\n if (id === `${this.namespace}.info.refresh_cloud_data` && state.val) {\n await this.handleManualCloudRefresh();\n await this.setStateAsync(id, { val: false, ack: true });\n return;\n }\n\n // Find which device this state belongs to\n const localId = id.replace(`${this.namespace}.`, \"\");\n if (!localId.startsWith(\"devices.\") && !localId.startsWith(\"groups.\")) {\n return;\n }\n\n const device = this.findDeviceForState(localId);\n if (!device) {\n return;\n }\n\n // Determine command from state suffix after device prefix\n const prefix = this.stateManager.devicePrefix(device);\n const stateSuffix = localId.slice(prefix.length + 1);\n\n // Resolve dropdown input \u2014 accept Number, numeric String, or label\n // (case-insensitive) against the state's common.states map. Returns\n // the canonical key as String so the rest of the handler sees the\n // same shape it always saw (e.g. \"1\"). Non-dropdown states are\n // passed through unchanged.\n const resolved = await this.resolveDropdownInput(id, state.val);\n if (!resolved.ok) {\n this.log.warn(`Unknown dropdown value for ${id}: ${String(state.val)} \u2014 ignoring`);\n return;\n }\n const val = resolved.val;\n\n // Group fan-out: route commands to each member device\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n await this.handleGroupFanOut(device, stateSuffix, val);\n await this.setStateAsync(id, { val, ack: true });\n if (stateSuffix === \"scenes.light_scene\" || stateSuffix === \"music.music_mode\") {\n await this.resetRelatedDropdowns(prefix, stateSuffix === \"scenes.light_scene\" ? \"lightScene\" : \"music\");\n }\n return;\n }\n\n // Handle local snapshot commands (no Cloud/MQTT needed)\n if (stateSuffix === \"snapshots.snapshot_save\" && typeof val === \"string\" && val.trim()) {\n await this.handleSnapshotSave(device, val.trim());\n await this.setStateAsync(id, { val: \"\", ack: true });\n return;\n }\n if (stateSuffix === \"snapshots.snapshot_local\") {\n if (val !== \"0\" && val !== 0) {\n await this.handleSnapshotRestore(device, val);\n await this.resetRelatedDropdowns(prefix, \"snapshotLocal\");\n }\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n if (stateSuffix === \"snapshots.snapshot_delete\" && typeof val === \"string\" && val.trim()) {\n this.handleSnapshotDelete(device, val.trim());\n await this.setStateAsync(id, { val: \"\", ack: true });\n return;\n }\n\n // Manual segments toggle/list \u2014 handler owns the ack because a parse\n // error rewrites manual_mode to false, and an outer ack with the\n // raw value would resurrect the rejected entry.\n if (stateSuffix === \"segments.manual_mode\" || stateSuffix === \"segments.manual_list\") {\n await this.handleManualSegmentsChange(device, stateSuffix, val);\n return;\n }\n\n // Diagnostics export button \u2014 throttled to 2 s per device so a repeated\n // or scripted trigger can't produce a burst of JSON serialisations.\n if (stateSuffix === \"diag.export\" && val) {\n const deviceKey = `${device.sku}:${device.deviceId}`;\n const now = Date.now();\n const last = this.diagnosticsLastRun.get(deviceKey) ?? 0;\n if (now - last < 2000) {\n this.log.debug(`Diagnostics export throttled for ${device.name} \u2014 last run ${now - last}ms ago`);\n await this.setStateAsync(id, { val: false, ack: true });\n return;\n }\n this.diagnosticsLastRun.set(deviceKey, now);\n const diag = this.deviceManager.generateDiagnostics(device, this.version ?? \"unknown\");\n const resultId = `${this.namespace}.${prefix}.diag.result`;\n await this.setStateAsync(resultId, {\n val: JSON.stringify(diag, null, 2),\n ack: true,\n });\n await this.setStateAsync(id, { val: false, ack: true });\n this.log.info(`Diagnostics exported for ${device.name} (${device.sku})`);\n return;\n }\n\n const command = this.stateToCommand(stateSuffix);\n\n if (!command) {\n // Try generic capability routing via state object metadata\n const obj = await this.getObjectAsync(id);\n const capType = obj?.native?.capabilityType;\n const capInstance = obj?.native?.capabilityInstance;\n if (typeof capType === \"string\" && typeof capInstance === \"string\") {\n try {\n await this.deviceManager.sendCapabilityCommand(device, capType, capInstance, val);\n await this.setStateAsync(id, { val, ack: true });\n } catch (err) {\n this.log.warn(`Command failed for ${device.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n } else {\n this.log.debug(`Unknown writable state: ${stateSuffix}`);\n }\n return;\n }\n\n // Dropdown reset to \"---\" (value 0) \u2014 acknowledge without sending command\n if ((command === \"lightScene\" || command === \"diyScene\" || command === \"snapshot\") && (val === \"0\" || val === 0)) {\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n\n // Scene speed: store on device, applied on next scene activation.\n // Persist to SKU cache so the user's choice survives a restart.\n if (command === \"sceneSpeed\") {\n const level = typeof val === \"number\" ? val : parseInt(String(val), 10);\n if (!isNaN(level)) {\n device.sceneSpeed = level;\n this.deviceManager?.persistDeviceToCache(device);\n }\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n\n try {\n // Music mode: combine all music states into one STRUCT command\n if (command === \"music\") {\n // music_mode \"---\" (value 0) \u2014 acknowledge without sending command\n if (stateSuffix === \"music.music_mode\" && (val === \"0\" || val === 0)) {\n await this.setStateAsync(id, { val, ack: true });\n return;\n }\n await this.sendMusicCommand(device, prefix, stateSuffix, val);\n await this.setStateAsync(id, { val, ack: true });\n // Reset scene/snapshot dropdowns when activating music mode\n if (stateSuffix === \"music.music_mode\") {\n await this.resetRelatedDropdowns(prefix, \"music\");\n }\n return;\n }\n\n await this.deviceManager.sendCommand(device, command, val);\n // Optimistic ack\n await this.setStateAsync(id, { val, ack: true });\n // Reset related dropdowns when switching modes.\n // Power-off is a special case \u2014 the device is off, so no mode is\n // active anymore; reset every mode dropdown so the UI reflects the\n // reality (scene/music/snapshot selections are now just history).\n if (command === \"power\" && val === false) {\n await this.resetModeDropdowns(prefix, \"\");\n } else {\n await this.resetRelatedDropdowns(prefix, command);\n }\n } catch (err) {\n this.log.warn(`Command failed for ${device.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n /**\n * Resolve a dropdown-state input value against the state's common.states\n * map. Returns the canonical key (always String form) so a user can write\n * either the index (\"1\"), the index as a number (1) or the label name\n * (\"Aurora\", case-insensitive) \u2014 all three land at the same canonical\n * value for the rest of the handler.\n *\n * Non-dropdown states (no common.states), reset sentinels (0/\"0\"/\"\") and\n * non-string/number inputs are passed through unchanged. A dropdown input\n * that doesn't match any key or label returns ok=false so the caller can\n * warn and skip the command.\n *\n * @param id Full state id\n * @param raw Raw input value as provided by the user/script\n */\n private async resolveDropdownInput(\n id: string,\n raw: ioBroker.StateValue,\n ): Promise<{ val: ioBroker.StateValue; ok: boolean }> {\n if (raw === null || raw === undefined) {\n return { val: raw, ok: true };\n }\n // Reset sentinels \u2014 let the existing branch handle them.\n if (raw === 0 || raw === \"0\" || raw === \"\") {\n return { val: raw, ok: true };\n }\n // Only dropdown candidates have common.states; non-dropdown inputs\n // can't be resolved here so they pass through.\n if (typeof raw !== \"number\" && typeof raw !== \"string\") {\n return { val: raw, ok: true };\n }\n const obj = await this.getObjectAsync(id);\n const states = obj?.common?.states;\n if (!states || typeof states !== \"object\") {\n return { val: raw, ok: true };\n }\n const resolved = resolveStatesValue(raw, states as Record<string, string>);\n if (resolved) {\n return { val: resolved.key, ok: true };\n }\n return { val: raw, ok: false };\n }\n\n /**\n * Build and send a music_setting STRUCT command.\n * Reads sibling music state values and combines them into one API call.\n *\n * @param device Target device\n * @param prefix Device state prefix\n * @param changedSuffix Which music state was changed\n * @param newValue New value for the changed state\n */\n private async sendMusicCommand(\n device: GoveeDevice,\n prefix: string,\n changedSuffix: string,\n newValue: ioBroker.StateValue,\n ): Promise<void> {\n const musicBase = `${this.namespace}.${prefix}.music`;\n\n // Read current sibling values\n const modeState = await this.getStateAsync(`${musicBase}.music_mode`);\n const sensState = await this.getStateAsync(`${musicBase}.music_sensitivity`);\n const autoState = await this.getStateAsync(`${musicBase}.music_auto_color`);\n\n // Apply the changed value, use siblings for the rest\n const musicMode =\n changedSuffix === \"music.music_mode\" ? parseInt(String(newValue), 10) : parseInt(String(modeState?.val ?? 0), 10);\n const sensitivity =\n changedSuffix === \"music.music_sensitivity\" ? (newValue as number) : ((sensState?.val as number) ?? 100);\n const autoColor = changedSuffix === \"music.music_auto_color\" ? (newValue ? 1 : 0) : autoState?.val ? 1 : 0;\n\n if (!musicMode || musicMode === 0) {\n this.log.debug(\"Music mode not selected, skipping command\");\n return;\n }\n\n // LAN first: send via ptReal BLE if device is on LAN\n if (device.lanIp && this.lanClient) {\n // Read current color for RGB-modes (Spectrum=1, Rolling=2)\n let r = 0,\n g = 0,\n b = 0;\n if (musicMode === 1 || musicMode === 2) {\n const colorState = await this.getStateAsync(`${this.namespace}.${prefix}.control.colorRgb`);\n if (colorState?.val && typeof colorState.val === \"string\") {\n ({ r, g, b } = hexToRgb(colorState.val));\n }\n }\n this.lanClient.setMusicMode(device.lanIp, musicMode, r, g, b);\n return;\n }\n\n // Cloud fallback\n const structValue: Record<string, unknown> = {\n musicMode,\n sensitivity,\n autoColor,\n };\n\n await this.deviceManager!.sendCapabilityCommand(\n device,\n \"devices.capabilities.music_setting\",\n \"musicMode\",\n structValue,\n );\n }\n\n /**\n * Called by device-manager when a device state changes\n *\n * @param device Updated device\n * @param state Changed state values\n */\n private onDeviceStateUpdate(device: GoveeDevice, state: Partial<DeviceState>): void {\n if (this.stateManager) {\n this.stateManager.updateDeviceState(device, state).catch(() => {});\n }\n this.updateConnectionState();\n\n // Update group reachability when member online status changes\n if (state.online !== undefined) {\n this.updateGroupReachability();\n }\n\n // Mirror power-off to mode-dropdown reset. Covers MQTT/LAN-initiated\n // power changes (Govee app or physical remote) so the UI stays honest:\n // a device that's off can't be \"playing Aurora-A\" anymore.\n if (state.power === false && this.stateManager) {\n const prefix = this.stateManager.devicePrefix(device);\n this.resetModeDropdowns(prefix, \"\").catch(() => undefined);\n }\n }\n\n /**\n * Fan out a group command to all member devices.\n * Basic controls (power, brightness, color) are sent directly.\n * Scenes/music are matched by name across members.\n *\n * @param group BaseGroup device\n * @param stateSuffix State path suffix (e.g. \"control.power\")\n * @param value Command value\n */\n private async handleGroupFanOut(group: GoveeDevice, stateSuffix: string, value: ioBroker.StateValue): Promise<void> {\n if (!this.deviceManager || !group.groupMembers) {\n return;\n }\n\n const devices = this.deviceManager.getDevices();\n const members = this.resolveGroupMembers(group, devices).filter(d => d.state.online);\n\n if (members.length === 0) {\n this.log.debug(`Group \"${group.name}\": no reachable members for fan-out`);\n return;\n }\n\n const command = this.stateToCommand(stateSuffix);\n if (!command) {\n return;\n }\n\n // Dropdown reset \u2014 no command needed\n if ((command === \"lightScene\" || command === \"music\") && (value === \"0\" || value === 0)) {\n return;\n }\n\n for (const member of members) {\n try {\n if (command === \"lightScene\") {\n await this.fanOutScene(group, member, value);\n } else if (command === \"music\") {\n await this.fanOutMusic(group, member, stateSuffix, value);\n } else {\n await this.deviceManager.sendCommand(member, command, value);\n }\n } catch (err) {\n this.log.debug(`Group fan-out to ${member.name}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n }\n\n /**\n * Fan out a scene command: match group scene name to member scene index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param value Dropdown index value\n */\n private async fanOutScene(group: GoveeDevice, member: GoveeDevice, value: ioBroker.StateValue): Promise<void> {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n\n // Get group scene name from dropdown value (1-based index)\n const groupPrefix = this.stateManager.devicePrefix(group);\n const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.scenes.light_scene`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const sceneName = groupStates?.[String(value)];\n if (!sceneName) {\n return;\n }\n\n // Find the same scene name in the member's scene list (1-based)\n const memberIdx = member.scenes.findIndex(s => s.name === sceneName);\n if (memberIdx >= 0) {\n await this.deviceManager.sendCommand(member, \"lightScene\", memberIdx + 1);\n }\n }\n\n /**\n * Fan out a music command: match group music name to member music index.\n *\n * @param group BaseGroup device\n * @param member Target member device\n * @param stateSuffix Music state path suffix\n * @param value Command value\n */\n private async fanOutMusic(\n group: GoveeDevice,\n member: GoveeDevice,\n stateSuffix: string,\n value: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n\n // For sensitivity/auto_color, forward directly \u2014 these are numeric values\n if (stateSuffix !== \"music.music_mode\") {\n await this.sendMusicCommand(member, this.stateManager.devicePrefix(member), stateSuffix, value);\n return;\n }\n\n // Get group music name from dropdown value (1-based index)\n const groupPrefix = this.stateManager.devicePrefix(group);\n const obj = await this.getObjectAsync(`${this.namespace}.${groupPrefix}.music.music_mode`);\n const groupStates = obj?.common?.states as Record<string, string> | undefined;\n const musicName = groupStates?.[String(value)];\n if (!musicName) {\n return;\n }\n\n // Find the same music name in the member's music library (1-based)\n const memberIdx = member.musicLibrary.findIndex(m => m.name === musicName);\n if (memberIdx >= 0) {\n // Build the music command struct for the member\n const memberPrefix = this.stateManager.devicePrefix(member);\n // Temporarily write the music mode value to trigger the member's music command\n await this.sendMusicCommand(member, memberPrefix, \"music.music_mode\", memberIdx + 1);\n }\n }\n\n /**\n * Resolve group member references to actual device objects.\n *\n * @param group BaseGroup device with groupMembers\n * @param devices Full device list to search\n */\n private resolveGroupMembers(group: GoveeDevice, devices: GoveeDevice[]): GoveeDevice[] {\n if (!group.groupMembers) {\n return [];\n }\n return group.groupMembers\n .map(m => devices.find(d => d.sku === m.sku && d.deviceId === m.deviceId))\n .filter((d): d is GoveeDevice => d !== undefined);\n }\n\n /**\n * Recalculate info.membersUnreachable for all groups.\n * Called when any device's online status changes.\n */\n private updateGroupReachability(): void {\n if (!this.deviceManager || !this.stateManager) {\n return;\n }\n const devices = this.deviceManager.getDevices();\n for (const group of devices) {\n if (group.sku !== \"BaseGroup\" || !group.groupMembers) {\n continue;\n }\n const memberDevices = this.resolveGroupMembers(group, devices);\n this.stateManager.updateGroupMembersUnreachable(group, memberDevices).catch(() => {});\n }\n }\n\n /**\n * Rebuild state definitions for one device and feed them into StateManager.\n * Used both from the full-list callback and from targeted refreshes\n * (e.g. after a local snapshot was added or removed \u2014 no reason to rebuild\n * the entire tree for every device then).\n *\n * @param device Target device\n * @param allDevices Full device list (needed to resolve group members)\n */\n private refreshDeviceStates(device: GoveeDevice, allDevices: GoveeDevice[]): void {\n if (!this.stateManager) {\n return;\n }\n const localSnaps = this.localSnapshots?.getSnapshots(device.sku, device.deviceId);\n let memberDevices: GoveeDevice[] | undefined;\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n memberDevices = this.resolveGroupMembers(device, allDevices);\n }\n const stateDefs = buildDeviceStateDefs(device, localSnaps, memberDevices);\n const p = this.stateManager\n .createDeviceStates(device, stateDefs)\n .then(async () => {\n // v2.1.0 \u2192 v2.1.1 layout migration: drop legacy info.diagnostics_*\n // before publishing the new diag.tier value. Idempotent.\n await this.stateManager?.migrateLegacyDiagnostics(device);\n await this.stateManager?.updateDeviceTier(device, getDeviceTier(device.sku));\n })\n .catch(e => {\n this.log.error(`createDeviceStates failed for ${device.name}: ${e instanceof Error ? e.message : String(e)}`);\n });\n // Until ready, collect so onReady can await the whole initial batch.\n // After ready, fire-and-forget \u2014 the queue would otherwise keep growing\n // with resolved promises for the lifetime of the adapter.\n if (!this.statesReady) {\n this.stateCreationQueue.push(p);\n } else {\n void p;\n }\n }\n\n /**\n * Called by device-manager when the device list changes\n *\n * @param devices Current list of all devices\n */\n private onDeviceListChanged(devices: GoveeDevice[]): void {\n if (!this.stateManager) {\n return;\n }\n\n for (const device of devices) {\n this.refreshDeviceStates(device, devices);\n }\n\n this.updateConnectionState();\n // Cache sync happens once after the initial setup completes (see\n // checkAllReady) \u2014 triggering here would fire on every device update\n // and spam the log.\n\n // Keep adapter-level per-device maps (diagnosticsLastRun, ...) aligned\n // with the new device list so removed devices don't leave orphan keys.\n // Skip during the initial boot phase \u2014 the startup cleanupTimer handles\n // that pass with proper LAN-scan-settled timing.\n if (this.statesReady) {\n this.reapStaleDevices().catch(() => undefined);\n }\n }\n\n /** Update global info.connection */\n private updateConnectionState(): void {\n const devices = this.deviceManager?.getDevices() ?? [];\n const hasDevices = devices.length > 0;\n const anyOnline = devices.some(d => d.state.online);\n const lanRunning = this.lanClient !== null;\n const connected = hasDevices ? anyOnline : lanRunning;\n this.setStateAsync(\"info.connection\", { val: connected, ack: true }).catch(() => {});\n }\n\n /**\n * Delete ioBroker objects for devices no longer present and drop the same\n * devices from adapter-level maps. Called after the initial-discovery\n * window and every time the device list changes.\n *\n * Scope of \"stale\" today: cleanupDevices compares the ioBroker object tree\n * against the live device-manager registry \u2014 it deletes objects that\n * outlive their entry in `DeviceManager.devices`. In v2.0 that registry is\n * monotonically growing within a single adapter lifetime (entries only\n * leave via cache pruning across restarts), so this primarily catches\n * tree leftovers from a previous adapter version after upgrade. The\n * adapter-level `diagnosticsLastRun` map is also reaped so it can't outlive\n * its devices either.\n *\n * A future stale-pruning step that explicitly retires devices from the\n * device-manager registry should also drop the device from\n * `deviceManager.devices` and call `getDiagnostics().forget(deviceId)` for\n * each retired device \u2014 those reaping APIs come in with the pruning patch,\n * not before (Memory `feedback_kein_phantom_schema`).\n */\n private async reapStaleDevices(): Promise<void> {\n if (!this.stateManager || !this.deviceManager) {\n return;\n }\n const currentDevices = this.deviceManager.getDevices();\n await this.stateManager.cleanupDevices(currentDevices);\n\n const liveKeys = new Set(currentDevices.map(d => `${d.sku}:${d.deviceId}`));\n for (const key of this.diagnosticsLastRun.keys()) {\n if (!liveKeys.has(key)) {\n this.diagnosticsLastRun.delete(key);\n }\n }\n }\n\n /**\n * Check if all configured channels are initialized and log ready message.\n * Called from MQTT onConnection callback and end of onReady.\n */\n private checkAllReady(): void {\n if (this.readyLogged) {\n return;\n }\n // Wait for first LAN scan (always active)\n if (!this.lanScanDone) {\n return;\n }\n // Wait for initial state creation to complete\n if (!this.statesReady) {\n return;\n }\n // Wait for Cloud init if configured\n if (this.cloudClient && !this.cloudInitDone) {\n return;\n }\n // Wait for MQTT connection if configured\n if (this.mqttClient && !this.mqttClient.connected) {\n return;\n }\n this.readyLogged = true;\n this.logDeviceSummary();\n // Persist any learned changes from the initial load (e.g. resolveSegmentCount\n // collapsing Cloud's 15 to the real 10 on H70D1). One-shot on first ready;\n // subsequent mutations persist themselves (MQTT bumps, wizard, manual-mode).\n this.deviceManager?.saveDevicesToCache();\n }\n\n /**\n * Log final ready message with device/group/channel summary.\n */\n private logDeviceSummary(): void {\n if (!this.deviceManager) {\n return;\n }\n const all = this.deviceManager.getDevices();\n const devices = all.filter(d => d.sku !== \"BaseGroup\");\n const groups = all.filter(d => d.sku === \"BaseGroup\");\n\n const channels: string[] = [\"LAN\"];\n if (this.cloudWasConnected) {\n channels.push(\"Cloud\");\n }\n if (this.mqttClient?.connected) {\n channels.push(\"MQTT\");\n }\n\n // If a channel is configured but not connected, note it honestly \u2014\n // background reconnect will continue and log \"restored\" on success.\n const pending: string[] = [];\n if (this.cloudClient && !this.cloudWasConnected) {\n pending.push(\"Cloud\");\n }\n if (this.mqttClient && !this.mqttClient.connected) {\n pending.push(\"MQTT\");\n }\n const pendingNote =\n pending.length > 0 ? `, ${pending.join(\"+\")} noch im Aufbau \u2014 wird im Hintergrund fortgesetzt` : \"\";\n\n if (devices.length === 0 && groups.length === 0) {\n this.log.info(`Govee adapter ready \u2014 no devices found (channels: ${channels.join(\"+\")}${pendingNote})`);\n return;\n }\n\n // Summary line\n const parts: string[] = [];\n if (devices.length > 0) {\n parts.push(`${devices.length} device${devices.length > 1 ? \"s\" : \"\"}`);\n }\n if (groups.length > 0) {\n parts.push(`${groups.length} group${groups.length > 1 ? \"s\" : \"\"}`);\n }\n this.log.info(`Govee adapter ready \u2014 ${parts.join(\", \")} (channels: ${channels.join(\"+\")}${pendingNote})`);\n }\n\n /**\n * Load current state for all Cloud devices and populate state values.\n * Called once after initial Cloud device list load.\n */\n private async loadCloudStates(): Promise<void> {\n if (!this.cloudClient || !this.deviceManager || !this.stateManager) {\n return;\n }\n\n const devices = this.deviceManager.getDevices();\n // LAN-first: never overwrite LAN states with Cloud values\n const lanStateIds = new Set(getDefaultLanStates().map(s => s.id));\n let loaded = 0;\n\n for (const device of devices) {\n if (!device.channels.cloud || device.capabilities.length === 0) {\n continue;\n }\n\n try {\n const caps = await this.cloudClient.getDeviceState(device.sku, device.deviceId);\n const prefix = this.stateManager.devicePrefix(device);\n\n const writes: Promise<unknown>[] = [];\n for (const cap of caps) {\n const mapped = mapCloudStateValue(cap);\n if (!mapped) {\n continue;\n }\n // Skip LAN-covered states for LAN-capable devices\n if (device.lanIp && lanStateIds.has(mapped.stateId)) {\n continue;\n }\n const statePath = this.stateManager.resolveStatePath(prefix, mapped.stateId);\n // Fire-and-forget \u2014 States are created before loadCloudStates runs;\n // a rejection here means the state was deleted out-of-band and\n // can be safely ignored.\n writes.push(\n this.setStateAsync(statePath, {\n val: mapped.value,\n ack: true,\n }).catch(() => undefined),\n );\n }\n await Promise.all(writes);\n loaded++;\n } catch {\n this.log.debug(`Could not load Cloud state for ${device.name} (${device.sku})`);\n }\n }\n\n if (loaded > 0) {\n this.log.debug(`Cloud states loaded for ${loaded} devices`);\n }\n }\n\n /**\n * Apply a list of synthesized Cloud-state capabilities to a single\n * device \u2014 the App-API poll and OpenAPI-MQTT events both use this path\n * so their values flow through the same `mapCloudStateValue` pipeline\n * that polled Cloud states use.\n *\n * @param device Target Govee device\n * @param caps Capabilities to apply\n */\n private async applyCloudCapabilities(device: GoveeDevice, caps: CloudStateCapability[]): Promise<void> {\n if (!this.stateManager) {\n return;\n }\n const lanStateIds = new Set(getDefaultLanStates().map(s => s.id));\n const prefix = this.stateManager.devicePrefix(device);\n const planned = planCloudCapabilityWrites(caps, Boolean(device.lanIp), lanStateIds);\n // App-API and OpenAPI-MQTT deliver state IDs (battery, temperature,\n // humidity, lackWater, \u2026) that the Cloud-capability pipeline doesn't\n // declare for sensor/appliance SKUs \u2014 the state objects therefore\n // don't exist yet on first write. ensureSyntheticStateObject creates\n // them lazily with the right channel + role + unit.\n for (const mapped of planned) {\n await this.stateManager.ensureSyntheticStateObject(prefix, mapped.stateId);\n }\n const writes = planned.map(mapped => {\n const statePath = this.stateManager!.resolveStatePath(prefix, mapped.stateId);\n return this.setStateAsync(statePath, {\n val: mapped.value,\n ack: true,\n }).catch(() => undefined);\n });\n await Promise.all(writes);\n }\n\n /**\n * Find device for a state ID\n *\n * @param localId Local state ID without namespace prefix\n */\n private findDeviceForState(localId: string): GoveeDevice | undefined {\n if (!this.deviceManager || !this.stateManager) {\n return undefined;\n }\n\n for (const device of this.deviceManager.getDevices()) {\n const prefix = this.stateManager.devicePrefix(device);\n if (localId.startsWith(`${prefix}.`)) {\n return device;\n }\n }\n return undefined;\n }\n\n /**\n * Map state suffix to command name.\n *\n * Simple suffixes live in a lookup table, segment indices need regex\n * extraction because they're dynamic. The three music states all route\n * to the same \"music\" command \u2014 the handler reads sibling values.\n *\n * @param suffix State ID suffix (e.g. \"power\", \"brightness\")\n */\n private stateToCommand(suffix: string): string | null {\n const direct = STATE_TO_COMMAND[suffix];\n if (direct) {\n return direct;\n }\n const segColorMatch = /^segments\\.(\\d+)\\.color$/.exec(suffix);\n if (segColorMatch) {\n return `segmentColor:${segColorMatch[1]}`;\n }\n const segBrightMatch = /^segments\\.(\\d+)\\.brightness$/.exec(suffix);\n if (segBrightMatch) {\n return `segmentBrightness:${segBrightMatch[1]}`;\n }\n return null;\n }\n\n /**\n * Central entry point for manual-segment updates. Sets the device flags,\n * rebuilds the segment tree (which writes manual_mode + manual_list with\n * ack=true), and persists to cache. Both the user state-change handler\n * and the wizard route their final decisions here.\n *\n * @param device Target device\n * @param mode Whether manual mode should be active\n * @param indices Physical indices when mode=true, ignored otherwise\n */\n private async applyManualSegments(device: GoveeDevice, mode: boolean, indices?: number[]): Promise<void> {\n if (!this.stateManager) {\n return;\n }\n device.manualMode = mode;\n device.manualSegments = mode && Array.isArray(indices) && indices.length > 0 ? indices.slice() : undefined;\n await this.stateManager.createSegmentStates(device);\n this.deviceManager?.persistDeviceToCache(device);\n }\n\n /**\n * React to manual-segments state changes \u2014 parses list, forwards to\n * {@link applyManualSegments}. On parse error disables manual mode so the\n * rejected value doesn't survive in the state tree.\n *\n * @param device Target device\n * @param suffix State suffix (either \"segments.manual_mode\" or \"segments.manual_list\")\n * @param newValue Written value\n */\n private async handleManualSegmentsChange(device: GoveeDevice, suffix: string, newValue: unknown): Promise<void> {\n // Peer value that wasn't just written comes from the device object\n // (kept in sync via createSegmentStates), not a separate state read.\n const modeVal = suffix === \"segments.manual_mode\" ? Boolean(newValue) : device.manualMode === true;\n const listVal =\n suffix === \"segments.manual_list\"\n ? typeof newValue === \"string\"\n ? newValue\n : \"\"\n : Array.isArray(device.manualSegments)\n ? device.manualSegments.join(\",\")\n : \"\";\n\n if (!modeVal) {\n this.log.info(`${device.name}: manual segments disabled \u2014 strip treated as contiguous`);\n await this.applyManualSegments(device, false);\n return;\n }\n\n // Upper bound: cap at the real length if known, otherwise the protocol limit.\n // Real length can still grow via MQTT discovery, so SEGMENT_HARD_MAX is the\n // absolute safety net.\n const maxIndex =\n typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount - 1 : SEGMENT_HARD_MAX;\n const parsed = parseSegmentList(listVal, maxIndex);\n if (parsed.error) {\n this.log.warn(`${device.name}: manual_list invalid (${parsed.error}) \u2014 disabling manual mode`);\n await this.applyManualSegments(device, false);\n return;\n }\n\n this.log.info(`${device.name}: manual segments active \u2014 ${parsed.indices.length} physical indices (${listVal})`);\n await this.applyManualSegments(device, true, parsed.indices);\n }\n\n // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Segment-Detection-Wizard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Handle incoming sendTo messages (from jsonConfig).\n *\n * @param obj ioBroker message object\n */\n private onMessage(obj: ioBroker.Message): void {\n if (!obj?.command) {\n return;\n }\n // Never let a rejection bubble up from the event handler \u2014 the ioBroker\n // event emitter doesn't catch it, which would crash the adapter.\n this.handleMessage(obj).catch(e => {\n this.log.warn(`onMessage handler crashed for ${obj.command}: ${e instanceof Error ? e.message : String(e)}`);\n this.sendMessageResponse(obj, {\n error: e instanceof Error ? e.message : String(e),\n });\n });\n }\n\n private async handleMessage(obj: ioBroker.Message): Promise<void> {\n try {\n if (obj.command === \"getSegmentDevices\") {\n const devices = this.deviceManager?.getDevices() ?? [];\n const list = devices\n .filter(d => d.sku !== \"BaseGroup\" && d.state?.online === true && resolveSegmentCount(d) > 0)\n .map(d => {\n const count = resolveSegmentCount(d);\n return {\n value: this.deviceKeyFor(d),\n label: `${d.name} (${d.sku}, bisher ${count} Segmente)`,\n };\n });\n // selectSendTo expects the array directly, not wrapped in an object\n this.sendMessageResponse(obj, list);\n return;\n }\n if (obj.command === \"segmentWizard\") {\n const payload = (obj.message ?? {}) as {\n action?: string;\n device?: string;\n };\n const response = await this.runWizardStep(payload.action ?? \"\", payload.device ?? \"\");\n this.sendMessageResponse(obj, response);\n return;\n }\n if (obj.command === \"mqttAuth\") {\n const payload = (obj.message ?? {}) as { action?: string };\n const response = await this.runMqttAuthAction(payload.action ?? \"\");\n this.sendMessageResponse(obj, response);\n return;\n }\n } catch (e) {\n this.log.warn(`onMessage failed for ${obj.command}: ${e instanceof Error ? e.message : String(e)}`);\n this.sendMessageResponse(obj, {\n error: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n /**\n * Handle the `mqttAuth` onMessage commands triggered by the admin UI.\n *\n * Two actions:\n * - `test` \u2014 try a one-shot login with the current settings (incl. any code) and\n * report a single-line plaintext result the admin can show as a toast.\n * - `requestCode` \u2014 POST to /account/rest/account/v1/verification so Govee emails a fresh\n * code. 30-second in-memory throttle prevents double-click email spam.\n *\n * @param action Action name from the jsonConfig sendTo button\n */\n private async runMqttAuthAction(action: string): Promise<{ result: string }> {\n const config = this.config as unknown as AdapterConfig;\n if (!config.goveeEmail || !config.goveePassword) {\n return { result: \"Email + Passwort in den Adapter-Einstellungen n\u00F6tig.\" };\n }\n\n if (action === \"test\") {\n // One-shot client: don't reuse this.mqttClient \u2014 we don't want this\n // probe to take over its reconnect timer or auth-failure counter.\n const probe = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n probe.setVerificationCode(config.mqttVerificationCode ?? \"\");\n try {\n let connected = false;\n await probe.connect(\n () => {},\n isConnected => {\n connected = isConnected;\n },\n );\n probe.disconnect();\n return {\n result: connected\n ? \"Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv.\"\n : \"Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten.\",\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n if (/Verification required/i.test(msg)) {\n return {\n result:\n \"Govee verlangt 2-Faktor-Best\u00E4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern.\",\n };\n }\n if (/Verification code invalid/i.test(msg)) {\n return {\n result: \"2-Faktor-Code ung\u00FCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern.\",\n };\n }\n if (/email not registered/i.test(msg)) {\n return { result: \"Diese E-Mail ist bei Govee nicht registriert.\" };\n }\n if (/Login failed/i.test(msg)) {\n return { result: \"Passwort wurde von Govee abgelehnt.\" };\n }\n if (/Rate limited/i.test(msg)) {\n return { result: \"Govee meldet Rate-Limit \u2014 bitte sp\u00E4ter erneut versuchen.\" };\n }\n if (/Account temporarily locked/i.test(msg)) {\n return { result: \"Govee-Account vor\u00FCbergehend gesperrt \u2014 Govee Home App \u00F6ffnen und Status pr\u00FCfen.\" };\n }\n return { result: `Login fehlgeschlagen: ${msg}` };\n }\n }\n\n if (action === \"requestCode\") {\n const now = Date.now();\n if (now - this.lastVerificationRequestMs < VERIFICATION_REQUEST_THROTTLE_MS) {\n const wait = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1000);\n return { result: `Bitte ${wait}s warten \u2014 gerade wurde schon ein Code angefordert.` };\n }\n this.lastVerificationRequestMs = now;\n const probe = new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n try {\n await probe.requestVerificationCode();\n return { result: \"Code wurde an deine Govee-E-Mail-Adresse gesendet (Spam-Ordner pr\u00FCfen).\" };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return { result: `Govee hat den Code-Versand abgelehnt: ${msg}` };\n }\n }\n\n return { result: `Unbekannte Aktion '${action}'.` };\n }\n\n /**\n * Helper: clear `mqttVerificationCode` in adapter native after a successful\n * login or a 455-fail. Triggers a settings-reload by the admin UI but the\n * adapter restart is gentle (only the field changed, not the connection).\n */\n private async clearVerificationCodeSetting(): Promise<void> {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { mqttVerificationCode: \"\" },\n });\n }\n\n /**\n * Read the persisted MQTT bundle out of adapter native. Returns null if\n * any required field is missing \u2014 the caller falls back to a fresh login.\n *\n * v2.1.0 had `encryptedNative` / `protectedNative` listed under\n * `/common` (schema-rejected) so the new MQTT bundle was written as\n * plaintext. v2.1.1 moved them to root, so js-controller now tries to\n * decrypt the previously plaintext-stored bytes \u2014 yielding garbage.\n * Detect that here by trying to base64-decode the P12 cert: if it\n * fails or the decoded length looks wrong, the bundle is unusable.\n * `null` return triggers fresh login + the caller wipes native.\n *\n * @param config Adapter native settings\n */\n private buildPersistedCredsFromConfig(config: AdapterConfig): PersistedMqttCredentials | null {\n const bearer = config.mqttBearerToken ?? \"\";\n const endpoint = config.mqttIotEndpoint ?? \"\";\n const p12 = config.mqttP12Cert ?? \"\";\n const p12Pass = config.mqttP12Pass ?? \"\";\n const accountId = config.mqttAccountId ?? \"\";\n const accountTopic = config.mqttAccountTopic ?? \"\";\n const expiresAt = Number(config.mqttTokenExpiresAt ?? 0);\n if (!bearer || !endpoint || !p12 || !accountId || !accountTopic || !expiresAt) {\n return null;\n }\n // Sanity-check the P12 cert: if base64-decode produces something tiny\n // or the bytes don't look like ASN.1 (P12 starts with a SEQUENCE\n // tag 0x30), the field was probably plaintext-stored under v2.1.0\n // and js-controller's decrypt step turned it into noise. Wipe it.\n try {\n const decoded = Buffer.from(p12, \"base64\");\n if (decoded.length < 100 || decoded[0] !== 0x30) {\n this.log.info(\n \"MQTT: stored credentials look corrupted (likely a v2.1.0 plaintext-storage leftover) \u2014 clearing and forcing fresh login.\",\n );\n void this.persistMqttCredentials({\n bearerToken: \"\",\n iotEndpoint: \"\",\n p12Cert: \"\",\n p12Pass: \"\",\n accountId: \"\",\n accountTopic: \"\",\n tokenExpiresAt: 0,\n });\n return null;\n }\n } catch {\n void this.persistMqttCredentials({\n bearerToken: \"\",\n iotEndpoint: \"\",\n p12Cert: \"\",\n p12Pass: \"\",\n accountId: \"\",\n accountTopic: \"\",\n tokenExpiresAt: 0,\n });\n return null;\n }\n return {\n bearerToken: bearer,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId,\n accountTopic,\n tokenExpiresAt: expiresAt,\n };\n }\n\n /**\n * Persist freshly-issued MQTT credentials back into adapter native. The\n * P12 cert + bearer are listed in `encryptedNative` / `protectedNative`\n * (io-package.json) so js-controller transparently encrypts them at rest.\n *\n * @param creds The freshly-issued MQTT bundle from a successful login\n */\n private async persistMqttCredentials(creds: PersistedMqttCredentials): Promise<void> {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: {\n mqttBearerToken: creds.bearerToken,\n mqttIotEndpoint: creds.iotEndpoint,\n mqttP12Cert: creds.p12Cert,\n mqttP12Pass: creds.p12Pass,\n mqttAccountId: creds.accountId,\n mqttAccountTopic: creds.accountTopic,\n mqttTokenExpiresAt: creds.tokenExpiresAt,\n },\n });\n }\n\n private sendMessageResponse(obj: ioBroker.Message, data: unknown): void {\n if (obj.callback && obj.from) {\n this.sendTo(obj.from, obj.command, data as Record<string, unknown>, obj.callback);\n }\n }\n\n /**\n * Stable device key for wizard session tracking.\n *\n * @param device Target device\n */\n private deviceKeyFor(device: GoveeDevice): string {\n return `${device.sku}:${device.deviceId}`;\n }\n\n private findDeviceByKey(key: string): GoveeDevice | undefined {\n const devices = this.deviceManager?.getDevices() ?? [];\n return devices.find(d => this.deviceKeyFor(d) === key);\n }\n\n /** Construct the host object passed into SegmentWizard. */\n private buildWizardHost(): WizardHost {\n return {\n log: this.log,\n getState: id => this.getStateAsync(id),\n sendCommand: async (device, command, value) => {\n await this.deviceManager?.sendCommand(device, command, value);\n },\n flashSegmentAtomic: (device, idx) => {\n if (!device.lanIp || !this.lanClient) {\n return Promise.resolve(false);\n }\n this.lanClient.flashSingleSegment(device.lanIp, idx);\n return Promise.resolve(true);\n },\n restoreStripAtomic: (device, total, color, brightness) => {\n if (!device.lanIp || !this.lanClient) {\n return Promise.resolve(false);\n }\n const r = (color >> 16) & 0xff;\n const g = (color >> 8) & 0xff;\n const b = color & 0xff;\n this.lanClient.restoreAllSegments(device.lanIp, total, r, g, b, brightness);\n return Promise.resolve(true);\n },\n findDevice: key => this.findDeviceByKey(key),\n namespace: this.namespace,\n devicePrefix: device => this.stateManager?.devicePrefix(device) ?? \"\",\n setTimeout: (cb, ms) => this.setTimeout(cb, ms),\n clearTimeout: h => this.clearTimeout(h as ioBroker.Timeout),\n applyWizardResult: (device, result) => this.applyWizardResult(device, result),\n getLanguage: () => this.adminLanguage,\n };\n }\n\n /**\n * Apply a finished wizard's measurement: set the real segment count, then\n * route through {@link applyManualSegments} so the same state-tree rebuild\n * and cache-persist path runs for both wizard results and user edits.\n *\n * @param device Target device\n * @param result Wizard's measurement\n */\n private async applyWizardResult(device: GoveeDevice, result: WizardResult): Promise<void> {\n device.segmentCount = result.segmentCount;\n if (result.hasGaps) {\n const parsed = parseSegmentList(result.manualList, result.segmentCount - 1);\n await this.applyManualSegments(device, true, parsed.error ? undefined : parsed.indices);\n } else {\n await this.applyManualSegments(device, false);\n }\n this.log.debug(\n `applyWizardResult: ${device.sku} \u2192 segmentCount=${result.segmentCount}, ` +\n `manualMode=${device.manualMode}, list=\"${result.manualList}\"`,\n );\n }\n\n /**\n * Execute one wizard step (start/yes/no/abort). Delegates to\n * {@link SegmentWizard} \u2014 see `lib/segment-wizard.ts`.\n *\n * @param action \"start\" | \"yes\" | \"no\" | \"abort\"\n * @param deviceKey device identifier (only required for \"start\")\n */\n private async runWizardStep(action: string, deviceKey: string): Promise<Record<string, unknown>> {\n if (!this.segmentWizard) {\n this.segmentWizard = new SegmentWizard(this.buildWizardHost());\n }\n const response = await this.segmentWizard.runStep(action, deviceKey);\n // Mirror the current wizard status into a plain state so admin's\n // `type: \"state\"` component can show it live via state subscription.\n const statusText = this.segmentWizard.getStatusText();\n await this.setStateAsync(\"info.wizardStatus\", {\n val: statusText,\n ack: true,\n });\n return response;\n }\n\n /**\n * Save current device state as a local snapshot.\n *\n * @param device Target device\n * @param name Snapshot name\n */\n private async handleSnapshotSave(device: GoveeDevice, name: string): Promise<void> {\n if (!this.localSnapshots || !this.stateManager) {\n return;\n }\n\n const prefix = this.stateManager.devicePrefix(device);\n const ns = this.namespace;\n\n // Read device-level state in parallel\n const [powerState, brightState, colorState, ctState] = await Promise.all([\n this.getStateAsync(`${ns}.${prefix}.control.power`),\n this.getStateAsync(`${ns}.${prefix}.control.brightness`),\n this.getStateAsync(`${ns}.${prefix}.control.colorRgb`),\n this.getStateAsync(`${ns}.${prefix}.control.colorTemperature`),\n ]);\n\n // Read per-segment states in parallel \u2014 20 segments \u00D7 2 reads used to run\n // sequentially (~80ms), parallel completes in a single round-trip.\n let segments: SnapshotSegment[] | undefined;\n const segCount = device.segmentCount ?? 0;\n if (segCount > 0) {\n const segReads: Promise<[ioBroker.State | null | undefined, ioBroker.State | null | undefined]>[] = [];\n for (let i = 0; i < segCount; i++) {\n segReads.push(\n Promise.all([\n this.getStateAsync(`${ns}.${prefix}.segments.${i}.color`),\n this.getStateAsync(`${ns}.${prefix}.segments.${i}.brightness`),\n ]),\n );\n }\n const segResults = await Promise.all(segReads);\n segments = segResults.map(([segColor, segBright]) => ({\n color: typeof segColor?.val === \"string\" ? segColor.val : \"#000000\",\n brightness: typeof segBright?.val === \"number\" ? segBright.val : 100,\n }));\n }\n\n const snapshot: LocalSnapshot = {\n name,\n power: powerState?.val === true,\n brightness: typeof brightState?.val === \"number\" ? brightState.val : 0,\n colorRgb: typeof colorState?.val === \"string\" ? colorState.val : \"#000000\",\n colorTemperature: typeof ctState?.val === \"number\" ? ctState.val : 0,\n segments,\n savedAt: Date.now(),\n };\n\n this.localSnapshots.saveSnapshot(device.sku, device.deviceId, snapshot);\n this.log.info(`Local snapshot saved: \"${name}\" for ${device.name}`);\n\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.refreshDeviceStates(device, this.deviceManager!.getDevices());\n }\n\n /**\n * Restore a local snapshot by index.\n *\n * @param device Target device\n * @param val Dropdown index value\n */\n private async handleSnapshotRestore(device: GoveeDevice, val: ioBroker.StateValue): Promise<void> {\n if (!this.localSnapshots || !this.deviceManager) {\n return;\n }\n\n const idx = parseInt(String(val), 10);\n if (idx < 1) {\n return;\n }\n\n const snaps = this.localSnapshots.getSnapshots(device.sku, device.deviceId);\n const snap = snaps[idx - 1];\n if (!snap) {\n this.log.warn(`Local snapshot index ${idx} not found for ${device.name}`);\n return;\n }\n\n this.log.info(`Restoring local snapshot \"${snap.name}\" for ${device.name}`);\n\n // Send each state via LAN \u2192 Cloud routing\n await this.deviceManager.sendCommand(device, \"power\", snap.power);\n if (snap.power) {\n await this.deviceManager.sendCommand(device, \"brightness\", snap.brightness);\n if (snap.colorTemperature > 0) {\n await this.deviceManager.sendCommand(device, \"colorTemperature\", snap.colorTemperature);\n } else {\n await this.deviceManager.sendCommand(device, \"colorRgb\", snap.colorRgb);\n }\n\n // Restore per-segment states via ptReal\n if (snap.segments && snap.segments.length > 0) {\n for (let i = 0; i < snap.segments.length; i++) {\n const seg = snap.segments[i];\n await this.deviceManager.sendCommand(device, `segmentColor:${i}`, seg.color);\n await this.deviceManager.sendCommand(device, `segmentBrightness:${i}`, seg.brightness);\n }\n }\n }\n }\n\n /**\n * Delete a local snapshot by name.\n *\n * @param device Target device\n * @param name Snapshot name to delete\n */\n private handleSnapshotDelete(device: GoveeDevice, name: string): void {\n if (!this.localSnapshots) {\n return;\n }\n\n if (this.localSnapshots.deleteSnapshot(device.sku, device.deviceId, name)) {\n this.log.info(`Local snapshot deleted: \"${name}\" for ${device.name}`);\n // Targeted refresh \u2014 only this device's snapshot_local dropdown changed.\n this.refreshDeviceStates(device, this.deviceManager!.getDevices());\n } else {\n this.log.warn(`Local snapshot \"${name}\" not found for ${device.name}`);\n }\n }\n\n /** Dropdowns whose value is a mode-selection \u2014 reset to \"---\" (0) when the mode stops. */\n private static readonly MODE_DROPDOWNS = [\n \"scenes.light_scene\",\n \"scenes.diy_scene\",\n \"snapshots.snapshot_cloud\",\n \"snapshots.snapshot_local\",\n \"music.music_mode\",\n ];\n\n /** Map command \u2192 its own dropdown path (excluded from reset when that mode is the one that was just activated). */\n private static readonly COMMAND_DROPDOWN: Record<string, string> = {\n lightScene: \"scenes.light_scene\",\n diyScene: \"scenes.diy_scene\",\n snapshot: \"snapshots.snapshot_cloud\",\n snapshotLocal: \"snapshots.snapshot_local\",\n music: \"music.music_mode\",\n colorRgb: \"\",\n colorTemperature: \"\",\n };\n\n /**\n * Reset related dropdown states when switching between scenes/snapshots/colors.\n * Each mode-switch resets all OTHER mode dropdowns to \"---\" (0).\n *\n * @param prefix Device state prefix\n * @param activeCommand The command that was just executed\n */\n private async resetRelatedDropdowns(prefix: string, activeCommand: string): Promise<void> {\n if (!(activeCommand in GoveeAdapter.COMMAND_DROPDOWN)) {\n return;\n }\n const ownDropdown = GoveeAdapter.COMMAND_DROPDOWN[activeCommand];\n await this.resetModeDropdowns(prefix, ownDropdown);\n }\n\n /**\n * Reset every mode dropdown except `keep` (empty = reset all). Used both for\n * mode-switches (keep the new mode's own dropdown) and for power-off\n * (reset everything \u2014 a device that's off has no active mode).\n *\n * @param prefix Device state prefix\n * @param keep Dropdown path to leave untouched (e.g. \"music.music_mode\"), or \"\" to reset all\n */\n private async resetModeDropdowns(prefix: string, keep: string): Promise<void> {\n await Promise.all(\n GoveeAdapter.MODE_DROPDOWNS.filter(d => d !== keep).map(async dropdown => {\n const stateId = `${this.namespace}.${prefix}.${dropdown}`;\n const current = await this.getStateAsync(stateId);\n if (current?.val && current.val !== \"0\" && current.val !== 0) {\n await this.setStateAsync(stateId, { val: \"0\", ack: true });\n }\n }),\n );\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new GoveeAdapter(options);\n} else {\n (() => new GoveeAdapter())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
package/devices.json CHANGED
@@ -146,6 +146,11 @@
146
146
  "status": "verified",
147
147
  "since": "1.0.0"
148
148
  },
149
+ "H61D5": {
150
+ "name": "LED Strip",
151
+ "type": "light",
152
+ "status": "verified"
153
+ },
149
154
  "H70B3": {
150
155
  "name": "Curtain Lights",
151
156
  "type": "light",
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.1.1",
4
+ "version": "2.1.2",
5
5
  "news": {
6
+ "2.1.2": {
7
+ "en": "Verification message no longer claims your account has 2FA when it doesn't. Stale credentials from v2.1.0 are cleared on first start. H61D5 added to the catalogue.",
8
+ "de": "Die Verifizierungs-Meldung behauptet nicht mehr 2FA bei Konten ohne 2FA. Alte Credentials aus v2.1.0 werden beim ersten Start aufgeraeumt. H61D5 im Katalog ergaenzt.",
9
+ "ru": "Сообщение о верификации больше не утверждает, что у аккаунта 2FA, если её нет. Старые креды из v2.1.0 чистятся при первом старте. H61D5 в каталоге.",
10
+ "pt": "A mensagem de verificacao nao afirma mais que sua conta tem 2FA quando nao tem. Credenciais antigas de v2.1.0 sao limpas no primeiro start. H61D5 adicionado ao catalogo.",
11
+ "nl": "De verificatie-melding claimt niet meer dat je account 2FA heeft als dat niet zo is. Oude credentials uit v2.1.0 worden bij de eerste start opgeruimd. H61D5 toegevoegd.",
12
+ "fr": "Le message de verification ne pretend plus que ton compte a la 2FA s'il ne l'a pas. Les anciens identifiants de v2.1.0 sont nettoyes au premier demarrage. H61D5 ajoute.",
13
+ "it": "Il messaggio di verifica non afferma piu che l'account ha la 2FA quando non l'ha. Le credenziali residue di v2.1.0 vengono pulite al primo avvio. H61D5 aggiunto al catalogo.",
14
+ "es": "El mensaje de verificacion ya no afirma que tu cuenta tenga 2FA cuando no la tiene. Las credenciales antiguas de v2.1.0 se limpian al primer arranque. H61D5 anadido.",
15
+ "pl": "Komunikat weryfikacji nie twierdzi juz, ze konto ma 2FA, gdy nie ma. Stare dane z v2.1.0 sa czyszczone przy pierwszym starcie. H61D5 dodano do katalogu.",
16
+ "uk": "Повідомлення верифікації більше не стверджує, що у акаунта 2FA, якщо її немає. Старі креди з v2.1.0 чистяться при першому старті. H61D5 у каталозі.",
17
+ "zh-cn": "验证消息不再误称账户开启了 2FA。来自 v2.1.0 的旧凭据在首次启动时清理。H61D5 加入支持目录。"
18
+ },
6
19
  "2.1.1": {
7
20
  "en": "Security fix: in v2.1.0 your MQTT login was stored unencrypted by mistake — now encrypted. Diagnostics renamed to diag.export/result/tier. 2FA warning no longer repeats on every reconnect.",
8
21
  "de": "Sicherheitsfix: in v2.1.0 wurde dein MQTT-Login versehentlich unverschluesselt gespeichert — jetzt verschluesselt. Diagnose-Datenpunkte heissen jetzt diag.*. 2FA-Warnung wiederholt sich nicht mehr.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Termometry Govee, grzejniki, czajniki, kostkarki i inne urzadzenia sa teraz wspierane razem z lampami. iobroker.govee-appliances jest deprecated i przechodzi tutaj.",
81
94
  "uk": "Термометри Govee, обігрівачі, чайники, льодогенератори та інші пристрої тепер підтримуються разом із лампами. iobroker.govee-appliances вважається застарілим і переноситься сюди.",
82
95
  "zh-cn": "Govee 温度计、加热器、水壶、制冰机等设备现在与灯具一起支持。iobroker.govee-appliances 已废弃并并入此处。"
83
- },
84
- "1.11.0": {
85
- "en": "Scene/DIY/snapshot/music-mode dropdowns now accept index-as-number, index-as-string and the entry name (case-insensitive). State type is mixed — no more 'expects type string but received number' warning. Duplicate scene names from the cloud are auto-disambiguated with ' (2)', ' (3)' suffixes.",
86
- "de": "Scene/DIY/Snapshot/Musik-Modus-Dropdowns akzeptieren jetzt Index als Zahl, Index als String und den Klartext-Namen (case-insensitive). State-Typ ist 'mixed' — kein 'expects type string but received number'-Warning mehr. Doppelte Szenennamen aus der Cloud werden automatisch mit ' (2)', ' (3)' suffixiert.",
87
- "ru": "Випадайки для сцен/DIY/snapshot/музики тепер приймають індекс як число, як string і назву елемента (case-insensitive). Тип state — mixed — попередження 'expects type string but received number' зникло. Дублікати назв сцен з cloud отримують суфікси ' (2)', ' (3)'.",
88
- "pt": "Os dropdowns de scene/DIY/snapshot/music-mode agora aceitam índice como número, como string e o nome da entrada (case-insensitive). O tipo do state é mixed — o aviso 'expects type string but received number' desapareceu. Nomes duplicados de cena vindos da nuvem são automaticamente desambiguados com sufixos ' (2)', ' (3)'.",
89
- "nl": "Scene/DIY/snapshot/music-mode dropdowns accepteren nu de index als number, als string en de entry-name (case-insensitive). State-type is mixed — de waarschuwing 'expects type string but received number' is weg. Dubbele scènenamen uit de cloud krijgen automatisch ' (2)', ' (3)' achtervoegsels.",
90
- "fr": "Les dropdowns scene/DIY/snapshot/music-mode acceptent désormais l'index en number, en string ou le nom (insensible à la casse). Le type de state est mixed — l'avertissement 'expects type string but received number' a disparu. Les noms de scène dupliqués depuis le cloud sont automatiquement suffixés ' (2)', ' (3)'.",
91
- "it": "I dropdown scene/DIY/snapshot/music-mode ora accettano l'indice come number, come string e il nome (case-insensitive). Il tipo di state è mixed — l'avviso 'expects type string but received number' è scomparso. I nomi di scena duplicati dal cloud vengono auto-disambiguati con suffissi ' (2)', ' (3)'.",
92
- "es": "Los desplegables scene/DIY/snapshot/music-mode aceptan ahora el índice como number, como string o el nombre (case-insensitive). El tipo de state es mixed — desaparece la advertencia 'expects type string but received number'. Los nombres de escena duplicados de la nube se desambiguan automáticamente con sufijos ' (2)', ' (3)'.",
93
- "pl": "Listy rozwijane scene/DIY/snapshot/music-mode akceptują teraz indeks jako number, jako string oraz nazwę (case-insensitive). Typ state to mixed — ostrzeżenie 'expects type string but received number' znika. Zduplikowane nazwy scen z chmury są automatycznie różnicowane sufiksami ' (2)', ' (3)'.",
94
- "uk": "Випадайки для scene/DIY/snapshot/music-mode тепер приймають індекс як number, як string і назву елемента (case-insensitive). Тип state — mixed — попередження 'expects type string but received number' зникло. Дубльовані назви сцен з cloud автоматично отримують суфікси ' (2)', ' (3)'.",
95
- "zh-cn": "场景/DIY/snapshot/音乐模式下拉菜单现在接受索引为数字、索引为字符串或条目名称(不区分大小写)。state 类型为 mixed —— 'expects type string but received number' 警告消失。云端重复场景名称自动添加 ' (2)'、' (3)' 后缀。"
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.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",