iobroker.govee-smart 2.32.1 → 2.34.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.
Files changed (51) hide show
  1. package/README.md +24 -19
  2. package/admin/custom/assets/index-C-fBGUPX.js +1 -0
  3. package/admin/custom/assets/{mf-entry-bootstrap-0-cd5e50f5.js → mf-entry-bootstrap-0-0502dc7c.js} +1 -1
  4. package/admin/custom/i18n/de.json +1 -1
  5. package/admin/custom/i18n/en.json +1 -1
  6. package/admin/custom/i18n/es.json +1 -1
  7. package/admin/custom/i18n/fr.json +1 -1
  8. package/admin/custom/i18n/it.json +1 -1
  9. package/admin/custom/i18n/nl.json +1 -1
  10. package/admin/custom/i18n/pl.json +1 -1
  11. package/admin/custom/i18n/pt.json +1 -1
  12. package/admin/custom/i18n/ru.json +1 -1
  13. package/admin/custom/i18n/uk.json +1 -1
  14. package/admin/custom/i18n/zh-cn.json +1 -1
  15. package/build/lib/capability-mapper.js +203 -52
  16. package/build/lib/capability-mapper.js.map +3 -3
  17. package/build/lib/command-router.js +6 -5
  18. package/build/lib/command-router.js.map +2 -2
  19. package/build/lib/device-manager/cloud-merge.js +8 -2
  20. package/build/lib/device-manager/cloud-merge.js.map +2 -2
  21. package/build/lib/device-manager/library-loader.js +3 -1
  22. package/build/lib/device-manager/library-loader.js.map +2 -2
  23. package/build/lib/device-manager/lookups.js +20 -0
  24. package/build/lib/device-manager/lookups.js.map +2 -2
  25. package/build/lib/device-manager.js +32 -4
  26. package/build/lib/device-manager.js.map +2 -2
  27. package/build/lib/device-registry.js +2 -1
  28. package/build/lib/device-registry.js.map +2 -2
  29. package/build/lib/diagnostics.js +10 -7
  30. package/build/lib/diagnostics.js.map +2 -2
  31. package/build/lib/govee-cloud-client.js +10 -7
  32. package/build/lib/govee-cloud-client.js.map +2 -2
  33. package/build/lib/govee-constants.js +3 -1
  34. package/build/lib/govee-constants.js.map +2 -2
  35. package/build/lib/govee-mqtt-client.js +2 -1
  36. package/build/lib/govee-mqtt-client.js.map +2 -2
  37. package/build/lib/handlers/cloud-state-loader.js +2 -6
  38. package/build/lib/handlers/cloud-state-loader.js.map +2 -2
  39. package/build/lib/handlers/dropdown-reset-helpers.js +3 -0
  40. package/build/lib/handlers/dropdown-reset-helpers.js.map +2 -2
  41. package/build/lib/handlers/state-change-router.js +69 -4
  42. package/build/lib/handlers/state-change-router.js.map +2 -2
  43. package/build/lib/state-manager.js +2 -2
  44. package/build/lib/state-manager.js.map +2 -2
  45. package/build/lib/types.js.map +2 -2
  46. package/build/main.js +9 -0
  47. package/build/main.js.map +2 -2
  48. package/devices.json +3396 -474
  49. package/io-package.json +27 -27
  50. package/package.json +5 -5
  51. package/admin/custom/assets/index-CStRjtPo.js +0 -1
package/README.md CHANGED
@@ -103,6 +103,30 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
103
103
  ### **WORK IN PROGRESS**
104
104
  -->
105
105
 
