iobroker.govee-smart 2.5.2 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,15 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
124
  ---
125
125
 
126
126
  ## Changelog
127
+ ### 2.5.4 (2026-05-04)
128
+
129
+ - Test-Coverage erweitert für den MQTT-Login-Pfad: `mqtt.connect` ist jetzt als optionaler Constructor-Parameter injizierbar (analog `httpsRequest` in v2.5.1) und 7 neue Mock-Tests decken die getIotKey-Authentifizierung und den persistierten-Credentials-Reuse-Pfad ab.
130
+
131
+ ### 2.5.3 (2026-05-04)
132
+
133
+ - Segment-Erkennungs-Wizard: kein „has no existing object"-Spam mehr für Indizes oberhalb der echten Strip-Länge — Echo-Pakete werden defensiv gegen `segmentCount` gefiltert (Issue #8).
134
+ - Adapter-Init nach Restart: Befehle gegen ein noch-nicht-geladenes Cloud-Backend werden still verworfen statt mit „No channel available" zu warnen — der Befehl ist eh hin, der WARN war irreführend.
135
+
127
136
  ### 2.5.2 (2026-05-04)
128
137
 
129
138
  - WARN-Spam alle 2 Min behoben: `groups.*.info.membersUnreachable` bleibt bei vollständig erreichbaren Gruppen mit leerem Wert vorhanden statt gelöscht zu werden.
@@ -137,14 +146,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
137
146
 
138
147
  - F4 final: `onMessage`-Handler (sendTo aus dem Admin-UI) ist jetzt eine eigene Klasse mit Host-Interface. main.ts deutlich kleiner, Login-Test/2FA-Code-Anforderung isoliert testbar. Verhalten identisch.
139
148
 
140
- ### 2.4.1 (2026-05-04)
141
-
142
- - Group-Fan-Out-Pfad (Mitglieder-Steuerung beim Schalten der Gruppe) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` nochmal kleiner. Verhalten identisch.
143
-
144
- ### 2.4.0 (2026-05-04)
145
-
146
- - Lokaler Snapshot-Manager (Save/Restore/Delete) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` ist kleiner und der Snapshot-Pfad ist isoliert testbar. Verhalten identisch.
147
-
148
149
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
149
150
 
150
151
  ## Support
@@ -183,6 +183,10 @@ class CommandRouter {
183
183
  await this.sendCloudCommand(device, command, value);
184
184
  return;
185
185
  }
186
+ if (device.channels.cloud && !this.cloudClient) {
187
+ this.log.debug(`Command for ${device.name} dropped: Cloud client not ready yet`);
188
+ return;
189
+ }
186
190
  this.log.warn(`No channel available for ${device.name} (${device.sku})`);
187
191
  }
