iobroker.govee-smart 2.6.6 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,6 +128,18 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
128
128
  Placeholder for the next version (at the beginning of the line):
129
129
  ### **WORK IN PROGRESS**
130
130
  -->
131
+ ### 2.7.0 (2026-05-10)
132
+
133
+ - Newly created snapshots in the Govee Home app now appear in the ioBroker dropdown — both after an adapter restart and via the refresh button. Previously the cache held the old list forever (Issue #13).
134
+ - Refresh button is now per device under `devices.<id>.snapshots.refresh_cloud` instead of an adapter-wide button: a press hits Govee only for that one light, not for every device on your account.
135
+ - The refresh also re-fetches the Govee device list, so a brand-new snapshot is picked up even when Govee's `/device/scenes` endpoint hasn't caught up to it yet.
136
+ - Three "Invalid JSON in HTTP 200 response" warnings for music/DIY/SKU library are gone — empty 200 bodies for SKUs without that data are now treated as "no data" instead of a parse error.
137
+ - `info.refresh_cloud_data` is removed in favour of the per-device button above. ioBroker scripts that wrote to it need to point at `devices.<id>.snapshots.refresh_cloud` instead.
138
+
139
+ ### 2.6.7 (2026-05-10)
140
+
141
+ - Cleaner ready-log: removed the device/sensor/group online-summary because it ran before the LAN scan had settled and could falsely show all lights as offline.
142
+
131
143
  ### 2.6.6 (2026-05-10)
132
144
 
133
145
  - Internal refactoring. No changes for users.
@@ -140,20 +152,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
140
152
 
141
153
  - Internal tooling refresh. No changes for users.
142
154
 
143
- ### 2.6.3 (2026-05-10)
144
-
145
- - Connection-status indicators recover on their own after a brief Govee outage instead of staying stuck until you restart the adapter.
146
- - Restoring a snapshot on a multi-segment LED strip is fast again — a 30-segment snapshot replays in well under a second instead of about nine.
147
- - A wrong manual pairing IP, a network blip, or a Cloud command that Govee rejects are now reported in the log instead of failing silently.
148
- - LED segment 0 turning blue when a colour was set with a short hex value (e.g. `#FF`) is fixed — invalid colour input falls back to black.
149
- - The segment-detection wizard restores the strip's previous look on adapter-stop, so the test pattern doesn't stay on the wall when you save the adapter settings.
150
- - A rejected Govee API key now surfaces an actionable hint in the log so you know where to look.
151
- - Various behind-the-scenes hardening of all four communication channels (LAN, MQTT, Cloud-events, Cloud REST) — invisible if everything was already running fine, robustness if something is unstable.
152
-
153
- ### 2.6.2 (2026-05-09)
154
-
155
- - Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names, descriptions and dropdown labels (11 languages) are unchanged. The user-visible segment-detection wizard text in the admin UI also remains localized.
156
-
157
155
  Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
158
156
 
159
157
  ## Support
@@ -797,6 +797,20 @@ function buildDeviceStateDefs(device, localSnapshots, memberDevices) {
797
797
  channel: "snapshots"
798
798
  });
799
799
  }