106
+ ### 2.34.0 (2026-09-10)
107
+
108
+ - Fixed: Air purifiers, heaters, humidifiers and fans — choosing a mode or a speed now reaches the device, where the adapter used to send a value Govee rejected as "Invalid parameter type" (#47)
109
+ - Fixed: The speed selector of an air purifier now offers the levels the device actually has, instead of the single unusable entry it showed before (#47)
110
+ - Fixed: On an appliance updating from an older version the level datapoint accepts values again — it kept the selection list of the previous version and refused every write against it
111
+ - Changed: On appliances whose modes share the same level numbers — kettles, some fans and humidifiers — the level is a plain number now; a selection list could only ever show one mode's levels
112
+ - Fixed: An installation with no light at all now reads its device states at start — filter life, air quality and every other reported value stayed empty forever (#47)
113
+ - Fixed: A heater's target temperature is sent in the shape the Govee API asks for, and the datapoint is labelled in the unit the heater itself reports — a 5–30 °C heater used to read °F
114
+ - Fixed: The current speed level now arrives from the cloud together with the mode — until now only the mode updated while the level datapoint kept showing its default
115
+ - Fixed: A command the Govee cloud rejects no longer counts as successful, so the datapoint stops showing a change the device never made, and the reason is named
116
+ - New: A device's night-light scene is selectable — the adapter received the scene list and the current scene from Govee and threw both away without creating a datapoint
117
+ - Fixed: The scene dropdown's "---" entry now carries the same value the adapter writes when it resets the dropdown, so the entry stopped being rewritten on every start
118
+ - New: The H7127 air purifier is confirmed by a user report — it is no longer listed as untested and no longer asks for the experimental switch at start
119
+ - Changed: The diagnostics report no longer repeats the privacy note the export button already shows, and says instead what only the file itself can say
120
+
121
+ ### 2.33.0 (2026-09-08)
122
+
123
+ - Fixed: A light without a local API stays reachable while it reports its own state — Govee's device list lagged behind the bulb and overrode it every two minutes (reported for the H600D)
124
+ - Fixed: A status message the Govee cloud replays after a reconnect no longer counts as a fresh sign of life for the next half hour
125
+ - New: The H600D GU10 smart bulb is recognised from a user report
126
+ - New: 486 more Govee models start as experimental — every model the homebridge-govee project lists as of September 2026, from bulbs and strips to fans, heaters and ice makers
127
+ - New: An experimental model is tried by enabling "experimental device support"; a diagnostics report from the Expert tab confirms it for everyone
128
+ - Changed: The wiki's device list folds each device type into one block with its counts, so 602 entries stay readable
129
+
106
130
  ### 2.32.1 (2026-09-07)
107
131
 
108
132
  - Fixed: Your devices and their recorded history no longer disappear from the object tree when the Govee cloud cannot be reached at startup
@@ -124,25 +148,6 @@ This adapter's MQTT authentication and BLE-over-LAN (ptReal) protocol implementa
124
148
 
125
149
  - Fixed: When the adapter met a device model it does not know yet, its log asked the user to press a button that 2.31.0 had removed — it now points at the Expert tab, where the report is actually made
126
150
 
127
- ### 2.31.0 (2026-09-03)
128
-
129
- - Fixed: On instances upgraded from 2.27.0 or newer, every admin card was dead — diagnostics, segment wizard and connection test alike; affected installations repair themselves on the next start
130
- - Fixed: A card that could not reach the adapter reported "no devices yet" instead of the real error
131
- - Changed: Segment detection and diagnostics now share one **Expert** tab with a button each
132
- - Changed: The per-device `diag.export` button is gone; the Expert tab builds the report and hands you the file in one press
133
- - Changed: `diag.lastExport` now records WHEN the last report was taken, instead of naming the file
134
- - Improved: Both cards say "Loading devices …" while they search, and explain the wait if it takes long
135
- - Fixed: The diagnostics report still described the reachability rule as it was before 2.30.0
136
-
137
- ### 2.30.0 (2026-09-03)
138
-
139
- - Fixed: Devices without a local API were shown as unreachable although they switched and reported normally; they now show as reachable for as long as they are
140
- - Fixed: Sensors and buttons behind a Govee gateway were shown as unreachable although their readings kept arriving; their gateway now decides whether they are reachable
141
- - Fixed: Appliances stayed marked as reachable for up to two minutes after they had actually gone offline
142
- - Improved: A device that is unplugged and put away is reported as unreachable within half an hour, instead of staying green until the adapter is restarted
143
- - New: 37 additional device models are recognised, including smart plugs, a button remote, an air quality monitor and an aroma diffuser
144
- - New: Battery buttons and remotes are supported as their own device kind, with battery level and reachability
145
-
146
151
  [Older changelogs can be found there](CHANGELOG_OLD.md)
147
152
 
148
153
  ## Support
@@ -0,0 +1 @@
1
+ import"./virtual_mf-exposes___mfe_internal__ConfigCustomGoveeSegmentSet__customComponents_js-DtuKK_L7.js";import"./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomGoveeSegmentSet__customComponents_js-TmDuENES.js";import{$t as e,Ba as t,La as n,Qr as r,Ya as i,an as a,cn as o,ho as s,ya as c,za as l}from"./_virtual_mf___mfe_internal__ConfigCustomGoveeSegmentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BqaV2gc_.js";import{t as u}from"./_virtual_mf___mfe_internal__ConfigCustomGoveeSegmentSet__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-ld5_knuQ.js";import{n as d,t as f}from"./ExpertPanel-BnVeJBS6.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})(),s();var p={gsw_deviceLabel:`Device`,gsw_noDevices:`No segment-capable devices found. Enter a Cloud API key in the adapter settings so device capabilities are known.`,gsw_start:`Start measuring`,gsw_measureHint:`Watch the strip — which segment lights up bright white?`,gsw_question:`Segment %s — is it lit?`,gsw_lit:`Lit`,gsw_dark:`Dark (gap)`,gsw_finish:`Finished — end of strip`,gsw_cancel:`Cancel`,gsw_reviewHint:`Click a cell to flip lit / gap, then apply.`,gsw_resultNoGaps:`%s segments, no gaps`,gsw_resultGaps:`%s segments, %s gap(s)`,gsw_apply:`Apply`,gsw_remeasure:`Measure again`,gsw_applied:`Applied: %s segments.`,gsw_error:`Wizard error: %s`,gsw_close:`Close`,gsw_conn_lan_title:`LAN — always on`,gsw_conn_lan_desc:`Local control for lights. No credentials needed.`,gsw_conn_cloud_title:`Cloud API key`,gsw_conn_cloud_desc:`Optional. Adds device names, scenes, snapshots and segments.`,gsw_conn_apikey_label:`Govee API key`,gsw_conn_rt_title:`Real-time status (Govee account)`,gsw_conn_rt_desc:`Optional. Enables live status push over the Govee cloud (AWS IoT).`,gsw_conn_email_label:`Account email`,gsw_conn_password_label:`Account password`,gsw_conn_connect_btn:`Connect`,gsw_conn_2fa_hint:`Govee sent a verification code by email. Enter it here to finish sign-in.`,gsw_conn_code_label:`Verification code`,gsw_conn_request_btn:`Request new code`,gsw_conn_verify_btn:`Confirm code`,gsw_conn_st_ok:`Connected. Save the settings to keep real-time status enabled.`,gsw_conn_st_verifyRequired:`Govee needs a verification code for this device. Check your email and enter the code.`,gsw_conn_st_codeInvalid:`The verification code is invalid. Request a new one and try again.`,gsw_conn_st_passwordRejected:`Email or password was rejected. Please check your credentials.`,gsw_conn_st_emailNotRegistered:`This email is not registered with Govee.`,gsw_conn_st_rateLimited:`Too many attempts. Wait a while before trying again.`,gsw_conn_st_accountLocked:`The account is temporarily locked by Govee. Try again later.`,gsw_conn_st_loginFailed:`Login failed. Please try again.`,gsw_conn_st_mqttNotUp:`Real-time service is not ready yet. Try again in a moment.`,gsw_conn_st_codeSent:`A new verification code was sent to your email.`,gsw_conn_st_codeRejected:`Govee rejected the verification code. Request a new one.`,gsw_conn_st_needCredentials:`Enter email and password first.`,gsw_conn_st_throttled:`Please wait a moment before requesting another code.`,gsw_conn_st_unknownAction:`Unknown action.`,gsw_diagIntro:`Pick a device and press the button: the adapter builds a diagnostics report and your browser saves it as a file. Attach that file to a GitHub issue — the issue forms ask for exactly it.`,gsw_diagDevice:`Device`,gsw_diagExport:`Create report and download`,gsw_diagDone:`Saved as %s.`,gsw_diagOpenIssue:`Open an issue`,gsw_diagNoDevices:`No devices yet. The report is per device — once the adapter has found one, it appears here.`,gsw_diagExportFailed:`The report could not be created. The adapter log says why.`,gsw_diagPrivacy:`The report is pseudonymised: addresses, mail addresses and device names are replaced by stable markers, device ids are shortened, and credentials never appear.`,gsw_loadingDevices:`Loading devices …`,gsw_loadingDevicesSlow:`Still loading — this needs the adapter instance to be running.`,gsw_listFailed:`Could not load the device list. Is the instance running?`,gsw_deviceOption:`%s — currently %s segments`,gsw_expertIntro:`Two tools for when something needs a closer look. Pick one — the device list loads when you do.`,gsw_expertWizard:`Segment detection`,gsw_expertDiagnostics:`Diagnostics`},m={gsw_deviceLabel:`Gerät`,gsw_noDevices:`Keine Geräte mit Segmenten gefunden. Trage einen Cloud-API-Key in den Adaptereinstellungen ein, damit die Gerätefähigkeiten bekannt sind.`,gsw_start:`Messung starten`,gsw_measureHint:`Beobachte den Strip — welches Segment leuchtet hell weiß auf?`,gsw_question:`Segment %s — leuchtet es?`,gsw_lit:`Leuchtet`,gsw_dark:`Dunkel (Lücke)`,gsw_finish:`Fertig — Strip zu Ende`,gsw_cancel:`Abbrechen`,gsw_reviewHint:`Zelle anklicken, um zwischen Leuchtet/Lücke zu wechseln, dann übernehmen.`,gsw_resultNoGaps:`%s Segmente, keine Lücke`,gsw_resultGaps:`%s Segmente, %s Lücke(n)`,gsw_apply:`Übernehmen`,gsw_remeasure:`Nochmal messen`,gsw_applied:`Übernommen: %s Segmente.`,gsw_error:`Wizard-Fehler: %s`,gsw_close:`Schließen`,gsw_conn_lan_title:`LAN — immer aktiv`,gsw_conn_lan_desc:`Lokale Steuerung für Lampen. Keine Zugangsdaten nötig.`,gsw_conn_cloud_title:`Cloud-API-Key`,gsw_conn_cloud_desc:`Optional. Ergänzt Gerätenamen, Szenen, Snapshots und Segmente.`,gsw_conn_apikey_label:`Govee-API-Key`,gsw_conn_rt_title:`Echtzeit-Status (Govee-Konto)`,gsw_conn_rt_desc:`Optional. Aktiviert Live-Status-Push über die Govee-Cloud (AWS IoT).`,gsw_conn_email_label:`Konto-E-Mail`,gsw_conn_password_label:`Konto-Passwort`,gsw_conn_connect_btn:`Verbinden`,gsw_conn_2fa_hint:`Govee hat einen Bestätigungscode per E-Mail geschickt. Zum Abschließen hier eingeben.`,gsw_conn_code_label:`Bestätigungscode`,gsw_conn_request_btn:`Neuen Code anfordern`,gsw_conn_verify_btn:`Code bestätigen`,gsw_conn_st_ok:`Verbunden. Zum dauerhaften Aktivieren die Einstellungen speichern.`,gsw_conn_st_verifyRequired:`Govee benötigt für dieses Gerät einen Bestätigungscode. E-Mail prüfen und Code eingeben.`,gsw_conn_st_codeInvalid:`Der Bestätigungscode ist ungültig. Neuen anfordern und erneut versuchen.`,gsw_conn_st_passwordRejected:`E-Mail oder Passwort wurde abgelehnt. Bitte Zugangsdaten prüfen.`,gsw_conn_st_emailNotRegistered:`Diese E-Mail ist bei Govee nicht registriert.`,gsw_conn_st_rateLimited:`Zu viele Versuche. Bitte etwas warten, bevor du es erneut versuchst.`,gsw_conn_st_accountLocked:`Das Konto ist vorübergehend von Govee gesperrt. Später erneut versuchen.`,gsw_conn_st_loginFailed:`Anmeldung fehlgeschlagen. Bitte erneut versuchen.`,gsw_conn_st_mqttNotUp:`Der Echtzeit-Dienst ist noch nicht bereit. Gleich erneut versuchen.`,gsw_conn_st_codeSent:`Ein neuer Bestätigungscode wurde an deine E-Mail geschickt.`,gsw_conn_st_codeRejected:`Govee hat den Bestätigungscode abgelehnt. Neuen anfordern.`,gsw_conn_st_needCredentials:`Zuerst E-Mail und Passwort eingeben.`,gsw_conn_st_throttled:`Bitte kurz warten, bevor du einen weiteren Code anforderst.`,gsw_conn_st_unknownAction:`Unbekannte Aktion.`,gsw_diagIntro:`Gerät wählen und auf den Knopf drücken: Der Adapter erstellt einen Diagnosebericht, und der Browser legt ihn als Datei ab. Diese Datei an ein GitHub-Issue anhängen — die Formulare fragen genau danach.`,gsw_diagDevice:`Gerät`,gsw_diagExport:`Bericht erstellen und herunterladen`,gsw_diagDone:`Gespeichert als %s.`,gsw_diagOpenIssue:`Issue eröffnen`,gsw_diagNoDevices:`Noch keine Geräte. Der Bericht gilt je Gerät — sobald der Adapter eines gefunden hat, erscheint es hier.`,gsw_diagExportFailed:`Der Bericht konnte nicht erstellt werden. Warum, steht im Adapter-Log.`,gsw_diagPrivacy:`Der Bericht ist anonymisiert: Adressen, E-Mail-Adressen und Gerätenamen werden durch gleichbleibende Marken ersetzt, Gerätekennungen gekürzt, Zugangsdaten tauchen gar nicht auf.`,gsw_loadingDevices:`Geräte werden geladen …`,gsw_loadingDevicesSlow:`Lädt noch — dafür muss die Adapter-Instanz laufen.`,gsw_listFailed:`Die Geräteliste konnte nicht geladen werden. Läuft die Instanz?`,gsw_deviceOption:`%s — bisher %s Segmente`,gsw_expertIntro:`Zwei Werkzeuge für den genaueren Blick. Eines auswählen — die Geräteliste wird dann geladen.`,gsw_expertWizard:`Segment-Erkennung`,gsw_expertDiagnostics:`Diagnose`},h={app:e=>({backgroundColor:e.palette.background.default,color:e.palette.text.primary,height:`100%`}),item:{padding:50,width:600}},g=class extends e{constructor(e){let t={...e};super(e,t),this.state={...this.state,data:{},originalData:{},theme:this.createTheme()},a.extendTranslations(p,`en`),a.extendTranslations(m,`de`);let n=(navigator.language||navigator.userLanguage||`en`).toLowerCase();a.setLanguage(n.substring(0,2))}render(){return this.state.loaded?l(n,{injectFirst:!0,children:l(c,{theme:this.state.theme,children:t(r,{sx:h.app,children:[l(`div`,{style:h.item,children:l(d,{socket:this.socket,namespace:`${this.adapterName}.${this.instance}`,values:{apiKey:String(this.state.data.apiKey??``),email:String(this.state.data.goveeEmail??``),password:String(this.state.data.goveePassword??``),code:String(this.state.data.mqttVerificationCode??``)},onChange:(e,t)=>this.setState({data:{...this.state.data,[e]:t}})})}),l(`div`,{style:h.item,children:l(f,{socket:this.socket,namespace:`${this.adapterName}.${this.instance}`})})]})})}):l(n,{injectFirst:!0,children:l(c,{theme:this.state.theme,children:l(o,{themeType:this.state.themeType})})})}};s(),window.adapterName=`govee-smart`;var _=document.getElementById(`root`);_&&u(_).render(l(i.StrictMode,{children:l(g,{socket:{port:8081}})}));
@@ -32,4 +32,4 @@ const __mfImport = (src) =>
32
32
  if (__mfModuleCache.pendingShareLoads) {
33
33
  await Promise.all(__mfModuleCache.pendingShareLoads);
34
34
  }
35
- })().then(() => __mfImport("./index-CStRjtPo.js"));
35
+ })().then(() => __mfImport("./index-C-fBGUPX.js"));
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Issue eröffnen",
52
52
  "gsw_diagNoDevices": "Noch keine Geräte. Der Bericht gilt je Gerät — sobald der Adapter eines gefunden hat, erscheint es hier.",
