iobroker.govee-smart 2.8.4 → 2.9.0

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
@@ -128,6 +128,10 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
128
128
  Placeholder for the next version (at the beginning of the line):
129
129
  ### **WORK IN PROGRESS**
130
130
  -->
131
+ ### 2.9.0 (2026-05-13)
132
+
133
+ - `info.online` for Lights now tracks real LAN reachability (90 s window). Cloud and MQTT push no longer write it — they produced false-positive `true` during real outages.
134
+
131
135
  ### 2.8.4 (2026-05-12)
132
136
 
133
137
  - The device trust tier state under each device no longer carries a multi-language label object that would crash the admin with "Error in GUI" if rendered as a dropdown.
@@ -147,11 +151,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
147
151
 
148
152
  - Info, warn and error logs are back to their normal short form. The channel-status prefix from 2.8.0 stays only in debug logs.
149
153
 
150
- ### 2.8.0 (2026-05-11)
151
-
152
- - Restart no longer briefly removes and re-creates scene, music and snapshot datapoints.
153
- - Lights without API key no longer have empty scene/snapshot dropdowns left over from earlier versions.
154
-
155
154
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
156
155
 
157
156
  ## Support
@@ -726,6 +726,7 @@ class DeviceManager {
726
726
  matched.lanIp = lanDevice.ip;
727
727
  matched.channels.lan = true;
728
728
  matched.lastSeenOnNetwork = Date.now();
729
+ matched.lastLanReplyAt = Date.now();
729
730
  if (ipChanged) {
730
731
  this.log.debug(`LAN: ${matched.name} (${matched.sku}) at ${lanDevice.ip}`);
731
732
  (_a = this.onLanIpChanged) == null ? void 0 : _a.call(this, matched, lanDevice.ip);
@@ -826,7 +827,10 @@ class DeviceManager {
826
827
  }
827
828
  device.channels.mqtt = true;
828
829
  device.lastSeenOnNetwork = Date.now();
829
- const state = { online: true };
830
+ const state = {};
831
+ if (device.type !== "devices.types.light") {
832
+ state.online = true;
833
+ }
830
834
  if (update.state) {
831
835
  const onOff = (0, import_types.coerceFiniteNumber)(update.state.onOff);
832
836
  if (onOff !== null) {
@@ -904,6 +908,7 @@ class DeviceManager {
904
908
  return;
905
909
  }
906
910
  device.lastSeenOnNetwork = Date.now();
911
+ device.lastLanReplyAt = Date.now();
907
912
  const { r, g, b } = status.color;
908
913
  const state = {
909
914
  online: true,
@@ -1059,7 +1064,9 @@ class DeviceManager {
1059
1064
  return false;
1060
1065
  }
1061
1066
  (_a = this.onCloudCapabilities) == null ? void 0 : _a.call(this, device, caps);
1062
- this.applyOnlineCap(device, caps);
1067
+ if (device.type !== "devices.types.light") {
1068
+ this.applyOnlineCap(device, caps);
1069
+ }
1063
1070
  this.diagnostics.setApiResponse(device.deviceId, "/device/rest/devices/v1/list", entry);
1064
1071
  return true;
1065
1072
  })
@@ -1133,7 +1140,9 @@ class DeviceManager {
1133
1140
  return;
1134
1141
  }
1135
1142
  (_a = this.onCloudCapabilities) == null ? void 0 : _a.call(this, device, event.capabilities);
1136
- this.applyOnlineCap(device, event.capabilities);
1143
+ if (device.type !== "devices.types.light") {
1144
+ this.applyOnlineCap(device, event.capabilities);
1145
+ }
1137
1146
  }
1138
1147
  }
1139
1148
  // Annotate the CommonJS export names for ESM import in node:
@@ -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 { getDeviceTier, isSeedAndDormant } from \"./device-registry\";\nimport { DiagnosticsCollector } from \"./diagnostics\";\nimport {\n deviceKey as deviceKeyHelper,\n findDeviceBySkuAndId as findDeviceBySkuAndIdHelper,\n parseMqttSegmentData,\n SEGMENT_HARD_MAX,\n type MqttSegmentData,\n} from \"./device-manager/lookups\";\nimport { buildCapabilitiesFromAppEntry as buildCapabilitiesFromAppEntryHelper } from \"./device-manager/mapping\";\nimport * as cacheHelpers from \"./device-manager/cache\";\nimport * as cloudMergeHelpers from \"./device-manager/cloud-merge\";\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 { SkuCache } from \"./sku-cache\";\nimport {\n classifyError,\n coerceFiniteNumber,\n logDedup,\n normalizeDeviceId,\n rgbToHex,\n type CloudDevice,\n type CloudLoadResult,\n type CloudScene,\n type CloudStateCapability,\n type DeviceState,\n type ErrorCategory,\n type GoveeDevice,\n type LanDevice,\n type MqttStatusUpdate,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\nimport { HttpError } from \"./http-client\";\n\n// Re-export for backwards compat \u2014 consumers (main.ts, segment-wizard, state-manager)\n// import these directly from \"./device-manager\".\nexport {\n parseMqttSegmentData,\n getEffectiveSegmentIndices,\n resolveSegmentCount,\n SEGMENT_HARD_MAX,\n type MqttSegmentData,\n} from \"./device-manager/lookups\";\nexport { buildCapabilitiesFromAppEntry, cloudDeviceToGoveeDevice } from \"./device-manager/mapping\";\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 /** Public for sub-module helpers (cache, cloud-merge). */\n public readonly log: ioBroker.Logger;\n /** Public for sub-module helpers (cache, cloud-merge, lookups). */\n public 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 /** Public for sub-module helpers (cache). */\n public skuCache: SkuCache | null = null;\n /** Public for sub-module helpers (cloud-merge). */\n public onDeviceUpdate: ((device: GoveeDevice, state: Partial<DeviceState>) => void) | null = null;\n /** Phase-specific callbacks \u2014 one per data source. See setCallbacks. */\n public onLanDeviceReady: ((device: GoveeDevice, allDevices: GoveeDevice[]) => void) | null = null;\n public onCloudDataReady: ((device: GoveeDevice, allDevices: GoveeDevice[]) => void) | null = null;\n public onGroupMembersReady: ((group: GoveeDevice, allDevices: 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 /** Dedup tracker for `loadGroupMembers` errors \u2014 first warn per category, rest debug. */\n private lastGroupMembersErrorCategory: 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 * Structured debug-log for failed undocumented App-API calls. Pulls apart\n * the cryptic \"Invalid JSON in HTTP 200 response \u2014 body starts with: <snippet>\"\n * message into addressable fields so the user can read the actual facts:\n * endpoint URL, HTTP status, bearer-token presence, body snippet.\n * No interpretation \u2014 just the data.\n *\n * @param sku Govee SKU (for log context)\n * @param what Human-readable name of the data being loaded\n * @param endpoint Endpoint identifier for diagnostics history\n * @param hasBearer Whether a bearer token was attached to the request\n * @param e Caught error\n */\n private logUndocApiFailure(sku: string, what: string, endpoint: string, hasBearer: boolean, e: unknown): void {\n const httpStatus = this.extractStatus(e);\n const msg = errMessage(e);\n // http-client formats invalid-JSON-200 errors as \"...body starts with: <snippet>\"\n const bodyMatch = msg.match(/body starts with: (.+)$/);\n const bodySnippet = bodyMatch?.[1] ?? \"\";\n const statusPart = httpStatus !== undefined ? ` httpStatus=${httpStatus}` : \"\";\n const bodyPart = bodySnippet ? ` body=\"${bodySnippet}\"` : ` error=\"${msg}\"`;\n this.log.debug(\n `Could not load ${what} for ${sku}: endpoint=${endpoint}${statusPart} bearer=${hasBearer ? \"yes\" : \"no\"}${bodyPart}`,\n );\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 the phase-specific callbacks. Each fires when its data source has\n * delivered its part of the picture \u2014 never with stale / half-filled data.\n *\n * @param callbacks Phase callbacks. See per-field JSDoc on DeviceManager.\n * @param callbacks.onUpdate Fired when a single device's state-fields change (LAN/MQTT/Cloud value update)\n * @param callbacks.onLanDeviceReady Fired when LAN-Discovery finds a device \u2014 only LAN data is available yet\n * @param callbacks.onCloudDataReady Fired when Cloud capabilities are available (cache merge OR live cloud)\n * @param callbacks.onGroupMembersReady Fired when group membership has been resolved via App-API\n */\n setCallbacks(callbacks: {\n onUpdate: (device: GoveeDevice, state: Partial<DeviceState>) => void;\n onLanDeviceReady: (device: GoveeDevice, allDevices: GoveeDevice[]) => void;\n onCloudDataReady: (device: GoveeDevice, allDevices: GoveeDevice[]) => void;\n onGroupMembersReady: (group: GoveeDevice, allDevices: GoveeDevice[]) => void;\n }): void {\n this.onDeviceUpdate = callbacks.onUpdate;\n this.onLanDeviceReady = callbacks.onLanDeviceReady;\n this.onCloudDataReady = callbacks.onCloudDataReady;\n this.onGroupMembersReady = callbacks.onGroupMembersReady;\n }\n\n /** Get all known devices */\n getDevices(): GoveeDevice[] {\n return Array.from(this.devices.values());\n }\n\n /**\n * Entfernt ein Ger\u00E4t aus dem internen Tracking. Aufgerufen wenn ein Ger\u00E4t\n * aus dem Govee-Account entfernt wurde \u2014 die jsonl-Objects r\u00E4umt\n * `cleanupDevices` (state-manager) ab; hier nur die in-memory-Maps.\n *\n * Returnt die deviceId des gedroppten Ger\u00E4ts (zur Diagnostics-Cleanup),\n * oder null wenn nichts zu entfernen war.\n *\n * @param sku Govee-SKU\n * @param deviceId Device-ID (mit/ohne Doppelpunkte)\n */\n removeDevice(sku: string, deviceId: string): string | null {\n const key = this.deviceKey(sku, deviceId);\n const dev = this.devices.get(key);\n if (!dev) {\n return null;\n }\n this.devices.delete(key);\n // nudgedSeedSkus bleibt \u2014 wir wollen den seed-Hinweis nicht erneut\n // pushen wenn ein gleicher SKU sp\u00E4ter wieder reinpoppt.\n return dev.deviceId;\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 const nowMs = Date.now();\n for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n const ageDays =\n typeof entry.lastSeenOnNetwork === \"number\" ? Math.round((nowMs - entry.lastSeenOnNetwork) / 86400000) : null;\n const ageInfo = ageDays === null ? \"no age data (legacy entry)\" : `${ageDays}d since last seen`;\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 this.log.debug(\n `Cache merged into LAN-discovered device ${entry.sku} ${entry.deviceId} (${ageInfo}, caps=${entry.capabilities.length})`,\n );\n } else {\n this.devices.set(key, cacheHelpers.cachedToGoveeDevice(entry));\n changed = true;\n this.log.debug(\n `Cache restored (no LAN discovery yet) for ${entry.sku} ${entry.deviceId} (${ageInfo}, caps=${entry.capabilities.length})`,\n );\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // Fire per-device phase callback right after merge. Devices with\n // non-empty caps go into Cloud-phase immediately (cache counts as Cloud-\n // data-ready); devices without caps stay in LAN-phase. The boot continues\n // \u2014 Cloud-Load will refresh dropdowns/scenes/snapshots later via\n // onCloudDataReady again (idempotent).\n const allDevices = this.getDevices();\n for (const device of allDevices) {\n if (device.capabilities.length > 0) {\n this.onCloudDataReady?.(device, allDevices);\n } else if (device.lanIp) {\n this.onLanDeviceReady?.(device, allDevices);\n }\n }\n\n // Always refetch cloud data on startup \u2014 scenesChecked is purely\n // diagnostic now, not a gate. Snapshots are user-content (created\n // dynamically in the Govee Home app) and would miss new entries if we\n // relied solely on the cache. The refetch costs one call per light per\n // startup, well within rate limits.\n const hasLight = allDevices.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 cacheHelpers.populateScenesFromLibrary(this, device);\n }\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 cacheHelpers.populateScenesFromLibrary(this, device);\n }\n\n if (changed) {\n const allDevices = this.getDevices();\n for (const device of allDevices) {\n if (device.sku === \"BaseGroup\") {\n // Groups go through onGroupMembersReady \u2014 see loadGroupMembers\n continue;\n }\n this.onCloudDataReady?.(device, allDevices);\n }\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 one specific device. Triggered\n * by the per-device `snapshots.refresh_cloud` button (\"a new snapshot/scene\n * was saved in the Govee Home app, show it here for THIS light\").\n *\n * Three Cloud calls happen in order:\n * 1. `/user/devices` \u2014 refreshes the whole capability set including the\n * authoritative snapshot-options list (this is what was missing in\n * v2.6.7's refresh path: stale capabilities meant the snapshot fallback\n * in `loadDeviceScenes` couldn't see new entries).\n * 2. `/device/scenes` + `/device/diy-scenes` (per loadDeviceScenes)\n * 3. `/appsku/v1/light-effect-libraries` \u00D7 3 (scene/music/DIY via\n * loadDeviceLibraries with force=true)\n *\n * Replaces the global `refreshSceneData()` removed in v2.7.0: refreshing all\n * lights cost N*5 Cloud calls vs 5 for the one device the user actually\n * touched. Rate-limit pressure scales linearly with account size.\n *\n * @param deviceId Target device's deviceId (mac-like identifier)\n * @returns true when scene/snapshot/library data changed\n */\n async refreshSceneDataForDevice(deviceId: string): Promise<boolean> {\n if (!this.cloudClient) {\n return false;\n }\n const target = Array.from(this.devices.values()).find(\n d => normalizeDeviceId(d.deviceId) === normalizeDeviceId(deviceId),\n );\n if (!target) {\n this.log.debug(`refreshSceneDataForDevice: device ${deviceId} not found`);\n return false;\n }\n this.diagnostics.addLog(target.deviceId, \"info\", `User-triggered refresh-cloud-data for ${target.sku}`);\n\n // Step 1: refetch the device list so cd.capabilities is current. Skipping\n // this was the v2.6.7 bug \u2014 the button re-ran /device/scenes only, which\n // never carries newly-created snapshots for some SKUs; the authoritative\n // list lives in /user/devices.\n try {\n const rawCloudDevices = await this.cloudClient.getDevices();\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 this.mergeCloudDevices(cloudDevices);\n } catch (e) {\n this.log.debug(`refreshSceneDataForDevice: getDevices failed: ${errMessage(e)}`);\n // Keep going with stale capabilities \u2014 better than aborting the refresh.\n }\n\n // Step 2: per-device scenes + libraries with fresh capabilities.\n const cd: CloudDevice = {\n sku: target.sku,\n device: target.deviceId,\n deviceName: target.name,\n type: target.type,\n capabilities: Array.isArray(target.capabilities) ? target.capabilities : [],\n };\n let changed = false;\n if (await this.loadDeviceScenes(target, cd)) {\n changed = true;\n }\n if (await this.loadDeviceLibraries(target, cd.sku, /* force */ true)) {\n changed = true;\n }\n if (changed) {\n this.saveDevicesToCache();\n cacheHelpers.populateScenesFromLibrary(this, target);\n // Per-device Cloud-phase fire \u2014 only the targeted device needs a rebuild.\n this.onCloudDataReady?.(target, this.getDevices());\n }\n return changed;\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 return cloudMergeHelpers.mergeCloudDevices(this, cloudDevices);\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 //\n // lightScene + diyScene: per-list guard against transient empties. Govee's\n // /device/scenes sometimes returns 149 lightScenes + 0 snapshots (or vice\n // versa) on back-to-back calls. One guard per list keeps the last-known-good\n // data in place for those types.\n //\n // snapshot: handled separately AFTER this block (see below). A per-list\n // guard alone froze the cached snapshot list forever once it was populated \u2014\n // user content (snapshots created in the Govee Home app) never surfaced\n // (Issue #13, tukey42, v2.6.7).\n let scenesCallSucceeded = false;\n let snapsFromScenesCall: CloudScene[] = [];\n const loadScenes = async (): Promise<void> => {\n try {\n const { lightScenes, diyScenes, snapshots } = await this.cloudClient!.getScenes(cd.sku, cd.device);\n scenesCallSucceeded = true;\n snapsFromScenesCall = snapshots;\n if (lightScenes.length > 0) {\n device.scenes = lightScenes;\n }\n if (diyScenes.length > 0) {\n device.diyScenes = diyScenes;\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}: ${errMessage(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}: ${errMessage(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadDiy, 2);\n }\n\n // Snapshots \u2014 three-way resolution:\n // 1. /device/scenes returned non-empty snapshots \u2192 trust that list.\n // 2. /device/scenes succeeded but returned empty \u2192 fall back to the\n // `snapshot` capability inside /user/devices (cd.capabilities).\n // This is the fix path for newly-created snapshots: /device/scenes\n // lags or omits them for some SKUs, but /user/devices carries them.\n // Empty capability options here is a legitimate \"user deleted all\n // snapshots in the app\" \u2014 we reflect that and clear the list.\n // 3. /device/scenes threw OR no snapshot capability exists at all \u2192\n // keep device.snapshots untouched (cache survives transient Cloud\n // outages and devices that simply don't expose the capability).\n if (snapsFromScenesCall.length > 0) {\n device.snapshots = snapsFromScenesCall;\n } else if (scenesCallSucceeded) {\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.\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 const hasBearer = this.apiClient.hasBearerToken();\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 this.log.debug(\n `Scene library for ${sku}: ${lib.length} scene(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"scene library\", ep, hasBearer, 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 this.log.debug(\n `Music library for ${sku}: ${lib.length} mode(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"music library\", ep, hasBearer, 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 this.log.debug(\n `DIY library for ${sku}: ${lib.length} effect(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"DIY library\", ep, hasBearer, 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 } else {\n this.log.debug(`SKU features for ${sku}: null \u2014 Govee returned no data for this SKU`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"SKU features\", ep, hasBearer, e);\n }\n });\n }\n\n // Load snapshot BLE commands for local activation.\n // `force` honoured so refresh_cloud also clears stale BLE-Cmds when the\n // user re-creates a snapshot in the Govee app and re-imports it. Without\n // the force-branch the gate was sticky \u2014 cached snapshot packets stayed\n // until the cache file was manually deleted (Issue #13 v2.8.2, tukey42).\n if ((force || !device.snapshotBleCmds) && device.snapshots.length > 0) {\n await runLimited(async () => {\n try {\n const snaps = await this.apiClient!.fetchSnapshots(sku, device.deviceId);\n this.log.debug(\n `Snapshot BLE for ${sku}: ${snaps.length} snapshot(s) with local data${snaps.length === 0 ? \" \u2014 Govee returned no BLE-cmds for this SKU/device\" : \"\"}`,\n );\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 }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${errMessage(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 const allDevices = this.getDevices();\n // Per-group Group-phase fire \u2014 only the BaseGroup state-trees need\n // rebuilding (intersection of member caps). Members themselves\n // haven't changed, so their phase callbacks don't fire.\n for (const group of allDevices.filter(d => d.sku === \"BaseGroup\")) {\n this.onGroupMembersReady?.(group, allDevices);\n }\n }\n // Reset dedup on success so a future failure warns again.\n this.lastGroupMembersErrorCategory = null;\n return changed;\n } catch (e) {\n // Group-membership is best-effort \u2014 but a persistent failure (e.g. API\n // permission revoked) should still surface once so the user knows\n // groups won't fan-out. logDedup demotes repeats to debug.\n this.lastGroupMembersErrorCategory = logDedup(\n this.log,\n this.lastGroupMembersErrorCategory,\n \"Group membership\",\n e,\n );\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 cacheHelpers.saveDevicesToCache(this);\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 // Prim\u00E4rer Match \u2014 exakte Ger\u00E4te-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 }\n // SKU-Fallback nur wenn EXACTLY ONE matchender Eintrag existiert.\n // Bei mehreren same-SKU-devices ohne lanIp k\u00F6nnte sonst das falsche\n // Ger\u00E4t gebunden werden (Memory `feedback_doppel_audit_pattern`).\n if (!matched) {\n const skuMatches = Array.from(this.devices.values()).filter(dev => dev.sku === lanDevice.sku && !dev.lanIp);\n if (skuMatches.length === 1) {\n matched = skuMatches[0];\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(\n `LAN: new device sku=${lanDevice.sku} deviceId=${lanDevice.device} ip=${lanDevice.ip} reachable=yes`,\n );\n this.maybeNudgeSeedSku(lanDevice.sku, device.name);\n // LAN-phase only \u2014 capabilities are empty at this point. Cloud-phase\n // will fire later from cache-merge or loadFromCloud once caps arrive.\n // Before v2.8.0 this fired a bulk onDeviceListChanged that triggered\n // the wipe-and-recreate bug. (Issue #13)\n this.onLanDeviceReady?.(device, 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 /**\n * Public for sub-module helpers (cloud-merge).\n *\n * @param sku Product SKU\n * @param displayName Display name from Cloud\n */\n public 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 \"Enable experimental device support\" 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 // API-Boundary: Govee schickt gelegentlich brightness/onOff/color als\n // String oder mit unerwarteten Typen. coerceFiniteNumber/coerceBool\n // returnt null bei Drift \u2192 Feld bleibt unver\u00E4ndert (vorhandener Wert\n // bleibt stehen, kein State-Schreibung mit kaputtem Wert).\n const onOff = coerceFiniteNumber(update.state.onOff);\n if (onOff !== null) {\n state.power = onOff === 1;\n }\n const brightness = coerceFiniteNumber(update.state.brightness);\n if (brightness !== null) {\n state.brightness = brightness;\n }\n if (update.state.color && typeof update.state.color === \"object\") {\n const r = coerceFiniteNumber((update.state.color as { r?: unknown }).r);\n const g = coerceFiniteNumber((update.state.color as { g?: unknown }).g);\n const b = coerceFiniteNumber((update.state.color as { b?: unknown }).b);\n if (r !== null && g !== null && b !== null) {\n state.colorRgb = rgbToHex(r, g, b);\n }\n }\n // 0 = \"not in colortemp mode\" \u2014 drop intentionally (Govee-Konvention).\n const ctk = coerceFiniteNumber(update.state.colorTemInKelvin);\n if (ctk !== null && ctk > 0) {\n state.colorTemperature = ctk;\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 // L6 \u2014 Plausibilit\u00E4ts-Cap: SEGMENT_HARD_MAX (55) ist die Govee-\n // Protokoll-Obergrenze. Werte dar\u00FCber kommen nur aus broken oder\n // spoofed Paketen \u2014 niemals persistieren.\n if (maxSeen > SEGMENT_HARD_MAX) {\n this.log.debug(`${device.name}: ignoring segmentCount=${maxSeen} (above protocol limit ${SEGMENT_HARD_MAX})`);\n return;\n }\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(cacheHelpers.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 * 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 return findDeviceBySkuAndIdHelper(this.devices, sku, deviceId);\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 deviceKeyHelper(sku, 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}: ${errMessage(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 * Persist a device's current runtime state to the SKU cache. Safe no-op\n * when no cache is configured.\n *\n * @param device Target device\n */\n public persistDeviceToCache(device: GoveeDevice): void {\n cacheHelpers.persistDeviceToCache(this, device);\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: ${errMessage(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 // Process all entries in parallel \u2014 each entry only touches its own\n // device (no shared mutation), and the downstream callbacks (onCloud-\n // Capabilities \u2192 main.applyCloudCapabilities \u2192 setStateAsync queue)\n // are async-safe. Sequential `for` blocked the App-API tick on a slow\n // setStateAsync round-trip per device.\n // Wrap each per-entry block in `Promise.resolve` so the iterable is a\n // true Thenable \u2014 synchronous returns confuse `await Promise.all`'s\n // type-checker (await-thenable lint rule) even though the runtime would\n // accept them. No-op at runtime, makes the intent explicit and lints\n // without `require-await`.\n const results = await Promise.all(\n entries.map(entry =>\n Promise.resolve().then(() => {\n const device = this.devices.get(this.deviceKey(entry.sku, entry.device));\n if (!device) {\n return false;\n }\n const caps = buildCapabilitiesFromAppEntryHelper(entry);\n if (caps.length === 0) {\n return false;\n }\n this.onCloudCapabilities?.(device, caps);\n // mapSingleCapability returns null for the synthetic `online` cap\n // (online is a device-level property, not a regular state), so\n // onCloudCapabilities never reaches info.online via the capability\n // pipeline. Pluck it out and apply it directly \u2014 otherwise sensor\n // SKUs like H5179 stay at info.online=false forever even while\n // their readings keep updating.\n this.applyOnlineCap(device, caps);\n this.diagnostics.setApiResponse(device.deviceId, \"/device/rest/devices/v1/list\", entry);\n return true;\n }),\n ),\n );\n return results.filter(Boolean).length;\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 cloudMergeHelpers.applyOnlineCap(this, device, caps);\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 /**\n * True wenn mindestens ein Device App-API-Werte konsumiert (Sensoren,\n * Appliances). Adapter-checkAllReady wartet darauf damit \u201Eready\" erst\n * geloggt wird wenn Sensor-Werte tats\u00E4chlich da sind.\n */\n public 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"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { hasDynamicSceneCapability } from \"./capability-mapper\";\nimport { CommandRouter } from \"./command-router\";\nimport { getDeviceTier, isSeedAndDormant } from \"./device-registry\";\nimport { DiagnosticsCollector } from \"./diagnostics\";\nimport {\n deviceKey as deviceKeyHelper,\n findDeviceBySkuAndId as findDeviceBySkuAndIdHelper,\n parseMqttSegmentData,\n SEGMENT_HARD_MAX,\n type MqttSegmentData,\n} from \"./device-manager/lookups\";\nimport { buildCapabilitiesFromAppEntry as buildCapabilitiesFromAppEntryHelper } from \"./device-manager/mapping\";\nimport * as cacheHelpers from \"./device-manager/cache\";\nimport * as cloudMergeHelpers from \"./device-manager/cloud-merge\";\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 { SkuCache } from \"./sku-cache\";\nimport {\n classifyError,\n coerceFiniteNumber,\n logDedup,\n normalizeDeviceId,\n rgbToHex,\n type CloudDevice,\n type CloudLoadResult,\n type CloudScene,\n type CloudStateCapability,\n type DeviceState,\n type ErrorCategory,\n type GoveeDevice,\n type LanDevice,\n type MqttStatusUpdate,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\nimport { HttpError } from \"./http-client\";\n\n// Re-export for backwards compat \u2014 consumers (main.ts, segment-wizard, state-manager)\n// import these directly from \"./device-manager\".\nexport {\n parseMqttSegmentData,\n getEffectiveSegmentIndices,\n resolveSegmentCount,\n SEGMENT_HARD_MAX,\n type MqttSegmentData,\n} from \"./device-manager/lookups\";\nexport { buildCapabilitiesFromAppEntry, cloudDeviceToGoveeDevice } from \"./device-manager/mapping\";\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 /** Public for sub-module helpers (cache, cloud-merge). */\n public readonly log: ioBroker.Logger;\n /** Public for sub-module helpers (cache, cloud-merge, lookups). */\n public 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 /** Public for sub-module helpers (cache). */\n public skuCache: SkuCache | null = null;\n /** Public for sub-module helpers (cloud-merge). */\n public onDeviceUpdate: ((device: GoveeDevice, state: Partial<DeviceState>) => void) | null = null;\n /** Phase-specific callbacks \u2014 one per data source. See setCallbacks. */\n public onLanDeviceReady: ((device: GoveeDevice, allDevices: GoveeDevice[]) => void) | null = null;\n public onCloudDataReady: ((device: GoveeDevice, allDevices: GoveeDevice[]) => void) | null = null;\n public onGroupMembersReady: ((group: GoveeDevice, allDevices: 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 /** Dedup tracker for `loadGroupMembers` errors \u2014 first warn per category, rest debug. */\n private lastGroupMembersErrorCategory: 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 * Structured debug-log for failed undocumented App-API calls. Pulls apart\n * the cryptic \"Invalid JSON in HTTP 200 response \u2014 body starts with: <snippet>\"\n * message into addressable fields so the user can read the actual facts:\n * endpoint URL, HTTP status, bearer-token presence, body snippet.\n * No interpretation \u2014 just the data.\n *\n * @param sku Govee SKU (for log context)\n * @param what Human-readable name of the data being loaded\n * @param endpoint Endpoint identifier for diagnostics history\n * @param hasBearer Whether a bearer token was attached to the request\n * @param e Caught error\n */\n private logUndocApiFailure(sku: string, what: string, endpoint: string, hasBearer: boolean, e: unknown): void {\n const httpStatus = this.extractStatus(e);\n const msg = errMessage(e);\n // http-client formats invalid-JSON-200 errors as \"...body starts with: <snippet>\"\n const bodyMatch = msg.match(/body starts with: (.+)$/);\n const bodySnippet = bodyMatch?.[1] ?? \"\";\n const statusPart = httpStatus !== undefined ? ` httpStatus=${httpStatus}` : \"\";\n const bodyPart = bodySnippet ? ` body=\"${bodySnippet}\"` : ` error=\"${msg}\"`;\n this.log.debug(\n `Could not load ${what} for ${sku}: endpoint=${endpoint}${statusPart} bearer=${hasBearer ? \"yes\" : \"no\"}${bodyPart}`,\n );\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 the phase-specific callbacks. Each fires when its data source has\n * delivered its part of the picture \u2014 never with stale / half-filled data.\n *\n * @param callbacks Phase callbacks. See per-field JSDoc on DeviceManager.\n * @param callbacks.onUpdate Fired when a single device's state-fields change (LAN/MQTT/Cloud value update)\n * @param callbacks.onLanDeviceReady Fired when LAN-Discovery finds a device \u2014 only LAN data is available yet\n * @param callbacks.onCloudDataReady Fired when Cloud capabilities are available (cache merge OR live cloud)\n * @param callbacks.onGroupMembersReady Fired when group membership has been resolved via App-API\n */\n setCallbacks(callbacks: {\n onUpdate: (device: GoveeDevice, state: Partial<DeviceState>) => void;\n onLanDeviceReady: (device: GoveeDevice, allDevices: GoveeDevice[]) => void;\n onCloudDataReady: (device: GoveeDevice, allDevices: GoveeDevice[]) => void;\n onGroupMembersReady: (group: GoveeDevice, allDevices: GoveeDevice[]) => void;\n }): void {\n this.onDeviceUpdate = callbacks.onUpdate;\n this.onLanDeviceReady = callbacks.onLanDeviceReady;\n this.onCloudDataReady = callbacks.onCloudDataReady;\n this.onGroupMembersReady = callbacks.onGroupMembersReady;\n }\n\n /** Get all known devices */\n getDevices(): GoveeDevice[] {\n return Array.from(this.devices.values());\n }\n\n /**\n * Entfernt ein Ger\u00E4t aus dem internen Tracking. Aufgerufen wenn ein Ger\u00E4t\n * aus dem Govee-Account entfernt wurde \u2014 die jsonl-Objects r\u00E4umt\n * `cleanupDevices` (state-manager) ab; hier nur die in-memory-Maps.\n *\n * Returnt die deviceId des gedroppten Ger\u00E4ts (zur Diagnostics-Cleanup),\n * oder null wenn nichts zu entfernen war.\n *\n * @param sku Govee-SKU\n * @param deviceId Device-ID (mit/ohne Doppelpunkte)\n */\n removeDevice(sku: string, deviceId: string): string | null {\n const key = this.deviceKey(sku, deviceId);\n const dev = this.devices.get(key);\n if (!dev) {\n return null;\n }\n this.devices.delete(key);\n // nudgedSeedSkus bleibt \u2014 wir wollen den seed-Hinweis nicht erneut\n // pushen wenn ein gleicher SKU sp\u00E4ter wieder reinpoppt.\n return dev.deviceId;\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 const nowMs = Date.now();\n for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n const ageDays =\n typeof entry.lastSeenOnNetwork === \"number\" ? Math.round((nowMs - entry.lastSeenOnNetwork) / 86400000) : null;\n const ageInfo = ageDays === null ? \"no age data (legacy entry)\" : `${ageDays}d since last seen`;\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 this.log.debug(\n `Cache merged into LAN-discovered device ${entry.sku} ${entry.deviceId} (${ageInfo}, caps=${entry.capabilities.length})`,\n );\n } else {\n this.devices.set(key, cacheHelpers.cachedToGoveeDevice(entry));\n changed = true;\n this.log.debug(\n `Cache restored (no LAN discovery yet) for ${entry.sku} ${entry.deviceId} (${ageInfo}, caps=${entry.capabilities.length})`,\n );\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // Fire per-device phase callback right after merge. Devices with\n // non-empty caps go into Cloud-phase immediately (cache counts as Cloud-\n // data-ready); devices without caps stay in LAN-phase. The boot continues\n // \u2014 Cloud-Load will refresh dropdowns/scenes/snapshots later via\n // onCloudDataReady again (idempotent).\n const allDevices = this.getDevices();\n for (const device of allDevices) {\n if (device.capabilities.length > 0) {\n this.onCloudDataReady?.(device, allDevices);\n } else if (device.lanIp) {\n this.onLanDeviceReady?.(device, allDevices);\n }\n }\n\n // Always refetch cloud data on startup \u2014 scenesChecked is purely\n // diagnostic now, not a gate. Snapshots are user-content (created\n // dynamically in the Govee Home app) and would miss new entries if we\n // relied solely on the cache. The refetch costs one call per light per\n // startup, well within rate limits.\n const hasLight = allDevices.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 cacheHelpers.populateScenesFromLibrary(this, device);\n }\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 cacheHelpers.populateScenesFromLibrary(this, device);\n }\n\n if (changed) {\n const allDevices = this.getDevices();\n for (const device of allDevices) {\n if (device.sku === \"BaseGroup\") {\n // Groups go through onGroupMembersReady \u2014 see loadGroupMembers\n continue;\n }\n this.onCloudDataReady?.(device, allDevices);\n }\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 one specific device. Triggered\n * by the per-device `snapshots.refresh_cloud` button (\"a new snapshot/scene\n * was saved in the Govee Home app, show it here for THIS light\").\n *\n * Three Cloud calls happen in order:\n * 1. `/user/devices` \u2014 refreshes the whole capability set including the\n * authoritative snapshot-options list (this is what was missing in\n * v2.6.7's refresh path: stale capabilities meant the snapshot fallback\n * in `loadDeviceScenes` couldn't see new entries).\n * 2. `/device/scenes` + `/device/diy-scenes` (per loadDeviceScenes)\n * 3. `/appsku/v1/light-effect-libraries` \u00D7 3 (scene/music/DIY via\n * loadDeviceLibraries with force=true)\n *\n * Replaces the global `refreshSceneData()` removed in v2.7.0: refreshing all\n * lights cost N*5 Cloud calls vs 5 for the one device the user actually\n * touched. Rate-limit pressure scales linearly with account size.\n *\n * @param deviceId Target device's deviceId (mac-like identifier)\n * @returns true when scene/snapshot/library data changed\n */\n async refreshSceneDataForDevice(deviceId: string): Promise<boolean> {\n if (!this.cloudClient) {\n return false;\n }\n const target = Array.from(this.devices.values()).find(\n d => normalizeDeviceId(d.deviceId) === normalizeDeviceId(deviceId),\n );\n if (!target) {\n this.log.debug(`refreshSceneDataForDevice: device ${deviceId} not found`);\n return false;\n }\n this.diagnostics.addLog(target.deviceId, \"info\", `User-triggered refresh-cloud-data for ${target.sku}`);\n\n // Step 1: refetch the device list so cd.capabilities is current. Skipping\n // this was the v2.6.7 bug \u2014 the button re-ran /device/scenes only, which\n // never carries newly-created snapshots for some SKUs; the authoritative\n // list lives in /user/devices.\n try {\n const rawCloudDevices = await this.cloudClient.getDevices();\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 this.mergeCloudDevices(cloudDevices);\n } catch (e) {\n this.log.debug(`refreshSceneDataForDevice: getDevices failed: ${errMessage(e)}`);\n // Keep going with stale capabilities \u2014 better than aborting the refresh.\n }\n\n // Step 2: per-device scenes + libraries with fresh capabilities.\n const cd: CloudDevice = {\n sku: target.sku,\n device: target.deviceId,\n deviceName: target.name,\n type: target.type,\n capabilities: Array.isArray(target.capabilities) ? target.capabilities : [],\n };\n let changed = false;\n if (await this.loadDeviceScenes(target, cd)) {\n changed = true;\n }\n if (await this.loadDeviceLibraries(target, cd.sku, /* force */ true)) {\n changed = true;\n }\n if (changed) {\n this.saveDevicesToCache();\n cacheHelpers.populateScenesFromLibrary(this, target);\n // Per-device Cloud-phase fire \u2014 only the targeted device needs a rebuild.\n this.onCloudDataReady?.(target, this.getDevices());\n }\n return changed;\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 return cloudMergeHelpers.mergeCloudDevices(this, cloudDevices);\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 //\n // lightScene + diyScene: per-list guard against transient empties. Govee's\n // /device/scenes sometimes returns 149 lightScenes + 0 snapshots (or vice\n // versa) on back-to-back calls. One guard per list keeps the last-known-good\n // data in place for those types.\n //\n // snapshot: handled separately AFTER this block (see below). A per-list\n // guard alone froze the cached snapshot list forever once it was populated \u2014\n // user content (snapshots created in the Govee Home app) never surfaced\n // (Issue #13, tukey42, v2.6.7).\n let scenesCallSucceeded = false;\n let snapsFromScenesCall: CloudScene[] = [];\n const loadScenes = async (): Promise<void> => {\n try {\n const { lightScenes, diyScenes, snapshots } = await this.cloudClient!.getScenes(cd.sku, cd.device);\n scenesCallSucceeded = true;\n snapsFromScenesCall = snapshots;\n if (lightScenes.length > 0) {\n device.scenes = lightScenes;\n }\n if (diyScenes.length > 0) {\n device.diyScenes = diyScenes;\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}: ${errMessage(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}: ${errMessage(e)}`);\n }\n };\n await this.commandRouter.executeRateLimited(loadDiy, 2);\n }\n\n // Snapshots \u2014 three-way resolution:\n // 1. /device/scenes returned non-empty snapshots \u2192 trust that list.\n // 2. /device/scenes succeeded but returned empty \u2192 fall back to the\n // `snapshot` capability inside /user/devices (cd.capabilities).\n // This is the fix path for newly-created snapshots: /device/scenes\n // lags or omits them for some SKUs, but /user/devices carries them.\n // Empty capability options here is a legitimate \"user deleted all\n // snapshots in the app\" \u2014 we reflect that and clear the list.\n // 3. /device/scenes threw OR no snapshot capability exists at all \u2192\n // keep device.snapshots untouched (cache survives transient Cloud\n // outages and devices that simply don't expose the capability).\n if (snapsFromScenesCall.length > 0) {\n device.snapshots = snapsFromScenesCall;\n } else if (scenesCallSucceeded) {\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.\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 const hasBearer = this.apiClient.hasBearerToken();\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 this.log.debug(\n `Scene library for ${sku}: ${lib.length} scene(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"scene library\", ep, hasBearer, 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 this.log.debug(\n `Music library for ${sku}: ${lib.length} mode(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"music library\", ep, hasBearer, 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 this.log.debug(\n `DIY library for ${sku}: ${lib.length} effect(s)${lib.length === 0 ? \" \u2014 empty (Govee returned no data for this SKU)\" : \"\"}`,\n );\n if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"DIY library\", ep, hasBearer, 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 } else {\n this.log.debug(`SKU features for ${sku}: null \u2014 Govee returned no data for this SKU`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.logUndocApiFailure(sku, \"SKU features\", ep, hasBearer, e);\n }\n });\n }\n\n // Load snapshot BLE commands for local activation.\n // `force` honoured so refresh_cloud also clears stale BLE-Cmds when the\n // user re-creates a snapshot in the Govee app and re-imports it. Without\n // the force-branch the gate was sticky \u2014 cached snapshot packets stayed\n // until the cache file was manually deleted (Issue #13 v2.8.2, tukey42).\n if ((force || !device.snapshotBleCmds) && device.snapshots.length > 0) {\n await runLimited(async () => {\n try {\n const snaps = await this.apiClient!.fetchSnapshots(sku, device.deviceId);\n this.log.debug(\n `Snapshot BLE for ${sku}: ${snaps.length} snapshot(s) with local data${snaps.length === 0 ? \" \u2014 Govee returned no BLE-cmds for this SKU/device\" : \"\"}`,\n );\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 }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${errMessage(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 const allDevices = this.getDevices();\n // Per-group Group-phase fire \u2014 only the BaseGroup state-trees need\n // rebuilding (intersection of member caps). Members themselves\n // haven't changed, so their phase callbacks don't fire.\n for (const group of allDevices.filter(d => d.sku === \"BaseGroup\")) {\n this.onGroupMembersReady?.(group, allDevices);\n }\n }\n // Reset dedup on success so a future failure warns again.\n this.lastGroupMembersErrorCategory = null;\n return changed;\n } catch (e) {\n // Group-membership is best-effort \u2014 but a persistent failure (e.g. API\n // permission revoked) should still surface once so the user knows\n // groups won't fan-out. logDedup demotes repeats to debug.\n this.lastGroupMembersErrorCategory = logDedup(\n this.log,\n this.lastGroupMembersErrorCategory,\n \"Group membership\",\n e,\n );\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 cacheHelpers.saveDevicesToCache(this);\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 // Prim\u00E4rer Match \u2014 exakte Ger\u00E4te-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 }\n // SKU-Fallback nur wenn EXACTLY ONE matchender Eintrag existiert.\n // Bei mehreren same-SKU-devices ohne lanIp k\u00F6nnte sonst das falsche\n // Ger\u00E4t gebunden werden (Memory `feedback_doppel_audit_pattern`).\n if (!matched) {\n const skuMatches = Array.from(this.devices.values()).filter(dev => dev.sku === lanDevice.sku && !dev.lanIp);\n if (skuMatches.length === 1) {\n matched = skuMatches[0];\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 matched.lastLanReplyAt = 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(\n `LAN: new device sku=${lanDevice.sku} deviceId=${lanDevice.device} ip=${lanDevice.ip} reachable=yes`,\n );\n this.maybeNudgeSeedSku(lanDevice.sku, device.name);\n // LAN-phase only \u2014 capabilities are empty at this point. Cloud-phase\n // will fire later from cache-merge or loadFromCloud once caps arrive.\n // Before v2.8.0 this fired a bulk onDeviceListChanged that triggered\n // the wipe-and-recreate bug. (Issue #13)\n this.onLanDeviceReady?.(device, 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 /**\n * Public for sub-module helpers (cloud-merge).\n *\n * @param sku Product SKU\n * @param displayName Display name from Cloud\n */\n public 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 \"Enable experimental device support\" 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 // MQTT-push proves the device talked to the Govee broker \u2014 but the broker\n // can replay last-will / retained messages and buffer reconnect events.\n // For Lights, info.online comes ONLY from LAN-direct replies (see\n // StateManager.syncInfoOnline). MQTT-push still updates power/brightness/\n // color but does NOT flip online for Lights. For Sensors/Appliances (which\n // never use this AWS-IoT MQTT path), the field stays as before.\n const state: Partial<DeviceState> = {};\n if (device.type !== \"devices.types.light\") {\n state.online = true;\n }\n\n if (update.state) {\n // API-Boundary: Govee schickt gelegentlich brightness/onOff/color als\n // String oder mit unerwarteten Typen. coerceFiniteNumber/coerceBool\n // returnt null bei Drift \u2192 Feld bleibt unver\u00E4ndert (vorhandener Wert\n // bleibt stehen, kein State-Schreibung mit kaputtem Wert).\n const onOff = coerceFiniteNumber(update.state.onOff);\n if (onOff !== null) {\n state.power = onOff === 1;\n }\n const brightness = coerceFiniteNumber(update.state.brightness);\n if (brightness !== null) {\n state.brightness = brightness;\n }\n if (update.state.color && typeof update.state.color === \"object\") {\n const r = coerceFiniteNumber((update.state.color as { r?: unknown }).r);\n const g = coerceFiniteNumber((update.state.color as { g?: unknown }).g);\n const b = coerceFiniteNumber((update.state.color as { b?: unknown }).b);\n if (r !== null && g !== null && b !== null) {\n state.colorRgb = rgbToHex(r, g, b);\n }\n }\n // 0 = \"not in colortemp mode\" \u2014 drop intentionally (Govee-Konvention).\n const ctk = coerceFiniteNumber(update.state.colorTemInKelvin);\n if (ctk !== null && ctk > 0) {\n state.colorTemperature = ctk;\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 // L6 \u2014 Plausibilit\u00E4ts-Cap: SEGMENT_HARD_MAX (55) ist die Govee-\n // Protokoll-Obergrenze. Werte dar\u00FCber kommen nur aus broken oder\n // spoofed Paketen \u2014 niemals persistieren.\n if (maxSeen > SEGMENT_HARD_MAX) {\n this.log.debug(`${device.name}: ignoring segmentCount=${maxSeen} (above protocol limit ${SEGMENT_HARD_MAX})`);\n return;\n }\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(cacheHelpers.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 device.lastLanReplyAt = 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 * 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 return findDeviceBySkuAndIdHelper(this.devices, sku, deviceId);\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 deviceKeyHelper(sku, 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}: ${errMessage(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 * Persist a device's current runtime state to the SKU cache. Safe no-op\n * when no cache is configured.\n *\n * @param device Target device\n */\n public persistDeviceToCache(device: GoveeDevice): void {\n cacheHelpers.persistDeviceToCache(this, device);\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: ${errMessage(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 // Process all entries in parallel \u2014 each entry only touches its own\n // device (no shared mutation), and the downstream callbacks (onCloud-\n // Capabilities \u2192 main.applyCloudCapabilities \u2192 setStateAsync queue)\n // are async-safe. Sequential `for` blocked the App-API tick on a slow\n // setStateAsync round-trip per device.\n // Wrap each per-entry block in `Promise.resolve` so the iterable is a\n // true Thenable \u2014 synchronous returns confuse `await Promise.all`'s\n // type-checker (await-thenable lint rule) even though the runtime would\n // accept them. No-op at runtime, makes the intent explicit and lints\n // without `require-await`.\n const results = await Promise.all(\n entries.map(entry =>\n Promise.resolve().then(() => {\n const device = this.devices.get(this.deviceKey(entry.sku, entry.device));\n if (!device) {\n return false;\n }\n const caps = buildCapabilitiesFromAppEntryHelper(entry);\n if (caps.length === 0) {\n return false;\n }\n this.onCloudCapabilities?.(device, caps);\n // mapSingleCapability returns null for the synthetic `online` cap\n // (online is a device-level property, not a regular state), so\n // onCloudCapabilities never reaches info.online via the capability\n // pipeline. Pluck it out and apply it directly \u2014 otherwise sensor\n // SKUs like H5179 stay at info.online=false forever even while\n // their readings keep updating.\n // Lights are excluded: their info.online is driven exclusively by\n // LAN-direct replies (StateManager.syncInfoOnline). Govee's Cloud\n // cache lags real LAN reachability by minutes and produced 2\u00D7\n // false-positive `true` writes during the 2026-05-13 outage capture.\n if (device.type !== \"devices.types.light\") {\n this.applyOnlineCap(device, caps);\n }\n this.diagnostics.setApiResponse(device.deviceId, \"/device/rest/devices/v1/list\", entry);\n return true;\n }),\n ),\n );\n return results.filter(Boolean).length;\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 cloudMergeHelpers.applyOnlineCap(this, device, caps);\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 /**\n * True wenn mindestens ein Device App-API-Werte konsumiert (Sensoren,\n * Appliances). Adapter-checkAllReady wartet darauf damit \u201Eready\" erst\n * geloggt wird wenn Sensor-Werte tats\u00E4chlich da sind.\n */\n public 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 // Lights are excluded (info.online comes only from LAN-direct replies via\n // StateManager.syncInfoOnline). Defensive \u2014 OpenAPI-MQTT in practice only\n // carries appliance events, but the guard prevents future regressions.\n if (device.type !== \"devices.types.light\") {\n this.applyOnlineCap(device, event.capabilities);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_lookups", "import_mapping", "cd", "_a", "findDeviceBySkuAndIdHelper", "deviceKeyHelper", "buildCapabilitiesFromAppEntryHelper"]
7
7
  }
