iobroker.zeptrion 1.0.9 → 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,10 @@ 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
+
179
183
  ### 1.0.9 (2026-08-08)
180
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.
181
185
 
@@ -195,10 +199,7 @@ npm run test:integration # starts a real js-controller (takes longer)
195
199
  ### 1.0.4 (2026-07-21)
196
200
  - Documentation only: added a Buy Me a Coffee link next to the PayPal donate badge. No functional changes.
197
201
 
198
- ### 1.0.3 (2026-07-17)
199
- - Documentation only: fixed the PayPal donate link, which previously used the wrong URL format and did not work.
200
-
201
- ### 1.0.2 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,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zeptrion",
4
- "version": "1.0.9",
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
+ },
6
19
  "1.0.9": {
7
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.",
8
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.",
@@ -80,19 +93,6 @@
80
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.",
81
94
  "uk": "Лише документація: додано посилання Buy Me a Coffee поруч зі значком пожертв PayPal у README.md/README_de.md та в пакеті npm (поле funding). Без функціональних змін.",
82
95
  "zh-cn": "仅文档:在 README.md/README_de.md 及 npm 包(funding 字段)中,于 PayPal 捐赠徽章旁添加了 Buy Me a Coffee 链接。无功能变更。"
83
- },
84
- "1.0.3": {
85
- "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.",
86
- "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.",
87
- "ru": "Только документация: исправлена ссылка для пожертвований PayPal в README (en/de), которая ранее имела неверный формат URL и не работала. Без функциональных изменений.",
88
- "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.",
89
- "nl": "Alleen documentatie: de PayPal-donatielink in README (en/de) gecorrigeerd, die voorheen het verkeerde URL-formaat had en niet werkte. Geen functionele wijzigingen.",
90
- "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.",
91
- "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.",
92
- "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.",
93
- "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.",
94
- "uk": "Лише документація: виправлено посилання для пожертв PayPal у README (en/de), яке раніше мало неправильний формат URL і не працювало. Без функціональних змін.",
95
- "zh-cn": "仅文档:修正了 README(en/de)中的 PayPal 捐赠链接,此前该链接使用了错误的 URL 格式且无法正常使用。无功能变更。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/main.js CHANGED
@@ -409,9 +409,9 @@ class Zeptrion extends utils.Adapter {
409
409
  for (const d of active) {
410
410
  const errs = this.validateDeviceRow(d);
411
411
  const sanId = this.sanitize(d.id);
412
- 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`);
413
413
  const hostKey = String(d.host).trim().toLowerCase();
414
- 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`);
415
415
  if (errs.length) {
416
416
  this.log.error(`Device "${d.name || d.id || d.host}" skipped: ${errs.join('; ')}`);
417
417
  continue;
@@ -487,50 +487,50 @@ class Zeptrion extends utils.Adapter {
487
487
  const errs = [];
488
488
  const host = String(d.host || '').trim();
489
489
  if (!host) {
490
- errs.push('Host fehlt');
490
+ errs.push('Host missing');
491
491
  } else if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
492
- 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)`);
493
493
  } else if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
494
494
  const octets = host.split('.').map(Number);
495
495
  if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {
496
- errs.push(`"${host}" ist keine gültige IPv4-Adresse`);
496
+ errs.push(`"${host}" is not a valid IPv4 address`);
497
497
  }
498
498
  }
499
499
  if (d.id && !/^[a-zA-Z0-9_-]+$/.test(String(d.id))) {
500
- 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, _, -)`);
501
501
  }
502
502
  const ch = parseInt(d.channels, 10);
503
503
  if (d.channels !== undefined && d.channels !== '' && (isNaN(ch) || ch < 1 || ch > 4)) {
504
- errs.push(`Kanäle "${d.channels}" ungültig (1-4)`);
504
+ errs.push(`Channels "${d.channels}" invalid (1-4)`);
505
505
  }
506
506
  if (d.kind !== undefined && d.kind !== '' && !['unknown', 'blind', 'light'].includes(String(d.kind))) {
507
- errs.push(`Art "${d.kind}" ungültig (unknown/blind/light)`);
507
+ errs.push(`Type "${d.kind}" invalid (unknown/blind/light)`);
508
508
  }
509
509
  const tt = parseInt(d.travelTimeSec, 10);
510
510
  if (d.travelTimeSec !== undefined && d.travelTimeSec !== '' && (isNaN(tt) || tt < 0 || tt > 300)) {
511
- errs.push(`Laufzeit "${d.travelTimeSec}" ungültig (0-300s)`);
511
+ errs.push(`Runtime "${d.travelTimeSec}" invalid (0-300s)`);
512
512
  }
513
513
  if (d.travelTimeSecCh !== undefined && String(d.travelTimeSecCh).trim() !== '') {
514
514
  const parts = String(d.travelTimeSecCh).split(',').map(s => s.trim());
515
515
  if (parts.length > 4) {
516
- errs.push(`Laufzeit/Kanal "${d.travelTimeSecCh}": maximal 4 Werte`);
516
+ errs.push(`Runtime/channel "${d.travelTimeSecCh}": maximum 4 values`);
517
517
  }
518
518
  for (const p of parts) {
519
519
  if (p === '') continue; // leerer Eintrag = Fallback auf travelTimeSec
520
520
  const v = parseInt(p, 10);
521
521
  if (isNaN(v) || v < 0 || v > 300 || String(v) !== p) {
522
- 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)`);
523
523
  break;
524
524
  }
525
525
  }
526
526
  }
527
527
  const tp = parseInt(d.tiltTimeMs, 10);
528
528
  if (d.tiltTimeMs !== undefined && d.tiltTimeMs !== '' && (isNaN(tp) || tp < 0 || tp > 5000)) {
529
- errs.push(`Kipp-Impuls "${d.tiltTimeMs}" ungültig (0-5000ms)`);
529
+ errs.push(`Tilt pulse "${d.tiltTimeMs}" invalid (0-5000ms)`);
530
530
  }
531
531
  const pi = parseInt(d.pollInterval, 10);
532
532
  if (d.pollInterval !== undefined && d.pollInterval !== '' && (isNaN(pi) || pi < 5 || pi > 3600)) {
533
- errs.push(`Poll-Intervall "${d.pollInterval}" ungültig (5-3600s)`);
533
+ errs.push(`Poll interval "${d.pollInterval}" invalid (5-3600s)`);
534
534
  }
535
535
  return errs;
536
536
  }
@@ -833,7 +833,7 @@ class Zeptrion extends utils.Adapter {
833
833
 
834
834
  async zrapGet(id, path, axiosOpts = {}) {
835
835
  const dev = this.devices[id];
836
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
836
+ if (!dev) throw new Error(`Unknown device ${id}`);
837
837
  const res = await dev.client.get(path, { responseType: 'text', transformResponse: [d => d], ...axiosOpts });
838
838
  if (res.status >= 400) throw new Error(`HTTP ${res.status}`);
839
839
  if (!res.data) return {};
@@ -847,7 +847,7 @@ class Zeptrion extends utils.Adapter {
847
847
 
848
848
  async zrapPost(id, path, bodyObj) {
849
849
  const dev = this.devices[id];
850
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
850
+ if (!dev) throw new Error(`Unknown device ${id}`);
851
851
  const data = Object.entries(bodyObj)
852
852
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
853
853
  .join('&');
@@ -863,7 +863,7 @@ class Zeptrion extends utils.Adapter {
863
863
  // 3340-2-B + Front 920-330x), daher separat und optional (Konfig-Checkbox).
864
864
  async zapiGet(id, path) {
865
865
  const dev = this.devices[id];
866
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
866
+ if (!dev) throw new Error(`Unknown device ${id}`);
867
867
  const res = await dev.client.get(path);
868
868
  if (res.status >= 400) throw new Error(`HTTP ${res.status}`);
869
869
  return res.data;
@@ -871,7 +871,7 @@ class Zeptrion extends utils.Adapter {
871
871
 
872
872
  async zapiPost(id, path, jsonBody) {
873
873
  const dev = this.devices[id];
874
- if (!dev) throw new Error(`Unbekanntes Gerät ${id}`);
874
+ if (!dev) throw new Error(`Unknown device ${id}`);
875
875
  const res = await dev.client.post(path, jsonBody, {
876
876
  headers: { 'Content-Type': 'application/json' }
877
877
  });
@@ -895,7 +895,7 @@ class Zeptrion extends utils.Adapter {
895
895
  const str = String(value ?? '');
896
896
  const bytes = Buffer.byteLength(str, 'utf8');
897
897
  if (bytes > maxBytes) {
898
- 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)`);
899
899
  }
900
900
  return str;
901
901
  }