800
+ if (isLight && (hasDynamicSceneCapability(device.capabilities, "lightScene") || hasDynamicSceneCapability(device.capabilities, "diyScene") || hasDynamicSceneCapability(device.capabilities, "snapshot"))) {
801
+ stateDefs.push({
802
+ id: "refresh_cloud",
803
+ name: (0, import_i18n_states.tName)("refreshCloud"),
804
+ desc: (0, import_i18n_states.tDesc)("refreshCloudDesc"),
805
+ type: "boolean",
806
+ role: "button",
807
+ write: true,
808
+ def: false,
809
+ capabilityType: "local",
810
+ capabilityInstance: "refreshCloud",
811
+ channel: "snapshots"
812
+ });
813
+ }
800
814
  if (isLight) {
801
815
  stateDefs.push({
802
816
  id: "snapshot_local",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/capability-mapper.ts"],
4
- "sourcesContent": ["import {\n buildUniqueLabelMap,\n rgbToHex,\n type CloudCapability,\n type CloudStateCapability,\n type GoveeDevice,\n} from \"./types\";\nimport { applyColorTempQuirk } from \"./device-registry\";\nimport { tDesc, tLabel, tName } from \"./i18n-states\";\n\n/** ioBroker state definition derived from a Govee capability */\nexport interface StateDefinition {\n /** State ID suffix (e.g. \"power\", \"brightness\", \"colorRgb\") */\n id: string;\n /**\n * Display name. Plain string for capability-derived names (e.g. from\n * `humanize(cap.instance)` of an unknown Govee capability \u2014 those aren't\n * predictable). For known states, a translation object `{en, de, ru, ...}`\n * built via `tName()` from `i18n-states.ts` \u2014 Admin/vis/Object-Browser\n * pick the user's language automatically.\n */\n name: string | Record<string, string>;\n /**\n * Human-readable description shown in the object browser \u2014 used to clarify\n * ambiguous state names (e.g. cloud vs local snapshots) where the id alone\n * isn't enough for a user to know what the state does.\n */\n desc?: string | Record<string, string>;\n /** ioBroker value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Whether state is writable */\n write: boolean;\n /** Unit string */\n unit?: string;\n /** Min value for numbers */\n min?: number;\n /** Max value for numbers */\n max?: number;\n /**\n * Predefined values for a select (value \u2192 label). Label can be a plain\n * string or a `{en, de, ...}` translation object (ioBroker Admin v6+).\n */\n states?: Record<string, string | Record<string, string>>;\n /** Default value for new states */\n def?: ioBroker.StateValue;\n /** Original capability type */\n capabilityType: string;\n /** Original capability instance */\n capabilityInstance: string;\n /** Target channel (control, scenes, music, snapshots). Defaults to \"control\". */\n channel?: string;\n}\n\n/**\n * Coerce arbitrary value to boolean. Accepts true/1/\"1\"/\"true\" as truthy.\n *\n * @param v Raw value from API\n */\nfunction coerceBool(v: unknown): boolean {\n return v === true || v === 1 || v === \"1\" || v === \"true\";\n}\n\n/**\n * Stringify an unknown raw API value for an ioBroker state. Objects /\n * functions go through JSON.stringify (so we don't get `[object Object]`);\n * everything else takes the primitive `String()` path. Centralised to keep\n * the no-base-to-string lint rule happy at the call sites without\n * sprinkling type assertions all over.\n *\n * @param v Raw value from API\n */\nfunction safeStringify(v: unknown): string {\n switch (typeof v) {\n case \"string\":\n return v;\n case \"number\":\n case \"bigint\":\n case \"boolean\":\n case \"symbol\":\n return v.toString();\n case \"undefined\":\n return \"undefined\";\n default:\n // object, function, null\n return JSON.stringify(v);\n }\n}\n\n/**\n * Coerce arbitrary value to finite number, or null if not parseable.\n *\n * @param v Raw value from API\n */\nfunction coerceNum(v: unknown): number | null {\n if (typeof v === \"number\" && Number.isFinite(v)) {\n return v;\n }\n if (typeof v === \"string\" && v.trim() !== \"\") {\n const n = Number(v);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n return null;\n}\n\n/**\n * Maps Govee Cloud API capabilities to ioBroker state definitions.\n * Pure function \u2014 no side effects, easily testable.\n *\n * @param capabilities Device capabilities from Cloud API\n */\nexport function mapCapabilities(capabilities: CloudCapability[]): StateDefinition[] {\n const states: StateDefinition[] = [];\n\n if (!Array.isArray(capabilities)) {\n return states;\n }\n\n for (const cap of capabilities) {\n const mapped = mapSingleCapability(cap);\n if (mapped) {\n states.push(...mapped);\n }\n }\n\n return states;\n}\n\n/**\n * Probe `capabilities` for a `devices.capabilities.dynamic_scene` entry of\n * the given instance (lightScene / diyScene / snapshot). Used to gate the\n * scene/snapshot dropdowns capability-driven instead of data-driven \u2014 a\n * device that exposes the cap should always show the dropdown, even if\n * the scene list hasn't been fetched yet.\n *\n * @param capabilities Device capabilities from Cloud API\n * @param instance The dynamic_scene instance to look up\n */\nexport function hasDynamicSceneCapability(\n capabilities: CloudCapability[],\n instance: \"lightScene\" | \"diyScene\" | \"snapshot\",\n): boolean {\n if (!Array.isArray(capabilities)) {\n return false;\n }\n return capabilities.some(\n cap =>\n typeof cap?.type === \"string\" &&\n typeof cap?.instance === \"string\" &&\n (cap.type === \"devices.capabilities.dynamic_scene\" || cap.type === \"dynamic_scene\") &&\n cap.instance === instance,\n );\n}\n\n/**\n * Default state definitions for LAN-only devices (no Cloud capabilities).\n * All LAN-capable Govee lights support: power, brightness, color, color temperature.\n */\nexport function getDefaultLanStates(): StateDefinition[] {\n return [\n {\n id: \"power\",\n name: tName(\"power\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: \"lan\",\n capabilityInstance: \"powerSwitch\",\n },\n {\n id: \"brightness\",\n name: tName(\"brightness\"),\n type: \"number\",\n role: \"level.brightness\",\n write: true,\n min: 0,\n max: 100,\n unit: \"%\",\n def: 0,\n capabilityType: \"lan\",\n capabilityInstance: \"brightness\",\n },\n {\n id: \"colorRgb\",\n name: tName(\"colorRgb\"),\n type: \"string\",\n role: \"level.color.rgb\",\n write: true,\n def: \"#000000\",\n capabilityType: \"lan\",\n capabilityInstance: \"colorRgb\",\n },\n {\n id: \"colorTemperature\",\n name: tName(\"colorTemperature\"),\n type: \"number\",\n role: \"level.color.temperature\",\n write: true,\n min: 2000,\n max: 9000,\n unit: \"K\",\n def: 2000,\n capabilityType: \"lan\",\n capabilityInstance: \"colorTemperatureK\",\n },\n ];\n}\n\n/**\n * Map a single capability to state definition(s)\n *\n * @param cap Cloud capability to map\n */\nfunction mapSingleCapability(cap: CloudCapability): StateDefinition[] | null {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n return null;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n\n switch (shortType) {\n case \"on_off\":\n return [\n {\n id: \"power\",\n name: tName(\"power\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"range\":\n return mapRange(cap);\n\n case \"color_setting\":\n return mapColorSetting(cap);\n\n case \"toggle\":\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"mode\":\n return mapMode(cap);\n\n case \"property\":\n return mapProperty(cap);\n\n case \"online\":\n // Handled separately \u2014 not a regular state\n return null;\n\n case \"segment_color_setting\":\n // Segments are handled specially by state-manager\n return [\n {\n id: `_segment_${sanitizeId(cap.instance)}`,\n name: humanize(cap.instance),\n type: \"string\",\n role: \"json\",\n write: true,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"dynamic_scene\":\n // lightScene / diyScene / snapshot get real dropdowns built later in\n // buildDeviceStateDefs from the scenes/snapshots arrays \u2014 skip the\n // generic stub here so we don't create and immediately delete it.\n if (cap.instance === \"lightScene\" || cap.instance === \"diyScene\" || cap.instance === \"snapshot\") {\n return null;\n }\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"string\",\n role: \"json\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"work_mode\":\n return mapWorkMode(cap);\n\n case \"temperature_setting\":\n return mapTemperatureSetting(cap);\n\n case \"event\":\n return mapEvent(cap);\n\n case \"music_setting\":\n return mapMusicSetting(cap);\n\n default:\n return null;\n }\n}\n\n/**\n * Map range capability (brightness, humidity, etc.)\n *\n * @param cap Cloud range capability\n */\nfunction mapRange(cap: CloudCapability): StateDefinition[] {\n const range = cap.parameters?.range;\n const isBrightness = cap.instance.toLowerCase().includes(\"brightness\");\n\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"number\",\n role: isBrightness ? \"level.brightness\" : \"level\",\n write: true,\n min: range?.min ?? 0,\n max: range?.max ?? 100,\n unit: normalizeUnit(cap.parameters?.unit),\n def: range?.min ?? 0,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map color_setting capability (RGB or color temperature)\n *\n * @param cap Cloud color setting capability\n */\nfunction mapColorSetting(cap: CloudCapability): StateDefinition[] {\n if (cap.instance === \"colorRgb\") {\n return [\n {\n id: \"colorRgb\",\n name: tName(\"colorRgb\"),\n type: \"string\",\n role: \"level.color.rgb\",\n write: true,\n def: \"#000000\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n if (cap.instance === \"colorTemperatureK\" || cap.instance.includes(\"colorTem\")) {\n const range = cap.parameters?.range;\n return [\n {\n id: \"colorTemperature\",\n name: tName(\"colorTemperature\"),\n type: \"number\",\n role: \"level.color.temperature\",\n write: true,\n min: range?.min ?? 2000,\n max: range?.max ?? 9000,\n unit: \"K\",\n def: range?.min ?? 2000,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n return [];\n}\n\n/**\n * Map mode capability (scenes with ENUM options)\n *\n * @param cap Cloud mode capability\n */\nfunction mapMode(cap: CloudCapability): StateDefinition[] {\n if (cap.instance !== \"presetScene\" || !Array.isArray(cap.parameters?.options)) {\n return [];\n }\n\n const states: Record<string, string> = {};\n for (const opt of cap.parameters.options) {\n if (!opt || typeof opt.name !== \"string\") {\n continue;\n }\n const val = typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value);\n states[val] = opt.name;\n }\n\n return [\n {\n id: \"scene\",\n name: tName(\"scene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map property capability (read-only sensors). Routes to the `sensor`\n * channel so a Heater's temperature reading sits cleanly next to other\n * sensor-style states instead of in `control`.\n *\n * @param cap Cloud property capability\n */\nfunction mapProperty(cap: CloudCapability): StateDefinition[] {\n const instance = cap.instance.toLowerCase();\n let role = \"value\";\n let unit: string | undefined;\n\n if (instance.includes(\"temperature\")) {\n role = \"value.temperature\";\n unit = \"\u00B0C\";\n } else if (instance.includes(\"humidity\")) {\n role = \"value.humidity\";\n unit = \"%\";\n } else if (instance.includes(\"battery\")) {\n role = \"value.battery\";\n unit = \"%\";\n } else if (instance.includes(\"co2\") || instance.includes(\"carbondioxide\")) {\n role = \"value.co2\";\n unit = \"ppm\";\n }\n\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"number\",\n role,\n write: false,\n unit: normalizeUnit(cap.parameters?.unit) ?? unit,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n channel: \"sensor\",\n },\n ];\n}\n\n/**\n * Map work_mode capability (STRUCT \u2014 Govee Heater/Humidifier/Fan/...).\n *\n * Two states max:\n * - `work_mode` \u2014 main mode dropdown (mixed type so users can write\n * either the numeric mode value or the label name)\n * - `mode_value` \u2014 secondary parameter (e.g. fan-speed level for the\n * \"manual\" mode); only created if the API actually exposes one\n *\n * @param cap Cloud work_mode capability\n */\nfunction mapWorkMode(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (!fields || fields.length === 0) {\n return [\n {\n id: \"work_mode\",\n name: tName(\"workMode\"),\n type: \"mixed\",\n role: \"level.mode\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n const states: StateDefinition[] = [];\n const modeField = fields.find(f => f && f.fieldName === \"workMode\");\n if (modeField?.options && modeField.options.length > 0) {\n const modeStates: Record<string, string> = {};\n for (const opt of modeField.options) {\n if (opt && typeof opt.name === \"string\") {\n modeStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n }\n states.push({\n id: \"work_mode\",\n name: tName(\"workMode\"),\n type: \"mixed\",\n role: \"level.mode\",\n write: true,\n states: modeStates,\n def: modeField.options[0] ? safeStringify(modeField.options[0].value) : \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n const valueField = fields.find(f => f && f.fieldName === \"modeValue\");\n if (valueField) {\n if (valueField.options && valueField.options.length > 0) {\n const valStates: Record<string, string> = {};\n for (const opt of valueField.options) {\n if (opt && typeof opt.name === \"string\") {\n valStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n }\n states.push({\n id: \"mode_value\",\n name: tName(\"modeValue\"),\n type: \"mixed\",\n role: \"level\",\n write: true,\n states: valStates,\n def: valueField.options[0] ? safeStringify(valueField.options[0].value) : \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n } else if (valueField.range) {\n states.push({\n id: \"mode_value\",\n name: tName(\"modeValue\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: valueField.range.min,\n max: valueField.range.max,\n def: valueField.range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n }\n\n return states;\n}\n\n/**\n * Map temperature_setting capability \u2014 Heater target-temp slider.\n * Honours the unit reported by the API (\u00B0F or \u00B0C); falls back to \u00B0F\n * because that's the more common Govee Heater default.\n *\n * @param cap Cloud temperature_setting capability\n */\nfunction mapTemperatureSetting(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (Array.isArray(fields) && fields.length > 0) {\n const tempField = fields.find(f => {\n if (!f || typeof f.fieldName !== \"string\") {\n return false;\n }\n if (f.fieldName === \"targetTemperature\") {\n return true;\n }\n return f.fieldName.toLowerCase().includes(\"temperature\");\n });\n if (tempField?.range) {\n const unit = normalizeUnit(cap.parameters?.unit) ?? \"\u00B0F\";\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"number\",\n role: \"level.temperature\",\n write: true,\n min: tempField.range.min,\n max: tempField.range.max,\n unit,\n def: tempField.range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n }\n\n const range = cap.parameters?.range;\n if (range) {\n const unit = normalizeUnit(cap.parameters?.unit) ?? \"\u00B0F\";\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"number\",\n role: \"level.temperature\",\n write: true,\n min: range.min,\n max: range.max,\n unit,\n def: range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n // No usable schema \u2014 expose the raw payload so the user at least sees\n // the attempt and can report it. Stays JSON to avoid pretending we\n // understand the structure.\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"string\",\n role: \"json\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map event capability (asynchronous OpenAPI-MQTT alarms \u2014 read-only).\n * Each event becomes a boolean indicator in the events/ channel\n * (lackWater, iceFull, bodyAppeared, dirtDetected, \u2026).\n *\n * @param cap Cloud event capability\n */\nfunction mapEvent(cap: CloudCapability): StateDefinition[] {\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"boolean\",\n role: \"indicator.alarm\",\n write: false,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n channel: \"events\",\n },\n ];\n}\n\n/**\n * Map music_setting capability to user-friendly states.\n * Parses STRUCT fields into: mode dropdown, sensitivity slider, auto-color toggle.\n *\n * @param cap Cloud music_setting capability\n */\nfunction mapMusicSetting(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (!Array.isArray(fields) || fields.length === 0) {\n // No field details from API \u2014 can't create usable states\n return [];\n }\n\n const states: StateDefinition[] = [];\n\n // Mode dropdown \u2014 only if API provides actual mode options\n const modeField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"musicMode\");\n if (modeField?.options && Array.isArray(modeField.options) && modeField.options.length > 0) {\n const modeStates: Record<string, string> = { 0: \"---\" };\n for (const opt of modeField.options) {\n if (!opt || typeof opt.name !== \"string\") {\n continue;\n }\n modeStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n states.push({\n id: \"music_mode\",\n name: tName(\"musicMode\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: modeStates,\n def: \"0\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // Sensitivity slider\n const sensField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"sensitivity\");\n if (sensField?.range) {\n states.push({\n id: \"music_sensitivity\",\n name: tName(\"musicSensitivity\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: sensField.range.min,\n max: sensField.range.max,\n unit: \"%\",\n def: sensField.range.max,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // Auto color toggle\n const autoColorField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"autoColor\");\n if (autoColorField) {\n states.push({\n id: \"music_auto_color\",\n name: tName(\"musicAutoColor\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: true,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // All music states belong to the music channel\n for (const s of states) {\n s.channel = \"music\";\n }\n return states;\n}\n\n/**\n * Apply device quirks to mapped state definitions.\n * Corrects wrong API data (e.g. color temperature range) for specific SKUs.\n *\n * @param sku Device model (e.g. \"H60A1\")\n * @param states State definitions to adjust\n */\nexport function applyQuirksToStates(sku: string, states: StateDefinition[]): StateDefinition[] {\n for (const state of states) {\n if (state.id === \"colorTemperature\" && state.min != null && state.max != null) {\n const corrected = applyColorTempQuirk(sku, state.min, state.max);\n state.min = corrected.min;\n state.max = corrected.max;\n state.def = corrected.min;\n }\n }\n return states;\n}\n\n/** Known Govee API unit strings \u2192 ioBroker units */\nconst UNIT_MAP: Record<string, string> = {\n \"unit.percent\": \"%\",\n \"unit.kelvin\": \"K\",\n \"unit.celsius\": \"\u00B0C\",\n \"unit.fahrenheit\": \"\u00B0F\",\n};\n\n/**\n * Normalize Govee API unit string to ioBroker standard\n *\n * @param unit Raw unit string from API\n */\nfunction normalizeUnit(unit?: string): string | undefined {\n if (!unit) {\n return undefined;\n }\n return UNIT_MAP[unit] ?? unit;\n}\n\n/**\n * Sanitize a string for use as ioBroker state ID\n *\n * @param str Input string to sanitize\n */\nfunction sanitizeId(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1_$2\")\n .replace(/[^a-zA-Z0-9_]/g, \"_\")\n .toLowerCase();\n}\n\n/**\n * Convert camelCase to human-readable name\n *\n * @param str camelCase input string\n */\nfunction humanize(str: string): string {\n // Reihenfolge: erst Underscore \u2192 Space, dann camelCase-split, dann\n // trim + erstes Zeichen uppercase. Vorher: leading-underscore-IDs\n // (z.B. `_segment_color`) wurden zu ` segment color` mit leading\n // Space und ohne Capitalization (^\\w matched space, nicht word-char).\n return str\n .replace(/_/g, \" \")\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .trim()\n .replace(/^./, c => c.toUpperCase());\n}\n\n/** Mapped Cloud state value: state ID + converted value */\nexport interface CloudStateValue {\n /** State ID in control/ channel (e.g. \"power\", \"brightness\", \"gradient_toggle\") */\n stateId: string;\n /** Converted value ready for ioBroker setStateAsync */\n value: ioBroker.StateValue;\n}\n\n/**\n * Map a Cloud device state capability to a state ID + converted value.\n * Uses the same ID logic as mapCapabilities so IDs always match.\n *\n * @param cap Cloud state capability with current value\n */\nexport function mapCloudStateValue(cap: CloudStateCapability): CloudStateValue | null {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n return null;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n const raw = cap.state?.value;\n if (raw === undefined || raw === null) {\n return null;\n }\n\n switch (shortType) {\n case \"on_off\":\n return { stateId: \"power\", value: coerceBool(raw) };\n\n case \"range\": {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: sanitizeId(cap.instance), value: n };\n }\n\n case \"color_setting\":\n if (cap.instance === \"colorRgb\") {\n const num = coerceNum(raw) ?? 0;\n return {\n stateId: \"colorRgb\",\n value: rgbToHex((num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff),\n };\n }\n if (cap.instance.includes(\"colorTem\")) {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: \"colorTemperature\", value: n };\n }\n return null;\n\n case \"toggle\":\n return { stateId: sanitizeId(cap.instance), value: coerceBool(raw) };\n\n case \"mode\":\n if (cap.instance === \"presetScene\") {\n return {\n stateId: \"scene\",\n value: safeStringify(raw),\n };\n }\n return null;\n\n case \"dynamic_scene\":\n return {\n stateId: sanitizeId(cap.instance),\n value: safeStringify(raw),\n };\n\n case \"work_mode\": {\n // STRUCT: { workMode: <number>, modeValue?: <number> }. Cloud\n // /device/state only returns the primary mode here \u2014 mode_value\n // (sub-parameter) follows via MQTT status push when the device\n // reports it, so we don't lose it just because it isn't in the\n // initial state response.\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const n = coerceNum(struct.workMode);\n if (n !== null) {\n return { stateId: \"work_mode\", value: n };\n }\n }\n const direct = coerceNum(raw);\n if (direct !== null) {\n return { stateId: \"work_mode\", value: direct };\n }\n return null;\n }\n\n case \"temperature_setting\": {\n // STRUCT: { targetTemperature: <number>, temperatureUnit?: ... }\n // \u2014 fall back to direct number for adapters that simplify.\n const direct = coerceNum(raw);\n if (direct !== null) {\n return { stateId: \"target_temperature\", value: direct };\n }\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const temp = struct.targetTemperature ?? struct.temperature ?? struct.temp;\n const n = coerceNum(temp);\n if (n !== null) {\n return { stateId: \"target_temperature\", value: n };\n }\n }\n return null;\n }\n\n case \"event\":\n return {\n stateId: sanitizeId(cap.instance),\n value: coerceBool(raw),\n };\n\n case \"music_setting\":\n // Extract mode value from STRUCT state\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const mode = coerceNum(struct.musicMode);\n return {\n stateId: \"music_mode\",\n value: mode !== null ? String(mode) : \"0\",\n };\n }\n return null;\n\n case \"property\": {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: sanitizeId(cap.instance), value: n };\n }\n\n default:\n return null;\n }\n}\n\n/**\n * Plan the per-state writes for a list of synthesised Cloud-state\n * capabilities. Used by the App-API poll and the OpenAPI-MQTT event\n * handler (both call into `applyCloudCapabilities` on the adapter side).\n *\n * Returns the resolved `(stateId, value)` pairs for capabilities that:\n * - decode via `mapCloudStateValue` to a non-null result, AND\n * - aren't shadowed by the LAN-state set when the device is LAN-capable\n * (lights with `lanIp` shouldn't have their LAN sub-second updates\n * overwritten by a Cloud-source value).\n *\n * Pure function \u2014 no adapter state, no I/O \u2014 so the LAN-shadow logic is\n * unit-testable independent of the live state-write pipeline.\n *\n * @param caps Capabilities to consider\n * @param hasLanIp Whether the target device has a known LAN IP\n * @param lanStateIds Default-LAN state IDs that LAN delivers authoritatively\n */\nexport function planCloudCapabilityWrites(\n caps: CloudStateCapability[],\n hasLanIp: boolean,\n lanStateIds: Set<string>,\n): CloudStateValue[] {\n const writes: CloudStateValue[] = [];\n if (!Array.isArray(caps)) {\n return writes;\n }\n for (const cap of caps) {\n const mapped = mapCloudStateValue(cap);\n if (!mapped) {\n continue;\n }\n if (hasLanIp && lanStateIds.has(mapped.stateId)) {\n continue;\n }\n writes.push(mapped);\n }\n return writes;\n}\n\n/**\n * Build complete state definitions for a device.\n * Combines LAN defaults, Cloud capabilities, quirks, scenes, snapshots, and diagnostics.\n * For groups: computes capability intersection of member devices (no snapshots/diagnostics).\n *\n * @param device Govee device with capabilities, scenes, etc.\n * @param localSnapshots Optional local snapshot names\n * @param memberDevices Resolved member devices (only for BaseGroup)\n */\nexport function buildDeviceStateDefs(\n device: GoveeDevice,\n localSnapshots?: { name: string }[],\n memberDevices?: GoveeDevice[],\n): StateDefinition[] {\n if (device.sku === \"BaseGroup\") {\n return buildGroupStateDefs(memberDevices || []);\n }\n let stateDefs: StateDefinition[];\n\n if (device.lanIp) {\n stateDefs = getDefaultLanStates();\n if (device.capabilities.length > 0) {\n const lanIds = new Set(stateDefs.map(d => d.id));\n const cloudDefs = mapCapabilities(device.capabilities);\n for (const cd of cloudDefs) {\n if (!lanIds.has(cd.id)) {\n stateDefs.push(cd);\n }\n }\n }\n } else {\n stateDefs = mapCapabilities(device.capabilities);\n }\n\n applyQuirksToStates(device.sku, stateDefs);\n\n // Light-only state defs \u2014 scenes / snapshots / music / scene_speed only\n // make sense for lights. Sensors and appliances would otherwise see\n // empty snapshot dropdowns and a useless save/delete button pair.\n const isLight = device.type === \"devices.types.light\";\n\n // Capability-driven: if the device's Cloud capabilities expose the\n // dynamic_scene instance, always create the dropdown \u2014 even before\n // /device/scenes has been queried (an empty list still beats a\n // missing state). Guards against the H61D5-style case where the\n // user's first restart shows no scenes / snapshots datapoints.\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"lightScene\")) {\n stateDefs.push({\n id: \"light_scene\",\n name: tName(\"lightScene\"),\n // mixed lets users write the index (\"1\"), the index as number (1),\n // or the scene name (\"Aurora\") \u2014 the onStateChange handler resolves\n // all three forms via the common.states map.\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.scenes),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"lightScene\",\n channel: \"scenes\",\n });\n }\n\n // Scene speed slider \u2014 only if any scene supports speed adjustment\n const maxSpeedLevel = device.sceneLibrary.reduce((max, entry) => {\n if (entry.speedInfo?.supSpeed && entry.speedInfo.config) {\n try {\n const parsed = JSON.parse(entry.speedInfo.config) as unknown;\n // Config can drift \u2014 if not an array, skip this entry silently\n if (!Array.isArray(parsed)) {\n return max;\n }\n for (const cfg of parsed as Array<{ moveIn?: number[] }>) {\n if (cfg && Array.isArray(cfg.moveIn) && cfg.moveIn.length - 1 > max) {\n max = cfg.moveIn.length - 1;\n }\n }\n } catch {\n /* ignore invalid config JSON */\n }\n }\n return max;\n }, -1);\n if (isLight && maxSpeedLevel > 0) {\n stateDefs.push({\n id: \"scene_speed\",\n name: tName(\"sceneSpeed\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: 0,\n max: maxSpeedLevel,\n def: 0,\n capabilityType: \"local\",\n capabilityInstance: \"sceneSpeed\",\n channel: \"scenes\",\n });\n }\n\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"diyScene\")) {\n stateDefs.push({\n id: \"diy_scene\",\n name: tName(\"diyScene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.diyScenes),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"diyScene\",\n channel: \"scenes\",\n });\n }\n\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"snapshot\")) {\n stateDefs.push({\n id: \"snapshot_cloud\",\n name: tName(\"cloudSnapshot\"),\n desc: tDesc(\"cloudSnapshotDesc\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.snapshots),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"snapshot\",\n channel: \"snapshots\",\n });\n }\n\n // Local snapshots \u2014 light-only feature (capture+restore RGB/segments;\n // makes no sense for thermometer/heater/kettle).\n if (isLight) {\n stateDefs.push({\n id: \"snapshot_local\",\n name: tName(\"localSnapshot\"),\n desc: tDesc(\"localSnapshotDesc\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(localSnapshots ?? []),\n def: \"0\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotLocal\",\n channel: \"snapshots\",\n });\n stateDefs.push({\n id: \"snapshot_save\",\n name: tName(\"saveLocalSnapshot\"),\n desc: tDesc(\"saveLocalSnapshotDesc\"),\n type: \"string\",\n role: \"text\",\n write: true,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotSave\",\n channel: \"snapshots\",\n });\n stateDefs.push({\n id: \"snapshot_delete\",\n name: tName(\"deleteLocalSnapshot\"),\n desc: tDesc(\"deleteLocalSnapshotDesc\"),\n type: \"string\",\n role: \"text\",\n write: true,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotDelete\",\n channel: \"snapshots\",\n });\n }\n\n // Diagnostics \u2014 own top-level `diag` channel on the device. Self-service\n // dump for users to attach to GitHub issues; clustered together so it's\n // immediately recognisable in the object tree.\n stateDefs.push({\n id: \"export\",\n name: tName(\"exportDiagnostics\"),\n type: \"boolean\",\n role: \"button\",\n write: true,\n def: false,\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsExport\",\n channel: \"diag\",\n });\n stateDefs.push({\n id: \"result\",\n name: tName(\"diagnosticsJson\"),\n type: \"string\",\n role: \"json\",\n write: false,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsResult\",\n channel: \"diag\",\n });\n // Trust tier for the user \u2014 verified/reported/seed/unknown. Tells\n // power users whether the experimental toggle would buy them anything,\n // and lets unknown-SKU users see at a glance that their device isn't\n // in the catalogue yet.\n stateDefs.push({\n id: \"tier\",\n name: tName(\"deviceTier\"),\n type: \"string\",\n role: \"text\",\n write: false,\n def: \"unknown\",\n states: {\n verified: tLabel(\"deviceTierVerified\"),\n reported: tLabel(\"deviceTierReported\"),\n seed: tLabel(\"deviceTierSeed\"),\n unknown: tLabel(\"deviceTierUnknown\"),\n },\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsTier\",\n channel: \"diag\",\n });\n\n return stateDefs;\n}\n\n/**\n * Check if a member device supports a given control state.\n * LAN-capable devices support all basic controls.\n *\n * @param member Group member device\n * @param stateId Control state ID (e.g. \"power\", \"brightness\")\n */\nfunction memberHasControlState(member: GoveeDevice, stateId: string): boolean {\n if (member.lanIp) {\n return true;\n }\n const caps = Array.isArray(member.capabilities) ? member.capabilities : [];\n switch (stateId) {\n case \"power\":\n return caps.some(c => c && typeof c.type === \"string\" && c.type.endsWith(\"on_off\"));\n case \"brightness\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"range\") &&\n c.instance === \"brightness\",\n );\n case \"colorRgb\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"color_setting\") &&\n c.instance === \"colorRgb\",\n );\n case \"colorTemperature\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"color_setting\") &&\n (c.instance === \"colorTem\" || c.instance === \"colorTemperatureK\"),\n );\n default:\n return false;\n }\n}\n\n/**\n * Build state definitions for a BaseGroup device.\n * Capabilities = intersection of controllable member devices.\n * No snapshots, no diagnostics, no segments.\n *\n * @param members Resolved member devices\n */\nfunction buildGroupStateDefs(members: GoveeDevice[]): StateDefinition[] {\n const controllable = members.filter(m => m.lanIp || m.channels.cloud);\n if (controllable.length === 0) {\n return [];\n }\n\n const stateDefs: StateDefinition[] = [];\n\n // Control states: intersection of member capabilities\n for (const ld of getDefaultLanStates()) {\n if (controllable.every(m => memberHasControlState(m, ld.id))) {\n stateDefs.push(ld);\n }\n }\n\n // Scenes: intersection of member scene names\n if (controllable.every(m => m.scenes.length > 0)) {\n const firstNames = controllable[0].scenes.map(s => s.name);\n const commonNames = firstNames.filter(name => controllable.every(m => m.scenes.some(s => s.name === name)));\n if (commonNames.length > 0) {\n stateDefs.push({\n id: \"light_scene\",\n name: tName(\"lightScene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(commonNames.map(name => ({ name }))),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"lightScene\",\n channel: \"scenes\",\n });\n }\n }\n\n // Music: intersection of member music libraries\n if (controllable.every(m => m.musicLibrary.length > 0)) {\n const firstNames = controllable[0].musicLibrary.map(m => m.name);\n const commonNames = firstNames.filter(name => controllable.every(m => m.musicLibrary.some(ml => ml.name === name)));\n if (commonNames.length > 0) {\n stateDefs.push({\n id: \"music_mode\",\n name: tName(\"musicMode\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(commonNames.map(name => ({ name }))),\n def: \"0\",\n capabilityType: \"devices.capabilities.music_setting\",\n capabilityInstance: \"musicMode\",\n channel: \"music\",\n });\n }\n }\n\n return stateDefs;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n buildUniqueLabelMap,\n rgbToHex,\n type CloudCapability,\n type CloudStateCapability,\n type GoveeDevice,\n} from \"./types\";\nimport { applyColorTempQuirk } from \"./device-registry\";\nimport { tDesc, tLabel, tName } from \"./i18n-states\";\n\n/** ioBroker state definition derived from a Govee capability */\nexport interface StateDefinition {\n /** State ID suffix (e.g. \"power\", \"brightness\", \"colorRgb\") */\n id: string;\n /**\n * Display name. Plain string for capability-derived names (e.g. from\n * `humanize(cap.instance)` of an unknown Govee capability \u2014 those aren't\n * predictable). For known states, a translation object `{en, de, ru, ...}`\n * built via `tName()` from `i18n-states.ts` \u2014 Admin/vis/Object-Browser\n * pick the user's language automatically.\n */\n name: string | Record<string, string>;\n /**\n * Human-readable description shown in the object browser \u2014 used to clarify\n * ambiguous state names (e.g. cloud vs local snapshots) where the id alone\n * isn't enough for a user to know what the state does.\n */\n desc?: string | Record<string, string>;\n /** ioBroker value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Whether state is writable */\n write: boolean;\n /** Unit string */\n unit?: string;\n /** Min value for numbers */\n min?: number;\n /** Max value for numbers */\n max?: number;\n /**\n * Predefined values for a select (value \u2192 label). Label can be a plain\n * string or a `{en, de, ...}` translation object (ioBroker Admin v6+).\n */\n states?: Record<string, string | Record<string, string>>;\n /** Default value for new states */\n def?: ioBroker.StateValue;\n /** Original capability type */\n capabilityType: string;\n /** Original capability instance */\n capabilityInstance: string;\n /** Target channel (control, scenes, music, snapshots). Defaults to \"control\". */\n channel?: string;\n}\n\n/**\n * Coerce arbitrary value to boolean. Accepts true/1/\"1\"/\"true\" as truthy.\n *\n * @param v Raw value from API\n */\nfunction coerceBool(v: unknown): boolean {\n return v === true || v === 1 || v === \"1\" || v === \"true\";\n}\n\n/**\n * Stringify an unknown raw API value for an ioBroker state. Objects /\n * functions go through JSON.stringify (so we don't get `[object Object]`);\n * everything else takes the primitive `String()` path. Centralised to keep\n * the no-base-to-string lint rule happy at the call sites without\n * sprinkling type assertions all over.\n *\n * @param v Raw value from API\n */\nfunction safeStringify(v: unknown): string {\n switch (typeof v) {\n case \"string\":\n return v;\n case \"number\":\n case \"bigint\":\n case \"boolean\":\n case \"symbol\":\n return v.toString();\n case \"undefined\":\n return \"undefined\";\n default:\n // object, function, null\n return JSON.stringify(v);\n }\n}\n\n/**\n * Coerce arbitrary value to finite number, or null if not parseable.\n *\n * @param v Raw value from API\n */\nfunction coerceNum(v: unknown): number | null {\n if (typeof v === \"number\" && Number.isFinite(v)) {\n return v;\n }\n if (typeof v === \"string\" && v.trim() !== \"\") {\n const n = Number(v);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n return null;\n}\n\n/**\n * Maps Govee Cloud API capabilities to ioBroker state definitions.\n * Pure function \u2014 no side effects, easily testable.\n *\n * @param capabilities Device capabilities from Cloud API\n */\nexport function mapCapabilities(capabilities: CloudCapability[]): StateDefinition[] {\n const states: StateDefinition[] = [];\n\n if (!Array.isArray(capabilities)) {\n return states;\n }\n\n for (const cap of capabilities) {\n const mapped = mapSingleCapability(cap);\n if (mapped) {\n states.push(...mapped);\n }\n }\n\n return states;\n}\n\n/**\n * Probe `capabilities` for a `devices.capabilities.dynamic_scene` entry of\n * the given instance (lightScene / diyScene / snapshot). Used to gate the\n * scene/snapshot dropdowns capability-driven instead of data-driven \u2014 a\n * device that exposes the cap should always show the dropdown, even if\n * the scene list hasn't been fetched yet.\n *\n * @param capabilities Device capabilities from Cloud API\n * @param instance The dynamic_scene instance to look up\n */\nexport function hasDynamicSceneCapability(\n capabilities: CloudCapability[],\n instance: \"lightScene\" | \"diyScene\" | \"snapshot\",\n): boolean {\n if (!Array.isArray(capabilities)) {\n return false;\n }\n return capabilities.some(\n cap =>\n typeof cap?.type === \"string\" &&\n typeof cap?.instance === \"string\" &&\n (cap.type === \"devices.capabilities.dynamic_scene\" || cap.type === \"dynamic_scene\") &&\n cap.instance === instance,\n );\n}\n\n/**\n * Default state definitions for LAN-only devices (no Cloud capabilities).\n * All LAN-capable Govee lights support: power, brightness, color, color temperature.\n */\nexport function getDefaultLanStates(): StateDefinition[] {\n return [\n {\n id: \"power\",\n name: tName(\"power\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: \"lan\",\n capabilityInstance: \"powerSwitch\",\n },\n {\n id: \"brightness\",\n name: tName(\"brightness\"),\n type: \"number\",\n role: \"level.brightness\",\n write: true,\n min: 0,\n max: 100,\n unit: \"%\",\n def: 0,\n capabilityType: \"lan\",\n capabilityInstance: \"brightness\",\n },\n {\n id: \"colorRgb\",\n name: tName(\"colorRgb\"),\n type: \"string\",\n role: \"level.color.rgb\",\n write: true,\n def: \"#000000\",\n capabilityType: \"lan\",\n capabilityInstance: \"colorRgb\",\n },\n {\n id: \"colorTemperature\",\n name: tName(\"colorTemperature\"),\n type: \"number\",\n role: \"level.color.temperature\",\n write: true,\n min: 2000,\n max: 9000,\n unit: \"K\",\n def: 2000,\n capabilityType: \"lan\",\n capabilityInstance: \"colorTemperatureK\",\n },\n ];\n}\n\n/**\n * Map a single capability to state definition(s)\n *\n * @param cap Cloud capability to map\n */\nfunction mapSingleCapability(cap: CloudCapability): StateDefinition[] | null {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n return null;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n\n switch (shortType) {\n case \"on_off\":\n return [\n {\n id: \"power\",\n name: tName(\"power\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"range\":\n return mapRange(cap);\n\n case \"color_setting\":\n return mapColorSetting(cap);\n\n case \"toggle\":\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"mode\":\n return mapMode(cap);\n\n case \"property\":\n return mapProperty(cap);\n\n case \"online\":\n // Handled separately \u2014 not a regular state\n return null;\n\n case \"segment_color_setting\":\n // Segments are handled specially by state-manager\n return [\n {\n id: `_segment_${sanitizeId(cap.instance)}`,\n name: humanize(cap.instance),\n type: \"string\",\n role: \"json\",\n write: true,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"dynamic_scene\":\n // lightScene / diyScene / snapshot get real dropdowns built later in\n // buildDeviceStateDefs from the scenes/snapshots arrays \u2014 skip the\n // generic stub here so we don't create and immediately delete it.\n if (cap.instance === \"lightScene\" || cap.instance === \"diyScene\" || cap.instance === \"snapshot\") {\n return null;\n }\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"string\",\n role: \"json\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n\n case \"work_mode\":\n return mapWorkMode(cap);\n\n case \"temperature_setting\":\n return mapTemperatureSetting(cap);\n\n case \"event\":\n return mapEvent(cap);\n\n case \"music_setting\":\n return mapMusicSetting(cap);\n\n default:\n return null;\n }\n}\n\n/**\n * Map range capability (brightness, humidity, etc.)\n *\n * @param cap Cloud range capability\n */\nfunction mapRange(cap: CloudCapability): StateDefinition[] {\n const range = cap.parameters?.range;\n const isBrightness = cap.instance.toLowerCase().includes(\"brightness\");\n\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"number\",\n role: isBrightness ? \"level.brightness\" : \"level\",\n write: true,\n min: range?.min ?? 0,\n max: range?.max ?? 100,\n unit: normalizeUnit(cap.parameters?.unit),\n def: range?.min ?? 0,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map color_setting capability (RGB or color temperature)\n *\n * @param cap Cloud color setting capability\n */\nfunction mapColorSetting(cap: CloudCapability): StateDefinition[] {\n if (cap.instance === \"colorRgb\") {\n return [\n {\n id: \"colorRgb\",\n name: tName(\"colorRgb\"),\n type: \"string\",\n role: \"level.color.rgb\",\n write: true,\n def: \"#000000\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n if (cap.instance === \"colorTemperatureK\" || cap.instance.includes(\"colorTem\")) {\n const range = cap.parameters?.range;\n return [\n {\n id: \"colorTemperature\",\n name: tName(\"colorTemperature\"),\n type: \"number\",\n role: \"level.color.temperature\",\n write: true,\n min: range?.min ?? 2000,\n max: range?.max ?? 9000,\n unit: \"K\",\n def: range?.min ?? 2000,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n return [];\n}\n\n/**\n * Map mode capability (scenes with ENUM options)\n *\n * @param cap Cloud mode capability\n */\nfunction mapMode(cap: CloudCapability): StateDefinition[] {\n if (cap.instance !== \"presetScene\" || !Array.isArray(cap.parameters?.options)) {\n return [];\n }\n\n const states: Record<string, string> = {};\n for (const opt of cap.parameters.options) {\n if (!opt || typeof opt.name !== \"string\") {\n continue;\n }\n const val = typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value);\n states[val] = opt.name;\n }\n\n return [\n {\n id: \"scene\",\n name: tName(\"scene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map property capability (read-only sensors). Routes to the `sensor`\n * channel so a Heater's temperature reading sits cleanly next to other\n * sensor-style states instead of in `control`.\n *\n * @param cap Cloud property capability\n */\nfunction mapProperty(cap: CloudCapability): StateDefinition[] {\n const instance = cap.instance.toLowerCase();\n let role = \"value\";\n let unit: string | undefined;\n\n if (instance.includes(\"temperature\")) {\n role = \"value.temperature\";\n unit = \"\u00B0C\";\n } else if (instance.includes(\"humidity\")) {\n role = \"value.humidity\";\n unit = \"%\";\n } else if (instance.includes(\"battery\")) {\n role = \"value.battery\";\n unit = \"%\";\n } else if (instance.includes(\"co2\") || instance.includes(\"carbondioxide\")) {\n role = \"value.co2\";\n unit = \"ppm\";\n }\n\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"number\",\n role,\n write: false,\n unit: normalizeUnit(cap.parameters?.unit) ?? unit,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n channel: \"sensor\",\n },\n ];\n}\n\n/**\n * Map work_mode capability (STRUCT \u2014 Govee Heater/Humidifier/Fan/...).\n *\n * Two states max:\n * - `work_mode` \u2014 main mode dropdown (mixed type so users can write\n * either the numeric mode value or the label name)\n * - `mode_value` \u2014 secondary parameter (e.g. fan-speed level for the\n * \"manual\" mode); only created if the API actually exposes one\n *\n * @param cap Cloud work_mode capability\n */\nfunction mapWorkMode(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (!fields || fields.length === 0) {\n return [\n {\n id: \"work_mode\",\n name: tName(\"workMode\"),\n type: \"mixed\",\n role: \"level.mode\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n const states: StateDefinition[] = [];\n const modeField = fields.find(f => f && f.fieldName === \"workMode\");\n if (modeField?.options && modeField.options.length > 0) {\n const modeStates: Record<string, string> = {};\n for (const opt of modeField.options) {\n if (opt && typeof opt.name === \"string\") {\n modeStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n }\n states.push({\n id: \"work_mode\",\n name: tName(\"workMode\"),\n type: \"mixed\",\n role: \"level.mode\",\n write: true,\n states: modeStates,\n def: modeField.options[0] ? safeStringify(modeField.options[0].value) : \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n const valueField = fields.find(f => f && f.fieldName === \"modeValue\");\n if (valueField) {\n if (valueField.options && valueField.options.length > 0) {\n const valStates: Record<string, string> = {};\n for (const opt of valueField.options) {\n if (opt && typeof opt.name === \"string\") {\n valStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n }\n states.push({\n id: \"mode_value\",\n name: tName(\"modeValue\"),\n type: \"mixed\",\n role: \"level\",\n write: true,\n states: valStates,\n def: valueField.options[0] ? safeStringify(valueField.options[0].value) : \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n } else if (valueField.range) {\n states.push({\n id: \"mode_value\",\n name: tName(\"modeValue\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: valueField.range.min,\n max: valueField.range.max,\n def: valueField.range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n }\n\n return states;\n}\n\n/**\n * Map temperature_setting capability \u2014 Heater target-temp slider.\n * Honours the unit reported by the API (\u00B0F or \u00B0C); falls back to \u00B0F\n * because that's the more common Govee Heater default.\n *\n * @param cap Cloud temperature_setting capability\n */\nfunction mapTemperatureSetting(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (Array.isArray(fields) && fields.length > 0) {\n const tempField = fields.find(f => {\n if (!f || typeof f.fieldName !== \"string\") {\n return false;\n }\n if (f.fieldName === \"targetTemperature\") {\n return true;\n }\n return f.fieldName.toLowerCase().includes(\"temperature\");\n });\n if (tempField?.range) {\n const unit = normalizeUnit(cap.parameters?.unit) ?? \"\u00B0F\";\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"number\",\n role: \"level.temperature\",\n write: true,\n min: tempField.range.min,\n max: tempField.range.max,\n unit,\n def: tempField.range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n }\n\n const range = cap.parameters?.range;\n if (range) {\n const unit = normalizeUnit(cap.parameters?.unit) ?? \"\u00B0F\";\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"number\",\n role: \"level.temperature\",\n write: true,\n min: range.min,\n max: range.max,\n unit,\n def: range.min,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n }\n\n // No usable schema \u2014 expose the raw payload so the user at least sees\n // the attempt and can report it. Stays JSON to avoid pretending we\n // understand the structure.\n return [\n {\n id: \"target_temperature\",\n name: tName(\"targetTemperature\"),\n type: \"string\",\n role: \"json\",\n write: true,\n def: \"\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n },\n ];\n}\n\n/**\n * Map event capability (asynchronous OpenAPI-MQTT alarms \u2014 read-only).\n * Each event becomes a boolean indicator in the events/ channel\n * (lackWater, iceFull, bodyAppeared, dirtDetected, \u2026).\n *\n * @param cap Cloud event capability\n */\nfunction mapEvent(cap: CloudCapability): StateDefinition[] {\n return [\n {\n id: sanitizeId(cap.instance),\n name: humanize(cap.instance),\n type: \"boolean\",\n role: \"indicator.alarm\",\n write: false,\n def: false,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n channel: \"events\",\n },\n ];\n}\n\n/**\n * Map music_setting capability to user-friendly states.\n * Parses STRUCT fields into: mode dropdown, sensitivity slider, auto-color toggle.\n *\n * @param cap Cloud music_setting capability\n */\nfunction mapMusicSetting(cap: CloudCapability): StateDefinition[] {\n const fields = cap.parameters?.fields;\n if (!Array.isArray(fields) || fields.length === 0) {\n // No field details from API \u2014 can't create usable states\n return [];\n }\n\n const states: StateDefinition[] = [];\n\n // Mode dropdown \u2014 only if API provides actual mode options\n const modeField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"musicMode\");\n if (modeField?.options && Array.isArray(modeField.options) && modeField.options.length > 0) {\n const modeStates: Record<string, string> = { 0: \"---\" };\n for (const opt of modeField.options) {\n if (!opt || typeof opt.name !== \"string\") {\n continue;\n }\n modeStates[typeof opt.value === \"object\" ? JSON.stringify(opt.value) : String(opt.value)] = opt.name;\n }\n states.push({\n id: \"music_mode\",\n name: tName(\"musicMode\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: modeStates,\n def: \"0\",\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // Sensitivity slider\n const sensField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"sensitivity\");\n if (sensField?.range) {\n states.push({\n id: \"music_sensitivity\",\n name: tName(\"musicSensitivity\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: sensField.range.min,\n max: sensField.range.max,\n unit: \"%\",\n def: sensField.range.max,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // Auto color toggle\n const autoColorField = fields.find(f => f && typeof f.fieldName === \"string\" && f.fieldName === \"autoColor\");\n if (autoColorField) {\n states.push({\n id: \"music_auto_color\",\n name: tName(\"musicAutoColor\"),\n type: \"boolean\",\n role: \"switch\",\n write: true,\n def: true,\n capabilityType: cap.type,\n capabilityInstance: cap.instance,\n });\n }\n\n // All music states belong to the music channel\n for (const s of states) {\n s.channel = \"music\";\n }\n return states;\n}\n\n/**\n * Apply device quirks to mapped state definitions.\n * Corrects wrong API data (e.g. color temperature range) for specific SKUs.\n *\n * @param sku Device model (e.g. \"H60A1\")\n * @param states State definitions to adjust\n */\nexport function applyQuirksToStates(sku: string, states: StateDefinition[]): StateDefinition[] {\n for (const state of states) {\n if (state.id === \"colorTemperature\" && state.min != null && state.max != null) {\n const corrected = applyColorTempQuirk(sku, state.min, state.max);\n state.min = corrected.min;\n state.max = corrected.max;\n state.def = corrected.min;\n }\n }\n return states;\n}\n\n/** Known Govee API unit strings \u2192 ioBroker units */\nconst UNIT_MAP: Record<string, string> = {\n \"unit.percent\": \"%\",\n \"unit.kelvin\": \"K\",\n \"unit.celsius\": \"\u00B0C\",\n \"unit.fahrenheit\": \"\u00B0F\",\n};\n\n/**\n * Normalize Govee API unit string to ioBroker standard\n *\n * @param unit Raw unit string from API\n */\nfunction normalizeUnit(unit?: string): string | undefined {\n if (!unit) {\n return undefined;\n }\n return UNIT_MAP[unit] ?? unit;\n}\n\n/**\n * Sanitize a string for use as ioBroker state ID\n *\n * @param str Input string to sanitize\n */\nfunction sanitizeId(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1_$2\")\n .replace(/[^a-zA-Z0-9_]/g, \"_\")\n .toLowerCase();\n}\n\n/**\n * Convert camelCase to human-readable name\n *\n * @param str camelCase input string\n */\nfunction humanize(str: string): string {\n // Reihenfolge: erst Underscore \u2192 Space, dann camelCase-split, dann\n // trim + erstes Zeichen uppercase. Vorher: leading-underscore-IDs\n // (z.B. `_segment_color`) wurden zu ` segment color` mit leading\n // Space und ohne Capitalization (^\\w matched space, nicht word-char).\n return str\n .replace(/_/g, \" \")\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .trim()\n .replace(/^./, c => c.toUpperCase());\n}\n\n/** Mapped Cloud state value: state ID + converted value */\nexport interface CloudStateValue {\n /** State ID in control/ channel (e.g. \"power\", \"brightness\", \"gradient_toggle\") */\n stateId: string;\n /** Converted value ready for ioBroker setStateAsync */\n value: ioBroker.StateValue;\n}\n\n/**\n * Map a Cloud device state capability to a state ID + converted value.\n * Uses the same ID logic as mapCapabilities so IDs always match.\n *\n * @param cap Cloud state capability with current value\n */\nexport function mapCloudStateValue(cap: CloudStateCapability): CloudStateValue | null {\n if (!cap || typeof cap.type !== \"string\" || typeof cap.instance !== \"string\") {\n return null;\n }\n const shortType = cap.type.replace(\"devices.capabilities.\", \"\");\n const raw = cap.state?.value;\n if (raw === undefined || raw === null) {\n return null;\n }\n\n switch (shortType) {\n case \"on_off\":\n return { stateId: \"power\", value: coerceBool(raw) };\n\n case \"range\": {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: sanitizeId(cap.instance), value: n };\n }\n\n case \"color_setting\":\n if (cap.instance === \"colorRgb\") {\n const num = coerceNum(raw) ?? 0;\n return {\n stateId: \"colorRgb\",\n value: rgbToHex((num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff),\n };\n }\n if (cap.instance.includes(\"colorTem\")) {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: \"colorTemperature\", value: n };\n }\n return null;\n\n case \"toggle\":\n return { stateId: sanitizeId(cap.instance), value: coerceBool(raw) };\n\n case \"mode\":\n if (cap.instance === \"presetScene\") {\n return {\n stateId: \"scene\",\n value: safeStringify(raw),\n };\n }\n return null;\n\n case \"dynamic_scene\":\n return {\n stateId: sanitizeId(cap.instance),\n value: safeStringify(raw),\n };\n\n case \"work_mode\": {\n // STRUCT: { workMode: <number>, modeValue?: <number> }. Cloud\n // /device/state only returns the primary mode here \u2014 mode_value\n // (sub-parameter) follows via MQTT status push when the device\n // reports it, so we don't lose it just because it isn't in the\n // initial state response.\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const n = coerceNum(struct.workMode);\n if (n !== null) {\n return { stateId: \"work_mode\", value: n };\n }\n }\n const direct = coerceNum(raw);\n if (direct !== null) {\n return { stateId: \"work_mode\", value: direct };\n }\n return null;\n }\n\n case \"temperature_setting\": {\n // STRUCT: { targetTemperature: <number>, temperatureUnit?: ... }\n // \u2014 fall back to direct number for adapters that simplify.\n const direct = coerceNum(raw);\n if (direct !== null) {\n return { stateId: \"target_temperature\", value: direct };\n }\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const temp = struct.targetTemperature ?? struct.temperature ?? struct.temp;\n const n = coerceNum(temp);\n if (n !== null) {\n return { stateId: \"target_temperature\", value: n };\n }\n }\n return null;\n }\n\n case \"event\":\n return {\n stateId: sanitizeId(cap.instance),\n value: coerceBool(raw),\n };\n\n case \"music_setting\":\n // Extract mode value from STRUCT state\n if (typeof raw === \"object\" && raw !== null) {\n const struct = raw as Record<string, unknown>;\n const mode = coerceNum(struct.musicMode);\n return {\n stateId: \"music_mode\",\n value: mode !== null ? String(mode) : \"0\",\n };\n }\n return null;\n\n case \"property\": {\n const n = coerceNum(raw);\n if (n === null) {\n return null;\n }\n return { stateId: sanitizeId(cap.instance), value: n };\n }\n\n default:\n return null;\n }\n}\n\n/**\n * Plan the per-state writes for a list of synthesised Cloud-state\n * capabilities. Used by the App-API poll and the OpenAPI-MQTT event\n * handler (both call into `applyCloudCapabilities` on the adapter side).\n *\n * Returns the resolved `(stateId, value)` pairs for capabilities that:\n * - decode via `mapCloudStateValue` to a non-null result, AND\n * - aren't shadowed by the LAN-state set when the device is LAN-capable\n * (lights with `lanIp` shouldn't have their LAN sub-second updates\n * overwritten by a Cloud-source value).\n *\n * Pure function \u2014 no adapter state, no I/O \u2014 so the LAN-shadow logic is\n * unit-testable independent of the live state-write pipeline.\n *\n * @param caps Capabilities to consider\n * @param hasLanIp Whether the target device has a known LAN IP\n * @param lanStateIds Default-LAN state IDs that LAN delivers authoritatively\n */\nexport function planCloudCapabilityWrites(\n caps: CloudStateCapability[],\n hasLanIp: boolean,\n lanStateIds: Set<string>,\n): CloudStateValue[] {\n const writes: CloudStateValue[] = [];\n if (!Array.isArray(caps)) {\n return writes;\n }\n for (const cap of caps) {\n const mapped = mapCloudStateValue(cap);\n if (!mapped) {\n continue;\n }\n if (hasLanIp && lanStateIds.has(mapped.stateId)) {\n continue;\n }\n writes.push(mapped);\n }\n return writes;\n}\n\n/**\n * Build complete state definitions for a device.\n * Combines LAN defaults, Cloud capabilities, quirks, scenes, snapshots, and diagnostics.\n * For groups: computes capability intersection of member devices (no snapshots/diagnostics).\n *\n * @param device Govee device with capabilities, scenes, etc.\n * @param localSnapshots Optional local snapshot names\n * @param memberDevices Resolved member devices (only for BaseGroup)\n */\nexport function buildDeviceStateDefs(\n device: GoveeDevice,\n localSnapshots?: { name: string }[],\n memberDevices?: GoveeDevice[],\n): StateDefinition[] {\n if (device.sku === \"BaseGroup\") {\n return buildGroupStateDefs(memberDevices || []);\n }\n let stateDefs: StateDefinition[];\n\n if (device.lanIp) {\n stateDefs = getDefaultLanStates();\n if (device.capabilities.length > 0) {\n const lanIds = new Set(stateDefs.map(d => d.id));\n const cloudDefs = mapCapabilities(device.capabilities);\n for (const cd of cloudDefs) {\n if (!lanIds.has(cd.id)) {\n stateDefs.push(cd);\n }\n }\n }\n } else {\n stateDefs = mapCapabilities(device.capabilities);\n }\n\n applyQuirksToStates(device.sku, stateDefs);\n\n // Light-only state defs \u2014 scenes / snapshots / music / scene_speed only\n // make sense for lights. Sensors and appliances would otherwise see\n // empty snapshot dropdowns and a useless save/delete button pair.\n const isLight = device.type === \"devices.types.light\";\n\n // Capability-driven: if the device's Cloud capabilities expose the\n // dynamic_scene instance, always create the dropdown \u2014 even before\n // /device/scenes has been queried (an empty list still beats a\n // missing state). Guards against the H61D5-style case where the\n // user's first restart shows no scenes / snapshots datapoints.\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"lightScene\")) {\n stateDefs.push({\n id: \"light_scene\",\n name: tName(\"lightScene\"),\n // mixed lets users write the index (\"1\"), the index as number (1),\n // or the scene name (\"Aurora\") \u2014 the onStateChange handler resolves\n // all three forms via the common.states map.\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.scenes),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"lightScene\",\n channel: \"scenes\",\n });\n }\n\n // Scene speed slider \u2014 only if any scene supports speed adjustment\n const maxSpeedLevel = device.sceneLibrary.reduce((max, entry) => {\n if (entry.speedInfo?.supSpeed && entry.speedInfo.config) {\n try {\n const parsed = JSON.parse(entry.speedInfo.config) as unknown;\n // Config can drift \u2014 if not an array, skip this entry silently\n if (!Array.isArray(parsed)) {\n return max;\n }\n for (const cfg of parsed as Array<{ moveIn?: number[] }>) {\n if (cfg && Array.isArray(cfg.moveIn) && cfg.moveIn.length - 1 > max) {\n max = cfg.moveIn.length - 1;\n }\n }\n } catch {\n /* ignore invalid config JSON */\n }\n }\n return max;\n }, -1);\n if (isLight && maxSpeedLevel > 0) {\n stateDefs.push({\n id: \"scene_speed\",\n name: tName(\"sceneSpeed\"),\n type: \"number\",\n role: \"level\",\n write: true,\n min: 0,\n max: maxSpeedLevel,\n def: 0,\n capabilityType: \"local\",\n capabilityInstance: \"sceneSpeed\",\n channel: \"scenes\",\n });\n }\n\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"diyScene\")) {\n stateDefs.push({\n id: \"diy_scene\",\n name: tName(\"diyScene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.diyScenes),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"diyScene\",\n channel: \"scenes\",\n });\n }\n\n if (isLight && hasDynamicSceneCapability(device.capabilities, \"snapshot\")) {\n stateDefs.push({\n id: \"snapshot_cloud\",\n name: tName(\"cloudSnapshot\"),\n desc: tDesc(\"cloudSnapshotDesc\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(device.snapshots),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"snapshot\",\n channel: \"snapshots\",\n });\n }\n\n // Per-device refresh button \u2014 only when the device exposes any dynamic-scene\n // capability. Thermometer/sensor/heater devices don't have scenes or\n // snapshots so the button would be inert noise on them. Replaces the global\n // info.refresh_cloud_data button (removed in v2.7.0) \u2014 5 API calls per\n // device-targeted refresh instead of N*5 for the whole account.\n if (\n isLight &&\n (hasDynamicSceneCapability(device.capabilities, \"lightScene\") ||\n hasDynamicSceneCapability(device.capabilities, \"diyScene\") ||\n hasDynamicSceneCapability(device.capabilities, \"snapshot\"))\n ) {\n stateDefs.push({\n id: \"refresh_cloud\",\n name: tName(\"refreshCloud\"),\n desc: tDesc(\"refreshCloudDesc\"),\n type: \"boolean\",\n role: \"button\",\n write: true,\n def: false,\n capabilityType: \"local\",\n capabilityInstance: \"refreshCloud\",\n channel: \"snapshots\",\n });\n }\n\n // Local snapshots \u2014 light-only feature (capture+restore RGB/segments;\n // makes no sense for thermometer/heater/kettle).\n if (isLight) {\n stateDefs.push({\n id: \"snapshot_local\",\n name: tName(\"localSnapshot\"),\n desc: tDesc(\"localSnapshotDesc\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(localSnapshots ?? []),\n def: \"0\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotLocal\",\n channel: \"snapshots\",\n });\n stateDefs.push({\n id: \"snapshot_save\",\n name: tName(\"saveLocalSnapshot\"),\n desc: tDesc(\"saveLocalSnapshotDesc\"),\n type: \"string\",\n role: \"text\",\n write: true,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotSave\",\n channel: \"snapshots\",\n });\n stateDefs.push({\n id: \"snapshot_delete\",\n name: tName(\"deleteLocalSnapshot\"),\n desc: tDesc(\"deleteLocalSnapshotDesc\"),\n type: \"string\",\n role: \"text\",\n write: true,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"snapshotDelete\",\n channel: \"snapshots\",\n });\n }\n\n // Diagnostics \u2014 own top-level `diag` channel on the device. Self-service\n // dump for users to attach to GitHub issues; clustered together so it's\n // immediately recognisable in the object tree.\n stateDefs.push({\n id: \"export\",\n name: tName(\"exportDiagnostics\"),\n type: \"boolean\",\n role: \"button\",\n write: true,\n def: false,\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsExport\",\n channel: \"diag\",\n });\n stateDefs.push({\n id: \"result\",\n name: tName(\"diagnosticsJson\"),\n type: \"string\",\n role: \"json\",\n write: false,\n def: \"\",\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsResult\",\n channel: \"diag\",\n });\n // Trust tier for the user \u2014 verified/reported/seed/unknown. Tells\n // power users whether the experimental toggle would buy them anything,\n // and lets unknown-SKU users see at a glance that their device isn't\n // in the catalogue yet.\n stateDefs.push({\n id: \"tier\",\n name: tName(\"deviceTier\"),\n type: \"string\",\n role: \"text\",\n write: false,\n def: \"unknown\",\n states: {\n verified: tLabel(\"deviceTierVerified\"),\n reported: tLabel(\"deviceTierReported\"),\n seed: tLabel(\"deviceTierSeed\"),\n unknown: tLabel(\"deviceTierUnknown\"),\n },\n capabilityType: \"local\",\n capabilityInstance: \"diagnosticsTier\",\n channel: \"diag\",\n });\n\n return stateDefs;\n}\n\n/**\n * Check if a member device supports a given control state.\n * LAN-capable devices support all basic controls.\n *\n * @param member Group member device\n * @param stateId Control state ID (e.g. \"power\", \"brightness\")\n */\nfunction memberHasControlState(member: GoveeDevice, stateId: string): boolean {\n if (member.lanIp) {\n return true;\n }\n const caps = Array.isArray(member.capabilities) ? member.capabilities : [];\n switch (stateId) {\n case \"power\":\n return caps.some(c => c && typeof c.type === \"string\" && c.type.endsWith(\"on_off\"));\n case \"brightness\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"range\") &&\n c.instance === \"brightness\",\n );\n case \"colorRgb\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"color_setting\") &&\n c.instance === \"colorRgb\",\n );\n case \"colorTemperature\":\n return caps.some(\n c =>\n c &&\n typeof c.type === \"string\" &&\n typeof c.instance === \"string\" &&\n c.type.endsWith(\"color_setting\") &&\n (c.instance === \"colorTem\" || c.instance === \"colorTemperatureK\"),\n );\n default:\n return false;\n }\n}\n\n/**\n * Build state definitions for a BaseGroup device.\n * Capabilities = intersection of controllable member devices.\n * No snapshots, no diagnostics, no segments.\n *\n * @param members Resolved member devices\n */\nfunction buildGroupStateDefs(members: GoveeDevice[]): StateDefinition[] {\n const controllable = members.filter(m => m.lanIp || m.channels.cloud);\n if (controllable.length === 0) {\n return [];\n }\n\n const stateDefs: StateDefinition[] = [];\n\n // Control states: intersection of member capabilities\n for (const ld of getDefaultLanStates()) {\n if (controllable.every(m => memberHasControlState(m, ld.id))) {\n stateDefs.push(ld);\n }\n }\n\n // Scenes: intersection of member scene names\n if (controllable.every(m => m.scenes.length > 0)) {\n const firstNames = controllable[0].scenes.map(s => s.name);\n const commonNames = firstNames.filter(name => controllable.every(m => m.scenes.some(s => s.name === name)));\n if (commonNames.length > 0) {\n stateDefs.push({\n id: \"light_scene\",\n name: tName(\"lightScene\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(commonNames.map(name => ({ name }))),\n def: \"0\",\n capabilityType: \"devices.capabilities.dynamic_scene\",\n capabilityInstance: \"lightScene\",\n channel: \"scenes\",\n });\n }\n }\n\n // Music: intersection of member music libraries\n if (controllable.every(m => m.musicLibrary.length > 0)) {\n const firstNames = controllable[0].musicLibrary.map(m => m.name);\n const commonNames = firstNames.filter(name => controllable.every(m => m.musicLibrary.some(ml => ml.name === name)));\n if (commonNames.length > 0) {\n stateDefs.push({\n id: \"music_mode\",\n name: tName(\"musicMode\"),\n type: \"mixed\",\n role: \"text\",\n write: true,\n states: buildUniqueLabelMap(commonNames.map(name => ({ name }))),\n def: \"0\",\n capabilityType: \"devices.capabilities.music_setting\",\n capabilityInstance: \"musicMode\",\n channel: \"music\",\n });\n }\n }\n\n return stateDefs;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -315,58 +315,73 @@ class DeviceManager {
315
315
  }
316
316
  }
317
317
  /**
318
- * Re-fetch scenes, snapshots AND libraries for all known light devices
319
- * without re-running the full Cloud bootstrap. Used by the
320
- * `info.refresh_cloud_data` button: "new snapshot/scene was saved in
321
- * the Govee Home app, show it here".
318
+ * Re-fetch scenes, snapshots and libraries for one specific device. Triggered
319
+ * by the per-device `snapshots.refresh_cloud` button ("a new snapshot/scene
320
+ * was saved in the Govee Home app, show it here for THIS light").
322
321
  *
323
- * v1.10.1 had skipped libraries to keep the per-click call count low —
324
- * but for users on accounts where a library actually grew (new
325
- * scene-set rolled out by Govee, new sceneCode minted) the only way
326
- * back to fresh data was a full adapter restart. v2.1.0 reinstates the
327
- * library refresh on the same button and forces past the
328
- * `length === 0` guards inside `loadDeviceLibraries`.
322
+ * Three Cloud calls happen in order:
323
+ * 1. `/user/devices` refreshes the whole capability set including the
324
+ * authoritative snapshot-options list (this is what was missing in
325
+ * v2.6.7's refresh path: stale capabilities meant the snapshot fallback
326
+ * in `loadDeviceScenes` couldn't see new entries).
327
+ * 2. `/device/scenes` + `/device/diy-scenes` (per loadDeviceScenes)
328
+ * 3. `/appsku/v1/light-effect-libraries` × 3 (scene/music/DIY via
329
+ * loadDeviceLibraries with force=true)
329
330
  *
330
- * @returns true when any device's scene/snapshot/library data changed
331
+ * Replaces the global `refreshSceneData()` removed in v2.7.0: refreshing all
332
+ * lights cost N*5 Cloud calls vs 5 for the one device the user actually
333
+ * touched. Rate-limit pressure scales linearly with account size.
334
+ *
335
+ * @param deviceId Target device's deviceId (mac-like identifier)
336
+ * @returns true when scene/snapshot/library data changed
331
337
  */
332
- async refreshSceneData() {
338
+ async refreshSceneDataForDevice(deviceId) {
333
339
  var _a;
334
340
  if (!this.cloudClient) {
335
341
  return false;
336
342
  }
337
- let anyChanged = false;
338
- const lights = Array.from(this.devices.values()).filter((d) => d.type === "devices.types.light");
339
- for (const dev of lights) {
340
- this.diagnostics.addLog(dev.deviceId, "info", `User-triggered refresh-cloud-data for ${dev.sku}`);
343
+ const target = Array.from(this.devices.values()).find(
344
+ (d) => (0, import_types.normalizeDeviceId)(d.deviceId) === (0, import_types.normalizeDeviceId)(deviceId)
345
+ );
346
+ if (!target) {
347
+ this.log.debug(`refreshSceneDataForDevice: device ${deviceId} not found`);
348
+ return false;
341
349
  }
342
- for (const device of lights) {
343
- const cd = {
344
- sku: device.sku,
345
- device: device.deviceId,
346
- deviceName: device.name,
347
- type: device.type,
348
- capabilities: Array.isArray(device.capabilities) ? device.capabilities : []
349
- };
350
- if (await this.loadDeviceScenes(device, cd)) {
351
- anyChanged = true;
352
- }
353
- if (await this.loadDeviceLibraries(
354
- device,
355
- cd.sku,
356
- /* force */
357
- true
358
- )) {
359
- anyChanged = true;
360
- }
350
+ this.diagnostics.addLog(target.deviceId, "info", `User-triggered refresh-cloud-data for ${target.sku}`);
351
+ try {
352
+ const rawCloudDevices = await this.cloudClient.getDevices();
353
+ const cloudDevices = Array.isArray(rawCloudDevices) ? rawCloudDevices.filter(
354
+ (cd2) => cd2 && typeof cd2.sku === "string" && typeof cd2.device === "string" && Array.isArray(cd2.capabilities) && cd2.capabilities.length > 0
355
+ ) : [];
356
+ this.mergeCloudDevices(cloudDevices);
357
+ } catch (e) {
358
+ this.log.debug(`refreshSceneDataForDevice: getDevices failed: ${(0, import_types.errMessage)(e)}`);
361
359
  }
362
- if (anyChanged) {
360
+ const cd = {
361
+ sku: target.sku,
362
+ device: target.deviceId,
363
+ deviceName: target.name,
364
+ type: target.type,
365
+ capabilities: Array.isArray(target.capabilities) ? target.capabilities : []
366
+ };
367
+ let changed = false;
368
+ if (await this.loadDeviceScenes(target, cd)) {
369
+ changed = true;
370
+ }
371
+ if (await this.loadDeviceLibraries(
372
+ target,
373
+ cd.sku,
374
+ /* force */
375
+ true
376
+ )) {
377
+ changed = true;
378
+ }
379
+ if (changed) {
363
380
  this.saveDevicesToCache();
364
- for (const device of this.devices.values()) {
365
- cacheHelpers.populateScenesFromLibrary(this, device);
366
- }
381
+ cacheHelpers.populateScenesFromLibrary(this, target);
367
382
  (_a = this.onDeviceListChanged) == null ? void 0 : _a.call(this, this.getDevices());
368
383
  }
369
- return anyChanged;
384
+ return changed;
370
385
  }
371
386
  /**
372
387
  * Merge Cloud device list into local device map.
@@ -388,18 +403,19 @@ class DeviceManager {
388
403
  async loadDeviceScenes(device, cd) {
389
404
  var _a;
390
405
  this.diagnostics.addLog(cd.device, "debug", `loadDeviceScenes called for ${cd.sku}`);
406
+ let scenesCallSucceeded = false;
407
+ let snapsFromScenesCall = [];
391
408
  const loadScenes = async () => {
392
409
  try {
393
410
  const { lightScenes, diyScenes, snapshots } = await this.cloudClient.getScenes(cd.sku, cd.device);
411
+ scenesCallSucceeded = true;
412
+ snapsFromScenesCall = snapshots;
394
413
  if (lightScenes.length > 0) {
395
414
  device.scenes = lightScenes;
396
415
  }
397
416
  if (diyScenes.length > 0) {
398
417
  device.diyScenes = diyScenes;
399
418
  }
400
- if (snapshots.length > 0) {
401
- device.snapshots = snapshots;
402
- }
403
419
  } catch (e) {
404
420
  this.diagnostics.recordApiFailure(cd.device, "/router/api/v1/device/scenes", e, this.extractStatus(e));
405
421
  this.log.debug(`Could not load scenes for ${cd.sku}: ${(0, import_types.errMessage)(e)}`);
@@ -420,7 +436,9 @@ class DeviceManager {
420
436
  };
421
437
  await this.commandRouter.executeRateLimited(loadDiy, 2);
422
438
  }
423
- if (device.snapshots.length === 0) {
439
+ if (snapsFromScenesCall.length > 0) {
440
+ device.snapshots = snapsFromScenesCall;
441
+ } else if (scenesCallSucceeded) {
424
442
  const caps = Array.isArray(cd.capabilities) ? cd.capabilities : [];
425
443
  const snapCap = caps.find(
426
444
  (c) => {