53
53
  "gsw_diagExportFailed": "Der Bericht konnte nicht erstellt werden. Warum, steht im Adapter-Log.",
54
- "gsw_diagPrivacy": "Der Bericht ist anonymisiert: Adressen, E-Mail-Adressen und Gerätenamen werden durch gleichbleibende Marken ersetzt, Gerätekennungen gekürzt, Zugangsdaten tauchen gar nicht auf. Die Datei erklärt das in ihrem eigenen Kopf.",
54
+ "gsw_diagPrivacy": "Der Bericht ist anonymisiert: Adressen, E-Mail-Adressen und Gerätenamen werden durch gleichbleibende Marken ersetzt, Gerätekennungen gekürzt, Zugangsdaten tauchen gar nicht auf.",
55
55
  "gsw_loadingDevices": "Geräte werden geladen …",
56
56
  "gsw_loadingDevicesSlow": "Lädt noch — dafür muss die Adapter-Instanz laufen.",
57
57
  "gsw_listFailed": "Die Geräteliste konnte nicht geladen werden. Läuft die Instanz?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Open an issue",
52
52
  "gsw_diagNoDevices": "No devices yet. The report is per device — once the adapter has found one, it appears here.",
53
53
  "gsw_diagExportFailed": "The report could not be created. The adapter log says why.",
