iobroker.govee-smart 2.12.2 → 2.12.3

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
@@ -129,6 +129,11 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
129
129
  Placeholder for the next version (at the beginning of the line):
130
130
  ### **WORK IN PROGRESS**
131
131
  -->
132
+ ### 2.12.3 (2026-05-21)
133
+
134
+ - Honest LAN status at startup — shows "LAN ✗" when no lights are reachable locally, with instructions to enable the local API
135
+ - Lights without local API now fall back to cloud control instead of failing silently
136
+
132
137
  ### 2.12.2 (2026-05-20)
133
138
 
134
139
  - Verified against Node.js 24. Internal cleanup for stricter ioBroker repochecker compliance.
@@ -146,13 +151,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
146
151
 
147
152
  - Internal cleanup. No user-facing changes.
148
153
 
149
- ### 2.11.0 (2026-05-16)
150
-
151
- - Security: the 2FA verification code is now stored encrypted (API key and Govee password were already encrypted in previous versions). If you had a 2FA code set, re-enter it once in the adapter settings.
152
- - Locally saved snapshots are now included in ioBroker backups (BackItUp / `iob backup`). Existing snapshot files migrate automatically on first start.
153
- - sendTo calls with an unknown command no longer hang in the admin — the adapter answers with a clear error.
154
- - Cleaner roles for ice-bucket / motion / dirt / water-tank sensors so they show up correctly in vis and smart-home integrations.
155
-
156
154
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
157
155
 
158
156
  ## Support
@@ -24,6 +24,7 @@ module.exports = __toCommonJS(command_router_exports);
24
24
  var import_types = require("./types");
25
25
  var import_govee_lan_client = require("./govee-lan-client");
26
26
  var import_device_registry = require("./device-registry");
27
+ var import_govee_constants = require("./govee-constants");
27
28
  const FORCE_COLOR_MODE_SETTLE_MS = 150;
