iobroker.govee-smart 2.1.2 → 2.1.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
@@ -1,7 +1,7 @@
1
1
  # ioBroker.govee-smart
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/iobroker.govee-smart)](https://www.npmjs.com/package/iobroker.govee-smart)
4
- ![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
4
+ ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
5
5
  ![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![npm downloads](https://img.shields.io/npm/dt/iobroker.govee-smart)](https://www.npmjs.com/package/iobroker.govee-smart)
@@ -63,7 +63,7 @@ Full user documentation lives in the **[Wiki](https://github.com/krobipd/ioBroke
63
63
 
64
64
  ## Requirements
65
65
 
66
- - Node.js >= 20
66
+ - Node.js >= 22
67
67
  - ioBroker js-controller >= 7.0.7
68
68
  - ioBroker Admin >= 7.7.22
69
69
  - A Govee account and at least one Govee WiFi device. LAN control needs a light with LAN mode enabled in the Govee Home app — see Govee's [LAN-supported device list](https://app-h5.govee.com/user-manual/wlan-guide).
@@ -124,6 +124,18 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
124
  ---
125
125
 
126
126
  ## Changelog
127
+ ### 2.1.4 (2026-05-03)
128
+
129
+ - Online status correct again after adapter restart — lights flip to online with the first LAN scan, sensors with the first cloud poll (5 s after start instead of 2 minutes).
130
+
131
+ ### 2.1.3 (2026-05-03)
132
+
133
+ - Critical fix: no more restart-loop after entering the verification code. The cached login is now stored in a state, not in the adapter config — saving the config doesn't trigger a restart anymore.
134
+ - Saving email + password in the adapter config works again. The previous loop made it look like only the "Test login" button worked.
135
+ - Honest startup messages: when MQTT really doesn't connect within the first minute, the log says why ("login rejected", "verification needed", etc.) instead of "still pending".
136
+ - Verification warning shortened. The full step-by-step instructions live in the Wiki, the log only states the action.
137
+ - "MQTT connected to AWS IoT" → "MQTT connected". "OpenAPI MQTT" → "Cloud-events" in user-facing logs.
138
+
127
139
  ### 2.1.2 (2026-05-02)
128
140
 
129
141
  - The verification message no longer claims your account has 2FA when it doesn't. Govee asks for a one-time check the first time it sees this client — same dialog, but the wording matches reality now.
@@ -149,15 +161,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
149
161
  - The Refresh Cloud Data button reloads the scene / music / DIY libraries again (had been skipped since v1.10.1).
150
162
  - Min js-controller `>=7.0.7`, min admin `>=7.7.22`.
151
163
 
152
- ### 2.0.3 (2026-04-26)
153
-
154
- - Min js-controller `>=6.0.11`, admin `>=7.6.20` (correcting an accidental bump in 2.0.2).
155
-
156
- ### 2.0.2 (2026-04-26)
157
-
158
- - Sensor and appliance events (lack-of-water, ice-bucket-full, etc.) now arrive reliably across reconnects. Govee used to treat each reconnect as a new connection and drop the subscription.
159
- - Min js-controller `>=7.0.23`.
160
-
161
164
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
162
165
 
163
166
  ## Support
@@ -701,7 +701,7 @@ class DeviceManager {
701
701
  * @param lanDevice Discovered LAN device
702
702
  */
703
703
  handleLanDiscovery(lanDevice) {
704
- var _a, _b;
704
+ var _a, _b, _c;
705
705
  let matched;
706
706
  for (const dev of this.devices.values()) {
707
707
  if ((0, import_types.normalizeDeviceId)(dev.deviceId) === (0, import_types.normalizeDeviceId)(lanDevice.device)) {
@@ -715,6 +715,7 @@ class DeviceManager {
715
715
  }
716
716
  if (matched) {
717
717
  const ipChanged = matched.lanIp !== lanDevice.ip;
718
+ const wasOffline = matched.state.online !== true;
718
719
  matched.lanIp = lanDevice.ip;
719
720
  matched.channels.lan = true;
720
721
  matched.lastSeenOnNetwork = Date.now();
@@ -722,6 +723,10 @@ class DeviceManager {
722
723
  this.log.debug(`LAN: ${matched.name} (${matched.sku}) at ${lanDevice.ip}`);
723
724
  (_a = this.onLanIpChanged) == null ? void 0 : _a.call(this, matched, lanDevice.ip);
724
725
  }
726
+ if (wasOffline) {
727
+ matched.state.online = true;
728
+ (_b = this.onDeviceUpdate) == null ? void 0 : _b.call(this, matched, { online: true });
729
+ }
725
730
  } else {
726
731
  const shortId = (0, import_types.normalizeDeviceId)(lanDevice.device).slice(-4);
727
732
  const device = {
@@ -746,7 +751,7 @@ class DeviceManager {
746
751
  this.diagnostics.addLog(lanDevice.device, "info", `LAN-discovered at ${lanDevice.ip}`);
747
752
  this.log.debug(`LAN: New device ${lanDevice.sku} at ${lanDevice.ip}`);
748
753
  this.maybeNudgeSeedSku(lanDevice.sku, device.name);
749
- (_b = this.onDeviceListChanged) == null ? void 0 : _b.call(this, this.getDevices());
754
+ (_c = this.onDeviceListChanged) == null ? void 0 : _c.call(this, this.getDevices());
750
755
  }
751
756
  }
752
757
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/device-manager.ts"],
4
- "sourcesContent": ["import { hasDynamicSceneCapability } from \"./capability-mapper\";\nimport { CommandRouter } from \"./command-router\";\nimport { getDeviceQuirks, getDeviceTier, isSeedAndDormant } from \"./device-registry\";\nimport { DiagnosticsCollector } from \"./diagnostics\";\nimport type { AppDeviceEntry, GoveeApiClient } from \"./govee-api-client\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\nimport type { CachedDeviceData, SkuCache } from \"./sku-cache\";\nimport {\n classifyError,\n normalizeDeviceId,\n rgbToHex,\n type CloudDevice,\n type CloudLoadResult,\n type CloudStateCapability,\n type DeviceState,\n type ErrorCategory,\n type GoveeDevice,\n type LanDevice,\n type MqttStatusUpdate,\n type TimerAdapter,\n} from \"./types\";\nimport { HttpError } from \"./http-client\";\n\n/** Parsed per-segment data from MQTT BLE packets */\nexport interface MqttSegmentData {\n /** Segment index (0-based) */\n index: number;\n /** Per-segment brightness 0-100 */\n brightness: number;\n /** Red channel 0-255 */\n r: number;\n /** Green channel 0-255 */\n g: number;\n /** Blue channel 0-255 */\n b: number;\n}\n\n/**\n * Parse AA A5 BLE notification packets from MQTT op.command.\n * 5 packets \u00D7 4 segment slots = max 20 segments per push. The device sends\n * exactly as many packets as it has physical segments \u2014 so parsing out all\n * slots (and filtering empty-slot padding) gives us a reliable count of\n * what actually exists on the strip.\n *\n * Format per slot: [Brightness 0-100] [R] [G] [B].\n *\n * An \"empty\" slot (brightness = 0 AND r = g = b = 0) is treated as padding\n * in a partially-filled final packet, not as a real unlit segment \u2014 this\n * matters for devices that don't pad their last packet to 4 slots.\n *\n * @param commands Base64-encoded BLE packets from MQTT op.command\n */\nexport function parseMqttSegmentData(commands: string[]): MqttSegmentData[] {\n if (!Array.isArray(commands)) {\n return [];\n }\n\n const segments: MqttSegmentData[] = [];\n // Track the highest packetNum seen so we know where real data ends.\n let highestPacket = 0;\n\n for (const cmd of commands) {\n if (typeof cmd !== \"string\") {\n continue;\n }\n const bytes = Buffer.from(cmd, \"base64\");\n if (bytes.length < 20 || bytes[0] !== 0xaa || bytes[1] !== 0xa5) {\n continue;\n }\n\n const packetNum = bytes[2];\n if (packetNum < 1 || packetNum > 5) {\n continue;\n }\n if (packetNum > highestPacket) {\n highestPacket = packetNum;\n }\n\n const baseIndex = (packetNum - 1) * 4;\n for (let slot = 0; slot < 4; slot++) {\n const segIdx = baseIndex + slot;\n const offset = 3 + slot * 4;\n segments.push({\n index: segIdx,\n brightness: bytes[offset],\n r: bytes[offset + 1],\n g: bytes[offset + 2],\n b: bytes[offset + 3],\n });\n }\n }\n\n // Trim trailing padding slots from the highest packet: Govee pads short\n // packets with 0x00 bytes, so a run of all-zero slots at the end is not\n // real segment data but filler. Keep any zero-slots that are followed by\n // a real one \u2014 they're legitimately-unlit middle segments.\n while (segments.length > 0) {\n const tail = segments[segments.length - 1];\n if (tail.brightness === 0 && tail.r === 0 && tail.g === 0 && tail.b === 0) {\n segments.pop();\n } else {\n break;\n }\n }\n\n return segments;\n}\n\n/**\n * Effective physical segment indices for a device.\n * Uses `device.manualSegments` when `device.manualMode=true` (cut strip override),\n * falls back to `0..segmentCount-1` otherwise. Empty if device has no segments.\n *\n * @param device Target device\n */\nexport function getEffectiveSegmentIndices(device: GoveeDevice): number[] {\n if (device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0) {\n return device.manualSegments.slice();\n }\n const count = device.segmentCount ?? 0;\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => i);\n}\n\n/**\n * Resolve the authoritative segment count for a device.\n *\n * Priority:\n * 1. `device.segmentCount` if already set (from cache, MQTT discovery, or wizard)\n * 2. Minimum of positive `segment_color_setting` capability counts\n * 3. 0 if no capability advertises segments\n *\n * Why `min` over the capability caps: Govee reports `segmentedBrightness` and\n * `segmentedColorRgb` separately, and on at least one SKU (H70D1) those two\n * disagree \u2014 brightness says 10, colorRgb says 15, real device has 10.\n * Picking the smaller value is the safer starting point; MQTT discovery can\n * then grow it if the real device pushes more slots.\n *\n * @param device Target device\n */\nexport function resolveSegmentCount(device: GoveeDevice): number {\n if (typeof device.segmentCount === \"number\" && device.segmentCount > 0) {\n return device.segmentCount;\n }\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n let min = Number.POSITIVE_INFINITY;\n for (const c of caps) {\n if (!c || typeof c.type !== \"string\" || !c.type.includes(\"segment_color_setting\")) {\n continue;\n }\n const params = (c as { parameters?: { fields?: unknown[] } }).parameters;\n const fields = Array.isArray(params?.fields) ? params.fields : [];\n for (const f of fields) {\n if (!f || typeof f !== \"object\") {\n continue;\n }\n const fn = (f as { fieldName?: unknown }).fieldName;\n const er = (f as { elementRange?: { max?: unknown } }).elementRange;\n const rawMax = er && typeof er.max === \"number\" ? er.max : -1;\n if (fn === \"segment\" && rawMax >= 0) {\n const n = rawMax + 1;\n if (n > 0 && n < min) {\n min = n;\n }\n }\n }\n }\n return Number.isFinite(min) ? min : 0;\n}\n\n/** Protocol limit: Govee's segment bitmask is 7 bytes \u00D7 8 bits = 56 slots (0..55). */\nexport const SEGMENT_HARD_MAX = 55;\n\n/**\n * Device manager \u2014 maintains unified device list and routes commands\n * through the fastest available channel: LAN \u2192 Cloud.\n * MQTT is status-push only and never used for commands.\n */\nexport class DeviceManager {\n private readonly log: ioBroker.Logger;\n private readonly devices = new Map<string, GoveeDevice>();\n private readonly commandRouter: CommandRouter;\n private readonly diagnostics: DiagnosticsCollector;\n /** SKUs we already nudged about \u2014 log only once per adapter lifetime, per SKU. */\n private readonly nudgedSeedSkus = new Set<string>();\n private cloudClient: GoveeCloudClient | null = null;\n private apiClient: GoveeApiClient | null = null;\n private skuCache: SkuCache | null = null;\n private onDeviceUpdate: ((device: GoveeDevice, state: Partial<DeviceState>) => void) | null = null;\n private onDeviceListChanged: ((devices: GoveeDevice[]) => void) | null = null;\n private onCloudCapabilities: ((device: GoveeDevice, caps: CloudStateCapability[]) => void) | null = null;\n /** Per-source dedup so a Cloud NETWORK error doesn't shadow an App-API one. */\n private lastErrorCategory: ErrorCategory | null = null;\n private lastAppApiErrorCategory: ErrorCategory | null = null;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper (forwarded to CommandRouter for\n * onUnload-safe delays).\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.commandRouter = new CommandRouter(log, timers);\n this.diagnostics = new DiagnosticsCollector();\n }\n\n /**\n * Expose the diagnostics collector so adapter-side hooks (MQTT,\n * Cloud, log wrapper) can write into the per-device ring buffers.\n */\n getDiagnostics(): DiagnosticsCollector {\n return this.diagnostics;\n }\n\n /**\n * Pull the HTTP status code out of any error shape we know about\n * (HttpError, Govee API responses with `.statusCode` / `.status`).\n * Returns undefined for network errors / generic failures so the\n * diagnostics entry shows \"no status \u2014 likely network/timeout\".\n *\n * @param e Caught error value\n */\n private extractStatus(e: unknown): number | undefined {\n if (e instanceof HttpError) {\n return e.statusCode;\n }\n if (typeof e === \"object\" && e !== null) {\n const x = e as { statusCode?: unknown; status?: unknown };\n if (typeof x.statusCode === \"number\") {\n return x.statusCode;\n }\n if (typeof x.status === \"number\") {\n return x.status;\n }\n }\n return undefined;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.commandRouter.setLanClient(client);\n }\n\n /**\n * Register the undocumented API client for scene/music/DIY libraries\n *\n * @param client API client instance\n */\n setApiClient(client: GoveeApiClient): void {\n this.apiClient = 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 this.commandRouter.setCloudClient(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.commandRouter.setRateLimiter(limiter);\n }\n\n /**\n * Register the SKU cache for persistent device data\n *\n * @param cache SKU cache instance\n */\n setSkuCache(cache: SkuCache): void {\n this.skuCache = cache;\n }\n\n /**\n * Set callbacks for device state changes and list changes.\n *\n * @param onUpdate Called when a device state changes (from any channel)\n * @param onListChanged Called when the device list changes (new/removed devices)\n */\n setCallbacks(\n onUpdate: (device: GoveeDevice, state: Partial<DeviceState>) => void,\n onListChanged: (devices: GoveeDevice[]) => void,\n ): void {\n this.onDeviceUpdate = onUpdate;\n this.onDeviceListChanged = onListChanged;\n }\n\n /** Get all known devices */\n getDevices(): GoveeDevice[] {\n return Array.from(this.devices.values());\n }\n\n /**\n * Load devices from local SKU cache.\n * Returns true if any devices were loaded (= Cloud not needed).\n */\n loadFromCache(): boolean {\n if (!this.skuCache) {\n return false;\n }\n\n const cached = this.skuCache.loadAll();\n if (cached.length === 0) {\n return false;\n }\n\n let changed = false;\n for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n if (existing) {\n // Merge cached data into LAN-discovered device. Segment-specific\n // fields (segmentCount, manualMode, manualSegments) MUST be merged\n // too \u2014 LAN discovery runs before the cache load on every start, so\n // the existing-branch is the normal path. Missing these three meant\n // every restart threw away the wizard/MQTT-learned segment state and\n // fell back to Cloud's min-advertised count.\n existing.name = entry.name || existing.name;\n existing.type = entry.type || existing.type;\n existing.capabilities = entry.capabilities;\n existing.scenes = entry.scenes;\n existing.diyScenes = entry.diyScenes;\n existing.snapshots = entry.snapshots;\n existing.sceneLibrary = entry.sceneLibrary;\n existing.musicLibrary = entry.musicLibrary;\n existing.diyLibrary = entry.diyLibrary;\n existing.skuFeatures = entry.skuFeatures;\n existing.snapshotBleCmds = entry.snapshotBleCmds;\n existing.scenesChecked = entry.scenesChecked;\n existing.lastSeenOnNetwork = entry.lastSeenOnNetwork;\n existing.segmentCount = entry.segmentCount;\n existing.manualMode = entry.manualMode;\n existing.manualSegments = entry.manualSegments;\n existing.channels.cloud = entry.capabilities.length > 0;\n changed = true;\n } else {\n this.devices.set(key, this.cachedToGoveeDevice(entry));\n changed = true;\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // Always refetch cloud data on startup \u2014 scenesChecked is purely diagnostic\n // now, not a gate. Snapshots are user-content (created dynamically in the\n // Govee Home app) and would miss new entries if we relied solely on the\n // cache. The refetch costs one call per light device per startup, well\n // within rate limits. Users can also trigger a fresh fetch without\n // restart via `info.refresh_cloud_data`.\n const hasLight = Array.from(this.devices.values()).some(d => d.type === \"devices.types.light\");\n if (hasLight) {\n this.log.debug(\"Cache loaded \u2014 will refresh scenes/snapshots via Cloud\");\n return false;\n }\n\n // Fill scenes from sceneLibrary for devices where Cloud scenes are missing\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n return cached.length > 0;\n }\n\n /**\n * Load devices from Cloud API and save to cache.\n * Only called when cache is empty (first start) or manual refresh.\n */\n async loadFromCloud(): Promise<CloudLoadResult> {\n if (!this.cloudClient) {\n return { ok: false, reason: \"transient\" };\n }\n\n try {\n const rawCloudDevices = await this.cloudClient.getDevices();\n\n // Hard-filter: Govee's Device-List API returns historical/stale entries\n // (deleted devices that are no longer in the app). Filter out entries\n // without capabilities \u2014 those are almost certainly stale registrations.\n const cloudDevices = Array.isArray(rawCloudDevices)\n ? rawCloudDevices.filter(\n cd =>\n cd &&\n typeof cd.sku === \"string\" &&\n typeof cd.device === \"string\" &&\n Array.isArray(cd.capabilities) &&\n cd.capabilities.length > 0,\n )\n : [];\n\n if (Array.isArray(rawCloudDevices) && rawCloudDevices.length !== cloudDevices.length) {\n this.log.info(\n `Cloud: received ${rawCloudDevices.length} devices raw, ${cloudDevices.length} after filter (skipped stale entries without capabilities)`,\n );\n }\n\n // Step 1: Merge Cloud devices into local device map\n let changed = this.mergeCloudDevices(cloudDevices);\n\n // Step 2: Load scenes, snapshots, and libraries for any device that\n // exposes a `dynamic_scene` capability \u2014 independent of `cd.type`.\n // Govee occasionally returns devices with `type` missing or a value\n // we don't recognise; keying off the capability is what the rest of\n // the codebase already uses to decide whether scene/snapshot states\n // exist, so the loader has to follow the same rule.\n for (const cd of cloudDevices) {\n const caps = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n const hasSceneCap =\n hasDynamicSceneCapability(caps, \"lightScene\") ||\n hasDynamicSceneCapability(caps, \"diyScene\") ||\n hasDynamicSceneCapability(caps, \"snapshot\");\n const isLight = cd.type === \"devices.types.light\" || hasSceneCap;\n if (isLight) {\n const device = this.devices.get(this.deviceKey(cd.sku, cd.device));\n if (device) {\n if (await this.loadDeviceScenes(device, cd)) {\n changed = true;\n }\n if (await this.loadDeviceLibraries(device, cd.sku)) {\n changed = true;\n }\n // Mark scenes as checked regardless of result \u2014 empty is legitimate,\n // and we've now confirmed that via Cloud. Prevents refetch loop.\n device.scenesChecked = true;\n }\n }\n }\n\n // Step 3: Prune stale cache entries (only after successful Cloud-load\n // with a plausible response \u2014 never prune on Cloud failure or empty list)\n if (this.skuCache && cloudDevices.length > 0) {\n this.skuCache.pruneStale(14);\n }\n\n // Step 4: Save to cache and finalize\n this.saveDevicesToCache();\n\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n this.lastErrorCategory = null;\n return { ok: true };\n } catch (err) {\n this.logDedup(\"Cloud device list failed\", err);\n\n // Govee 429: respect Retry-After header (default 60s if missing)\n if (err instanceof HttpError && err.statusCode === 429) {\n const retryAfterRaw = err.headers[\"retry-after\"];\n const retryAfterSec =\n typeof retryAfterRaw === \"string\" && /^\\d+$/.test(retryAfterRaw) ? parseInt(retryAfterRaw, 10) : 60;\n return {\n ok: false,\n reason: \"rate-limited\",\n retryAfterMs: retryAfterSec * 1000,\n };\n }\n\n // Auth failure: API-Key falsch oder widerrufen \u2014 KEIN Retry\n const category = classifyError(err);\n if (category === \"AUTH\") {\n return {\n ok: false,\n reason: \"auth-failed\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n\n // Netzwerk/Timeout/Unknown: transient, einfach sp\u00E4ter\n return { ok: false, reason: \"transient\" };\n }\n }\n\n /**\n * Re-fetch scenes, snapshots AND libraries for all known light devices\n * without re-running the full Cloud bootstrap. Used by the\n * `info.refresh_cloud_data` button: \"new snapshot/scene was saved in\n * the Govee Home app, show it here\".\n *\n * v1.10.1 had skipped libraries to keep the per-click call count low \u2014\n * but for users on accounts where a library actually grew (new\n * scene-set rolled out by Govee, new sceneCode minted) the only way\n * back to fresh data was a full adapter restart. v2.1.0 reinstates the\n * library refresh on the same button and forces past the\n * `length === 0` guards inside `loadDeviceLibraries`.\n *\n * @returns true when any device's scene/snapshot/library data changed\n */\n async refreshSceneData(): Promise<boolean> {\n if (!this.cloudClient) {\n return false;\n }\n let anyChanged = false;\n const lights = Array.from(this.devices.values()).filter(d => d.type === \"devices.types.light\");\n for (const dev of lights) {\n this.diagnostics.addLog(dev.deviceId, \"info\", `User-triggered refresh-cloud-data for ${dev.sku}`);\n }\n for (const device of lights) {\n const cd: CloudDevice = {\n sku: device.sku,\n device: device.deviceId,\n deviceName: device.name,\n type: device.type,\n capabilities: Array.isArray(device.capabilities) ? device.capabilities : [],\n };\n if (await this.loadDeviceScenes(device, cd)) {\n anyChanged = true;\n }\n if (await this.loadDeviceLibraries(device, cd.sku, /* force */ true)) {\n anyChanged = true;\n }\n }\n if (anyChanged) {\n this.saveDevicesToCache();\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n this.onDeviceListChanged?.(this.getDevices());\n }\n return anyChanged;\n }\n\n /**\n * Merge Cloud device list into local device map.\n * Updates existing devices, adds new ones.\n *\n * @param cloudDevices Devices from Cloud API\n * @returns true if any new devices were added\n */\n private mergeCloudDevices(cloudDevices: CloudDevice[]): boolean {\n let changed = false;\n if (!Array.isArray(cloudDevices)) {\n return false;\n }\n for (const cd of cloudDevices) {\n // Defensive guard against malformed cloud entries\n if (!cd || typeof cd.sku !== \"string\" || typeof cd.device !== \"string\") {\n continue;\n }\n const existing = this.devices.get(this.deviceKey(cd.sku, cd.device));\n if (existing) {\n existing.name = cd.deviceName || existing.name;\n existing.capabilities = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n existing.type = cd.type;\n existing.channels.cloud = true;\n } else {\n const device = this.cloudDeviceToGoveeDevice(cd);\n this.devices.set(this.deviceKey(cd.sku, cd.device), device);\n changed = true;\n this.log.debug(`Cloud: New device ${cd.deviceName} (${cd.sku})`);\n this.maybeNudgeSeedSku(cd.sku, cd.deviceName);\n }\n\n const quirks = getDeviceQuirks(cd.sku);\n if (quirks?.brokenPlatformApi) {\n this.log.debug(`${cd.sku} has known broken platform API metadata \u2014 capabilities may be incomplete`);\n }\n }\n return changed;\n }\n\n /**\n * Load scenes, DIY scenes, and snapshots for a device from Cloud API.\n *\n * @param device Target device to populate\n * @param cd Cloud device data with capabilities\n * @returns true if any scene data changed\n */\n private async loadDeviceScenes(device: GoveeDevice, cd: CloudDevice): Promise<boolean> {\n this.diagnostics.addLog(cd.device, \"debug\", `loadDeviceScenes called for ${cd.sku}`);\n // Scenes from dedicated scenes endpoint (rate-limited).\n // Guards are per-list, not combined: Govee's /device/scenes sometimes\n // returns 149 lightScenes + 0 snapshots (or vice versa) on back-to-back\n // calls even though the snapshot exists. A combined guard (if any list\n // non-empty, overwrite all) would wipe the other lists on that call and\n // break the dropdown until the next lucky round-trip. One guard per\n // list keeps the last-known-good data in place.\n const loadScenes = async (): Promise<void> => {\n try {\n const { lightScenes, diyScenes, snapshots } = await this.cloudClient!.getScenes(cd.sku, cd.device);\n // Cloud-client already captured the raw response on success \u2014 but\n // record the catch path here so the diag JSON shows \"/device/scenes\n // attempted, returned 403\" instead of an empty slot.\n if (lightScenes.length > 0) {\n device.scenes = lightScenes;\n }\n if (diyScenes.length > 0) {\n device.diyScenes = diyScenes;\n }\n if (snapshots.length > 0) {\n device.snapshots = snapshots;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(cd.device, \"/router/api/v1/device/scenes\", e, this.extractStatus(e));\n this.log.debug(`Could not load scenes for ${cd.sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadScenes, 2);\n\n // DIY scenes from dedicated endpoint\n if (device.diyScenes.length === 0) {\n const loadDiy = async (): Promise<void> => {\n try {\n const diy = await this.cloudClient!.getDiyScenes(cd.sku, cd.device);\n if (diy.length > 0) {\n device.diyScenes = diy;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(cd.device, \"/router/api/v1/device/diy-scenes\", e, this.extractStatus(e));\n this.log.debug(`Could not load DIY scenes for ${cd.sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadDiy, 2);\n }\n\n // Snapshots from device capabilities (fallback)\n if (device.snapshots.length === 0) {\n const caps = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n const snapCap = caps.find(\n c =>\n c &&\n c.type === \"devices.capabilities.dynamic_scene\" &&\n c.instance === \"snapshot\" &&\n Array.isArray(c.parameters?.options),\n );\n if (snapCap?.parameters?.options) {\n device.snapshots = snapCap.parameters.options\n .filter(o => o && typeof o.name === \"string\" && o.value !== undefined && o.value !== null)\n .map(o => ({\n name: o.name,\n value: typeof o.value === \"number\" ? o.value : (o.value as Record<string, unknown>),\n }));\n this.log.debug(`Snapshots from capabilities for ${cd.sku}: ${device.snapshots.length}`);\n }\n }\n\n // \"Changed\" = we ended up with any scene/snapshot data. Inner tracking\n // was redundant with this single-source check.\n return device.scenes.length > 0 || device.diyScenes.length > 0 || device.snapshots.length > 0;\n }\n\n /**\n * Load scene/music/DIY libraries and SKU features from undocumented API.\n *\n * Each fetch runs through the rate-limiter so a fresh install with 10\n * devices doesn't slam app2.govee.com with 40 back-to-back requests \u2014\n * those endpoints are undocumented and aggressive callers can get the\n * account temporarily locked.\n *\n * @param device Target device to populate\n * @param sku Product model\n * @param force When true, refetch every endpoint regardless of cache \u2014\n * used by the user-triggered refresh button so a stale library\n * actually gets replaced\n * @returns true if any library data changed\n */\n private async loadDeviceLibraries(device: GoveeDevice, sku: string, force = false): Promise<boolean> {\n if (!this.apiClient) {\n return false;\n }\n\n this.diagnostics.addLog(device.deviceId, \"debug\", `loadDeviceLibraries called for ${sku} (force=${force})`);\n let changed = false;\n\n // Run each fetch inside a rate-limited slot. Priority 2 = below\n // control commands and scene/snapshot loads; library data is cache-only\n // and can wait for a quieter moment.\n const runLimited = async (fn: () => Promise<void>): Promise<void> => {\n await this.commandRouter.executeRateLimited(fn, 2);\n };\n\n if (force || device.sceneLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/light-effect-libraries?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchSceneLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(s => s.name) });\n if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n this.log.debug(`Scene library for ${sku}: ${lib.length} scenes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load scene library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || device.musicLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/light-effect-libraries-music?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchMusicLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(m => m.name) });\n if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n this.log.debug(`Music library for ${sku}: ${lib.length} modes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load music library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || device.diyLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/diy-effect-libraries?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchDiyLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(d => d.name) });\n if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n this.log.debug(`DIY library for ${sku}: ${lib.length} effects`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load DIY library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || !device.skuFeatures) {\n await runLimited(async () => {\n const ep = `/sku-features?sku=${sku}`;\n try {\n const features = await this.apiClient!.fetchSkuFeatures(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, features);\n if (features) {\n device.skuFeatures = features;\n changed = true;\n this.log.debug(`SKU features for ${sku}: ${JSON.stringify(features).slice(0, 200)}`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load SKU features for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n // Load snapshot BLE commands for local activation\n if (!device.snapshotBleCmds && device.snapshots.length > 0) {\n await runLimited(async () => {\n try {\n const snaps = await this.apiClient!.fetchSnapshots(sku, device.deviceId);\n if (snaps.length > 0) {\n device.snapshotBleCmds = device.snapshots.map(ds => {\n const match = snaps.find(s => s.name === ds.name);\n return match?.bleCmds ?? [];\n });\n changed = true;\n this.log.debug(`Snapshot BLE for ${sku}: ${snaps.length} snapshots with local data`);\n }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n return changed;\n }\n\n /**\n * Load group membership from undocumented API and attach to BaseGroup devices.\n * Resolves member device references against the current device map.\n *\n * @returns true if any group memberships were resolved\n */\n async loadGroupMembers(): Promise<boolean> {\n if (!this.apiClient) {\n return false;\n }\n if (!this.apiClient.hasBearerToken()) {\n this.log.debug(\"Group membership requires Email+Password \u2014 skipping member resolution\");\n return false;\n }\n\n try {\n const apiGroups = await this.apiClient.fetchGroupMembers();\n if (apiGroups.length === 0) {\n this.log.debug(\"No group membership data from API\");\n return false;\n }\n\n let changed = false;\n for (const group of this.devices.values()) {\n if (group.sku !== \"BaseGroup\") {\n continue;\n }\n // Match by groupId: BaseGroup deviceId is the numeric group ID as string\n const apiGroup = apiGroups.find(g => String(g.groupId) === group.deviceId);\n if (!apiGroup) {\n continue;\n }\n\n // Resolve member devices against our device map\n const members: { sku: string; deviceId: string }[] = [];\n for (const m of apiGroup.devices) {\n const resolved = this.findDeviceBySkuAndId(m.sku, m.deviceId);\n if (resolved) {\n members.push({ sku: resolved.sku, deviceId: resolved.deviceId });\n } else {\n this.log.debug(`Group \"${group.name}\": member ${m.sku}/${m.deviceId} not in device map`);\n }\n }\n\n group.groupMembers = members;\n if (members.length > 0) {\n changed = true;\n }\n this.log.debug(`Group \"${group.name}\": ${members.length}/${apiGroup.devices.length} members resolved`);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n return changed;\n } catch (e) {\n this.log.debug(`Could not load group members: ${e instanceof Error ? e.message : String(e)}`);\n return false;\n }\n }\n\n /** Save all devices to SKU cache, skipping only those never confirmed via Cloud yet. */\n public saveDevicesToCache(): void {\n if (!this.skuCache) {\n return;\n }\n\n let cachedCount = 0;\n let skippedCount = 0;\n for (const device of this.devices.values()) {\n const isLight = device.type === \"devices.types.light\";\n // Skip only if we never asked Cloud yet \u2014 empty scenes are legitimate\n // once confirmed via scenesChecked=true.\n if (isLight && !device.scenesChecked) {\n skippedCount++;\n this.log.debug(`Not caching ${device.name} (${device.sku}) \u2014 scenes not yet checked`);\n } else {\n this.skuCache.save(this.goveeDeviceToCached(device));\n cachedCount++;\n }\n }\n // Routine persistence \u2014 debug level only. Users don't need a play-by-play\n // for every cache write. Significant events (scenes fetched, MQTT bumps)\n // log themselves elsewhere.\n if (skippedCount > 0) {\n this.log.debug(`Cached ${cachedCount} device(s), skipped ${skippedCount} not yet checked`);\n } else {\n this.log.debug(`Cached ${cachedCount} device(s) \u2014 next start uses cache`);\n }\n }\n\n /**\n * Handle LAN device discovery \u2014 match against known devices or create new.\n *\n * @param lanDevice Discovered LAN device\n */\n handleLanDiscovery(lanDevice: LanDevice): void {\n // Try to find by device ID (colon-separated in Cloud, varies in LAN)\n let matched: GoveeDevice | undefined;\n for (const dev of this.devices.values()) {\n if (normalizeDeviceId(dev.deviceId) === normalizeDeviceId(lanDevice.device)) {\n matched = dev;\n break;\n }\n // Also match by SKU if device IDs don't match format\n if (dev.sku === lanDevice.sku && !dev.lanIp) {\n matched = dev;\n break;\n }\n }\n\n if (matched) {\n const ipChanged = matched.lanIp !== lanDevice.ip;\n matched.lanIp = lanDevice.ip;\n matched.channels.lan = true;\n matched.lastSeenOnNetwork = Date.now();\n if (ipChanged) {\n this.log.debug(`LAN: ${matched.name} (${matched.sku}) at ${lanDevice.ip}`);\n this.onLanIpChanged?.(matched, lanDevice.ip);\n }\n } else {\n // LAN-only device (no Cloud data yet)\n // Include short device ID suffix for uniqueness (multiple devices can share same SKU)\n const shortId = normalizeDeviceId(lanDevice.device).slice(-4);\n const device: GoveeDevice = {\n sku: lanDevice.sku,\n deviceId: lanDevice.device,\n name: `${lanDevice.sku}_${shortId}`,\n type: \"devices.types.light\",\n lanIp: lanDevice.ip,\n capabilities: [],\n scenes: [],\n diyScenes: [],\n snapshots: [],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n lastSeenOnNetwork: Date.now(),\n state: { online: true },\n channels: { lan: true, mqtt: false, cloud: false },\n };\n this.devices.set(this.deviceKey(lanDevice.sku, lanDevice.device), device);\n this.diagnostics.addLog(lanDevice.device, \"info\", `LAN-discovered at ${lanDevice.ip}`);\n this.log.debug(`LAN: New device ${lanDevice.sku} at ${lanDevice.ip}`);\n this.maybeNudgeSeedSku(lanDevice.sku, device.name);\n this.onDeviceListChanged?.(this.getDevices());\n }\n }\n\n /**\n * Log the device's trust tier \u2014 once per SKU per adapter lifetime, so\n * device reconnects don't spam the log. Behaviour by tier:\n * - verified / reported: silent (the catalog backs the device, no\n * action needed). The tier is still surfaced via the\n * `diag.tier` state for any user who wants to check.\n * - seed (toggle off): warn \u2014 points the user at the experimental\n * toggle that gates the per-SKU corrections we'd otherwise apply.\n * - seed (toggle on): info \u2014 confirms quirks are active.\n * - unknown: warn \u2014 asks for a diagnostics export so we can add the\n * SKU to the catalogue.\n *\n * @param sku Govee SKU\n * @param displayName Device name as shown in Govee Home\n */\n private maybeNudgeSeedSku(sku: string, displayName: string | undefined): void {\n const upper = (typeof sku === \"string\" ? sku : \"\").toUpperCase();\n if (!upper || this.nudgedSeedSkus.has(upper)) {\n return;\n }\n this.nudgedSeedSkus.add(upper);\n const tier = getDeviceTier(upper);\n const label = displayName ? `${displayName} (${upper})` : upper;\n switch (tier) {\n case \"verified\":\n case \"reported\":\n return;\n case \"seed\":\n if (isSeedAndDormant(upper)) {\n this.log.warn(\n `Device ${label} is in beta and needs the \"Experimentelle Ger\u00E4te-Unterst\u00FCtzung aktivieren\" toggle in adapter settings to apply known per-SKU corrections.`,\n );\n } else {\n this.log.info(`Device ${label} is in beta \u2014 experimental quirks are active.`);\n }\n return;\n case \"unknown\":\n this.log.warn(\n `Device ${label} is not in the supported device list. Please trigger diag.export and post the resulting JSON in a GitHub issue so the SKU can be added.`,\n );\n return;\n }\n }\n\n /**\n * Handle MQTT status update \u2014 update device state.\n *\n * @param update MQTT status message\n */\n handleMqttStatus(update: MqttStatusUpdate): void {\n const device = this.findDeviceBySkuAndId(update.sku, update.device);\n if (!device) {\n this.log.debug(`MQTT: Unknown device ${update.sku} ${update.device}`);\n return;\n }\n\n device.channels.mqtt = true;\n device.lastSeenOnNetwork = Date.now();\n const state: Partial<DeviceState> = { online: true };\n\n if (update.state) {\n if (update.state.onOff !== undefined) {\n state.power = update.state.onOff === 1;\n }\n if (update.state.brightness !== undefined) {\n state.brightness = update.state.brightness;\n }\n if (update.state.color) {\n const { r, g, b } = update.state.color;\n state.colorRgb = rgbToHex(r, g, b);\n }\n if (update.state.colorTemInKelvin) {\n state.colorTemperature = update.state.colorTemInKelvin;\n }\n }\n\n // Merge into device state\n Object.assign(device.state, state);\n this.onDeviceUpdate?.(device, state);\n\n // Parse per-segment data from BLE notification packets (AA A5).\n // MQTT is authoritative for segment count \u2014 the device tells us what it\n // actually has. Cloud only gives an initial best-guess from capabilities.\n if (update.op?.command) {\n const segData = parseMqttSegmentData(update.op.command);\n\n if (segData.length > 0) {\n const maxSeen = Math.max(...segData.map(s => s.index)) + 1;\n const current = device.segmentCount ?? 0;\n if (maxSeen > current) {\n this.log.info(\n `${device.name}: detected ${maxSeen} segments via MQTT (was ${current}) \u2014 rebuilding state tree`,\n );\n device.segmentCount = maxSeen;\n // Persist now so a restart starts from the real value instead of\n // falling back to Cloud capabilities and deleting the extra slots.\n if (this.skuCache) {\n this.skuCache.save(this.goveeDeviceToCached(device));\n }\n // Skip per-segment sync for this push \u2014 the new datapoints don't\n // exist yet. The next AA A5 push hits the fully-built tree.\n this.onSegmentCountGrown?.(device);\n return;\n }\n }\n\n // Filter by manual-segments override if active \u2014 ignore indices the\n // user has declared as \"not physically present\" (cut strip).\n const filtered =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? segData.filter(s => device.manualSegments!.includes(s.index))\n : segData;\n if (filtered.length > 0) {\n this.onMqttSegmentUpdate?.(device, filtered);\n }\n }\n }\n\n /**\n * Handle LAN status response.\n *\n * @param ip Source IP address\n * @param status LAN status data\n * @param status.onOff Power state (1=on, 0=off)\n * @param status.brightness Brightness 0-100\n * @param status.color RGB color values\n * @param status.color.r Red channel 0-255\n * @param status.color.g Green channel 0-255\n * @param status.color.b Blue channel 0-255\n * @param status.colorTemInKelvin Color temperature in Kelvin\n */\n handleLanStatus(\n ip: string,\n status: {\n onOff: number;\n brightness: number;\n color: { r: number; g: number; b: number };\n colorTemInKelvin: number;\n },\n ): void {\n // Find device by LAN IP\n let device: GoveeDevice | undefined;\n for (const dev of this.devices.values()) {\n if (dev.lanIp === ip) {\n device = dev;\n break;\n }\n }\n if (!device) {\n return;\n }\n\n device.lastSeenOnNetwork = Date.now();\n const { r, g, b } = status.color;\n const state: Partial<DeviceState> = {\n online: true,\n power: status.onOff === 1,\n brightness: status.brightness,\n colorRgb: rgbToHex(r, g, b),\n colorTemperature: status.colorTemInKelvin || undefined,\n };\n\n Object.assign(device.state, state);\n this.onDeviceUpdate?.(device, state);\n }\n\n /**\n * Set the callback for batch segment state sync.\n * Forwards to the internal CommandRouter.\n *\n * @param callback Called when a segment batch command updates segment states\n */\n set onSegmentBatchUpdate(\n callback:\n | ((device: GoveeDevice, batch: { segments: number[]; color?: number; brightness?: number }) => void)\n | undefined,\n ) {\n this.commandRouter.onSegmentBatchUpdate = callback;\n }\n\n /**\n * Send a command to a device \u2014 routes through LAN \u2192 Cloud.\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 return this.commandRouter.sendCommand(device, command, value);\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 return this.commandRouter.sendCapabilityCommand(device, capabilityType, capabilityInstance, value);\n }\n\n /** Callback when device LAN IP changes */\n onLanIpChanged?: (device: GoveeDevice, ip: string) => void;\n\n /** Callback when MQTT delivers per-segment state data (AA A5 BLE packets) */\n onMqttSegmentUpdate?: (device: GoveeDevice, segments: MqttSegmentData[]) => void;\n\n /**\n * Callback when the device's physical segment count turns out to be\n * larger than the Cloud-reported value (observed via MQTT AA A5 stream).\n * The adapter rebuilds the state tree in response so the extra indices\n * appear as datapoints.\n */\n onSegmentCountGrown?: (device: GoveeDevice) => void;\n\n /**\n * Convert Cloud device to internal device model\n *\n * @param cd Cloud API device data\n */\n private cloudDeviceToGoveeDevice(cd: CloudDevice): GoveeDevice {\n return {\n sku: cd.sku,\n deviceId: cd.device,\n name: cd.deviceName || cd.sku,\n type: cd.type || \"unknown\",\n capabilities: Array.isArray(cd.capabilities) ? cd.capabilities : [],\n scenes: [],\n diyScenes: [],\n snapshots: [],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n state: { online: true },\n channels: { lan: false, mqtt: false, cloud: true },\n };\n }\n\n /**\n * Find device by SKU and device ID (handles format differences)\n *\n * @param sku Product model\n * @param deviceId Device identifier\n */\n private findDeviceBySkuAndId(sku: string, deviceId: string): GoveeDevice | undefined {\n // Direct key lookup\n const direct = this.devices.get(this.deviceKey(sku, deviceId));\n if (direct) {\n return direct;\n }\n\n // Normalized search\n const normalizedId = normalizeDeviceId(deviceId);\n for (const dev of this.devices.values()) {\n if (dev.sku === sku && normalizeDeviceId(dev.deviceId) === normalizedId) {\n return dev;\n }\n }\n return undefined;\n }\n\n /**\n * Generate unique key for a device\n *\n * @param sku Product model\n * @param deviceId Device identifier\n */\n private deviceKey(sku: string, deviceId: string): string {\n return `${sku}_${normalizeDeviceId(deviceId)}`;\n }\n\n /**\n * Log error with dedup \u2014 only warn on category change, debug on repeat.\n *\n * @param context Error context description\n * @param err Error to log\n */\n private logDedup(context: string, err: unknown): void {\n const category = classifyError(err);\n const msg = `${context}: ${err instanceof Error ? err.message : String(err)}`;\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(`${msg} (repeated)`);\n }\n }\n\n /**\n * Fill device.scenes from sceneLibrary when Cloud scenes are missing.\n * ptReal activation matches by name, so sceneLibrary names are sufficient.\n *\n * @param device Device to populate scenes for\n */\n private populateScenesFromLibrary(device: GoveeDevice): void {\n if (device.scenes.length === 0 && device.sceneLibrary.length > 0) {\n device.scenes = device.sceneLibrary.map(entry => ({\n name: entry.name,\n value: {}, // ptReal uses sceneLibrary directly, Cloud payload not needed\n }));\n this.log.debug(`${device.sku}: ${device.scenes.length} scenes from library (Cloud scenes missing)`);\n }\n }\n\n /**\n * Convert cached data to a GoveeDevice (runtime fields set to defaults)\n *\n * @param cached Cached device data\n */\n private cachedToGoveeDevice(cached: CachedDeviceData): GoveeDevice {\n return {\n sku: cached.sku,\n deviceId: cached.deviceId,\n name: cached.name,\n type: cached.type,\n capabilities: cached.capabilities,\n scenes: cached.scenes,\n diyScenes: cached.diyScenes,\n snapshots: cached.snapshots,\n sceneLibrary: cached.sceneLibrary,\n musicLibrary: cached.musicLibrary,\n diyLibrary: cached.diyLibrary,\n skuFeatures: cached.skuFeatures,\n snapshotBleCmds: cached.snapshotBleCmds,\n scenesChecked: cached.scenesChecked,\n lastSeenOnNetwork: cached.lastSeenOnNetwork,\n // Restore learned count so it wins over Cloud capability on next start.\n segmentCount: cached.segmentCount,\n manualMode: cached.manualMode,\n manualSegments: cached.manualSegments,\n sceneSpeed: cached.sceneSpeed,\n state: { online: false },\n channels: { lan: false, mqtt: false, cloud: false },\n };\n }\n\n /**\n * Persist a device's current runtime state to the SKU cache.\n * Safe no-op when no cache is configured.\n *\n * @param device Target device\n */\n public persistDeviceToCache(device: GoveeDevice): void {\n if (!this.skuCache) {\n return;\n }\n this.skuCache.save(this.goveeDeviceToCached(device));\n }\n\n /**\n * Extract cacheable data from a GoveeDevice.\n *\n * @param device Runtime device\n */\n private goveeDeviceToCached(device: GoveeDevice): CachedDeviceData {\n return {\n sku: device.sku,\n deviceId: device.deviceId,\n name: device.name,\n type: device.type,\n capabilities: device.capabilities,\n scenes: device.scenes,\n diyScenes: device.diyScenes,\n snapshots: device.snapshots,\n sceneLibrary: device.sceneLibrary,\n musicLibrary: device.musicLibrary,\n diyLibrary: device.diyLibrary,\n skuFeatures: device.skuFeatures,\n snapshotBleCmds: device.snapshotBleCmds,\n scenesChecked: device.scenesChecked,\n lastSeenOnNetwork: device.lastSeenOnNetwork,\n segmentCount:\n typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : undefined,\n manualMode: device.manualMode ? true : undefined,\n manualSegments:\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.slice()\n : undefined,\n sceneSpeed: typeof device.sceneSpeed === \"number\" && device.sceneSpeed > 0 ? device.sceneSpeed : undefined,\n cachedAt: Date.now(),\n };\n }\n\n /**\n * Generate diagnostics data for a device \u2014 structured JSON for GitHub\n * issue submission. Delegates to the DiagnosticsCollector so the JSON\n * also includes ring-buffer context (recent logs, MQTT packets, last\n * API responses).\n *\n * @param device Target device\n * @param adapterVersion Adapter version string\n */\n generateDiagnostics(device: GoveeDevice, adapterVersion: string): Record<string, unknown> {\n return this.diagnostics.generate(device, adapterVersion);\n }\n\n /**\n * Poll the undocumented app-API for sensor-like devices (H5179 et al.)\n * where OpenAPI v2 `/device/state` returns empty. Each entry is converted\n * to synthetic capabilities and routed back through the same callback as\n * regular Cloud state, so the existing setState pipeline picks it up\n * without a special-case branch.\n *\n * Bearer token comes from the MQTT login flow \u2014 without MQTT credentials\n * (Email + Password) this is a no-op.\n *\n * @returns Number of devices that received an update\n */\n async pollAppApi(): Promise<number> {\n if (!this.apiClient || !this.apiClient.hasBearerToken()) {\n return 0;\n }\n // Skip the entire round-trip when no device in the registry would\n // actually consume App-API readings. The App API is only used for\n // sensor and appliance state (thermometers, heaters, kettles, \u2026);\n // a Lights-only setup would otherwise burn one Govee call every 2\n // minutes for nothing.\n if (!this.hasDeviceNeedingAppApi()) {\n return 0;\n }\n let entries: AppDeviceEntry[];\n try {\n entries = await this.apiClient.fetchDeviceList();\n } catch (err) {\n const category = classifyError(err);\n const msg = `App API fetch failed: ${err instanceof Error ? err.message : String(err)}`;\n if (category !== this.lastAppApiErrorCategory) {\n this.lastAppApiErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n return 0;\n }\n // Reset on success so the next failure warns again.\n this.lastAppApiErrorCategory = null;\n let updated = 0;\n for (const entry of entries) {\n const device = this.devices.get(this.deviceKey(entry.sku, entry.device));\n if (!device) {\n continue;\n }\n const caps = buildCapabilitiesFromAppEntry(entry);\n if (caps.length === 0) {\n continue;\n }\n // Route synthetic capabilities through the existing\n // onCloudCapabilities callback so main.ts's normal setState\n // pipeline (mapCloudStateValue + setStateAsync) handles them.\n this.onCloudCapabilities?.(device, caps);\n // mapSingleCapability returns null for the synthetic `online`\n // cap (online is a device-level property, not a regular state),\n // so onCloudCapabilities never reaches info.online via the\n // capability pipeline. Pluck it out and apply it directly \u2014\n // otherwise sensor SKUs like H5179 stay at info.online=false\n // forever even while their readings keep updating.\n this.applyOnlineCap(device, caps);\n this.diagnostics.setApiResponse(device.deviceId, \"/device/rest/devices/v1/list\", entry);\n updated++;\n }\n return updated;\n }\n\n /**\n * Pull the `devices.capabilities.online` entry (if any) out of a\n * synthetic capability list and apply it directly to\n * `device.state.online` plus `lastSeenOnNetwork`. Surfaces via\n * onDeviceUpdate so the adapter's `info.online` state matches the\n * App-API / OpenAPI-MQTT signal. If no online cap is in the list but\n * the list is non-empty (i.e. fresh data arrived), the device is\n * considered online \u2014 same convention as the LAN/MQTT paths.\n *\n * @param device Target device\n * @param caps Capability list from the source pipeline\n */\n private applyOnlineCap(device: GoveeDevice, caps: CloudStateCapability[]): void {\n let online: boolean | undefined;\n for (const c of caps) {\n if (\n c &&\n typeof c.type === \"string\" &&\n (c.type === \"devices.capabilities.online\" || c.type === \"online\") &&\n c.state &&\n typeof c.state.value === \"boolean\"\n ) {\n online = c.state.value;\n break;\n }\n }\n // Fresh data with no online flag \u2192 assume online (LAN/MQTT use the\n // same \"we just heard from the device\" convention).\n if (online === undefined && caps.length > 0) {\n online = true;\n }\n if (online === undefined) {\n return;\n }\n if (device.state.online === online && online === true) {\n // Already online + still online \u2014 only refresh the lastSeen ts\n // and skip the onDeviceUpdate noise.\n device.lastSeenOnNetwork = Date.now();\n return;\n }\n device.state.online = online;\n if (online) {\n device.lastSeenOnNetwork = Date.now();\n }\n this.onDeviceUpdate?.(device, { online });\n }\n\n /**\n * Hook callback for sources that emit `CloudStateCapability[]` updates\n * outside the normal Cloud-poll path (App-API, OpenAPI-MQTT). Caller is\n * responsible for wiring it to the adapter-side state-write path.\n *\n * @param cb Callback receiving (device, caps)\n */\n setOnCloudCapabilities(cb: ((device: GoveeDevice, caps: CloudStateCapability[]) => void) | null): void {\n this.onCloudCapabilities = cb;\n }\n\n /**\n * Whether at least one device in the registry would consume App-API\n * readings (sensors, appliances). Used to skip the App-API poll on\n * Lights-only installations.\n */\n private hasDeviceNeedingAppApi(): boolean {\n for (const dev of this.devices.values()) {\n if (dev.type !== \"devices.types.light\" && dev.sku !== \"BaseGroup\") {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Process a parsed OpenAPI-MQTT event by forwarding its capabilities\n * through the same hook used by App-API polls. Called from the\n * adapter-side OpenAPI-MQTT message handler.\n *\n * @param event Parsed event from the OpenAPI-MQTT broker\n * @param event.sku Govee SKU (e.g. \"H5179\")\n * @param event.device MAC-style device identifier\n * @param event.capabilities Capability list synthesised from the broker payload\n */\n handleOpenApiEvent(event: { sku: string; device: string; capabilities: CloudStateCapability[] }): void {\n if (!event || typeof event.sku !== \"string\" || typeof event.device !== \"string\") {\n return;\n }\n if (!Array.isArray(event.capabilities) || event.capabilities.length === 0) {\n return;\n }\n const device = this.devices.get(this.deviceKey(event.sku, event.device));\n if (!device) {\n return;\n }\n this.onCloudCapabilities?.(device, event.capabilities);\n // Same online-cap unwrap as the App-API path. OpenAPI-MQTT events\n // are the only signal we get for appliance state (heater on/off,\n // ice-bucket-full, \u2026) \u2014 without this, info.online for those SKUs\n // never flips to true even while events stream in.\n this.applyOnlineCap(device, event.capabilities);\n }\n}\n\n/**\n * Convert an app-API device entry into a list of synthetic Cloud-state\n * capabilities the existing `mapCloudStateValue` pipeline can consume.\n *\n * Govee stores temperature and humidity as integer hundredths of a unit\n * (`tem: 2370` \u2192 23.70 \u00B0C, `hum: 4290` \u2192 42.90 % RH). Battery may live\n * either at the lastData level or in deviceSettings \u2014 lastData wins\n * because it's the more recent reading.\n *\n * @param entry One entry from `GoveeApiClient.fetchDeviceList()`\n */\nexport function buildCapabilitiesFromAppEntry(entry: AppDeviceEntry): CloudStateCapability[] {\n const caps: CloudStateCapability[] = [];\n const last = entry.lastData;\n if (!last) {\n return caps;\n }\n if (typeof last.online === \"boolean\") {\n caps.push({\n type: \"devices.capabilities.online\",\n instance: \"online\",\n state: { value: last.online },\n });\n }\n if (typeof last.tem === \"number\" && Number.isFinite(last.tem)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorTemperature\",\n state: { value: last.tem / 100 },\n });\n }\n if (typeof last.hum === \"number\" && Number.isFinite(last.hum)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorHumidity\",\n state: { value: last.hum / 100 },\n });\n }\n if (typeof last.battery === \"number\" && Number.isFinite(last.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: last.battery },\n });\n } else if (entry.settings && typeof entry.settings.battery === \"number\" && Number.isFinite(entry.settings.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: entry.settings.battery },\n });\n }\n return caps;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { hasDynamicSceneCapability } from \"./capability-mapper\";\nimport { CommandRouter } from \"./command-router\";\nimport { getDeviceQuirks, getDeviceTier, isSeedAndDormant } from \"./device-registry\";\nimport { DiagnosticsCollector } from \"./diagnostics\";\nimport type { AppDeviceEntry, GoveeApiClient } from \"./govee-api-client\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\nimport type { CachedDeviceData, SkuCache } from \"./sku-cache\";\nimport {\n classifyError,\n normalizeDeviceId,\n rgbToHex,\n type CloudDevice,\n type CloudLoadResult,\n type CloudStateCapability,\n type DeviceState,\n type ErrorCategory,\n type GoveeDevice,\n type LanDevice,\n type MqttStatusUpdate,\n type TimerAdapter,\n} from \"./types\";\nimport { HttpError } from \"./http-client\";\n\n/** Parsed per-segment data from MQTT BLE packets */\nexport interface MqttSegmentData {\n /** Segment index (0-based) */\n index: number;\n /** Per-segment brightness 0-100 */\n brightness: number;\n /** Red channel 0-255 */\n r: number;\n /** Green channel 0-255 */\n g: number;\n /** Blue channel 0-255 */\n b: number;\n}\n\n/**\n * Parse AA A5 BLE notification packets from MQTT op.command.\n * 5 packets \u00D7 4 segment slots = max 20 segments per push. The device sends\n * exactly as many packets as it has physical segments \u2014 so parsing out all\n * slots (and filtering empty-slot padding) gives us a reliable count of\n * what actually exists on the strip.\n *\n * Format per slot: [Brightness 0-100] [R] [G] [B].\n *\n * An \"empty\" slot (brightness = 0 AND r = g = b = 0) is treated as padding\n * in a partially-filled final packet, not as a real unlit segment \u2014 this\n * matters for devices that don't pad their last packet to 4 slots.\n *\n * @param commands Base64-encoded BLE packets from MQTT op.command\n */\nexport function parseMqttSegmentData(commands: string[]): MqttSegmentData[] {\n if (!Array.isArray(commands)) {\n return [];\n }\n\n const segments: MqttSegmentData[] = [];\n // Track the highest packetNum seen so we know where real data ends.\n let highestPacket = 0;\n\n for (const cmd of commands) {\n if (typeof cmd !== \"string\") {\n continue;\n }\n const bytes = Buffer.from(cmd, \"base64\");\n if (bytes.length < 20 || bytes[0] !== 0xaa || bytes[1] !== 0xa5) {\n continue;\n }\n\n const packetNum = bytes[2];\n if (packetNum < 1 || packetNum > 5) {\n continue;\n }\n if (packetNum > highestPacket) {\n highestPacket = packetNum;\n }\n\n const baseIndex = (packetNum - 1) * 4;\n for (let slot = 0; slot < 4; slot++) {\n const segIdx = baseIndex + slot;\n const offset = 3 + slot * 4;\n segments.push({\n index: segIdx,\n brightness: bytes[offset],\n r: bytes[offset + 1],\n g: bytes[offset + 2],\n b: bytes[offset + 3],\n });\n }\n }\n\n // Trim trailing padding slots from the highest packet: Govee pads short\n // packets with 0x00 bytes, so a run of all-zero slots at the end is not\n // real segment data but filler. Keep any zero-slots that are followed by\n // a real one \u2014 they're legitimately-unlit middle segments.\n while (segments.length > 0) {\n const tail = segments[segments.length - 1];\n if (tail.brightness === 0 && tail.r === 0 && tail.g === 0 && tail.b === 0) {\n segments.pop();\n } else {\n break;\n }\n }\n\n return segments;\n}\n\n/**\n * Effective physical segment indices for a device.\n * Uses `device.manualSegments` when `device.manualMode=true` (cut strip override),\n * falls back to `0..segmentCount-1` otherwise. Empty if device has no segments.\n *\n * @param device Target device\n */\nexport function getEffectiveSegmentIndices(device: GoveeDevice): number[] {\n if (device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0) {\n return device.manualSegments.slice();\n }\n const count = device.segmentCount ?? 0;\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => i);\n}\n\n/**\n * Resolve the authoritative segment count for a device.\n *\n * Priority:\n * 1. `device.segmentCount` if already set (from cache, MQTT discovery, or wizard)\n * 2. Minimum of positive `segment_color_setting` capability counts\n * 3. 0 if no capability advertises segments\n *\n * Why `min` over the capability caps: Govee reports `segmentedBrightness` and\n * `segmentedColorRgb` separately, and on at least one SKU (H70D1) those two\n * disagree \u2014 brightness says 10, colorRgb says 15, real device has 10.\n * Picking the smaller value is the safer starting point; MQTT discovery can\n * then grow it if the real device pushes more slots.\n *\n * @param device Target device\n */\nexport function resolveSegmentCount(device: GoveeDevice): number {\n if (typeof device.segmentCount === \"number\" && device.segmentCount > 0) {\n return device.segmentCount;\n }\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n let min = Number.POSITIVE_INFINITY;\n for (const c of caps) {\n if (!c || typeof c.type !== \"string\" || !c.type.includes(\"segment_color_setting\")) {\n continue;\n }\n const params = (c as { parameters?: { fields?: unknown[] } }).parameters;\n const fields = Array.isArray(params?.fields) ? params.fields : [];\n for (const f of fields) {\n if (!f || typeof f !== \"object\") {\n continue;\n }\n const fn = (f as { fieldName?: unknown }).fieldName;\n const er = (f as { elementRange?: { max?: unknown } }).elementRange;\n const rawMax = er && typeof er.max === \"number\" ? er.max : -1;\n if (fn === \"segment\" && rawMax >= 0) {\n const n = rawMax + 1;\n if (n > 0 && n < min) {\n min = n;\n }\n }\n }\n }\n return Number.isFinite(min) ? min : 0;\n}\n\n/** Protocol limit: Govee's segment bitmask is 7 bytes \u00D7 8 bits = 56 slots (0..55). */\nexport const SEGMENT_HARD_MAX = 55;\n\n/**\n * Device manager \u2014 maintains unified device list and routes commands\n * through the fastest available channel: LAN \u2192 Cloud.\n * MQTT is status-push only and never used for commands.\n */\nexport class DeviceManager {\n private readonly log: ioBroker.Logger;\n private readonly devices = new Map<string, GoveeDevice>();\n private readonly commandRouter: CommandRouter;\n private readonly diagnostics: DiagnosticsCollector;\n /** SKUs we already nudged about \u2014 log only once per adapter lifetime, per SKU. */\n private readonly nudgedSeedSkus = new Set<string>();\n private cloudClient: GoveeCloudClient | null = null;\n private apiClient: GoveeApiClient | null = null;\n private skuCache: SkuCache | null = null;\n private onDeviceUpdate: ((device: GoveeDevice, state: Partial<DeviceState>) => void) | null = null;\n private onDeviceListChanged: ((devices: GoveeDevice[]) => void) | null = null;\n private onCloudCapabilities: ((device: GoveeDevice, caps: CloudStateCapability[]) => void) | null = null;\n /** Per-source dedup so a Cloud NETWORK error doesn't shadow an App-API one. */\n private lastErrorCategory: ErrorCategory | null = null;\n private lastAppApiErrorCategory: ErrorCategory | null = null;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper (forwarded to CommandRouter for\n * onUnload-safe delays).\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.commandRouter = new CommandRouter(log, timers);\n this.diagnostics = new DiagnosticsCollector();\n }\n\n /**\n * Expose the diagnostics collector so adapter-side hooks (MQTT,\n * Cloud, log wrapper) can write into the per-device ring buffers.\n */\n getDiagnostics(): DiagnosticsCollector {\n return this.diagnostics;\n }\n\n /**\n * Pull the HTTP status code out of any error shape we know about\n * (HttpError, Govee API responses with `.statusCode` / `.status`).\n * Returns undefined for network errors / generic failures so the\n * diagnostics entry shows \"no status \u2014 likely network/timeout\".\n *\n * @param e Caught error value\n */\n private extractStatus(e: unknown): number | undefined {\n if (e instanceof HttpError) {\n return e.statusCode;\n }\n if (typeof e === \"object\" && e !== null) {\n const x = e as { statusCode?: unknown; status?: unknown };\n if (typeof x.statusCode === \"number\") {\n return x.statusCode;\n }\n if (typeof x.status === \"number\") {\n return x.status;\n }\n }\n return undefined;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.commandRouter.setLanClient(client);\n }\n\n /**\n * Register the undocumented API client for scene/music/DIY libraries\n *\n * @param client API client instance\n */\n setApiClient(client: GoveeApiClient): void {\n this.apiClient = 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 this.commandRouter.setCloudClient(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.commandRouter.setRateLimiter(limiter);\n }\n\n /**\n * Register the SKU cache for persistent device data\n *\n * @param cache SKU cache instance\n */\n setSkuCache(cache: SkuCache): void {\n this.skuCache = cache;\n }\n\n /**\n * Set callbacks for device state changes and list changes.\n *\n * @param onUpdate Called when a device state changes (from any channel)\n * @param onListChanged Called when the device list changes (new/removed devices)\n */\n setCallbacks(\n onUpdate: (device: GoveeDevice, state: Partial<DeviceState>) => void,\n onListChanged: (devices: GoveeDevice[]) => void,\n ): void {\n this.onDeviceUpdate = onUpdate;\n this.onDeviceListChanged = onListChanged;\n }\n\n /** Get all known devices */\n getDevices(): GoveeDevice[] {\n return Array.from(this.devices.values());\n }\n\n /**\n * Load devices from local SKU cache.\n * Returns true if any devices were loaded (= Cloud not needed).\n */\n loadFromCache(): boolean {\n if (!this.skuCache) {\n return false;\n }\n\n const cached = this.skuCache.loadAll();\n if (cached.length === 0) {\n return false;\n }\n\n let changed = false;\n for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n if (existing) {\n // Merge cached data into LAN-discovered device. Segment-specific\n // fields (segmentCount, manualMode, manualSegments) MUST be merged\n // too \u2014 LAN discovery runs before the cache load on every start, so\n // the existing-branch is the normal path. Missing these three meant\n // every restart threw away the wizard/MQTT-learned segment state and\n // fell back to Cloud's min-advertised count.\n existing.name = entry.name || existing.name;\n existing.type = entry.type || existing.type;\n existing.capabilities = entry.capabilities;\n existing.scenes = entry.scenes;\n existing.diyScenes = entry.diyScenes;\n existing.snapshots = entry.snapshots;\n existing.sceneLibrary = entry.sceneLibrary;\n existing.musicLibrary = entry.musicLibrary;\n existing.diyLibrary = entry.diyLibrary;\n existing.skuFeatures = entry.skuFeatures;\n existing.snapshotBleCmds = entry.snapshotBleCmds;\n existing.scenesChecked = entry.scenesChecked;\n existing.lastSeenOnNetwork = entry.lastSeenOnNetwork;\n existing.segmentCount = entry.segmentCount;\n existing.manualMode = entry.manualMode;\n existing.manualSegments = entry.manualSegments;\n existing.channels.cloud = entry.capabilities.length > 0;\n changed = true;\n } else {\n this.devices.set(key, this.cachedToGoveeDevice(entry));\n changed = true;\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // Always refetch cloud data on startup \u2014 scenesChecked is purely diagnostic\n // now, not a gate. Snapshots are user-content (created dynamically in the\n // Govee Home app) and would miss new entries if we relied solely on the\n // cache. The refetch costs one call per light device per startup, well\n // within rate limits. Users can also trigger a fresh fetch without\n // restart via `info.refresh_cloud_data`.\n const hasLight = Array.from(this.devices.values()).some(d => d.type === \"devices.types.light\");\n if (hasLight) {\n this.log.debug(\"Cache loaded \u2014 will refresh scenes/snapshots via Cloud\");\n return false;\n }\n\n // Fill scenes from sceneLibrary for devices where Cloud scenes are missing\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n return cached.length > 0;\n }\n\n /**\n * Load devices from Cloud API and save to cache.\n * Only called when cache is empty (first start) or manual refresh.\n */\n async loadFromCloud(): Promise<CloudLoadResult> {\n if (!this.cloudClient) {\n return { ok: false, reason: \"transient\" };\n }\n\n try {\n const rawCloudDevices = await this.cloudClient.getDevices();\n\n // Hard-filter: Govee's Device-List API returns historical/stale entries\n // (deleted devices that are no longer in the app). Filter out entries\n // without capabilities \u2014 those are almost certainly stale registrations.\n const cloudDevices = Array.isArray(rawCloudDevices)\n ? rawCloudDevices.filter(\n cd =>\n cd &&\n typeof cd.sku === \"string\" &&\n typeof cd.device === \"string\" &&\n Array.isArray(cd.capabilities) &&\n cd.capabilities.length > 0,\n )\n : [];\n\n if (Array.isArray(rawCloudDevices) && rawCloudDevices.length !== cloudDevices.length) {\n this.log.info(\n `Cloud: received ${rawCloudDevices.length} devices raw, ${cloudDevices.length} after filter (skipped stale entries without capabilities)`,\n );\n }\n\n // Step 1: Merge Cloud devices into local device map\n let changed = this.mergeCloudDevices(cloudDevices);\n\n // Step 2: Load scenes, snapshots, and libraries for any device that\n // exposes a `dynamic_scene` capability \u2014 independent of `cd.type`.\n // Govee occasionally returns devices with `type` missing or a value\n // we don't recognise; keying off the capability is what the rest of\n // the codebase already uses to decide whether scene/snapshot states\n // exist, so the loader has to follow the same rule.\n for (const cd of cloudDevices) {\n const caps = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n const hasSceneCap =\n hasDynamicSceneCapability(caps, \"lightScene\") ||\n hasDynamicSceneCapability(caps, \"diyScene\") ||\n hasDynamicSceneCapability(caps, \"snapshot\");\n const isLight = cd.type === \"devices.types.light\" || hasSceneCap;\n if (isLight) {\n const device = this.devices.get(this.deviceKey(cd.sku, cd.device));\n if (device) {\n if (await this.loadDeviceScenes(device, cd)) {\n changed = true;\n }\n if (await this.loadDeviceLibraries(device, cd.sku)) {\n changed = true;\n }\n // Mark scenes as checked regardless of result \u2014 empty is legitimate,\n // and we've now confirmed that via Cloud. Prevents refetch loop.\n device.scenesChecked = true;\n }\n }\n }\n\n // Step 3: Prune stale cache entries (only after successful Cloud-load\n // with a plausible response \u2014 never prune on Cloud failure or empty list)\n if (this.skuCache && cloudDevices.length > 0) {\n this.skuCache.pruneStale(14);\n }\n\n // Step 4: Save to cache and finalize\n this.saveDevicesToCache();\n\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n this.lastErrorCategory = null;\n return { ok: true };\n } catch (err) {\n this.logDedup(\"Cloud device list failed\", err);\n\n // Govee 429: respect Retry-After header (default 60s if missing)\n if (err instanceof HttpError && err.statusCode === 429) {\n const retryAfterRaw = err.headers[\"retry-after\"];\n const retryAfterSec =\n typeof retryAfterRaw === \"string\" && /^\\d+$/.test(retryAfterRaw) ? parseInt(retryAfterRaw, 10) : 60;\n return {\n ok: false,\n reason: \"rate-limited\",\n retryAfterMs: retryAfterSec * 1000,\n };\n }\n\n // Auth failure: API-Key falsch oder widerrufen \u2014 KEIN Retry\n const category = classifyError(err);\n if (category === \"AUTH\") {\n return {\n ok: false,\n reason: \"auth-failed\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n\n // Netzwerk/Timeout/Unknown: transient, einfach sp\u00E4ter\n return { ok: false, reason: \"transient\" };\n }\n }\n\n /**\n * Re-fetch scenes, snapshots AND libraries for all known light devices\n * without re-running the full Cloud bootstrap. Used by the\n * `info.refresh_cloud_data` button: \"new snapshot/scene was saved in\n * the Govee Home app, show it here\".\n *\n * v1.10.1 had skipped libraries to keep the per-click call count low \u2014\n * but for users on accounts where a library actually grew (new\n * scene-set rolled out by Govee, new sceneCode minted) the only way\n * back to fresh data was a full adapter restart. v2.1.0 reinstates the\n * library refresh on the same button and forces past the\n * `length === 0` guards inside `loadDeviceLibraries`.\n *\n * @returns true when any device's scene/snapshot/library data changed\n */\n async refreshSceneData(): Promise<boolean> {\n if (!this.cloudClient) {\n return false;\n }\n let anyChanged = false;\n const lights = Array.from(this.devices.values()).filter(d => d.type === \"devices.types.light\");\n for (const dev of lights) {\n this.diagnostics.addLog(dev.deviceId, \"info\", `User-triggered refresh-cloud-data for ${dev.sku}`);\n }\n for (const device of lights) {\n const cd: CloudDevice = {\n sku: device.sku,\n device: device.deviceId,\n deviceName: device.name,\n type: device.type,\n capabilities: Array.isArray(device.capabilities) ? device.capabilities : [],\n };\n if (await this.loadDeviceScenes(device, cd)) {\n anyChanged = true;\n }\n if (await this.loadDeviceLibraries(device, cd.sku, /* force */ true)) {\n anyChanged = true;\n }\n }\n if (anyChanged) {\n this.saveDevicesToCache();\n for (const device of this.devices.values()) {\n this.populateScenesFromLibrary(device);\n }\n this.onDeviceListChanged?.(this.getDevices());\n }\n return anyChanged;\n }\n\n /**\n * Merge Cloud device list into local device map.\n * Updates existing devices, adds new ones.\n *\n * @param cloudDevices Devices from Cloud API\n * @returns true if any new devices were added\n */\n private mergeCloudDevices(cloudDevices: CloudDevice[]): boolean {\n let changed = false;\n if (!Array.isArray(cloudDevices)) {\n return false;\n }\n for (const cd of cloudDevices) {\n // Defensive guard against malformed cloud entries\n if (!cd || typeof cd.sku !== \"string\" || typeof cd.device !== \"string\") {\n continue;\n }\n const existing = this.devices.get(this.deviceKey(cd.sku, cd.device));\n if (existing) {\n existing.name = cd.deviceName || existing.name;\n existing.capabilities = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n existing.type = cd.type;\n existing.channels.cloud = true;\n } else {\n const device = this.cloudDeviceToGoveeDevice(cd);\n this.devices.set(this.deviceKey(cd.sku, cd.device), device);\n changed = true;\n this.log.debug(`Cloud: New device ${cd.deviceName} (${cd.sku})`);\n this.maybeNudgeSeedSku(cd.sku, cd.deviceName);\n }\n\n const quirks = getDeviceQuirks(cd.sku);\n if (quirks?.brokenPlatformApi) {\n this.log.debug(`${cd.sku} has known broken platform API metadata \u2014 capabilities may be incomplete`);\n }\n }\n return changed;\n }\n\n /**\n * Load scenes, DIY scenes, and snapshots for a device from Cloud API.\n *\n * @param device Target device to populate\n * @param cd Cloud device data with capabilities\n * @returns true if any scene data changed\n */\n private async loadDeviceScenes(device: GoveeDevice, cd: CloudDevice): Promise<boolean> {\n this.diagnostics.addLog(cd.device, \"debug\", `loadDeviceScenes called for ${cd.sku}`);\n // Scenes from dedicated scenes endpoint (rate-limited).\n // Guards are per-list, not combined: Govee's /device/scenes sometimes\n // returns 149 lightScenes + 0 snapshots (or vice versa) on back-to-back\n // calls even though the snapshot exists. A combined guard (if any list\n // non-empty, overwrite all) would wipe the other lists on that call and\n // break the dropdown until the next lucky round-trip. One guard per\n // list keeps the last-known-good data in place.\n const loadScenes = async (): Promise<void> => {\n try {\n const { lightScenes, diyScenes, snapshots } = await this.cloudClient!.getScenes(cd.sku, cd.device);\n // Cloud-client already captured the raw response on success \u2014 but\n // record the catch path here so the diag JSON shows \"/device/scenes\n // attempted, returned 403\" instead of an empty slot.\n if (lightScenes.length > 0) {\n device.scenes = lightScenes;\n }\n if (diyScenes.length > 0) {\n device.diyScenes = diyScenes;\n }\n if (snapshots.length > 0) {\n device.snapshots = snapshots;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(cd.device, \"/router/api/v1/device/scenes\", e, this.extractStatus(e));\n this.log.debug(`Could not load scenes for ${cd.sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadScenes, 2);\n\n // DIY scenes from dedicated endpoint\n if (device.diyScenes.length === 0) {\n const loadDiy = async (): Promise<void> => {\n try {\n const diy = await this.cloudClient!.getDiyScenes(cd.sku, cd.device);\n if (diy.length > 0) {\n device.diyScenes = diy;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(cd.device, \"/router/api/v1/device/diy-scenes\", e, this.extractStatus(e));\n this.log.debug(`Could not load DIY scenes for ${cd.sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadDiy, 2);\n }\n\n // Snapshots from device capabilities (fallback)\n if (device.snapshots.length === 0) {\n const caps = Array.isArray(cd.capabilities) ? cd.capabilities : [];\n const snapCap = caps.find(\n c =>\n c &&\n c.type === \"devices.capabilities.dynamic_scene\" &&\n c.instance === \"snapshot\" &&\n Array.isArray(c.parameters?.options),\n );\n if (snapCap?.parameters?.options) {\n device.snapshots = snapCap.parameters.options\n .filter(o => o && typeof o.name === \"string\" && o.value !== undefined && o.value !== null)\n .map(o => ({\n name: o.name,\n value: typeof o.value === \"number\" ? o.value : (o.value as Record<string, unknown>),\n }));\n this.log.debug(`Snapshots from capabilities for ${cd.sku}: ${device.snapshots.length}`);\n }\n }\n\n // \"Changed\" = we ended up with any scene/snapshot data. Inner tracking\n // was redundant with this single-source check.\n return device.scenes.length > 0 || device.diyScenes.length > 0 || device.snapshots.length > 0;\n }\n\n /**\n * Load scene/music/DIY libraries and SKU features from undocumented API.\n *\n * Each fetch runs through the rate-limiter so a fresh install with 10\n * devices doesn't slam app2.govee.com with 40 back-to-back requests \u2014\n * those endpoints are undocumented and aggressive callers can get the\n * account temporarily locked.\n *\n * @param device Target device to populate\n * @param sku Product model\n * @param force When true, refetch every endpoint regardless of cache \u2014\n * used by the user-triggered refresh button so a stale library\n * actually gets replaced\n * @returns true if any library data changed\n */\n private async loadDeviceLibraries(device: GoveeDevice, sku: string, force = false): Promise<boolean> {\n if (!this.apiClient) {\n return false;\n }\n\n this.diagnostics.addLog(device.deviceId, \"debug\", `loadDeviceLibraries called for ${sku} (force=${force})`);\n let changed = false;\n\n // Run each fetch inside a rate-limited slot. Priority 2 = below\n // control commands and scene/snapshot loads; library data is cache-only\n // and can wait for a quieter moment.\n const runLimited = async (fn: () => Promise<void>): Promise<void> => {\n await this.commandRouter.executeRateLimited(fn, 2);\n };\n\n if (force || device.sceneLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/light-effect-libraries?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchSceneLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(s => s.name) });\n if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n this.log.debug(`Scene library for ${sku}: ${lib.length} scenes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load scene library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || device.musicLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/light-effect-libraries-music?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchMusicLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(m => m.name) });\n if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n this.log.debug(`Music library for ${sku}: ${lib.length} modes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load music library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || device.diyLibrary.length === 0) {\n await runLimited(async () => {\n const ep = `/diy-effect-libraries?sku=${sku}`;\n try {\n const lib = await this.apiClient!.fetchDiyLibrary(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, { count: lib.length, names: lib.map(d => d.name) });\n if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n this.log.debug(`DIY library for ${sku}: ${lib.length} effects`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load DIY library for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n if (force || !device.skuFeatures) {\n await runLimited(async () => {\n const ep = `/sku-features?sku=${sku}`;\n try {\n const features = await this.apiClient!.fetchSkuFeatures(sku);\n this.diagnostics.recordApiSuccess(device.deviceId, ep, features);\n if (features) {\n device.skuFeatures = features;\n changed = true;\n this.log.debug(`SKU features for ${sku}: ${JSON.stringify(features).slice(0, 200)}`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.log.debug(`Could not load SKU features for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n // Load snapshot BLE commands for local activation\n if (!device.snapshotBleCmds && device.snapshots.length > 0) {\n await runLimited(async () => {\n try {\n const snaps = await this.apiClient!.fetchSnapshots(sku, device.deviceId);\n if (snaps.length > 0) {\n device.snapshotBleCmds = device.snapshots.map(ds => {\n const match = snaps.find(s => s.name === ds.name);\n return match?.bleCmds ?? [];\n });\n changed = true;\n this.log.debug(`Snapshot BLE for ${sku}: ${snaps.length} snapshots with local data`);\n }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${e instanceof Error ? e.message : String(e)}`);\n }\n });\n }\n\n return changed;\n }\n\n /**\n * Load group membership from undocumented API and attach to BaseGroup devices.\n * Resolves member device references against the current device map.\n *\n * @returns true if any group memberships were resolved\n */\n async loadGroupMembers(): Promise<boolean> {\n if (!this.apiClient) {\n return false;\n }\n if (!this.apiClient.hasBearerToken()) {\n this.log.debug(\"Group membership requires Email+Password \u2014 skipping member resolution\");\n return false;\n }\n\n try {\n const apiGroups = await this.apiClient.fetchGroupMembers();\n if (apiGroups.length === 0) {\n this.log.debug(\"No group membership data from API\");\n return false;\n }\n\n let changed = false;\n for (const group of this.devices.values()) {\n if (group.sku !== \"BaseGroup\") {\n continue;\n }\n // Match by groupId: BaseGroup deviceId is the numeric group ID as string\n const apiGroup = apiGroups.find(g => String(g.groupId) === group.deviceId);\n if (!apiGroup) {\n continue;\n }\n\n // Resolve member devices against our device map\n const members: { sku: string; deviceId: string }[] = [];\n for (const m of apiGroup.devices) {\n const resolved = this.findDeviceBySkuAndId(m.sku, m.deviceId);\n if (resolved) {\n members.push({ sku: resolved.sku, deviceId: resolved.deviceId });\n } else {\n this.log.debug(`Group \"${group.name}\": member ${m.sku}/${m.deviceId} not in device map`);\n }\n }\n\n group.groupMembers = members;\n if (members.length > 0) {\n changed = true;\n }\n this.log.debug(`Group \"${group.name}\": ${members.length}/${apiGroup.devices.length} members resolved`);\n }\n\n if (changed) {\n this.onDeviceListChanged?.(this.getDevices());\n }\n return changed;\n } catch (e) {\n this.log.debug(`Could not load group members: ${e instanceof Error ? e.message : String(e)}`);\n return false;\n }\n }\n\n /** Save all devices to SKU cache, skipping only those never confirmed via Cloud yet. */\n public saveDevicesToCache(): void {\n if (!this.skuCache) {\n return;\n }\n\n let cachedCount = 0;\n let skippedCount = 0;\n for (const device of this.devices.values()) {\n const isLight = device.type === \"devices.types.light\";\n // Skip only if we never asked Cloud yet \u2014 empty scenes are legitimate\n // once confirmed via scenesChecked=true.\n if (isLight && !device.scenesChecked) {\n skippedCount++;\n this.log.debug(`Not caching ${device.name} (${device.sku}) \u2014 scenes not yet checked`);\n } else {\n this.skuCache.save(this.goveeDeviceToCached(device));\n cachedCount++;\n }\n }\n // Routine persistence \u2014 debug level only. Users don't need a play-by-play\n // for every cache write. Significant events (scenes fetched, MQTT bumps)\n // log themselves elsewhere.\n if (skippedCount > 0) {\n this.log.debug(`Cached ${cachedCount} device(s), skipped ${skippedCount} not yet checked`);\n } else {\n this.log.debug(`Cached ${cachedCount} device(s) \u2014 next start uses cache`);\n }\n }\n\n /**\n * Handle LAN device discovery \u2014 match against known devices or create new.\n *\n * @param lanDevice Discovered LAN device\n */\n handleLanDiscovery(lanDevice: LanDevice): void {\n // Try to find by device ID (colon-separated in Cloud, varies in LAN)\n let matched: GoveeDevice | undefined;\n for (const dev of this.devices.values()) {\n if (normalizeDeviceId(dev.deviceId) === normalizeDeviceId(lanDevice.device)) {\n matched = dev;\n break;\n }\n // Also match by SKU if device IDs don't match format\n if (dev.sku === lanDevice.sku && !dev.lanIp) {\n matched = dev;\n break;\n }\n }\n\n if (matched) {\n const ipChanged = matched.lanIp !== lanDevice.ip;\n const wasOffline = matched.state.online !== true;\n matched.lanIp = lanDevice.ip;\n matched.channels.lan = true;\n matched.lastSeenOnNetwork = Date.now();\n if (ipChanged) {\n this.log.debug(`LAN: ${matched.name} (${matched.sku}) at ${lanDevice.ip}`);\n this.onLanIpChanged?.(matched, lanDevice.ip);\n }\n // Discovery-Antwort beweist dass das Ger\u00E4t am Netz ist. main.ts skipped\n // den expliziten devStatus-Poll wenn MQTT connected ist, und MQTT pushed\n // nur bei tats\u00E4chlichen Zustandswechseln \u2014 d.h. ohne diesen Pfad bleibt\n // info.online f\u00FCr gecachte Lichter forever false.\n if (wasOffline) {\n matched.state.online = true;\n this.onDeviceUpdate?.(matched, { online: true });\n }\n } else {\n // LAN-only device (no Cloud data yet)\n // Include short device ID suffix for uniqueness (multiple devices can share same SKU)\n const shortId = normalizeDeviceId(lanDevice.device).slice(-4);\n const device: GoveeDevice = {\n sku: lanDevice.sku,\n deviceId: lanDevice.device,\n name: `${lanDevice.sku}_${shortId}`,\n type: \"devices.types.light\",\n lanIp: lanDevice.ip,\n capabilities: [],\n scenes: [],\n diyScenes: [],\n snapshots: [],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n lastSeenOnNetwork: Date.now(),\n state: { online: true },\n channels: { lan: true, mqtt: false, cloud: false },\n };\n this.devices.set(this.deviceKey(lanDevice.sku, lanDevice.device), device);\n this.diagnostics.addLog(lanDevice.device, \"info\", `LAN-discovered at ${lanDevice.ip}`);\n this.log.debug(`LAN: New device ${lanDevice.sku} at ${lanDevice.ip}`);\n this.maybeNudgeSeedSku(lanDevice.sku, device.name);\n this.onDeviceListChanged?.(this.getDevices());\n }\n }\n\n /**\n * Log the device's trust tier \u2014 once per SKU per adapter lifetime, so\n * device reconnects don't spam the log. Behaviour by tier:\n * - verified / reported: silent (the catalog backs the device, no\n * action needed). The tier is still surfaced via the\n * `diag.tier` state for any user who wants to check.\n * - seed (toggle off): warn \u2014 points the user at the experimental\n * toggle that gates the per-SKU corrections we'd otherwise apply.\n * - seed (toggle on): info \u2014 confirms quirks are active.\n * - unknown: warn \u2014 asks for a diagnostics export so we can add the\n * SKU to the catalogue.\n *\n * @param sku Govee SKU\n * @param displayName Device name as shown in Govee Home\n */\n private maybeNudgeSeedSku(sku: string, displayName: string | undefined): void {\n const upper = (typeof sku === \"string\" ? sku : \"\").toUpperCase();\n if (!upper || this.nudgedSeedSkus.has(upper)) {\n return;\n }\n this.nudgedSeedSkus.add(upper);\n const tier = getDeviceTier(upper);\n const label = displayName ? `${displayName} (${upper})` : upper;\n switch (tier) {\n case \"verified\":\n case \"reported\":\n return;\n case \"seed\":\n if (isSeedAndDormant(upper)) {\n this.log.warn(\n `Device ${label} is in beta and needs the \"Experimentelle Ger\u00E4te-Unterst\u00FCtzung aktivieren\" toggle in adapter settings to apply known per-SKU corrections.`,\n );\n } else {\n this.log.info(`Device ${label} is in beta \u2014 experimental quirks are active.`);\n }\n return;\n case \"unknown\":\n this.log.warn(\n `Device ${label} is not in the supported device list. Please trigger diag.export and post the resulting JSON in a GitHub issue so the SKU can be added.`,\n );\n return;\n }\n }\n\n /**\n * Handle MQTT status update \u2014 update device state.\n *\n * @param update MQTT status message\n */\n handleMqttStatus(update: MqttStatusUpdate): void {\n const device = this.findDeviceBySkuAndId(update.sku, update.device);\n if (!device) {\n this.log.debug(`MQTT: Unknown device ${update.sku} ${update.device}`);\n return;\n }\n\n device.channels.mqtt = true;\n device.lastSeenOnNetwork = Date.now();\n const state: Partial<DeviceState> = { online: true };\n\n if (update.state) {\n if (update.state.onOff !== undefined) {\n state.power = update.state.onOff === 1;\n }\n if (update.state.brightness !== undefined) {\n state.brightness = update.state.brightness;\n }\n if (update.state.color) {\n const { r, g, b } = update.state.color;\n state.colorRgb = rgbToHex(r, g, b);\n }\n if (update.state.colorTemInKelvin) {\n state.colorTemperature = update.state.colorTemInKelvin;\n }\n }\n\n // Merge into device state\n Object.assign(device.state, state);\n this.onDeviceUpdate?.(device, state);\n\n // Parse per-segment data from BLE notification packets (AA A5).\n // MQTT is authoritative for segment count \u2014 the device tells us what it\n // actually has. Cloud only gives an initial best-guess from capabilities.\n if (update.op?.command) {\n const segData = parseMqttSegmentData(update.op.command);\n\n if (segData.length > 0) {\n const maxSeen = Math.max(...segData.map(s => s.index)) + 1;\n const current = device.segmentCount ?? 0;\n if (maxSeen > current) {\n this.log.info(\n `${device.name}: detected ${maxSeen} segments via MQTT (was ${current}) \u2014 rebuilding state tree`,\n );\n device.segmentCount = maxSeen;\n // Persist now so a restart starts from the real value instead of\n // falling back to Cloud capabilities and deleting the extra slots.\n if (this.skuCache) {\n this.skuCache.save(this.goveeDeviceToCached(device));\n }\n // Skip per-segment sync for this push \u2014 the new datapoints don't\n // exist yet. The next AA A5 push hits the fully-built tree.\n this.onSegmentCountGrown?.(device);\n return;\n }\n }\n\n // Filter by manual-segments override if active \u2014 ignore indices the\n // user has declared as \"not physically present\" (cut strip).\n const filtered =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? segData.filter(s => device.manualSegments!.includes(s.index))\n : segData;\n if (filtered.length > 0) {\n this.onMqttSegmentUpdate?.(device, filtered);\n }\n }\n }\n\n /**\n * Handle LAN status response.\n *\n * @param ip Source IP address\n * @param status LAN status data\n * @param status.onOff Power state (1=on, 0=off)\n * @param status.brightness Brightness 0-100\n * @param status.color RGB color values\n * @param status.color.r Red channel 0-255\n * @param status.color.g Green channel 0-255\n * @param status.color.b Blue channel 0-255\n * @param status.colorTemInKelvin Color temperature in Kelvin\n */\n handleLanStatus(\n ip: string,\n status: {\n onOff: number;\n brightness: number;\n color: { r: number; g: number; b: number };\n colorTemInKelvin: number;\n },\n ): void {\n // Find device by LAN IP\n let device: GoveeDevice | undefined;\n for (const dev of this.devices.values()) {\n if (dev.lanIp === ip) {\n device = dev;\n break;\n }\n }\n if (!device) {\n return;\n }\n\n device.lastSeenOnNetwork = Date.now();\n const { r, g, b } = status.color;\n const state: Partial<DeviceState> = {\n online: true,\n power: status.onOff === 1,\n brightness: status.brightness,\n colorRgb: rgbToHex(r, g, b),\n colorTemperature: status.colorTemInKelvin || undefined,\n };\n\n Object.assign(device.state, state);\n this.onDeviceUpdate?.(device, state);\n }\n\n /**\n * Set the callback for batch segment state sync.\n * Forwards to the internal CommandRouter.\n *\n * @param callback Called when a segment batch command updates segment states\n */\n set onSegmentBatchUpdate(\n callback:\n | ((device: GoveeDevice, batch: { segments: number[]; color?: number; brightness?: number }) => void)\n | undefined,\n ) {\n this.commandRouter.onSegmentBatchUpdate = callback;\n }\n\n /**\n * Send a command to a device \u2014 routes through LAN \u2192 Cloud.\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 return this.commandRouter.sendCommand(device, command, value);\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 return this.commandRouter.sendCapabilityCommand(device, capabilityType, capabilityInstance, value);\n }\n\n /** Callback when device LAN IP changes */\n onLanIpChanged?: (device: GoveeDevice, ip: string) => void;\n\n /** Callback when MQTT delivers per-segment state data (AA A5 BLE packets) */\n onMqttSegmentUpdate?: (device: GoveeDevice, segments: MqttSegmentData[]) => void;\n\n /**\n * Callback when the device's physical segment count turns out to be\n * larger than the Cloud-reported value (observed via MQTT AA A5 stream).\n * The adapter rebuilds the state tree in response so the extra indices\n * appear as datapoints.\n */\n onSegmentCountGrown?: (device: GoveeDevice) => void;\n\n /**\n * Convert Cloud device to internal device model\n *\n * @param cd Cloud API device data\n */\n private cloudDeviceToGoveeDevice(cd: CloudDevice): GoveeDevice {\n return {\n sku: cd.sku,\n deviceId: cd.device,\n name: cd.deviceName || cd.sku,\n type: cd.type || \"unknown\",\n capabilities: Array.isArray(cd.capabilities) ? cd.capabilities : [],\n scenes: [],\n diyScenes: [],\n snapshots: [],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n state: { online: true },\n channels: { lan: false, mqtt: false, cloud: true },\n };\n }\n\n /**\n * Find device by SKU and device ID (handles format differences)\n *\n * @param sku Product model\n * @param deviceId Device identifier\n */\n private findDeviceBySkuAndId(sku: string, deviceId: string): GoveeDevice | undefined {\n // Direct key lookup\n const direct = this.devices.get(this.deviceKey(sku, deviceId));\n if (direct) {\n return direct;\n }\n\n // Normalized search\n const normalizedId = normalizeDeviceId(deviceId);\n for (const dev of this.devices.values()) {\n if (dev.sku === sku && normalizeDeviceId(dev.deviceId) === normalizedId) {\n return dev;\n }\n }\n return undefined;\n }\n\n /**\n * Generate unique key for a device\n *\n * @param sku Product model\n * @param deviceId Device identifier\n */\n private deviceKey(sku: string, deviceId: string): string {\n return `${sku}_${normalizeDeviceId(deviceId)}`;\n }\n\n /**\n * Log error with dedup \u2014 only warn on category change, debug on repeat.\n *\n * @param context Error context description\n * @param err Error to log\n */\n private logDedup(context: string, err: unknown): void {\n const category = classifyError(err);\n const msg = `${context}: ${err instanceof Error ? err.message : String(err)}`;\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(`${msg} (repeated)`);\n }\n }\n\n /**\n * Fill device.scenes from sceneLibrary when Cloud scenes are missing.\n * ptReal activation matches by name, so sceneLibrary names are sufficient.\n *\n * @param device Device to populate scenes for\n */\n private populateScenesFromLibrary(device: GoveeDevice): void {\n if (device.scenes.length === 0 && device.sceneLibrary.length > 0) {\n device.scenes = device.sceneLibrary.map(entry => ({\n name: entry.name,\n value: {}, // ptReal uses sceneLibrary directly, Cloud payload not needed\n }));\n this.log.debug(`${device.sku}: ${device.scenes.length} scenes from library (Cloud scenes missing)`);\n }\n }\n\n /**\n * Convert cached data to a GoveeDevice (runtime fields set to defaults)\n *\n * @param cached Cached device data\n */\n private cachedToGoveeDevice(cached: CachedDeviceData): GoveeDevice {\n return {\n sku: cached.sku,\n deviceId: cached.deviceId,\n name: cached.name,\n type: cached.type,\n capabilities: cached.capabilities,\n scenes: cached.scenes,\n diyScenes: cached.diyScenes,\n snapshots: cached.snapshots,\n sceneLibrary: cached.sceneLibrary,\n musicLibrary: cached.musicLibrary,\n diyLibrary: cached.diyLibrary,\n skuFeatures: cached.skuFeatures,\n snapshotBleCmds: cached.snapshotBleCmds,\n scenesChecked: cached.scenesChecked,\n lastSeenOnNetwork: cached.lastSeenOnNetwork,\n // Restore learned count so it wins over Cloud capability on next start.\n segmentCount: cached.segmentCount,\n manualMode: cached.manualMode,\n manualSegments: cached.manualSegments,\n sceneSpeed: cached.sceneSpeed,\n state: { online: false },\n channels: { lan: false, mqtt: false, cloud: false },\n };\n }\n\n /**\n * Persist a device's current runtime state to the SKU cache.\n * Safe no-op when no cache is configured.\n *\n * @param device Target device\n */\n public persistDeviceToCache(device: GoveeDevice): void {\n if (!this.skuCache) {\n return;\n }\n this.skuCache.save(this.goveeDeviceToCached(device));\n }\n\n /**\n * Extract cacheable data from a GoveeDevice.\n *\n * @param device Runtime device\n */\n private goveeDeviceToCached(device: GoveeDevice): CachedDeviceData {\n return {\n sku: device.sku,\n deviceId: device.deviceId,\n name: device.name,\n type: device.type,\n capabilities: device.capabilities,\n scenes: device.scenes,\n diyScenes: device.diyScenes,\n snapshots: device.snapshots,\n sceneLibrary: device.sceneLibrary,\n musicLibrary: device.musicLibrary,\n diyLibrary: device.diyLibrary,\n skuFeatures: device.skuFeatures,\n snapshotBleCmds: device.snapshotBleCmds,\n scenesChecked: device.scenesChecked,\n lastSeenOnNetwork: device.lastSeenOnNetwork,\n segmentCount:\n typeof device.segmentCount === \"number\" && device.segmentCount > 0 ? device.segmentCount : undefined,\n manualMode: device.manualMode ? true : undefined,\n manualSegments:\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.slice()\n : undefined,\n sceneSpeed: typeof device.sceneSpeed === \"number\" && device.sceneSpeed > 0 ? device.sceneSpeed : undefined,\n cachedAt: Date.now(),\n };\n }\n\n /**\n * Generate diagnostics data for a device \u2014 structured JSON for GitHub\n * issue submission. Delegates to the DiagnosticsCollector so the JSON\n * also includes ring-buffer context (recent logs, MQTT packets, last\n * API responses).\n *\n * @param device Target device\n * @param adapterVersion Adapter version string\n */\n generateDiagnostics(device: GoveeDevice, adapterVersion: string): Record<string, unknown> {\n return this.diagnostics.generate(device, adapterVersion);\n }\n\n /**\n * Poll the undocumented app-API for sensor-like devices (H5179 et al.)\n * where OpenAPI v2 `/device/state` returns empty. Each entry is converted\n * to synthetic capabilities and routed back through the same callback as\n * regular Cloud state, so the existing setState pipeline picks it up\n * without a special-case branch.\n *\n * Bearer token comes from the MQTT login flow \u2014 without MQTT credentials\n * (Email + Password) this is a no-op.\n *\n * @returns Number of devices that received an update\n */\n async pollAppApi(): Promise<number> {\n if (!this.apiClient || !this.apiClient.hasBearerToken()) {\n return 0;\n }\n // Skip the entire round-trip when no device in the registry would\n // actually consume App-API readings. The App API is only used for\n // sensor and appliance state (thermometers, heaters, kettles, \u2026);\n // a Lights-only setup would otherwise burn one Govee call every 2\n // minutes for nothing.\n if (!this.hasDeviceNeedingAppApi()) {\n return 0;\n }\n let entries: AppDeviceEntry[];\n try {\n entries = await this.apiClient.fetchDeviceList();\n } catch (err) {\n const category = classifyError(err);\n const msg = `App API fetch failed: ${err instanceof Error ? err.message : String(err)}`;\n if (category !== this.lastAppApiErrorCategory) {\n this.lastAppApiErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n return 0;\n }\n // Reset on success so the next failure warns again.\n this.lastAppApiErrorCategory = null;\n let updated = 0;\n for (const entry of entries) {\n const device = this.devices.get(this.deviceKey(entry.sku, entry.device));\n if (!device) {\n continue;\n }\n const caps = buildCapabilitiesFromAppEntry(entry);\n if (caps.length === 0) {\n continue;\n }\n // Route synthetic capabilities through the existing\n // onCloudCapabilities callback so main.ts's normal setState\n // pipeline (mapCloudStateValue + setStateAsync) handles them.\n this.onCloudCapabilities?.(device, caps);\n // mapSingleCapability returns null for the synthetic `online`\n // cap (online is a device-level property, not a regular state),\n // so onCloudCapabilities never reaches info.online via the\n // capability pipeline. Pluck it out and apply it directly \u2014\n // otherwise sensor SKUs like H5179 stay at info.online=false\n // forever even while their readings keep updating.\n this.applyOnlineCap(device, caps);\n this.diagnostics.setApiResponse(device.deviceId, \"/device/rest/devices/v1/list\", entry);\n updated++;\n }\n return updated;\n }\n\n /**\n * Pull the `devices.capabilities.online` entry (if any) out of a\n * synthetic capability list and apply it directly to\n * `device.state.online` plus `lastSeenOnNetwork`. Surfaces via\n * onDeviceUpdate so the adapter's `info.online` state matches the\n * App-API / OpenAPI-MQTT signal. If no online cap is in the list but\n * the list is non-empty (i.e. fresh data arrived), the device is\n * considered online \u2014 same convention as the LAN/MQTT paths.\n *\n * @param device Target device\n * @param caps Capability list from the source pipeline\n */\n private applyOnlineCap(device: GoveeDevice, caps: CloudStateCapability[]): void {\n let online: boolean | undefined;\n for (const c of caps) {\n if (\n c &&\n typeof c.type === \"string\" &&\n (c.type === \"devices.capabilities.online\" || c.type === \"online\") &&\n c.state &&\n typeof c.state.value === \"boolean\"\n ) {\n online = c.state.value;\n break;\n }\n }\n // Fresh data with no online flag \u2192 assume online (LAN/MQTT use the\n // same \"we just heard from the device\" convention).\n if (online === undefined && caps.length > 0) {\n online = true;\n }\n if (online === undefined) {\n return;\n }\n if (device.state.online === online && online === true) {\n // Already online + still online \u2014 only refresh the lastSeen ts\n // and skip the onDeviceUpdate noise.\n device.lastSeenOnNetwork = Date.now();\n return;\n }\n device.state.online = online;\n if (online) {\n device.lastSeenOnNetwork = Date.now();\n }\n this.onDeviceUpdate?.(device, { online });\n }\n\n /**\n * Hook callback for sources that emit `CloudStateCapability[]` updates\n * outside the normal Cloud-poll path (App-API, OpenAPI-MQTT). Caller is\n * responsible for wiring it to the adapter-side state-write path.\n *\n * @param cb Callback receiving (device, caps)\n */\n setOnCloudCapabilities(cb: ((device: GoveeDevice, caps: CloudStateCapability[]) => void) | null): void {\n this.onCloudCapabilities = cb;\n }\n\n /**\n * Whether at least one device in the registry would consume App-API\n * readings (sensors, appliances). Used to skip the App-API poll on\n * Lights-only installations.\n */\n private hasDeviceNeedingAppApi(): boolean {\n for (const dev of this.devices.values()) {\n if (dev.type !== \"devices.types.light\" && dev.sku !== \"BaseGroup\") {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Process a parsed OpenAPI-MQTT event by forwarding its capabilities\n * through the same hook used by App-API polls. Called from the\n * adapter-side OpenAPI-MQTT message handler.\n *\n * @param event Parsed event from the OpenAPI-MQTT broker\n * @param event.sku Govee SKU (e.g. \"H5179\")\n * @param event.device MAC-style device identifier\n * @param event.capabilities Capability list synthesised from the broker payload\n */\n handleOpenApiEvent(event: { sku: string; device: string; capabilities: CloudStateCapability[] }): void {\n if (!event || typeof event.sku !== \"string\" || typeof event.device !== \"string\") {\n return;\n }\n if (!Array.isArray(event.capabilities) || event.capabilities.length === 0) {\n return;\n }\n const device = this.devices.get(this.deviceKey(event.sku, event.device));\n if (!device) {\n return;\n }\n this.onCloudCapabilities?.(device, event.capabilities);\n // Same online-cap unwrap as the App-API path. OpenAPI-MQTT events\n // are the only signal we get for appliance state (heater on/off,\n // ice-bucket-full, \u2026) \u2014 without this, info.online for those SKUs\n // never flips to true even while events stream in.\n this.applyOnlineCap(device, event.capabilities);\n }\n}\n\n/**\n * Convert an app-API device entry into a list of synthetic Cloud-state\n * capabilities the existing `mapCloudStateValue` pipeline can consume.\n *\n * Govee stores temperature and humidity as integer hundredths of a unit\n * (`tem: 2370` \u2192 23.70 \u00B0C, `hum: 4290` \u2192 42.90 % RH). Battery may live\n * either at the lastData level or in deviceSettings \u2014 lastData wins\n * because it's the more recent reading.\n *\n * @param entry One entry from `GoveeApiClient.fetchDeviceList()`\n */\nexport function buildCapabilitiesFromAppEntry(entry: AppDeviceEntry): CloudStateCapability[] {\n const caps: CloudStateCapability[] = [];\n const last = entry.lastData;\n if (!last) {\n return caps;\n }\n if (typeof last.online === \"boolean\") {\n caps.push({\n type: \"devices.capabilities.online\",\n instance: \"online\",\n state: { value: last.online },\n });\n }\n if (typeof last.tem === \"number\" && Number.isFinite(last.tem)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorTemperature\",\n state: { value: last.tem / 100 },\n });\n }\n if (typeof last.hum === \"number\" && Number.isFinite(last.hum)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"sensorHumidity\",\n state: { value: last.hum / 100 },\n });\n }\n if (typeof last.battery === \"number\" && Number.isFinite(last.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: last.battery },\n });\n } else if (entry.settings && typeof entry.settings.battery === \"number\" && Number.isFinite(entry.settings.battery)) {\n caps.push({\n type: \"devices.capabilities.property\",\n instance: \"battery\",\n state: { value: entry.settings.battery },\n });\n }\n return caps;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
@@ -141,6 +141,35 @@ class GoveeMqttClient {
141
141
  get token() {
142
142
  return this._bearerToken;
143
143
  }
144
+ /**
145
+ * Short user-facing reason for "MQTT not connected", or null if the
146
+ * client has never seen an error. Used by the adapter ready-summary
147
+ * to give a concrete message instead of "still pending".
148
+ */
149
+ getFailureReason() {
150
+ if (this.connected) {
151
+ return null;
152
+ }
153
+ switch (this.lastErrorCategory) {
154
+ case "VERIFICATION_PENDING":
155
+ return "Govee asked for verification \u2014 request a code in adapter settings";
156
+ case "VERIFICATION_FAILED":
157
+ return "verification code rejected \u2014 request a fresh code";
158
+ case "AUTH":
159
+ return this.authFailCount >= MAX_AUTH_FAILURES ? "login rejected \u2014 check email/password" : "login failed (will retry)";
160
+ case "RATE_LIMIT":
161
+ return "rate-limited by Govee \u2014 will retry";
162
+ case "NETWORK":
163
+ return "cannot reach Govee servers \u2014 will retry";
164
+ case "TIMEOUT":
165
+ return "connection timeout \u2014 will retry";
166
+ case "UNKNOWN":
167
+ return "login rejected \u2014 see earlier log";
168
+ case null:
169
+ default:
170
+ return null;
171
+ }
172
+ }
144
173
  /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */
145
174
  persisted = null;
146
175
  /** Hook fired after a successful login so the adapter can persist the new credentials. */
@@ -263,9 +292,7 @@ class GoveeMqttClient {
263
292
  const isNew = this.lastErrorCategory !== category;
264
293
  this.lastErrorCategory = category;
265
294
  if (isNew) {
266
- this.log.warn(
267
- 'Govee asked for one-time client verification (HTTP 454). Open adapter settings, click "Request verification code", paste the code from the email into the field, save. Govee remembers this client afterwards. Account-level 2FA is not required.'
268
- );
295
+ this.log.warn("MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings");
269
296
  } else {
270
297
  this.log.debug("MQTT verification still pending (Govee returned 454 again)");
271
298
  }
@@ -278,9 +305,7 @@ class GoveeMqttClient {
278
305
  const isNew = this.lastErrorCategory !== category;
279
306
  this.lastErrorCategory = category;
280
307
  if (isNew) {
281
- this.log.warn(
282
- "Govee rejected the verification code (HTTP 455) \u2014 request a fresh code via the adapter settings."
283
- );
308
+ this.log.warn("MQTT not connected: verification code rejected \u2014 request a fresh code");
284
309
  } else {
285
310
  this.log.debug("MQTT verification code rejected again (Govee returned 455)");
286
311
  }
@@ -292,7 +317,7 @@ class GoveeMqttClient {
292
317
  if (category === "AUTH") {
293
318
  this.authFailCount++;
294
319
  if (this.authFailCount >= MAX_AUTH_FAILURES) {
295
- this.log.warn(`MQTT login failed ${this.authFailCount} times \u2014 check email/password in adapter settings`);
320
+ this.log.warn("MQTT not connected: login rejected \u2014 check email/password");
296
321
  return;
297
322
  }
298
323
  } else {
@@ -415,7 +440,7 @@ class GoveeMqttClient {
415
440
  this.accountTopic = creds.accountTopic;
416
441
  (_a = this.onToken) == null ? void 0 : _a.call(this, this._bearerToken);
417
442
  const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;
418
- this.log.info("MQTT: trying cached credentials (no fresh login)");
443
+ this.log.debug("MQTT: trying cached credentials (no fresh login)");
419
444
  this.persistedAttemptInFlight = true;
420
445
  this.client = mqtt.connect(`mqtts://${creds.iotEndpoint}:8883`, {
421
446
  clientId,
@@ -449,7 +474,7 @@ class GoveeMqttClient {
449
474
  this.log.info("MQTT connection restored");
450
475
  this.lastErrorCategory = null;
451
476
  } else {
452
- this.log.info("MQTT connected to AWS IoT");
477
+ this.log.info("MQTT connected");
453
478
  }
454
479
  (_a = this.client) == null ? void 0 : _a.subscribe(this.accountTopic, { qos: 0 }, (err) => {
455
480
  var _a2;
@@ -473,7 +498,7 @@ class GoveeMqttClient {
473
498
  if (this.persistedAttemptInFlight) {
474
499
  this.persistedAttemptInFlight = false;
475
500
  this.persisted = null;
476
- this.log.info("MQTT: cached credentials rejected \u2014 falling back to fresh login");
501
+ this.log.debug("MQTT: cached credentials rejected \u2014 falling back to fresh login");
477
502
  }
478
503
  if (!this.lastErrorCategory) {
479
504
  this.lastErrorCategory = "NETWORK";