54
- "gsw_diagPrivacy": "The report is pseudonymised: addresses, mail addresses and device names are replaced by stable markers, device ids are shortened, and credentials never appear. The file explains this in its own header.",
54
+ "gsw_diagPrivacy": "The report is pseudonymised: addresses, mail addresses and device names are replaced by stable markers, device ids are shortened, and credentials never appear.",
55
55
  "gsw_loadingDevices": "Loading devices …",
56
56
  "gsw_loadingDevicesSlow": "Still loading — this needs the adapter instance to be running.",
57
57
  "gsw_listFailed": "Could not load the device list. Is the instance running?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Abrir una issue",
52
52
  "gsw_diagNoDevices": "Aún no hay dispositivos. El informe es por dispositivo: en cuanto el adaptador encuentre uno, aparecerá aquí.",
53
53
  "gsw_diagExportFailed": "No se pudo crear el informe. El registro del adaptador explica por qué.",
54
- "gsw_diagPrivacy": "El informe está seudonimizado: direcciones, direcciones de correo y nombres de dispositivos se sustituyen por marcadores estables, los identificadores se acortan y las credenciales nunca aparecen. El archivo lo explica en su propia cabecera.",
54
+ "gsw_diagPrivacy": "El informe está seudonimizado: direcciones, direcciones de correo y nombres de dispositivos se sustituyen por marcadores estables, los identificadores se acortan y las credenciales nunca aparecen.",
55
55
  "gsw_loadingDevices": "Cargando dispositivos …",
56
56
  "gsw_loadingDevicesSlow": "Sigue cargando: la instancia del adaptador debe estar en ejecución.",
57
57
  "gsw_listFailed": "No se pudo cargar la lista de dispositivos. ¿Está la instancia en ejecución?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Ouvrir une issue",
52
52
  "gsw_diagNoDevices": "Aucun appareil pour l'instant. Le rapport est par appareil — dès que l'adaptateur en trouve un, il apparaît ici.",
53
53
  "gsw_diagExportFailed": "Le rapport n'a pas pu être créé. Le journal de l'adaptateur en donne la raison.",
54
- "gsw_diagPrivacy": "Le rapport est pseudonymisé : adresses, adresses mail et noms d'appareils sont remplacés par des marqueurs stables, les identifiants sont raccourcis et les identifiants de connexion n'y figurent jamais. Le fichier l'explique dans son en-tête.",
54
+ "gsw_diagPrivacy": "Le rapport est pseudonymisé : adresses, adresses mail et noms d'appareils sont remplacés par des marqueurs stables, les identifiants sont raccourcis et les identifiants de connexion n'y figurent jamais.",
55
55
  "gsw_loadingDevices": "Chargement des appareils …",
56
56
  "gsw_loadingDevicesSlow": "Chargement en cours — l'instance de l'adaptateur doit être démarrée.",
57
57
  "gsw_listFailed": "Impossible de charger la liste des appareils. L'instance est-elle démarrée ?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Apri una issue",
52
52
  "gsw_diagNoDevices": "Ancora nessun dispositivo. Il rapporto è per dispositivo — appena l'adattatore ne trova uno, compare qui.",
53
53
  "gsw_diagExportFailed": "Non è stato possibile creare il rapporto. Il log dell'adattatore spiega perché.",
54
- "gsw_diagPrivacy": "Il rapporto è pseudonimizzato: indirizzi, indirizzi mail e nomi dei dispositivi sono sostituiti da marcatori stabili, gli id sono accorciati e le credenziali non compaiono mai. Il file lo spiega nella propria intestazione.",
54
+ "gsw_diagPrivacy": "Il rapporto è pseudonimizzato: indirizzi, indirizzi mail e nomi dei dispositivi sono sostituiti da marcatori stabili, gli id sono accorciati e le credenziali non compaiono mai.",
55
55
  "gsw_loadingDevices": "Caricamento dispositivi …",
56
56
  "gsw_loadingDevicesSlow": "Ancora in caricamento: l'istanza dell'adattatore deve essere in esecuzione.",
57
57
  "gsw_listFailed": "Impossibile caricare l'elenco dei dispositivi. L'istanza è in esecuzione?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Issue openen",
