MIgrate adapter to Typescript

This commit is contained in:
GermanBluefox
2026-03-31 19:44:44 +02:00
parent 92140fe771
commit ebca1ad36c
39 changed files with 1176 additions and 1325 deletions
+15
View File
@@ -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:*)"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
node_modules node_modules
.idea .idea
tmp tmp
build
admin/i18n/flat.txt admin/i18n/flat.txt
admin/i18n/*/flat.txt admin/i18n/*/flat.txt
iob_npm.done iob_npm.done
+4 -1
View File
@@ -1,3 +1,6 @@
{ {
"plugins": ["iobroker", "license"] "plugins": ["iobroker", "license"],
"exec": {
"before_commit": "npm run build"
}
} }
+66
View File
@@ -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.
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?:"
}
+8
View File
@@ -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 ?"
}
-8
View File
@@ -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 ?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -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?"
}
-8
View File
@@ -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?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Домашний помощник IP",
"Home assistant WS Port": "Домашний помощник WS Порт",
"Password repeat": "Повтор пароля",
"Password": "Пароль",
"Passwords missmatch!": "Пароли не совпадают!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Домашний помощник IP:",
"Home assistant WS Port:": "Домашний помощник WS Порт:",
"Password repeat:": "Повтор пароля:",
"Password:": "Пароль:",
"Passwords missmatch!": "Пароли не совпадают!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Домашній помічник IP",
"Home assistant WS Port": "Домашній помічник WS Порт",
"Password repeat": "Повторення пароля",
"Password": "Пароль",
"Passwords missmatch!": "Паролі не збігаються!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Домашній помічник IP:",
"Home assistant WS Port:": "Домашній помічник WS Порт:",
"Password repeat:": "Повторення пароля:",
"Password:": "Пароль:",
"Passwords missmatch!": "Паролі не збігаються!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "家庭助理IP",
"Home assistant WS Port": "家庭助理 WS 端口:",
"Password repeat": "密码重复:",
"Password": "密码:",
"Passwords missmatch!": "密码不匹配!",
"Secure": "HTTPS"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "家庭助理IP",
"Home assistant WS Port:": "家庭助理 WS 端口:",
"Password repeat:": "密码重复:",
"Password:": "密码:",
"Passwords missmatch!": "密码不匹配!",
"Secure:": "HTTPS"
}
-108
View File
@@ -1,108 +0,0 @@
<html>
<head>
<!-- Materialze style -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (settings.password === undefined) settings.password = '';
settings.passwordRepeat = settings.password;
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$value.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
// Signal to admin, that no changes yet
onChange(false);
}
function save(callback) {
var obj = {};
if ($('#password').val() !== $('#passwordRepeat').val()) {
showMessage(_('Passwords missmatch!'), _('Warning'), 'alert');
return;
}
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
delete obj.passwordRepeat;
callback(obj);
}
</script>
</head>
<body>
<div class="m adapter-container">
<div class="row">
<div class="col s12 m4 l2">
<img src="hass.png" class="logo">
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s4 input-field">
<input type="text" class="value" id="host" />
<label for="host" class="translate">Home assistant IP:</label>
</div>
<div class="col s4 input-field">
<input type="text" class="value" id="port" size="5" maxlength="5"/>
<label for="port" class="translate">Home assistant WS Port:</label>
</div>
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s6 m4 input-field">
<input type="password" class="value" id="password" />
<label for="password" class="translate">Password:</label>
</div>
<div class="col s6 m4 input-field">
<input type="password" class="value" id="passwordRepeat" />
<label for="passwordRepeat" class="translate">Password repeat:</label>
</div>
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s6 m4 input-field">
<input id="secure" type="checkbox" class="value" />
<span for="secure" class="translate">Secure:</span>
</div>
</div>
</div>
</div>
</body>
</html>
+44
View File
@@ -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
}
}
}
-17
View File
@@ -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"},
};
+3 -4
View File
@@ -23,14 +23,13 @@ export default [
}, },
{ {
ignores: [ ignores: [
'node_modules/**/*',
'build/**/*', 'build/**/*',
'src/lib/**/*.js',
'admin/**/*', 'admin/**/*',
'test/**/*', 'test/**/*',
'src-admin/**/*', 'src-admin/**/*',
'src-rules/**/*', 'tmp/**/*',
'tasks.js', '**/*.mjs',
'**/*.mjs'
], ],
}, },
]; ];
+14 -12
View File
@@ -5,16 +5,16 @@
"title": "Home Assistant", "title": "Home Assistant",
"titleLang": { "titleLang": {
"en": "Home Assistant", "en": "Home Assistant",
"de": "Home-Assistent", "de": "Home Assistant",
"ru": "Домашний помощник", "ru": "Home Assistant",
"pt": "Home Assistant", "pt": "Home Assistant",
"nl": "Thuisassistent", "nl": "Home Assistant",
"fr": "Assistante à domicile", "fr": "Home Assistant",
"it": "Assistente domiciliare", "it": "Home Assistant",
"es": "Asistente de hogar", "es": "Home Assistant",
"pl": "Asystent domowy", "pl": "Home Assistant",
"zh-cn": "家庭助理", "zh-cn": "Home Assistant",
"uk": "Домашній помічник" "uk": "Home Assistant"
}, },
"desc": { "desc": {
"en": "Home Assistant connection for ioBroker", "en": "Home Assistant connection for ioBroker",
@@ -128,7 +128,9 @@
"icon": "hass.png", "icon": "hass.png",
"enabled": true, "enabled": true,
"compact": true, "compact": true,
"materialize": true, "adminUI": {
"config": "json"
},
"license": "MIT", "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": [
@@ -144,12 +146,12 @@
], ],
"dependencies": [ "dependencies": [
{ {
"js-controller": ">=3.0.0" "js-controller": ">=6.0.11"
} }
], ],
"globalDependencies": [ "globalDependencies": [
{ {
"admin": ">=5.1.28" "admin": ">=6.0.0"
} }
], ],
"plugins": { "plugins": {
-282
View File
@@ -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;
-471
View File
@@ -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();
}
+183 -318
View File
@@ -10,7 +10,6 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iobroker/adapter-core": "^3.3.2", "@iobroker/adapter-core": "^3.3.2",
"websocket": "^1.0.35",
"ws": "^8.20.0" "ws": "^8.20.0"
}, },
"devDependencies": { "devDependencies": {
@@ -18,9 +17,11 @@
"@alcalzone/release-script-plugin-iobroker": "^5.1.2", "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"@alcalzone/release-script-plugin-license": "^5.1.1", "@alcalzone/release-script-plugin-license": "^5.1.1",
"@iobroker/adapter-dev": "^1.5.0", "@iobroker/adapter-dev": "^1.5.0",
"@iobroker/build-tools": "^3.0.1",
"@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",
"mocha": "^11.7.5" "mocha": "^11.7.5"
} }
}, },
@@ -1305,6 +1306,102 @@
"node": "^20.19.0 || ^22.12.0 || >=23" "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": { "node_modules/@iobroker/eslint-config": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz",
@@ -1754,6 +1851,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.0", "version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", "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==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "node_modules/builtin-modules": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
@@ -3013,15 +3111,6 @@
"node": ">= 8" "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": { "node_modules/data-uri-to-buffer": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "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" "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": { "node_modules/deep-eql": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -3476,40 +3557,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -4143,25 +4190,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/espree": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -4239,15 +4267,6 @@
"node": ">=0.10.0" "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": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4272,19 +4291,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1" "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": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5576,11 +5582,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -6118,11 +6119,6 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/nano-spawn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@@ -6143,11 +6139,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/node-domexception": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -6221,6 +6212,7 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
"optional": true,
"bin": { "bin": {
"node-gyp-build": "bin.js", "node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js", "node-gyp-build-optional": "optional.js",
@@ -7615,11 +7607,6 @@
"strip-bom": "^3.0.0" "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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -7721,14 +7708,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/typescript": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
@@ -7853,15 +7832,6 @@
"punycode": "^2.1.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7889,23 +7859,6 @@
"node": ">= 8" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8100,14 +8053,6 @@
"node": ">=10" "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": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "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": { "@iobroker/eslint-config": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@iobroker/eslint-config/-/eslint-config-2.2.0.tgz",
@@ -9276,6 +9283,15 @@
"integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==",
"dev": true "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": { "@typescript-eslint/eslint-plugin": {
"version": "8.58.0", "version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", "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==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "builtin-modules": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
@@ -10062,15 +10070,6 @@
"which": "^2.0.1" "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": { "data-uri-to-buffer": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "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" "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": { "deep-eql": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -10413,36 +10404,6 @@
"is-symbol": "^1.0.4" "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": { "esbuild": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -10889,24 +10850,6 @@
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true "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": { "espree": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -10956,15 +10899,6 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true "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": { "execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -10982,21 +10916,6 @@
"strip-final-newline": "^2.0.0" "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": { "extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -11791,11 +11710,6 @@
"which-typed-array": "^1.1.16" "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": { "is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "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": { "nano-spawn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@@ -12190,11 +12099,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "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": { "node-domexception": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -12233,9 +12137,9 @@
} }
}, },
"node-gyp-build": { "node-gyp-build": {
"version": "4.2.3", "version": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" "optional": true
}, },
"node-releases": { "node-releases": {
"version": "2.0.36", "version": "2.0.36",
@@ -13172,11 +13076,6 @@
"strip-bom": "^3.0.0" "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": { "type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -13245,14 +13144,6 @@
"reflect.getprototypeof": "^1.0.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": { "typescript": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
@@ -13324,14 +13215,6 @@
"punycode": "^2.1.0" "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13350,19 +13233,6 @@
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"dev": true "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": { "which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -13485,11 +13355,6 @@
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true "dev": true
}, },
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
},
"yargs": { "yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+13 -6
View File
@@ -25,7 +25,6 @@
"url": "https://github.com/ioBroker/ioBroker.hass" "url": "https://github.com/ioBroker/ioBroker.hass"
}, },
"dependencies": { "dependencies": {
"websocket": "^1.0.35",
"ws": "^8.20.0", "ws": "^8.20.0",
"@iobroker/adapter-core": "^3.3.2" "@iobroker/adapter-core": "^3.3.2"
}, },
@@ -34,20 +33,28 @@
"@alcalzone/release-script-plugin-iobroker": "^5.1.2", "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"@alcalzone/release-script-plugin-license": "^5.1.1", "@alcalzone/release-script-plugin-license": "^5.1.1",
"@iobroker/adapter-dev": "^1.5.0", "@iobroker/adapter-dev": "^1.5.0",
"@iobroker/build-tools": "^3.0.1",
"@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",
"mocha": "^11.7.5" "mocha": "^11.7.5"
}, },
"main": "main.js", "main": "build/main.js",
"files": [ "files": [
"lib/", "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
"admin/", "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
"build/",
"io-package.json", "io-package.json",
"main.js" "LICENSE"
], ],
"scripts": { "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": "release-script",
"release-patch": "release-script patch --yes", "release-patch": "release-script patch --yes",
"release-minor": "release-script minor --yes", "release-minor": "release-script minor --yes",
+248
View File
@@ -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<number, string> = {
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<number, HassRequest> = {};
private _connected: boolean = false;
private connectTimeout: ReturnType<typeof setTimeout> | 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<string, any>,
): 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<string, any>,
target: Record<string, any>,
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();
}
}
}
+480
View File
@@ -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<string, { write: boolean; read: boolean; unit: string }> = {
azimuth: { write: false, read: true, unit: '°' },
elevation: { write: false, read: true, unit: '°' },
};
const mapTypes: Record<string, ioBroker.CommonType> = {
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<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
private stopped: boolean = false;
public constructor(options: Partial<AdapterOptions> = {}) {
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<string, any> = {};
const fields: { entity_id: string } = this.hassObjects[id].native.fields;
const target: Record<string, any> = {};
let requestFields: Record<string, any> = {};
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<void> {
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<void> {
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<string, { [serviceType: string]: { description: string; fields: { entity_id: string } } }>,
): Promise<void> {
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<void> {
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<void> => {
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<AdapterOptions> | undefined) => new HassAdapter(options);
} else {
// otherwise start the instance directly
(() => new HassAdapter())();
}
+5 -2
View File
@@ -4,6 +4,9 @@
"allowJs": false, "allowJs": false,
"checkJs": false, "checkJs": false,
"noEmit": false, "noEmit": false,
"declaration": false "declaration": false,
} "rootDir": "src",
"types": ["@iobroker/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
} }
+7 -11
View File
@@ -1,27 +1,23 @@
// Root tsconfig to set the settings and power editor support for all TS files
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"noEmit": true, "noEmit": true,
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"skipLibCheck": true,
"skipLibCheck": true, // Don't report errors in 3rd party definitions
"noEmitOnError": true, "noEmitOnError": true,
"outDir": "./build", "outDir": "./build",
"esModuleInterop": true, "removeComments": false,
"resolveJsonModule": true,
"module": "Node16", "module": "Node16",
"moduleResolution": "node16", "moduleResolution": "node16",
"sourceMap": true, // required for debug "esModuleInterop": true,
"inlineSources": true, // required for debug "resolveJsonModule": true,
"strict": true, "strict": true,
"target": "es2022", "target": "es2022",
"sourceMap": true,
"inlineSourceMap": false, "inlineSourceMap": false,
"useUnknownInCatchVariables": false, "useUnknownInCatchVariables": false,
"types": ["node", "@iobroker/types"] "types": ["@iobroker/types"]
}, },
"include": ["src/**/*.*", "src/**/*.d.*"], "include": ["src/**/*.ts", "src/**/*.d.ts"]
"exclude": ["build/**", "node_modules/**", "src-admin/**"]
} }