iobroker.zeptrion 1.0.8 → 1.0.10

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
@@ -176,6 +176,13 @@ npm run test:integration # starts a real js-controller (takes longer)
176
176
 
177
177
  ## Changelog
178
178
 
179
+ ### 1.0.10 (2026-08-10)
180
+
181
+ - Fix all remaining findings from the follow-up review: translated 18 German error messages in validateDeviceRow(), the 5 error-code-to-message translations in handleDeviceError(), 9 German strings in thrown Error objects, and the bonjour-service install-hint rejection message. Per explicit maintainer direction, all UI-facing result text in onMessage (CSV import report, device test results, discovery summary) is now English as well, superseding the earlier decision to keep it German for the target audience. No migration needed - none of these fixes touch persisted object common properties.
182
+
183
+ ### 1.0.9 (2026-08-08)
184
+ - Fix: the common.name i18n conversion from 1.0.8 only applied to newly created objects (setObjectNotExistsAsync/ensureState never update existing ones) - any installation upgrading from <=1.0.7 kept the old plain-German name strings forever. migrateObjectRoles() now also force-corrects these on every startup via a value-based lookup table generated from the same translations already used in the object-creation code, plus dedicated regex rules for the two dynamic cases (scene button names, tilt pulse duration). Also fixes two translation gaps that were missed in 1.0.8 (network info fields and the shutter position estimate/move descriptions) which the extraction script used to build the migration table happened to catch. Verified against a live object dump (409 objects, 4 devices): corrects exactly the 385 affected objects, 0 false positives on user-configured device/room names.
185
+
179
186
  ### 1.0.8 (2026-08-08)
