iobroker.govee-smart 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/build/lib/govee-mqtt-client.js +1 -1
- package/build/lib/govee-mqtt-client.js.map +2 -2
- package/build/main.js +3 -1
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
128
128
|
Placeholder for the next version (at the beginning of the line):
|
|
129
129
|
### **WORK IN PROGRESS**
|
|
130
130
|
-->
|
|
131
|
+
### 2.7.1 (2026-05-10)
|
|
132
|
+
|
|
133
|
+
- Cleaner start-up log: the `Starting` line now hints to wait for the `ready` message, and the redundant `MQTT connected` info line is gone (the ready summary already lists all active channels).
|
|
134
|
+
|
|
131
135
|
### 2.7.0 (2026-05-10)
|
|
132
136
|
|
|
133
137
|
- Newly created snapshots in the Govee Home app now appear in the ioBroker dropdown — both after an adapter restart and via the refresh button. Previously the cache held the old list forever (Issue #13).
|
|
@@ -148,10 +152,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
148
152
|
|
|
149
153
|
- Internal refactoring. No changes for users.
|
|
150
154
|
|
|
151
|
-
### 2.6.4 (2026-05-10)
|
|
152
|
-
|
|
153
|
-
- Internal tooling refresh. No changes for users.
|
|
154
|
-
|
|
155
155
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
156
156
|
|
|
157
157
|
## Support
|
|
@@ -505,7 +505,7 @@ class GoveeMqttClient {
|
|
|
505
505
|
this.log.info(`MQTT connection restored`);
|
|
506
506
|
this.lastErrorCategory = null;
|
|
507
507
|
} else {
|
|
508
|
-
this.log.
|
|
508
|
+
this.log.debug(`MQTT connected`);
|
|
509
509
|
}
|
|
510
510
|
(_a = this.client) == null ? void 0 : _a.subscribe(this.accountTopic, { qos: 0 }, (err) => {
|
|
511
511
|
var _a2, _b;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/govee-mqtt-client.ts"],
|
|
4
|
-
"sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport { MQTT_MAX_AUTH_FAILURES, VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MQTT_MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to\n // clear the settings field AND clear the in-memory copy so a\n // subsequent reconnect-login doesn't replay a now-consumed code (Govee\n // would reject it as `Verification code invalid` and trip the\n // VERIFICATION_FAILED branch unnecessarily).\n if (codeWasSent) {\n this.verificationCode = \"\";\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings`);\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: verification code rejected \u2014 request a fresh code`);\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n this.log.warn(`MQTT not connected: login rejected \u2014 check email/password`);\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n // disposed-check guards the reconnect path against schedule-after-stop:\n // disconnect() clears the timer, but a close-event-driven scheduleReconnect\n // could fire AFTER disconnect() returned, leaving a future-firing timer.\n if (this.disposed) {\n return;\n }\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.disposed) {\n return;\n }\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(`MQTT connection restored`);\n this.lastErrorCategory = null;\n } else {\n this.log.info(`MQTT connected`);\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n // Subscribe-fail is rare (AWS-IoT policy mismatch, account flagged)\n // but the TCP connection stays alive \u2014 protocol-level keepalive\n // pings keep answering, so `close` would never fire on its own.\n // Without forcing a close, `info.mqttConnected` would stay `false`\n // indefinitely with no reconnect \u2014 permanent silent death.\n // Forcing the close triggers the close-handler \u2192 scheduleReconnect.\n this.log.warn(`MQTT subscribe failed: ${err.message} \u2014 forcing reconnect`);\n try {\n this.client?.end(true);\n } catch {\n // ignore \u2014 close-event handler will pick it up either way\n }\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Last time `requestVerificationCode` actually issued a request \u2014 guards against\n * Govee marking the account as suspicious from rapid-fire user clicks.\n */\n private lastVerificationRequestMs = 0;\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 * In-memory throttle of {@link VERIFICATION_REQUEST_THROTTLE_MS} (30 s)\n * mirrors the message-router's user-side throttle but lives here too so\n * other entry points (programmatic restarts, future scripted access)\n * can't bypass it.\n *\n * Throws on non-200, network failure or throttle-block so the caller\n * (onMessage handler) can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const now = Date.now();\n const elapsed = now - this.lastVerificationRequestMs;\n if (this.lastVerificationRequestMs > 0 && elapsed < VERIFICATION_REQUEST_THROTTLE_MS) {\n const waitSec = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - elapsed) / 1000);\n throw new Error(`Verification code request throttled \u2014 wait ${waitSec}s before retrying.`);\n }\n this.lastVerificationRequestMs = now;\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport { MQTT_MAX_AUTH_FAILURES, VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MQTT_MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to\n // clear the settings field AND clear the in-memory copy so a\n // subsequent reconnect-login doesn't replay a now-consumed code (Govee\n // would reject it as `Verification code invalid` and trip the\n // VERIFICATION_FAILED branch unnecessarily).\n if (codeWasSent) {\n this.verificationCode = \"\";\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings`);\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: verification code rejected \u2014 request a fresh code`);\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n this.log.warn(`MQTT not connected: login rejected \u2014 check email/password`);\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n // disposed-check guards the reconnect path against schedule-after-stop:\n // disconnect() clears the timer, but a close-event-driven scheduleReconnect\n // could fire AFTER disconnect() returned, leaving a future-firing timer.\n if (this.disposed) {\n return;\n }\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.disposed) {\n return;\n }\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(`MQTT connection restored`);\n this.lastErrorCategory = null;\n } else {\n // Initial connect is implicit in the ready-message (\"channels:\n // LAN+Cloud+MQTT+...\"), so this stays debug \u2014 only the recovery\n // path above earns an info-level event.\n this.log.debug(`MQTT connected`);\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n // Subscribe-fail is rare (AWS-IoT policy mismatch, account flagged)\n // but the TCP connection stays alive \u2014 protocol-level keepalive\n // pings keep answering, so `close` would never fire on its own.\n // Without forcing a close, `info.mqttConnected` would stay `false`\n // indefinitely with no reconnect \u2014 permanent silent death.\n // Forcing the close triggers the close-handler \u2192 scheduleReconnect.\n this.log.warn(`MQTT subscribe failed: ${err.message} \u2014 forcing reconnect`);\n try {\n this.client?.end(true);\n } catch {\n // ignore \u2014 close-event handler will pick it up either way\n }\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Last time `requestVerificationCode` actually issued a request \u2014 guards against\n * Govee marking the account as suspicious from rapid-fire user clicks.\n */\n private lastVerificationRequestMs = 0;\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 * In-memory throttle of {@link VERIFICATION_REQUEST_THROTTLE_MS} (30 s)\n * mirrors the message-router's user-side throttle but lives here too so\n * other entry points (programmatic restarts, future scripted access)\n * can't bypass it.\n *\n * Throws on non-200, network failure or throttle-block so the caller\n * (onMessage handler) can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const now = Date.now();\n const elapsed = now - this.lastVerificationRequestMs;\n if (this.lastVerificationRequestMs > 0 && elapsed < VERIFICATION_REQUEST_THROTTLE_MS) {\n const waitSec = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - elapsed) / 1000);\n throw new Error(`Verification code request throttled \u2014 wait ${waitSec}s before retrying.`);\n }\n this.lastVerificationRequestMs = now;\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -276,7 +276,9 @@ class GoveeAdapter extends utils.Adapter {
|
|
|
276
276
|
if (config.goveeEmail && config.goveePassword) {
|
|
277
277
|
startChannels.push("MQTT");
|
|
278
278
|
}
|
|
279
|
-
this.log.info(
|
|
279
|
+
this.log.info(
|
|
280
|
+
`Starting (${startChannels.join(", ")}) \u2014 please wait, a "ready" message will follow when all channels are up`
|
|
281
|
+
);
|
|
280
282
|
this.lanClient = new import_govee_lan_client.GoveeLanClient(this.log, this);
|
|
281
283
|
this.deviceManager.setLanClient(this.lanClient);
|
|
282
284
|
this.lanClient.start(
|
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 { initDeviceRegistry } from \"./lib/device-registry\";\nimport { DeviceManager, resolveSegmentCount } 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 } from \"./lib/local-snapshots\";\nimport { SnapshotHandler } from \"./lib/snapshot-handler\";\nimport { GroupFanoutHandler } from \"./lib/group-fanout\";\nimport { MessageRouter, type MessageRouterHost } from \"./lib/message-router\";\nimport type { CloudRetryLoop } from \"./lib/cloud-retry\";\nimport * as cloudCreds from \"./lib/handlers/cloud-creds-handler\";\nimport * as cloudRetryHandler from \"./lib/handlers/cloud-retry-handler\";\nimport * as cloudStateLoader from \"./lib/handlers/cloud-state-loader\";\nimport * as connectionState from \"./lib/handlers/connection-state\";\nimport * as deviceEvents from \"./lib/handlers/device-events\";\nimport * as groupFanoutHandler from \"./lib/handlers/group-fanout-handler\";\nimport * as groupStateHelpers from \"./lib/handlers/group-state-helpers\";\nimport * as snapshotHandlerGlue from \"./lib/handlers/snapshot-handler-glue\";\nimport * as stateChangeRouter from \"./lib/handlers/state-change-router\";\nimport * as wizardHandler from \"./lib/handlers/wizard-handler\";\nimport { RateLimiter } from \"./lib/rate-limiter\";\nimport type { SegmentWizard } from \"./lib/segment-wizard\";\nimport { wizardIdleText } from \"./lib/segment-wizard\";\nimport { SkuCache } from \"./lib/sku-cache\";\nimport { StateManager } from \"./lib/state-manager\";\nimport {\n errMessage,\n rgbIntToHex,\n rgbToHex,\n type AdapterConfig,\n type CloudStateCapability,\n type GoveeDevice,\n} from \"./lib/types\";\nimport { APP_API_INITIAL_DELAY_MS, APP_API_POLL_INTERVAL_MS, CLOUD_FULL_LIMITS } from \"./lib/timing-constants\";\n\n// Rate-limit defaults moved to lib/timing-constants.ts as CLOUD_FULL_LIMITS so\n// every module that touches Govee budgeting reads the same canonical values.\n\nclass GoveeAdapter extends utils.Adapter {\n /** Public for handler modules (state-change-router, group-fanout, wizard, snapshot, diagnostics). */\n public deviceManager: DeviceManager | null = null;\n /** Public for handler modules. */\n public stateManager: StateManager | null = null;\n /** Public for handler modules. */\n public lanClient: GoveeLanClient | null = null;\n /** Public for handler modules (connection-state). */\n public mqttClient: GoveeMqttClient | null = null;\n /** Public for handler modules (connection-state). */\n public openapiMqttClient: GoveeOpenapiMqttClient | null = null;\n /** Public for handler modules. */\n public 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 /**\n * One-shot timer for the FIRST app-api poll (5s nach start) \u2014 Handle\n * damit onUnload das wegr\u00E4umen kann bevor es ins Leere feuert.\n */\n private appApiInitialTimer: ioBroker.Timeout | undefined;\n /** One-shot timer for cloud-init 60s safety timeout \u2014 gleiches Pattern. */\n /** Public for handler modules. */\n public cloudInitTimer: ioBroker.Timeout | undefined;\n /**\n * Letzter info.connection-Wert \u2014 Cache damit nicht jeder device-update\n * einen unn\u00F6tigen setStateAsync macht (H4).\n */\n /** Public for handler modules (connection-state). */\n public lastConnectionState: boolean | null = null;\n // === Lifecycle-Flags (Adapter-Boot-Sequenz) ===\n // checkAllReady() pr\u00FCft alle 5 Voraussetzungen gleichzeitig \u2014 sie laufen\n // parallel ab, kein lineares STATE_MACHINE-Pattern weil Channels\n // unabh\u00E4ngig connecten.\n /** LAN-Scan-Initial-Wait abgeschlossen \u2014 public for connection-state handler. */\n public lanScanDone = false;\n /** State-Tree-Erstellung fertig \u2014 public for connection-state + device-events handlers. */\n public statesReady = false;\n /** Cloud-Init-Phase abgeschlossen \u2014 public for connection-state handler. */\n public cloudInitDone = false;\n /** App-API-Poll fertig \u2014 public for connection-state handler. */\n public appApiInitialPollDone = false;\n /** Mehrfach-Ready-Log-Guard \u2014 public for connection-state handler. */\n public readyLogged = false;\n /** Cloud war mindestens einmal connected \u2014 f\u00FCr \u201Erestored\"-Log nach Down. */\n /** Public for handler modules. */\n public cloudWasConnected = false;\n /** T\u00E4gliches Interval f\u00FCr App-Version-Drift-Check gegen App-Store. */\n private appVersionCheckTimer: ioBroker.Interval | undefined;\n // === Sub-Komponenten ===\n private skuCache: SkuCache | null = null;\n /** Public for handler modules. */\n public localSnapshots: LocalSnapshotStore | null = null;\n /** Public for handler modules (state-change-router). */\n public snapshotHandler: SnapshotHandler | null = null;\n /** Public for handler modules (state-change-router). */\n public groupFanout: GroupFanoutHandler | null = null;\n private messageRouter: MessageRouter | null = null;\n /** Public for handler modules (device-events). */\n public stateCreationQueue: Promise<void>[] = [];\n private lanScanTimer: ioBroker.Timeout | undefined;\n private cleanupTimer: ioBroker.Timeout | undefined;\n private readyTimer: ioBroker.Timeout | undefined;\n /** Public for handler modules. Undefined until first ensureCloudRetry() call. */\n public cloudRetry: CloudRetryLoop | undefined;\n /** Public for handlers/wizard-handler \u2014 lazily instantiated by `runWizardStep`. */\n public 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 /** Public for handler modules (state-change-router, diagnostics). */\n public diagnosticsLastRun = new Map<string, number>();\n /** Cached admin language from system.config \u2014 used for wizard UI text */\n /** Public for handler modules. */\n public adminLanguage = \"en\";\n /** Last time `requestCode` was triggered via onMessage \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n /**\n * Set true at the start of onUnload \u2014 async paths (onStateChange,\n * applyCloudCapabilities, retrySceneData, \u2026) check this between awaits\n * and bail before further setStateAsync against a torn-down adapter.\n */\n /** Public for handler modules (state-change-router). */\n public unloading = false;\n /** Initial app-version-check timer (2 min after start) \u2014 kept so onUnload can clear it. */\n private appVersionInitialTimer: ioBroker.Timeout | undefined;\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 stateChangeRouter\n .onStateChange(this, id, state)\n .catch(e => this.log.warn(`onStateChange crashed for ${id}: ${errMessage(e)}`)),\n );\n this.on(\"message\", obj => this.messageRouter?.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 // Load admin language from system.config so wizard prose matches the\n // user's Admin UI. Falls back to English on any lookup failure. Adapter\n // logs themselves stay English by ioBroker convention; this language is\n // used only for the segment wizard's user-facing status text.\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.snapshotHandler = new SnapshotHandler(snapshotHandlerGlue.buildSnapshotHost(this));\n this.groupFanout = new GroupFanoutHandler(groupFanoutHandler.buildGroupFanoutHost(this));\n this.messageRouter = new MessageRouter(this.buildMessageRouterHost());\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) => deviceEvents.onDeviceStateUpdate(this, device, state),\n devices => deviceEvents.onDeviceListChanged(this, 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 // Wichtig: Wizard sendet `segmentBatch` mit Indizes 0..SEGMENT_HARD_MAX\n // damit das Ger\u00E4t die echte Strip-L\u00E4nge selbst zeigt. Wir d\u00FCrfen das\n // ECHO aber nur in States schreiben die wirklich existieren \u2014 sonst\n // produziert js-controller den \u201Ehas no existing object\"-WARN f\u00FCr\n // jeden index oberhalb der Cap (z.B. segments.51..55 bei 19-Strip).\n this.deviceManager.onSegmentBatchUpdate = (device, batch) => {\n const prefix = this.stateManager!.devicePrefix(device);\n const cap = typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : 0;\n for (const idx of batch.segments) {\n if (cap === 0 || idx >= cap) {\n continue;\n }\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 // Gleicher Cap-Filter wie bei batch \u2014 defensive vor stale Pakete.\n this.deviceManager.onMqttSegmentUpdate = (device, segments) => {\n const prefix = this.stateManager!.devicePrefix(device);\n const cap = typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : 0;\n for (const seg of segments) {\n if (cap === 0 || seg.index >= cap) {\n continue;\n }\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(`Failed to rebuild segment tree for ${device.name} after count growth: ${errMessage(e)}`);\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 (${startChannels.join(\", \")})`);\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 connectionState.checkAllReady(this);\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 cloudCreds.clearVerificationCodeSetting(this).catch(e => {\n this.log.warn(`Could not clear mqttVerificationCode: ${errMessage(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 cloudCreds.clearVerificationCodeSetting(this).catch(() => {});\n }\n });\n\n // Re-use cached MQTT credentials across restarts. Stored in the\n // info.mqttCredentials state (NOT in adapter native): writing to\n // system.adapter.X.0 native triggers a js-controller adapter\n // restart, which would loop endlessly on every login. States are\n // restart-safe.\n //\n // One-shot: clean up legacy v2.1.0/v2.1.1/v2.1.2 native fields\n // that contained plaintext credentials. Best-effort.\n await cloudCreds.cleanupLegacyMqttNativeOnce(this);\n const cachedCreds = await cloudCreds.loadPersistedCredsFromState(this);\n if (cachedCreds) {\n this.mqttClient.setPersistedCredentials(cachedCreds);\n }\n this.mqttClient.setOnCredentialsRefresh(creds => {\n cloudCreds.persistCredsToState(this, creds).catch(e => {\n this.log.warn(`Could not persist MQTT credentials: ${errMessage(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 connectionState.checkAllReady(this);\n }\n connectionState.updateConnectionState(this);\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 cloudStateLoader\n .applyCloudCapabilities(this, device, caps)\n .catch(e => this.log.warn(`applyCloudCapabilities failed for ${device.sku}: ${errMessage(e)}`));\n });\n\n this.rateLimiter = new RateLimiter(this.log, this, CLOUD_FULL_LIMITS.perMinute, CLOUD_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 const triggerAppApiPoll = (): void => {\n this.deviceManager\n ?.pollAppApi()\n .then(() => {\n // H2 \u2014 Mark initial-poll-done und re-check Ready damit der\n // Adapter \u201Eready\" loggen kann sobald Sensor-Werte da sind.\n if (!this.appApiInitialPollDone) {\n this.appApiInitialPollDone = true;\n connectionState.checkAllReady(this);\n }\n })\n .catch(e => this.log.debug(`pollAppApi failed: ${errMessage(e)}`));\n };\n this.appApiPollTimer = this.setInterval(triggerAppApiPoll, APP_API_POLL_INTERVAL_MS);\n // Initial poll: gibt MQTT Zeit f\u00FCr den Bearer-Login. Ohne diesen\n // Sofort-Poll bleiben Sensoren wie H5179 die ersten 2 Minuten nach\n // Start offline (Online-Signal kommt nur via App-API). Handle in\n // Member-Variable damit onUnload den Timer cleart.\n this.appApiInitialTimer = this.setTimeout(triggerAppApiPoll, APP_API_INITIAL_DELAY_MS);\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 cloudRetryHandler.cloudInitWithTimeout(this);\n this.cloudWasConnected = result.ok;\n cloudRetryHandler.ensureCloudRetry(this).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 cloudStateLoader.loadCloudStates(this);\n } else {\n cloudRetryHandler.handleCloudFailure(this, result);\n }\n } else {\n this.log.info(`Using cached device data \u2014 no Cloud calls needed`);\n this.cloudWasConnected = true;\n cloudRetryHandler.ensureCloudRetry(this).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\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 connectionState.reapStaleDevices(this).catch(e => this.log.debug(`Device cleanup failed: ${errMessage(e)}`));\n }, 30_000);\n\n // App-Version-Drift-Monitor \u2014 daily check + initial nach 2 min wenn der\n // Adapter-Start ohne MQTT-Login durchgeschlagen ist (z.B. LAN-only).\n this.appVersionCheckTimer = this.setInterval(\n () => {\n connectionState\n .checkAppVersionDrift(this)\n .catch(e => this.log.debug(`App version check error: ${errMessage(e)}`));\n },\n 24 * 60 * 60 * 1000,\n );\n this.appVersionInitialTimer = this.setTimeout(\n () => {\n this.appVersionInitialTimer = undefined;\n if (this.unloading) {\n return;\n }\n connectionState\n .checkAppVersionDrift(this)\n .catch(e => this.log.debug(`App version check error: ${errMessage(e)}`));\n },\n 2 * 60 * 1000,\n );\n\n connectionState.updateConnectionState(this);\n\n // Check if all channels are ready \u2014 may already be true if MQTT connected fast\n connectionState.checkAllReady(this);\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 // Safety-Timeout: log ready trotzdem auch wenn ein Channel zu lange\n // braucht. READY_TIMEOUT_MS deckt normalen MQTT-Connect + 1 Reconnect.\n this.readyLogged = true;\n connectionState.logDeviceSummary(this);\n }\n }, 60_000);\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous.\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n // Set first \u2014 async paths read this between awaits and bail before\n // further setStateAsync, sendCommand, etc. against a torn-down adapter.\n this.unloading = true;\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 if (this.appApiInitialTimer) {\n this.clearTimeout(this.appApiInitialTimer);\n this.appApiInitialTimer = undefined;\n }\n if (this.cloudInitTimer) {\n this.clearTimeout(this.cloudInitTimer);\n this.cloudInitTimer = undefined;\n }\n if (this.appVersionCheckTimer) {\n this.clearInterval(this.appVersionCheckTimer);\n this.appVersionCheckTimer = undefined;\n }\n if (this.appVersionInitialTimer) {\n this.clearTimeout(this.appVersionInitialTimer);\n this.appVersionInitialTimer = 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 * Public delegate to stateChangeRouter \u2014 required by GroupFanoutHandlerAdapter interface.\n *\n * @param device Target device\n * @param prefix Device state prefix\n * @param changedSuffix State suffix that changed\n * @param newValue New value written\n */\n public async sendMusicCommand(\n device: GoveeDevice,\n prefix: string,\n changedSuffix: string,\n newValue: ioBroker.StateValue,\n ): Promise<void> {\n return stateChangeRouter.sendMusicCommand(this, device, prefix, changedSuffix, newValue);\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\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 /**\n * Public delegate for snapshot-glue + state-change-router modules.\n *\n * @param device Target device\n * @param allDevices Full device list\n */\n public refreshDeviceStates(device: GoveeDevice, allDevices: GoveeDevice[]): void {\n deviceEvents.refreshDeviceStates(this, device, allDevices);\n }\n\n /**\n * Called by device-manager when the device list changes\n *\n * @param devices Current list of all devices\n */\n\n /** Public delegate \u2014 connection-state handler exports the real implementation. */\n public reapStaleDevices(): Promise<void> {\n return connectionState.reapStaleDevices(this);\n }\n\n /**\n * Find device for a state ID\n *\n * @param localId Local state ID without namespace prefix\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 /**\n * Public delegate for handler modules \u2014 stateless lookup, lives in lib/handlers/group-state-helpers.\n *\n * @param suffix State suffix\n */\n public stateToCommand(suffix: string): string | null {\n return groupStateHelpers.stateToCommand(suffix);\n }\n\n /** Public delegate for cloud-retry-handler's CloudRetryHandlerAdapter interface. */\n public loadCloudStates(): Promise<void> {\n return cloudStateLoader.loadCloudStates(this);\n }\n\n /**\n * Public for OpenAPI-MQTT + App-API pipelines feeding sensor/appliance state.\n *\n * @param device Target device\n * @param caps Cloud-state capabilities\n */\n public applyCloudCapabilities(device: GoveeDevice, caps: CloudStateCapability[]): Promise<void> {\n return cloudStateLoader.applyCloudCapabilities(this, device, caps);\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 /**\n * Public for handler modules (wizard, state-change-router).\n *\n * @param device Target device\n * @param mode Manual mode flag\n * @param indices Physical segment indices\n */\n public 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 // \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 /** Construct host object for MessageRouter. */\n private buildMessageRouterHost(): MessageRouterHost {\n return {\n log: this.log,\n getConfig: () => {\n const config = this.config as unknown as AdapterConfig;\n return {\n goveeEmail: config.goveeEmail,\n goveePassword: config.goveePassword,\n mqttVerificationCode: config.mqttVerificationCode,\n };\n },\n sendResponse: (obj, data) => this.sendMessageResponse(obj, data),\n createMqttProbeClient: () => {\n const config = this.config as unknown as AdapterConfig;\n return new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n },\n getSegmentDeviceList: () => {\n const devices = this.deviceManager?.getDevices() ?? [];\n return devices\n .filter(d => d.sku !== \"BaseGroup\" && d.state?.online === true && resolveSegmentCount(d) > 0)\n .map(d => ({\n value: wizardHandler.deviceKeyFor(d),\n label: `${d.name} (${d.sku}, bisher ${resolveSegmentCount(d)} Segmente)`,\n }));\n },\n runWizardStep: (action, deviceKey) => wizardHandler.runWizardStep(this, action, deviceKey),\n };\n }\n\n /**\n * Helper: clear `mqttVerificationCode` in adapter native after a successful\n * login or a 455-fail.\n *\n * Idempotent: liest erst den aktuellen Wert, schreibt nur wenn dirty.\n * Verhindert den Adapter-Restart der durch jeden\n * `extendForeignObjectAsync(system.adapter.X, native:...)`-Call ausgel\u00F6st\n * wird (Memory v2.1.3-Bug). Vorher gab es nach jedem 2FA-Login einen\n * unn\u00F6tigen Restart.\n *\n * @param obj ioBroker message object\n * @param data Response data payload\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 /** Construct host object for SnapshotHandler \u2014 adapter dependencies injected. */\n /** Dropdowns whose value is a mode-selection \u2014 reset to \"---\" (0) when the mode stops. */\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 { initDeviceRegistry } from \"./lib/device-registry\";\nimport { DeviceManager, resolveSegmentCount } 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 } from \"./lib/local-snapshots\";\nimport { SnapshotHandler } from \"./lib/snapshot-handler\";\nimport { GroupFanoutHandler } from \"./lib/group-fanout\";\nimport { MessageRouter, type MessageRouterHost } from \"./lib/message-router\";\nimport type { CloudRetryLoop } from \"./lib/cloud-retry\";\nimport * as cloudCreds from \"./lib/handlers/cloud-creds-handler\";\nimport * as cloudRetryHandler from \"./lib/handlers/cloud-retry-handler\";\nimport * as cloudStateLoader from \"./lib/handlers/cloud-state-loader\";\nimport * as connectionState from \"./lib/handlers/connection-state\";\nimport * as deviceEvents from \"./lib/handlers/device-events\";\nimport * as groupFanoutHandler from \"./lib/handlers/group-fanout-handler\";\nimport * as groupStateHelpers from \"./lib/handlers/group-state-helpers\";\nimport * as snapshotHandlerGlue from \"./lib/handlers/snapshot-handler-glue\";\nimport * as stateChangeRouter from \"./lib/handlers/state-change-router\";\nimport * as wizardHandler from \"./lib/handlers/wizard-handler\";\nimport { RateLimiter } from \"./lib/rate-limiter\";\nimport type { SegmentWizard } from \"./lib/segment-wizard\";\nimport { wizardIdleText } from \"./lib/segment-wizard\";\nimport { SkuCache } from \"./lib/sku-cache\";\nimport { StateManager } from \"./lib/state-manager\";\nimport {\n errMessage,\n rgbIntToHex,\n rgbToHex,\n type AdapterConfig,\n type CloudStateCapability,\n type GoveeDevice,\n} from \"./lib/types\";\nimport { APP_API_INITIAL_DELAY_MS, APP_API_POLL_INTERVAL_MS, CLOUD_FULL_LIMITS } from \"./lib/timing-constants\";\n\n// Rate-limit defaults moved to lib/timing-constants.ts as CLOUD_FULL_LIMITS so\n// every module that touches Govee budgeting reads the same canonical values.\n\nclass GoveeAdapter extends utils.Adapter {\n /** Public for handler modules (state-change-router, group-fanout, wizard, snapshot, diagnostics). */\n public deviceManager: DeviceManager | null = null;\n /** Public for handler modules. */\n public stateManager: StateManager | null = null;\n /** Public for handler modules. */\n public lanClient: GoveeLanClient | null = null;\n /** Public for handler modules (connection-state). */\n public mqttClient: GoveeMqttClient | null = null;\n /** Public for handler modules (connection-state). */\n public openapiMqttClient: GoveeOpenapiMqttClient | null = null;\n /** Public for handler modules. */\n public 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 /**\n * One-shot timer for the FIRST app-api poll (5s nach start) \u2014 Handle\n * damit onUnload das wegr\u00E4umen kann bevor es ins Leere feuert.\n */\n private appApiInitialTimer: ioBroker.Timeout | undefined;\n /** One-shot timer for cloud-init 60s safety timeout \u2014 gleiches Pattern. */\n /** Public for handler modules. */\n public cloudInitTimer: ioBroker.Timeout | undefined;\n /**\n * Letzter info.connection-Wert \u2014 Cache damit nicht jeder device-update\n * einen unn\u00F6tigen setStateAsync macht (H4).\n */\n /** Public for handler modules (connection-state). */\n public lastConnectionState: boolean | null = null;\n // === Lifecycle-Flags (Adapter-Boot-Sequenz) ===\n // checkAllReady() pr\u00FCft alle 5 Voraussetzungen gleichzeitig \u2014 sie laufen\n // parallel ab, kein lineares STATE_MACHINE-Pattern weil Channels\n // unabh\u00E4ngig connecten.\n /** LAN-Scan-Initial-Wait abgeschlossen \u2014 public for connection-state handler. */\n public lanScanDone = false;\n /** State-Tree-Erstellung fertig \u2014 public for connection-state + device-events handlers. */\n public statesReady = false;\n /** Cloud-Init-Phase abgeschlossen \u2014 public for connection-state handler. */\n public cloudInitDone = false;\n /** App-API-Poll fertig \u2014 public for connection-state handler. */\n public appApiInitialPollDone = false;\n /** Mehrfach-Ready-Log-Guard \u2014 public for connection-state handler. */\n public readyLogged = false;\n /** Cloud war mindestens einmal connected \u2014 f\u00FCr \u201Erestored\"-Log nach Down. */\n /** Public for handler modules. */\n public cloudWasConnected = false;\n /** T\u00E4gliches Interval f\u00FCr App-Version-Drift-Check gegen App-Store. */\n private appVersionCheckTimer: ioBroker.Interval | undefined;\n // === Sub-Komponenten ===\n private skuCache: SkuCache | null = null;\n /** Public for handler modules. */\n public localSnapshots: LocalSnapshotStore | null = null;\n /** Public for handler modules (state-change-router). */\n public snapshotHandler: SnapshotHandler | null = null;\n /** Public for handler modules (state-change-router). */\n public groupFanout: GroupFanoutHandler | null = null;\n private messageRouter: MessageRouter | null = null;\n /** Public for handler modules (device-events). */\n public stateCreationQueue: Promise<void>[] = [];\n private lanScanTimer: ioBroker.Timeout | undefined;\n private cleanupTimer: ioBroker.Timeout | undefined;\n private readyTimer: ioBroker.Timeout | undefined;\n /** Public for handler modules. Undefined until first ensureCloudRetry() call. */\n public cloudRetry: CloudRetryLoop | undefined;\n /** Public for handlers/wizard-handler \u2014 lazily instantiated by `runWizardStep`. */\n public 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 /** Public for handler modules (state-change-router, diagnostics). */\n public diagnosticsLastRun = new Map<string, number>();\n /** Cached admin language from system.config \u2014 used for wizard UI text */\n /** Public for handler modules. */\n public adminLanguage = \"en\";\n /** Last time `requestCode` was triggered via onMessage \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n /**\n * Set true at the start of onUnload \u2014 async paths (onStateChange,\n * applyCloudCapabilities, retrySceneData, \u2026) check this between awaits\n * and bail before further setStateAsync against a torn-down adapter.\n */\n /** Public for handler modules (state-change-router). */\n public unloading = false;\n /** Initial app-version-check timer (2 min after start) \u2014 kept so onUnload can clear it. */\n private appVersionInitialTimer: ioBroker.Timeout | undefined;\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 stateChangeRouter\n .onStateChange(this, id, state)\n .catch(e => this.log.warn(`onStateChange crashed for ${id}: ${errMessage(e)}`)),\n );\n this.on(\"message\", obj => this.messageRouter?.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 // Load admin language from system.config so wizard prose matches the\n // user's Admin UI. Falls back to English on any lookup failure. Adapter\n // logs themselves stay English by ioBroker convention; this language is\n // used only for the segment wizard's user-facing status text.\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.snapshotHandler = new SnapshotHandler(snapshotHandlerGlue.buildSnapshotHost(this));\n this.groupFanout = new GroupFanoutHandler(groupFanoutHandler.buildGroupFanoutHost(this));\n this.messageRouter = new MessageRouter(this.buildMessageRouterHost());\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) => deviceEvents.onDeviceStateUpdate(this, device, state),\n devices => deviceEvents.onDeviceListChanged(this, 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 // Wichtig: Wizard sendet `segmentBatch` mit Indizes 0..SEGMENT_HARD_MAX\n // damit das Ger\u00E4t die echte Strip-L\u00E4nge selbst zeigt. Wir d\u00FCrfen das\n // ECHO aber nur in States schreiben die wirklich existieren \u2014 sonst\n // produziert js-controller den \u201Ehas no existing object\"-WARN f\u00FCr\n // jeden index oberhalb der Cap (z.B. segments.51..55 bei 19-Strip).\n this.deviceManager.onSegmentBatchUpdate = (device, batch) => {\n const prefix = this.stateManager!.devicePrefix(device);\n const cap = typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : 0;\n for (const idx of batch.segments) {\n if (cap === 0 || idx >= cap) {\n continue;\n }\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 // Gleicher Cap-Filter wie bei batch \u2014 defensive vor stale Pakete.\n this.deviceManager.onMqttSegmentUpdate = (device, segments) => {\n const prefix = this.stateManager!.devicePrefix(device);\n const cap = typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : 0;\n for (const seg of segments) {\n if (cap === 0 || seg.index >= cap) {\n continue;\n }\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(`Failed to rebuild segment tree for ${device.name} after count growth: ${errMessage(e)}`);\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(\n `Starting (${startChannels.join(\", \")}) \u2014 please wait, a \"ready\" message will follow when all channels are up`,\n );\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 connectionState.checkAllReady(this);\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 cloudCreds.clearVerificationCodeSetting(this).catch(e => {\n this.log.warn(`Could not clear mqttVerificationCode: ${errMessage(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 cloudCreds.clearVerificationCodeSetting(this).catch(() => {});\n }\n });\n\n // Re-use cached MQTT credentials across restarts. Stored in the\n // info.mqttCredentials state (NOT in adapter native): writing to\n // system.adapter.X.0 native triggers a js-controller adapter\n // restart, which would loop endlessly on every login. States are\n // restart-safe.\n //\n // One-shot: clean up legacy v2.1.0/v2.1.1/v2.1.2 native fields\n // that contained plaintext credentials. Best-effort.\n await cloudCreds.cleanupLegacyMqttNativeOnce(this);\n const cachedCreds = await cloudCreds.loadPersistedCredsFromState(this);\n if (cachedCreds) {\n this.mqttClient.setPersistedCredentials(cachedCreds);\n }\n this.mqttClient.setOnCredentialsRefresh(creds => {\n cloudCreds.persistCredsToState(this, creds).catch(e => {\n this.log.warn(`Could not persist MQTT credentials: ${errMessage(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 connectionState.checkAllReady(this);\n }\n connectionState.updateConnectionState(this);\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 cloudStateLoader\n .applyCloudCapabilities(this, device, caps)\n .catch(e => this.log.warn(`applyCloudCapabilities failed for ${device.sku}: ${errMessage(e)}`));\n });\n\n this.rateLimiter = new RateLimiter(this.log, this, CLOUD_FULL_LIMITS.perMinute, CLOUD_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 const triggerAppApiPoll = (): void => {\n this.deviceManager\n ?.pollAppApi()\n .then(() => {\n // H2 \u2014 Mark initial-poll-done und re-check Ready damit der\n // Adapter \u201Eready\" loggen kann sobald Sensor-Werte da sind.\n if (!this.appApiInitialPollDone) {\n this.appApiInitialPollDone = true;\n connectionState.checkAllReady(this);\n }\n })\n .catch(e => this.log.debug(`pollAppApi failed: ${errMessage(e)}`));\n };\n this.appApiPollTimer = this.setInterval(triggerAppApiPoll, APP_API_POLL_INTERVAL_MS);\n // Initial poll: gibt MQTT Zeit f\u00FCr den Bearer-Login. Ohne diesen\n // Sofort-Poll bleiben Sensoren wie H5179 die ersten 2 Minuten nach\n // Start offline (Online-Signal kommt nur via App-API). Handle in\n // Member-Variable damit onUnload den Timer cleart.\n this.appApiInitialTimer = this.setTimeout(triggerAppApiPoll, APP_API_INITIAL_DELAY_MS);\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 cloudRetryHandler.cloudInitWithTimeout(this);\n this.cloudWasConnected = result.ok;\n cloudRetryHandler.ensureCloudRetry(this).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 cloudStateLoader.loadCloudStates(this);\n } else {\n cloudRetryHandler.handleCloudFailure(this, result);\n }\n } else {\n this.log.info(`Using cached device data \u2014 no Cloud calls needed`);\n this.cloudWasConnected = true;\n cloudRetryHandler.ensureCloudRetry(this).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\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 connectionState.reapStaleDevices(this).catch(e => this.log.debug(`Device cleanup failed: ${errMessage(e)}`));\n }, 30_000);\n\n // App-Version-Drift-Monitor \u2014 daily check + initial nach 2 min wenn der\n // Adapter-Start ohne MQTT-Login durchgeschlagen ist (z.B. LAN-only).\n this.appVersionCheckTimer = this.setInterval(\n () => {\n connectionState\n .checkAppVersionDrift(this)\n .catch(e => this.log.debug(`App version check error: ${errMessage(e)}`));\n },\n 24 * 60 * 60 * 1000,\n );\n this.appVersionInitialTimer = this.setTimeout(\n () => {\n this.appVersionInitialTimer = undefined;\n if (this.unloading) {\n return;\n }\n connectionState\n .checkAppVersionDrift(this)\n .catch(e => this.log.debug(`App version check error: ${errMessage(e)}`));\n },\n 2 * 60 * 1000,\n );\n\n connectionState.updateConnectionState(this);\n\n // Check if all channels are ready \u2014 may already be true if MQTT connected fast\n connectionState.checkAllReady(this);\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 // Safety-Timeout: log ready trotzdem auch wenn ein Channel zu lange\n // braucht. READY_TIMEOUT_MS deckt normalen MQTT-Connect + 1 Reconnect.\n this.readyLogged = true;\n connectionState.logDeviceSummary(this);\n }\n }, 60_000);\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous.\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n // Set first \u2014 async paths read this between awaits and bail before\n // further setStateAsync, sendCommand, etc. against a torn-down adapter.\n this.unloading = true;\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 if (this.appApiInitialTimer) {\n this.clearTimeout(this.appApiInitialTimer);\n this.appApiInitialTimer = undefined;\n }\n if (this.cloudInitTimer) {\n this.clearTimeout(this.cloudInitTimer);\n this.cloudInitTimer = undefined;\n }\n if (this.appVersionCheckTimer) {\n this.clearInterval(this.appVersionCheckTimer);\n this.appVersionCheckTimer = undefined;\n }\n if (this.appVersionInitialTimer) {\n this.clearTimeout(this.appVersionInitialTimer);\n this.appVersionInitialTimer = 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 * Public delegate to stateChangeRouter \u2014 required by GroupFanoutHandlerAdapter interface.\n *\n * @param device Target device\n * @param prefix Device state prefix\n * @param changedSuffix State suffix that changed\n * @param newValue New value written\n */\n public async sendMusicCommand(\n device: GoveeDevice,\n prefix: string,\n changedSuffix: string,\n newValue: ioBroker.StateValue,\n ): Promise<void> {\n return stateChangeRouter.sendMusicCommand(this, device, prefix, changedSuffix, newValue);\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\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 /**\n * Public delegate for snapshot-glue + state-change-router modules.\n *\n * @param device Target device\n * @param allDevices Full device list\n */\n public refreshDeviceStates(device: GoveeDevice, allDevices: GoveeDevice[]): void {\n deviceEvents.refreshDeviceStates(this, device, allDevices);\n }\n\n /**\n * Called by device-manager when the device list changes\n *\n * @param devices Current list of all devices\n */\n\n /** Public delegate \u2014 connection-state handler exports the real implementation. */\n public reapStaleDevices(): Promise<void> {\n return connectionState.reapStaleDevices(this);\n }\n\n /**\n * Find device for a state ID\n *\n * @param localId Local state ID without namespace prefix\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 /**\n * Public delegate for handler modules \u2014 stateless lookup, lives in lib/handlers/group-state-helpers.\n *\n * @param suffix State suffix\n */\n public stateToCommand(suffix: string): string | null {\n return groupStateHelpers.stateToCommand(suffix);\n }\n\n /** Public delegate for cloud-retry-handler's CloudRetryHandlerAdapter interface. */\n public loadCloudStates(): Promise<void> {\n return cloudStateLoader.loadCloudStates(this);\n }\n\n /**\n * Public for OpenAPI-MQTT + App-API pipelines feeding sensor/appliance state.\n *\n * @param device Target device\n * @param caps Cloud-state capabilities\n */\n public applyCloudCapabilities(device: GoveeDevice, caps: CloudStateCapability[]): Promise<void> {\n return cloudStateLoader.applyCloudCapabilities(this, device, caps);\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 /**\n * Public for handler modules (wizard, state-change-router).\n *\n * @param device Target device\n * @param mode Manual mode flag\n * @param indices Physical segment indices\n */\n public 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 // \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 /** Construct host object for MessageRouter. */\n private buildMessageRouterHost(): MessageRouterHost {\n return {\n log: this.log,\n getConfig: () => {\n const config = this.config as unknown as AdapterConfig;\n return {\n goveeEmail: config.goveeEmail,\n goveePassword: config.goveePassword,\n mqttVerificationCode: config.mqttVerificationCode,\n };\n },\n sendResponse: (obj, data) => this.sendMessageResponse(obj, data),\n createMqttProbeClient: () => {\n const config = this.config as unknown as AdapterConfig;\n return new GoveeMqttClient(config.goveeEmail, config.goveePassword, this.log, this);\n },\n getSegmentDeviceList: () => {\n const devices = this.deviceManager?.getDevices() ?? [];\n return devices\n .filter(d => d.sku !== \"BaseGroup\" && d.state?.online === true && resolveSegmentCount(d) > 0)\n .map(d => ({\n value: wizardHandler.deviceKeyFor(d),\n label: `${d.name} (${d.sku}, bisher ${resolveSegmentCount(d)} Segmente)`,\n }));\n },\n runWizardStep: (action, deviceKey) => wizardHandler.runWizardStep(this, action, deviceKey),\n };\n }\n\n /**\n * Helper: clear `mqttVerificationCode` in adapter native after a successful\n * login or a 455-fail.\n *\n * Idempotent: liest erst den aktuellen Wert, schreibt nur wenn dirty.\n * Verhindert den Adapter-Restart der durch jeden\n * `extendForeignObjectAsync(system.adapter.X, native:...)`-Call ausgel\u00F6st\n * wird (Memory v2.1.3-Bug). Vorher gab es nach jedem 2FA-Login einen\n * unn\u00F6tigen Restart.\n *\n * @param obj ioBroker message object\n * @param data Response data payload\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 /** Construct host object for SnapshotHandler \u2014 adapter dependencies injected. */\n /** Dropdowns whose value is a mode-selection \u2014 reset to \"---\" (0) when the mode stops. */\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/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "govee-smart",
|
|
4
|
-
"version": "2.7.
|
|
4
|
+
"version": "2.7.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.7.1": {
|
|
7
|
+
"en": "Cleaner start-up log: the 'Starting' line now hints to wait for the 'ready' message, and the 'MQTT connected' info line is dropped (the ready summary lists all active channels anyway).",
|
|
8
|
+
"de": "Aufgeräumter Start-Log: die 'Starting'-Zeile weist jetzt darauf hin auf die 'ready'-Meldung zu warten, und die 'MQTT connected'-Info ist raus (die Ready-Zusammenfassung listet eh alle aktiven Kanäle).",
|
|
9
|
+
"ru": "Чище стартовый лог: строка 'Starting' теперь подсказывает дождаться сообщения 'ready', а 'MQTT connected' убрана из info-уровня (ready-сводка и так показывает все активные каналы).",
|
|
10
|
+
"pt": "Log de arranque mais limpo: a linha 'Starting' agora indica esperar pela mensagem 'ready', e o info 'MQTT connected' foi removido (o resumo ready já lista os canais ativos).",
|
|
11
|
+
"nl": "Schoner startup-log: de 'Starting'-regel verwijst nu naar het 'ready'-bericht, en 'MQTT connected' is geen info-regel meer (de ready-samenvatting toont toch alle actieve kanalen).",
|
|
12
|
+
"fr": "Log de démarrage plus propre : la ligne 'Starting' indique d'attendre le message 'ready', et l'info 'MQTT connected' est supprimée (le résumé ready liste de toute façon les canaux actifs).",
|
|
13
|
+
"it": "Log di avvio più pulito: la riga 'Starting' ora indica di attendere il messaggio 'ready', e l'info 'MQTT connected' è rimossa (il riepilogo ready elenca comunque i canali attivi).",
|
|
14
|
+
"es": "Log de arranque más limpio: la línea 'Starting' ahora indica esperar al mensaje 'ready', y el info 'MQTT connected' se eliminó (el resumen ready ya lista los canales activos).",
|
|
15
|
+
"pl": "Czystszy log startowy: linia 'Starting' wskazuje teraz, by czekać na komunikat 'ready', a info 'MQTT connected' usunięto (podsumowanie ready i tak listuje aktywne kanały).",
|
|
16
|
+
"uk": "Чистіший стартовий лог: рядок 'Starting' тепер вказує дочекатися повідомлення 'ready', а 'MQTT connected' прибрано з info (ready-зведення й так показує активні канали).",
|
|
17
|
+
"zh-cn": "更干净的启动日志:'Starting' 行现在提示等待 'ready' 消息,'MQTT connected' info 行已移除(ready 摘要本来就会列出所有活动通道)。"
|
|
18
|
+
},
|
|
6
19
|
"2.7.0": {
|
|
7
20
|
"en": "Snapshots created in the Govee Home app now appear in the dropdown (Issue #13). The refresh button is per-device under snapshots.refresh_cloud; info.refresh_cloud_data is removed.",
|
|
8
21
|
"de": "In der Govee Home App erstellte Snapshots erscheinen jetzt im Dropdown (Issue #13). Der Refresh-Button liegt pro Gerät unter snapshots.refresh_cloud; info.refresh_cloud_data ist entfernt.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Wskaźniki statusu połączenia same się przywracają po awarii Govee. Przywracanie snapshotu na taśmie LED z segmentami znów jest szybkie. Nieprawidłowe IP parowania i po cichu odrzucone polecenia Cloud są teraz raportowane. Segment 0 robił się niebieski przy krótkim hex – naprawione. Kreator wykrywania segmentów przywraca poprzedni widok przy zatrzymaniu adaptera. Odrzucony klucz API Govee daje teraz czytelną wskazówkę w logu.",
|
|
81
94
|
"uk": "Індикатори стану з'єднання відновлюються самі після збою Govee. Відновлення знімка на LED-стрічці з сегментами знову швидке. Неправильні IP сполучення та тихо відхилені команди Cloud тепер повідомляються. Сегмент 0 ставав синім за коротким hex – виправлено. Майстер виявлення сегментів відновлює попередній вигляд при зупинці адаптера. Відхилений Govee API-ключ тепер дає зрозумілу підказку в журналі.",
|
|
82
95
|
"zh-cn": "Govee 中断后连接状态指示自动恢复。多分段灯带的快照恢复再次变快。无效的配对 IP 和被悄悄拒绝的云命令现在都会上报。短 hex 输入下分段 0 显示蓝色已修复。停止适配器时段检测向导恢复先前画面。被拒绝的 Govee API key 现在会在日志中给出明确提示。"
|
|
83
|
-
},
|
|
84
|
-
"2.6.2": {
|
|
85
|
-
"en": "Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names, descriptions and dropdown labels (11 languages) and the segment-detection wizard text remain unchanged.",
|
|
86
|
-
"de": "Adapter-Logs sind jetzt nur noch auf Englisch, gemäß ioBroker-Community-Standard. Lokalisierte Datenpunkt-Namen, Beschreibungen und Dropdown-Labels (11 Sprachen) sowie der Segment-Erkennungs-Wizard bleiben erhalten.",
|
|
87
|
-
"ru": "Сообщения журнала теперь только на английском, согласно стандарту сообщества ioBroker. Локализованные имена состояний, описания и подписи (11 языков), а также мастер обнаружения сегментов сохраняются.",
|
|
88
|
-
"pt": "As mensagens de log do adaptador agora são apenas em inglês, conforme o padrão da comunidade ioBroker. Nomes de estados, descrições e rótulos localizados (11 idiomas) e o assistente de deteção de segmentos permanecem inalterados.",
|
|
89
|
-
"nl": "Adapter-logberichten zijn nu alleen Engels, conform de ioBroker-communitystandaard. Gelokaliseerde statusnamen, beschrijvingen en dropdown-labels (11 talen) en de segmentdetectie-wizard blijven ongewijzigd.",
|
|
90
|
-
"fr": "Les messages de log de l'adaptateur sont désormais uniquement en anglais, conformément au standard de la communauté ioBroker. Les noms d'états, descriptions et libellés localisés (11 langues) ainsi que l'assistant de détection de segments restent inchangés.",
|
|
91
|
-
"it": "I messaggi di log dell'adattatore sono ora solo in inglese, secondo lo standard della community ioBroker. I nomi degli stati, descrizioni ed etichette localizzati (11 lingue) e la procedura guidata di rilevamento dei segmenti rimangono invariati.",
|
|
92
|
-
"es": "Los mensajes de registro del adaptador ahora son solo en inglés, conforme al estándar de la comunidad ioBroker. Los nombres de estados, descripciones y etiquetas localizados (11 idiomas) y el asistente de detección de segmentos permanecen sin cambios.",
|
|
93
|
-
"pl": "Komunikaty dziennika adaptera są teraz wyłącznie po angielsku, zgodnie ze standardem społeczności ioBroker. Zlokalizowane nazwy stanów, opisy i etykiety (11 języków) oraz kreator wykrywania segmentów pozostają bez zmian.",
|
|
94
|
-
"uk": "Повідомлення журналу адаптера тепер лише англійською, відповідно до стандарту спільноти ioBroker. Локалізовані назви станів, описи та мітки (11 мов) і майстер виявлення сегментів залишаються без змін.",
|
|
95
|
-
"zh-cn": "适配器日志消息现在仅为英文,符合 ioBroker 社区标准。本地化的数据点名称、描述和下拉标签(11 种语言)以及段检测向导保持不变。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|