@@ -908,10 +908,10 @@ class Zeptrion extends utils.Adapter {
908
908
  */
909
909
  sendChannelCommand(id, chNum, cmd) {
910
910
  if (!isValidChCmd(cmd)) {
911
- return Promise.reject(new Error(`Ungültiger Kanalbefehl "${cmd}"`));
911
+ return Promise.reject(new Error(`Invalid channel command "${cmd}"`));
912
912
  }
913
913
  const dev = this.devices[id];
914
- if (!dev) return Promise.reject(new Error(`Unbekanntes Gerät ${id}`));
914
+ if (!dev) return Promise.reject(new Error(`Unknown device ${id}`));
915
915
 
916
916
  return new Promise((resolve, reject) => {
917
917
  dev.pendingCmds[chNum] = cmd;
@@ -1068,7 +1068,7 @@ class Zeptrion extends utils.Adapter {
1068
1068
  const dev = this.devices[id];
1069
1069
  if (!dev) return;
1070
1070
  if (dev.cfg.kind !== 'blind' || !dev.cfg.travelTimeMsByCh[chNum]) {
1071
- 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');
1072
1072
  }
1073
1073
  target = Math.max(0, Math.min(100, Math.round(Number(target))));
1074
1074
  const travel = dev.cfg.travelTimeMsByCh[chNum];
@@ -1393,11 +1393,11 @@ class Zeptrion extends utils.Adapter {
1393
1393
  handleDeviceError(id, err, context) {
1394
1394
  let msg = (err && err.message) || String(err);
1395
1395
  const code = err && err.code;
1396
- if (code === 'ECONNREFUSED') msg = 'Verbindung verweigert (Gerät aus oder falsche IP?)';
1397
- else if (code === 'ECONNABORTED') msg = 'Zeitüberschreitung (Gerät nicht erreichbar)';
1398
- else if (code === 'EHOSTUNREACH') msg = 'Host nicht erreichbar (Netzwerk/Routing prüfen)';
1399
- else if (code === 'ENOTFOUND') msg = 'Hostname/mDNS-Name nicht auflösbar';
1400
- 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';
1401
1401
  this.log.warn(`[${id}] Error during ${context}: ${msg}`);
1402
1402
  this.setStateAsync(`${id}.info.lastError`, { val: msg, ack: true }).catch(() => {});
1403
1403
  this.markConnected(id, false);
@@ -1523,7 +1523,7 @@ class Zeptrion extends utils.Adapter {
1523
1523
  try {
1524
1524
  body = JSON.parse(String(state.val));
1525
1525
  } catch (err) {
1526
- 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"}]`);
1527
1527
  }
1528
1528
  await this.zapiPost(id, '/zapi/smartfront/led', body);
1529
1529
  await this.setStateAsync(idFull, { val: state.val, ack: true });
@@ -1634,7 +1634,7 @@ class Zeptrion extends utils.Adapter {
1634
1634
  discoverDevices(timeoutMs = 4000) {
1635
1635
  return new Promise((resolve, reject) => {
1636
1636
  if (!Bonjour) {
1637
- 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.'));
1638
1638
  return;
1639
1639
  }
1640
1640
  const bonjour = new Bonjour();
@@ -1742,7 +1742,7 @@ class Zeptrion extends utils.Adapter {
1742
1742
  try {
1743
1743
  const csv = String((obj.message && obj.message.csv) || '').trim();
1744
1744
  if (!csv) {
1745
- 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);
1746
1746
  return;
1747
1747
  }
1748
1748
  const delim = csv.includes(';') ? ';' : ',';
@@ -1775,9 +1775,9 @@ class Zeptrion extends utils.Adapter {
1775
1775
  if (['licht', 'lampe'].includes(row.kind)) row.kind = 'light';
1776
1776
 
1777
1777
  const errs = this.validateDeviceRow(row);
1778
- if (existingHosts.has(row.host.toLowerCase())) errs.push('Host bereits konfiguriert');
1778
+ if (existingHosts.has(row.host.toLowerCase())) errs.push('Host already configured');
1779
1779
  if (errs.length) {
1780
- report.push(`❌ Zeile ${i + 1} (${row.host || '?'}): ${errs.join('; ')}`);
1780
+ report.push(`❌ Row ${i + 1} (${row.host || '?'}): ${errs.join('; ')}`);
1781
1781
  continue;
1782
1782
  }
1783
1783
  // ID aus Host ableiten, Kollisionen auflösen
@@ -1802,7 +1802,7 @@ class Zeptrion extends utils.Adapter {
1802
1802
  });
1803
1803
  existingHosts.add(row.host.toLowerCase());
1804
1804
  existingIds.add(candidate);
1805
- 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}"`);
1806
1806
  added++;
1807
1807
  }
1808
1808
 
@@ -1810,14 +1810,13 @@ class Zeptrion extends utils.Adapter {
1810
1810
  instObj.native.devices = devices;
1811
1811
  await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, instObj);
1812
1812
  }
1813
- 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')}`;
1814
1814
  this.log.info(`CSV import: ${added}/${lines.length} rows imported`);
1815
1815
  if (obj.callback) this.sendTo(obj.from, obj.command, { result }, obj.callback);
1816
1816
  } catch (err) {
1817
- // UI-facing text (shown in the admin config dialog) stays German for the
1818
- // German-speaking user base; the log entry itself must be English per checklist.
1819
- const msg = `CSV-Import fehlgeschlagen: ${err.message || err}`;
1820
- 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);
1821
1820
  if (obj.callback) this.sendTo(obj.from, obj.command, { error: msg }, obj.callback);
1822
1821
  }
1823
1822
  return;
@@ -1827,7 +1826,7 @@ class Zeptrion extends utils.Adapter {
1827
1826
  const devicesCfg = Array.isArray(this.config.devices) ? this.config.devices : [];
1828
1827
  const rows = devicesCfg.filter(d => d && d.host);
1829
1828
  if (!rows.length) {
1830
- 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);
1831
1830
  return;
1832
1831
  }
1833
1832
  const lines = [];
@@ -1838,7 +1837,7 @@ class Zeptrion extends utils.Adapter {
1838
1837
  if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
1839
1838
  const octets = host.split('.').map(Number);
1840
1839
  if (octets.some(o => o > 255)) {
1841
- lines.push(`❌ ${label} (${host}): ungültige IP-Adresse`);
1840
+ lines.push(`❌ ${label} (${host}): invalid IP address`);
1842
1841
  continue;
1843
1842
  }
1844
1843
  }
@@ -1851,14 +1850,14 @@ class Zeptrion extends utils.Adapter {
1851
1850
  const idData = (rootKey && parsed[rootKey]) || {};
1852
1851
  if (String(idData.sys ?? '').toUpperCase() === 'ZEPTRION') {
1853
1852
  const m = String(idData.type ?? '').match(/^3340-(\d)-/);
1854
- const ch = m ? `, ${m[1]} Kanäle` : '';
1853
+ const ch = m ? `, ${m[1]} channels` : '';
1855
1854
  lines.push(`✅ ${label} (${host}): zeptrion ${idData.type ?? '?'}${ch}, SW ${idData.sw ?? '?'}, SN ${idData.sn ?? '?'}`);
1856
1855
  } else {
1857
- 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'}")`);
1858
1857
  }
1859
1858
  } catch (err) {
1860
1859
  const code = err.code || (err.message || '').substring(0, 40);
1861
- lines.push(`❌ ${label} (${host}): nicht erreichbar (${code})`);
1860
+ lines.push(`❌ ${label} (${host}): unreachable (${code})`);
1862
1861
  }
1863
1862
  }
1864
1863
  const result = lines.join('\n');
@@ -1872,10 +1871,9 @@ class Zeptrion extends utils.Adapter {
1872
1871
  this.log.info('Starting mDNS discovery for zeptrion devices...');
1873
1872
  const results = await this.discoverDevices(4000);
1874
1873
  const added = await this.mergeDiscoveredDevices(results);
1875
- // UI-facing text (shown in the admin config dialog) stays German for the
1876
- // German-speaking user base; the log entry itself must be English per checklist.
1877
- const msg = `Suche abgeschlossen: ${results.length} Gerät(e) im Netz gefunden, ${added} neu (deaktiviert) übernommen. ` +
1878
- `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.`;
1879
1877
  this.log.info(`Discovery finished: ${results.length} device(s) found on the network, ${added} newly added (disabled).`);
1880
1878
  if (obj.callback) {
1881
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.9",
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",