Compare commits

...
68 Commits
Author SHA1 Message Date
GermanBluefox fbabfaae94 chore: release v2.0.1
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
2026-03-31 20:15:35 +02:00
GermanBluefox 8b6e297800 Fix CI 2026-03-31 20:13:36 +02:00
GermanBluefox f0b6a23ad6 chore: release v2.0.0
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
2026-03-31 20:10:03 +02:00
GermanBluefox 4905caf6c2 Typos 2026-03-31 20:09:51 +02:00
GermanBluefox 11a4b0bafe Remove settings for claude 2026-03-31 20:07:45 +02:00
GermanBluefox ebca1ad36c MIgrate adapter to Typescript 2026-03-31 19:44:44 +02:00
GermanBluefox 92140fe771 Update packages 2026-03-31 17:00:15 +02:00
dependabot[bot]andGitHub 7bf04e494a Bump follow-redirects from 1.15.4 to 1.15.6 (#108) 2024-03-16 22:50:14 +00:00
dependabot[bot]andGitHub ed5cfbc952 Bump es5-ext from 0.10.53 to 0.10.63 (#105) 2024-02-27 12:56:00 +00:00
dependabot[bot]andGitHub 030aeaf0ea Bump follow-redirects from 1.15.1 to 1.15.4 (#101) 2024-01-10 05:04:34 +00:00
dependabot[bot]andGitHub 440f672f9e Bump @alcalzone/release-script from 3.6.0 to 3.7.0 (#96) 2023-12-01 07:16:58 +00:00
BluefoxandGitHub 0c9b1b1ed1 Merge pull request #92 from ioBroker/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-11-06 09:37:37 +01:00
BluefoxandGitHub 0090c6cfe9 Merge pull request #94 from ioBroker/dependabot/github_actions/actions/setup-node-4
Bump actions/setup-node from 3 to 4
2023-11-06 09:32:58 +01:00
dependabot[bot]andGitHub 42100114c6 Bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 03:19:50 +00:00
dependabot[bot]andGitHub a4039286c7 Bump chai from 4.3.8 to 4.3.10 (#90) 2023-10-01 04:56:17 +00:00
dependabot[bot]andGitHub c497e40bef Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 03:15:33 +00:00
dependabot[bot]andGitHub 5573276759 Bump chai from 4.3.7 to 4.3.8 (#89) 2023-09-01 08:00:49 +00:00
dependabot[bot]andGitHub 8ad88617c0 Bump @alcalzone/release-script from 3.5.9 to 3.6.0 (#87) 2023-08-01 07:32:35 +00:00
dependabot[bot]andGitHub f7ba3f0cad Bump @alcalzone/release-script-plugin-iobroker from 3.5.9 to 3.6.0 (#85) 2023-08-01 04:45:54 +00:00
dependabot[bot]andGitHub 4721397d40 Bump word-wrap from 1.2.3 to 1.2.4 (#83) 2023-07-20 02:56:12 +00:00
BluefoxandGitHub ff423942fc Merge pull request #78 from ioBroker/dependabot/npm_and_yarn/ws-8.13.0
Bump ws from 8.12.1 to 8.13.0
2023-04-05 09:01:58 +02:00
dependabot[bot]andGitHub 7129b7f231 Bump ws from 8.12.1 to 8.13.0
Bumps [ws](https://github.com/websockets/ws) from 8.12.1 to 8.13.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.12.1...8.13.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 05:19:02 +00:00
dependabot[bot]andGitHub 97ca358ea5 Bump @iobroker/adapter-core from 2.6.7 to 2.6.8 (#77) 2023-04-01 04:50:47 +00:00
BluefoxandGitHub ddea7eb899 Merge pull request #76 from ioBrokerTranslator/weblate-adapters-hass
Translations update from ioBroker Translation System
2023-03-29 20:12:46 +02:00
diemadeandWeblate 781d103d93 Translated using Weblate (German)
Currently translated at 100.0% (6 of 6 strings)

Translation: ioBroker Adapters/hass
Translate-URL: https://weblate.iobroker.net/projects/adapters/hass/de/
2023-03-20 02:43:55 +01:00
dependabot[bot]andGitHub c211767bb3 Bump ws from 8.12.0 to 8.12.1 (#75) 2023-03-01 07:18:12 +00:00
BluefoxandGitHub c18cc4f9f8 Merge pull request #74 from ioBroker/dependabot/npm_and_yarn/ws-8.12.0
Bump ws from 8.11.0 to 8.12.0
2023-02-27 14:23:46 +01:00
dependabot[bot]andGitHub 90def09878 Bump ws from 8.11.0 to 8.12.0
Bumps [ws](https://github.com/websockets/ws) from 8.11.0 to 8.12.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.12.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-01 04:47:11 +00:00
Ingo Fischer 976c611f97 chore: release v1.4.0
* (Apollon77) Added more guidance logging when setting services incorrectly
* (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
2023-01-03 22:53:58 +01:00
Ingo Fischer 3887500a62 * update license years 2023-01-03 22:53:11 +01:00
Ingo Fischer 62ef7cabf8 * detect json better 2023-01-03 00:38:58 +01:00
dependabot[bot]andGitHub 8fc6f4ac01 Bump mocha from 10.1.0 to 10.2.0 (#72) 2023-01-01 04:47:06 +00:00
Ingo Fischer 649bdfce2f * fix 2022-12-31 09:22:56 +01:00
Ingo Fischer e0fcc28746 * add translations 2022-12-22 23:29:01 +01:00
Ingo FischerandGitHub 71813c2df9 Merge pull request #71 from ioBrokerTranslator/weblate-adapters-hass
Translations update from ioBroker Translation System
2022-12-22 23:27:42 +01:00
mcm1957andWeblate ac8d3617b0 Translated using Weblate (Spanish)
Currently translated at 0.0% (0 of 6 strings)

Translation: ioBroker Adapters/hass
Translate-URL: https://weblate.iobroker.net/projects/adapters/hass/es/
2022-12-22 21:28:58 +01:00
Ingo Fischer 2986071d2e * update readme 2022-12-21 20:34:47 +01:00
Ingo Fischer 56a9cbbb1c * (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
2022-12-08 23:31:18 +01:00
Ingo FischerandGitHub 5a8e22b408 Merge pull request #70 from ioBroker/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-08 23:12:34 +01:00
dependabot[bot]andGitHub 1c46a7772f Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 22:09:10 +00:00
Ingo Fischer 659152be2e * add more guidance on how to call 2022-12-08 23:08:37 +01:00
Ingo Fischer ad8f360c17 * add adapter-dev, remove gulp
* add more guidance on how to call
* update deps
2022-12-08 23:06:36 +01:00
dependabot[bot]andGitHub 88d027d575 Update axios requirement from ^1.1.3 to ^1.2.0 (#67) 2022-12-01 11:50:51 +00:00
dependabot[bot]andGitHub 9727116d46 Update chai requirement from ^4.3.6 to ^4.3.7 (#66) 2022-12-01 08:12:36 +00:00
BluefoxandGitHub 03fba79027 Merge pull request #60 from ioBroker/dependabot/npm_and_yarn/ws-tw-8.10.0
Update ws requirement from ^8.8.1 to ^8.10.0
2022-12-01 01:42:19 +01:00
BluefoxandGitHub 50fd571f0b Merge pull request #63 from ioBroker/dependabot/npm_and_yarn/axios-1.1.3
Bump axios from 0.27.2 to 1.1.3
2022-12-01 01:42:04 +01:00
BluefoxandGitHub 39bd989a41 Merge pull request #64 from lgtm-migrator/codeql
Add CodeQL workflow for GitHub code scanning
2022-12-01 01:41:54 +01:00
LGTM Migrator 407b5bfb07 Add CodeQL workflow for GitHub code scanning 2022-11-10 18:24:33 +00:00
dependabot[bot]andGitHub cdc3aba0b2 Update ws requirement from ^8.8.1 to ^8.10.0
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.8.1...8.10.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 09:23:11 +00:00
dependabot[bot]andGitHub 8a2933411f Update @iobroker/adapter-core requirement from ^2.6.6 to ^2.6.7 (#62) 2022-11-01 09:21:45 +00:00
dependabot[bot]andGitHub 68c6bd989f Bump axios from 0.27.2 to 1.1.3
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 1.1.3.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v1.1.3)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 06:06:26 +00:00
dependabot[bot]andGitHub e03a2cfc80 Update mocha requirement from ^10.0.0 to ^10.1.0 (#61) 2022-11-01 06:04:19 +00:00
dependabot[bot]andGitHub 9558c39deb Update @iobroker/adapter-core requirement from ^2.6.0 to ^2.6.6 (#58) 2022-10-01 04:01:09 +00:00
dependabot[bot]andGitHub 2956ee506a Update ws requirement from ^8.8.0 to ^8.8.1 (#57) 2022-08-01 03:13:53 +00:00
Ingo Fischer 62ce173d22 chore: release v1.3.0
* (Apollon77) Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped
2022-07-01 10:10:38 +02:00
Ingo Fischer 45b0550e0c * add logging 2022-06-30 17:42:24 +02:00
Ingo Fischer 0ac6c66aaf * (Apollon77) Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped 2022-06-30 11:51:23 +02:00
Ingo Fischer b54916d743 chore: release v1.2.0
* (Apollon77) IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!
* (Apollon77) make sure a "null" value in state changes is not crashing
2022-06-17 23:23:11 +02:00
Ingo Fischer 52d8bddb11 * (Apollon77) make sure a "null" value in state changes is not crashing 2022-06-17 23:20:26 +02:00
dependabot[bot]andGitHub 271bd03d14 Update @alcalzone/release-script requirement from ^3.5.8 to ^3.5.9 (#53) 2022-06-01 09:45:24 +00:00
dependabot[bot]andGitHub 36bc7c99fd Update @alcalzone/release-script-plugin-license requirement (#51) 2022-06-01 07:48:32 +00:00
dependabot[bot]andGitHub cc76a49d62 Update @alcalzone/release-script-plugin-iobroker requirement (#54) 2022-06-01 07:05:24 +00:00
dependabot[bot]andGitHub 7a2475ba02 Update @alcalzone/release-script-plugin-license requirement (#47) 2022-05-01 14:15:54 +00:00
dependabot[bot]andGitHub 47cb4cfad8 Update @alcalzone/release-script requirement from ^3.5.6 to ^3.5.8 (#49) 2022-05-01 11:54:23 +00:00
dependabot[bot]andGitHub 2a473f88b0 Update @alcalzone/release-script-plugin-iobroker requirement (#48) 2022-05-01 09:16:22 +00:00
dependabot[bot]andGitHub 245032a178 Bump axios from 0.26.1 to 0.27.2 (#46) 2022-05-01 05:37:18 +00:00
BluefoxandGitHub 96e6c5b471 Merge pull request #45 from ioBroker/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-04-03 14:39:36 +02:00
dependabot[bot]andGitHub 82614cdad0 Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-01 04:20:10 +00:00
52 changed files with 14715 additions and 2681 deletions
+1 -1
View File
@@ -12,5 +12,5 @@ updates:
interval: monthly
time: "04:00"
timezone: Europe/Berlin
open-pull-requests-limit: 20
open-pull-requests-limit: 5
versioning-strategy: increase
+41
View File
@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: "4 5 * * 5"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check if PR should be auto-merged
uses: ahmadnassri/action-dependabot-auto-merge@v2
+27 -113
View File
@@ -1,6 +1,3 @@
# This is a composition of lint and test scripts
# Make sure to update this file along with the others
name: Test and Release
# Run this job on all pushes and pull requests
@@ -8,14 +5,19 @@ name: Test and Release
on:
push:
branches:
- '*'
- "master"
tags:
# normal versions
- "v?[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+"
# pre-releases
- "v?[0-9]+.[0-9]+.[0-9]+-**"
- "v[0-9]+.[0-9]+.[0-9]+-**"
pull_request: {}
# Cancel previous PR/branch runs when a new commit is pushed
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
# Performs quick checks before the expensive test runs
check-and-lint:
@@ -23,134 +25,46 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- uses: ioBroker/testing-action-check@v1
with:
node-version: ${{ matrix.node-version }}
node-version: '22.x'
lint: true
- name: Install Dependencies
run: npm install
# - name: Perform a type check
# run: npm run check:ts
# env:
# CI: true
# - name: Lint TypeScript code
# run: npm run lint
# - name: Test package files
# run: npm run test:package
# Runs adapter tests on all supported node versions and OSes
adapter-tests:
if: contains(github.event.head_commit.message, '[skip ci]') == false
needs: [check-and-lint]
if: contains(github.event.head_commit.message, '[skip ci]') == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [20.x, 22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- uses: ioBroker/testing-action-adapter@v1
with:
node-version: ${{ matrix.node-version }}
os: ${{ matrix.os }}
- name: Install Dependencies
run: npm install
- name: Run local tests
run: npm test
# - name: Run unit tests
# run: npm run test:unit
# - name: Run integration tests # (linux/osx)
# if: startsWith(runner.OS, 'windows') == false
# run: DEBUG=testing:* npm run test:integration
# - name: Run integration tests # (windows)
# if: startsWith(runner.OS, 'windows')
# run: set DEBUG=testing:* & npm run test:integration
# Deploys the final package to NPM
deploy:
needs: [adapter-tests]
needs: [check-and-lint, adapter-tests]
# Permissions are required to create GitHub releases and npm trusted publishing
permissions:
contents: write
id-token: write
# Trigger this step only when a commit on master is tagged with a version number
if: |
contains(github.event.head_commit.message, '[skip ci]') == false &&
github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/')
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- uses: ioBroker/testing-action-deploy@v1
with:
node-version: ${{ matrix.node-version }}
- name: Extract the version and commit body from the tag
id: extract_release
# The body may be multiline, therefore we need to escape some characters
run: |
VERSION="${{ github.ref }}"
VERSION=${VERSION##*/}
VERSION=${VERSION##*v}
echo "::set-output name=VERSION::$VERSION"
BODY=$(git show -s --format=%b)
BODY="${BODY//'%'/'%25'}"
BODY="${BODY//$'\n'/'%0A'}"
BODY="${BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$BODY"
- name: Install Dependencies
run: npm install
# - name: Create a clean build
# run: npm run build
- name: Publish package to npm
run: |
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami
npm publish
- name: Create Github Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release v${{ steps.extract_release.outputs.VERSION }}
draft: false
# Prerelease versions create prereleases on Github
prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }}
body: ${{ steps.extract_release.outputs.BODY }}
- name: Notify Sentry.io about the release
run: |
npm i -g @sentry/cli
export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
export SENTRY_URL=https://sentry.iobroker.net
export SENTRY_ORG=iobroker
export SENTRY_PROJECT=iobroker-hass
export SENTRY_VERSION=iobroker.hass@${{ steps.extract_release.outputs.VERSION }}
sentry-cli releases new $SENTRY_VERSION
sentry-cli releases set-commits $SENTRY_VERSION --auto
sentry-cli releases finalize $SENTRY_VERSION
# Add the following line BEFORE finalize if sourcemap uploads are needed
# sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/
node-version: '22.x'
github-token: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -1,7 +1,9 @@
node_modules
.idea
tmp
build
admin/i18n/flat.txt
admin/i18n/*/flat.txt
iob_npm.done
package-lock.json
.claude/settings.local.json
-5
View File
@@ -1,5 +0,0 @@
{
"require": [
"./test/mocha.setup.js"
]
}
-12
View File
@@ -1,12 +0,0 @@
/**/*
/gulpfile.js
gulpfile.js
!/admin/**/*
!/admin/*
!/lib/**/*
!/lib/*
!/io-package.json
!/package.json
!/LICENSE
!/main.js
!/README.md
+4 -1
View File
@@ -1,3 +1,6 @@
{
"plugins": ["iobroker", "license"]
"plugins": ["iobroker", "license"],
"exec": {
"before_commit": "npm run build"
}
}
+66
View File
@@ -0,0 +1,66 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ioBroker.hass is an ioBroker adapter that connects Home Assistant to ioBroker via WebSocket API. It reads Home Assistant entities/services and exposes them as ioBroker objects, and forwards ioBroker state changes back as Home Assistant service calls.
## Commands
```bash
# Build TypeScript
npm run build
# Run all tests (Mocha)
npm test
# Run only package validation tests
npm run test:package
# Lint
npm run lint
# Format check
npx prettier --check .
# Translate adapter strings
npm run translate
# Release (patch/minor/major)
npm run release-patch
npm run release-minor
npm run release-major
```
## Architecture
TypeScript class-based adapter. Source in `src/`, compiled output in `build/`.
- **src/main.ts** — `HassAdapter` class extending `Adapter`. Daemon mode entry point. Handles:
- Connecting to Home Assistant and syncing entities/services into ioBroker objects
- `parseStates()` — maps HASS entities, attributes, and services to ioBroker channels/states
- `onStateChange()` — converts ioBroker commands (`ack=false`) into HASS `callService()` calls, supporting both direct values (single-field services) and JSON-stringified objects (multi-field services)
- Object/state synchronization via `syncObjects`/`syncStates`
- **src/lib/hass.ts** — `HASS` class extending `EventEmitter`. WebSocket client for Home Assistant with:
- Auto-reconnect (3s delay)
- Message ID tracking for request-response correlation
- Methods: `getConfig`, `getStates`, `getServices`, `getPanels`, `callService`
- Events: `connected`, `disconnected`, `error`, `state_changed`
The codebase uses **callback-based async** (no promises/async-await).
## Testing
Tests use Mocha with two ioBroker-specific frameworks:
- `@iobroker/legacy-testing` — spins up a js-controller instance for integration tests (`test/testAdapter.js`)
- `@iobroker/testing` — validates package.json and io-package.json structure (`test/testPackageFiles.js`)
## Configuration
Adapter config (defined in `io-package.json`): `host`, `port`, `password` (long-lived access token), `secure` (boolean for wss).
## CI
GitHub Actions runs lint on Node 22, adapter tests on Node 20/22/24 across Linux/Windows/macOS. Deploys to npm on semantic version tags via OIDC trusted publishing.
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018-2022 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2026 bluefox <dogafox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+47 -19
View File
@@ -15,16 +15,26 @@
This adapter allows the connecting of Home Assistant to ioBroker.
## Usage
Create a long term token in HASS and use it as PW (copy it also in the repeat field).
Create a long-term token in HASS and use it as PW (copy it also in the repeat field).
Then it should read out all attributes for all devices. Services might be controllable (e.g. "turn_on"). To control services you have two options:
* Set the state with an ack=false value which is not a string (e.g. Boolean true) then it will be triggered also in HASS without additional service data
* Set the state with an ack=false String value which is a stringified JSON object to call the service and use the JSON object as service data
Then it should read out all attributes for all devices. Services might be controllable (e.g. "turn_on"). To control services, you have two options:
For the last option on a light.turn_off with e.g. `{"transition":10,"flash":"short"}` these two service data details are sent with the call to HASS. The available fields with their data definition can be seen in the JSON definition of the ioBroker object in the native.fields section and would look like the following in the above example:
### Set a direct value
Set the state with an ack=false value which is not a string (e.g. Boolean true) then it will be triggered also in HASS without additional service data. This will only work if the service has one field to be sent - then the value is sent as this field! If the service has more than one field, you will find a warning in the log that provides more details about the fields that are possible to be sent, e.g.
`
...
```
Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!
Allowed field keys are: temperature, target_temp_high, target_temp_low, hvac_mode
```
### Set a stringified JSON to provide one or multiple fields
Set the state with an ack=false String value which is a stringified JSON object to call the service and use the JSON object as service data
For the last option on a light.turn_off with e.g. `{"transition":10,"flash":"short"}` these two service data details are sent with the call to HASS. The available fields with their exact data definition can be seen in the JSON definition of the ioBroker object in the `native` fields section and would look like the following in the above example:
```json5
{
// ...
native: {
"fields": {
"transition": {
@@ -56,13 +66,15 @@ For the last option on a light.turn_off with e.g. `{"transition":10,"flash":"sho
"attr": "turn_off",
"type": "light"
}
...
`
For some services like set_speed it is required to call with a JSON object like `{speed: "high"}` in general to provide required values. In this case the field definition look e.g. like:
//...
}
```
...
For some services like set_speed it is required to call with a JSON object like `{speed: "high"}` in general to provide required values. In this case the field definition looks e.g. like:
```json5
{
//...
native: {
"fields": {
"speed": {
@@ -75,9 +87,10 @@ For some services like set_speed it is required to call with a JSON object like
}
}
}
...
// ...
}
// ...
}
...
```
## Configuration
@@ -85,14 +98,29 @@ There is a good article about connection.
Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen/
**Unfortunately only in german, but the [google translate works rather good](https://translate.google.com/translate?hl=en&sl=de&tl=en&u=https%3A%2F%2Fwww.smarthomejetzt.de%2Fmit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen%2F)**
**Unfortunately only in German, but the [Google Translate works rather good](https://translate.google.com/translate?hl=en&sl=de&tl=en&u=https%3A%2F%2Fwww.smarthomejetzt.de%2Fmit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen%2F)**
<!--
Placeholder for the next version (at the beginning of the line):
### __WORK IN PROGRESS__
### **WORK IN PROGRESS**
-->
## Changelog
### 2.0.1 (2026-03-31)
* (@GermanBluefox) Adapter was updated and migrated to TypeScript
### 1.4.0 (2023-01-03)
* (Apollon77) Added more guidance logging when setting services incorrectly
* (Apollon77) Prevent crashes when attributes contain "." at the end of their names
* (Apollon77) Added logging for state updates for unknown objects
### 1.3.0 (2022-07-01)
* (Apollon77) Further optimize sending data to HASS and allow setting values like numbers as normal states if the service has one attribute and it can be mapped
### 1.2.0 (2022-06-17)
* (Apollon77) IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!
* (Apollon77) make sure a "null" value in state changes is not crashing
### 1.1.2 (2022-03-29)
* (Apollon77) Fix crash cases reported by Sentry
@@ -106,7 +134,7 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
* (Apollon7) Add Sentry for crash reporting
### 1.0.1 (2021-09-04)
* IMPORTANT: js-controller 2.0 is needed st least!
* IMPORTANT: js-controller 2.0 is needed at least!
* (Apollon77) Fix start issue
* (Apollon77/Garfonso) Fix issue where value could not be set in hass
@@ -119,7 +147,7 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
## License
The MIT License (MIT)
Copyright (c) 2018-2022 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2026 bluefox <dogafox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "IP-Adresse von Home Assistant",
"Home assistant WS Port": "Home Assistant WebSocket-Port",
"Password repeat": "Passwort wiederholen",
"Password": "Passwort",
"Passwords missmatch!": "Passwörter stimmen nicht überein!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "IP-Adresse des Heimassistenten:",
"Home assistant WS Port:": "Home Assistant WS-Port:",
"Password repeat:": "Passwort wiederholen:",
"Password:": "Passwort:",
"Passwords missmatch!": "Passwörter stimmen nicht überein!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Home assistant IP",
"Home assistant WS Port": "Home assistant WS Port",
"Password repeat": "Password repeat",
"Password": "Password",
"Passwords missmatch!": "Passwords missmatch!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Home assistant IP:",
"Home assistant WS Port:": "Home assistant WS Port:",
"Password repeat:": "Password repeat:",
"Password:": "Password:",
"Passwords missmatch!": "Passwords missmatch!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "IP del asistente de hogar",
"Home assistant WS Port": "Asistente doméstico Puerto WS",
"Password repeat": "Repite la contraseña",
"Password": "Clave",
"Passwords missmatch!": "¡Las contraseñas no coinciden!",
"Secure": "¿HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "IP del asistente de hogar:",
"Home assistant WS Port:": "Asistente doméstico Puerto WS:",
"Password repeat:": "Repite la contraseña:",
"Password:": "Clave:",
"Passwords missmatch!": "¡Las contraseñas no coinciden!",
"Secure:": "¿HTTPS?:"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "IP de l'assistant à domicile ",
"Home assistant WS Port": "Port WS de l'assistant domestique ",
"Password repeat": "Répéter le mot de passe ",
"Password": "Mot de passe",
"Passwords missmatch!": "Les mots de passe ne correspondent pas !",
"Secure": "HTTPS ?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "IP de l'assistant à domicile :",
"Home assistant WS Port:": "Port WS de l'assistant domestique :",
"Password repeat:": "Répéter le mot de passe :",
"Password:": "Mot de passe:",
"Passwords missmatch!": "Les mots de passe ne correspondent pas !",
"Secure:": "HTTPS ?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "IP assistente domestico",
"Home assistant WS Port": "Porta WS dell'assistente domestico",
"Password repeat": "Ripeti password",
"Password": "Parola d'ordine",
"Passwords missmatch!": "Le password non corrispondono!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "IP assistente domestico:",
"Home assistant WS Port:": "Porta WS dell'assistente domestico:",
"Password repeat:": "Ripeti password:",
"Password:": "Parola d'ordine:",
"Passwords missmatch!": "Le password non corrispondono!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Thuisassistent IP",
"Home assistant WS Port": "Thuisassistent WS Poort",
"Password repeat": "Wachtwoord herhalen",
"Password": "Wachtwoord",
"Passwords missmatch!": "Wachtwoorden komen niet overeen!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Thuisassistent IP:",
"Home assistant WS Port:": "Thuisassistent WS Poort:",
"Password repeat:": "Wachtwoord herhalen:",
"Password:": "Wachtwoord:",
"Passwords missmatch!": "Wachtwoorden komen niet overeen!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Adres IP asystenta domowego",
"Home assistant WS Port": "Asystent domowy Port WS",
"Password repeat": "Powtórz hasło",
"Password": "Hasło",
"Passwords missmatch!": "Niezgodność haseł!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Adres IP asystenta domowego:",
"Home assistant WS Port:": "Asystent domowy Port WS:",
"Password repeat:": "Powtórz hasło:",
"Password:": "Hasło:",
"Passwords missmatch!": "Niezgodność haseł!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "IP do assistente doméstico",
"Home assistant WS Port": "Assistente doméstico Porta WS",
"Password repeat": "Repetição de senha",
"Password": "Senha",
"Passwords missmatch!": "As senhas não correspondem!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "IP do assistente doméstico:",
"Home assistant WS Port:": "Assistente doméstico Porta WS:",
"Password repeat:": "Repetição de senha:",
"Password:": "Senha:",
"Passwords missmatch!": "As senhas não correspondem!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Домашний помощник IP",
"Home assistant WS Port": "Домашний помощник WS Порт",
"Password repeat": "Повтор пароля",
"Password": "Пароль",
"Passwords missmatch!": "Пароли не совпадают!",
"Secure": "HTTPS?"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "Домашний помощник IP:",
"Home assistant WS Port:": "Домашний помощник WS Порт:",
"Password repeat:": "Повтор пароля:",
"Password:": "Пароль:",
"Passwords missmatch!": "Пароли не совпадают!",
"Secure:": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "Домашній помічник IP",
"Home assistant WS Port": "Домашній помічник WS Порт",
"Password repeat": "Повторення пароля",
"Password": "Пароль",
"Passwords missmatch!": "Паролі не збігаються!",
"Secure": "HTTPS?"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Home assistant IP": "家庭助理IP",
"Home assistant WS Port": "家庭助理 WS 端口:",
"Password repeat": "密码重复:",
"Password": "密码:",
"Passwords missmatch!": "密码不匹配!",
"Secure": "HTTPS"
}
-8
View File
@@ -1,8 +0,0 @@
{
"Home assistant IP:": "家庭助理IP",
"Home assistant WS Port:": "家庭助理 WS 端口:",
"Password repeat:": "密码重复:",
"Password:": "密码:",
"Passwords missmatch!": "密码不匹配!",
"Secure:": "HTTPS"
}
-108
View File
@@ -1,108 +0,0 @@
<html>
<head>
<!-- Materialze style -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (settings.password === undefined) settings.password = '';
settings.passwordRepeat = settings.password;
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$value.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
// Signal to admin, that no changes yet
onChange(false);
}
function save(callback) {
var obj = {};
if ($('#password').val() !== $('#passwordRepeat').val()) {
showMessage(_('Passwords missmatch!'), _('Warning'), 'alert');
return;
}
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
delete obj.passwordRepeat;
callback(obj);
}
</script>
</head>
<body>
<div class="m adapter-container">
<div class="row">
<div class="col s12 m4 l2">
<img src="hass.png" class="logo">
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s4 input-field">
<input type="text" class="value" id="host" />
<label for="host" class="translate">Home assistant IP:</label>
</div>
<div class="col s4 input-field">
<input type="text" class="value" id="port" size="5" maxlength="5"/>
<label for="port" class="translate">Home assistant WS Port:</label>
</div>
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s6 m4 input-field">
<input type="password" class="value" id="password" />
<label for="password" class="translate">Password:</label>
</div>
<div class="col s6 m4 input-field">
<input type="password" class="value" id="passwordRepeat" />
<label for="passwordRepeat" class="translate">Password repeat:</label>
</div>
</div>
</div>
<div class="row">
<div class="col s12 m8 l8">
<div class="col s6 m4 input-field">
<input id="secure" type="checkbox" class="value" />
<span for="secure" class="translate">Secure:</span>
</div>
</div>
</div>
</div>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
{
"i18n": true,
"type": "panel",
"items": {
"host": {
"type": "text",
"label": "Home assistant IP",
"sm": 12,
"md": 6,
"lg": 4
},
"port": {
"type": "number",
"label": "Home assistant WS Port",
"min": 1,
"max": 65535,
"sm": 12,
"md": 6,
"lg": 4
},
"_divider1": {
"type": "divider"
},
"password": {
"type": "password",
"label": "Password",
"repeat": true,
"visible": true,
"sm": 12,
"md": 6,
"lg": 4
},
"_divider2": {
"type": "divider"
},
"secure": {
"type": "checkbox",
"label": "Secure",
"sm": 12,
"md": 6,
"lg": 4
}
}
}
-11
View File
@@ -1,11 +0,0 @@
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"Home assistant IP:": { "en": "Home assistant IP:", "de": "IP-Adresse des Heimassistenten:", "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:", "zh-cn": "家庭助理IP"},
"Home assistant WS Port:": { "en": "Home assistant WS Port:", "de": "Home Assistant WS-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:", "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:", "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:", "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ł!", "zh-cn": "密码不匹配!"},
"Secure:": { "en": "HTTPS?", "de": "HTTPS?", "ru": "HTTPS?", "pt": "HTTPS?", "nl": "HTTPS?", "fr": "HTTPS ?", "it": "HTTPS?", "es": "¿HTTPS?", "pl": "HTTPS?", "zh-cn": "HTTPS"},
};
+35
View File
@@ -0,0 +1,35 @@
import config from '@iobroker/eslint-config';
export default [
...config,
{
languageOptions: {
parserOptions: {
allowDefaultProject: {
allow: ['*.js', '*.mjs'],
},
tsconfigRootDir: import.meta.dirname,
// project: './tsconfig.json',
},
},
},
{
// disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc'
rules: {
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param': 'off',
'jsdoc/check-param-names': 'off',
},
},
{
ignores: [
'node_modules/**/*',
'build/**/*',
'admin/**/*',
'test/**/*',
'src-admin/**/*',
'tmp/**/*',
'**/*.mjs',
],
},
];
-337
View File
@@ -1,337 +0,0 @@
/*!
* ioBroker gulpfile
* Date: 2019-01-28
*/
'use strict';
const gulp = require('gulp');
const fs = require('fs');
const pkg = require('./package.json');
const iopackage = require('./io-package.json');
const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
const fileName = 'words.js';
const EMPTY = '';
const translate = require('./lib/tools').translateText;
const languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {},
'zh-cn': {}
};
function lang2data(lang) {
let str ='{\n';
let count = 0;
for (const w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
const key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
if (!count) {
return '{\n}';
} else {
return str.substring(0, str.length - 2) + '\n}';
}
}
function readWordJs(src) {
try {
let words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
words = words.substring(words.indexOf('{'), words.length);
words = words.substring(0, words.lastIndexOf(';'));
const resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
return null;
}
}
function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
let text = '';
text += '/*global systemDictionary:true */\n';
text += "'use strict';\n\n";
text += 'systemDictionary = {\n';
for (const word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
let line = '';
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
}
if (line) {
line = line.trim();
line = line.substring(0, line.length - 1);
}
text += line + '},\n';
}
}
text += '};';
if (fs.existsSync(src + 'js/' + fileName)) {
fs.writeFileSync(src + 'js/' + fileName, text);
} else {
fs.writeFileSync(src + '' + fileName, text);
}
}
function words2languages(src) {
const langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (const l in langs) {
if (!langs.hasOwnProperty(l))
continue;
const keys = Object.keys(langs[l]);
keys.sort();
const obj = {};
for (let k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
fs.mkdirSync(src + 'i18n/' + l);
}
fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj));
}
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function languages2words(src) {
const dirs = fs.readdirSync(src + 'i18n/');
const langs = {};
const bigOne = {};
const order = Object.keys(languages);
dirs.sort(function (a, b) {
const posA = order.indexOf(a);
const posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b)
return 1;
if (a < b)
return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB)
return 1;
if (posA < posB)
return -1;
return 0;
}
});
for (const lang of dirs) {
if (lang === 'flat.txt')
continue;
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
const aWords = readWordJs();
const temporaryIgnore = ['flat.txt'];
if (aWords) {
// Merge words together
for (const w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w];
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1)
return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
async function translateNotExisting(obj, baseText, yandex) {
let t = obj['en'];
if (!t) {
t = baseText;
}
if (t) {
for (let l in languages) {
if (!obj[l]) {
const time = new Date().getTime();
obj[l] = await translate(t, l, yandex);
console.log('en -> ' + l + ' ' + (new Date().getTime() - time) + ' ms');
}
}
}
}
//TASKS
gulp.task('adminWords2languages', function (done) {
words2languages('./admin/');
done();
});
gulp.task('adminLanguages2words', function (done) {
languages2words('./admin/');
done();
});
gulp.task('updatePackages', function (done) {
iopackage.common.version = pkg.version;
iopackage.common.news = iopackage.common.news || {};
if (!iopackage.common.news[pkg.version]) {
const news = iopackage.common.news;
const newNews = {};
newNews[pkg.version] = {
en: 'news',
de: 'neues',
ru: 'новое',
pt: 'novidades',
nl: 'nieuws',
fr: 'nouvelles',
it: 'notizie',
es: 'noticias',
pl: 'nowości',
'zh-cn': '新'
};
iopackage.common.news = Object.assign(newNews, news);
}
fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4));
done();
});
gulp.task('updateReadme', function (done) {
const readme = fs.readFileSync('README.md').toString();
const pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
const readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
const readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
const timestamp = new Date();
const date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
let news = '';
if (iopackage.common.news && iopackage.common.news[pkg.version]) {
news += '* ' + iopackage.common.news[pkg.version].en;
}
fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd);
}
}
done();
});
gulp.task('translate', async function (done) {
let yandex;
const i = process.argv.indexOf('--yandex');
if (i > -1) {
yandex = process.argv[i + 1];
}
if (iopackage && iopackage.common) {
if (iopackage.common.news) {
console.log('Translate News');
for (let k in iopackage.common.news) {
console.log('News: ' + k);
let nw = iopackage.common.news[k];
await translateNotExisting(nw, null, yandex);
}
}
if (iopackage.common.titleLang) {
console.log('Translate Title');
await translateNotExisting(iopackage.common.titleLang, iopackage.common.title, yandex);
}
if (iopackage.common.desc) {
console.log('Translate Description');
await translateNotExisting(iopackage.common.desc, null, yandex);
}
if (fs.existsSync('./admin/i18n/en/translations.json')) {
let enTranslations = require('./admin/i18n/en/translations.json');
for (let l in languages) {
console.log('Translate Text: ' + l);
let existing = {};
if (fs.existsSync('./admin/i18n/' + l + '/translations.json')) {
existing = require('./admin/i18n/' + l + '/translations.json');
}
for (let t in enTranslations) {
if (!existing[t]) {
existing[t] = await translate(enTranslations[t], l, yandex);
}
}
if (!fs.existsSync('./admin/i18n/' + l + '/')) {
fs.mkdirSync('./admin/i18n/' + l + '/');
}
fs.writeFileSync('./admin/i18n/' + l + '/translations.json', JSON.stringify(existing, null, 4));
}
}
}
fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4));
});
gulp.task('translateAndUpdateWordsJS', gulp.series('translate', 'adminLanguages2words', 'adminWords2languages'));
gulp.task('default', gulp.series('updatePackages', 'updateReadme'));
+119 -54
View File
@@ -1,19 +1,20 @@
{
"common": {
"name": "hass",
"version": "1.1.2",
"version": "2.0.1",
"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": "家庭助理"
"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",
@@ -25,9 +26,62 @@
"it": "Connessione Home Assistant per ioBroker",
"es": "Conexión de Home Assistant para ioBroker",
"pl": "Połączenie Home Assistant dla ioBroker",
"zh-cn": "ioBroker 的家庭助理连接"
"zh-cn": "ioBroker 的家庭助理连接",
"uk": "Підключення Home Assistant для ioBroker"
},
"news": {
"2.0.1": {
"en": "Adapter was updated and migrated to TypeScript",
"de": "Adapter wurde aktualisiert und auf TypeScript migriert",
"ru": "Адаптер был обновлен и перенесен на TypeScript",
"pt": "O adaptador foi atualizado e migrado para TypeScript",
"nl": "Adapter is bijgewerkt en gemigreerd naar TypeScript",
"fr": "Adaptateur a été mis à jour et migré vers TypeScript",
"it": "L'adattatore è stato aggiornato e migrato a TypeScript",
"es": "Adaptador fue actualizado y migrado a TipoScript",
"pl": "Adapter został zaktualizowany i przeniesiony do TypeScript",
"uk": "Адаптер був оновлений і мігрований до TypeScript",
"zh-cn": "适应器被更新并迁移到 TypeScript"
},
"1.4.0": {
"en": "Added more guidance logging when setting services incorrectly\nPrevent crashes when attributes contain \".\" at the end of their names\nAdded logging for state updates for unknown objects",
"de": "Mehr Anleitungsprotokollierung hinzugefügt, wenn Dienste falsch eingestellt werden\nVerhindern Sie Abstürzen, wenn Attribute \" enthalten.\" am Ende ihrer Namen\nHinzugefügt Protokollierung für Zustandsaktualisierungen für unbekannte Objekte",
"ru": "Добавили больше руководства, когда установка услуг неправильно\nПредотвратить сбои, когда атрибуты содержат \".\" в конце их имен\nДобавлен вход для государственных обновлений для неизвестных объектов",
"pt": "Adicionado mais registro de orientação ao definir serviços incorretamente\nImpedir falhas quando os atributos contêm \". no final de seus nomes\nRegisto adicionado para atualizações de estado para objetos desconhecidos",
"nl": "Voeg meer begeleiding toe als ze onjuiste diensten indienen\nVertaling:\nVermeld inloggen op staatsinformatie voor onbekende objecten",
"fr": "Ajout d'autres lignes directrices lorsqu'on établit des services incorrectement\nPrévenir les crashes lorsque les attributs contiennent \".\" à la fin de leurs noms\nAjout de logging pour les mises à jour de l'état des objets inconnus",
"it": "Aggiunto più guida logging quando imposta i servizi in modo errato\nPrevenire crash quando gli attributi contengono \". alla fine dei loro nomi\nAggiunto logging per aggiornamenti di stato per oggetti sconosciuti",
"es": "Se agregó más registro de guía cuando se establecen los servicios incorrectamente\nEvitar fallos cuando los atributos contienen \".\" al final de sus nombres\nRegistro añadido para actualizaciones estatales para objetos desconocidos",
"pl": "Wprowadzone są bardziej wytyczne, gdy ustawienie usług nieprawidłowo błędnie\nPrevent rozbija się, gdy atrybuty zawierają „”\nWprowadzone logowanie dla aktualizacji stanów nieznanych obiektów",
"uk": "Додайте більше звітів при установці послуг неправильно\nПрофілактика аварійних випадків, коли атрибути містять \".\" в кінці їх імен\nДодана записка для державних оновлень для невідомих об'єктів",
"zh-cn": "在不正确地建立服务时增加指导标志\n在名称末尾含有“第2款”的预防事故\n增加用于不明物品的国家更新的伐木"
},
"1.3.0": {
"en": "Further optimize sending data to HASS and allow to set values like numbers as normal states if the service has one attribute and it can be mapped",
"de": "Weitere optimieren Sie das Senden von Daten an HASS und ermöglichen es, Werte wie Zahlen als normale Zustände festzulegen, wenn der Dienst ein Attribut hat und es abgebildet werden kann",
"ru": "Далее оптимизируйте отправку данных в HASS и позвольте установить значения, такие как цифры, как нормальные состояния, если услуга имеет один атрибут, и она может быть загружена",
"pt": "Além disso otimizar o envio de dados para HASS e permitir definir valores como números como estados normais se o serviço tem um atributo e pode ser mapeado",
"nl": "Verder optimiliseer het sturen van gegevens naar HASS en sta toe om waarden als normale staten te plaatsen als de dienst een Attribuut heeft en het kan worden in kaart gebracht",
"fr": "Optimiser l'envoi de données à HASS et permettre de définir des valeurs comme des nombres comme des états normaux si le service a un attribut et il peut être mappé",
"it": "Ottimizzare ulteriormente l'invio di dati a HASS e consentire di impostare valori come numeri come stati normali se il servizio ha un attributo e può essere mappato",
"es": "Optimizar aún más el envío de datos a HASS y permitir establecer valores como números como estados normales si el servicio tiene un atributo y puede ser mapeado",
"pl": "Dalsze optymalizowanie danych do HASS i umożliwienie ustalenie wartości takich jak stany normalne, jeśli usługa ma jeden atrybut i może być zmapowana",
"zh-cn": "进一步优化向人道协调厅系统传送数据,如果该服务具有属性,它可以绘制图表,从而能够将数字象正常国家一样。",
"uk": "Додатково оптимізуйте надсилання даних до HASS і дозвольте встановлювати такі значення, як числа, як звичайні стани, якщо служба має один атрибут і його можна зіставити"
},
"1.2.0": {
"en": "IMPORTANT: Replace special characters in entity attribute names with an underscore! Object IDs might change!\nmake sure a \"null\" value in state changes is not crashing",
"de": "WICHTIG: Ersetzen Sie Sonderzeichen in Namen des Attributes der Einheit mit einem Unterpunkt! Objekt-IDs können sich ändern!\nsicherstellen, dass ein nullwert bei zustandsänderungen nicht abstürzt",
"ru": "ВАЖНО: Замените специальные символы в имени сущности с подчеркиванием! ID объекта может измениться!\nубедитесь, что значение null в государственных изменениях не падает",
"pt": "IMPORTANTE: Substitua caracteres especiais em nomes de atributos de entidade com um sublinhado! IDs de objeto podem mudar!\ncertifique-se de que um valor nulo em mudanças de estado não está caindo",
"nl": "Vervang speciale personages in entiteitsadvocaten met een onderscore! ID's kunnen veranderen!\nzorg ervoor dat een waarde van de staat niet crasht",
"fr": "IMPORTANT: Remplacer les caractères spéciaux dans les noms d'attributs d'entité par un accent! Les identifiants d'objet peuvent changer !\ns'assurer qu'une valeur nulle dans les changements d'état ne s'écrase pas",
"it": "IMPORTANTE: Sostituisci caratteri speciali in nomi attributo entità con un underscore! Gli ID degli Oggetti potrebbero cambiare!\nassicurarsi che un valore nullo nei cambiamenti di stato non è crash",
"es": "IMPORTANTE: Reemplazar caracteres especiales en nombres de atributos de entidad con un subrayado! ¡Las identificaciones de objetos pueden cambiar!\nasegúrese de que un valor nulo en los cambios estatales no se estrella",
"pl": "IMPORTANT: Replace special characters in identity attribution names with an underscore! (ang.). Zmiany obiektów mogą się zmieniać!\nniewątpliwą wartością w zmianach stanowych nie jest rozbicie się",
"zh-cn": "IMPORTANT:将实体的特色性改为强调的! 目标国际组织可能改变!\n确保在国家变化中确保无效不失",
"uk": "ВАЖЛИВО: Замініть спеціальні символи в іменах атрибутів сутності на підкреслення! Ідентифікатори об'єктів можуть змінитися!\nпереконайтеся, що \"нульове\" значення в змінах стану не аварійно завершує роботу"
},
"1.1.2": {
"en": "Fix crash cases reported by Sentry",
"de": "Von Sentry gemeldete Absturzfälle beheben",
@@ -38,7 +92,8 @@
"it": "Risolvi i casi di crash segnalati da Sentry",
"es": "Solucionar casos de fallas informados por Sentry",
"pl": "Napraw przypadki awarii zgłoszone przez Sentry",
"zh-cn": "修复 Sentry 报告的崩溃案例"
"zh-cn": "修复 Sentry 报告的崩溃案例",
"uk": "Виправте випадки збоїв, про які повідомляє Sentry"
},
"1.1.1": {
"en": "Show password fields masked again in config",
@@ -50,7 +105,8 @@
"it": "Mostra i campi password mascherati di nuovo in config",
"es": "Mostrar campos de contraseña enmascarados nuevamente en la configuración",
"pl": "Pokaż ponownie zamaskowane pola haseł w konfiguracji",
"zh-cn": "在配置中再次显示被屏蔽的密码字段"
"zh-cn": "在配置中再次显示被屏蔽的密码字段",
"uk": "Знову показати замасковані поля пароля в конфігурації"
},
"1.1.0": {
"en": "IMPORTANT: You need to re-enter the password once after installing this version!\nImplement Service triggers to use any value to trigger or stringified JSON to call with fields\nOptimize unload handling\nAdd Sentry for crash reporting",
@@ -62,43 +118,8 @@
"it": "IMPORTANTE: è necessario reinserire la password una volta dopo aver installato questa versione!\nImplementa i trigger del servizio per utilizzare qualsiasi valore per attivare o JSON in formato stringa per chiamare con i campi\nOttimizza la gestione dello scarico\nAggiungi Sentinella per la segnalazione degli arresti anomali",
"es": "IMPORTANTE: ¡Debe volver a ingresar la contraseña una vez después de instalar esta versión!\nImplementar disparadores de servicio para usar cualquier valor para disparar o JSON en cadena para llamar con campos\nOptimizar el manejo de descarga\nAgregar Sentry para informes de fallas",
"pl": "WAŻNE: Po zainstalowaniu tej wersji należy raz ponownie wprowadzić hasło!\nZaimplementuj wyzwalacze usługi, aby używać dowolnej wartości do wyzwalania lub skróconego JSON do wywołania z polami\nZoptymalizuj obsługę rozładunku\nDodaj Sentry do zgłaszania awarii",
"zh-cn": "重要提示:安装此版本后需要重新输入一次密码!\n实现服务触发器以使用任何值触发或字符串化 JSON 以使用字段调用\n优化卸载处理\n添加 Sentry 以进行崩溃报告"
},
"1.0.1": {
"en": "IMPORTANT: js-controller 2.0 is needed st least!\nFix start issue\n(Apollon77/Garfonso) Fix issue where value could not be set in hass",
"de": "WICHTIG: js-controller 2.0 wird am wenigsten benötigt!\nStartproblem beheben\n(Apollon77/Garfonso) Problem behoben, bei dem der Wert nicht in Hass festgelegt werden konnte",
"ru": "ВАЖНО: js-controller 2.0 нужен как минимум!\nИсправить проблему с запуском\n(Apollon77 / Garfonso) Исправлена ошибка, из-за которой значение не могло быть установлено в hass.",
"pt": "IMPORTANTE: o js-controller 2.0 é o mínimo necessário!\nCorrigir problema inicial\n(Apollon77 / Garfonso) Correção do problema em que o valor não podia ser definido em hass",
"nl": "BELANGRIJK: js-controller 2.0 is minimaal nodig!\nStartprobleem oplossen\n(Apollon77/Garfonso) Probleem opgelost waarbij de waarde niet kon worden ingesteld in hass",
"fr": "IMPORTANT : js-controller 2.0 est au moins nécessaire !\nRésoudre le problème de démarrage\n(Apollon77/Garfonso) Correction d'un problème où la valeur ne pouvait pas être définie dans hass",
"it": "IMPORTANTE: come minimo è necessario js-controller 2.0!\nRisolvi il problema di avvio\n(Apollon77/Garfonso) Risolto il problema per cui il valore non poteva essere impostato in hass",
"es": "IMPORTANTE: ¡js-controller 2.0 es lo menos necesario!\nSolucionar problema de inicio\n(Apollon77 / Garfonso) Se solucionó el problema por el cual no se podía establecer el valor en hass",
"pl": "WAŻNE: js-controller 2.0 jest potrzebny przynajmniej!\nNapraw problem z uruchomieniem\n(Apollon77/Garfonso) Napraw problem, w którym nie można było ustawić wartości w hass",
"zh-cn": "重要提示:至少需要 js-controller 2.0\n修复启动问题\n(Apollon77/Garfonso) 修复无法在 hass 中设置值的问题"
},
"1.0.0": {
"en": "added the support of compact mode",
"de": "fügte die Unterstützung des Kompaktmodus hinzu",
"ru": "добавлена поддержка компактного режима",
"pt": "adicionado o suporte do modo compacto",
"nl": "de ondersteuning van de compacte modus toegevoegd",
"fr": "ajouté le support du mode compact",
"it": "aggiunto il supporto della modalità compatta",
"es": "agregó el soporte del modo compacto",
"pl": "dodano obsługę trybu kompaktowego",
"zh-cn": "增加了对紧凑模式的支持"
},
"0.1.0": {
"en": "initial version",
"de": "erste Version",
"ru": "Первоначальная версия",
"pt": "versão inicial",
"nl": "eerste versie",
"fr": "version initiale",
"it": "versione iniziale",
"es": "versión inicial",
"pl": "początkowa wersja",
"zh-cn": "初始版本"
"zh-cn": "重要提示:安装此版本后需要重新输入一次密码!\n实现服务触发器以使用任何值触发或字符串化 JSON 以使用字段调用\n优化卸载处理\n添加 Sentry 以进行崩溃报告",
"uk": "ВАЖЛИВО: після встановлення цієї версії потрібно повторно ввести пароль!\nРеалізуйте тригери служби, щоб використовувати будь-яке значення для запуску або рядковий JSON для виклику з полями\nОптимізуйте роботу з розвантаженням\nДодайте Sentry для звітування про збої"
}
},
"localLink": "http://%host%:8123",
@@ -107,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": [
@@ -123,12 +146,12 @@
],
"dependencies": [
{
"js-controller": ">=3.0.0"
"js-controller": ">=6.0.11"
}
],
"globalDependencies": [
{
"admin": ">=5.1.28"
"admin": ">=6.0.0"
}
],
"plugins": {
@@ -158,7 +181,8 @@
"it": "Reinserire la password dopo l'aggiornamento!",
"es": "Vuelva a ingresar la contraseña después de ¡Actualizar!",
"pl": "Wprowadź hasło ponownie po aktualizacji!",
"zh-cn": "更新后重新输入密码!"
"zh-cn": "更新后重新输入密码!",
"uk": "Повторно введіть пароль після оновлення!"
},
"text": {
"en": "After installing this update the password/token used to access the HASS installation needs to be re-entered in the Instance-Settings!",
@@ -170,7 +194,48 @@
"it": "Dopo aver installato questo aggiornamento, la password/token utilizzato per accedere all'installazione HASS deve essere reinserito nelle impostazioni dell'istanza!",
"es": "¡Después de instalar esta actualización, la contraseña/token utilizada para acceder a la instalación de HASS debe volver a ingresarse en la configuración de la instancia!",
"pl": "Po zainstalowaniu tej aktualizacji hasło/token używane do uzyskania dostępu do instalacji HASS należy ponownie wprowadzić w ustawieniach instancji!",
"zh-cn": "安装此更新后,用于访问 HASS 安装的密码/令牌需要在实例设置中重新输入!"
"zh-cn": "安装此更新后,用于访问 HASS 安装的密码/令牌需要在实例设置中重新输入!",
"uk": "Після встановлення цього оновлення пароль/токен, який використовується для доступу до встановлення HASS, потрібно повторно ввести в налаштуваннях екземпляра!"
},
"level": "warn",
"buttons": [
"agree",
"cancel"
]
},
{
"condition": {
"operand": "and",
"rules": [
"oldVersion<1.2.0",
"newVersion>=1.2.0"
]
},
"title": {
"en": "Important notice!",
"de": "Wichtiger Hinweis!",
"ru": "Важное замечание!",
"pt": "Notícia importante!",
"nl": "Belangrijke mededeling!",
"fr": "Avis important!",
"it": "Avviso IMPORTANTE!",
"es": "Noticia importante!",
"pl": "Ważna uwaga!",
"zh-cn": "重要通知!",
"uk": "Важливе повідомлення!"
},
"text": {
"en": "The new version 1.2.x will potentially change the Names of Objects when special characters like \".\", \"-\" or spaces were used in entity attributes! Please delete the old objects manually if needed.",
"de": "Die neue Version 1.2.x wird die Namen von Objekten möglicherweise ändern, wenn spezielle Zeichen wie \".\", \"-\" oder Leerzeichen in Entity-Attributen verwendet wurden! Alten Objekte bitte bei Bedarf manuell löschen.",
"ru": "Новая версия 1.2.x будет потенциально менять имена объектов, когда специальные символы типа \".\", \"-\" или пространства были использованы в атрибутах лица! Пожалуйста, удалите старые объекты вручную, если это необходимо.",
"pt": "A nova versão 1.2.x irá potencialmente alterar os Nomes de Objetos quando caracteres especiais como \".\", \"-\" ou espaços foram usados em atributos de entidade! Por favor, exclua os objetos antigos manualmente, se necessário.",
"nl": "De nieuwe versie 1.2.x zal de Namen van Objects veranderen als speciale personages zoals \", \" of ruimtes werden gebruikt in entiteitsaanklachten! Verwijder de oude objecten handmatig als het nodig is.",
"fr": "La nouvelle version 1.2.x modifiera potentiellement les Noms des Objets lorsque des caractères spéciaux comme \", \"-\" ou des espaces ont été utilisés dans des attributs d'entité ! Veuillez supprimer les anciens objets manuellement si nécessaire.",
"it": "La nuova versione 1.2.x cambierà potenzialmente i nomi degli oggetti quando caratteri speciali come \".\", \"-\" o spazi sono stati utilizzati negli attributi dell'entità! Si prega di eliminare i vecchi oggetti manualmente se necessario.",
"es": "La nueva versión 1.2.x potencialmente cambiará los Nombres de Objetos cuando caracteres especiales como \"., \"-\" o espacios fueron utilizados en atributos de entidad! Por favor, borre los viejos objetos manualmente si es necesario.",
"pl": "Nowa wersja 1.2.x będzie potencjalnie zmieniać nazwy obiektów, kiedy szczególne postacie takie jak „.”, „-” lub przestrzenie były używane w atrybutach. Okazuje się, że stare obiekty ręcznie będą potrzebne.",
"zh-cn": "新版本1.2.x将可能改变物体的名称,如“......”、“-”或空间在实体的属性中使用。 请在必要时删除旧物体。.",
"uk": "Нова версія 1.2.x потенційно змінить назви об’єктів, якщо в атрибутах об’єктів використовувалися спеціальні символи, такі як «.», «-» або пробіли! За потреби видаліть старі об’єкти вручну."
},
"level": "warn",
"buttons": [
-270
View File
@@ -1,270 +0,0 @@
const util = require('util');
const EventEmitter = require('events').EventEmitter;
const WebSocket = require('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;
-92
View File
@@ -1,92 +0,0 @@
const axios = require("axios");
/**
* Tests whether the given variable is a real object and not an Array
* @param {any} it The variable to test
* @returns {it is Record<string, any>}
*/
function isObject(it) {
// This is necessary because:
// typeof null === 'object'
// typeof [] === 'object'
// [] instanceof Object === true
return Object.prototype.toString.call(it) === "[object Object]";
}
/**
* Tests whether the given variable is really an Array
* @param {any} it The variable to test
* @returns {it is any[]}
*/
function isArray(it) {
if (Array.isArray != null)
return Array.isArray(it);
return Object.prototype.toString.call(it) === "[object Array]";
}
/**
* Choose the right tranalation API
* @param {string} text The text to translate
* @param {string} targetLang The target languate
* @param {string} yandex api key
* @returns {Promise<string>}
*/
async function translateText(text, targetLang, yandex) {
if (targetLang === "en") {
return text;
}
if (yandex) {
return await translateYandex(text, targetLang, yandex);
} else {
return await translateGoogle(text, targetLang);
}
}
/**
* Translates text with Yandex API
* @param {string} text The text to translate
* @param {string} targetLang The target languate
* @param {string} yandex api key
* @returns {Promise<string>}
*/
async function translateYandex(text, targetLang, yandex) {
if (targetLang === "zh-cn") {
targetLang = "zh";
}
try {
const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${yandex}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`;
const response = await axios({url, timeout: 15000});
if (response.data && response.data['text']) {
return response.data['text'][0];
}
throw new Error("Invalid response for translate request");
} catch (e) {
throw new Error(`Could not translate to "${targetLang}": ${e}`);
}
}
/**
* Translates text with Google API
* @param {string} text The text to translate
* @param {string} targetLang The target languate
* @returns {Promise<string>}
*/
async function translateGoogle(text, targetLang) {
try {
const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}&ie=UTF-8&oe=UTF-8`;
const response = await axios({url, timeout: 15000});
if (isArray(response.data)) {
// we got a valid response
return response.data[0][0][0];
}
throw new Error("Invalid response for translate request");
} catch (e) {
throw new Error(`Could not translate to "${targetLang}": ${e}`);
}
}
module.exports = {
isArray,
isObject,
translateText
};
-457
View File
@@ -1,457 +0,0 @@
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
'use strict';
const utils = require('@iobroker/adapter-core');
const HASS = require('./lib/hass');
const adapterName = require('./package.json').name.split('.').pop();
let connected = false;
let hass;
let adapter;
const hassObjects = {};
let delayTimeout = null;
let stopped = false;
function startAdapter(options) {
options = options || {};
Object.assign(options, {name: adapterName, 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') {
try {
requestFields = JSON.parse(state.val);
} catch (err) {
adapter.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
requestFields = {};
}
}
for (const field in fields) {
if (!fields.hasOwnProperty(field)) {
continue;
}
if (field === 'entity_id') {
target.entity_id = hassObjects[id].native.entity_id
} else if (requestFields[field] !== undefined) {
serviceData[field] = requestFields[field];
}
}
serviceData.entity_id = hassObjects[id].native.entity_id
adapter.log.debug(`Send to HASS for service ${hassObjects[id].native.attr} with ${hassObjects[id].native.domain || hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`)
hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, target, err =>
err && adapter.log.error('Cannot control ' + id + ': ' + err));
}
}
}
});
// 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 getUnit(name) {
name = name.toLowerCase();
if (name.indexOf('temperature') !== -1) {
return '°C';
} else if (name.indexOf('humidity') !== -1) {
return '%';
} else if (name.indexOf('pressure') !== -1) {
return 'hPa';
} else if (name.indexOf('degrees') !== -1) {
return '°';
} else if (name.indexOf('speed') !== -1) {
return 'kmh';
}
return undefined;
}
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 => {
err && adapter.log.error(err);
setImmediate(syncStates, states, cb);
});
}
function syncObjects(objects, cb) {
if (!objects || !objects.length) {
return cb();
}
const obj = objects.shift();
hassObjects[obj._id] = obj;
adapter.getForeignObject(obj._id, (err, oldObj) => {
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 => {
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 => {
err => adapter.log.error(err);
setImmediate(syncObjects, objects, cb);
});
} else {
setImmediate(syncObjects, objects, cb);
}
}
});
}
function syncRoom(room, members, cb) {
adapter.getForeignObject('enum.rooms.' + room, (err, obj) => {
if (!obj) {
obj = {
_id: 'enum.rooms.' + room,
type: 'enum',
common: {
name: room,
members: members
},
native: {}
};
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
obj.common = obj.common || {};
obj.common.members = obj.common.members || [];
let changed = false;
for (let m = 0; m < members.length; m++) {
if (obj.common.members.indexOf(members[m]) === -1) {
changed = true;
obj.common.members.push(members[m]);
}
}
if (changed) {
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
cb();
}
}
});
}
const knownAttributes = {
azimuth: {write: false, read: true, unit: '°'},
elevation: {write: false, read: true, unit: '°'}
};
const ERRORS = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST'
};
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 (entity.attributes.hasOwnProperty(attr)) {
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
let common;
if (knownAttributes[attr]) {
common = Object.assign({}, knownAttributes[attr]);
} else {
common = {};
}
obj = {
_id: `${adapter.namespace}.entities.${entity.entity_id}.${attr}`,
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 (service.hasOwnProperty(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 = adapter.namespace + '.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) {
adapter.setForeignState(id + 'state', {val: entity.state, ack: true, lc: lc, ts: ts});
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (!entity.attributes.hasOwnProperty(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);
}
adapter.setForeignState(id + attr, {val, ack: true, lc, ts});
}
}
});
hass.on('connected', () => {
if (!connected) {
adapter.log.debug('Connected');
connected = true;
adapter.setState('info.connection', true, true);
hass.getConfig((err, config) => {
if (err) {
adapter.log.error('Cannot read config: ' + err);
return;
}
//adapter.log.debug(JSON.stringify(config));
delayTimeout = setTimeout(() => {
delayTimeout = null;
!stopped && hass.getStates((err, states) => {
if (stopped) {
return;
}
if (err) {
return adapter.log.error('Cannot read states: ' + err);
}
//adapter.log.debug(JSON.stringify(states));
delayTimeout = setTimeout(() => {
delayTimeout = null;
!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();
}
+13409
View File
File diff suppressed because it is too large Load Diff
+29 -14
View File
@@ -1,6 +1,6 @@
{
"name": "iobroker.hass",
"version": "1.1.2",
"version": "2.0.1",
"description": "Home Assistant",
"author": {
"name": "bluefox",
@@ -25,26 +25,41 @@
"url": "https://github.com/ioBroker/ioBroker.hass"
},
"dependencies": {
"websocket": "^1.0.34",
"ws": "^8.5.0",
"@iobroker/adapter-core": "^2.6.0"
"ws": "^8.20.0",
"@iobroker/adapter-core": "^3.3.2"
},
"devDependencies": {
"@alcalzone/release-script": "^3.5.6",
"@alcalzone/release-script-plugin-iobroker": "^3.5.7",
"@alcalzone/release-script-plugin-license": "^3.5.3",
"axios": "^0.26.1",
"gulp": "^4.0.2",
"mocha": "^9.2.2",
"chai": "^4.3.6"
"@alcalzone/release-script": "^5.1.1",
"@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": [
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
"build/",
"io-package.json",
"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",
"release-major": "release-script major --yes"
"release-major": "release-script major --yes",
"translate": "translate-adapter"
},
"bugs": {
"url": "https://github.com/ioBroker/ioBroker.hass/issues"
+3
View File
@@ -0,0 +1,3 @@
import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs';
export default prettierConfig;
+248
View File
@@ -0,0 +1,248 @@
import { EventEmitter } from 'node:events';
import WebSocket from 'ws';
interface HassOptions {
host: string;
port: number;
password?: string;
secure?: boolean;
}
interface HassRequest {
type: string;
ts: number;
cb?: (err: boolean | string | null, result?: any) => void;
}
/*
const ERRORS: Record<number, string> = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST',
};
*/
export default class HASS extends EventEmitter {
private socket: WebSocket | null = null;
private readonly options: HassOptions;
private readonly log: ioBroker.Logger;
private currentId: number = 1;
private readonly requests: Record<number, HassRequest> = {};
private _connected: boolean = false;
private connectTimeout: ReturnType<typeof setTimeout> | null = null;
private closed: boolean = false;
constructor(options: HassOptions, log: ioBroker.Logger) {
super();
this.options = {
host: options.host || '127.0.0.1',
port: parseInt(String(options.port), 10) || 8123,
password: options.password,
secure: options.secure,
};
this.log = log;
}
private subscribeEvents(socket: WebSocket, callback?: (err: boolean | string | null) => void): void {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
socket.send(
JSON.stringify({
id,
type: 'subscribe_events',
}),
);
} else {
callback?.('not connected');
}
}
private sendCommand(
socket: WebSocket | null,
type: string,
callback?: (err: boolean | string | null, result?: any) => void,
extra?: Record<string, any>,
): void {
if (socket && typeof socket.send === 'function') {
const id = this.currentId++;
this.requests[id] = { type, cb: callback, ts: Date.now() };
socket.send(
JSON.stringify({
id,
type,
...extra,
}),
);
} else {
callback?.('not connected');
}
}
private sendAuth(socket: WebSocket, pass: string): void {
if (socket && typeof socket.send === 'function') {
socket.send(
JSON.stringify({
type: 'auth',
access_token: pass,
}),
);
}
}
private initSocket(socket: WebSocket): void {
socket.on('message', (msg: WebSocket.Data): void => {
const msgStr = (msg as Buffer).toString();
this.log.silly(msgStr);
const response = JSON.parse(msgStr);
if (response.type === 'event') {
if (response.event?.data && response.event.event_type === 'system_log_event') {
if (response.event.data.level === 'WARNING') {
this.log.warn(`EVENT: ${response.event.data.message}`);
} else if (response.event.data.level === 'ERROR') {
this.log.error(`EVENT: ${response.event.data.message}`);
} else {
this.log.debug(`EVENT: ${response.event.data.message}`);
}
} else if (response.event?.event_type === 'state_changed') {
this.emit('state_changed', response.event.data.new_state);
}
} else if (response.type === 'auth_required') {
if (!this.options.password) {
this.emit('error', 'Password required. Connection closed');
socket.terminate();
} else {
setTimeout(() => this.sendAuth(socket, this.options.password!), 50);
}
} else if (response.type === 'auth_ok') {
setImmediate(() =>
this.subscribeEvents(socket, err => {
if (!err) {
this._connected = true;
this.emit('connected');
}
}),
);
} else if (response.id === undefined) {
this.log.error(`Invalid answer: ${msgStr}`);
} else {
if (response.type === 'result' && this.requests[response.id]) {
this.log.debug(
`got answer for ${this.requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`,
);
if (typeof this.requests[response.id].cb === 'function') {
this.requests[response.id].cb!(!response.success, response.result);
delete this.requests[response.id];
}
}
}
});
socket.on('error', (err: Error) => {
this.socket = null;
if (err?.message?.indexOf('RSV2 and RSV3 must be clear') !== -1) {
// ignore deflate error
} else {
this.log.error(err.toString());
}
});
socket.on('open', () => {
// connection opened
});
socket.on('close', () => {
this.socket = null;
if (this._connected) {
this._connected = false;
this.emit('disconnected');
}
if (!this.connectTimeout && !this.closed) {
this.connectTimeout = setTimeout(() => {
this.connectTimeout = null;
this.connect();
}, 3000);
}
});
}
isConnected(): boolean {
return this._connected;
}
getConfig(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_config', callback);
}
}
getStates(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_states', callback);
}
}
getServices(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_services', callback);
}
}
getPanels(callback: (err: boolean | string | null, result?: any) => void): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'get_panels', callback);
}
}
callService(
service: string,
domain: string,
serviceData: Record<string, any>,
target: Record<string, any>,
callback: (err: boolean | string | null, result?: any) => void,
): void {
if (!this._connected) {
callback('not connected');
} else {
this.sendCommand(this.socket, 'call_service', callback, {
domain: domain || '',
service,
service_data: serviceData,
target,
});
}
}
connect(): void {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socket = new WebSocket(
`ws${this.options.secure ? 's' : ''}://${this.options.host}:${this.options.port}/api/websocket`,
{ perMessageDeflate: false },
);
this.initSocket(this.socket);
}
close(): void {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.closed = true;
if (this.socket) {
this.socket.close();
}
}
}
+480
View File
@@ -0,0 +1,480 @@
import { Adapter, type AdapterOptions } from '@iobroker/adapter-core';
import HASS from './lib/hass';
interface HassAdapterConfig {
host: string;
port: number;
password: string;
secure: boolean;
}
const knownAttributes: Record<string, { write: boolean; read: boolean; unit: string }> = {
azimuth: { write: false, read: true, unit: '°' },
elevation: { write: false, read: true, unit: '°' },
};
const mapTypes: Record<string, ioBroker.CommonType> = {
string: 'string',
number: 'number',
object: 'mixed',
boolean: 'boolean',
};
const skipServices: string[] = ['persistent_notification'];
class HassAdapter extends Adapter {
declare config: HassAdapterConfig;
private hassConnected: boolean = false;
private hass: HASS | null = null;
private readonly hassObjects: Record<string, ioBroker.ChannelObject | ioBroker.StateObject> = {};
private delayTimeout: ReturnType<typeof setTimeout> | null = null;
private stopped: boolean = false;
public constructor(options: Partial<AdapterOptions> = {}) {
super({
...options,
name: 'hass',
ready: () => this.main(),
unload: callback => this.onUnload(callback),
stateChange: (id, state) => this.onStateChange(id, state),
});
}
private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
if (!state || state.ack) {
return;
}
if (!this.hassConnected) {
this.log.warn(`Cannot send command to "${id}", because not connected`);
return;
}
if (!this.hassObjects[id]) {
return;
}
if (!(this.hassObjects[id] as ioBroker.StateObject).common.write) {
this.log.warn(`Object ${id} is not writable!`);
return;
}
const serviceData: Record<string, any> = {};
const fields: { entity_id: string } = this.hassObjects[id].native.fields;
const target: Record<string, any> = {};
let requestFields: Record<string, any> = {};
if (typeof state.val === 'string') {
state.val = state.val.trim();
if (state.val.startsWith('{') && state.val.endsWith('}')) {
try {
requestFields = JSON.parse(state.val) || {};
} catch (err) {
this.log.info(`Ignore data for service call ${id} is no valid JSON: ${err.message}`);
requestFields = {};
}
}
}
// If a non-JSON value was set, and we only have one relevant field, use this field as value
if (fields && !Object.keys(requestFields).length) {
const fieldList = Object.keys(fields);
if (fieldList.length === 1 && fieldList[0] !== 'entity_id') {
requestFields[fieldList[0]] = state.val;
} else if (fieldList.length === 2 && fields.entity_id) {
requestFields[fieldList[1 - fieldList.indexOf('entity_id')]] = state.val;
}
}
this.log.debug(
`Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`,
);
if (fields) {
for (const field in fields) {
if (!Object.prototype.hasOwnProperty.call(fields, field)) {
continue;
}
if (field === 'entity_id') {
target.entity_id = this.hassObjects[id].native.entity_id;
} else if (requestFields[field] !== undefined) {
serviceData[field] = requestFields[field];
}
}
}
const noFields = Object.keys(serviceData).length === 0;
serviceData.entity_id = this.hassObjects[id].native.entity_id;
this.log.debug(
`Send to HASS for service ${this.hassObjects[id].native.attr} with ${this.hassObjects[id].native.domain || this.hassObjects[id].native.type} and data ${JSON.stringify(serviceData)}`,
);
this.hass!.callService(
this.hassObjects[id].native.attr,
this.hassObjects[id].native.domain || this.hassObjects[id].native.type,
serviceData,
target,
err => {
if (err) {
this.log.error(`Cannot control ${id}: ${err}`);
}
if (err && fields && noFields) {
this.log.warn(
`Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`,
);
this.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
}
},
);
}
private onUnload(callback?: () => void): void {
this.stopped = true;
if (this.delayTimeout) {
clearTimeout(this.delayTimeout);
this.delayTimeout = null;
}
this.hass?.close();
callback?.();
}
private async syncStates(
states: { id?: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[],
): Promise<void> {
if (states?.length) {
for (const state of states) {
const id = state.id!;
delete state.id;
try {
await this.setForeignStateAsync(id, state);
} catch (err) {
this.log.error(err.toString());
}
}
}
}
private async syncObjects(objects: (ioBroker.ChannelObject | ioBroker.StateObject)[]): Promise<void> {
if (objects?.length) {
for (const obj of objects) {
this.hassObjects[obj._id] = obj;
try {
const oldObj = await this.getForeignObjectAsync(obj._id);
if (!oldObj) {
this.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
this.hassObjects[obj._id] = obj;
await this.setForeignObjectAsync(obj._id, obj);
} else {
this.hassObjects[obj._id] = oldObj as ioBroker.StateObject | ioBroker.ChannelObject;
if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) {
oldObj.native = obj.native;
this.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
await this.setForeignObjectAsync(obj._id, oldObj);
}
}
} catch (err) {
this.log.error(err.toString());
}
}
}
}
private async parseStates(
entities: {
name: string;
attributes: { friendly_name?: string; attribution?: string; unit_of_measurement?: string };
entity_id: string;
object_id: string;
last_changed?: string;
last_updated?: string;
state: ioBroker.StateValue;
domain: string;
}[],
services: Record<string, { [serviceType: string]: { description: string; fields: { entity_id: string } } }>,
): Promise<void> {
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
for (let e = 0; e < entities.length; e++) {
const entity = entities[e];
if (!entity) {
continue;
}
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
const desc = entity.attributes?.attribution || undefined;
const channel: ioBroker.ChannelObject = {
_id: `${this.namespace}.entities.${entity.entity_id}`,
common: {
name,
},
type: 'channel',
native: {
object_id: entity.object_id,
entity_id: entity.entity_id,
},
};
if (desc) {
channel.common.desc = desc;
}
objs.push(channel);
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
const obj: ioBroker.StateObject = {
_id: `${this.namespace}.entities.${entity.entity_id}.state`,
type: 'state',
common: {
name: `${name} STATE`,
type: typeof entity.state as ioBroker.CommonType,
role: 'state',
read: true,
write: false,
},
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
},
};
if (entity.attributes?.unit_of_measurement) {
obj.common.unit = entity.attributes.unit_of_measurement;
}
this.log.debug(
`Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj);
let val = entity.state;
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
let common: ioBroker.StateCommon;
if (knownAttributes[attr]) {
common = { ...knownAttributes[attr] } as ioBroker.StateCommon;
} else {
common = {} as ioBroker.StateCommon;
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
const obj: ioBroker.StateObject = {
_id: `${this.namespace}.entities.${entity.entity_id}.${attrId}`,
type: 'state',
common,
native: {
object_id: entity.object_id,
domain: entity.domain,
entity_id: entity.entity_id,
attr,
},
};
common.name ||= `${name} ${attr.replace(/_/g, ' ')}`;
common.read ??= true;
common.write ??= false;
common.type ??=
mapTypes[
typeof entity.attributes[
attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'
]
];
this.log.debug(
`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj);
let val: string | null =
entity.attributes[attr as 'friendly_name' | 'attribution' | 'unit_of_measurement'] ?? null;
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
states.push({ id: obj._id, lc, ts, val, ack: true });
}
}
}
const serviceType = entity.entity_id.split('.')[0];
if (services[serviceType] && !skipServices.includes(serviceType)) {
const service = services[serviceType];
for (const s in service) {
if (Object.prototype.hasOwnProperty.call(service, s)) {
const obj: ioBroker.StateObject = {
_id: `${this.namespace}.entities.${entity.entity_id}.${s}`,
type: 'state',
common: {
name: entity.entity_id,
desc: service[s].description,
read: false,
write: true,
type: 'mixed' as ioBroker.CommonType,
role: 'state',
},
native: {
object_id: entity.object_id,
domain: entity.domain,
fields: service[s].fields,
entity_id: entity.entity_id,
attr: s,
type: serviceType,
},
};
this.log.debug(
`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj);
}
}
}
}
await this.syncObjects(objs);
await this.syncStates(states);
}
private async main(): Promise<void> {
this.config.host ||= '127.0.0.1';
this.config.port = parseInt(String(this.config.port), 10) || 8123;
await this.setStateAsync('info.connection', false, true);
this.hass = new HASS(this.config, this.log);
this.hass.on('error', err => this.log.error(err));
this.hass.on('state_changed', entity => {
this.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
if (!entity || typeof entity.entity_id !== 'string') {
return;
}
const id = `entities.${entity.entity_id}.`;
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) {
if (this.hassObjects[`${this.namespace}.${id}state`]) {
this.setState(`${id}state`, { val: entity.state, ack: true, lc, ts });
} else {
this.log.info(
`State changed for unknown object ${id}state. Please restart the adapter to resync the objects.`,
);
}
}
if (entity.attributes) {
for (const attr in entity.attributes) {
if (
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length
) {
continue;
}
let val = entity.attributes[attr];
if ((typeof val === 'object' && val !== null) || Array.isArray(val)) {
val = JSON.stringify(val);
}
const attrId = attr.replace(this.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
if (this.hassObjects[`${this.namespace}.${id}state`]) {
this.setState(id + attrId, { val, ack: true, lc, ts });
} else {
this.log.info(
`State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
);
}
}
}
});
this.hass.on('connected', () => {
if (!this.hassConnected) {
this.log.debug('Connected');
this.hassConnected = true;
this.setState('info.connection', true, true);
this.hass!.getConfig(err => {
if (err) {
this.log.error(`Cannot read config: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass!.getStates((err, states) => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read states: ${err}`);
return;
}
this.delayTimeout = setTimeout(() => {
this.delayTimeout = null;
if (!this.stopped) {
this.hass!.getServices(async (err, services): Promise<void> => {
if (this.stopped) {
return;
}
if (err) {
this.log.error(`Cannot read states: ${err}`);
} else {
await this.parseStates(states, services);
this.log.debug(
'Initial parsing of states done, subscribe to ioBroker states',
);
await this.subscribeStatesAsync('*');
}
});
}
}, 100);
});
}
}, 100);
});
}
});
this.hass.on('disconnected', () => {
if (this.hassConnected) {
this.log.debug('Disconnected');
this.hassConnected = false;
this.setState('info.connection', false, true);
}
});
this.hass.connect();
}
}
export default HassAdapter;
if (require.main !== module) {
// Export the constructor in compact mode
module.exports = (options: Partial<AdapterOptions> | undefined) => new HassAdapter(options);
} else {
// otherwise start the instance directly
(() => new HassAdapter())();
}
-968
View File
@@ -1,968 +0,0 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const rootDir = path.normalize(__dirname + '/../../');
const pkg = require(rootDir + 'package.json');
const debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
let JSONLDB;
let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
let adapterStarted = false;
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
function loadJSONLDB() {
if (!JSONLDB) {
const dbPath = require.resolve('@alcalzone/jsonl-db', {
paths: [rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller']
});
console.log('JSONLDB path: ' + dbPath);
try {
const { JsonlDB } = require(dbPath);
JSONLDB = JsonlDB;
} catch (err) {
console.log('Jsonl require error: ' + err);
}
}
}
const appName = getAppName().toLowerCase();
let objects;
let states;
let pid = null;
let systemConfig = null;
function copyFileSync(source, target) {
let targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log('file copy error: ' +source +' -> ' + targetFile + ' (error ignored)');
}
}
function copyFolderRecursiveSync(source, target, ignore) {
let files = [];
let base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
const targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
const curSource = path.join(source, file);
const curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
async function storeOriginalFiles() {
console.log('Store original files...');
const dataDir = rootDir + 'tmp/' + appName + '-data/';
if (fs.existsSync(dataDir + 'objects.json')) {
const f = fs.readFileSync(dataDir + 'objects.json');
const objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
console.log('Store original objects.json');
}
if (fs.existsSync(dataDir + 'states.json')) {
try {
const f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
console.log('Store original states.json');
} catch (err) {
console.log('no states.json found - ignore');
}
}
if (fs.existsSync(dataDir + 'objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(dataDir + 'objects.jsonl');
await db.open();
const admin0 = db.get('system.adapter.admin.0');
if (admin0) {
if (admin0.common) {
admin0.common.enabled = false;
db.set('system.adapter.admin.0', admin0);
}
}
const admin1 = db.get('system.adapter.admin.1');
if (admin1) {
if (admin1.common) {
admin1.common.enabled = false;
db.set('system.adapter.admin.1', admin1);
}
}
await db.close();
const f = fs.readFileSync(dataDir + 'objects.jsonl');
fs.writeFileSync(dataDir + 'objects.jsonl.original', f);
console.log('Store original objects.jsonl');
}
if (fs.existsSync(dataDir + 'states.jsonl')) {
const f = fs.readFileSync(dataDir + 'states.jsonl');
fs.writeFileSync(dataDir + 'states.jsonl.original', f);
console.log('Store original states.jsonl');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
const dataDir = rootDir + 'tmp/' + appName + '-data/';
if (fs.existsSync(dataDir + 'objects.json.original')) {
const f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
}
if (fs.existsSync(dataDir + 'objects.json.original')) {
const f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
if (fs.existsSync(dataDir + 'objects.jsonl.original')) {
const f = fs.readFileSync(dataDir + 'objects.jsonl.original');
fs.writeFileSync(dataDir + 'objects.jsonl', f);
}
if (fs.existsSync(dataDir + 'objects.jsonl.original')) {
const f = fs.readFileSync(dataDir + 'states.jsonl.original');
fs.writeFileSync(dataDir + 'states.jsonl', f);
}
}
async function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
const dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
if (fs.existsSync(dataDir + 'objects.json')) {
const f = fs.readFileSync(dataDir + 'objects.json');
const objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} else if (fs.existsSync(dataDir + 'objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(dataDir + 'objects.jsonl');
try {
await db.open();
} catch (err) {
if (err.message.includes('Failed to lock DB file')) {
console.log('checkIsAdapterInstalled: DB still opened ...');
}
throw err;
}
const obj = db.get('system.adapter.' + customName + '.0');
await db.close();
if (obj) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} else {
console.error('checkIsAdapterInstalled: No objects file found in datadir ' + dataDir);
}
} catch (err) {
console.log('checkIsAdapterInstalled: catch ' + err);
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
async function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
const dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
if (fs.existsSync(dataDir + 'objects.json')) {
const f = fs.readFileSync(dataDir + 'objects.json');
const objects = JSON.parse(f.toString());
if (objects['system.certificates']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} else if (fs.existsSync(dataDir + 'objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(dataDir + 'objects.jsonl');
try {
await db.open();
} catch (err) {
if (err.message.includes('Failed to lock DB file')) {
console.log('checkIsControllerInstalled: DB still opened ...');
}
throw err;
}
const obj = db.get('system.certificates');
await db.close();
if (obj) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} else {
console.error('checkIsControllerInstalled: No objects file found in datadir ' + dataDir);
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
const startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
let _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
let __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
// TEST WISE!
//config.objects.type = 'jsonl';
//config.states.type = 'jsonl';
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(async function () {
await storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
const client = new require('net').Socket();
client.on('error', () => {});
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install dev build from npm');
child_process.execSync('npm install ' + appName + '.js-controller@dev --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
let __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
let _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
// TEST WISE!
//config.objects.type = 'jsonl';
//config.states.type = 'jsonl';
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(async function () {
await storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
const dirPath = rootDir + 'tmp/log';
let files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (let i = 0; i < files.length; i++) {
const filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
const dirPath = rootDir + 'tmp/iobroker-data/sqlite';
let files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear sqlite DB...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (let i = 0; i < files.length; i++) {
const filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Clear sqlite DB');
} catch (err) {
console.error('cannot clear DB: ' + err);
}
}
}
function setupController(cb) {
installJsController(async function (isInited) {
try {
clearControllerLog();
clearDB();
if (!isInited) {
restoreOriginalFiles();
copyAdapterToController();
}
// read system.config object
const dataDir = rootDir + 'tmp/' + appName + '-data/';
if (fs.existsSync(dataDir + 'objects.json')) {
let objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
} catch (e) {
console.log('ERROR reading/parsing system configuration. Ignore');
objs = {'system.config': {}};
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
systemConfig = objs['system.config'];
if (cb) cb(objs['system.config']);
} else if (fs.existsSync(dataDir + 'objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(dataDir + 'objects.jsonl');
await db.open();
let config = db.get('system.config');
systemConfig = config || {};
await db.close();
if (cb) cb(systemConfig);
} else {
console.error('read SystemConfig: No objects file found in datadir ' + dataDir);
}
} catch (err) {
console.error('setupController: ' + err);
}
});
}
async function getSecret() {
var dataDir = rootDir + 'tmp/' + appName + '-data/';
if (systemConfig) {
return systemConfig.native.secret;
}
if (fs.existsSync(dataDir + 'objects.json')) {
let objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
}
catch (e) {
console.warn("Could not load secret. Reason: " + e);
return null;
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
return objs['system.config'].native.secre;
} else if (fs.existsSync(dataDir + 'objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(dataDir + 'objects.jsonl');
await db.open();
let config = db.get('system.config');
config = config || {};
await db.close();
return config.native.secret;
} else {
console.error('read secret: No objects file found in datadir ' + dataDir);
}
}
function encrypt (key, value) {
var result = '';
for (var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function startAdapter(objects, states, callback) {
if (adapterStarted) {
console.log('Adapter already started ...');
if (callback) callback(objects, states);
return;
}
adapterStarted = true;
console.log('startAdapter...');
if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) {
try {
if (debug) {
// start controller
pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
} else {
// start controller
pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
}
} catch (error) {
console.error(JSON.stringify(error));
}
} else {
console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main);
}
if (callback) callback(objects, states);
}
function startController(isStartAdapter, onObjectChange, onStateChange, callback) {
if (typeof isStartAdapter === 'function') {
callback = onStateChange;
onStateChange = onObjectChange;
onObjectChange = isStartAdapter;
isStartAdapter = true;
}
if (onStateChange === undefined) {
callback = onObjectChange;
onObjectChange = undefined;
}
if (pid) {
console.error('Controller is already started!');
} else {
console.log('startController...');
try {
const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
adapterStarted = false;
let isObjectConnected;
let isStatesConnected;
// rootDir + 'tmp/node_modules
const objPath = require.resolve(`@iobroker/db-objects-${config.objects.type}`, {
paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller']
});
console.log('Objects Path: ' + objPath);
const Objects = require(objPath).Server;
objects = new Objects({
connection: {
'type': config.objects.type,
'host': '127.0.0.1',
'port': 19001,
'user': '',
'pass': '',
'noFileCache': false,
'connectTimeout': 2000
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.warn(msg);
},
error: function (msg) {
console.error(msg);
}
},
connected: function () {
isObjectConnected = true;
if (isStatesConnected) {
console.log('startController: started!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onObjectChange
});
// Just open in memory DB itself
const statePath = require.resolve(`@iobroker/db-states-${config.states.type}`, {
paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller']
});
console.log('States Path: ' + statePath);
const States = require(statePath).Server;
states = new States({
connection: {
type: config.states.type,
host: '127.0.0.1',
port: 19000,
options: {
auth_pass: null,
retry_max_delay: 15000
}
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
},
connected: function () {
isStatesConnected = true;
if (isObjectConnected) {
console.log('startController: started!!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onStateChange
});
} catch (err) {
console.log(err);
}
}
}
function stopAdapter(cb) {
if (!pid) {
console.error('Controller is not running!');
if (cb) {
setTimeout(function () {
cb(false);
}, 0);
}
} else {
adapterStarted = false;
pid.on('exit', function (code, signal) {
if (pid) {
console.log('child process terminated due to receipt of signal ' + signal);
if (cb) cb();
pid = null;
}
});
pid.on('close', function (code, signal) {
if (pid) {
if (cb) cb();
pid = null;
}
});
pid.kill('SIGTERM');
}
}
function _stopController() {
if (objects) {
objects.destroy();
objects = null;
}
if (states) {
states.destroy();
states = null;
}
}
function stopController(cb) {
let timeout;
if (objects) {
console.log('Set system.adapter.' + pkg.name + '.0');
objects.setObject('system.adapter.' + pkg.name + '.0', {
common:{
enabled: false
}
});
}
stopAdapter(function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
_stopController();
if (cb) {
cb(true);
cb = null;
}
});
timeout = setTimeout(function () {
timeout = null;
console.log('child process NOT terminated');
_stopController();
if (cb) {
cb(false);
cb = null;
}
pid = null;
}, 5000);
}
// Setup the adapter
async function setAdapterConfig(common, native, instance) {
const id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.json')) {
const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
if (common) objects[id].common = common;
if (native) objects[id].native = native;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects));
} else if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(rootDir + 'tmp/' + appName + '-data/objects.jsonl');
await db.open();
let obj = db.get(id);
if (common) obj.common = common;
if (native) obj.native = native;
db.set(id, obj);
await db.close();
} else {
console.error('setAdapterConfig: No objects file found in datadir ' + rootDir + 'tmp/' + appName + '-data/');
}
}
// Read config of the adapter
async function getAdapterConfig(instance) {
const id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.json')) {
const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
return objects[id];
} else if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.jsonl')) {
loadJSONLDB();
const db = new JSONLDB(rootDir + 'tmp/' + appName + '-data/objects.jsonl');
await db.open();
let obj = db.get(id);
await db.close();
return obj;
} else {
console.error('getAdapterConfig: No objects file found in datadir ' + rootDir + 'tmp/' + appName + '-data/');
}
}
if (typeof module !== undefined && module.parent) {
module.exports.getAdapterConfig = getAdapterConfig;
module.exports.setAdapterConfig = setAdapterConfig;
module.exports.startController = startController;
module.exports.stopController = stopController;
module.exports.setupController = setupController;
module.exports.stopAdapter = stopAdapter;
module.exports.startAdapter = startAdapter;
module.exports.installAdapter = installAdapter;
module.exports.appName = appName;
module.exports.adapterName = adapterName;
module.exports.adapterStarted = adapterStarted;
module.exports.getSecret = getSecret;
module.exports.encrypt = encrypt;
}
+3 -1
View File
@@ -1 +1,3 @@
process.on("unhandledRejection", (r) => { throw r; });
process.on('unhandledRejection', r => {
throw r;
});
+21 -83
View File
@@ -1,84 +1,20 @@
/* jshint -W097 */// jshint strict:false
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
const setup = require('@iobroker/legacy-testing');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
let objects = null;
let states = null;
const onStateChanged = null;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1);
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
describe(`Test ${adapterShortName} adapter`, function () {
before(`Test ${adapterShortName} adapter: Start js-controller`, function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(async function () {
var config = await setup.getAdapterConfig();
setup.setupController(async () => {
const config = await setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
@@ -87,14 +23,16 @@ describe('Test ' + adapterShortName + ' adapter', function() {
await setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
setup.startController(
true,
(id, obj) => {},
(id, state) => onStateChanged?.(id, state),
(_objects, _states) => {
objects = _objects;
states = _states;
_done();
});
},
);
});
});
@@ -115,11 +53,11 @@ describe('Test ' + adapterShortName + ' adapter', function() {
}, 5000);
});
*/
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
after(`Test ${adapterShortName} adapter: Stop js-controller`, function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
setup.stopController(normalTerminated => {
console.log(`Adapter normal terminated: ${normalTerminated}`);
done();
});
});
+4 -46
View File
@@ -1,47 +1,5 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
const path = require('path');
const { tests } = require('@iobroker/testing');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version).to.exist;
expect(npmPackage.version).to.exist;
if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) {
console.log('ERROR: Version numbers in package.json and io-package.json differ!!');
}
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
}
expect(ioPackage.common.authors).to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length).to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0]).to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors).to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('Testing for set authors field in io-package skipped because template adapter');
}
done();
});
});
// Validate the package files
tests.packageFiles(path.join(__dirname, '..'));
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"noEmit": false,
"declaration": false,
"rootDir": "src",
"types": ["@iobroker/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"compileOnSave": true,
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"noEmitOnError": true,
"outDir": "./build",
"removeComments": false,
"module": "Node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true,
"target": "es2022",
"sourceMap": true,
"inlineSourceMap": false,
"useUnknownInCatchVariables": false,
"types": ["@iobroker/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}