Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d39cc9d1d | ||
|
|
c0defc48a5 | ||
|
|
d525d5b26b | ||
|
|
a64ef243f0 | ||
|
|
9a24c2be91 | ||
|
|
d541114da1 | ||
|
|
eb7b8b0e68 | ||
|
|
e139c9bc27 | ||
|
|
40550a747b | ||
|
|
9a886b1155 | ||
|
|
039e856f7a | ||
|
|
06cdfbd9e4 | ||
|
|
e92d488529 | ||
|
|
d5f874cf84 | ||
|
|
952680454b | ||
|
|
c4fb2d37aa | ||
|
|
1f06d5afac | ||
|
|
a64edae70c | ||
|
|
477e910371 | ||
|
|
959a5f7a3f | ||
|
|
90f53d8ce8 | ||
|
|
360baf112e | ||
|
|
164a8c6bee | ||
|
|
89a588b73d | ||
|
|
36f296ee8f | ||
|
|
b9e92864a0 | ||
|
|
906e6d172c | ||
|
|
2f63e3a6dd | ||
|
|
bc74169c7e | ||
|
|
fbabfaae94 | ||
|
|
8b6e297800 | ||
|
|
1ed00c8778 | ||
|
|
d1442504e8 | ||
|
|
2ca9828262 | ||
|
|
2e78c1f42b | ||
|
|
8dbde9bbcb | ||
|
|
1b6d9bbeea |
+14
-10
@@ -1,16 +1,20 @@
|
|||||||
|
# Dependabot will run on day 17 of each month at 02:15 (Europe/Berlin timezone)
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: github-actions
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: "cron"
|
||||||
time: "04:00"
|
timezone: "Europe/Berlin"
|
||||||
timezone: Europe/Berlin
|
cronjob: "15 2 17 * *"
|
||||||
- package-ecosystem: npm
|
open-pull-requests-limit: 15
|
||||||
|
|
||||||
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: "cron"
|
||||||
time: "04:00"
|
timezone: "Europe/Berlin"
|
||||||
timezone: Europe/Berlin
|
cronjob: "15 2 17 * *"
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 15
|
||||||
versioning-strategy: increase
|
versioning-strategy: "increase"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Workflow for auto-merging Dependabot PRs
|
||||||
|
# This workflow uses the action-automerge-dependabot action to automatically merge
|
||||||
|
# Dependabot PRs based on the rules defined in .github/auto-merge.yml
|
||||||
|
|
||||||
|
name: Auto-Merge Dependabot PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Trigger when a PR is opened or updated
|
||||||
|
# WARNING: This needs to be run in the PR base, DO NOT build untrusted code in this action
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if actor is dependabot
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Auto-merge Dependabot PRs
|
||||||
|
uses: iobroker-bot-orga/action-automerge-dependabot@v1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Optional: Path to your auto-merge configuration file
|
||||||
|
# config-file-path: '.github/auto-merge.yml'
|
||||||
|
# Optional: Merge method (merge, squash, or rebase)
|
||||||
|
# merge-method: 'squash'
|
||||||
|
# Optional: Wait for other checks to complete
|
||||||
|
# wait-for-checks: 'true'
|
||||||
|
# Optional: Maximum time to wait for checks in seconds (default: 3600)
|
||||||
|
# max-wait-time: '3600'
|
||||||
@@ -24,18 +24,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{ matrix.language }}"
|
category: "/language:${{ matrix.language }}"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: ioBroker/testing-action-check@v1
|
- uses: ioBroker/testing-action-check@v1
|
||||||
with:
|
with:
|
||||||
node-version: '22.x'
|
node-version: '24.x'
|
||||||
lint: true
|
lint: true
|
||||||
|
|
||||||
adapter-tests:
|
adapter-tests:
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
extra-tests: npm run test:js
|
build: true
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: [check-and-lint, adapter-tests]
|
needs: [check-and-lint, adapter-tests]
|
||||||
@@ -67,5 +67,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: ioBroker/testing-action-deploy@v1
|
- uses: ioBroker/testing-action-deploy@v1
|
||||||
with:
|
with:
|
||||||
node-version: '22.x'
|
node-version: '24.x'
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build: true
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ admin/i18n/flat.txt
|
|||||||
admin/i18n/*/flat.txt
|
admin/i18n/*/flat.txt
|
||||||
iob_npm.done
|
iob_npm.done
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
#ignore .commitinfo created by ioBroker release script
|
||||||
|
.commitinfo
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
|||||||
@@ -106,8 +106,12 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
### 2.0.0 (2026-03-31)
|
### 2.0.4 (2026-05-05)
|
||||||
|
* (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS
|
||||||
|
|
||||||
|
### 2.0.3 (2026-04-02)
|
||||||
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
|
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
|
||||||
|
* (@Titanium177) Added roles for states and added debouncing for reading states from hass
|
||||||
|
|
||||||
### 1.4.0 (2023-01-03)
|
### 1.4.0 (2023-01-03)
|
||||||
* (Apollon77) Added more guidance logging when setting services incorrectly
|
* (Apollon77) Added more guidance logging when setting services incorrectly
|
||||||
|
|||||||
+30
-27
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"name": "hass",
|
"name": "hass",
|
||||||
"version": "2.0.0",
|
"version": "2.0.4",
|
||||||
"title": "Home Assistant",
|
"title": "Home Assistant",
|
||||||
"titleLang": {
|
"titleLang": {
|
||||||
"en": "Home Assistant",
|
"en": "Home Assistant",
|
||||||
@@ -30,18 +30,31 @@
|
|||||||
"uk": "Підключення Home Assistant для ioBroker"
|
"uk": "Підключення Home Assistant для ioBroker"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"2.0.0": {
|
"2.0.4": {
|
||||||
"en": "Adapter was updated and migrated to TypeScript",
|
"en": "Tried to keep the custom settings of the objects when updating them with new data from HASS",
|
||||||
"de": "Adapter wurde aktualisiert und auf TypeScript migriert",
|
"de": "Versuche, die benutzerdefinierten Einstellungen der Objekte bei der Aktualisierung mit neuen Daten von HASS zu halten",
|
||||||
"ru": "Адаптер был обновлен и перенесен на TypeScript",
|
"ru": "Пытался сохранить пользовательские настройки объектов при обновлении их новыми данными из HASS",
|
||||||
"pt": "O adaptador foi atualizado e migrado para TypeScript",
|
"pt": "Tentou manter as configurações personalizadas dos objetos ao atualizá-los com novos dados do HASS",
|
||||||
"nl": "Adapter is bijgewerkt en gemigreerd naar TypeScript",
|
"nl": "Geprobeerd om de aangepaste instellingen van de objecten te behouden bij het bijwerken ervan met nieuwe gegevens van HASS",
|
||||||
"fr": "Adaptateur a été mis à jour et migré vers TypeScript",
|
"fr": "Essai de conserver les paramètres personnalisés des objets lors de leur mise à jour avec de nouvelles données de HASS",
|
||||||
"it": "L'adattatore è stato aggiornato e migrato a TypeScript",
|
"it": "Ho cercato di mantenere le impostazioni personalizzate degli oggetti durante l'aggiornamento con nuovi dati da HASS",
|
||||||
"es": "Adaptador fue actualizado y migrado a TipoScript",
|
"es": "Trató de mantener la configuración personalizada de los objetos al actualizarlos con nuevos datos de HASS",
|
||||||
"pl": "Adapter został zaktualizowany i przeniesiony do TypeScript",
|
"pl": "Próbował zachować niestandardowe ustawienia obiektów podczas ich aktualizacji nowymi danymi z HASS",
|
||||||
"uk": "Адаптер був оновлений і мігрований до TypeScript",
|
"uk": "При оновленні нових даних від HASS",
|
||||||
"zh-cn": "适应器被更新并迁移到 TypeScript"
|
"zh-cn": "在用 HASS 的新数据更新对象时尝试保留对象的自定义设置"
|
||||||
|
},
|
||||||
|
"2.0.3": {
|
||||||
|
"en": "Adapter was updated and migrated to TypeScript\nAdded roles for states and added debouncing for reading states from hass",
|
||||||
|
"de": "Adapter wurde aktualisiert und auf TypeScript migriert\nHinzugefügt Rollen für Staaten und hinzugefügt Debouncing für Lesezustände aus hass",
|
||||||
|
"ru": "Адаптер был обновлен и перенесен на TypeScript\nДобавлены роли для штатов и добавлены отговорки для чтения штатов из хэша",
|
||||||
|
"pt": "O adaptador foi atualizado e migrado para TypeScript\nAdicionado papéis para estados e debuncing adicionado para estados de leitura de hass",
|
||||||
|
"nl": "Adapter is bijgewerkt en gemigreerd naar TypeScript\nToegevoegde rollen voor staten en toegevoegd debouncing voor het lezen van staten uit hass",
|
||||||
|
"fr": "Adaptateur a été mis à jour et migré vers TypeScript\nAjout de rôles pour les états et ajout de débonflage pour les états de lecture de hass",
|
||||||
|
"it": "L'adattatore è stato aggiornato e migrato a TypeScript\nAggiunti i ruoli per gli stati e aggiunto debouncing per la lettura di stati da hass",
|
||||||
|
"es": "Adaptador fue actualizado y migrado a TipoScript\nFunciones adicionales para los estados y desembolsos añadidos para la lectura de estados de hass",
|
||||||
|
"pl": "Adapter został zaktualizowany i przeniesiony do TypeScript\nDodano role dla państw i dodano debouncing do czytania stanów z hass",
|
||||||
|
"uk": "Адаптер був оновлений і мігрований до TypeScript\nДодано ролі для штатів і додано деблінг для читання станів з",
|
||||||
|
"zh-cn": "适应器被更新并迁移到 TypeScript\n添加状态角色, 添加读状态的解跳"
|
||||||
},
|
},
|
||||||
"1.4.0": {
|
"1.4.0": {
|
||||||
"en": "Added more guidance logging when setting services incorrectly\nPrevent crashes when attributes contain \".\" at the end of their names\nAdded logging for state updates for unknown objects",
|
"en": "Added more guidance logging when setting services incorrectly\nPrevent crashes when attributes contain \".\" at the end of their names\nAdded logging for state updates for unknown objects",
|
||||||
@@ -107,19 +120,6 @@
|
|||||||
"pl": "Pokaż ponownie zamaskowane pola haseł w konfiguracji",
|
"pl": "Pokaż ponownie zamaskowane pola haseł w konfiguracji",
|
||||||
"zh-cn": "在配置中再次显示被屏蔽的密码字段",
|
"zh-cn": "在配置中再次显示被屏蔽的密码字段",
|
||||||
"uk": "Знову показати замасковані поля пароля в конфігурації"
|
"uk": "Знову показати замасковані поля пароля в конфігурації"
|
||||||
},
|
|
||||||
"1.1.0": {
|
|
||||||
"en": "IMPORTANT: You need to re-enter the password once after installing this version!\nImplement Service triggers to use any value to trigger or stringified JSON to call with fields\nOptimize unload handling\nAdd Sentry for crash reporting",
|
|
||||||
"de": "WICHTIG: Nach der Installation dieser Version müssen Sie das Passwort einmalig neu eingeben!\nImplementieren Sie Dienstauslöser, um einen beliebigen Wert zum Auslösen oder stringifiziertes JSON zum Aufrufen mit Feldern zu verwenden\nEntladehandling optimieren\nFügen Sie Sentry für Absturzberichte hinzu",
|
|
||||||
"ru": "ВАЖНО: Вам необходимо повторно ввести пароль один раз после установки этой версии!\nРеализовать триггеры службы, чтобы использовать любое значение для запуска или строкового JSON для вызова с полями.\nОптимизация обработки разгрузки\nДобавьте Sentry для отчетов о сбоях",
|
|
||||||
"pt": "IMPORTANTE: Você precisa redigitar a senha uma vez depois de instalar esta versão!\nImplemente gatilhos de serviço para usar qualquer valor para acionar ou JSON stringified para chamar com campos\nOtimize o manuseio de descarga\nAdicionar Sentinela para relatórios de falhas",
|
|
||||||
"nl": "BELANGRIJK: u moet het wachtwoord één keer opnieuw invoeren na het installeren van deze versie!\nImplementeer servicetriggers om elke waarde te gebruiken om te activeren of stringified JSON om met velden aan te roepen\nOptimaliseer de losverwerking\nSentry toevoegen voor crashrapportage",
|
|
||||||
"fr": "IMPORTANT : Vous devez ressaisir le mot de passe une fois après l'installation de cette version !\nImplémentez des déclencheurs de service pour utiliser n'importe quelle valeur pour déclencher ou JSON stringifié pour appeler avec des champs\nOptimiser la gestion des déchargements\nAjouter Sentry pour les rapports de plantage",
|
|
||||||
"it": "IMPORTANTE: è necessario reinserire la password una volta dopo aver installato questa versione!\nImplementa i trigger del servizio per utilizzare qualsiasi valore per attivare o JSON in formato stringa per chiamare con i campi\nOttimizza la gestione dello scarico\nAggiungi Sentinella per la segnalazione degli arresti anomali",
|
|
||||||
"es": "IMPORTANTE: ¡Debe volver a ingresar la contraseña una vez después de instalar esta versión!\nImplementar disparadores de servicio para usar cualquier valor para disparar o JSON en cadena para llamar con campos\nOptimizar el manejo de descarga\nAgregar Sentry para informes de fallas",
|
|
||||||
"pl": "WAŻNE: Po zainstalowaniu tej wersji należy raz ponownie wprowadzić hasło!\nZaimplementuj wyzwalacze usługi, aby używać dowolnej wartości do wyzwalania lub skróconego JSON do wywołania z polami\nZoptymalizuj obsługę rozładunku\nDodaj Sentry do zgłaszania awarii",
|
|
||||||
"zh-cn": "重要提示:安装此版本后需要重新输入一次密码!\n实现服务触发器以使用任何值触发或字符串化 JSON 以使用字段调用\n优化卸载处理\n添加 Sentry 以进行崩溃报告",
|
|
||||||
"uk": "ВАЖЛИВО: після встановлення цієї версії потрібно повторно ввести пароль!\nРеалізуйте тригери служби, щоб використовувати будь-яке значення для запуску або рядковий JSON для виклику з полями\nОптимізуйте роботу з розвантаженням\nДодайте Sentry для звітування про збої"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"localLink": "http://%host%:8123",
|
"localLink": "http://%host%:8123",
|
||||||
@@ -131,7 +131,10 @@
|
|||||||
"adminUI": {
|
"adminUI": {
|
||||||
"config": "json"
|
"config": "json"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"licenseInformation": {
|
||||||
|
"type": "free",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png",
|
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"HASS",
|
"HASS",
|
||||||
|
|||||||
Generated
+23
-27
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.0",
|
"version": "2.0.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.0",
|
"version": "2.0.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iobroker/adapter-core": "^3.3.2",
|
"@iobroker/adapter-core": "^3.3.2",
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
"@iobroker/eslint-config": "^2.2.0",
|
"@iobroker/eslint-config": "^2.2.0",
|
||||||
"@iobroker/legacy-testing": "^2.0.2",
|
"@iobroker/legacy-testing": "^2.0.2",
|
||||||
"@iobroker/testing": "^5.2.2",
|
"@iobroker/testing": "^5.2.2",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1"
|
||||||
"mocha": "^11.7.5"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alcalzone/esbuild-register": {
|
"node_modules/@alcalzone/esbuild-register": {
|
||||||
@@ -2578,13 +2577,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.14.0",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||||
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^2.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -4442,9 +4440,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4452,7 +4450,6 @@
|
|||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
},
|
},
|
||||||
@@ -6649,12 +6646,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
|
||||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
"@protobufjs/base64": "^1.1.2",
|
"@protobufjs/base64": "^1.1.2",
|
||||||
@@ -9723,12 +9719,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "1.14.0",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||||
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^2.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -11000,9 +10996,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"for-each": {
|
"for-each": {
|
||||||
@@ -12418,9 +12414,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protobufjs": {
|
"protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
|
||||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "iobroker.hass",
|
"name": "iobroker.hass",
|
||||||
"version": "2.0.0",
|
"version": "2.0.4",
|
||||||
"description": "Home Assistant",
|
"description": "Home Assistant",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "bluefox",
|
"name": "bluefox",
|
||||||
@@ -37,8 +37,7 @@
|
|||||||
"@iobroker/eslint-config": "^2.2.0",
|
"@iobroker/eslint-config": "^2.2.0",
|
||||||
"@iobroker/legacy-testing": "^2.0.2",
|
"@iobroker/legacy-testing": "^2.0.2",
|
||||||
"@iobroker/testing": "^5.2.2",
|
"@iobroker/testing": "^5.2.2",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1"
|
||||||
"mocha": "^11.7.5"
|
|
||||||
},
|
},
|
||||||
"main": "build/main.js",
|
"main": "build/main.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
+395
-85
@@ -8,6 +8,26 @@ interface HassAdapterConfig {
|
|||||||
secure: boolean;
|
secure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HassEntity {
|
||||||
|
name: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
entity_id: string;
|
||||||
|
object_id: string;
|
||||||
|
last_changed?: string;
|
||||||
|
last_updated?: string;
|
||||||
|
state: ioBroker.StateValue;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HassServices {
|
||||||
|
[domain: string]: {
|
||||||
|
[serviceName: string]: {
|
||||||
|
description: string;
|
||||||
|
fields: Record<string, any>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const knownAttributes: Record<string, { write: boolean; read: boolean; unit: string }> = {
|
const knownAttributes: Record<string, { write: boolean; read: boolean; unit: string }> = {
|
||||||
azimuth: { write: false, read: true, unit: '°' },
|
azimuth: { write: false, read: true, unit: '°' },
|
||||||
elevation: { write: false, read: true, unit: '°' },
|
elevation: { write: false, read: true, unit: '°' },
|
||||||
@@ -22,6 +42,143 @@ const mapTypes: Record<string, ioBroker.CommonType> = {
|
|||||||
|
|
||||||
const skipServices: string[] = ['persistent_notification'];
|
const skipServices: string[] = ['persistent_notification'];
|
||||||
|
|
||||||
|
function getRoleForState(entity: HassEntity): string {
|
||||||
|
const domain = entity.domain || entity.entity_id.split('.')[0];
|
||||||
|
const state = entity.state;
|
||||||
|
|
||||||
|
switch (domain) {
|
||||||
|
case 'light':
|
||||||
|
return 'switch';
|
||||||
|
case 'switch':
|
||||||
|
return 'switch';
|
||||||
|
case 'binary_sensor':
|
||||||
|
return 'sensor.binary';
|
||||||
|
case 'sensor':
|
||||||
|
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
|
||||||
|
if (entity.attributes?.unit_of_measurement) {
|
||||||
|
const unit = entity.attributes.unit_of_measurement;
|
||||||
|
if (unit === '°C' || unit === '°F' || unit === 'K') {
|
||||||
|
return 'value.temperature';
|
||||||
|
}
|
||||||
|
if (unit === '%') {
|
||||||
|
return 'value.humidity';
|
||||||
|
}
|
||||||
|
if (unit === 'hPa' || unit === 'mbar') {
|
||||||
|
return 'value.pressure';
|
||||||
|
}
|
||||||
|
if (unit === 'W' || unit === 'kW') {
|
||||||
|
return 'value.power';
|
||||||
|
}
|
||||||
|
if (unit === 'V') {
|
||||||
|
return 'value.voltage';
|
||||||
|
}
|
||||||
|
if (unit === 'A') {
|
||||||
|
return 'value.current';
|
||||||
|
}
|
||||||
|
if (unit.indexOf('m/s') !== -1 || unit.indexOf('km/h') !== -1) {
|
||||||
|
return 'value.speed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'value';
|
||||||
|
}
|
||||||
|
return 'text';
|
||||||
|
case 'climate':
|
||||||
|
return 'thermostat';
|
||||||
|
case 'cover':
|
||||||
|
return 'blind';
|
||||||
|
case 'lock':
|
||||||
|
return 'state';
|
||||||
|
case 'input_boolean':
|
||||||
|
return 'switch';
|
||||||
|
case 'input_number':
|
||||||
|
return 'level';
|
||||||
|
case 'input_text':
|
||||||
|
return 'text';
|
||||||
|
case 'input_select':
|
||||||
|
return 'text';
|
||||||
|
case 'media_player':
|
||||||
|
return 'media.state';
|
||||||
|
case 'device_tracker':
|
||||||
|
return 'state';
|
||||||
|
case 'scene':
|
||||||
|
return 'button';
|
||||||
|
case 'script':
|
||||||
|
return 'button';
|
||||||
|
case 'automation':
|
||||||
|
return 'switch';
|
||||||
|
case 'vacuum':
|
||||||
|
return 'state';
|
||||||
|
case 'weather':
|
||||||
|
return 'weather';
|
||||||
|
default:
|
||||||
|
if (state === 'on' || state === 'off') {
|
||||||
|
return 'switch';
|
||||||
|
}
|
||||||
|
if (typeof state === 'number' || !isNaN(parseFloat(String(state)))) {
|
||||||
|
return 'value';
|
||||||
|
}
|
||||||
|
if (typeof state === 'boolean') {
|
||||||
|
return 'indicator';
|
||||||
|
}
|
||||||
|
return 'state';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleForAttribute(attr: string, value: ioBroker.StateValue, type: ioBroker.CommonType): string {
|
||||||
|
const attrLower = attr.toLowerCase();
|
||||||
|
if (attrLower.includes('temperature')) {
|
||||||
|
return 'value.temperature';
|
||||||
|
}
|
||||||
|
if (attrLower.includes('humidity')) {
|
||||||
|
return 'value.humidity';
|
||||||
|
}
|
||||||
|
if (attrLower.includes('pressure')) {
|
||||||
|
return 'value.pressure';
|
||||||
|
}
|
||||||
|
if (attrLower === 'brightness' || attrLower === 'current_position') {
|
||||||
|
return 'level.dimmer';
|
||||||
|
}
|
||||||
|
if (attrLower === 'rgb_color' || attrLower === 'xy_color') {
|
||||||
|
return 'level.color.rgb';
|
||||||
|
}
|
||||||
|
if (attrLower === 'color_temp') {
|
||||||
|
return 'level.color.temperature';
|
||||||
|
}
|
||||||
|
if (attrLower === 'battery_level' || attrLower === 'battery') {
|
||||||
|
return 'value.battery';
|
||||||
|
}
|
||||||
|
if (attrLower === 'locked') {
|
||||||
|
return 'indicator';
|
||||||
|
}
|
||||||
|
if (attrLower === 'volume_level') {
|
||||||
|
return 'level.volume';
|
||||||
|
}
|
||||||
|
if (attrLower === 'position') {
|
||||||
|
return 'level';
|
||||||
|
}
|
||||||
|
if (attrLower === 'speed' || attrLower === 'percentage') {
|
||||||
|
return 'level';
|
||||||
|
}
|
||||||
|
if (attrLower === 'mode' || attrLower === 'preset_mode') {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
return 'value';
|
||||||
|
case 'boolean':
|
||||||
|
return 'indicator';
|
||||||
|
case 'string':
|
||||||
|
return 'text';
|
||||||
|
case 'object':
|
||||||
|
case 'mixed':
|
||||||
|
case 'array':
|
||||||
|
return 'json';
|
||||||
|
default:
|
||||||
|
return 'state';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class HassAdapter extends Adapter {
|
class HassAdapter extends Adapter {
|
||||||
declare config: HassAdapterConfig;
|
declare config: HassAdapterConfig;
|
||||||
|
|
||||||
@@ -29,6 +186,7 @@ class HassAdapter extends Adapter {
|
|||||||
private hass: HASS | null = null;
|
private hass: HASS | null = null;
|
||||||
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
|
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
|
||||||
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
|
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private syncDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
private stopped: boolean = false;
|
private stopped: boolean = false;
|
||||||
|
|
||||||
public constructor(options: Partial<AdapterOptions> = {}) {
|
public constructor(options: Partial<AdapterOptions> = {}) {
|
||||||
@@ -41,6 +199,29 @@ class HassAdapter extends Adapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private debouncedSync(callback?: () => void): void {
|
||||||
|
if (this.syncDebounceTimeout) {
|
||||||
|
clearTimeout(this.syncDebounceTimeout);
|
||||||
|
}
|
||||||
|
this.syncDebounceTimeout = setTimeout(() => {
|
||||||
|
this.syncDebounceTimeout = null;
|
||||||
|
this.hass!.getStates((err, states) => {
|
||||||
|
if (err) {
|
||||||
|
this.log.error(`Cannot read states during resync: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hass!.getServices(async (err, services) => {
|
||||||
|
if (err) {
|
||||||
|
this.log.error(`Cannot read services during resync: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.parseStates(states, services);
|
||||||
|
callback?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
|
private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
|
||||||
if (!state || state.ack) {
|
if (!state || state.ack) {
|
||||||
return;
|
return;
|
||||||
@@ -59,8 +240,35 @@ class HassAdapter extends Adapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle boolean state toggle
|
||||||
|
if (id.endsWith('.state_boolean')) {
|
||||||
|
const entityId = this.hassObjects[id].native.entity_id;
|
||||||
|
const domain = entityId
|
||||||
|
? entityId.split('.')[0]
|
||||||
|
: this.hassObjects[id].native.domain || this.hassObjects[id].native.type;
|
||||||
|
const service = state.val ? 'turn_on' : 'turn_off';
|
||||||
|
|
||||||
|
this.log.debug(`Processing boolean state change for ${id}`);
|
||||||
|
this.log.debug(
|
||||||
|
`Domain: ${domain}, Entity: ${this.hassObjects[id].native.entity_id}, Service: ${service}, Value: ${state.val}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
const serviceData = { entity_id: this.hassObjects[id].native.entity_id };
|
||||||
|
this.hass!.callService(service, domain, serviceData, {}, err => {
|
||||||
|
if (err) {
|
||||||
|
this.log.error(`Cannot control ${id}: ${err}`);
|
||||||
|
} else {
|
||||||
|
this.log.debug(`Successfully sent command to HASS for ${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log.warn(`No domain found for ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
const serviceData: Record<string, any> = {};
|
const serviceData: Record<string, any> = {};
|
||||||
const fields: { entity_id: string } = this.hassObjects[id].native.fields;
|
const fields: Record<string, any> = this.hassObjects[id].native.fields;
|
||||||
const target: Record<string, any> = {};
|
const target: Record<string, any> = {};
|
||||||
|
|
||||||
let requestFields: Record<string, any> = {};
|
let requestFields: Record<string, any> = {};
|
||||||
@@ -70,7 +278,7 @@ class HassAdapter extends Adapter {
|
|||||||
try {
|
try {
|
||||||
requestFields = JSON.parse(state.val) || {};
|
requestFields = JSON.parse(state.val) || {};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
|
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${(err as Error).message}`);
|
||||||
requestFields = {};
|
requestFields = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +343,10 @@ class HassAdapter extends Adapter {
|
|||||||
clearTimeout(this.delayTimeout);
|
clearTimeout(this.delayTimeout);
|
||||||
this.delayTimeout = null;
|
this.delayTimeout = null;
|
||||||
}
|
}
|
||||||
|
if (this.syncDebounceTimeout) {
|
||||||
|
clearTimeout(this.syncDebounceTimeout);
|
||||||
|
this.syncDebounceTimeout = null;
|
||||||
|
}
|
||||||
this.hass?.close();
|
this.hass?.close();
|
||||||
callback?.();
|
callback?.();
|
||||||
}
|
}
|
||||||
@@ -150,13 +362,17 @@ class HassAdapter extends Adapter {
|
|||||||
try {
|
try {
|
||||||
await this.setForeignStateAsync(id, state);
|
await this.setForeignStateAsync(id, state);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err.toString());
|
this.log.error((err as Error).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<void> {
|
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<{
|
||||||
|
newCount: number;
|
||||||
|
updatedCount: number;
|
||||||
|
}> {
|
||||||
|
const stats = { newCount: 0, updatedCount: 0 };
|
||||||
if (objects?.length) {
|
if (objects?.length) {
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
this.hassObjects[obj._id] = obj;
|
this.hassObjects[obj._id] = obj;
|
||||||
@@ -167,36 +383,77 @@ class HassAdapter extends Adapter {
|
|||||||
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
|
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
|
||||||
this.hassObjects[obj._id] = obj;
|
this.hassObjects[obj._id] = obj;
|
||||||
await this.setForeignObjectAsync(obj._id, obj);
|
await this.setForeignObjectAsync(obj._id, obj);
|
||||||
|
stats.newCount++;
|
||||||
} else {
|
} else {
|
||||||
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
|
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
|
||||||
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
|
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
|
||||||
oldObj.native = obj.native;
|
oldObj.native = obj.native;
|
||||||
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
|
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
|
||||||
await this.setForeignObjectAsync(obj._id, oldObj);
|
await this.setForeignObjectAsync(obj._id, oldObj);
|
||||||
|
stats.updatedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err.toString());
|
this.log.error((err as Error).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseStates(
|
private async deleteStaleObjects(expectedObjects: Set<string>): Promise<number> {
|
||||||
entities: {
|
const objectsToDelete: string[] = [];
|
||||||
name: string;
|
let knownCount = 0;
|
||||||
attributes: { friendly_name?: string; attribution?: string; unit_of_measurement?: string };
|
for (const id in this.hassObjects) {
|
||||||
entity_id: string;
|
if (
|
||||||
object_id: string;
|
Object.prototype.hasOwnProperty.call(this.hassObjects, id) &&
|
||||||
last_changed?: string;
|
id.startsWith(`${this.namespace}.entities.`)
|
||||||
last_updated?: string;
|
) {
|
||||||
state: ioBroker.StateValue;
|
knownCount++;
|
||||||
domain: string;
|
if (!expectedObjects.has(id)) {
|
||||||
}[],
|
objectsToDelete.push(id);
|
||||||
services: Record<string, { [serviceType: string]: { description: string; fields: { entity_id: string } } }>,
|
}
|
||||||
): Promise<void> {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity guard: if HASS returned a drastically smaller entity set than what we
|
||||||
|
// know (e.g. mid-startup after a restart), assume the data is incomplete and
|
||||||
|
// skip deletion. The next sync will retry.
|
||||||
|
if (knownCount > 10 && objectsToDelete.length > knownCount / 2) {
|
||||||
|
this.log.warn(
|
||||||
|
`Skipping deletion of ${objectsToDelete.length}/${knownCount} stale objects — HASS likely returned an incomplete state list. Will retry on next sync.`,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deletedCount = 0;
|
||||||
|
for (const id of objectsToDelete) {
|
||||||
|
try {
|
||||||
|
// Never auto-delete objects that hold user configuration like
|
||||||
|
// common.custom (history/influxdb/sql adapter settings) — losing
|
||||||
|
// those silently on a transient HASS hiccup would force the user
|
||||||
|
// to recreate them. See issue #165.
|
||||||
|
const existing = await this.getForeignObjectAsync(id);
|
||||||
|
const custom = (existing?.common as { custom?: Record<string, unknown> } | undefined)?.custom;
|
||||||
|
if (custom && Object.keys(custom).length) {
|
||||||
|
this.log.debug(`Keeping "${id}" despite being stale: object holds custom adapter configuration`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await this.delObjectAsync(id);
|
||||||
|
delete this.hassObjects[id];
|
||||||
|
deletedCount++;
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error(`Error deleting object ${id}: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseStates(entities: HassEntity[], services: HassServices): Promise<void> {
|
||||||
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
|
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
|
||||||
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
|
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
|
||||||
|
const expectedObjects = new Set<string>();
|
||||||
|
|
||||||
for (let e = 0; e < entities.length; e++) {
|
for (let e = 0; e < entities.length; e++) {
|
||||||
const entity = entities[e];
|
const entity = entities[e];
|
||||||
@@ -207,8 +464,11 @@ class HassAdapter extends Adapter {
|
|||||||
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
|
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
|
||||||
const desc = entity.attributes?.attribution || undefined;
|
const desc = entity.attributes?.attribution || undefined;
|
||||||
|
|
||||||
|
const channelId = `${this.namespace}.entities.${entity.entity_id}`;
|
||||||
|
expectedObjects.add(channelId);
|
||||||
|
|
||||||
const channel: ioBroker.ChannelObject = {
|
const channel: ioBroker.ChannelObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}`,
|
_id: channelId,
|
||||||
common: {
|
common: {
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
@@ -227,13 +487,16 @@ class HassAdapter extends Adapter {
|
|||||||
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
|
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
|
||||||
|
|
||||||
if (entity.state !== undefined) {
|
if (entity.state !== undefined) {
|
||||||
|
const stateId = `${channelId}.state`;
|
||||||
|
expectedObjects.add(stateId);
|
||||||
|
|
||||||
const obj: ioBroker.StateObject = {
|
const obj: ioBroker.StateObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.state`,
|
_id: stateId,
|
||||||
type: 'state',
|
type: 'state',
|
||||||
common: {
|
common: {
|
||||||
name: `${name} STATE`,
|
name: `${name} STATE`,
|
||||||
type: typeof entity.state as ioBroker.CommonType,
|
type: typeof entity.state as ioBroker.CommonType,
|
||||||
role: 'state',
|
role: getRoleForState(entity),
|
||||||
read: true,
|
read: true,
|
||||||
write: false,
|
write: false,
|
||||||
},
|
},
|
||||||
@@ -246,69 +509,96 @@ class HassAdapter extends Adapter {
|
|||||||
if (entity.attributes?.unit_of_measurement) {
|
if (entity.attributes?.unit_of_measurement) {
|
||||||
obj.common.unit = entity.attributes.unit_of_measurement;
|
obj.common.unit = entity.attributes.unit_of_measurement;
|
||||||
}
|
}
|
||||||
this.log.debug(
|
|
||||||
`Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
objs.push(obj);
|
objs.push(obj);
|
||||||
|
|
||||||
let val = entity.state;
|
let val = entity.state;
|
||||||
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
||||||
val = JSON.stringify(val);
|
val = JSON.stringify(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
states.push({ id: obj._id, lc, ts, val, ack: true });
|
states.push({ id: obj._id, lc, ts, val, ack: true });
|
||||||
|
|
||||||
|
// Create boolean state for on/off entities
|
||||||
|
const boolStateId = `${channelId}.state_boolean`;
|
||||||
|
expectedObjects.add(boolStateId);
|
||||||
|
|
||||||
|
if (!objs.find(o => o._id === boolStateId)) {
|
||||||
|
const booleanObj: ioBroker.StateObject = {
|
||||||
|
_id: boolStateId,
|
||||||
|
type: 'state',
|
||||||
|
common: {
|
||||||
|
name: `${name} STATE_BOOLEAN`,
|
||||||
|
type: 'boolean',
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
role: 'switch',
|
||||||
|
},
|
||||||
|
native: {
|
||||||
|
object_id: entity.object_id,
|
||||||
|
domain: entity.domain,
|
||||||
|
entity_id: entity.entity_id,
|
||||||
|
attr: 'state',
|
||||||
|
type: entity.domain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
objs.push(booleanObj);
|
||||||
|
states.push({
|
||||||
|
id: boolStateId,
|
||||||
|
lc: lc || Date.now(),
|
||||||
|
ts: ts || Date.now(),
|
||||||
|
val: entity.state === 'on',
|
||||||
|
ack: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.attributes) {
|
if (entity.attributes) {
|
||||||
for (const attr in entity.attributes) {
|
for (const attr in entity.attributes) {
|
||||||
if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
|
if (
|
||||||
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
|
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
|
||||||
continue;
|
attr === 'friendly_name' ||
|
||||||
}
|
attr === 'unit_of_measurement' ||
|
||||||
|
attr === 'icon' ||
|
||||||
let common: ioBroker.StateCommon;
|
!attr.length
|
||||||
if (knownAttributes[attr]) {
|
) {
|
||||||
common = { ...knownAttributes[attr] } as ioBroker.StateCommon;
|
continue;
|
||||||
} else {
|
|
||||||
common = {} as ioBroker.StateCommon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
|
|
||||||
const obj: ioBroker.StateObject = {
|
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.${attrId}`,
|
|
||||||
type: 'state',
|
|
||||||
common,
|
|
||||||
native: {
|
|
||||||
object_id: entity.object_id,
|
|
||||||
domain: entity.domain,
|
|
||||||
entity_id: entity.entity_id,
|
|
||||||
attr,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
|
|
||||||
common.read ??= true;
|
|
||||||
common.write ??= false;
|
|
||||||
common.type ??=
|
|
||||||
mapTypes[
|
|
||||||
typeof entity.attributes[
|
|
||||||
attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
this.log.debug(
|
|
||||||
`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
objs.push(obj);
|
|
||||||
|
|
||||||
let val: string | null =
|
|
||||||
entity.attributes[attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'] ?? null;
|
|
||||||
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
|
||||||
val = JSON.stringify(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
states.push({ id: obj._id, lc, ts, val, ack: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let common: ioBroker.StateCommon;
|
||||||
|
if (knownAttributes[attr]) {
|
||||||
|
common = { ...knownAttributes[attr] } as ioBroker.StateCommon;
|
||||||
|
} else {
|
||||||
|
common = {} as ioBroker.StateCommon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
|
||||||
|
const fullAttrId = `${channelId}.${attrId}`;
|
||||||
|
expectedObjects.add(fullAttrId);
|
||||||
|
|
||||||
|
const obj: ioBroker.StateObject = {
|
||||||
|
_id: fullAttrId,
|
||||||
|
type: 'state',
|
||||||
|
common,
|
||||||
|
native: {
|
||||||
|
object_id: entity.object_id,
|
||||||
|
domain: entity.domain,
|
||||||
|
entity_id: entity.entity_id,
|
||||||
|
attr,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
|
||||||
|
common.read ??= true;
|
||||||
|
common.write ??= false;
|
||||||
|
common.type ??= mapTypes[typeof entity.attributes[attr]];
|
||||||
|
common.role ??= getRoleForAttribute(attr, entity.attributes[attr], common.type);
|
||||||
|
|
||||||
|
objs.push(obj);
|
||||||
|
|
||||||
|
let val = entity.attributes[attr];
|
||||||
|
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
|
||||||
|
val = JSON.stringify(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
states.push({ id: obj._id, lc, ts, val, ack: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,8 +608,11 @@ class HassAdapter extends Adapter {
|
|||||||
const service = services[serviceType];
|
const service = services[serviceType];
|
||||||
for (const s in service) {
|
for (const s in service) {
|
||||||
if (Object.prototype.hasOwnProperty.call(service, s)) {
|
if (Object.prototype.hasOwnProperty.call(service, s)) {
|
||||||
|
const serviceId = `${channelId}.${s}`;
|
||||||
|
expectedObjects.add(serviceId);
|
||||||
|
|
||||||
const obj: ioBroker.StateObject = {
|
const obj: ioBroker.StateObject = {
|
||||||
_id: `${this.namespace}.entities.${entity.entity_id}.${s}`,
|
_id: serviceId,
|
||||||
type: 'state',
|
type: 'state',
|
||||||
common: {
|
common: {
|
||||||
name: entity.entity_id,
|
name: entity.entity_id,
|
||||||
@@ -327,7 +620,7 @@ class HassAdapter extends Adapter {
|
|||||||
read: false,
|
read: false,
|
||||||
write: true,
|
write: true,
|
||||||
type: 'mixed' as ioBroker.CommonType,
|
type: 'mixed' as ioBroker.CommonType,
|
||||||
role: 'state',
|
role: 'button',
|
||||||
},
|
},
|
||||||
native: {
|
native: {
|
||||||
object_id: entity.object_id,
|
object_id: entity.object_id,
|
||||||
@@ -339,18 +632,26 @@ class HassAdapter extends Adapter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.log.debug(
|
|
||||||
`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
objs.push(obj);
|
objs.push(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncObjects(objs);
|
const deletedCount = await this.deleteStaleObjects(expectedObjects);
|
||||||
|
const syncStats = await this.syncObjects(objs);
|
||||||
await this.syncStates(states);
|
await this.syncStates(states);
|
||||||
|
|
||||||
|
if (syncStats.newCount > 0 || deletedCount > 0) {
|
||||||
|
const changes: string[] = [];
|
||||||
|
if (syncStats.newCount > 0) {
|
||||||
|
changes.push(`${syncStats.newCount} created`);
|
||||||
|
}
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
changes.push(`${deletedCount} deleted`);
|
||||||
|
}
|
||||||
|
this.log.info(`Synchronization completed: ${changes.join(', ')}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async main(): Promise<void> {
|
private async main(): Promise<void> {
|
||||||
@@ -378,8 +679,18 @@ class HassAdapter extends Adapter {
|
|||||||
this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
|
this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
|
||||||
} else {
|
} else {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`State changed for unknown object ${id}state. Please restart the adapter to resync the objects.`,
|
`State changed for unknown object ${id}state. Triggering synchronization to resync the objects.`,
|
||||||
);
|
);
|
||||||
|
this.debouncedSync();
|
||||||
|
}
|
||||||
|
// Update boolean state
|
||||||
|
if (this.hassObjects[`${this.namespace}.${id}state_boolean`]) {
|
||||||
|
this.setState(`${id}state_boolean`, {
|
||||||
|
val: entity.state === 'on',
|
||||||
|
ack: true,
|
||||||
|
lc: lc || Date.now(),
|
||||||
|
ts: ts || Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +714,9 @@ class HassAdapter extends Adapter {
|
|||||||
this.setState(id + attrId, { val, ack: true, lc, ts });
|
this.setState(id + attrId, { val, ack: true, lc, ts });
|
||||||
} else {
|
} else {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
|
`State changed for unknown object ${id + attrId}. Triggering synchronization to resync the objects.`,
|
||||||
);
|
);
|
||||||
|
this.debouncedSync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,12 +751,10 @@ class HassAdapter extends Adapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.error(`Cannot read states: ${err}`);
|
this.log.error(`Cannot read services: ${err}`);
|
||||||
} else {
|
} else {
|
||||||
await this.parseStates(states, services);
|
await this.parseStates(states, services);
|
||||||
this.log.debug(
|
this.log.info('Initialization completed');
|
||||||
'Initial parsing of states done, subscribe to ioBroker states',
|
|
||||||
);
|
|
||||||
await this.subscribeStatesAsync('*');
|
await this.subscribeStatesAsync('*');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user