iobroker.govee-smart 2.5.1 → 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,11 @@ 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
+
127
132
  ### 2.5.1 (2026-05-04)
128
133
 
129
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.
@@ -140,10 +145,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
140
145
 
141
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.
142
147
 
143
- ### 2.3.1 (2026-05-04)
144
-
145
- - 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.
146
-
147
148
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
148
149
 
149
150
  ## Support
@@ -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": "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",
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,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "govee-smart",
4
- "version": "2.5.1",
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
+ },
6
19
  "2.5.1": {
7
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.",
8
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.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Monitor driftu App-version: codzienny lookup iTunes ostrzega gdy lokalna Govee-App-Version jest przestarzala. Plus higiena kodu: onStateChange handlers wydzielone.",
81
94
  "uk": "Монітор дрейфу App-версії: щоденний iTunes-лукап попереджає коли локальна Govee-App-Version застаріла. Плюс гігієна коду: onStateChange handlers виділені.",
82
95
  "zh-cn": "App 版本漂移监控:每日 iTunes 查询,本地 Govee 应用版本过旧时发出警告。代码卫生:onStateChange handlers 拆分。"
83
- },
84
- "2.2.0": {
85
- "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.",
86
- "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.",
87
- "ru": "Quality-релиз: 2FA-верификация без рестарта, MQTT-пуши типобезопасны, сенсоры в правильном канале. Новый ready-лог со статусом каналов, постоянные UDP-сокеты, прерываемые HTTP, без group-WARN-спама.",
88
- "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.",
89
- "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.",
90
- "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.",
91
- "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.",
92
- "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.",
93
- "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.",
94
- "uk": "Quality-реліз: 2FA-верифікація без рестарту, MQTT типобезпечно, сенсори у правильному каналі. Новий ready-лог зі статусом каналів, постійні UDP-сокети, переривані HTTP, без group-WARN-спаму.",
95
- "zh-cn": "质量发布:2FA 验证不再重启;MQTT 推送类型安全;传感器进入正确通道。新 ready 日志带通道状态、持久 UDP socket、可中断 HTTP、不再有 group-WARN 刷屏。"
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.1",
3
+ "version": "2.5.2",
4
4
  "description": "Control Govee WiFi devices via LAN, MQTT and Cloud.",
5
5
  "author": {
6
6
  "name": "krobi",