iobroker.govee-smart 2.12.0 → 2.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/build/lib/command-router.js +1 -1
- package/build/lib/command-router.js.map +2 -2
- package/build/lib/govee-mqtt-client.js +2 -2
- package/build/lib/govee-mqtt-client.js.map +2 -2
- package/build/lib/message-router.js +4 -2
- package/build/lib/message-router.js.map +2 -2
- package/build/lib/test-helpers.js +2 -1
- package/build/lib/test-helpers.js.map +2 -2
- package/build/lib/types.js.map +1 -1
- package/io-package.json +27 -27
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -98,11 +98,11 @@ All ports are fixed by the Govee LAN protocol and cannot be changed.
|
|
|
98
98
|
|
|
99
99
|
Each device shows where its model sits in the catalogue under `diag.tier`:
|
|
100
100
|
|
|
101
|
-
| Tier | Meaning
|
|
102
|
-
| ------------ |
|
|
103
|
-
| **verified** | Confirmed on real hardware — known per-SKU corrections active.
|
|
104
|
-
| **reported** | Community-reported, treated as verified.
|
|
105
|
-
| **seed** | Beta. Known per-SKU corrections only apply when **Enable experimental device support** is on in adapter settings.
|
|
101
|
+
| Tier | Meaning |
|
|
102
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| **verified** | Confirmed on real hardware — known per-SKU corrections active. |
|
|
104
|
+
| **reported** | Community-reported, treated as verified. |
|
|
105
|
+
| **seed** | Beta. Known per-SKU corrections only apply when **Enable experimental device support** is on in adapter settings. |
|
|
106
106
|
| **unknown** | The model isn't in the catalogue yet. Press `diag.export` on the device and post the resulting JSON in a GitHub issue so it can be added. |
|
|
107
107
|
|
|
108
108
|
The adapter writes one log line per model on startup if the model is `seed` (without the toggle) or `unknown` — once per startup, not on every reconnect.
|
|
@@ -124,10 +124,19 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
126
|
## Changelog
|
|
127
|
+
|
|
127
128
|
<!--
|
|
128
129
|
Placeholder for the next version (at the beginning of the line):
|
|
129
130
|
### **WORK IN PROGRESS**
|
|
130
131
|
-->
|
|
132
|
+
### 2.12.2 (2026-05-20)
|
|
133
|
+
|
|
134
|
+
- Verified against Node.js 24. Internal cleanup for stricter ioBroker repochecker compliance.
|
|
135
|
+
|
|
136
|
+
### 2.12.1 (2026-05-19)
|
|
137
|
+
|
|
138
|
+
- Code quality enforced with standard formatting.
|
|
139
|
+
|
|
131
140
|
### 2.12.0 (2026-05-17)
|
|
132
141
|
|
|
133
142
|
- Removed unused `info.legacyMqttCleaned` datapoint (internal migration marker, no user value)
|
|
@@ -144,15 +153,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
|
|
|
144
153
|
- sendTo calls with an unknown command no longer hang in the admin — the adapter answers with a clear error.
|
|
145
154
|
- Cleaner roles for ice-bucket / motion / dirt / water-tank sensors so they show up correctly in vis and smart-home integrations.
|
|
146
155
|
|
|
147
|
-
### 2.10.1 (2026-05-14)
|
|
148
|
-
|
|
149
|
-
- Cleaner adapter log: each connected channel now shows ✓ or ✗ status. Cloud and network errors come as readable user-messages with a retry hint instead of raw Node internals.
|
|
150
|
-
|
|
151
|
-
### 2.10.0 (2026-05-14)
|
|
152
|
-
|
|
153
|
-
- Snapshots on Curtain Lights (H70B3), Christmas Strings (H70C5) and Outdoor Neon (H61A8) now work; matrix-light scenes too.
|
|
154
|
-
- New per-SKU quirks system: device-specific fixes ship via a one-line catalog entry, no full adapter release needed.
|
|
155
|
-
|
|
156
156
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
157
157
|
|
|
158
158
|
## Support
|
|
@@ -196,4 +196,4 @@ SOFTWARE.
|
|
|
196
196
|
|
|
197
197
|
---
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
*Developed with assistance from Claude.ai*
|
|
@@ -114,7 +114,7 @@ class CommandRouter {
|
|
|
114
114
|
const current = typeof device.state.colorRgb === "string" ? device.state.colorRgb : null;
|
|
115
115
|
const { r, g, b } = current ? (0, import_types.hexToRgb)(current) : { r: 255, g: 255, b: 255 };
|
|
116
116
|
this.lanClient.setColor(device.lanIp, r, g, b);
|
|
117
|
-
await
|
|
117
|
+
await this.timers.delay(FORCE_COLOR_MODE_SETTLE_MS);
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
120
|
* Look up the quirk-driven transport override for a (device, command) pair.
|
|
@@ -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 // Delay routed through the adapter's timer wrapper so it gets cancelled\n // if the adapter unloads mid-delay. Native setTimeout would leave a\n // pending handle that fires into a half-torn-down adapter.\n await new Promise<void>(resolve => this.timers.setTimeout(() => resolve(), FORCE_COLOR_MODE_SETTLE_MS));\n }\n\n /**\n * 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\";\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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -687,8 +687,8 @@ class GoveeMqttClient {
|
|
|
687
687
|
const now = Date.now();
|
|
688
688
|
const elapsed = now - this.lastVerificationRequestMs;
|
|
689
689
|
if (this.lastVerificationRequestMs > 0 && elapsed < import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS) {
|
|
690
|
-
const
|
|
691
|
-
throw new Error(`Verification code request throttled \u2014 wait ${
|
|
690
|
+
const throttleRemainingSec = Math.ceil((import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS - elapsed) / 1e3);
|
|
691
|
+
throw new Error(`Verification code request throttled \u2014 wait ${throttleRemainingSec}s before retrying.`);
|
|
692
692
|
}
|
|
693
693
|
this.lastVerificationRequestMs = now;
|
|
694
694
|
const url = "https://app2.govee.com/account/rest/account/v1/verification";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/govee-mqtt-client.ts"],
|
|
4
|
-
"sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport { MQTT_MAX_AUTH_FAILURES, VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and either an op.command hex string (BLE bytes) or the\n * raw JSON envelope. The hook is responsible for forwarding to a\n * DiagnosticsCollector if one is set up. v2.9.1: rawJson is always\n * forwarded so state-only pushes (no op.command) are captured too \u2014\n * old shape only fired on op.command BLE strings.\n */\n private onPacket: ((deviceId: string, topic: string, payload: { hex?: string; rawJson?: string }) => void) | null =\n null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MQTT_MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Dump the full response body (capped) so a bug report with\n // debug log shows exactly what Govee returned \u2014 useful when\n // they change error semantics or add new status codes.\n this.log.debug(`MQTT login error response body: ${JSON.stringify(loginResp).slice(0, 300)}`);\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to\n // clear the settings field AND clear the in-memory copy so a\n // subsequent reconnect-login doesn't replay a now-consumed code (Govee\n // would reject it as `Verification code invalid` and trip the\n // VERIFICATION_FAILED branch unnecessarily).\n if (codeWasSent) {\n this.verificationCode = \"\";\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings`);\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: verification code rejected \u2014 request a fresh code`);\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n this.log.warn(`MQTT not connected: login rejected \u2014 check email/password`);\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n const rawText = payload.toString();\n try {\n const raw = JSON.parse(rawText) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device) {\n // v2.9.1 \u2014 always forward the raw envelope so state-only pushes\n // (state with no op.command) are visible in diag. Previously the\n // hook only fired on op.command BLE bytes \u2014 state changes from\n // the Govee app / physical remote went uncaptured.\n if (Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, { hex: cmd, rawJson: rawText });\n }\n }\n } else {\n this.onPacket(device, topic, { rawJson: rawText });\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${rawText.slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings + the raw JSON envelope into\n * the DiagnosticsCollector for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, {hex?, rawJson?})\n */\n setPacketHook(\n cb: ((deviceId: string, topic: string, payload: { hex?: string; rawJson?: string }) => void) | null,\n ): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n // disposed-check guards the reconnect path against schedule-after-stop:\n // disconnect() clears the timer, but a close-event-driven scheduleReconnect\n // could fire AFTER disconnect() returned, leaving a future-firing timer.\n if (this.disposed) {\n return;\n }\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.disposed) {\n return;\n }\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n const wasCached = this.persistedAttemptInFlight;\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n const broker = this.persisted?.iotEndpoint ?? \"?\";\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n const authMode = wasCached ? \"cached\" : \"fresh\";\n if (this.lastErrorCategory) {\n this.log.info(`MQTT connection restored: broker=${broker} clientId=${clientId} authMode=${authMode}`);\n this.lastErrorCategory = null;\n } else {\n // Initial connect is implicit in the ready-message (\"channels:\n // LAN+Cloud+MQTT+...\"), so this stays debug \u2014 only the recovery\n // path above earns an info-level event.\n this.log.debug(`MQTT connected: broker=${broker} clientId=${clientId} authMode=${authMode}`);\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n // Subscribe-fail is rare (AWS-IoT policy mismatch, account flagged)\n // but the TCP connection stays alive \u2014 protocol-level keepalive\n // pings keep answering, so `close` would never fire on its own.\n // Without forcing a close, `info.mqttConnected` would stay `false`\n // indefinitely with no reconnect \u2014 permanent silent death.\n // Forcing the close triggers the close-handler \u2192 scheduleReconnect.\n this.log.warn(`MQTT subscribe failed: ${err.message} \u2014 forcing reconnect`);\n try {\n this.client?.end(true);\n } catch {\n // ignore \u2014 close-event handler will pick it up either way\n }\n } else {\n this.log.debug(`MQTT subscribed to account topic: topic=${this.accountTopic} qos=0`);\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private async login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n const result = await this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n if (!result.value) {\n throw new Error(\n `Govee login returned no body (status=${result.statusCode}${result.fallback ? `, fallback=${result.fallback}` : \"\"}${result.bodySnippet ? `, body=${JSON.stringify(result.bodySnippet)}` : \"\"})`,\n );\n }\n return result.value;\n }\n\n /**\n * Last time `requestVerificationCode` actually issued a request \u2014 guards against\n * Govee marking the account as suspicious from rapid-fire user clicks.\n */\n private lastVerificationRequestMs = 0;\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * In-memory throttle of {@link VERIFICATION_REQUEST_THROTTLE_MS} (30 s)\n * mirrors the message-router's user-side throttle but lives here too so\n * other entry points (programmatic restarts, future scripted access)\n * can't bypass it.\n *\n * Throws on non-200, network failure or throttle-block so the caller\n * (onMessage handler) can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const now = Date.now();\n const elapsed = now - this.lastVerificationRequestMs;\n if (this.lastVerificationRequestMs > 0 && elapsed < VERIFICATION_REQUEST_THROTTLE_MS) {\n const waitSec = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - elapsed) / 1000);\n throw new Error(`Verification code request throttled \u2014 wait ${waitSec}s before retrying.`);\n }\n this.lastVerificationRequestMs = now;\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private async getIotKey(): Promise<GoveeIotKeyResponse> {\n const result = await this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n if (!result.value) {\n throw new Error(\n `Govee IoT-key request returned no body (status=${result.statusCode}${result.fallback ? `, fallback=${result.fallback}` : \"\"})`,\n );\n }\n return result.value;\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as crypto from \"node:crypto\";\nimport * as forge from \"node-forge\";\nimport * as mqtt from \"mqtt\";\nimport { httpsRequest, type HttpsRequestFn } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-constants\";\nimport { MQTT_MAX_AUTH_FAILURES, VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\nimport {\n classifyError,\n logDedup,\n type ErrorCategory,\n type GoveeIotKeyResponse,\n type GoveeLoginResponse,\n type MqttStatusUpdate,\n type PersistedMqttCredentials,\n type TimerAdapter,\n errMessage,\n} from \"./types\";\n\nconst LOGIN_URL = \"https://app2.govee.com/account/rest/account/v2/login\";\nconst IOT_KEY_URL = \"https://app2.govee.com/app/v1/account/iot/key\";\n\n/** Amazon Root CA 1 \u2014 required for AWS IoT Core TLS */\nconst AMAZON_ROOT_CA1 = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----`;\n\n/**\n * Signature f\u00FCr `mqtt.connect`-Factory \u2014 Tests k\u00F6nnen einen FakeMqttClient\n * injizieren ohne die echte Network-Lib zu starten. Default = `mqtt.connect`.\n */\nexport type MqttConnectFn = (url: string, opts: mqtt.IClientOptions) => mqtt.MqttClient;\n\n/** Callback for MQTT status updates */\nexport type MqttStatusCallback = (update: MqttStatusUpdate) => void;\n\n/** Callback for MQTT connection state changes */\nexport type MqttConnectionCallback = (connected: boolean) => void;\n\n/** Callback fired each time the login hands us a fresh bearer token */\nexport type MqttTokenCallback = (token: string) => void;\n\n/**\n * Govee AWS IoT MQTT client for real-time status and control.\n * Authenticates via Govee account, connects to AWS IoT Core with mutual TLS.\n */\nexport class GoveeMqttClient {\n private readonly email: string;\n private readonly password: string;\n private readonly log: ioBroker.Logger;\n private readonly timers: TimerAdapter;\n private readonly httpsRequestImpl: HttpsRequestFn;\n private readonly mqttConnectImpl: MqttConnectFn;\n private client: mqtt.MqttClient | null = null;\n private accountTopic = \"\";\n private _bearerToken = \"\";\n private accountId = \"\";\n /**\n * Stable session UUID, generated once per adapter process.\n * AWS IoT uses the clientId to track connection ownership \u2014 reusing the\n * same id on reconnect lets the broker cleanly take over from a stale\n * socket instead of refusing a new connection while the old one lingers.\n */\n private readonly sessionUuid: string = crypto.randomUUID();\n private reconnectTimer: ioBroker.Timeout | undefined = undefined;\n private reconnectAttempts = 0;\n private authFailCount = 0;\n private lastErrorCategory: ErrorCategory | null = null;\n private onStatus: MqttStatusCallback | null = null;\n private onConnection: MqttConnectionCallback | null = null;\n private onToken: MqttTokenCallback | null = null;\n /**\n * Diagnostics hook \u2014 called for each parsed message with the device id,\n * source topic and either an op.command hex string (BLE bytes) or the\n * raw JSON envelope. The hook is responsible for forwarding to a\n * DiagnosticsCollector if one is set up. v2.9.1: rawJson is always\n * forwarded so state-only pushes (no op.command) are captured too \u2014\n * old shape only fired on op.command BLE strings.\n */\n private onPacket: ((deviceId: string, topic: string, payload: { hex?: string; rawJson?: string }) => void) | null =\n null;\n\n /**\n * Set true in disconnect(); refreshBearerSilently bails as first step\n * if true, so timers that fire after dispose are no-ops.\n */\n private disposed = false;\n\n /** Account-derived client ID (UUIDv5(email)) \u2014 stable per account, distinct per user. */\n private readonly clientId: string;\n\n /** Optional 2FA code \u2014 set once after a 454, sent in the next login body, then cleared. */\n private verificationCode: string = \"\";\n\n /** Fired after a successful login that consumed a verification code, so the adapter can blank the settings field. */\n private onVerificationConsumed: (() => void) | null = null;\n\n /** Fired on 454 (pending) or 455 (failed) so the adapter can surface the actionable warning + auto-clear the code on failed. */\n private onVerificationFailed: ((reason: \"pending\" | \"failed\") => void) | null = null;\n\n /**\n * @param email Govee account email\n * @param password Govee account password\n * @param log ioBroker logger\n * @param timers Timer adapter\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n * @param mqttConnectImpl optional DI f\u00FCr Tests \u2014 Default ist die echte mqtt.connect\n */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n mqttConnectImpl: MqttConnectFn = mqtt.connect,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\n this.mqttConnectImpl = mqttConnectImpl;\n this.clientId = deriveGoveeClientId(email);\n }\n\n /**\n * Set the optional 2FA verification code. Empty string clears it.\n *\n * @param code Code from the Govee verification email\n */\n setVerificationCode(code: string): void {\n this.verificationCode = (code ?? \"\").trim();\n }\n\n /**\n * Hook called when a login successfully consumed a verification code.\n * Adapter wires this to clear the settings field.\n *\n * @param cb Callback\n */\n setOnVerificationConsumed(cb: (() => void) | null): void {\n this.onVerificationConsumed = cb;\n }\n\n /**\n * Hook called when Govee returned 454 (pending) or 455 (failed). Reason\n * lets the adapter clear the settings field on `failed` and prompt the\n * user to request a code on `pending`.\n *\n * @param cb Callback\n */\n setOnVerificationFailed(cb: ((reason: \"pending\" | \"failed\") => void) | null): void {\n this.onVerificationFailed = cb;\n }\n\n /** Bearer token from login \u2014 available after connect, used for undocumented API */\n get token(): string {\n return this._bearerToken;\n }\n\n /**\n * Short user-facing reason for \"MQTT not connected\", or null if the\n * client has never seen an error. Used by the adapter ready-summary\n * to give a concrete message instead of \"still pending\".\n */\n getFailureReason(): string | null {\n if (this.connected) {\n return null;\n }\n switch (this.lastErrorCategory) {\n case \"VERIFICATION_PENDING\":\n return \"Govee asked for verification \u2014 request a code in adapter settings\";\n case \"VERIFICATION_FAILED\":\n return \"verification code rejected \u2014 request a fresh code\";\n case \"AUTH\":\n return this.authFailCount >= MQTT_MAX_AUTH_FAILURES\n ? \"login rejected \u2014 check email/password\"\n : \"login failed (will retry)\";\n case \"RATE_LIMIT\":\n return \"rate-limited by Govee \u2014 will retry\";\n case \"NETWORK\":\n return \"cannot reach Govee servers \u2014 will retry\";\n case \"TIMEOUT\":\n return \"connection timeout \u2014 will retry\";\n case \"UNKNOWN\":\n return \"login rejected \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /** Persisted credentials from a previous run; null until setPersistedCredentials() is called. */\n private persisted: PersistedMqttCredentials | null = null;\n /** Hook fired after a successful login so the adapter can persist the new credentials. */\n private onCredentialsRefresh: ((creds: PersistedMqttCredentials) => void) | null = null;\n /** Pre-scheduled timer for proactive token refresh (5 min before expiry). */\n private refreshTimer: ioBroker.Timeout | undefined = undefined;\n\n /**\n * True between calling mqtt.connect() with persisted creds and the first\n * `connect` event. If `close` fires while this is still true, the cached\n * cert/token are invalid \u2014 wipe them so the next attempt does a fresh login.\n */\n private persistedAttemptInFlight = false;\n\n /**\n * Hand the client persisted credentials from a previous successful login.\n * If the bearer token is not yet expired, the next connect() will skip the\n * full login flow and try MQTT with the stored cert directly.\n *\n * @param creds Persisted credentials, or null to clear\n */\n setPersistedCredentials(creds: PersistedMqttCredentials | null): void {\n this.persisted = creds;\n }\n\n /**\n * Fired after a successful login so the adapter can write the bundle to\n * `encryptedNative`/`native`. Includes the (potentially refreshed) TTL.\n *\n * @param cb Callback\n */\n setOnCredentialsRefresh(cb: ((creds: PersistedMqttCredentials) => void) | null): void {\n this.onCredentialsRefresh = cb;\n }\n\n /**\n * Connect to Govee MQTT.\n * Flow: Login \u2192 Get IoT Key \u2192 Extract certs from P12 \u2192 Connect MQTT\n *\n * @param onStatus Called on device status updates\n * @param onConnection Called on connection state changes\n * @param onToken Called with every fresh bearer token (initial + each reconnect-login)\n */\n async connect(\n onStatus: MqttStatusCallback,\n onConnection: MqttConnectionCallback,\n onToken?: MqttTokenCallback,\n ): Promise<void> {\n this.onStatus = onStatus;\n this.onConnection = onConnection;\n if (onToken) {\n this.onToken = onToken;\n }\n\n try {\n // Step 0: Try the persisted credentials first. If the cached bearer\n // token is still inside its TTL and the stored P12 cert lets us connect,\n // skip the full login flow \u2014 that avoids spamming the user's email\n // with a 2FA verification request on every adapter restart.\n if (this.tryPersistedReuse()) {\n return;\n }\n\n // Step 1: Login\n const codeWasSent = (this.verificationCode ?? \"\").trim().length > 0;\n const loginResp = await this.login();\n if (!loginResp.client) {\n const apiStatus = loginResp.status ?? 0;\n const apiMsg = loginResp.message ?? \"unknown error\";\n const statusStr = `(status ${apiStatus || \"?\"})`;\n // Dump the full response body (capped) so a bug report with\n // debug log shows exactly what Govee returned \u2014 useful when\n // they change error semantics or add new status codes.\n this.log.debug(`MQTT login error response body: ${JSON.stringify(loginResp).slice(0, 300)}`);\n // Classify the Govee response to avoid misleading error messages.\n // 454/455 (2FA) MUST come before generic AUTH so the user gets the\n // correct \"request a code\" hint instead of \"check email/password\".\n if (apiStatus === 455 || (apiStatus === 454 && codeWasSent)) {\n throw new Error(`Verification code invalid or expired ${statusStr}`);\n }\n if (apiStatus === 454) {\n throw new Error(`Verification required by Govee \u2014 request a code via Adapter settings ${statusStr}`);\n }\n if (apiStatus === 429 || /too many|rate.?limit|frequent|throttl/i.test(apiMsg)) {\n throw new Error(`Rate limited by Govee: ${apiMsg} ${statusStr}`);\n }\n if (apiStatus === 451 || /not.*registered/i.test(apiMsg)) {\n throw new Error(`Login failed: email not registered ${statusStr}`);\n }\n if (apiStatus === 401 || /password|credential|unauthorized/i.test(apiMsg)) {\n throw new Error(`Login failed: ${apiMsg} ${statusStr}`);\n }\n // Account temporarily locked \u2014 NOT a credential error, keep reconnecting\n if (/abnormal|blocked|suspended|disabled/i.test(apiMsg)) {\n throw new Error(`Account temporarily locked by Govee: ${apiMsg} ${statusStr}`);\n }\n // Other account issues, maintenance, etc.\n throw new Error(`Govee login rejected: ${apiMsg} ${statusStr}`);\n }\n // Login OK \u2014 if a verification code was used, signal the adapter to\n // clear the settings field AND clear the in-memory copy so a\n // subsequent reconnect-login doesn't replay a now-consumed code (Govee\n // would reject it as `Verification code invalid` and trip the\n // VERIFICATION_FAILED branch unnecessarily).\n if (codeWasSent) {\n this.verificationCode = \"\";\n this.onVerificationConsumed?.();\n }\n // H11 \u2014 Login-Response-Validation. Govee schickt accountId + topic\n // bei erfolgreichem Login. Fehlt eines, w\u00E4re die clientId\n // `AP/undefined/<uuid>` und Govee-Broker rejected mit unklarem\n // disconnect. Fr\u00FChzeitig validieren mit klarem Fehler.\n const accIdRaw = loginResp.client.accountId;\n if (typeof accIdRaw !== \"string\" && typeof accIdRaw !== \"number\") {\n throw new Error(`Login response missing accountId (got ${typeof accIdRaw})`);\n }\n const topicRaw = loginResp.client.topic;\n if (typeof topicRaw !== \"string\" || topicRaw.length === 0) {\n throw new Error(`Login response missing account topic (got ${typeof topicRaw})`);\n }\n this._bearerToken = loginResp.client.token;\n this.accountId = String(accIdRaw);\n this.accountTopic = topicRaw;\n // Notify dependents (e.g. api-client for authenticated library endpoints)\n // so they don't keep a stale token after a long-delay reconnect.\n this.onToken?.(this._bearerToken);\n\n // Step 2: Get IoT credentials\n const iotResp = await this.getIotKey();\n if (!iotResp.data?.endpoint) {\n throw new Error(\"IoT key response missing endpoint/certificate data\");\n }\n const { endpoint, p12, p12Pass } = iotResp.data;\n\n // Step 3: Extract key + cert from P12\n const { key, cert, ca } = this.extractCertsFromP12(p12, p12Pass);\n\n // Persist the fresh credentials so the next adapter restart skips this\n // whole login dance (and avoids the 2FA email storm). TTL comes from\n // Govee \u2014 `token_expire_cycle` (snake) or `tokenExpireCycle` (camel),\n // depending on the response variant. 1h fallback if Govee sends nothing.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const expiresAt = Date.now() + ttlSec * 1000;\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: endpoint,\n p12Cert: p12,\n p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: expiresAt,\n });\n this.scheduleProactiveRefresh(expiresAt);\n\n // Step 4: Connect MQTT with mutual TLS\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n this.client = this.mqttConnectImpl(`mqtts://${endpoint}:8883`, {\n clientId,\n key,\n cert,\n ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0, // We handle reconnect ourselves\n rejectUnauthorized: true,\n });\n\n this.attachClientHandlers();\n } catch (err) {\n const category = classifyError(err);\n const msg = `MQTT connection failed: ${errMessage(err)}`;\n\n // State-Sync: connect() throw = not connected, unabh\u00E4ngig von Fehlertyp\n this.onConnection?.(false);\n\n // Govee verification 454: pause reconnect until the user submits a\n // code via Settings (which triggers an adapter restart). Don't\n // increment auth-failure counter \u2014 this is not a credential error.\n //\n // Wording: Govee returns 454 the first time a particular client-id\n // tries to log in, regardless of whether the user enabled 2FA on\n // their account. It's a \"new client, please verify once\" handshake\n // \u2014 not \"you have 2FA enabled\". Earlier wording was scaring users\n // whose accounts are 2FA-free. The actual message says: this is a\n // one-time setup per client.\n //\n // Dedup: only warn on the FIRST occurrence of this category (per\n // adapter lifetime). Subsequent reconnect attempts that hit the\n // same 454 are demoted to debug.\n if (category === \"VERIFICATION_PENDING\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: Govee asked for verification \u2014 request a code in adapter settings`);\n } else {\n this.log.debug(\"MQTT verification still pending (Govee returned 454 again)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"pending\");\n }\n return;\n }\n if (category === \"VERIFICATION_FAILED\") {\n const isNew = this.lastErrorCategory !== category;\n this.lastErrorCategory = category;\n if (isNew) {\n this.log.warn(`MQTT not connected: verification code rejected \u2014 request a fresh code`);\n } else {\n this.log.debug(\"MQTT verification code rejected again (Govee returned 455)\");\n }\n if (this.onVerificationFailed) {\n this.onVerificationFailed(\"failed\");\n }\n return;\n }\n\n // Auth backoff \u2014 stop reconnecting after repeated auth failures\n if (category === \"AUTH\") {\n this.authFailCount++;\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n this.log.warn(`MQTT not connected: login rejected \u2014 check email/password`);\n return;\n }\n } else {\n this.authFailCount = 0;\n }\n\n // Error dedup \u2014 warn on first/new category, debug on repeat\n if (category !== this.lastErrorCategory) {\n this.lastErrorCategory = category;\n this.log.warn(msg);\n } else {\n this.log.debug(msg);\n }\n\n this.scheduleReconnect();\n }\n }\n\n /** Whether MQTT is currently connected */\n get connected(): boolean {\n return this.client?.connected ?? false;\n }\n\n /** Disconnect and cleanup */\n disconnect(): void {\n this.disposed = true;\n if (this.reconnectTimer) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n // refreshTimer l\u00F6scht der Adapter-Stop sonst nicht \u2014 w\u00FCrde nach\n // disconnect() noch refreshBearerSilently() triggern und Login-Calls\n // gegen einen abgebauten Adapter feuern.\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n if (this.client) {\n this.client.removeAllListeners();\n this.client.on(\"error\", () => {\n /* ignore late errors */\n });\n this.client.end(true);\n this.client = null;\n }\n }\n\n /**\n * Parse MQTT status message\n *\n * @param payload Raw MQTT message buffer\n * @param topic AWS-IoT topic the message arrived on\n */\n private handleMessage(payload: Buffer, topic: string): void {\n const rawText = payload.toString();\n try {\n const raw = JSON.parse(rawText) as Record<string, unknown>;\n\n // Defensive \u2014 blind casts would crash downstream if Govee pushes\n // unexpected types. Validate each field before constructing the update.\n const sku = typeof raw.sku === \"string\" ? raw.sku : \"\";\n const device = typeof raw.device === \"string\" ? raw.device : \"\";\n const state = raw.state && typeof raw.state === \"object\" ? (raw.state as MqttStatusUpdate[\"state\"]) : undefined;\n const op = raw.op && typeof raw.op === \"object\" ? (raw.op as MqttStatusUpdate[\"op\"]) : undefined;\n\n if (sku || device) {\n this.onStatus?.({ sku, device, state, op });\n if (this.onPacket && device) {\n // v2.9.1 \u2014 always forward the raw envelope so state-only pushes\n // (state with no op.command) are visible in diag. Previously the\n // hook only fired on op.command BLE bytes \u2014 state changes from\n // the Govee app / physical remote went uncaptured.\n if (Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, { hex: cmd, rawJson: rawText });\n }\n }\n } else {\n this.onPacket(device, topic, { rawJson: rawText });\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${rawText.slice(0, 200)}`);\n }\n }\n\n /**\n * Register a hook called for every parsed MQTT packet. Used by the\n * adapter to forward op.command hex strings + the raw JSON envelope into\n * the DiagnosticsCollector for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, {hex?, rawJson?})\n */\n setPacketHook(\n cb: ((deviceId: string, topic: string, payload: { hex?: string; rawJson?: string }) => void) | null,\n ): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n // disposed-check guards the reconnect path against schedule-after-stop:\n // disconnect() clears the timer, but a close-event-driven scheduleReconnect\n // could fire AFTER disconnect() returned, leaving a future-firing timer.\n if (this.disposed) {\n return;\n }\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= MQTT_MAX_AUTH_FAILURES) {\n return;\n }\n\n this.reconnectAttempts++;\n // M6 \u2014 Jitter gegen Thundering Herd. Bei verteilter Govee-Outage syncen\n // sonst tausende Adapter exakt zur Sekunde.\n const base = Math.min(5_000 * Math.pow(2, this.reconnectAttempts - 1), 300_000);\n const jitter = Math.random() * Math.min(base, 30_000);\n const delay = Math.round(base + jitter);\n this.log.debug(`MQTT: Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);\n\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.disposed) {\n return;\n }\n if (this.onStatus && this.onConnection) {\n void this.connect(this.onStatus, this.onConnection);\n }\n }, delay);\n }\n\n /**\n * Reuse path: if a persisted bundle exists and is not expired yet, try\n * MQTT directly with the stored cert. Returns true if a connection was\n * initiated (caller should NOT continue to login).\n *\n * Uses the same ON-event handlers as the full login path \u2014 a successful\n * connect publishes `mqttConnected: true` exactly like a fresh login.\n * On failure (cert rejected, token revoked, network) we just return false\n * and the caller falls through to the full login.\n */\n private tryPersistedReuse(): boolean {\n const creds = this.persisted;\n if (!creds || !creds.bearerToken || !creds.iotEndpoint || !creds.p12Cert) {\n return false;\n }\n if (creds.tokenExpiresAt <= Date.now()) {\n return false;\n }\n let extracted;\n try {\n extracted = this.extractCertsFromP12(creds.p12Cert, creds.p12Pass);\n } catch (e) {\n this.log.debug(`Persisted P12 cert unusable: ${errMessage(e)} \u2014 falling back to fresh login`);\n return false;\n }\n this._bearerToken = creds.bearerToken;\n this.accountId = creds.accountId;\n this.accountTopic = creds.accountTopic;\n this.onToken?.(this._bearerToken);\n const clientId = `AP/${creds.accountId}/${this.sessionUuid}`;\n this.log.debug(\"MQTT: trying cached credentials (no fresh login)\");\n this.persistedAttemptInFlight = true;\n this.client = this.mqttConnectImpl(`mqtts://${creds.iotEndpoint}:8883`, {\n clientId,\n key: extracted.key,\n cert: extracted.cert,\n ca: extracted.ca,\n protocolVersion: 4,\n keepalive: 60,\n reconnectPeriod: 0,\n rejectUnauthorized: true,\n });\n this.attachClientHandlers();\n this.scheduleProactiveRefresh(creds.tokenExpiresAt);\n return true;\n }\n\n /**\n * Attach the standard `connect` / `message` / `error` / `close` handlers\n * to the current `this.client`. Extracted so both paths (fresh login and\n * persisted reuse) share exactly the same event wiring.\n */\n private attachClientHandlers(): void {\n if (!this.client) {\n return;\n }\n this.client.on(\"connect\", () => {\n const wasCached = this.persistedAttemptInFlight;\n this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n const broker = this.persisted?.iotEndpoint ?? \"?\";\n const clientId = `AP/${this.accountId}/${this.sessionUuid}`;\n const authMode = wasCached ? \"cached\" : \"fresh\";\n if (this.lastErrorCategory) {\n this.log.info(`MQTT connection restored: broker=${broker} clientId=${clientId} authMode=${authMode}`);\n this.lastErrorCategory = null;\n } else {\n // Initial connect is implicit in the ready-message (\"channels:\n // LAN+Cloud+MQTT+...\"), so this stays debug \u2014 only the recovery\n // path above earns an info-level event.\n this.log.debug(`MQTT connected: broker=${broker} clientId=${clientId} authMode=${authMode}`);\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n // Subscribe-fail is rare (AWS-IoT policy mismatch, account flagged)\n // but the TCP connection stays alive \u2014 protocol-level keepalive\n // pings keep answering, so `close` would never fire on its own.\n // Without forcing a close, `info.mqttConnected` would stay `false`\n // indefinitely with no reconnect \u2014 permanent silent death.\n // Forcing the close triggers the close-handler \u2192 scheduleReconnect.\n this.log.warn(`MQTT subscribe failed: ${err.message} \u2014 forcing reconnect`);\n try {\n this.client?.end(true);\n } catch {\n // ignore \u2014 close-event handler will pick it up either way\n }\n } else {\n this.log.debug(`MQTT subscribed to account topic: topic=${this.accountTopic} qos=0`);\n this.onConnection?.(true);\n }\n });\n });\n this.client.on(\"message\", (topic, payload) => {\n this.handleMessage(payload, topic);\n });\n this.client.on(\"error\", err => {\n // H10 \u2014 error-events klassifizieren, sonst sieht der User nur debug.\n // close-event-fallback f\u00E4ngt vieles, aber nicht spurious network\n // errors die nicht zu Disconnect f\u00FChren.\n this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"MQTT\", err);\n });\n this.client.on(\"close\", () => {\n this.onConnection?.(false);\n // Cached cert/token failed before producing a single successful\n // connect \u2014 assume the bundle is stale (cert revoked, token\n // expired before our TTL guess, account topic changed). Wipe it\n // so scheduleReconnect \u2192 connect() falls through to a fresh login.\n if (this.persistedAttemptInFlight) {\n this.persistedAttemptInFlight = false;\n this.persisted = null;\n this.log.debug(\"MQTT: cached credentials rejected \u2014 falling back to fresh login\");\n }\n if (!this.lastErrorCategory) {\n this.lastErrorCategory = \"NETWORK\";\n this.log.debug(\"MQTT disconnected \u2014 will reconnect\");\n }\n this.scheduleReconnect();\n });\n }\n\n /**\n * Schedule a proactive token refresh 5 minutes before bearer expiry.\n *\n * v2.1.0 disconnect+reconnect was disruptive: it killed the live MQTT\n * session, then triggered a fresh login. If Govee responded with 454\n * (e.g. account flagged for re-verification), the user saw the 2FA\n * warning even though MQTT was previously working \u2014 and the\n * disconnect dropped status push for the duration of the re-auth.\n *\n * v2.1.1: silent re-login. We just call /v1/login, save the new\n * bearer + cert (so the next adapter restart skips full login), and\n * let the existing MQTT session keep running. The current cert may\n * stay valid past the bearer's expiry \u2014 losing the bearer only\n * affects API-key-less REST calls, not the live MQTT push channel.\n *\n * @param expiresAt ms-timestamp at which the bearer token will be rejected\n */\n private scheduleProactiveRefresh(expiresAt: number): void {\n if (this.refreshTimer) {\n this.timers.clearTimeout(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n const refreshAt = expiresAt - 5 * 60 * 1000;\n const delay = refreshAt - Date.now();\n if (delay <= 0) {\n return;\n }\n this.refreshTimer = this.timers.setTimeout(() => {\n this.refreshTimer = undefined;\n void this.refreshBearerSilently();\n }, delay);\n }\n\n /**\n * Refresh the bearer token without disconnecting MQTT. Called by the\n * proactive-refresh timer. Failures don't disrupt the live session \u2014\n * the next reconnect-cycle (if Govee invalidates the cert) handles\n * recovery via the normal connect() path.\n */\n private async refreshBearerSilently(): Promise<void> {\n if (this.disposed) {\n // Adapter wurde gestoppt zwischen Timer-Schedule und Timer-Fire \u2014\n // nicht mehr loggen + nicht mehr Login-Call.\n return;\n }\n this.log.debug(\"Proactive MQTT bearer refresh triggered\");\n try {\n const loginResp = await this.login();\n if (!loginResp.client) {\n // Login was rejected (454 / 455 / locked / rate-limited). Keep\n // the current MQTT connection alive. If the bearer is needed\n // for a REST call later, that call's catch path will surface\n // the actual error to the user.\n const status = loginResp.status ?? 0;\n this.log.debug(`Silent bearer refresh declined by Govee (status ${status}) \u2014 current session kept`);\n return;\n }\n this._bearerToken = loginResp.client.token;\n this.onToken?.(this._bearerToken);\n // Persist the new bearer + cert so the next restart skips full\n // login. Cert may be the same as before (unchanged P12) \u2014 js-controller\n // re-encrypts identical bytes anyway, no harm done.\n const ttlSec = loginResp.client.token_expire_cycle ?? loginResp.client.tokenExpireCycle ?? 3600;\n const newExpiresAt = Date.now() + ttlSec * 1000;\n try {\n const iotResp = await this.getIotKey();\n if (iotResp?.data?.endpoint) {\n this.onCredentialsRefresh?.({\n bearerToken: this._bearerToken,\n iotEndpoint: iotResp.data.endpoint,\n p12Cert: iotResp.data.p12,\n p12Pass: iotResp.data.p12Pass,\n accountId: this.accountId,\n accountTopic: this.accountTopic,\n tokenExpiresAt: newExpiresAt,\n });\n }\n } catch (e) {\n this.log.debug(`Silent IoT-key refresh failed: ${errMessage(e)}`);\n }\n this.scheduleProactiveRefresh(newExpiresAt);\n } catch (e) {\n // Network error / 5xx \u2014 not a release-blocker. The live MQTT\n // session continues; the next reconnect-cycle (if needed) will\n // try a full login.\n this.log.debug(`Silent bearer refresh failed: ${errMessage(e)} \u2014 current session kept`);\n }\n }\n\n /** Login to Govee account */\n private async login(): Promise<GoveeLoginResponse> {\n const body: Record<string, string> = {\n email: this.email,\n password: this.password,\n client: this.clientId,\n };\n const code = (this.verificationCode ?? \"\").trim();\n if (code) {\n body.code = code;\n }\n const result = await this.httpsRequestImpl<GoveeLoginResponse>({\n method: \"POST\",\n url: LOGIN_URL,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body,\n });\n if (!result.value) {\n throw new Error(\n `Govee login returned no body (status=${result.statusCode}${result.fallback ? `, fallback=${result.fallback}` : \"\"}${result.bodySnippet ? `, body=${JSON.stringify(result.bodySnippet)}` : \"\"})`,\n );\n }\n return result.value;\n }\n\n /**\n * Last time `requestVerificationCode` actually issued a request \u2014 guards against\n * Govee marking the account as suspicious from rapid-fire user clicks.\n */\n private lastVerificationRequestMs = 0;\n\n /**\n * Trigger Govee's verification-code email. Govee sends a one-time code\n * to the account email; the user pastes it into Settings.\n *\n * Status 200 \u2192 email queued. The response body is irrelevant for the\n * adapter \u2014 Govee may include a tracking token but we don't use it.\n *\n * In-memory throttle of {@link VERIFICATION_REQUEST_THROTTLE_MS} (30 s)\n * mirrors the message-router's user-side throttle but lives here too so\n * other entry points (programmatic restarts, future scripted access)\n * can't bypass it.\n *\n * Throws on non-200, network failure or throttle-block so the caller\n * (onMessage handler) can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const now = Date.now();\n const elapsed = now - this.lastVerificationRequestMs;\n if (this.lastVerificationRequestMs > 0 && elapsed < VERIFICATION_REQUEST_THROTTLE_MS) {\n const throttleRemainingSec = Math.ceil((VERIFICATION_REQUEST_THROTTLE_MS - elapsed) / 1000);\n throw new Error(`Verification code request throttled \u2014 wait ${throttleRemainingSec}s before retrying.`);\n }\n this.lastVerificationRequestMs = now;\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await this.httpsRequestImpl<unknown>({\n method: \"POST\",\n url,\n headers: {\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n iotVersion: \"0\",\n timestamp: String(Date.now()),\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n body: {\n type: 8,\n email: this.email,\n },\n });\n }\n\n /** Get IoT key (P12 certificate) */\n private async getIotKey(): Promise<GoveeIotKeyResponse> {\n const result = await this.httpsRequestImpl<GoveeIotKeyResponse>({\n method: \"GET\",\n url: IOT_KEY_URL,\n headers: {\n Authorization: `Bearer ${this._bearerToken}`,\n appVersion: GOVEE_APP_VERSION,\n clientId: this.clientId,\n clientType: GOVEE_CLIENT_TYPE,\n \"User-Agent\": GOVEE_USER_AGENT,\n },\n });\n if (!result.value) {\n throw new Error(\n `Govee IoT-key request returned no body (status=${result.statusCode}${result.fallback ? `, fallback=${result.fallback}` : \"\"})`,\n );\n }\n return result.value;\n }\n\n /**\n * Extract PEM key + cert from PKCS12\n *\n * @param p12Base64 Base64-encoded PKCS12 data\n * @param password PKCS12 password\n */\n private extractCertsFromP12(p12Base64: string, password: string): { key: string; cert: string; ca: string } {\n const p12Der = forge.util.decode64(p12Base64);\n const p12Asn1 = forge.asn1.fromDer(p12Der);\n const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);\n\n // Extract private key\n const keyBags = p12.getBags({\n bagType: forge.pki.oids.pkcs8ShroudedKeyBag,\n });\n const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];\n if (!keyBag?.key) {\n throw new Error(\"No private key found in P12\");\n }\n const key = forge.pki.privateKeyToPem(keyBag.key);\n\n // Extract certificate\n const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });\n const certBag = certBags[forge.pki.oids.certBag]?.[0];\n if (!certBag?.cert) {\n throw new Error(\"No certificate found in P12\");\n }\n const cert = forge.pki.certificateToPem(certBag.cert);\n\n // AWS IoT uses Amazon Root CA\n const ca = AMAZON_ROOT_CA1;\n\n return { key, cert, ca };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a", "_b"]
|
|
7
7
|
}
|
|
@@ -140,8 +140,10 @@ class MessageRouter {
|
|
|
140
140
|
if (action === "requestCode") {
|
|
141
141
|
const now = Date.now();
|
|
142
142
|
if (now - this.lastVerificationRequestMs < import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS) {
|
|
143
|
-
const
|
|
144
|
-
|
|
143
|
+
const remainingSec = Math.ceil(
|
|
144
|
+
(import_timing_constants.VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1e3
|
|
145
|
+
);
|
|
146
|
+
return { result: `Bitte ${remainingSec}s warten \u2014 gerade wurde schon ein Code angefordert.` };
|
|
145
147
|
}
|
|
146
148
|
this.lastVerificationRequestMs = now;
|
|
147
149
|
const probe = this.host.createMqttProbeClient();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/message-router.ts"],
|
|
4
|
-
"sourcesContent": ["import { errMessage } from \"./types\";\nimport type { GoveeMqttClient } from \"./govee-mqtt-client\";\nimport { VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\n\n/**\n * Host-Interface f\u00FCr MessageRouter.\n *\n * Pattern analog SnapshotHandler/GroupFanoutHandler. main.ts bleibt\n * schlank, der onMessage/sendTo-Pfad ist isoliert testbar.\n */\nexport interface MessageRouterHost {\n /** Adapter logger. */\n log: ioBroker.Logger;\n /** Liefert die Adapter-Konfiguration f\u00FCr den runMqttAuthAction-Pfad. */\n getConfig: () => { goveeEmail: string; goveePassword: string; mqttVerificationCode?: string };\n /** Sendet die JSON-Response zur\u00FCck an den Caller (sendMessageResponse-Pfad). */\n sendResponse: (obj: ioBroker.Message, data: unknown) => void;\n /** Factory f\u00FCr ein One-Shot-MqttClient (f\u00FCr Login-Test). */\n createMqttProbeClient: () => GoveeMqttClient;\n /** Liefert die Liste der Devices die Segmente haben (f\u00FCr getSegmentDevices). */\n getSegmentDeviceList: () => Array<{ value: string; label: string }>;\n /** Wizard-Step-Routing \u2014 main.ts beh\u00E4lt den Wizard-State. */\n runWizardStep: (action: string, deviceKey: string) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Router f\u00FCr ioBroker.Message events (sendTo aus dem Admin-UI).\n *\n * Dispatcht 3 Commands:\n * - `getSegmentDevices` \u2014 selectSendTo-Datenquelle f\u00FCr den Wizard\n * - `segmentWizard` \u2014 Wizard-Step (start/yes/no/done/abort)\n * - `mqttAuth` \u2014 Login-Test + Verification-Code-Anforderung\n */\nexport class MessageRouter {\n /** Last time `requestCode` was triggered \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n\n /**\n * @param host Adapter dependencies via Host-Interface\n */\n constructor(private readonly host: MessageRouterHost) {}\n\n /**\n * Sync entry-point \u2014 registered as `this.on(\"message\", ...)`. Wraps\n * the async handler in a catch so unhandled rejections k\u00F6nnen den\n * Adapter nicht crashen.\n *\n * @param obj Inkommende ioBroker-Message\n */\n onMessage(obj: ioBroker.Message): void {\n if (!obj?.command) {\n return;\n }\n this.handleMessage(obj).catch(e => {\n this.host.log.warn(`onMessage handler crashed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n });\n }\n\n /**\n * Async handler \u2014 dispatcht zu den 3 Sub-Handlern.\n *\n * @param obj Inkommende ioBroker-Message\n */\n private async handleMessage(obj: ioBroker.Message): Promise<void> {\n try {\n if (obj.command === \"getSegmentDevices\") {\n this.host.sendResponse(obj, this.host.getSegmentDeviceList());\n return;\n }\n if (obj.command === \"segmentWizard\") {\n const payload = (obj.message ?? {}) as { action?: string; device?: string };\n const response = await this.host.runWizardStep(payload.action ?? \"\", payload.device ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n if (obj.command === \"mqttAuth\") {\n const payload = (obj.message ?? {}) as { action?: string };\n const response = await this.runMqttAuthAction(payload.action ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n // Unknown command \u2014 must respond, otherwise the admin sendTo() call\n // hangs in its 5s timeout (pattern from beszel v0.4.4 H4 fix).\n this.host.log.debug(`onMessage: unknown command '${obj.command}'`);\n this.host.sendResponse(obj, { error: `Unknown command '${obj.command}'` });\n } catch (e) {\n this.host.log.warn(`onMessage failed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n /**\n * Handle the `mqttAuth` onMessage commands.\n *\n * Two actions:\n * - `test` \u2014 try a one-shot login mit der aktuellen Settings-Combo\n * und liefere ein einzelnes user-readable Ergebnis.\n * - `requestCode` \u2014 POST an /verification, Govee mailt fresh code.\n * 30s in-memory throttle gegen double-click email-spam.\n *\n * @param action Action-Name aus dem jsonConfig sendTo-Button\n */\n private async runMqttAuthAction(action: string): Promise<{ result: string }> {\n const config = this.host.getConfig();\n if (!config.goveeEmail || !config.goveePassword) {\n return { result: \"Email + Passwort in den Adapter-Einstellungen n\u00F6tig.\" };\n }\n if (action === \"test\") {\n const probe = this.host.createMqttProbeClient();\n probe.setVerificationCode(config.mqttVerificationCode ?? \"\");\n try {\n let connected = false;\n await probe.connect(\n () => {},\n isConnected => {\n connected = isConnected;\n },\n );\n probe.disconnect();\n return {\n result: connected\n ? \"Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv.\"\n : \"Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten.\",\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n if (/Verification required/i.test(msg)) {\n return {\n result:\n \"Govee verlangt 2-Faktor-Best\u00E4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern.\",\n };\n }\n if (/Verification code invalid/i.test(msg)) {\n return { result: \"2-Faktor-Code ung\u00FCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern.\" };\n }\n if (/email not registered/i.test(msg)) {\n return { result: \"Diese E-Mail ist bei Govee nicht registriert.\" };\n }\n if (/Login failed/i.test(msg)) {\n return { result: \"Passwort wurde von Govee abgelehnt.\" };\n }\n if (/Rate limited/i.test(msg)) {\n return { result: \"Govee meldet Rate-Limit \u2014 bitte sp\u00E4ter erneut versuchen.\" };\n }\n if (/Account temporarily locked/i.test(msg)) {\n return { result: \"Govee-Account vor\u00FCbergehend gesperrt \u2014 Govee Home App \u00F6ffnen und Status pr\u00FCfen.\" };\n }\n return { result: `Login fehlgeschlagen: ${msg}` };\n }\n }\n if (action === \"requestCode\") {\n const now = Date.now();\n if (now - this.lastVerificationRequestMs < VERIFICATION_REQUEST_THROTTLE_MS) {\n const
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { errMessage } from \"./types\";\nimport type { GoveeMqttClient } from \"./govee-mqtt-client\";\nimport { VERIFICATION_REQUEST_THROTTLE_MS } from \"./timing-constants\";\n\n/**\n * Host-Interface f\u00FCr MessageRouter.\n *\n * Pattern analog SnapshotHandler/GroupFanoutHandler. main.ts bleibt\n * schlank, der onMessage/sendTo-Pfad ist isoliert testbar.\n */\nexport interface MessageRouterHost {\n /** Adapter logger. */\n log: ioBroker.Logger;\n /** Liefert die Adapter-Konfiguration f\u00FCr den runMqttAuthAction-Pfad. */\n getConfig: () => { goveeEmail: string; goveePassword: string; mqttVerificationCode?: string };\n /** Sendet die JSON-Response zur\u00FCck an den Caller (sendMessageResponse-Pfad). */\n sendResponse: (obj: ioBroker.Message, data: unknown) => void;\n /** Factory f\u00FCr ein One-Shot-MqttClient (f\u00FCr Login-Test). */\n createMqttProbeClient: () => GoveeMqttClient;\n /** Liefert die Liste der Devices die Segmente haben (f\u00FCr getSegmentDevices). */\n getSegmentDeviceList: () => Array<{ value: string; label: string }>;\n /** Wizard-Step-Routing \u2014 main.ts beh\u00E4lt den Wizard-State. */\n runWizardStep: (action: string, deviceKey: string) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Router f\u00FCr ioBroker.Message events (sendTo aus dem Admin-UI).\n *\n * Dispatcht 3 Commands:\n * - `getSegmentDevices` \u2014 selectSendTo-Datenquelle f\u00FCr den Wizard\n * - `segmentWizard` \u2014 Wizard-Step (start/yes/no/done/abort)\n * - `mqttAuth` \u2014 Login-Test + Verification-Code-Anforderung\n */\nexport class MessageRouter {\n /** Last time `requestCode` was triggered \u2014 guards against double-click email spam. */\n private lastVerificationRequestMs = 0;\n\n /**\n * @param host Adapter dependencies via Host-Interface\n */\n constructor(private readonly host: MessageRouterHost) {}\n\n /**\n * Sync entry-point \u2014 registered as `this.on(\"message\", ...)`. Wraps\n * the async handler in a catch so unhandled rejections k\u00F6nnen den\n * Adapter nicht crashen.\n *\n * @param obj Inkommende ioBroker-Message\n */\n onMessage(obj: ioBroker.Message): void {\n if (!obj?.command) {\n return;\n }\n this.handleMessage(obj).catch(e => {\n this.host.log.warn(`onMessage handler crashed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n });\n }\n\n /**\n * Async handler \u2014 dispatcht zu den 3 Sub-Handlern.\n *\n * @param obj Inkommende ioBroker-Message\n */\n private async handleMessage(obj: ioBroker.Message): Promise<void> {\n try {\n if (obj.command === \"getSegmentDevices\") {\n this.host.sendResponse(obj, this.host.getSegmentDeviceList());\n return;\n }\n if (obj.command === \"segmentWizard\") {\n const payload = (obj.message ?? {}) as { action?: string; device?: string };\n const response = await this.host.runWizardStep(payload.action ?? \"\", payload.device ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n if (obj.command === \"mqttAuth\") {\n const payload = (obj.message ?? {}) as { action?: string };\n const response = await this.runMqttAuthAction(payload.action ?? \"\");\n this.host.sendResponse(obj, response);\n return;\n }\n // Unknown command \u2014 must respond, otherwise the admin sendTo() call\n // hangs in its 5s timeout (pattern from beszel v0.4.4 H4 fix).\n this.host.log.debug(`onMessage: unknown command '${obj.command}'`);\n this.host.sendResponse(obj, { error: `Unknown command '${obj.command}'` });\n } catch (e) {\n this.host.log.warn(`onMessage failed for ${obj.command}: ${errMessage(e)}`);\n this.host.sendResponse(obj, { error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n /**\n * Handle the `mqttAuth` onMessage commands.\n *\n * Two actions:\n * - `test` \u2014 try a one-shot login mit der aktuellen Settings-Combo\n * und liefere ein einzelnes user-readable Ergebnis.\n * - `requestCode` \u2014 POST an /verification, Govee mailt fresh code.\n * 30s in-memory throttle gegen double-click email-spam.\n *\n * @param action Action-Name aus dem jsonConfig sendTo-Button\n */\n private async runMqttAuthAction(action: string): Promise<{ result: string }> {\n const config = this.host.getConfig();\n if (!config.goveeEmail || !config.goveePassword) {\n return { result: \"Email + Passwort in den Adapter-Einstellungen n\u00F6tig.\" };\n }\n if (action === \"test\") {\n const probe = this.host.createMqttProbeClient();\n probe.setVerificationCode(config.mqttVerificationCode ?? \"\");\n try {\n let connected = false;\n await probe.connect(\n () => {},\n isConnected => {\n connected = isConnected;\n },\n );\n probe.disconnect();\n return {\n result: connected\n ? \"Login erfolgreich \u2014 Echtzeit-Status-Updates aktiv.\"\n : \"Login angenommen, MQTT-Verbindung steht aber noch nicht \u2014 Adapter neu starten.\",\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n if (/Verification required/i.test(msg)) {\n return {\n result:\n \"Govee verlangt 2-Faktor-Best\u00E4tigung. Bitte 'Verifizierungs-Code anfordern' klicken, Code aus der E-Mail eintragen und Speichern.\",\n };\n }\n if (/Verification code invalid/i.test(msg)) {\n return { result: \"2-Faktor-Code ung\u00FCltig oder abgelaufen \u2014 bitte einen neuen Code anfordern.\" };\n }\n if (/email not registered/i.test(msg)) {\n return { result: \"Diese E-Mail ist bei Govee nicht registriert.\" };\n }\n if (/Login failed/i.test(msg)) {\n return { result: \"Passwort wurde von Govee abgelehnt.\" };\n }\n if (/Rate limited/i.test(msg)) {\n return { result: \"Govee meldet Rate-Limit \u2014 bitte sp\u00E4ter erneut versuchen.\" };\n }\n if (/Account temporarily locked/i.test(msg)) {\n return { result: \"Govee-Account vor\u00FCbergehend gesperrt \u2014 Govee Home App \u00F6ffnen und Status pr\u00FCfen.\" };\n }\n return { result: `Login fehlgeschlagen: ${msg}` };\n }\n }\n if (action === \"requestCode\") {\n const now = Date.now();\n if (now - this.lastVerificationRequestMs < VERIFICATION_REQUEST_THROTTLE_MS) {\n const remainingSec = Math.ceil(\n (VERIFICATION_REQUEST_THROTTLE_MS - (now - this.lastVerificationRequestMs)) / 1000,\n );\n return { result: `Bitte ${remainingSec}s warten \u2014 gerade wurde schon ein Code angefordert.` };\n }\n this.lastVerificationRequestMs = now;\n const probe = this.host.createMqttProbeClient();\n try {\n await probe.requestVerificationCode();\n return { result: \"Code wurde an deine Govee-E-Mail-Adresse gesendet (Spam-Ordner pr\u00FCfen).\" };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return { result: `Govee hat den Code-Versand abgelehnt: ${msg}` };\n }\n }\n return { result: `Unbekannte Aktion '${action}'.` };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/test-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Geteilte Test-Mocks und Factories f\u00FCr alle Test-Dateien.\n *\n * Vorher waren `mockLog`, `mockTimers`, `createTestDevice` etc. inline in\n * `device-manager.test.ts` \u2014 andere Tests dupliziert. Hier zentral.\n */\n\nimport type { CloudCapability, GoveeDevice } from \"./types\";\n\n/** No-op Logger mit allen ioBroker.Logger-Methoden. */\nexport const mockLog: ioBroker.Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n debug: () => {},\n silly: () => {},\n level: \"info\",\n};\n\n/**\n * Timer-Adapter f\u00FCr Tests.\n *\n * `setTimeout` feuert SOFORT \u2014 async-await-Pfade die auf `await new Promise(r => setTimeout(r, ...))`\n * basieren, w\u00FCrden sonst stallen. `setInterval` feuert NICHT \u2014 Tests die\n * Polling beobachten brauchen explizit eine andere Mock-Strategie.\n */\nexport const mockTimers = {\n setInterval: () => undefined,\n clearInterval: () => undefined,\n setTimeout: (cb: () => void) => {\n cb();\n return undefined;\n },\n clearTimeout: () => undefined,\n} as never;\n\n/**\n * Standard-Capability-Set f\u00FCr ein regul\u00E4res Light-Ger\u00E4t (H6160-artig).\n * Reicht f\u00FCr die meisten Tests die Capabilities erwarten ohne sich f\u00FCr\n * Details zu interessieren.\n */\nexport function lightCapabilities(): CloudCapability[] {\n return [\n { type: \"devices.capabilities.on_off\", instance: \"powerSwitch\", parameters: { dataType: \"ENUM\" } },\n {\n type: \"devices.capabilities.range\",\n instance: \"brightness\",\n parameters: { dataType: \"INTEGER\", range: { min: 0, max: 100, precision: 1 } },\n },\n { type: \"devices.capabilities.color_setting\", instance: \"colorRgb\", parameters: { dataType: \"INTEGER\" } },\n {\n type: \"devices.capabilities.color_setting\",\n instance: \"colorTemperatureK\",\n parameters: { dataType: \"INTEGER\", range: { min: 2000, max: 9000, precision: 1 } },\n },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"lightScene\" },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"diyScene\" },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"snapshot\" },\n {\n type: \"devices.capabilities.segment_color_setting\",\n instance: \"segmentedColorRgb\",\n parameters: { dataType: \"STRUCT\", fields: [{ fieldName: \"segment\", range: { min: 0, max: 14 } }] },\n },\n {\n type: \"devices.capabilities.segment_color_setting\",\n instance: \"segmentedBrightness\",\n parameters: { dataType: \"STRUCT\", fields: [{ fieldName: \"segment\", range: { min: 0, max: 14 } }] },\n },\n ] as CloudCapability[];\n}\n\n/**\n * Erstelle ein Test-GoveeDevice mit sinnvollen Defaults. Override-Pattern\n * via Spread: `createTestDevice({ sku: \"H1234\" })`.\n *\n * @param overrides Override einzelner Felder\n */\nexport function createTestDevice(overrides: Partial<GoveeDevice> = {}): GoveeDevice {\n return {\n sku: \"H6160\",\n deviceId: \"AA:BB:CC:DD:EE:FF:00:11\",\n name: \"Test Device\",\n type: \"devices.types.light\",\n lanIp: \"192.168.1.100\",\n capabilities: lightCapabilities(),\n scenes: [\n { name: \"Sunrise\", value: { id: 1, paramId: \"sunrise\" } },\n { name: \"Sunset\", value: { id: 2, paramId: \"sunset\" } },\n ],\n diyScenes: [\n { name: \"DIY1\", value: { id: 100, paramId: \"diy1\" } },\n { name: \"DIY2\", value: { id: 101, paramId: \"diy2\" } },\n ],\n snapshots: [\n { name: \"Snap1\", value: 1 },\n { name: \"Snap2\", value: 2 },\n ],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n lastSeenOnNetwork: Date.now(),\n state: { online: true },\n channels: { lan: true, mqtt: false, cloud: false },\n segmentCount: 15,\n ...overrides,\n };\n}\n\n/**\n * Recorder f\u00FCr Mock-Method-Aufrufe in Tests.\n *\n * Nutzung:\n * ```ts\n * const tracker = createCallTracker();\n * mockObj.someMethod = tracker.track(\"someMethod\");\n * // ... call code ...\n * expect(tracker.calls).to.deep.equal([{method: \"someMethod\", args: [...]}]);\n * ```\n */\nexport function createCallTracker(): {\n calls: Array<{ method: string; args: unknown[] }>;\n track: (method: string) => (...args: unknown[]) => void;\n} {\n const calls: Array<{ method: string; args: unknown[] }> = [];\n return {\n calls,\n track:\n (method: string) =>\n (...args: unknown[]) => {\n calls.push({ method, args });\n },\n };\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,UAA2B;AAAA,EACtC,MAAM,MAAM;AAAA,EAAC;AAAA,EACb,MAAM,MAAM;AAAA,EAAC;AAAA,EACb,OAAO,MAAM;AAAA,EAAC;AAAA,EACd,OAAO,MAAM;AAAA,EAAC;AAAA,EACd,OAAO,MAAM;AAAA,EAAC;AAAA,EACd,OAAO;AACT;AASO,MAAM,aAAa;AAAA,EACxB,aAAa,MAAM;AAAA,EACnB,eAAe,MAAM;AAAA,EACrB,YAAY,CAAC,OAAmB;AAC9B,OAAG;AACH,WAAO;AAAA,EACT;AAAA,EACA,cAAc,MAAM;
|
|
4
|
+
"sourcesContent": ["/**\n * Geteilte Test-Mocks und Factories f\u00FCr alle Test-Dateien.\n *\n * Vorher waren `mockLog`, `mockTimers`, `createTestDevice` etc. inline in\n * `device-manager.test.ts` \u2014 andere Tests dupliziert. Hier zentral.\n */\n\nimport type { CloudCapability, GoveeDevice } from \"./types\";\n\n/** No-op Logger mit allen ioBroker.Logger-Methoden. */\nexport const mockLog: ioBroker.Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n debug: () => {},\n silly: () => {},\n level: \"info\",\n};\n\n/**\n * Timer-Adapter f\u00FCr Tests.\n *\n * `setTimeout` feuert SOFORT \u2014 async-await-Pfade die auf `await new Promise(r => setTimeout(r, ...))`\n * basieren, w\u00FCrden sonst stallen. `setInterval` feuert NICHT \u2014 Tests die\n * Polling beobachten brauchen explizit eine andere Mock-Strategie.\n */\nexport const mockTimers = {\n setInterval: () => undefined,\n clearInterval: () => undefined,\n setTimeout: (cb: () => void) => {\n cb();\n return undefined;\n },\n clearTimeout: () => undefined,\n delay: () => Promise.resolve(),\n} as never;\n\n/**\n * Standard-Capability-Set f\u00FCr ein regul\u00E4res Light-Ger\u00E4t (H6160-artig).\n * Reicht f\u00FCr die meisten Tests die Capabilities erwarten ohne sich f\u00FCr\n * Details zu interessieren.\n */\nexport function lightCapabilities(): CloudCapability[] {\n return [\n { type: \"devices.capabilities.on_off\", instance: \"powerSwitch\", parameters: { dataType: \"ENUM\" } },\n {\n type: \"devices.capabilities.range\",\n instance: \"brightness\",\n parameters: { dataType: \"INTEGER\", range: { min: 0, max: 100, precision: 1 } },\n },\n { type: \"devices.capabilities.color_setting\", instance: \"colorRgb\", parameters: { dataType: \"INTEGER\" } },\n {\n type: \"devices.capabilities.color_setting\",\n instance: \"colorTemperatureK\",\n parameters: { dataType: \"INTEGER\", range: { min: 2000, max: 9000, precision: 1 } },\n },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"lightScene\" },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"diyScene\" },\n { type: \"devices.capabilities.dynamic_scene\", instance: \"snapshot\" },\n {\n type: \"devices.capabilities.segment_color_setting\",\n instance: \"segmentedColorRgb\",\n parameters: { dataType: \"STRUCT\", fields: [{ fieldName: \"segment\", range: { min: 0, max: 14 } }] },\n },\n {\n type: \"devices.capabilities.segment_color_setting\",\n instance: \"segmentedBrightness\",\n parameters: { dataType: \"STRUCT\", fields: [{ fieldName: \"segment\", range: { min: 0, max: 14 } }] },\n },\n ] as CloudCapability[];\n}\n\n/**\n * Erstelle ein Test-GoveeDevice mit sinnvollen Defaults. Override-Pattern\n * via Spread: `createTestDevice({ sku: \"H1234\" })`.\n *\n * @param overrides Override einzelner Felder\n */\nexport function createTestDevice(overrides: Partial<GoveeDevice> = {}): GoveeDevice {\n return {\n sku: \"H6160\",\n deviceId: \"AA:BB:CC:DD:EE:FF:00:11\",\n name: \"Test Device\",\n type: \"devices.types.light\",\n lanIp: \"192.168.1.100\",\n capabilities: lightCapabilities(),\n scenes: [\n { name: \"Sunrise\", value: { id: 1, paramId: \"sunrise\" } },\n { name: \"Sunset\", value: { id: 2, paramId: \"sunset\" } },\n ],\n diyScenes: [\n { name: \"DIY1\", value: { id: 100, paramId: \"diy1\" } },\n { name: \"DIY2\", value: { id: 101, paramId: \"diy2\" } },\n ],\n snapshots: [\n { name: \"Snap1\", value: 1 },\n { name: \"Snap2\", value: 2 },\n ],\n sceneLibrary: [],\n musicLibrary: [],\n diyLibrary: [],\n skuFeatures: null,\n lastSeenOnNetwork: Date.now(),\n state: { online: true },\n channels: { lan: true, mqtt: false, cloud: false },\n segmentCount: 15,\n ...overrides,\n };\n}\n\n/**\n * Recorder f\u00FCr Mock-Method-Aufrufe in Tests.\n *\n * Nutzung:\n * ```ts\n * const tracker = createCallTracker();\n * mockObj.someMethod = tracker.track(\"someMethod\");\n * // ... call code ...\n * expect(tracker.calls).to.deep.equal([{method: \"someMethod\", args: [...]}]);\n * ```\n */\nexport function createCallTracker(): {\n calls: Array<{ method: string; args: unknown[] }>;\n track: (method: string) => (...args: unknown[]) => void;\n} {\n const calls: Array<{ method: string; args: unknown[] }> = [];\n return {\n calls,\n track:\n (method: string) =>\n (...args: unknown[]) => {\n calls.push({ method, args });\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Ergebnis eines Cloud-Load-Versuchs. Der Retry-Loop wertet `reason` aus,\n * um Rate-Limits und permanente Fehler korrekt zu behandeln.\n */\nexport type CloudLoadResult =\n /** Erfolgreich */\n | { ok: true }\n /** Netzwerk/Timeout \u2014 einfach sp\u00E4ter retryen */\n | { ok: false; reason: \"transient\" }\n /** Govee 429 \u2014 retry-after respektieren */\n | { ok: false; reason: \"rate-limited\"; retryAfterMs: number }\n /** Auth-Fehler (ung\u00FCltiger API-Key) \u2014 KEIN Retry, User muss Config korrigieren */\n | { ok: false; reason: \"auth-failed\"; message: string };\n\n// --- Cloud API v2 Types ---\n\n/** Device from Cloud API GET /router/api/v1/user/devices */\nexport interface CloudDevice {\n /** Product model (e.g. H6160) */\n sku: string;\n /** Unique device identifier */\n device: string;\n /** User-assigned device name */\n deviceName: string;\n /** Device category (e.g. \"devices.types.light\") */\n type: string;\n /** Device capabilities from Cloud API */\n capabilities: CloudCapability[];\n}\n\n/** A single capability from the Cloud API */\nexport interface CloudCapability {\n /** Capability type (e.g. \"devices.capabilities.on_off\") */\n type: string;\n /** Capability instance (e.g. \"powerSwitch\", \"brightness\") */\n instance: string;\n /** Parameter definition for this capability (optional \u2014 API can omit it) */\n parameters?: CapabilityParameters;\n}\n\n/** Parameter definition for a capability */\nexport interface CapabilityParameters {\n /** Value data type */\n dataType: \"ENUM\" | \"INTEGER\" | \"STRUCT\";\n /** Available options for ENUM type */\n options?: CapabilityOption[];\n /** Value range for INTEGER type */\n range?: { min: number; max: number; precision: number };\n /** Unit of measurement */\n unit?: string;\n /** Field definitions for STRUCT type */\n fields?: CapabilityField[];\n}\n\n/** ENUM option */\nexport interface CapabilityOption {\n /** Display name of the option */\n name: string;\n /** Option value (number, string, or complex object) */\n value: number | string | Record<string, unknown>;\n}\n\n/** STRUCT field definition */\nexport interface CapabilityField {\n /** Field name identifier */\n fieldName: string;\n /** Value data type */\n dataType?: \"ENUM\" | \"INTEGER\" | \"STRUCT\" | \"Array\";\n /** Available options for ENUM fields */\n options?: CapabilityOption[];\n /** Value range for INTEGER fields */\n range?: { min: number; max: number; precision: number };\n /** Element range for Array fields (0-based, segment count = max + 1) */\n elementRange?: { min: number; max: number };\n /** Whether this field is required */\n required?: boolean;\n}\n\n/** Cloud API device list response */\nexport interface CloudDeviceListResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** List of devices */\n data: CloudDevice[];\n}\n\n/** Cloud API device state response */\nexport interface CloudDeviceStateResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** Device state data */\n data: {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Current capability states */\n capabilities: CloudStateCapability[];\n };\n}\n\n/** A capability value from state response */\nexport interface CloudStateCapability {\n /** Capability type */\n type: string;\n /** Capability instance */\n instance: string;\n /** Current state value */\n state: { value: unknown };\n}\n\n/** Cloud API scenes response \u2014 payload contains capabilities with options */\nexport interface CloudScenesResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** Payload with capabilities (scenes endpoint format) */\n payload?: {\n /** Scene capabilities with options */\n capabilities: CloudCapability[];\n };\n}\n\n/** A scene/snapshot option from the Cloud API */\nexport interface CloudScene {\n /** Display name */\n name: string;\n /** Activation value (passed directly to control endpoint) \u2014 object for scenes, integer for snapshots */\n value: Record<string, unknown> | number;\n}\n\n// --- AWS IoT MQTT Types ---\n\n/** Login response from app2.govee.com */\nexport interface GoveeLoginResponse {\n /** API status code (200 = success) */\n status?: number;\n /** API status message */\n message?: string;\n /** Client authentication data (missing on auth failure) */\n client?: {\n /** Bearer token for API calls */\n token: string;\n /** Account identifier (numeric) */\n accountId: number | string;\n /** MQTT topic for status updates */\n topic: string;\n /** Token TTL in seconds. Govee uses this name; some responses also send `tokenExpireCycle`. */\n token_expire_cycle?: number;\n tokenExpireCycle?: number;\n };\n}\n\n/**\n * Bundle of credentials persisted across adapter restarts so we don't have\n * to log in every time (which would spam the Govee 2FA email each restart).\n * Populated from `native` after a successful login, fed back into the next\n * MQTT connect.\n */\nexport interface PersistedMqttCredentials {\n /** Govee bearer token from /v1/login. */\n bearerToken: string;\n /** AWS IoT endpoint hostname (xxx-ats.iot.<region>.amazonaws.com). */\n iotEndpoint: string;\n /** Base64-encoded PKCS#12 cert bundle from /iot/key. */\n p12Cert: string;\n /** Password for the P12 cert (also from /iot/key). */\n p12Pass: string;\n /** Govee account-id, used to build the MQTT clientId. */\n accountId: string;\n /** MQTT topic the account subscribes to for status push. */\n accountTopic: string;\n /** ms-timestamp at which the bearer token expires (Date.now() + ttlMs). */\n tokenExpiresAt: number;\n}\n\n/** IoT key response from app2.govee.com */\nexport interface GoveeIotKeyResponse {\n /** IoT credential data */\n data?: {\n /** AWS IoT endpoint hostname */\n endpoint: string;\n /** Base64-encoded PKCS12 certificate */\n p12: string;\n /** Password for the PKCS12 certificate */\n p12Pass: string;\n };\n}\n\n/** MQTT status update received on account topic */\nexport interface MqttStatusUpdate {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Device state values */\n state?: {\n /** Power state (1 = on, 0 = off) */\n onOff?: number;\n /** Brightness percentage 0-100 */\n brightness?: number;\n /** RGB color values */\n color?: { r: number; g: number; b: number };\n /** Color temperature in Kelvin */\n colorTemInKelvin?: number;\n };\n /** Operation data */\n op?: {\n /** Command strings */\n command?: string[];\n };\n}\n\n// --- LAN API Types ---\n\n/** LAN discovery response */\nexport interface LanDevice {\n /** Device IP address */\n ip: string;\n /** Device identifier */\n device: string;\n /** Product model */\n sku: string;\n}\n\n/** LAN status response */\nexport interface LanStatus {\n /** Power state (1 = on, 0 = off) */\n onOff: number;\n /** Brightness percentage 0-100 */\n brightness: number;\n /** RGB color values */\n color: { r: number; g: number; b: number };\n /** Color temperature in Kelvin */\n colorTemInKelvin: number;\n}\n\n/** LAN command message wrapper */\nexport interface LanMessage {\n /** Message payload */\n msg: {\n /** Command name */\n cmd: string;\n /** Command data */\n data: Record<string, unknown>;\n };\n}\n\n// --- Internal Device Model ---\n\n/** Unified device representation used by device-manager */\nexport interface GoveeDevice {\n /** Product model (e.g. H6160) */\n sku: string;\n /** Unique device ID (8-byte hex) */\n deviceId: string;\n /** Display name (from Cloud or SKU fallback) */\n name: string;\n /** Device type from Cloud (e.g. \"devices.types.light\") */\n type: string;\n /** LAN IP address if discovered */\n lanIp?: string;\n /** Capabilities from Cloud API */\n capabilities: CloudCapability[];\n /** Available light scenes (from Cloud scenes endpoint) */\n scenes: CloudScene[];\n /** Available DIY scenes (from Cloud scenes endpoint) */\n diyScenes: CloudScene[];\n /** Available snapshots (from Cloud scenes endpoint) */\n snapshots: CloudScene[];\n /** Scene library entries with scene codes for ptReal (from undocumented API) */\n sceneLibrary: Array<{\n name: string;\n /** BLE scene code (> 0 = usable via ptReal) */\n sceneCode: number;\n /** Base64-encoded BLE scene parameter data */\n scenceParam?: string;\n /** Speed control info (from scene library API) */\n speedInfo?: {\n /** Whether this scene supports speed adjustment */\n supSpeed: boolean;\n /** Default speed level index */\n speedIndex: number;\n /** JSON config with per-level moveIn/color/bright overrides */\n config: string;\n };\n }>;\n /** Music effect library entries for ptReal local music mode (authenticated API) */\n musicLibrary: Array<{\n name: string;\n /** BLE music effect code */\n musicCode: number;\n /** Base64-encoded BLE parameter data */\n scenceParam?: string;\n /** Music sub-mode index */\n mode?: number;\n }>;\n /** DIY light effect library entries for ptReal local DIY activation (authenticated API) */\n diyLibrary: Array<{\n name: string;\n /** BLE DIY effect code */\n diyCode: number;\n /** Base64-encoded BLE parameter data */\n scenceParam?: string;\n }>;\n /** Supported feature flags per SKU (from authenticated API) */\n skuFeatures: Record<string, unknown> | null;\n /** Group member devices (only for BaseGroup) */\n groupMembers?: { sku: string; deviceId: string }[];\n /** Last known state */\n state: DeviceState;\n /**\n * Number of LED segments on this device. Resolved by\n * {@link resolveSegmentCount} from Cache \u2192 MQTT-discovered \u2192 Cloud min.\n * Persisted via SKU cache so learned values survive restarts.\n */\n segmentCount?: number;\n /** BLE packets per cloud snapshot for ptReal activation [snapshotIdx][cmdIdx][packetBase64] */\n snapshotBleCmds?: string[][][];\n /** Current speed level for scene playback (0-based, applied on next scene activation) */\n sceneSpeed?: number;\n /**\n * Set to true after a Cloud scene-fetch attempt completed (success or confirmed empty).\n * Used to distinguish \"not yet tried\" from \"legitimately empty\" \u2014 prevents endless refetch.\n */\n scenesChecked?: boolean;\n /**\n * Manual-mode flag for cut strips (physical segments with gaps). When true,\n * `manualSegments` lists the indices that actually light up; all others\n * (within `0..segmentCount-1`) are skipped. Orthogonal to `segmentCount`:\n * the total is still the strip's real length, manualMode just masks gaps.\n */\n manualMode?: boolean;\n /**\n * Explicit physical segment indices (parsed from `segments.manual_list` state).\n * Only used when `manualMode=true`. Indices must be within `0..segmentCount-1`.\n */\n manualSegments?: number[];\n /**\n * Timestamp (ms) when device was last seen via LAN discovery or MQTT status push.\n * Used for cache pruning \u2014 stale entries without recent network sighting get removed.\n */\n lastSeenOnNetwork?: number;\n /**\n * Timestamp (ms) when device last replied to a LAN-direct probe (multicast\n * discovery or unicast devStatus). Only set by the LAN-Discovery / LAN-Status\n * paths \u2014 NOT by MQTT-push (broker buffering risk) and NOT by Cloud caps.\n * Used as the sole truth-source for `info.online` of Lights via the\n * StateManager.syncInfoOnline resolver (90s freshness window).\n */\n lastLanReplyAt?: number;\n /** Which channels are available */\n channels: {\n /** LAN UDP reachable */\n lan: boolean;\n /** MQTT connected */\n mqtt: boolean;\n /** Cloud API available */\n cloud: boolean;\n };\n}\n\n/** Current device state */\nexport interface DeviceState {\n /** Whether device is reachable */\n online: boolean;\n /** Power on/off */\n power?: boolean;\n /** Brightness 0-100 */\n brightness?: number;\n /** Color as \"#RRGGBB\" hex string */\n colorRgb?: string;\n /** Color temperature in Kelvin */\n colorTemperature?: number;\n /** Active scene name */\n scene?: string;\n /** Additional dynamic state values */\n [key: string]: unknown;\n}\n\n/**\n * Normalize device ID \u2014 remove colons, lowercase.\n * Returns empty string if input is not a string (defensive against malformed API data).\n *\n * @param id Raw device identifier\n */\nexport function normalizeDeviceId(id: string): string {\n if (typeof id !== \"string\") {\n return \"\";\n }\n return id.replace(/:/g, \"\").toLowerCase();\n}\n\n/** Error categories for dedup logging */\nexport type ErrorCategory =\n | \"NETWORK\"\n | \"TIMEOUT\"\n | \"AUTH\"\n | \"RATE_LIMIT\"\n /** Govee returned 454 with no code in the request body \u2014 user must request a verification code via Settings. */\n | \"VERIFICATION_PENDING\"\n /** Govee returned 454 with code already sent, or 455 \u2014 code is wrong or expired, user must request a fresh one. */\n | \"VERIFICATION_FAILED\"\n | \"UNKNOWN\";\n\n/**\n * Classify an error into a category for dedup logging.\n * Only the category is used as key \u2014 not context or full message.\n *\n * @param err Error to classify\n */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Error) {\n const code = (err as NodeJS.ErrnoException).code;\n if (\n code === \"ECONNREFUSED\" ||\n code === \"EHOSTUNREACH\" ||\n code === \"ENOTFOUND\" ||\n code === \"ENETUNREACH\" ||\n code === \"ECONNRESET\" ||\n code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (code === \"ETIMEDOUT\" || err.message.includes(\"timed out\")) {\n return \"TIMEOUT\";\n }\n }\n const msg = err instanceof Error ? err.message : String(err);\n if (\n msg.includes(\"ECONNREFUSED\") ||\n msg.includes(\"ENOTFOUND\") ||\n msg.includes(\"ENETUNREACH\") ||\n msg.includes(\"ECONNRESET\")\n ) {\n return \"NETWORK\";\n }\n if (msg.includes(\"Timeout\")) {\n return \"TIMEOUT\";\n }\n if (msg.includes(\"429\") || msg.includes(\"Rate limit\") || msg.includes(\"Rate limited\")) {\n return \"RATE_LIMIT\";\n }\n // 2FA-pending classification must come before AUTH \u2014 Govee returns 454 with\n // a leading \"454\" or \"Verification\" marker that would otherwise fall into AUTH\n // and trip the auth-failure backoff. Two distinct categories so the adapter\n // can pause reconnect on PENDING (waiting for user-entered code) but reset\n // on FAILED (code was sent but rejected, user retries via Settings button).\n if (msg.includes(\"Verification required\") || (msg.includes(\"status 454\") && !msg.includes(\"invalid\"))) {\n return \"VERIFICATION_PENDING\";\n }\n if (msg.includes(\"Verification code invalid\") || msg.includes(\"status 455\")) {\n return \"VERIFICATION_FAILED\";\n }\n if (msg.includes(\"401\") || msg.includes(\"403\") || msg.includes(\"Login failed\") || msg.includes(\"auth\")) {\n return \"AUTH\";\n }\n return \"UNKNOWN\";\n}\n\n/**\n * Render an unknown error to a string for logging.\n *\n * Prefer `e.stack` over `e.message` so warn/error logs include where the\n * error originated. For non-Error values falls back to `String(...)`.\n *\n * @param e Caught value (usually `unknown` in catch blocks)\n */\nexport function errMessage(e: unknown): string {\n if (e instanceof Error) {\n return e.message;\n }\n return String(e);\n}\n\n/**\n * Parse JSON without throwing. Returns null on parse failure or non-string\n * input. Caller decides how to handle null (skip, fallback, log).\n *\n * @param raw Raw JSON string\n */\nexport function safeJsonParse<T>(raw: unknown): T | null {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return null;\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Coerce an unknown value to a finite number. Returns null for NaN, Infinity,\n * non-numeric strings, objects, etc. Use at API boundaries where external\n * payloads might send numbers as strings, or send malformed values.\n *\n * Strings that contain a finite number are accepted (Govee occasionally sends\n * `brightness: \"50\"` instead of `brightness: 50`).\n *\n * @param raw Unknown input\n */\nexport function coerceFiniteNumber(raw: unknown): number | null {\n if (typeof raw === \"number\") {\n return Number.isFinite(raw) ? raw : null;\n }\n if (typeof raw === \"string\" && raw.trim().length > 0) {\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce an unknown value to a boolean. Accepts native bool, 0/1 numbers,\n * and the strings \"true\"/\"false\"/\"0\"/\"1\". Returns null for everything else.\n *\n * @param raw Unknown input\n */\nexport function coerceBool(raw: unknown): boolean | null {\n if (typeof raw === \"boolean\") {\n return raw;\n }\n if (raw === 0 || raw === 1) {\n return raw === 1;\n }\n if (typeof raw === \"string\") {\n const s = raw.trim().toLowerCase();\n if (s === \"true\" || s === \"1\") {\n return true;\n }\n if (s === \"false\" || s === \"0\") {\n return false;\n }\n }\n return null;\n}\n\n/**\n * Dedup-aware error logger.\n *\n * Compares the new error category against the caller's last category. On\n * change \u2192 warn (so the user sees fresh failures). On repeat \u2192 debug (so the\n * log doesn't spam). Returns the new category so the caller can update its\n * `lastErrorCategory` member.\n *\n * Caller pattern:\n * ```ts\n * this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"Cloud\", err);\n * ```\n *\n * @param log Adapter logger\n * @param last Previous category (null on first call)\n * @param context Short prefix (e.g. \"Cloud\", \"MQTT\", \"App-API\")\n * @param err Caught error\n * @returns New category (assign to caller's tracker)\n */\nexport function logDedup(\n log: ioBroker.Logger,\n last: ErrorCategory | null,\n context: string,\n err: unknown,\n): ErrorCategory {\n const category = classifyError(err);\n const msg = errMessage(err);\n if (category !== last) {\n log.warn(`${context}: ${msg}`);\n } else {\n log.debug(`${context}: ${msg} (repeated)`);\n }\n return category;\n}\n\n/**\n * Clamp a value to the 0-255 byte range. NaN/non-numeric inputs become 0.\n *\n * @param v Input value\n */\nfunction clampByte(v: unknown): number {\n const n = typeof v === \"number\" && Number.isFinite(v) ? v : 0;\n return Math.max(0, Math.min(255, Math.round(n)));\n}\n\n/**\n * Convert RGB values to hex color string \"#RRGGBB\".\n * Out-of-range or non-numeric inputs are clamped to produce valid hex.\n *\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\nexport function rgbToHex(r: number, g: number, b: number): string {\n const rr = clampByte(r).toString(16).padStart(2, \"0\");\n const gg = clampByte(g).toString(16).padStart(2, \"0\");\n const bb = clampByte(b).toString(16).padStart(2, \"0\");\n return `#${rr}${gg}${bb}`;\n}\n\n/**\n * Parse hex color string to RGB values. Returns black for non-string,\n * wrong-length or malformed input (defensive \u2014 upstream may pass unexpected\n * types or shortened forms like \"FF\" that would otherwise yield blue=255\n * via the bitshift below).\n *\n * @param hex Color string (e.g. \"#FF6600\" or \"FF6600\")\n */\nexport function hexToRgb(hex: string): { r: number; g: number; b: number } {\n if (typeof hex !== \"string\") {\n return { r: 0, g: 0, b: 0 };\n }\n const cleaned = hex.replace(\"#\", \"\");\n // Reject anything that isn't exactly 6 hex digits \u2014 accepting \"FF\" or\n // \"FFAABBCC\" would silently yield non-obvious RGB values via parseInt\n // truncation/sign-extension.\n if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {\n return { r: 0, g: 0, b: 0 };\n }\n const num = parseInt(cleaned, 16) || 0;\n return { r: (num >> 16) & 0xff, g: (num >> 8) & 0xff, b: num & 0xff };\n}\n\n/**\n * Convert packed RGB integer to hex color string \"#RRGGBB\"\n *\n * @param rgb Packed integer (r << 16 | g << 8 | b)\n */\nexport function rgbIntToHex(rgb: number): string {\n return `#${(rgb & 0xffffff).toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Result of parsing a manual-segments string like \"0-9\", \"0-2,4-9\", \"0,3,5\".\n *\n * indices Deduplicated, sorted list of segment indices\n *\n * error Human-readable error (null on success)\n */\nexport interface SegmentListParseResult {\n /** Deduplicated, sorted list of segment indices */\n indices: number[];\n /** Human-readable error (null on success) */\n error: string | null;\n}\n\n/**\n * Parse a user-provided segment-list string.\n * Accepts comma-separated singles (\"0,1,2\"), ranges (\"0-9\"), mixed\n * (\"0-8,10-14\"); whitespace-tolerant. Deduplicates automatically and\n * returns the result sorted ascending.\n *\n * @param input User-input string\n * @param maxIndex Per-device upper bound (e.g. device.segmentCount - 1). Indices > maxIndex are rejected.\n * @returns SegmentListParseResult with indices + optional error\n */\nexport function parseSegmentList(input: string, maxIndex: number): SegmentListParseResult {\n // Backstop matches the Govee bitmask protocol limit (`SEGMENT_HARD_MAX = 55`\n // in device-manager). Higher values would be silently dropped at the\n // ptReal layer anyway; rejecting them up front gives a clearer user error.\n const HARD_MAX = 55;\n if (typeof input !== \"string\") {\n return { indices: [], error: \"input must be a string\" };\n }\n const trimmed = input.trim();\n if (trimmed === \"\") {\n return { indices: [], error: \"list is empty\" };\n }\n const effectiveMax = Math.min(Number.isFinite(maxIndex) && maxIndex >= 0 ? Math.floor(maxIndex) : HARD_MAX, HARD_MAX);\n const set = new Set<number>();\n const parts = trimmed.split(\",\");\n for (const raw of parts) {\n const part = raw.trim();\n if (part === \"\") {\n continue;\n }\n const rangeMatch = /^(\\d+)\\s*-\\s*(\\d+)$/.exec(part);\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n if (start > end) {\n return {\n indices: [],\n error: `invalid range \"${part}\" (start > end)`,\n };\n }\n for (let i = start; i <= end; i++) {\n if (i < 0 || i > effectiveMax) {\n return {\n indices: [],\n error: `segment ${i} is outside 0-${effectiveMax} for this device`,\n };\n }\n set.add(i);\n }\n continue;\n }\n if (!/^\\d+$/.test(part)) {\n return {\n indices: [],\n error: `invalid entry \"${part}\" (only digits and ranges allowed)`,\n };\n }\n const idx = parseInt(part, 10);\n if (idx < 0 || idx > effectiveMax) {\n return {\n indices: [],\n error: `segment ${idx} is outside 0-${effectiveMax} for this device`,\n };\n }\n set.add(idx);\n }\n if (set.size === 0) {\n return { indices: [], error: \"no valid indices in list\" };\n }\n return {\n indices: Array.from(set).sort((a, b) => a - b),\n error: null,\n };\n}\n\n/**\n * Disambiguate a list of names by appending \" (2)\", \" (3)\" to repeats,\n * preserving the order. The first occurrence keeps the original name.\n *\n * Used both when building common.states maps and when reverse-resolving\n * a label back to an index \u2014 the SAME function on both sides guarantees\n * the user-visible label and the lookup target stay in sync, even when\n * the source list (cloud scenes etc.) contains duplicates.\n *\n * @param names Raw name list, possibly containing duplicates\n */\nexport function disambiguateLabels(names: string[]): string[] {\n const counts = new Map<string, number>();\n return names.map(name => {\n const seen = counts.get(name) ?? 0;\n counts.set(name, seen + 1);\n return seen === 0 ? name : `${name} (${seen + 1})`;\n });\n}\n\n/**\n * Build a `common.states` map from a list of named items, with index 0\n * reserved for a sentinel entry (default \"---\" = no selection).\n *\n * Duplicate names are disambiguated via `disambiguateLabels`, so each\n * value in the resulting map is unique and the reverse-lookup is\n * deterministic.\n *\n * @param items Source list \u2014 each item must have a `name` field\n * @param zeroLabel Label for index 0 (default \"---\" = no selection)\n */\nexport function buildUniqueLabelMap<T extends { name: string }>(items: T[], zeroLabel = \"---\"): Record<string, string> {\n const labels = disambiguateLabels(items.map(item => item.name));\n const result: Record<string, string> = { 0: zeroLabel };\n labels.forEach((label, i) => {\n result[String(i + 1)] = label;\n });\n return result;\n}\n\n/**\n * Result of resolving a state value against a `common.states` map.\n * `key` is the matching map key (string form, as stored in the map),\n * `canonical` is the matching label (the canonical, disambiguated form\n * \u2014 what the dropdown displays).\n */\nexport interface ResolvedStatesValue {\n /** The matching key from the states map, in string form */\n key: string;\n /** Canonical label as stored in the states map */\n canonical: string;\n}\n\n/**\n * Reverse-resolve a state value against a `common.states` map, accepting\n * three input forms:\n * - number `1` \u2192 direct key lookup\n * - string matching a key \u2192 direct key match (case-sensitive \u2014 keys\n * are identifiers like \"1\" or \"spectrum\")\n * - string matching a label \u2192 case-insensitive trim match against\n * the map values\n *\n * Returns null when no match is found. The caller decides whether to\n * warn, ack=false, or fall back to a default \u2014 this helper is pure.\n *\n * @param input User-supplied state value (number, string, or other)\n * @param statesMap The state's `common.states` map (key \u2192 label)\n */\nexport function resolveStatesValue(input: unknown, statesMap: Record<string, string>): ResolvedStatesValue | null {\n if (typeof input === \"number\" && Number.isFinite(input)) {\n const key = String(input);\n const canonical = statesMap[key];\n if (canonical !== undefined) {\n return { key, canonical };\n }\n return null;\n }\n if (typeof input === \"string\") {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n return null;\n }\n // Direct key match \u2014 handles numeric-string keys (\"1\") and\n // identifier-string keys (\"spectrum\") in one pass.\n const directLabel = statesMap[trimmed];\n if (directLabel !== undefined) {\n return { key: trimmed, canonical: directLabel };\n }\n // Label match \u2014 case-insensitive, trim. Lets users write the\n // human-readable name (e.g. \"Aurora\") regardless of casing.\n const needle = trimmed.toLowerCase();\n for (const [key, label] of Object.entries(statesMap)) {\n if (typeof label === \"string\" && label.trim().toLowerCase() === needle) {\n return { key, canonical: label };\n }\n }\n }\n return null;\n}\n\n/**\n * Event message from the OpenAPI-MQTT broker (mqtt.openapi.govee.com:8883).\n * Govee pushes one of these per device-capability state change \u2014 primarily\n * appliance events like lackWater, iceFull, bodyAppeared.\n */\nexport interface OpenApiMqttEvent {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Event capabilities (typically a single event entry) */\n capabilities: CloudStateCapability[];\n}\n\n/** Timer/callback interfaces for helper classes */\nexport interface TimerAdapter {\n /** Create a repeating interval timer */\n setInterval(callback: () => void, ms: number): ioBroker.Interval | undefined;\n /** Clear a repeating interval timer */\n clearInterval(timer: ioBroker.Interval): void;\n /** Create a one-shot timeout timer */\n setTimeout(callback: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Clear a one-shot timeout timer */\n clearTimeout(timer: ioBroker.Timeout): void;\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * Ergebnis eines Cloud-Load-Versuchs. Der Retry-Loop wertet `reason` aus,\n * um Rate-Limits und permanente Fehler korrekt zu behandeln.\n */\nexport type CloudLoadResult =\n /** Erfolgreich */\n | { ok: true }\n /** Netzwerk/Timeout \u2014 einfach sp\u00E4ter retryen */\n | { ok: false; reason: \"transient\" }\n /** Govee 429 \u2014 retry-after respektieren */\n | { ok: false; reason: \"rate-limited\"; retryAfterMs: number }\n /** Auth-Fehler (ung\u00FCltiger API-Key) \u2014 KEIN Retry, User muss Config korrigieren */\n | { ok: false; reason: \"auth-failed\"; message: string };\n\n// --- Cloud API v2 Types ---\n\n/** Device from Cloud API GET /router/api/v1/user/devices */\nexport interface CloudDevice {\n /** Product model (e.g. H6160) */\n sku: string;\n /** Unique device identifier */\n device: string;\n /** User-assigned device name */\n deviceName: string;\n /** Device category (e.g. \"devices.types.light\") */\n type: string;\n /** Device capabilities from Cloud API */\n capabilities: CloudCapability[];\n}\n\n/** A single capability from the Cloud API */\nexport interface CloudCapability {\n /** Capability type (e.g. \"devices.capabilities.on_off\") */\n type: string;\n /** Capability instance (e.g. \"powerSwitch\", \"brightness\") */\n instance: string;\n /** Parameter definition for this capability (optional \u2014 API can omit it) */\n parameters?: CapabilityParameters;\n}\n\n/** Parameter definition for a capability */\nexport interface CapabilityParameters {\n /** Value data type */\n dataType: \"ENUM\" | \"INTEGER\" | \"STRUCT\";\n /** Available options for ENUM type */\n options?: CapabilityOption[];\n /** Value range for INTEGER type */\n range?: { min: number; max: number; precision: number };\n /** Unit of measurement */\n unit?: string;\n /** Field definitions for STRUCT type */\n fields?: CapabilityField[];\n}\n\n/** ENUM option */\nexport interface CapabilityOption {\n /** Display name of the option */\n name: string;\n /** Option value (number, string, or complex object) */\n value: number | string | Record<string, unknown>;\n}\n\n/** STRUCT field definition */\nexport interface CapabilityField {\n /** Field name identifier */\n fieldName: string;\n /** Value data type */\n dataType?: \"ENUM\" | \"INTEGER\" | \"STRUCT\" | \"Array\";\n /** Available options for ENUM fields */\n options?: CapabilityOption[];\n /** Value range for INTEGER fields */\n range?: { min: number; max: number; precision: number };\n /** Element range for Array fields (0-based, segment count = max + 1) */\n elementRange?: { min: number; max: number };\n /** Whether this field is required */\n required?: boolean;\n}\n\n/** Cloud API device list response */\nexport interface CloudDeviceListResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** List of devices */\n data: CloudDevice[];\n}\n\n/** Cloud API device state response */\nexport interface CloudDeviceStateResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** Device state data */\n data: {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Current capability states */\n capabilities: CloudStateCapability[];\n };\n}\n\n/** A capability value from state response */\nexport interface CloudStateCapability {\n /** Capability type */\n type: string;\n /** Capability instance */\n instance: string;\n /** Current state value */\n state: { value: unknown };\n}\n\n/** Cloud API scenes response \u2014 payload contains capabilities with options */\nexport interface CloudScenesResponse {\n /** Response status code */\n code: number;\n /** Response message */\n message: string;\n /** Payload with capabilities (scenes endpoint format) */\n payload?: {\n /** Scene capabilities with options */\n capabilities: CloudCapability[];\n };\n}\n\n/** A scene/snapshot option from the Cloud API */\nexport interface CloudScene {\n /** Display name */\n name: string;\n /** Activation value (passed directly to control endpoint) \u2014 object for scenes, integer for snapshots */\n value: Record<string, unknown> | number;\n}\n\n// --- AWS IoT MQTT Types ---\n\n/** Login response from app2.govee.com */\nexport interface GoveeLoginResponse {\n /** API status code (200 = success) */\n status?: number;\n /** API status message */\n message?: string;\n /** Client authentication data (missing on auth failure) */\n client?: {\n /** Bearer token for API calls */\n token: string;\n /** Account identifier (numeric) */\n accountId: number | string;\n /** MQTT topic for status updates */\n topic: string;\n /** Token TTL in seconds. Govee uses this name; some responses also send `tokenExpireCycle`. */\n token_expire_cycle?: number;\n tokenExpireCycle?: number;\n };\n}\n\n/**\n * Bundle of credentials persisted across adapter restarts so we don't have\n * to log in every time (which would spam the Govee 2FA email each restart).\n * Populated from `native` after a successful login, fed back into the next\n * MQTT connect.\n */\nexport interface PersistedMqttCredentials {\n /** Govee bearer token from /v1/login. */\n bearerToken: string;\n /** AWS IoT endpoint hostname (xxx-ats.iot.<region>.amazonaws.com). */\n iotEndpoint: string;\n /** Base64-encoded PKCS#12 cert bundle from /iot/key. */\n p12Cert: string;\n /** Password for the P12 cert (also from /iot/key). */\n p12Pass: string;\n /** Govee account-id, used to build the MQTT clientId. */\n accountId: string;\n /** MQTT topic the account subscribes to for status push. */\n accountTopic: string;\n /** ms-timestamp at which the bearer token expires (Date.now() + ttlMs). */\n tokenExpiresAt: number;\n}\n\n/** IoT key response from app2.govee.com */\nexport interface GoveeIotKeyResponse {\n /** IoT credential data */\n data?: {\n /** AWS IoT endpoint hostname */\n endpoint: string;\n /** Base64-encoded PKCS12 certificate */\n p12: string;\n /** Password for the PKCS12 certificate */\n p12Pass: string;\n };\n}\n\n/** MQTT status update received on account topic */\nexport interface MqttStatusUpdate {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Device state values */\n state?: {\n /** Power state (1 = on, 0 = off) */\n onOff?: number;\n /** Brightness percentage 0-100 */\n brightness?: number;\n /** RGB color values */\n color?: { r: number; g: number; b: number };\n /** Color temperature in Kelvin */\n colorTemInKelvin?: number;\n };\n /** Operation data */\n op?: {\n /** Command strings */\n command?: string[];\n };\n}\n\n// --- LAN API Types ---\n\n/** LAN discovery response */\nexport interface LanDevice {\n /** Device IP address */\n ip: string;\n /** Device identifier */\n device: string;\n /** Product model */\n sku: string;\n}\n\n/** LAN status response */\nexport interface LanStatus {\n /** Power state (1 = on, 0 = off) */\n onOff: number;\n /** Brightness percentage 0-100 */\n brightness: number;\n /** RGB color values */\n color: { r: number; g: number; b: number };\n /** Color temperature in Kelvin */\n colorTemInKelvin: number;\n}\n\n/** LAN command message wrapper */\nexport interface LanMessage {\n /** Message payload */\n msg: {\n /** Command name */\n cmd: string;\n /** Command data */\n data: Record<string, unknown>;\n };\n}\n\n// --- Internal Device Model ---\n\n/** Unified device representation used by device-manager */\nexport interface GoveeDevice {\n /** Product model (e.g. H6160) */\n sku: string;\n /** Unique device ID (8-byte hex) */\n deviceId: string;\n /** Display name (from Cloud or SKU fallback) */\n name: string;\n /** Device type from Cloud (e.g. \"devices.types.light\") */\n type: string;\n /** LAN IP address if discovered */\n lanIp?: string;\n /** Capabilities from Cloud API */\n capabilities: CloudCapability[];\n /** Available light scenes (from Cloud scenes endpoint) */\n scenes: CloudScene[];\n /** Available DIY scenes (from Cloud scenes endpoint) */\n diyScenes: CloudScene[];\n /** Available snapshots (from Cloud scenes endpoint) */\n snapshots: CloudScene[];\n /** Scene library entries with scene codes for ptReal (from undocumented API) */\n sceneLibrary: Array<{\n name: string;\n /** BLE scene code (> 0 = usable via ptReal) */\n sceneCode: number;\n /** Base64-encoded BLE scene parameter data */\n scenceParam?: string;\n /** Speed control info (from scene library API) */\n speedInfo?: {\n /** Whether this scene supports speed adjustment */\n supSpeed: boolean;\n /** Default speed level index */\n speedIndex: number;\n /** JSON config with per-level moveIn/color/bright overrides */\n config: string;\n };\n }>;\n /** Music effect library entries for ptReal local music mode (authenticated API) */\n musicLibrary: Array<{\n name: string;\n /** BLE music effect code */\n musicCode: number;\n /** Base64-encoded BLE parameter data */\n scenceParam?: string;\n /** Music sub-mode index */\n mode?: number;\n }>;\n /** DIY light effect library entries for ptReal local DIY activation (authenticated API) */\n diyLibrary: Array<{\n name: string;\n /** BLE DIY effect code */\n diyCode: number;\n /** Base64-encoded BLE parameter data */\n scenceParam?: string;\n }>;\n /** Supported feature flags per SKU (from authenticated API) */\n skuFeatures: Record<string, unknown> | null;\n /** Group member devices (only for BaseGroup) */\n groupMembers?: { sku: string; deviceId: string }[];\n /** Last known state */\n state: DeviceState;\n /**\n * Number of LED segments on this device. Resolved by\n * {@link resolveSegmentCount} from Cache \u2192 MQTT-discovered \u2192 Cloud min.\n * Persisted via SKU cache so learned values survive restarts.\n */\n segmentCount?: number;\n /** BLE packets per cloud snapshot for ptReal activation [snapshotIdx][cmdIdx][packetBase64] */\n snapshotBleCmds?: string[][][];\n /** Current speed level for scene playback (0-based, applied on next scene activation) */\n sceneSpeed?: number;\n /**\n * Set to true after a Cloud scene-fetch attempt completed (success or confirmed empty).\n * Used to distinguish \"not yet tried\" from \"legitimately empty\" \u2014 prevents endless refetch.\n */\n scenesChecked?: boolean;\n /**\n * Manual-mode flag for cut strips (physical segments with gaps). When true,\n * `manualSegments` lists the indices that actually light up; all others\n * (within `0..segmentCount-1`) are skipped. Orthogonal to `segmentCount`:\n * the total is still the strip's real length, manualMode just masks gaps.\n */\n manualMode?: boolean;\n /**\n * Explicit physical segment indices (parsed from `segments.manual_list` state).\n * Only used when `manualMode=true`. Indices must be within `0..segmentCount-1`.\n */\n manualSegments?: number[];\n /**\n * Timestamp (ms) when device was last seen via LAN discovery or MQTT status push.\n * Used for cache pruning \u2014 stale entries without recent network sighting get removed.\n */\n lastSeenOnNetwork?: number;\n /**\n * Timestamp (ms) when device last replied to a LAN-direct probe (multicast\n * discovery or unicast devStatus). Only set by the LAN-Discovery / LAN-Status\n * paths \u2014 NOT by MQTT-push (broker buffering risk) and NOT by Cloud caps.\n * Used as the sole truth-source for `info.online` of Lights via the\n * StateManager.syncInfoOnline resolver (90s freshness window).\n */\n lastLanReplyAt?: number;\n /** Which channels are available */\n channels: {\n /** LAN UDP reachable */\n lan: boolean;\n /** MQTT connected */\n mqtt: boolean;\n /** Cloud API available */\n cloud: boolean;\n };\n}\n\n/** Current device state */\nexport interface DeviceState {\n /** Whether device is reachable */\n online: boolean;\n /** Power on/off */\n power?: boolean;\n /** Brightness 0-100 */\n brightness?: number;\n /** Color as \"#RRGGBB\" hex string */\n colorRgb?: string;\n /** Color temperature in Kelvin */\n colorTemperature?: number;\n /** Active scene name */\n scene?: string;\n /** Additional dynamic state values */\n [key: string]: unknown;\n}\n\n/**\n * Normalize device ID \u2014 remove colons, lowercase.\n * Returns empty string if input is not a string (defensive against malformed API data).\n *\n * @param id Raw device identifier\n */\nexport function normalizeDeviceId(id: string): string {\n if (typeof id !== \"string\") {\n return \"\";\n }\n return id.replace(/:/g, \"\").toLowerCase();\n}\n\n/** Error categories for dedup logging */\nexport type ErrorCategory =\n | \"NETWORK\"\n | \"TIMEOUT\"\n | \"AUTH\"\n | \"RATE_LIMIT\"\n /** Govee returned 454 with no code in the request body \u2014 user must request a verification code via Settings. */\n | \"VERIFICATION_PENDING\"\n /** Govee returned 454 with code already sent, or 455 \u2014 code is wrong or expired, user must request a fresh one. */\n | \"VERIFICATION_FAILED\"\n | \"UNKNOWN\";\n\n/**\n * Classify an error into a category for dedup logging.\n * Only the category is used as key \u2014 not context or full message.\n *\n * @param err Error to classify\n */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Error) {\n const code = (err as NodeJS.ErrnoException).code;\n if (\n code === \"ECONNREFUSED\" ||\n code === \"EHOSTUNREACH\" ||\n code === \"ENOTFOUND\" ||\n code === \"ENETUNREACH\" ||\n code === \"ECONNRESET\" ||\n code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (code === \"ETIMEDOUT\" || err.message.includes(\"timed out\")) {\n return \"TIMEOUT\";\n }\n }\n const msg = err instanceof Error ? err.message : String(err);\n if (\n msg.includes(\"ECONNREFUSED\") ||\n msg.includes(\"ENOTFOUND\") ||\n msg.includes(\"ENETUNREACH\") ||\n msg.includes(\"ECONNRESET\")\n ) {\n return \"NETWORK\";\n }\n if (msg.includes(\"Timeout\")) {\n return \"TIMEOUT\";\n }\n if (msg.includes(\"429\") || msg.includes(\"Rate limit\") || msg.includes(\"Rate limited\")) {\n return \"RATE_LIMIT\";\n }\n // 2FA-pending classification must come before AUTH \u2014 Govee returns 454 with\n // a leading \"454\" or \"Verification\" marker that would otherwise fall into AUTH\n // and trip the auth-failure backoff. Two distinct categories so the adapter\n // can pause reconnect on PENDING (waiting for user-entered code) but reset\n // on FAILED (code was sent but rejected, user retries via Settings button).\n if (msg.includes(\"Verification required\") || (msg.includes(\"status 454\") && !msg.includes(\"invalid\"))) {\n return \"VERIFICATION_PENDING\";\n }\n if (msg.includes(\"Verification code invalid\") || msg.includes(\"status 455\")) {\n return \"VERIFICATION_FAILED\";\n }\n if (msg.includes(\"401\") || msg.includes(\"403\") || msg.includes(\"Login failed\") || msg.includes(\"auth\")) {\n return \"AUTH\";\n }\n return \"UNKNOWN\";\n}\n\n/**\n * Render an unknown error to a string for logging.\n *\n * Prefer `e.stack` over `e.message` so warn/error logs include where the\n * error originated. For non-Error values falls back to `String(...)`.\n *\n * @param e Caught value (usually `unknown` in catch blocks)\n */\nexport function errMessage(e: unknown): string {\n if (e instanceof Error) {\n return e.message;\n }\n return String(e);\n}\n\n/**\n * Parse JSON without throwing. Returns null on parse failure or non-string\n * input. Caller decides how to handle null (skip, fallback, log).\n *\n * @param raw Raw JSON string\n */\nexport function safeJsonParse<T>(raw: unknown): T | null {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return null;\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Coerce an unknown value to a finite number. Returns null for NaN, Infinity,\n * non-numeric strings, objects, etc. Use at API boundaries where external\n * payloads might send numbers as strings, or send malformed values.\n *\n * Strings that contain a finite number are accepted (Govee occasionally sends\n * `brightness: \"50\"` instead of `brightness: 50`).\n *\n * @param raw Unknown input\n */\nexport function coerceFiniteNumber(raw: unknown): number | null {\n if (typeof raw === \"number\") {\n return Number.isFinite(raw) ? raw : null;\n }\n if (typeof raw === \"string\" && raw.trim().length > 0) {\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce an unknown value to a boolean. Accepts native bool, 0/1 numbers,\n * and the strings \"true\"/\"false\"/\"0\"/\"1\". Returns null for everything else.\n *\n * @param raw Unknown input\n */\nexport function coerceBool(raw: unknown): boolean | null {\n if (typeof raw === \"boolean\") {\n return raw;\n }\n if (raw === 0 || raw === 1) {\n return raw === 1;\n }\n if (typeof raw === \"string\") {\n const s = raw.trim().toLowerCase();\n if (s === \"true\" || s === \"1\") {\n return true;\n }\n if (s === \"false\" || s === \"0\") {\n return false;\n }\n }\n return null;\n}\n\n/**\n * Dedup-aware error logger.\n *\n * Compares the new error category against the caller's last category. On\n * change \u2192 warn (so the user sees fresh failures). On repeat \u2192 debug (so the\n * log doesn't spam). Returns the new category so the caller can update its\n * `lastErrorCategory` member.\n *\n * Caller pattern:\n * ```ts\n * this.lastErrorCategory = logDedup(this.log, this.lastErrorCategory, \"Cloud\", err);\n * ```\n *\n * @param log Adapter logger\n * @param last Previous category (null on first call)\n * @param context Short prefix (e.g. \"Cloud\", \"MQTT\", \"App-API\")\n * @param err Caught error\n * @returns New category (assign to caller's tracker)\n */\nexport function logDedup(\n log: ioBroker.Logger,\n last: ErrorCategory | null,\n context: string,\n err: unknown,\n): ErrorCategory {\n const category = classifyError(err);\n const msg = errMessage(err);\n if (category !== last) {\n log.warn(`${context}: ${msg}`);\n } else {\n log.debug(`${context}: ${msg} (repeated)`);\n }\n return category;\n}\n\n/**\n * Clamp a value to the 0-255 byte range. NaN/non-numeric inputs become 0.\n *\n * @param v Input value\n */\nfunction clampByte(v: unknown): number {\n const n = typeof v === \"number\" && Number.isFinite(v) ? v : 0;\n return Math.max(0, Math.min(255, Math.round(n)));\n}\n\n/**\n * Convert RGB values to hex color string \"#RRGGBB\".\n * Out-of-range or non-numeric inputs are clamped to produce valid hex.\n *\n * @param r Red channel 0-255\n * @param g Green channel 0-255\n * @param b Blue channel 0-255\n */\nexport function rgbToHex(r: number, g: number, b: number): string {\n const rr = clampByte(r).toString(16).padStart(2, \"0\");\n const gg = clampByte(g).toString(16).padStart(2, \"0\");\n const bb = clampByte(b).toString(16).padStart(2, \"0\");\n return `#${rr}${gg}${bb}`;\n}\n\n/**\n * Parse hex color string to RGB values. Returns black for non-string,\n * wrong-length or malformed input (defensive \u2014 upstream may pass unexpected\n * types or shortened forms like \"FF\" that would otherwise yield blue=255\n * via the bitshift below).\n *\n * @param hex Color string (e.g. \"#FF6600\" or \"FF6600\")\n */\nexport function hexToRgb(hex: string): { r: number; g: number; b: number } {\n if (typeof hex !== \"string\") {\n return { r: 0, g: 0, b: 0 };\n }\n const cleaned = hex.replace(\"#\", \"\");\n // Reject anything that isn't exactly 6 hex digits \u2014 accepting \"FF\" or\n // \"FFAABBCC\" would silently yield non-obvious RGB values via parseInt\n // truncation/sign-extension.\n if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {\n return { r: 0, g: 0, b: 0 };\n }\n const num = parseInt(cleaned, 16) || 0;\n return { r: (num >> 16) & 0xff, g: (num >> 8) & 0xff, b: num & 0xff };\n}\n\n/**\n * Convert packed RGB integer to hex color string \"#RRGGBB\"\n *\n * @param rgb Packed integer (r << 16 | g << 8 | b)\n */\nexport function rgbIntToHex(rgb: number): string {\n return `#${(rgb & 0xffffff).toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Result of parsing a manual-segments string like \"0-9\", \"0-2,4-9\", \"0,3,5\".\n *\n * indices Deduplicated, sorted list of segment indices\n *\n * error Human-readable error (null on success)\n */\nexport interface SegmentListParseResult {\n /** Deduplicated, sorted list of segment indices */\n indices: number[];\n /** Human-readable error (null on success) */\n error: string | null;\n}\n\n/**\n * Parse a user-provided segment-list string.\n * Accepts comma-separated singles (\"0,1,2\"), ranges (\"0-9\"), mixed\n * (\"0-8,10-14\"); whitespace-tolerant. Deduplicates automatically and\n * returns the result sorted ascending.\n *\n * @param input User-input string\n * @param maxIndex Per-device upper bound (e.g. device.segmentCount - 1). Indices > maxIndex are rejected.\n * @returns SegmentListParseResult with indices + optional error\n */\nexport function parseSegmentList(input: string, maxIndex: number): SegmentListParseResult {\n // Backstop matches the Govee bitmask protocol limit (`SEGMENT_HARD_MAX = 55`\n // in device-manager). Higher values would be silently dropped at the\n // ptReal layer anyway; rejecting them up front gives a clearer user error.\n const HARD_MAX = 55;\n if (typeof input !== \"string\") {\n return { indices: [], error: \"input must be a string\" };\n }\n const trimmed = input.trim();\n if (trimmed === \"\") {\n return { indices: [], error: \"list is empty\" };\n }\n const effectiveMax = Math.min(Number.isFinite(maxIndex) && maxIndex >= 0 ? Math.floor(maxIndex) : HARD_MAX, HARD_MAX);\n const set = new Set<number>();\n const parts = trimmed.split(\",\");\n for (const raw of parts) {\n const part = raw.trim();\n if (part === \"\") {\n continue;\n }\n const rangeMatch = /^(\\d+)\\s*-\\s*(\\d+)$/.exec(part);\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10);\n const end = parseInt(rangeMatch[2], 10);\n if (start > end) {\n return {\n indices: [],\n error: `invalid range \"${part}\" (start > end)`,\n };\n }\n for (let i = start; i <= end; i++) {\n if (i < 0 || i > effectiveMax) {\n return {\n indices: [],\n error: `segment ${i} is outside 0-${effectiveMax} for this device`,\n };\n }\n set.add(i);\n }\n continue;\n }\n if (!/^\\d+$/.test(part)) {\n return {\n indices: [],\n error: `invalid entry \"${part}\" (only digits and ranges allowed)`,\n };\n }\n const idx = parseInt(part, 10);\n if (idx < 0 || idx > effectiveMax) {\n return {\n indices: [],\n error: `segment ${idx} is outside 0-${effectiveMax} for this device`,\n };\n }\n set.add(idx);\n }\n if (set.size === 0) {\n return { indices: [], error: \"no valid indices in list\" };\n }\n return {\n indices: Array.from(set).sort((a, b) => a - b),\n error: null,\n };\n}\n\n/**\n * Disambiguate a list of names by appending \" (2)\", \" (3)\" to repeats,\n * preserving the order. The first occurrence keeps the original name.\n *\n * Used both when building common.states maps and when reverse-resolving\n * a label back to an index \u2014 the SAME function on both sides guarantees\n * the user-visible label and the lookup target stay in sync, even when\n * the source list (cloud scenes etc.) contains duplicates.\n *\n * @param names Raw name list, possibly containing duplicates\n */\nexport function disambiguateLabels(names: string[]): string[] {\n const counts = new Map<string, number>();\n return names.map(name => {\n const seen = counts.get(name) ?? 0;\n counts.set(name, seen + 1);\n return seen === 0 ? name : `${name} (${seen + 1})`;\n });\n}\n\n/**\n * Build a `common.states` map from a list of named items, with index 0\n * reserved for a sentinel entry (default \"---\" = no selection).\n *\n * Duplicate names are disambiguated via `disambiguateLabels`, so each\n * value in the resulting map is unique and the reverse-lookup is\n * deterministic.\n *\n * @param items Source list \u2014 each item must have a `name` field\n * @param zeroLabel Label for index 0 (default \"---\" = no selection)\n */\nexport function buildUniqueLabelMap<T extends { name: string }>(items: T[], zeroLabel = \"---\"): Record<string, string> {\n const labels = disambiguateLabels(items.map(item => item.name));\n const result: Record<string, string> = { 0: zeroLabel };\n labels.forEach((label, i) => {\n result[String(i + 1)] = label;\n });\n return result;\n}\n\n/**\n * Result of resolving a state value against a `common.states` map.\n * `key` is the matching map key (string form, as stored in the map),\n * `canonical` is the matching label (the canonical, disambiguated form\n * \u2014 what the dropdown displays).\n */\nexport interface ResolvedStatesValue {\n /** The matching key from the states map, in string form */\n key: string;\n /** Canonical label as stored in the states map */\n canonical: string;\n}\n\n/**\n * Reverse-resolve a state value against a `common.states` map, accepting\n * three input forms:\n * - number `1` \u2192 direct key lookup\n * - string matching a key \u2192 direct key match (case-sensitive \u2014 keys\n * are identifiers like \"1\" or \"spectrum\")\n * - string matching a label \u2192 case-insensitive trim match against\n * the map values\n *\n * Returns null when no match is found. The caller decides whether to\n * warn, ack=false, or fall back to a default \u2014 this helper is pure.\n *\n * @param input User-supplied state value (number, string, or other)\n * @param statesMap The state's `common.states` map (key \u2192 label)\n */\nexport function resolveStatesValue(input: unknown, statesMap: Record<string, string>): ResolvedStatesValue | null {\n if (typeof input === \"number\" && Number.isFinite(input)) {\n const key = String(input);\n const canonical = statesMap[key];\n if (canonical !== undefined) {\n return { key, canonical };\n }\n return null;\n }\n if (typeof input === \"string\") {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n return null;\n }\n // Direct key match \u2014 handles numeric-string keys (\"1\") and\n // identifier-string keys (\"spectrum\") in one pass.\n const directLabel = statesMap[trimmed];\n if (directLabel !== undefined) {\n return { key: trimmed, canonical: directLabel };\n }\n // Label match \u2014 case-insensitive, trim. Lets users write the\n // human-readable name (e.g. \"Aurora\") regardless of casing.\n const needle = trimmed.toLowerCase();\n for (const [key, label] of Object.entries(statesMap)) {\n if (typeof label === \"string\" && label.trim().toLowerCase() === needle) {\n return { key, canonical: label };\n }\n }\n }\n return null;\n}\n\n/**\n * Event message from the OpenAPI-MQTT broker (mqtt.openapi.govee.com:8883).\n * Govee pushes one of these per device-capability state change \u2014 primarily\n * appliance events like lackWater, iceFull, bodyAppeared.\n */\nexport interface OpenApiMqttEvent {\n /** Product model */\n sku: string;\n /** Device identifier */\n device: string;\n /** Event capabilities (typically a single event entry) */\n capabilities: CloudStateCapability[];\n}\n\n/** Timer/callback interfaces for helper classes */\nexport interface TimerAdapter {\n /** Create a repeating interval timer */\n setInterval(callback: () => void, ms: number): ioBroker.Interval | undefined;\n /** Clear a repeating interval timer */\n clearInterval(timer: ioBroker.Interval): void;\n /** Create a one-shot timeout timer */\n setTimeout(callback: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Clear a one-shot timeout timer */\n clearTimeout(timer: ioBroker.Timeout): void;\n /** Async delay that gets cancelled on adapter unload */\n delay(ms: number): Promise<void>;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuYO,SAAS,kBAAkB,IAAoB;AACpD,MAAI,OAAO,OAAO,UAAU;AAC1B,WAAO;AAAA,EACT;AACA,SAAO,GAAG,QAAQ,MAAM,EAAE,EAAE,YAAY;AAC1C;AAoBO,SAAS,cAAc,KAA6B;AACzD,MAAI,eAAe,OAAO;AACxB,UAAM,OAAQ,IAA8B;AAC5C,QACE,SAAS,kBACT,SAAS,kBACT,SAAS,eACT,SAAS,iBACT,SAAS,gBACT,SAAS,aACT;AACA,aAAO;AAAA,IACT;AACA,QAAI,SAAS,eAAe,IAAI,QAAQ,SAAS,WAAW,GAAG;AAC7D,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,MACE,IAAI,SAAS,cAAc,KAC3B,IAAI,SAAS,WAAW,KACxB,IAAI,SAAS,aAAa,KAC1B,IAAI,SAAS,YAAY,GACzB;AACA,WAAO;AAAA,EACT;AACA,MAAI,IAAI,SAAS,SAAS,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,YAAY,KAAK,IAAI,SAAS,cAAc,GAAG;AACrF,WAAO;AAAA,EACT;AAMA,MAAI,IAAI,SAAS,uBAAuB,KAAM,IAAI,SAAS,YAAY,KAAK,CAAC,IAAI,SAAS,SAAS,GAAI;AACrG,WAAO;AAAA,EACT;AACA,MAAI,IAAI,SAAS,2BAA2B,KAAK,IAAI,SAAS,YAAY,GAAG;AAC3E,WAAO;AAAA,EACT;AACA,MAAI,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,cAAc,KAAK,IAAI,SAAS,MAAM,GAAG;AACtG,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAUO,SAAS,WAAW,GAAoB;AAC7C,MAAI,aAAa,OAAO;AACtB,WAAO,EAAE;AAAA,EACX;AACA,SAAO,OAAO,CAAC;AACjB;AAQO,SAAS,cAAiB,KAAwB;AACvD,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,WAAO;AAAA,EACT;AACA,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,mBAAmB,KAA6B;AAC9D,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO,OAAO,SAAS,GAAG,IAAI,MAAM;AAAA,EACtC;AACA,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,EAAE,SAAS,GAAG;AACpD,UAAM,IAAI,OAAO,GAAG;AACpB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAQO,SAAS,WAAW,KAA8B;AACvD,MAAI,OAAO,QAAQ,WAAW;AAC5B,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,KAAK,QAAQ,GAAG;AAC1B,WAAO,QAAQ;AAAA,EACjB;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI,IAAI,KAAK,EAAE,YAAY;AACjC,QAAI,MAAM,UAAU,MAAM,KAAK;AAC7B,aAAO;AAAA,IACT;AACA,QAAI,MAAM,WAAW,MAAM,KAAK;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAqBO,SAAS,SACd,KACA,MACA,SACA,KACe;AACf,QAAM,WAAW,cAAc,GAAG;AAClC,QAAM,MAAM,WAAW,GAAG;AAC1B,MAAI,aAAa,MAAM;AACrB,QAAI,KAAK,GAAG,OAAO,KAAK,GAAG,EAAE;AAAA,EAC/B,OAAO;AACL,QAAI,MAAM,GAAG,OAAO,KAAK,GAAG,aAAa;AAAA,EAC3C;AACA,SAAO;AACT;AAOA,SAAS,UAAU,GAAoB;AACrC,QAAM,IAAI,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAC5D,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACjD;AAUO,SAAS,SAAS,GAAW,GAAW,GAAmB;AAChE,QAAM,KAAK,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACpD,QAAM,KAAK,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACpD,QAAM,KAAK,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACpD,SAAO,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE;AACzB;AAUO,SAAS,SAAS,KAAkD;AACzE,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AAAA,EAC5B;AACA,QAAM,UAAU,IAAI,QAAQ,KAAK,EAAE;AAInC,MAAI,CAAC,mBAAmB,KAAK,OAAO,GAAG;AACrC,WAAO,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AAAA,EAC5B;AACA,QAAM,MAAM,SAAS,SAAS,EAAE,KAAK;AACrC,SAAO,EAAE,GAAI,OAAO,KAAM,KAAM,GAAI,OAAO,IAAK,KAAM,GAAG,MAAM,IAAK;AACtE;AAOO,SAAS,YAAY,KAAqB;AAC/C,SAAO,KAAK,MAAM,UAAU,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAC3D;AA0BO,SAAS,iBAAiB,OAAe,UAA0C;AAIxF,QAAM,WAAW;AACjB,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,yBAAyB;AAAA,EACxD;AACA,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,IAAI;AAClB,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,gBAAgB;AAAA,EAC/C;AACA,QAAM,eAAe,KAAK,IAAI,OAAO,SAAS,QAAQ,KAAK,YAAY,IAAI,KAAK,MAAM,QAAQ,IAAI,UAAU,QAAQ;AACpH,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,aAAW,OAAO,OAAO;AACvB,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,SAAS,IAAI;AACf;AAAA,IACF;AACA,UAAM,aAAa,sBAAsB,KAAK,IAAI;AAClD,QAAI,YAAY;AACd,YAAM,QAAQ,SAAS,WAAW,CAAC,GAAG,EAAE;AACxC,YAAM,MAAM,SAAS,WAAW,CAAC,GAAG,EAAE;AACtC,UAAI,QAAQ,KAAK;AACf,eAAO;AAAA,UACL,SAAS,CAAC;AAAA,UACV,OAAO,kBAAkB,IAAI;AAAA,QAC/B;AAAA,MACF;AACA,eAAS,IAAI,OAAO,KAAK,KAAK,KAAK;AACjC,YAAI,IAAI,KAAK,IAAI,cAAc;AAC7B,iBAAO;AAAA,YACL,SAAS,CAAC;AAAA,YACV,OAAO,WAAW,CAAC,iBAAiB,YAAY;AAAA,UAClD;AAAA,QACF;AACA,YAAI,IAAI,CAAC;AAAA,MACX;AACA;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,KAAK,IAAI,GAAG;AACvB,aAAO;AAAA,QACL,SAAS,CAAC;AAAA,QACV,OAAO,kBAAkB,IAAI;AAAA,MAC/B;AAAA,IACF;AACA,UAAM,MAAM,SAAS,MAAM,EAAE;AAC7B,QAAI,MAAM,KAAK,MAAM,cAAc;AACjC,aAAO;AAAA,QACL,SAAS,CAAC;AAAA,QACV,OAAO,WAAW,GAAG,iBAAiB,YAAY;AAAA,MACpD;AAAA,IACF;AACA,QAAI,IAAI,GAAG;AAAA,EACb;AACA,MAAI,IAAI,SAAS,GAAG;AAClB,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,2BAA2B;AAAA,EAC1D;AACA,SAAO;AAAA,IACL,SAAS,MAAM,KAAK,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,IAC7C,OAAO;AAAA,EACT;AACF;AAaO,SAAS,mBAAmB,OAA2B;AAC5D,QAAM,SAAS,oBAAI,IAAoB;AACvC,SAAO,MAAM,IAAI,UAAQ;AAjuB3B;AAkuBI,UAAM,QAAO,YAAO,IAAI,IAAI,MAAf,YAAoB;AACjC,WAAO,IAAI,MAAM,OAAO,CAAC;AACzB,WAAO,SAAS,IAAI,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC;AAAA,EACjD,CAAC;AACH;AAaO,SAAS,oBAAgD,OAAY,YAAY,OAA+B;AACrH,QAAM,SAAS,mBAAmB,MAAM,IAAI,UAAQ,KAAK,IAAI,CAAC;AAC9D,QAAM,SAAiC,EAAE,GAAG,UAAU;AACtD,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,WAAO,OAAO,IAAI,CAAC,CAAC,IAAI;AAAA,EAC1B,CAAC;AACD,SAAO;AACT;AA8BO,SAAS,mBAAmB,OAAgB,WAA+D;AAChH,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG;AACvD,UAAM,MAAM,OAAO,KAAK;AACxB,UAAM,YAAY,UAAU,GAAG;AAC/B,QAAI,cAAc,QAAW;AAC3B,aAAO,EAAE,KAAK,UAAU;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,YAAY,IAAI;AAClB,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,UAAU,OAAO;AACrC,QAAI,gBAAgB,QAAW;AAC7B,aAAO,EAAE,KAAK,SAAS,WAAW,YAAY;AAAA,IAChD;AAGA,UAAM,SAAS,QAAQ,YAAY;AACnC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,UAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,YAAY,MAAM,QAAQ;AACtE,eAAO,EAAE,KAAK,WAAW,MAAM;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "govee-smart",
|
|
4
|
-
"version": "2.12.
|
|
4
|
+
"version": "2.12.2",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.12.2": {
|
|
7
|
+
"en": "Verified against Node.js 24. Internal cleanup for stricter ioBroker repochecker compliance.",
|
|
8
|
+
"de": "Getestet mit Node.js 24. Interne Bereinigung für strengere ioBroker-Repochecker-Konformität.",
|
|
9
|
+
"ru": "Проверено с Node.js 24. Внутренняя оптимизация для строгого соответствия проверке ioBroker.",
|
|
10
|
+
"pt": "Verificado com Node.js 24. Limpeza interna para conformidade mais rigorosa com o repochecker do ioBroker.",
|
|
11
|
+
"nl": "Getest met Node.js 24. Interne opruiming voor striktere ioBroker-repochecker-naleving.",
|
|
12
|
+
"fr": "Vérifié avec Node.js 24. Nettoyage interne pour une conformité plus stricte avec le repochecker ioBroker.",
|
|
13
|
+
"it": "Verificato con Node.js 24. Pulizia interna per una conformità più rigorosa con il repochecker ioBroker.",
|
|
14
|
+
"es": "Verificado con Node.js 24. Limpieza interna para un cumplimiento más estricto del repochecker de ioBroker.",
|
|
15
|
+
"pl": "Zweryfikowano z Node.js 24. Wewnętrzne porządki dla ściślejszej zgodności z repocheckerem ioBroker.",
|
|
16
|
+
"uk": "Перевірено з Node.js 24. Внутрішнє прибирання для суворішої відповідності repochecker ioBroker.",
|
|
17
|
+
"zh-cn": "已通过 Node.js 24 验证。内部清理以更严格符合 ioBroker repochecker 要求。"
|
|
18
|
+
},
|
|
19
|
+
"2.12.1": {
|
|
20
|
+
"en": "Code quality enforced with standard formatting.",
|
|
21
|
+
"de": "Codequalität durch einheitliche Formatierung sichergestellt.",
|
|
22
|
+
"ru": "Качество кода обеспечено единым форматированием.",
|
|
23
|
+
"pt": "Qualidade do código garantida com formatação padronizada.",
|
|
24
|
+
"nl": "Codekwaliteit gewaarborgd met standaard opmaak.",
|
|
25
|
+
"fr": "Qualité du code assurée par un formatage standardisé.",
|
|
26
|
+
"it": "Qualità del codice assicurata con formattazione standard.",
|
|
27
|
+
"es": "Calidad del código asegurada con formato estándar.",
|
|
28
|
+
"pl": "Jakość kodu zapewniona przez standardowe formatowanie.",
|
|
29
|
+
"uk": "Якість коду забезпечено стандартним форматуванням.",
|
|
30
|
+
"zh-cn": "通过标准格式化确保代码质量。"
|
|
31
|
+
},
|
|
6
32
|
"2.12.0": {
|
|
7
33
|
"en": "Removed unused info.legacyMqttCleaned datapoint. All info datapoint names now show in 11 languages instead of English-only.",
|
|
8
34
|
"de": "Unbenutzter Datenpunkt info.legacyMqttCleaned entfernt. Alle Info-Datenpunktnamen werden jetzt in 11 Sprachen angezeigt statt nur Englisch.",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
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.",
|
|
68
94
|
"uk": "Знімки на Curtain Lights (H70B3), Christmas Strings (H70C5) і Outdoor Neon (H61A8) працюють; сцени матричних теж. Нова система квірків: правки через каталог, без релізу.",
|
|
69
95
|
"zh-cn": "Curtain Lights (H70B3)、Christmas Strings (H70C5) 和 Outdoor Neon (H61A8) 的快照可用;矩阵场景也是。新增 quirks 系统:设备修复通过目录,无需新版本。"
|
|
70
|
-
},
|
|
71
|
-
"2.9.1": {
|
|
72
|
-
"en": "Issue reports now include the device's stored scene, snapshot and effect data, so device-specific problems are analysable from the export alone. Groups now have their own diagnostics button.",
|
|
73
|
-
"de": "Issue-Berichte enthalten jetzt die gespeicherten Szenen-, Snapshot- und Effekt-Daten. Probleme aus dem Export analysierbar. Gruppen haben jetzt einen eigenen Diagnose-Button.",
|
|
74
|
-
"ru": "Отчёты об ошибках теперь содержат сохранённые данные сцен, снимков и эффектов устройства. Проблемы анализируются прямо из экспорта. У групп — своя кнопка диагностики.",
|
|
75
|
-
"pt": "Relatórios incluem agora os dados de cenas, snapshots e efeitos guardados no dispositivo. Problemas analisáveis a partir do próprio export. Grupos têm botão de diagnóstico próprio.",
|
|
76
|
-
"nl": "Issue-rapporten bevatten nu de opgeslagen scène-, snapshot- en effect-data. Problemen alleen al uit de export analyseerbaar. Groepen krijgen een eigen diagnose-knop.",
|
|
77
|
-
"fr": "Les rapports d'incident contiennent maintenant les données de scènes, snapshots et effets. Problèmes analysables depuis le seul export. Les groupes ont leur propre bouton diagnostic.",
|
|
78
|
-
"it": "I rapporti includono ora i dati di scene, snapshot ed effetti salvati nel dispositivo. Problemi analizzabili dal solo export. I gruppi hanno un proprio pulsante di diagnostica.",
|
|
79
|
-
"es": "Los informes incluyen ahora los datos de escenas, snapshots y efectos guardados. Los problemas se analizan desde la propia exportación. Los grupos tienen su propio botón de diagnóstico.",
|
|
80
|
-
"pl": "Raporty zawierają teraz zapisane dane scen, snapshotów i efektów urządzenia. Problemy analizowalne z samego eksportu. Grupy mają teraz własny przycisk diagnostyki.",
|
|
81
|
-
"uk": "Звіти про проблеми тепер містять збережені дані сцен, снапшотів і ефектів пристрою. Проблеми аналізуються з самого експорту. У груп — своя кнопка діагностики.",
|
|
82
|
-
"zh-cn": "故障报告现在包含设备保存的场景、快照与效果数据,设备问题可直接通过导出分析。分组也拥有独立的诊断按钮。"
|
|
83
|
-
},
|
|
84
|
-
"2.9.0": {
|
|
85
|
-
"en": "info.online for Lights now tracks real LAN reachability (90 s window). Cloud and MQTT push no longer write it — they produced false-positive true during real outages.",
|
|
86
|
-
"de": "info.online für Lights spiegelt jetzt die echte LAN-Erreichbarkeit wider (90-s-Fenster). Cloud und MQTT-Push schreiben den State nicht mehr — sie haben bei echten Ausfällen fälschlich true geliefert.",
|
|
87
|
-
"ru": "info.online для светильников теперь отражает реальную LAN-доступность (окно 90 с). Cloud и MQTT-push больше не записывают этот state — раньше они выдавали ложные true во время реальных отключений.",
|
|
88
|
-
"pt": "info.online para luzes agora reflete a acessibilidade LAN real (janela de 90 s). Cloud e MQTT push já não escrevem o estado — anteriormente geravam true falsos durante falhas reais.",
|
|
89
|
-
"nl": "info.online voor lampen volgt nu de echte LAN-bereikbaarheid (90 s venster). Cloud en MQTT-push schrijven het niet meer — ze leverden eerder onterechte true tijdens echte storingen.",
|
|
90
|
-
"fr": "info.online pour les lumières reflète maintenant la véritable joignabilité LAN (fenêtre de 90 s). Cloud et MQTT push ne l'écrivent plus — ils produisaient des true erronés lors de vraies pannes.",
|
|
91
|
-
"it": "info.online per le luci ora rispecchia la reale raggiungibilità LAN (finestra di 90 s). Cloud e MQTT push non lo scrivono più — in passato producevano true erronei durante interruzioni reali.",
|
|
92
|
-
"es": "info.online para luces ahora refleja la accesibilidad LAN real (ventana de 90 s). Cloud y MQTT push ya no lo escriben — antes producían true falsos durante apagones reales.",
|
|
93
|
-
"pl": "info.online dla lamp odzwierciedla teraz rzeczywistą dostępność LAN (okno 90 s). Chmura i MQTT-push już go nie zapisują — wcześniej dawały fałszywe true podczas prawdziwych awarii.",
|
|
94
|
-
"uk": "info.online для світильників тепер відображає реальну LAN-доступність (вікно 90 с). Cloud та MQTT-push більше не записують стан — раніше вони видавали хибні true під час реальних відключень.",
|
|
95
|
-
"zh-cn": "灯具的 info.online 现在反映真实的 LAN 可达性(90 秒窗口)。Cloud 与 MQTT 推送不再写入该状态——它们曾在真实掉线时产生错误的 true。"
|
|
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.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "krobi",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
"@types/iobroker": "npm:@iobroker/types@^7.1.2",
|
|
49
49
|
"@types/node": "^22.0.0",
|
|
50
50
|
"@types/node-forge": "^1.3.14",
|
|
51
|
-
"@
|
|
51
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
52
52
|
"rimraf": "^6.1.3",
|
|
53
53
|
"ts-node": "^10.9.2",
|
|
54
54
|
"typescript": "~6.0.3",
|
|
55
|
-
"vitest": "^4.1.5"
|
|
56
|
-
"@vitest/coverage-v8": "^4.1.5"
|
|
55
|
+
"vitest": "^4.1.5"
|
|
57
56
|
},
|
|
58
57
|
"main": "build/main.js",
|
|
59
58
|
"files": [
|