MIgrate adapter to Typescript
This commit is contained in:
@@ -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,6 +1,7 @@
|
||||
node_modules
|
||||
.idea
|
||||
tmp
|
||||
build
|
||||
admin/i18n/flat.txt
|
||||
admin/i18n/*/flat.txt
|
||||
iob_npm.done
|
||||
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"plugins": ["iobroker", "license"]
|
||||
"plugins": ["iobroker", "license"],
|
||||
"exec": {
|
||||
"before_commit": "npm run build"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?:"
|
||||
}
|
||||
@@ -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 ?"
|
||||
}
|
||||
@@ -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 ?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -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?"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Home assistant IP": "Домашний помощник IP",
|
||||
"Home assistant WS Port": "Домашний помощник WS Порт",
|
||||
"Password repeat": "Повтор пароля",
|
||||
"Password": "Пароль",
|
||||
"Passwords missmatch!": "Пароли не совпадают!",
|
||||
"Secure": "HTTPS?"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Home assistant IP:": "Домашний помощник IP:",
|
||||
"Home assistant WS Port:": "Домашний помощник WS Порт:",
|
||||
"Password repeat:": "Повтор пароля:",
|
||||
"Password:": "Пароль:",
|
||||
"Passwords missmatch!": "Пароли не совпадают!",
|
||||
"Secure:": "HTTPS?"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Home assistant IP": "Домашній помічник IP",
|
||||
"Home assistant WS Port": "Домашній помічник WS Порт",
|
||||
"Password repeat": "Повторення пароля",
|
||||
"Password": "Пароль",
|
||||
"Passwords missmatch!": "Паролі не збігаються!",
|
||||
"Secure": "HTTPS?"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Home assistant IP:": "Домашній помічник IP:",
|
||||
"Home assistant WS Port:": "Домашній помічник WS Порт:",
|
||||
"Password repeat:": "Повторення пароля:",
|
||||
"Password:": "Пароль:",
|
||||
"Passwords missmatch!": "Паролі не збігаються!",
|
||||
"Secure:": "HTTPS?"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Home assistant IP": "家庭助理IP:",
|
||||
"Home assistant WS Port": "家庭助理 WS 端口:",
|
||||
"Password repeat": "密码重复:",
|
||||
"Password": "密码:",
|
||||
"Passwords missmatch!": "密码不匹配!",
|
||||
"Secure": "HTTPS?"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Home assistant IP:": "家庭助理IP:",
|
||||
"Home assistant WS Port:": "家庭助理 WS 端口:",
|
||||
"Password repeat:": "密码重复:",
|
||||
"Password:": "密码:",
|
||||
"Passwords missmatch!": "密码不匹配!",
|
||||
"Secure:": "HTTPS?"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -23,14 +23,13 @@ export default [
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**/*',
|
||||
'build/**/*',
|
||||
'src/lib/**/*.js',
|
||||
'admin/**/*',
|
||||
'test/**/*',
|
||||
'src-admin/**/*',
|
||||
'src-rules/**/*',
|
||||
'tasks.js',
|
||||
'**/*.mjs'
|
||||
'tmp/**/*',
|
||||
'**/*.mjs',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
+14
-12
@@ -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": {
|
||||
|
||||
-282
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
Generated
+183
-318
@@ -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",
|
||||
|
||||
+13
-6
@@ -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",
|
||||
|
||||
+248
@@ -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
@@ -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())();
|
||||
}
|
||||
+10
-7
@@ -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"]
|
||||
}
|
||||
|
||||
+7
-11
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user