28
29
  class CommandRouter {
29
30
  log;
@@ -182,6 +183,9 @@ class CommandRouter {
182
183
  return { kind: "lan", reason: "default" };
183
184
  }
184
185
  if (device.channels.cloud && this.cloudClient) {
186
+ if (device.type === import_govee_constants.GOVEE_DEVICE_TYPE.LIGHT && !device.lanIp) {
187
+ return { kind: "cloud", reason: "light-no-lan-fallback" };
188
+ }
185
189
  return { kind: "cloud", reason: "no-lan" };
186
190
  }
187
191
  return { kind: "skip", reason: "no-channel" };
@@ -198,6 +202,9 @@ class CommandRouter {
198
202
  case "lan":
199
203
  return "LAN";
200
204
  case "cloud":
205
+ if (decision.reason === "light-no-lan-fallback") {
206
+ return "Cloud (no LAN, fallback)";
207
+ }
201
208
  return decision.reason === "override" ? "Cloud (override)" : decision.reason === "no-segments-heuristic" ? "Cloud (no-segments)" : "Cloud";
202
209
  case "skip":
203
210
  return decision.reason === "override-cloud-missing" ? "skip (cloud-override, no cloud)" : "skip (no-channel)";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/command-router.ts"],
4
- "sourcesContent": ["import { hexToRgb, logDedup, type ErrorCategory, type GoveeDevice, type TimerAdapter } from \"./types\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport { applySceneSpeed } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\nimport { getDeviceQuirks, type ConfigurableOverrideCommand, type TransportTarget } from \"./device-registry\";\n\n/**\n * Delay between switching the device into static-color mode and sending the\n * follow-up segment commands. Empirically the firmware needs ~150 ms for the\n * mode flip; shorter delays leave the device still in scene/music mode and the\n * subsequent segment writes are silently dropped.\n */\nconst FORCE_COLOR_MODE_SETTLE_MS = 150;\n\n/**\n * Outcome of `resolveTransport` \u2014 decides which channel handles a command\n * before any I/O happens. Carries the reason so diag-logs and tests can\n * tell an override-routed cloud send apart from a default cloud fallback.\n */\nexport type TransportDecision =\n | { kind: \"lan\"; reason: \"default\" }\n | {\n kind: \"cloud\";\n reason: \"override\" | \"no-lan\" | \"no-segments-heuristic\";\n }\n | { kind: \"skip\"; reason: \"no-channel\" | \"override-cloud-missing\" };\n\n/**\n * Command router \u2014 routes device commands through the fastest available\n * channel: LAN \u2192 Cloud. Quirk-driven overrides (devices.json\n * `transportOverrides`) take precedence over the LAN-first default.\n */\nexport class CommandRouter {\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private lanClient: GoveeLanClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /**\n * Per-category dedup tracker. Replaces the older split between\n * `lastCloudFallbackError` and `lastNoChannelCategory` \u2014 one map, one\n * lookup, keyed by a short category string (`cloud-fallback`,\n * `no-channel`, `override-missing-cloud`).\n */\n private lastErrorByCategory = new Map<string, ErrorCategory | null>();\n\n /** Callback for batch segment state sync */\n onSegmentBatchUpdate?: (\n device: GoveeDevice,\n batch: { segments: number[]; color?: number; brightness?: number },\n ) => void;\n\n /**\n * Optional diag-log hook fired once per `sendCommand` call so the per-device\n * diag ring buffer carries the channel-routing decision (\"LAN took it\",\n * \"Cloud fallback\", \"no channel available\"). Without this, the diag JSON\n * couldn't show why a user's state-write didn't reach the device.\n */\n onDiagLog?: (deviceId: string, level: \"debug\" | \"info\" | \"warn\", msg: string) => void;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper \u2014 routed through `this.setTimeout` so\n * pending color-mode delays get cleared on onUnload.\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.lanClient = client;\n }\n\n /**\n * Register the Cloud client\n *\n * @param client Cloud API client instance\n */\n setCloudClient(client: GoveeCloudClient): void {\n this.cloudClient = client;\n }\n\n /**\n * Register the rate limiter for cloud calls\n *\n * @param limiter Rate limiter instance\n */\n setRateLimiter(limiter: RateLimiter): void {\n this.rateLimiter = limiter;\n }\n\n /**\n * Execute a function through the rate limiter if available, or directly.\n *\n * @param fn Async function to execute\n * @param priority Queue priority (0 = highest)\n */\n async executeRateLimited(fn: () => Promise<void>, priority = 0): Promise<void> {\n if (this.rateLimiter) {\n await this.rateLimiter.tryExecute(fn, priority);\n } else {\n await fn();\n }\n }\n\n /**\n * Force the device into static-color mode before sending segment_color_setting\n * ptReal packets. Without this, the device silently ignores segment-level\n * overrides while it's in Scene/Gradient/Music mode \u2014 the classic \"I set\n * segment 5 red and nothing happened\" symptom. Sends a `colorwc` command with\n * the device's last-known colorRgb (so the strip doesn't visibly change if it\n * was already in color mode), then waits 150 ms so the firmware can switch.\n *\n * As a bonus: once the device is in color mode, subsequent segment commands\n * trigger AA A5 MQTT pushes \u2014 so the adapter learns the real segmentCount\n * automatically the first time the user touches segment controls.\n *\n * @param device Target device\n */\n private async forceColorMode(device: GoveeDevice): Promise<void> {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n const current = typeof device.state.colorRgb === \"string\" ? device.state.colorRgb : null;\n const { r, g, b } = current ? hexToRgb(current) : { r: 255, g: 255, b: 255 };\n this.lanClient.setColor(device.lanIp, r, g, b);\n await this.timers.delay(FORCE_COLOR_MODE_SETTLE_MS);\n }\n\n /**\n * Look up the quirk-driven transport override for a (device, command) pair.\n * Segment-suffix commands (segmentColor:N / segmentBrightness:N) inherit\n * the segmentBatch override \u2014 devices.json carries one key for all segment\n * ops, not one per index.\n *\n * @param device Target device\n * @param command Command type\n */\n private lookupOverride(device: GoveeDevice, command: string): TransportTarget | undefined {\n const overrides = getDeviceQuirks(device.sku)?.transportOverrides;\n if (!overrides) {\n return undefined;\n }\n if (command in overrides) {\n return overrides[command as ConfigurableOverrideCommand];\n }\n if (command.startsWith(\"segmentColor:\") || command.startsWith(\"segmentBrightness:\")) {\n return overrides.segmentBatch;\n }\n return undefined;\n }\n\n /**\n * Catch for unkatalogisierte no-segment SKUs: when a lightScene activation\n * with scenceParam data hits a device that doesn't have any segments, the\n * A3-framed multi-packet ptReal protocol gets silently dropped by the\n * firmware. Cloud activation is the safer default. SKUs known to need\n * this go into devices.json `transportOverrides.lightScene = \"cloud\"` \u2014\n * the heuristic only fires for SKUs not (yet) in the catalog.\n *\n * @param device Target device\n * @param command Command type\n */\n private shouldHeuristicallyUseCloud(device: GoveeDevice, command: string): boolean {\n if (command !== \"lightScene\") {\n return false;\n }\n const hasSegments = typeof device.segmentCount === \"number\" && device.segmentCount > 0;\n return !hasSegments;\n }\n\n /**\n * Single point of truth for channel routing. Quirk-driven `transportOverrides`\n * take precedence over the LAN-first default. Returns a `TransportDecision`\n * carrying both the chosen kind and a reason \u2014 caller emits the reason\n * into the diag log so a cloud-override and a cloud-fallback aren't\n * confused in user-submitted JSON.\n *\n * @param device Target device\n * @param command Command type\n */\n resolveTransport(device: GoveeDevice, command: string): TransportDecision {\n const overrideTarget = this.lookupOverride(device, command);\n if (overrideTarget === \"cloud\") {\n if (device.channels.cloud && this.cloudClient) {\n return { kind: \"cloud\", reason: \"override\" };\n }\n return { kind: \"skip\", reason: \"override-cloud-missing\" };\n }\n // overrideTarget === \"lan\" is a no-op fall-through to default routing.\n\n if (device.lanIp && this.lanClient) {\n if (this.shouldHeuristicallyUseCloud(device, command)) {\n return device.channels.cloud && this.cloudClient\n ? { kind: \"cloud\", reason: \"no-segments-heuristic\" }\n : { kind: \"skip\", reason: \"no-channel\" };\n }\n return { kind: \"lan\", reason: \"default\" };\n }\n if (device.channels.cloud && this.cloudClient) {\n return { kind: \"cloud\", reason: \"no-lan\" };\n }\n return { kind: \"skip\", reason: \"no-channel\" };\n }\n\n /**\n * Format a decision into a human-readable channel marker for the diag\n * log. One line per `sendCommand` so user-submitted JSON shows what the\n * router decided, not what it was nominally configured for.\n *\n * @param decision Output of resolveTransport\n */\n private decisionToChannelMarker(decision: TransportDecision): string {\n switch (decision.kind) {\n case \"lan\":\n return \"LAN\";\n case \"cloud\":\n return decision.reason === \"override\"\n ? \"Cloud (override)\"\n : decision.reason === \"no-segments-heuristic\"\n ? \"Cloud (no-segments)\"\n : \"Cloud\";\n case \"skip\":\n return decision.reason === \"override-cloud-missing\" ? \"skip (cloud-override, no cloud)\" : \"skip (no-channel)\";\n }\n }\n\n /**\n * Skip-handler \u2014 emits the right log level depending on why we couldn't\n * route. Override+no-cloud is a configurable mismatch (user's fault, but\n * we tell them once); regular no-channel during init-race is debug.\n *\n * @param device Target device\n * @param command Command type\n * @param reason Skip reason from resolveTransport\n */\n private handleSkip(device: GoveeDevice, command: string, reason: \"no-channel\" | \"override-cloud-missing\"): void {\n if (reason === \"override-cloud-missing\") {\n const prev = this.lastErrorByCategory.get(\"override-missing-cloud\") ?? null;\n this.lastErrorByCategory.set(\n \"override-missing-cloud\",\n logDedup(\n this.log,\n prev,\n `Cloud transport override for ${device.name}/${command} but no Cloud channel available`,\n new Error(\"override-cloud-missing\"),\n ),\n );\n return;\n }\n // no-channel: init-race or genuinely orphan device\n if (device.channels.cloud && !this.cloudClient) {\n this.log.debug(`Command for ${device.name} dropped: Cloud client not ready yet`);\n return;\n }\n this.log.warn(`No channel available for ${device.name} (${device.sku})`);\n }\n\n /**\n * Send a command to a device. Routing is decided up-front by\n * `resolveTransport`; segment-special-cases (segmentColor:N / segmentBatch /\n * segmentBrightness:N) have their own Cloud-side handlers because cloud\n * routing for batch segment ops goes through `sendSegmentBatchParsed`,\n * not `sendCloudCommand`.\n *\n * MQTT is status-push only and never used for commands.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n async sendCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n const decision = this.resolveTransport(device, command);\n\n // Diag-log: one line, marker derived from the actual decision (not the\n // configured channel). JSON.stringify keeps `[object Object]` out of\n // the trace for object-valued commands like segmentBatch.\n const summary = `${command}=${JSON.stringify(value)}`;\n this.onDiagLog?.(device.deviceId, \"debug\", `sendCommand ${summary} \u2192 ${this.decisionToChannelMarker(decision)}`);\n\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n\n // Segment-special cases \u2014 they bypass sendCloudCommand for Cloud sends\n // because the batch ops resolve their own capability set via\n // sendSegmentBatchParsed.\n if (command.startsWith(\"segmentColor:\")) {\n await this.dispatchSegmentColor(device, command, value, decision);\n return;\n }\n if (command === \"segmentBatch\") {\n await this.dispatchSegmentBatch(device, value, decision);\n return;\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n await this.dispatchSegmentBrightness(device, command, value, decision);\n return;\n }\n\n // Generic dispatch\n if (decision.kind === \"lan\") {\n this.sendLanCommand(device, command, value);\n return;\n }\n // decision.kind === \"cloud\"\n await this.sendCloudCommand(device, command, value);\n }\n\n /**\n * Segment-color dispatcher honouring the resolved transport decision.\n *\n * @param device Target device\n * @param command Command type (segmentColor:N form)\n * @param value Color value (hex string)\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentColor(\n device: GoveeDevice,\n command: string,\n value: unknown,\n decision: TransportDecision,\n ): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, [segIdx]);\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendCloudCommand(device, command, value);\n }\n }\n\n /**\n * Segment-batch dispatcher. LAN path issues one multi-segment ptReal\n * burst; Cloud path goes through `sendSegmentBatchParsed` which resolves\n * segment_color_setting + segment-brightness capabilities separately.\n *\n * @param device Target device\n * @param value Either a batch-syntax string or a pre-parsed object\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentBatch(device: GoveeDevice, value: unknown, decision: TransportDecision): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, \"segmentBatch\", decision.reason);\n return;\n }\n const parsed = typeof value === \"string\" ? this.parseSegmentBatch(device, value) : this.coerceParsedBatch(value);\n if (!parsed) {\n return;\n }\n this.onSegmentBatchUpdate?.(device, parsed);\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n if (parsed.color !== undefined) {\n const r = (parsed.color >> 16) & 0xff;\n const g = (parsed.color >> 8) & 0xff;\n const b = parsed.color & 0xff;\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, parsed.segments);\n }\n if (parsed.brightness !== undefined) {\n this.lanClient.setSegmentBrightness(device.lanIp, parsed.brightness, parsed.segments);\n }\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendSegmentBatchParsed(device, typeof value === \"string\" ? value : \"\", parsed);\n }\n }\n\n /**\n * Segment-brightness dispatcher honouring the resolved transport decision.\n *\n * @param device Target device\n * @param command Command type (segmentBrightness:N form)\n * @param value Brightness value (0-100)\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentBrightness(\n device: GoveeDevice,\n command: string,\n value: unknown,\n decision: TransportDecision,\n ): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n this.lanClient.setSegmentBrightness(device.lanIp, value as number, [segIdx]);\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendCloudCommand(device, command, value);\n }\n }\n\n /**\n * Send a generic capability command via Cloud API.\n * Used for capability types not explicitly handled (toggle, dynamic_scene, etc.)\n *\n * @param device Target device\n * @param capabilityType Full capability type (e.g. \"devices.capabilities.toggle\")\n * @param capabilityInstance Capability instance name (e.g. \"gradientToggle\")\n * @param value Command value\n */\n async sendCapabilityCommand(\n device: GoveeDevice,\n capabilityType: string,\n capabilityInstance: string,\n value: unknown,\n ): Promise<void> {\n if (!this.cloudClient || !device.channels.cloud) {\n this.log.debug(`Cloud not available for generic command on ${device.name}`);\n return;\n }\n\n const shortType = capabilityType.replace(\"devices.capabilities.\", \"\");\n let cloudValue: unknown = value;\n\n if (shortType === \"toggle\") {\n cloudValue = value ? 1 : 0;\n }\n\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n capabilityType,\n capabilityInstance,\n cloudValue,\n );\n };\n\n await this.executeRateLimited(execute);\n }\n\n /**\n * Send a batch segment command with pre-parsed data.\n *\n * @param device Target device\n * @param commandStr Original command string (for error messages)\n * @param parsed Pre-parsed batch data (null = invalid command)\n */\n private async sendSegmentBatchParsed(\n device: GoveeDevice,\n commandStr: string,\n parsed: { segments: number[]; color?: number; brightness?: number } | null,\n ): Promise<void> {\n if (!this.cloudClient) {\n return;\n }\n\n if (!parsed) {\n this.log.warn(`Invalid segment command \"${commandStr}\" for ${device.name}`);\n return;\n }\n\n const cap = this.findCapabilityForCommand(device, \"segmentColor:0\");\n if (!cap) {\n this.log.debug(`No segment capability for ${device.name}`);\n return;\n }\n\n if (parsed.color !== undefined) {\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, {\n segment: parsed.segments,\n rgb: parsed.color,\n });\n };\n await this.executeRateLimited(execute);\n }\n\n if (parsed.brightness !== undefined) {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n const brightCap = caps.find(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.includes(\"segment_color_setting\") &&\n c.instance.toLowerCase().includes(\"brightness\"),\n );\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n (brightCap ?? cap).type,\n (brightCap ?? cap).instance,\n { segment: parsed.segments, brightness: parsed.brightness },\n );\n };\n await this.executeRateLimited(execute);\n }\n\n // Update individual segment states to stay in sync\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n\n /**\n * Parse batch segment command string.\n *\n * @param device Target device (for segment count)\n * @param cmd Command string (e.g. \"1-5:#ff0000:20\")\n */\n parseSegmentBatch(\n device: GoveeDevice,\n cmd: string,\n ): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n // Defensive guard \u2014 non-string input (e.g. from internal caller passing\n // an already-parsed object) would crash cmd.split(). Treat as no-op.\n if (typeof cmd !== \"string\") {\n return null;\n }\n const parts = cmd.split(\":\");\n if (parts.length < 1 || !parts[0]) {\n return null;\n }\n\n // Effective physical segments: honor manual override for cut strips\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? new Set(device.manualSegments)\n : null;\n const segCount = device.segmentCount ?? 0;\n const isValid = (i: number): boolean => (validIndices ? validIndices.has(i) : i >= 0 && i < segCount);\n\n // Parse segment indices\n const segStr = parts[0].trim();\n let segments: number[];\n\n if (segStr === \"all\") {\n // \"all\" expands to valid physical segments only (skip cut ones)\n segments = validIndices\n ? Array.from(validIndices).sort((a, b) => a - b)\n : Array.from({ length: segCount }, (_, i) => i);\n } else {\n segments = [];\n for (const part of segStr.split(\",\")) {\n const rangeMatch = /^(\\d+)-(\\d+)$/.exec(part.trim());\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n for (let i = start; i <= end; i++) {\n if (isValid(i)) {\n segments.push(i);\n }\n }\n } else {\n const idx = parseInt(part.trim(), 10);\n if (!isNaN(idx) && isValid(idx)) {\n segments.push(idx);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n return null;\n }\n\n // Parse color (#RRGGBB \u2192 packed int)\n let color: number | undefined;\n if (parts.length >= 2 && parts[1]) {\n const colorStr = parts[1].trim();\n if (/^#?[0-9a-fA-F]{6}$/.test(colorStr)) {\n color = parseInt(colorStr.replace(\"#\", \"\"), 16);\n }\n }\n\n // Parse brightness (0-100)\n let brightness: number | undefined;\n if (parts.length >= 3 && parts[2]) {\n const bri = parseInt(parts[2].trim(), 10);\n if (!isNaN(bri) && bri >= 0 && bri <= 100) {\n brightness = bri;\n }\n }\n\n if (color === undefined && brightness === undefined) {\n return null;\n }\n\n return { segments, color, brightness };\n }\n\n /**\n * Coerce a pre-parsed batch object (from internal callers) to the canonical\n * shape. Returns null if the input is not a valid {segments, ...} object.\n *\n * @param value Candidate object\n */\n private coerceParsedBatch(value: unknown): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n if (!value || typeof value !== \"object\") {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (!Array.isArray(v.segments) || v.segments.length === 0) {\n return null;\n }\n const segments = v.segments.filter(n => typeof n === \"number\" && Number.isFinite(n) && n >= 0) as number[];\n if (segments.length === 0) {\n return null;\n }\n const color = typeof v.color === \"number\" && Number.isFinite(v.color) ? v.color & 0xffffff : undefined;\n const brightness =\n typeof v.brightness === \"number\" && Number.isFinite(v.brightness)\n ? Math.max(0, Math.min(100, Math.round(v.brightness)))\n : undefined;\n if (color === undefined && brightness === undefined) {\n return null;\n }\n return { segments, color, brightness };\n }\n\n /**\n * Convert adapter value to Cloud API value\n *\n * @param device Target device (for scene/snapshot lookup)\n * @param command Command type\n * @param value Adapter-side value to convert\n */\n toCloudValue(device: GoveeDevice, command: string, value: unknown): unknown {\n switch (command) {\n case \"power\":\n return value ? 1 : 0;\n case \"brightness\":\n return value;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n return (r << 16) | (g << 8) | b;\n }\n case \"colorTemperature\":\n return value;\n case \"scene\":\n return value;\n case \"gradientToggle\":\n // Govee toggle-cap expects 0/1, not boolean.\n return value ? 1 : 0;\n case \"lightScene\": {\n // Value is the dropdown index (string) \u2014 resolve to scene activation payload\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return value;\n }\n return device.scenes[idx - 1].value;\n }\n case \"diyScene\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return value;\n }\n return device.diyScenes[idx - 1].value;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return value;\n }\n return device.snapshots[idx - 1].value;\n }\n default:\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n const { r, g, b } = hexToRgb(value as string);\n return { segment: [segIdx], rgb: (r << 16) | (g << 8) | b };\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n return { segment: [segIdx], brightness: value };\n }\n return value;\n }\n }\n\n /**\n * Find capability matching a command name\n *\n * @param device Target device\n * @param command Command type to find capability for\n */\n findCapabilityForCommand(device: GoveeDevice, command: string): { type: string; instance: string } | undefined {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n continue;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n if (command === \"power\" && shortType === \"on_off\") {\n return cap;\n }\n if (command === \"brightness\" && shortType === \"range\" && cap.instance.toLowerCase().includes(\"brightness\")) {\n return cap;\n }\n if (command === \"colorRgb\" && shortType === \"color_setting\" && cap.instance === \"colorRgb\") {\n return cap;\n }\n if (command === \"colorTemperature\" && shortType === \"color_setting\" && cap.instance.includes(\"colorTem\")) {\n return cap;\n }\n if (command === \"scene\" && shortType === \"mode\" && cap.instance === \"presetScene\") {\n return cap;\n }\n if (command === \"lightScene\" && shortType === \"dynamic_scene\" && cap.instance === \"lightScene\") {\n return cap;\n }\n if (command === \"diyScene\" && shortType === \"dynamic_scene\" && cap.instance === \"diyScene\") {\n return cap;\n }\n if (command === \"snapshot\" && shortType === \"dynamic_scene\" && cap.instance === \"snapshot\") {\n return cap;\n }\n if (command === \"gradientToggle\" && shortType === \"toggle\" && cap.instance === \"gradientToggle\") {\n return cap;\n }\n if (\n command.startsWith(\"segmentColor:\") &&\n shortType === \"segment_color_setting\" &&\n !cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n if (\n command.startsWith(\"segmentBrightness:\") &&\n shortType === \"segment_color_setting\" &&\n cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n }\n return undefined;\n }\n\n /**\n * Send command via LAN UDP\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private sendLanCommand(device: GoveeDevice, command: string, value: unknown): void {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n\n switch (command) {\n case \"power\":\n this.lanClient.setPower(device.lanIp, value as boolean);\n break;\n case \"brightness\":\n this.lanClient.setBrightness(device.lanIp, value as number);\n break;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setColor(device.lanIp, r, g, b);\n break;\n }\n case \"colorTemperature\":\n this.lanClient.setColorTemperature(device.lanIp, value as number);\n break;\n case \"gradientToggle\":\n this.lanClient.setGradient(device.lanIp, value as boolean);\n break;\n case \"diyScene\": {\n // Try ptReal BLE-over-LAN if DIY scene is in library\n const diyIdx = parseInt(String(value), 10);\n if (isNaN(diyIdx) || diyIdx < 1 || diyIdx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return;\n }\n const diyScene = device.diyScenes[diyIdx - 1];\n if (diyScene) {\n const diyLib = device.diyLibrary.find(d => d.name === diyScene.name);\n if (diyLib) {\n this.log.debug(`ptReal DIY: ${diyScene.name} \u2192 code=${diyLib.diyCode}`);\n this.lanClient.setDiyScene(device.lanIp, diyLib.scenceParam ?? \"\");\n return;\n }\n }\n // No library match \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n case \"lightScene\": {\n // Try ptReal BLE-over-LAN if scene is in scene library.\n // The no-segments \u2192 Cloud heuristic now lives centrally in\n // resolveTransport.shouldHeuristicallyUseCloud \u2014 if we get here,\n // the device either has segments or is unknown to the catalog with\n // a registered scene library. Either way the local ptReal path is\n // worth trying.\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return;\n }\n const scene = device.scenes[idx - 1];\n if (scene) {\n // Match by exact name first, then by base name (strip -A/-B suffix)\n const baseName = scene.name.replace(/-[A-Z]$/, \"\");\n const libEntry =\n device.sceneLibrary.find(s => s.name === scene.name) ?? device.sceneLibrary.find(s => s.name === baseName);\n if (libEntry) {\n const baseParam = libEntry.scenceParam ?? \"\";\n let param = baseParam;\n if (\n device.sceneSpeed !== undefined &&\n device.sceneSpeed > 0 &&\n libEntry.speedInfo?.supSpeed &&\n libEntry.speedInfo.config\n ) {\n param = applySceneSpeed(param, device.sceneSpeed, libEntry.speedInfo.config);\n }\n this.log.debug(`ptReal: ${scene.name} \u2192 code=${libEntry.sceneCode}`);\n this.lanClient.setScene(device.lanIp, libEntry.sceneCode, param);\n return;\n }\n }\n // Scene not in library \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return;\n }\n const cmdGroups = device.snapshotBleCmds?.[idx - 1];\n if (cmdGroups && cmdGroups.length > 0) {\n const allPackets = cmdGroups.flat();\n if (allPackets.length > 0) {\n this.log.debug(`ptReal Snapshot: ${device.snapshots[idx - 1].name} \u2192 ${allPackets.length} packets`);\n this.lanClient.sendPtReal(device.lanIp, allPackets);\n return;\n }\n }\n // No BLE data \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n default:\n // LAN doesn't support this command \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n }\n }\n\n /**\n * Fire-and-forget Cloud fallback when a LAN-case can't service the\n * command locally (library miss, no BLE data, unsupported). Dedup\n * through the shared category map so log spam is bounded.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private cloudFallbackForCase(device: GoveeDevice, command: string, value: unknown): void {\n this.sendCloudCommand(device, command, value).catch(e => {\n const prev = this.lastErrorByCategory.get(\"cloud-fallback\") ?? null;\n this.lastErrorByCategory.set(\n \"cloud-fallback\",\n logDedup(this.log, prev, `Cloud fallback for ${device.name}/${command}`, e),\n );\n });\n }\n\n /**\n * Send command via Cloud API (rate-limited)\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private async sendCloudCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // M19 \u2014 Closure capture: lokale Variable nach Guard. Verhindert Race\n // wenn `setCloudClient(null)` zwischen Guard-Check und executeRateLimited\n // l\u00E4uft (z.B. Adapter-Stop mid-await).\n const cloudClient = this.cloudClient;\n if (!cloudClient) {\n return;\n }\n\n // Find the matching capability\n const cap = this.findCapabilityForCommand(device, command);\n if (!cap) {\n // M20 \u2014 dedup-warn statt nur debug. User klickt einen State, kein\n // Channel-Match \u2192 Fehlersuche braucht das Erstauftreten als warn.\n const prev = this.lastErrorByCategory.get(\"no-capability\") ?? null;\n this.lastErrorByCategory.set(\n \"no-capability\",\n logDedup(this.log, prev, `No channel for ${device.name}/${command}`, new Error(\"no matching capability\")),\n );\n return;\n }\n\n const cloudValue = this.toCloudValue(device, command, value);\n\n const execute = async (): Promise<void> => {\n await cloudClient.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, cloudValue);\n };\n\n await this.executeRateLimited(execute);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { hexToRgb, logDedup, type ErrorCategory, type GoveeDevice, type TimerAdapter } from \"./types\";\nimport type { GoveeCloudClient } from \"./govee-cloud-client\";\nimport type { GoveeLanClient } from \"./govee-lan-client\";\nimport { applySceneSpeed } from \"./govee-lan-client\";\nimport type { RateLimiter } from \"./rate-limiter\";\nimport { getDeviceQuirks, type ConfigurableOverrideCommand, type TransportTarget } from \"./device-registry\";\nimport { GOVEE_DEVICE_TYPE } from \"./govee-constants\";\n\n/**\n * Delay between switching the device into static-color mode and sending the\n * follow-up segment commands. Empirically the firmware needs ~150 ms for the\n * mode flip; shorter delays leave the device still in scene/music mode and the\n * subsequent segment writes are silently dropped.\n */\nconst FORCE_COLOR_MODE_SETTLE_MS = 150;\n\n/**\n * Outcome of `resolveTransport` \u2014 decides which channel handles a command\n * before any I/O happens. Carries the reason so diag-logs and tests can\n * tell an override-routed cloud send apart from a default cloud fallback.\n */\nexport type TransportDecision =\n | { kind: \"lan\"; reason: \"default\" }\n | {\n kind: \"cloud\";\n reason: \"override\" | \"no-lan\" | \"no-segments-heuristic\" | \"light-no-lan-fallback\";\n }\n | { kind: \"skip\"; reason: \"no-channel\" | \"override-cloud-missing\" };\n\n/**\n * Command router \u2014 routes device commands through the fastest available\n * channel: LAN \u2192 Cloud. Quirk-driven overrides (devices.json\n * `transportOverrides`) take precedence over the LAN-first default.\n */\nexport class CommandRouter {\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private lanClient: GoveeLanClient | null = null;\n private cloudClient: GoveeCloudClient | null = null;\n private rateLimiter: RateLimiter | null = null;\n /**\n * Per-category dedup tracker. Replaces the older split between\n * `lastCloudFallbackError` and `lastNoChannelCategory` \u2014 one map, one\n * lookup, keyed by a short category string (`cloud-fallback`,\n * `no-channel`, `override-missing-cloud`).\n */\n private lastErrorByCategory = new Map<string, ErrorCategory | null>();\n\n /** Callback for batch segment state sync */\n onSegmentBatchUpdate?: (\n device: GoveeDevice,\n batch: { segments: number[]; color?: number; brightness?: number },\n ) => void;\n\n /**\n * Optional diag-log hook fired once per `sendCommand` call so the per-device\n * diag ring buffer carries the channel-routing decision (\"LAN took it\",\n * \"Cloud fallback\", \"no channel available\"). Without this, the diag JSON\n * couldn't show why a user's state-write didn't reach the device.\n */\n onDiagLog?: (deviceId: string, level: \"debug\" | \"info\" | \"warn\", msg: string) => void;\n\n /**\n * @param log ioBroker logger\n * @param timers Adapter timer wrapper \u2014 routed through `this.setTimeout` so\n * pending color-mode delays get cleared on onUnload.\n */\n constructor(log: ioBroker.Logger, timers: TimerAdapter) {\n this.log = log;\n this.timers = timers;\n }\n\n /**\n * Register the LAN client\n *\n * @param client LAN UDP client instance\n */\n setLanClient(client: GoveeLanClient): void {\n this.lanClient = client;\n }\n\n /**\n * Register the Cloud client\n *\n * @param client Cloud API client instance\n */\n setCloudClient(client: GoveeCloudClient): void {\n this.cloudClient = client;\n }\n\n /**\n * Register the rate limiter for cloud calls\n *\n * @param limiter Rate limiter instance\n */\n setRateLimiter(limiter: RateLimiter): void {\n this.rateLimiter = limiter;\n }\n\n /**\n * Execute a function through the rate limiter if available, or directly.\n *\n * @param fn Async function to execute\n * @param priority Queue priority (0 = highest)\n */\n async executeRateLimited(fn: () => Promise<void>, priority = 0): Promise<void> {\n if (this.rateLimiter) {\n await this.rateLimiter.tryExecute(fn, priority);\n } else {\n await fn();\n }\n }\n\n /**\n * Force the device into static-color mode before sending segment_color_setting\n * ptReal packets. Without this, the device silently ignores segment-level\n * overrides while it's in Scene/Gradient/Music mode \u2014 the classic \"I set\n * segment 5 red and nothing happened\" symptom. Sends a `colorwc` command with\n * the device's last-known colorRgb (so the strip doesn't visibly change if it\n * was already in color mode), then waits 150 ms so the firmware can switch.\n *\n * As a bonus: once the device is in color mode, subsequent segment commands\n * trigger AA A5 MQTT pushes \u2014 so the adapter learns the real segmentCount\n * automatically the first time the user touches segment controls.\n *\n * @param device Target device\n */\n private async forceColorMode(device: GoveeDevice): Promise<void> {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n const current = typeof device.state.colorRgb === \"string\" ? device.state.colorRgb : null;\n const { r, g, b } = current ? hexToRgb(current) : { r: 255, g: 255, b: 255 };\n this.lanClient.setColor(device.lanIp, r, g, b);\n await this.timers.delay(FORCE_COLOR_MODE_SETTLE_MS);\n }\n\n /**\n * Look up the quirk-driven transport override for a (device, command) pair.\n * Segment-suffix commands (segmentColor:N / segmentBrightness:N) inherit\n * the segmentBatch override \u2014 devices.json carries one key for all segment\n * ops, not one per index.\n *\n * @param device Target device\n * @param command Command type\n */\n private lookupOverride(device: GoveeDevice, command: string): TransportTarget | undefined {\n const overrides = getDeviceQuirks(device.sku)?.transportOverrides;\n if (!overrides) {\n return undefined;\n }\n if (command in overrides) {\n return overrides[command as ConfigurableOverrideCommand];\n }\n if (command.startsWith(\"segmentColor:\") || command.startsWith(\"segmentBrightness:\")) {\n return overrides.segmentBatch;\n }\n return undefined;\n }\n\n /**\n * Catch for unkatalogisierte no-segment SKUs: when a lightScene activation\n * with scenceParam data hits a device that doesn't have any segments, the\n * A3-framed multi-packet ptReal protocol gets silently dropped by the\n * firmware. Cloud activation is the safer default. SKUs known to need\n * this go into devices.json `transportOverrides.lightScene = \"cloud\"` \u2014\n * the heuristic only fires for SKUs not (yet) in the catalog.\n *\n * @param device Target device\n * @param command Command type\n */\n private shouldHeuristicallyUseCloud(device: GoveeDevice, command: string): boolean {\n if (command !== \"lightScene\") {\n return false;\n }\n const hasSegments = typeof device.segmentCount === \"number\" && device.segmentCount > 0;\n return !hasSegments;\n }\n\n /**\n * Single point of truth for channel routing. Quirk-driven `transportOverrides`\n * take precedence over the LAN-first default. Returns a `TransportDecision`\n * carrying both the chosen kind and a reason \u2014 caller emits the reason\n * into the diag log so a cloud-override and a cloud-fallback aren't\n * confused in user-submitted JSON.\n *\n * @param device Target device\n * @param command Command type\n */\n resolveTransport(device: GoveeDevice, command: string): TransportDecision {\n const overrideTarget = this.lookupOverride(device, command);\n if (overrideTarget === \"cloud\") {\n if (device.channels.cloud && this.cloudClient) {\n return { kind: \"cloud\", reason: \"override\" };\n }\n return { kind: \"skip\", reason: \"override-cloud-missing\" };\n }\n // overrideTarget === \"lan\" is a no-op fall-through to default routing.\n\n if (device.lanIp && this.lanClient) {\n if (this.shouldHeuristicallyUseCloud(device, command)) {\n return device.channels.cloud && this.cloudClient\n ? { kind: \"cloud\", reason: \"no-segments-heuristic\" }\n : { kind: \"skip\", reason: \"no-channel\" };\n }\n return { kind: \"lan\", reason: \"default\" };\n }\n if (device.channels.cloud && this.cloudClient) {\n if (device.type === GOVEE_DEVICE_TYPE.LIGHT && !device.lanIp) {\n return { kind: \"cloud\", reason: \"light-no-lan-fallback\" };\n }\n return { kind: \"cloud\", reason: \"no-lan\" };\n }\n return { kind: \"skip\", reason: \"no-channel\" };\n }\n\n /**\n * Format a decision into a human-readable channel marker for the diag\n * log. One line per `sendCommand` so user-submitted JSON shows what the\n * router decided, not what it was nominally configured for.\n *\n * @param decision Output of resolveTransport\n */\n private decisionToChannelMarker(decision: TransportDecision): string {\n switch (decision.kind) {\n case \"lan\":\n return \"LAN\";\n case \"cloud\":\n if (decision.reason === \"light-no-lan-fallback\") {\n return \"Cloud (no LAN, fallback)\";\n }\n return decision.reason === \"override\"\n ? \"Cloud (override)\"\n : decision.reason === \"no-segments-heuristic\"\n ? \"Cloud (no-segments)\"\n : \"Cloud\";\n case \"skip\":\n return decision.reason === \"override-cloud-missing\" ? \"skip (cloud-override, no cloud)\" : \"skip (no-channel)\";\n }\n }\n\n /**\n * Skip-handler \u2014 emits the right log level depending on why we couldn't\n * route. Override+no-cloud is a configurable mismatch (user's fault, but\n * we tell them once); regular no-channel during init-race is debug.\n *\n * @param device Target device\n * @param command Command type\n * @param reason Skip reason from resolveTransport\n */\n private handleSkip(device: GoveeDevice, command: string, reason: \"no-channel\" | \"override-cloud-missing\"): void {\n if (reason === \"override-cloud-missing\") {\n const prev = this.lastErrorByCategory.get(\"override-missing-cloud\") ?? null;\n this.lastErrorByCategory.set(\n \"override-missing-cloud\",\n logDedup(\n this.log,\n prev,\n `Cloud transport override for ${device.name}/${command} but no Cloud channel available`,\n new Error(\"override-cloud-missing\"),\n ),\n );\n return;\n }\n // no-channel: init-race or genuinely orphan device\n if (device.channels.cloud && !this.cloudClient) {\n this.log.debug(`Command for ${device.name} dropped: Cloud client not ready yet`);\n return;\n }\n this.log.warn(`No channel available for ${device.name} (${device.sku})`);\n }\n\n /**\n * Send a command to a device. Routing is decided up-front by\n * `resolveTransport`; segment-special-cases (segmentColor:N / segmentBatch /\n * segmentBrightness:N) have their own Cloud-side handlers because cloud\n * routing for batch segment ops goes through `sendSegmentBatchParsed`,\n * not `sendCloudCommand`.\n *\n * MQTT is status-push only and never used for commands.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n async sendCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n const decision = this.resolveTransport(device, command);\n\n // Diag-log: one line, marker derived from the actual decision (not the\n // configured channel). JSON.stringify keeps `[object Object]` out of\n // the trace for object-valued commands like segmentBatch.\n const summary = `${command}=${JSON.stringify(value)}`;\n this.onDiagLog?.(device.deviceId, \"debug\", `sendCommand ${summary} \u2192 ${this.decisionToChannelMarker(decision)}`);\n\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n\n // Segment-special cases \u2014 they bypass sendCloudCommand for Cloud sends\n // because the batch ops resolve their own capability set via\n // sendSegmentBatchParsed.\n if (command.startsWith(\"segmentColor:\")) {\n await this.dispatchSegmentColor(device, command, value, decision);\n return;\n }\n if (command === \"segmentBatch\") {\n await this.dispatchSegmentBatch(device, value, decision);\n return;\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n await this.dispatchSegmentBrightness(device, command, value, decision);\n return;\n }\n\n // Generic dispatch\n if (decision.kind === \"lan\") {\n this.sendLanCommand(device, command, value);\n return;\n }\n // decision.kind === \"cloud\"\n await this.sendCloudCommand(device, command, value);\n }\n\n /**\n * Segment-color dispatcher honouring the resolved transport decision.\n *\n * @param device Target device\n * @param command Command type (segmentColor:N form)\n * @param value Color value (hex string)\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentColor(\n device: GoveeDevice,\n command: string,\n value: unknown,\n decision: TransportDecision,\n ): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, [segIdx]);\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendCloudCommand(device, command, value);\n }\n }\n\n /**\n * Segment-batch dispatcher. LAN path issues one multi-segment ptReal\n * burst; Cloud path goes through `sendSegmentBatchParsed` which resolves\n * segment_color_setting + segment-brightness capabilities separately.\n *\n * @param device Target device\n * @param value Either a batch-syntax string or a pre-parsed object\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentBatch(device: GoveeDevice, value: unknown, decision: TransportDecision): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, \"segmentBatch\", decision.reason);\n return;\n }\n const parsed = typeof value === \"string\" ? this.parseSegmentBatch(device, value) : this.coerceParsedBatch(value);\n if (!parsed) {\n return;\n }\n this.onSegmentBatchUpdate?.(device, parsed);\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n if (parsed.color !== undefined) {\n const r = (parsed.color >> 16) & 0xff;\n const g = (parsed.color >> 8) & 0xff;\n const b = parsed.color & 0xff;\n this.lanClient.setSegmentColor(device.lanIp, r, g, b, parsed.segments);\n }\n if (parsed.brightness !== undefined) {\n this.lanClient.setSegmentBrightness(device.lanIp, parsed.brightness, parsed.segments);\n }\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendSegmentBatchParsed(device, typeof value === \"string\" ? value : \"\", parsed);\n }\n }\n\n /**\n * Segment-brightness dispatcher honouring the resolved transport decision.\n *\n * @param device Target device\n * @param command Command type (segmentBrightness:N form)\n * @param value Brightness value (0-100)\n * @param decision Routing decision from resolveTransport\n */\n private async dispatchSegmentBrightness(\n device: GoveeDevice,\n command: string,\n value: unknown,\n decision: TransportDecision,\n ): Promise<void> {\n if (decision.kind === \"skip\") {\n this.handleSkip(device, command, decision.reason);\n return;\n }\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n return;\n }\n if (decision.kind === \"lan\" && device.lanIp && this.lanClient) {\n await this.forceColorMode(device);\n this.lanClient.setSegmentBrightness(device.lanIp, value as number, [segIdx]);\n return;\n }\n if (decision.kind === \"cloud\") {\n await this.sendCloudCommand(device, command, value);\n }\n }\n\n /**\n * Send a generic capability command via Cloud API.\n * Used for capability types not explicitly handled (toggle, dynamic_scene, etc.)\n *\n * @param device Target device\n * @param capabilityType Full capability type (e.g. \"devices.capabilities.toggle\")\n * @param capabilityInstance Capability instance name (e.g. \"gradientToggle\")\n * @param value Command value\n */\n async sendCapabilityCommand(\n device: GoveeDevice,\n capabilityType: string,\n capabilityInstance: string,\n value: unknown,\n ): Promise<void> {\n if (!this.cloudClient || !device.channels.cloud) {\n this.log.debug(`Cloud not available for generic command on ${device.name}`);\n return;\n }\n\n const shortType = capabilityType.replace(\"devices.capabilities.\", \"\");\n let cloudValue: unknown = value;\n\n if (shortType === \"toggle\") {\n cloudValue = value ? 1 : 0;\n }\n\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n capabilityType,\n capabilityInstance,\n cloudValue,\n );\n };\n\n await this.executeRateLimited(execute);\n }\n\n /**\n * Send a batch segment command with pre-parsed data.\n *\n * @param device Target device\n * @param commandStr Original command string (for error messages)\n * @param parsed Pre-parsed batch data (null = invalid command)\n */\n private async sendSegmentBatchParsed(\n device: GoveeDevice,\n commandStr: string,\n parsed: { segments: number[]; color?: number; brightness?: number } | null,\n ): Promise<void> {\n if (!this.cloudClient) {\n return;\n }\n\n if (!parsed) {\n this.log.warn(`Invalid segment command \"${commandStr}\" for ${device.name}`);\n return;\n }\n\n const cap = this.findCapabilityForCommand(device, \"segmentColor:0\");\n if (!cap) {\n this.log.debug(`No segment capability for ${device.name}`);\n return;\n }\n\n if (parsed.color !== undefined) {\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, {\n segment: parsed.segments,\n rgb: parsed.color,\n });\n };\n await this.executeRateLimited(execute);\n }\n\n if (parsed.brightness !== undefined) {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n const brightCap = caps.find(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.includes(\"segment_color_setting\") &&\n c.instance.toLowerCase().includes(\"brightness\"),\n );\n const execute = async (): Promise<void> => {\n await this.cloudClient!.controlDevice(\n device.sku,\n device.deviceId,\n (brightCap ?? cap).type,\n (brightCap ?? cap).instance,\n { segment: parsed.segments, brightness: parsed.brightness },\n );\n };\n await this.executeRateLimited(execute);\n }\n\n // Update individual segment states to stay in sync\n this.onSegmentBatchUpdate?.(device, parsed);\n }\n\n /**\n * Parse batch segment command string.\n *\n * @param device Target device (for segment count)\n * @param cmd Command string (e.g. \"1-5:#ff0000:20\")\n */\n parseSegmentBatch(\n device: GoveeDevice,\n cmd: string,\n ): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n // Defensive guard \u2014 non-string input (e.g. from internal caller passing\n // an already-parsed object) would crash cmd.split(). Treat as no-op.\n if (typeof cmd !== \"string\") {\n return null;\n }\n const parts = cmd.split(\":\");\n if (parts.length < 1 || !parts[0]) {\n return null;\n }\n\n // Effective physical segments: honor manual override for cut strips\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? new Set(device.manualSegments)\n : null;\n const segCount = device.segmentCount ?? 0;\n const isValid = (i: number): boolean => (validIndices ? validIndices.has(i) : i >= 0 && i < segCount);\n\n // Parse segment indices\n const segStr = parts[0].trim();\n let segments: number[];\n\n if (segStr === \"all\") {\n // \"all\" expands to valid physical segments only (skip cut ones)\n segments = validIndices\n ? Array.from(validIndices).sort((a, b) => a - b)\n : Array.from({ length: segCount }, (_, i) => i);\n } else {\n segments = [];\n for (const part of segStr.split(\",\")) {\n const rangeMatch = /^(\\d+)-(\\d+)$/.exec(part.trim());\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n for (let i = start; i <= end; i++) {\n if (isValid(i)) {\n segments.push(i);\n }\n }\n } else {\n const idx = parseInt(part.trim(), 10);\n if (!isNaN(idx) && isValid(idx)) {\n segments.push(idx);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n return null;\n }\n\n // Parse color (#RRGGBB \u2192 packed int)\n let color: number | undefined;\n if (parts.length >= 2 && parts[1]) {\n const colorStr = parts[1].trim();\n if (/^#?[0-9a-fA-F]{6}$/.test(colorStr)) {\n color = parseInt(colorStr.replace(\"#\", \"\"), 16);\n }\n }\n\n // Parse brightness (0-100)\n let brightness: number | undefined;\n if (parts.length >= 3 && parts[2]) {\n const bri = parseInt(parts[2].trim(), 10);\n if (!isNaN(bri) && bri >= 0 && bri <= 100) {\n brightness = bri;\n }\n }\n\n if (color === undefined && brightness === undefined) {\n return null;\n }\n\n return { segments, color, brightness };\n }\n\n /**\n * Coerce a pre-parsed batch object (from internal callers) to the canonical\n * shape. Returns null if the input is not a valid {segments, ...} object.\n *\n * @param value Candidate object\n */\n private coerceParsedBatch(value: unknown): {\n segments: number[];\n color?: number;\n brightness?: number;\n } | null {\n if (!value || typeof value !== \"object\") {\n return null;\n }\n const v = value as Record<string, unknown>;\n if (!Array.isArray(v.segments) || v.segments.length === 0) {\n return null;\n }\n const segments = v.segments.filter(n => typeof n === \"number\" && Number.isFinite(n) && n >= 0) as number[];\n if (segments.length === 0) {\n return null;\n }\n const color = typeof v.color === \"number\" && Number.isFinite(v.color) ? v.color & 0xffffff : undefined;\n const brightness =\n typeof v.brightness === \"number\" && Number.isFinite(v.brightness)\n ? Math.max(0, Math.min(100, Math.round(v.brightness)))\n : undefined;\n if (color === undefined && brightness === undefined) {\n return null;\n }\n return { segments, color, brightness };\n }\n\n /**\n * Convert adapter value to Cloud API value\n *\n * @param device Target device (for scene/snapshot lookup)\n * @param command Command type\n * @param value Adapter-side value to convert\n */\n toCloudValue(device: GoveeDevice, command: string, value: unknown): unknown {\n switch (command) {\n case \"power\":\n return value ? 1 : 0;\n case \"brightness\":\n return value;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n return (r << 16) | (g << 8) | b;\n }\n case \"colorTemperature\":\n return value;\n case \"scene\":\n return value;\n case \"gradientToggle\":\n // Govee toggle-cap expects 0/1, not boolean.\n return value ? 1 : 0;\n case \"lightScene\": {\n // Value is the dropdown index (string) \u2014 resolve to scene activation payload\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return value;\n }\n return device.scenes[idx - 1].value;\n }\n case \"diyScene\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return value;\n }\n return device.diyScenes[idx - 1].value;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return value;\n }\n return device.snapshots[idx - 1].value;\n }\n default:\n if (command.startsWith(\"segmentColor:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n const { r, g, b } = hexToRgb(value as string);\n return { segment: [segIdx], rgb: (r << 16) | (g << 8) | b };\n }\n if (command.startsWith(\"segmentBrightness:\")) {\n const segIdx = parseInt(command.split(\":\")[1], 10);\n if (isNaN(segIdx) || segIdx < 0) {\n this.log.warn(`${device.sku}: invalid segment index in ${command}`);\n return value;\n }\n return { segment: [segIdx], brightness: value };\n }\n return value;\n }\n }\n\n /**\n * Find capability matching a command name\n *\n * @param device Target device\n * @param command Command type to find capability for\n */\n findCapabilityForCommand(device: GoveeDevice, command: string): { type: string; instance: string } | undefined {\n const caps = Array.isArray(device.capabilities) ? device.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n continue;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n if (command === \"power\" && shortType === \"on_off\") {\n return cap;\n }\n if (command === \"brightness\" && shortType === \"range\" && cap.instance.toLowerCase().includes(\"brightness\")) {\n return cap;\n }\n if (command === \"colorRgb\" && shortType === \"color_setting\" && cap.instance === \"colorRgb\") {\n return cap;\n }\n if (command === \"colorTemperature\" && shortType === \"color_setting\" && cap.instance.includes(\"colorTem\")) {\n return cap;\n }\n if (command === \"scene\" && shortType === \"mode\" && cap.instance === \"presetScene\") {\n return cap;\n }\n if (command === \"lightScene\" && shortType === \"dynamic_scene\" && cap.instance === \"lightScene\") {\n return cap;\n }\n if (command === \"diyScene\" && shortType === \"dynamic_scene\" && cap.instance === \"diyScene\") {\n return cap;\n }\n if (command === \"snapshot\" && shortType === \"dynamic_scene\" && cap.instance === \"snapshot\") {\n return cap;\n }\n if (command === \"gradientToggle\" && shortType === \"toggle\" && cap.instance === \"gradientToggle\") {\n return cap;\n }\n if (\n command.startsWith(\"segmentColor:\") &&\n shortType === \"segment_color_setting\" &&\n !cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n if (\n command.startsWith(\"segmentBrightness:\") &&\n shortType === \"segment_color_setting\" &&\n cap.instance.toLowerCase().includes(\"brightness\")\n ) {\n return cap;\n }\n }\n return undefined;\n }\n\n /**\n * Send command via LAN UDP\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private sendLanCommand(device: GoveeDevice, command: string, value: unknown): void {\n if (!device.lanIp || !this.lanClient) {\n return;\n }\n\n switch (command) {\n case \"power\":\n this.lanClient.setPower(device.lanIp, value as boolean);\n break;\n case \"brightness\":\n this.lanClient.setBrightness(device.lanIp, value as number);\n break;\n case \"colorRgb\": {\n const { r, g, b } = hexToRgb(value as string);\n this.lanClient.setColor(device.lanIp, r, g, b);\n break;\n }\n case \"colorTemperature\":\n this.lanClient.setColorTemperature(device.lanIp, value as number);\n break;\n case \"gradientToggle\":\n this.lanClient.setGradient(device.lanIp, value as boolean);\n break;\n case \"diyScene\": {\n // Try ptReal BLE-over-LAN if DIY scene is in library\n const diyIdx = parseInt(String(value), 10);\n if (isNaN(diyIdx) || diyIdx < 1 || diyIdx > device.diyScenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return;\n }\n const diyScene = device.diyScenes[diyIdx - 1];\n if (diyScene) {\n const diyLib = device.diyLibrary.find(d => d.name === diyScene.name);\n if (diyLib) {\n this.log.debug(`ptReal DIY: ${diyScene.name} \u2192 code=${diyLib.diyCode}`);\n this.lanClient.setDiyScene(device.lanIp, diyLib.scenceParam ?? \"\");\n return;\n }\n }\n // No library match \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n case \"lightScene\": {\n // Try ptReal BLE-over-LAN if scene is in scene library.\n // The no-segments \u2192 Cloud heuristic now lives centrally in\n // resolveTransport.shouldHeuristicallyUseCloud \u2014 if we get here,\n // the device either has segments or is unknown to the catalog with\n // a registered scene library. Either way the local ptReal path is\n // worth trying.\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.scenes.length) {\n this.log.warn(`${device.sku}: invalid scene index ${String(value)}`);\n return;\n }\n const scene = device.scenes[idx - 1];\n if (scene) {\n // Match by exact name first, then by base name (strip -A/-B suffix)\n const baseName = scene.name.replace(/-[A-Z]$/, \"\");\n const libEntry =\n device.sceneLibrary.find(s => s.name === scene.name) ?? device.sceneLibrary.find(s => s.name === baseName);\n if (libEntry) {\n const baseParam = libEntry.scenceParam ?? \"\";\n let param = baseParam;\n if (\n device.sceneSpeed !== undefined &&\n device.sceneSpeed > 0 &&\n libEntry.speedInfo?.supSpeed &&\n libEntry.speedInfo.config\n ) {\n param = applySceneSpeed(param, device.sceneSpeed, libEntry.speedInfo.config);\n }\n this.log.debug(`ptReal: ${scene.name} \u2192 code=${libEntry.sceneCode}`);\n this.lanClient.setScene(device.lanIp, libEntry.sceneCode, param);\n return;\n }\n }\n // Scene not in library \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n case \"snapshot\": {\n const idx = parseInt(String(value), 10);\n if (isNaN(idx) || idx < 1 || idx > device.snapshots.length) {\n this.log.warn(`${device.sku}: invalid snapshot index ${String(value)}`);\n return;\n }\n const cmdGroups = device.snapshotBleCmds?.[idx - 1];\n if (cmdGroups && cmdGroups.length > 0) {\n const allPackets = cmdGroups.flat();\n if (allPackets.length > 0) {\n this.log.debug(`ptReal Snapshot: ${device.snapshots[idx - 1].name} \u2192 ${allPackets.length} packets`);\n this.lanClient.sendPtReal(device.lanIp, allPackets);\n return;\n }\n }\n // No BLE data \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n break;\n }\n default:\n // LAN doesn't support this command \u2014 fall through to Cloud\n this.cloudFallbackForCase(device, command, value);\n }\n }\n\n /**\n * Fire-and-forget Cloud fallback when a LAN-case can't service the\n * command locally (library miss, no BLE data, unsupported). Dedup\n * through the shared category map so log spam is bounded.\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private cloudFallbackForCase(device: GoveeDevice, command: string, value: unknown): void {\n this.sendCloudCommand(device, command, value).catch(e => {\n const prev = this.lastErrorByCategory.get(\"cloud-fallback\") ?? null;\n this.lastErrorByCategory.set(\n \"cloud-fallback\",\n logDedup(this.log, prev, `Cloud fallback for ${device.name}/${command}`, e),\n );\n });\n }\n\n /**\n * Send command via Cloud API (rate-limited)\n *\n * @param device Target device\n * @param command Command type\n * @param value Command value\n */\n private async sendCloudCommand(device: GoveeDevice, command: string, value: unknown): Promise<void> {\n // M19 \u2014 Closure capture: lokale Variable nach Guard. Verhindert Race\n // wenn `setCloudClient(null)` zwischen Guard-Check und executeRateLimited\n // l\u00E4uft (z.B. Adapter-Stop mid-await).\n const cloudClient = this.cloudClient;\n if (!cloudClient) {\n return;\n }\n\n // Find the matching capability\n const cap = this.findCapabilityForCommand(device, command);\n if (!cap) {\n // M20 \u2014 dedup-warn statt nur debug. User klickt einen State, kein\n // Channel-Match \u2192 Fehlersuche braucht das Erstauftreten als warn.\n const prev = this.lastErrorByCategory.get(\"no-capability\") ?? null;\n this.lastErrorByCategory.set(\n \"no-capability\",\n logDedup(this.log, prev, `No channel for ${device.name}/${command}`, new Error(\"no matching capability\")),\n );\n return;\n }\n\n const cloudValue = this.toCloudValue(device, command, value);\n\n const execute = async (): Promise<void> => {\n await cloudClient.controlDevice(device.sku, device.deviceId, cap.type, cap.instance, cloudValue);\n };\n\n await this.executeRateLimited(execute);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -32,7 +32,9 @@ function updateConnectionState(adapter) {
32
32
  var _a, _b, _c, _d;
33
33
  const devices = (_b = (_a = adapter.deviceManager) == null ? void 0 : _a.getDevices()) != null ? _b : [];
34
34
  const hasDevices = devices.length > 0;
35
- const anyOnline = devices.some((d) => d.state.online);
35
+ const anyOnline = devices.some(
36
+ (d) => d.state.online || d.type === import_govee_constants.GOVEE_DEVICE_TYPE.LIGHT && !d.lanIp && d.channels.cloud && adapter.cloudWasConnected
37
+ );
36
38
  const lanRunning = adapter.lanClient !== null;
37
39
  const connected = hasDevices ? anyOnline : lanRunning;
38
40
  if (connected !== adapter.lastConnectionState) {
@@ -134,7 +136,12 @@ function checkAllReady(adapter) {
134
136
  (_b = adapter.deviceManager) == null ? void 0 : _b.saveDevicesToCache();
135
137
  }
136
138
  function logDeviceSummary(adapter) {
137
- const parts = ["LAN \u2713"];
139
+ var _a, _b;
140
+ const allDevices = (_b = (_a = adapter.deviceManager) == null ? void 0 : _a.getDevices()) != null ? _b : [];
141
+ const lights = allDevices.filter((d) => d.type === import_govee_constants.GOVEE_DEVICE_TYPE.LIGHT);
142
+ const anyLightOnLan = lights.some((d) => d.lanIp);
143
+ const lanOk = lights.length === 0 || anyLightOnLan;
144
+ const parts = [lanOk ? "LAN \u2713" : "LAN \u2717"];
138
145
  if (adapter.cloudClient) {
139
146
  parts.push(adapter.cloudWasConnected ? "Cloud REST \u2713" : "Cloud REST \u2717");
140
147
  }
@@ -153,6 +160,16 @@ function logDeviceSummary(adapter) {
153
160
  const reason = adapter.mqttClient.getFailureReason();
154
161
  adapter.log.warn(reason ? `Lights Push: ${reason}` : `Lights Push: not connected \u2014 see earlier errors`);
155
162
  }
163
+ if (!lanOk) {
164
+ adapter.log.warn(
165
+ "LAN: no lights reachable on local network \u2014 cloud-only mode is ~100\xD7 slower (5-10s vs 50ms per command) and rate-limited (10/min). Enable the local API in the Govee Home app: https://app-h5.govee.com/user-manual/wlan-guide"
166
+ );
167
+ for (const d of lights) {
168
+ if (!d.lanIp) {
169
+ adapter.log.info(`${d.name} (${d.sku}): no LAN \u2014 enable the local API in the Govee Home app`);
170
+ }
171
+ }
172
+ }
156
173
  }
157
174
  // Annotate the CommonJS export names for ESM import in node:
158
175
  0 && (module.exports = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/handlers/connection-state.ts"],
4
- "sourcesContent": ["import type { DeviceManager } from \"../device-manager\";\nimport type { GoveeCloudClient } from \"../govee-cloud-client\";\nimport type { GoveeMqttClient } from \"../govee-mqtt-client\";\nimport type { GoveeOpenapiMqttClient } from \"../govee-openapi-mqtt-client\";\nimport { httpsRequest } from \"../http-client\";\nimport type { ChannelStatusSnapshot } from \"../log-prefix\";\nimport { errMessage } from \"../types\";\nimport { GOVEE_APP_VERSION } from \"../govee-constants\";\n\n/**\n * Adapter surface required by the connection-state helpers \u2014 covers the\n * info.connection bookkeeping plus ready-summary + app-version drift\n * monitoring + stale-device reaping.\n */\nexport interface ConnectionStateAdapter {\n readonly log: ioBroker.Logger;\n readonly deviceManager: DeviceManager | null;\n readonly cloudClient: GoveeCloudClient | null;\n readonly cloudWasConnected: boolean;\n readonly diagnosticsLastRun: Map<string, number>;\n readonly mqttClient: GoveeMqttClient | null;\n readonly openapiMqttClient: GoveeOpenapiMqttClient | null;\n readonly lanClient: unknown;\n readonly stateManager: { cleanupDevices(devices: unknown[]): Promise<unknown> } | null;\n readonly lanScanDone: boolean;\n readonly statesReady: boolean;\n readonly cloudInitDone: boolean;\n readonly appApiInitialPollDone: boolean;\n readyLogged: boolean;\n lastConnectionState: boolean | null;\n setStateAsync(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n}\n\n/**\n * Update global `info.connection` \u2014 the ioBroker-IDC indicator.\n *\n * Semantik:\n * - Mit Devices: `connected = true` wenn MIND. ein Device online ist.\n * Wenn alle offline \u2192 false (User sieht: kein Device antwortet).\n * - Ohne Devices: `connected = true` wenn der LAN-Stack l\u00E4uft. Sonst\n * false (z.B. EADDRINUSE oder bind-Fehler).\n *\n * Write-only-on-change cache (lastConnectionState) so we don't spam\n * setStateAsync on every device-state-update.\n *\n */\nexport function updateConnectionState(adapter: ConnectionStateAdapter): void {\n const devices = adapter.deviceManager?.getDevices() ?? [];\n const hasDevices = devices.length > 0;\n const anyOnline = devices.some(d => d.state.online);\n const lanRunning = adapter.lanClient !== null;\n const connected = hasDevices ? anyOnline : lanRunning;\n if (connected !== adapter.lastConnectionState) {\n adapter.lastConnectionState = connected;\n adapter.setStateAsync(\"info.connection\", { val: connected, ack: true }).catch(() => {});\n }\n\n // Sync the in-memory channelStatus snapshot used by the log-prefix wrapper.\n // Only flips between \"on\" and \"off\" \u2014 \"n/a\" (not configured) is set once\n // in onReady from config and never overridden here.\n const cs = (adapter as { channelStatus?: ChannelStatusSnapshot }).channelStatus;\n if (cs) {\n if (cs.lan !== \"n/a\") {\n cs.lan = hasDevices ? \"on\" : \"off\";\n }\n if (cs.cloud !== \"n/a\") {\n cs.cloud = adapter.cloudWasConnected ? \"on\" : \"off\";\n }\n if (cs.mqtt !== \"n/a\") {\n cs.mqtt = adapter.mqttClient?.connected ? \"on\" : \"off\";\n }\n if (cs.openapi !== \"n/a\") {\n cs.openapi = adapter.openapiMqttClient?.connected ? \"on\" : \"off\";\n }\n }\n}\n\n/**\n * Daily app-version-drift check vs. the iTunes app-store lookup.\n *\n * Govee's app2.govee.com endpoints reject very stale User-Agent strings.\n * Compares live iOS app version with local `GOVEE_APP_VERSION`. On drift\n * > 2 minor: warn-Log + state `info.appVersionDrift`. Failures (5xx,\n * network) are silent debug-logged \u2014 no user impact.\n *\n */\nexport async function checkAppVersionDrift(adapter: ConnectionStateAdapter): Promise<void> {\n try {\n const result = await httpsRequest<{ resultCount?: number; results?: Array<{ version?: string }> }>({\n method: \"GET\",\n url: \"https://itunes.apple.com/lookup?bundleId=com.ihoment.GoVeeSensor\",\n headers: { \"User-Agent\": \"ioBroker.govee-smart\" },\n timeout: 10_000,\n });\n const liveVersion = result.value?.results?.[0]?.version;\n if (typeof liveVersion !== \"string\" || liveVersion.length === 0) {\n return;\n }\n const localParts = GOVEE_APP_VERSION.split(\".\").map(Number);\n const liveParts = liveVersion.split(\".\").map(Number);\n const localMajor = localParts[0] ?? 0;\n const localMinor = localParts[1] ?? 0;\n const liveMajor = liveParts[0] ?? 0;\n const liveMinor = liveParts[1] ?? 0;\n const liveTotal = liveMajor * 100 + liveMinor;\n const localTotal = localMajor * 100 + localMinor;\n const driftMinor = liveTotal - localTotal;\n const driftMessage =\n driftMinor === 0\n ? `current (live=${liveVersion}, local=${GOVEE_APP_VERSION})`\n : driftMinor <= 2\n ? `minor drift (live=${liveVersion}, local=${GOVEE_APP_VERSION})`\n : `STALE (live=${liveVersion}, local=${GOVEE_APP_VERSION}) \u2014 bump GOVEE_APP_VERSION`;\n await adapter.setStateAsync(\"info.appVersionDrift\", { val: driftMessage, ack: true }).catch(() => undefined);\n if (driftMinor > 2) {\n adapter.log.warn(\n `Govee app version drift: live ${liveVersion} vs local ${GOVEE_APP_VERSION} \u2014 undocumented endpoints may start failing. Run sync-govee-app-version.py + release a new adapter version.`,\n );\n } else {\n adapter.log.debug(`App version: ${driftMessage}`);\n }\n } catch (e) {\n adapter.log.debug(`App version check failed: ${errMessage(e)}`);\n }\n}\n\n/**\n * Delete ioBroker objects for devices no longer present and drop the same\n * devices from adapter-level maps. Diagnostics-buffer + diagnosticsLastRun\n * are reaped so removed-device data doesn't leak into the next adapter\n * lifetime.\n *\n */\nexport async function reapStaleDevices(adapter: ConnectionStateAdapter): Promise<void> {\n if (!adapter.stateManager || !adapter.deviceManager) {\n return;\n }\n const currentDevices = adapter.deviceManager.getDevices();\n await adapter.stateManager.cleanupDevices(currentDevices);\n\n const liveDeviceIds = new Set(currentDevices.map(d => d.deviceId));\n adapter.deviceManager.getDiagnostics().pruneOrphans(liveDeviceIds);\n\n const liveKeys = new Set(currentDevices.map(d => `${d.sku}:${d.deviceId}`));\n for (const key of adapter.diagnosticsLastRun.keys()) {\n if (!liveKeys.has(key)) {\n adapter.diagnosticsLastRun.delete(key);\n }\n }\n}\n\n/**\n * Check if all configured channels are initialized and log ready message.\n * Called from MQTT onConnection callback and end of onReady.\n *\n */\nexport function checkAllReady(adapter: ConnectionStateAdapter): void {\n if (adapter.readyLogged) {\n return;\n }\n if (!adapter.lanScanDone) {\n return;\n }\n if (!adapter.statesReady) {\n return;\n }\n if (adapter.cloudClient && !adapter.cloudInitDone) {\n return;\n }\n if (adapter.mqttClient && !adapter.mqttClient.connected) {\n return;\n }\n if (adapter.openapiMqttClient && !adapter.openapiMqttClient.connected) {\n return;\n }\n if (adapter.deviceManager?.hasDeviceNeedingAppApi() && !adapter.appApiInitialPollDone) {\n return;\n }\n adapter.readyLogged = true;\n logDeviceSummary(adapter);\n // Persist any learned changes from the initial load (e.g. resolveSegmentCount\n // collapsing Cloud's 15 to the real 10 on H70D1). One-shot on first ready;\n // subsequent mutations persist themselves (MQTT bumps, wizard, manual-mode).\n adapter.deviceManager?.saveDevicesToCache();\n}\n\n/**\n * Log final ready message with device/group/channel summary.\n *\n */\nexport function logDeviceSummary(adapter: ConnectionStateAdapter): void {\n // Device/sensor/group counts are intentionally not logged here: at\n // ready-time the LAN scan and MQTT push are still settling, so an\n // \"X online, Y offline\" summary often shows lights as offline that\n // come up moments later. The user-visible online state lives in the\n // state tree where it stays accurate.\n //\n // Channel-Status (v2.10.1): nur konfigurierte channels werden gezeigt, mit\n // \u2713 (ready) oder \u2717 (init-Versuch gescheitert). Pro \u2717 folgt eine WARN-Zeile\n // mit konkretem Grund + Retry-Verhalten. Channel-Namen sind so umbenannt\n // dass User sie auseinanderhalten kann (Cloud REST vs Lights Push vs\n // Sensor Push \u2014 vorher hie\u00DF alles uneinheitlich \u201ECloud\", \u201EMQTT\",\n // \u201ECloud-events\").\n const parts: string[] = [\"LAN \u2713\"];\n if (adapter.cloudClient) {\n parts.push(adapter.cloudWasConnected ? \"Cloud REST \u2713\" : \"Cloud REST \u2717\");\n }\n if (adapter.mqttClient) {\n parts.push(adapter.mqttClient.connected ? \"Lights Push \u2713\" : \"Lights Push \u2717\");\n }\n if (adapter.openapiMqttClient) {\n parts.push(adapter.openapiMqttClient.connected ? \"Sensor Push \u2713\" : \"Sensor Push \u2717\");\n }\n adapter.log.info(`Govee adapter ready \u2014 ${parts.join(\" \")}`);\n\n if (adapter.cloudClient && !adapter.cloudWasConnected) {\n const reason = adapter.cloudClient.getFailureReason();\n adapter.log.warn(reason ? `Cloud REST: ${reason}` : `Cloud REST: not connected \u2014 see earlier errors`);\n }\n if (adapter.mqttClient && !adapter.mqttClient.connected) {\n const reason = adapter.mqttClient.getFailureReason();\n adapter.log.warn(reason ? `Lights Push: ${reason}` : `Lights Push: not connected \u2014 see earlier errors`);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { DeviceManager } from \"../device-manager\";\nimport type { GoveeCloudClient } from \"../govee-cloud-client\";\nimport type { GoveeMqttClient } from \"../govee-mqtt-client\";\nimport type { GoveeOpenapiMqttClient } from \"../govee-openapi-mqtt-client\";\nimport { httpsRequest } from \"../http-client\";\nimport type { ChannelStatusSnapshot } from \"../log-prefix\";\nimport { errMessage } from \"../types\";\nimport { GOVEE_APP_VERSION, GOVEE_DEVICE_TYPE } from \"../govee-constants\";\n\n/**\n * Adapter surface required by the connection-state helpers \u2014 covers the\n * info.connection bookkeeping plus ready-summary + app-version drift\n * monitoring + stale-device reaping.\n */\nexport interface ConnectionStateAdapter {\n readonly log: ioBroker.Logger;\n readonly deviceManager: DeviceManager | null;\n readonly cloudClient: GoveeCloudClient | null;\n readonly cloudWasConnected: boolean;\n readonly diagnosticsLastRun: Map<string, number>;\n readonly mqttClient: GoveeMqttClient | null;\n readonly openapiMqttClient: GoveeOpenapiMqttClient | null;\n readonly lanClient: unknown;\n readonly stateManager: { cleanupDevices(devices: unknown[]): Promise<unknown> } | null;\n readonly lanScanDone: boolean;\n readonly statesReady: boolean;\n readonly cloudInitDone: boolean;\n readonly appApiInitialPollDone: boolean;\n readyLogged: boolean;\n lastConnectionState: boolean | null;\n setStateAsync(id: string, state: ioBroker.SettableState | ioBroker.StateValue): Promise<unknown>;\n}\n\n/**\n * Update global `info.connection` \u2014 the ioBroker-IDC indicator.\n *\n * Semantik:\n * - Mit Devices: `connected = true` wenn MIND. ein Device online ist.\n * Wenn alle offline \u2192 false (User sieht: kein Device antwortet).\n * - Ohne Devices: `connected = true` wenn der LAN-Stack l\u00E4uft. Sonst\n * false (z.B. EADDRINUSE oder bind-Fehler).\n *\n * Write-only-on-change cache (lastConnectionState) so we don't spam\n * setStateAsync on every device-state-update.\n *\n */\nexport function updateConnectionState(adapter: ConnectionStateAdapter): void {\n const devices = adapter.deviceManager?.getDevices() ?? [];\n const hasDevices = devices.length > 0;\n const anyOnline = devices.some(\n d =>\n d.state.online ||\n (d.type === GOVEE_DEVICE_TYPE.LIGHT && !d.lanIp && d.channels.cloud && adapter.cloudWasConnected),\n );\n const lanRunning = adapter.lanClient !== null;\n const connected = hasDevices ? anyOnline : lanRunning;\n if (connected !== adapter.lastConnectionState) {\n adapter.lastConnectionState = connected;\n adapter.setStateAsync(\"info.connection\", { val: connected, ack: true }).catch(() => {});\n }\n\n // Sync the in-memory channelStatus snapshot used by the log-prefix wrapper.\n // Only flips between \"on\" and \"off\" \u2014 \"n/a\" (not configured) is set once\n // in onReady from config and never overridden here.\n const cs = (adapter as { channelStatus?: ChannelStatusSnapshot }).channelStatus;\n if (cs) {\n if (cs.lan !== \"n/a\") {\n cs.lan = hasDevices ? \"on\" : \"off\";\n }\n if (cs.cloud !== \"n/a\") {\n cs.cloud = adapter.cloudWasConnected ? \"on\" : \"off\";\n }\n if (cs.mqtt !== \"n/a\") {\n cs.mqtt = adapter.mqttClient?.connected ? \"on\" : \"off\";\n }\n if (cs.openapi !== \"n/a\") {\n cs.openapi = adapter.openapiMqttClient?.connected ? \"on\" : \"off\";\n }\n }\n}\n\n/**\n * Daily app-version-drift check vs. the iTunes app-store lookup.\n *\n * Govee's app2.govee.com endpoints reject very stale User-Agent strings.\n * Compares live iOS app version with local `GOVEE_APP_VERSION`. On drift\n * > 2 minor: warn-Log + state `info.appVersionDrift`. Failures (5xx,\n * network) are silent debug-logged \u2014 no user impact.\n *\n */\nexport async function checkAppVersionDrift(adapter: ConnectionStateAdapter): Promise<void> {\n try {\n const result = await httpsRequest<{ resultCount?: number; results?: Array<{ version?: string }> }>({\n method: \"GET\",\n url: \"https://itunes.apple.com/lookup?bundleId=com.ihoment.GoVeeSensor\",\n headers: { \"User-Agent\": \"ioBroker.govee-smart\" },\n timeout: 10_000,\n });\n const liveVersion = result.value?.results?.[0]?.version;\n if (typeof liveVersion !== \"string\" || liveVersion.length === 0) {\n return;\n }\n const localParts = GOVEE_APP_VERSION.split(\".\").map(Number);\n const liveParts = liveVersion.split(\".\").map(Number);\n const localMajor = localParts[0] ?? 0;\n const localMinor = localParts[1] ?? 0;\n const liveMajor = liveParts[0] ?? 0;\n const liveMinor = liveParts[1] ?? 0;\n const liveTotal = liveMajor * 100 + liveMinor;\n const localTotal = localMajor * 100 + localMinor;\n const driftMinor = liveTotal - localTotal;\n const driftMessage =\n driftMinor === 0\n ? `current (live=${liveVersion}, local=${GOVEE_APP_VERSION})`\n : driftMinor <= 2\n ? `minor drift (live=${liveVersion}, local=${GOVEE_APP_VERSION})`\n : `STALE (live=${liveVersion}, local=${GOVEE_APP_VERSION}) \u2014 bump GOVEE_APP_VERSION`;\n await adapter.setStateAsync(\"info.appVersionDrift\", { val: driftMessage, ack: true }).catch(() => undefined);\n if (driftMinor > 2) {\n adapter.log.warn(\n `Govee app version drift: live ${liveVersion} vs local ${GOVEE_APP_VERSION} \u2014 undocumented endpoints may start failing. Run sync-govee-app-version.py + release a new adapter version.`,\n );\n } else {\n adapter.log.debug(`App version: ${driftMessage}`);\n }\n } catch (e) {\n adapter.log.debug(`App version check failed: ${errMessage(e)}`);\n }\n}\n\n/**\n * Delete ioBroker objects for devices no longer present and drop the same\n * devices from adapter-level maps. Diagnostics-buffer + diagnosticsLastRun\n * are reaped so removed-device data doesn't leak into the next adapter\n * lifetime.\n *\n */\nexport async function reapStaleDevices(adapter: ConnectionStateAdapter): Promise<void> {\n if (!adapter.stateManager || !adapter.deviceManager) {\n return;\n }\n const currentDevices = adapter.deviceManager.getDevices();\n await adapter.stateManager.cleanupDevices(currentDevices);\n\n const liveDeviceIds = new Set(currentDevices.map(d => d.deviceId));\n adapter.deviceManager.getDiagnostics().pruneOrphans(liveDeviceIds);\n\n const liveKeys = new Set(currentDevices.map(d => `${d.sku}:${d.deviceId}`));\n for (const key of adapter.diagnosticsLastRun.keys()) {\n if (!liveKeys.has(key)) {\n adapter.diagnosticsLastRun.delete(key);\n }\n }\n}\n\n/**\n * Check if all configured channels are initialized and log ready message.\n * Called from MQTT onConnection callback and end of onReady.\n *\n */\nexport function checkAllReady(adapter: ConnectionStateAdapter): void {\n if (adapter.readyLogged) {\n return;\n }\n if (!adapter.lanScanDone) {\n return;\n }\n if (!adapter.statesReady) {\n return;\n }\n if (adapter.cloudClient && !adapter.cloudInitDone) {\n return;\n }\n if (adapter.mqttClient && !adapter.mqttClient.connected) {\n return;\n }\n if (adapter.openapiMqttClient && !adapter.openapiMqttClient.connected) {\n return;\n }\n if (adapter.deviceManager?.hasDeviceNeedingAppApi() && !adapter.appApiInitialPollDone) {\n return;\n }\n adapter.readyLogged = true;\n logDeviceSummary(adapter);\n // Persist any learned changes from the initial load (e.g. resolveSegmentCount\n // collapsing Cloud's 15 to the real 10 on H70D1). One-shot on first ready;\n // subsequent mutations persist themselves (MQTT bumps, wizard, manual-mode).\n adapter.deviceManager?.saveDevicesToCache();\n}\n\n/**\n * Log final ready message with device/group/channel summary.\n *\n */\nexport function logDeviceSummary(adapter: ConnectionStateAdapter): void {\n // Device/sensor/group counts are intentionally not logged here: at\n // ready-time the LAN scan and MQTT push are still settling, so an\n // \"X online, Y offline\" summary often shows lights as offline that\n // come up moments later. The user-visible online state lives in the\n // state tree where it stays accurate.\n //\n // Channel-Status (v2.10.1): nur konfigurierte channels werden gezeigt, mit\n // \u2713 (ready) oder \u2717 (init-Versuch gescheitert). Pro \u2717 folgt eine WARN-Zeile\n // mit konkretem Grund + Retry-Verhalten. Channel-Namen sind so umbenannt\n // dass User sie auseinanderhalten kann (Cloud REST vs Lights Push vs\n // Sensor Push \u2014 vorher hie\u00DF alles uneinheitlich \u201ECloud\", \u201EMQTT\",\n // \u201ECloud-events\").\n const allDevices = adapter.deviceManager?.getDevices() ?? [];\n const lights = allDevices.filter(d => d.type === GOVEE_DEVICE_TYPE.LIGHT);\n const anyLightOnLan = lights.some(d => d.lanIp);\n const lanOk = lights.length === 0 || anyLightOnLan;\n const parts: string[] = [lanOk ? \"LAN \u2713\" : \"LAN \u2717\"];\n if (adapter.cloudClient) {\n parts.push(adapter.cloudWasConnected ? \"Cloud REST \u2713\" : \"Cloud REST \u2717\");\n }\n if (adapter.mqttClient) {\n parts.push(adapter.mqttClient.connected ? \"Lights Push \u2713\" : \"Lights Push \u2717\");\n }\n if (adapter.openapiMqttClient) {\n parts.push(adapter.openapiMqttClient.connected ? \"Sensor Push \u2713\" : \"Sensor Push \u2717\");\n }\n adapter.log.info(`Govee adapter ready \u2014 ${parts.join(\" \")}`);\n\n if (adapter.cloudClient && !adapter.cloudWasConnected) {\n const reason = adapter.cloudClient.getFailureReason();\n adapter.log.warn(reason ? `Cloud REST: ${reason}` : `Cloud REST: not connected \u2014 see earlier errors`);\n }\n if (adapter.mqttClient && !adapter.mqttClient.connected) {\n const reason = adapter.mqttClient.getFailureReason();\n adapter.log.warn(reason ? `Lights Push: ${reason}` : `Lights Push: not connected \u2014 see earlier errors`);\n }\n if (!lanOk) {\n adapter.log.warn(\n \"LAN: no lights reachable on local network \u2014 cloud-only mode is ~100\u00D7 slower (5-10s vs 50ms per command) and rate-limited (10/min). Enable the local API in the Govee Home app: https://app-h5.govee.com/user-manual/wlan-guide\",\n );\n for (const d of lights) {\n if (!d.lanIp) {\n adapter.log.info(`${d.name} (${d.sku}): no LAN \u2014 enable the local API in the Govee Home app`);\n }\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.12.2",
4
+ "version": "2.12.3",
5
5
  "news": {
6
+ "2.12.3": {
7
+ "en": "Startup now shows honest LAN status (LAN ✗ when no lights reachable). Lights without local API fall back to cloud control instead of failing silently.",
8
+ "de": "Start zeigt jetzt ehrlichen LAN-Status (LAN ✗ wenn keine Lampen erreichbar). Lampen ohne lokale API nutzen Cloud-Steuerung statt still zu scheitern.",
9
+ "ru": "При запуске отображается реальный статус LAN (LAN ✗ если лампы недоступны). Лампы без локального API переключаются на облачное управление вместо тихого отказа.",
10
+ "pt": "Inicialização mostra status LAN real (LAN ✗ sem luminárias acessíveis). Luminárias sem API local usam controle por nuvem em vez de falhar silenciosamente.",
11
+ "nl": "Opstarten toont nu eerlijke LAN-status (LAN ✗ als geen lampen bereikbaar). Lampen zonder lokale API schakelen over naar cloudbesturing in plaats van stil te falen.",
12
+ "fr": "Le démarrage affiche un statut LAN honnête (LAN ✗ sans lampes accessibles). Les lampes sans API locale basculent sur le contrôle cloud au lieu d'échouer.",
13
+ "it": "L'avvio mostra lo stato LAN reale (LAN ✗ se nessuna lampada raggiungibile). Le lampade senza API locale usano il controllo cloud invece di fallire.",
14
+ "es": "El inicio muestra el estado LAN real (LAN ✗ sin lámparas accesibles). Las lámparas sin API local usan control por nube en lugar de fallar silenciosamente.",
15
+ "pl": "Start pokazuje rzeczywisty status LAN (LAN ✗ gdy brak lamp). Lampy bez lokalnego API przełączają się na sterowanie chmurowe zamiast cicho zawodzić.",
16
+ "uk": "Запуск показує реальний статус LAN (LAN ✗ коли лампи недоступні). Лампи без локального API переключаються на хмарне керування замість тихої відмови.",
17
+ "zh-cn": "启动时显示真实LAN状态(无灯具可用时显示 LAN ✗)。没有本地API的灯具现改用云控制,不再静默失败。"
18
+ },
6
19
  "2.12.2": {
7
20
  "en": "Verified against Node.js 24. Internal cleanup for stricter ioBroker repochecker compliance.",
8
21
  "de": "Getestet mit Node.js 24. Interne Bereinigung für strengere ioBroker-Repochecker-Konformität.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Czytelniejszy log adaptera: każdy podłączony kanał pokazuje ✓ lub ✗. Błędy cloud i sieci jako czytelne wiadomości z podpowiedzią retry, bez wnętrza Node.",
81
94
  "uk": "Зрозуміліший лог адаптера: кожен підключений канал показано як ✓ або ✗. Помилки cloud/мережі — читаємі повідомлення з підказкою retry, без внутрішніх деталей Node.",
82
95
  "zh-cn": "更清晰的适配器日志:每个已连接通道显示 ✓ 或 ✗。云端和网络错误以可读消息显示,带重试提示,不再有 Node 内部细节。"
83
- },
84
- "2.10.0": {
85
- "en": "Snapshots on Curtain Lights (H70B3), Christmas Strings (H70C5) and Outdoor Neon (H61A8) now work; matrix-light scenes too. New quirks system: device-fixes ship via catalog, no release needed.",
86
- "de": "Snapshots auf Curtain Lights (H70B3), Christmas Strings (H70C5) und Outdoor Neon (H61A8) funktionieren; Matrix-Light-Szenen auch. Neues Quirks-System: Geräte-Fixes via Katalog statt Release.",
87
- "ru": "Снимки на Curtain Lights (H70B3), Christmas Strings (H70C5) и Outdoor Neon (H61A8) работают; и сцены на матричных. Новая система квирков: правки по каталогу, без релиза.",
88
- "pt": "Snapshots em Curtain Lights (H70B3), Christmas Strings (H70C5), Outdoor Neon (H61A8) funcionam; cenas matrix também. Novo sistema de quirks: correções via catálogo, sem release.",
89
- "nl": "Snapshots op Curtain Lights (H70B3), Christmas Strings (H70C5) en Outdoor Neon (H61A8) werken; matrix-scènes ook. Nieuw quirks-systeem: device-fixes via catalogus, geen release.",
90
- "fr": "Snapshots sur Curtain Lights (H70B3), Christmas Strings (H70C5) et Outdoor Neon (H61A8) fonctionnent ; scènes matrix aussi. Nouveau système quirks : fixes via catalogue, sans release.",
91
- "it": "Snapshot su Curtain Lights (H70B3), Christmas Strings (H70C5) e Outdoor Neon (H61A8) ok; scene matrix anche. Nuovo sistema quirks: fix via catalogo, senza release.",
92
- "es": "Snapshots en Curtain Lights (H70B3), Christmas Strings (H70C5) y Outdoor Neon (H61A8) funcionan; escenas matrix también. Nuevo sistema quirks: arreglos por catálogo, sin release.",
93
- "pl": "Snapshoty na Curtain Lights (H70B3), Christmas Strings (H70C5) i Outdoor Neon (H61A8) działają; sceny matrix też. Nowy system quirks: poprawki przez katalog, bez releasu.",
94
- "uk": "Знімки на Curtain Lights (H70B3), Christmas Strings (H70C5) і Outdoor Neon (H61A8) працюють; сцени матричних теж. Нова система квірків: правки через каталог, без релізу.",
95
- "zh-cn": "Curtain Lights (H70B3)、Christmas Strings (H70C5) 和 Outdoor Neon (H61A8) 的快照可用;矩阵场景也是。新增 quirks 系统:设备修复通过目录,无需新版本。"
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.12.2",
3
+ "version": "2.12.3",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",