180
187
  - Fix all findings from the manual maintainer review (PR #6327): removed the manual npm installation section from README.md/README_de.md (E6012, prohibited regardless of stated intent); added a verified link to the Feller product page; translated all 40+ German log messages to English (UI-facing result text for CSV import/discovery, shown in the admin config dialog, is intentionally kept German and decoupled from the log call); converted all 50 German common.name strings (incl. the CH_BUTTONS constant and dynamic channel/scene names) to full 11-language i18n objects; completed io-package.json instanceObjects translations for 'info' and 'control' (info.connection already had all 11 languages).
181
188
 
@@ -192,13 +199,7 @@ npm run test:integration # starts a real js-controller (takes longer)
192
199
  ### 1.0.4 (2026-07-21)
193
200
  - Documentation only: added a Buy Me a Coffee link next to the PayPal donate badge. No functional changes.
194
201
 
195
- ### 1.0.3 (2026-07-17)
196
- - Documentation only: fixed the PayPal donate link, which previously used the wrong URL format and did not work.
197
-
198
- ### 1.0.2 (2026-07-17)
199
- - FIX: the global (adapter-level) info.connection translations added in 1.0.1 are only synced automatically by js-controller on certain update paths (e.g. 'iobroker upgrade'), not reliably when installing via 'iobroker url' - added this to the startup migration too, so it no longer depends on that.
200
-
201
- ### 1.0.1 and older
202
+ ### 1.0.3 and older
202
203
 
203
204
  Older changelog entries can be found in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
204
205
 
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zeptrion",
4
- "version": "1.0.8",
4
+ "version": "1.0.10",
5
5
  "news": {
6
+ "1.0.10": {
7
+ "en": "Fix all remaining findings from the follow-up review: translated 18 German error messages in validateDeviceRow() (row/duplicate validation, including 6 that were missed in an earlier pass), the 5 error-code-to-message translations in handleDeviceError() (also written to the info.lastError state), 9 German strings in thrown Error objects, and the bonjour-service install-hint rejection message. Per explicit maintainer direction, all UI-facing result text in onMessage (CSV import report lines, device test results, discovery summary) is now English as well, superseding the earlier design decision to keep it German for the target audience - the maintainer clarified that all user-facing text must be English or full i18n, with no exception for admin-dialog result strings. No migration needed: none of these fixes touch persisted object common properties, only log output, transient state values that refresh on the next occurrence, and one-off message-callback payloads.",
8
+ "de": "Behebt alle verbleibenden Befunde aus dem Folge-Review: 18 deutsche Fehlermeldungen in validateDeviceRow() übersetzt (Zeilen-/Duplikat-Validierung, davon 6 in einem früheren Durchgang übersehen), die 5 Fehlercode-zu-Meldung-Übersetzungen in handleDeviceError() (werden auch in den info.lastError-State geschrieben), 9 deutsche Strings in geworfenen Error-Objekten sowie die bonjour-service-Installationshinweis-Meldung. Auf explizite Anweisung des Maintainers ist jetzt auch aller UI-facing Ergebnistext in onMessage (CSV-Import-Bericht, Gerätetest-Ergebnisse, Discovery-Zusammenfassung) Englisch - das ersetzt die frühere Design-Entscheidung, diesen für die Zielgruppe Deutsch zu belassen; der Maintainer stellte klar, dass aller nutzerseitiger Text Englisch oder vollständig i18n sein muss, ohne Ausnahme für Admin-Dialog-Ergebnistexte. Keine Migration nötig: keiner dieser Fixes betrifft persistierte Objekt-common-Eigenschaften, nur Log-Ausgabe, transiente State-Werte (aktualisieren sich beim nächsten Ereignis von selbst) und einmalige Message-Callback-Payloads.",
9
+ "ru": "Исправлены все оставшиеся замечания из повторного обзора: переведены немецкие сообщения об ошибках и весь текст, отправляемый пользователю через onMessage, теперь на английском по явному указанию мейнтейнера. Миграция не требуется.",
10
+ "pt": "Corrigidos todos os pontos restantes da revisão de acompanhamento: mensagens de erro em alemão traduzidas e todo o texto enviado ao utilizador via onMessage agora em inglês, por indicação explícita do mantenedor. Não é necessária migração.",
11
+ "nl": "Alle resterende bevindingen uit de vervolgreview verholpen: Duitse foutmeldingen vertaald en alle tekst die via onMessage naar de gebruiker wordt gestuurd nu in het Engels, op expliciete aanwijzing van de maintainer. Geen migratie nodig.",
12
+ "fr": "Correction de tous les points restants du contrôle de suivi : messages d'erreur allemands traduits et tout le texte envoyé à l'utilisateur via onMessage désormais en anglais, sur instruction explicite du mainteneur. Aucune migration nécessaire.",
13
+ "it": "Corretti tutti i rilievi rimanenti della revisione di follow-up: messaggi di errore in tedesco tradotti e tutto il testo inviato all'utente tramite onMessage ora in inglese, su indicazione esplicita del maintainer. Nessuna migrazione necessaria.",
14
+ "es": "Se corrigieron todos los hallazgos restantes de la revisión de seguimiento: mensajes de error en alemán traducidos y todo el texto enviado al usuario a través de onMessage ahora en inglés, por indicación explícita del mantenedor. No se necesita migración.",
15
+ "pl": "Naprawiono wszystkie pozostałe uwagi z ponownego przeglądu: przetłumaczono niemieckie komunikaty błędów, a cały tekst wysyłany do użytkownika przez onMessage jest teraz po angielsku, zgodnie z wyraźnym wskazaniem opiekuna. Migracja nie jest potrzebna.",
16
+ "uk": "Виправлено всі залишкові зауваження повторного огляду: перекладено німецькі повідомлення про помилки, увесь текст, що надсилається користувачу через onMessage, тепер англійською за явною вказівкою мейнтейнера. Міграція не потрібна.",
17
+ "zh-cn": "修复了后续审查中的所有剩余问题:翻译了德语错误消息,根据维护者的明确指示,通过 onMessage 发送给用户的所有文本现在均为英文。无需迁移。"
18
+ },
19
+ "1.0.9": {
20
+ "en": "Fix: the common.name i18n conversion from 1.0.8 only applied to newly created objects (setObjectNotExistsAsync/ensureState never update existing ones) - any installation upgrading from <=1.0.7 kept the old plain-German name strings forever. migrateObjectRoles() now also force-corrects these on every startup via a value-based lookup table generated from the same translations already used in the object-creation code, plus dedicated regex rules for the two dynamic cases (scene button names, tilt pulse duration). Also fixes two translation gaps that were missed in 1.0.8 (network info fields and the shutter position estimate/move descriptions) which the extraction script used to build the migration table happened to catch. Verified against a live object dump (409 objects, 4 devices): corrects exactly the 385 affected objects, 0 false positives on user-configured device/room names.",
21
+ "de": "Fix: Die common.name-i18n-Umstellung aus 1.0.8 griff nur bei neu erstellten Objekten (setObjectNotExistsAsync/ensureState aktualisieren bestehende Objekte nie) - jedes Update von <=1.0.7 behielt die alten deutschen Plain-String-Namen für immer. migrateObjectRoles() korrigiert diese jetzt bei jedem Start ebenfalls, über eine wertbasierte Nachschlagetabelle, generiert aus denselben Übersetzungen, die bereits im Objekterstellungs-Code stehen, plus dedizierte Regex-Regeln für die zwei dynamischen Fälle (Szenen-Button-Namen, Lamellen-Impulsdauer). Behebt zudem zwei Übersetzungslücken, die in 1.0.8 übersehen wurden (Netzwerk-Info-Felder und die Storen-Positions-Schätzung/-Anfahrt-Beschreibungen), die das Extraktionsskript zum Aufbau der Migrationstabelle zufällig aufgedeckt hat. Verifiziert gegen einen echten Objekt-Dump (409 Objekte, 4 Geräte): korrigiert exakt die 385 betroffenen Objekte, 0 Fehltreffer bei nutzerkonfigurierten Geräte-/Raumnamen.",
22
+ "ru": "Исправление: миграция теперь также исправляет старые немецкие имена объектов на уже существующих установках, а не только на новых объектах. Проверено на реальном дампе объектов (409 объектов): исправлено ровно 385 затронутых объектов.",
23
+ "pt": "Correção: a migração agora também corrige os nomes de objetos em alemão em instalações já existentes, não apenas em novos objetos. Verificado num dump real (409 objetos): corrigidos exatamente os 385 objetos afetados.",
24
+ "nl": "Fix: de migratie corrigeert nu ook oude Duitse objectnamen op reeds bestaande installaties, niet alleen op nieuwe objecten. Geverifieerd tegen een echte objectdump (409 objecten): precies de 385 betrokken objecten gecorrigeerd.",
25
+ "fr": "Correction : la migration corrige désormais aussi les anciens noms d'objets allemands sur les installations déjà existantes, pas seulement sur les nouveaux objets. Vérifié sur un vidage d'objets réel (409 objets) : correction exacte des 385 objets concernés.",
26
+ "it": "Correzione: la migrazione ora corregge anche i vecchi nomi di oggetti in tedesco sulle installazioni già esistenti, non solo sui nuovi oggetti. Verificato su un dump di oggetti reale (409 oggetti): corretti esattamente i 385 oggetti interessati.",
27
+ "es": "Corrección: la migración ahora también corrige los antiguos nombres de objetos en alemán en instalaciones ya existentes, no solo en objetos nuevos. Verificado contra un volcado de objetos real (409 objetos): se corrigieron exactamente los 385 objetos afectados.",
28
+ "pl": "Poprawka: migracja naprawia teraz również stare niemieckie nazwy obiektów w już istniejących instalacjach, nie tylko w nowych obiektach. Zweryfikowano na rzeczywistym zrzucie obiektów (409 obiektów): naprawiono dokładnie 385 dotkniętych obiektów.",
29
+ "uk": "Виправлення: міграція тепер також виправляє старі німецькі назви об'єктів на вже існуючих інсталяціях, а не лише на нових об'єктах. Перевірено на реальному дампі об'єктів (409 об'єктів): виправлено рівно 385 уражених об'єктів.",
30
+ "zh-cn": "修复:迁移现在也会修正已有安装中的旧德语对象名称,而不仅仅是新对象。已针对真实对象转储(409 个对象)验证:正好修正了 385 个受影响的对象。"
31
+ },
6
32
  "1.0.8": {
7
33
  "en": "Fix all findings from the manual maintainer review (PR #6327): removed the manual npm installation section from README.md/README_de.md (E6012, prohibited regardless of stated intent); added a verified link to the Feller product page; translated all 40+ German log messages to English (UI-facing result text for CSV import/discovery, shown in the admin config dialog, is intentionally kept German and decoupled from the log call); converted all 50 German common.name strings (incl. the CH_BUTTONS constant and dynamic channel/scene names) to full 11-language i18n objects; completed io-package.json instanceObjects translations for 'info' and 'control' (info.connection already had all 11 languages).",
8
34
  "de": "Behebt alle Befunde aus dem manuellen Maintainer-Review (PR #6327): manuelle npm-Installationsanleitung aus README.md/README_de.md entfernt (E6012, unabhängig von der Absicht untersagt); verifizierten Link zur Feller-Produktseite ergänzt; alle 40+ deutschen Log-Meldungen ins Englische übersetzt (UI-Feedback-Text für CSV-Import/Discovery im Admin-Dialog bewusst Deutsch belassen und vom Log-Aufruf entkoppelt); alle 50 deutschen common.name-Strings (inkl. CH_BUTTONS-Konstante und dynamischer Kanal-/Szenen-Namen) auf vollständige 11-Sprachen-i18n-Objekte umgestellt; io-package.json instanceObjects-Übersetzungen für 'info' und 'control' vervollständigt (info.connection hatte bereits alle 11 Sprachen).",
@@ -67,32 +93,6 @@
67
93
  "pl": "Tylko dokumentacja: dodano link Buy Me a Coffee obok odznaki dotacji PayPal w README.md/README_de.md oraz w pakiecie npm (pole funding). Brak zmian funkcjonalnych.",
68
94
  "uk": "Лише документація: додано посилання Buy Me a Coffee поруч зі значком пожертв PayPal у README.md/README_de.md та в пакеті npm (поле funding). Без функціональних змін.",
69
95
  "zh-cn": "仅文档:在 README.md/README_de.md 及 npm 包(funding 字段)中,于 PayPal 捐赠徽章旁添加了 Buy Me a Coffee 链接。无功能变更。"
70
- },
71
- "1.0.3": {
72
- "en": "Documentation only: fixed the PayPal donate link in README (en/de), which previously used the wrong URL format and did not work. No functional/object changes.",
73
- "de": "Nur Dokumentation: den PayPal-Spenden-Link im README (en/de) korrigiert, der zuvor das falsche URL-Format hatte und nicht funktionierte. Keine funktionalen Änderungen/Objektänderungen.",
74
- "ru": "Только документация: исправлена ссылка для пожертвований PayPal в README (en/de), которая ранее имела неверный формат URL и не работала. Без функциональных изменений.",
75
- "pt": "Apenas documentação: corrigido o link de doação PayPal no README (en/de), que anteriormente tinha o formato de URL errado e não funcionava. Sem alterações funcionais.",
76
- "nl": "Alleen documentatie: de PayPal-donatielink in README (en/de) gecorrigeerd, die voorheen het verkeerde URL-formaat had en niet werkte. Geen functionele wijzigingen.",
77
- "fr": "Documentation uniquement : correction du lien de don PayPal dans le README (en/de), qui avait auparavant le mauvais format d'URL et ne fonctionnait pas. Aucun changement fonctionnel.",
78
- "it": "Solo documentazione: corretto il link di donazione PayPal nel README (en/de), che in precedenza aveva il formato URL errato e non funzionava. Nessuna modifica funzionale.",
79
- "es": "Solo documentación: se corrigió el enlace de donación de PayPal en el README (en/de), que anteriormente tenía el formato de URL incorrecto y no funcionaba. Sin cambios funcionales.",
80
- "pl": "Tylko dokumentacja: naprawiono link do darowizn PayPal w README (en/de), który wcześniej miał zły format URL i nie działał. Brak zmian funkcjonalnych.",
81
- "uk": "Лише документація: виправлено посилання для пожертв PayPal у README (en/de), яке раніше мало неправильний формат URL і не працювало. Без функціональних змін.",
82
- "zh-cn": "仅文档:修正了 README(en/de)中的 PayPal 捐赠链接,此前该链接使用了错误的 URL 格式且无法正常使用。无功能变更。"
83
- },
84
- "1.0.2": {
85
- "en": "FIX: the global (adapter-level) info.connection translations added in 1.0.1 are only synced automatically by js-controller on certain update paths (e.g. 'iobroker upgrade'), not reliably when installing via 'iobroker url' - added this to the startup migration too, so it no longer depends on that.",
86
- "de": "FIX: die in 1.0.1 ergänzten Übersetzungen für das globale (adapter-eigene) info.connection werden von js-controller nur bei bestimmten Update-Wegen (z.B. 'iobroker upgrade') automatisch synchronisiert, nicht zuverlässig bei Installation via 'iobroker url' - auch in die Start-Migration aufgenommen, damit das nicht mehr davon abhängt.",
87
- "ru": "ИСПРАВЛЕНИЕ: глобальные переводы info.connection, добавленные в 1.0.1, синхронизируются js-controller автоматически только при определённых способах обновления, ненадёжно при установке через 'iobroker url' - добавлено также в миграцию при запуске.",
88
- "pt": "CORREÇÃO: as traduções globais de info.connection adicionadas na 1.0.1 só são sincronizadas automaticamente pelo js-controller em determinados caminhos de atualização, não de forma fiável ao instalar via 'iobroker url' - adicionado também à migração de arranque.",
89
- "nl": "FIX: de globale info.connection-vertalingen toegevoegd in 1.0.1 worden alleen automatisch gesynchroniseerd door js-controller bij bepaalde update-paden, niet betrouwbaar bij installatie via 'iobroker url' - ook toegevoegd aan de opstartmigratie.",
90
- "fr": "CORRECTIF : les traductions globales info.connection ajoutées en 1.0.1 ne sont synchronisées automatiquement par js-controller que sur certains chemins de mise à jour, pas de manière fiable lors d'une installation via 'iobroker url' - ajouté également à la migration au démarrage.",
91
- "it": "CORREZIONE: le traduzioni globali di info.connection aggiunte nella 1.0.1 vengono sincronizzate automaticamente da js-controller solo su determinati percorsi di aggiornamento, non in modo affidabile quando si installa tramite 'iobroker url' - aggiunto anche alla migrazione all'avvio.",
92
- "es": "CORRECCIÓN: las traducciones globales de info.connection añadidas en 1.0.1 solo se sincronizan automáticamente mediante js-controller en determinadas rutas de actualización, no de forma fiable al instalar mediante 'iobroker url' - añadido también a la migración de inicio.",
93
- "pl": "POPRAWKA: globalne tłumaczenia info.connection dodane w 1.0.1 są automatycznie synchronizowane przez js-controller tylko przy określonych ścieżkach aktualizacji, niewiarygodnie przy instalacji przez 'iobroker url' - dodano również do migracji przy starcie.",
94
- "uk": "ВИПРАВЛЕННЯ: глобальні переклади info.connection, додані в 1.0.1, синхронізуються js-controller автоматично лише за певних шляхів оновлення, ненадійно при встановленні через 'iobroker url' - додано також до міграції під час запуску.",
95
- "zh-cn": "修复:1.0.1 中添加的全局 info.connection 翻译仅在特定更新路径(例如 'iobroker upgrade')下由 js-controller 自动同步,通过 'iobroker url' 安装时并不可靠——现已同样加入启动迁移中。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/main.js CHANGED
@@ -106,6 +106,109 @@ const CH_BUTTONS = {
106
106
  dim_down: { en: 'Dim down (hold button)', de: 'Dimmen runter (Taste halten)', ru: 'Уменьшить яркость (удерж. кнопку)', pt: 'Diminuir luminosidade (manter botão)', nl: 'Dimmen omlaag (knop ingedrukt houden)', fr: 'Diminuer la lumière (maintenir le bouton)', it: 'Diminuisci luminosità (tieni premuto)', es: 'Disminuir brillo (mantener pulsado)', pl: 'Przyciemnij (przytrzymaj przycisk)', uk: 'Зменшити яскравість (утримувати кнопку)', 'zh-cn': '调暗(长按按钮)' }
107
107
  };
108
108
 
109
+ // Value-based migration table: maps a plain-string common.name (as created by versions <1.0.8)
110
+ // to the corresponding full 11-language i18n object. Used in migrateObjectRoles() to force-
111
+ // correct EXISTING objects on already-running installations (setObjectNotExistsAsync/ensureState
112
+ // only ever create, they never update an object that already exists). Deliberately a strict,
113
+ // exact-match lookup - not a "contains German characters" heuristic - so user-configured device/
114
+ // room names (e.g. "Buero", "Kueche") can never accidentally match and get overwritten.
115
+ const NAME_MIGRATION_MAP = {
116
+ 'ACHTUNG: Werksreset - löscht ALLE Einstellungen inkl. WLAN, Gerät fällt vom Netz! Erfordert vorheriges system.unlock (30s-Fenster)': { en: 'WARNING: Factory reset - deletes ALL settings incl. WLAN, device will drop off the network! Requires prior system.unlock (30s window)', de: 'ACHTUNG: Werksreset - löscht ALLE Einstellungen inkl. WLAN, Gerät fällt vom Netz! Erfordert vorheriges system.unlock (30s-Fenster)', ru: 'ВНИМАНИЕ: сброс до заводских настроек - удаляет ВСЕ настройки, вкл. WLAN, устройство отключится от сети! Требуется предварительный system.unlock (окно 30с)', pt: 'ATENÇÃO: reposição de fábrica - apaga TODAS as definições incl. WLAN, o dispositivo sairá da rede! Requer system.unlock prévio (janela de 30s)', nl: 'LET OP: fabrieksreset - wist ALLE instellingen incl. WLAN, apparaat valt van het netwerk! Vereist voorafgaand system.unlock (30s-venster)', fr: 'ATTENTION : réinitialisation d\'usine - supprime TOUS les réglages, y compris le WLAN, l\'appareil sera déconnecté du réseau ! Nécessite un system.unlock préalable (fenêtre de 30s)', it: 'ATTENZIONE: ripristino di fabbrica - cancella TUTTE le impostazioni incl. WLAN, il dispositivo si disconnette dalla rete! Richiede system.unlock preventivo (finestra di 30s)', es: 'ATENCIÓN: restablecimiento de fábrica - borra TODOS los ajustes incl. WLAN, ¡el dispositivo se desconectará de la red! Requiere system.unlock previo (ventana de 30s)', pl: 'UWAGA: przywrócenie ustawień fabrycznych - usuwa WSZYSTKIE ustawienia wraz z WLAN, urządzenie odłączy się od sieci! Wymaga wcześniejszego system.unlock (okno 30s)', uk: 'УВАГА: скидання до заводських налаштувань - видаляє ВСІ налаштування, включно з WLAN, пристрій відключиться від мережі! Потрібен попередній system.unlock (вікно 30с)', 'zh-cn': '警告:恢复出厂设置 - 将删除所有设置(包括 WLAN),设备将离线!需要事先执行 system.unlock(30秒窗口)' },
117
+ 'ALLE Storen schliessen (z.B. Hagelalarm)': { en: 'Close ALL shutters (e.g. hail alarm)', de: 'ALLE Storen schliessen (z.B. Hagelalarm)', ru: 'Закрыть ВСЕ жалюзи (напр. градовая тревога)', pt: 'Fechar TODOS os estores (p.ex. alarme de granizo)', nl: 'ALLE zonweringen sluiten (bijv. hagelalarm)', fr: 'Fermer TOUS les stores (p.ex. alarme grêle)', it: 'Chiudi TUTTE le tapparelle (es. allarme grandine)', es: 'Cerrar TODAS las persianas (p.ej. alarma de granizo)', pl: 'Zamknij WSZYSTKIE rolety (np. alarm gradowy)', uk: 'Закрити ВСІ жалюзі (напр. градова тривога)', 'zh-cn': '关闭所有卷帘(例如冰雹警报)' },
118
+ 'Abfrageintervall in Stunden (0=deaktiviert)': { en: 'Polling interval in hours (0=disabled)', de: 'Abfrageintervall in Stunden (0=deaktiviert)', ru: 'Интервал опроса в часах (0=отключено)', pt: 'Intervalo de consulta em horas (0=desativado)', nl: 'Poll-interval in uren (0=uitgeschakeld)', fr: 'Intervalle d\'interrogation en heures (0=désactivé)', it: 'Intervallo di polling in ore (0=disattivato)', es: 'Intervalo de consulta en horas (0=desactivado)', pl: 'Interwał odpytywania w godzinach (0=wyłączone)', uk: 'Інтервал опитування в годинах (0=вимкнено)', 'zh-cn': '轮询间隔(小时,0=禁用)' },
119
+ 'Aktueller LED-Status (JSON, read-only)': { en: 'Current LED status (JSON, read-only)', de: 'Aktueller LED-Status (JSON, read-only)', ru: 'Текущий статус светодиода (JSON, только чтение)', pt: 'Estado atual do LED (JSON, só leitura)', nl: 'Huidige LED-status (JSON, alleen-lezen)', fr: 'État actuel de la LED (JSON, lecture seule)', it: 'Stato attuale del LED (JSON, sola lettura)', es: 'Estado actual del LED (JSON, solo lectura)', pl: 'Aktualny stan LED (JSON, tylko do odczytu)', uk: 'Поточний стан світлодіода (JSON, лише читання)', 'zh-cn': '当前 LED 状态(JSON,只读)' },
120
+ 'Alle Storen stoppen': { en: 'Stop all shutters', de: 'Alle Storen stoppen', ru: 'Остановить все жалюзи', pt: 'Parar todos os estores', nl: 'Alle zonweringen stoppen', fr: 'Arrêter tous les stores', it: 'Ferma tutte le tapparelle', es: 'Detener todas las persianas', pl: 'Zatrzymaj wszystkie rolety', uk: 'Зупинити всі жалюзі', 'zh-cn': '停止所有卷帘' },
121
+ 'Alle Storen öffnen': { en: 'Open all shutters', de: 'Alle Storen öffnen', ru: 'Открыть все жалюзи', pt: 'Abrir todos os estores', nl: 'Alle zonweringen openen', fr: 'Ouvrir tous les stores', it: 'Apri tutte le tapparelle', es: 'Abrir todas las persianas', pl: 'Otwórz wszystkie rolety', uk: 'Відкрити всі жалюзі', 'zh-cn': '打开所有卷帘' },
122
+ 'Bootloader-Version': { en: 'Bootloader version', de: 'Bootloader-Version', ru: 'Версия загрузчика', pt: 'Versão do bootloader', nl: 'Bootloaderversie', fr: 'Version du bootloader', it: 'Versione bootloader', es: 'Versión del bootloader', pl: 'Wersja bootloadera', uk: 'Версія завантажувача', 'zh-cn': '引导程序版本' },
123
+ 'Button: Geräte-Uhrzeit mit ioBroker-Host synchronisieren': { en: 'Button: synchronize device time with the ioBroker host', de: 'Button: Geräte-Uhrzeit mit ioBroker-Host synchronisieren', ru: 'Кнопка: синхронизировать время устройства с хостом ioBroker', pt: 'Botão: sincronizar hora do dispositivo com o host ioBroker', nl: 'Knop: apparaatklok synchroniseren met de ioBroker-host', fr: 'Bouton : synchroniser l\'heure de l\'appareil avec l\'hôte ioBroker', it: 'Pulsante: sincronizza l\'ora del dispositivo con l\'host ioBroker', es: 'Botón: sincronizar la hora del dispositivo con el host de ioBroker', pl: 'Przycisk: synchronizuj czas urządzenia z hostem ioBroker', uk: 'Кнопка: синхронізувати час пристрою з хостом ioBroker', 'zh-cn': '按钮:将设备时间与 ioBroker 主机同步' },
124
+ 'Datum/Zeit': { en: 'Date/time', de: 'Datum/Zeit', ru: 'Дата/время', pt: 'Data/hora', nl: 'Datum/tijd', fr: 'Date/heure', it: 'Data/ora', es: 'Fecha/hora', pl: 'Data/godzina', uk: 'Дата/час', 'zh-cn': '日期/时间' },
125
+ 'Entriegelung für Werksreset (Sicherheitsverriegelung: muss max. 30s VOR factoryDefault auf true gesetzt werden)': { en: 'Unlock for factory reset (safety interlock: must be set to true max. 30s BEFORE factoryDefault)', de: 'Entriegelung für Werksreset (Sicherheitsverriegelung: muss max. 30s VOR factoryDefault auf true gesetzt werden)', ru: 'Разблокировка для сброса до заводских настроек (интерлок: должно быть true не более чем за 30с ДО factoryDefault)', pt: 'Desbloqueio para reposição de fábrica (interbloqueio de segurança: deve ser definido como true no máx. 30s ANTES de factoryDefault)', nl: 'Ontgrendeling voor fabrieksreset (veiligheidsvergrendeling: moet max. 30s VOOR factoryDefault op true gezet worden)', fr: 'Déverrouillage pour réinitialisation d\'usine (verrouillage de sécurité : doit être mis à true max. 30s AVANT factoryDefault)', it: 'Sblocco per il ripristino di fabbrica (interblocco di sicurezza: deve essere impostato su true max. 30s PRIMA di factoryDefault)', es: 'Desbloqueo para restablecimiento de fábrica (enclavamiento de seguridad: debe ponerse a true máx. 30s ANTES de factoryDefault)', pl: 'Odblokowanie przywracania ustawień fabrycznych (blokada bezpieczeństwa: musi być ustawione na true maks. 30s PRZED factoryDefault)', uk: 'Розблокування для скидання до заводських налаштувань (блокування безпеки: має бути true не більше ніж за 30с ДО factoryDefault)', 'zh-cn': '解锁恢复出厂设置(安全联锁:必须在 factoryDefault 之前最多 30 秒设置为 true)' },
126
+ 'Freier Befehl (z.B. dim_2000, move_close_5000, recall_s1 …)': { en: 'Free-text command (e.g. dim_2000, move_close_5000, recall_s1 ...)', de: 'Freier Befehl (z.B. dim_2000, move_close_5000, recall_s1 …)', ru: 'Произвольная команда (напр. dim_2000, move_close_5000, recall_s1 …)', pt: 'Comando livre (p.ex. dim_2000, move_close_5000, recall_s1 …)', nl: 'Vrije opdracht (bijv. dim_2000, move_close_5000, recall_s1 …)', fr: 'Commande libre (p.ex. dim_2000, move_close_5000, recall_s1 …)', it: 'Comando libero (es. dim_2000, move_close_5000, recall_s1 …)', es: 'Comando libre (p.ej. dim_2000, move_close_5000, recall_s1 …)', pl: 'Dowolne polecenie (np. dim_2000, move_close_5000, recall_s1 …)', uk: 'Довільна команда (напр. dim_2000, move_close_5000, recall_s1 …)', 'zh-cn': '自由命令(例如 dim_2000, move_close_5000, recall_s1 …)' },
127
+ 'Geräteinformationen': { en: 'Device information', de: 'Geräteinformationen', ru: 'Информация об устройстве', pt: 'Informação do dispositivo', nl: 'Apparaatinformatie', fr: 'Informations sur l\'appareil', it: 'Informazioni sul dispositivo', es: 'Información del dispositivo', pl: 'Informacje o urządzeniu', uk: 'Інформація про пристрій', 'zh-cn': '设备信息' },
128
+ 'Gerätetyp (Device ID)': { en: 'Device type (Device ID)', de: 'Gerätetyp (Device ID)', ru: 'Тип устройства (Device ID)', pt: 'Tipo de dispositivo (Device ID)', nl: 'Apparaattype (Device ID)', fr: 'Type d\'appareil (Device ID)', it: 'Tipo di dispositivo (Device ID)', es: 'Tipo de dispositivo (Device ID)', pl: 'Typ urządzenia (Device ID)', uk: 'Тип пристрою (Device ID)', 'zh-cn': '设备类型 (Device ID)' },
129
+ 'Gruppe (chdes)': { en: 'Group (chdes)', de: 'Gruppe (chdes)', ru: 'Группа (chdes)', pt: 'Grupo (chdes)', nl: 'Groep (chdes)', fr: 'Groupe (chdes)', it: 'Gruppo (chdes)', es: 'Grupo (chdes)', pl: 'Grupa (chdes)', uk: 'Група (chdes)', 'zh-cn': '分组 (chdes)' },
130
+ 'Hardware-Version': { en: 'Hardware version', de: 'Hardware-Version', ru: 'Версия оборудования', pt: 'Versão do hardware', nl: 'Hardwareversie', fr: 'Version matérielle', it: 'Versione hardware', es: 'Versión de hardware', pl: 'Wersja sprzętu', uk: 'Версія обладнання', 'zh-cn': '硬件版本' },
131
+ 'Helligkeit': { en: 'Brightness', de: 'Helligkeit', ru: 'Яркость', pt: 'Luminosidade', nl: 'Helderheid', fr: 'Luminosité', it: 'Luminosità', es: 'Luminosidad', pl: 'Jasność', uk: 'Яскравість', 'zh-cn': '亮度' },
132
+ 'Icon (chdes)': { en: 'Icon (chdes)', de: 'Icon (chdes)', ru: 'Иконка (chdes)', pt: 'Ícone (chdes)', nl: 'Icoon (chdes)', fr: 'Icône (chdes)', it: 'Icona (chdes)', es: 'Icono (chdes)', pl: 'Ikona (chdes)', uk: 'Іконка (chdes)', 'zh-cn': '图标 (chdes)' },
133
+ 'Kanalname (chdes)': { en: 'Channel name (chdes)', de: 'Kanalname (chdes)', ru: 'Имя канала (chdes)', pt: 'Nome do canal (chdes)', nl: 'Kanaalnaam (chdes)', fr: 'Nom du canal (chdes)', it: 'Nome canale (chdes)', es: 'Nombre del canal (chdes)', pl: 'Nazwa kanału (chdes)', uk: 'Ім\'я каналу (chdes)', 'zh-cn': '通道名称 (chdes)' },
134
+ 'Kanäle': { en: 'Channels', de: 'Kanäle', ru: 'Каналы', pt: 'Canais', nl: 'Kanalen', fr: 'Canaux', it: 'Canali', es: 'Canales', pl: 'Kanały', uk: 'Канали', 'zh-cn': '通道' },
135
+ 'Kategorie-Code (chdes)': { en: 'Category code (chdes)', de: 'Kategorie-Code (chdes)', ru: 'Код категории (chdes)', pt: 'Código de categoria (chdes)', nl: 'Categoriecode (chdes)', fr: 'Code de catégorie (chdes)', it: 'Codice categoria (chdes)', es: 'Código de categoría (chdes)', pl: 'Kod kategorii (chdes)', uk: 'Код категорії (chdes)', 'zh-cn': '类别代码 (chdes)' },
136
+ 'LED(s) setzen - JSON-Array wie in API-Doku 5.1.3.4, z.B. [{"id":2,"bg":"#220000"}]. Laut Doku nur "bg" (Hintergrundfarbe) unbedenklich extern setzbar.': { en: 'Set LED(s) - JSON array as in API doc 5.1.3.4, e.g. [{"id":2,"bg":"#220000"}]. Per the docs, only "bg" (background color) is safe to set externally.', de: 'LED(s) setzen - JSON-Array wie in API-Doku 5.1.3.4, z.B. [{"id":2,"bg":"#220000"}]. Laut Doku nur "bg" (Hintergrundfarbe) unbedenklich extern setzbar.', ru: 'Установить LED - JSON-массив как в документации API 5.1.3.4, напр. [{"id":2,"bg":"#220000"}]. Согласно документации, только "bg" (цвет фона) безопасно устанавливать извне.', pt: 'Definir LED(s) - array JSON conforme doc. API 5.1.3.4, p.ex. [{"id":2,"bg":"#220000"}]. Segundo a documentação, apenas "bg" (cor de fundo) pode ser definido externamente com segurança.', nl: 'LED(s) instellen - JSON-array zoals in API-doc 5.1.3.4, bijv. [{"id":2,"bg":"#220000"}]. Volgens de documentatie is alleen "bg" (achtergrondkleur) veilig extern instelbaar.', fr: 'Définir la/les LED - tableau JSON comme dans la doc API 5.1.3.4, p.ex. [{"id":2,"bg":"#220000"}]. Selon la doc, seul "bg" (couleur de fond) peut être défini en externe sans risque.', it: 'Imposta LED - array JSON come da documentazione API 5.1.3.4, es. [{"id":2,"bg":"#220000"}]. Secondo la documentazione, solo "bg" (colore di sfondo) è sicuro da impostare esternamente.', es: 'Definir LED(s) - array JSON como en la doc. API 5.1.3.4, p.ej. [{"id":2,"bg":"#220000"}]. Según la documentación, solo "bg" (color de fondo) es seguro de establecer externamente.', pl: 'Ustaw LED - tablica JSON jak w dokumentacji API 5.1.3.4, np. [{"id":2,"bg":"#220000"}]. Wg dokumentacji tylko "bg" (kolor tła) można bezpiecznie ustawiać zewnętrznie.', uk: 'Встановити світлодіоди - JSON-масив як у документації API 5.1.3.4, напр. [{"id":2,"bg":"#220000"}]. Згідно з документацією, лише "bg" (колір фону) безпечно встановлювати ззовні.', 'zh-cn': '设置 LED - JSON 数组,格式见 API 文档 5.1.3.4,例如 [{"id":2,"bg":"#220000"}]。根据文档,仅 "bg"(背景色)可安全地从外部设置。' },
137
+ 'Letzter Fehler': { en: 'Last error', de: 'Letzter Fehler', ru: 'Последняя ошибка', pt: 'Último erro', nl: 'Laatste fout', fr: 'Dernière erreur', it: 'Ultimo errore', es: 'Último error', pl: 'Ostatni błąd', uk: 'Остання помилка', 'zh-cn': '最近错误' },
138
+ 'Luftfeuchtigkeit': { en: 'Humidity', de: 'Luftfeuchtigkeit', ru: 'Влажность', pt: 'Humidade', nl: 'Luchtvochtigheid', fr: 'Humidité', it: 'Umidità', es: 'Humedad', pl: 'Wilgotność', uk: 'Вологість', 'zh-cn': '湿度' },
139
+ 'Mindestens ein Gerät erreichbar': {
140
+ en: 'At least one device reachable',
141
+ de: 'Mindestens ein Gerät erreichbar',
142
+ ru: 'Доступно хотя бы одно устройство',
143
+ pt: 'Pelo menos um dispositivo acessível',
144
+ nl: 'Ten minste één apparaat bereikbaar',
145
+ fr: 'Au moins un appareil accessible',
146
+ it: 'Almeno un dispositivo raggiungibile',
147
+ es: 'Al menos un dispositivo accesible',
148
+ pl: 'Co najmniej jedno urządzenie dostępne',
149
+ uk: 'Принаймні один пристрій доступний',
150
+ 'zh-cn': '至少有一个设备可访问'
151
+ },
152
+ 'NTP': { en: 'NTP', de: 'NTP', ru: 'NTP', pt: 'NTP', nl: 'NTP', fr: 'NTP', it: 'NTP', es: 'NTP', pl: 'NTP', uk: 'NTP', 'zh-cn': 'NTP' },
153
+ 'NTP-Server (URL/IP, max. 32 Zeichen)': { en: 'NTP server (URL/IP, max. 32 characters)', de: 'NTP-Server (URL/IP, max. 32 Zeichen)', ru: 'NTP-сервер (URL/IP, макс. 32 символа)', pt: 'Servidor NTP (URL/IP, máx. 32 caracteres)', nl: 'NTP-server (URL/IP, max. 32 tekens)', fr: 'Serveur NTP (URL/IP, max. 32 caractères)', it: 'Server NTP (URL/IP, max. 32 caratteri)', es: 'Servidor NTP (URL/IP, máx. 32 caracteres)', pl: 'Serwer NTP (URL/IP, maks. 32 znaki)', uk: 'NTP-сервер (URL/IP, макс. 32 символи)', 'zh-cn': 'NTP 服务器(URL/IP,最多 32 个字符)' },
154
+ 'Netzwerk': { en: 'Network', de: 'Netzwerk', ru: 'Сеть', pt: 'Rede', nl: 'Netwerk', fr: 'Réseau', it: 'Rete', es: 'Red', pl: 'Sieć', uk: 'Мережа', 'zh-cn': '网络' },
155
+ 'Neustart': { en: 'Restart', de: 'Neustart', ru: 'Перезапуск', pt: 'Reiniciar', nl: 'Herstarten', fr: 'Redémarrer', it: 'Riavvio', es: 'Reiniciar', pl: 'Restart', uk: 'Перезапуск', 'zh-cn': '重启' },
156
+ 'RFC1123 Zeitstempel (muss GMT sein)': { en: 'RFC1123 timestamp (must be GMT)', de: 'RFC1123 Zeitstempel (muss GMT sein)', ru: 'Метка времени RFC1123 (должна быть GMT)', pt: 'Timestamp RFC1123 (deve ser GMT)', nl: 'RFC1123-tijdstempel (moet GMT zijn)', fr: 'Horodatage RFC1123 (doit être GMT)', it: 'Timestamp RFC1123 (deve essere GMT)', es: 'Marca de tiempo RFC1123 (debe ser GMT)', pl: 'Znacznik czasu RFC1123 (musi być GMT)', uk: 'Мітка часу RFC1123 (має бути GMT)', 'zh-cn': 'RFC1123 时间戳(必须为 GMT)' },
157
+ 'Sammelbefehle': { en: 'Collective commands', de: 'Sammelbefehle', ru: 'Групповые команды', pt: 'Comandos coletivos', nl: 'Verzamelcommando\'s', fr: 'Commandes groupées', it: 'Comandi collettivi', es: 'Comandos colectivos', pl: 'Polecenia zbiorcze', uk: 'Групові команди', 'zh-cn': '集合命令' },
158
+ 'Schätzung setzen OHNE Fahrt (z.B. nach manueller Bedienung am Wandtaster): aktuellen Ist-Zustand in % eintragen': { en: 'Set the estimate WITHOUT moving (e.g. after manual operation at the wall switch): enter the current actual state in %', de: 'Schätzung setzen OHNE Fahrt (z.B. nach manueller Bedienung am Wandtaster): aktuellen Ist-Zustand in % eintragen', ru: 'Установить оценку БЕЗ движения (напр. после ручного управления настенным выключателем): ввести текущее фактическое состояние в %', pt: 'Definir a estimativa SEM movimento (p.ex. após operação manual no interruptor de parede): introduzir o estado atual em %', nl: 'Schatting instellen ZONDER beweging (bijv. na handmatige bediening op de wandschakelaar): huidige werkelijke status in % invoeren', fr: 'Définir l\'estimation SANS mouvement (p.ex. après commande manuelle sur l\'interrupteur mural) : saisir l\'état réel actuel en %', it: 'Imposta la stima SENZA movimento (es. dopo comando manuale sul pulsante a muro): inserire lo stato attuale in %', es: 'Definir la estimación SIN movimiento (p.ej. tras el manejo manual en el interruptor de pared): introducir el estado actual en %', pl: 'Ustaw szacunek BEZ ruchu (np. po ręcznej obsłudze przełącznika ściennego): wpisz aktualny stan rzeczywisty w %', uk: 'Встановити оцінку БЕЗ руху (напр. після ручного керування настінним вимикачем): ввести поточний фактичний стан у %', 'zh-cn': '设置估计值但不移动(例如手动操作墙壁开关后):输入当前实际状态百分比' },
159
+ 'Seriennummer': { en: 'Serial number', de: 'Seriennummer', ru: 'Серийный номер', pt: 'Número de série', nl: 'Serienummer', fr: 'Numéro de série', it: 'Numero di serie', es: 'Número de serie', pl: 'Numer seryjny', uk: 'Серійний номер', 'zh-cn': '序列号' },
160
+ 'Signalstärke': { en: 'Signal strength', de: 'Signalstärke', ru: 'Мощность сигнала', pt: 'Força do sinal', nl: 'Signaalsterkte', fr: 'Force du signal', it: 'Potenza del segnale', es: 'Intensidad de la señal', pl: 'Siła sygnału', uk: 'Потужність сигналу', 'zh-cn': '信号强度' },
161
+ 'Smartfront': { en: 'Smartfront', de: 'Smartfront', ru: 'Smartfront', pt: 'Smartfront', nl: 'Smartfront', fr: 'Smartfront', it: 'Smartfront', es: 'Smartfront', pl: 'Smartfront', uk: 'Smartfront', 'zh-cn': 'Smartfront' },
162
+ 'Software-Version': { en: 'Software version', de: 'Software-Version', ru: 'Версия ПО', pt: 'Versão do software', nl: 'Softwareversie', fr: 'Version logicielle', it: 'Versione software', es: 'Versión de software', pl: 'Wersja oprogramowania', uk: 'Версія ПЗ', 'zh-cn': '软件版本' },
163
+ 'Sommerzeit-Offset HHMM': { en: 'Daylight saving offset HHMM', de: 'Sommerzeit-Offset HHMM', ru: 'Смещение летнего времени HHMM', pt: 'Offset de horário de verão HHMM', nl: 'Zomertijd-offset HHMM', fr: 'Décalage heure d\'été HHMM', it: 'Offset ora legale HHMM', es: 'Desfase de horario de verano HHMM', pl: 'Przesunięcie czasu letniego HHMM', uk: 'Зміщення літнього часу HHMM', 'zh-cn': '夏令时偏移 HHMM' },
164
+ 'Standort': { en: 'Location', de: 'Standort', ru: 'Местоположение', pt: 'Localização', nl: 'Locatie', fr: 'Emplacement', it: 'Posizione', es: 'Ubicación', pl: 'Lokalizacja', uk: 'Розташування', 'zh-cn': '位置' },
165
+ 'Standortbezeichnung (frei wählbar, z.B. "Fideris Valzigg")': { en: 'Location label (freely choosable, e.g. "Fideris Valzigg")', de: 'Standortbezeichnung (frei wählbar, z.B. "Fideris Valzigg")', ru: 'Обозначение местоположения (произвольное, напр. "Fideris Valzigg")', pt: 'Designação da localização (livremente escolhível, p.ex. "Fideris Valzigg")', nl: 'Locatieomschrijving (vrij te kiezen, bijv. "Fideris Valzigg")', fr: 'Nom de l\'emplacement (libre, p.ex. "Fideris Valzigg")', it: 'Descrizione della posizione (a scelta libera, es. "Fideris Valzigg")', es: 'Nombre de la ubicación (libre, p.ej. "Fideris Valzigg")', pl: 'Nazwa lokalizacji (dowolna, np. "Fideris Valzigg")', uk: 'Позначення розташування (довільне, напр. "Fideris Valzigg")', 'zh-cn': '位置名称(可自定义,例如 "Fideris Valzigg")' },
166
+ 'Statische Infos neu laden (id/net/chdes)': { en: 'Reload static info (id/net/chdes)', de: 'Statische Infos neu laden (id/net/chdes)', ru: 'Перезагрузить статическую информацию (id/net/chdes)', pt: 'Recarregar informação estática (id/net/chdes)', nl: 'Statische info opnieuw laden (id/net/chdes)', fr: 'Recharger les infos statiques (id/net/chdes)', it: 'Ricarica informazioni statiche (id/net/chdes)', es: 'Recargar información estática (id/net/chdes)', pl: 'Odśwież informacje statyczne (id/net/chdes)', uk: 'Перезавантажити статичну інформацію (id/net/chdes)', 'zh-cn': '重新加载静态信息 (id/net/chdes)' },
167
+ 'System-Name': { en: 'System name', de: 'System-Name', ru: 'Имя системы', pt: 'Nome do sistema', nl: 'Systeemnaam', fr: 'Nom du système', it: 'Nome del sistema', es: 'Nombre del sistema', pl: 'Nazwa systemu', uk: 'Ім\'я системи', 'zh-cn': '系统名称' },
168
+ 'Systembefehle': { en: 'System commands', de: 'Systembefehle', ru: 'Системные команды', pt: 'Comandos do sistema', nl: 'Systeemcommando\'s', fr: 'Commandes système', it: 'Comandi di sistema', es: 'Comandos del sistema', pl: 'Polecenia systemowe', uk: 'Системні команди', 'zh-cn': '系统命令' },
169
+ 'Temperatur': { en: 'Temperature', de: 'Temperatur', ru: 'Температура', pt: 'Temperatura', nl: 'Temperatuur', fr: 'Température', it: 'Temperatura', es: 'Temperatura', pl: 'Temperatura', uk: 'Температура', 'zh-cn': '温度' },
170
+ 'Typ-Code (chdes)': { en: 'Type code (chdes)', de: 'Typ-Code (chdes)', ru: 'Код типа (chdes)', pt: 'Código de tipo (chdes)', nl: 'Typecode (chdes)', fr: 'Code de type (chdes)', it: 'Codice tipo (chdes)', es: 'Código de tipo (chdes)', pl: 'Kod typu (chdes)', uk: 'Код типу (chdes)', 'zh-cn': '类型代码 (chdes)' },
171
+ 'Verbindung OK': {
172
+ en: 'Connection OK',
173
+ de: 'Verbindung OK',
174
+ ru: 'Соединение в порядке',
175
+ pt: 'Ligação OK',
176
+ nl: 'Verbinding OK',
177
+ fr: 'Connexion OK',
178
+ it: 'Connessione OK',
179
+ es: 'Conexión OK',
180
+ pl: 'Połączenie OK',
181
+ uk: "З'єднання в порядку",
182
+ 'zh-cn': '连接正常'
183
+ },
184
+ 'Zeitzonen-Offset HHMM (z.B. +0200)': { en: 'Timezone offset HHMM (e.g. +0200)', de: 'Zeitzonen-Offset HHMM (z.B. +0200)', ru: 'Смещение часового пояса HHMM (напр. +0200)', pt: 'Offset de fuso horário HHMM (p.ex. +0200)', nl: 'Tijdzone-offset HHMM (bijv. +0200)', fr: 'Décalage de fuseau horaire HHMM (p.ex. +0200)', it: 'Offset fuso orario HHMM (es. +0200)', es: 'Desfase horario HHMM (p.ej. +0200)', pl: 'Przesunięcie strefy czasowej HHMM (np. +0200)', uk: 'Зміщення часового поясу HHMM (напр. +0200)', 'zh-cn': '时区偏移 HHMM(例如 +0200)' },
185
+ 'Zurück in Access-Point-Modus (Konfiguration bleibt erhalten)': { en: 'Back to access point mode (configuration is retained)', de: 'Zurück in Access-Point-Modus (Konfiguration bleibt erhalten)', ru: 'Вернуться в режим точки доступа (конфигурация сохраняется)', pt: 'Voltar ao modo ponto de acesso (a configuração é mantida)', nl: 'Terug naar access point-modus (configuratie blijft behouden)', fr: 'Retour en mode point d\'accès (la configuration est conservée)', it: 'Torna alla modalità access point (la configurazione viene mantenuta)', es: 'Volver al modo punto de acceso (se conserva la configuración)', pl: 'Powrót do trybu punktu dostępu (konfiguracja zostaje zachowana)', uk: 'Повернутися в режим точки доступу (конфігурація зберігається)', 'zh-cn': '返回接入点模式(保留配置)' },
186
+ 'Zustand (0-100, bei Storen meist -1=unbekannt)': { en: 'State (0-100, for shutters usually -1=unknown)', de: 'Zustand (0-100, bei Storen meist -1=unbekannt)', ru: 'Состояние (0-100, для жалюзи обычно -1=неизвестно)', pt: 'Estado (0-100, em estores geralmente -1=desconhecido)', nl: 'Status (0-100, bij zonwering meestal -1=onbekend)', fr: 'État (0-100, pour les stores généralement -1=inconnu)', it: 'Stato (0-100, per le tapparelle solitamente -1=sconosciuto)', es: 'Estado (0-100, en persianas normalmente -1=desconocido)', pl: 'Stan (0-100, dla rolet zwykle -1=nieznany)', uk: 'Стан (0-100, для жалюзі зазвичай -1=невідомо)', 'zh-cn': '状态 (0-100,卷帘通常 -1=未知)' },
187
+ 'Stopp': { en: 'Stop', de: 'Stopp', ru: 'Стоп', pt: 'Parar', nl: 'Stop', fr: 'Arrêt', it: 'Stop', es: 'Detener', pl: 'Stop', uk: 'Стоп', 'zh-cn': '停止' },
188
+ 'Ein (100%)': { en: 'On (100%)', de: 'Ein (100%)', ru: 'Вкл (100%)', pt: 'Ligado (100%)', nl: 'Aan (100%)', fr: 'Marche (100%)', it: 'Acceso (100%)', es: 'Encendido (100%)', pl: 'Wł. (100%)', uk: 'Увімк (100%)', 'zh-cn': '开 (100%)' },
189
+ 'Aus (0%)': { en: 'Off (0%)', de: 'Aus (0%)', ru: 'Выкл (0%)', pt: 'Desligado (0%)', nl: 'Uit (0%)', fr: 'Arrêt (0%)', it: 'Spento (0%)', es: 'Apagado (0%)', pl: 'Wył. (0%)', uk: 'Вимк (0%)', 'zh-cn': '关 (0%)' },
190
+ 'Umschalten': { en: 'Toggle', de: 'Umschalten', ru: 'Переключить', pt: 'Alternar', nl: 'Omschakelen', fr: 'Basculer', it: 'Commuta', es: 'Alternar', pl: 'Przełącz', uk: 'Перемкнути', 'zh-cn': '切换' },
191
+ 'Öffnen': { en: 'Open', de: 'Öffnen', ru: 'Открыть', pt: 'Abrir', nl: 'Openen', fr: 'Ouvrir', it: 'Apri', es: 'Abrir', pl: 'Otwórz', uk: 'Відкрити', 'zh-cn': '打开' },
192
+ 'Schliessen': { en: 'Close', de: 'Schliessen', ru: 'Закрыть', pt: 'Fechar', nl: 'Sluiten', fr: 'Fermer', it: 'Chiudi', es: 'Cerrar', pl: 'Zamknij', uk: 'Закрити', 'zh-cn': '关闭' },
193
+ 'Öffnen (Taste halten)': { en: 'Open (hold button)', de: 'Öffnen (Taste halten)', ru: 'Открыть (удерж. кнопку)', pt: 'Abrir (manter botão)', nl: 'Openen (knop ingedrukt houden)', fr: 'Ouvrir (maintenir le bouton)', it: 'Apri (tieni premuto)', es: 'Abrir (mantener pulsado)', pl: 'Otwórz (przytrzymaj przycisk)', uk: 'Відкрити (утримувати кнопку)', 'zh-cn': '打开(长按按钮)' },
194
+ 'Schliessen (Taste halten)': { en: 'Close (hold button)', de: 'Schliessen (Taste halten)', ru: 'Закрыть (удерж. кнопку)', pt: 'Fechar (manter botão)', nl: 'Sluiten (knop ingedrukt houden)', fr: 'Fermer (maintenir le bouton)', it: 'Chiudi (tieni premuto)', es: 'Cerrar (mantener pulsado)', pl: 'Zamknij (przytrzymaj przycisk)', uk: 'Закрити (утримувати кнопку)', 'zh-cn': '关闭(长按按钮)' },
195
+ 'Dimmen hoch (Taste halten)': { en: 'Dim up (hold button)', de: 'Dimmen hoch (Taste halten)', ru: 'Увеличить яркость (удерж. кнопку)', pt: 'Aumentar luminosidade (manter botão)', nl: 'Dimmen omhoog (knop ingedrukt houden)', fr: 'Augmenter la lumière (maintenir le bouton)', it: 'Aumenta luminosità (tieni premuto)', es: 'Aumentar brillo (mantener pulsado)', pl: 'Rozjaśnij (przytrzymaj przycisk)', uk: 'Збільшити яскравість (утримувати кнопку)', 'zh-cn': '调亮(长按按钮)' },
196
+ 'Dimmen runter (Taste halten)': { en: 'Dim down (hold button)', de: 'Dimmen runter (Taste halten)', ru: 'Уменьшить яркость (удерж. кнопку)', pt: 'Diminuir luminosidade (manter botão)', nl: 'Dimmen omlaag (knop ingedrukt houden)', fr: 'Diminuer la lumière (maintenir le bouton)', it: 'Diminuisci luminosità (tieni premuto)', es: 'Disminuir brillo (mantener pulsado)', pl: 'Przyciemnij (przytrzymaj przycisk)', uk: 'Зменшити яскравість (утримувати кнопку)', 'zh-cn': '调暗(长按按钮)' },
197
+ 'SSID': { en: 'SSID', de: 'SSID', ru: 'SSID', pt: 'SSID', nl: 'SSID', fr: 'SSID', it: 'SSID', es: 'SSID', pl: 'SSID', uk: 'SSID', 'zh-cn': 'SSID' },
198
+ 'IP-Adresse': { en: 'IP address', de: 'IP-Adresse', ru: 'IP-адрес', pt: 'Endereço IP', nl: 'IP-adres', fr: 'Adresse IP', it: 'Indirizzo IP', es: 'Dirección IP', pl: 'Adres IP', uk: 'IP-адреса', 'zh-cn': 'IP 地址' },
199
+ 'MAC-Adresse': { en: 'MAC address', de: 'MAC-Adresse', ru: 'MAC-адрес', pt: 'Endereço MAC', nl: 'MAC-adres', fr: 'Adresse MAC', it: 'Indirizzo MAC', es: 'Dirección MAC', pl: 'Adres MAC', uk: 'MAC-адреса', 'zh-cn': 'MAC 地址' },
200
+ 'Netzwerkmodus (0=AccessPoint, 1=Associate)': { en: 'Network mode (0=AccessPoint, 1=Associate)', de: 'Netzwerkmodus (0=AccessPoint, 1=Associate)', ru: 'Режим сети (0=точка доступа, 1=подключение)', pt: 'Modo de rede (0=Ponto de Acesso, 1=Associar)', nl: 'Netwerkmodus (0=AccessPoint, 1=Associate)', fr: 'Mode réseau (0=point d\'accès, 1=associé)', it: 'Modalità di rete (0=Access Point, 1=Associato)', es: 'Modo de red (0=Punto de acceso, 1=Asociado)', pl: 'Tryb sieci (0=punkt dostępu, 1=powiązanie)', uk: 'Режим мережі (0=точка доступу, 1=підключення)', 'zh-cn': '网络模式 (0=接入点, 1=关联)' },
201
+ 'Verschlüsselung': { en: 'Encryption', de: 'Verschlüsselung', ru: 'Шифрование', pt: 'Encriptação', nl: 'Versleuteling', fr: 'Chiffrement', it: 'Crittografia', es: 'Cifrado', pl: 'Szyfrowanie', uk: 'Шифрування', 'zh-cn': '加密' },
202
+ 'Subnetzmaske': { en: 'Subnet mask', de: 'Subnetzmaske', ru: 'Маска подсети', pt: 'Máscara de sub-rede', nl: 'Subnetmasker', fr: 'Masque de sous-réseau', it: 'Maschera di sottorete', es: 'Máscara de subred', pl: 'Maska podsieci', uk: 'Маска підмережі', 'zh-cn': '子网掩码' },
203
+ 'Gateway': { en: 'Gateway', de: 'Gateway', ru: 'Шлюз', pt: 'Gateway', nl: 'Gateway', fr: 'Passerelle', it: 'Gateway', es: 'Puerta de enlace', pl: 'Brama', uk: 'Шлюз', 'zh-cn': '网关' },
204
+ 'MAC-Adresse Access Point': { en: 'MAC address access point', de: 'MAC-Adresse Access Point', ru: 'MAC-адрес точки доступа', pt: 'Endereço MAC do ponto de acesso', nl: 'MAC-adres access point', fr: 'Adresse MAC du point d\'accès', it: 'Indirizzo MAC access point', es: 'Dirección MAC del punto de acceso', pl: 'Adres MAC punktu dostępu', uk: 'MAC-адреса точки доступу', 'zh-cn': '接入点 MAC 地址' },
205
+ 'Geschätzte Ist-Position 0=zu/100=offen (reine Anzeige der Software-Schätzung, KEINE Hardware-Rückmeldung; zum Kalibrieren "calibrate" verwenden, zum Anfahren "setPosition")': { en: 'Estimated actual position 0=closed/100=open (software estimate display only, NO hardware feedback; use "calibrate" to calibrate, "setPosition" to move)', de: 'Geschätzte Ist-Position 0=zu/100=offen (reine Anzeige der Software-Schätzung, KEINE Hardware-Rückmeldung; zum Kalibrieren "calibrate" verwenden, zum Anfahren "setPosition")', ru: 'Расчётная текущая позиция 0=закрыто/100=открыто (только программная оценка, БЕЗ обратной связи от оборудования; для калибровки используйте "calibrate", для перемещения "setPosition")', pt: 'Posição atual estimada 0=fechado/100=aberto (apenas exibição de estimativa por software, SEM feedback de hardware; use "calibrate" para calibrar, "setPosition" para mover)', nl: 'Geschatte werkelijke positie 0=dicht/100=open (alleen software-schatting, GEEN hardware-terugkoppeling; gebruik "calibrate" om te kalibreren, "setPosition" om te bewegen)', fr: 'Position réelle estimée 0=fermé/100=ouvert (affichage logiciel uniquement, PAS de retour matériel ; utiliser "calibrate" pour calibrer, "setPosition" pour déplacer)', it: 'Posizione effettiva stimata 0=chiuso/100=aperto (solo visualizzazione stima software, NESSUN feedback hardware; usare "calibrate" per calibrare, "setPosition" per muovere)', es: 'Posición real estimada 0=cerrado/100=abierto (solo visualización de estimación por software, SIN retroalimentación de hardware; usar "calibrate" para calibrar, "setPosition" para mover)', pl: 'Szacowana pozycja rzeczywista 0=zamknięte/100=otwarte (tylko wyświetlanie szacunku programowego, BRAK sprzężenia zwrotnego sprzętu; użyj "calibrate" do kalibracji, "setPosition" do przesunięcia)', uk: 'Розрахункова фактична позиція 0=закрито/100=відкрито (лише програмна оцінка, БЕЗ апаратного зворотного зв\'язку; для калібрування використовуйте "calibrate", для переміщення "setPosition")', 'zh-cn': '估计的实际位置 0=关闭/100=打开(仅软件估算显示,无硬件反馈;使用 "calibrate" 校准,"setPosition" 移动)' },
206
+ 'Geschätzte Position (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)': { en: 'Estimated position (disabled - set "Shutter motor runtime" to >0s)', de: 'Geschätzte Position (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)', ru: 'Расчётная позиция (отключено - установите "Время работы мотора жалюзи" > 0с)', pt: 'Posição estimada (desativado - definir "Tempo de funcionamento do motor" para >0s)', nl: 'Geschatte positie (uitgeschakeld - "Looptijd zonweringmotor" op >0s zetten)', fr: 'Position estimée (désactivé - régler "Durée du moteur de store" sur >0s)', it: 'Posizione stimata (disattivato - impostare "Tempo di funzionamento motore tapparella" su >0s)', es: 'Posición estimada (desactivado - establecer "Tiempo de funcionamiento del motor de persiana" a >0s)', pl: 'Szacowana pozycja (wyłączone - ustaw "Czas pracy silnika rolety" na >0s)', uk: 'Розрахункова позиція (вимкнено - встановіть "Час роботи двигуна жалюзі" > 0с)', 'zh-cn': '估计位置(已禁用 - 将"卷帘电机运行时间"设置为大于0秒)' },
207
+ 'Position anfahren 0=zu/100=offen (zeitbasiert über move-Impulse; 0/100 fahren als echte Endlagenfahrt und rekalibrieren die Schätzung)': { en: 'Move to position 0=closed/100=open (time-based via move pulses; 0/100 run as a real end-limit run and recalibrate the estimate)', de: 'Position anfahren 0=zu/100=offen (zeitbasiert über move-Impulse; 0/100 fahren als echte Endlagenfahrt und rekalibrieren die Schätzung)', ru: 'Переместить в позицию 0=закрыто/100=открыто (по времени через импульсы move; 0/100 выполняются как реальный ход до концевого положения и перекалибровка оценки)', pt: 'Mover para a posição 0=fechado/100=aberto (baseado em tempo via impulsos move; 0/100 executam como percurso real até ao fim de curso e recalibram a estimativa)', nl: 'Naar positie bewegen 0=dicht/100=open (tijdgebaseerd via move-pulsen; 0/100 rijden als echte eindpositierit en herkalibreren de schatting)', fr: 'Déplacer vers la position 0=fermé/100=ouvert (basé sur le temps via des impulsions move ; 0/100 effectuent une vraie course en fin de course et recalibrent l\'estimation)', it: 'Sposta in posizione 0=chiuso/100=aperto (basato sul tempo tramite impulsi move; 0/100 eseguono una vera corsa di fine corsa e ricalibrano la stima)', es: 'Mover a la posición 0=cerrado/100=abierto (basado en tiempo mediante impulsos move; 0/100 se ejecutan como una carrera real hasta el final de recorrido y recalibran la estimación)', pl: 'Przesuń do pozycji 0=zamknięte/100=otwarte (oparte na czasie za pomocą impulsów move; 0/100 wykonują rzeczywisty przebieg do położenia krańcowego i rekalibrują szacunek)', uk: 'Перемістити в позицію 0=закрито/100=відкрито (за часом через імпульси move; 0/100 виконуються як реальний хід до кінцевого положення та перекалібровують оцінку)', 'zh-cn': '移动到位置 0=关闭/100=打开(通过 move 脉冲按时间计算;0/100 作为真实限位行程运行并重新校准估算值)' },
208
+ 'Position anfahren (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)': { en: 'Move to position (disabled - set "Shutter motor runtime" to >0s)', de: 'Position anfahren (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)', ru: 'Переместить в позицию (отключено - установите "Время работы мотора жалюзи" > 0с)', pt: 'Mover para a posição (desativado - definir "Tempo de funcionamento do motor" para >0s)', nl: 'Naar positie bewegen (uitgeschakeld - "Looptijd zonweringmotor" op >0s zetten)', fr: 'Déplacer vers la position (désactivé - régler "Durée du moteur de store" sur >0s)', it: 'Sposta in posizione (disattivato - impostare "Tempo di funzionamento motore tapparella" su >0s)', es: 'Mover a la posición (desactivado - establecer "Tiempo de funcionamiento del motor de persiana" a >0s)', pl: 'Przesuń do pozycji (wyłączone - ustaw "Czas pracy silnika rolety" na >0s)', uk: 'Перемістити в позицію (вимкнено - встановіть "Час роботи двигуна жалюзі" > 0с)', 'zh-cn': '移动到位置(已禁用 - 将"卷帘电机运行时间"设置为大于0秒)' },
209
+ 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)': { en: 'Tilt slats (disabled - set "Tilt pulse (ms)" in the configuration)', de: 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)', ru: 'Наклонить ламели (отключено - установите "Импульс наклона (мс)" в конфигурации)', pt: 'Inclinar réguas (desativado - definir "Impulso de inclinação (ms)" na configuração)', nl: 'Lamellen kantelen (uitgeschakeld - "Kantelpuls (ms)" in de configuratie instellen)', fr: 'Incliner les lamelles (désactivé - régler "Impulsion d\'inclinaison (ms)" dans la configuration)', it: 'Inclina lamelle (disattivato - impostare "Impulso di inclinazione (ms)" nella configurazione)', es: 'Inclinar lamas (desactivado - establecer "Impulso de inclinación (ms)" en la configuración)', pl: 'Przechyl lamele (wyłączone - ustaw "Impuls przechyłu (ms)" w konfiguracji)', uk: 'Нахилити ламелі (вимкнено - встановіть "Імпульс нахилу (мс)" у конфігурації)', 'zh-cn': '倾斜叶片(已禁用 - 请在配置中设置"倾斜脉冲(毫秒)")' }
210
+ };
211
+
109
212
  class Zeptrion extends utils.Adapter {
110
213
  constructor(options) {
111
214
  super({ ...options, name: 'zeptrion' });
@@ -133,7 +236,7 @@ class Zeptrion extends utils.Adapter {
133
236
  let fixedCount = 0;
134
237
  for (const id of Object.keys(objects)) {
135
238
  const obj = objects[id];
136
- if (!obj || obj.type !== 'state' || !obj.common) {
239
+ if (!obj || (obj.type !== 'state' && obj.type !== 'channel') || !obj.common) {
137
240
  continue;
138
241
  }
139
242
  if (/\.calibrate$/.test(id) && (obj.common.role === 'value' || obj.common.read === false)) {
@@ -191,6 +294,54 @@ class Zeptrion extends utils.Adapter {
191
294
  });
192
295
  fixedCount++;
193
296
  }
297
+
298
+ // Name migration (added 1.0.8): plain-string common.name from a version <1.0.8
299
+ // -> full 11-language i18n object. Independent of the role-fix chain above, so it
300
+ // runs for every object regardless of whether a role fix also applied.
301
+ if (typeof obj.common.name === 'string') {
302
+ const mapped = NAME_MIGRATION_MAP[obj.common.name];
303
+ if (mapped) {
304
+ await this.extendObjectAsync(id, { common: { name: mapped } });
305
+ fixedCount++;
306
+ } else {
307
+ // Dynamic scene button names: "Szene 1 abrufen" / "... speichern" / "... löschen"
308
+ const sceneMatch = obj.common.name.match(/^Szene (\d+) (abrufen|speichern|löschen)$/);
309
+ if (sceneMatch) {
310
+ const [, num, verbDe] = sceneMatch;
311
+ const verbMap = {
312
+ abrufen: { en: 'recall', de: 'abrufen', ru: 'вызвать', pt: 'chamar', nl: 'oproepen', fr: 'rappeler', it: 'richiama', es: 'recuperar', pl: 'przywołaj', uk: 'викликати', 'zh-cn': '调用' },
313
+ speichern: { en: 'store', de: 'speichern', ru: 'сохранить', pt: 'guardar', nl: 'opslaan', fr: 'enregistrer', it: 'salva', es: 'guardar', pl: 'zapisz', uk: 'зберегти', 'zh-cn': '保存' },
314
+ 'löschen': { en: 'delete', de: 'löschen', ru: 'удалить', pt: 'eliminar', nl: 'verwijderen', fr: 'supprimer', it: 'elimina', es: 'eliminar', pl: 'usuń', uk: 'видалити', 'zh-cn': '删除' }
315
+ };
316
+ const verb = verbMap[verbDe];
317
+ if (verb) {
318
+ await this.extendObjectAsync(id, { common: { name: {
319
+ en: `Scene ${num} ${verb.en}`, de: `Szene ${num} ${verb.de}`, ru: `Сцена ${num}: ${verb.ru}`,
320
+ pt: `Cena ${num} ${verb.pt}`, nl: `Scène ${num} ${verb.nl}`, fr: `Scène ${num} ${verb.fr}`,
321
+ it: `Scena ${num} ${verb.it}`, es: `Escena ${num} ${verb.es}`, pl: `Scena ${num}: ${verb.pl}`,
322
+ uk: `Сцена ${num}: ${verb.uk}`, 'zh-cn': `场景 ${num} ${verb['zh-cn']}`
323
+ } } });
324
+ fixedCount++;
325
+ }
326
+ } else {
327
+ // Dynamic tilt names: "Lamellen kippen Richtung offen/zu (Impuls Nms)"
328
+ const tiltMatch = obj.common.name.match(/^Lamellen kippen Richtung (offen|zu) \(Impuls (\d+)ms\)$/);
329
+ if (tiltMatch) {
330
+ const [, dirDe, ms] = tiltMatch;
331
+ if (dirDe === 'offen') {
332
+ await this.extendObjectAsync(id, { common: { name: {
333
+ en: `Tilt slats open (pulse ${ms}ms)`, de: `Lamellen kippen Richtung offen (Impuls ${ms}ms)`, ru: `Наклонить ламели открыто (импульс ${ms}мс)`, pt: `Inclinar réguas para abrir (impulso ${ms}ms)`, nl: `Lamellen kantelen richting open (puls ${ms}ms)`, fr: `Incliner les lamelles vers l'ouverture (impulsion ${ms}ms)`, it: `Inclina lamelle verso apertura (impulso ${ms}ms)`, es: `Inclinar lamas hacia abierto (impulso ${ms}ms)`, pl: `Przechyl lamele w kierunku otwarcia (impuls ${ms}ms)`, uk: `Нахилити ламелі відкрито (імпульс ${ms}мс)`, 'zh-cn': `叶片向开启方向倾斜(脉冲 ${ms}毫秒)`
334
+ } } });
335
+ } else {
336
+ await this.extendObjectAsync(id, { common: { name: {
337
+ en: `Tilt slats closed (pulse ${ms}ms)`, de: `Lamellen kippen Richtung zu (Impuls ${ms}ms)`, ru: `Наклонить ламели закрыто (импульс ${ms}мс)`, pt: `Inclinar réguas para fechar (impulso ${ms}ms)`, nl: `Lamellen kantelen richting dicht (puls ${ms}ms)`, fr: `Incliner les lamelles vers la fermeture (impulsion ${ms}ms)`, it: `Inclina lamelle verso chiusura (impulso ${ms}ms)`, es: `Inclinar lamas hacia cerrado (impulso ${ms}ms)`, pl: `Przechyl lamele w kierunku zamknięcia (impuls ${ms}ms)`, uk: `Нахилити ламелі закрито (імпульс ${ms}мс)`, 'zh-cn': `叶片向关闭方向倾斜(脉冲 ${ms}毫秒)`
338
+ } } });
339
+ }
340
+ fixedCount++;
341
+ }
342
+ }
343
+ }
344
+ }
194
345
  }
195
346
  if (fixedCount > 0) {
196
347
  this.log.info(`Migration: corrected role/name on ${fixedCount} existing object(s) created by an older version.`);
@@ -258,9 +409,9 @@ class Zeptrion extends utils.Adapter {
258
409
  for (const d of active) {
259
410
  const errs = this.validateDeviceRow(d);
260
411
  const sanId = this.sanitize(d.id);
261
- if (seenIds.has(sanId)) errs.push(`ID "${d.id}" (sanitisiert "${sanId}") ist doppelt vergeben`);
412
+ if (seenIds.has(sanId)) errs.push(`ID "${d.id}" (sanitized "${sanId}") is assigned twice`);
262
413
  const hostKey = String(d.host).trim().toLowerCase();
263
- if (seenHosts.has(hostKey)) errs.push(`Host "${d.host}" ist doppelt konfiguriert`);
414
+ if (seenHosts.has(hostKey)) errs.push(`Host "${d.host}" is configured twice`);
264
415
  if (errs.length) {
265
416
  this.log.error(`Device "${d.name || d.id || d.host}" skipped: ${errs.join('; ')}`);
266
417
  continue;
@@ -336,50 +487,50 @@ class Zeptrion extends utils.Adapter {
336
487
  const errs = [];
337
488
  const host = String(d.host || '').trim();
338
489
  if (!host) {
339
- errs.push('Host fehlt');
490
+ errs.push('Host missing');
340
491
  } else if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
341
- errs.push(`Host "${host}" enthält ungültige Zeichen (kein http://, keine Leerzeichen, kein Port)`);
492
+ errs.push(`Host "${host}" contains invalid characters (no http://, no spaces, no port)`);
342
493
  } else if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
343
494
  const octets = host.split('.').map(Number);
344
495
  if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {
345
- errs.push(`"${host}" ist keine gültige IPv4-Adresse`);
496
+ errs.push(`"${host}" is not a valid IPv4 address`);
346
497
  }
347
498
  }
348
499
  if (d.id && !/^[a-zA-Z0-9_-]+$/.test(String(d.id))) {
349
- errs.push(`ID "${d.id}" enthält ungültige Zeichen (erlaubt: a-z, 0-9, _, -)`);
500
+ errs.push(`ID "${d.id}" contains invalid characters (allowed: a-z, 0-9, _, -)`);
350
501
  }
351
502
  const ch = parseInt(d.channels, 10);
352
503
  if (d.channels !== undefined && d.channels !== '' && (isNaN(ch) || ch < 1 || ch > 4)) {
353
- errs.push(`Kanäle "${d.channels}" ungültig (1-4)`);
504
+ errs.push(`Channels "${d.channels}" invalid (1-4)`);
354
505
  }
355
506
  if (d.kind !== undefined && d.kind !== '' && !['unknown', 'blind', 'light'].includes(String(d.kind))) {
356
- errs.push(`Art "${d.kind}" ungültig (unknown/blind/light)`);
507
+ errs.push(`Type "${d.kind}" invalid (unknown/blind/light)`);
357
508
  }
358
509
  const tt = parseInt(d.travelTimeSec, 10);
359
510
  if (d.travelTimeSec !== undefined && d.travelTimeSec !== '' && (isNaN(tt) || tt < 0 || tt > 300)) {
360
- errs.push(`Laufzeit "${d.travelTimeSec}" ungültig (0-300s)`);
511
+ errs.push(`Runtime "${d.travelTimeSec}" invalid (0-300s)`);
361
512
  }
362
513
  if (d.travelTimeSecCh !== undefined && String(d.travelTimeSecCh).trim() !== '') {
363
514
  const parts = String(d.travelTimeSecCh).split(',').map(s => s.trim());
364
515
  if (parts.length > 4) {
365
- errs.push(`Laufzeit/Kanal "${d.travelTimeSecCh}": maximal 4 Werte`);
516
+ errs.push(`Runtime/channel "${d.travelTimeSecCh}": maximum 4 values`);
366
517
  }
367
518
  for (const p of parts) {
368
519
  if (p === '') continue; // leerer Eintrag = Fallback auf travelTimeSec
369
520
  const v = parseInt(p, 10);
370
521
  if (isNaN(v) || v < 0 || v > 300 || String(v) !== p) {
371
- errs.push(`Laufzeit/Kanal "${d.travelTimeSecCh}": Wert "${p}" ungültig (0-300, ganzzahlig)`);
522
+ errs.push(`Runtime/channel "${d.travelTimeSecCh}": value "${p}" invalid (0-300, integer)`);
372
523
  break;
373
524
  }
374
525
  }
375
526
  }
376
527
  const tp = parseInt(d.tiltTimeMs, 10);
377
528
  if (d.tiltTimeMs !== undefined && d.tiltTimeMs !== '' && (isNaN(tp) || tp < 0 || tp > 5000)) {
378
- errs.push(`Kipp-Impuls "${d.tiltTimeMs}" ungültig (0-5000ms)`);
529
+ errs.push(`Tilt pulse "${d.tiltTimeMs}" invalid (0-5000ms)`);
379
530
  }
380
531
  const pi = parseInt(d.pollInterval, 10);
381
532
  if (d.pollInterval !== undefined && d.pollInterval !== '' && (isNaN(pi) || pi < 5 || pi > 3600)) {
382
- errs.push(`Poll-Intervall "${d.pollInterval}" ungültig (5-3600s)`);
533
+ errs.push(`Poll interval "${d.pollInterval}" invalid (5-3600s)`);
383
534
  }
384
535
  return errs;
385
536
  }
@@ -513,9 +664,14 @@ class Zeptrion extends utils.Adapter {
513
664
  // --- network (read-only Anzeige, siehe README für Gründe) ---
514
665
  await this.setObjectNotExistsAsync(`${id}.network`, { type: 'channel', common: { name: { en: 'Network', de: 'Netzwerk', ru: 'Сеть', pt: 'Rede', nl: 'Netwerk', fr: 'Réseau', it: 'Rete', es: 'Red', pl: 'Sieć', uk: 'Мережа', 'zh-cn': '网络' } }, native: {} });
515
666
  const netFields = {
516
- ssid: 'SSID', ip: 'IP-Adresse', mac: 'MAC-Adresse',
517
- mode: 'Netzwerkmodus (0=AccessPoint, 1=Associate)', enc: 'Verschlüsselung',
518
- mask: 'Subnetzmaske', gw: 'Gateway', bssid: 'MAC-Adresse Access Point'
667
+ ssid: { en: 'SSID', de: 'SSID', ru: 'SSID', pt: 'SSID', nl: 'SSID', fr: 'SSID', it: 'SSID', es: 'SSID', pl: 'SSID', uk: 'SSID', 'zh-cn': 'SSID' },
668
+ ip: { en: 'IP address', de: 'IP-Adresse', ru: 'IP-адрес', pt: 'Endereço IP', nl: 'IP-adres', fr: 'Adresse IP', it: 'Indirizzo IP', es: 'Dirección IP', pl: 'Adres IP', uk: 'IP-адреса', 'zh-cn': 'IP 地址' },
669
+ mac: { en: 'MAC address', de: 'MAC-Adresse', ru: 'MAC-адрес', pt: 'Endereço MAC', nl: 'MAC-adres', fr: 'Adresse MAC', it: 'Indirizzo MAC', es: 'Dirección MAC', pl: 'Adres MAC', uk: 'MAC-адреса', 'zh-cn': 'MAC 地址' },
670
+ mode: { en: 'Network mode (0=AccessPoint, 1=Associate)', de: 'Netzwerkmodus (0=AccessPoint, 1=Associate)', ru: 'Режим сети (0=точка доступа, 1=подключение)', pt: 'Modo de rede (0=Ponto de Acesso, 1=Associar)', nl: 'Netwerkmodus (0=AccessPoint, 1=Associate)', fr: 'Mode réseau (0=point d\'accès, 1=associé)', it: 'Modalità di rete (0=Access Point, 1=Associato)', es: 'Modo de red (0=Punto de acceso, 1=Asociado)', pl: 'Tryb sieci (0=punkt dostępu, 1=powiązanie)', uk: 'Режим мережі (0=точка доступу, 1=підключення)', 'zh-cn': '网络模式 (0=接入点, 1=关联)' },
671
+ enc: { en: 'Encryption', de: 'Verschlüsselung', ru: 'Шифрование', pt: 'Encriptação', nl: 'Versleuteling', fr: 'Chiffrement', it: 'Crittografia', es: 'Cifrado', pl: 'Szyfrowanie', uk: 'Шифрування', 'zh-cn': '加密' },
672
+ mask: { en: 'Subnet mask', de: 'Subnetzmaske', ru: 'Маска подсети', pt: 'Máscara de sub-rede', nl: 'Subnetmasker', fr: 'Masque de sous-réseau', it: 'Maschera di sottorete', es: 'Máscara de subred', pl: 'Maska podsieci', uk: 'Маска підмережі', 'zh-cn': '子网掩码' },
673
+ gw: { en: 'Gateway', de: 'Gateway', ru: 'Шлюз', pt: 'Gateway', nl: 'Gateway', fr: 'Passerelle', it: 'Gateway', es: 'Puerta de enlace', pl: 'Brama', uk: 'Шлюз', 'zh-cn': '网关' },
674
+ bssid: { en: 'MAC address access point', de: 'MAC-Adresse Access Point', ru: 'MAC-адрес точки доступа', pt: 'Endereço MAC do ponto de acesso', nl: 'MAC-adres access point', fr: 'Adresse MAC du point d\'accès', it: 'Indirizzo MAC access point', es: 'Dirección MAC del punto de acceso', pl: 'Adres MAC punktu dostępu', uk: 'MAC-адреса точки доступу', 'zh-cn': '接入点 MAC 地址' }
519
675
  };
520
676
  for (const [key, name] of Object.entries(netFields)) {
521
677
  await this.ensureState(`${id}.network.${key}`, { name, type: 'string', role: 'text', read: true, write: false });
@@ -601,8 +757,8 @@ class Zeptrion extends utils.Adapter {
601
757
  type: 'state',
602
758
  common: {
603
759
  name: hasTravel
604
- ? 'Geschätzte Ist-Position 0=zu/100=offen (reine Anzeige der Software-Schätzung, KEINE Hardware-Rückmeldung; zum Kalibrieren "calibrate" verwenden, zum Anfahren "setPosition")'
605
- : 'Geschätzte Position (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)',
760
+ ? { en: 'Estimated actual position 0=closed/100=open (software estimate display only, NO hardware feedback; use "calibrate" to calibrate, "setPosition" to move)', de: 'Geschätzte Ist-Position 0=zu/100=offen (reine Anzeige der Software-Schätzung, KEINE Hardware-Rückmeldung; zum Kalibrieren "calibrate" verwenden, zum Anfahren "setPosition")', ru: 'Расчётная текущая позиция 0=закрыто/100=открыто (только программная оценка, БЕЗ обратной связи от оборудования; для калибровки используйте "calibrate", для перемещения "setPosition")', pt: 'Posição atual estimada 0=fechado/100=aberto (apenas exibição de estimativa por software, SEM feedback de hardware; use "calibrate" para calibrar, "setPosition" para mover)', nl: 'Geschatte werkelijke positie 0=dicht/100=open (alleen software-schatting, GEEN hardware-terugkoppeling; gebruik "calibrate" om te kalibreren, "setPosition" om te bewegen)', fr: 'Position réelle estimée 0=fermé/100=ouvert (affichage logiciel uniquement, PAS de retour matériel ; utiliser "calibrate" pour calibrer, "setPosition" pour déplacer)', it: 'Posizione effettiva stimata 0=chiuso/100=aperto (solo visualizzazione stima software, NESSUN feedback hardware; usare "calibrate" per calibrare, "setPosition" per muovere)', es: 'Posición real estimada 0=cerrado/100=abierto (solo visualización de estimación por software, SIN retroalimentación de hardware; usar "calibrate" para calibrar, "setPosition" para mover)', pl: 'Szacowana pozycja rzeczywista 0=zamknięte/100=otwarte (tylko wyświetlanie szacunku programowego, BRAK sprzężenia zwrotnego sprzętu; użyj "calibrate" do kalibracji, "setPosition" do przesunięcia)', uk: 'Розрахункова фактична позиція 0=закрито/100=відкрито (лише програмна оцінка, БЕЗ апаратного зворотного зв\'язку; для калібрування використовуйте "calibrate", для переміщення "setPosition")', 'zh-cn': '估计的实际位置 0=关闭/100=打开(仅软件估算显示,无硬件反馈;使用 "calibrate" 校准,"setPosition" 移动)' }
761
+ : { en: 'Estimated position (disabled - set "Shutter motor runtime" to >0s)', de: 'Geschätzte Position (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)', ru: 'Расчётная позиция (отключено - установите "Время работы мотора жалюзи" > 0с)', pt: 'Posição estimada (desativado - definir "Tempo de funcionamento do motor" para >0s)', nl: 'Geschatte positie (uitgeschakeld - "Looptijd zonweringmotor" op >0s zetten)', fr: 'Position estimée (désactivé - régler "Durée du moteur de store" sur >0s)', it: 'Posizione stimata (disattivato - impostare "Tempo di funzionamento motore tapparella" su >0s)', es: 'Posición estimada (desactivado - establecer "Tiempo de funcionamiento del motor de persiana" a >0s)', pl: 'Szacowana pozycja (wyłączone - ustaw "Czas pracy silnika rolety" na >0s)', uk: 'Розрахункова позиція (вимкнено - встановіть "Час роботи двигуна жалюзі" > 0с)', 'zh-cn': '估计位置(已禁用 - 将"卷帘电机运行时间"设置为大于0秒)' },
606
762
  type: 'number', role: 'value.blind', min: 0, max: 100, read: true, write: false
607
763
  },
608
764
  native: {}
@@ -611,8 +767,8 @@ class Zeptrion extends utils.Adapter {
611
767
  type: 'state',
612
768
  common: {
613
769
  name: hasTravel
614
- ? 'Position anfahren 0=zu/100=offen (zeitbasiert über move-Impulse; 0/100 fahren als echte Endlagenfahrt und rekalibrieren die Schätzung)'
615
- : 'Position anfahren (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)',
770
+ ? { en: 'Move to position 0=closed/100=open (time-based via move pulses; 0/100 run as a real end-limit run and recalibrate the estimate)', de: 'Position anfahren 0=zu/100=offen (zeitbasiert über move-Impulse; 0/100 fahren als echte Endlagenfahrt und rekalibrieren die Schätzung)', ru: 'Переместить в позицию 0=закрыто/100=открыто (по времени через импульсы move; 0/100 выполняются как реальный ход до концевого положения и перекалибровка оценки)', pt: 'Mover para a posição 0=fechado/100=aberto (baseado em tempo via impulsos move; 0/100 executam como percurso real até ao fim de curso e recalibram a estimativa)', nl: 'Naar positie bewegen 0=dicht/100=open (tijdgebaseerd via move-pulsen; 0/100 rijden als echte eindpositierit en herkalibreren de schatting)', fr: 'Déplacer vers la position 0=fermé/100=ouvert (basé sur le temps via des impulsions move ; 0/100 effectuent une vraie course en fin de course et recalibrent l\'estimation)', it: 'Sposta in posizione 0=chiuso/100=aperto (basato sul tempo tramite impulsi move; 0/100 eseguono una vera corsa di fine corsa e ricalibrano la stima)', es: 'Mover a la posición 0=cerrado/100=abierto (basado en tiempo mediante impulsos move; 0/100 se ejecutan como una carrera real hasta el final de recorrido y recalibran la estimación)', pl: 'Przesuń do pozycji 0=zamknięte/100=otwarte (oparte na czasie za pomocą impulsów move; 0/100 wykonują rzeczywisty przebieg do położenia krańcowego i rekalibrują szacunek)', uk: 'Перемістити в позицію 0=закрито/100=відкрито (за часом через імпульси move; 0/100 виконуються як реальний хід до кінцевого положення та перекалібровують оцінку)', 'zh-cn': '移动到位置 0=关闭/100=打开(通过 move 脉冲按时间计算;0/100 作为真实限位行程运行并重新校准估算值)' }
771
+ : { en: 'Move to position (disabled - set "Shutter motor runtime" to >0s)', de: 'Position anfahren (deaktiviert - "Laufzeit Storenmotor" auf >0s setzen)', ru: 'Переместить в позицию (отключено - установите "Время работы мотора жалюзи" > 0с)', pt: 'Mover para a posição (desativado - definir "Tempo de funcionamento do motor" para >0s)', nl: 'Naar positie bewegen (uitgeschakeld - "Looptijd zonweringmotor" op >0s zetten)', fr: 'Déplacer vers la position (désactivé - régler "Durée du moteur de store" sur >0s)', it: 'Sposta in posizione (disattivato - impostare "Tempo di funzionamento motore tapparella" su >0s)', es: 'Mover a la posición (desactivado - establecer "Tiempo de funcionamiento del motor de persiana" a >0s)', pl: 'Przesuń do pozycji (wyłączone - ustaw "Czas pracy silnika rolety" na >0s)', uk: 'Перемістити в позицію (вимкнено - встановіть "Час роботи двигуна жалюзі" > 0с)', 'zh-cn': '移动到位置(已禁用 - 将"卷帘电机运行时间"设置为大于0秒)' },
616
772
  type: 'number', role: 'level.blind', min: 0, max: 100, read: true, write: true
617
773
  },
618
774
  native: {}
@@ -625,8 +781,8 @@ class Zeptrion extends utils.Adapter {
625
781
  type: 'state',
626
782
  common: {
627
783
  name: dev.cfg.tiltTimeMs
628
- ? `Lamellen kippen Richtung offen (Impuls ${dev.cfg.tiltTimeMs}ms)`
629
- : 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)',
784
+ ? { en: `Tilt slats open (pulse ${dev.cfg.tiltTimeMs}ms)`, de: `Lamellen kippen Richtung offen (Impuls ${dev.cfg.tiltTimeMs}ms)`, ru: `Наклонить ламели открыто (импульс ${dev.cfg.tiltTimeMs}мс)`, pt: `Inclinar réguas para abrir (impulso ${dev.cfg.tiltTimeMs}ms)`, nl: `Lamellen kantelen richting open (puls ${dev.cfg.tiltTimeMs}ms)`, fr: `Incliner les lamelles vers l'ouverture (impulsion ${dev.cfg.tiltTimeMs}ms)`, it: `Inclina lamelle verso apertura (impulso ${dev.cfg.tiltTimeMs}ms)`, es: `Inclinar lamas hacia abierto (impulso ${dev.cfg.tiltTimeMs}ms)`, pl: `Przechyl lamele w kierunku otwarcia (impuls ${dev.cfg.tiltTimeMs}ms)`, uk: `Нахилити ламелі відкрито (імпульс ${dev.cfg.tiltTimeMs}мс)`, 'zh-cn': `叶片向开启方向倾斜(脉冲 ${dev.cfg.tiltTimeMs}毫秒)` }
785
+ : { en: 'Tilt slats (disabled - set "Tilt pulse (ms)" in the configuration)', de: 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)', ru: 'Наклонить ламели (отключено - установите "Импульс наклона (мс)" в конфигурации)', pt: 'Inclinar réguas (desativado - definir "Impulso de inclinação (ms)" na configuração)', nl: 'Lamellen kantelen (uitgeschakeld - "Kantelpuls (ms)" in de configuratie instellen)', fr: 'Incliner les lamelles (désactivé - régler "Impulsion d\'inclinaison (ms)" dans la configuration)', it: 'Inclina lamelle (disattivato - impostare "Impulso di inclinazione (ms)" nella configurazione)', es: 'Inclinar lamas (desactivado - establecer "Impulso de inclinación (ms)" en la configuración)', pl: 'Przechyl lamele (wyłączone - ustaw "Impuls przechyłu (ms)" w konfiguracji)', uk: 'Нахилити ламелі (вимкнено - встановіть "Імпульс нахилу (мс)" у конфігурації)', 'zh-cn': '倾斜叶片(已禁用 - 请在配置中设置"倾斜脉冲(毫秒)")' },
630
786
  type: 'boolean', role: 'button', read: false, write: true, def: false
631
787
  },
632
788
  native: {}
@@ -635,8 +791,8 @@ class Zeptrion extends utils.Adapter {
635
791
  type: 'state',
636
792
  common: {
637
793
  name: dev.cfg.tiltTimeMs
638
- ? `Lamellen kippen Richtung zu (Impuls ${dev.cfg.tiltTimeMs}ms)`
639
- : 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)',
794
+ ? { en: `Tilt slats closed (pulse ${dev.cfg.tiltTimeMs}ms)`, de: `Lamellen kippen Richtung zu (Impuls ${dev.cfg.tiltTimeMs}ms)`, ru: `Наклонить ламели закрыто (импульс ${dev.cfg.tiltTimeMs}мс)`, pt: `Inclinar réguas para fechar (impulso ${dev.cfg.tiltTimeMs}ms)`, nl: `Lamellen kantelen richting dicht (puls ${dev.cfg.tiltTimeMs}ms)`, fr: `Incliner les lamelles vers la fermeture (impulsion ${dev.cfg.tiltTimeMs}ms)`, it: `Inclina lamelle verso chiusura (impulso ${dev.cfg.tiltTimeMs}ms)`, es: `Inclinar lamas hacia cerrado (impulso ${dev.cfg.tiltTimeMs}ms)`, pl: `Przechyl lamele w kierunku zamknięcia (impuls ${dev.cfg.tiltTimeMs}ms)`, uk: `Нахилити ламелі закрито (імпульс ${dev.cfg.tiltTimeMs}мс)`, 'zh-cn': `叶片向关闭方向倾斜(脉冲 ${dev.cfg.tiltTimeMs}毫秒)` }
795
+ : { en: 'Tilt slats (disabled - set "Tilt pulse (ms)" in the configuration)', de: 'Lamellen kippen (deaktiviert - "Kipp-Impuls (ms)" in der Konfiguration setzen)', ru: 'Наклонить ламели (отключено - установите "Импульс наклона (мс)" в конфигурации)', pt: 'Inclinar réguas (desativado - definir "Impulso de inclinação (ms)" na configuração)', nl: 'Lamellen kantelen (uitgeschakeld - "Kantelpuls (ms)" in de configuratie instellen)', fr: 'Incliner les lamelles (désactivé - régler "Impulsion d\'inclinaison (ms)" dans la configuration)', it: 'Inclina lamelle (disattivato - impostare "Impulso di inclinazione (ms)" nella configurazione)', es: 'Inclinar lamas (desactivado - establecer "Impulso de inclinación (ms)" en la configuración)', pl: 'Przechyl lamele (wyłączone - ustaw "Impuls przechyłu (ms)" w konfiguracji)', uk: 'Нахилити ламелі (вимкнено - встановіть "Імпульс нахилу (мс)" у конфігурації)', 'zh-cn': '倾斜叶片(已禁用 - 请在配置中设置"倾斜脉冲(毫秒)")' },
640
796
  type: 'boolean', role: 'button', read: false, write: true, def: false
641
797
  },
642
798
  native: {}
@@ -677,7 +833,7 @@ class Zeptrion extends utils.Adapter {
677
833
 
678
834
  async zrapGet(id, path, axiosOpts = {}) {
679
835
  const dev = this.devices[id];
680
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
836
+ if (!dev) throw new Error(`Unknown device ${id}`);
681
837
  const res = await dev.client.get(path, { responseType: 'text', transformResponse: [d => d], ...axiosOpts });
682
838
  if (res.status >= 400) throw new Error(`HTTP ${res.status}`);
683
839
  if (!res.data) return {};
@@ -691,7 +847,7 @@ class Zeptrion extends utils.Adapter {
691
847
 
692
848
  async zrapPost(id, path, bodyObj) {
693
849
  const dev = this.devices[id];
694
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
850
+ if (!dev) throw new Error(`Unknown device ${id}`);
695
851
  const data = Object.entries(bodyObj)
696
852
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
697
853
  .join('&');
@@ -707,7 +863,7 @@ class Zeptrion extends utils.Adapter {
707
863
  // 3340-2-B + Front 920-330x), daher separat und optional (Konfig-Checkbox).
708
864
  async zapiGet(id, path) {
709
865
  const dev = this.devices[id];
710
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
866
+ if (!dev) throw new Error(`Unknown device ${id}`);
711
867
  const res = await dev.client.get(path);
712
868
  if (res.status >= 400) throw new Error(`HTTP ${res.status}`);
713
869
  return res.data;
@@ -715,7 +871,7 @@ class Zeptrion extends utils.Adapter {
715
871
 
716
872
  async zapiPost(id, path, jsonBody) {
717
873
  const dev = this.devices[id];
718
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
874
+ if (!dev) throw new Error(`Unknown device ${id}`);
719
875
  const res = await dev.client.post(path, jsonBody, {
720
876
  headers: { 'Content-Type': 'application/json' }
721
877
  });
@@ -739,7 +895,7 @@ class Zeptrion extends utils.Adapter {
739
895
  const str = String(value ?? '');
740
896
  const bytes = Buffer.byteLength(str, 'utf8');
741
897
  if (bytes > maxBytes) {
742
- throw new Error(`${fieldName}: ${bytes} Bytes überschreiten das API-Limit von ${maxBytes} Bytes (Achtung: Umlaute zählen als 2 Bytes)`);
898
+ throw new Error(`${fieldName}: ${bytes} bytes exceed the API limit of ${maxBytes} bytes (note: umlauts count as 2 bytes)`);
743
899
  }
744
900
  return str;
745
901
  }
@@ -752,10 +908,10 @@ class Zeptrion extends utils.Adapter {
752
908
  */
753
909
  sendChannelCommand(id, chNum, cmd) {
754
910
  if (!isValidChCmd(cmd)) {
755
- return Promise.reject(new Error(`Ungültiger Kanalbefehl "${cmd}"`));
911
+ return Promise.reject(new Error(`Invalid channel command "${cmd}"`));
756
912
  }
757
913
  const dev = this.devices[id];
758
- if (!dev) return Promise.reject(new Error(`Unbekanntes Gerät ${id}`));
914
+ if (!dev) return Promise.reject(new Error(`Unknown device ${id}`));
759
915
 
760
916
  return new Promise((resolve, reject) => {
761
917
  dev.pendingCmds[chNum] = cmd;
@@ -912,7 +1068,7 @@ class Zeptrion extends utils.Adapter {
912
1068
  const dev = this.devices[id];
913
1069
  if (!dev) return;
914
1070
  if (dev.cfg.kind !== 'blind' || !dev.cfg.travelTimeMsByCh[chNum]) {
915
- throw new Error('setPosition erfordert Art=Storen und eine konfigurierte Motor-Laufzeit (>0s) für diesen Kanal');
1071
+ throw new Error('setPosition requires Type=Shutter and a configured motor runtime (>0s) for this channel');
916
1072
  }
917
1073
  target = Math.max(0, Math.min(100, Math.round(Number(target))));
918
1074
  const travel = dev.cfg.travelTimeMsByCh[chNum];
@@ -1237,11 +1393,11 @@ class Zeptrion extends utils.Adapter {
1237
1393
  handleDeviceError(id, err, context) {
1238
1394
  let msg = (err && err.message) || String(err);
1239
1395
  const code = err && err.code;
1240
- if (code === 'ECONNREFUSED') msg = 'Verbindung verweigert (Gerät aus oder falsche IP?)';
1241
- else if (code === 'ECONNABORTED') msg = 'Zeitüberschreitung (Gerät nicht erreichbar)';
1242
- else if (code === 'EHOSTUNREACH') msg = 'Host nicht erreichbar (Netzwerk/Routing prüfen)';
1243
- else if (code === 'ENOTFOUND') msg = 'Hostname/mDNS-Name nicht auflösbar';
1244
- else if (code === 'ETIMEDOUT') msg = 'Zeitüberschreitung beim Verbindungsaufbau';
1396
+ if (code === 'ECONNREFUSED') msg = 'Connection refused (device off or wrong IP?)';
1397
+ else if (code === 'ECONNABORTED') msg = 'Timeout (device not reachable)';
1398
+ else if (code === 'EHOSTUNREACH') msg = 'Host unreachable (check network/routing)';
1399
+ else if (code === 'ENOTFOUND') msg = 'Hostname/mDNS name could not be resolved';
1400
+ else if (code === 'ETIMEDOUT') msg = 'Timeout while establishing the connection';
1245
1401
  this.log.warn(`[${id}] Error during ${context}: ${msg}`);
1246
1402
  this.setStateAsync(`${id}.info.lastError`, { val: msg, ack: true }).catch(() => {});
1247
1403
  this.markConnected(id, false);
@@ -1367,7 +1523,7 @@ class Zeptrion extends utils.Adapter {
1367
1523
  try {
1368
1524
  body = JSON.parse(String(state.val));
1369
1525
  } catch (err) {
1370
- throw new Error(`ledSet: kein gültiges JSON ("${err.message}"). Beispiel: [{"id":2,"bg":"#220000"}]`);
1526
+ throw new Error(`ledSet: not valid JSON ("${err.message}"). Example: [{"id":2,"bg":"#220000"}]`);
1371
1527
  }
1372
1528
  await this.zapiPost(id, '/zapi/smartfront/led', body);
1373
1529
  await this.setStateAsync(idFull, { val: state.val, ack: true });
@@ -1478,7 +1634,7 @@ class Zeptrion extends utils.Adapter {
1478
1634
  discoverDevices(timeoutMs = 4000) {
1479
1635
  return new Promise((resolve, reject) => {
1480
1636
  if (!Bonjour) {
1481
- reject(new Error('Modul "bonjour-service" ist nicht installiert. "npm install bonjour-service" im Adapterverzeichnis ausführen.'));
1637
+ reject(new Error('Module "bonjour-service" is not installed. Run "npm install bonjour-service" in the adapter directory.'));
1482
1638
  return;
1483
1639
  }
1484
1640
  const bonjour = new Bonjour();
@@ -1586,7 +1742,7 @@ class Zeptrion extends utils.Adapter {
1586
1742
  try {
1587
1743
  const csv = String((obj.message && obj.message.csv) || '').trim();
1588
1744
  if (!csv) {
1589
- if (obj.callback) this.sendTo(obj.from, obj.command, { result: 'CSV-Feld ist leer. Format: host;name;kanäle;art;laufzeit_s;kipp_ms;smartfront;poll_s;laufzeit_kanal_s (nur host ist Pflicht).' }, obj.callback);
1745
+ if (obj.callback) this.sendTo(obj.from, obj.command, { result: 'CSV field is empty. Format: host;name;channels;type;runtime_s;tilt_ms;smartfront;poll_s;runtime_channel_s (only host is required).' }, obj.callback);
1590
1746
  return;
1591
1747
  }
1592
1748
  const delim = csv.includes(';') ? ';' : ',';
@@ -1619,9 +1775,9 @@ class Zeptrion extends utils.Adapter {
1619
1775
  if (['licht', 'lampe'].includes(row.kind)) row.kind = 'light';
1620
1776
 
1621
1777
  const errs = this.validateDeviceRow(row);
1622
- if (existingHosts.has(row.host.toLowerCase())) errs.push('Host bereits konfiguriert');
1778
+ if (existingHosts.has(row.host.toLowerCase())) errs.push('Host already configured');
1623
1779
  if (errs.length) {
1624
- report.push(`❌ Zeile ${i + 1} (${row.host || '?'}): ${errs.join('; ')}`);
1780
+ report.push(`❌ Row ${i + 1} (${row.host || '?'}): ${errs.join('; ')}`);
1625
1781
  continue;
1626
1782
  }
1627
1783
  // ID aus Host ableiten, Kollisionen auflösen
@@ -1646,7 +1802,7 @@ class Zeptrion extends utils.Adapter {
1646
1802
  });
1647
1803
  existingHosts.add(row.host.toLowerCase());
1648
1804
  existingIds.add(candidate);
1649
- report.push(`✅ Zeile ${i + 1}: ${row.name || row.host} (${row.host}) als "${candidate}" übernommen`);
1805
+ report.push(`✅ Row ${i + 1}: ${row.name || row.host} (${row.host}) imported as "${candidate}"`);
1650
1806
  added++;
1651
1807
  }
1652
1808
 
@@ -1654,14 +1810,13 @@ class Zeptrion extends utils.Adapter {
1654
1810
  instObj.native.devices = devices;
1655
1811
  await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, instObj);
1656
1812
  }
1657
- const result = `${added} von ${lines.length} Zeile(n) importiert.${added ? ' Adapter startet neu; Dialog schliessen und neu öffnen.' : ''}\n\n${report.join('\n')}`;
1813
+ const result = `${added} of ${lines.length} row(s) imported.${added ? ' Adapter restarts; close and reopen the dialog.' : ''}\n\n${report.join('\n')}`;
1658
1814
  this.log.info(`CSV import: ${added}/${lines.length} rows imported`);
1659
1815
  if (obj.callback) this.sendTo(obj.from, obj.command, { result }, obj.callback);
1660
1816
  } catch (err) {
1661
- // UI-facing text (shown in the admin config dialog) stays German for the
1662
- // German-speaking user base; the log entry itself must be English per checklist.
1663
- const msg = `CSV-Import fehlgeschlagen: ${err.message || err}`;
1664
- this.log.warn(`CSV import failed: ${err.message || err}`);
1817
+ // All text sent to the user (log and UI dialog alike) must be English.
1818
+ const msg = `CSV import failed: ${err.message || err}`;
1819
+ this.log.warn(msg);
1665
1820
  if (obj.callback) this.sendTo(obj.from, obj.command, { error: msg }, obj.callback);
1666
1821
  }
1667
1822
  return;
@@ -1671,7 +1826,7 @@ class Zeptrion extends utils.Adapter {
1671
1826
  const devicesCfg = Array.isArray(this.config.devices) ? this.config.devices : [];
1672
1827
  const rows = devicesCfg.filter(d => d && d.host);
1673
1828
  if (!rows.length) {
1674
- if (obj.callback) this.sendTo(obj.from, obj.command, { result: 'Keine Geräte mit Host in der Tabelle.' }, obj.callback);
1829
+ if (obj.callback) this.sendTo(obj.from, obj.command, { result: 'No devices with a host in the table.' }, obj.callback);
1675
1830
  return;
1676
1831
  }
1677
1832
  const lines = [];
@@ -1682,7 +1837,7 @@ class Zeptrion extends utils.Adapter {
1682
1837
  if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
1683
1838
  const octets = host.split('.').map(Number);
1684
1839
  if (octets.some(o => o > 255)) {
1685
- lines.push(`❌ ${label} (${host}): ungültige IP-Adresse`);
1840
+ lines.push(`❌ ${label} (${host}): invalid IP address`);
1686
1841
  continue;
1687
1842
  }
1688
1843
  }
@@ -1695,14 +1850,14 @@ class Zeptrion extends utils.Adapter {
1695
1850
  const idData = (rootKey && parsed[rootKey]) || {};
1696
1851
  if (String(idData.sys ?? '').toUpperCase() === 'ZEPTRION') {
1697
1852
  const m = String(idData.type ?? '').match(/^3340-(\d)-/);
1698
- const ch = m ? `, ${m[1]} Kanäle` : '';
1853
+ const ch = m ? `, ${m[1]} channels` : '';
1699
1854
  lines.push(`✅ ${label} (${host}): zeptrion ${idData.type ?? '?'}${ch}, SW ${idData.sw ?? '?'}, SN ${idData.sn ?? '?'}`);
1700
1855
  } else {
1701
- lines.push(`⚠️ ${label} (${host}): antwortet, aber KEIN zeptrion-Gerät (sys="${idData.sys ?? 'unbekannt'}")`);
1856
+ lines.push(`⚠️ ${label} (${host}): responds, but is NOT a zeptrion device (sys="${idData.sys ?? 'unknown'}")`);
1702
1857
  }
1703
1858
  } catch (err) {
1704
1859
  const code = err.code || (err.message || '').substring(0, 40);
1705
- lines.push(`❌ ${label} (${host}): nicht erreichbar (${code})`);
1860
+ lines.push(`❌ ${label} (${host}): unreachable (${code})`);
1706
1861
  }
1707
1862
  }
1708
1863
  const result = lines.join('\n');
@@ -1716,10 +1871,9 @@ class Zeptrion extends utils.Adapter {
1716
1871
  this.log.info('Starting mDNS discovery for zeptrion devices...');
1717
1872
  const results = await this.discoverDevices(4000);
1718
1873
  const added = await this.mergeDiscoveredDevices(results);
1719
- // UI-facing text (shown in the admin config dialog) stays German for the
1720
- // German-speaking user base; the log entry itself must be English per checklist.
1721
- const msg = `Suche abgeschlossen: ${results.length} Gerät(e) im Netz gefunden, ${added} neu (deaktiviert) übernommen. ` +
1722
- `Instanz-Konfiguration schliessen und neu öffnen, um sie in der Tabelle zu sehen und zu aktivieren.`;
1874
+ // All text sent to the user (log and UI dialog alike) must be English.
1875
+ const msg = `Discovery finished: ${results.length} device(s) found on the network, ${added} newly added (disabled). ` +
1876
+ `Close and reopen the instance configuration to see them in the table and enable them.`;
1723
1877
  this.log.info(`Discovery finished: ${results.length} device(s) found on the network, ${added} newly added (disabled).`);
1724
1878
  if (obj.callback) {
1725
1879
  this.sendTo(obj.from, obj.command, { result: msg, devices: results }, obj.callback);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zeptrion",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "ioBroker Adapter für Feller zeptrion / zApp WLAN-Aktoren (WLAN-Nebenstelle 4K, WLAN-Zwischenmodul 2K) über die zrap Webservice API - inkl. Sammelbefehlen für Hagelalarm und mDNS-Discovery",
5
5
  "author": "Stefan Bühler",
6
6
  "license": "MIT",