188
192
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/command-router.ts"],
4
- "sourcesContent": ["import { hexToRgb, logDedup, type ErrorCategory, type GoveeDevice, type TimerAdapter } from \"./types\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport { applySceneSpeed } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\n\n/**\n * Delay between switching the device into static-color mode and sending the\n * follow-up segment commands. Empirically the firmware needs ~150 ms for the\n * mode flip; shorter delays leave the device still in scene/music mode and the\n * subsequent segment writes are silently dropped.\n */\nconst FORCE_COLOR_MODE_SETTLE_MS = 150;\n\n/**\n * Command router \u2014 routes device commands through the fastest available\n * channel: LAN \u2192 Cloud.\n */\nexport class CommandRouter {\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private lanClient: GoveeLanClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /**\n * Letzte Fehler-Kategorie pro Cloud-Fallback-Fail \u2014 verhindert\n * log-Spam wenn das gleiche Symptom mehrfach kommt.\n */\n private lastCloudFallbackError: ErrorCategory | null = null;\n /** Dedup-Tracker f\u00FCr \u201Ekein Cloud-channel\"-Warns (M20). */\n private lastNoChannelCategory: ErrorCategory | null = null;\n\n /** Callback for batch segment state sync */\n onSegmentBatchUpdate?: (\n device: GoveeDevice,\n batch: { segments: number[]; color?: number; brightness?: number },\n ) => void;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper \u2014 routed through `this.setTimeout` so\n * pending color-mode delays get cleared on onUnload.\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.lanClient = client;\n }\n\n /**\n * Register the Cloud client\n *\n * @param client Cloud API client instance\n */\n setCloudClient(client: GoveeCloudClient): void {\n this.cloudClient = client;\n }\n\n /**\n * Register the rate limiter for cloud calls\n *\n * @param limiter Rate limiter instance\n */\n setRateLimiter(limiter: RateLimiter): void {\n this.rateLimiter = limiter;\n }\n\n /**\n * Execute a function through the rate limiter if available, or directly.\n *\n * @param fn Async function to execute\n * @param priority Queue priority (0 = highest)\n */\n async executeRateLimited(fn: () => Promise<void>, priority = 0): Promise<void> {\n if (this.rateLimiter) {\n await this.rateLimiter.tryExecute(fn, priority);\n } else {\n await fn();\n }\n }\n\n /**\n * Force the device into static-color mode before sending segment_color_setting\n * ptReal packets. Without this, the device silently ignores segment-level\n * overrides while it's in Scene/Gradient/Music mode \u2014 the classic \"I set\n * segment 5 red and nothing happened\" symptom. Sends a `colorwc` command with\n * the device's last-known colorRgb (so the strip doesn't visibly change if it\n * was already in color mode), then waits 150 ms so the firmware can switch.\n *\n * As a bonus: once the device is in color mode, subsequent segment commands\n * trigger AA A5 MQTT pushes \u2014 so the adapter learns the real segmentCount\n * automatically the first time the user touches segment controls.\n *\n * @param device Target device\n */\n private async forceColorMode(device: GoveeDevice): Promise<void> {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n const current = typeof device.state.colorRgb === \"string\" ? device.state.colorRgb : null;\n const { r, g, b } = current ? hexToRgb(current) : { r: 255, g: 255, b: 255 };\n this.lanClient.setColor(device.lanIp, r, g, b);\n // Delay routed through the adapter's timer wrapper so it gets cancelled\n // if the adapter unloads mid-delay. Native setTimeout would leave a\n // pending handle that fires into a half-torn-down adapter.\n await new Promise<void>(resolve => this.timers.setTimeout(() => resolve(), FORCE_COLOR_MODE_SETTLE_MS));\n }\n\n /**\n * Send a command to a device \u2014 routes through LAN \u2192 Cloud.\n * MQTT is status-push only and never used for commands.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n async sendCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // Segment color: LAN ptReal first, Cloud fallback\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, [segIdx]);\n return;\n }\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n return;\n }\n\n // Segment batch: LAN ptReal first (multi-segment bitmask), Cloud fallback.\n // Accepts either a string in the user-batch syntax (\"0-5:#ff0000:20\") or a\n // pre-parsed object { segments, color?, brightness? } from internal callers\n // like the detection wizard (avoids a string\u2192parse round-trip).\n if (command === \"segmentBatch\") {\n const parsed = typeof value === \"string\" ? this.parseSegmentBatch(device, value) : this.coerceParsedBatch(value);\n if (parsed) {\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n if (device.lanIp && this.lanClient && parsed) {\n await this.forceColorMode(device);\n if (parsed.color !== undefined) {\n const r = (parsed.color >> 16) & 0xff;\n const g = (parsed.color >> 8) & 0xff;\n const b = parsed.color & 0xff;\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, parsed.segments);\n }\n if (parsed.brightness !== undefined) {\n this.lanClient.setSegmentBrightness(device.lanIp, parsed.brightness, parsed.segments);\n }\n return;\n }\n if (device.channels.cloud && this.cloudClient && parsed) {\n await this.sendSegmentBatchParsed(device, typeof value === \"string\" ? value : \"\", parsed);\n return;\n }\n return;\n }\n\n // Segment brightness: LAN ptReal first, Cloud fallback\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n this.lanClient.setSegmentBrightness(device.lanIp, value as number, [segIdx]);\n return;\n }\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n return;\n }\n\n // Priority 1: LAN\n if (device.lanIp && this.lanClient) {\n this.sendLanCommand(device, command, value);\n return;\n }\n\n // Priority 2: Cloud (rate-limited)\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n\n this.log.warn(`No channel available for ${device.name} (${device.sku})`);\n }\n\n /**\n * Send a generic capability command via Cloud API.\n * Used for capability types not explicitly handled (toggle, dynamic_scene, etc.)\n *\n * @param device Target device\n * @param capabilityType Full capability type (e.g. \"devices.capabilities.toggle\")\n * @param capabilityInstance Capability instance name (e.g. \"gradientToggle\")\n * @param value Command value\n */\n async sendCapabilityCommand(\n device: GoveeDevice,\n capabilityType: string,\n capabilityInstance: string,\n value: unknown,\n ): Promise<void> {\n if (!this.cloudClient || !device.channels.cloud) {\n this.log.debug(`Cloud not available for generic command on ${device.name}`);\n return;\n }\n\n const shortType = capabilityType.replace(\"devices.capabilities.\", \"\");\n let cloudValue: unknown = value;\n\n if (shortType === \"toggle\") {\n cloudValue = value ? 1 : 0;\n }\n\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n capabilityType,\n capabilityInstance,\n cloudValue,\n );\n };\n\n await this.executeRateLimited(execute);\n }\n\n /**\n * Send a batch segment command with pre-parsed data.\n *\n * @param device Target device\n * @param commandStr Original command string (for error messages)\n * @param parsed Pre-parsed batch data (null = invalid command)\n */\n private async sendSegmentBatchParsed(\n device: GoveeDevice,\n commandStr: string,\n parsed: { segments: number[]; color?: number; brightness?: number } | null,\n ): Promise<void> {\n if (!this.cloudClient) {\n return;\n }\n\n if (!parsed) {\n this.log.warn(`Invalid segment command \"${commandStr}\" for ${device.name}`);\n return;\n }\n\n const cap = this.findCapabilityForCommand(device, \"segmentColor:0\");\n if (!cap) {\n this.log.debug(`No segment capability for ${device.name}`);\n return;\n }\n\n if (parsed.color !== undefined) {\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, {\n segment: parsed.segments,\n rgb: parsed.color,\n });\n };\n await this.executeRateLimited(execute);\n }\n\n if (parsed.brightness !== undefined) {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n const brightCap = caps.find(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.includes(\"segment_color_setting\") &&\n c.instance.toLowerCase().includes(\"brightness\"),\n );\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n (brightCap ?? cap).type,\n (brightCap ?? cap).instance,\n { segment: parsed.segments, brightness: parsed.brightness },\n );\n };\n await this.executeRateLimited(execute);\n }\n\n // Update individual segment states to stay in sync\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n\n /**\n * Parse batch segment command string.\n *\n * @param device Target device (for segment count)\n * @param cmd Command string (e.g. \"1-5:#ff0000:20\")\n */\n parseSegmentBatch(\n device: GoveeDevice,\n cmd: string,\n ): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n // Defensive guard \u2014 non-string input (e.g. from internal caller passing\n // an already-parsed object) would crash cmd.split(). Treat as no-op.\n if (typeof cmd !== \"string\") {\n return null;\n }\n const parts = cmd.split(\":\");\n if (parts.length < 1 || !parts[0]) {\n return null;\n }\n\n // Effective physical segments: honor manual override for cut strips\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? new Set(device.manualSegments)\n : null;\n const segCount = device.segmentCount ?? 0;\n const isValid = (i: number): boolean => (validIndices ? validIndices.has(i) : i >= 0 && i < segCount);\n\n // Parse segment indices\n const segStr = parts[0].trim();\n let segments: number[];\n\n if (segStr === \"all\") {\n // \"all\" expands to valid physical segments only (skip cut ones)\n segments = validIndices\n ? Array.from(validIndices).sort((a, b) => a - b)\n : Array.from({ length: segCount }, (_, i) => i);\n } else {\n segments = [];\n for (const part of segStr.split(\",\")) {\n const rangeMatch = /^(\\d+)-(\\d+)$/.exec(part.trim());\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n for (let i = start; i <= end; i++) {\n if (isValid(i)) {\n segments.push(i);\n }\n }\n } else {\n const idx = parseInt(part.trim(), 10);\n if (!isNaN(idx) && isValid(idx)) {\n segments.push(idx);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n return null;\n }\n\n // Parse color (#RRGGBB \u2192 packed int)\n let color: number | undefined;\n if (parts.length >= 2 && parts[1]) {\n const colorStr = parts[1].trim();\n if (/^#?[0-9a-fA-F]{6}$/.test(colorStr)) {\n color = parseInt(colorStr.replace(\"#\", \"\"), 16);\n }\n }\n\n // Parse brightness (0-100)\n let brightness: number | undefined;\n if (parts.length >= 3 && parts[2]) {\n const bri = parseInt(parts[2].trim(), 10);\n if (!isNaN(bri) && bri >= 0 && bri <= 100) {\n brightness = bri;\n }\n }\n\n if (color === undefined && brightness === undefined) {\n return null;\n }\n\n return { segments, color, brightness };\n }\n\n /**\n * Coerce a pre-parsed batch object (from internal callers) to the canonical\n * shape. Returns null if the input is not a valid {segments, ...} object.\n *\n * @param value Candidate object\n */\n private coerceParsedBatch(value: unknown): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n if (!value || typeof value !== \"object\") {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (!Array.isArray(v.segments) || v.segments.length === 0) {\n return null;\n }\n const segments = v.segments.filter(n => typeof n === \"number\" && Number.isFinite(n) && n >= 0) as number[];\n if (segments.length === 0) {\n return null;\n }\n const color = typeof v.color === \"number\" && Number.isFinite(v.color) ? v.color & 0xffffff : undefined;\n const brightness =\n typeof v.brightness === \"number\" && Number.isFinite(v.brightness)\n ? Math.max(0, Math.min(100, Math.round(v.brightness)))\n : undefined;\n if (color === undefined && brightness === undefined) {\n return null;\n }\n return { segments, color, brightness };\n }\n\n /**\n * Convert adapter value to Cloud API value\n *\n * @param device Target device (for scene/snapshot lookup)\n * @param command Command type\n * @param value Adapter-side value to convert\n */\n toCloudValue(device: GoveeDevice, command: string, value: unknown): unknown {\n switch (command) {\n case \"power\":\n return value ? 1 : 0;\n case \"brightness\":\n return value;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n return (r << 16) | (g << 8) | b;\n }\n case \"colorTemperature\":\n return value;\n case \"scene\":\n return value;\n case \"lightScene\": {\n // Value is the dropdown index (string) \u2014 resolve to scene activation payload\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid light scene index ${String(value)} for cloud`);\n return value;\n }\n return device.scenes[idx - 1].value;\n }\n case \"diyScene\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid DIY scene index ${String(value)} for cloud`);\n return value;\n }\n return device.diyScenes[idx - 1].value;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)} for cloud`);\n return value;\n }\n return device.snapshots[idx - 1].value;\n }\n default:\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n const { r, g, b } = hexToRgb(value as string);\n return { segment: [segIdx], rgb: (r << 16) | (g << 8) | b };\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n return { segment: [segIdx], brightness: value };\n }\n return value;\n }\n }\n\n /**\n * Find capability matching a command name\n *\n * @param device Target device\n * @param command Command type to find capability for\n */\n findCapabilityForCommand(device: GoveeDevice, command: string): { type: string; instance: string } | undefined {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n continue;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n if (command === \"power\" && shortType === \"on_off\") {\n return cap;\n }\n if (command === \"brightness\" && shortType === \"range\" && cap.instance.toLowerCase().includes(\"brightness\")) {\n return cap;\n }\n if (command === \"colorRgb\" && shortType === \"color_setting\" && cap.instance === \"colorRgb\") {\n return cap;\n }\n if (command === \"colorTemperature\" && shortType === \"color_setting\" && cap.instance.includes(\"colorTem\")) {\n return cap;\n }\n if (command === \"scene\" && shortType === \"mode\" && cap.instance === \"presetScene\") {\n return cap;\n }\n if (command === \"lightScene\" && shortType === \"dynamic_scene\" && cap.instance === \"lightScene\") {\n return cap;\n }\n if (command === \"diyScene\" && shortType === \"dynamic_scene\" && cap.instance === \"diyScene\") {\n return cap;\n }\n if (command === \"snapshot\" && shortType === \"dynamic_scene\" && cap.instance === \"snapshot\") {\n return cap;\n }\n if (\n command.startsWith(\"segmentColor:\") &&\n shortType === \"segment_color_setting\" &&\n !cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n if (\n command.startsWith(\"segmentBrightness:\") &&\n shortType === \"segment_color_setting\" &&\n cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n }\n return undefined;\n }\n\n /**\n * Send command via LAN UDP\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private sendLanCommand(device: GoveeDevice, command: string, value: unknown): void {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n\n switch (command) {\n case \"power\":\n this.lanClient.setPower(device.lanIp, value as boolean);\n break;\n case \"brightness\":\n this.lanClient.setBrightness(device.lanIp, value as number);\n break;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setColor(device.lanIp, r, g, b);\n break;\n }\n case \"colorTemperature\":\n this.lanClient.setColorTemperature(device.lanIp, value as number);\n break;\n case \"gradientToggle\":\n this.lanClient.setGradient(device.lanIp, value as boolean);\n break;\n case \"diyScene\": {\n // Try ptReal BLE-over-LAN if DIY scene is in library\n const diyIdx = parseInt(String(value), 10);\n if (isNaN(diyIdx) || diyIdx < 1 || diyIdx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid DIY scene index ${String(value)}`);\n return;\n }\n const diyScene = device.diyScenes[diyIdx - 1];\n if (diyScene) {\n const diyLib = device.diyLibrary.find(d => d.name === diyScene.name);\n if (diyLib) {\n this.log.debug(`ptReal DIY: ${diyScene.name} \u2192 code=${diyLib.diyCode}`);\n this.lanClient.setDiyScene(device.lanIp, diyLib.scenceParam ?? \"\");\n return;\n }\n }\n // No library match \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n case \"lightScene\": {\n // Try ptReal BLE-over-LAN if scene is in scene library\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid light scene index ${String(value)}`);\n return;\n }\n const scene = device.scenes[idx - 1];\n if (scene) {\n // Match by exact name first, then by base name (strip -A/-B suffix)\n const baseName = scene.name.replace(/-[A-Z]$/, \"\");\n const libEntry =\n device.sceneLibrary.find(s => s.name === scene.name) ?? device.sceneLibrary.find(s => s.name === baseName);\n if (libEntry) {\n const baseParam = libEntry.scenceParam ?? \"\";\n // Devices without segment hardware (e.g. H70B3 Curtain Lights)\n // can't parse the A3-framed multi-packet ptReal protocol that\n // `scenceParam` carries \u2014 those packets describe per-segment\n // animation data. On such devices the packets are silently\n // dropped: the scene activation never happens locally. Fall\n // straight to Cloud activation for them so the scene still\n // works. Simple scenes without scenceParam (older presets like\n // \"Valentine's Day\") still take the ptReal path \u2014 the single\n // activation packet is understood by every Govee light.\n const hasSegments = typeof device.segmentCount === \"number\" && device.segmentCount > 0;\n if (!hasSegments && baseParam.length > 0) {\n this.log.debug(\n `ptReal scene ${scene.name} skipped \u2014 ${device.sku} has no segments, falling through to Cloud`,\n );\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n return;\n }\n let param = baseParam;\n if (\n device.sceneSpeed !== undefined &&\n device.sceneSpeed > 0 &&\n libEntry.speedInfo?.supSpeed &&\n libEntry.speedInfo.config\n ) {\n param = applySceneSpeed(param, device.sceneSpeed, libEntry.speedInfo.config);\n }\n this.log.debug(`ptReal: ${scene.name} \u2192 code=${libEntry.sceneCode}`);\n this.lanClient.setScene(device.lanIp, libEntry.sceneCode, param);\n return;\n }\n }\n // Scene not in library \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return;\n }\n const cmdGroups = device.snapshotBleCmds?.[idx - 1];\n if (cmdGroups && cmdGroups.length > 0) {\n const allPackets = cmdGroups.flat();\n if (allPackets.length > 0) {\n this.log.debug(`ptReal Snapshot: ${device.snapshots[idx - 1].name} \u2192 ${allPackets.length} packets`);\n this.lanClient.sendPtReal(device.lanIp, allPackets);\n return;\n }\n }\n // No BLE data \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n default:\n // LAN doesn't support this command \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n }\n }\n\n /**\n * Send command via Cloud API (rate-limited)\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private async sendCloudCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // M19 \u2014 Closure capture: lokale Variable nach Guard. Verhindert Race\n // wenn `setCloudClient(null)` zwischen Guard-Check und executeRateLimited\n // l\u00E4uft (z.B. Adapter-Stop mid-await).\n const cloudClient = this.cloudClient;\n if (!cloudClient) {\n return;\n }\n\n // Find the matching capability\n const cap = this.findCapabilityForCommand(device, command);\n if (!cap) {\n // M20 \u2014 dedup-warn statt nur debug. User klickt einen State, kein\n // Channel-Match \u2192 Fehlersuche braucht das Erstauftreten als warn.\n this.lastNoChannelCategory = logDedup(\n this.log,\n this.lastNoChannelCategory,\n `No channel for ${device.name}/${command}`,\n new Error(\"no matching capability\"),\n );\n return;\n }\n\n const cloudValue = this.toCloudValue(device, command, value);\n\n const execute = async (): Promise<void> => {\n await cloudClient.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, cloudValue);\n };\n\n await this.executeRateLimited(execute);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { hexToRgb, logDedup, type ErrorCategory, type GoveeDevice, type TimerAdapter } from \"./types\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport { applySceneSpeed } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\n\n/**\n * Delay between switching the device into static-color mode and sending the\n * follow-up segment commands. Empirically the firmware needs ~150 ms for the\n * mode flip; shorter delays leave the device still in scene/music mode and the\n * subsequent segment writes are silently dropped.\n */\nconst FORCE_COLOR_MODE_SETTLE_MS = 150;\n\n/**\n * Command router \u2014 routes device commands through the fastest available\n * channel: LAN \u2192 Cloud.\n */\nexport class CommandRouter {\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private lanClient: GoveeLanClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /**\n * Letzte Fehler-Kategorie pro Cloud-Fallback-Fail \u2014 verhindert\n * log-Spam wenn das gleiche Symptom mehrfach kommt.\n */\n private lastCloudFallbackError: ErrorCategory | null = null;\n /** Dedup-Tracker f\u00FCr \u201Ekein Cloud-channel\"-Warns (M20). */\n private lastNoChannelCategory: ErrorCategory | null = null;\n\n /** Callback for batch segment state sync */\n onSegmentBatchUpdate?: (\n device: GoveeDevice,\n batch: { segments: number[]; color?: number; brightness?: number },\n ) => void;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper \u2014 routed through `this.setTimeout` so\n * pending color-mode delays get cleared on onUnload.\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.lanClient = client;\n }\n\n /**\n * Register the Cloud client\n *\n * @param client Cloud API client instance\n */\n setCloudClient(client: GoveeCloudClient): void {\n this.cloudClient = client;\n }\n\n /**\n * Register the rate limiter for cloud calls\n *\n * @param limiter Rate limiter instance\n */\n setRateLimiter(limiter: RateLimiter): void {\n this.rateLimiter = limiter;\n }\n\n /**\n * Execute a function through the rate limiter if available, or directly.\n *\n * @param fn Async function to execute\n * @param priority Queue priority (0 = highest)\n */\n async executeRateLimited(fn: () => Promise<void>, priority = 0): Promise<void> {\n if (this.rateLimiter) {\n await this.rateLimiter.tryExecute(fn, priority);\n } else {\n await fn();\n }\n }\n\n /**\n * Force the device into static-color mode before sending segment_color_setting\n * ptReal packets. Without this, the device silently ignores segment-level\n * overrides while it's in Scene/Gradient/Music mode \u2014 the classic \"I set\n * segment 5 red and nothing happened\" symptom. Sends a `colorwc` command with\n * the device's last-known colorRgb (so the strip doesn't visibly change if it\n * was already in color mode), then waits 150 ms so the firmware can switch.\n *\n * As a bonus: once the device is in color mode, subsequent segment commands\n * trigger AA A5 MQTT pushes \u2014 so the adapter learns the real segmentCount\n * automatically the first time the user touches segment controls.\n *\n * @param device Target device\n */\n private async forceColorMode(device: GoveeDevice): Promise<void> {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n const current = typeof device.state.colorRgb === \"string\" ? device.state.colorRgb : null;\n const { r, g, b } = current ? hexToRgb(current) : { r: 255, g: 255, b: 255 };\n this.lanClient.setColor(device.lanIp, r, g, b);\n // Delay routed through the adapter's timer wrapper so it gets cancelled\n // if the adapter unloads mid-delay. Native setTimeout would leave a\n // pending handle that fires into a half-torn-down adapter.\n await new Promise<void>(resolve => this.timers.setTimeout(() => resolve(), FORCE_COLOR_MODE_SETTLE_MS));\n }\n\n /**\n * Send a command to a device \u2014 routes through LAN \u2192 Cloud.\n * MQTT is status-push only and never used for commands.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n async sendCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // Segment color: LAN ptReal first, Cloud fallback\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, [segIdx]);\n return;\n }\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n return;\n }\n\n // Segment batch: LAN ptReal first (multi-segment bitmask), Cloud fallback.\n // Accepts either a string in the user-batch syntax (\"0-5:#ff0000:20\") or a\n // pre-parsed object { segments, color?, brightness? } from internal callers\n // like the detection wizard (avoids a string\u2192parse round-trip).\n if (command === \"segmentBatch\") {\n const parsed = typeof value === \"string\" ? this.parseSegmentBatch(device, value) : this.coerceParsedBatch(value);\n if (parsed) {\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n if (device.lanIp && this.lanClient && parsed) {\n await this.forceColorMode(device);\n if (parsed.color !== undefined) {\n const r = (parsed.color >> 16) & 0xff;\n const g = (parsed.color >> 8) & 0xff;\n const b = parsed.color & 0xff;\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, parsed.segments);\n }\n if (parsed.brightness !== undefined) {\n this.lanClient.setSegmentBrightness(device.lanIp, parsed.brightness, parsed.segments);\n }\n return;\n }\n if (device.channels.cloud && this.cloudClient && parsed) {\n await this.sendSegmentBatchParsed(device, typeof value === \"string\" ? value : \"\", parsed);\n return;\n }\n return;\n }\n\n // Segment brightness: LAN ptReal first, Cloud fallback\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n this.lanClient.setSegmentBrightness(device.lanIp, value as number, [segIdx]);\n return;\n }\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n return;\n }\n\n // Priority 1: LAN\n if (device.lanIp && this.lanClient) {\n this.sendLanCommand(device, command, value);\n return;\n }\n\n // Priority 2: Cloud (rate-limited)\n if (device.channels.cloud && this.cloudClient) {\n await this.sendCloudCommand(device, command, value);\n return;\n }\n\n // Cloud-f\u00E4hig aber Client noch nicht ready (Init-Phase nach Restart):\n // typischer Race-Fall, wenn der User sofort einen Befehl schickt w\u00E4hrend\n // der Adapter Cloud-Login + IoT-Key noch erledigt. Kein WARN, nur debug \u2014\n // der Befehl ist verloren, aber der User sieht eh wenige Sekunden sp\u00E4ter\n // dass alles l\u00E4uft. WARN war hier ein false alarm.\n if (device.channels.cloud && !this.cloudClient) {\n this.log.debug(`Command for ${device.name} dropped: Cloud client not ready yet`);\n return;\n }\n\n this.log.warn(`No channel available for ${device.name} (${device.sku})`);\n }\n\n /**\n * Send a generic capability command via Cloud API.\n * Used for capability types not explicitly handled (toggle, dynamic_scene, etc.)\n *\n * @param device Target device\n * @param capabilityType Full capability type (e.g. \"devices.capabilities.toggle\")\n * @param capabilityInstance Capability instance name (e.g. \"gradientToggle\")\n * @param value Command value\n */\n async sendCapabilityCommand(\n device: GoveeDevice,\n capabilityType: string,\n capabilityInstance: string,\n value: unknown,\n ): Promise<void> {\n if (!this.cloudClient || !device.channels.cloud) {\n this.log.debug(`Cloud not available for generic command on ${device.name}`);\n return;\n }\n\n const shortType = capabilityType.replace(\"devices.capabilities.\", \"\");\n let cloudValue: unknown = value;\n\n if (shortType === \"toggle\") {\n cloudValue = value ? 1 : 0;\n }\n\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n capabilityType,\n capabilityInstance,\n cloudValue,\n );\n };\n\n await this.executeRateLimited(execute);\n }\n\n /**\n * Send a batch segment command with pre-parsed data.\n *\n * @param device Target device\n * @param commandStr Original command string (for error messages)\n * @param parsed Pre-parsed batch data (null = invalid command)\n */\n private async sendSegmentBatchParsed(\n device: GoveeDevice,\n commandStr: string,\n parsed: { segments: number[]; color?: number; brightness?: number } | null,\n ): Promise<void> {\n if (!this.cloudClient) {\n return;\n }\n\n if (!parsed) {\n this.log.warn(`Invalid segment command \"${commandStr}\" for ${device.name}`);\n return;\n }\n\n const cap = this.findCapabilityForCommand(device, \"segmentColor:0\");\n if (!cap) {\n this.log.debug(`No segment capability for ${device.name}`);\n return;\n }\n\n if (parsed.color !== undefined) {\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, {\n segment: parsed.segments,\n rgb: parsed.color,\n });\n };\n await this.executeRateLimited(execute);\n }\n\n if (parsed.brightness !== undefined) {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n const brightCap = caps.find(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.includes(\"segment_color_setting\") &&\n c.instance.toLowerCase().includes(\"brightness\"),\n );\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n (brightCap ?? cap).type,\n (brightCap ?? cap).instance,\n { segment: parsed.segments, brightness: parsed.brightness },\n );\n };\n await this.executeRateLimited(execute);\n }\n\n // Update individual segment states to stay in sync\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n\n /**\n * Parse batch segment command string.\n *\n * @param device Target device (for segment count)\n * @param cmd Command string (e.g. \"1-5:#ff0000:20\")\n */\n parseSegmentBatch(\n device: GoveeDevice,\n cmd: string,\n ): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n // Defensive guard \u2014 non-string input (e.g. from internal caller passing\n // an already-parsed object) would crash cmd.split(). Treat as no-op.\n if (typeof cmd !== \"string\") {\n return null;\n }\n const parts = cmd.split(\":\");\n if (parts.length < 1 || !parts[0]) {\n return null;\n }\n\n // Effective physical segments: honor manual override for cut strips\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? new Set(device.manualSegments)\n : null;\n const segCount = device.segmentCount ?? 0;\n const isValid = (i: number): boolean => (validIndices ? validIndices.has(i) : i >= 0 && i < segCount);\n\n // Parse segment indices\n const segStr = parts[0].trim();\n let segments: number[];\n\n if (segStr === \"all\") {\n // \"all\" expands to valid physical segments only (skip cut ones)\n segments = validIndices\n ? Array.from(validIndices).sort((a, b) => a - b)\n : Array.from({ length: segCount }, (_, i) => i);\n } else {\n segments = [];\n for (const part of segStr.split(\",\")) {\n const rangeMatch = /^(\\d+)-(\\d+)$/.exec(part.trim());\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n for (let i = start; i <= end; i++) {\n if (isValid(i)) {\n segments.push(i);\n }\n }\n } else {\n const idx = parseInt(part.trim(), 10);\n if (!isNaN(idx) && isValid(idx)) {\n segments.push(idx);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n return null;\n }\n\n // Parse color (#RRGGBB \u2192 packed int)\n let color: number | undefined;\n if (parts.length >= 2 && parts[1]) {\n const colorStr = parts[1].trim();\n if (/^#?[0-9a-fA-F]{6}$/.test(colorStr)) {\n color = parseInt(colorStr.replace(\"#\", \"\"), 16);\n }\n }\n\n // Parse brightness (0-100)\n let brightness: number | undefined;\n if (parts.length >= 3 && parts[2]) {\n const bri = parseInt(parts[2].trim(), 10);\n if (!isNaN(bri) && bri >= 0 && bri <= 100) {\n brightness = bri;\n }\n }\n\n if (color === undefined && brightness === undefined) {\n return null;\n }\n\n return { segments, color, brightness };\n }\n\n /**\n * Coerce a pre-parsed batch object (from internal callers) to the canonical\n * shape. Returns null if the input is not a valid {segments, ...} object.\n *\n * @param value Candidate object\n */\n private coerceParsedBatch(value: unknown): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n if (!value || typeof value !== \"object\") {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (!Array.isArray(v.segments) || v.segments.length === 0) {\n return null;\n }\n const segments = v.segments.filter(n => typeof n === \"number\" && Number.isFinite(n) && n >= 0) as number[];\n if (segments.length === 0) {\n return null;\n }\n const color = typeof v.color === \"number\" && Number.isFinite(v.color) ? v.color & 0xffffff : undefined;\n const brightness =\n typeof v.brightness === \"number\" && Number.isFinite(v.brightness)\n ? Math.max(0, Math.min(100, Math.round(v.brightness)))\n : undefined;\n if (color === undefined && brightness === undefined) {\n return null;\n }\n return { segments, color, brightness };\n }\n\n /**\n * Convert adapter value to Cloud API value\n *\n * @param device Target device (for scene/snapshot lookup)\n * @param command Command type\n * @param value Adapter-side value to convert\n */\n toCloudValue(device: GoveeDevice, command: string, value: unknown): unknown {\n switch (command) {\n case \"power\":\n return value ? 1 : 0;\n case \"brightness\":\n return value;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n return (r << 16) | (g << 8) | b;\n }\n case \"colorTemperature\":\n return value;\n case \"scene\":\n return value;\n case \"lightScene\": {\n // Value is the dropdown index (string) \u2014 resolve to scene activation payload\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid light scene index ${String(value)} for cloud`);\n return value;\n }\n return device.scenes[idx - 1].value;\n }\n case \"diyScene\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid DIY scene index ${String(value)} for cloud`);\n return value;\n }\n return device.diyScenes[idx - 1].value;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)} for cloud`);\n return value;\n }\n return device.snapshots[idx - 1].value;\n }\n default:\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n const { r, g, b } = hexToRgb(value as string);\n return { segment: [segIdx], rgb: (r << 16) | (g << 8) | b };\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n return { segment: [segIdx], brightness: value };\n }\n return value;\n }\n }\n\n /**\n * Find capability matching a command name\n *\n * @param device Target device\n * @param command Command type to find capability for\n */\n findCapabilityForCommand(device: GoveeDevice, command: string): { type: string; instance: string } | undefined {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n continue;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n if (command === \"power\" && shortType === \"on_off\") {\n return cap;\n }\n if (command === \"brightness\" && shortType === \"range\" && cap.instance.toLowerCase().includes(\"brightness\")) {\n return cap;\n }\n if (command === \"colorRgb\" && shortType === \"color_setting\" && cap.instance === \"colorRgb\") {\n return cap;\n }\n if (command === \"colorTemperature\" && shortType === \"color_setting\" && cap.instance.includes(\"colorTem\")) {\n return cap;\n }\n if (command === \"scene\" && shortType === \"mode\" && cap.instance === \"presetScene\") {\n return cap;\n }\n if (command === \"lightScene\" && shortType === \"dynamic_scene\" && cap.instance === \"lightScene\") {\n return cap;\n }\n if (command === \"diyScene\" && shortType === \"dynamic_scene\" && cap.instance === \"diyScene\") {\n return cap;\n }\n if (command === \"snapshot\" && shortType === \"dynamic_scene\" && cap.instance === \"snapshot\") {\n return cap;\n }\n if (\n command.startsWith(\"segmentColor:\") &&\n shortType === \"segment_color_setting\" &&\n !cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n if (\n command.startsWith(\"segmentBrightness:\") &&\n shortType === \"segment_color_setting\" &&\n cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n }\n return undefined;\n }\n\n /**\n * Send command via LAN UDP\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private sendLanCommand(device: GoveeDevice, command: string, value: unknown): void {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n\n switch (command) {\n case \"power\":\n this.lanClient.setPower(device.lanIp, value as boolean);\n break;\n case \"brightness\":\n this.lanClient.setBrightness(device.lanIp, value as number);\n break;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setColor(device.lanIp, r, g, b);\n break;\n }\n case \"colorTemperature\":\n this.lanClient.setColorTemperature(device.lanIp, value as number);\n break;\n case \"gradientToggle\":\n this.lanClient.setGradient(device.lanIp, value as boolean);\n break;\n case \"diyScene\": {\n // Try ptReal BLE-over-LAN if DIY scene is in library\n const diyIdx = parseInt(String(value), 10);\n if (isNaN(diyIdx) || diyIdx < 1 || diyIdx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid DIY scene index ${String(value)}`);\n return;\n }\n const diyScene = device.diyScenes[diyIdx - 1];\n if (diyScene) {\n const diyLib = device.diyLibrary.find(d => d.name === diyScene.name);\n if (diyLib) {\n this.log.debug(`ptReal DIY: ${diyScene.name} \u2192 code=${diyLib.diyCode}`);\n this.lanClient.setDiyScene(device.lanIp, diyLib.scenceParam ?? \"\");\n return;\n }\n }\n // No library match \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n case \"lightScene\": {\n // Try ptReal BLE-over-LAN if scene is in scene library\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid light scene index ${String(value)}`);\n return;\n }\n const scene = device.scenes[idx - 1];\n if (scene) {\n // Match by exact name first, then by base name (strip -A/-B suffix)\n const baseName = scene.name.replace(/-[A-Z]$/, \"\");\n const libEntry =\n device.sceneLibrary.find(s => s.name === scene.name) ?? device.sceneLibrary.find(s => s.name === baseName);\n if (libEntry) {\n const baseParam = libEntry.scenceParam ?? \"\";\n // Devices without segment hardware (e.g. H70B3 Curtain Lights)\n // can't parse the A3-framed multi-packet ptReal protocol that\n // `scenceParam` carries \u2014 those packets describe per-segment\n // animation data. On such devices the packets are silently\n // dropped: the scene activation never happens locally. Fall\n // straight to Cloud activation for them so the scene still\n // works. Simple scenes without scenceParam (older presets like\n // \"Valentine's Day\") still take the ptReal path \u2014 the single\n // activation packet is understood by every Govee light.\n const hasSegments = typeof device.segmentCount === \"number\" && device.segmentCount > 0;\n if (!hasSegments && baseParam.length > 0) {\n this.log.debug(\n `ptReal scene ${scene.name} skipped \u2014 ${device.sku} has no segments, falling through to Cloud`,\n );\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n return;\n }\n let param = baseParam;\n if (\n device.sceneSpeed !== undefined &&\n device.sceneSpeed > 0 &&\n libEntry.speedInfo?.supSpeed &&\n libEntry.speedInfo.config\n ) {\n param = applySceneSpeed(param, device.sceneSpeed, libEntry.speedInfo.config);\n }\n this.log.debug(`ptReal: ${scene.name} \u2192 code=${libEntry.sceneCode}`);\n this.lanClient.setScene(device.lanIp, libEntry.sceneCode, param);\n return;\n }\n }\n // Scene not in library \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return;\n }\n const cmdGroups = device.snapshotBleCmds?.[idx - 1];\n if (cmdGroups && cmdGroups.length > 0) {\n const allPackets = cmdGroups.flat();\n if (allPackets.length > 0) {\n this.log.debug(`ptReal Snapshot: ${device.snapshots[idx - 1].name} \u2192 ${allPackets.length} packets`);\n this.lanClient.sendPtReal(device.lanIp, allPackets);\n return;\n }\n }\n // No BLE data \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n break;\n }\n default:\n // LAN doesn't support this command \u2014 fall through to Cloud\n this.sendCloudCommand(device, command, value).catch(e => {\n this.lastCloudFallbackError = logDedup(\n this.log,\n this.lastCloudFallbackError,\n `Cloud fallback for ${device.name}/${command}`,\n e,\n );\n });\n }\n }\n\n /**\n * Send command via Cloud API (rate-limited)\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private async sendCloudCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // M19 \u2014 Closure capture: lokale Variable nach Guard. Verhindert Race\n // wenn `setCloudClient(null)` zwischen Guard-Check und executeRateLimited\n // l\u00E4uft (z.B. Adapter-Stop mid-await).\n const cloudClient = this.cloudClient;\n if (!cloudClient) {\n return;\n }\n\n // Find the matching capability\n const cap = this.findCapabilityForCommand(device, command);\n if (!cap) {\n // M20 \u2014 dedup-warn statt nur debug. User klickt einen State, kein\n // Channel-Match \u2192 Fehlersuche braucht das Erstauftreten als warn.\n this.lastNoChannelCategory = logDedup(\n this.log,\n this.lastNoChannelCategory,\n `No channel for ${device.name}/${command}`,\n new Error(\"no matching capability\"),\n );\n return;\n }\n\n const cloudValue = this.toCloudValue(device, command, value);\n\n const execute = async (): Promise<void> => {\n await cloudClient.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, cloudValue);\n };\n\n await this.executeRateLimited(execute);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -66,6 +66,7 @@ class GoveeMqttClient {
66
66
  log;
67
67
  timers;
68
68
  httpsRequestImpl;
69
+ mqttConnectImpl;
69
70
  client = null;
70
71
  accountTopic = "";
71
72
  _bearerToken = "";
@@ -109,13 +110,15 @@ class GoveeMqttClient {
109
110
  * @param log ioBroker logger
110
111
  * @param timers Timer adapter
111
112
  * @param httpsRequestImpl optional DI für Tests — Default ist die echte httpsRequest
113
+ * @param mqttConnectImpl optional DI für Tests — Default ist die echte mqtt.connect
112
114
  */
113
- constructor(email, password, log, timers, httpsRequestImpl = import_http_client.httpsRequest) {
115
+ constructor(email, password, log, timers, httpsRequestImpl = import_http_client.httpsRequest, mqttConnectImpl = mqtt.connect) {
114
116
  this.email = email;
115
117
  this.password = password;
116
118
  this.log = log;
117
119
  this.timers = timers;
118
120
  this.httpsRequestImpl = httpsRequestImpl;
121
+ this.mqttConnectImpl = mqttConnectImpl;
119
122
  this.clientId = (0, import_govee_constants.deriveGoveeClientId)(email);
120
123
  }
121
124
  /**
@@ -288,7 +291,7 @@ class GoveeMqttClient {
288
291
  });
289
292
  this.scheduleProactiveRefresh(expiresAt);
290
293
  const clientId = `AP/${this.accountId}/${this.sessionUuid}`;
291
- this.client = mqtt.connect(`mqtts://${endpoint}:8883`, {
294
+ this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {
292
295
  clientId,
293
296
  key,
294
297
  cert,
@@ -463,7 +466,7 @@ class GoveeMqttClient {
463
466
  const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;
464
467
  this.log.debug("MQTT: trying cached credentials (no fresh login)");
465
468
  this.persistedAttemptInFlight = true;
466
- this.client = mqtt.connect(`mqtts://${creds.iotEndpoint}:8883`, {
469
+ this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {
467
470
  clientId,
468
471
  key: extracted.key,
469
472
  cert: extracted.cert,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/govee-mqtt-client.ts"],
4
- "sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to clear it\n if (codeWasSent) {\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = mqtt.connect(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings\");\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: verification code rejected \u2014 request a fresh code\");\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\"MQTT not connected: login rejected \u2014 check email/password\");\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = mqtt.connect(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to clear it\n if (codeWasSent) {\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings\");\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(\"MQTT not connected: verification code rejected \u2014 request a fresh code\");\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\"MQTT not connected: login rejected \u2014 check email/password\");\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n try {\n const raw = JSON.parse(payload.toString()) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n return this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n }\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private getIotKey(): Promise<GoveeIotKeyResponse> {\n return this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
package/build/main.js CHANGED
@@ -209,7 +209,11 @@ class GoveeAdapter extends utils.Adapter {
209
209
  };
210
210
  this.deviceManager.onSegmentBatchUpdate = (device, batch) => {
211
211
  const prefix = this.stateManager.devicePrefix(device);
212
+ const cap = typeof device.segmentCount === "number" && device.segmentCount > 0 ? device.segmentCount : 0;
212
213
  for (const idx of batch.segments) {
214
+ if (cap === 0 || idx >= cap) {
215
+ continue;
216
+ }
213
217
  if (batch.color !== void 0) {
214
218
  const hex = (0, import_types.rgbIntToHex)(batch.color);
215
219
  this.setStateAsync(`${prefix}.segments.${idx}.color`, {
@@ -229,7 +233,11 @@ class GoveeAdapter extends utils.Adapter {
229
233
  };
230
234
  this.deviceManager.onMqttSegmentUpdate = (device, segments) => {
231
235
  const prefix = this.stateManager.devicePrefix(device);
236
+ const cap = typeof device.segmentCount === "number" && device.segmentCount > 0 ? device.segmentCount : 0;
232
237
  for (const seg of segments) {
238
+ if (cap === 0 || seg.index >= cap) {
239
+ continue;
240
+ }
233
241
  this.setStateAsync(`${prefix}.segments.${seg.index}.color`, {
234
242
  val: (0, import_types.rgbToHex)(seg.r, seg.g, seg.b),
235
243
  ack: true