From c95adb48d5a4281c9cde335c8d8e8195b13c9d4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:32:11 +0000 Subject: [PATCH 1/9] Initial plan From b536a281d258263700a595c03cb8642abcfa4354 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:37:19 +0000 Subject: [PATCH 2/9] Add Pillow version customization feature - Add PillowVersion config field to io-package.json - Add Pillow version selector in admin UI - Add translations for Pillow version configuration - Implement PyPI API integration to fetch available Pillow versions - Update main.js to use configurable Pillow version - All linter checks pass Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com> --- admin/i18n/de/translations.json | 2 + admin/i18n/en/translations.json | 2 + admin/i18n/es/translations.json | 2 + admin/i18n/fr/translations.json | 2 + admin/i18n/it/translations.json | 2 + admin/i18n/nl/translations.json | 2 + admin/i18n/pl/translations.json | 2 + admin/i18n/pt/translations.json | 2 + admin/i18n/ru/translations.json | 2 + admin/i18n/uk/translations.json | 2 + admin/i18n/zh-cn/translations.json | 2 + admin/jsonConfig.json5 | 12 ++++ io-package.json | 1 + main.js | 103 ++++++++++++++++++++++++++++- 14 files changed, 137 insertions(+), 1 deletion(-) diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json index 56b0348..687bc84 100644 --- a/admin/i18n/de/translations.json +++ b/admin/i18n/de/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Alle von diesem Adapter verwalteten Geräte und deren Verbindungsstatus", "hlpDiscoveryListeningAddress": "Nur Geräte auf dieser Schnittstelle erkennen", "hlpESPHomeDashboardVersion": "Auswählen welche Version von ESPHome Dashboard verwendet werden soll", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "API-Passwort (alt, bitte Verschlüsselungsschlüssel verwenden!)", "hlpInputDeviceIP": "Ein vorhandenes Gerät auswählen oder die IP des neuen Geräts eingeben", "hlpInputEncryptionKey": "Einen gerätespezifischen Verschlüsselungsschlüssel angeben (bevorzugt)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Anhörungsadresse", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "ESPHome Dashboard-Version", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "IP-Adresse, unter der ESPHome Dashboard zur Integration in ioBroker Admin ausgeführt wird", "lblESPHomePort": "Port des ESPHome-Dashboards", "lblEnableESPHome": "Sie können ESPHome Dashboard entweder separat ausführen oder die Option unten aktivieren, um es nativ in ioBroker auszuführen", diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json index 45f014b..67a806b 100644 --- a/admin/i18n/en/translations.json +++ b/admin/i18n/en/translations.json @@ -4,6 +4,7 @@ "existDevicesHeader": "All devices managed by this adapter and their connection state", "hlpDiscoveryListeningAddress": "Only discover devices on this interface", "hlpESPHomeDashboardVersion": "Choose which version of ESPHome Dashboard must be used", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "API password (legacy, please use encryption key!)", "hlpInputDeviceIP": "Select existing device or Enter IP of new device", "hlpInputEncryptionKey": "Provide device specific Encryption Key (preferred)", @@ -23,6 +24,7 @@ "lblDevicesTable": "All current (known) devices and their connection state", "lblDiscoveryListeningAddress": "Listening address", "lblESPHomeDashboardVersion": "ESPHome Dashboard version", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "IP-Address where ESPHome Dashboard is running to integrate in ioBroker Admin", "lblESPHomePort": "Port of ESPHome Dashboard", "lblEnableESPHome": "You can either run ESPHome Dashboard separately, or activate the option below to run it native in ioBroker", diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json index 33ed608..e902194 100644 --- a/admin/i18n/es/translations.json +++ b/admin/i18n/es/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Todos los dispositivos gestionados por este adaptador y su estado de conexión.", "hlpDiscoveryListeningAddress": "Descubrir solo dispositivos en esta interfaz", "hlpESPHomeDashboardVersion": "Elija qué versión de ESPHome Dashboard debe usarse", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Contraseña de API (heredada, ¡use una clave de cifrado!)", "hlpInputDeviceIP": "Seleccione el dispositivo existente o ingrese la IP del nuevo dispositivo", "hlpInputEncryptionKey": "Proporcionar clave de cifrado específica del dispositivo (preferido)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "dirección de escucha", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Versión del panel ESPHome", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "Dirección IP donde se ejecuta ESPHome Dashboard para integrarse en ioBroker Admin", "lblESPHomePort": "Puerto de ESPHome Dashboard", "lblEnableESPHome": "Puede ejecutar ESPHome Dashboard por separado o activar la siguiente opción para ejecutarlo de forma nativa en ioBroker.", diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json index 4f28e7c..3c8901a 100644 --- a/admin/i18n/fr/translations.json +++ b/admin/i18n/fr/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Tous les appareils gérés par cet adaptateur et leur état de connexion", "hlpDiscoveryListeningAddress": "Découvrez uniquement les appareils sur cette interface", "hlpESPHomeDashboardVersion": "Choisissez quelle version d'ESPHome Dashboard doit être utilisée", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Mot de passe API (ancien, veuillez utiliser la clé de cryptage !)", "hlpInputDeviceIP": "Sélectionnez l'appareil existant ou entrez l'adresse IP du nouvel appareil", "hlpInputEncryptionKey": "Fournir une clé de cryptage spécifique à l'appareil (de préférence)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Adresse d'écoute", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Version du tableau de bord ESPHome", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "Adresse IP sur laquelle ESPHome Dashboard est exécuté pour s'intégrer dans ioBroker Admin", "lblESPHomePort": "Port du tableau de bord ESPHome", "lblEnableESPHome": "Vous pouvez soit exécuter ESPHome Dashboard séparément, soit activer l'option ci-dessous pour l'exécuter nativement dans ioBroker", diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json index 178ec91..3b9c4c9 100644 --- a/admin/i18n/it/translations.json +++ b/admin/i18n/it/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Tutti i dispositivi gestiti da questo adattatore e il relativo stato di connessione", "hlpDiscoveryListeningAddress": "Rileva solo i dispositivi su questa interfaccia", "hlpESPHomeDashboardVersion": "Scegli quale versione di ESPHome Dashboard deve essere utilizzata", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Password API (legacy, utilizza la chiave di crittografia!)", "hlpInputDeviceIP": "Seleziona il dispositivo esistente o inserisci l'IP del nuovo dispositivo", "hlpInputEncryptionKey": "Fornire una chiave di crittografia specifica per il dispositivo (preferibile)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Indirizzo d'ascolto", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Versione ESPHome Dashboard", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "Indirizzo IP su cui è in esecuzione ESPHome Dashboard per l'integrazione in ioBroker Admin", "lblESPHomePort": "Porta della dashboard ESPHome", "lblEnableESPHome": "Puoi eseguire ESPHome Dashboard separatamente o attivare l'opzione seguente per eseguirlo nativo in ioBroker", diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json index 6295233..161d3df 100644 --- a/admin/i18n/nl/translations.json +++ b/admin/i18n/nl/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Alle apparaten die door deze adapter worden beheerd en hun verbindingsstatus", "hlpDiscoveryListeningAddress": "Ontdek alleen apparaten op deze interface", "hlpESPHomeDashboardVersion": "Kies welke versie van ESPHome Dashboard gebruikt moet worden", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "API-wachtwoord (verouderd, gebruik een coderingssleutel!)", "hlpInputDeviceIP": "Selecteer bestaand apparaat of voer IP van nieuw apparaat in", "hlpInputEncryptionKey": "Geef apparaatspecifieke coderingssleutel op (bij voorkeur)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Luisteradres", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "ESPHome Dashboard-versie", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "IP-adres waar ESPHome Dashboard wordt uitgevoerd om te integreren in ioBroker Admin", "lblESPHomePort": "Haven van ESPHome-dashboard", "lblEnableESPHome": "U kunt ESPHome Dashboard afzonderlijk uitvoeren of de onderstaande optie activeren om het native in ioBroker uit te voeren", diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl/translations.json index ad9f7e7..8b804fe 100644 --- a/admin/i18n/pl/translations.json +++ b/admin/i18n/pl/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Wszystkie urządzenia zarządzane przez tę kartę i ich stan połączenia", "hlpDiscoveryListeningAddress": "Odkrywaj tylko urządzenia na tym interfejsie", "hlpESPHomeDashboardVersion": "Wybierz, która wersja panelu ESPHome ma być używana", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Hasło API (starsze, użyj klucza szyfrującego!)", "hlpInputDeviceIP": "Wybierz istniejące urządzenie lub wprowadź adres IP nowego urządzenia", "hlpInputEncryptionKey": "Podaj klucz szyfrowania specyficzny dla urządzenia (preferowany)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Adres słuchania", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Wersja panelu ESPHome", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "Adres IP, na którym działa panel ESPHome Dashboard w celu integracji z administratorem ioBroker", "lblESPHomePort": "Port panelu ESPHome", "lblEnableESPHome": "Możesz uruchomić panel ESPHome osobno lub aktywować poniższą opcję, aby uruchomić go natywnie w ioBroker", diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json index 13eff7f..964ed78 100644 --- a/admin/i18n/pt/translations.json +++ b/admin/i18n/pt/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Todos os dispositivos gerenciados por este adaptador e seu estado de conexão", "hlpDiscoveryListeningAddress": "Descubra apenas dispositivos nesta interface", "hlpESPHomeDashboardVersion": "Escolha qual versão do ESPHome Dashboard deve ser usada", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Senha da API (herdada, use a chave de criptografia!)", "hlpInputDeviceIP": "Selecione o dispositivo existente ou insira o IP do novo dispositivo", "hlpInputEncryptionKey": "Forneça a chave de criptografia específica do dispositivo (preferencial)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Endereço de escuta", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Versão do painel ESPHome", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "Endereço IP onde o ESPHome Dashboard está sendo executado para integração no ioBroker Admin", "lblESPHomePort": "Painel do Porto do ESPHome", "lblEnableESPHome": "Você pode executar o ESPHome Dashboard separadamente ou ativar a opção abaixo para executá-lo nativamente no ioBroker", diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json index 3a74887..e1ccaf0 100644 --- a/admin/i18n/ru/translations.json +++ b/admin/i18n/ru/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "Все устройства, управляемые этим адаптером, и состояние их подключения", "hlpDiscoveryListeningAddress": "Обнаруживать устройства только на этом интерфейсе", "hlpESPHomeDashboardVersion": "Выберите, какую версию ESPHome Dashboard необходимо использовать.", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "Пароль API (устаревший, используйте ключ шифрования!)", "hlpInputDeviceIP": "Выберите существующее устройство или введите IP нового устройства.", "hlpInputEncryptionKey": "Предоставьте ключ шифрования для конкретного устройства (предпочтительно)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Адрес прослушивания", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "Версия панели управления ESPHome", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "IP-адрес, на котором работает панель управления ESPHome, для интеграции в администрацию ioBroker.", "lblESPHomePort": "Панель управления портом ESPHome", "lblEnableESPHome": "Вы можете запустить панель ESPHome Dashboard отдельно или активировать опцию ниже, чтобы запустить ее в ioBroker.", diff --git a/admin/i18n/uk/translations.json b/admin/i18n/uk/translations.json index e795e1c..5d061b2 100644 --- a/admin/i18n/uk/translations.json +++ b/admin/i18n/uk/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "All devices managed by this adapter and their connection state", "hlpDiscoveryListeningAddress": "Only discover devices on this interface", "hlpESPHomeDashboardVersion": "Choose which version of ESPHome Dashboard must be used", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "API password (legacy, please use encryption key!)", "hlpInputDeviceIP": "Select existing device or Enter IP of new device", "hlpInputEncryptionKey": "Provide device specific Encryption Key (preferred)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "Listening address", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "ESPHome Dashboard version", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "IP-Address where ESPHome Dashboard is running to integrate in ioBroker Admin", "lblESPHomePort": "Port of ESPHome Dashboard", "lblEnableESPHome": "You can either run ESPHome Dashboard separately, or activate the option below to run it native in ioBroker", diff --git a/admin/i18n/zh-cn/translations.json b/admin/i18n/zh-cn/translations.json index e79aab6..7ce3150 100644 --- a/admin/i18n/zh-cn/translations.json +++ b/admin/i18n/zh-cn/translations.json @@ -8,6 +8,7 @@ "existDevicesHeader": "该适配器管理的所有设备及其连接状态", "hlpDiscoveryListeningAddress": "只发现该接口上的设备", "hlpESPHomeDashboardVersion": "选择必须使用哪个版本的 ESPHome Dashboard", + "hlpPillowVersion": "Choose which version of Pillow must be used", "hlpInputApiPassword": "API密码(旧的,请使用加密密钥!)", "hlpInputDeviceIP": "选择现有设备或输入新设备的IP", "hlpInputEncryptionKey": "提供设备特定的加密密钥(首选)", @@ -30,6 +31,7 @@ "lblDiscoveryListeningAddress": "收听地址", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "ESPHome 仪表板版本", + "lblPillowVersion": "Pillow version", "lblESPHomeIP": "运行 ESPHome 仪表板以集成到 ioBroker Admin 中的 IP 地址", "lblESPHomePort": "ESPHome 仪表板端口", "lblEnableESPHome": "您可以单独运行 ESPHome Dashboard,也可以激活以下选项以在 ioBroker 中本机运行它", diff --git a/admin/jsonConfig.json5 b/admin/jsonConfig.json5 index be279ec..e0ef307 100644 --- a/admin/jsonConfig.json5 +++ b/admin/jsonConfig.json5 @@ -559,6 +559,18 @@ "command": "getESPHomeDashboardVersion", "jsonData": "{ \"service\": \"${data.deviceInputIP}\" }" }, + "PillowVersion": { + "type": "autocompleteSendTo", + "xs": 12, + "sm": 6, + "md": 6, + "lg": 6, + "xl": 6, + "label": "lblPillowVersion", + "help": "hlpPillowVersion", + "command": "getPillowVersion", + "jsonData": "{ \"service\": \"${data.deviceInputIP}\" }" + }, "ESPHomeDashboardIP": { "type": "autocompleteSendTo", "newLine": true, diff --git a/io-package.json b/io-package.json index 222d44d..c2c2cdc 100644 --- a/io-package.json +++ b/io-package.json @@ -181,6 +181,7 @@ ], "native": { "ESPHomeDashboardVersion": "Always last available", + "PillowVersion": "Always last available", "reconnectInterval": 30, "apiPass": "", "encryptionKey": "", diff --git a/main.js b/main.js index 80f5401..7b2e40a 100644 --- a/main.js +++ b/main.js @@ -274,6 +274,21 @@ class Esphome extends utils.Adapter { useDashBoardVersion = lastUsed; } + // Determine Pillow version to use + let usePillowVersion = "11.3.0"; // Default version + + // Check if user has configured a specific pillow version + if ( + this.config.PillowVersion && + this.config.PillowVersion !== "" && + this.config.PillowVersion !== "Always last available" + ) { + usePillowVersion = this.config.PillowVersion; + } + // If "Always last available" is selected, keep the default latest version + + this.log.debug(`Using Pillow version: ${usePillowVersion}`); + // Start Dashboard Process if (this.config.ESPHomeDashboardEnabled) { this.log.info( @@ -290,7 +305,7 @@ class Esphome extends utils.Adapter { pythonVersion: "3.13.2", // Use any Python 3.13.x version. requirements: [ { name: "esphome", version: `==${useDashBoardVersion}` }, - { name: "pillow", version: "==11.3.0" }, + { name: "pillow", version: `==${usePillowVersion}` }, ], // Use latest esphome }); } catch (error) { @@ -1816,6 +1831,92 @@ class Esphome extends utils.Adapter { } break; + // Front End message handler to load Pillow version dropdown with available versions + case "getPillowVersion": + { + const dropDownEntry = []; + dropDownEntry.push("Always last available"); + + // Fetch available Pillow versions from PyPI + try { + const response = await fetch("https://pypi.org/pypi/pillow/json"); + if (response.ok) { + const data = await response.json(); + const versions = Object.keys(data.releases) + .filter( + (v) => + !v.includes("a") && !v.includes("b") && !v.includes("rc"), + ) // Filter out alpha/beta/rc versions + .sort((a, b) => { + // Sort versions in descending order (newest first) + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + for ( + let i = 0; + i < Math.max(aParts.length, bParts.length); + i++ + ) { + const aVal = aParts[i] || 0; + const bVal = bParts[i] || 0; + if (aVal !== bVal) { + return bVal - aVal; + } + } + return 0; + }) + .slice(0, 20); // Limit to 20 most recent versions + + for (const version of versions) { + dropDownEntry.push({ + label: version, + value: version, + }); + } + } else { + this.log.warn( + `Unable to fetch Pillow versions from PyPI: ${response.status}`, + ); + // Add some common fallback versions + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + for (const version of fallbackVersions) { + dropDownEntry.push({ + label: version, + value: version, + }); + } + } + } catch (error) { + this.log.error( + `Error fetching Pillow versions: ${error.message}`, + ); + // Add fallback versions + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + for (const version of fallbackVersions) { + dropDownEntry.push({ + label: version, + value: version, + }); + } + } + + this.sendTo(obj.from, obj.command, dropDownEntry, obj.callback); + } + break; + // Front End message handler to host IP-Address(es) case "getHostIp": { From 083ea73c0cffc2afae2f32e80edd3ad700c4411f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:38:23 +0000 Subject: [PATCH 3/9] Update README.md with Pillow version customization feature Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d28cd1c..bdc2d9e 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,7 @@ If you like my work, please consider a personal donation * (DutchmanNL) --> ### __WORK IN PROGRESS__ - add capability for fans & clean cache +* (@copilot) **NEW**: Allow customization of Pillow version used by ESPHome Dashboard, similar to ESPHome version selector * (@copilot) **NEW**: Add "Clear Autopy Cache" button in ESPHome Dashboard configuration tab to resolve dashboard loading issues (#209) * (DutchmanNL) **FIXED**: Fan component not working #205 From 4cd2fec892b922f8f5d591e2958242fd410f4cb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:39:32 +0000 Subject: [PATCH 4/9] Refactor: Extract fallback versions to constant to avoid duplication Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com> --- main.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/main.js b/main.js index 7b2e40a..cfbee78 100644 --- a/main.js +++ b/main.js @@ -1837,6 +1837,16 @@ class Esphome extends utils.Adapter { const dropDownEntry = []; dropDownEntry.push("Always last available"); + // Fallback versions in case PyPI is unavailable + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + // Fetch available Pillow versions from PyPI try { const response = await fetch("https://pypi.org/pypi/pillow/json"); @@ -1876,15 +1886,7 @@ class Esphome extends utils.Adapter { this.log.warn( `Unable to fetch Pillow versions from PyPI: ${response.status}`, ); - // Add some common fallback versions - const fallbackVersions = [ - "11.3.0", - "11.2.0", - "11.1.0", - "11.0.0", - "10.4.0", - "10.3.0", - ]; + // Add fallback versions for (const version of fallbackVersions) { dropDownEntry.push({ label: version, @@ -1897,14 +1899,6 @@ class Esphome extends utils.Adapter { `Error fetching Pillow versions: ${error.message}`, ); // Add fallback versions - const fallbackVersions = [ - "11.3.0", - "11.2.0", - "11.1.0", - "11.0.0", - "10.4.0", - "10.3.0", - ]; for (const version of fallbackVersions) { dropDownEntry.push({ label: version, From 601d2fb35a0c0daffa372a79bbdfd44f0e035f15 Mon Sep 17 00:00:00 2001 From: Rafal Drozda Date: Mon, 16 Feb 2026 21:13:16 +0100 Subject: [PATCH 5/9] Update main.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/main.js b/main.js index cfbee78..2f22c22 100644 --- a/main.js +++ b/main.js @@ -1876,11 +1876,23 @@ class Esphome extends utils.Adapter { }) .slice(0, 20); // Limit to 20 most recent versions - for (const version of versions) { - dropDownEntry.push({ - label: version, - value: version, - }); + if (versions.length === 0) { + this.log.warn( + "No stable Pillow versions found on PyPI, using fallback versions", + ); + for (const version of fallbackVersions) { + dropDownEntry.push({ + label: version, + value: version, + }); + } + } else { + for (const version of versions) { + dropDownEntry.push({ + label: version, + value: version, + }); + } } } else { this.log.warn( From 038a21dafea7b3cb78e3dcd2214fd771ccc1cd9c Mon Sep 17 00:00:00 2001 From: Rafal Drozda Date: Mon, 16 Feb 2026 21:15:14 +0100 Subject: [PATCH 6/9] Update admin/i18n/de/translations.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- admin/i18n/de/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json index 687bc84..8efe46a 100644 --- a/admin/i18n/de/translations.json +++ b/admin/i18n/de/translations.json @@ -8,7 +8,7 @@ "existDevicesHeader": "Alle von diesem Adapter verwalteten Geräte und deren Verbindungsstatus", "hlpDiscoveryListeningAddress": "Nur Geräte auf dieser Schnittstelle erkennen", "hlpESPHomeDashboardVersion": "Auswählen welche Version von ESPHome Dashboard verwendet werden soll", - "hlpPillowVersion": "Choose which version of Pillow must be used", + "hlpPillowVersion": "Wählen Sie aus, welche Version von Pillow verwendet werden soll", "hlpInputApiPassword": "API-Passwort (alt, bitte Verschlüsselungsschlüssel verwenden!)", "hlpInputDeviceIP": "Ein vorhandenes Gerät auswählen oder die IP des neuen Geräts eingeben", "hlpInputEncryptionKey": "Einen gerätespezifischen Verschlüsselungsschlüssel angeben (bevorzugt)", From 81e57261243078834f6163c380a282e02342948d Mon Sep 17 00:00:00 2001 From: Rafal Drozda Date: Mon, 16 Feb 2026 21:15:39 +0100 Subject: [PATCH 7/9] Update admin/i18n/de/translations.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- admin/i18n/de/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json index 8efe46a..bbc88b6 100644 --- a/admin/i18n/de/translations.json +++ b/admin/i18n/de/translations.json @@ -31,7 +31,7 @@ "lblDiscoveryListeningAddress": "Anhörungsadresse", "lblDownloadYamlButton": "Download File", "lblESPHomeDashboardVersion": "ESPHome Dashboard-Version", - "lblPillowVersion": "Pillow version", + "lblPillowVersion": "Pillow-Version", "lblESPHomeIP": "IP-Adresse, unter der ESPHome Dashboard zur Integration in ioBroker Admin ausgeführt wird", "lblESPHomePort": "Port des ESPHome-Dashboards", "lblEnableESPHome": "Sie können ESPHome Dashboard entweder separat ausführen oder die Option unten aktivieren, um es nativ in ioBroker auszuführen", From 8ca41374c02bba22666e05d92283faebdb904485 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:19:52 +0000 Subject: [PATCH 8/9] Implement Pillow version caching and clean up admin config - Add pillowVersions array to cache PyPI versions at startup - Fetch Pillow versions from PyPI once during initialization with proper error handling - Cache versions in _ESPHomeDashboard.pillowVersionCache state - Update getPillowVersion handler to use cached versions instead of fetching on every request - Remove unused jsonData from both ESPHomeDashboardVersion and PillowVersion config - Add PillowVersion to dashboard integration test - Add clear logging when using fallback or cached versions Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com> --- admin/jsonConfig.json5 | 6 +- main.js | 204 +++++++++++++++-------- test/integrationTests/dashboard_tests.js | 2 + 3 files changed, 136 insertions(+), 76 deletions(-) diff --git a/admin/jsonConfig.json5 b/admin/jsonConfig.json5 index e0ef307..e35ba73 100644 --- a/admin/jsonConfig.json5 +++ b/admin/jsonConfig.json5 @@ -556,8 +556,7 @@ "xl": 6, "label": "lblESPHomeDashboardVersion", "help": "hlpESPHomeDashboardVersion", - "command": "getESPHomeDashboardVersion", - "jsonData": "{ \"service\": \"${data.deviceInputIP}\" }" + "command": "getESPHomeDashboardVersion" }, "PillowVersion": { "type": "autocompleteSendTo", @@ -568,8 +567,7 @@ "xl": 6, "label": "lblPillowVersion", "help": "hlpPillowVersion", - "command": "getPillowVersion", - "jsonData": "{ \"service\": \"${data.deviceInputIP}\" }" + "command": "getPillowVersion" }, "ESPHomeDashboardIP": { "type": "autocompleteSendTo", diff --git a/main.js b/main.js index 2f22c22..968f354 100644 --- a/main.js +++ b/main.js @@ -23,6 +23,7 @@ let autodiscovery, dashboardProcess, createConfigStates, discovery; const clientDetails = {}; // Memory cache of all devices and their connection status const newlyDiscoveredClient = {}; // Memory cache of all newly discovered devices and their connection status const dashboardVersions = []; +const pillowVersions = []; // Memory cache for available Pillow versions class Esphome extends utils.Adapter { /** * @param {Partial} [options] - Adapter configuration options @@ -274,6 +275,118 @@ class Esphome extends utils.Adapter { useDashBoardVersion = lastUsed; } + // Fetch Pillow versions from PyPI and cache them + try { + const response = await fetch("https://pypi.org/pypi/pillow/json"); + if (response.ok) { + const data = await response.json(); + const versions = Object.keys(data.releases) + .filter( + (v) => !v.includes("a") && !v.includes("b") && !v.includes("rc"), + ) // Filter out alpha/beta/rc versions + .sort((a, b) => { + // Sort versions in descending order (newest first) + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aVal = aParts[i] || 0; + const bVal = bParts[i] || 0; + if (aVal !== bVal) { + return bVal - aVal; + } + } + return 0; + }) + .slice(0, 20); // Limit to 20 most recent versions + + // Cache the versions + await this.stateSetCreate( + `_ESPHomeDashboard.pillowVersionCache`, + "pillowVersionCache", + JSON.stringify(versions), + ); + + // Store in memory array + pillowVersions.length = 0; // Clear array + pillowVersions.push(...versions); + + if (versions.length > 0) { + await this.stateSetCreate( + `_ESPHomeDashboard.newestPillowVersion`, + "newestPillowVersion", + versions[0], + ); + this.log.debug( + `Fetched ${versions.length} Pillow versions from PyPI, newest: ${versions[0]}`, + ); + } + } else { + this.log.warn( + `Unable to fetch Pillow versions from PyPI: ${response.status}, using cached values`, + ); + // Try to load from cache + const cachedPillowVersions = await this.getStateAsync( + `_ESPHomeDashboard.pillowVersionCache`, + ); + if (cachedPillowVersions && cachedPillowVersions.val) { + const versions = JSON.parse(cachedPillowVersions.val); + pillowVersions.length = 0; + pillowVersions.push(...versions); + this.log.info( + `Loaded ${pillowVersions.length} Pillow versions from cache`, + ); + } + } + } catch (error) { + this.log.warn( + `Error fetching Pillow versions from PyPI: ${error.message}, using cached or fallback versions`, + ); + // Try to load from cache + try { + const cachedPillowVersions = await this.getStateAsync( + `_ESPHomeDashboard.pillowVersionCache`, + ); + if (cachedPillowVersions && cachedPillowVersions.val) { + const versions = JSON.parse(cachedPillowVersions.val); + pillowVersions.length = 0; + pillowVersions.push(...versions); + this.log.info( + `Loaded ${pillowVersions.length} Pillow versions from cache`, + ); + } else { + // Use fallback versions if cache is also not available + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + pillowVersions.length = 0; + pillowVersions.push(...fallbackVersions); + this.log.info( + `Using ${fallbackVersions.length} fallback Pillow versions`, + ); + } + } catch { + // If cache read fails, use fallback + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + pillowVersions.length = 0; + pillowVersions.push(...fallbackVersions); + this.log.info( + `Using ${fallbackVersions.length} fallback Pillow versions`, + ); + } + } + // Determine Pillow version to use let usePillowVersion = "11.3.0"; // Default version @@ -1837,80 +1950,27 @@ class Esphome extends utils.Adapter { const dropDownEntry = []; dropDownEntry.push("Always last available"); - // Fallback versions in case PyPI is unavailable - const fallbackVersions = [ - "11.3.0", - "11.2.0", - "11.1.0", - "11.0.0", - "10.4.0", - "10.3.0", - ]; - - // Fetch available Pillow versions from PyPI - try { - const response = await fetch("https://pypi.org/pypi/pillow/json"); - if (response.ok) { - const data = await response.json(); - const versions = Object.keys(data.releases) - .filter( - (v) => - !v.includes("a") && !v.includes("b") && !v.includes("rc"), - ) // Filter out alpha/beta/rc versions - .sort((a, b) => { - // Sort versions in descending order (newest first) - const aParts = a.split(".").map(Number); - const bParts = b.split(".").map(Number); - for ( - let i = 0; - i < Math.max(aParts.length, bParts.length); - i++ - ) { - const aVal = aParts[i] || 0; - const bVal = bParts[i] || 0; - if (aVal !== bVal) { - return bVal - aVal; - } - } - return 0; - }) - .slice(0, 20); // Limit to 20 most recent versions - - if (versions.length === 0) { - this.log.warn( - "No stable Pillow versions found on PyPI, using fallback versions", - ); - for (const version of fallbackVersions) { - dropDownEntry.push({ - label: version, - value: version, - }); - } - } else { - for (const version of versions) { - dropDownEntry.push({ - label: version, - value: version, - }); - } - } - } else { - this.log.warn( - `Unable to fetch Pillow versions from PyPI: ${response.status}`, - ); - // Add fallback versions - for (const version of fallbackVersions) { - dropDownEntry.push({ - label: version, - value: version, - }); - } + // Use cached versions from memory + if (pillowVersions.length > 0) { + for (const version of pillowVersions) { + dropDownEntry.push({ + label: version, + value: version, + }); } - } catch (error) { - this.log.error( - `Error fetching Pillow versions: ${error.message}`, + } else { + // Fallback versions if cache is empty + this.log.info( + "No cached Pillow versions available, using fallback versions", ); - // Add fallback versions + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; for (const version of fallbackVersions) { dropDownEntry.push({ label: version, diff --git a/test/integrationTests/dashboard_tests.js b/test/integrationTests/dashboard_tests.js index 4895cc4..1d5704a 100644 --- a/test/integrationTests/dashboard_tests.js +++ b/test/integrationTests/dashboard_tests.js @@ -21,6 +21,7 @@ const { expect } = require("chai"); // Test configuration constants const DASHBOARD_PORT = 6052; const DASHBOARD_VERSION = "Always last available"; +const PILLOW_VERSION = "Always last available"; const DASHBOARD_INITIALIZATION_DELAY_MS = 10000; // 10 seconds initial delay for dashboard to start initializing const REACHABILITY_CHECK_DELAY_MS = 3000; // 3 seconds between reachability checks const TOTAL_TIMEOUT_MS = 300000; // 5 minutes total test timeout @@ -105,6 +106,7 @@ exports.runTests = function (suite) { ESPHomeDashboardEnabled: true, ESPHomeDashboardPort: DASHBOARD_PORT, ESPHomeDashboardVersion: DASHBOARD_VERSION, + PillowVersion: PILLOW_VERSION, }, }); From e15c248c957f1134cd385ea6126193cd1b8a4806 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:22:00 +0000 Subject: [PATCH 9/9] Extract version fetching to reusable method and add integration test - Create fetchAndCachePillowVersions() helper method - Simplify espHomeDashboard() by using the new helper - Add version_fetch_tests.js integration test to verify: - Versions are fetched from PyPI - Versions are cached in state - Only stable versions (no alpha/beta/rc) - Versions are sorted newest first - Integrate version fetch test into test suite Co-authored-by: DutchmanNL <7318445+DutchmanNL@users.noreply.github.com> --- main.js | 201 +++++++++---------- test/integrationTests/index.js | 2 + test/integrationTests/version_fetch_tests.js | 110 ++++++++++ 3 files changed, 203 insertions(+), 110 deletions(-) create mode 100644 test/integrationTests/version_fetch_tests.js diff --git a/main.js b/main.js index 968f354..f4385d4 100644 --- a/main.js +++ b/main.js @@ -276,116 +276,9 @@ class Esphome extends utils.Adapter { } // Fetch Pillow versions from PyPI and cache them - try { - const response = await fetch("https://pypi.org/pypi/pillow/json"); - if (response.ok) { - const data = await response.json(); - const versions = Object.keys(data.releases) - .filter( - (v) => !v.includes("a") && !v.includes("b") && !v.includes("rc"), - ) // Filter out alpha/beta/rc versions - .sort((a, b) => { - // Sort versions in descending order (newest first) - const aParts = a.split(".").map(Number); - const bParts = b.split(".").map(Number); - for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { - const aVal = aParts[i] || 0; - const bVal = bParts[i] || 0; - if (aVal !== bVal) { - return bVal - aVal; - } - } - return 0; - }) - .slice(0, 20); // Limit to 20 most recent versions - - // Cache the versions - await this.stateSetCreate( - `_ESPHomeDashboard.pillowVersionCache`, - "pillowVersionCache", - JSON.stringify(versions), - ); - - // Store in memory array - pillowVersions.length = 0; // Clear array - pillowVersions.push(...versions); - - if (versions.length > 0) { - await this.stateSetCreate( - `_ESPHomeDashboard.newestPillowVersion`, - "newestPillowVersion", - versions[0], - ); - this.log.debug( - `Fetched ${versions.length} Pillow versions from PyPI, newest: ${versions[0]}`, - ); - } - } else { - this.log.warn( - `Unable to fetch Pillow versions from PyPI: ${response.status}, using cached values`, - ); - // Try to load from cache - const cachedPillowVersions = await this.getStateAsync( - `_ESPHomeDashboard.pillowVersionCache`, - ); - if (cachedPillowVersions && cachedPillowVersions.val) { - const versions = JSON.parse(cachedPillowVersions.val); - pillowVersions.length = 0; - pillowVersions.push(...versions); - this.log.info( - `Loaded ${pillowVersions.length} Pillow versions from cache`, - ); - } - } - } catch (error) { - this.log.warn( - `Error fetching Pillow versions from PyPI: ${error.message}, using cached or fallback versions`, - ); - // Try to load from cache - try { - const cachedPillowVersions = await this.getStateAsync( - `_ESPHomeDashboard.pillowVersionCache`, - ); - if (cachedPillowVersions && cachedPillowVersions.val) { - const versions = JSON.parse(cachedPillowVersions.val); - pillowVersions.length = 0; - pillowVersions.push(...versions); - this.log.info( - `Loaded ${pillowVersions.length} Pillow versions from cache`, - ); - } else { - // Use fallback versions if cache is also not available - const fallbackVersions = [ - "11.3.0", - "11.2.0", - "11.1.0", - "11.0.0", - "10.4.0", - "10.3.0", - ]; - pillowVersions.length = 0; - pillowVersions.push(...fallbackVersions); - this.log.info( - `Using ${fallbackVersions.length} fallback Pillow versions`, - ); - } - } catch { - // If cache read fails, use fallback - const fallbackVersions = [ - "11.3.0", - "11.2.0", - "11.1.0", - "11.0.0", - "10.4.0", - "10.3.0", - ]; - pillowVersions.length = 0; - pillowVersions.push(...fallbackVersions); - this.log.info( - `Using ${fallbackVersions.length} fallback Pillow versions`, - ); - } - } + const versions = await this.fetchAndCachePillowVersions(); + pillowVersions.length = 0; // Clear array + pillowVersions.push(...versions); // Determine Pillow version to use let usePillowVersion = "11.3.0"; // Default version @@ -2238,6 +2131,94 @@ class Esphome extends utils.Adapter { } } + /** + * Fetch Pillow versions from PyPI and cache them + * + * @returns {Promise} Array of available Pillow versions + */ + async fetchAndCachePillowVersions() { + const fallbackVersions = [ + "11.3.0", + "11.2.0", + "11.1.0", + "11.0.0", + "10.4.0", + "10.3.0", + ]; + + try { + const response = await fetch("https://pypi.org/pypi/pillow/json"); + if (response.ok) { + const data = await response.json(); + const versions = Object.keys(data.releases) + .filter( + (v) => !v.includes("a") && !v.includes("b") && !v.includes("rc"), + ) // Filter out alpha/beta/rc versions + .sort((a, b) => { + // Sort versions in descending order (newest first) + const aParts = a.split(".").map(Number); + const bParts = b.split(".").map(Number); + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aVal = aParts[i] || 0; + const bVal = bParts[i] || 0; + if (aVal !== bVal) { + return bVal - aVal; + } + } + return 0; + }) + .slice(0, 20); // Limit to 20 most recent versions + + if (versions.length > 0) { + // Cache the versions + await this.stateSetCreate( + `_ESPHomeDashboard.pillowVersionCache`, + "pillowVersionCache", + JSON.stringify(versions), + ); + await this.stateSetCreate( + `_ESPHomeDashboard.newestPillowVersion`, + "newestPillowVersion", + versions[0], + ); + this.log.debug( + `Fetched ${versions.length} Pillow versions from PyPI, newest: ${versions[0]}`, + ); + return versions; + } + this.log.warn( + "No stable Pillow versions found on PyPI, using cached or fallback versions", + ); + } else { + this.log.warn( + `Unable to fetch Pillow versions from PyPI: ${response.status}, using cached values`, + ); + } + } catch (error) { + this.log.warn( + `Error fetching Pillow versions from PyPI: ${error.message}, using cached or fallback versions`, + ); + } + + // Try to load from cache + try { + const cachedPillowVersions = await this.getStateAsync( + `_ESPHomeDashboard.pillowVersionCache`, + ); + if (cachedPillowVersions && cachedPillowVersions.val) { + const versions = JSON.parse(cachedPillowVersions.val); + this.log.info(`Loaded ${versions.length} Pillow versions from cache`); + return versions; + } + } catch { + // Cache read failed, will use fallback + } + + // Use fallback versions + this.log.info(`Using ${fallbackVersions.length} fallback Pillow versions`); + return fallbackVersions; + } + /** * Clear the autopy cache directory */ diff --git a/test/integrationTests/index.js b/test/integrationTests/index.js index 42d79ec..f9ea4c8 100644 --- a/test/integrationTests/index.js +++ b/test/integrationTests/index.js @@ -1,5 +1,7 @@ const dashboard_tests = require("./dashboard_tests"); +const version_fetch_tests = require("./version_fetch_tests"); exports.runTests = function (suite) { dashboard_tests.runTests(suite); + version_fetch_tests.runTests(suite); }; diff --git a/test/integrationTests/version_fetch_tests.js b/test/integrationTests/version_fetch_tests.js new file mode 100644 index 0000000..88be12e --- /dev/null +++ b/test/integrationTests/version_fetch_tests.js @@ -0,0 +1,110 @@ +/** + * Version Fetch Integration Test + * + * Tests the Pillow version fetching and caching mechanism. + * This verifies that the adapter can properly: + * 1. Fetch Pillow versions from PyPI + * 2. Cache versions in state + * 3. Fall back to cache when PyPI is unavailable + * 4. Use fallback versions when both PyPI and cache are unavailable + */ + +const { expect } = require("chai"); + +exports.runTests = function (suite) { + suite("Pillow Version Fetching", (getHarness) => { + it("should fetch and cache Pillow versions during initialization", async function () { + this.timeout(60000); // Extended timeout for API call + + const harness = getHarness(); + + try { + // Stop the adapter if it's already running + if (harness.isAdapterRunning()) { + console.log("Stopping running adapter..."); + await harness.stopAdapter(); + } + + console.log("Enabling dashboard to trigger version fetch..."); + // Enable the dashboard which triggers version fetching + await harness.changeAdapterConfig("esphome", { + native: { + ESPHomeDashboardEnabled: true, + ESPHomeDashboardPort: 6052, + ESPHomeDashboardVersion: "Always last available", + PillowVersion: "Always last available", + }, + }); + + // Small delay to ensure configuration is persisted + await new Promise((resolve) => setTimeout(resolve, 1000)); + + console.log("Starting adapter to fetch versions..."); + await harness.startAdapterAndWait(); + + // Wait for version fetch to complete + console.log("Waiting for version fetch to complete..."); + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Check if Pillow version cache state exists and has data + console.log("Checking Pillow version cache..."); + const pillowVersionCache = await harness.states.getStateAsync( + "esphome.0._ESPHomeDashboard.pillowVersionCache", + ); + + expect(pillowVersionCache).to.exist; + expect(pillowVersionCache.val).to.be.a("string"); + + const cachedVersions = JSON.parse(pillowVersionCache.val); + expect(cachedVersions).to.be.an("array"); + expect(cachedVersions).to.have.length.greaterThan(0); + + console.log( + `✓ Successfully cached ${cachedVersions.length} Pillow versions`, + ); + console.log(`✓ Newest version: ${cachedVersions[0]}`); + + // Verify the newest version is also stored separately + const newestPillowVersion = await harness.states.getStateAsync( + "esphome.0._ESPHomeDashboard.newestPillowVersion", + ); + + if (newestPillowVersion) { + expect(newestPillowVersion.val).to.equal(cachedVersions[0]); + console.log( + `✓ Newest version state matches: ${newestPillowVersion.val}`, + ); + } + + // Verify versions are filtered (no alpha/beta/rc) + const hasPreRelease = cachedVersions.some( + (v) => v.includes("a") || v.includes("b") || v.includes("rc"), + ); + expect(hasPreRelease).to.be.false; + console.log("✓ All versions are stable releases (no alpha/beta/rc)"); + + // Verify versions are sorted (newest first) + const versionParts = cachedVersions[0].split(".").map(Number); + expect(versionParts[0]).to.be.greaterThan(9); // Major version should be 10+ + console.log("✓ Versions are properly sorted (newest first)"); + + console.log("✓ Version fetching test passed completely"); + } catch (error) { + console.error(`Version fetch test failed: ${error.message}`); + throw error; + } finally { + // Ensure the adapter is always stopped + try { + if (harness && harness.isAdapterRunning()) { + console.log("Stopping adapter in cleanup (finally)..."); + await harness.stopAdapter(); + } + } catch (cleanupError) { + console.error( + `Failed to stop adapter during version fetch test cleanup: ${cleanupError.message}`, + ); + } + } + }); + }); +};