diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..0a4f452
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find /c/pWork/ioBroker.hass -name *.ts -o -name src/**/*)",
+ "Bash(grep -E '^\\\\./\\(src/|[^/]+\\\\.\\(json|ts|js\\)$\\)')",
+ "Bash(find C:/pWork/ioBroker.ping/test -type f -name *.ts -o -name *.js)",
+ "Bash(npm install:*)",
+ "Bash(npm run:*)",
+ "Bash(rm main.js)",
+ "Bash(rm lib/hass.js)",
+ "Bash(rmdir lib:*)",
+ "Bash(npx mocha:*)"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index e4a6c09..3c8dcf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
node_modules
.idea
tmp
+build
admin/i18n/flat.txt
admin/i18n/*/flat.txt
iob_npm.done
diff --git a/.releaseconfig.json b/.releaseconfig.json
index bb9bf40..65cb56b 100644
--- a/.releaseconfig.json
+++ b/.releaseconfig.json
@@ -1,3 +1,6 @@
{
- "plugins": ["iobroker", "license"]
+ "plugins": ["iobroker", "license"],
+ "exec": {
+ "before_commit": "npm run build"
+ }
}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..a0d5ccc
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,66 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+ioBroker.hass is an ioBroker adapter that connects Home Assistant to ioBroker via WebSocket API. It reads Home Assistant entities/services and exposes them as ioBroker objects, and forwards ioBroker state changes back as Home Assistant service calls.
+
+## Commands
+
+```bash
+# Build TypeScript
+npm run build
+
+# Run all tests (Mocha)
+npm test
+
+# Run only package validation tests
+npm run test:package
+
+# Lint
+npm run lint
+
+# Format check
+npx prettier --check .
+
+# Translate adapter strings
+npm run translate
+
+# Release (patch/minor/major)
+npm run release-patch
+npm run release-minor
+npm run release-major
+```
+
+## Architecture
+
+TypeScript class-based adapter. Source in `src/`, compiled output in `build/`.
+
+- **src/main.ts** — `HassAdapter` class extending `Adapter`. Daemon mode entry point. Handles:
+ - Connecting to Home Assistant and syncing entities/services into ioBroker objects
+ - `parseStates()` — maps HASS entities, attributes, and services to ioBroker channels/states
+ - `onStateChange()` — converts ioBroker commands (`ack=false`) into HASS `callService()` calls, supporting both direct values (single-field services) and JSON-stringified objects (multi-field services)
+ - Object/state synchronization via `syncObjects`/`syncStates`
+
+- **src/lib/hass.ts** — `HASS` class extending `EventEmitter`. WebSocket client for Home Assistant with:
+ - Auto-reconnect (3s delay)
+ - Message ID tracking for request-response correlation
+ - Methods: `getConfig`, `getStates`, `getServices`, `getPanels`, `callService`
+ - Events: `connected`, `disconnected`, `error`, `state_changed`
+
+The codebase uses **callback-based async** (no promises/async-await).
+
+## Testing
+
+Tests use Mocha with two ioBroker-specific frameworks:
+- `@iobroker/legacy-testing` — spins up a js-controller instance for integration tests (`test/testAdapter.js`)
+- `@iobroker/testing` — validates package.json and io-package.json structure (`test/testPackageFiles.js`)
+
+## Configuration
+
+Adapter config (defined in `io-package.json`): `host`, `port`, `password` (long-lived access token), `secure` (boolean for wss).
+
+## CI
+
+GitHub Actions runs lint on Node 22, adapter tests on Node 20/22/24 across Linux/Windows/macOS. Deploys to npm on semantic version tags via OIDC trusted publishing.
diff --git a/admin/i18n/de.json b/admin/i18n/de.json
new file mode 100644
index 0000000..58113a2
--- /dev/null
+++ b/admin/i18n/de.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "IP-Adresse von Home Assistant",
+ "Home assistant WS Port": "Home Assistant WebSocket-Port",
+ "Password repeat": "Passwort wiederholen",
+ "Password": "Passwort",
+ "Passwords missmatch!": "Passwörter stimmen nicht überein!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json
deleted file mode 100644
index 42f070b..0000000
--- a/admin/i18n/de/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "IP-Adresse von Home Assistant:",
- "Home assistant WS Port:": "Home Assistant WebSocket-Port:",
- "Password repeat:": "Passwort wiederholen:",
- "Password:": "Passwort:",
- "Passwords missmatch!": "Passwörter stimmen nicht überein!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/en.json b/admin/i18n/en.json
new file mode 100644
index 0000000..da742d4
--- /dev/null
+++ b/admin/i18n/en.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "Home assistant IP",
+ "Home assistant WS Port": "Home assistant WS Port",
+ "Password repeat": "Password repeat",
+ "Password": "Password",
+ "Passwords missmatch!": "Passwords missmatch!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json
deleted file mode 100644
index 38000e7..0000000
--- a/admin/i18n/en/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "Home assistant IP:",
- "Home assistant WS Port:": "Home assistant WS Port:",
- "Password repeat:": "Password repeat:",
- "Password:": "Password:",
- "Passwords missmatch!": "Passwords missmatch!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/es.json b/admin/i18n/es.json
new file mode 100644
index 0000000..68e495b
--- /dev/null
+++ b/admin/i18n/es.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "IP del asistente de hogar",
+ "Home assistant WS Port": "Asistente doméstico Puerto WS",
+ "Password repeat": "Repite la contraseña",
+ "Password": "Clave",
+ "Passwords missmatch!": "¡Las contraseñas no coinciden!",
+ "Secure": "¿HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json
deleted file mode 100644
index 1d0e618..0000000
--- a/admin/i18n/es/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "IP del asistente de hogar:",
- "Home assistant WS Port:": "Asistente doméstico Puerto WS:",
- "Password repeat:": "Repite la contraseña:",
- "Password:": "Clave:",
- "Passwords missmatch!": "¡Las contraseñas no coinciden!",
- "Secure:": "¿HTTPS?:"
-}
diff --git a/admin/i18n/fr.json b/admin/i18n/fr.json
new file mode 100644
index 0000000..18129d7
--- /dev/null
+++ b/admin/i18n/fr.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "IP de l'assistant à domicile ",
+ "Home assistant WS Port": "Port WS de l'assistant domestique ",
+ "Password repeat": "Répéter le mot de passe ",
+ "Password": "Mot de passe",
+ "Passwords missmatch!": "Les mots de passe ne correspondent pas !",
+ "Secure": "HTTPS ?"
+}
\ No newline at end of file
diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json
deleted file mode 100644
index 512a128..0000000
--- a/admin/i18n/fr/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "IP de l'assistant à domicile :",
- "Home assistant WS Port:": "Port WS de l'assistant domestique :",
- "Password repeat:": "Répéter le mot de passe :",
- "Password:": "Mot de passe:",
- "Passwords missmatch!": "Les mots de passe ne correspondent pas !",
- "Secure:": "HTTPS ?"
-}
diff --git a/admin/i18n/it.json b/admin/i18n/it.json
new file mode 100644
index 0000000..8df3f2d
--- /dev/null
+++ b/admin/i18n/it.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "IP assistente domestico",
+ "Home assistant WS Port": "Porta WS dell'assistente domestico",
+ "Password repeat": "Ripeti password",
+ "Password": "Parola d'ordine",
+ "Passwords missmatch!": "Le password non corrispondono!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json
deleted file mode 100644
index 1c776c4..0000000
--- a/admin/i18n/it/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "IP assistente domestico:",
- "Home assistant WS Port:": "Porta WS dell'assistente domestico:",
- "Password repeat:": "Ripeti password:",
- "Password:": "Parola d'ordine:",
- "Passwords missmatch!": "Le password non corrispondono!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/nl.json b/admin/i18n/nl.json
new file mode 100644
index 0000000..0bda09c
--- /dev/null
+++ b/admin/i18n/nl.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "Thuisassistent IP",
+ "Home assistant WS Port": "Thuisassistent WS Poort",
+ "Password repeat": "Wachtwoord herhalen",
+ "Password": "Wachtwoord",
+ "Passwords missmatch!": "Wachtwoorden komen niet overeen!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json
deleted file mode 100644
index 134cbf6..0000000
--- a/admin/i18n/nl/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "Thuisassistent IP:",
- "Home assistant WS Port:": "Thuisassistent WS Poort:",
- "Password repeat:": "Wachtwoord herhalen:",
- "Password:": "Wachtwoord:",
- "Passwords missmatch!": "Wachtwoorden komen niet overeen!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/pl.json b/admin/i18n/pl.json
new file mode 100644
index 0000000..6fd04e2
--- /dev/null
+++ b/admin/i18n/pl.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "Adres IP asystenta domowego",
+ "Home assistant WS Port": "Asystent domowy Port WS",
+ "Password repeat": "Powtórz hasło",
+ "Password": "Hasło",
+ "Passwords missmatch!": "Niezgodność haseł!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl/translations.json
deleted file mode 100644
index 029edcd..0000000
--- a/admin/i18n/pl/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "Adres IP asystenta domowego:",
- "Home assistant WS Port:": "Asystent domowy Port WS:",
- "Password repeat:": "Powtórz hasło:",
- "Password:": "Hasło:",
- "Passwords missmatch!": "Niezgodność haseł!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/pt.json b/admin/i18n/pt.json
new file mode 100644
index 0000000..79758db
--- /dev/null
+++ b/admin/i18n/pt.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "IP do assistente doméstico",
+ "Home assistant WS Port": "Assistente doméstico Porta WS",
+ "Password repeat": "Repetição de senha",
+ "Password": "Senha",
+ "Passwords missmatch!": "As senhas não correspondem!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json
deleted file mode 100644
index c36389c..0000000
--- a/admin/i18n/pt/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "IP do assistente doméstico:",
- "Home assistant WS Port:": "Assistente doméstico Porta WS:",
- "Password repeat:": "Repetição de senha:",
- "Password:": "Senha:",
- "Passwords missmatch!": "As senhas não correspondem!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/ru.json b/admin/i18n/ru.json
new file mode 100644
index 0000000..9fa0e29
--- /dev/null
+++ b/admin/i18n/ru.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "Домашний помощник IP",
+ "Home assistant WS Port": "Домашний помощник WS Порт",
+ "Password repeat": "Повтор пароля",
+ "Password": "Пароль",
+ "Passwords missmatch!": "Пароли не совпадают!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json
deleted file mode 100644
index 00f3d82..0000000
--- a/admin/i18n/ru/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "Домашний помощник IP:",
- "Home assistant WS Port:": "Домашний помощник WS Порт:",
- "Password repeat:": "Повтор пароля:",
- "Password:": "Пароль:",
- "Passwords missmatch!": "Пароли не совпадают!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/uk.json b/admin/i18n/uk.json
new file mode 100644
index 0000000..ca2bca5
--- /dev/null
+++ b/admin/i18n/uk.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "Домашній помічник IP",
+ "Home assistant WS Port": "Домашній помічник WS Порт",
+ "Password repeat": "Повторення пароля",
+ "Password": "Пароль",
+ "Passwords missmatch!": "Паролі не збігаються!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/uk/translations.json b/admin/i18n/uk/translations.json
deleted file mode 100644
index 4f1841a..0000000
--- a/admin/i18n/uk/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "Домашній помічник IP:",
- "Home assistant WS Port:": "Домашній помічник WS Порт:",
- "Password repeat:": "Повторення пароля:",
- "Password:": "Пароль:",
- "Passwords missmatch!": "Паролі не збігаються!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/i18n/zh-cn.json b/admin/i18n/zh-cn.json
new file mode 100644
index 0000000..61b553f
--- /dev/null
+++ b/admin/i18n/zh-cn.json
@@ -0,0 +1,8 @@
+{
+ "Home assistant IP": "家庭助理IP:",
+ "Home assistant WS Port": "家庭助理 WS 端口:",
+ "Password repeat": "密码重复:",
+ "Password": "密码:",
+ "Passwords missmatch!": "密码不匹配!",
+ "Secure": "HTTPS?"
+}
\ No newline at end of file
diff --git a/admin/i18n/zh-cn/translations.json b/admin/i18n/zh-cn/translations.json
deleted file mode 100644
index c5fdef3..0000000
--- a/admin/i18n/zh-cn/translations.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Home assistant IP:": "家庭助理IP:",
- "Home assistant WS Port:": "家庭助理 WS 端口:",
- "Password repeat:": "密码重复:",
- "Password:": "密码:",
- "Passwords missmatch!": "密码不匹配!",
- "Secure:": "HTTPS?"
-}
diff --git a/admin/index_m.html b/admin/index_m.html
deleted file mode 100644
index def5042..0000000
--- a/admin/index_m.html
+++ /dev/null
@@ -1,108 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json
new file mode 100644
index 0000000..3e39ad4
--- /dev/null
+++ b/admin/jsonConfig.json
@@ -0,0 +1,44 @@
+{
+ "i18n": true,
+ "type": "panel",
+ "items": {
+ "host": {
+ "type": "text",
+ "label": "Home assistant IP",
+ "sm": 12,
+ "md": 6,
+ "lg": 4
+ },
+ "port": {
+ "type": "number",
+ "label": "Home assistant WS Port",
+ "min": 1,
+ "max": 65535,
+ "sm": 12,
+ "md": 6,
+ "lg": 4
+ },
+ "_divider1": {
+ "type": "divider"
+ },
+ "password": {
+ "type": "password",
+ "label": "Password",
+ "repeat": true,
+ "visible": true,
+ "sm": 12,
+ "md": 6,
+ "lg": 4
+ },
+ "_divider2": {
+ "type": "divider"
+ },
+ "secure": {
+ "type": "checkbox",
+ "label": "Secure",
+ "sm": 12,
+ "md": 6,
+ "lg": 4
+ }
+ }
+}
diff --git a/admin/words.js b/admin/words.js
deleted file mode 100644
index 26314d5..0000000
--- a/admin/words.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/*global systemDictionary:true */
-/*
-+===================== DO NOT MODIFY ======================+
-| This file was generated by translate-adapter, please use |
-| `translate-adapter adminLanguages2words` to update it. |
-+===================== DO NOT MODIFY ======================+
-*/
-'use strict';
-
-systemDictionary = {
- "Home assistant IP:": { "en": "Home assistant IP:", "de": "IP-Adresse von Home Assistant:", "ru": "Домашний помощник IP:", "pt": "IP do assistente doméstico:", "nl": "Thuisassistent IP:", "fr": "IP de l'assistant à domicile :", "it": "IP assistente domestico:", "es": "IP del asistente de hogar:", "pl": "Adres IP asystenta domowego:", "uk": "Домашній помічник IP:", "zh-cn": "家庭助理IP:"},
- "Home assistant WS Port:": { "en": "Home assistant WS Port:", "de": "Home Assistant WebSocket-Port:", "ru": "Домашний помощник WS Порт:", "pt": "Assistente doméstico Porta WS:", "nl": "Thuisassistent WS Poort:", "fr": "Port WS de l'assistant domestique :", "it": "Porta WS dell'assistente domestico:", "es": "Asistente doméstico Puerto WS:", "pl": "Asystent domowy Port WS:", "uk": "Домашній помічник WS Порт:", "zh-cn": "家庭助理 WS 端口:"},
- "Password repeat:": { "en": "Password repeat:", "de": "Passwort wiederholen:", "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:", "uk": "Повторення пароля:", "zh-cn": "密码重复:"},
- "Password:": { "en": "Password:", "de": "Passwort:", "ru": "Пароль:", "pt": "Senha:", "nl": "Wachtwoord:", "fr": "Mot de passe:", "it": "Parola d'ordine:", "es": "Clave:", "pl": "Hasło:", "uk": "Пароль:", "zh-cn": "密码:"},
- "Passwords missmatch!": { "en": "Passwords missmatch!", "de": "Passwörter stimmen nicht überein!", "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ł!", "uk": "Паролі не збігаються!", "zh-cn": "密码不匹配!"},
- "Secure:": { "en": "HTTPS?", "de": "HTTPS?", "ru": "HTTPS?", "pt": "HTTPS?", "nl": "HTTPS?", "fr": "HTTPS ?", "it": "HTTPS?", "es": "¿HTTPS?:", "pl": "HTTPS?", "uk": "HTTPS?", "zh-cn": "HTTPS?"},
-};
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
index a624115..e07a11c 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -23,14 +23,13 @@ export default [
},
{
ignores: [
+ 'node_modules/**/*',
'build/**/*',
- 'src/lib/**/*.js',
'admin/**/*',
'test/**/*',
'src-admin/**/*',
- 'src-rules/**/*',
- 'tasks.js',
- '**/*.mjs'
+ 'tmp/**/*',
+ '**/*.mjs',
],
},
];
diff --git a/io-package.json b/io-package.json
index ee69fc3..f31e5c8 100644
--- a/io-package.json
+++ b/io-package.json
@@ -5,16 +5,16 @@
"title": "Home Assistant",
"titleLang": {
"en": "Home Assistant",
- "de": "Home-Assistent",
- "ru": "Домашний помощник",
+ "de": "Home Assistant",
+ "ru": "Home Assistant",
"pt": "Home Assistant",
- "nl": "Thuisassistent",
- "fr": "Assistante à domicile",
- "it": "Assistente domiciliare",
- "es": "Asistente de hogar",
- "pl": "Asystent domowy",
- "zh-cn": "家庭助理",
- "uk": "Домашній помічник"
+ "nl": "Home Assistant",
+ "fr": "Home Assistant",
+ "it": "Home Assistant",
+ "es": "Home Assistant",
+ "pl": "Home Assistant",
+ "zh-cn": "Home Assistant",
+ "uk": "Home Assistant"
},
"desc": {
"en": "Home Assistant connection for ioBroker",
@@ -128,7 +128,9 @@
"icon": "hass.png",
"enabled": true,
"compact": true,
- "materialize": true,
+ "adminUI": {
+ "config": "json"
+ },
"license": "MIT",
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png",
"keywords": [
@@ -144,12 +146,12 @@
],
"dependencies": [
{
- "js-controller": ">=3.0.0"
+ "js-controller": ">=6.0.11"
}
],
"globalDependencies": [
{
- "admin": ">=5.1.28"
+ "admin": ">=6.0.0"
}
],
"plugins": {
diff --git a/lib/hass.js b/lib/hass.js
deleted file mode 100644
index 44c75a8..0000000
--- a/lib/hass.js
+++ /dev/null
@@ -1,282 +0,0 @@
-const util = require('node:util');
-const { EventEmitter } = require('node:events');
-const WebSocket = require('node:ws');
-
-function HASS(options, log) {
- if (!(this instanceof HASS)) {
- return new HASS(options);
- }
-
- options = options || {};
- options.host = options.host || '127.0.0.1';
- options.port = parseInt(options.port, 10) || 8123;
-
- const ERRORS = {
- 1: 'ERR_CANNOT_CONNECT',
- 2: 'ERR_INVALID_AUTH',
- 3: 'ERR_CONNECTION_LOST',
- };
-
- this.socket = null;
- const that = this;
- let currentId = 1;
- const requests = {};
- let connected;
- let connectTimeout = null;
- let closed = false;
-
- function subscribeEvents(socket, callback) {
- if (socket && typeof socket.send === 'function') {
- const id = currentId++;
- requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
-
- socket.send(
- JSON.stringify({
- id: id,
- type: 'subscribe_events',
- /*event_type: 'state_changed'*/
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- function getConfig(socket, callback) {
- if (socket && typeof socket.send === 'function') {
- const id = currentId++;
- requests[id] = { type: 'get_config', cb: callback, ts: Date.now() };
- socket.send(
- JSON.stringify({
- id: id,
- type: 'get_config',
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- function getStates(socket, callback) {
- if (socket && typeof socket.send === 'function') {
- const id = currentId++;
- requests[id] = { type: 'get_states', cb: callback, ts: Date.now() };
- socket.send(
- JSON.stringify({
- id: id,
- type: 'get_states',
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- function getPanels(socket, callback) {
- if (socket && typeof socket.send === 'function') {
- const id = currentId++;
- requests[id] = { type: 'get_panels', cb: callback, ts: Date.now() };
- socket.send(
- JSON.stringify({
- id: id,
- type: 'get_panels',
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- function getServices(socket, callback) {
- if (socket && typeof socket.send === 'function') {
- const id = currentId++;
- requests[id] = { type: 'get_services', cb: callback, ts: Date.now() };
- socket.send(
- JSON.stringify({
- id: id,
- type: 'get_services',
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- 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() };
- socket.send(
- JSON.stringify({
- id: id,
- type: 'call_service',
- domain: domain || '',
- service: service,
- service_data: serviceData,
- target,
- }),
- );
- } else {
- callback && callback('not connected');
- }
- }
-
- function sendAuth(socket, pass) {
- if (socket && typeof socket.send === 'function') {
- socket.send(
- JSON.stringify({
- type: 'auth',
- access_token: pass,
- }),
- );
- }
- }
-
- function initSocket(socket) {
- socket.on('message', msg => {
- log.silly(msg);
-
- const response = JSON.parse(msg);
- if (response.type === 'event') {
- if (response.event.data && response.event.event_type === 'system_log_event') {
- if (response.event.data.level === 'WARNING') {
- log.warn('EVENT: ' + response.event.data.message);
- } else if (response.event.data.level === 'ERROR') {
- log.error('EVENT: ' + response.event.data.message);
- } else {
- log.debug('EVENT: ' + response.event.data.message);
- }
- } else if (response.event && response.event.event_type === 'state_changed') {
- that.emit('state_changed', response.event.data.new_state);
- }
- } else if (response.type === 'auth_required') {
- if (!options.password) {
- that.emit('error', 'Password required. Connection closed');
- socket.terminate();
- } else {
- setTimeout(() => sendAuth(socket, options.password), 50);
- }
- } else if (response.type === 'auth_ok') {
- setImmediate(() =>
- subscribeEvents(socket, err => {
- if (!err) {
- connected = true;
- that.emit('connected');
- }
- }),
- );
- } else if (response.id === undefined) {
- log.error(`Invalid answer: ${msg}`);
- } else {
- if (response.type === 'result' && requests[response.id]) {
- log.debug(
- `got answer for ${requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`,
- );
- if (typeof requests[response.id].cb === 'function') {
- requests[response.id].cb(!response.success, response.result);
- delete requests[response.id];
- }
- }
- }
- });
-
- socket.on('error', err => {
- socket = null;
- if (err && err.message.indexOf('RSV2 and RSV3 must be clear') !== -1) {
- // ignore deflate error
- } else {
- log.error(err);
- }
- });
- socket.on('open', () => {
- if (!connected) {
- }
- });
- socket.on('close', () => {
- that.socket = null;
- if (connected) {
- connected = false;
- that.emit('disconnected');
- }
- if (!connectTimeout && !closed) {
- setTimeout(() => {
- connectTimeout = null;
- that.connect();
- }, 3000);
- }
- });
- }
-
- this.isConnected = () => connected;
-
- this.getConfig = function (callback) {
- if (!connected) {
- typeof callback === 'function' && callback('not connected');
- } else {
- getConfig(this.socket, callback);
- }
- };
-
- this.getStates = function (callback) {
- if (!connected) {
- typeof callback === 'function' && callback('not connected');
- } else {
- getStates(this.socket, callback);
- }
- };
-
- this.getServices = function (callback) {
- if (!connected) {
- typeof callback === 'function' && callback('not connected');
- } else {
- getServices(this.socket, callback);
- }
- };
-
- this.getPanels = function (callback) {
- if (!connected) {
- typeof callback === 'function' && callback('not connected');
- } else {
- getPanels(this.socket, callback);
- }
- };
-
- this.callService = function (service, domain, serviceData, target, callback) {
- if (!connected) {
- typeof callback === 'function' && callback('not connected');
- } else {
- callService(this.socket, service, domain, serviceData, target, callback);
- }
- };
-
- this.connect = function () {
- if (connectTimeout) {
- clearTimeout(connectTimeout);
- connectTimeout = null;
- }
-
- this.socket = new WebSocket(`ws${options.secure ? 's' : ''}://${options.host}:${options.port}/api/websocket`, {
- perMessageDeflate: false,
- });
-
- initSocket(this.socket);
- };
-
- this.close = function () {
- if (connectTimeout) {
- clearTimeout(connectTimeout);
- connectTimeout = null;
- }
- closed = true;
- if (this.socket) {
- this.socket.close();
- }
- };
-
- return this;
-}
-
-// extend the EventEmitter class using our class
-util.inherits(HASS, EventEmitter);
-
-module.exports = HASS;
diff --git a/main.js b/main.js
deleted file mode 100644
index 9188b3f..0000000
--- a/main.js
+++ /dev/null
@@ -1,471 +0,0 @@
-/* jshint -W097 */
-/* jshint strict: false */
-/* jslint node: true */
-
-'use strict';
-
-const utils = require('@iobroker/adapter-core');
-const HASS = require('./lib/hass');
-
-let connected = false;
-let hass;
-let adapter;
-const hassObjects = {};
-let delayTimeout = null;
-let stopped = false;
-
-function startAdapter(options) {
- options = options || {};
- Object.assign(options, { name: 'hass', unload: stop });
- adapter = new utils.Adapter(options);
-
- // is called if a subscribed state changes
- adapter.on('stateChange', (id, state) => {
- // you can use the ack flag to detect if it is status (true) or command (false)
- if (state && !state.ack) {
- if (!connected) {
- return adapter.log.warn(`Cannot send command to "${id}", because not connected`);
- }
- /*if (id === adapter.namespace + '.' + '.info.resync') {
- queue.push({command: 'resync'});
- processQueue();
- } else */
- if (hassObjects[id]) {
- if (!hassObjects[id].common.write) {
- adapter.log.warn(`Object ${id} is not writable!`);
- } else {
- const serviceData = {};
- const fields = hassObjects[id].native.fields;
- const target = {};
-
- let requestFields = {};
- if (typeof state.val === 'string') {
- state.val = state.val.trim();
- if (state.val.startsWith('{') && state.val.endsWith('}')) {
- try {
- requestFields = JSON.parse(state.val) || {};
- } catch (err) {
- adapter.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
- requestFields = {};
- }
- }
- }
-
- // If a non-JSON value was set, and we only have one relevant field, use this field as value
- if (fields && Object.keys(requestFields).length === 0) {
- const fieldList = Object.keys(fields);
- if (fieldList.length === 1 && fieldList[0] !== 'entity_id') {
- requestFields[fieldList[0]] = state.val;
- } else if (fieldList.length === 2 && fields.entity_id) {
- requestFields[fieldList[1 - fields.indexOf('entity_id')]] = state.val;
- }
- }
-
- adapter.log.debug(
- `Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`,
- );
- if (fields) {
- for (const field in fields) {
- if (!Object.prototype.hasOwnProperty.call(fields, field)) {
- continue;
- }
-
- if (field === 'entity_id') {
- target.entity_id = hassObjects[id].native.entity_id;
- } else if (requestFields[field] !== undefined) {
- serviceData[field] = requestFields[field];
- }
- }
- }
- const noFields = Object.keys(serviceData).length === 0;
- 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,
- target,
- err => {
- err && adapter.log.error(`Cannot control ${id}: ${err}`);
- if (err && fields && noFields) {
- adapter.log.warn(
- `Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`,
- );
- adapter.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
- }
- },
- );
- }
- }
- }
- });
-
- // is called when databases are connected and adapter received configuration.
- // start here!
- adapter.on('ready', main);
-
- return adapter;
-}
-
-function stop(callback) {
- stopped = true;
- delayTimeout && clearTimeout(delayTimeout);
- hass && hass.close();
- callback && callback();
-}
-
-function syncStates(states, cb) {
- if (!states || !states.length) {
- return cb();
- }
- const state = states.shift();
- const id = state.id;
- delete state.id;
-
- adapter.setForeignState(id, state, err => {
- if (err) {
- adapter.log.error(err);
- }
- setImmediate(syncStates, states, cb);
- });
-}
-
-function syncObjects(objects, cb) {
- if (!objects?.length) {
- return cb();
- }
- const obj = objects.shift();
- hassObjects[obj._id] = obj;
-
- adapter.getForeignObject(obj._id, (err, oldObj) => {
- if (err) {
- adapter.log.error(err);
- }
-
- if (!oldObj) {
- adapter.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
- hassObjects[obj._id] = obj;
- adapter.setForeignObject(obj._id, obj, err => {
- if (err) {
- adapter.log.error(err);
- }
- setImmediate(() => syncObjects(objects, cb));
- });
- } else {
- hassObjects[obj._id] = oldObj;
- if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
- oldObj.native = obj.native;
-
- adapter.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
- adapter.setForeignObject(obj._id, oldObj, err => {
- if (err) {
- adapter.log.error(err);
- }
- setImmediate(() => syncObjects(objects, cb));
- });
- } else {
- setImmediate(() => syncObjects(objects, cb));
- }
- }
- });
-}
-
-const knownAttributes = {
- azimuth: { write: false, read: true, unit: '°' },
- elevation: { write: false, read: true, unit: '°' },
-};
-
-const mapTypes = {
- string: 'string',
- number: 'number',
- object: 'mixed',
- boolean: 'boolean',
-};
-const skipServices = ['persistent_notification'];
-
-function parseStates(entities, services, callback) {
- const objs = [];
- const states = [];
- let obj;
- let channel;
- for (let e = 0; e < entities.length; e++) {
- const entity = entities[e];
- if (!entity) {
- continue;
- }
-
- const name =
- entity.name ||
- (entity.attributes && entity.attributes.friendly_name ? entity.attributes.friendly_name : entity.entity_id);
- const desc = entity.attributes && entity.attributes.attribution ? entity.attributes.attribution : undefined;
-
- channel = {
- _id: `${adapter.namespace}.entities.${entity.entity_id}`,
- common: {
- name: name,
- },
- type: 'channel',
- native: {
- object_id: entity.object_id,
- entity_id: entity.entity_id,
- },
- };
- if (desc) {
- channel.common.desc = desc;
- }
- objs.push(channel);
-
- const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
- const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
-
- if (entity.state !== undefined) {
- obj = {
- _id: `${adapter.namespace}.entities.${entity.entity_id}.state`,
- type: 'state',
- common: {
- name: `${name} STATE`,
- type: typeof entity.state,
- read: true,
- write: false,
- },
- native: {
- object_id: entity.object_id,
- domain: entity.domain,
- entity_id: entity.entity_id,
- },
- };
- if (entity.attributes && entity.attributes.unit_of_measurement) {
- obj.common.unit = entity.attributes.unit_of_measurement;
- }
- adapter.log.debug(
- `Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
- );
- objs.push(obj);
-
- let val = entity.state;
- if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
- val = JSON.stringify(val);
- }
-
- states.push({ id: obj._id, lc, ts, val, ack: true });
- }
-
- if (entity.attributes) {
- for (const attr in entity.attributes) {
- if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
- if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
- continue;
- }
-
- let common;
- if (knownAttributes[attr]) {
- common = Object.assign({}, knownAttributes[attr]);
- } else {
- common = {};
- }
-
- const attrId = attr.replace(adapter.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
- obj = {
- _id: `${adapter.namespace}.entities.${entity.entity_id}.${attrId}`,
- type: 'state',
- common: common,
- native: {
- object_id: entity.object_id,
- domain: entity.domain,
- entity_id: entity.entity_id,
- attr: attr,
- },
- };
- if (!common.name) {
- common.name = `${name} ${attr.replace(/_/g, ' ')}`;
- }
- if (common.read === undefined) {
- common.read = true;
- }
- if (common.write === undefined) {
- common.write = false;
- }
- if (common.type === undefined) {
- common.type = mapTypes[typeof entity.attributes[attr]];
- }
-
- adapter.log.debug(
- `Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
- );
-
- 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 });
- }
- }
- }
-
- const serviceType = entity.entity_id.split('.')[0];
-
- if (services[serviceType] && !skipServices.includes(serviceType)) {
- const service = services[serviceType];
- for (const s in service) {
- if (Object.prototype.hasOwnProperty.call(service, s)) {
- obj = {
- _id: `${adapter.namespace}.entities.${entity.entity_id}.${s}`,
- type: 'state',
- common: {
- desc: service[s].description,
- read: false,
- write: true,
- type: 'mixed',
- },
- native: {
- object_id: entity.object_id,
- domain: entity.domain,
- fields: service[s].fields,
- entity_id: entity.entity_id,
- attr: s,
- type: serviceType,
- },
- };
-
- adapter.log.debug(
- `Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
- );
-
- objs.push(obj);
- }
- }
- }
- }
-
- syncObjects(objs, () => syncStates(states, callback));
-}
-
-function main() {
- adapter.config.host = adapter.config.host || '127.0.0.1';
- adapter.config.port = parseInt(adapter.config.port, 10) || 8123;
-
- adapter.setState('info.connection', false, true);
-
- hass = new HASS(adapter.config, adapter.log);
-
- hass.on('error', err => adapter.log.error(err));
-
- hass.on('state_changed', entity => {
- adapter.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
- if (!entity || typeof entity.entity_id !== 'string') {
- return;
- }
-
- const id = `entities.${entity.entity_id}.`;
- const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
- const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
- if (entity.state !== undefined) {
- if (hassObjects[`${adapter.namespace}.${id}state`]) {
- adapter.setState(`${id}state`, { val: entity.state, ack: true, lc: lc, ts: ts });
- } else {
- adapter.log.info(
- `State changed for unknown object ${`${id}state`}. Please restart the adapter to resync the objects.`,
- );
- }
- }
- if (entity.attributes) {
- for (const attr in entity.attributes) {
- if (
- !Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
- attr === 'friendly_name' ||
- attr === 'unit_of_measurement' ||
- attr === 'icon' ||
- !attr.length
- ) {
- continue;
- }
- let val = entity.attributes[attr];
- if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
- val = JSON.stringify(val);
- }
- const attrId = attr.replace(adapter.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
- if (hassObjects[`${adapter.namespace}.${id}state`]) {
- adapter.setState(id + attrId, { val, ack: true, lc, ts });
- } else {
- adapter.log.info(
- `State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
- );
- }
- }
- }
- });
-
- hass.on('connected', () => {
- if (!connected) {
- adapter.log.debug('Connected');
- connected = true;
- adapter.setState('info.connection', true, true);
- hass.getConfig(err => {
- if (err) {
- adapter.log.error(`Cannot read config: ${err}`);
- return;
- }
- //adapter.log.debug(JSON.stringify(config));
- delayTimeout = setTimeout(() => {
- delayTimeout = null;
- if (!stopped) {
- hass.getStates((err, states) => {
- if (stopped) {
- return;
- }
- if (err) {
- return adapter.log.error(`Cannot read states: ${err}`);
- }
- //adapter.log.debug(JSON.stringify(states));
- delayTimeout = setTimeout(() => {
- delayTimeout = null;
- if (!stopped) {
- hass.getServices((err, services) => {
- if (stopped) {
- return;
- }
- if (err) {
- adapter.log.error(`Cannot read states: ${err}`);
- } else {
- //adapter.log.debug(JSON.stringify(services));
- parseStates(states, services, () => {
- adapter.log.debug(
- 'Initial parsing of states done, subscribe to ioBroker states',
- );
- adapter.subscribeStates('*');
- });
- }
- });
- }
- }, 100);
- });
- }
- }, 100);
- });
- }
- });
-
- hass.on('disconnected', () => {
- if (connected) {
- adapter.log.debug('Disconnected');
- connected = false;
- adapter.setState('info.connection', false, true);
- }
- });
-
- hass.connect();
-}
-
-// If started as allInOne/compact mode => return function to create instance
-if (module && module.parent) {
- module.exports = startAdapter;
-} else {
- // or start the instance directly
- startAdapter();
-}
diff --git a/package-lock.json b/package-lock.json
index 2177ee6..7dfdd39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,6 @@
"license": "MIT",
"dependencies": {
"@iobroker/adapter-core": "^3.3.2",
- "websocket": "^1.0.35",
"ws": "^8.20.0"
},
"devDependencies": {
@@ -18,9 +17,11 @@
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"@alcalzone/release-script-plugin-license": "^5.1.1",
"@iobroker/adapter-dev": "^1.5.0",
+ "@iobroker/build-tools": "^3.0.1",
"@iobroker/eslint-config": "^2.2.0",
"@iobroker/legacy-testing": "^2.0.2",
"@iobroker/testing": "^5.2.2",
+ "@types/ws": "^8.18.1",
"mocha": "^11.7.5"
}
},
@@ -1305,6 +1306,102 @@
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
+ "node_modules/@iobroker/build-tools": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@iobroker/build-tools/-/build-tools-3.0.1.tgz",
+ "integrity": "sha512-i5PsBOHNscMAqmdYrdYxJUplyFCkTwPUVJVL5iQozbG7+e79bHTHXuLCHwslaN/m5ACfr0KmtpoMram8gzGnPg==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^13.0.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/lru-cache": {
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@iobroker/build-tools/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@iobroker/eslint-config": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz",
@@ -1754,6 +1851,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
@@ -2597,15 +2704,6 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
- "node_modules/bufferutil": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
- "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
- "hasInstallScript": true,
- "dependencies": {
- "node-gyp-build": "^4.2.0"
- }
- },
"node_modules/builtin-modules": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
@@ -3013,15 +3111,6 @@
"node": ">= 8"
}
},
- "node_modules/d": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
- "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
- "dependencies": {
- "es5-ext": "^0.10.50",
- "type": "^1.0.1"
- }
- },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -3086,14 +3175,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
"node_modules/deep-eql": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -3476,40 +3557,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/es5-ext": {
- "version": "0.10.63",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.63.tgz",
- "integrity": "sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ==",
- "hasInstallScript": true,
- "dependencies": {
- "es6-iterator": "^2.0.3",
- "es6-symbol": "^3.1.3",
- "esniff": "^2.0.1",
- "next-tick": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/es6-iterator": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
- "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
- "dependencies": {
- "d": "1",
- "es5-ext": "^0.10.35",
- "es6-symbol": "^3.1.1"
- }
- },
- "node_modules/es6-symbol": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
- "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
- "dependencies": {
- "d": "^1.0.1",
- "ext": "^1.1.2"
- }
- },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -4143,25 +4190,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/esniff": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
- "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
- "dependencies": {
- "d": "^1.0.1",
- "es5-ext": "^0.10.62",
- "event-emitter": "^0.3.5",
- "type": "^2.7.2"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esniff/node_modules/type": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
- "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
- },
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -4239,15 +4267,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/event-emitter": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
- "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
- "dependencies": {
- "d": "1",
- "es5-ext": "~0.10.14"
- }
- },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4272,19 +4291,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
- "node_modules/ext": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/ext/-/ext-1.5.0.tgz",
- "integrity": "sha512-+ONcYoWj/SoQwUofMr94aGu05Ou4FepKi7N7b+O8T4jVfyIsZQV1/xeS8jpaBzF0csAk0KLXoHCxU7cKYZjo1Q==",
- "dependencies": {
- "type": "^2.5.0"
- }
- },
- "node_modules/ext/node_modules/type": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
- "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
- },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5576,11 +5582,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
- },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -6118,11 +6119,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- },
"node_modules/nano-spawn": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@@ -6143,11 +6139,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/next-tick": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
- "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
- },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -6221,6 +6212,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
+ "optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
@@ -7615,11 +7607,6 @@
"strip-bom": "^3.0.0"
}
},
- "node_modules/type": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
- "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -7721,14 +7708,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/typedarray-to-buffer": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
- "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
- "dependencies": {
- "is-typedarray": "^1.0.0"
- }
- },
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
@@ -7853,15 +7832,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/utf-8-validate": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
- "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==",
- "hasInstallScript": true,
- "dependencies": {
- "node-gyp-build": "^4.2.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7889,23 +7859,6 @@
"node": ">= 8"
}
},
- "node_modules/websocket": {
- "version": "1.0.35",
- "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
- "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "bufferutil": "^4.0.1",
- "debug": "^2.2.0",
- "es5-ext": "^0.10.63",
- "typedarray-to-buffer": "^3.1.5",
- "utf-8-validate": "^5.0.2",
- "yaeti": "^0.0.6"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8100,14 +8053,6 @@
"node": ">=10"
}
},
- "node_modules/yaeti": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
- "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
- "engines": {
- "node": ">=0.10.32"
- }
- },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -8944,6 +8889,68 @@
}
}
},
+ "@iobroker/build-tools": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@iobroker/build-tools/-/build-tools-3.0.1.tgz",
+ "integrity": "sha512-i5PsBOHNscMAqmdYrdYxJUplyFCkTwPUVJVL5iQozbG7+e79bHTHXuLCHwslaN/m5ACfr0KmtpoMram8gzGnPg==",
+ "dev": true,
+ "requires": {
+ "glob": "^13.0.1"
+ },
+ "dependencies": {
+ "balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^4.0.2"
+ }
+ },
+ "glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "requires": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ }
+ },
+ "lru-cache": {
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^5.0.5"
+ }
+ },
+ "path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ }
+ }
+ }
+ },
"@iobroker/eslint-config": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz",
@@ -9276,6 +9283,15 @@
"integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==",
"dev": true
},
+ "@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@typescript-eslint/eslint-plugin": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
@@ -9793,14 +9809,6 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
- "bufferutil": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
- "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
- "requires": {
- "node-gyp-build": "^4.2.0"
- }
- },
"builtin-modules": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
@@ -10062,15 +10070,6 @@
"which": "^2.0.1"
}
},
- "d": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
- "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
- "requires": {
- "es5-ext": "^0.10.50",
- "type": "^1.0.1"
- }
- },
"data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -10110,14 +10109,6 @@
"is-data-view": "^1.0.1"
}
},
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "requires": {
- "ms": "2.0.0"
- }
- },
"deep-eql": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -10413,36 +10404,6 @@
"is-symbol": "^1.0.4"
}
},
- "es5-ext": {
- "version": "0.10.63",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.63.tgz",
- "integrity": "sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ==",
- "requires": {
- "es6-iterator": "^2.0.3",
- "es6-symbol": "^3.1.3",
- "esniff": "^2.0.1",
- "next-tick": "^1.1.0"
- }
- },
- "es6-iterator": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
- "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
- "requires": {
- "d": "1",
- "es5-ext": "^0.10.35",
- "es6-symbol": "^3.1.1"
- }
- },
- "es6-symbol": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
- "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
- "requires": {
- "d": "^1.0.1",
- "ext": "^1.1.2"
- }
- },
"esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -10889,24 +10850,6 @@
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true
},
- "esniff": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
- "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
- "requires": {
- "d": "^1.0.1",
- "es5-ext": "^0.10.62",
- "event-emitter": "^0.3.5",
- "type": "^2.7.2"
- },
- "dependencies": {
- "type": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
- "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
- }
- }
- },
"espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -10956,15 +10899,6 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
- "event-emitter": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
- "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
- "requires": {
- "d": "1",
- "es5-ext": "~0.10.14"
- }
- },
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -10982,21 +10916,6 @@
"strip-final-newline": "^2.0.0"
}
},
- "ext": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/ext/-/ext-1.5.0.tgz",
- "integrity": "sha512-+ONcYoWj/SoQwUofMr94aGu05Ou4FepKi7N7b+O8T4jVfyIsZQV1/xeS8jpaBzF0csAk0KLXoHCxU7cKYZjo1Q==",
- "requires": {
- "type": "^2.5.0"
- },
- "dependencies": {
- "type": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
- "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
- }
- }
- },
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -11791,11 +11710,6 @@
"which-typed-array": "^1.1.16"
}
},
- "is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
- },
"is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -12173,11 +12087,6 @@
}
}
},
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- },
"nano-spawn": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@@ -12190,11 +12099,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
- "next-tick": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
- "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
- },
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -12233,9 +12137,9 @@
}
},
"node-gyp-build": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
- "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
+ "version": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
+ "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
+ "optional": true
},
"node-releases": {
"version": "2.0.36",
@@ -13172,11 +13076,6 @@
"strip-bom": "^3.0.0"
}
},
- "type": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
- "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
- },
"type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -13245,14 +13144,6 @@
"reflect.getprototypeof": "^1.0.6"
}
},
- "typedarray-to-buffer": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
- "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
- "requires": {
- "is-typedarray": "^1.0.0"
- }
- },
"typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
@@ -13324,14 +13215,6 @@
"punycode": "^2.1.0"
}
},
- "utf-8-validate": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
- "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==",
- "requires": {
- "node-gyp-build": "^4.2.0"
- }
- },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13350,19 +13233,6 @@
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"dev": true
},
- "websocket": {
- "version": "1.0.35",
- "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
- "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
- "requires": {
- "bufferutil": "^4.0.1",
- "debug": "^2.2.0",
- "es5-ext": "^0.10.63",
- "typedarray-to-buffer": "^3.1.5",
- "utf-8-validate": "^5.0.2",
- "yaeti": "^0.0.6"
- }
- },
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -13485,11 +13355,6 @@
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
- "yaeti": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
- "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
- },
"yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
diff --git a/package.json b/package.json
index aa7fbef..f425ccc 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,6 @@
"url": "https://github.com/ioBroker/ioBroker.hass"
},
"dependencies": {
- "websocket": "^1.0.35",
"ws": "^8.20.0",
"@iobroker/adapter-core": "^3.3.2"
},
@@ -34,20 +33,28 @@
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"@alcalzone/release-script-plugin-license": "^5.1.1",
"@iobroker/adapter-dev": "^1.5.0",
+ "@iobroker/build-tools": "^3.0.1",
"@iobroker/eslint-config": "^2.2.0",
"@iobroker/legacy-testing": "^2.0.2",
"@iobroker/testing": "^5.2.2",
+ "@types/ws": "^8.18.1",
"mocha": "^11.7.5"
},
- "main": "main.js",
+ "main": "build/main.js",
"files": [
- "lib/",
- "admin/",
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
+ "build/",
"io-package.json",
- "main.js"
+ "LICENSE"
],
"scripts": {
- "test": "node node_modules/mocha/bin/mocha --exit",
+ "test:integration": "mocha --exit",
+ "test:package": "mocha test/testPackageFiles.js --exit",
+ "test": "npm run test:integration",
+ "build:tsc": "tsc -p tsconfig.build.json",
+ "build": "npm run build:tsc",
+ "lint": "eslint -c eslint.config.mjs",
"release": "release-script",
"release-patch": "release-script patch --yes",
"release-minor": "release-script minor --yes",
diff --git a/src/lib/hass.ts b/src/lib/hass.ts
new file mode 100644
index 0000000..21ed9f5
--- /dev/null
+++ b/src/lib/hass.ts
@@ -0,0 +1,248 @@
+import { EventEmitter } from 'node:events';
+import WebSocket from 'ws';
+
+interface HassOptions {
+ host: string;
+ port: number;
+ password?: string;
+ secure?: boolean;
+}
+
+interface HassRequest {
+ type: string;
+ ts: number;
+ cb?: (err: boolean | string | null, result?: any) => void;
+}
+
+/*
+const ERRORS: Record = {
+ 1: 'ERR_CANNOT_CONNECT',
+ 2: 'ERR_INVALID_AUTH',
+ 3: 'ERR_CONNECTION_LOST',
+};
+*/
+export default class HASS extends EventEmitter {
+ private socket: WebSocket | null = null;
+ private readonly options: HassOptions;
+ private readonly log: ioBroker.Logger;
+ private currentId: number = 1;
+ private readonly requests: Record = {};
+ private _connected: boolean = false;
+ private connectTimeout: ReturnType | null = null;
+ private closed: boolean = false;
+
+ constructor(options: HassOptions, log: ioBroker.Logger) {
+ super();
+ this.options = {
+ host: options.host || '127.0.0.1',
+ port: parseInt(String(options.port), 10) || 8123,
+ password: options.password,
+ secure: options.secure,
+ };
+ this.log = log;
+ }
+
+ private subscribeEvents(socket: WebSocket, callback?: (err: boolean | string | null) => void): void {
+ if (socket && typeof socket.send === 'function') {
+ const id = this.currentId++;
+ this.requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
+ socket.send(
+ JSON.stringify({
+ id,
+ type: 'subscribe_events',
+ }),
+ );
+ } else {
+ callback?.('not connected');
+ }
+ }
+
+ private sendCommand(
+ socket: WebSocket | null,
+ type: string,
+ callback?: (err: boolean | string | null, result?: any) => void,
+ extra?: Record,
+ ): void {
+ if (socket && typeof socket.send === 'function') {
+ const id = this.currentId++;
+ this.requests[id] = { type, cb: callback, ts: Date.now() };
+ socket.send(
+ JSON.stringify({
+ id,
+ type,
+ ...extra,
+ }),
+ );
+ } else {
+ callback?.('not connected');
+ }
+ }
+
+ private sendAuth(socket: WebSocket, pass: string): void {
+ if (socket && typeof socket.send === 'function') {
+ socket.send(
+ JSON.stringify({
+ type: 'auth',
+ access_token: pass,
+ }),
+ );
+ }
+ }
+
+ private initSocket(socket: WebSocket): void {
+ socket.on('message', (msg: WebSocket.Data): void => {
+ const msgStr = (msg as Buffer).toString();
+ this.log.silly(msgStr);
+
+ const response = JSON.parse(msgStr);
+ if (response.type === 'event') {
+ if (response.event?.data && response.event.event_type === 'system_log_event') {
+ if (response.event.data.level === 'WARNING') {
+ this.log.warn(`EVENT: ${response.event.data.message}`);
+ } else if (response.event.data.level === 'ERROR') {
+ this.log.error(`EVENT: ${response.event.data.message}`);
+ } else {
+ this.log.debug(`EVENT: ${response.event.data.message}`);
+ }
+ } else if (response.event?.event_type === 'state_changed') {
+ this.emit('state_changed', response.event.data.new_state);
+ }
+ } else if (response.type === 'auth_required') {
+ if (!this.options.password) {
+ this.emit('error', 'Password required. Connection closed');
+ socket.terminate();
+ } else {
+ setTimeout(() => this.sendAuth(socket, this.options.password!), 50);
+ }
+ } else if (response.type === 'auth_ok') {
+ setImmediate(() =>
+ this.subscribeEvents(socket, err => {
+ if (!err) {
+ this._connected = true;
+ this.emit('connected');
+ }
+ }),
+ );
+ } else if (response.id === undefined) {
+ this.log.error(`Invalid answer: ${msgStr}`);
+ } else {
+ if (response.type === 'result' && this.requests[response.id]) {
+ this.log.debug(
+ `got answer for ${this.requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`,
+ );
+ if (typeof this.requests[response.id].cb === 'function') {
+ this.requests[response.id].cb!(!response.success, response.result);
+ delete this.requests[response.id];
+ }
+ }
+ }
+ });
+
+ socket.on('error', (err: Error) => {
+ this.socket = null;
+ if (err?.message?.indexOf('RSV2 and RSV3 must be clear') !== -1) {
+ // ignore deflate error
+ } else {
+ this.log.error(err.toString());
+ }
+ });
+
+ socket.on('open', () => {
+ // connection opened
+ });
+
+ socket.on('close', () => {
+ this.socket = null;
+ if (this._connected) {
+ this._connected = false;
+ this.emit('disconnected');
+ }
+ if (!this.connectTimeout && !this.closed) {
+ this.connectTimeout = setTimeout(() => {
+ this.connectTimeout = null;
+ this.connect();
+ }, 3000);
+ }
+ });
+ }
+
+ isConnected(): boolean {
+ return this._connected;
+ }
+
+ getConfig(callback: (err: boolean | string | null, result?: any) => void): void {
+ if (!this._connected) {
+ callback('not connected');
+ } else {
+ this.sendCommand(this.socket, 'get_config', callback);
+ }
+ }
+
+ getStates(callback: (err: boolean | string | null, result?: any) => void): void {
+ if (!this._connected) {
+ callback('not connected');
+ } else {
+ this.sendCommand(this.socket, 'get_states', callback);
+ }
+ }
+
+ getServices(callback: (err: boolean | string | null, result?: any) => void): void {
+ if (!this._connected) {
+ callback('not connected');
+ } else {
+ this.sendCommand(this.socket, 'get_services', callback);
+ }
+ }
+
+ getPanels(callback: (err: boolean | string | null, result?: any) => void): void {
+ if (!this._connected) {
+ callback('not connected');
+ } else {
+ this.sendCommand(this.socket, 'get_panels', callback);
+ }
+ }
+
+ callService(
+ service: string,
+ domain: string,
+ serviceData: Record,
+ target: Record,
+ callback: (err: boolean | string | null, result?: any) => void,
+ ): void {
+ if (!this._connected) {
+ callback('not connected');
+ } else {
+ this.sendCommand(this.socket, 'call_service', callback, {
+ domain: domain || '',
+ service,
+ service_data: serviceData,
+ target,
+ });
+ }
+ }
+
+ connect(): void {
+ if (this.connectTimeout) {
+ clearTimeout(this.connectTimeout);
+ this.connectTimeout = null;
+ }
+
+ this.socket = new WebSocket(
+ `ws${this.options.secure ? 's' : ''}://${this.options.host}:${this.options.port}/api/websocket`,
+ { perMessageDeflate: false },
+ );
+
+ this.initSocket(this.socket);
+ }
+
+ close(): void {
+ if (this.connectTimeout) {
+ clearTimeout(this.connectTimeout);
+ this.connectTimeout = null;
+ }
+ this.closed = true;
+ if (this.socket) {
+ this.socket.close();
+ }
+ }
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..8e21a47
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,480 @@
+import { Adapter, type AdapterOptions } from '@iobroker/adapter-core';
+import HASS from './lib/hass';
+
+interface HassAdapterConfig {
+ host: string;
+ port: number;
+ password: string;
+ secure: boolean;
+}
+
+const knownAttributes: Record = {
+ azimuth: { write: false, read: true, unit: '°' },
+ elevation: { write: false, read: true, unit: '°' },
+};
+
+const mapTypes: Record = {
+ string: 'string',
+ number: 'number',
+ object: 'mixed',
+ boolean: 'boolean',
+};
+
+const skipServices: string[] = ['persistent_notification'];
+
+class HassAdapter extends Adapter {
+ declare config: HassAdapterConfig;
+
+ private hassConnected: boolean = false;
+ private hass: HASS | null = null;
+ private readonly hassObjects: Record = {};
+ private delayTimeout: ReturnType | null = null;
+ private stopped: boolean = false;
+
+ public constructor(options: Partial = {}) {
+ super({
+ ...options,
+ name: 'hass',
+ ready: () => this.main(),
+ unload: callback => this.onUnload(callback),
+ stateChange: (id, state) => this.onStateChange(id, state),
+ });
+ }
+
+ private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
+ if (!state || state.ack) {
+ return;
+ }
+ if (!this.hassConnected) {
+ this.log.warn(`Cannot send command to "${id}", because not connected`);
+ return;
+ }
+
+ if (!this.hassObjects[id]) {
+ return;
+ }
+
+ if (!(this.hassObjects[id] as ioBroker.StateObject).common.write) {
+ this.log.warn(`Object ${id} is not writable!`);
+ return;
+ }
+
+ const serviceData: Record = {};
+ const fields: { entity_id: string } = this.hassObjects[id].native.fields;
+ const target: Record = {};
+
+ let requestFields: Record = {};
+ if (typeof state.val === 'string') {
+ state.val = state.val.trim();
+ if (state.val.startsWith('{') && state.val.endsWith('}')) {
+ try {
+ requestFields = JSON.parse(state.val) || {};
+ } catch (err) {
+ this.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
+ requestFields = {};
+ }
+ }
+ }
+
+ // If a non-JSON value was set, and we only have one relevant field, use this field as value
+ if (fields && !Object.keys(requestFields).length) {
+ const fieldList = Object.keys(fields);
+ if (fieldList.length === 1 && fieldList[0] !== 'entity_id') {
+ requestFields[fieldList[0]] = state.val;
+ } else if (fieldList.length === 2 && fields.entity_id) {
+ requestFields[fieldList[1 - fieldList.indexOf('entity_id')]] = state.val;
+ }
+ }
+
+ this.log.debug(
+ `Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`,
+ );
+
+ if (fields) {
+ for (const field in fields) {
+ if (!Object.prototype.hasOwnProperty.call(fields, field)) {
+ continue;
+ }
+ if (field === 'entity_id') {
+ target.entity_id = this.hassObjects[id].native.entity_id;
+ } else if (requestFields[field] !== undefined) {
+ serviceData[field] = requestFields[field];
+ }
+ }
+ }
+
+ const noFields = Object.keys(serviceData).length === 0;
+ serviceData.entity_id = this.hassObjects[id].native.entity_id;
+
+ this.log.debug(
+ `Send to HASS for service ${this.hassObjects[id].native.attr} with ${this.hassObjects[id].native.domain || this.hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`,
+ );
+
+ this.hass!.callService(
+ this.hassObjects[id].native.attr,
+ this.hassObjects[id].native.domain || this.hassObjects[id].native.type,
+ serviceData,
+ target,
+ err => {
+ if (err) {
+ this.log.error(`Cannot control ${id}: ${err}`);
+ }
+ if (err && fields && noFields) {
+ this.log.warn(
+ `Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`,
+ );
+ this.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
+ }
+ },
+ );
+ }
+
+ private onUnload(callback?: () => void): void {
+ this.stopped = true;
+ if (this.delayTimeout) {
+ clearTimeout(this.delayTimeout);
+ this.delayTimeout = null;
+ }
+ this.hass?.close();
+ callback?.();
+ }
+
+ private async syncStates(
+ states: { id?: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[],
+ ): Promise {
+ if (states?.length) {
+ for (const state of states) {
+ const id = state.id!;
+ delete state.id;
+
+ try {
+ await this.setForeignStateAsync(id, state);
+ } catch (err) {
+ this.log.error(err.toString());
+ }
+ }
+ }
+ }
+
+ private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise {
+ if (objects?.length) {
+ for (const obj of objects) {
+ this.hassObjects[obj._id] = obj;
+
+ try {
+ const oldObj = await this.getForeignObjectAsync(obj._id);
+ if (!oldObj) {
+ this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
+ this.hassObjects[obj._id] = obj;
+ await this.setForeignObjectAsync(obj._id, obj);
+ } else {
+ this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
+ if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
+ oldObj.native = obj.native;
+ this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
+ await this.setForeignObjectAsync(obj._id, oldObj);
+ }
+ }
+ } catch (err) {
+ this.log.error(err.toString());
+ }
+ }
+ }
+ }
+
+ private async parseStates(
+ entities: {
+ name: string;
+ attributes: { friendly_name?: string; attribution?: string; unit_of_measurement?: string };
+ entity_id: string;
+ object_id: string;
+ last_changed?: string;
+ last_updated?: string;
+ state: ioBroker.StateValue;
+ domain: string;
+ }[],
+ services: Record,
+ ): Promise {
+ const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
+ const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
+
+ for (let e = 0; e < entities.length; e++) {
+ const entity = entities[e];
+ if (!entity) {
+ continue;
+ }
+
+ const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
+ const desc = entity.attributes?.attribution || undefined;
+
+ const channel: ioBroker.ChannelObject = {
+ _id: `${this.namespace}.entities.${entity.entity_id}`,
+ common: {
+ name,
+ },
+ type: 'channel',
+ native: {
+ object_id: entity.object_id,
+ entity_id: entity.entity_id,
+ },
+ };
+ if (desc) {
+ channel.common.desc = desc;
+ }
+ objs.push(channel);
+
+ const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
+ const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
+
+ if (entity.state !== undefined) {
+ const obj: ioBroker.StateObject = {
+ _id: `${this.namespace}.entities.${entity.entity_id}.state`,
+ type: 'state',
+ common: {
+ name: `${name} STATE`,
+ type: typeof entity.state as ioBroker.CommonType,
+ role: 'state',
+ read: true,
+ write: false,
+ },
+ native: {
+ object_id: entity.object_id,
+ domain: entity.domain,
+ entity_id: entity.entity_id,
+ },
+ };
+ if (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);
+
+ let val = entity.state;
+ if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
+ val = JSON.stringify(val);
+ }
+
+ states.push({ id: obj._id, lc, ts, val, ack: true });
+ }
+
+ if (entity.attributes) {
+ for (const attr in entity.attributes) {
+ if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
+ if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
+ continue;
+ }
+
+ 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 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 });
+ }
+ }
+ }
+
+ const serviceType = entity.entity_id.split('.')[0];
+
+ if (services[serviceType] && !skipServices.includes(serviceType)) {
+ const service = services[serviceType];
+ for (const s in service) {
+ if (Object.prototype.hasOwnProperty.call(service, s)) {
+ const obj: ioBroker.StateObject = {
+ _id: `${this.namespace}.entities.${entity.entity_id}.${s}`,
+ type: 'state',
+ common: {
+ name: entity.entity_id,
+ desc: service[s].description,
+ read: false,
+ write: true,
+ type: 'mixed' as ioBroker.CommonType,
+ role: 'state',
+ },
+ native: {
+ object_id: entity.object_id,
+ domain: entity.domain,
+ fields: service[s].fields,
+ entity_id: entity.entity_id,
+ attr: s,
+ type: serviceType,
+ },
+ };
+
+ this.log.debug(
+ `Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
+ );
+
+ objs.push(obj);
+ }
+ }
+ }
+ }
+
+ await this.syncObjects(objs);
+ await this.syncStates(states);
+ }
+
+ private async main(): Promise {
+ this.config.host ||= '127.0.0.1';
+ this.config.port = parseInt(String(this.config.port), 10) || 8123;
+
+ await this.setStateAsync('info.connection', false, true);
+
+ this.hass = new HASS(this.config, this.log);
+
+ this.hass.on('error', err => this.log.error(err));
+
+ this.hass.on('state_changed', entity => {
+ this.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
+ if (!entity || typeof entity.entity_id !== 'string') {
+ return;
+ }
+
+ const id = `entities.${entity.entity_id}.`;
+ const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
+ const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
+
+ if (entity.state !== undefined) {
+ if (this.hassObjects[`${this.namespace}.${id}state`]) {
+ this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
+ } else {
+ this.log.info(
+ `State changed for unknown object ${id}state. Please restart the adapter to resync the objects.`,
+ );
+ }
+ }
+
+ if (entity.attributes) {
+ for (const attr in entity.attributes) {
+ if (
+ !Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
+ attr === 'friendly_name' ||
+ attr === 'unit_of_measurement' ||
+ attr === 'icon' ||
+ !attr.length
+ ) {
+ continue;
+ }
+ let val = entity.attributes[attr];
+ if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
+ val = JSON.stringify(val);
+ }
+ const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
+ if (this.hassObjects[`${this.namespace}.${id}state`]) {
+ this.setState(id + attrId, { val, ack: true, lc, ts });
+ } else {
+ this.log.info(
+ `State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
+ );
+ }
+ }
+ }
+ });
+
+ this.hass.on('connected', () => {
+ if (!this.hassConnected) {
+ this.log.debug('Connected');
+ this.hassConnected = true;
+ this.setState('info.connection', true, true);
+ this.hass!.getConfig(err => {
+ if (err) {
+ this.log.error(`Cannot read config: ${err}`);
+ return;
+ }
+ this.delayTimeout = setTimeout(() => {
+ this.delayTimeout = null;
+ if (!this.stopped) {
+ this.hass!.getStates((err, states) => {
+ if (this.stopped) {
+ return;
+ }
+ if (err) {
+ this.log.error(`Cannot read states: ${err}`);
+ return;
+ }
+ this.delayTimeout = setTimeout(() => {
+ this.delayTimeout = null;
+ if (!this.stopped) {
+ this.hass!.getServices(async (err, services): Promise => {
+ if (this.stopped) {
+ return;
+ }
+ if (err) {
+ this.log.error(`Cannot read states: ${err}`);
+ } else {
+ await this.parseStates(states, services);
+ this.log.debug(
+ 'Initial parsing of states done, subscribe to ioBroker states',
+ );
+ await this.subscribeStatesAsync('*');
+ }
+ });
+ }
+ }, 100);
+ });
+ }
+ }, 100);
+ });
+ }
+ });
+
+ this.hass.on('disconnected', () => {
+ if (this.hassConnected) {
+ this.log.debug('Disconnected');
+ this.hassConnected = false;
+ this.setState('info.connection', false, true);
+ }
+ });
+
+ this.hass.connect();
+ }
+}
+
+export default HassAdapter;
+
+if (require.main !== module) {
+ // Export the constructor in compact mode
+ module.exports = (options: Partial | undefined) => new HassAdapter(options);
+} else {
+ // otherwise start the instance directly
+ (() => new HassAdapter())();
+}
diff --git a/tsconfig.build.json b/tsconfig.build.json
index a1b27c1..55588af 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -1,9 +1,12 @@
{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "allowJs": false,
- "checkJs": false,
- "noEmit": false,
- "declaration": false
- }
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "allowJs": false,
+ "checkJs": false,
+ "noEmit": false,
+ "declaration": false,
+ "rootDir": "src",
+ "types": ["@iobroker/types"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts"]
}
diff --git a/tsconfig.json b/tsconfig.json
index 11dae05..12fdfe0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,27 +1,23 @@
-// Root tsconfig to set the settings and power editor support for all TS files
{
"compileOnSave": true,
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"checkJs": true,
-
- "skipLibCheck": true, // Don't report errors in 3rd party definitions
+ "skipLibCheck": true,
"noEmitOnError": true,
"outDir": "./build",
- "esModuleInterop": true,
- "resolveJsonModule": true,
+ "removeComments": false,
"module": "Node16",
"moduleResolution": "node16",
- "sourceMap": true, // required for debug
- "inlineSources": true, // required for debug
-
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
"strict": true,
"target": "es2022",
+ "sourceMap": true,
"inlineSourceMap": false,
"useUnknownInCatchVariables": false,
- "types": ["node", "@iobroker/types"]
+ "types": ["@iobroker/types"]
},
- "include": ["src/**/*.*", "src/**/*.d.*"],
- "exclude": ["build/**", "node_modules/**", "src-admin/**"]
+ "include": ["src/**/*.ts", "src/**/*.d.ts"]
}