52
52
  "gsw_diagNoDevices": "Nog geen apparaten. Het rapport is per apparaat — zodra de adapter er een vindt, verschijnt het hier.",
53
53
  "gsw_diagExportFailed": "Het rapport kon niet worden gemaakt. Het adapterlogboek zegt waarom.",
54
- "gsw_diagPrivacy": "Het rapport is gepseudonimiseerd: adressen, mailadressen en apparaatnamen worden vervangen door vaste markeringen, apparaat-ids worden ingekort en inloggegevens komen er niet in voor. Het bestand legt dit uit in zijn eigen kop.",
54
+ "gsw_diagPrivacy": "Het rapport is gepseudonimiseerd: adressen, mailadressen en apparaatnamen worden vervangen door vaste markeringen, apparaat-ids worden ingekort en inloggegevens komen er niet in voor.",
55
55
  "gsw_loadingDevices": "Apparaten laden …",
56
56
  "gsw_loadingDevicesSlow": "Nog bezig — hiervoor moet de adapterinstantie draaien.",
57
57
  "gsw_listFailed": "Kon de apparatenlijst niet laden. Draait de instantie?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Otwórz zgłoszenie",
52
52
  "gsw_diagNoDevices": "Brak urządzeń. Raport dotyczy pojedynczego urządzenia — gdy adapter jakieś znajdzie, pojawi się tutaj.",
53
53
  "gsw_diagExportFailed": "Nie udało się utworzyć raportu. Powód znajdziesz w logu adaptera.",
54
- "gsw_diagPrivacy": "Raport jest pseudonimizowany: adresy, adresy e-mail i nazwy urządzeń zastępują stałe znaczniki, identyfikatory są skracane, a dane logowania nigdy się w nim nie pojawiają. Plik wyjaśnia to we własnym nagłówku.",
54
+ "gsw_diagPrivacy": "Raport jest pseudonimizowany: adresy, adresy e-mail i nazwy urządzeń zastępują stałe znaczniki, identyfikatory są skracane, a dane logowania nigdy się w nim nie pojawiają.",
55
55
  "gsw_loadingDevices": "Wczytywanie urządzeń …",
56
56
  "gsw_loadingDevicesSlow": "Nadal trwa wczytywanie — instancja adaptera musi być uruchomiona.",
57
57
  "gsw_listFailed": "Nie udało się wczytać listy urządzeń. Czy instancja działa?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Abrir um issue",
52
52
  "gsw_diagNoDevices": "Ainda sem dispositivos. O relatório é por dispositivo — assim que o adaptador encontrar um, aparece aqui.",
53
53
  "gsw_diagExportFailed": "Não foi possível criar o relatório. O log do adaptador diz porquê.",
54
- "gsw_diagPrivacy": "O relatório é pseudonimizado: endereços, endereços de e-mail e nomes de dispositivos são substituídos por marcadores estáveis, os identificadores são encurtados e as credenciais nunca aparecem. O ficheiro explica isso no seu cabeçalho.",
54
+ "gsw_diagPrivacy": "O relatório é pseudonimizado: endereços, endereços de e-mail e nomes de dispositivos são substituídos por marcadores estáveis, os identificadores são encurtados e as credenciais nunca aparecem.",
55
55
  "gsw_loadingDevices": "A carregar dispositivos …",
56
56
  "gsw_loadingDevicesSlow": "Ainda a carregar — a instância do adaptador tem de estar em execução.",
57
57
  "gsw_listFailed": "Não foi possível carregar a lista de dispositivos. A instância está a correr?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Открыть issue",
52
52
  "gsw_diagNoDevices": "Пока нет устройств. Отчёт составляется по устройству — как только адаптер найдёт хотя бы одно, оно появится здесь.",
53
53
  "gsw_diagExportFailed": "Не удалось создать отчёт. Причина указана в журнале адаптера.",
54
- "gsw_diagPrivacy": "Отчёт псевдонимизирован: адреса, почтовые адреса и имена устройств заменены постоянными метками, идентификаторы сокращены, учётные данные не попадают в него. Файл поясняет это в своём заголовке.",
54
+ "gsw_diagPrivacy": "Отчёт псевдонимизирован: адреса, почтовые адреса и имена устройств заменены постоянными метками, идентификаторы сокращены, учётные данные не попадают в него.",
55
55
  "gsw_loadingDevices": "Загрузка устройств …",
56
56
  "gsw_loadingDevicesSlow": "Всё ещё загружается — для этого экземпляр адаптера должен быть запущен.",
57
57
  "gsw_listFailed": "Не удалось загрузить список устройств. Экземпляр запущен?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "Відкрити issue",
52
52
  "gsw_diagNoDevices": "Пристроїв ще немає. Звіт складається для пристрою — щойно адаптер знайде хоч один, він з'явиться тут.",
53
53
  "gsw_diagExportFailed": "Не вдалося створити звіт. Причина — у журналі адаптера.",
54
- "gsw_diagPrivacy": "Звіт псевдонімізовано: адреси, поштові адреси та назви пристроїв замінено сталими мітками, ідентифікатори скорочено, а облікові дані не потрапляють до нього. Файл пояснює це у власному заголовку.",
54
+ "gsw_diagPrivacy": "Звіт псевдонімізовано: адреси, поштові адреси та назви пристроїв замінено сталими мітками, ідентифікатори скорочено, а облікові дані не потрапляють до нього.",
55
55
  "gsw_loadingDevices": "Завантаження пристроїв …",
56
56
  "gsw_loadingDevicesSlow": "Ще завантажується — для цього екземпляр адаптера має працювати.",
57
57
  "gsw_listFailed": "Не вдалося завантажити список пристроїв. Чи запущено екземпляр?",
@@ -51,7 +51,7 @@
51
51
  "gsw_diagOpenIssue": "创建 issue",
52
52
  "gsw_diagNoDevices": "暂无设备。报告是按设备生成的——适配器找到设备后会显示在这里。",
53
53
  "gsw_diagExportFailed": "无法生成报告。原因见适配器日志。",
54
- "gsw_diagPrivacy": "报告已做匿名化处理:地址、邮件地址和设备名称被替换为固定标记,设备标识被缩短,凭据完全不会出现。文件在其头部对此作了说明。",
54
+ "gsw_diagPrivacy": "报告已做匿名化处理:地址、邮件地址和设备名称被替换为固定标记,设备标识被缩短,凭据完全不会出现。",
55
55
  "gsw_loadingDevices": "正在加载设备 …",
