diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 31e73e8..2293592 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -28,7 +28,7 @@ jobs: node-version: [12.x] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -60,7 +60,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -140,19 +140,17 @@ jobs: prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} body: ${{ steps.extract_release.outputs.BODY }} - #- name: Notify Sentry.io about the release - # run: | - # npm i -g @sentry/cli - # export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - # export SENTRY_URL=https://sentry.iobroker.net - # export SENTRY_ORG=iobroker - # export SENTRY_PROJECT=iobroker-hass - # export SENTRY_VERSION=iobroker.hass@${{ steps.extract_release.outputs.VERSION }} - # sentry-cli releases new $SENTRY_VERSION - # sentry-cli releases finalize $SENTRY_VERSION - - # Add the following line BEFORE finalize if repositories are connected in Sentry - # sentry-cli releases set-commits $SENTRY_VERSION --auto + - name: Notify Sentry.io about the release + run: | + npm i -g @sentry/cli + export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} + export SENTRY_URL=https://sentry.iobroker.net + export SENTRY_ORG=iobroker + export SENTRY_PROJECT=iobroker-hass + export SENTRY_VERSION=iobroker.hass@${{ steps.extract_release.outputs.VERSION }} + sentry-cli releases new $SENTRY_VERSION + sentry-cli releases set-commits $SENTRY_VERSION --auto + sentry-cli releases finalize $SENTRY_VERSION # Add the following line BEFORE finalize if sourcemap uploads are needed # sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/ diff --git a/.releaseconfig.json b/.releaseconfig.json new file mode 100644 index 0000000..bb9bf40 --- /dev/null +++ b/.releaseconfig.json @@ -0,0 +1,3 @@ +{ + "plugins": ["iobroker", "license"] +} diff --git a/LICENSE b/LICENSE index d3e1995..2db2dfb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018-2020 bluefox +Copyright (c) 2018-2022 bluefox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b647266..06691c8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ ![Logo](admin/hass.png) # ioBroker.hass +![Number of Installations](http://iobroker.live/badges/hass-installed.svg) +![Number of Installations](http://iobroker.live/badges/hass-stable.svg) [![NPM version](http://img.shields.io/npm/v/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass) + +![Test and Release](https://github.com/ioBroker/ioBroker.hass/workflows/Test%20and%20Release/badge.svg) +[![Translation status](https://weblate.iobroker.net/widgets/adapters/-/hass/svg-badge.svg)](https://weblate.iobroker.net/engage/adapters/?utm_source=widget) [![Downloads](https://img.shields.io/npm/dm/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass) +**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0. + This adapter allows the connecting of Home Assistant to ioBroker. @@ -24,6 +31,11 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista ## Changelog +### __WORK IN PROGRESS__ +* (Apollon77) Implement Service triggers to use any value to trigger or stringified JSON to call with fields +* (Apollon77) Optimize unload handling +* (Apollon7) Add Sentry for crash reporting + ### 1.0.1 (2021-09-04) * IMPORTANT: js-controller 2.0 is needed st least! * (Apollon77) Fix start issue @@ -38,7 +50,7 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista ## License The MIT License (MIT) -Copyright (c) 2018-2020 bluefox +Copyright (c) 2018-2022 bluefox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json new file mode 100644 index 0000000..266ff82 --- /dev/null +++ b/admin/i18n/de/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet IP:", + "FHEM Telnet Port:": "FHEM Telnet Port:", + "FHEM adapter settings": "FHEM Adapter Einstellungen", + "Password repeat:": "Kennwort-Woederholung:", + "Password:": "Kennwort:", + "Passwords missmatch!": "Kennworte sind unterschiedlich!", + "Prompt:": "Prompt:", + "tooltip_password": "Optional. Nur falls in FHEM konfiguriert", + "tooltip_prompt": "Man kann hier ein eigenes Prompt setzen. Default ist 'hass>'" +} \ No newline at end of file diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json new file mode 100644 index 0000000..d22a7c4 --- /dev/null +++ b/admin/i18n/en/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet IP:", + "FHEM Telnet Port:": "FHEM Telnet Port:", + "FHEM adapter settings": "FHEM adapter settings", + "Password repeat:": "Password repeat:", + "Password:": "Password:", + "Passwords missmatch!": "Passwords missmatch!", + "Prompt:": "Prompt:", + "tooltip_password": "Optional. Only if FHEM requires password.", + "tooltip_prompt": "You can change standard prompt from 'hass>' to something else." +} \ No newline at end of file diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json new file mode 100644 index 0000000..39d5624 --- /dev/null +++ b/admin/i18n/es/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet IP:", + "FHEM Telnet Port:": "Puerto Telnet FHEM:", + "FHEM adapter settings": "Configuración del adaptador FHEM", + "Password repeat:": "Repite la contraseña:", + "Password:": "Clave:", + "Passwords missmatch!": "¡Las contraseñas no coinciden!", + "Prompt:": "Inmediato:", + "tooltip_password": "Opcional. ", + "tooltip_prompt": "Puede cambiar el aviso estándar de 'hass>' a otra cosa." +} \ No newline at end of file diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json new file mode 100644 index 0000000..b83bbc9 --- /dev/null +++ b/admin/i18n/fr/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "IP Telnet FHEM :", + "FHEM Telnet Port:": "Port Telnet FHEM :", + "FHEM adapter settings": "Paramètres de l'adaptateur FHEM", + "Password repeat:": "Répéter le mot de passe :", + "Password:": "Mot de passe:", + "Passwords missmatch!": "Les mots de passe ne correspondent pas !", + "Prompt:": "Rapide:", + "tooltip_password": "Optionnel. ", + "tooltip_prompt": "Vous pouvez changer l'invite standard de 'hass>' en quelque chose d'autre." +} \ No newline at end of file diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json new file mode 100644 index 0000000..7b59d24 --- /dev/null +++ b/admin/i18n/it/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "IP Telnet FHEM:", + "FHEM Telnet Port:": "Porta Telnet FHEM:", + "FHEM adapter settings": "Impostazioni dell'adattatore FHEM", + "Password repeat:": "Ripeti password:", + "Password:": "Parola d'ordine:", + "Passwords missmatch!": "Le password non corrispondono!", + "Prompt:": "Richiesta:", + "tooltip_password": "Opzionale. ", + "tooltip_prompt": "Puoi cambiare il prompt standard da 'hass>' a qualcos'altro." +} \ No newline at end of file diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json new file mode 100644 index 0000000..e6dc4c2 --- /dev/null +++ b/admin/i18n/nl/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet-IP:", + "FHEM Telnet Port:": "FHEM Telnet-poort:", + "FHEM adapter settings": "Instellingen FHEM-adapter", + "Password repeat:": "Wachtwoord herhalen:", + "Password:": "Wachtwoord:", + "Passwords missmatch!": "Wachtwoorden komen niet overeen!", + "Prompt:": "Snel:", + "tooltip_password": "Optioneel. ", + "tooltip_prompt": "U kunt de standaardprompt wijzigen van 'hass>' naar iets anders." +} \ No newline at end of file diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl/translations.json new file mode 100644 index 0000000..c55019c --- /dev/null +++ b/admin/i18n/pl/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "Adres IP Telnetu FHEM:", + "FHEM Telnet Port:": "Port Telnet FHEM:", + "FHEM adapter settings": "Ustawienia adaptera FHEM", + "Password repeat:": "Powtórz hasło:", + "Password:": "Hasło:", + "Passwords missmatch!": "Niezgodność haseł!", + "Prompt:": "Podpowiedź:", + "tooltip_password": "Opcjonalny. ", + "tooltip_prompt": "Możesz zmienić standardowy monit z „hass>” na coś innego." +} \ No newline at end of file diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json new file mode 100644 index 0000000..5cbb647 --- /dev/null +++ b/admin/i18n/pt/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet IP:", + "FHEM Telnet Port:": "Porta Telnet FHEM:", + "FHEM adapter settings": "Configurações do adaptador FHEM", + "Password repeat:": "Repetição de senha:", + "Password:": "Senha:", + "Passwords missmatch!": "As senhas não correspondem!", + "Prompt:": "Pronto:", + "tooltip_password": "Opcional. ", + "tooltip_prompt": "Você pode alterar o prompt padrão de 'hass>' para outra coisa." +} \ No newline at end of file diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json new file mode 100644 index 0000000..d2056c2 --- /dev/null +++ b/admin/i18n/ru/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM Telnet IP:", + "FHEM Telnet Port:": "FHEM Telnet порт:", + "FHEM adapter settings": "Настройки драйвера FHEM", + "Password repeat:": "Подтверждение пароля:", + "Password:": "Пароль:", + "Passwords missmatch!": "Пароли не совпадают!", + "Prompt:": "Промпт:", + "tooltip_password": "Не обязательно. Только если настроено в FHEM", + "tooltip_prompt": "Можно задать собственный промпт. По умолчанию 'hass>'" +} \ No newline at end of file diff --git a/admin/i18n/zh-cn/translations.json b/admin/i18n/zh-cn/translations.json new file mode 100644 index 0000000..9b128b4 --- /dev/null +++ b/admin/i18n/zh-cn/translations.json @@ -0,0 +1,11 @@ +{ + "FHEM Telnet IP:": "FHEM 远程登录 IP:", + "FHEM Telnet Port:": "FHEM 远程登录端口:", + "FHEM adapter settings": "FHEM 适配器设置", + "Password repeat:": "密码重复:", + "Password:": "密码:", + "Passwords missmatch!": "密码不匹配!", + "Prompt:": "迅速的:", + "tooltip_password": "可选的。", + "tooltip_prompt": "您可以将标准提示从“hass>”更改为其他内容。" +} \ No newline at end of file diff --git a/admin/words.js b/admin/words.js index 513a18c..86bc1c2 100644 --- a/admin/words.js +++ b/admin/words.js @@ -1,19 +1,14 @@ +/*global systemDictionary:true */ +'use strict'; + systemDictionary = { - "FHEM adapter settings":{"de": "FHEM Adapter Einstellungen", "ru": "Настройки драйвера FHEM"}, - "FHEM Telnet IP:": {"de": "FHEM Telnet IP:", "ru": "FHEM Telnet IP:"}, - "FHEM Telnet Port:": {"en": "FHEM Telnet Port:", "de": "FHEM Telnet Port:", "ru": "FHEM Telnet порт:"}, - "Password:": {"en": "Password:", "de": "Kennwort:", "ru": "Пароль:"}, - "Password repeat:": {"en": "Password repeat:", "de": "Kennwort-Woederholung:", "ru": "Подтверждение пароля:"}, - "Passwords missmatch!": {"en": "Passwords missmatch!", "de": "Kennworte sind unterschiedlich!", "ru": "Пароли не совпадают!"}, - "Prompt:": {"en": "Prompt:", "de": "Prompt:", "ru": "Промпт:"}, - "tooltip_password": { - "en": "Optional. Only if FHEM requires password.", - "de": "Optional. Nur falls in FHEM konfiguriert", - "ru": "Не обязательно. Только если настроено в FHEM" - }, - "tooltip_prompt": { - "en": "You can change standard prompt from 'hass>' to something else.", - "de": "Man kann hier ein eigenes Prompt setzen. Default ist 'hass>'", - "ru": "Можно задать собственный промпт. По умолчанию 'hass>'" - } + "FHEM Telnet IP:": { "en": "FHEM Telnet IP:", "de": "FHEM Telnet IP:", "ru": "FHEM Telnet IP:", "pt": "FHEM Telnet IP:", "nl": "FHEM Telnet-IP:", "fr": "IP Telnet FHEM :", "it": "IP Telnet FHEM:", "es": "FHEM Telnet IP:", "pl": "Adres IP Telnetu FHEM:", "zh-cn": "FHEM 远程登录 IP:"}, + "FHEM Telnet Port:": { "en": "FHEM Telnet Port:", "de": "FHEM Telnet Port:", "ru": "FHEM Telnet порт:", "pt": "Porta Telnet FHEM:", "nl": "FHEM Telnet-poort:", "fr": "Port Telnet FHEM :", "it": "Porta Telnet FHEM:", "es": "Puerto Telnet FHEM:", "pl": "Port Telnet FHEM:", "zh-cn": "FHEM 远程登录端口:"}, + "FHEM adapter settings": { "en": "FHEM adapter settings", "de": "FHEM Adapter Einstellungen", "ru": "Настройки драйвера FHEM", "pt": "Configurações do adaptador FHEM", "nl": "Instellingen FHEM-adapter", "fr": "Paramètres de l'adaptateur FHEM", "it": "Impostazioni dell'adattatore FHEM", "es": "Configuración del adaptador FHEM", "pl": "Ustawienia adaptera FHEM", "zh-cn": "FHEM 适配器设置"}, + "Password repeat:": { "en": "Password repeat:", "de": "Kennwort-Woederholung:", "ru": "Подтверждение пароля:", "pt": "Repetição de senha:", "nl": "Wachtwoord herhalen:", "fr": "Répéter le mot de passe :", "it": "Ripeti password:", "es": "Repite la contraseña:", "pl": "Powtórz hasło:", "zh-cn": "密码重复:"}, + "Password:": { "en": "Password:", "de": "Kennwort:", "ru": "Пароль:", "pt": "Senha:", "nl": "Wachtwoord:", "fr": "Mot de passe:", "it": "Parola d'ordine:", "es": "Clave:", "pl": "Hasło:", "zh-cn": "密码:"}, + "Passwords missmatch!": { "en": "Passwords missmatch!", "de": "Kennworte sind unterschiedlich!", "ru": "Пароли не совпадают!", "pt": "As senhas não correspondem!", "nl": "Wachtwoorden komen niet overeen!", "fr": "Les mots de passe ne correspondent pas !", "it": "Le password non corrispondono!", "es": "¡Las contraseñas no coinciden!", "pl": "Niezgodność haseł!", "zh-cn": "密码不匹配!"}, + "Prompt:": { "en": "Prompt:", "de": "Prompt:", "ru": "Промпт:", "pt": "Pronto:", "nl": "Snel:", "fr": "Rapide:", "it": "Richiesta:", "es": "Inmediato:", "pl": "Podpowiedź:", "zh-cn": "迅速的:"}, + "tooltip_password": { "en": "Optional. Only if FHEM requires password.", "de": "Optional. Nur falls in FHEM konfiguriert", "ru": "Не обязательно. Только если настроено в FHEM", "pt": "Opcional. ", "nl": "Optioneel. ", "fr": "Optionnel. ", "it": "Opzionale. ", "es": "Opcional. ", "pl": "Opcjonalny. ", "zh-cn": "可选的。"}, + "tooltip_prompt": { "en": "You can change standard prompt from 'hass>' to something else.", "de": "Man kann hier ein eigenes Prompt setzen. Default ist 'hass>'", "ru": "Можно задать собственный промпт. По умолчанию 'hass>'", "pt": "Você pode alterar o prompt padrão de 'hass>' para outra coisa.", "nl": "U kunt de standaardprompt wijzigen van 'hass>' naar iets anders.", "fr": "Vous pouvez changer l'invite standard de 'hass>' en quelque chose d'autre.", "it": "Puoi cambiare il prompt standard da 'hass>' a qualcos'altro.", "es": "Puede cambiar el aviso estándar de 'hass>' a otra cosa.", "pl": "Możesz zmienić standardowy monit z „hass>” na coś innego.", "zh-cn": "您可以将标准提示从“hass>”更改为其他内容。"}, }; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index cb67dc7..a5d5acf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,19 +1,18 @@ +/*! + * ioBroker gulpfile + * Date: 2019-01-28 + */ 'use strict'; -const gulp = require('gulp'); -const fs = require('fs'); -const pkg = require('./package.json'); +const gulp = require('gulp'); +const fs = require('fs'); +const pkg = require('./package.json'); const iopackage = require('./io-package.json'); -const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; -/*var appName = getAppName(); - -function getAppName() { - var parts = __dirname.replace(/\\/g, '/').split('/'); - return parts[parts.length - 1].split('.')[0].toLowerCase(); -} -*/ +const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; const fileName = 'words.js'; -let languages = { +const EMPTY = ''; +const translate = require('./lib/tools').translateText; +const languages = { en: {}, de: {}, ru: {}, @@ -26,25 +25,18 @@ let languages = { 'zh-cn': {} }; -function lang2data(lang, isFlat) { - let str = isFlat ? '' : '{\n'; +function lang2data(lang) { + let str ='{\n'; let count = 0; for (const w in lang) { if (lang.hasOwnProperty(w)) { count++; - if (isFlat) { - str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; - } else { - const key = ' "' + w.replace(/"/g, '\\"') + '": '; - str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; - } + const key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; } } if (!count) { - return isFlat ? '' : '{\n}'; - } - if (isFlat) { - return str; + return '{\n}'; } else { return str.substring(0, str.length - 2) + '\n}'; } @@ -58,26 +50,9 @@ function readWordJs(src) { } else { words = fs.readFileSync(src + fileName).toString(); } + words = words.substring(words.indexOf('{'), words.length); + words = words.substring(0, words.lastIndexOf(';')); - const lines = words.split(/\r\n|\r|\n/g); - let i = 0; - while (!lines[i].match(/^systemDictionary = {/)) { - i++; - } - lines.splice(0, i); - - // remove last empty lines - i = lines.length - 1; - while (!lines[i]) { - i--; - } - if (i < lines.length - 1) { - lines.splice(i + 1); - } - - lines[0] = lines[0].replace('systemDictionary = ', ''); - lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); - words = lines.join('\n'); const resultFunc = new Function('return ' + words + ';'); return resultFunc(); @@ -85,14 +60,15 @@ function readWordJs(src) { return null; } } + function padRight(text, totalLength) { return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); } -function writeWordJs(data, src) { - let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n'; - text += '/*global systemDictionary:true */\n'; - text += '\'use strict\';\n\n'; +function writeWordJs(data, src) { + let text = ''; + text += '/*global systemDictionary:true */\n'; + text += "'use strict';\n\n"; text += 'systemDictionary = {\n'; for (const word in data) { if (data.hasOwnProperty(word)) { @@ -110,9 +86,7 @@ function writeWordJs(data, src) { text += line + '},\n'; } } - text = text.replace(/},\n$/, '}\n'); text += '};'; - if (fs.existsSync(src + 'js/' + fileName)) { fs.writeFileSync(src + 'js/' + fileName, text); } else { @@ -120,8 +94,6 @@ function writeWordJs(data, src) { } } -const EMPTY = ''; - function words2languages(src) { const langs = Object.assign({}, languages); const data = readWordJs(src); @@ -145,10 +117,11 @@ function words2languages(src) { fs.mkdirSync(src + 'i18n/'); } for (const l in langs) { - if (!langs.hasOwnProperty(l)) continue; + if (!langs.hasOwnProperty(l)) + continue; const keys = Object.keys(langs[l]); - //keys.sort(); - let obj = {}; + keys.sort(); + const obj = {}; for (let k = 0; k < keys.length; k++) { obj[keys[k]] = langs[l][keys[k]]; } @@ -162,144 +135,36 @@ function words2languages(src) { console.error('Cannot read or parse ' + fileName); } } -function words2languagesFlat(src) { - let langs = Object.assign({}, languages); - const data = readWordJs(src); - if (data) { - for (const word in data) { - if (data.hasOwnProperty(word)) { - for (const lang in data[word]) { - if (data[word].hasOwnProperty(lang)) { - langs[lang][word] = data[word][lang]; - // pre-fill all other languages - for (const j in langs) { - if (langs.hasOwnProperty(j)) { - langs[j][word] = langs[j][word] || EMPTY; - } - } - } - } - } - } - const keys = Object.keys(langs.en); - //keys.sort(); - for (const l in langs) { - if (!langs.hasOwnProperty(l)) continue; - let obj = {}; - for (let k = 0; k < keys.length; k++) { - obj[keys[k]] = langs[l][keys[k]]; - } - langs[l] = obj; - } - if (!fs.existsSync(src + 'i18n/')) { - fs.mkdirSync(src + 'i18n/'); - } - for (const ll in langs) { - if (!langs.hasOwnProperty(ll)) continue; - if (!fs.existsSync(src + 'i18n/' + ll)) { - fs.mkdirSync(src + 'i18n/' + ll); - } - fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); - } - fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); - } else { - console.error('Cannot read or parse ' + fileName); - } -} -function languagesFlat2words(src) { - const dirs = fs.readdirSync(src + 'i18n/'); - let langs = {}; - let bigOne = {}; - const order = Object.keys(languages); - dirs.sort((a, b) => { - const posA = order.indexOf(a); - const posB = order.indexOf(b); - if (posA === -1 && posB === -1) { - if (a > b) return 1; - if (a < b) return -1; - return 0; - } else if (posA === -1) { - return -1; - } else if (posB === -1) { - return 1; - } else { - if (posA > posB) return 1; - if (posA < posB) return -1; - return 0; - } - }); - const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); - - for (let l = 0; l < dirs.length; l++) { - if (dirs[l] === 'flat.txt') continue; - let lang = dirs[l]; - const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); - langs[lang] = {}; - keys.forEach(function (word, i) { - langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); - }); - - const words = langs[lang]; - for (const word in words) { - if (words.hasOwnProperty(word)) { - bigOne[word] = bigOne[word] || {}; - if (words[word] !== EMPTY) { - bigOne[word][lang] = words[word]; - } - } - } - } - // read actual words.js - const aWords = readWordJs(); - - const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; - if (aWords) { - // Merge words together - for (const w in aWords) { - if (aWords.hasOwnProperty(w)) { - if (!bigOne[w]) { - console.warn('Take from actual words.js: ' + w); - bigOne[w] = aWords[w] - } - dirs.forEach(function (lang) { - if (temporaryIgnore.indexOf(lang) !== -1) return; - if (!bigOne[w][lang]) { - console.warn('Missing "' + lang + '": ' + w); - } - }); - } - } - - } - - writeWordJs(bigOne, src); -} function languages2words(src) { const dirs = fs.readdirSync(src + 'i18n/'); - let langs = {}; - let bigOne = {}; + const langs = {}; + const bigOne = {}; const order = Object.keys(languages); - dirs.sort((a, b) => { + dirs.sort(function (a, b) { const posA = order.indexOf(a); const posB = order.indexOf(b); if (posA === -1 && posB === -1) { - if (a > b) return 1; - if (a < b) return -1; + if (a > b) + return 1; + if (a < b) + return -1; return 0; } else if (posA === -1) { return -1; } else if (posB === -1) { return 1; } else { - if (posA > posB) return 1; - if (posA < posB) return -1; + if (posA > posB) + return 1; + if (posA < posB) + return -1; return 0; } }); - for (let l = 0; l < dirs.length; l++) { - if (dirs[l] === 'flat.txt') continue; - const lang = dirs[l]; + for (const lang of dirs) { + if (lang === 'flat.txt') + continue; langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); langs[lang] = JSON.parse(langs[lang]); const words = langs[lang]; @@ -315,17 +180,18 @@ function languages2words(src) { // read actual words.js const aWords = readWordJs(); - const temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + const temporaryIgnore = ['flat.txt']; if (aWords) { // Merge words together for (const w in aWords) { if (aWords.hasOwnProperty(w)) { if (!bigOne[w]) { console.warn('Take from actual words.js: ' + w); - bigOne[w] = aWords[w] + bigOne[w] = aWords[w]; } dirs.forEach(function (lang) { - if (temporaryIgnore.indexOf(lang) !== -1) return; + if (temporaryIgnore.indexOf(lang) !== -1) + return; if (!bigOne[w][lang]) { console.warn('Missing "' + lang + '": ' + w); } @@ -338,37 +204,53 @@ function languages2words(src) { writeWordJs(bigOne, src); } -gulp.task('admin (words.js => json)', done => { +async function translateNotExisting(obj, baseText, yandex) { + let t = obj['en']; + if (!t) { + t = baseText; + } + + if (t) { + for (let l in languages) { + if (!obj[l]) { + const time = new Date().getTime(); + obj[l] = await translate(t, l, yandex); + console.log('en -> ' + l + ' ' + (new Date().getTime() - time) + ' ms'); + } + } + } +} + +//TASKS + +gulp.task('adminWords2languages', function (done) { words2languages('./admin/'); done(); }); -gulp.task('admin (words.js => flat)', done => { - words2languagesFlat('./admin/'); - done(); -}); - -gulp.task('admin (flat => words.js)', done => { - languagesFlat2words('./admin/'); - done(); -}); - -gulp.task('admin (json => words.js)', done => { +gulp.task('adminLanguages2words', function (done) { languages2words('./admin/'); done(); }); -gulp.task('updatePackages', done => { +gulp.task('updatePackages', function (done) { iopackage.common.version = pkg.version; iopackage.common.news = iopackage.common.news || {}; if (!iopackage.common.news[pkg.version]) { const news = iopackage.common.news; - let newNews = {}; + const newNews = {}; newNews[pkg.version] = { en: 'news', de: 'neues', - ru: 'новое' + ru: 'новое', + pt: 'novidades', + nl: 'nieuws', + fr: 'nouvelles', + it: 'notizie', + es: 'noticias', + pl: 'nowości', + 'zh-cn': '新' }; iopackage.common.news = Object.assign(newNews, news); } @@ -376,12 +258,12 @@ gulp.task('updatePackages', done => { done(); }); -gulp.task('updateReadme', done => { +gulp.task('updateReadme', function (done) { const readme = fs.readFileSync('README.md').toString(); const pos = readme.indexOf('## Changelog\n'); if (pos !== -1) { - let readmeStart = readme.substring(0, pos + '## Changelog\n'.length); - let readmeEnd = readme.substring(pos + '## Changelog\n'.length); + const readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + const readmeEnd = readme.substring(pos + '## Changelog\n'.length); if (readme.indexOf(version) === -1) { const timestamp = new Date(); @@ -400,4 +282,56 @@ gulp.task('updateReadme', done => { done(); }); +gulp.task('translate', async function (done) { + + let yandex; + const i = process.argv.indexOf('--yandex'); + if (i > -1) { + yandex = process.argv[i + 1]; + } + + if (iopackage && iopackage.common) { + if (iopackage.common.news) { + console.log('Translate News'); + for (let k in iopackage.common.news) { + console.log('News: ' + k); + let nw = iopackage.common.news[k]; + await translateNotExisting(nw, null, yandex); + } + } + if (iopackage.common.titleLang) { + console.log('Translate Title'); + await translateNotExisting(iopackage.common.titleLang, iopackage.common.title, yandex); + } + if (iopackage.common.desc) { + console.log('Translate Description'); + await translateNotExisting(iopackage.common.desc, null, yandex); + } + + if (fs.existsSync('./admin/i18n/en/translations.json')) { + let enTranslations = require('./admin/i18n/en/translations.json'); + for (let l in languages) { + console.log('Translate Text: ' + l); + let existing = {}; + if (fs.existsSync('./admin/i18n/' + l + '/translations.json')) { + existing = require('./admin/i18n/' + l + '/translations.json'); + } + for (let t in enTranslations) { + if (!existing[t]) { + existing[t] = await translate(enTranslations[t], l, yandex); + } + } + if (!fs.existsSync('./admin/i18n/' + l + '/')) { + fs.mkdirSync('./admin/i18n/' + l + '/'); + } + fs.writeFileSync('./admin/i18n/' + l + '/translations.json', JSON.stringify(existing, null, 4)); + } + } + + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); +}); + +gulp.task('translateAndUpdateWordsJS', gulp.series('translate', 'adminLanguages2words', 'adminWords2languages')); + gulp.task('default', gulp.series('updatePackages', 'updateReadme')); \ No newline at end of file diff --git a/io-package.json b/io-package.json index d8fc2a7..b63f716 100644 --- a/io-package.json +++ b/io-package.json @@ -24,7 +24,8 @@ "fr": "Connexion Home Assistant pour ioBroker", "it": "Connessione Home Assistant per ioBroker", "es": "Conexión de Home Assistant para ioBroker", - "pl": "Połączenie Home Assistant dla ioBroker" + "pl": "Połączenie Home Assistant dla ioBroker", + "zh-cn": "ioBroker 的家庭助理连接" }, "news": { "1.0.1": { @@ -60,7 +61,8 @@ "fr": "version initiale", "it": "versione iniziale", "es": "versión inicial", - "pl": "początkowa wersja" + "pl": "początkowa wersja", + "zh-cn": "初始版本" } }, "localLink": "http://%host%:8123", @@ -88,6 +90,11 @@ "js-controller": ">=2.0.0" } ], + "plugins": { + "sentry": { + "dsn": "https://1b2a1e8335f84cc49a67dd5ac2b07573@sentry.iobroker.net/182" + } + }, "connectionType": "local", "dataSource": "push", "tier": 2 diff --git a/lib/hass.js b/lib/hass.js index c9e75ed..854bbdb 100644 --- a/lib/hass.js +++ b/lib/hass.js @@ -92,7 +92,7 @@ function HASS(options, log) { } } - function callService(socket, service, domain, serviceData, callback) { + function callService(socket, service, domain, serviceData, target, callback) { if (socket && typeof socket.send === 'function') { const id = currentId++; requests[id] = {type: 'call_service', cb: callback, ts: Date.now()}; @@ -101,7 +101,8 @@ function HASS(options, log) { type: 'call_service', domain: domain || '', service: service, - service_data: serviceData + service_data: serviceData, + target })); } else { callback && callback('not connected'); @@ -228,11 +229,11 @@ function HASS(options, log) { } }; - this.callService = function (service, domain, serviceData, callback) { + this.callService = function (service, domain, serviceData, target, callback) { if (!connected) { typeof callback === 'function' && callback('not connected'); } else { - callService(this.socket, service, domain, serviceData, callback); + callService(this.socket, service, domain, serviceData, target, callback); } }; diff --git a/lib/tools.js b/lib/tools.js new file mode 100644 index 0000000..7798313 --- /dev/null +++ b/lib/tools.js @@ -0,0 +1,92 @@ +const axios = require("axios"); + +/** + * Tests whether the given variable is a real object and not an Array + * @param {any} it The variable to test + * @returns {it is Record} + */ +function isObject(it) { + // This is necessary because: + // typeof null === 'object' + // typeof [] === 'object' + // [] instanceof Object === true + return Object.prototype.toString.call(it) === "[object Object]"; +} + +/** + * Tests whether the given variable is really an Array + * @param {any} it The variable to test + * @returns {it is any[]} + */ +function isArray(it) { + if (Array.isArray != null) + return Array.isArray(it); + return Object.prototype.toString.call(it) === "[object Array]"; +} + +/** + * Choose the right tranalation API + * @param {string} text The text to translate + * @param {string} targetLang The target languate + * @param {string} yandex api key + * @returns {Promise} + */ +async function translateText(text, targetLang, yandex) { + if (targetLang === "en") { + return text; + } + if (yandex) { + return await translateYandex(text, targetLang, yandex); + } else { + return await translateGoogle(text, targetLang); + } +} + +/** + * Translates text with Yandex API + * @param {string} text The text to translate + * @param {string} targetLang The target languate + * @param {string} yandex api key + * @returns {Promise} + */ +async function translateYandex(text, targetLang, yandex) { + if (targetLang === "zh-cn") { + targetLang = "zh"; + } + try { + const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${yandex}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`; + const response = await axios({url, timeout: 15000}); + if (response.data && response.data['text']) { + return response.data['text'][0]; + } + throw new Error("Invalid response for translate request"); + } catch (e) { + throw new Error(`Could not translate to "${targetLang}": ${e}`); + } +} + +/** + * Translates text with Google API + * @param {string} text The text to translate + * @param {string} targetLang The target languate + * @returns {Promise} + */ +async function translateGoogle(text, targetLang) { + try { + const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}&ie=UTF-8&oe=UTF-8`; + const response = await axios({url, timeout: 15000}); + if (isArray(response.data)) { + // we got a valid response + return response.data[0][0][0]; + } + throw new Error("Invalid response for translate request"); + } catch (e) { + throw new Error(`Could not translate to "${targetLang}": ${e}`); + } +} + +module.exports = { + isArray, + isObject, + translateText +}; \ No newline at end of file diff --git a/main.js b/main.js index cb8a97a..9248026 100644 --- a/main.js +++ b/main.js @@ -12,6 +12,8 @@ let connected = false; let hass; let adapter; const hassObjects = {}; +let delayTimeout = null; +let stopped = false; function startAdapter(options) { options = options || {}; @@ -35,6 +37,17 @@ function startAdapter(options) { } else { const serviceData = {}; const fields = hassObjects[id].native.fields; + const target = {}; + + let requestFields = {}; + if (typeof state.val === 'string') { + try { + requestFields = JSON.parse(state.val); + } catch (err) { + adapter.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`); + requestFields = {}; + } + } for (const field in fields) { if (!fields.hasOwnProperty(field)) { @@ -42,15 +55,15 @@ function startAdapter(options) { } if (field === 'entity_id') { - serviceData.entity_id = hassObjects[id].native.entity_id - } else { - serviceData[field] = state.val; + target.entity_id = hassObjects[id].native.entity_id + } else if (requestFields[field] !== undefined) { + serviceData[field] = requestFields[field]; } } serviceData.entity_id = hassObjects[id].native.entity_id adapter.log.debug(`Send to HASS for service ${hassObjects[id].native.attr} with ${hassObjects[id].native.domain || hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`) - hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, err => + hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, target, err => err && adapter.log.error('Cannot control ' + id + ': ' + err)); } } @@ -65,6 +78,8 @@ function startAdapter(options) { } function stop(callback) { + stopped = true; + delayTimeout && clearTimeout(delayTimeout); hass && hass.close(); callback && callback(); } @@ -318,7 +333,8 @@ function parseStates(entities, services, callback) { common: { desc: service[s].description, read: false, - write: true + write: true, + type: 'mixed' }, native: { object_id: entity.object_id, @@ -386,14 +402,22 @@ function main() { return; } //adapter.log.debug(JSON.stringify(config)); - setTimeout(() => - hass.getStates((err, states) => { + delayTimeout = setTimeout(() => { + delayTimeout = null; + !stopped && hass.getStates((err, states) => { + if (stopped) { + return; + } if (err) { return adapter.log.error('Cannot read states: ' + err); } //adapter.log.debug(JSON.stringify(states)); - setTimeout(() => - hass.getServices((err, services) => { + delayTimeout = setTimeout(() => { + delayTimeout = null; + !stopped && hass.getServices((err, services) => { + if (stopped) { + return; + } if (err) { adapter.log.error('Cannot read states: ' + err); } else { @@ -403,8 +427,8 @@ function main() { adapter.subscribeStates('*'); }); } - }), 100); - }), 100); + })}, 100); + })}, 100); }); } }); diff --git a/package.json b/package.json index 8c42146..4ad5bb7 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,21 @@ "@iobroker/adapter-core": "^2.6.0" }, "devDependencies": { - "@alcalzone/release-script": "^2.2.1", + "@alcalzone/release-script": "^3.5.6", + "@alcalzone/release-script-plugin-iobroker": "^3.5.7", + "@alcalzone/release-script-plugin-license": "^3.5.3", + "axios": "^0.26.1", "gulp": "^4.0.2", - "mocha": "^9.2.1", + "mocha": "^9.2.2", "chai": "^4.3.6" }, "main": "main.js", "scripts": { "test": "node node_modules/mocha/bin/mocha --exit", - "release": "node node_modules/@alcalzone/release-script/bin/release.js" + "release": "release-script", + "release-patch": "release-script patch --yes", + "release-minor": "release-script minor --yes", + "release-major": "release-script major --yes" }, "bugs": { "url": "https://github.com/ioBroker/ioBroker.hass/issues"