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 +5 -4
- package/io-package.json +14 -14
- package/main.js +44 -46
- package/package.json +1 -1
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
|
|
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.
|
|
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}" (
|
|
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}"
|
|
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
|
|
490
|
+
errs.push('Host missing');
|
|
491
491
|
} else if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
|
|
492
|
-
errs.push(`Host "${host}"
|
|
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}"
|
|
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}"
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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}
|
|
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(`
|
|
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(`
|
|
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
|
|
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 = '
|
|
1397
|
-
else if (code === 'ECONNABORTED') msg = '
|
|
1398
|
-
else if (code === 'EHOSTUNREACH') msg = 'Host
|
|
1399
|
-
else if (code === 'ENOTFOUND') msg = 'Hostname/mDNS
|
|
1400
|
-
else if (code === 'ETIMEDOUT') msg = '
|
|
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:
|
|
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('
|
|
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
|
|
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
|
|
1778
|
+
if (existingHosts.has(row.host.toLowerCase())) errs.push('Host already configured');
|
|
1779
1779
|
if (errs.length) {
|
|
1780
|
-
report.push(`❌
|
|
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(`✅
|
|
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}
|
|
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
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
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: '
|
|
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}):
|
|
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]}
|
|
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}):
|
|
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}):
|
|
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
|
-
//
|
|
1876
|
-
|
|
1877
|
-
|
|
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.
|
|
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",
|