@@ -48,6 +48,9 @@ function onDeviceStateUpdate(adapter, device, state) {
48
48
  connectionState.updateConnectionState(adapter);
49
49
  if (state.online !== void 0) {
50
50
  groupFanoutHandler.updateGroupReachability(adapter);
51
+ if (device.type === "devices.types.light" && adapter.stateManager) {
52
+ adapter.stateManager.syncInfoOnline(device).catch(() => void 0);
53
+ }
51
54
  }
52
55
  const powerOff = state.power === false || state.power === 0;
53
56
  if (powerOff && adapter.stateManager) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/handlers/device-events.ts"],
4
- "sourcesContent": ["import { buildCloudStateDefs } from \"../capability-mapper\";\nimport type { DeviceManager } from \"../device-manager\";\nimport { getDeviceTier } from \"../device-registry\";\nimport type { LocalSnapshotStore } from \"../local-snapshots\";\nimport type { StateManager } from \"../state-manager\";\nimport { errMessage, type DeviceState, type GoveeDevice } from \"../types\";\nimport * as connectionState from \"./connection-state\";\nimport * as groupFanoutHandler from \"./group-fanout-handler\";\nimport * as groupStateHelpers from \"./group-state-helpers\";\n\n/**\n * Adapter surface required by the device-event helpers \u2014 covers the\n * onDeviceStateUpdate + onDeviceListChanged + refreshDeviceStates path.\n *\n * Composes ConnectionStateAdapter (for updateConnectionState) plus\n * GroupFanoutHandlerAdapter and GroupStateHelpersAdapter via duck-typing\n * \u2014 the calling adapter implements all three sets implicitly.\n */\nexport interface DeviceEventsAdapter {\n readonly log: ioBroker.Logger;\n readonly namespace: string;\n readonly language?: ioBroker.Languages;\n readonly deviceManager: DeviceManager | null;\n readonly stateManager: StateManager | null;\n readonly localSnapshots: LocalSnapshotStore | null;\n readonly statesReady: boolean;\n readonly stateCreationQueue: Promise<void>[];\n /** Re-fired into stateManager + connection-state + groupFanout-reachability. */\n setStateAsync(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n /** Optional reapStaleDevices delegate \u2014 owned by main.ts because it touches diagnosticsLastRun. */\n reapStaleDevices?(): Promise<void>;\n}\n\n/**\n * Called by device-manager when a device's per-state values change. Mirrors\n * the updates into stateManager, refreshes the global connection-state\n * indicator, updates group reachability, and resets all mode dropdowns\n * when the device just powered off (the user shouldn't see \"playing\n * Aurora-A\" on a device that's off).\n *\n */\nexport function onDeviceStateUpdate<\n T extends DeviceEventsAdapter &\n connectionState.ConnectionStateAdapter &\n groupFanoutHandler.GroupFanoutHandlerAdapter &\n groupStateHelpers.GroupStateHelpersAdapter,\n>(adapter: T, device: GoveeDevice, state: Partial<DeviceState>): void {\n if (adapter.stateManager) {\n adapter.stateManager.updateDeviceState(device, state).catch(() => {});\n }\n connectionState.updateConnectionState(adapter);\n\n if (state.online !== undefined) {\n groupFanoutHandler.updateGroupReachability(adapter);\n }\n\n // Mirror power-off to mode-dropdown reset. Covers MQTT/LAN-initiated\n // power changes (Govee app or physical remote) so the UI stays honest:\n // a device that's off can't be \"playing Aurora-A\" anymore.\n // L11 \u2014 defensive auch 0 als false akzeptieren (Govee schickt Power\n // theoretisch als boolean, aber MQTT-Boundary k\u00F6nnte 0 durchschleusen).\n const powerOff = state.power === false || (state.power as unknown) === 0;\n if (powerOff && adapter.stateManager) {\n const prefix = adapter.stateManager.devicePrefix(device);\n groupStateHelpers.resetModeDropdowns(adapter, prefix, \"\").catch(() => undefined);\n }\n}\n\n/**\n * Internal \u2014 schedule a state-creation promise. Until adapter.statesReady,\n * promises accumulate in stateCreationQueue so onReady can await the full\n * initial batch. After ready, fire-and-forget.\n */\nfunction trackStateCreation(adapter: DeviceEventsAdapter, p: Promise<void>): void {\n if (!adapter.statesReady) {\n adapter.stateCreationQueue.push(p);\n } else {\n void p;\n }\n}\n\n/**\n * Phase 1 callback \u2014 LAN-Discovery has found a device. Creates info-channel\n * states (always-existing metadata) plus LAN-default control states (power,\n * brightness, colorRgb, colorTemperature).\n *\n * Does NOT create scenes/music/snapshots \u2014 those need Cloud data. If the\n * device later gets cloud capabilities, onCloudDataReady will fill them in\n * additively.\n *\n */\nexport function onLanDeviceReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n device: GoveeDevice,\n _allDevices: GoveeDevice[],\n): void {\n if (!adapter.stateManager) {\n return;\n }\n const sm = adapter.stateManager;\n const p = (async () => {\n await sm.createInfoStates(device);\n await sm.createLanStates(device);\n })().catch(e => {\n adapter.log.error(`onLanDeviceReady failed for ${device.name}: ${errMessage(e)}`);\n });\n trackStateCreation(adapter, p);\n connectionState.updateConnectionState(adapter);\n}\n\n/**\n * Phase 2 callback \u2014 Cloud-Data is available for a device (from cache-merge,\n * loadFromCloud success, refreshSceneDataForDevice, snapshot save/delete, or\n * wizard-apply). Creates the full state-tree: info + LAN + Cloud states.\n *\n * createInfoStates and createLanStates are idempotent \u2014 calling them again\n * after a LAN-phase has run only updates `info.online`/`info.ip` values.\n *\n */\nexport function onCloudDataReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n device: GoveeDevice,\n allDevices: GoveeDevice[],\n): void {\n if (!adapter.stateManager) {\n return;\n }\n const sm = adapter.stateManager;\n const localSnaps = adapter.localSnapshots?.getSnapshots(device.sku, device.deviceId);\n let memberDevices: GoveeDevice[] | undefined;\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n memberDevices = groupFanoutHandler.resolveGroupMembers(device, allDevices);\n }\n const cloudDefs = buildCloudStateDefs(device, adapter.log, localSnaps, memberDevices, adapter.language ?? \"en\");\n const capN = Array.isArray(device.capabilities) ? device.capabilities.length : 0;\n adapter.log.debug(\n `buildCloudStateDefs for ${device.sku} ${device.deviceId}: ${capN} cap(s) in \u2192 ${cloudDefs.length} state def(s) out`,\n );\n const p = (async () => {\n await sm.createInfoStates(device);\n await sm.createLanStates(device);\n await sm.createCloudStates(device, cloudDefs);\n await sm.migrateLegacyDiagnostics(device);\n await sm.updateDeviceTier(device, getDeviceTier(device.sku));\n })().catch(e => {\n adapter.log.error(`onCloudDataReady failed for ${device.name}: ${errMessage(e)}`);\n });\n trackStateCreation(adapter, p);\n connectionState.updateConnectionState(adapter);\n if (adapter.statesReady) {\n adapter.reapStaleDevices?.().catch(() => undefined);\n }\n}\n\n/**\n * Phase 3 callback \u2014 Group members have been resolved (loadGroupMembers\n * success). Rebuilds the BaseGroup state-tree with the intersection of\n * member device capabilities.\n *\n * Member devices fire their own onLanDeviceReady / onCloudDataReady\n * independently \u2014 this callback only handles the group itself.\n *\n */\nexport function onGroupMembersReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n group: GoveeDevice,\n allDevices: GoveeDevice[],\n): void {\n // BaseGroups go through the same Cloud-data path \u2014 group state-defs are\n // intersection of member capabilities, which is Cloud-derived.\n onCloudDataReady(adapter, group, allDevices);\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAAoC;AAEpC,6BAA8B;AAG9B,mBAA+D;AAC/D,sBAAiC;AACjC,yBAAoC;AACpC,wBAAmC;AAiC5B,SAAS,oBAKd,SAAY,QAAqB,OAAmC;AACpE,MAAI,QAAQ,cAAc;AACxB,YAAQ,aAAa,kBAAkB,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACtE;AACA,kBAAgB,sBAAsB,OAAO;AAE7C,MAAI,MAAM,WAAW,QAAW;AAC9B,uBAAmB,wBAAwB,OAAO;AAAA,EACpD;AAOA,QAAM,WAAW,MAAM,UAAU,SAAU,MAAM,UAAsB;AACvE,MAAI,YAAY,QAAQ,cAAc;AACpC,UAAM,SAAS,QAAQ,aAAa,aAAa,MAAM;AACvD,sBAAkB,mBAAmB,SAAS,QAAQ,EAAE,EAAE,MAAM,MAAM,MAAS;AAAA,EACjF;AACF;AAOA,SAAS,mBAAmB,SAA8B,GAAwB;AAChF,MAAI,CAAC,QAAQ,aAAa;AACxB,YAAQ,mBAAmB,KAAK,CAAC;AAAA,EACnC,OAAO;AACL,SAAK;AAAA,EACP;AACF;AAYO,SAAS,iBACd,SACA,QACA,aACM;AACN,MAAI,CAAC,QAAQ,cAAc;AACzB;AAAA,EACF;AACA,QAAM,KAAK,QAAQ;AACnB,QAAM,KAAK,YAAY;AACrB,UAAM,GAAG,iBAAiB,MAAM;AAChC,UAAM,GAAG,gBAAgB,MAAM;AAAA,EACjC,GAAG,EAAE,MAAM,OAAK;AACd,YAAQ,IAAI,MAAM,+BAA+B,OAAO,IAAI,SAAK,yBAAW,CAAC,CAAC,EAAE;AAAA,EAClF,CAAC;AACD,qBAAmB,SAAS,CAAC;AAC7B,kBAAgB,sBAAsB,OAAO;AAC/C;AAWO,SAAS,iBACd,SACA,QACA,YACM;AA3HR;AA4HE,MAAI,CAAC,QAAQ,cAAc;AACzB;AAAA,EACF;AACA,QAAM,KAAK,QAAQ;AACnB,QAAM,cAAa,aAAQ,mBAAR,mBAAwB,aAAa,OAAO,KAAK,OAAO;AAC3E,MAAI;AACJ,MAAI,OAAO,QAAQ,eAAe,OAAO,cAAc;AACrD,oBAAgB,mBAAmB,oBAAoB,QAAQ,UAAU;AAAA,EAC3E;AACA,QAAM,gBAAY,8CAAoB,QAAQ,QAAQ,KAAK,YAAY,gBAAe,aAAQ,aAAR,YAAoB,IAAI;AAC9G,QAAM,OAAO,MAAM,QAAQ,OAAO,YAAY,IAAI,OAAO,aAAa,SAAS;AAC/E,UAAQ,IAAI;AAAA,IACV,2BAA2B,OAAO,GAAG,IAAI,OAAO,QAAQ,KAAK,IAAI,qBAAgB,UAAU,MAAM;AAAA,EACnG;AACA,QAAM,KAAK,YAAY;AACrB,UAAM,GAAG,iBAAiB,MAAM;AAChC,UAAM,GAAG,gBAAgB,MAAM;AAC/B,UAAM,GAAG,kBAAkB,QAAQ,SAAS;AAC5C,UAAM,GAAG,yBAAyB,MAAM;AACxC,UAAM,GAAG,iBAAiB,YAAQ,sCAAc,OAAO,GAAG,CAAC;AAAA,EAC7D,GAAG,EAAE,MAAM,OAAK;AACd,YAAQ,IAAI,MAAM,+BAA+B,OAAO,IAAI,SAAK,yBAAW,CAAC,CAAC,EAAE;AAAA,EAClF,CAAC;AACD,qBAAmB,SAAS,CAAC;AAC7B,kBAAgB,sBAAsB,OAAO;AAC7C,MAAI,QAAQ,aAAa;AACvB,kBAAQ,qBAAR,iCAA6B,MAAM,MAAM;AAAA,EAC3C;AACF;AAWO,SAAS,oBACd,SACA,OACA,YACM;AAGN,mBAAiB,SAAS,OAAO,UAAU;AAC7C;",
4
+ "sourcesContent": ["import { buildCloudStateDefs } from \"../capability-mapper\";\nimport type { DeviceManager } from \"../device-manager\";\nimport { getDeviceTier } from \"../device-registry\";\nimport type { LocalSnapshotStore } from \"../local-snapshots\";\nimport type { StateManager } from \"../state-manager\";\nimport { errMessage, type DeviceState, type GoveeDevice } from \"../types\";\nimport * as connectionState from \"./connection-state\";\nimport * as groupFanoutHandler from \"./group-fanout-handler\";\nimport * as groupStateHelpers from \"./group-state-helpers\";\n\n/**\n * Adapter surface required by the device-event helpers \u2014 covers the\n * onDeviceStateUpdate + onDeviceListChanged + refreshDeviceStates path.\n *\n * Composes ConnectionStateAdapter (for updateConnectionState) plus\n * GroupFanoutHandlerAdapter and GroupStateHelpersAdapter via duck-typing\n * \u2014 the calling adapter implements all three sets implicitly.\n */\nexport interface DeviceEventsAdapter {\n readonly log: ioBroker.Logger;\n readonly namespace: string;\n readonly language?: ioBroker.Languages;\n readonly deviceManager: DeviceManager | null;\n readonly stateManager: StateManager | null;\n readonly localSnapshots: LocalSnapshotStore | null;\n readonly statesReady: boolean;\n readonly stateCreationQueue: Promise<void>[];\n /** Re-fired into stateManager + connection-state + groupFanout-reachability. */\n setStateAsync(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n /** Optional reapStaleDevices delegate \u2014 owned by main.ts because it touches diagnosticsLastRun. */\n reapStaleDevices?(): Promise<void>;\n}\n\n/**\n * Called by device-manager when a device's per-state values change. Mirrors\n * the updates into stateManager, refreshes the global connection-state\n * indicator, updates group reachability, and resets all mode dropdowns\n * when the device just powered off (the user shouldn't see \"playing\n * Aurora-A\" on a device that's off).\n *\n */\nexport function onDeviceStateUpdate<\n T extends DeviceEventsAdapter &\n connectionState.ConnectionStateAdapter &\n groupFanoutHandler.GroupFanoutHandlerAdapter &\n groupStateHelpers.GroupStateHelpersAdapter,\n>(adapter: T, device: GoveeDevice, state: Partial<DeviceState>): void {\n if (adapter.stateManager) {\n adapter.stateManager.updateDeviceState(device, state).catch(() => {});\n }\n connectionState.updateConnectionState(adapter);\n\n if (state.online !== undefined) {\n groupFanoutHandler.updateGroupReachability(adapter);\n // For Lights the updateDeviceState path no longer writes info.online \u2014\n // syncInfoOnline owns it. Trigger it here so a wasOffline \u2192 online\n // transition from handleLanDiscovery reflects in info.online within\n // milliseconds instead of waiting up to one sync-timer cycle (20 s).\n if (device.type === \"devices.types.light\" && adapter.stateManager) {\n adapter.stateManager.syncInfoOnline(device).catch(() => undefined);\n }\n }\n\n // Mirror power-off to mode-dropdown reset. Covers MQTT/LAN-initiated\n // power changes (Govee app or physical remote) so the UI stays honest:\n // a device that's off can't be \"playing Aurora-A\" anymore.\n // L11 \u2014 defensive auch 0 als false akzeptieren (Govee schickt Power\n // theoretisch als boolean, aber MQTT-Boundary k\u00F6nnte 0 durchschleusen).\n const powerOff = state.power === false || (state.power as unknown) === 0;\n if (powerOff && adapter.stateManager) {\n const prefix = adapter.stateManager.devicePrefix(device);\n groupStateHelpers.resetModeDropdowns(adapter, prefix, \"\").catch(() => undefined);\n }\n}\n\n/**\n * Internal \u2014 schedule a state-creation promise. Until adapter.statesReady,\n * promises accumulate in stateCreationQueue so onReady can await the full\n * initial batch. After ready, fire-and-forget.\n */\nfunction trackStateCreation(adapter: DeviceEventsAdapter, p: Promise<void>): void {\n if (!adapter.statesReady) {\n adapter.stateCreationQueue.push(p);\n } else {\n void p;\n }\n}\n\n/**\n * Phase 1 callback \u2014 LAN-Discovery has found a device. Creates info-channel\n * states (always-existing metadata) plus LAN-default control states (power,\n * brightness, colorRgb, colorTemperature).\n *\n * Does NOT create scenes/music/snapshots \u2014 those need Cloud data. If the\n * device later gets cloud capabilities, onCloudDataReady will fill them in\n * additively.\n *\n */\nexport function onLanDeviceReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n device: GoveeDevice,\n _allDevices: GoveeDevice[],\n): void {\n if (!adapter.stateManager) {\n return;\n }\n const sm = adapter.stateManager;\n const p = (async () => {\n await sm.createInfoStates(device);\n await sm.createLanStates(device);\n })().catch(e => {\n adapter.log.error(`onLanDeviceReady failed for ${device.name}: ${errMessage(e)}`);\n });\n trackStateCreation(adapter, p);\n connectionState.updateConnectionState(adapter);\n}\n\n/**\n * Phase 2 callback \u2014 Cloud-Data is available for a device (from cache-merge,\n * loadFromCloud success, refreshSceneDataForDevice, snapshot save/delete, or\n * wizard-apply). Creates the full state-tree: info + LAN + Cloud states.\n *\n * createInfoStates and createLanStates are idempotent \u2014 calling them again\n * after a LAN-phase has run only updates `info.online`/`info.ip` values.\n *\n */\nexport function onCloudDataReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n device: GoveeDevice,\n allDevices: GoveeDevice[],\n): void {\n if (!adapter.stateManager) {\n return;\n }\n const sm = adapter.stateManager;\n const localSnaps = adapter.localSnapshots?.getSnapshots(device.sku, device.deviceId);\n let memberDevices: GoveeDevice[] | undefined;\n if (device.sku === \"BaseGroup\" && device.groupMembers) {\n memberDevices = groupFanoutHandler.resolveGroupMembers(device, allDevices);\n }\n const cloudDefs = buildCloudStateDefs(device, adapter.log, localSnaps, memberDevices, adapter.language ?? \"en\");\n const capN = Array.isArray(device.capabilities) ? device.capabilities.length : 0;\n adapter.log.debug(\n `buildCloudStateDefs for ${device.sku} ${device.deviceId}: ${capN} cap(s) in \u2192 ${cloudDefs.length} state def(s) out`,\n );\n const p = (async () => {\n await sm.createInfoStates(device);\n await sm.createLanStates(device);\n await sm.createCloudStates(device, cloudDefs);\n await sm.migrateLegacyDiagnostics(device);\n await sm.updateDeviceTier(device, getDeviceTier(device.sku));\n })().catch(e => {\n adapter.log.error(`onCloudDataReady failed for ${device.name}: ${errMessage(e)}`);\n });\n trackStateCreation(adapter, p);\n connectionState.updateConnectionState(adapter);\n if (adapter.statesReady) {\n adapter.reapStaleDevices?.().catch(() => undefined);\n }\n}\n\n/**\n * Phase 3 callback \u2014 Group members have been resolved (loadGroupMembers\n * success). Rebuilds the BaseGroup state-tree with the intersection of\n * member device capabilities.\n *\n * Member devices fire their own onLanDeviceReady / onCloudDataReady\n * independently \u2014 this callback only handles the group itself.\n *\n */\nexport function onGroupMembersReady<T extends DeviceEventsAdapter & connectionState.ConnectionStateAdapter>(\n adapter: T,\n group: GoveeDevice,\n allDevices: GoveeDevice[],\n): void {\n // BaseGroups go through the same Cloud-data path \u2014 group state-defs are\n // intersection of member capabilities, which is Cloud-derived.\n onCloudDataReady(adapter, group, allDevices);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }