iobroker.govee-smart 2.8.1 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,15 +128,20 @@ 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.8.2 (2026-05-11)
132
+
133
+ - Snapshot refresh per device now also reloads the activation data — when you re-record a snapshot in the Govee app it takes effect right away, no manual cache reset.
134
+ - Three redundant messages around optional device features are gone from the debug log.
135
+ - Commands that don't reach the device on the local network now appear as a warning instead of failing silently.
136
+
131
137
  ### 2.8.1 (2026-05-11)
132
138
 
133
- - The channel-status prefix that 2.8.0 added now shows only on debug-level lines. Info, warn and error logs are back to their normal short form the prefix was meant as a diagnostic aid for debug, not as user-facing noise.
139
+ - 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.
134
140
 
135
141
  ### 2.8.0 (2026-05-11)
136
142
 
137
- - Restarting the adapter no longer briefly deletes and re-creates the scene, music and snapshot datapoints. The internal state-creation now runs in clear phases — LAN data is written from LAN discovery, Cloud data from the cloud refresh — and one phase no longer touches the other's territory.
138
- - Pure-LAN devices (no API key configured) no longer have empty scene/snapshot dropdowns lingering from earlier versions. A one-time cleanup on first start of 2.8.0 removes them.
139
- - Debug logs now carry the current channel status as a prefix and use structured key=value details, so a single log line tells you which channels were up at that moment and what the adapter actually saw.
143
+ - Restart no longer briefly removes and re-creates scene, music and snapshot datapoints.
144
+ - Lights without API key no longer have empty scene/snapshot dropdowns left over from earlier versions.
140
145
 
141
146
  ### 2.7.1 (2026-05-10)
142
147
 
@@ -149,10 +154,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
149
154
  - The refresh also re-fetches the Govee device list, so a brand-new snapshot from the Govee Home app is picked up even when Govee's cloud hasn't caught up to it yet.
150
155
  - `info.refresh_cloud_data` is removed in favour of the per-device button above. ioBroker scripts that wrote to it need to point at `devices.<id>.snapshots.refresh_cloud` instead.
151
156
 
152
- ### 2.6.7 (2026-05-10)
153
-
154
- - Cleaner ready-log: removed the device/sensor/group online-summary because it ran before the LAN scan had settled and could falsely show all lights as offline.
155
-
156
157
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
157
158
 
158
159
  ## Support
@@ -594,7 +594,7 @@ class DeviceManager {
594
594
  }
595
595
  });
596
596
  }
597
- if (!device.snapshotBleCmds && device.snapshots.length > 0) {
597
+ if ((force || !device.snapshotBleCmds) && device.snapshots.length > 0) {
598
598
  await runLimited(async () => {
599
599
  try {
600
600
  const snaps = await this.apiClient.fetchSnapshots(sku, device.deviceId);
@@ -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 for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n if (existing) {\n // Merge cached data into LAN-discovered device. Segment-specific\n // fields (segmentCount, manualMode, manualSegments) MUST be merged\n // too \u2014 LAN discovery runs before the cache load on every start, so\n // the existing-branch is the normal path. Missing these three meant\n // every restart threw away the wizard/MQTT-learned segment state and\n // fell back to Cloud's min-advertised count.\n existing.name = entry.name || existing.name;\n existing.type = entry.type || existing.type;\n existing.capabilities = entry.capabilities;\n existing.scenes = entry.scenes;\n existing.diyScenes = entry.diyScenes;\n existing.snapshots = entry.snapshots;\n existing.sceneLibrary = entry.sceneLibrary;\n existing.musicLibrary = entry.musicLibrary;\n existing.diyLibrary = entry.diyLibrary;\n existing.skuFeatures = entry.skuFeatures;\n existing.snapshotBleCmds = entry.snapshotBleCmds;\n existing.scenesChecked = entry.scenesChecked;\n existing.lastSeenOnNetwork = entry.lastSeenOnNetwork;\n existing.segmentCount = entry.segmentCount;\n existing.manualMode = entry.manualMode;\n existing.manualSegments = entry.manualSegments;\n existing.channels.cloud = entry.capabilities.length > 0;\n changed = true;\n } else {\n this.devices.set(key, cacheHelpers.cachedToGoveeDevice(entry));\n changed = true;\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // 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 if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n this.log.debug(`Scene library for ${sku}: ${lib.length} scenes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n this.log.debug(`Music library for ${sku}: ${lib.length} modes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n this.log.debug(`DIY library for ${sku}: ${lib.length} effects`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 }\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 if (!device.snapshotBleCmds && device.snapshots.length > 0) {\n await runLimited(async () => {\n try {\n const snaps = await this.apiClient!.fetchSnapshots(sku, device.deviceId);\n if (snaps.length > 0) {\n device.snapshotBleCmds = device.snapshots.map(ds => {\n const match = snaps.find(s => s.name === ds.name);\n return match?.bleCmds ?? [];\n });\n changed = true;\n this.log.debug(`Snapshot BLE for ${sku}: ${snaps.length} snapshots with local data`);\n }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${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 for (const entry of cached) {\n const key = this.deviceKey(entry.sku, entry.deviceId);\n const existing = this.devices.get(key);\n if (existing) {\n // Merge cached data into LAN-discovered device. Segment-specific\n // fields (segmentCount, manualMode, manualSegments) MUST be merged\n // too \u2014 LAN discovery runs before the cache load on every start, so\n // the existing-branch is the normal path. Missing these three meant\n // every restart threw away the wizard/MQTT-learned segment state and\n // fell back to Cloud's min-advertised count.\n existing.name = entry.name || existing.name;\n existing.type = entry.type || existing.type;\n existing.capabilities = entry.capabilities;\n existing.scenes = entry.scenes;\n existing.diyScenes = entry.diyScenes;\n existing.snapshots = entry.snapshots;\n existing.sceneLibrary = entry.sceneLibrary;\n existing.musicLibrary = entry.musicLibrary;\n existing.diyLibrary = entry.diyLibrary;\n existing.skuFeatures = entry.skuFeatures;\n existing.snapshotBleCmds = entry.snapshotBleCmds;\n existing.scenesChecked = entry.scenesChecked;\n existing.lastSeenOnNetwork = entry.lastSeenOnNetwork;\n existing.segmentCount = entry.segmentCount;\n existing.manualMode = entry.manualMode;\n existing.manualSegments = entry.manualSegments;\n existing.channels.cloud = entry.capabilities.length > 0;\n changed = true;\n } else {\n this.devices.set(key, cacheHelpers.cachedToGoveeDevice(entry));\n changed = true;\n }\n }\n\n if (changed) {\n this.log.info(`Loaded ${cached.length} device(s) from cache`);\n }\n\n // 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 if (lib.length > 0) {\n device.sceneLibrary = lib;\n changed = true;\n this.log.debug(`Scene library for ${sku}: ${lib.length} scenes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 if (lib.length > 0) {\n device.musicLibrary = lib;\n changed = true;\n this.log.debug(`Music library for ${sku}: ${lib.length} modes`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 if (lib.length > 0) {\n device.diyLibrary = lib;\n changed = true;\n this.log.debug(`DIY library for ${sku}: ${lib.length} effects`);\n }\n } catch (e) {\n this.diagnostics.recordApiFailure(device.deviceId, ep, e, this.extractStatus(e));\n this.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 }\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 if (snaps.length > 0) {\n device.snapshotBleCmds = device.snapshots.map(ds => {\n const match = snaps.find(s => s.name === ds.name);\n return match?.bleCmds ?? [];\n });\n changed = true;\n this.log.debug(`Snapshot BLE for ${sku}: ${snaps.length} snapshots with local data`);\n }\n } catch (e) {\n this.log.debug(`Could not load snapshot BLE for ${sku}: ${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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",
6
6
  "names": ["import_lookups", "import_mapping", "cd", "_a", "findDeviceBySkuAndIdHelper", "deviceKeyHelper", "buildCapabilitiesFromAppEntryHelper"]
7
7
  }
@@ -296,7 +296,9 @@ class GoveeLanClient {
296
296
  }
297
297
  this.sendSocket.send(buf, 0, buf.length, COMMAND_PORT, ip, (err) => {
298
298
  if (err) {
299
- this.log.debug(`LAN ptReal error to ${ip}: ${err.message}`);
299
+ this.log.warn(`LAN ptReal error to ${ip}: ${err.message}`);
300
+ } else {
301
+ this.log.debug(`LAN ptReal sent to ${ip}: ${base64Packets.length} packet(s), ${buf.length} bytes`);
300
302
  }
301
303
  });
302
304
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/govee-lan-client.ts"],
4
- "sourcesContent": ["import * as dgram from \"node:dgram\";\nimport type { LanDevice, LanMessage, LanStatus, TimerAdapter } from \"./types\";\n\nconst MULTICAST_ADDR = \"239.255.255.250\";\nconst SCAN_PORT = 4001;\nconst LISTEN_PORT = 4002;\nconst COMMAND_PORT = 4003;\n\n/** Callback for discovered LAN devices */\nexport type LanDiscoveryCallback = (device: LanDevice) => void;\n\n/** Callback for status updates (matched by source IP, not device ID) */\nexport type LanStatusCallback = (sourceIp: string, status: LanStatus) => void;\n\n/**\n * Govee LAN UDP client for device discovery and control.\n * Handles multicast discovery on port 4001, listens on 4002, sends commands to 4003.\n */\nexport class GoveeLanClient {\n private scanSocket: dgram.Socket | null = null;\n private listenSocket: dgram.Socket | null = null;\n /**\n * Persistent send-socket \u2014 vorher wurde pro Command ein neuer dgram-\n * Socket angelegt, gesendet, geschlossen. Beim Adapter-Stop mid-send\n * konnte der Callback in einen halb-zerlegten Adapter feuern.\n */\n private sendSocket: dgram.Socket | null = null;\n private scanTimer: ioBroker.Interval | undefined = undefined;\n /**\n * True after `stop()` was called \u2014 bind-callbacks check this flag before\n * starting timers/scans, so a `stop()` during the async listen+scan bind\n * sequence cannot leave a runaway scanTimer behind.\n */\n private stopped = false;\n /**\n * Pending one-shot timeouts created by {@link flashSingleSegment} \u2014 kept\n * so {@link stop} can cancel them before the deferred ptReal burst fires\n * into a torn-down LAN client.\n */\n private readonly pendingFlashTimers = new Set<ioBroker.Timeout>();\n private readonly timers: TimerAdapter;\n private readonly log: ioBroker.Logger;\n private onDiscovery: LanDiscoveryCallback | null = null;\n private onStatus: LanStatusCallback | null = null;\n private readonly seenDeviceIps = new Set<string>();\n /** Multicast-Membership-Adresse \u2014 gemerkt f\u00FCr dropMembership in stop(). */\n private multicastBind: string | undefined;\n\n /**\n * @param log ioBroker logger\n * @param timers Timer adapter for setInterval/setTimeout\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Start LAN discovery and listening for responses.\n *\n * @param onDiscovery Called when a new device is found\n * @param onStatus Called when a status response arrives\n * @param scanIntervalMs How often to send multicast scan (default 30s)\n * @param networkInterface IP of network interface to bind to (empty = all)\n */\n start(\n onDiscovery: LanDiscoveryCallback,\n onStatus: LanStatusCallback,\n scanIntervalMs = 30_000,\n networkInterface = \"\",\n ): void {\n this.onDiscovery = onDiscovery;\n this.onStatus = onStatus;\n\n const bindAddr = networkInterface && networkInterface !== \"0.0.0.0\" ? networkInterface : undefined;\n if (bindAddr) {\n this.log.info(`LAN binding to network interface ${bindAddr}`);\n }\n\n this.multicastBind = bindAddr;\n\n // Persistent Send-Socket f\u00FCr Commands \u2014 einmal anlegen, in stop() schliessen.\n this.sendSocket = dgram.createSocket(\"udp4\");\n this.sendSocket.on(\"error\", err => {\n this.log.debug(`LAN send socket error: ${err.message}`);\n });\n\n // Listen socket for responses (port 4002) \u2014 must be ready before first scan\n this.listenSocket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.listenSocket.on(\"message\", (msg, rinfo) => {\n this.handleMessage(msg, rinfo.address);\n });\n this.listenSocket.on(\"error\", err => {\n // EADDRINUSE = Port 4002 schon belegt (zweite Adapter-Instanz?). User\n // muss das wissen \u2014 Adapter w\u00E4re sonst halb-tot (Discovery via scan\n // geht, Status-Antworten verloren).\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EADDRINUSE\") {\n this.log.warn(`LAN listen port ${LISTEN_PORT} already in use \u2014 second instance? Status updates will be lost.`);\n } else {\n this.log.debug(`LAN listen socket error: ${err.message}`);\n }\n });\n this.listenSocket.bind(LISTEN_PORT, bindAddr, () => {\n // stop() may have been called between listenSocket.bind and this\n // callback firing \u2014 bail out so we don't spin up a scan socket and a\n // recurring timer behind the back of a torn-down LAN client.\n if (this.stopped) {\n return;\n }\n this.log.debug(`LAN listening on port ${LISTEN_PORT}`);\n\n // Scan socket for multicast discovery (port 4001) \u2014 started after listen is ready\n this.scanSocket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.scanSocket.on(\"error\", err => {\n this.log.debug(`LAN scan socket error: ${err.message}`);\n });\n // bind(0, bindAddr) \u2014 ephemeraler Port, aber an gew\u00E4hltes Interface\n // gebunden. Vorher: bind() ohne Adresse \u2192 ANY \u2192 asymmetrisch zum\n // Listen-Socket der das bindAddr nutzt.\n this.scanSocket.bind(0, bindAddr, () => {\n if (this.stopped) {\n return;\n }\n this.scanSocket?.setBroadcast(true);\n try {\n this.scanSocket?.addMembership(MULTICAST_ADDR, bindAddr);\n } catch {\n // Membership-fail typischerweise OS-Routing-Issue (z.B. interface\n // bindAddr nicht in der multicast-routing-table). LAN-Discovery\n // bleibt trotzdem partial-funktional \u00FCber setBroadcast(true), aber\n // das ist asymmetrisch \u2014 Antworten kommen evtl. nicht zur\u00FCck.\n // Auf info-level loggen so der User die Ursache der \"no devices\"-\n // Symptomatik in den Logs findet.\n this.log.info(\n `LAN: could not join multicast group on ${bindAddr ?? \"default interface\"} \u2014 discovery may be incomplete`,\n );\n }\n this.sendScan();\n });\n\n // Periodic scan \u2014 guard the timer creation in case stop() raced with\n // the bind callback, otherwise we'd leave a setInterval running\n // against a torn-down LAN client.\n if (!this.stopped) {\n this.scanTimer = this.timers.setInterval(() => {\n this.sendScan();\n }, scanIntervalMs);\n }\n });\n }\n\n /** Stop all sockets and timers */\n stop(): void {\n this.stopped = true;\n if (this.scanTimer) {\n this.timers.clearInterval(this.scanTimer);\n this.scanTimer = undefined;\n }\n // Cancel any deferred wizard-flash bursts that haven't fired yet.\n for (const handle of this.pendingFlashTimers) {\n this.timers.clearTimeout(handle);\n }\n this.pendingFlashTimers.clear();\n if (this.scanSocket) {\n // dropMembership symmetrisch zu addMembership \u2014 auf macOS/Windows kann\n // der Multicast-Filter sonst auf der NIC bis Process-Exit h\u00E4ngenbleiben.\n try {\n if (this.multicastBind) {\n this.scanSocket.dropMembership(MULTICAST_ADDR, this.multicastBind);\n }\n } catch {\n /* ignore \u2014 best-effort */\n }\n try {\n this.scanSocket.close();\n } catch {\n /* ignore */\n }\n this.scanSocket = null;\n }\n if (this.listenSocket) {\n try {\n this.listenSocket.close();\n } catch {\n /* ignore */\n }\n this.listenSocket = null;\n }\n if (this.sendSocket) {\n try {\n this.sendSocket.close();\n } catch {\n /* ignore */\n }\n this.sendSocket = null;\n }\n // L4 \u2014 seenDeviceIps clear, sonst \u00FCberleben Eintr\u00E4ge zwischen\n // start/stop-Zyklen im selben Process (korrektheit nicht beeinflusst,\n // aber discovery-log-info bei IP-Wechsel w\u00FCrde verloren gehen).\n this.seenDeviceIps.clear();\n this.multicastBind = undefined;\n }\n\n /**\n * Send a control command to a device via LAN.\n *\n * @param ip Device IP address\n * @param cmd Command name (turn, brightness, colorwc, devStatus)\n * @param data Command data\n */\n private sendCommand(ip: string, cmd: string, data: Record<string, unknown>): void {\n if (!this.sendSocket) {\n this.log.debug(`LAN send dropped (socket not ready): ${cmd} \u2192 ${ip}`);\n return;\n }\n const message: LanMessage = {\n msg: { cmd, data },\n };\n const buf = Buffer.from(JSON.stringify(message));\n // L5 \u2014 Govee-Spec ist UDP-PMTU-bound. Bei sehr grossen ptReal-Payloads\n // (viele BLE-Pakete in JSON) warnen damit der User wei\u00DF warum etwas\n // verloren geht.\n if (buf.length > 1400) {\n this.log.debug(`LAN payload large (${buf.length} bytes) \u2014 may be PMTU-fragmented for ${ip}`);\n }\n this.sendSocket.send(buf, 0, buf.length, COMMAND_PORT, ip, err => {\n if (err) {\n this.log.debug(`LAN send error to ${ip}: ${err.message}`);\n }\n });\n }\n\n /**\n * Send power command\n *\n * @param ip Device IP address\n * @param on Power state\n */\n setPower(ip: string, on: boolean): void {\n this.sendCommand(ip, \"turn\", { value: on ? 1 : 0 });\n }\n\n /**\n * Send brightness command\n *\n * @param ip Device IP address\n * @param brightness Brightness 0-100\n */\n setBrightness(ip: string, brightness: number): void {\n this.sendCommand(ip, \"brightness\", {\n value: clampByte0_100(brightness),\n });\n }\n\n /**\n * Send color command. Inputs are clamped to 0-255 \u2014 out-of-range values\n * from upstream coercion paths (capability-mapper, command-router) would\n * otherwise be sent verbatim and produce undefined-behaviour at the device.\n *\n * @param ip Device IP address\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\n setColor(ip: string, r: number, g: number, b: number): void {\n this.sendCommand(ip, \"colorwc\", {\n color: { r: clampByte(r), g: clampByte(g), b: clampByte(b) },\n colorTemInKelvin: 0,\n });\n }\n\n /**\n * Send color temperature command. Out-of-band kelvin values are clamped\n * to Govee's published 2000-9000 K range (per-device range may be tighter,\n * those are corrected via {@link applyColorTempQuirk} upstream).\n *\n * @param ip Device IP address\n * @param kelvin Color temperature in Kelvin\n */\n setColorTemperature(ip: string, kelvin: number): void {\n const clamped = Number.isFinite(kelvin) ? Math.max(2000, Math.min(9000, Math.round(kelvin))) : 2000;\n this.sendCommand(ip, \"colorwc\", {\n color: { r: 0, g: 0, b: 0 },\n colorTemInKelvin: clamped,\n });\n }\n\n /**\n * Send a scene via ptReal BLE-passthrough.\n * Builds multi-packet BLE data from scenceParam + final scene-code packet.\n *\n * @param ip Device IP address\n * @param sceneCode Scene code from scene library (must be > 0)\n * @param scenceParam Base64-encoded scene parameter data (may be empty for simple presets)\n */\n setScene(ip: string, sceneCode: number, scenceParam: string): void {\n if (sceneCode <= 0) {\n return;\n }\n const packets = buildScenePackets(sceneCode, scenceParam);\n this.sendPtReal(ip, packets);\n }\n\n /**\n * Send raw ptReal BLE-passthrough packets to a device.\n *\n * @param ip Device IP address\n * @param base64Packets Array of Base64-encoded 20-byte BLE packets\n */\n sendPtReal(ip: string, base64Packets: string[]): void {\n if (!this.sendSocket) {\n this.log.debug(`LAN ptReal dropped (socket not ready): ${ip}`);\n return;\n }\n const message = {\n msg: { cmd: \"ptReal\", data: { command: base64Packets } },\n };\n const buf = Buffer.from(JSON.stringify(message));\n if (buf.length > 1400) {\n this.log.debug(`ptReal payload large (${buf.length} bytes) \u2014 may be PMTU-fragmented for ${ip}`);\n }\n this.sendSocket.send(buf, 0, buf.length, COMMAND_PORT, ip, err => {\n if (err) {\n this.log.debug(`LAN ptReal error to ${ip}: ${err.message}`);\n }\n });\n }\n\n /**\n * Set gradient toggle via ptReal BLE-passthrough.\n *\n * @param ip Device IP address\n * @param on Gradient on/off\n */\n setGradient(ip: string, on: boolean): void {\n this.sendPtReal(ip, [buildGradientPacket(on)]);\n }\n\n /**\n * Activate a DIY scene via ptReal BLE-passthrough.\n * Sends A1 multi-packet data (if provided) + activation command.\n *\n * @param ip Device IP address\n * @param scenceParam Base64-encoded DIY parameter data (may be empty to activate last DIY)\n */\n setDiyScene(ip: string, scenceParam: string): void {\n const packets = buildDiyPackets(scenceParam);\n this.sendPtReal(ip, packets);\n }\n\n /**\n * Set music mode via ptReal BLE-passthrough.\n * Sub-modes 1 (Spectrum) and 2 (Rolling) use RGB color.\n *\n * @param ip Device IP address\n * @param subMode Music sub-mode (0-3)\n * @param r Red channel 0-255 (used by modes 1, 2)\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\n setMusicMode(ip: string, subMode: number, r = 0, g = 0, b = 0): void {\n this.sendPtReal(ip, [buildMusicModePacket(subMode, r, g, b)]);\n }\n\n /**\n * Set segment color via ptReal BLE-passthrough (command 33 05 15 01).\n *\n * @param ip Device IP address\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param segments Array of 0-based segment indices\n */\n setSegmentColor(ip: string, r: number, g: number, b: number, segments: number[]): void {\n this.sendPtReal(ip, [buildSegmentColorPacket(r, g, b, segments)]);\n }\n\n /**\n * Set segment brightness via ptReal BLE-passthrough (command 33 05 15 02).\n *\n * @param ip Device IP address\n * @param brightness Brightness 0-100\n * @param segments Array of 0-based segment indices\n */\n setSegmentBrightness(ip: string, brightness: number, segments: number[]): void {\n this.sendPtReal(ip, [buildSegmentBrightnessPacket(brightness, segments)]);\n }\n\n /**\n * Flash a single segment bright white and dim all other segments, in ONE\n * atomic ptReal transmission. All three required BLE packets are bundled\n * into a single UDP datagram so the device cannot drop intermediate steps.\n *\n * The \"dim everything else\" packet targets the full bitmask width (56\n * segments \u2014 the Govee protocol's upper bound: 7 bytes \u00D7 8 bits). This\n * covers under-report cases where the Cloud says \"15 segments\" but the\n * strip physically has more. Without this the unreported segments keep\n * shining at whatever brightness they had before the wizard started.\n *\n * Packet order:\n * 0. `colorwc` \u2014 force static-color mode (segment_color_setting packets\n * are ignored while the device is in Scene/Gradient/Music mode)\n * 1. All segments except idx (up to idx 55) \u2192 brightness 0\n * 2. Target segment \u2192 color 0xFFFFFF (full white)\n * 3. Target segment \u2192 brightness 100 (make it bright)\n *\n * @param ip Device IP address\n * @param idx Target segment index (0-based) to flash white\n */\n flashSingleSegment(ip: string, idx: number): void {\n if (idx < 0 || idx >= 56) {\n return;\n }\n const MAX_SEGMENTS = 56;\n const others = Array.from({ length: MAX_SEGMENTS }, (_, i) => i).filter(i => i !== idx);\n // Step 0: force color mode. Without this, the strip stays in whatever\n // mode it was (Scene/Gradient/Music) and silently ignores the three\n // ptReal packets below. The colorwc command resets to a known static\n // state that accepts segment-level overrides.\n this.setColor(ip, 0xff, 0xff, 0xff);\n // Small delay so the firmware can apply the mode switch before the\n // next UDP burst \u2014 Govee's observed minimum is ~50 ms. The handle is\n // tracked so stop() can cancel it explicitly: ioBroker timers eventually\n // fire into a torn-down adapter otherwise (the wrapper-tracked handles\n // get cleared in onUnload, but only via this Set).\n const delayMs = 150;\n const handle = this.timers.setTimeout(() => {\n if (handle !== undefined) {\n this.pendingFlashTimers.delete(handle);\n }\n if (this.stopped || !this.sendSocket) {\n return;\n }\n this.sendPtReal(ip, [\n buildSegmentBrightnessPacket(0, others),\n buildSegmentColorPacket(0xff, 0xff, 0xff, [idx]),\n buildSegmentBrightnessPacket(100, [idx]),\n ]);\n }, delayMs);\n if (handle !== undefined) {\n this.pendingFlashTimers.add(handle);\n }\n }\n\n /**\n * Restore a segment strip to a uniform color + brightness in one atomic\n * ptReal transmission. Used at wizard end/abort to put the strip back to\n * the captured baseline.\n *\n * @param ip Device IP address\n * @param total Total number of segments\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param brightness Brightness 0-100\n */\n restoreAllSegments(ip: string, total: number, r: number, g: number, b: number, brightness: number): void {\n if (total <= 0) {\n return;\n }\n const all = Array.from({ length: total }, (_, i) => i);\n this.sendPtReal(ip, [buildSegmentColorPacket(r, g, b, all), buildSegmentBrightnessPacket(brightness, all)]);\n }\n\n /**\n * Request device status\n *\n * @param ip Device IP address\n */\n requestStatus(ip: string): void {\n this.sendCommand(ip, \"devStatus\", {});\n }\n\n /** Send multicast scan */\n private sendScan(): void {\n const scanMsg: LanMessage = {\n msg: { cmd: \"scan\", data: { account_topic: \"reserve\" } },\n };\n const buf = Buffer.from(JSON.stringify(scanMsg));\n this.scanSocket?.send(buf, 0, buf.length, SCAN_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.log.debug(`LAN scan send error: ${err.message}`);\n }\n });\n }\n\n /**\n * Parse incoming UDP message\n *\n * @param msg Raw UDP message buffer\n * @param sourceIp Source IP address from UDP rinfo\n */\n private handleMessage(msg: Buffer, sourceIp: string): void {\n // L9 \u2014 Size-Bound. Pathologische Pakete (mehrere reassembled UDP-Frames)\n // k\u00F6nnten 64KB+ haben. JSON.parse von 64KB+ blockiert den Event-Loop\n // f\u00FCr ms-Spannen \u2014 bei vielen Devices gleichzeitig sp\u00FCrbar.\n if (msg.length > 8192) {\n this.log.debug(`LAN message dropped from ${sourceIp}: oversize ${msg.length} bytes`);\n return;\n }\n try {\n const data = JSON.parse(msg.toString()) as {\n msg?: { cmd?: string; data?: Record<string, unknown> };\n };\n if (!data.msg?.cmd || typeof data.msg.cmd !== \"string\") {\n return;\n }\n\n const cmd: string = data.msg.cmd;\n const rawPayload = data.msg.data;\n const payload: Record<string, unknown> =\n rawPayload && typeof rawPayload === \"object\" && !Array.isArray(rawPayload) ? rawPayload : {};\n\n if (cmd === \"scan\") {\n this.handleScanResponse(payload);\n } else if (cmd === \"devStatus\") {\n this.handleStatusResponse(payload, sourceIp);\n }\n } catch {\n this.log.debug(`LAN: Failed to parse message: ${msg.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Handle scan response \u2014 new device found\n *\n * @param data Parsed scan response payload\n */\n private handleScanResponse(data: Record<string, unknown>): void {\n // Defensive type checks \u2014 LAN payload comes over the wire, treat as untrusted\n if (\n typeof data.ip !== \"string\" ||\n typeof data.device !== \"string\" ||\n typeof data.sku !== \"string\" ||\n !data.ip ||\n !data.device ||\n !data.sku\n ) {\n return;\n }\n\n const lanDevice: LanDevice = {\n ip: data.ip,\n device: data.device,\n sku: data.sku,\n };\n\n const key = `${lanDevice.device}:${lanDevice.ip}`;\n if (!this.seenDeviceIps.has(key)) {\n // Evict any stale entries for the same device at different IPs so the\n // set stays bounded by the actual number of devices, not the full\n // history of IPs they ever had.\n const staleSuffix = `${lanDevice.device}:`;\n for (const existing of this.seenDeviceIps) {\n if (existing.startsWith(staleSuffix) && existing !== key) {\n this.seenDeviceIps.delete(existing);\n }\n }\n this.seenDeviceIps.add(key);\n this.log.debug(`LAN: Found ${lanDevice.sku} (${lanDevice.device}) at ${lanDevice.ip}`);\n }\n this.onDiscovery?.(lanDevice);\n }\n\n /**\n * Handle status response \u2014 matched to device by source IP.\n * Defensive against malformed/partial payloads \u2014 all fields coerced to safe defaults.\n *\n * @param data Parsed status response payload\n * @param sourceIp Source IP address from UDP message\n */\n private handleStatusResponse(data: Record<string, unknown>, sourceIp: string): void {\n const toNum = (v: unknown): number => (typeof v === \"number\" && Number.isFinite(v) ? v : 0);\n const colorRaw = data.color;\n const color =\n colorRaw && typeof colorRaw === \"object\"\n ? {\n r: toNum((colorRaw as Record<string, unknown>).r),\n g: toNum((colorRaw as Record<string, unknown>).g),\n b: toNum((colorRaw as Record<string, unknown>).b),\n }\n : { r: 0, g: 0, b: 0 };\n\n const status: LanStatus = {\n onOff: toNum(data.onOff),\n brightness: toNum(data.brightness),\n color,\n colorTemInKelvin: toNum(data.colorTemInKelvin),\n };\n\n this.onStatus?.(sourceIp, status);\n }\n}\n\n// --- BLE Packet Builder for ptReal ---\n\n/**\n * Clamp a value to 0-255. NaN / non-numeric \u2192 0. Centralised so every LAN\n * command goes through the same bounds-check.\n *\n * @param v Input value\n */\nfunction clampByte(v: number): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) {\n return 0;\n }\n return Math.max(0, Math.min(255, Math.round(v)));\n}\n\n/**\n * Clamp a value to 0-100. NaN / non-numeric \u2192 0.\n *\n * @param v Input value\n */\nfunction clampByte0_100(v: number): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) {\n return 0;\n }\n return Math.max(0, Math.min(100, Math.round(v)));\n}\n\n/**\n * XOR checksum over all bytes\n *\n * @param data Array of byte values\n */\nfunction xorChecksum(data: number[]): number {\n let checksum = 0;\n for (const b of data) {\n checksum ^= b;\n }\n return checksum;\n}\n\n/**\n * Pad data to 19 bytes + append XOR checksum = 20-byte BLE packet\n *\n * @param data Array of byte values to pad and checksum\n */\nfunction finishPacket(data: number[]): number[] {\n while (data.length < 19) {\n data.push(0);\n }\n data.push(xorChecksum(data));\n return data;\n}\n\n/**\n * Build Base64-encoded BLE packets for scene activation via ptReal.\n *\n * @param sceneCode Scene code from library (> 0)\n * @param scenceParam Base64-encoded scene parameter data (may be empty)\n */\nexport function buildScenePackets(sceneCode: number, scenceParam: string): string[] {\n const packets: string[] = [];\n\n // Multi-packet scene data from scenceParam (A3 header protocol)\n if (scenceParam) {\n const paramBytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n // Build A3-framed packets: first chunk starts with A3 00 01 00 02\n const rawData: number[] = [0xa3, 0x00, 0x01, 0x00, 0x02];\n let numLines = 0;\n let lastLineMarker = 1;\n\n for (const b of paramBytes) {\n if (rawData.length % 19 === 0) {\n numLines++;\n rawData.push(0xa3);\n lastLineMarker = rawData.length;\n rawData.push(numLines);\n }\n rawData.push(b);\n }\n rawData[lastLineMarker] = 0xff;\n rawData[3] = numLines + 1;\n\n // Split into 19-byte chunks, pad + checksum each\n for (let i = 0; i < rawData.length; i += 19) {\n const chunk = rawData.slice(i, i + 19);\n const pkt = finishPacket([...chunk]);\n packets.push(Buffer.from(pkt).toString(\"base64\"));\n }\n }\n\n // Final scene-code activation packet: 33 05 04 lo hi\n const lo = sceneCode & 0xff;\n const hi = (sceneCode >> 8) & 0xff;\n const activatePacket = finishPacket([0x33, 0x05, 0x04, lo, hi]);\n packets.push(Buffer.from(activatePacket).toString(\"base64\"));\n\n return packets;\n}\n\n/**\n * Build Base64-encoded BLE packets for DIY scene activation via ptReal.\n * Uses A1 framing for multi-packet data, then sends activation command.\n *\n * @param scenceParam Base64-encoded DIY parameter data (may be empty)\n */\nexport function buildDiyPackets(scenceParam: string): string[] {\n const packets: string[] = [];\n\n if (scenceParam) {\n const paramBytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n // A1-framed packets: start A1 02 00 <total>\n const rawData: number[] = [0xa1, 0x02, 0x00, 0x00];\n let numLines = 0;\n let lastLineMarker = 2;\n\n for (const b of paramBytes) {\n if (rawData.length % 19 === 0) {\n numLines++;\n rawData.push(0xa1, 0x02);\n lastLineMarker = rawData.length - 1;\n rawData.push(numLines);\n }\n rawData.push(b);\n }\n rawData[lastLineMarker] = 0xff;\n rawData[3] = numLines + 1;\n\n for (let i = 0; i < rawData.length; i += 19) {\n const chunk = rawData.slice(i, i + 19);\n packets.push(Buffer.from(finishPacket([...chunk])).toString(\"base64\"));\n }\n }\n\n // Activation: 33 05 0A\n packets.push(Buffer.from(finishPacket([0x33, 0x05, 0x0a])).toString(\"base64\"));\n return packets;\n}\n\n/**\n * Build a Base64-encoded BLE packet for gradient toggle via ptReal.\n *\n * @param on Gradient on/off\n */\nexport function buildGradientPacket(on: boolean): string {\n return Buffer.from(finishPacket([0x33, 0x14, on ? 0x01 : 0x00])).toString(\"base64\");\n}\n\n/**\n * Build a Base64-encoded BLE packet for music mode via ptReal.\n * Sub-modes 1 (Spectrum) and 2 (Rolling) include RGB color.\n *\n * @param subMode Music sub-mode (0=Energic, 1=Spectrum, 2=Rolling, 3=Rhythm)\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\nexport function buildMusicModePacket(subMode: number, r = 0, g = 0, b = 0): string {\n const data = [0x33, 0x05, 0x01, subMode & 0xff];\n if (subMode === 1 || subMode === 2) {\n data.push(r & 0xff, g & 0xff, b & 0xff);\n }\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Build a little-endian segment bitmask.\n * Segment 0 = byte[0] bit 0, Segment 8 = byte[1] bit 0, etc.\n *\n * @param segments Array of 0-based segment indices\n * @param byteCount Number of bitmask bytes (7 for color, 14 for brightness)\n */\nexport function buildSegmentBitmask(segments: number[], byteCount: number): number[] {\n const mask = new Array<number>(byteCount).fill(0);\n for (const seg of segments) {\n const byteIdx = Math.floor(seg / 8);\n const bitIdx = seg % 8;\n if (byteIdx < byteCount) {\n mask[byteIdx] |= 1 << bitIdx;\n }\n }\n return mask;\n}\n\n/**\n * Build a Base64-encoded BLE packet for segment color via ptReal.\n * Command: 33 05 15 01 RR GG BB 00\u00D75 bitmask\u00D77\n *\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param segments Array of 0-based segment indices\n */\nexport function buildSegmentColorPacket(r: number, g: number, b: number, segments: number[]): string {\n const data = [\n 0x33,\n 0x05,\n 0x15,\n 0x01,\n r & 0xff,\n g & 0xff,\n b & 0xff,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n ...buildSegmentBitmask(segments, 7),\n ];\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Build a Base64-encoded BLE packet for segment brightness via ptReal.\n * Command: 33 05 15 02 BB bitmask\u00D714\n *\n * @param brightness Brightness 0-100\n * @param segments Array of 0-based segment indices\n */\nexport function buildSegmentBrightnessPacket(brightness: number, segments: number[]): string {\n const data = [0x33, 0x05, 0x15, 0x02, Math.max(0, Math.min(100, brightness)), ...buildSegmentBitmask(segments, 14)];\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Apply speed level to a scene's scenceParam by replacing speed bytes in each page.\n * scenceParam structure: byte[0] = page count, then per page: 1 byte length + N bytes data.\n * Speed byte position within each page: pageLength - 5.\n *\n * @param scenceParam Base64-encoded scene parameter data\n * @param speedLevel Speed level index (0-based)\n * @param speedConfig JSON config string from speedInfo.config\n * @returns Modified Base64-encoded scenceParam with speed bytes replaced\n */\nexport function applySceneSpeed(scenceParam: string, speedLevel: number, speedConfig: string): string {\n if (!scenceParam || !speedConfig) {\n return scenceParam;\n }\n\n let configEntries: Array<{\n page: number;\n moveIn?: number[];\n }>;\n try {\n configEntries = JSON.parse(speedConfig);\n } catch {\n // Govee's speedInfo.config schema can drift \u2014 surface the failure on\n // debug so a \"scene speed has no effect\" complaint is traceable.\n // Returning the un-modified scenceParam keeps the feature partially\n // working (default speed) instead of failing the activation.\n return scenceParam;\n }\n\n if (!Array.isArray(configEntries) || configEntries.length === 0) {\n return scenceParam;\n }\n\n const bytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n if (bytes.length === 0) {\n return scenceParam;\n }\n\n const pageCount = bytes[0];\n let offset = 1;\n\n for (let pageIdx = 0; pageIdx < pageCount && offset < bytes.length; pageIdx++) {\n const pageLen = bytes[offset];\n if (offset + 1 + pageLen > bytes.length) {\n break;\n }\n\n const cfg = configEntries.find(c => c.page === pageIdx);\n if (cfg?.moveIn && speedLevel >= 0 && speedLevel < cfg.moveIn.length) {\n const speedBytePos = offset + 1 + (pageLen - 5);\n if (speedBytePos > offset && speedBytePos < offset + 1 + pageLen) {\n bytes[speedBytePos] = cfg.moveIn[speedLevel];\n }\n }\n\n offset += 1 + pageLen;\n }\n\n return Buffer.from(bytes).toString(\"base64\");\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as dgram from \"node:dgram\";\nimport type { LanDevice, LanMessage, LanStatus, TimerAdapter } from \"./types\";\n\nconst MULTICAST_ADDR = \"239.255.255.250\";\nconst SCAN_PORT = 4001;\nconst LISTEN_PORT = 4002;\nconst COMMAND_PORT = 4003;\n\n/** Callback for discovered LAN devices */\nexport type LanDiscoveryCallback = (device: LanDevice) => void;\n\n/** Callback for status updates (matched by source IP, not device ID) */\nexport type LanStatusCallback = (sourceIp: string, status: LanStatus) => void;\n\n/**\n * Govee LAN UDP client for device discovery and control.\n * Handles multicast discovery on port 4001, listens on 4002, sends commands to 4003.\n */\nexport class GoveeLanClient {\n private scanSocket: dgram.Socket | null = null;\n private listenSocket: dgram.Socket | null = null;\n /**\n * Persistent send-socket \u2014 vorher wurde pro Command ein neuer dgram-\n * Socket angelegt, gesendet, geschlossen. Beim Adapter-Stop mid-send\n * konnte der Callback in einen halb-zerlegten Adapter feuern.\n */\n private sendSocket: dgram.Socket | null = null;\n private scanTimer: ioBroker.Interval | undefined = undefined;\n /**\n * True after `stop()` was called \u2014 bind-callbacks check this flag before\n * starting timers/scans, so a `stop()` during the async listen+scan bind\n * sequence cannot leave a runaway scanTimer behind.\n */\n private stopped = false;\n /**\n * Pending one-shot timeouts created by {@link flashSingleSegment} \u2014 kept\n * so {@link stop} can cancel them before the deferred ptReal burst fires\n * into a torn-down LAN client.\n */\n private readonly pendingFlashTimers = new Set<ioBroker.Timeout>();\n private readonly timers: TimerAdapter;\n private readonly log: ioBroker.Logger;\n private onDiscovery: LanDiscoveryCallback | null = null;\n private onStatus: LanStatusCallback | null = null;\n private readonly seenDeviceIps = new Set<string>();\n /** Multicast-Membership-Adresse \u2014 gemerkt f\u00FCr dropMembership in stop(). */\n private multicastBind: string | undefined;\n\n /**\n * @param log ioBroker logger\n * @param timers Timer adapter for setInterval/setTimeout\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Start LAN discovery and listening for responses.\n *\n * @param onDiscovery Called when a new device is found\n * @param onStatus Called when a status response arrives\n * @param scanIntervalMs How often to send multicast scan (default 30s)\n * @param networkInterface IP of network interface to bind to (empty = all)\n */\n start(\n onDiscovery: LanDiscoveryCallback,\n onStatus: LanStatusCallback,\n scanIntervalMs = 30_000,\n networkInterface = \"\",\n ): void {\n this.onDiscovery = onDiscovery;\n this.onStatus = onStatus;\n\n const bindAddr = networkInterface && networkInterface !== \"0.0.0.0\" ? networkInterface : undefined;\n if (bindAddr) {\n this.log.info(`LAN binding to network interface ${bindAddr}`);\n }\n\n this.multicastBind = bindAddr;\n\n // Persistent Send-Socket f\u00FCr Commands \u2014 einmal anlegen, in stop() schliessen.\n this.sendSocket = dgram.createSocket(\"udp4\");\n this.sendSocket.on(\"error\", err => {\n this.log.debug(`LAN send socket error: ${err.message}`);\n });\n\n // Listen socket for responses (port 4002) \u2014 must be ready before first scan\n this.listenSocket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.listenSocket.on(\"message\", (msg, rinfo) => {\n this.handleMessage(msg, rinfo.address);\n });\n this.listenSocket.on(\"error\", err => {\n // EADDRINUSE = Port 4002 schon belegt (zweite Adapter-Instanz?). User\n // muss das wissen \u2014 Adapter w\u00E4re sonst halb-tot (Discovery via scan\n // geht, Status-Antworten verloren).\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EADDRINUSE\") {\n this.log.warn(`LAN listen port ${LISTEN_PORT} already in use \u2014 second instance? Status updates will be lost.`);\n } else {\n this.log.debug(`LAN listen socket error: ${err.message}`);\n }\n });\n this.listenSocket.bind(LISTEN_PORT, bindAddr, () => {\n // stop() may have been called between listenSocket.bind and this\n // callback firing \u2014 bail out so we don't spin up a scan socket and a\n // recurring timer behind the back of a torn-down LAN client.\n if (this.stopped) {\n return;\n }\n this.log.debug(`LAN listening on port ${LISTEN_PORT}`);\n\n // Scan socket for multicast discovery (port 4001) \u2014 started after listen is ready\n this.scanSocket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.scanSocket.on(\"error\", err => {\n this.log.debug(`LAN scan socket error: ${err.message}`);\n });\n // bind(0, bindAddr) \u2014 ephemeraler Port, aber an gew\u00E4hltes Interface\n // gebunden. Vorher: bind() ohne Adresse \u2192 ANY \u2192 asymmetrisch zum\n // Listen-Socket der das bindAddr nutzt.\n this.scanSocket.bind(0, bindAddr, () => {\n if (this.stopped) {\n return;\n }\n this.scanSocket?.setBroadcast(true);\n try {\n this.scanSocket?.addMembership(MULTICAST_ADDR, bindAddr);\n } catch {\n // Membership-fail typischerweise OS-Routing-Issue (z.B. interface\n // bindAddr nicht in der multicast-routing-table). LAN-Discovery\n // bleibt trotzdem partial-funktional \u00FCber setBroadcast(true), aber\n // das ist asymmetrisch \u2014 Antworten kommen evtl. nicht zur\u00FCck.\n // Auf info-level loggen so der User die Ursache der \"no devices\"-\n // Symptomatik in den Logs findet.\n this.log.info(\n `LAN: could not join multicast group on ${bindAddr ?? \"default interface\"} \u2014 discovery may be incomplete`,\n );\n }\n this.sendScan();\n });\n\n // Periodic scan \u2014 guard the timer creation in case stop() raced with\n // the bind callback, otherwise we'd leave a setInterval running\n // against a torn-down LAN client.\n if (!this.stopped) {\n this.scanTimer = this.timers.setInterval(() => {\n this.sendScan();\n }, scanIntervalMs);\n }\n });\n }\n\n /** Stop all sockets and timers */\n stop(): void {\n this.stopped = true;\n if (this.scanTimer) {\n this.timers.clearInterval(this.scanTimer);\n this.scanTimer = undefined;\n }\n // Cancel any deferred wizard-flash bursts that haven't fired yet.\n for (const handle of this.pendingFlashTimers) {\n this.timers.clearTimeout(handle);\n }\n this.pendingFlashTimers.clear();\n if (this.scanSocket) {\n // dropMembership symmetrisch zu addMembership \u2014 auf macOS/Windows kann\n // der Multicast-Filter sonst auf der NIC bis Process-Exit h\u00E4ngenbleiben.\n try {\n if (this.multicastBind) {\n this.scanSocket.dropMembership(MULTICAST_ADDR, this.multicastBind);\n }\n } catch {\n /* ignore \u2014 best-effort */\n }\n try {\n this.scanSocket.close();\n } catch {\n /* ignore */\n }\n this.scanSocket = null;\n }\n if (this.listenSocket) {\n try {\n this.listenSocket.close();\n } catch {\n /* ignore */\n }\n this.listenSocket = null;\n }\n if (this.sendSocket) {\n try {\n this.sendSocket.close();\n } catch {\n /* ignore */\n }\n this.sendSocket = null;\n }\n // L4 \u2014 seenDeviceIps clear, sonst \u00FCberleben Eintr\u00E4ge zwischen\n // start/stop-Zyklen im selben Process (korrektheit nicht beeinflusst,\n // aber discovery-log-info bei IP-Wechsel w\u00FCrde verloren gehen).\n this.seenDeviceIps.clear();\n this.multicastBind = undefined;\n }\n\n /**\n * Send a control command to a device via LAN.\n *\n * @param ip Device IP address\n * @param cmd Command name (turn, brightness, colorwc, devStatus)\n * @param data Command data\n */\n private sendCommand(ip: string, cmd: string, data: Record<string, unknown>): void {\n if (!this.sendSocket) {\n this.log.debug(`LAN send dropped (socket not ready): ${cmd} \u2192 ${ip}`);\n return;\n }\n const message: LanMessage = {\n msg: { cmd, data },\n };\n const buf = Buffer.from(JSON.stringify(message));\n // L5 \u2014 Govee-Spec ist UDP-PMTU-bound. Bei sehr grossen ptReal-Payloads\n // (viele BLE-Pakete in JSON) warnen damit der User wei\u00DF warum etwas\n // verloren geht.\n if (buf.length > 1400) {\n this.log.debug(`LAN payload large (${buf.length} bytes) \u2014 may be PMTU-fragmented for ${ip}`);\n }\n this.sendSocket.send(buf, 0, buf.length, COMMAND_PORT, ip, err => {\n if (err) {\n this.log.debug(`LAN send error to ${ip}: ${err.message}`);\n }\n });\n }\n\n /**\n * Send power command\n *\n * @param ip Device IP address\n * @param on Power state\n */\n setPower(ip: string, on: boolean): void {\n this.sendCommand(ip, \"turn\", { value: on ? 1 : 0 });\n }\n\n /**\n * Send brightness command\n *\n * @param ip Device IP address\n * @param brightness Brightness 0-100\n */\n setBrightness(ip: string, brightness: number): void {\n this.sendCommand(ip, \"brightness\", {\n value: clampByte0_100(brightness),\n });\n }\n\n /**\n * Send color command. Inputs are clamped to 0-255 \u2014 out-of-range values\n * from upstream coercion paths (capability-mapper, command-router) would\n * otherwise be sent verbatim and produce undefined-behaviour at the device.\n *\n * @param ip Device IP address\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\n setColor(ip: string, r: number, g: number, b: number): void {\n this.sendCommand(ip, \"colorwc\", {\n color: { r: clampByte(r), g: clampByte(g), b: clampByte(b) },\n colorTemInKelvin: 0,\n });\n }\n\n /**\n * Send color temperature command. Out-of-band kelvin values are clamped\n * to Govee's published 2000-9000 K range (per-device range may be tighter,\n * those are corrected via {@link applyColorTempQuirk} upstream).\n *\n * @param ip Device IP address\n * @param kelvin Color temperature in Kelvin\n */\n setColorTemperature(ip: string, kelvin: number): void {\n const clamped = Number.isFinite(kelvin) ? Math.max(2000, Math.min(9000, Math.round(kelvin))) : 2000;\n this.sendCommand(ip, \"colorwc\", {\n color: { r: 0, g: 0, b: 0 },\n colorTemInKelvin: clamped,\n });\n }\n\n /**\n * Send a scene via ptReal BLE-passthrough.\n * Builds multi-packet BLE data from scenceParam + final scene-code packet.\n *\n * @param ip Device IP address\n * @param sceneCode Scene code from scene library (must be > 0)\n * @param scenceParam Base64-encoded scene parameter data (may be empty for simple presets)\n */\n setScene(ip: string, sceneCode: number, scenceParam: string): void {\n if (sceneCode <= 0) {\n return;\n }\n const packets = buildScenePackets(sceneCode, scenceParam);\n this.sendPtReal(ip, packets);\n }\n\n /**\n * Send raw ptReal BLE-passthrough packets to a device.\n *\n * @param ip Device IP address\n * @param base64Packets Array of Base64-encoded 20-byte BLE packets\n */\n sendPtReal(ip: string, base64Packets: string[]): void {\n if (!this.sendSocket) {\n this.log.debug(`LAN ptReal dropped (socket not ready): ${ip}`);\n return;\n }\n const message = {\n msg: { cmd: \"ptReal\", data: { command: base64Packets } },\n };\n const buf = Buffer.from(JSON.stringify(message));\n if (buf.length > 1400) {\n this.log.debug(`ptReal payload large (${buf.length} bytes) \u2014 may be PMTU-fragmented for ${ip}`);\n }\n this.sendSocket.send(buf, 0, buf.length, COMMAND_PORT, ip, err => {\n if (err) {\n // warn (was: debug) \u2014 a silent UDP-send failure leaves the user\n // wondering why a scene/snapshot \"did nothing\". One log per failed\n // send is acceptable noise; recurring sends to the same offline IP\n // will repeat, which is the right signal.\n this.log.warn(`LAN ptReal error to ${ip}: ${err.message}`);\n } else {\n // Success on debug: confirms the UDP datagram left the socket so\n // a \"snapshot/scene did not activate\" report can be triaged\n // without enabling silly-level wire logging.\n this.log.debug(`LAN ptReal sent to ${ip}: ${base64Packets.length} packet(s), ${buf.length} bytes`);\n }\n });\n }\n\n /**\n * Set gradient toggle via ptReal BLE-passthrough.\n *\n * @param ip Device IP address\n * @param on Gradient on/off\n */\n setGradient(ip: string, on: boolean): void {\n this.sendPtReal(ip, [buildGradientPacket(on)]);\n }\n\n /**\n * Activate a DIY scene via ptReal BLE-passthrough.\n * Sends A1 multi-packet data (if provided) + activation command.\n *\n * @param ip Device IP address\n * @param scenceParam Base64-encoded DIY parameter data (may be empty to activate last DIY)\n */\n setDiyScene(ip: string, scenceParam: string): void {\n const packets = buildDiyPackets(scenceParam);\n this.sendPtReal(ip, packets);\n }\n\n /**\n * Set music mode via ptReal BLE-passthrough.\n * Sub-modes 1 (Spectrum) and 2 (Rolling) use RGB color.\n *\n * @param ip Device IP address\n * @param subMode Music sub-mode (0-3)\n * @param r Red channel 0-255 (used by modes 1, 2)\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\n setMusicMode(ip: string, subMode: number, r = 0, g = 0, b = 0): void {\n this.sendPtReal(ip, [buildMusicModePacket(subMode, r, g, b)]);\n }\n\n /**\n * Set segment color via ptReal BLE-passthrough (command 33 05 15 01).\n *\n * @param ip Device IP address\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param segments Array of 0-based segment indices\n */\n setSegmentColor(ip: string, r: number, g: number, b: number, segments: number[]): void {\n this.sendPtReal(ip, [buildSegmentColorPacket(r, g, b, segments)]);\n }\n\n /**\n * Set segment brightness via ptReal BLE-passthrough (command 33 05 15 02).\n *\n * @param ip Device IP address\n * @param brightness Brightness 0-100\n * @param segments Array of 0-based segment indices\n */\n setSegmentBrightness(ip: string, brightness: number, segments: number[]): void {\n this.sendPtReal(ip, [buildSegmentBrightnessPacket(brightness, segments)]);\n }\n\n /**\n * Flash a single segment bright white and dim all other segments, in ONE\n * atomic ptReal transmission. All three required BLE packets are bundled\n * into a single UDP datagram so the device cannot drop intermediate steps.\n *\n * The \"dim everything else\" packet targets the full bitmask width (56\n * segments \u2014 the Govee protocol's upper bound: 7 bytes \u00D7 8 bits). This\n * covers under-report cases where the Cloud says \"15 segments\" but the\n * strip physically has more. Without this the unreported segments keep\n * shining at whatever brightness they had before the wizard started.\n *\n * Packet order:\n * 0. `colorwc` \u2014 force static-color mode (segment_color_setting packets\n * are ignored while the device is in Scene/Gradient/Music mode)\n * 1. All segments except idx (up to idx 55) \u2192 brightness 0\n * 2. Target segment \u2192 color 0xFFFFFF (full white)\n * 3. Target segment \u2192 brightness 100 (make it bright)\n *\n * @param ip Device IP address\n * @param idx Target segment index (0-based) to flash white\n */\n flashSingleSegment(ip: string, idx: number): void {\n if (idx < 0 || idx >= 56) {\n return;\n }\n const MAX_SEGMENTS = 56;\n const others = Array.from({ length: MAX_SEGMENTS }, (_, i) => i).filter(i => i !== idx);\n // Step 0: force color mode. Without this, the strip stays in whatever\n // mode it was (Scene/Gradient/Music) and silently ignores the three\n // ptReal packets below. The colorwc command resets to a known static\n // state that accepts segment-level overrides.\n this.setColor(ip, 0xff, 0xff, 0xff);\n // Small delay so the firmware can apply the mode switch before the\n // next UDP burst \u2014 Govee's observed minimum is ~50 ms. The handle is\n // tracked so stop() can cancel it explicitly: ioBroker timers eventually\n // fire into a torn-down adapter otherwise (the wrapper-tracked handles\n // get cleared in onUnload, but only via this Set).\n const delayMs = 150;\n const handle = this.timers.setTimeout(() => {\n if (handle !== undefined) {\n this.pendingFlashTimers.delete(handle);\n }\n if (this.stopped || !this.sendSocket) {\n return;\n }\n this.sendPtReal(ip, [\n buildSegmentBrightnessPacket(0, others),\n buildSegmentColorPacket(0xff, 0xff, 0xff, [idx]),\n buildSegmentBrightnessPacket(100, [idx]),\n ]);\n }, delayMs);\n if (handle !== undefined) {\n this.pendingFlashTimers.add(handle);\n }\n }\n\n /**\n * Restore a segment strip to a uniform color + brightness in one atomic\n * ptReal transmission. Used at wizard end/abort to put the strip back to\n * the captured baseline.\n *\n * @param ip Device IP address\n * @param total Total number of segments\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param brightness Brightness 0-100\n */\n restoreAllSegments(ip: string, total: number, r: number, g: number, b: number, brightness: number): void {\n if (total <= 0) {\n return;\n }\n const all = Array.from({ length: total }, (_, i) => i);\n this.sendPtReal(ip, [buildSegmentColorPacket(r, g, b, all), buildSegmentBrightnessPacket(brightness, all)]);\n }\n\n /**\n * Request device status\n *\n * @param ip Device IP address\n */\n requestStatus(ip: string): void {\n this.sendCommand(ip, \"devStatus\", {});\n }\n\n /** Send multicast scan */\n private sendScan(): void {\n const scanMsg: LanMessage = {\n msg: { cmd: \"scan\", data: { account_topic: \"reserve\" } },\n };\n const buf = Buffer.from(JSON.stringify(scanMsg));\n this.scanSocket?.send(buf, 0, buf.length, SCAN_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.log.debug(`LAN scan send error: ${err.message}`);\n }\n });\n }\n\n /**\n * Parse incoming UDP message\n *\n * @param msg Raw UDP message buffer\n * @param sourceIp Source IP address from UDP rinfo\n */\n private handleMessage(msg: Buffer, sourceIp: string): void {\n // L9 \u2014 Size-Bound. Pathologische Pakete (mehrere reassembled UDP-Frames)\n // k\u00F6nnten 64KB+ haben. JSON.parse von 64KB+ blockiert den Event-Loop\n // f\u00FCr ms-Spannen \u2014 bei vielen Devices gleichzeitig sp\u00FCrbar.\n if (msg.length > 8192) {\n this.log.debug(`LAN message dropped from ${sourceIp}: oversize ${msg.length} bytes`);\n return;\n }\n try {\n const data = JSON.parse(msg.toString()) as {\n msg?: { cmd?: string; data?: Record<string, unknown> };\n };\n if (!data.msg?.cmd || typeof data.msg.cmd !== \"string\") {\n return;\n }\n\n const cmd: string = data.msg.cmd;\n const rawPayload = data.msg.data;\n const payload: Record<string, unknown> =\n rawPayload && typeof rawPayload === \"object\" && !Array.isArray(rawPayload) ? rawPayload : {};\n\n if (cmd === \"scan\") {\n this.handleScanResponse(payload);\n } else if (cmd === \"devStatus\") {\n this.handleStatusResponse(payload, sourceIp);\n }\n } catch {\n this.log.debug(`LAN: Failed to parse message: ${msg.toString().slice(0, 200)}`);\n }\n }\n\n /**\n * Handle scan response \u2014 new device found\n *\n * @param data Parsed scan response payload\n */\n private handleScanResponse(data: Record<string, unknown>): void {\n // Defensive type checks \u2014 LAN payload comes over the wire, treat as untrusted\n if (\n typeof data.ip !== \"string\" ||\n typeof data.device !== \"string\" ||\n typeof data.sku !== \"string\" ||\n !data.ip ||\n !data.device ||\n !data.sku\n ) {\n return;\n }\n\n const lanDevice: LanDevice = {\n ip: data.ip,\n device: data.device,\n sku: data.sku,\n };\n\n const key = `${lanDevice.device}:${lanDevice.ip}`;\n if (!this.seenDeviceIps.has(key)) {\n // Evict any stale entries for the same device at different IPs so the\n // set stays bounded by the actual number of devices, not the full\n // history of IPs they ever had.\n const staleSuffix = `${lanDevice.device}:`;\n for (const existing of this.seenDeviceIps) {\n if (existing.startsWith(staleSuffix) && existing !== key) {\n this.seenDeviceIps.delete(existing);\n }\n }\n this.seenDeviceIps.add(key);\n this.log.debug(`LAN: Found ${lanDevice.sku} (${lanDevice.device}) at ${lanDevice.ip}`);\n }\n this.onDiscovery?.(lanDevice);\n }\n\n /**\n * Handle status response \u2014 matched to device by source IP.\n * Defensive against malformed/partial payloads \u2014 all fields coerced to safe defaults.\n *\n * @param data Parsed status response payload\n * @param sourceIp Source IP address from UDP message\n */\n private handleStatusResponse(data: Record<string, unknown>, sourceIp: string): void {\n const toNum = (v: unknown): number => (typeof v === \"number\" && Number.isFinite(v) ? v : 0);\n const colorRaw = data.color;\n const color =\n colorRaw && typeof colorRaw === \"object\"\n ? {\n r: toNum((colorRaw as Record<string, unknown>).r),\n g: toNum((colorRaw as Record<string, unknown>).g),\n b: toNum((colorRaw as Record<string, unknown>).b),\n }\n : { r: 0, g: 0, b: 0 };\n\n const status: LanStatus = {\n onOff: toNum(data.onOff),\n brightness: toNum(data.brightness),\n color,\n colorTemInKelvin: toNum(data.colorTemInKelvin),\n };\n\n this.onStatus?.(sourceIp, status);\n }\n}\n\n// --- BLE Packet Builder for ptReal ---\n\n/**\n * Clamp a value to 0-255. NaN / non-numeric \u2192 0. Centralised so every LAN\n * command goes through the same bounds-check.\n *\n * @param v Input value\n */\nfunction clampByte(v: number): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) {\n return 0;\n }\n return Math.max(0, Math.min(255, Math.round(v)));\n}\n\n/**\n * Clamp a value to 0-100. NaN / non-numeric \u2192 0.\n *\n * @param v Input value\n */\nfunction clampByte0_100(v: number): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) {\n return 0;\n }\n return Math.max(0, Math.min(100, Math.round(v)));\n}\n\n/**\n * XOR checksum over all bytes\n *\n * @param data Array of byte values\n */\nfunction xorChecksum(data: number[]): number {\n let checksum = 0;\n for (const b of data) {\n checksum ^= b;\n }\n return checksum;\n}\n\n/**\n * Pad data to 19 bytes + append XOR checksum = 20-byte BLE packet\n *\n * @param data Array of byte values to pad and checksum\n */\nfunction finishPacket(data: number[]): number[] {\n while (data.length < 19) {\n data.push(0);\n }\n data.push(xorChecksum(data));\n return data;\n}\n\n/**\n * Build Base64-encoded BLE packets for scene activation via ptReal.\n *\n * @param sceneCode Scene code from library (> 0)\n * @param scenceParam Base64-encoded scene parameter data (may be empty)\n */\nexport function buildScenePackets(sceneCode: number, scenceParam: string): string[] {\n const packets: string[] = [];\n\n // Multi-packet scene data from scenceParam (A3 header protocol)\n if (scenceParam) {\n const paramBytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n // Build A3-framed packets: first chunk starts with A3 00 01 00 02\n const rawData: number[] = [0xa3, 0x00, 0x01, 0x00, 0x02];\n let numLines = 0;\n let lastLineMarker = 1;\n\n for (const b of paramBytes) {\n if (rawData.length % 19 === 0) {\n numLines++;\n rawData.push(0xa3);\n lastLineMarker = rawData.length;\n rawData.push(numLines);\n }\n rawData.push(b);\n }\n rawData[lastLineMarker] = 0xff;\n rawData[3] = numLines + 1;\n\n // Split into 19-byte chunks, pad + checksum each\n for (let i = 0; i < rawData.length; i += 19) {\n const chunk = rawData.slice(i, i + 19);\n const pkt = finishPacket([...chunk]);\n packets.push(Buffer.from(pkt).toString(\"base64\"));\n }\n }\n\n // Final scene-code activation packet: 33 05 04 lo hi\n const lo = sceneCode & 0xff;\n const hi = (sceneCode >> 8) & 0xff;\n const activatePacket = finishPacket([0x33, 0x05, 0x04, lo, hi]);\n packets.push(Buffer.from(activatePacket).toString(\"base64\"));\n\n return packets;\n}\n\n/**\n * Build Base64-encoded BLE packets for DIY scene activation via ptReal.\n * Uses A1 framing for multi-packet data, then sends activation command.\n *\n * @param scenceParam Base64-encoded DIY parameter data (may be empty)\n */\nexport function buildDiyPackets(scenceParam: string): string[] {\n const packets: string[] = [];\n\n if (scenceParam) {\n const paramBytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n // A1-framed packets: start A1 02 00 <total>\n const rawData: number[] = [0xa1, 0x02, 0x00, 0x00];\n let numLines = 0;\n let lastLineMarker = 2;\n\n for (const b of paramBytes) {\n if (rawData.length % 19 === 0) {\n numLines++;\n rawData.push(0xa1, 0x02);\n lastLineMarker = rawData.length - 1;\n rawData.push(numLines);\n }\n rawData.push(b);\n }\n rawData[lastLineMarker] = 0xff;\n rawData[3] = numLines + 1;\n\n for (let i = 0; i < rawData.length; i += 19) {\n const chunk = rawData.slice(i, i + 19);\n packets.push(Buffer.from(finishPacket([...chunk])).toString(\"base64\"));\n }\n }\n\n // Activation: 33 05 0A\n packets.push(Buffer.from(finishPacket([0x33, 0x05, 0x0a])).toString(\"base64\"));\n return packets;\n}\n\n/**\n * Build a Base64-encoded BLE packet for gradient toggle via ptReal.\n *\n * @param on Gradient on/off\n */\nexport function buildGradientPacket(on: boolean): string {\n return Buffer.from(finishPacket([0x33, 0x14, on ? 0x01 : 0x00])).toString(\"base64\");\n}\n\n/**\n * Build a Base64-encoded BLE packet for music mode via ptReal.\n * Sub-modes 1 (Spectrum) and 2 (Rolling) include RGB color.\n *\n * @param subMode Music sub-mode (0=Energic, 1=Spectrum, 2=Rolling, 3=Rhythm)\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\nexport function buildMusicModePacket(subMode: number, r = 0, g = 0, b = 0): string {\n const data = [0x33, 0x05, 0x01, subMode & 0xff];\n if (subMode === 1 || subMode === 2) {\n data.push(r & 0xff, g & 0xff, b & 0xff);\n }\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Build a little-endian segment bitmask.\n * Segment 0 = byte[0] bit 0, Segment 8 = byte[1] bit 0, etc.\n *\n * @param segments Array of 0-based segment indices\n * @param byteCount Number of bitmask bytes (7 for color, 14 for brightness)\n */\nexport function buildSegmentBitmask(segments: number[], byteCount: number): number[] {\n const mask = new Array<number>(byteCount).fill(0);\n for (const seg of segments) {\n const byteIdx = Math.floor(seg / 8);\n const bitIdx = seg % 8;\n if (byteIdx < byteCount) {\n mask[byteIdx] |= 1 << bitIdx;\n }\n }\n return mask;\n}\n\n/**\n * Build a Base64-encoded BLE packet for segment color via ptReal.\n * Command: 33 05 15 01 RR GG BB 00\u00D75 bitmask\u00D77\n *\n * @param r Red 0-255\n * @param g Green 0-255\n * @param b Blue 0-255\n * @param segments Array of 0-based segment indices\n */\nexport function buildSegmentColorPacket(r: number, g: number, b: number, segments: number[]): string {\n const data = [\n 0x33,\n 0x05,\n 0x15,\n 0x01,\n r & 0xff,\n g & 0xff,\n b & 0xff,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n ...buildSegmentBitmask(segments, 7),\n ];\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Build a Base64-encoded BLE packet for segment brightness via ptReal.\n * Command: 33 05 15 02 BB bitmask\u00D714\n *\n * @param brightness Brightness 0-100\n * @param segments Array of 0-based segment indices\n */\nexport function buildSegmentBrightnessPacket(brightness: number, segments: number[]): string {\n const data = [0x33, 0x05, 0x15, 0x02, Math.max(0, Math.min(100, brightness)), ...buildSegmentBitmask(segments, 14)];\n return Buffer.from(finishPacket(data)).toString(\"base64\");\n}\n\n/**\n * Apply speed level to a scene's scenceParam by replacing speed bytes in each page.\n * scenceParam structure: byte[0] = page count, then per page: 1 byte length + N bytes data.\n * Speed byte position within each page: pageLength - 5.\n *\n * @param scenceParam Base64-encoded scene parameter data\n * @param speedLevel Speed level index (0-based)\n * @param speedConfig JSON config string from speedInfo.config\n * @returns Modified Base64-encoded scenceParam with speed bytes replaced\n */\nexport function applySceneSpeed(scenceParam: string, speedLevel: number, speedConfig: string): string {\n if (!scenceParam || !speedConfig) {\n return scenceParam;\n }\n\n let configEntries: Array<{\n page: number;\n moveIn?: number[];\n }>;\n try {\n configEntries = JSON.parse(speedConfig);\n } catch {\n // Govee's speedInfo.config schema can drift \u2014 surface the failure on\n // debug so a \"scene speed has no effect\" complaint is traceable.\n // Returning the un-modified scenceParam keeps the feature partially\n // working (default speed) instead of failing the activation.\n return scenceParam;\n }\n\n if (!Array.isArray(configEntries) || configEntries.length === 0) {\n return scenceParam;\n }\n\n const bytes = Array.from(Buffer.from(scenceParam, \"base64\"));\n if (bytes.length === 0) {\n return scenceParam;\n }\n\n const pageCount = bytes[0];\n let offset = 1;\n\n for (let pageIdx = 0; pageIdx < pageCount && offset < bytes.length; pageIdx++) {\n const pageLen = bytes[offset];\n if (offset + 1 + pageLen > bytes.length) {\n break;\n }\n\n const cfg = configEntries.find(c => c.page === pageIdx);\n if (cfg?.moveIn && speedLevel >= 0 && speedLevel < cfg.moveIn.length) {\n const speedBytePos = offset + 1 + (pageLen - 5);\n if (speedBytePos > offset && speedBytePos < offset + 1 + pageLen) {\n bytes[speedBytePos] = cfg.moveIn[speedLevel];\n }\n }\n\n offset += 1 + pageLen;\n }\n\n return Buffer.from(bytes).toString(\"base64\");\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -76,7 +76,12 @@ function httpsRequest(options) {
76
76
  reject(new HttpError(`HTTP ${statusCode}`, statusCode, res.headers, raw));
77
77
  return;
78
78
  }
79
- if (raw.trim().length === 0) {
79
+ const trimmed = raw.trim();
80
+ if (trimmed.length === 0) {
81
+ resolve(null);
82
+ return;
83
+ }
84
+ if (trimmed.length < 100 && /^\d{3}\s+\S/.test(trimmed)) {
80
85
  resolve(null);
81
86
  return;
82
87
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/http-client.ts"],
4
- "sourcesContent": ["import * as https from \"node:https\";\n\n/**\n * Module-level keep-alive Agent \u2014 vermeidet TLS-Handshake (~200ms) pro\n * Request. maxSockets begrenzt parallele Verbindungen pro Host damit wir\n * nicht aus Versehen Govee mit 100 gleichzeitigen Calls treffen.\n */\nconst keepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 4 });\n\n/** Options for an HTTPS request */\nexport interface HttpRequestOptions {\n /** HTTP method */\n method: \"GET\" | \"POST\";\n /** Full URL */\n url: string;\n /** HTTP headers */\n headers: Record<string, string>;\n /** Request body (POST only, will be JSON-serialized) */\n body?: unknown;\n /** Timeout in milliseconds (default 15000) */\n timeout?: number;\n /** Optional AbortSignal \u2014 wird der Request abgebrochen sobald abort() */\n signal?: AbortSignal;\n}\n\n/**\n * Signature der httpsRequest-Funktion. Cloud/Mqtt-Clients nehmen das als\n * optionalen DI-Parameter \u2014 Default ist die echte httpsRequest, Tests k\u00F6nnen\n * einen Mock injizieren ohne Module-Replacement.\n */\nexport type HttpsRequestFn = <T>(options: HttpRequestOptions) => Promise<T>;\n\n/**\n * Perform an HTTPS request and parse the JSON response.\n *\n * @param options Request options\n */\nexport function httpsRequest<T>(options: HttpRequestOptions): Promise<T> {\n return new Promise((resolve, reject) => {\n const u = new URL(options.url);\n const postData = options.body ? JSON.stringify(options.body) : undefined;\n\n const reqOptions: https.RequestOptions = {\n method: options.method,\n hostname: u.hostname,\n path: u.pathname + u.search,\n headers: {\n ...options.headers,\n ...(postData\n ? {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(postData),\n }\n : {}),\n },\n timeout: options.timeout ?? 15_000,\n agent: keepAliveAgent,\n };\n\n // Track the abort listener so we can detach it when the request resolves\n // or rejects normally \u2014 without this the AbortSignal accumulates one\n // dead listener per completed request, leaking memory if the same signal\n // is re-used for many requests.\n let onAbort: (() => void) | null = null;\n const cleanupAbort = (): void => {\n if (onAbort && options.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n onAbort = null;\n }\n };\n\n const req = https.request(reqOptions, res => {\n const chunks: Buffer[] = [];\n // res.on(\"error\") catches mid-stream failures (TCP RST after headers,\n // socket-close before \"end\" fires). Without this, such errors propagate\n // to the global \"uncaughtException\" handler instead of rejecting the\n // promise \u2014 and the caller sees the request hang until the 15 s timeout.\n res.on(\"error\", err => {\n cleanupAbort();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n cleanupAbort();\n const raw = Buffer.concat(chunks).toString();\n const statusCode = res.statusCode ?? 0;\n\n if (statusCode < 200 || statusCode >= 400) {\n // M4 \u2014 Body-Snippet aus Error-Message rausnehmen damit\n // Tokens/API-Keys nicht im warn-Log auftauchen wenn der\n // Server sie reflektiert. responseBody bleibt f\u00FCr debug\n // separat verf\u00FCgbar.\n reject(new HttpError(`HTTP ${statusCode}`, statusCode, res.headers, raw));\n return;\n }\n\n // Empty/whitespace-only 2xx body is legitimate for several Govee\n // undocumented endpoints \u2014 `/appsku/v1/music-effect-libraries`,\n // `diy-light-effect-libraries`, and `sku-supported-feature` all\n // return a bare 200 with no body for SKUs they don't recognise.\n // Resolve as `null` so the caller can treat it as \"no data\" via the\n // existing optional-chaining guards instead of seeing an\n // `Invalid JSON` stack trace in the log (Issue #13).\n if (raw.trim().length === 0) {\n resolve(null as T);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch (parseErr) {\n // Include a 100-char prefix of the body so a \"this endpoint\n // returned HTML / a non-JSON 200\" can be diagnosed without\n // enabling debug log. Body cap is intentional \u2014 Govee may echo\n // request data and we don't want full payloads in warn logs.\n const snippet = raw.length > 100 ? `${raw.slice(0, 100)}\u2026` : raw;\n const detail = parseErr instanceof Error ? parseErr.message : String(parseErr);\n reject(new Error(`Invalid JSON in HTTP ${statusCode} response: ${detail} \u2014 body starts with: ${snippet}`));\n }\n });\n });\n\n req.on(\"error\", err => {\n cleanupAbort();\n reject(err);\n });\n req.on(\"timeout\", () => req.destroy(new Error(\"Timeout\")));\n\n // M3 \u2014 AbortSignal-Support. Wer den Request macht kann ihn abbrechen\n // (z.B. Adapter-onUnload via AbortController) damit der Stop nicht\n // 15s auf das Timeout warten muss.\n if (options.signal) {\n if (options.signal.aborted) {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n return;\n }\n onAbort = (): void => {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n };\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n if (postData) {\n req.write(postData);\n }\n req.end();\n });\n}\n\n/** HTTP error with status code, response headers, and response body (debug-only) */\nexport class HttpError extends Error {\n /** HTTP status code */\n readonly statusCode: number;\n /** Response headers */\n readonly headers: Record<string, string | string[] | undefined>;\n /**\n * Raw response body \u2014 NICHT in `message` damit Tokens/API-Keys nicht\n * via warn-Log geleakt werden. Nur f\u00FCr gezieltes debug-Logging beim\n * Caller verf\u00FCgbar.\n */\n readonly responseBody: string;\n\n /**\n * @param message Error message (Body-frei)\n * @param statusCode HTTP status code\n * @param headers Response headers\n * @param responseBody Raw response body (kann sensitive Echo-Daten enthalten)\n */\n constructor(\n message: string,\n statusCode: number,\n headers: Record<string, string | string[] | undefined> = {},\n responseBody: string = \"\",\n ) {\n super(message);\n this.name = \"HttpError\";\n this.statusCode = statusCode;\n this.headers = headers;\n this.responseBody = responseBody;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as https from \"node:https\";\n\n/**\n * Module-level keep-alive Agent \u2014 vermeidet TLS-Handshake (~200ms) pro\n * Request. maxSockets begrenzt parallele Verbindungen pro Host damit wir\n * nicht aus Versehen Govee mit 100 gleichzeitigen Calls treffen.\n */\nconst keepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 4 });\n\n/** Options for an HTTPS request */\nexport interface HttpRequestOptions {\n /** HTTP method */\n method: \"GET\" | \"POST\";\n /** Full URL */\n url: string;\n /** HTTP headers */\n headers: Record<string, string>;\n /** Request body (POST only, will be JSON-serialized) */\n body?: unknown;\n /** Timeout in milliseconds (default 15000) */\n timeout?: number;\n /** Optional AbortSignal \u2014 wird der Request abgebrochen sobald abort() */\n signal?: AbortSignal;\n}\n\n/**\n * Signature der httpsRequest-Funktion. Cloud/Mqtt-Clients nehmen das als\n * optionalen DI-Parameter \u2014 Default ist die echte httpsRequest, Tests k\u00F6nnen\n * einen Mock injizieren ohne Module-Replacement.\n */\nexport type HttpsRequestFn = <T>(options: HttpRequestOptions) => Promise<T>;\n\n/**\n * Perform an HTTPS request and parse the JSON response.\n *\n * @param options Request options\n */\nexport function httpsRequest<T>(options: HttpRequestOptions): Promise<T> {\n return new Promise((resolve, reject) => {\n const u = new URL(options.url);\n const postData = options.body ? JSON.stringify(options.body) : undefined;\n\n const reqOptions: https.RequestOptions = {\n method: options.method,\n hostname: u.hostname,\n path: u.pathname + u.search,\n headers: {\n ...options.headers,\n ...(postData\n ? {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(postData),\n }\n : {}),\n },\n timeout: options.timeout ?? 15_000,\n agent: keepAliveAgent,\n };\n\n // Track the abort listener so we can detach it when the request resolves\n // or rejects normally \u2014 without this the AbortSignal accumulates one\n // dead listener per completed request, leaking memory if the same signal\n // is re-used for many requests.\n let onAbort: (() => void) | null = null;\n const cleanupAbort = (): void => {\n if (onAbort && options.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n onAbort = null;\n }\n };\n\n const req = https.request(reqOptions, res => {\n const chunks: Buffer[] = [];\n // res.on(\"error\") catches mid-stream failures (TCP RST after headers,\n // socket-close before \"end\" fires). Without this, such errors propagate\n // to the global \"uncaughtException\" handler instead of rejecting the\n // promise \u2014 and the caller sees the request hang until the 15 s timeout.\n res.on(\"error\", err => {\n cleanupAbort();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n cleanupAbort();\n const raw = Buffer.concat(chunks).toString();\n const statusCode = res.statusCode ?? 0;\n\n if (statusCode < 200 || statusCode >= 400) {\n // M4 \u2014 Body-Snippet aus Error-Message rausnehmen damit\n // Tokens/API-Keys nicht im warn-Log auftauchen wenn der\n // Server sie reflektiert. responseBody bleibt f\u00FCr debug\n // separat verf\u00FCgbar.\n reject(new HttpError(`HTTP ${statusCode}`, statusCode, res.headers, raw));\n return;\n }\n\n // Empty/whitespace-only 2xx body is legitimate for several Govee\n // undocumented endpoints \u2014 `/appsku/v1/music-effect-libraries`,\n // `diy-light-effect-libraries`, and `sku-supported-feature` all\n // return a bare 200 with no body for SKUs they don't recognise.\n // Resolve as `null` so the caller can treat it as \"no data\" via the\n // existing optional-chaining guards instead of seeing an\n // `Invalid JSON` stack trace in the log (Issue #13).\n const trimmed = raw.trim();\n if (trimmed.length === 0) {\n resolve(null as T);\n return;\n }\n\n // Govee also returns HTTP 200 with a plain-text *HTTP-status-line*\n // body for SKU/Bearer combos without permission \u2014 e.g. the literal\n // string `\"403 Forbbiden\"` (their typo). The conservative regex\n // `^<3-digit-status> <non-whitespace>` plus a 100-char length cap\n // catches these without swallowing JSON literals that happen to\n // start with a number (`123.45` lacks the trailing whitespace+text).\n // Caller sees the same `null` shape as the empty-body case above.\n // Issue #13 follow-up (tukey42, H61A8 \u2014 2026-05-12).\n if (trimmed.length < 100 && /^\\d{3}\\s+\\S/.test(trimmed)) {\n resolve(null as T);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch (parseErr) {\n // Include a 100-char prefix of the body so a \"this endpoint\n // returned HTML / a non-JSON 200\" can be diagnosed without\n // enabling debug log. Body cap is intentional \u2014 Govee may echo\n // request data and we don't want full payloads in warn logs.\n const snippet = raw.length > 100 ? `${raw.slice(0, 100)}\u2026` : raw;\n const detail = parseErr instanceof Error ? parseErr.message : String(parseErr);\n reject(new Error(`Invalid JSON in HTTP ${statusCode} response: ${detail} \u2014 body starts with: ${snippet}`));\n }\n });\n });\n\n req.on(\"error\", err => {\n cleanupAbort();\n reject(err);\n });\n req.on(\"timeout\", () => req.destroy(new Error(\"Timeout\")));\n\n // M3 \u2014 AbortSignal-Support. Wer den Request macht kann ihn abbrechen\n // (z.B. Adapter-onUnload via AbortController) damit der Stop nicht\n // 15s auf das Timeout warten muss.\n if (options.signal) {\n if (options.signal.aborted) {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n return;\n }\n onAbort = (): void => {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n };\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n if (postData) {\n req.write(postData);\n }\n req.end();\n });\n}\n\n/** HTTP error with status code, response headers, and response body (debug-only) */\nexport class HttpError extends Error {\n /** HTTP status code */\n readonly statusCode: number;\n /** Response headers */\n readonly headers: Record<string, string | string[] | undefined>;\n /**\n * Raw response body \u2014 NICHT in `message` damit Tokens/API-Keys nicht\n * via warn-Log geleakt werden. Nur f\u00FCr gezieltes debug-Logging beim\n * Caller verf\u00FCgbar.\n */\n readonly responseBody: string;\n\n /**\n * @param message Error message (Body-frei)\n * @param statusCode HTTP status code\n * @param headers Response headers\n * @param responseBody Raw response body (kann sensitive Echo-Daten enthalten)\n */\n constructor(\n message: string,\n statusCode: number,\n headers: Record<string, string | string[] | undefined> = {},\n responseBody: string = \"\",\n ) {\n super(message);\n this.name = \"HttpError\";\n this.statusCode = statusCode;\n this.headers = headers;\n this.responseBody = responseBody;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
package/io-package.json CHANGED
@@ -1,33 +1,46 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.8.1",
4
+ "version": "2.8.2",
5
5
  "news": {
6
+ "2.8.2": {
7
+ "en": "Per-device snapshot refresh also reloads activation data — re-recorded snapshots work right away. Cleaner debug log. Failed local commands appear as a warning.",
8
+ "de": "Snapshot-Refresh am Gerät lädt jetzt auch die Aktivierungs-Daten — neu aufgenommene Snapshots greifen sofort. Sauberes Debug-Log. Fehlgeschlagene lokale Befehle als Warnung.",
9
+ "ru": "Обновление снепшота на устройстве теперь перезагружает и данные активации — пересозданные снепшоты работают сразу. Чище debug-лог. Неудачные локальные команды — как warning.",
10
+ "pt": "Atualização do snapshot por dispositivo recarrega também os dados de ativação — snapshots regravados funcionam de imediato. Debug log mais limpo. Comandos locais falhados como aviso.",
11
+ "nl": "Snapshot-vernieuwen per apparaat herlaadt ook activatiegegevens — heropgenomen snapshots werken direct. Schoner debug-log. Mislukte lokale opdrachten als waarschuwing.",
12
+ "fr": "L'actualisation snapshot par appareil recharge aussi les données d'activation — les snapshots ré-enregistrés s'activent direct. Journal debug propre. Commandes locales en échec en warning.",
13
+ "it": "L'aggiornamento snapshot per dispositivo ricarica anche i dati di attivazione — gli snapshot ri-registrati funzionano subito. Log debug pulito. Comandi locali falliti come avviso.",
14
+ "es": "La actualización por dispositivo recarga también los datos de activación del snapshot — snapshots regrabados funcionan al instante. Log debug limpio. Comandos locales fallidos como aviso.",
15
+ "pl": "Odświeżanie migawki na urządzeniu odświeża też dane aktywacji — ponownie nagrane migawki działają od razu. Czysty debug-log. Nieudane lokalne polecenia jako ostrzeżenie.",
16
+ "uk": "Оновлення снепшоту на пристрої перезавантажує і дані активації — перезаписані снепшоти діють одразу. Чистий debug-лог. Невдалі локальні команди як warning.",
17
+ "zh-cn": "每个设备的快照刷新现在也会重载激活数据——重新录制的快照立刻生效。debug 日志更干净。本地命令失败时以警告级别记录。"
18
+ },
6
19
  "2.8.1": {
7
- "en": "The channel-status prefix added in 2.8.0 now shows only on debug-level lines. Info, warn and error logs are back to their normal short form.",
8
- "de": "Der Channel-Status-Prefix aus 2.8.0 erscheint jetzt nur noch auf debug-Zeilen. Info-, Warn- und Error-Logs sind wieder kurz und übersichtlich.",
9
- "ru": "Префикс статуса каналов из 2.8.0 теперь появляется только в строках уровня debug. Логи info, warn и error снова в коротком виде.",
10
- "pt": "O prefixo de estado dos canais introduzido na 2.8.0 aparece agora apenas nas linhas de debug. Os logs info, warn e error voltam ao formato curto.",
11
- "nl": "De kanaalstatus-prefix uit 2.8.0 verschijnt nu alleen nog op debug-regels. Info-, warn- en error-logs zijn weer kort en overzichtelijk.",
12
- "fr": "Le préfixe d'état des canaux ajouté en 2.8.0 n'apparaît plus que sur les lignes debug. Les logs info, warn et error redeviennent courts.",
13
- "it": "Il prefisso di stato dei canali introdotto nella 2.8.0 ora compare solo nelle righe debug. I log info, warn ed error tornano alla forma breve.",
14
- "es": "El prefijo de estado de canales añadido en 2.8.0 ahora solo aparece en las líneas de debug. Los logs info, warn y error vuelven al formato corto.",
15
- "pl": "Prefiks statusu kanałów dodany w 2.8.0 pojawia się teraz tylko w wierszach debug. Logi info, warn i error wracają do krótkiej formy.",
16
- "uk": "Префікс статусу каналів з 2.8.0 тепер з'являється лише в рядках рівня debug. Логи info, warn та error знову в короткій формі.",
17
- "zh-cn": "2.8.0 添加的通道状态前缀现在只出现在 debug 级别的日志行。info、warn 和 error 日志回到简短格式。"
20
+ "en": "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.",
21
+ "de": "Info-, Warn- und Error-Logs sind wieder kurz und übersichtlich. Der Channel-Status-Prefix aus 2.8.0 bleibt nur in Debug-Logs.",
22
+ "ru": "Логи info, warn и error снова в коротком виде. Префикс статуса каналов из 2.8.0 остаётся только в debug-логах.",
23
+ "pt": "Os logs info, warn e error voltam ao formato curto. O prefixo de estado dos canais da 2.8.0 permanece nos logs de debug.",
24
+ "nl": "Info-, warn- en error-logs zijn weer kort en overzichtelijk. De kanaalstatus-prefix uit 2.8.0 blijft alleen in debug-logs.",
25
+ "fr": "Les logs info, warn et error redeviennent courts. Le préfixe d'état des canaux de 2.8.0 ne reste que dans les logs debug.",
26
+ "it": "I log info, warn ed error tornano alla forma breve. Il prefisso di stato dei canali della 2.8.0 resta solo nei log debug.",
27
+ "es": "Los logs info, warn y error vuelven al formato corto. El prefijo de estado de canales de 2.8.0 permanece solo en los logs de debug.",
28
+ "pl": "Logi info, warn i error wracają do krótkiej formy. Prefiks statusu kanałów z 2.8.0 pozostaje tylko w logach debug.",
29
+ "uk": "Логи info, warn та error знову в короткій формі. Префікс статусу каналів з 2.8.0 залишається лише в debug-логах.",
30
+ "zh-cn": "info、warn 和 error 日志回到简短格式。2.8.0 添加的通道状态前缀仅保留在 debug 日志中。"
18
31
  },
19
32
  "2.8.0": {
20
- "en": "Restart no longer wipes and re-creates scene/music/snapshot datapoints. Pure-LAN devices get a one-time cleanup. Debug logs carry channel status and structured key=value details.",
21
- "de": "Beim Neustart werden Szenen-/Musik-/Snapshot-Datenpunkte nicht mehr kurz gelöscht und neu angelegt. Pure-LAN-Geräte werden einmalig aufgeräumt. Debug-Logs zeigen Channel-Status und key=value-Details.",
22
- "ru": "При перезапуске точки сцен, музыки и снимков больше не удаляются и не создаются заново. Чистые LAN-устройства один раз очищаются. Отладочные логи показывают статус каналов и key=value-детали.",
23
- "pt": "O reinício já não apaga e volta a criar os pontos de cena, música e snapshot. Dispositivos só-LAN recebem uma limpeza única. Logs de debug mostram o estado dos canais e detalhes key=value.",
24
- "nl": "Bij herstart worden scène-/muziek-/snapshot-datapoints niet meer kort verwijderd en opnieuw aangemaakt. Pure-LAN krijgt eenmalig schoonmaak. Debug-logs tonen kanaalstatus en key=value.",
25
- "fr": "Au redémarrage, les points scène/musique/snapshot ne sont plus supprimés puis recréés. Les appareils LAN-only ont un nettoyage unique. Les logs debug affichent l'état des canaux et clé=valeur.",
26
- "it": "Al riavvio i datapoint scene/musica/snapshot non vengono più eliminati e ricreati. Solo-LAN ottiene pulizia una tantum. I log di debug mostrano lo stato dei canali e key=value.",
27
- "es": "Al reiniciar, los puntos de escena, música y snapshots ya no se eliminan y vuelven a crear. Los dispositivos solo-LAN reciben una limpieza única. Los registros de debug muestran el estado y key=value.",
28
- "pl": "Przy restarcie stany scen, muzyki i migawek nie usuwane i tworzone od nowa. Urządzenia tylko-LAN raz oczyszczone. Debug pokazuje status kanałów oraz dodatkowe pola klucz=wartość.",
29
- "uk": "Під час перезапуску точки сцен, музики й знімків більше не видаляються та не створюються повторно. Пристрої лише з LAN отримують одноразове очищення. Debug показує статус каналів і key=value.",
30
- "zh-cn": "重启时不再删除并重新创建场景、音乐和快照数据点。仅 LAN 设备一次性清理。调试日志显示通道状态和结构化 key=value 详情。"
33
+ "en": "Restart no longer briefly removes and re-creates scene, music and snapshot datapoints. Lights without API key no longer have empty scene/snapshot dropdowns left over from earlier versions.",
34
+ "de": "Beim Neustart verschwinden Szenen-, Musik- und Snapshot-Datenpunkte nicht mehr kurz. Lichter ohne API-Key haben keine leeren Szenen-/Snapshot-Dropdowns mehr aus älteren Versionen.",
35
+ "ru": "При перезапуске точки сцен, музыки и снимков больше не пропадают на короткое время. Лампы без API-ключа не имеют пустых списков сцен/снимков, оставшихся от прошлых версий.",
36
+ "pt": "O reinício já não remove e volta a criar brevemente os pontos de cena, música e snapshot. Lâmpadas sem chave API não têm listas vazias deixadas por versões anteriores.",
37
+ "nl": "Bij herstart verdwijnen scène-, muziek- en snapshot-datapoints niet meer kort. Lampen zonder API-key hebben geen lege scène-/snapshot-lijsten meer uit oudere versies.",
38
+ "fr": "Au redémarrage, les points de scène, musique et instantanés ne disparaissent plus brièvement. Les lampes sans clé API n'ont plus de listes vides héritées d'anciennes versions.",
39
+ "it": "Al riavvio i datapoint di scene, musica e snapshot non scompaiono più brevemente. Le luci senza chiave API non hanno più liste vuote rimaste da versioni precedenti.",
40
+ "es": "Al reiniciar, los puntos de escena, música y snapshot ya no desaparecen brevemente. Las luces sin clave API ya no tienen listas vacías heredadas de versiones anteriores.",
41
+ "pl": "Przy restarcie stany scen, muzyki i migawek nie znikają już na krótko. Lampy bez klucza API nie mają już pustych list pozostałych po starszych wersjach.",
42
+ "uk": "Під час перезапуску точки сцен, музики й знімків більше не зникають на короткий час. Лампи без API-ключа не мають порожніх списків зі старих версій.",
43
+ "zh-cn": "重启时场景、音乐和快照数据点不再短暂消失。没有 API key 的灯具不再有从旧版本遗留的空场景/快照下拉列表。"
31
44
  },
32
45
  "2.7.1": {
33
46
  "en": "Cleaner startup log: the first line now tells you to wait for the \"ready\" message, and one redundant connection-info line is gone.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Wewnętrzny refaktoring (main.ts na celu ~800 LOC, 11 handlerów w lib/handlers/). Bez zmian dla użytkowników.",
81
94
  "uk": "Внутрішній рефакторинг (main.ts на цільові ~800 LOC, 11 хендлерів у lib/handlers/). Без змін для користувачів.",
82
95
  "zh-cn": "内部重构(main.ts 达到 ~800 行的计划目标,lib/handlers/ 有 11 个处理器模块)。用户无任何变化。"
83
- },
84
- "2.6.5": {
85
- "en": "Internal refactoring (main.ts and device-manager.ts split into smaller modules). No changes for users.",
86
- "de": "Internes Refactoring (main.ts und device-manager.ts in kleinere Module aufgeteilt). Keine Änderungen für Benutzer.",
87
- "ru": "Внутренний рефакторинг (main.ts и device-manager.ts разбиты на меньшие модули). Для пользователей ничего не меняется.",
88
- "pt": "Refatoração interna (main.ts e device-manager.ts divididos em módulos menores). Sem alterações para utilizadores.",
89
- "nl": "Interne refactoring (main.ts en device-manager.ts opgesplitst in kleinere modules). Geen wijzigingen voor gebruikers.",
90
- "fr": "Refactorisation interne (main.ts et device-manager.ts divisés en modules plus petits). Aucun changement pour les utilisateurs.",
91
- "it": "Refactoring interno (main.ts e device-manager.ts suddivisi in moduli più piccoli). Nessuna modifica per gli utenti.",
92
- "es": "Refactorización interna (main.ts y device-manager.ts divididos en módulos más pequeños). Sin cambios para los usuarios.",
93
- "pl": "Wewnętrzny refaktoring (main.ts i device-manager.ts podzielone na mniejsze moduły). Bez zmian dla użytkowników.",
94
- "uk": "Внутрішній рефакторинг (main.ts і device-manager.ts розбиті на менші модулі). Без змін для користувачів.",
95
- "zh-cn": "内部重构(main.ts 与 device-manager.ts 拆分为更小的模块)。用户无任何变化。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.govee-smart",
3
- "version": "2.8.1",
3
+ "version": "2.8.2",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",