iobroker.govee-smart 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,15 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
124
  ---
125
125
 
126
126
  ## Changelog
127
+ ### 2.5.2 (2026-05-04)
128
+
129
+ - WARN-Spam alle 2 Min behoben: `groups.*.info.membersUnreachable` bleibt bei vollständig erreichbaren Gruppen mit leerem Wert vorhanden statt gelöscht zu werden.
130
+ - Neu verifiziert: H61A8 Outdoor Neon LED Strip 10m (gemeldet von tukey42 in Issue #11).
131
+
132
+ ### 2.5.1 (2026-05-04)
133
+
134
+ - Cloud-Rate-Limit-Hinweis zeigt bei 429 jetzt „rate-limited by Govee" statt der generischen Cloud-Fehlermeldung. Plus 33 Mock-Tests für Cloud + MQTT-Login.
135
+
127
136
  ### 2.5.0 (2026-05-04)
128
137
 
129
138
  - F4 final: `onMessage`-Handler (sendTo aus dem Admin-UI) ist jetzt eine eigene Klasse mit Host-Interface. main.ts deutlich kleiner, Login-Test/2FA-Code-Anforderung isoliert testbar. Verhalten identisch.
@@ -136,15 +145,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
136
145
 
137
146
  - Lokaler Snapshot-Manager (Save/Restore/Delete) ist jetzt eine eigene Klasse mit Host-Interface — `main.ts` ist kleiner und der Snapshot-Pfad ist isoliert testbar. Verhalten identisch.
138
147
 
139
- ### 2.3.1 (2026-05-04)
140
-
141
- - Smoke-Tests für GoveeCloudClient + GoveeMqttClient — Initial-State-Checks für getFailureReason, token, connected, plus Setter-Smoke-Tests (637 → 637+9 Tests). Volle Pfade über https/mqtt-Mocks kommen separat.
142
-
143
- ### 2.3.0 (2026-05-04)
144
-
145
- - App-Version-Drift-Monitor: täglicher iTunes-Lookup vergleicht die im Adapter hinterlegte Govee-App-Version mit der aktuellen iOS-Version. Bei Drift > 2 Minor wird gewarnt — Govees undokumentierte Endpoints rejecten gelegentlich zu alte Clients.
146
- - Code-Hygiene: `onStateChange`-Handler in eigene Methoden für Diagnostics-Export und Generic-Capability-Routing aufgeteilt. Magic-Numbers durch `timing-constants.ts` ersetzt. Lifecycle-Flags besser dokumentiert.
147
-
148
148
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
149
149
 
150
150
  ## Support
@@ -27,6 +27,7 @@ const BASE_URL = "https://openapi.api.govee.com";
27
27
  class GoveeCloudClient {
28
28
  apiKey;
29
29
  log;
30
+ httpsRequestImpl;
30
31
  /**
31
32
  * Diagnostics hook — receives (deviceId, endpoint, body) for each
32
33
  * response. Optional; the adapter wires it to a DiagnosticsCollector
@@ -41,10 +42,12 @@ class GoveeCloudClient {
41
42
  /**
42
43
  * @param apiKey Govee API key
43
44
  * @param log ioBroker logger
45
+ * @param httpsRequestImpl optional DI für Tests — Default ist die echte httpsRequest
44
46
  */
45
- constructor(apiKey, log) {
47
+ constructor(apiKey, log, httpsRequestImpl = import_http_client.httpsRequest) {
46
48
  this.apiKey = apiKey;
47
49
  this.log = log;
50
+ this.httpsRequestImpl = httpsRequestImpl;
48
51
  }
49
52
  /**
50
53
  * Short user-facing reason for "Cloud not connected", or null wenn der
@@ -205,7 +208,7 @@ class GoveeCloudClient {
205
208
  var _a;
206
209
  this.log.debug(`Cloud API: ${method} ${path}`);
207
210
  try {
208
- const result = await (0, import_http_client.httpsRequest)({
211
+ const result = await this.httpsRequestImpl({
209
212
  method,
210
213
  url: new URL(path, BASE_URL).toString(),
211
214
  headers: { "Govee-API-Key": this.apiKey },
@@ -214,11 +217,12 @@ class GoveeCloudClient {
214
217
  this.lastErrorCategory = null;
215
218
  return result;
216
219
  } catch (err) {
217
- this.lastErrorCategory = (0, import_types.classifyError)(err);
218
220
  if (err instanceof import_http_client.HttpError && err.statusCode === 429) {
221
+ this.lastErrorCategory = "RATE_LIMIT";
219
222
  const retryAfter = String((_a = err.headers["retry-after"]) != null ? _a : "unknown");
220
223
  throw new import_http_client.HttpError(`Rate limited \u2014 retry after ${retryAfter}s`, 429, err.headers);
221
224
  }
225
+ this.lastErrorCategory = (0, import_types.classifyError)(err);
222
226
  throw err;
223
227
  }
224
228
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/govee-cloud-client.ts"],
4
- "sourcesContent": ["import { httpsRequest, HttpError } from \"./http-client\";\nimport {\n classifyError,\n type CloudDevice,\n type CloudDeviceListResponse,\n type CloudDeviceStateResponse,\n type CloudScene,\n type CloudScenesResponse,\n type CloudStateCapability,\n type ErrorCategory,\n} from \"./types\";\n\nconst BASE_URL = \"https://openapi.api.govee.com\";\n\n/**\n * Govee Cloud API v2 client.\n * Used for device list, capabilities, scenes, segments, and as control fallback.\n */\nexport class GoveeCloudClient {\n private readonly apiKey: string;\n private readonly log: ioBroker.Logger;\n /**\n * Diagnostics hook \u2014 receives (deviceId, endpoint, body) for each\n * response. Optional; the adapter wires it to a DiagnosticsCollector\n * for `diag.export`.\n */\n private onResponse: ((deviceId: string, endpoint: string, body: unknown) => void) | null = null;\n\n /**\n * Letzte Fehler-Kategorie f\u00FCr getFailureReason() \u2014 gesetzt bei jedem\n * HTTP-Fehler im request-Pfad.\n */\n private lastErrorCategory: ErrorCategory | null = null;\n\n /**\n * @param apiKey Govee API key\n * @param log ioBroker logger\n */\n constructor(apiKey: string, log: ioBroker.Logger) {\n this.apiKey = apiKey;\n this.log = log;\n }\n\n /**\n * Short user-facing reason for \"Cloud not connected\", or null wenn der\n * Client noch keinen Fehler gesehen hat. Analog zu mqtt-client \u2014\n * `logDeviceSummary` nutzt das damit der Adapter klare Diagnose-Texte\n * statt \u201Esee earlier errors\" loggen kann.\n */\n getFailureReason(): string | null {\n switch (this.lastErrorCategory) {\n case \"AUTH\":\n return \"API key rejected \u2014 check Govee API key\";\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 \"Cloud request timeout\";\n case \"UNKNOWN\":\n return \"Cloud request failed \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /**\n * Register a hook called after every successful Cloud API response.\n * Used to populate the DiagnosticsCollector ring buffer.\n *\n * @param cb Callback receiving (deviceId, endpoint, body)\n */\n setResponseHook(cb: ((deviceId: string, endpoint: string, body: unknown) => void) | null): void {\n this.onResponse = cb;\n }\n\n /** Fetch all devices with their capabilities */\n async getDevices(): Promise<CloudDevice[]> {\n const resp = await this.request<CloudDeviceListResponse>(\"GET\", \"/router/api/v1/user/devices\");\n // Defensive \u2014 API can drift. Guard for non-array to protect downstream iteration.\n return Array.isArray(resp?.data) ? resp.data : [];\n }\n\n /**\n * Fetch current state of a device\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getDeviceState(sku: string, device: string): Promise<CloudStateCapability[]> {\n const resp = await this.request<CloudDeviceStateResponse>(\"POST\", \"/router/api/v1/device/state\", {\n requestId: `state_${Date.now()}`,\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/state\", resp);\n const caps = resp?.data?.capabilities;\n return Array.isArray(caps) ? caps : [];\n }\n\n /**\n * Send a control command to a device\n *\n * @param sku Product model\n * @param device Device ID\n * @param capabilityType Full capability type string\n * @param instance Capability instance name\n * @param value Value to set\n */\n async controlDevice(\n sku: string,\n device: string,\n capabilityType: string,\n instance: string,\n value: unknown,\n ): Promise<void> {\n const reqBody = {\n requestId: `ctrl_${Date.now()}`,\n payload: {\n sku,\n device,\n capability: {\n type: capabilityType,\n instance,\n value,\n },\n },\n };\n const resp = await this.request(\"POST\", \"/router/api/v1/device/control\", reqBody);\n this.onResponse?.(device, \"/router/api/v1/device/control\", { request: reqBody.payload.capability, response: resp });\n }\n\n /**\n * Fetch dynamic scenes and snapshots for a device.\n * The scenes endpoint returns capabilities with options.\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getScenes(\n sku: string,\n device: string,\n ): Promise<{\n lightScenes: CloudScene[];\n diyScenes: CloudScene[];\n snapshots: CloudScene[];\n }> {\n const resp = await this.request<CloudScenesResponse>(\"POST\", \"/router/api/v1/device/scenes\", {\n requestId: \"scenes\",\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/scenes\", resp);\n\n const lightScenes: CloudScene[] = [];\n const diyScenes: CloudScene[] = [];\n const snapshots: CloudScene[] = [];\n\n const caps = Array.isArray(resp?.payload?.capabilities) ? resp.payload.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.instance !== \"string\") {\n continue;\n }\n const opts = Array.isArray(cap.parameters?.options) ? cap.parameters.options : [];\n this.log.debug(`Scenes endpoint: instance=${cap.instance}, options=${opts.length}`);\n const mapped: CloudScene[] = opts\n .filter(\n (o): o is { name: string; value: Record<string, unknown> } =>\n !!o && typeof o.name === \"string\" && typeof o.value === \"object\",\n )\n .map(o => ({\n name: o.name,\n value: o.value,\n }));\n\n if (cap.instance === \"lightScene\") {\n lightScenes.push(...mapped);\n } else if (cap.instance === \"diyScene\") {\n diyScenes.push(...mapped);\n } else if (cap.instance === \"snapshot\") {\n snapshots.push(...mapped);\n }\n }\n\n return { lightScenes, diyScenes, snapshots };\n }\n\n /**\n * Fetch DIY scenes for a device from the dedicated diy-scenes endpoint.\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getDiyScenes(sku: string, device: string): Promise<CloudScene[]> {\n const resp = await this.request<CloudScenesResponse>(\"POST\", \"/router/api/v1/device/diy-scenes\", {\n requestId: \"diy-scenes\",\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/diy-scenes\", resp);\n\n const scenes: CloudScene[] = [];\n const caps = Array.isArray(resp?.payload?.capabilities) ? resp.payload.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.instance !== \"string\") {\n continue;\n }\n const opts = Array.isArray(cap.parameters?.options) ? cap.parameters.options : [];\n this.log.debug(`DIY-Scenes endpoint: instance=${cap.instance}, options=${opts.length}`);\n scenes.push(\n ...opts\n .filter(\n (o): o is { name: string; value: Record<string, unknown> } =>\n !!o && typeof o.name === \"string\" && typeof o.value === \"object\",\n )\n .map(o => ({ name: o.name, value: o.value })),\n );\n }\n\n return scenes;\n }\n\n /**\n * Make an HTTPS request to the Govee Cloud API\n *\n * @param method HTTP method (GET, POST)\n * @param path API endpoint path\n * @param body Optional request body\n */\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n this.log.debug(`Cloud API: ${method} ${path}`);\n try {\n const result = await httpsRequest<T>({\n method: method as \"GET\" | \"POST\",\n url: new URL(path, BASE_URL).toString(),\n headers: { \"Govee-API-Key\": this.apiKey },\n body,\n });\n // Reset Failure-Kategorie bei Erfolg \u2014 getFailureReason() returnt\n // dann null bis zum n\u00E4chsten Fehler.\n this.lastErrorCategory = null;\n return result;\n } catch (err) {\n this.lastErrorCategory = classifyError(err);\n // Enhance 429 errors with retry-after info\n if (err instanceof HttpError && err.statusCode === 429) {\n const retryAfter = String(err.headers[\"retry-after\"] ?? \"unknown\");\n throw new HttpError(`Rate limited \u2014 retry after ${retryAfter}s`, 429, err.headers);\n }\n throw err;\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { httpsRequest, HttpError, type HttpsRequestFn } from \"./http-client\";\nimport {\n classifyError,\n type CloudDevice,\n type CloudDeviceListResponse,\n type CloudDeviceStateResponse,\n type CloudScene,\n type CloudScenesResponse,\n type CloudStateCapability,\n type ErrorCategory,\n} from \"./types\";\n\nconst BASE_URL = \"https://openapi.api.govee.com\";\n\n/**\n * Govee Cloud API v2 client.\n * Used for device list, capabilities, scenes, segments, and as control fallback.\n */\nexport class GoveeCloudClient {\n private readonly apiKey: string;\n private readonly log: ioBroker.Logger;\n private readonly httpsRequestImpl: HttpsRequestFn;\n /**\n * Diagnostics hook \u2014 receives (deviceId, endpoint, body) for each\n * response. Optional; the adapter wires it to a DiagnosticsCollector\n * for `diag.export`.\n */\n private onResponse: ((deviceId: string, endpoint: string, body: unknown) => void) | null = null;\n\n /**\n * Letzte Fehler-Kategorie f\u00FCr getFailureReason() \u2014 gesetzt bei jedem\n * HTTP-Fehler im request-Pfad.\n */\n private lastErrorCategory: ErrorCategory | null = null;\n\n /**\n * @param apiKey Govee API key\n * @param log ioBroker logger\n * @param httpsRequestImpl optional DI f\u00FCr Tests \u2014 Default ist die echte httpsRequest\n */\n constructor(apiKey: string, log: ioBroker.Logger, httpsRequestImpl: HttpsRequestFn = httpsRequest) {\n this.apiKey = apiKey;\n this.log = log;\n this.httpsRequestImpl = httpsRequestImpl;\n }\n\n /**\n * Short user-facing reason for \"Cloud not connected\", or null wenn der\n * Client noch keinen Fehler gesehen hat. Analog zu mqtt-client \u2014\n * `logDeviceSummary` nutzt das damit der Adapter klare Diagnose-Texte\n * statt \u201Esee earlier errors\" loggen kann.\n */\n getFailureReason(): string | null {\n switch (this.lastErrorCategory) {\n case \"AUTH\":\n return \"API key rejected \u2014 check Govee API key\";\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 \"Cloud request timeout\";\n case \"UNKNOWN\":\n return \"Cloud request failed \u2014 see earlier log\";\n case null:\n default:\n return null;\n }\n }\n\n /**\n * Register a hook called after every successful Cloud API response.\n * Used to populate the DiagnosticsCollector ring buffer.\n *\n * @param cb Callback receiving (deviceId, endpoint, body)\n */\n setResponseHook(cb: ((deviceId: string, endpoint: string, body: unknown) => void) | null): void {\n this.onResponse = cb;\n }\n\n /** Fetch all devices with their capabilities */\n async getDevices(): Promise<CloudDevice[]> {\n const resp = await this.request<CloudDeviceListResponse>(\"GET\", \"/router/api/v1/user/devices\");\n // Defensive \u2014 API can drift. Guard for non-array to protect downstream iteration.\n return Array.isArray(resp?.data) ? resp.data : [];\n }\n\n /**\n * Fetch current state of a device\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getDeviceState(sku: string, device: string): Promise<CloudStateCapability[]> {\n const resp = await this.request<CloudDeviceStateResponse>(\"POST\", \"/router/api/v1/device/state\", {\n requestId: `state_${Date.now()}`,\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/state\", resp);\n const caps = resp?.data?.capabilities;\n return Array.isArray(caps) ? caps : [];\n }\n\n /**\n * Send a control command to a device\n *\n * @param sku Product model\n * @param device Device ID\n * @param capabilityType Full capability type string\n * @param instance Capability instance name\n * @param value Value to set\n */\n async controlDevice(\n sku: string,\n device: string,\n capabilityType: string,\n instance: string,\n value: unknown,\n ): Promise<void> {\n const reqBody = {\n requestId: `ctrl_${Date.now()}`,\n payload: {\n sku,\n device,\n capability: {\n type: capabilityType,\n instance,\n value,\n },\n },\n };\n const resp = await this.request(\"POST\", \"/router/api/v1/device/control\", reqBody);\n this.onResponse?.(device, \"/router/api/v1/device/control\", { request: reqBody.payload.capability, response: resp });\n }\n\n /**\n * Fetch dynamic scenes and snapshots for a device.\n * The scenes endpoint returns capabilities with options.\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getScenes(\n sku: string,\n device: string,\n ): Promise<{\n lightScenes: CloudScene[];\n diyScenes: CloudScene[];\n snapshots: CloudScene[];\n }> {\n const resp = await this.request<CloudScenesResponse>(\"POST\", \"/router/api/v1/device/scenes\", {\n requestId: \"scenes\",\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/scenes\", resp);\n\n const lightScenes: CloudScene[] = [];\n const diyScenes: CloudScene[] = [];\n const snapshots: CloudScene[] = [];\n\n const caps = Array.isArray(resp?.payload?.capabilities) ? resp.payload.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.instance !== \"string\") {\n continue;\n }\n const opts = Array.isArray(cap.parameters?.options) ? cap.parameters.options : [];\n this.log.debug(`Scenes endpoint: instance=${cap.instance}, options=${opts.length}`);\n const mapped: CloudScene[] = opts\n .filter(\n (o): o is { name: string; value: Record<string, unknown> } =>\n !!o && typeof o.name === \"string\" && typeof o.value === \"object\",\n )\n .map(o => ({\n name: o.name,\n value: o.value,\n }));\n\n if (cap.instance === \"lightScene\") {\n lightScenes.push(...mapped);\n } else if (cap.instance === \"diyScene\") {\n diyScenes.push(...mapped);\n } else if (cap.instance === \"snapshot\") {\n snapshots.push(...mapped);\n }\n }\n\n return { lightScenes, diyScenes, snapshots };\n }\n\n /**\n * Fetch DIY scenes for a device from the dedicated diy-scenes endpoint.\n *\n * @param sku Product model\n * @param device Device identifier\n */\n async getDiyScenes(sku: string, device: string): Promise<CloudScene[]> {\n const resp = await this.request<CloudScenesResponse>(\"POST\", \"/router/api/v1/device/diy-scenes\", {\n requestId: \"diy-scenes\",\n payload: { sku, device },\n });\n this.onResponse?.(device, \"/router/api/v1/device/diy-scenes\", resp);\n\n const scenes: CloudScene[] = [];\n const caps = Array.isArray(resp?.payload?.capabilities) ? resp.payload.capabilities : [];\n for (const cap of caps) {\n if (!cap || typeof cap.instance !== \"string\") {\n continue;\n }\n const opts = Array.isArray(cap.parameters?.options) ? cap.parameters.options : [];\n this.log.debug(`DIY-Scenes endpoint: instance=${cap.instance}, options=${opts.length}`);\n scenes.push(\n ...opts\n .filter(\n (o): o is { name: string; value: Record<string, unknown> } =>\n !!o && typeof o.name === \"string\" && typeof o.value === \"object\",\n )\n .map(o => ({ name: o.name, value: o.value })),\n );\n }\n\n return scenes;\n }\n\n /**\n * Make an HTTPS request to the Govee Cloud API\n *\n * @param method HTTP method (GET, POST)\n * @param path API endpoint path\n * @param body Optional request body\n */\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n this.log.debug(`Cloud API: ${method} ${path}`);\n try {\n const result = await this.httpsRequestImpl<T>({\n method: method as \"GET\" | \"POST\",\n url: new URL(path, BASE_URL).toString(),\n headers: { \"Govee-API-Key\": this.apiKey },\n body,\n });\n // Reset Failure-Kategorie bei Erfolg \u2014 getFailureReason() returnt\n // dann null bis zum n\u00E4chsten Fehler.\n this.lastErrorCategory = null;\n return result;\n } catch (err) {\n // 429 explizit per statusCode klassifizieren \u2014 classifyError schaut nur in\n // err.message, und HttpError(\"Too many requests\", 429, \u2026) hat keinen \"429\"-\n // oder \"Rate limit\"-Marker im Text. Ohne diesen Branch landet 429 als\n // UNKNOWN, getFailureReason() liefert dann den falschen Ready-Hint.\n if (err instanceof HttpError && err.statusCode === 429) {\n this.lastErrorCategory = \"RATE_LIMIT\";\n const retryAfter = String(err.headers[\"retry-after\"] ?? \"unknown\");\n throw new HttpError(`Rate limited \u2014 retry after ${retryAfter}s`, 429, err.headers);\n }\n this.lastErrorCategory = classifyError(err);\n throw err;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -65,6 +65,7 @@ class GoveeMqttClient {
65
65
  password;
66
66
  log;
67
67
  timers;
68
+ httpsRequestImpl;
68
69
  client = null;
69
70
  accountTopic = "";
70
71
  _bearerToken = "";
@@ -107,12 +108,14 @@ class GoveeMqttClient {
107
108
  * @param password Govee account password
108
109
  * @param log ioBroker logger
109
110
  * @param timers Timer adapter
111
+ * @param httpsRequestImpl optional DI für Tests — Default ist die echte httpsRequest
110
112
  */
111
- constructor(email, password, log, timers) {
113
+ constructor(email, password, log, timers, httpsRequestImpl = import_http_client.httpsRequest) {
112
114
  this.email = email;
113
115
  this.password = password;
114
116
  this.log = log;
115
117
  this.timers = timers;
118
+ this.httpsRequestImpl = httpsRequestImpl;
116
119
  this.clientId = (0, import_govee_constants.deriveGoveeClientId)(email);
117
120
  }
118
121
  /**
@@ -613,7 +616,7 @@ class GoveeMqttClient {
613
616
  if (code) {
614
617
  body.code = code;
615
618
  }
616
- return (0, import_http_client.httpsRequest)({
619
+ return this.httpsRequestImpl({
617
620
  method: "POST",
618
621
  url: LOGIN_URL,
619
622
  headers: {
@@ -639,7 +642,7 @@ class GoveeMqttClient {
639
642
  */
640
643
  async requestVerificationCode() {
641
644
  const url = "https://app2.govee.com/account/rest/account/v1/verification";
642
- await (0, import_http_client.httpsRequest)({
645
+ await this.httpsRequestImpl({
643
646
  method: "POST",
644
647
  url,
645
648
  headers: {
@@ -658,7 +661,7 @@ class GoveeMqttClient {
658
661
  }
659
662
  /** Get IoT key (P12 certificate) */
660
663
  getIotKey() {
661
- return (0, import_http_client.httpsRequest)({
664
+ return this.httpsRequestImpl({
662
665
  method: "GET",
663
666
  url: IOT_KEY_URL,
664
667
  headers: {
@@ -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 } from \"./http-client\";\nimport { GOVEE_APP_VERSION, GOVEE_CLIENT_TYPE, GOVEE_USER_AGENT, deriveGoveeClientId } from \"./govee-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\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\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/** 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 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 any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = 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 */\n constructor(email: string, password: string, log: ioBroker.Logger, timers: TimerAdapter) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\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 >= 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 // 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 clear it\n if (codeWasSent) {\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 = mqtt.connect(`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 >= 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 try {\n const raw = JSON.parse(payload.toString()) 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 && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().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 into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= 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.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 = mqtt.connect(`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 this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\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 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 return httpsRequest<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 }\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 * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\n const url = \"https://app2.govee.com/account/rest/account/v1/verification\";\n await httpsRequest<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 getIotKey(): Promise<GoveeIotKeyResponse> {\n return httpsRequest<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 }\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 {\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\n/** Max consecutive auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\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/** 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 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 any op.command hex strings. The hook is responsible\n * for forwarding to a DiagnosticsCollector if one is set up.\n */\n private onPacket: ((deviceId: string, topic: string, hex: string) => void) | null = 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 */\n constructor(\n email: string,\n password: string,\n log: ioBroker.Logger,\n timers: TimerAdapter,\n httpsRequestImpl: HttpsRequestFn = httpsRequest,\n ) {\n this.email = email;\n this.password = password;\n this.log = log;\n this.timers = timers;\n this.httpsRequestImpl = httpsRequestImpl;\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 >= 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 // 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 clear it\n if (codeWasSent) {\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 = mqtt.connect(`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 >= 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 try {\n const raw = JSON.parse(payload.toString()) 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 && Array.isArray(op?.command)) {\n for (const cmd of op.command) {\n if (typeof cmd === \"string\" && cmd) {\n this.onPacket(device, topic, cmd);\n }\n }\n }\n }\n } catch {\n this.log.debug(`MQTT: Failed to parse message: ${payload.toString().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 into the DiagnosticsCollector\n * for `diag.export`.\n *\n * @param cb Callback receiving (deviceId, topic, hex)\n */\n setPacketHook(cb: ((deviceId: string, topic: string, hex: string) => void) | null): void {\n this.onPacket = cb;\n }\n\n /** Schedule reconnect with exponential backoff */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return;\n }\n if (this.authFailCount >= 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.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 = mqtt.connect(`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 this.persistedAttemptInFlight = false;\n this.reconnectAttempts = 0;\n this.authFailCount = 0;\n if (this.lastErrorCategory) {\n this.log.info(\"MQTT connection restored\");\n this.lastErrorCategory = null;\n } else {\n this.log.info(\"MQTT connected\");\n }\n this.client?.subscribe(this.accountTopic, { qos: 0 }, err => {\n if (err) {\n this.log.warn(`MQTT subscribe failed: ${err.message}`);\n } else {\n this.log.debug(\"MQTT subscribed to account topic\");\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 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 return 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 }\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 * Throws on non-200 or network failure so the caller (onMessage handler)\n * can surface the error to the admin UI.\n */\n async requestVerificationCode(): Promise<void> {\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 getIotKey(): Promise<GoveeIotKeyResponse> {\n return 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 }\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"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/http-client.ts"],
4
- "sourcesContent": ["import * as https from \"node:https\";\n\n/**\n * Module-level keep-alive Agent \u2014 vermeidet TLS-Handshake (~200ms) pro\n * Request. maxSockets begrenzt parallele Verbindungen pro Host damit wir\n * nicht aus Versehen Govee mit 100 gleichzeitigen Calls treffen.\n */\nconst keepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 4 });\n\n/** Options for an HTTPS request */\nexport interface HttpRequestOptions {\n /** HTTP method */\n method: \"GET\" | \"POST\";\n /** Full URL */\n url: string;\n /** HTTP headers */\n headers: Record<string, string>;\n /** Request body (POST only, will be JSON-serialized) */\n body?: unknown;\n /** Timeout in milliseconds (default 15000) */\n timeout?: number;\n /** Optional AbortSignal \u2014 wird der Request abgebrochen sobald abort() */\n signal?: AbortSignal;\n}\n\n/**\n * Perform an HTTPS request and parse the JSON response.\n *\n * @param options Request options\n */\nexport function httpsRequest<T>(options: HttpRequestOptions): Promise<T> {\n return new Promise((resolve, reject) => {\n const u = new URL(options.url);\n const postData = options.body ? JSON.stringify(options.body) : undefined;\n\n const reqOptions: https.RequestOptions = {\n method: options.method,\n hostname: u.hostname,\n path: u.pathname + u.search,\n headers: {\n ...options.headers,\n ...(postData\n ? {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(postData),\n }\n : {}),\n },\n timeout: options.timeout ?? 15_000,\n agent: keepAliveAgent,\n };\n\n const req = https.request(reqOptions, res => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const raw = Buffer.concat(chunks).toString();\n const statusCode = res.statusCode ?? 0;\n\n if (statusCode < 200 || statusCode >= 400) {\n // M4 \u2014 Body-Snippet aus Error-Message rausnehmen damit\n // Tokens/API-Keys nicht im warn-Log auftauchen wenn der\n // Server sie reflektiert. responseBody bleibt f\u00FCr debug\n // separat verf\u00FCgbar.\n reject(new HttpError(`HTTP ${statusCode}`, statusCode, res.headers, raw));\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`Invalid JSON in HTTP ${statusCode} response`));\n }\n });\n });\n\n req.on(\"error\", reject);\n req.on(\"timeout\", () => req.destroy(new Error(\"Timeout\")));\n\n // M3 \u2014 AbortSignal-Support. Wer den Request macht kann ihn abbrechen\n // (z.B. Adapter-onUnload via AbortController) damit der Stop nicht\n // 15s auf das Timeout warten muss.\n if (options.signal) {\n if (options.signal.aborted) {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n return;\n }\n const onAbort = (): void => {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n };\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n if (postData) {\n req.write(postData);\n }\n req.end();\n });\n}\n\n/** HTTP error with status code, response headers, and response body (debug-only) */\nexport class HttpError extends Error {\n /** HTTP status code */\n readonly statusCode: number;\n /** Response headers */\n readonly headers: Record<string, string | string[] | undefined>;\n /**\n * Raw response body \u2014 NICHT in `message` damit Tokens/API-Keys nicht\n * via warn-Log geleakt werden. Nur f\u00FCr gezieltes debug-Logging beim\n * Caller verf\u00FCgbar.\n */\n readonly responseBody: string;\n\n /**\n * @param message Error message (Body-frei)\n * @param statusCode HTTP status code\n * @param headers Response headers\n * @param responseBody Raw response body (kann sensitive Echo-Daten enthalten)\n */\n constructor(\n message: string,\n statusCode: number,\n headers: Record<string, string | string[] | undefined> = {},\n responseBody: string = \"\",\n ) {\n super(message);\n this.name = \"HttpError\";\n this.statusCode = statusCode;\n this.headers = headers;\n this.responseBody = responseBody;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AAOvB,MAAM,iBAAiB,IAAI,MAAM,MAAM,EAAE,WAAW,MAAM,YAAY,EAAE,CAAC;AAuBlE,SAAS,aAAgB,SAAyC;AACvE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AA/B1C;AAgCI,UAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AAC7B,UAAM,WAAW,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;AAE/D,UAAM,aAAmC;AAAA,MACvC,QAAQ,QAAQ;AAAA,MAChB,UAAU,EAAE;AAAA,MACZ,MAAM,EAAE,WAAW,EAAE;AAAA,MACrB,SAAS;AAAA,QACP,GAAG,QAAQ;AAAA,QACX,GAAI,WACA;AAAA,UACE,gBAAgB;AAAA,UAChB,kBAAkB,OAAO,WAAW,QAAQ;AAAA,QAC9C,IACA,CAAC;AAAA,MACP;AAAA,MACA,UAAS,aAAQ,YAAR,YAAmB;AAAA,MAC5B,OAAO;AAAA,IACT;AAEA,UAAM,MAAM,MAAM,QAAQ,YAAY,SAAO;AAC3C,YAAM,SAAmB,CAAC;AAC1B,UAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,UAAI,GAAG,OAAO,MAAM;AAvD1B,YAAAA;AAwDQ,cAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS;AAC3C,cAAM,cAAaA,MAAA,IAAI,eAAJ,OAAAA,MAAkB;AAErC,YAAI,aAAa,OAAO,cAAc,KAAK;AAKzC,iBAAO,IAAI,UAAU,QAAQ,UAAU,IAAI,YAAY,IAAI,SAAS,GAAG,CAAC;AACxE;AAAA,QACF;AAEA,YAAI;AACF,kBAAQ,KAAK,MAAM,GAAG,CAAM;AAAA,QAC9B,QAAQ;AACN,iBAAO,IAAI,MAAM,wBAAwB,UAAU,WAAW,CAAC;AAAA,QACjE;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,QAAI,GAAG,SAAS,MAAM;AACtB,QAAI,GAAG,WAAW,MAAM,IAAI,QAAQ,IAAI,MAAM,SAAS,CAAC,CAAC;AAKzD,QAAI,QAAQ,QAAQ;AAClB,UAAI,QAAQ,OAAO,SAAS;AAC1B,YAAI,QAAQ,IAAI,MAAM,SAAS,CAAC;AAChC,eAAO,IAAI,MAAM,SAAS,CAAC;AAC3B;AAAA,MACF;AACA,YAAM,UAAU,MAAY;AAC1B,YAAI,QAAQ,IAAI,MAAM,SAAS,CAAC;AAChC,eAAO,IAAI,MAAM,SAAS,CAAC;AAAA,MAC7B;AACA,cAAQ,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,IAClE;AAEA,QAAI,UAAU;AACZ,UAAI,MAAM,QAAQ;AAAA,IACpB;AACA,QAAI,IAAI;AAAA,EACV,CAAC;AACH;AAGO,MAAM,kBAAkB,MAAM;AAAA;AAAA,EAE1B;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQT,YACE,SACA,YACA,UAAyD,CAAC,GAC1D,eAAuB,IACvB;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,eAAe;AAAA,EACtB;AACF;",
4
+ "sourcesContent": ["import * as https from \"node:https\";\n\n/**\n * Module-level keep-alive Agent \u2014 vermeidet TLS-Handshake (~200ms) pro\n * Request. maxSockets begrenzt parallele Verbindungen pro Host damit wir\n * nicht aus Versehen Govee mit 100 gleichzeitigen Calls treffen.\n */\nconst keepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 4 });\n\n/** Options for an HTTPS request */\nexport interface HttpRequestOptions {\n /** HTTP method */\n method: \"GET\" | \"POST\";\n /** Full URL */\n url: string;\n /** HTTP headers */\n headers: Record<string, string>;\n /** Request body (POST only, will be JSON-serialized) */\n body?: unknown;\n /** Timeout in milliseconds (default 15000) */\n timeout?: number;\n /** Optional AbortSignal \u2014 wird der Request abgebrochen sobald abort() */\n signal?: AbortSignal;\n}\n\n/**\n * Signature der httpsRequest-Funktion. Cloud/Mqtt-Clients nehmen das als\n * optionalen DI-Parameter \u2014 Default ist die echte httpsRequest, Tests k\u00F6nnen\n * einen Mock injizieren ohne Module-Replacement.\n */\nexport type HttpsRequestFn = <T>(options: HttpRequestOptions) => Promise<T>;\n\n/**\n * Perform an HTTPS request and parse the JSON response.\n *\n * @param options Request options\n */\nexport function httpsRequest<T>(options: HttpRequestOptions): Promise<T> {\n return new Promise((resolve, reject) => {\n const u = new URL(options.url);\n const postData = options.body ? JSON.stringify(options.body) : undefined;\n\n const reqOptions: https.RequestOptions = {\n method: options.method,\n hostname: u.hostname,\n path: u.pathname + u.search,\n headers: {\n ...options.headers,\n ...(postData\n ? {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(postData),\n }\n : {}),\n },\n timeout: options.timeout ?? 15_000,\n agent: keepAliveAgent,\n };\n\n const req = https.request(reqOptions, res => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const raw = Buffer.concat(chunks).toString();\n const statusCode = res.statusCode ?? 0;\n\n if (statusCode < 200 || statusCode >= 400) {\n // M4 \u2014 Body-Snippet aus Error-Message rausnehmen damit\n // Tokens/API-Keys nicht im warn-Log auftauchen wenn der\n // Server sie reflektiert. responseBody bleibt f\u00FCr debug\n // separat verf\u00FCgbar.\n reject(new HttpError(`HTTP ${statusCode}`, statusCode, res.headers, raw));\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`Invalid JSON in HTTP ${statusCode} response`));\n }\n });\n });\n\n req.on(\"error\", reject);\n req.on(\"timeout\", () => req.destroy(new Error(\"Timeout\")));\n\n // M3 \u2014 AbortSignal-Support. Wer den Request macht kann ihn abbrechen\n // (z.B. Adapter-onUnload via AbortController) damit der Stop nicht\n // 15s auf das Timeout warten muss.\n if (options.signal) {\n if (options.signal.aborted) {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n return;\n }\n const onAbort = (): void => {\n req.destroy(new Error(\"Aborted\"));\n reject(new Error(\"Aborted\"));\n };\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n if (postData) {\n req.write(postData);\n }\n req.end();\n });\n}\n\n/** HTTP error with status code, response headers, and response body (debug-only) */\nexport class HttpError extends Error {\n /** HTTP status code */\n readonly statusCode: number;\n /** Response headers */\n readonly headers: Record<string, string | string[] | undefined>;\n /**\n * Raw response body \u2014 NICHT in `message` damit Tokens/API-Keys nicht\n * via warn-Log geleakt werden. Nur f\u00FCr gezieltes debug-Logging beim\n * Caller verf\u00FCgbar.\n */\n readonly responseBody: string;\n\n /**\n * @param message Error message (Body-frei)\n * @param statusCode HTTP status code\n * @param headers Response headers\n * @param responseBody Raw response body (kann sensitive Echo-Daten enthalten)\n */\n constructor(\n message: string,\n statusCode: number,\n headers: Record<string, string | string[] | undefined> = {},\n responseBody: string = \"\",\n ) {\n super(message);\n this.name = \"HttpError\";\n this.statusCode = statusCode;\n this.headers = headers;\n this.responseBody = responseBody;\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AAOvB,MAAM,iBAAiB,IAAI,MAAM,MAAM,EAAE,WAAW,MAAM,YAAY,EAAE,CAAC;AA8BlE,SAAS,aAAgB,SAAyC;AACvE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAtC1C;AAuCI,UAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AAC7B,UAAM,WAAW,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;AAE/D,UAAM,aAAmC;AAAA,MACvC,QAAQ,QAAQ;AAAA,MAChB,UAAU,EAAE;AAAA,MACZ,MAAM,EAAE,WAAW,EAAE;AAAA,MACrB,SAAS;AAAA,QACP,GAAG,QAAQ;AAAA,QACX,GAAI,WACA;AAAA,UACE,gBAAgB;AAAA,UAChB,kBAAkB,OAAO,WAAW,QAAQ;AAAA,QAC9C,IACA,CAAC;AAAA,MACP;AAAA,MACA,UAAS,aAAQ,YAAR,YAAmB;AAAA,MAC5B,OAAO;AAAA,IACT;AAEA,UAAM,MAAM,MAAM,QAAQ,YAAY,SAAO;AAC3C,YAAM,SAAmB,CAAC;AAC1B,UAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,UAAI,GAAG,OAAO,MAAM;AA9D1B,YAAAA;AA+DQ,cAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS;AAC3C,cAAM,cAAaA,MAAA,IAAI,eAAJ,OAAAA,MAAkB;AAErC,YAAI,aAAa,OAAO,cAAc,KAAK;AAKzC,iBAAO,IAAI,UAAU,QAAQ,UAAU,IAAI,YAAY,IAAI,SAAS,GAAG,CAAC;AACxE;AAAA,QACF;AAEA,YAAI;AACF,kBAAQ,KAAK,MAAM,GAAG,CAAM;AAAA,QAC9B,QAAQ;AACN,iBAAO,IAAI,MAAM,wBAAwB,UAAU,WAAW,CAAC;AAAA,QACjE;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,QAAI,GAAG,SAAS,MAAM;AACtB,QAAI,GAAG,WAAW,MAAM,IAAI,QAAQ,IAAI,MAAM,SAAS,CAAC,CAAC;AAKzD,QAAI,QAAQ,QAAQ;AAClB,UAAI,QAAQ,OAAO,SAAS;AAC1B,YAAI,QAAQ,IAAI,MAAM,SAAS,CAAC;AAChC,eAAO,IAAI,MAAM,SAAS,CAAC;AAC3B;AAAA,MACF;AACA,YAAM,UAAU,MAAY;AAC1B,YAAI,QAAQ,IAAI,MAAM,SAAS,CAAC;AAChC,eAAO,IAAI,MAAM,SAAS,CAAC;AAAA,MAC7B;AACA,cAAQ,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,IAClE;AAEA,QAAI,UAAU;AACZ,UAAI,MAAM,QAAQ;AAAA,IACpB;AACA,QAAI,IAAI;AAAA,EACV,CAAC;AACH;AAGO,MAAM,kBAAkB,MAAM;AAAA;AAAA,EAE1B;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQT,YACE,SACA,YACA,UAAyD,CAAC,GAC1D,eAAuB,IACvB;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,eAAe;AAAA,EACtB;AACF;",
6
6
  "names": ["_a"]
7
7
  }
@@ -650,7 +650,16 @@ class StateManager {
650
650
  }
651
651
  /**
652
652
  * Update info.membersUnreachable for a group.
653
- * Creates the state if unreachable members exist, deletes it when all are reachable.
653
+ *
654
+ * Pflegt den state IMMER (existing) und schreibt eine comma-separated
655
+ * Liste der unreachable members oder einen leeren String wenn alle
656
+ * online sind. Vorher haben wir bei „alle reachable" das Object
657
+ * gelöscht — das produzierte aber js-controller-WARN „State
658
+ * 'X.membersUnreachable' has no existing object" alle ~2 Minuten,
659
+ * weil parallele updateGroupReachability-Aufrufe (LAN+MQTT-Status-
660
+ * Updates feuern fast gleichzeitig) eine race condition zwischen
661
+ * setStateAsync (Object existiert) und safeDeleteState (Object weg)
662
+ * triggern können. State immer existent zu halten umgeht das komplett.
654
663
  *
655
664
  * @param group BaseGroup device
656
665
  * @param memberDevices Resolved member devices
@@ -662,15 +671,11 @@ class StateManager {
662
671
  const shortId = (0, import_types.normalizeDeviceId)(m.deviceId).slice(-4);
663
672
  return sanitize(`${m.sku}_${shortId}`);
664
673
  });
665
- if (unreachable.length === 0) {
666
- await this.safeDeleteState(stateId);
667
- } else {
668
- await this.ensureState(stateId, "Unreachable Members", "string", "text", false);
669
- await this.adapter.setStateAsync(stateId, {
670
- val: unreachable.join(", "),
671
- ack: true
672
- });
673
- }
674
+ await this.ensureState(stateId, "Unreachable Members", "string", "text", false);
675
+ await this.adapter.setStateAsync(stateId, {
676
+ val: unreachable.join(", "),
677
+ ack: true
678
+ });
674
679
  }
675
680
  /**
676
681
  * Cleanup stale devices that no longer exist.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/state-manager.ts"],
4
- "sourcesContent": ["import type * as utils from \"@iobroker/adapter-core\";\nimport type { StateDefinition } from \"./capability-mapper\";\nimport { GROUP_ICON, iconForGoveeType, shortenGoveeType } from \"./device-icons\";\nimport { resolveSegmentCount } from \"./device-manager\";\nimport { normalizeDeviceId, type DeviceState, type GoveeDevice } from \"./types\";\n\n/**\n * Sanitize a string for ioBroker object ID\n *\n * @param str Input string to sanitize\n */\nfunction sanitize(str: string): string {\n return str.replace(/[^a-zA-Z0-9_-]/g, \"_\").toLowerCase();\n}\n\n/**\n * Channels whose state-set is fully described by capability-driven stateDefs.\n * Only these get the stale-state cleanup pass \u2014 `info` is intentionally absent\n * because it mixes capability-driven states (diagnostics_export/result) with\n * adapter-managed ones (online, model, serial, ip, members) that come from\n * ensureState instead of stateDefs. Cleaning `info` by stateDef-set would\n * delete the adapter-managed ones.\n */\nconst MANAGED_CHANNELS = [\"control\", \"scenes\", \"music\", \"snapshots\", \"sensor\", \"events\"];\n/**\n * Display names used when the channel object is (re-)created. `info` is\n * listed here even though it's not in MANAGED_CHANNELS \u2014 capability-mapper\n * emits states with `channel: \"info\"`, and without this entry the create\n * path would overwrite the original \"Device Information\" name with the\n * literal \"info\".\n */\nconst CHANNEL_NAMES: Record<string, string> = {\n control: \"Controls\",\n scenes: \"Scenes\",\n music: \"Music\",\n snapshots: \"Snapshots\",\n sensor: \"Sensor Data\",\n events: \"Events\",\n info: \"Device Information\",\n diag: \"Diagnostics\",\n};\n\n/**\n * Synthetic capabilities written by the App-API poll and OpenAPI-MQTT\n * handler arrive as `(stateId, value)` pairs without a channel hint \u2014\n * `mapCloudStateValue` returns only what the Govee response carries.\n *\n * For lights the LAN/Cloud-state pipeline pre-populates `stateChannelMap`\n * via `createDeviceStates`, so `resolveStatePath` finds the channel.\n * For thermometer/appliance state IDs that are *only* delivered via the\n * App-API path (battery, temperature, humidity, CO\u2082, lackWater, \u2026) the map\n * is empty and the lookup would default to \"control\" \u2014 visibly wrong:\n * `info: control.battery has no existing object`.\n *\n * This routing table assigns those IDs to their semantic channel without\n * needing a separate `createDeviceStates` pass for sensor-only devices.\n * Keep IDs lowercase; resolveStatePath calls this on the raw stateId.\n */\n// Beide Lookup-Sets enthalten zwei Schreibweisen pro State-ID:\n// - \u201Eraw\"-Form (z.B. `temperature`) f\u00FCr instances die direkt so hei\u00DFen\n// - sanitizeId-Output (z.B. `sensor_temperature`) f\u00FCr camelCase-instances\n// die durch sanitizeId zu snake_case konvertiert wurden\n// `sanitizeId` in capability-mapper konvertiert camelCase \u2192 snake_case, also\n// werden \u201EsensorTemperature\" zu \u201Esensor_temperature\" und \u201ElackWaterEvent\"\n// zu \u201Elack_water_event\". Ohne diese Aliase fielen sanitize-Varianten auf den\n// safe-default \u201Econtrol\" zur\u00FCck und die States w\u00E4ren nicht erreichbar.\nconst SENSOR_STATE_IDS = new Set([\n // raw forms\n \"temperature\",\n \"humidity\",\n \"battery\",\n \"co2\",\n \"carbondioxide\",\n \"online\",\n // sanitizeId(instance) forms\n \"sensor_temperature\",\n \"sensor_humidity\",\n \"sensor_battery\",\n]);\nconst EVENT_STATE_IDS = new Set([\n // raw forms (no underscore separator)\n \"lackwater\",\n \"lackwaterevent\",\n \"icefull\",\n \"icefullevent\",\n \"bodyappeared\",\n \"dirtdetected\",\n // sanitizeId(instance) forms (camelCase \u2192 snake_case)\n \"lack_water\",\n \"lack_water_event\",\n \"ice_full\",\n \"ice_full_event\",\n \"body_appeared\",\n \"dirt_detected\",\n]);\n\n/**\n * Best-effort channel routing for state IDs that don't have a\n * stateChannelMap entry yet (e.g. App-API synthetic caps before the device\n * has gone through createDeviceStates). Empty input falls back to the safe\n * default \"control\".\n *\n * @param stateId The raw state ID (e.g. \"battery\", \"lackWater\")\n */\nfunction inferChannelFromStateId(stateId: string): string {\n const normalised = stateId.toLowerCase();\n if (SENSOR_STATE_IDS.has(normalised)) {\n return \"sensor\";\n }\n if (EVENT_STATE_IDS.has(normalised)) {\n return \"events\";\n }\n return \"control\";\n}\n\n/** Per-stateId metadata for synthetic states (App-API/OpenAPI-MQTT pipe). */\ninterface SyntheticStateMeta {\n type: \"boolean\" | \"number\";\n role: string;\n unit?: string;\n name: string;\n}\nconst SYNTHETIC_STATE_META: Record<string, SyntheticStateMeta> = {\n temperature: {\n type: \"number\",\n role: \"value.temperature\",\n unit: \"\u00B0C\",\n name: \"Temperature\",\n },\n humidity: {\n type: \"number\",\n role: \"value.humidity\",\n unit: \"%\",\n name: \"Humidity\",\n },\n battery: {\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n name: \"Battery\",\n },\n co2: { type: \"number\", role: \"value.co2\", unit: \"ppm\", name: \"CO\u2082\" },\n carbondioxide: {\n type: \"number\",\n role: \"value.co2\",\n unit: \"ppm\",\n name: \"CO\u2082\",\n },\n online: { type: \"boolean\", role: \"indicator.connected\", name: \"Online\" },\n lackwater: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n lackwaterevent: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n icefull: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n icefullevent: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n bodyappeared: { type: \"boolean\", role: \"indicator\", name: \"Body Detected\" },\n dirtdetected: { type: \"boolean\", role: \"indicator\", name: \"Dirt Detected\" },\n // sanitizeId(instance) Aliases \u2014 gleiche Meta wie raw-Form, decoupled\n // damit der Adapter beim ersten Sensor-State-Write den richtigen Channel\n // (sensor/ bzw. events/) anlegt.\n sensor_temperature: {\n type: \"number\",\n role: \"value.temperature\",\n unit: \"\u00B0C\",\n name: \"Temperature\",\n },\n sensor_humidity: {\n type: \"number\",\n role: \"value.humidity\",\n unit: \"%\",\n name: \"Humidity\",\n },\n sensor_battery: {\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n name: \"Battery\",\n },\n lack_water: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n lack_water_event: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n ice_full: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n ice_full_event: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n body_appeared: { type: \"boolean\", role: \"indicator\", name: \"Body Detected\" },\n dirt_detected: { type: \"boolean\", role: \"indicator\", name: \"Dirt Detected\" },\n};\n\n/** Manages ioBroker state creation and updates for Govee devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\n /** Maps deviceKey (sku_deviceId) \u2192 current object prefix */\n private readonly prefixMap = new Map<string, string>();\n /** Maps \"prefix.stateId\" \u2192 channel name (populated during createDeviceStates) */\n private readonly stateChannelMap = new Map<string, string>();\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: utils.AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Idempotent state-delete: pr\u00FCft erst ob das Object existiert. Wenn nicht,\n * no-op (verhindert \u201Ehas no existing object\"-WARN den `delStateAsync`\n * sonst intern triggert wenn das Object weg ist).\n *\n * Pattern: Caller will ein State l\u00F6schen (z.B. weil der Zustand \u201Ecleaned\"\n * geworden ist), aber wei\u00DF nicht ob das Object jemals da war. delObject\n * + delState ist nur dann sicher wenn das Object EXISTIERT.\n *\n * @param id Voller State-Pfad (`devices.X.info.Y`)\n */\n private async safeDeleteState(id: string): Promise<void> {\n const obj = await this.adapter.getObjectAsync(id).catch(() => null);\n if (!obj) {\n return;\n }\n await this.adapter.delStateAsync(id).catch(() => undefined);\n await this.adapter.delObjectAsync(id).catch(() => undefined);\n }\n\n /**\n * Push the device's trust tier (verified/reported/seed/unknown) into\n * the user-visible `diag.tier` state. Called after every device-state\n * refresh so the value tracks any registry change between adapter\n * restarts (e.g. seed \u2192 verified once a tester confirms). No-op for\n * groups (BaseGroup has no per-device tier).\n *\n * @param device Govee device\n * @param tier Canonical tier label\n */\n async updateDeviceTier(device: GoveeDevice, tier: string): Promise<void> {\n if (device.sku === \"BaseGroup\") {\n return;\n }\n const prefix = this.devicePrefix(device);\n await this.adapter.setStateAsync(`${prefix}.diag.tier`, { val: tier, ack: true }).catch(() => undefined);\n }\n\n /**\n * Migrate v2.1.0 layout (`info.diagnostics_*`) to v2.1.1 layout\n * (`diag.*`). Deletes the three old objects + states; the new ones get\n * created by the regular `createDeviceStates` pass. Idempotent \u2014 calling\n * twice is a no-op once the old objects are gone.\n *\n * @param device Govee device\n */\n async migrateLegacyDiagnostics(device: GoveeDevice): Promise<void> {\n if (device.sku === \"BaseGroup\") {\n return;\n }\n const prefix = this.devicePrefix(device);\n for (const stale of [\"diagnostics_export\", \"diagnostics_result\", \"diagnostics_tier\"]) {\n await this.safeDeleteState(`${prefix}.info.${stale}`);\n this.stateChannelMap.delete(`${prefix}.${stale}`);\n }\n }\n\n /**\n * Resolve full state path for a given device prefix and state ID.\n * Routes the state to the correct channel (control, scenes, music, snapshots).\n *\n * @param prefix Device object ID prefix\n * @param stateId State ID suffix\n */\n resolveStatePath(prefix: string, stateId: string): string {\n const channel = this.stateChannelMap.get(`${prefix}.${stateId}`) ?? inferChannelFromStateId(stateId);\n return `${prefix}.${channel}.${stateId}`;\n }\n\n /**\n * Lazily create the channel + state object for synthetic state IDs the\n * App-API poll and OpenAPI-MQTT pipeline write. Cloud-capability defs\n * for sensor SKUs (e.g. H5179) are often empty in OpenAPI v2, so the\n * usual `createDeviceStates` pass would not declare battery / temperature\n * / events.* \u2014 without this helper the first write logs\n * `info: <id> has no existing object`.\n *\n * Idempotent: skips when the meta table doesn't know the stateId, and\n * `setObjectNotExistsAsync` is itself a no-op for existing objects.\n *\n * @param prefix Device prefix (e.g. \"devices.h5179_aabb\")\n * @param stateId State ID without channel (e.g. \"battery\")\n */\n async ensureSyntheticStateObject(prefix: string, stateId: string): Promise<void> {\n const meta = SYNTHETIC_STATE_META[stateId.toLowerCase()];\n if (!meta) {\n return;\n }\n const channel = inferChannelFromStateId(stateId);\n // Channel object first \u2014 sensors land in the new sensor/ subtree, events\n // in events/. Without an extendObject the channel parent stays missing\n // and Admin shows the state directly under the device root.\n await this.adapter\n .extendObjectAsync(`${prefix}.${channel}`, {\n type: \"channel\",\n common: { name: CHANNEL_NAMES[channel] ?? channel },\n native: {},\n })\n .catch(() => undefined);\n // extendObjectAsync (idempotent + repariert partial-formed Objects).\n // setObjectNotExistsAsync w\u00E4re no-op auf existing \u2014 und Objects aus\n // alten Layouts (v2.0.x\u2192v2.1.x-Migration) k\u00F6nnen unvollst\u00E4ndige\n // common-Felder haben, die dann beim ersten setStateAsync warnen.\n await this.adapter\n .extendObjectAsync(`${prefix}.${channel}.${stateId}`, {\n type: \"state\",\n common: {\n name: meta.name,\n type: meta.type,\n role: meta.role,\n read: true,\n write: false,\n ...(meta.unit !== undefined ? { unit: meta.unit } : {}),\n def: meta.type === \"boolean\" ? false : 0,\n },\n native: {},\n })\n .catch(() => undefined);\n this.stateChannelMap.set(`${prefix}.${stateId}`, channel);\n }\n\n /**\n * Create device object and all states from capability definitions.\n *\n * @param device Govee device\n * @param stateDefs State definitions from capability mapper\n */\n async createDeviceStates(device: GoveeDevice, stateDefs: StateDefinition[]): Promise<void> {\n const key = this.deviceKey(device);\n const newPrefix = this.devicePrefix(device);\n const oldPrefix = this.prefixMap.get(key);\n\n // Migrate if prefix changed (e.g., old naming scheme)\n if (oldPrefix && oldPrefix !== newPrefix) {\n this.adapter.log.debug(`Migrating device ${device.sku}: ${oldPrefix} \u2192 ${newPrefix}`);\n await this.adapter.delObjectAsync(oldPrefix, { recursive: true });\n // Drop stale channel-map entries under the old prefix so they don't\n // shadow resolveStatePath lookups after the rename.\n const oldChannelKey = `${oldPrefix}.`;\n for (const mapKey of this.stateChannelMap.keys()) {\n if (mapKey.startsWith(oldChannelKey)) {\n this.stateChannelMap.delete(mapKey);\n }\n }\n }\n this.prefixMap.set(key, newPrefix);\n\n const prefix = newPrefix;\n const isGroup = device.sku === \"BaseGroup\";\n\n // Device object with online status indicator + type-aware icon.\n // Groups use the general groups.info.online state instead of per-group online.\n const onlineId = isGroup\n ? `${this.adapter.namespace}.groups.info.online`\n : `${this.adapter.namespace}.${prefix}.info.online`;\n const icon = isGroup ? GROUP_ICON : iconForGoveeType(device.type);\n await this.adapter.extendObjectAsync(prefix, {\n type: \"device\",\n common: {\n name: device.name,\n icon,\n statusStates: { onlineId },\n } as ioBroker.DeviceCommon,\n native: {\n sku: device.sku,\n deviceId: device.deviceId,\n },\n });\n\n // Info channel \u2014 groups only get name (no individual online)\n await this.adapter.extendObjectAsync(`${prefix}.info`, {\n type: \"channel\",\n common: { name: \"Device Information\" },\n native: {},\n });\n\n await this.ensureState(`${prefix}.info.name`, \"Name\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.name`, {\n val: device.name,\n ack: true,\n });\n\n if (!isGroup) {\n await this.ensureState(`${prefix}.info.online`, \"Online\", \"boolean\", \"indicator.reachable\", false);\n await this.adapter.setStateAsync(`${prefix}.info.online`, {\n val: device.state.online ?? false,\n ack: true,\n });\n await this.ensureState(`${prefix}.info.model`, \"Model\", \"string\", \"text\", false);\n await this.ensureState(`${prefix}.info.serial`, \"Serial Number\", \"string\", \"text\", false);\n await this.ensureState(`${prefix}.info.ip`, \"IP Address\", \"string\", \"info.ip\", false);\n // Device-type marker \u2014 short label like \"light\", \"thermometer\",\n // \"heater\" (Govee API type without the \"devices.types.\" prefix).\n // Lets scripts filter `*.info.type === \"light\"` without parsing.\n await this.ensureState(`${prefix}.info.type`, \"Device Type\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.model`, {\n val: device.sku,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.serial`, {\n val: device.deviceId,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.ip`, {\n val: device.lanIp ?? \"\",\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.type`, {\n val: shortenGoveeType(device.type),\n ack: true,\n });\n } else {\n // Group members: comma-separated device prefix IDs\n const memberIds = (device.groupMembers ?? [])\n .map(m => {\n const shortId = normalizeDeviceId(m.deviceId).slice(-4);\n return sanitize(`${m.sku}_${shortId}`);\n })\n .join(\", \");\n await this.ensureState(`${prefix}.info.members`, \"Members\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.members`, {\n val: memberIds,\n ack: true,\n });\n\n // Legacy cleanup \u2014 groups never carry device-level info states or\n // diagnostics, but older installs had them. Drop any leftovers so the\n // tree reflects the current layout.\n for (const staleId of [\n \"online\",\n \"model\",\n \"serial\",\n \"ip\",\n \"diagnostics_export\",\n \"diagnostics_result\",\n \"diagnostics_tier\",\n ]) {\n await this.safeDeleteState(`${prefix}.info.${staleId}`);\n }\n // Groups never had a `diag` channel \u2014 drop any leftover from migrated installs.\n await this.adapter.delObjectAsync(`${prefix}.diag`, { recursive: true }).catch(() => {});\n }\n\n // Group state defs by channel (control, scenes, music, snapshots)\n const nonSegmentDefs = stateDefs.filter(d => !d.id.startsWith(\"_segment_\"));\n const channelGroups = new Map<string, StateDefinition[]>();\n for (const def of nonSegmentDefs) {\n const channel = def.channel ?? \"control\";\n this.stateChannelMap.set(`${prefix}.${def.id}`, channel);\n if (!channelGroups.has(channel)) {\n channelGroups.set(channel, []);\n }\n channelGroups.get(channel)!.push(def);\n }\n\n this.adapter.log.debug(\n `createDeviceStates ${device.sku}: ${nonSegmentDefs.length} states in ${channelGroups.size} channel(s)`,\n );\n\n // Create states in each channel\n for (const [channel, defs] of channelGroups) {\n await this.adapter.extendObjectAsync(`${prefix}.${channel}`, {\n type: \"channel\",\n common: { name: CHANNEL_NAMES[channel] ?? channel },\n native: {},\n });\n\n for (const def of defs) {\n const common: Partial<ioBroker.StateCommon> = {\n name: def.name,\n type: def.type,\n role: def.role,\n read: true,\n write: def.write,\n };\n\n if (def.unit) {\n common.unit = def.unit;\n }\n if (def.min !== undefined) {\n common.min = def.min;\n }\n if (def.max !== undefined) {\n common.max = def.max;\n }\n if (def.states) {\n common.states = def.states;\n }\n if (def.def !== undefined) {\n common.def = def.def;\n }\n if (def.desc) {\n common.desc = def.desc;\n }\n\n await this.adapter.extendObjectAsync(`${prefix}.${channel}.${def.id}`, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {\n capabilityType: def.capabilityType,\n capabilityInstance: def.capabilityInstance,\n },\n });\n\n // Initialize or validate state value\n if (def.def !== undefined) {\n const current = await this.adapter.getStateAsync(`${prefix}.${channel}.${def.id}`);\n if (!current || current.val === null || current.val === undefined) {\n // Set default value for new states\n await this.adapter.setStateAsync(`${prefix}.${channel}.${def.id}`, {\n val: def.def,\n ack: true,\n });\n } else if (def.states && !(String(current.val) in def.states)) {\n // Reset dropdown to default if current value is no longer valid\n this.adapter.log.debug(\n `Resetting stale dropdown: ${prefix}.${channel}.${def.id} = \"${String(current.val)}\" \u2192 \"${String(def.def)}\"`,\n );\n await this.adapter.setStateAsync(`${prefix}.${channel}.${def.id}`, {\n val: def.def,\n ack: true,\n });\n }\n }\n }\n }\n\n // Remove stale states across all managed channels\n await this.cleanupAllChannelStates(prefix, nonSegmentDefs);\n\n // Check if device has segment capabilities\n const segmentDefs = stateDefs.filter(d => d.id.startsWith(\"_segment_\"));\n if (segmentDefs.length > 0) {\n await this.createSegmentStates(device);\n }\n }\n\n /**\n * Create segment channel with per-segment color + brightness states.\n *\n * @param device Govee device\n */\n async createSegmentStates(device: GoveeDevice): Promise<void> {\n const prefix = this.devicePrefix(device);\n\n await this.adapter.extendObjectAsync(`${prefix}.segments`, {\n type: \"channel\",\n common: { name: \"LED Segments\" },\n native: {},\n });\n\n // Resolve the authoritative count: cache/MQTT-learned wins over Cloud\n // capabilities. A manual list can only grow the count (never shrink it)\n // so users editing manual_list can reveal hidden indices without losing\n // the already-learned total.\n const resolved = resolveSegmentCount(device);\n const manualMax =\n Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? Math.max(...device.manualSegments) + 1\n : 0;\n const segmentCount = Math.max(resolved, manualMax);\n device.segmentCount = segmentCount;\n\n // Effective segment list \u2014 honor manual override if active (cut-strip support)\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.slice().sort((a, b) => a - b)\n : Array.from({ length: segmentCount }, (_, i) => i);\n const reportedCount = validIndices.length;\n\n await this.ensureState(`${prefix}.segments.count`, \"Segment Count\", \"number\", \"value\", false);\n await this.adapter.setStateAsync(`${prefix}.segments.count`, {\n val: reportedCount,\n ack: true,\n });\n\n // Manual-mode toggle and list \u2014 user-writable for cut-strip overrides\n await this.adapter.extendObjectAsync(`${prefix}.segments.manual_mode`, {\n type: \"state\",\n common: {\n name: \"Manual Segments Active\",\n type: \"boolean\",\n role: \"switch\",\n read: true,\n write: true,\n def: false,\n desc: \"Enable manual segment list (e.g. for cut LED strips with fewer physical segments than reported)\",\n } as ioBroker.StateCommon,\n native: {},\n });\n await this.adapter.extendObjectAsync(`${prefix}.segments.manual_list`, {\n type: \"state\",\n common: {\n name: \"Manual Segment List\",\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n def: \"\",\n desc: 'Comma-separated indices + ranges, e.g. \"0-9\" or \"0-8,10-14\" (only used when manual_mode=true)',\n } as ioBroker.StateCommon,\n native: {},\n });\n\n // Sync manual_mode / manual_list states back from the runtime device\n // (restored from cache on startup, or updated by the wizard). Using\n // ack=true keeps this out of the user-change handler path.\n const manualModeVal = device.manualMode === true;\n const manualListVal =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.join(\",\")\n : \"\";\n await this.adapter.setStateAsync(`${prefix}.segments.manual_mode`, {\n val: manualModeVal,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.segments.manual_list`, {\n val: manualListVal,\n ack: true,\n });\n\n for (const i of validIndices) {\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}`, {\n type: \"channel\",\n common: { name: `Segment ${i}` },\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}.color`, {\n type: \"state\",\n common: {\n name: \"Color\",\n type: \"string\",\n role: \"level.color.rgb\",\n read: true,\n write: true,\n } as ioBroker.StateCommon,\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}.brightness`, {\n type: \"state\",\n common: {\n name: \"Brightness\",\n type: \"number\",\n role: \"level.brightness\",\n read: true,\n write: true,\n min: 0,\n max: 100,\n unit: \"%\",\n } as ioBroker.StateCommon,\n native: {},\n });\n }\n\n // Comfort command state for batch segment control\n await this.adapter.extendObjectAsync(`${prefix}.segments.command`, {\n type: \"state\",\n common: {\n name: \"Batch Segment Command\",\n type: \"string\",\n role: \"text\",\n read: false,\n write: true,\n desc: \"Format: segments:color:brightness \u2014 e.g. 1-5:#ff0000:20, all:#00ff00, 0,3,7::50\",\n } as ioBroker.StateCommon,\n native: {},\n });\n\n // Remove segment channels that aren't in the valid list (supports gaps for manual mode)\n await this.cleanupExcessSegments(prefix, validIndices);\n }\n\n /**\n * Remove segment sub-channels that are not in the valid-indices list.\n * Supports gaps (e.g. manual list \"0-8,10-14\" \u2192 segment 9 channel gets removed).\n *\n * @param prefix Device prefix\n * @param validIndices Valid segment indices (all others will be deleted)\n */\n private async cleanupExcessSegments(prefix: string, validIndices: number[]): Promise<void> {\n const valid = new Set(validIndices);\n const segPrefix = `${this.adapter.namespace}.${prefix}.segments.`;\n const existing = await this.adapter.getObjectViewAsync(\"system\", \"channel\", {\n startkey: segPrefix,\n endkey: `${segPrefix}\\u9999`,\n });\n\n if (!existing?.rows) {\n return;\n }\n\n for (const row of existing.rows) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n const segPart = localId.replace(`${prefix}.segments.`, \"\");\n const segIdx = parseInt(segPart, 10);\n if (!isNaN(segIdx) && !valid.has(segIdx)) {\n this.adapter.log.debug(`Removing excess segment: ${localId}`);\n await this.adapter.delObjectAsync(localId, { recursive: true });\n }\n }\n }\n\n /**\n * Update device state from any source (LAN, MQTT, Cloud).\n *\n * Writes are fire-and-forget and run in parallel \u2014 they're independent,\n * and the \"does this state exist?\" check that used to guard each write\n * was an extra object-read on the hot path (one MQTT push = one update\n * call). createDeviceStates has already run before any update lands,\n * so the states are guaranteed to exist; if one disappears (manual\n * deletion), the setStateAsync will reject and we swallow it.\n *\n * @param device Govee device\n * @param state Partial state update\n */\n async updateDeviceState(device: GoveeDevice, state: Partial<DeviceState>): Promise<void> {\n const prefix = this.devicePrefix(device);\n const writes: Promise<unknown>[] = [];\n\n const set = (id: string, val: ioBroker.StateValue): void => {\n writes.push(this.adapter.setStateAsync(id, { val, ack: true }).catch(() => undefined));\n };\n\n if (state.online !== undefined) {\n set(`${prefix}.info.online`, state.online);\n }\n if (state.power !== undefined) {\n set(`${prefix}.control.power`, state.power);\n }\n if (state.brightness !== undefined) {\n set(`${prefix}.control.brightness`, state.brightness);\n }\n if (state.colorRgb !== undefined) {\n set(`${prefix}.control.colorRgb`, state.colorRgb);\n }\n if (state.colorTemperature !== undefined) {\n set(`${prefix}.control.colorTemperature`, state.colorTemperature);\n }\n if (state.scene !== undefined) {\n set(`${prefix}.control.scene`, state.scene);\n }\n\n await Promise.all(writes);\n }\n\n /**\n * Create the general groups.info.online state (reflects Cloud connection).\n *\n * @param online Initial online value\n */\n async createGroupsOnlineState(online: boolean): Promise<void> {\n await this.adapter.extendObjectAsync(\"groups\", {\n type: \"folder\",\n common: { name: \"Groups\" },\n native: {},\n });\n await this.adapter.extendObjectAsync(\"groups.info\", {\n type: \"channel\",\n common: { name: \"Groups Status\" },\n native: {},\n });\n await this.ensureState(\"groups.info.online\", \"Cloud Online\", \"boolean\", \"indicator.reachable\", false);\n await this.adapter.setStateAsync(\"groups.info.online\", {\n val: online,\n ack: true,\n });\n }\n\n /**\n * Update the general groups online state.\n *\n * @param online Cloud connection status\n */\n async updateGroupsOnline(online: boolean): Promise<void> {\n await this.adapter.setStateAsync(\"groups.info.online\", { val: online, ack: true }).catch(() => undefined);\n }\n\n /**\n * Update info.membersUnreachable for a group.\n * Creates the state if unreachable members exist, deletes it when all are reachable.\n *\n * @param group BaseGroup device\n * @param memberDevices Resolved member devices\n */\n async updateGroupMembersUnreachable(group: GoveeDevice, memberDevices: GoveeDevice[]): Promise<void> {\n const prefix = this.devicePrefix(group);\n const stateId = `${prefix}.info.membersUnreachable`;\n\n const unreachable = memberDevices\n .filter(m => !m.state.online)\n .map(m => {\n const shortId = normalizeDeviceId(m.deviceId).slice(-4);\n return sanitize(`${m.sku}_${shortId}`);\n });\n\n if (unreachable.length === 0) {\n // All members reachable \u2014 drop the state via safeDeleteState (no-op\n // wenn nie ein unreachable-Member existierte, damit kein WARN-Spam\n // alle 2 Minuten beim refresh).\n await this.safeDeleteState(stateId);\n } else {\n await this.ensureState(stateId, \"Unreachable Members\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(stateId, {\n val: unreachable.join(\", \"),\n ack: true,\n });\n }\n }\n\n /**\n * Cleanup stale devices that no longer exist.\n *\n * Returns the prefixes of removed devices so callers (DeviceManager,\n * adapter-level maps) can drop their own entries for the same devices\n * and prevent unbounded map growth across the adapter's lifetime.\n *\n * @param currentDevices Current device list\n * @returns Prefixes of removed devices (e.g. \"devices.h61be_1d6f\")\n */\n async cleanupDevices(currentDevices: GoveeDevice[]): Promise<string[]> {\n const currentPrefixes = new Set(currentDevices.map(d => this.devicePrefix(d)));\n const removed: string[] = [];\n\n // Cleanup both devices/ and groups/ folders\n for (const folder of [\"devices\", \"groups\"]) {\n const existingObjects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.${folder}.`,\n endkey: `${this.adapter.namespace}.${folder}.\\u9999`,\n });\n\n if (!existingObjects?.rows) {\n continue;\n }\n\n for (const row of existingObjects.rows) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (!currentPrefixes.has(localId)) {\n this.adapter.log.debug(`Removing stale device: ${localId}`);\n // Recursive delObject removes the object tree but can leave\n // orphan state values in the state-table \u2014 clean those too so\n // historical values don't survive a device removal.\n const stateRows = await this.adapter\n .getObjectViewAsync(\"system\", \"state\", {\n startkey: `${row.id}.`,\n endkey: `${row.id}.\u9999`,\n })\n .catch(() => undefined);\n if (stateRows?.rows) {\n for (const stateRow of stateRows.rows) {\n const stateLocalId = stateRow.id.replace(`${this.adapter.namespace}.`, \"\");\n await this.adapter.delStateAsync(stateLocalId).catch(() => undefined);\n }\n }\n await this.adapter.delObjectAsync(localId, { recursive: true });\n this.forgetPrefix(localId);\n removed.push(localId);\n }\n }\n }\n return removed;\n }\n\n /**\n * Remove stale states across all managed channels.\n * Also handles migration from old single-control layout.\n *\n * One broad view-query across the whole device prefix replaces the old\n * four-per-device pass \u2014 the channel partition is recovered by parsing\n * the object id, saving three round-trips per device on every refresh.\n *\n * @param prefix Device prefix\n * @param stateDefs Current state definitions (non-segment)\n */\n private async cleanupAllChannelStates(prefix: string, stateDefs: StateDefinition[]): Promise<void> {\n // Build expected state set per channel\n const expectedByChannel = new Map<string, Set<string>>();\n for (const def of stateDefs) {\n const channel = def.channel ?? \"control\";\n if (!expectedByChannel.has(channel)) {\n expectedByChannel.set(channel, new Set());\n }\n expectedByChannel.get(channel)!.add(def.id);\n }\n\n const devicePrefix = `${this.adapter.namespace}.${prefix}.`;\n const existing = await this.adapter.getObjectViewAsync(\"system\", \"state\", {\n startkey: devicePrefix,\n endkey: `${devicePrefix}\\u9999`,\n });\n if (!existing?.rows) {\n return;\n }\n\n const totalsPerChannel = new Map<string, { seen: number; deleted: number }>();\n for (const row of existing.rows) {\n const rest = row.id.replace(devicePrefix, \"\");\n const dotIdx = rest.indexOf(\".\");\n if (dotIdx < 0) {\n continue;\n }\n const channel = rest.slice(0, dotIdx);\n const stateId = rest.slice(dotIdx + 1);\n if (!MANAGED_CHANNELS.includes(channel)) {\n continue;\n }\n const totals = totalsPerChannel.get(channel) ?? { seen: 0, deleted: 0 };\n totals.seen++;\n const validIds = expectedByChannel.get(channel) ?? new Set<string>();\n if (!validIds.has(stateId)) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n this.adapter.log.debug(`Removing stale state: ${localId}`);\n await this.adapter.delObjectAsync(localId);\n await this.adapter.delStateAsync(localId).catch(() => {});\n totals.deleted++;\n }\n totalsPerChannel.set(channel, totals);\n }\n\n // Remove empty channel objects \u2014 no surviving states for this channel\n for (const [channel, totals] of totalsPerChannel) {\n if (totals.deleted > 0 && totals.deleted === totals.seen) {\n this.adapter.log.debug(`Removing empty channel: ${prefix}.${channel}`);\n await this.adapter.delObjectAsync(`${prefix}.${channel}`).catch(() => undefined);\n }\n }\n }\n\n /**\n * Get device object ID prefix \u2014 stable SKU + short device ID.\n * Groups (BaseGroup) go under groups/, devices under devices/.\n * Human-readable name is in common.name, not in the object ID.\n *\n * @param device Govee device\n */\n devicePrefix(device: GoveeDevice): string {\n const shortId = normalizeDeviceId(device.deviceId).slice(-4);\n const folder = device.sku === \"BaseGroup\" ? \"groups\" : \"devices\";\n return `${folder}.${sanitize(`${device.sku}_${shortId}`)}`;\n }\n\n /**\n * Drop prefix + stateChannel entries for a device that was removed.\n * Prevents the maps from growing indefinitely across adapter lifetime.\n *\n * @param prefix Device prefix that was removed\n */\n private forgetPrefix(prefix: string): void {\n for (const key of this.prefixMap.keys()) {\n if (this.prefixMap.get(key) === prefix) {\n this.prefixMap.delete(key);\n }\n }\n const stalePrefix = `${prefix}.`;\n for (const key of this.stateChannelMap.keys()) {\n if (key.startsWith(stalePrefix)) {\n this.stateChannelMap.delete(key);\n }\n }\n }\n\n /**\n * Unique key for internal tracking (not used as object ID).\n *\n * @param device Govee device\n */\n private deviceKey(device: GoveeDevice): string {\n // Use normalizeDeviceId which is defensive against non-string input \u2014\n // cached data on disk could theoretically be tampered with.\n return `${device.sku}_${normalizeDeviceId(device.deviceId)}`;\n }\n\n /**\n * Create a state if it doesn't exist\n *\n * @param id State object ID\n * @param name Display name\n * @param type Value type\n * @param role ioBroker role\n * @param write Whether state is writable\n * @param unit Optional unit of measurement\n */\n private async ensureState(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n write: boolean,\n unit?: string,\n ): Promise<void> {\n const common: Partial<ioBroker.StateCommon> = {\n name,\n type,\n role,\n read: true,\n write,\n };\n if (unit) {\n common.unit = unit;\n }\n await this.adapter.extendObjectAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,0BAA+D;AAC/D,4BAAoC;AACpC,mBAAsE;AAOtE,SAAS,SAAS,KAAqB;AACrC,SAAO,IAAI,QAAQ,mBAAmB,GAAG,EAAE,YAAY;AACzD;AAUA,MAAM,mBAAmB,CAAC,WAAW,UAAU,SAAS,aAAa,UAAU,QAAQ;AAQvF,MAAM,gBAAwC;AAAA,EAC5C,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AACR;AA0BA,MAAM,mBAAmB,oBAAI,IAAI;AAAA;AAAA,EAE/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,MAAM,kBAAkB,oBAAI,IAAI;AAAA;AAAA,EAE9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAUD,SAAS,wBAAwB,SAAyB;AACxD,QAAM,aAAa,QAAQ,YAAY;AACvC,MAAI,iBAAiB,IAAI,UAAU,GAAG;AACpC,WAAO;AAAA,EACT;AACA,MAAI,gBAAgB,IAAI,UAAU,GAAG;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AASA,MAAM,uBAA2D;AAAA,EAC/D,aAAa;AAAA,IACX,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,KAAK,EAAE,MAAM,UAAU,MAAM,aAAa,MAAM,OAAO,MAAM,WAAM;AAAA,EACnE,eAAe;AAAA,IACb,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,QAAQ,EAAE,MAAM,WAAW,MAAM,uBAAuB,MAAM,SAAS;AAAA,EACvE,WAAW;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,gBAAgB;AAAA,IACd,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,SAAS,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,kBAAkB;AAAA,EACvE,cAAc,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,kBAAkB;AAAA,EAC5E,cAAc,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,gBAAgB;AAAA,EAC1E,cAAc,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA,EAI1E,oBAAoB;AAAA,IAClB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,gBAAgB;AAAA,IACd,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,YAAY;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,kBAAkB;AAAA,IAChB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,UAAU,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,kBAAkB;AAAA,EACxE,gBAAgB,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,kBAAkB;AAAA,EAC9E,eAAe,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,gBAAgB;AAAA,EAC3E,eAAe,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,gBAAgB;AAC7E;AAGO,MAAM,aAAa;AAAA,EACP;AAAA;AAAA,EAEA,YAAY,oBAAI,IAAoB;AAAA;AAAA,EAEpC,kBAAkB,oBAAI,IAAoB;AAAA;AAAA,EAG3D,YAAY,SAAgC;AAC1C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,gBAAgB,IAA2B;AACvD,UAAM,MAAM,MAAM,KAAK,QAAQ,eAAe,EAAE,EAAE,MAAM,MAAM,IAAI;AAClE,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AACA,UAAM,KAAK,QAAQ,cAAc,EAAE,EAAE,MAAM,MAAM,MAAS;AAC1D,UAAM,KAAK,QAAQ,eAAe,EAAE,EAAE,MAAM,MAAM,MAAS;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,iBAAiB,QAAqB,MAA6B;AACvE,QAAI,OAAO,QAAQ,aAAa;AAC9B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,cAAc,EAAE,KAAK,MAAM,KAAK,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EACzG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBAAyB,QAAoC;AACjE,QAAI,OAAO,QAAQ,aAAa;AAC9B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,eAAW,SAAS,CAAC,sBAAsB,sBAAsB,kBAAkB,GAAG;AACpF,YAAM,KAAK,gBAAgB,GAAG,MAAM,SAAS,KAAK,EAAE;AACpD,WAAK,gBAAgB,OAAO,GAAG,MAAM,IAAI,KAAK,EAAE;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,iBAAiB,QAAgB,SAAyB;AArR5D;AAsRI,UAAM,WAAU,UAAK,gBAAgB,IAAI,GAAG,MAAM,IAAI,OAAO,EAAE,MAA/C,YAAoD,wBAAwB,OAAO;AACnG,WAAO,GAAG,MAAM,IAAI,OAAO,IAAI,OAAO;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,2BAA2B,QAAgB,SAAgC;AAxSnF;AAySI,UAAM,OAAO,qBAAqB,QAAQ,YAAY,CAAC;AACvD,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AACA,UAAM,UAAU,wBAAwB,OAAO;AAI/C,UAAM,KAAK,QACR,kBAAkB,GAAG,MAAM,IAAI,OAAO,IAAI;AAAA,MACzC,MAAM;AAAA,MACN,QAAQ,EAAE,OAAM,mBAAc,OAAO,MAArB,YAA0B,QAAQ;AAAA,MAClD,QAAQ,CAAC;AAAA,IACX,CAAC,EACA,MAAM,MAAM,MAAS;AAKxB,UAAM,KAAK,QACR,kBAAkB,GAAG,MAAM,IAAI,OAAO,IAAI,OAAO,IAAI;AAAA,MACpD,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,MAAM;AAAA,QACN,OAAO;AAAA,QACP,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,QACrD,KAAK,KAAK,SAAS,YAAY,QAAQ;AAAA,MACzC;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC,EACA,MAAM,MAAM,MAAS;AACxB,SAAK,gBAAgB,IAAI,GAAG,MAAM,IAAI,OAAO,IAAI,OAAO;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,QAAqB,WAA6C;AApV7F;AAqVI,UAAM,MAAM,KAAK,UAAU,MAAM;AACjC,UAAM,YAAY,KAAK,aAAa,MAAM;AAC1C,UAAM,YAAY,KAAK,UAAU,IAAI,GAAG;AAGxC,QAAI,aAAa,cAAc,WAAW;AACxC,WAAK,QAAQ,IAAI,MAAM,oBAAoB,OAAO,GAAG,KAAK,SAAS,WAAM,SAAS,EAAE;AACpF,YAAM,KAAK,QAAQ,eAAe,WAAW,EAAE,WAAW,KAAK,CAAC;AAGhE,YAAM,gBAAgB,GAAG,SAAS;AAClC,iBAAW,UAAU,KAAK,gBAAgB,KAAK,GAAG;AAChD,YAAI,OAAO,WAAW,aAAa,GAAG;AACpC,eAAK,gBAAgB,OAAO,MAAM;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AACA,SAAK,UAAU,IAAI,KAAK,SAAS;AAEjC,UAAM,SAAS;AACf,UAAM,UAAU,OAAO,QAAQ;AAI/B,UAAM,WAAW,UACb,GAAG,KAAK,QAAQ,SAAS,wBACzB,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AACvC,UAAM,OAAO,UAAU,qCAAa,sCAAiB,OAAO,IAAI;AAChE,UAAM,KAAK,QAAQ,kBAAkB,QAAQ;AAAA,MAC3C,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,MAAM,OAAO;AAAA,QACb;AAAA,QACA,cAAc,EAAE,SAAS;AAAA,MAC3B;AAAA,MACA,QAAQ;AAAA,QACN,KAAK,OAAO;AAAA,QACZ,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,CAAC;AAGD,UAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,SAAS;AAAA,MACrD,MAAM;AAAA,MACN,QAAQ,EAAE,MAAM,qBAAqB;AAAA,MACrC,QAAQ,CAAC;AAAA,IACX,CAAC;AAED,UAAM,KAAK,YAAY,GAAG,MAAM,cAAc,QAAQ,UAAU,QAAQ,KAAK;AAC7E,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,cAAc;AAAA,MACtD,KAAK,OAAO;AAAA,MACZ,KAAK;AAAA,IACP,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,YAAY,GAAG,MAAM,gBAAgB,UAAU,WAAW,uBAAuB,KAAK;AACjG,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,gBAAgB;AAAA,QACxD,MAAK,YAAO,MAAM,WAAb,YAAuB;AAAA,QAC5B,KAAK;AAAA,MACP,CAAC;AACD,YAAM,KAAK,YAAY,GAAG,MAAM,eAAe,SAAS,UAAU,QAAQ,KAAK;AAC/E,YAAM,KAAK,YAAY,GAAG,MAAM,gBAAgB,iBAAiB,UAAU,QAAQ,KAAK;AACxF,YAAM,KAAK,YAAY,GAAG,MAAM,YAAY,cAAc,UAAU,WAAW,KAAK;AAIpF,YAAM,KAAK,YAAY,GAAG,MAAM,cAAc,eAAe,UAAU,QAAQ,KAAK;AACpF,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,eAAe;AAAA,QACvD,KAAK,OAAO;AAAA,QACZ,KAAK;AAAA,MACP,CAAC;AACD,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,gBAAgB;AAAA,QACxD,KAAK,OAAO;AAAA,QACZ,KAAK;AAAA,MACP,CAAC;AACD,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,YAAY;AAAA,QACpD,MAAK,YAAO,UAAP,YAAgB;AAAA,QACrB,KAAK;AAAA,MACP,CAAC;AACD,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,cAAc;AAAA,QACtD,SAAK,sCAAiB,OAAO,IAAI;AAAA,QACjC,KAAK;AAAA,MACP,CAAC;AAAA,IACH,OAAO;AAEL,YAAM,cAAa,YAAO,iBAAP,YAAuB,CAAC,GACxC,IAAI,OAAK;AACR,cAAM,cAAU,gCAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE;AACtD,eAAO,SAAS,GAAG,EAAE,GAAG,IAAI,OAAO,EAAE;AAAA,MACvC,CAAC,EACA,KAAK,IAAI;AACZ,YAAM,KAAK,YAAY,GAAG,MAAM,iBAAiB,WAAW,UAAU,QAAQ,KAAK;AACnF,YAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,iBAAiB;AAAA,QACzD,KAAK;AAAA,QACL,KAAK;AAAA,MACP,CAAC;AAKD,iBAAW,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,GAAG;AACD,cAAM,KAAK,gBAAgB,GAAG,MAAM,SAAS,OAAO,EAAE;AAAA,MACxD;AAEA,YAAM,KAAK,QAAQ,eAAe,GAAG,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACzF;AAGA,UAAM,iBAAiB,UAAU,OAAO,OAAK,CAAC,EAAE,GAAG,WAAW,WAAW,CAAC;AAC1E,UAAM,gBAAgB,oBAAI,IAA+B;AACzD,eAAW,OAAO,gBAAgB;AAChC,YAAM,WAAU,SAAI,YAAJ,YAAe;AAC/B,WAAK,gBAAgB,IAAI,GAAG,MAAM,IAAI,IAAI,EAAE,IAAI,OAAO;AACvD,UAAI,CAAC,cAAc,IAAI,OAAO,GAAG;AAC/B,sBAAc,IAAI,SAAS,CAAC,CAAC;AAAA,MAC/B;AACA,oBAAc,IAAI,OAAO,EAAG,KAAK,GAAG;AAAA,IACtC;AAEA,SAAK,QAAQ,IAAI;AAAA,MACf,sBAAsB,OAAO,GAAG,KAAK,eAAe,MAAM,cAAc,cAAc,IAAI;AAAA,IAC5F;AAGA,eAAW,CAAC,SAAS,IAAI,KAAK,eAAe;AAC3C,YAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,IAAI,OAAO,IAAI;AAAA,QAC3D,MAAM;AAAA,QACN,QAAQ,EAAE,OAAM,mBAAc,OAAO,MAArB,YAA0B,QAAQ;AAAA,QAClD,QAAQ,CAAC;AAAA,MACX,CAAC;AAED,iBAAW,OAAO,MAAM;AACtB,cAAM,SAAwC;AAAA,UAC5C,MAAM,IAAI;AAAA,UACV,MAAM,IAAI;AAAA,UACV,MAAM,IAAI;AAAA,UACV,MAAM;AAAA,UACN,OAAO,IAAI;AAAA,QACb;AAEA,YAAI,IAAI,MAAM;AACZ,iBAAO,OAAO,IAAI;AAAA,QACpB;AACA,YAAI,IAAI,QAAQ,QAAW;AACzB,iBAAO,MAAM,IAAI;AAAA,QACnB;AACA,YAAI,IAAI,QAAQ,QAAW;AACzB,iBAAO,MAAM,IAAI;AAAA,QACnB;AACA,YAAI,IAAI,QAAQ;AACd,iBAAO,SAAS,IAAI;AAAA,QACtB;AACA,YAAI,IAAI,QAAQ,QAAW;AACzB,iBAAO,MAAM,IAAI;AAAA,QACnB;AACA,YAAI,IAAI,MAAM;AACZ,iBAAO,OAAO,IAAI;AAAA,QACpB;AAEA,cAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE,IAAI;AAAA,UACrE,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,YACN,gBAAgB,IAAI;AAAA,YACpB,oBAAoB,IAAI;AAAA,UAC1B;AAAA,QACF,CAAC;AAGD,YAAI,IAAI,QAAQ,QAAW;AACzB,gBAAM,UAAU,MAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE,EAAE;AACjF,cAAI,CAAC,WAAW,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,QAAW;AAEjE,kBAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE,IAAI;AAAA,cACjE,KAAK,IAAI;AAAA,cACT,KAAK;AAAA,YACP,CAAC;AAAA,UACH,WAAW,IAAI,UAAU,EAAE,OAAO,QAAQ,GAAG,KAAK,IAAI,SAAS;AAE7D,iBAAK,QAAQ,IAAI;AAAA,cACf,6BAA6B,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE,OAAO,OAAO,QAAQ,GAAG,CAAC,aAAQ,OAAO,IAAI,GAAG,CAAC;AAAA,YAC3G;AACA,kBAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE,IAAI;AAAA,cACjE,KAAK,IAAI;AAAA,cACT,KAAK;AAAA,YACP,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,KAAK,wBAAwB,QAAQ,cAAc;AAGzD,UAAM,cAAc,UAAU,OAAO,OAAK,EAAE,GAAG,WAAW,WAAW,CAAC;AACtE,QAAI,YAAY,SAAS,GAAG;AAC1B,YAAM,KAAK,oBAAoB,MAAM;AAAA,IACvC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,oBAAoB,QAAoC;AAC5D,UAAM,SAAS,KAAK,aAAa,MAAM;AAEvC,UAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,aAAa;AAAA,MACzD,MAAM;AAAA,MACN,QAAQ,EAAE,MAAM,eAAe;AAAA,MAC/B,QAAQ,CAAC;AAAA,IACX,CAAC;AAMD,UAAM,eAAW,2CAAoB,MAAM;AAC3C,UAAM,YACJ,MAAM,QAAQ,OAAO,cAAc,KAAK,OAAO,eAAe,SAAS,IACnE,KAAK,IAAI,GAAG,OAAO,cAAc,IAAI,IACrC;AACN,UAAM,eAAe,KAAK,IAAI,UAAU,SAAS;AACjD,WAAO,eAAe;AAGtB,UAAM,eACJ,OAAO,cAAc,MAAM,QAAQ,OAAO,cAAc,KAAK,OAAO,eAAe,SAAS,IACxF,OAAO,eAAe,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,IAClD,MAAM,KAAK,EAAE,QAAQ,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC;AACtD,UAAM,gBAAgB,aAAa;AAEnC,UAAM,KAAK,YAAY,GAAG,MAAM,mBAAmB,iBAAiB,UAAU,SAAS,KAAK;AAC5F,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,mBAAmB;AAAA,MAC3D,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAGD,UAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,yBAAyB;AAAA,MACrE,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,QACL,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,yBAAyB;AAAA,MACrE,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,QACL,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AAKD,UAAM,gBAAgB,OAAO,eAAe;AAC5C,UAAM,gBACJ,OAAO,cAAc,MAAM,QAAQ,OAAO,cAAc,KAAK,OAAO,eAAe,SAAS,IACxF,OAAO,eAAe,KAAK,GAAG,IAC9B;AACN,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,yBAAyB;AAAA,MACjE,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AACD,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,yBAAyB;AAAA,MACjE,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAED,eAAW,KAAK,cAAc;AAC5B,YAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,aAAa,CAAC,IAAI;AAAA,QAC9D,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,WAAW,CAAC,GAAG;AAAA,QAC/B,QAAQ,CAAC;AAAA,MACX,CAAC;AAED,YAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,aAAa,CAAC,UAAU;AAAA,QACpE,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,QACA,QAAQ,CAAC;AAAA,MACX,CAAC;AAED,YAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,aAAa,CAAC,eAAe;AAAA,QACzE,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,KAAK;AAAA,UACL,KAAK;AAAA,UACL,MAAM;AAAA,QACR;AAAA,QACA,QAAQ,CAAC;AAAA,MACX,CAAC;AAAA,IACH;AAGA,UAAM,KAAK,QAAQ,kBAAkB,GAAG,MAAM,qBAAqB;AAAA,MACjE,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AAGD,UAAM,KAAK,sBAAsB,QAAQ,YAAY;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,sBAAsB,QAAgB,cAAuC;AACzF,UAAM,QAAQ,IAAI,IAAI,YAAY;AAClC,UAAM,YAAY,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AACrD,UAAM,WAAW,MAAM,KAAK,QAAQ,mBAAmB,UAAU,WAAW;AAAA,MAC1E,UAAU;AAAA,MACV,QAAQ,GAAG,SAAS;AAAA,IACtB,CAAC;AAED,QAAI,EAAC,qCAAU,OAAM;AACnB;AAAA,IACF;AAEA,eAAW,OAAO,SAAS,MAAM;AAC/B,YAAM,UAAU,IAAI,GAAG,QAAQ,GAAG,KAAK,QAAQ,SAAS,KAAK,EAAE;AAC/D,YAAM,UAAU,QAAQ,QAAQ,GAAG,MAAM,cAAc,EAAE;AACzD,YAAM,SAAS,SAAS,SAAS,EAAE;AACnC,UAAI,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,IAAI,MAAM,GAAG;AACxC,aAAK,QAAQ,IAAI,MAAM,4BAA4B,OAAO,EAAE;AAC5D,cAAM,KAAK,QAAQ,eAAe,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,kBAAkB,QAAqB,OAA4C;AACvF,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,SAA6B,CAAC;AAEpC,UAAM,MAAM,CAAC,IAAY,QAAmC;AAC1D,aAAO,KAAK,KAAK,QAAQ,cAAc,IAAI,EAAE,KAAK,KAAK,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS,CAAC;AAAA,IACvF;AAEA,QAAI,MAAM,WAAW,QAAW;AAC9B,UAAI,GAAG,MAAM,gBAAgB,MAAM,MAAM;AAAA,IAC3C;AACA,QAAI,MAAM,UAAU,QAAW;AAC7B,UAAI,GAAG,MAAM,kBAAkB,MAAM,KAAK;AAAA,IAC5C;AACA,QAAI,MAAM,eAAe,QAAW;AAClC,UAAI,GAAG,MAAM,uBAAuB,MAAM,UAAU;AAAA,IACtD;AACA,QAAI,MAAM,aAAa,QAAW;AAChC,UAAI,GAAG,MAAM,qBAAqB,MAAM,QAAQ;AAAA,IAClD;AACA,QAAI,MAAM,qBAAqB,QAAW;AACxC,UAAI,GAAG,MAAM,6BAA6B,MAAM,gBAAgB;AAAA,IAClE;AACA,QAAI,MAAM,UAAU,QAAW;AAC7B,UAAI,GAAG,MAAM,kBAAkB,MAAM,KAAK;AAAA,IAC5C;AAEA,UAAM,QAAQ,IAAI,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,wBAAwB,QAAgC;AAC5D,UAAM,KAAK,QAAQ,kBAAkB,UAAU;AAAA,MAC7C,MAAM;AAAA,MACN,QAAQ,EAAE,MAAM,SAAS;AAAA,MACzB,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,QAAQ,kBAAkB,eAAe;AAAA,MAClD,MAAM;AAAA,MACN,QAAQ,EAAE,MAAM,gBAAgB;AAAA,MAChC,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,YAAY,sBAAsB,gBAAgB,WAAW,uBAAuB,KAAK;AACpG,UAAM,KAAK,QAAQ,cAAc,sBAAsB;AAAA,MACrD,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,mBAAmB,QAAgC;AACvD,UAAM,KAAK,QAAQ,cAAc,sBAAsB,EAAE,KAAK,QAAQ,KAAK,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAC1G;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,8BAA8B,OAAoB,eAA6C;AACnG,UAAM,SAAS,KAAK,aAAa,KAAK;AACtC,UAAM,UAAU,GAAG,MAAM;AAEzB,UAAM,cAAc,cACjB,OAAO,OAAK,CAAC,EAAE,MAAM,MAAM,EAC3B,IAAI,OAAK;AACR,YAAM,cAAU,gCAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE;AACtD,aAAO,SAAS,GAAG,EAAE,GAAG,IAAI,OAAO,EAAE;AAAA,IACvC,CAAC;AAEH,QAAI,YAAY,WAAW,GAAG;AAI5B,YAAM,KAAK,gBAAgB,OAAO;AAAA,IACpC,OAAO;AACL,YAAM,KAAK,YAAY,SAAS,uBAAuB,UAAU,QAAQ,KAAK;AAC9E,YAAM,KAAK,QAAQ,cAAc,SAAS;AAAA,QACxC,KAAK,YAAY,KAAK,IAAI;AAAA,QAC1B,KAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,eAAe,gBAAkD;AACrE,UAAM,kBAAkB,IAAI,IAAI,eAAe,IAAI,OAAK,KAAK,aAAa,CAAC,CAAC,CAAC;AAC7E,UAAM,UAAoB,CAAC;AAG3B,eAAW,UAAU,CAAC,WAAW,QAAQ,GAAG;AAC1C,YAAM,kBAAkB,MAAM,KAAK,QAAQ,mBAAmB,UAAU,UAAU;AAAA,QAChF,UAAU,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AAAA,QAC7C,QAAQ,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AAAA,MAC7C,CAAC;AAED,UAAI,EAAC,mDAAiB,OAAM;AAC1B;AAAA,MACF;AAEA,iBAAW,OAAO,gBAAgB,MAAM;AACtC,cAAM,UAAU,IAAI,GAAG,QAAQ,GAAG,KAAK,QAAQ,SAAS,KAAK,EAAE;AAC/D,YAAI,CAAC,gBAAgB,IAAI,OAAO,GAAG;AACjC,eAAK,QAAQ,IAAI,MAAM,0BAA0B,OAAO,EAAE;AAI1D,gBAAM,YAAY,MAAM,KAAK,QAC1B,mBAAmB,UAAU,SAAS;AAAA,YACrC,UAAU,GAAG,IAAI,EAAE;AAAA,YACnB,QAAQ,GAAG,IAAI,EAAE;AAAA,UACnB,CAAC,EACA,MAAM,MAAM,MAAS;AACxB,cAAI,uCAAW,MAAM;AACnB,uBAAW,YAAY,UAAU,MAAM;AACrC,oBAAM,eAAe,SAAS,GAAG,QAAQ,GAAG,KAAK,QAAQ,SAAS,KAAK,EAAE;AACzE,oBAAM,KAAK,QAAQ,cAAc,YAAY,EAAE,MAAM,MAAM,MAAS;AAAA,YACtE;AAAA,UACF;AACA,gBAAM,KAAK,QAAQ,eAAe,SAAS,EAAE,WAAW,KAAK,CAAC;AAC9D,eAAK,aAAa,OAAO;AACzB,kBAAQ,KAAK,OAAO;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,wBAAwB,QAAgB,WAA6C;AAx3BrG;AA03BI,UAAM,oBAAoB,oBAAI,IAAyB;AACvD,eAAW,OAAO,WAAW;AAC3B,YAAM,WAAU,SAAI,YAAJ,YAAe;AAC/B,UAAI,CAAC,kBAAkB,IAAI,OAAO,GAAG;AACnC,0BAAkB,IAAI,SAAS,oBAAI,IAAI,CAAC;AAAA,MAC1C;AACA,wBAAkB,IAAI,OAAO,EAAG,IAAI,IAAI,EAAE;AAAA,IAC5C;AAEA,UAAM,eAAe,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AACxD,UAAM,WAAW,MAAM,KAAK,QAAQ,mBAAmB,UAAU,SAAS;AAAA,MACxE,UAAU;AAAA,MACV,QAAQ,GAAG,YAAY;AAAA,IACzB,CAAC;AACD,QAAI,EAAC,qCAAU,OAAM;AACnB;AAAA,IACF;AAEA,UAAM,mBAAmB,oBAAI,IAA+C;AAC5E,eAAW,OAAO,SAAS,MAAM;AAC/B,YAAM,OAAO,IAAI,GAAG,QAAQ,cAAc,EAAE;AAC5C,YAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,UAAI,SAAS,GAAG;AACd;AAAA,MACF;AACA,YAAM,UAAU,KAAK,MAAM,GAAG,MAAM;AACpC,YAAM,UAAU,KAAK,MAAM,SAAS,CAAC;AACrC,UAAI,CAAC,iBAAiB,SAAS,OAAO,GAAG;AACvC;AAAA,MACF;AACA,YAAM,UAAS,sBAAiB,IAAI,OAAO,MAA5B,YAAiC,EAAE,MAAM,GAAG,SAAS,EAAE;AACtE,aAAO;AACP,YAAM,YAAW,uBAAkB,IAAI,OAAO,MAA7B,YAAkC,oBAAI,IAAY;AACnE,UAAI,CAAC,SAAS,IAAI,OAAO,GAAG;AAC1B,cAAM,UAAU,IAAI,GAAG,QAAQ,GAAG,KAAK,QAAQ,SAAS,KAAK,EAAE;AAC/D,aAAK,QAAQ,IAAI,MAAM,yBAAyB,OAAO,EAAE;AACzD,cAAM,KAAK,QAAQ,eAAe,OAAO;AACzC,cAAM,KAAK,QAAQ,cAAc,OAAO,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AACxD,eAAO;AAAA,MACT;AACA,uBAAiB,IAAI,SAAS,MAAM;AAAA,IACtC;AAGA,eAAW,CAAC,SAAS,MAAM,KAAK,kBAAkB;AAChD,UAAI,OAAO,UAAU,KAAK,OAAO,YAAY,OAAO,MAAM;AACxD,aAAK,QAAQ,IAAI,MAAM,2BAA2B,MAAM,IAAI,OAAO,EAAE;AACrE,cAAM,KAAK,QAAQ,eAAe,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE,MAAM,MAAM,MAAS;AAAA,MACjF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,QAA6B;AACxC,UAAM,cAAU,gCAAkB,OAAO,QAAQ,EAAE,MAAM,EAAE;AAC3D,UAAM,SAAS,OAAO,QAAQ,cAAc,WAAW;AACvD,WAAO,GAAG,MAAM,IAAI,SAAS,GAAG,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,aAAa,QAAsB;AACzC,eAAW,OAAO,KAAK,UAAU,KAAK,GAAG;AACvC,UAAI,KAAK,UAAU,IAAI,GAAG,MAAM,QAAQ;AACtC,aAAK,UAAU,OAAO,GAAG;AAAA,MAC3B;AAAA,IACF;AACA,UAAM,cAAc,GAAG,MAAM;AAC7B,eAAW,OAAO,KAAK,gBAAgB,KAAK,GAAG;AAC7C,UAAI,IAAI,WAAW,WAAW,GAAG;AAC/B,aAAK,gBAAgB,OAAO,GAAG;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,UAAU,QAA6B;AAG7C,WAAO,GAAG,OAAO,GAAG,QAAI,gCAAkB,OAAO,QAAQ,CAAC;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,YACZ,IACA,MACA,MACA,MACA,OACA,MACe;AACf,UAAM,SAAwC;AAAA,MAC5C;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AACA,QAAI,MAAM;AACR,aAAO,OAAO;AAAA,IAChB;AACA,UAAM,KAAK,QAAQ,kBAAkB,IAAI;AAAA,MACvC,MAAM;AAAA,MACN;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type * as utils from \"@iobroker/adapter-core\";\nimport type { StateDefinition } from \"./capability-mapper\";\nimport { GROUP_ICON, iconForGoveeType, shortenGoveeType } from \"./device-icons\";\nimport { resolveSegmentCount } from \"./device-manager\";\nimport { normalizeDeviceId, type DeviceState, type GoveeDevice } from \"./types\";\n\n/**\n * Sanitize a string for ioBroker object ID\n *\n * @param str Input string to sanitize\n */\nfunction sanitize(str: string): string {\n return str.replace(/[^a-zA-Z0-9_-]/g, \"_\").toLowerCase();\n}\n\n/**\n * Channels whose state-set is fully described by capability-driven stateDefs.\n * Only these get the stale-state cleanup pass \u2014 `info` is intentionally absent\n * because it mixes capability-driven states (diagnostics_export/result) with\n * adapter-managed ones (online, model, serial, ip, members) that come from\n * ensureState instead of stateDefs. Cleaning `info` by stateDef-set would\n * delete the adapter-managed ones.\n */\nconst MANAGED_CHANNELS = [\"control\", \"scenes\", \"music\", \"snapshots\", \"sensor\", \"events\"];\n/**\n * Display names used when the channel object is (re-)created. `info` is\n * listed here even though it's not in MANAGED_CHANNELS \u2014 capability-mapper\n * emits states with `channel: \"info\"`, and without this entry the create\n * path would overwrite the original \"Device Information\" name with the\n * literal \"info\".\n */\nconst CHANNEL_NAMES: Record<string, string> = {\n control: \"Controls\",\n scenes: \"Scenes\",\n music: \"Music\",\n snapshots: \"Snapshots\",\n sensor: \"Sensor Data\",\n events: \"Events\",\n info: \"Device Information\",\n diag: \"Diagnostics\",\n};\n\n/**\n * Synthetic capabilities written by the App-API poll and OpenAPI-MQTT\n * handler arrive as `(stateId, value)` pairs without a channel hint \u2014\n * `mapCloudStateValue` returns only what the Govee response carries.\n *\n * For lights the LAN/Cloud-state pipeline pre-populates `stateChannelMap`\n * via `createDeviceStates`, so `resolveStatePath` finds the channel.\n * For thermometer/appliance state IDs that are *only* delivered via the\n * App-API path (battery, temperature, humidity, CO\u2082, lackWater, \u2026) the map\n * is empty and the lookup would default to \"control\" \u2014 visibly wrong:\n * `info: control.battery has no existing object`.\n *\n * This routing table assigns those IDs to their semantic channel without\n * needing a separate `createDeviceStates` pass for sensor-only devices.\n * Keep IDs lowercase; resolveStatePath calls this on the raw stateId.\n */\n// Beide Lookup-Sets enthalten zwei Schreibweisen pro State-ID:\n// - \u201Eraw\"-Form (z.B. `temperature`) f\u00FCr instances die direkt so hei\u00DFen\n// - sanitizeId-Output (z.B. `sensor_temperature`) f\u00FCr camelCase-instances\n// die durch sanitizeId zu snake_case konvertiert wurden\n// `sanitizeId` in capability-mapper konvertiert camelCase \u2192 snake_case, also\n// werden \u201EsensorTemperature\" zu \u201Esensor_temperature\" und \u201ElackWaterEvent\"\n// zu \u201Elack_water_event\". Ohne diese Aliase fielen sanitize-Varianten auf den\n// safe-default \u201Econtrol\" zur\u00FCck und die States w\u00E4ren nicht erreichbar.\nconst SENSOR_STATE_IDS = new Set([\n // raw forms\n \"temperature\",\n \"humidity\",\n \"battery\",\n \"co2\",\n \"carbondioxide\",\n \"online\",\n // sanitizeId(instance) forms\n \"sensor_temperature\",\n \"sensor_humidity\",\n \"sensor_battery\",\n]);\nconst EVENT_STATE_IDS = new Set([\n // raw forms (no underscore separator)\n \"lackwater\",\n \"lackwaterevent\",\n \"icefull\",\n \"icefullevent\",\n \"bodyappeared\",\n \"dirtdetected\",\n // sanitizeId(instance) forms (camelCase \u2192 snake_case)\n \"lack_water\",\n \"lack_water_event\",\n \"ice_full\",\n \"ice_full_event\",\n \"body_appeared\",\n \"dirt_detected\",\n]);\n\n/**\n * Best-effort channel routing for state IDs that don't have a\n * stateChannelMap entry yet (e.g. App-API synthetic caps before the device\n * has gone through createDeviceStates). Empty input falls back to the safe\n * default \"control\".\n *\n * @param stateId The raw state ID (e.g. \"battery\", \"lackWater\")\n */\nfunction inferChannelFromStateId(stateId: string): string {\n const normalised = stateId.toLowerCase();\n if (SENSOR_STATE_IDS.has(normalised)) {\n return \"sensor\";\n }\n if (EVENT_STATE_IDS.has(normalised)) {\n return \"events\";\n }\n return \"control\";\n}\n\n/** Per-stateId metadata for synthetic states (App-API/OpenAPI-MQTT pipe). */\ninterface SyntheticStateMeta {\n type: \"boolean\" | \"number\";\n role: string;\n unit?: string;\n name: string;\n}\nconst SYNTHETIC_STATE_META: Record<string, SyntheticStateMeta> = {\n temperature: {\n type: \"number\",\n role: \"value.temperature\",\n unit: \"\u00B0C\",\n name: \"Temperature\",\n },\n humidity: {\n type: \"number\",\n role: \"value.humidity\",\n unit: \"%\",\n name: \"Humidity\",\n },\n battery: {\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n name: \"Battery\",\n },\n co2: { type: \"number\", role: \"value.co2\", unit: \"ppm\", name: \"CO\u2082\" },\n carbondioxide: {\n type: \"number\",\n role: \"value.co2\",\n unit: \"ppm\",\n name: \"CO\u2082\",\n },\n online: { type: \"boolean\", role: \"indicator.connected\", name: \"Online\" },\n lackwater: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n lackwaterevent: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n icefull: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n icefullevent: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n bodyappeared: { type: \"boolean\", role: \"indicator\", name: \"Body Detected\" },\n dirtdetected: { type: \"boolean\", role: \"indicator\", name: \"Dirt Detected\" },\n // sanitizeId(instance) Aliases \u2014 gleiche Meta wie raw-Form, decoupled\n // damit der Adapter beim ersten Sensor-State-Write den richtigen Channel\n // (sensor/ bzw. events/) anlegt.\n sensor_temperature: {\n type: \"number\",\n role: \"value.temperature\",\n unit: \"\u00B0C\",\n name: \"Temperature\",\n },\n sensor_humidity: {\n type: \"number\",\n role: \"value.humidity\",\n unit: \"%\",\n name: \"Humidity\",\n },\n sensor_battery: {\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n name: \"Battery\",\n },\n lack_water: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n lack_water_event: {\n type: \"boolean\",\n role: \"indicator.alarm\",\n name: \"Lack of Water\",\n },\n ice_full: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n ice_full_event: { type: \"boolean\", role: \"indicator\", name: \"Ice Bucket Full\" },\n body_appeared: { type: \"boolean\", role: \"indicator\", name: \"Body Detected\" },\n dirt_detected: { type: \"boolean\", role: \"indicator\", name: \"Dirt Detected\" },\n};\n\n/** Manages ioBroker state creation and updates for Govee devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\n /** Maps deviceKey (sku_deviceId) \u2192 current object prefix */\n private readonly prefixMap = new Map<string, string>();\n /** Maps \"prefix.stateId\" \u2192 channel name (populated during createDeviceStates) */\n private readonly stateChannelMap = new Map<string, string>();\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: utils.AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Idempotent state-delete: pr\u00FCft erst ob das Object existiert. Wenn nicht,\n * no-op (verhindert \u201Ehas no existing object\"-WARN den `delStateAsync`\n * sonst intern triggert wenn das Object weg ist).\n *\n * Pattern: Caller will ein State l\u00F6schen (z.B. weil der Zustand \u201Ecleaned\"\n * geworden ist), aber wei\u00DF nicht ob das Object jemals da war. delObject\n * + delState ist nur dann sicher wenn das Object EXISTIERT.\n *\n * @param id Voller State-Pfad (`devices.X.info.Y`)\n */\n private async safeDeleteState(id: string): Promise<void> {\n const obj = await this.adapter.getObjectAsync(id).catch(() => null);\n if (!obj) {\n return;\n }\n await this.adapter.delStateAsync(id).catch(() => undefined);\n await this.adapter.delObjectAsync(id).catch(() => undefined);\n }\n\n /**\n * Push the device's trust tier (verified/reported/seed/unknown) into\n * the user-visible `diag.tier` state. Called after every device-state\n * refresh so the value tracks any registry change between adapter\n * restarts (e.g. seed \u2192 verified once a tester confirms). No-op for\n * groups (BaseGroup has no per-device tier).\n *\n * @param device Govee device\n * @param tier Canonical tier label\n */\n async updateDeviceTier(device: GoveeDevice, tier: string): Promise<void> {\n if (device.sku === \"BaseGroup\") {\n return;\n }\n const prefix = this.devicePrefix(device);\n await this.adapter.setStateAsync(`${prefix}.diag.tier`, { val: tier, ack: true }).catch(() => undefined);\n }\n\n /**\n * Migrate v2.1.0 layout (`info.diagnostics_*`) to v2.1.1 layout\n * (`diag.*`). Deletes the three old objects + states; the new ones get\n * created by the regular `createDeviceStates` pass. Idempotent \u2014 calling\n * twice is a no-op once the old objects are gone.\n *\n * @param device Govee device\n */\n async migrateLegacyDiagnostics(device: GoveeDevice): Promise<void> {\n if (device.sku === \"BaseGroup\") {\n return;\n }\n const prefix = this.devicePrefix(device);\n for (const stale of [\"diagnostics_export\", \"diagnostics_result\", \"diagnostics_tier\"]) {\n await this.safeDeleteState(`${prefix}.info.${stale}`);\n this.stateChannelMap.delete(`${prefix}.${stale}`);\n }\n }\n\n /**\n * Resolve full state path for a given device prefix and state ID.\n * Routes the state to the correct channel (control, scenes, music, snapshots).\n *\n * @param prefix Device object ID prefix\n * @param stateId State ID suffix\n */\n resolveStatePath(prefix: string, stateId: string): string {\n const channel = this.stateChannelMap.get(`${prefix}.${stateId}`) ?? inferChannelFromStateId(stateId);\n return `${prefix}.${channel}.${stateId}`;\n }\n\n /**\n * Lazily create the channel + state object for synthetic state IDs the\n * App-API poll and OpenAPI-MQTT pipeline write. Cloud-capability defs\n * for sensor SKUs (e.g. H5179) are often empty in OpenAPI v2, so the\n * usual `createDeviceStates` pass would not declare battery / temperature\n * / events.* \u2014 without this helper the first write logs\n * `info: <id> has no existing object`.\n *\n * Idempotent: skips when the meta table doesn't know the stateId, and\n * `setObjectNotExistsAsync` is itself a no-op for existing objects.\n *\n * @param prefix Device prefix (e.g. \"devices.h5179_aabb\")\n * @param stateId State ID without channel (e.g. \"battery\")\n */\n async ensureSyntheticStateObject(prefix: string, stateId: string): Promise<void> {\n const meta = SYNTHETIC_STATE_META[stateId.toLowerCase()];\n if (!meta) {\n return;\n }\n const channel = inferChannelFromStateId(stateId);\n // Channel object first \u2014 sensors land in the new sensor/ subtree, events\n // in events/. Without an extendObject the channel parent stays missing\n // and Admin shows the state directly under the device root.\n await this.adapter\n .extendObjectAsync(`${prefix}.${channel}`, {\n type: \"channel\",\n common: { name: CHANNEL_NAMES[channel] ?? channel },\n native: {},\n })\n .catch(() => undefined);\n // extendObjectAsync (idempotent + repariert partial-formed Objects).\n // setObjectNotExistsAsync w\u00E4re no-op auf existing \u2014 und Objects aus\n // alten Layouts (v2.0.x\u2192v2.1.x-Migration) k\u00F6nnen unvollst\u00E4ndige\n // common-Felder haben, die dann beim ersten setStateAsync warnen.\n await this.adapter\n .extendObjectAsync(`${prefix}.${channel}.${stateId}`, {\n type: \"state\",\n common: {\n name: meta.name,\n type: meta.type,\n role: meta.role,\n read: true,\n write: false,\n ...(meta.unit !== undefined ? { unit: meta.unit } : {}),\n def: meta.type === \"boolean\" ? false : 0,\n },\n native: {},\n })\n .catch(() => undefined);\n this.stateChannelMap.set(`${prefix}.${stateId}`, channel);\n }\n\n /**\n * Create device object and all states from capability definitions.\n *\n * @param device Govee device\n * @param stateDefs State definitions from capability mapper\n */\n async createDeviceStates(device: GoveeDevice, stateDefs: StateDefinition[]): Promise<void> {\n const key = this.deviceKey(device);\n const newPrefix = this.devicePrefix(device);\n const oldPrefix = this.prefixMap.get(key);\n\n // Migrate if prefix changed (e.g., old naming scheme)\n if (oldPrefix && oldPrefix !== newPrefix) {\n this.adapter.log.debug(`Migrating device ${device.sku}: ${oldPrefix} \u2192 ${newPrefix}`);\n await this.adapter.delObjectAsync(oldPrefix, { recursive: true });\n // Drop stale channel-map entries under the old prefix so they don't\n // shadow resolveStatePath lookups after the rename.\n const oldChannelKey = `${oldPrefix}.`;\n for (const mapKey of this.stateChannelMap.keys()) {\n if (mapKey.startsWith(oldChannelKey)) {\n this.stateChannelMap.delete(mapKey);\n }\n }\n }\n this.prefixMap.set(key, newPrefix);\n\n const prefix = newPrefix;\n const isGroup = device.sku === \"BaseGroup\";\n\n // Device object with online status indicator + type-aware icon.\n // Groups use the general groups.info.online state instead of per-group online.\n const onlineId = isGroup\n ? `${this.adapter.namespace}.groups.info.online`\n : `${this.adapter.namespace}.${prefix}.info.online`;\n const icon = isGroup ? GROUP_ICON : iconForGoveeType(device.type);\n await this.adapter.extendObjectAsync(prefix, {\n type: \"device\",\n common: {\n name: device.name,\n icon,\n statusStates: { onlineId },\n } as ioBroker.DeviceCommon,\n native: {\n sku: device.sku,\n deviceId: device.deviceId,\n },\n });\n\n // Info channel \u2014 groups only get name (no individual online)\n await this.adapter.extendObjectAsync(`${prefix}.info`, {\n type: \"channel\",\n common: { name: \"Device Information\" },\n native: {},\n });\n\n await this.ensureState(`${prefix}.info.name`, \"Name\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.name`, {\n val: device.name,\n ack: true,\n });\n\n if (!isGroup) {\n await this.ensureState(`${prefix}.info.online`, \"Online\", \"boolean\", \"indicator.reachable\", false);\n await this.adapter.setStateAsync(`${prefix}.info.online`, {\n val: device.state.online ?? false,\n ack: true,\n });\n await this.ensureState(`${prefix}.info.model`, \"Model\", \"string\", \"text\", false);\n await this.ensureState(`${prefix}.info.serial`, \"Serial Number\", \"string\", \"text\", false);\n await this.ensureState(`${prefix}.info.ip`, \"IP Address\", \"string\", \"info.ip\", false);\n // Device-type marker \u2014 short label like \"light\", \"thermometer\",\n // \"heater\" (Govee API type without the \"devices.types.\" prefix).\n // Lets scripts filter `*.info.type === \"light\"` without parsing.\n await this.ensureState(`${prefix}.info.type`, \"Device Type\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.model`, {\n val: device.sku,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.serial`, {\n val: device.deviceId,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.ip`, {\n val: device.lanIp ?? \"\",\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.type`, {\n val: shortenGoveeType(device.type),\n ack: true,\n });\n } else {\n // Group members: comma-separated device prefix IDs\n const memberIds = (device.groupMembers ?? [])\n .map(m => {\n const shortId = normalizeDeviceId(m.deviceId).slice(-4);\n return sanitize(`${m.sku}_${shortId}`);\n })\n .join(\", \");\n await this.ensureState(`${prefix}.info.members`, \"Members\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(`${prefix}.info.members`, {\n val: memberIds,\n ack: true,\n });\n\n // Legacy cleanup \u2014 groups never carry device-level info states or\n // diagnostics, but older installs had them. Drop any leftovers so the\n // tree reflects the current layout.\n for (const staleId of [\n \"online\",\n \"model\",\n \"serial\",\n \"ip\",\n \"diagnostics_export\",\n \"diagnostics_result\",\n \"diagnostics_tier\",\n ]) {\n await this.safeDeleteState(`${prefix}.info.${staleId}`);\n }\n // Groups never had a `diag` channel \u2014 drop any leftover from migrated installs.\n await this.adapter.delObjectAsync(`${prefix}.diag`, { recursive: true }).catch(() => {});\n }\n\n // Group state defs by channel (control, scenes, music, snapshots)\n const nonSegmentDefs = stateDefs.filter(d => !d.id.startsWith(\"_segment_\"));\n const channelGroups = new Map<string, StateDefinition[]>();\n for (const def of nonSegmentDefs) {\n const channel = def.channel ?? \"control\";\n this.stateChannelMap.set(`${prefix}.${def.id}`, channel);\n if (!channelGroups.has(channel)) {\n channelGroups.set(channel, []);\n }\n channelGroups.get(channel)!.push(def);\n }\n\n this.adapter.log.debug(\n `createDeviceStates ${device.sku}: ${nonSegmentDefs.length} states in ${channelGroups.size} channel(s)`,\n );\n\n // Create states in each channel\n for (const [channel, defs] of channelGroups) {\n await this.adapter.extendObjectAsync(`${prefix}.${channel}`, {\n type: \"channel\",\n common: { name: CHANNEL_NAMES[channel] ?? channel },\n native: {},\n });\n\n for (const def of defs) {\n const common: Partial<ioBroker.StateCommon> = {\n name: def.name,\n type: def.type,\n role: def.role,\n read: true,\n write: def.write,\n };\n\n if (def.unit) {\n common.unit = def.unit;\n }\n if (def.min !== undefined) {\n common.min = def.min;\n }\n if (def.max !== undefined) {\n common.max = def.max;\n }\n if (def.states) {\n common.states = def.states;\n }\n if (def.def !== undefined) {\n common.def = def.def;\n }\n if (def.desc) {\n common.desc = def.desc;\n }\n\n await this.adapter.extendObjectAsync(`${prefix}.${channel}.${def.id}`, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {\n capabilityType: def.capabilityType,\n capabilityInstance: def.capabilityInstance,\n },\n });\n\n // Initialize or validate state value\n if (def.def !== undefined) {\n const current = await this.adapter.getStateAsync(`${prefix}.${channel}.${def.id}`);\n if (!current || current.val === null || current.val === undefined) {\n // Set default value for new states\n await this.adapter.setStateAsync(`${prefix}.${channel}.${def.id}`, {\n val: def.def,\n ack: true,\n });\n } else if (def.states && !(String(current.val) in def.states)) {\n // Reset dropdown to default if current value is no longer valid\n this.adapter.log.debug(\n `Resetting stale dropdown: ${prefix}.${channel}.${def.id} = \"${String(current.val)}\" \u2192 \"${String(def.def)}\"`,\n );\n await this.adapter.setStateAsync(`${prefix}.${channel}.${def.id}`, {\n val: def.def,\n ack: true,\n });\n }\n }\n }\n }\n\n // Remove stale states across all managed channels\n await this.cleanupAllChannelStates(prefix, nonSegmentDefs);\n\n // Check if device has segment capabilities\n const segmentDefs = stateDefs.filter(d => d.id.startsWith(\"_segment_\"));\n if (segmentDefs.length > 0) {\n await this.createSegmentStates(device);\n }\n }\n\n /**\n * Create segment channel with per-segment color + brightness states.\n *\n * @param device Govee device\n */\n async createSegmentStates(device: GoveeDevice): Promise<void> {\n const prefix = this.devicePrefix(device);\n\n await this.adapter.extendObjectAsync(`${prefix}.segments`, {\n type: \"channel\",\n common: { name: \"LED Segments\" },\n native: {},\n });\n\n // Resolve the authoritative count: cache/MQTT-learned wins over Cloud\n // capabilities. A manual list can only grow the count (never shrink it)\n // so users editing manual_list can reveal hidden indices without losing\n // the already-learned total.\n const resolved = resolveSegmentCount(device);\n const manualMax =\n Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? Math.max(...device.manualSegments) + 1\n : 0;\n const segmentCount = Math.max(resolved, manualMax);\n device.segmentCount = segmentCount;\n\n // Effective segment list \u2014 honor manual override if active (cut-strip support)\n const validIndices =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.slice().sort((a, b) => a - b)\n : Array.from({ length: segmentCount }, (_, i) => i);\n const reportedCount = validIndices.length;\n\n await this.ensureState(`${prefix}.segments.count`, \"Segment Count\", \"number\", \"value\", false);\n await this.adapter.setStateAsync(`${prefix}.segments.count`, {\n val: reportedCount,\n ack: true,\n });\n\n // Manual-mode toggle and list \u2014 user-writable for cut-strip overrides\n await this.adapter.extendObjectAsync(`${prefix}.segments.manual_mode`, {\n type: \"state\",\n common: {\n name: \"Manual Segments Active\",\n type: \"boolean\",\n role: \"switch\",\n read: true,\n write: true,\n def: false,\n desc: \"Enable manual segment list (e.g. for cut LED strips with fewer physical segments than reported)\",\n } as ioBroker.StateCommon,\n native: {},\n });\n await this.adapter.extendObjectAsync(`${prefix}.segments.manual_list`, {\n type: \"state\",\n common: {\n name: \"Manual Segment List\",\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n def: \"\",\n desc: 'Comma-separated indices + ranges, e.g. \"0-9\" or \"0-8,10-14\" (only used when manual_mode=true)',\n } as ioBroker.StateCommon,\n native: {},\n });\n\n // Sync manual_mode / manual_list states back from the runtime device\n // (restored from cache on startup, or updated by the wizard). Using\n // ack=true keeps this out of the user-change handler path.\n const manualModeVal = device.manualMode === true;\n const manualListVal =\n device.manualMode && Array.isArray(device.manualSegments) && device.manualSegments.length > 0\n ? device.manualSegments.join(\",\")\n : \"\";\n await this.adapter.setStateAsync(`${prefix}.segments.manual_mode`, {\n val: manualModeVal,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.segments.manual_list`, {\n val: manualListVal,\n ack: true,\n });\n\n for (const i of validIndices) {\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}`, {\n type: \"channel\",\n common: { name: `Segment ${i}` },\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}.color`, {\n type: \"state\",\n common: {\n name: \"Color\",\n type: \"string\",\n role: \"level.color.rgb\",\n read: true,\n write: true,\n } as ioBroker.StateCommon,\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.segments.${i}.brightness`, {\n type: \"state\",\n common: {\n name: \"Brightness\",\n type: \"number\",\n role: \"level.brightness\",\n read: true,\n write: true,\n min: 0,\n max: 100,\n unit: \"%\",\n } as ioBroker.StateCommon,\n native: {},\n });\n }\n\n // Comfort command state for batch segment control\n await this.adapter.extendObjectAsync(`${prefix}.segments.command`, {\n type: \"state\",\n common: {\n name: \"Batch Segment Command\",\n type: \"string\",\n role: \"text\",\n read: false,\n write: true,\n desc: \"Format: segments:color:brightness \u2014 e.g. 1-5:#ff0000:20, all:#00ff00, 0,3,7::50\",\n } as ioBroker.StateCommon,\n native: {},\n });\n\n // Remove segment channels that aren't in the valid list (supports gaps for manual mode)\n await this.cleanupExcessSegments(prefix, validIndices);\n }\n\n /**\n * Remove segment sub-channels that are not in the valid-indices list.\n * Supports gaps (e.g. manual list \"0-8,10-14\" \u2192 segment 9 channel gets removed).\n *\n * @param prefix Device prefix\n * @param validIndices Valid segment indices (all others will be deleted)\n */\n private async cleanupExcessSegments(prefix: string, validIndices: number[]): Promise<void> {\n const valid = new Set(validIndices);\n const segPrefix = `${this.adapter.namespace}.${prefix}.segments.`;\n const existing = await this.adapter.getObjectViewAsync(\"system\", \"channel\", {\n startkey: segPrefix,\n endkey: `${segPrefix}\\u9999`,\n });\n\n if (!existing?.rows) {\n return;\n }\n\n for (const row of existing.rows) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n const segPart = localId.replace(`${prefix}.segments.`, \"\");\n const segIdx = parseInt(segPart, 10);\n if (!isNaN(segIdx) && !valid.has(segIdx)) {\n this.adapter.log.debug(`Removing excess segment: ${localId}`);\n await this.adapter.delObjectAsync(localId, { recursive: true });\n }\n }\n }\n\n /**\n * Update device state from any source (LAN, MQTT, Cloud).\n *\n * Writes are fire-and-forget and run in parallel \u2014 they're independent,\n * and the \"does this state exist?\" check that used to guard each write\n * was an extra object-read on the hot path (one MQTT push = one update\n * call). createDeviceStates has already run before any update lands,\n * so the states are guaranteed to exist; if one disappears (manual\n * deletion), the setStateAsync will reject and we swallow it.\n *\n * @param device Govee device\n * @param state Partial state update\n */\n async updateDeviceState(device: GoveeDevice, state: Partial<DeviceState>): Promise<void> {\n const prefix = this.devicePrefix(device);\n const writes: Promise<unknown>[] = [];\n\n const set = (id: string, val: ioBroker.StateValue): void => {\n writes.push(this.adapter.setStateAsync(id, { val, ack: true }).catch(() => undefined));\n };\n\n if (state.online !== undefined) {\n set(`${prefix}.info.online`, state.online);\n }\n if (state.power !== undefined) {\n set(`${prefix}.control.power`, state.power);\n }\n if (state.brightness !== undefined) {\n set(`${prefix}.control.brightness`, state.brightness);\n }\n if (state.colorRgb !== undefined) {\n set(`${prefix}.control.colorRgb`, state.colorRgb);\n }\n if (state.colorTemperature !== undefined) {\n set(`${prefix}.control.colorTemperature`, state.colorTemperature);\n }\n if (state.scene !== undefined) {\n set(`${prefix}.control.scene`, state.scene);\n }\n\n await Promise.all(writes);\n }\n\n /**\n * Create the general groups.info.online state (reflects Cloud connection).\n *\n * @param online Initial online value\n */\n async createGroupsOnlineState(online: boolean): Promise<void> {\n await this.adapter.extendObjectAsync(\"groups\", {\n type: \"folder\",\n common: { name: \"Groups\" },\n native: {},\n });\n await this.adapter.extendObjectAsync(\"groups.info\", {\n type: \"channel\",\n common: { name: \"Groups Status\" },\n native: {},\n });\n await this.ensureState(\"groups.info.online\", \"Cloud Online\", \"boolean\", \"indicator.reachable\", false);\n await this.adapter.setStateAsync(\"groups.info.online\", {\n val: online,\n ack: true,\n });\n }\n\n /**\n * Update the general groups online state.\n *\n * @param online Cloud connection status\n */\n async updateGroupsOnline(online: boolean): Promise<void> {\n await this.adapter.setStateAsync(\"groups.info.online\", { val: online, ack: true }).catch(() => undefined);\n }\n\n /**\n * Update info.membersUnreachable for a group.\n *\n * Pflegt den state IMMER (existing) und schreibt eine comma-separated\n * Liste der unreachable members oder einen leeren String wenn alle\n * online sind. Vorher haben wir bei \u201Ealle reachable\" das Object\n * gel\u00F6scht \u2014 das produzierte aber js-controller-WARN \u201EState\n * 'X.membersUnreachable' has no existing object\" alle ~2 Minuten,\n * weil parallele updateGroupReachability-Aufrufe (LAN+MQTT-Status-\n * Updates feuern fast gleichzeitig) eine race condition zwischen\n * setStateAsync (Object existiert) und safeDeleteState (Object weg)\n * triggern k\u00F6nnen. State immer existent zu halten umgeht das komplett.\n *\n * @param group BaseGroup device\n * @param memberDevices Resolved member devices\n */\n async updateGroupMembersUnreachable(group: GoveeDevice, memberDevices: GoveeDevice[]): Promise<void> {\n const prefix = this.devicePrefix(group);\n const stateId = `${prefix}.info.membersUnreachable`;\n\n const unreachable = memberDevices\n .filter(m => !m.state.online)\n .map(m => {\n const shortId = normalizeDeviceId(m.deviceId).slice(-4);\n return sanitize(`${m.sku}_${shortId}`);\n });\n\n await this.ensureState(stateId, \"Unreachable Members\", \"string\", \"text\", false);\n await this.adapter.setStateAsync(stateId, {\n val: unreachable.join(\", \"),\n ack: true,\n });\n }\n\n /**\n * Cleanup stale devices that no longer exist.\n *\n * Returns the prefixes of removed devices so callers (DeviceManager,\n * adapter-level maps) can drop their own entries for the same devices\n * and prevent unbounded map growth across the adapter's lifetime.\n *\n * @param currentDevices Current device list\n * @returns Prefixes of removed devices (e.g. \"devices.h61be_1d6f\")\n */\n async cleanupDevices(currentDevices: GoveeDevice[]): Promise<string[]> {\n const currentPrefixes = new Set(currentDevices.map(d => this.devicePrefix(d)));\n const removed: string[] = [];\n\n // Cleanup both devices/ and groups/ folders\n for (const folder of [\"devices\", \"groups\"]) {\n const existingObjects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.${folder}.`,\n endkey: `${this.adapter.namespace}.${folder}.\\u9999`,\n });\n\n if (!existingObjects?.rows) {\n continue;\n }\n\n for (const row of existingObjects.rows) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (!currentPrefixes.has(localId)) {\n this.adapter.log.debug(`Removing stale device: ${localId}`);\n // Recursive delObject removes the object tree but can leave\n // orphan state values in the state-table \u2014 clean those too so\n // historical values don't survive a device removal.\n const stateRows = await this.adapter\n .getObjectViewAsync(\"system\", \"state\", {\n startkey: `${row.id}.`,\n endkey: `${row.id}.\u9999`,\n })\n .catch(() => undefined);\n if (stateRows?.rows) {\n for (const stateRow of stateRows.rows) {\n const stateLocalId = stateRow.id.replace(`${this.adapter.namespace}.`, \"\");\n await this.adapter.delStateAsync(stateLocalId).catch(() => undefined);\n }\n }\n await this.adapter.delObjectAsync(localId, { recursive: true });\n this.forgetPrefix(localId);\n removed.push(localId);\n }\n }\n }\n return removed;\n }\n\n /**\n * Remove stale states across all managed channels.\n * Also handles migration from old single-control layout.\n *\n * One broad view-query across the whole device prefix replaces the old\n * four-per-device pass \u2014 the channel partition is recovered by parsing\n * the object id, saving three round-trips per device on every refresh.\n *\n * @param prefix Device prefix\n * @param stateDefs Current state definitions (non-segment)\n */\n private async cleanupAllChannelStates(prefix: string, stateDefs: StateDefinition[]): Promise<void> {\n // Build expected state set per channel\n const expectedByChannel = new Map<string, Set<string>>();\n for (const def of stateDefs) {\n const channel = def.channel ?? \"control\";\n if (!expectedByChannel.has(channel)) {\n expectedByChannel.set(channel, new Set());\n }\n expectedByChannel.get(channel)!.add(def.id);\n }\n\n const devicePrefix = `${this.adapter.namespace}.${prefix}.`;\n const existing = await this.adapter.getObjectViewAsync(\"system\", \"state\", {\n startkey: devicePrefix,\n endkey: `${devicePrefix}\\u9999`,\n });\n if (!existing?.rows) {\n return;\n }\n\n const totalsPerChannel = new Map<string, { seen: number; deleted: number }>();\n for (const row of existing.rows) {\n const rest = row.id.replace(devicePrefix, \"\");\n const dotIdx = rest.indexOf(\".\");\n if (dotIdx < 0) {\n continue;\n }\n const channel = rest.slice(0, dotIdx);\n const stateId = rest.slice(dotIdx + 1);\n if (!MANAGED_CHANNELS.includes(channel)) {\n continue;\n }\n const totals = totalsPerChannel.get(channel) ?? { seen: 0, deleted: 0 };\n totals.seen++;\n const validIds = expectedByChannel.get(channel) ?? new Set<string>();\n if (!validIds.has(stateId)) {\n const localId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n this.adapter.log.debug(`Removing stale state: ${localId}`);\n await this.adapter.delObjectAsync(localId);\n await this.adapter.delStateAsync(localId).catch(() => {});\n totals.deleted++;\n }\n totalsPerChannel.set(channel, totals);\n }\n\n // Remove empty channel objects \u2014 no surviving states for this channel\n for (const [channel, totals] of totalsPerChannel) {\n if (totals.deleted > 0 && totals.deleted === totals.seen) {\n this.adapter.log.debug(`Removing empty channel: ${prefix}.${channel}`);\n await this.adapter.delObjectAsync(`${prefix}.${channel}`).catch(() => undefined);\n }\n }\n }\n\n /**\n * Get device object ID prefix \u2014 stable SKU + short device ID.\n * Groups (BaseGroup) go under groups/, devices under devices/.\n * Human-readable name is in common.name, not in the object ID.\n *\n * @param device Govee device\n */\n devicePrefix(device: GoveeDevice): string {\n const shortId = normalizeDeviceId(device.deviceId).slice(-4);\n const folder = device.sku === \"BaseGroup\" ? \"groups\" : \"devices\";\n return `${folder}.${sanitize(`${device.sku}_${shortId}`)}`;\n }\n\n /**\n * Drop prefix + stateChannel entries for a device that was removed.\n * Prevents the maps from growing indefinitely across adapter lifetime.\n *\n * @param prefix Device prefix that was removed\n */\n private forgetPrefix(prefix: string): void {\n for (const key of this.prefixMap.keys()) {\n if (this.prefixMap.get(key) === prefix) {\n this.prefixMap.delete(key);\n }\n }\n const stalePrefix = `${prefix}.`;\n for (const key of this.stateChannelMap.keys()) {\n if (key.startsWith(stalePrefix)) {\n this.stateChannelMap.delete(key);\n }\n }\n }\n\n /**\n * Unique key for internal tracking (not used as object ID).\n *\n * @param device Govee device\n */\n private deviceKey(device: GoveeDevice): string {\n // Use normalizeDeviceId which is defensive against non-string input \u2014\n // cached data on disk could theoretically be tampered with.\n return `${device.sku}_${normalizeDeviceId(device.deviceId)}`;\n }\n\n /**\n * Create a state if it doesn't exist\n *\n * @param id State object ID\n * @param name Display name\n * @param type Value type\n * @param role ioBroker role\n * @param write Whether state is writable\n * @param unit Optional unit of measurement\n */\n private async ensureState(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n write: boolean,\n unit?: string,\n ): Promise<void> {\n const common: Partial<ioBroker.StateCommon> = {\n name,\n type,\n role,\n read: true,\n write,\n };\n if (unit) {\n common.unit = unit;\n }\n await this.adapter.extendObjectAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/devices.json CHANGED
@@ -154,6 +154,12 @@
154
154
  "status": "verified",
155
155
  "since": "1.0.0"
156
156
  },
157
+ "H61A8": {
158
+ "name": "Outdoor Neon LED Strip 10m",
159
+ "type": "light",
160
+ "status": "verified",
161
+ "since": "2.5.2"
162
+ },
157
163
  "H61BE": {
158
164
  "name": "Glide Wall Light Wide",
159
165
  "type": "light",
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.5.0",
4
+ "version": "2.5.2",
5
5
  "news": {
6
+ "2.5.2": {
7
+ "en": "Stops the 'membersUnreachable has no existing object' WARN spam every 2 min — group state stays present with empty value. Plus verified H61A8 Outdoor Neon LED Strip 10m.",
8
+ "de": "Behebt WARN-Spam 'membersUnreachable has no existing object' alle 2 Min — Gruppen-State bleibt existent mit leerem Wert. Plus verifiziert: H61A8 Outdoor Neon LED Strip 10m.",
9
+ "ru": "Устраняет WARN-спам 'membersUnreachable has no existing object' каждые 2 мин — state группы остается с пустым значением. Плюс H61A8 Outdoor Neon LED Strip 10m verified.",
10
+ "pt": "Corrige o WARN spam 'membersUnreachable has no existing object' a cada 2 min — state do grupo permanece com valor vazio. Plus H61A8 Outdoor Neon LED Strip 10m verificado.",
11
+ "nl": "Lost WARN-spam 'membersUnreachable has no existing object' elke 2 min op — group state blijft aanwezig met lege waarde. Plus H61A8 Outdoor Neon LED Strip 10m verified.",
12
+ "fr": "Corrige le spam WARN 'membersUnreachable has no existing object' toutes les 2 min — l'etat du groupe reste present avec valeur vide. Plus H61A8 Outdoor Neon LED Strip 10m verifie.",
13
+ "it": "Corregge lo spam WARN 'membersUnreachable has no existing object' ogni 2 min — lo state del gruppo resta presente con valore vuoto. Plus H61A8 Outdoor Neon LED Strip 10m verificato.",
14
+ "es": "Corrige el spam WARN 'membersUnreachable has no existing object' cada 2 min — el state del grupo permanece presente con valor vacio. Plus H61A8 Outdoor Neon LED Strip 10m verificado.",
15
+ "pl": "Usuwa WARN-spam 'membersUnreachable has no existing object' co 2 min — state grupy pozostaje obecny z pustą wartością. Plus H61A8 Outdoor Neon LED Strip 10m verified.",
16
+ "uk": "Усуває WARN-спам 'membersUnreachable has no existing object' кожні 2 хв — state групи залишається з порожнім значенням. Плюс H61A8 Outdoor Neon LED Strip 10m verified.",
17
+ "zh-cn": "修复每 2 分钟 'membersUnreachable has no existing object' WARN 刷屏 — 组 state 保持存在并写入空值。新增已验证设备:H61A8 Outdoor Neon LED Strip 10m。"
18
+ },
19
+ "2.5.1": {
20
+ "en": "Cloud rate-limit hint now reads 'rate-limited by Govee' on 429 instead of the generic cloud-error message. Plus 33 mock tests for cloud + MQTT login paths.",
21
+ "de": "Cloud-Rate-Limit-Hinweis zeigt bei 429 jetzt 'rate-limited by Govee' statt der generischen Cloud-Fehlermeldung. Plus 33 Mock-Tests fuer Cloud + MQTT-Login.",
22
+ "ru": "Сообщение о лимите Cloud при 429 теперь корректно показывает 'rate-limited by Govee' вместо общей ошибки Cloud. Плюс 33 mock-теста для cloud + MQTT-login.",
23
+ "pt": "Aviso de rate-limit Cloud agora mostra 'rate-limited by Govee' em 429 em vez do erro generico. Mais 33 mock tests para cloud + MQTT login.",
24
+ "nl": "Cloud rate-limit melding toont bij 429 nu 'rate-limited by Govee' i.p.v. de generieke cloud-fout. Plus 33 mock tests voor cloud + MQTT-login.",
25
+ "fr": "Le message de rate-limit Cloud affiche maintenant 'rate-limited by Govee' sur 429 au lieu de l'erreur generique. Plus 33 mock tests pour cloud + MQTT-login.",
26
+ "it": "Avviso rate-limit Cloud ora mostra 'rate-limited by Govee' su 429 invece dell'errore generico. Piu 33 mock test per cloud + MQTT-login.",
27
+ "es": "Aviso de rate-limit Cloud ahora muestra 'rate-limited by Govee' en 429 en vez del error generico. Mas 33 mock tests para cloud + MQTT-login.",
28
+ "pl": "Komunikat o limicie Cloud przy 429 teraz pokazuje 'rate-limited by Govee' zamiast generycznego bledu. Plus 33 mock-testy dla cloud + MQTT-login.",
29
+ "uk": "Повідомлення про ліміт Cloud при 429 тепер показує 'rate-limited by Govee' замість загальної помилки. Плюс 33 mock-тести для cloud + MQTT-login.",
30
+ "zh-cn": "Cloud 限流提示:429 现在正确显示 'rate-limited by Govee' 而非通用 Cloud 错误。新增 33 个 cloud + MQTT 登录路径 mock 测试。"
31
+ },
6
32
  "2.5.0": {
7
33
  "en": "Code-hygiene: onMessage handler (sendTo from admin UI) is now its own class with host-interface. main.ts shrinks; behaviour unchanged.",
8
34
  "de": "Code-Hygiene: onMessage-Handler (sendTo aus dem Admin-UI) ist jetzt eigene Klasse mit Host-Interface. main.ts wird kleiner; Verhalten identisch.",
@@ -67,32 +93,6 @@
67
93
  "pl": "Monitor driftu App-version: codzienny lookup iTunes ostrzega gdy lokalna Govee-App-Version jest przestarzala. Plus higiena kodu: onStateChange handlers wydzielone.",
68
94
  "uk": "Монітор дрейфу App-версії: щоденний iTunes-лукап попереджає коли локальна Govee-App-Version застаріла. Плюс гігієна коду: onStateChange handlers виділені.",
69
95
  "zh-cn": "App 版本漂移监控:每日 iTunes 查询,本地 Govee 应用版本过旧时发出警告。代码卫生:onStateChange handlers 拆分。"
70
- },
71
- "2.2.0": {
72
- "en": "Quality release: 2FA no longer restarts, MQTT pushes type-safe, sensors in right channel. Ready-log shows channel status, persistent UDP sockets, abortable HTTP, no more group-WARN spam.",
73
- "de": "Quality-Release: 2FA ohne Restart, MQTT-Pushes typsicher, Sensoren im richtigen Kanal. Ready-Log mit Channel-Status, persistente UDP-Sockets, abbrechbares HTTP, kein Group-WARN-Spam.",
74
- "ru": "Quality-релиз: 2FA-верификация без рестарта, MQTT-пуши типобезопасны, сенсоры в правильном канале. Новый ready-лог со статусом каналов, постоянные UDP-сокеты, прерываемые HTTP, без group-WARN-спама.",
75
- "pt": "Release quality: verificacao 2FA sem reinicio, MQTT type-safe, sensores no canal certo. Novo log ready com status de canais, sockets UDP persistentes, HTTP cancelavel, sem mais spam group-WARN.",
76
- "nl": "Quality release: 2FA-verificatie geen restart meer, MQTT type-safe, sensors in juiste kanaal. Nieuw ready-log, persistente UDP-sockets, afbreekbare HTTP, geen group-WARN spam.",
77
- "fr": "Release qualite : verification 2FA sans redemarrage, MQTT type-safe, capteurs dans le bon canal. Nouveau ready-log avec statut canaux, sockets UDP persistants, HTTP annulable, plus de group-WARN spam.",
78
- "it": "Release quality: verifica 2FA senza riavvio, MQTT type-safe, sensori nel canale giusto. Nuovo ready-log con stato canali, socket UDP persistenti, HTTP annullabile, niente piu group-WARN spam.",
79
- "es": "Release de calidad: verificacion 2FA sin reinicio, MQTT type-safe, sensores al canal correcto. Nuevo ready-log con estado de canales, sockets UDP persistentes, HTTP cancelable, sin spam group-WARN.",
80
- "pl": "Quality release: weryfikacja 2FA bez restartu, MQTT type-safe, sensory we wlasciwym kanale. Nowy ready-log ze statusem kanalow, persistentne UDP, anulowalne HTTP, bez spamu group-WARN.",
81
- "uk": "Quality-реліз: 2FA-верифікація без рестарту, MQTT типобезпечно, сенсори у правильному каналі. Новий ready-лог зі статусом каналів, постійні UDP-сокети, переривані HTTP, без group-WARN-спаму.",
82
- "zh-cn": "质量发布:2FA 验证不再重启;MQTT 推送类型安全;传感器进入正确通道。新 ready 日志带通道状态、持久 UDP socket、可中断 HTTP、不再有 group-WARN 刷屏。"
83
- },
84
- "2.1.4": {
85
- "en": "Online status correct again after adapter restart — lights flip to online with the first LAN scan, sensors with the first cloud poll (5 s after start instead of 2 minutes).",
86
- "de": "Online-Status nach Adapter-Restart wieder korrekt — Lichter werden direkt mit dem ersten LAN-Scan online, Sensoren mit dem ersten Cloud-Poll (5 s nach Start statt 2 Minuten).",
87
- "ru": "Состояние «онлайн» снова корректно после рестарта адаптера — лампы переключаются на онлайн с первым LAN-сканом, сенсоры — с первым облачным опросом (5 с после старта вместо 2 минут).",
88
- "pt": "Estado online correto novamente apos reiniciar o adapter — luzes ficam online com o primeiro scan LAN, sensores com o primeiro polling de nuvem (5 s apos o inicio em vez de 2 minutos).",
89
- "nl": "Online-status weer correct na herstart van de adapter — lampen springen op online bij de eerste LAN-scan, sensoren bij de eerste cloud-poll (5 s na start in plaats van 2 minuten).",
90
- "fr": "Statut en ligne correct apres redemarrage de l'adapter — les lumieres passent online au premier scan LAN, les capteurs au premier poll cloud (5 s apres le demarrage au lieu de 2 minutes).",
91
- "it": "Stato online di nuovo corretto dopo il riavvio dell'adapter — le luci passano online al primo scan LAN, i sensori al primo poll cloud (5 s dopo l'avvio invece di 2 minuti).",
92
- "es": "Estado online correcto otra vez tras reiniciar el adaptador — las luces pasan a online con el primer escaneo LAN, los sensores con el primer poll de nube (5 s tras el inicio en vez de 2 minutos).",
93
- "pl": "Status online znow poprawny po restarcie adaptera — lampy przechodza w online przy pierwszym skanie LAN, sensory przy pierwszym pollu chmury (5 s po starcie zamiast 2 minut).",
94
- "uk": "Стан «онлайн» знову коректний після рестарту адаптера — лампи перемикаються на онлайн з першим LAN-сканом, сенсори — з першим хмарним опитуванням (5 с після старту замість 2 хвилин).",
95
- "zh-cn": "适配器重启后在线状态恢复准确——灯具会在第一次 LAN 扫描时切换为在线,传感器在第一次云轮询时(启动后 5 秒,而不是 2 分钟)。"
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.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",