56
56
  "gsw_loadingDevicesSlow": "仍在加载 — 这需要适配器实例正在运行。",
57
57
  "gsw_listFailed": "无法加载设备列表。适配器实例正在运行吗?",
@@ -25,13 +25,16 @@ __export(capability_mapper_exports, {
25
25
  buildCloudStateDefs: () => buildCloudStateDefs,
26
26
  buildLanStateDefs: () => buildLanStateDefs,
27
27
  canonicalSyntheticId: () => canonicalSyntheticId,
28
+ classifyModeLevels: () => classifyModeLevels,
28
29
  getDefaultLanStates: () => getDefaultLanStates,
29
30
  getMusicModeOptions: () => getMusicModeOptions,
30
31
  hasDynamicSceneCapability: () => hasDynamicSceneCapability,
31
32
  mapCapabilities: () => mapCapabilities,
32
33
  mapCloudStateValue: () => mapCloudStateValue,
34
+ mapCloudStateValues: () => mapCloudStateValues,
33
35
  musicModeNameUsesRgb: () => musicModeNameUsesRgb,
34
- planCloudCapabilityWrites: () => planCloudCapabilityWrites
36
+ planCloudCapabilityWrites: () => planCloudCapabilityWrites,
37
+ resolveWorkModeStruct: () => resolveWorkModeStruct
35
38
  });
36
39
  module.exports = __toCommonJS(capability_mapper_exports);
37
40
  var import_types = require("./types");
@@ -293,10 +296,10 @@ function mapColorSetting(cap) {
293
296
  }
294
297
  function mapMode(cap) {
295
298
  var _a;
296
- if (cap.instance !== "presetScene" || !Array.isArray((_a = cap.parameters) == null ? void 0 : _a.options)) {
299
+ if (typeof cap.instance !== "string" || !Array.isArray((_a = cap.parameters) == null ? void 0 : _a.options)) {
297
300
  return [];
298
301
  }
299
- const states = { "": "---" };
302
+ const states = { 0: "---" };
300
303
  for (const opt of cap.parameters.options) {
301
304
  if (!opt || typeof opt.name !== "string") {
302
305
  continue;
@@ -306,13 +309,23 @@ function mapMode(cap) {
306
309
  }
307
310
  return [
308
311
  {
309
- id: "scene",
310
- name: (0, import_i18n.tName)("scene"),
312
+ // `presetScene` keeps the id it has had since the beginning — renaming it
313
+ // would orphan the datapoint on every existing installation. Every other
314
+ // instance takes its own sanitised name (nightlightScene →
315
+ // nightlight_scene), the same rule the rest of the mapper follows.
316
+ id: cap.instance === "presetScene" ? "scene" : sanitizeId(cap.instance),
317
+ // `capabilityName` is the shared resolver every other generic branch
318
+ // uses: it hands back the translation object for a known instance
319
+ // (`capNightlightScene` has existed in all 11 languages the whole time —
320
+ // only `mapMode` never asked for it) and the vendor text for an unknown
321
+ // one. `humanize()` here would put a plain string into `common.name`,
322
+ // which the object-inventory gate rejects fleet-wide.
323
+ name: cap.instance === "presetScene" ? (0, import_i18n.tName)("scene") : capabilityName(cap.instance),
311
324
  type: "mixed",
312
325
  role: "state",
313
326
  write: true,
314
327
  states,
315
- def: "",
328
+ def: "0",
316
329
  capabilityType: cap.type,
317
330
  capabilityInstance: cap.instance
318
331
  }
@@ -370,6 +383,107 @@ function mapProperty(cap) {
370
383
  }
371
384
  ];
372
385
  }
386
+ function classifyModeLevels(options) {
387
+ const levels = [];
388
+ const ranges = [];
389
+ for (const opt of options) {
390
+ if (!opt) {
391
+ continue;
392
+ }
393
+ const group = typeof opt.name === "string" ? opt.name : "";
394
+ if (Array.isArray(opt.options) && opt.options.length > 0) {
395
+ for (const sub of opt.options) {
396
+ const n2 = coerceNum(sub == null ? void 0 : sub.value);
397
+ if (n2 !== null) {
398
+ levels.push({ group, value: n2, name: typeof sub.name === "string" ? sub.name : void 0 });
399
+ }
400
+ }
401
+ continue;
402
+ }
403
+ const range = opt.range;
404
+ const rMin = coerceNum(range == null ? void 0 : range.min);
405
+ const rMax = coerceNum(range == null ? void 0 : range.max);
406
+ if (rMin !== null && rMax !== null) {
407
+ ranges.push({ min: rMin, max: rMax });
408
+ continue;
409
+ }
410
+ const n = coerceNum(opt.value);
411
+ if (n !== null) {
412
+ levels.push({ group: "", value: n, name: group || void 0 });
413
+ }
414
+ }
415
+ if (levels.length === 0 && ranges.length === 0) {
416
+ return { kind: "none" };
417
+ }
418
+ const named = levels.every((l) => l.name !== void 0);
419
+ const owners = /* @__PURE__ */ new Map();
420
+ let collision = false;
421
+ for (const l of levels) {
422
+ const seen = owners.get(l.value);
423
+ if (seen !== void 0 && seen !== l.group) {
424
+ collision = true;
425
+ break;
426
+ }
427
+ owners.set(l.value, l.group);
428
+ }
429
+ if (named && !collision && ranges.length === 0) {
430
+ const states = {};
431
+ for (const l of levels) {
432
+ states[safeStringify(l.value)] = l.name;
433
+ }
434
+ return { kind: "dropdown", states };
435
+ }
436
+ const all = [...levels.map((l) => l.value), ...ranges.map((r) => r.min), ...ranges.map((r) => r.max)];
437
+ return { kind: "number", min: Math.min(...all), max: Math.max(...all) };
438
+ }
439
+ function resolveWorkModeStruct(cap, workModeKey, modeValueKey, changed) {
440
+ var _a, _b, _c, _d, _e, _f, _g;
441
+ const fields = (_b = (_a = cap.parameters) == null ? void 0 : _a.fields) != null ? _b : [];
442
+ const modeOptions = (_d = (_c = fields.find((f) => f && f.fieldName === "workMode")) == null ? void 0 : _c.options) != null ? _d : [];
443
+ const levelOptions = (_f = (_e = fields.find((f) => f && f.fieldName === "modeValue")) == null ? void 0 : _e.options) != null ? _f : [];
444
+ const pick = (options, raw) => {
445
+ var _a2;
446
+ if (raw === null || raw === void 0) {
447
+ return void 0;
448
+ }
449
+ const s = String(raw).trim();
450
+ if (!s) {
451
+ return void 0;
452
+ }
453
+ return (_a2 = options.find((o) => o && o.value !== void 0 && o.value !== null && safeStringify(o.value) === s)) != null ? _a2 : options.find((o) => o && typeof o.name === "string" && o.name.toLowerCase() === s.toLowerCase());
454
+ };
455
+ const groups = levelOptions.filter((o) => o && Array.isArray(o.options) && o.options.length > 0);
456
+ const flatLevels = levelOptions.filter(
457
+ (o) => o && !Array.isArray(o.options) && o.value !== void 0 && o.value !== null
458
+ );
459
+ let modeOpt;
460
+ if (changed === "modeValue" && groups.length > 0 && classifyModeLevels(levelOptions).kind === "dropdown") {
461
+ const owner = groups.find((g) => pick(g.options, modeValueKey) !== void 0);
462
+ modeOpt = owner ? modeOptions.find((o) => o.name === owner.name) : void 0;
463
+ }
464
+ modeOpt != null ? modeOpt : modeOpt = pick(modeOptions, workModeKey);
465
+ const workMode = coerceNum(modeOpt == null ? void 0 : modeOpt.value);
466
+ if (modeOpt === void 0 || workMode === null) {
467
+ return null;
468
+ }
469
+ const groupEntry = levelOptions.find((o) => o && o.name === modeOpt.name);
470
+ const ownLevels = Array.isArray(groupEntry == null ? void 0 : groupEntry.options) && groupEntry.options.length > 0 ? groupEntry.options : flatLevels;
471
+ if (ownLevels.length > 0) {
472
+ const chosen = (_g = pick(ownLevels, modeValueKey)) != null ? _g : ownLevels[0];
473
+ const modeValue = coerceNum(chosen.value);
474
+ return modeValue === null ? null : { workMode, modeValue };
475
+ }
476
+ const range = groupEntry == null ? void 0 : groupEntry.range;
477
+ const rMin = coerceNum(range == null ? void 0 : range.min);
478
+ const rMax = coerceNum(range == null ? void 0 : range.max);
479
+ if (rMin !== null && rMax !== null) {
480
+ const written = coerceNum(modeValueKey);
481
+ const modeValue = written === null ? rMin : Math.min(Math.max(written, rMin), rMax);
482
+ return { workMode, modeValue };
483
+ }
484
+ const fallback = coerceNum(groupEntry == null ? void 0 : groupEntry.defaultValue);
485
+ return { workMode, modeValue: fallback != null ? fallback : 0 };
486
+ }
373
487
  function mapWorkMode(cap) {
374
488
  var _a;
375
489
  const fields = (_a = cap.parameters) == null ? void 0 : _a.fields;
@@ -413,24 +527,36 @@ function mapWorkMode(cap) {
413
527
  const valueField = fields.find((f) => f && f.fieldName === "modeValue");
414
528
  if (valueField) {
415
529
  if (valueField.options && valueField.options.length > 0) {
416
- const valStates = {};
417
- for (const opt of valueField.options) {
418
- if (opt && typeof opt.name === "string") {
419
- valStates[safeStringify(opt.value)] = opt.name;
420
- }
530
+ const level = classifyModeLevels(valueField.options);
531
+ if (level.kind === "dropdown") {
532
+ const firstKey = Object.keys(level.states)[0];
533
+ states.push({
534
+ id: "mode_value",
535
+ name: (0, import_i18n.tName)("modeValue"),
536
+ desc: (0, import_i18n.tDesc)("descModeValue"),
537
+ type: "mixed",
538
+ role: "state",
539
+ write: true,
540
+ states: level.states,
541
+ def: firstKey,
542
+ capabilityType: cap.type,
543
+ capabilityInstance: cap.instance
544
+ });
545
+ } else if (level.kind === "number") {
546
+ states.push({
547
+ id: "mode_value",
548
+ name: (0, import_i18n.tName)("modeValue"),
549
+ desc: (0, import_i18n.tDesc)("descModeValue"),
550
+ type: "number",
551
+ role: "level",
552
+ write: true,
553
+ min: level.min,
554
+ max: level.max,
555
+ def: level.min,
556
+ capabilityType: cap.type,
557
+ capabilityInstance: cap.instance
558
+ });
421
559
  }
422
- states.push({
423
- id: "mode_value",
424
- name: (0, import_i18n.tName)("modeValue"),
425
- desc: (0, import_i18n.tDesc)("descModeValue"),
426
- type: "mixed",
427
- role: "state",
428
- write: true,
429
- states: valStates,
430
- def: valueField.options[0] ? safeStringify(valueField.options[0].value) : "",
431
- capabilityType: cap.type,
432
- capabilityInstance: cap.instance
433
- });
434
560
  } else if (valueField.range) {
435
561
  states.push({
436
562
  id: "mode_value",
@@ -450,7 +576,7 @@ function mapWorkMode(cap) {
450
576
  return states;
451
577
  }
452
578
  function mapTemperatureSetting(cap) {
453
- var _a, _b, _c, _d, _e, _f;
579
+ var _a, _b;
454
580
  const fields = (_a = cap.parameters) == null ? void 0 : _a.fields;
455
581
  if (Array.isArray(fields) && fields.length > 0) {
456
582
  const tempField = fields.find((f) => {
@@ -463,7 +589,7 @@ function mapTemperatureSetting(cap) {
463
589
  return f.fieldName.toLowerCase().includes("temperature");
464
590
  });
465
591
  if (tempField == null ? void 0 : tempField.range) {
466
- const unit = (_c = normalizeUnit((_b = cap.parameters) == null ? void 0 : _b.unit)) != null ? _c : "\xB0F";
592
+ const unit = temperatureUnit(cap);
467
593
  return [
468
594
  {
469
595
  id: "target_temperature",
@@ -481,9 +607,9 @@ function mapTemperatureSetting(cap) {
481
607
  ];
482
608
  }
483
609
  }
484
- const range = (_d = cap.parameters) == null ? void 0 : _d.range;
610
+ const range = (_b = cap.parameters) == null ? void 0 : _b.range;
485
611
  if (range) {
486
- const unit = (_f = normalizeUnit((_e = cap.parameters) == null ? void 0 : _e.unit)) != null ? _f : "\xB0F";
612
+ const unit = temperatureUnit(cap);
487
613
  return [
488
614
  {
489
615
  id: "target_temperature",
@@ -628,8 +754,19 @@ const UNIT_MAP = {
628
754
  "unit.percent": "%",
629
755
  "unit.kelvin": "K",
630
756
  "unit.celsius": "\xB0C",
631
- "unit.fahrenheit": "\xB0F"
757
+ "unit.fahrenheit": "\xB0F",
758
+ // The temperature_setting STRUCT names its unit like this (H7131 capture).
759
+ Celsius: "\xB0C",
760
+ Fahrenheit: "\xB0F"
632
761
  };
762
+ function temperatureUnit(cap) {
763
+ var _a, _b, _c, _d, _e, _f;
764
+ const fields = (_a = cap.parameters) == null ? void 0 : _a.fields;
765
+ const unitField = fields == null ? void 0 : fields.find((f) => f && f.fieldName === "unit");
766
+ const declared = typeof (unitField == null ? void 0 : unitField.defaultValue) === "string" ? unitField.defaultValue : void 0;
767
+ const onTemperature = (_b = fields == null ? void 0 : fields.find((f) => f && f.fieldName === "temperature")) == null ? void 0 : _b.unit;
768
+ return (_f = (_e = (_c = normalizeUnit(declared)) != null ? _c : normalizeUnit(typeof onTemperature === "string" ? onTemperature : void 0)) != null ? _e : normalizeUnit((_d = cap.parameters) == null ? void 0 : _d.unit)) != null ? _f : "\xB0F";
769
+ }
633
770
  function normalizeUnit(unit) {
634
771
  var _a;
635
772
  if (!unit) {
@@ -730,13 +867,13 @@ function mapCloudStateValue(cap) {
730
867
  case "toggle":
731
868
  return { stateId: sanitizeId(cap.instance), value: coerceBool(raw) };
732
869
  case "mode":
733
- if (cap.instance === "presetScene") {
734
- return {
735
- stateId: "scene",
736
- value: safeStringify(raw)
737
- };
870
+ if (typeof cap.instance !== "string") {
871
+ return null;
738
872
  }
739
- return null;
873
+ return {
874
+ stateId: cap.instance === "presetScene" ? "scene" : sanitizeId(cap.instance),
875
+ value: safeStringify(raw)
876
+ };
740
877
  case "dynamic_scene":
741
878
  if (cap.instance === "snapshot") {
742
879
  return null;
@@ -746,18 +883,12 @@ function mapCloudStateValue(cap) {
746
883
  value: safeStringify(raw)
747
884
  };
748
885
  case "work_mode": {
749
- if (typeof raw === "object" && raw !== null) {
750
- const struct = raw;
751
- const n = coerceNum(struct.workMode);
752
- if (n !== null) {
753
- return { stateId: "work_mode", value: n };
754
- }
755
- }
756
- const direct = coerceNum(raw);
757
- if (direct !== null) {
758
- return { stateId: "work_mode", value: direct };
886
+ const struct = typeof raw === "object" && raw !== null ? raw : void 0;
887
+ const n = coerceNum(struct ? struct.workMode : raw);
888
+ if (n === null) {
889
+ return null;
759
890
  }
760
- return null;
891
+ return { stateId: "work_mode", value: safeStringify(n) };
761
892
  }
762
893
  case "temperature_setting": {
763
894
  const direct = coerceNum(raw);
@@ -800,16 +931,12 @@ function mapCloudStateValue(cap) {
800
931
  return null;
801
932
  }
802
933
  }
803
- function planCloudCapabilityWrites(caps, hasLanIp, lanStateIds) {
934
+ function planCloudCapabilityWrites(caps, hasLanIp, lanStateIds, declared) {
804
935
  const writes = [];
805
936
  if (!Array.isArray(caps)) {
806
937
  return writes;
807
938
  }
808
- for (const cap of caps) {
809
- const mapped = mapCloudStateValue(cap);
810
- if (!mapped) {
811
- continue;
812
- }
939
+ for (const mapped of caps.flatMap((cap) => mapCloudStateValues(cap, declared))) {
813
940
  if (hasLanIp && lanStateIds.has(mapped.stateId)) {
814
941
  continue;
815
942
  }
@@ -817,6 +944,27 @@ function planCloudCapabilityWrites(caps, hasLanIp, lanStateIds) {
817
944
  }
818
945
  return writes;
819
946
  }
947
+ function mapCloudStateValues(cap, declared) {
948
+ var _a, _b, _c, _d, _e;
949
+ const primary = mapCloudStateValue(cap);
950
+ if (!primary) {
951
+ return [];
952
+ }
953
+ if (primary.stateId !== "work_mode") {
954
+ return [primary];
955
+ }
956
+ const raw = (_a = cap.state) == null ? void 0 : _a.value;
957
+ if (typeof raw !== "object" || raw === null) {
958
+ return [primary];
959
+ }
960
+ const level = coerceNum(raw.modeValue);
961
+ if (level === null) {
962
+ return [primary];
963
+ }
964
+ const levelOptions = (_e = (_d = (_c = (_b = declared == null ? void 0 : declared.find((c) => c.type === cap.type && c.instance === cap.instance)) == null ? void 0 : _b.parameters) == null ? void 0 : _c.fields) == null ? void 0 : _d.find((f) => f && f.fieldName === "modeValue")) == null ? void 0 : _e.options;
965
+ const asNumber = levelOptions !== void 0 && classifyModeLevels(levelOptions).kind === "number";
966
+ return [primary, { stateId: "mode_value", value: asNumber ? level : safeStringify(level) }];
967
+ }
820
968
  const SCENE_DROPDOWN_RULES = [
821
969
  { id: "light_scene", cap: "lightScene", nameKey: "lightScene", channel: "scenes", source: (d) => d.scenes },
822
970
  { id: "diy_scene", cap: "diyScene", nameKey: "diyScene", channel: "scenes", source: (d) => d.diyScenes },
@@ -1082,12 +1230,15 @@ function buildGroupStateDefs(members) {
1082
1230
  buildCloudStateDefs,
1083
1231
  buildLanStateDefs,
1084
1232
  canonicalSyntheticId,
1233
+ classifyModeLevels,
1085
1234
  getDefaultLanStates,
1086
1235
  getMusicModeOptions,
1087
1236
  hasDynamicSceneCapability,
1088
1237
  mapCapabilities,
1089
1238
  mapCloudStateValue,
1239
+ mapCloudStateValues,
1090
1240
  musicModeNameUsesRgb,
1091
- planCloudCapabilityWrites
1241
+ planCloudCapabilityWrites,
1242
+ resolveWorkModeStruct
1092
1243
  });
1093
1244
  //# sourceMappingURL=capability-mapper.js.map