Update packages

This commit is contained in:
GermanBluefox
2026-03-31 17:00:15 +02:00
parent 7bf04e494a
commit 92140fe771
17 changed files with 13927 additions and 8128 deletions
+24 -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 name: Test and Release
# Run this job on all pushes and pull requests # Run this job on all pushes and pull requests
@@ -8,14 +5,15 @@ name: Test and Release
on: on:
push: push:
branches: branches:
- '*' - "master"
tags: tags:
# normal versions # normal versions
- "v?[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+"
# pre-releases # pre-releases
- "v?[0-9]+.[0-9]+.[0-9]+-**" - "v[0-9]+.[0-9]+.[0-9]+-**"
pull_request: {} pull_request: {}
# Cancel previous PR/branch runs when a new commit is pushed
concurrency: concurrency:
group: ${{ github.ref }} group: ${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -27,134 +25,47 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- uses: actions/checkout@v4 - uses: ioBroker/testing-action-check@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: '22.x'
lint: true
- name: Install Dependencies
run: npm ci
# - 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: adapter-tests:
if: contains(github.event.head_commit.message, '[skip ci]') == false
needs: [check-and-lint] needs: [check-and-lint]
if: contains(github.event.head_commit.message, '[skip ci]') == false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x, 18.x] node-version: [20.x, 22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: ioBroker/testing-action-adapter@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
os: ${{ matrix.os }}
extra-tests: npm run test:js
- name: Install Dependencies
run: npm ci
- 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: 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: | if: |
contains(github.event.head_commit.message, '[skip ci]') == false && contains(github.event.head_commit.message, '[skip ci]') == false &&
github.event_name == 'push' && github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/') startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- name: Checkout code - uses: ioBroker/testing-action-deploy@v1
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: '22.x'
github-token: ${{ secrets.GITHUB_TOKEN }}
- 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 ci
# - 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/
-5
View File
@@ -1,5 +0,0 @@
{
"require": [
"./test/mocha.setup.js"
]
}
-10
View File
@@ -1,10 +0,0 @@
/**/*
!/admin/**/*
!/admin/*
!/lib/**/*
!/lib/*
!/io-package.json
!/package.json
!/LICENSE
!/main.js
!/README.md
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2023 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+15 -11
View File
@@ -30,10 +30,11 @@ Allowed field keys are: temperature, target_temp_high, target_temp_low, hvac_mod
### Set a stringified JSON to provide one or multiple fields ### 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 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: 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: { native: {
"fields": { "fields": {
"transition": { "transition": {
@@ -65,13 +66,15 @@ For the last option on a light.turn_off with e.g. `{"transition":10,"flash":"sho
"attr": "turn_off", "attr": "turn_off",
"type": "light" "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 look e.g. like:
``` ```json5
... {
//...
native: { native: {
"fields": { "fields": {
"speed": { "speed": {
@@ -84,9 +87,10 @@ For some services like set_speed it is required to call with a JSON object like
} }
} }
} }
... // ...
} }
... // ...
}
``` ```
## Configuration ## Configuration
@@ -98,7 +102,7 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
<!-- <!--
Placeholder for the next version (at the beginning of the line): Placeholder for the next version (at the beginning of the line):
### __WORK IN PROGRESS__ ### **WORK IN PROGRESS**
--> -->
## Changelog ## Changelog
@@ -140,7 +144,7 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
## License ## License
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2023 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+36
View File
@@ -0,0 +1,36 @@
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: [
'build/**/*',
'src/lib/**/*.js',
'admin/**/*',
'test/**/*',
'src-admin/**/*',
'src-rules/**/*',
'tasks.js',
'**/*.mjs'
],
},
];
+55 -43
View File
@@ -1,6 +1,6 @@
const util = require('util'); const util = require('node:util');
const EventEmitter = require('events').EventEmitter; const { EventEmitter } = require('node:events');
const WebSocket = require('ws'); const WebSocket = require('node:ws');
function HASS(options, log) { function HASS(options, log) {
if (!(this instanceof HASS)) { if (!(this instanceof HASS)) {
@@ -14,7 +14,7 @@ function HASS(options, log) {
const ERRORS = { const ERRORS = {
1: 'ERR_CANNOT_CONNECT', 1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH', 2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST' 3: 'ERR_CONNECTION_LOST',
}; };
this.socket = null; this.socket = null;
@@ -28,13 +28,15 @@ function HASS(options, log) {
function subscribeEvents(socket, callback) { function subscribeEvents(socket, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'subscribe_events', ts: Date.now(), cb: callback}; requests[id] = { type: 'subscribe_events', ts: Date.now(), cb: callback };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'subscribe_events' type: 'subscribe_events',
/*event_type: 'state_changed'*/ /*event_type: 'state_changed'*/
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -43,11 +45,13 @@ function HASS(options, log) {
function getConfig(socket, callback) { function getConfig(socket, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'get_config', cb: callback, ts: Date.now()}; requests[id] = { type: 'get_config', cb: callback, ts: Date.now() };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'get_config' type: 'get_config',
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -56,11 +60,13 @@ function HASS(options, log) {
function getStates(socket, callback) { function getStates(socket, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'get_states', cb: callback, ts: Date.now()}; requests[id] = { type: 'get_states', cb: callback, ts: Date.now() };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'get_states' type: 'get_states',
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -69,11 +75,13 @@ function HASS(options, log) {
function getPanels(socket, callback) { function getPanels(socket, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'get_panels', cb: callback, ts: Date.now()}; requests[id] = { type: 'get_panels', cb: callback, ts: Date.now() };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'get_panels' type: 'get_panels',
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -82,11 +90,13 @@ function HASS(options, log) {
function getServices(socket, callback) { function getServices(socket, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'get_services', cb: callback, ts: Date.now()}; requests[id] = { type: 'get_services', cb: callback, ts: Date.now() };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'get_services' type: 'get_services',
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -95,15 +105,17 @@ function HASS(options, log) {
function callService(socket, service, domain, serviceData, target, callback) { function callService(socket, service, domain, serviceData, target, callback) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
const id = currentId++; const id = currentId++;
requests[id] = {type: 'call_service', cb: callback, ts: Date.now()}; requests[id] = { type: 'call_service', cb: callback, ts: Date.now() };
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
id: id, id: id,
type: 'call_service', type: 'call_service',
domain: domain || '', domain: domain || '',
service: service, service: service,
service_data: serviceData, service_data: serviceData,
target target,
})); }),
);
} else { } else {
callback && callback('not connected'); callback && callback('not connected');
} }
@@ -111,10 +123,12 @@ function HASS(options, log) {
function sendAuth(socket, pass) { function sendAuth(socket, pass) {
if (socket && typeof socket.send === 'function') { if (socket && typeof socket.send === 'function') {
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
type: 'auth', type: 'auth',
access_token: pass access_token: pass,
})); }),
);
} }
} }
@@ -127,8 +141,7 @@ function HASS(options, log) {
if (response.event.data && response.event.event_type === 'system_log_event') { if (response.event.data && response.event.event_type === 'system_log_event') {
if (response.event.data.level === 'WARNING') { if (response.event.data.level === 'WARNING') {
log.warn('EVENT: ' + response.event.data.message); log.warn('EVENT: ' + response.event.data.message);
} else } else if (response.event.data.level === 'ERROR') {
if (response.event.data.level === 'ERROR') {
log.error('EVENT: ' + response.event.data.message); log.error('EVENT: ' + response.event.data.message);
} else { } else {
log.debug('EVENT: ' + response.event.data.message); log.debug('EVENT: ' + response.event.data.message);
@@ -136,28 +149,29 @@ function HASS(options, log) {
} else if (response.event && response.event.event_type === 'state_changed') { } else if (response.event && response.event.event_type === 'state_changed') {
that.emit('state_changed', response.event.data.new_state); that.emit('state_changed', response.event.data.new_state);
} }
} else } else if (response.type === 'auth_required') {
if (response.type === 'auth_required') {
if (!options.password) { if (!options.password) {
that.emit('error', 'Password required. Connection closed'); that.emit('error', 'Password required. Connection closed');
socket.terminate(); socket.terminate();
} else { } else {
setTimeout(() => sendAuth(socket, options.password), 50); setTimeout(() => sendAuth(socket, options.password), 50);
} }
} else } else if (response.type === 'auth_ok') {
if (response.type === 'auth_ok') {
setImmediate(() => setImmediate(() =>
subscribeEvents(socket, err => { subscribeEvents(socket, err => {
if (!err) { if (!err) {
connected = true; connected = true;
that.emit('connected'); that.emit('connected');
} }
})); }),
);
} else if (response.id === undefined) { } else if (response.id === undefined) {
log.error(`Invalid answer: ${msg}`); log.error(`Invalid answer: ${msg}`);
} else { } else {
if (response.type === 'result' && requests[response.id]) { if (response.type === 'result' && requests[response.id]) {
log.debug(`got answer for ${requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`); log.debug(
`got answer for ${requests[response.id].type} success = ${response.success}, result = ${JSON.stringify(response.result)}`,
);
if (typeof requests[response.id].cb === 'function') { if (typeof requests[response.id].cb === 'function') {
requests[response.id].cb(!response.success, response.result); requests[response.id].cb(!response.success, response.result);
delete requests[response.id]; delete requests[response.id];
@@ -176,7 +190,6 @@ function HASS(options, log) {
}); });
socket.on('open', () => { socket.on('open', () => {
if (!connected) { if (!connected) {
} }
}); });
socket.on('close', () => { socket.on('close', () => {
@@ -184,7 +197,6 @@ function HASS(options, log) {
if (connected) { if (connected) {
connected = false; connected = false;
that.emit('disconnected'); that.emit('disconnected');
} }
if (!connectTimeout && !closed) { if (!connectTimeout && !closed) {
setTimeout(() => { setTimeout(() => {
@@ -244,7 +256,7 @@ function HASS(options, log) {
} }
this.socket = new WebSocket(`ws${options.secure ? 's' : ''}://${options.host}:${options.port}/api/websocket`, { this.socket = new WebSocket(`ws${options.secure ? 's' : ''}://${options.host}:${options.port}/api/websocket`, {
perMessageDeflate: false perMessageDeflate: false,
}); });
initSocket(this.socket); initSocket(this.socket);
@@ -259,7 +271,7 @@ function HASS(options, log) {
if (this.socket) { if (this.socket) {
this.socket.close(); this.socket.close();
} }
} };
return this; return this;
} }
+108 -126
View File
@@ -6,7 +6,6 @@
const utils = require('@iobroker/adapter-core'); const utils = require('@iobroker/adapter-core');
const HASS = require('./lib/hass'); const HASS = require('./lib/hass');
const adapterName = require('./package.json').name.split('.').pop();
let connected = false; let connected = false;
let hass; let hass;
@@ -17,7 +16,7 @@ let stopped = false;
function startAdapter(options) { function startAdapter(options) {
options = options || {}; options = options || {};
Object.assign(options, {name: adapterName, unload: stop}); Object.assign(options, { name: 'hass', unload: stop });
adapter = new utils.Adapter(options); adapter = new utils.Adapter(options);
// is called if a subscribed state changes // is called if a subscribed state changes
@@ -62,31 +61,43 @@ function startAdapter(options) {
} }
} }
adapter.log.debug(`Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`); adapter.log.debug(
`Prepare service call for ${id} with (mapped) request parameters ${JSON.stringify(requestFields)} from value: ${JSON.stringify(state.val)}`,
);
if (fields) { if (fields) {
for (const field in fields) { for (const field in fields) {
if (!fields.hasOwnProperty(field)) { if (!Object.prototype.hasOwnProperty.call(fields, field)) {
continue; continue;
} }
if (field === 'entity_id') { if (field === 'entity_id') {
target.entity_id = hassObjects[id].native.entity_id target.entity_id = hassObjects[id].native.entity_id;
} else if (requestFields[field] !== undefined) { } else if (requestFields[field] !== undefined) {
serviceData[field] = requestFields[field]; serviceData[field] = requestFields[field];
} }
} }
} }
const noFields = Object.keys(serviceData).length === 0; const noFields = Object.keys(serviceData).length === 0;
serviceData.entity_id = hassObjects[id].native.entity_id 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)}`) adapter.log.debug(
hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, target, err => { `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}`); err && adapter.log.error(`Cannot control ${id}: ${err}`);
if (err && fields && noFields) { if (err && fields && noFields) {
adapter.log.warn(`Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`); adapter.log.warn(
`Please make sure to provide a stringified JSON as value to set relevant fields! Please refer to the Readme for details!`,
);
adapter.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`); adapter.log.warn(`Allowed field keys are: ${Object.keys(fields).join(', ')}`);
} }
}); },
);
} }
} }
} }
@@ -106,22 +117,6 @@ function stop(callback) {
callback && callback(); 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) { function syncStates(states, cb) {
if (!states || !states.length) { if (!states || !states.length) {
return cb(); return cb();
@@ -131,28 +126,33 @@ function syncStates(states, cb) {
delete state.id; delete state.id;
adapter.setForeignState(id, state, err => { adapter.setForeignState(id, state, err => {
err && adapter.log.error(err); if (err) {
adapter.log.error(err);
}
setImmediate(syncStates, states, cb); setImmediate(syncStates, states, cb);
}); });
} }
function syncObjects(objects, cb) { function syncObjects(objects, cb) {
if (!objects || !objects.length) { if (!objects?.length) {
return cb(); return cb();
} }
const obj = objects.shift(); const obj = objects.shift();
hassObjects[obj._id] = obj; hassObjects[obj._id] = obj;
adapter.getForeignObject(obj._id, (err, oldObj) => { adapter.getForeignObject(obj._id, (err, oldObj) => {
if (err) {
err && adapter.log.error(err); adapter.log.error(err);
}
if (!oldObj) { if (!oldObj) {
adapter.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`); adapter.log.debug(`Create "${obj._id}": ${JSON.stringify(obj.common)}`);
hassObjects[obj._id] = obj; hassObjects[obj._id] = obj;
adapter.setForeignObject(obj._id, obj, err => { adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err); if (err) {
setImmediate(syncObjects, objects, cb); adapter.log.error(err);
}
setImmediate(() => syncObjects(objects, cb));
}); });
} else { } else {
hassObjects[obj._id] = oldObj; hassObjects[obj._id] = oldObj;
@@ -161,76 +161,30 @@ function syncObjects(objects, cb) {
adapter.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`); adapter.log.debug(`Update "${obj._id}": ${JSON.stringify(obj.common)}`);
adapter.setForeignObject(obj._id, oldObj, err => { adapter.setForeignObject(obj._id, oldObj, err => {
err => adapter.log.error(err); if (err) {
setImmediate(syncObjects, objects, cb); adapter.log.error(err);
}
setImmediate(() => syncObjects(objects, cb));
}); });
} else { } else {
setImmediate(syncObjects, objects, cb); 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 = { const knownAttributes = {
azimuth: {write: false, read: true, unit: '°'}, azimuth: { write: false, read: true, unit: '°' },
elevation: {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 = { const mapTypes = {
'string': 'string', string: 'string',
'number': 'number', number: 'number',
'object': 'mixed', object: 'mixed',
'boolean': 'boolean' boolean: 'boolean',
}; };
const skipServices = [ const skipServices = ['persistent_notification'];
'persistent_notification'
];
function parseStates(entities, services, callback) { function parseStates(entities, services, callback) {
const objs = []; const objs = [];
@@ -239,23 +193,29 @@ function parseStates(entities, services, callback) {
let channel; let channel;
for (let e = 0; e < entities.length; e++) { for (let e = 0; e < entities.length; e++) {
const entity = entities[e]; const entity = entities[e];
if (!entity) continue; if (!entity) {
continue;
}
const name = entity.name || (entity.attributes && entity.attributes.friendly_name ? entity.attributes.friendly_name : entity.entity_id); 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; const desc = entity.attributes && entity.attributes.attribution ? entity.attributes.attribution : undefined;
channel = { channel = {
_id: `${adapter.namespace}.entities.${entity.entity_id}`, _id: `${adapter.namespace}.entities.${entity.entity_id}`,
common: { common: {
name: name name: name,
}, },
type: 'channel', type: 'channel',
native: { native: {
object_id: entity.object_id, object_id: entity.object_id,
entity_id: entity.entity_id entity_id: entity.entity_id,
} },
}; };
if (desc) channel.common.desc = desc; if (desc) {
channel.common.desc = desc;
}
objs.push(channel); objs.push(channel);
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined; const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
@@ -269,18 +229,20 @@ function parseStates(entities, services, callback) {
name: `${name} STATE`, name: `${name} STATE`,
type: typeof entity.state, type: typeof entity.state,
read: true, read: true,
write: false write: false,
}, },
native: { native: {
object_id: entity.object_id, object_id: entity.object_id,
domain: entity.domain, domain: entity.domain,
entity_id: entity.entity_id entity_id: entity.entity_id,
} },
}; };
if (entity.attributes && entity.attributes.unit_of_measurement) { if (entity.attributes && entity.attributes.unit_of_measurement) {
obj.common.unit = 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)}`) adapter.log.debug(
`Found Entity state ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj); objs.push(obj);
let val = entity.state; let val = entity.state;
@@ -288,12 +250,12 @@ function parseStates(entities, services, callback) {
val = JSON.stringify(val); val = JSON.stringify(val);
} }
states.push({id: obj._id, lc, ts, val, ack: true}) states.push({ id: obj._id, lc, ts, val, ack: true });
} }
if (entity.attributes) { if (entity.attributes) {
for (const attr in entity.attributes) { for (const attr in entity.attributes) {
if (entity.attributes.hasOwnProperty(attr)) { if (Object.prototype.hasOwnProperty.call(entity.attributes, attr)) {
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') { if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue; continue;
} }
@@ -314,8 +276,8 @@ function parseStates(entities, services, callback) {
object_id: entity.object_id, object_id: entity.object_id,
domain: entity.domain, domain: entity.domain,
entity_id: entity.entity_id, entity_id: entity.entity_id,
attr: attr attr: attr,
} },
}; };
if (!common.name) { if (!common.name) {
common.name = `${name} ${attr.replace(/_/g, ' ')}`; common.name = `${name} ${attr.replace(/_/g, ' ')}`;
@@ -330,7 +292,9 @@ function parseStates(entities, services, callback) {
common.type = mapTypes[typeof entity.attributes[attr]]; common.type = mapTypes[typeof entity.attributes[attr]];
} }
adapter.log.debug(`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`) adapter.log.debug(
`Found Entity attribute ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj); objs.push(obj);
@@ -339,7 +303,7 @@ function parseStates(entities, services, callback) {
val = JSON.stringify(val); val = JSON.stringify(val);
} }
states.push({id: obj._id, lc, ts, val, ack: true}); states.push({ id: obj._id, lc, ts, val, ack: true });
} }
} }
} }
@@ -349,7 +313,7 @@ function parseStates(entities, services, callback) {
if (services[serviceType] && !skipServices.includes(serviceType)) { if (services[serviceType] && !skipServices.includes(serviceType)) {
const service = services[serviceType]; const service = services[serviceType];
for (const s in service) { for (const s in service) {
if (service.hasOwnProperty(s)) { if (Object.prototype.hasOwnProperty.call(service, s)) {
obj = { obj = {
_id: `${adapter.namespace}.entities.${entity.entity_id}.${s}`, _id: `${adapter.namespace}.entities.${entity.entity_id}.${s}`,
type: 'state', type: 'state',
@@ -357,7 +321,7 @@ function parseStates(entities, services, callback) {
desc: service[s].description, desc: service[s].description,
read: false, read: false,
write: true, write: true,
type: 'mixed' type: 'mixed',
}, },
native: { native: {
object_id: entity.object_id, object_id: entity.object_id,
@@ -365,11 +329,13 @@ function parseStates(entities, services, callback) {
fields: service[s].fields, fields: service[s].fields,
entity_id: entity.entity_id, entity_id: entity.entity_id,
attr: s, attr: s,
type: serviceType type: serviceType,
} },
}; };
adapter.log.debug(`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`) adapter.log.debug(
`Found Entity service ${obj._id}: ${JSON.stringify(obj.common)} / ${JSON.stringify(obj.native)}`,
);
objs.push(obj); objs.push(obj);
} }
@@ -377,8 +343,7 @@ function parseStates(entities, services, callback) {
} }
} }
syncObjects(objs, () => syncObjects(objs, () => syncStates(states, callback));
syncStates(states, callback));
} }
function main() { function main() {
@@ -389,8 +354,7 @@ function main() {
hass = new HASS(adapter.config, adapter.log); hass = new HASS(adapter.config, adapter.log);
hass.on('error', err => hass.on('error', err => adapter.log.error(err));
adapter.log.error(err));
hass.on('state_changed', entity => { hass.on('state_changed', entity => {
adapter.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`); adapter.log.debug(`HASS-Message: State Changed: ${JSON.stringify(entity)}`);
@@ -403,14 +367,22 @@ function main() {
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined; const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
if (entity.state !== undefined) { if (entity.state !== undefined) {
if (hassObjects[`${adapter.namespace}.${id}state`]) { if (hassObjects[`${adapter.namespace}.${id}state`]) {
adapter.setState(`${id}state`, {val: entity.state, ack: true, lc: lc, ts: ts}); adapter.setState(`${id}state`, { val: entity.state, ack: true, lc: lc, ts: ts });
} else { } else {
adapter.log.info(`State changed for unknown object ${`${id}state`}. Please restart the adapter to resync the objects.`); adapter.log.info(
`State changed for unknown object ${`${id}state`}. Please restart the adapter to resync the objects.`,
);
} }
} }
if (entity.attributes) { if (entity.attributes) {
for (const attr in entity.attributes) { for (const attr in entity.attributes) {
if (!entity.attributes.hasOwnProperty(attr) || attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon'|| !attr.length) { if (
!Object.prototype.hasOwnProperty.call(entity.attributes, attr) ||
attr === 'friendly_name' ||
attr === 'unit_of_measurement' ||
attr === 'icon' ||
!attr.length
) {
continue; continue;
} }
let val = entity.attributes[attr]; let val = entity.attributes[attr];
@@ -419,9 +391,11 @@ function main() {
} }
const attrId = attr.replace(adapter.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_'); const attrId = attr.replace(adapter.FORBIDDEN_CHARS, '_').replace(/\.+$/, '_');
if (hassObjects[`${adapter.namespace}.${id}state`]) { if (hassObjects[`${adapter.namespace}.${id}state`]) {
adapter.setState(id + attrId, {val, ack: true, lc, ts}); adapter.setState(id + attrId, { val, ack: true, lc, ts });
} else { } else {
adapter.log.info(`State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`); adapter.log.info(
`State changed for unknown object ${id + attrId}. Please restart the adapter to resync the objects.`,
);
} }
} }
} }
@@ -432,7 +406,7 @@ function main() {
adapter.log.debug('Connected'); adapter.log.debug('Connected');
connected = true; connected = true;
adapter.setState('info.connection', true, true); adapter.setState('info.connection', true, true);
hass.getConfig((err, config) => { hass.getConfig(err => {
if (err) { if (err) {
adapter.log.error(`Cannot read config: ${err}`); adapter.log.error(`Cannot read config: ${err}`);
return; return;
@@ -440,7 +414,8 @@ function main() {
//adapter.log.debug(JSON.stringify(config)); //adapter.log.debug(JSON.stringify(config));
delayTimeout = setTimeout(() => { delayTimeout = setTimeout(() => {
delayTimeout = null; delayTimeout = null;
!stopped && hass.getStates((err, states) => { if (!stopped) {
hass.getStates((err, states) => {
if (stopped) { if (stopped) {
return; return;
} }
@@ -450,7 +425,8 @@ function main() {
//adapter.log.debug(JSON.stringify(states)); //adapter.log.debug(JSON.stringify(states));
delayTimeout = setTimeout(() => { delayTimeout = setTimeout(() => {
delayTimeout = null; delayTimeout = null;
!stopped && hass.getServices((err, services) => { if (!stopped) {
hass.getServices((err, services) => {
if (stopped) { if (stopped) {
return; return;
} }
@@ -459,12 +435,18 @@ function main() {
} else { } else {
//adapter.log.debug(JSON.stringify(services)); //adapter.log.debug(JSON.stringify(services));
parseStates(states, services, () => { parseStates(states, services, () => {
adapter.log.debug('Initial parsing of states done, subscribe to ioBroker states'); adapter.log.debug(
'Initial parsing of states done, subscribe to ioBroker states',
);
adapter.subscribeStates('*'); adapter.subscribeStates('*');
}); });
} }
})}, 100); });
})}, 100); }
}, 100);
});
}
}, 100);
}); });
} }
}); });
+9955 -3063
View File
File diff suppressed because it is too large Load Diff
+17 -9
View File
@@ -25,19 +25,27 @@
"url": "https://github.com/ioBroker/ioBroker.hass" "url": "https://github.com/ioBroker/ioBroker.hass"
}, },
"dependencies": { "dependencies": {
"websocket": "^1.0.34", "websocket": "^1.0.35",
"ws": "^8.13.0", "ws": "^8.20.0",
"@iobroker/adapter-core": "^2.6.8" "@iobroker/adapter-core": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@alcalzone/release-script": "^3.7.0", "@alcalzone/release-script": "^5.1.1",
"@alcalzone/release-script-plugin-iobroker": "^3.6.0", "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"@alcalzone/release-script-plugin-license": "^3.5.9", "@alcalzone/release-script-plugin-license": "^5.1.1",
"@iobroker/adapter-dev": "^1.2.0", "@iobroker/adapter-dev": "^1.5.0",
"mocha": "^10.2.0", "@iobroker/eslint-config": "^2.2.0",
"chai": "^4.3.10" "@iobroker/legacy-testing": "^2.0.2",
"@iobroker/testing": "^5.2.2",
"mocha": "^11.7.5"
}, },
"main": "main.js", "main": "main.js",
"files": [
"lib/",
"admin/",
"io-package.json",
"main.js"
],
"scripts": { "scripts": {
"test": "node node_modules/mocha/bin/mocha --exit", "test": "node node_modules/mocha/bin/mocha --exit",
"release": "release-script", "release": "release-script",
+3
View File
@@ -0,0 +1,3 @@
import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs';
export default prettierConfig;
-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;
});
+24 -86
View File
@@ -1,84 +1,20 @@
/* jshint -W097 */// jshint strict:false /* jshint -W097 */
/*jslint node: true */ /* jshint strict: false */
var expect = require('chai').expect; /* jslint node: true */
var setup = require(__dirname + '/lib/setup'); const setup = require('@iobroker/legacy-testing');
var objects = null; let objects = null;
var states = null; let states = null;
var onStateChanged = null; const onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1);
function checkConnectionOfAdapter(cb, counter) { describe(`Test ${adapterShortName} adapter`, function () {
counter = counter || 0; before(`Test ${adapterShortName} adapter: Start js-controller`, function (_done) {
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) {
this.timeout(600000); // because of first install from npm this.timeout(600000); // because of first install from npm
setup.setupController(async function () { setup.setupController(async () => {
var config = await setup.getAdapterConfig(); const config = await setup.getAdapterConfig();
// enable adapter // enable adapter
config.common.enabled = true; config.common.enabled = true;
config.common.loglevel = 'debug'; config.common.loglevel = 'debug';
@@ -87,21 +23,23 @@ describe('Test ' + adapterShortName + ' adapter', function() {
await setup.setAdapterConfig(config.common, config.native); await setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) { setup.startController(
if (onStateChanged) onStateChanged(id, state); true,
}, (id, obj) => {},
function (_objects, _states) { (id, state) => onStateChanged?.(id, state),
(_objects, _states) => {
objects = _objects; objects = _objects;
states = _states; states = _states;
_done(); _done();
}); },
);
}); });
}); });
/* /*
ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY
*/ */
/* /*
it('Test ' + adapterShortName + ' adapter: Check if connected', function (done) { it('Test ' + adapterShortName + ' adapter: Check if connected', function (done) {
this.timeout(60000); this.timeout(60000);
setTimeout(function () { setTimeout(function () {
@@ -115,11 +53,11 @@ describe('Test ' + adapterShortName + ' adapter', function() {
}, 5000); }, 5000);
}); });
*/ */
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { after(`Test ${adapterShortName} adapter: Stop js-controller`, function (done) {
this.timeout(10000); this.timeout(10000);
setup.stopController(function (normalTerminated) { setup.stopController(normalTerminated => {
console.log('Adapter normal terminated: ' + normalTerminated); console.log(`Adapter normal terminated: ${normalTerminated}`);
done(); done();
}); });
}); });
+4 -46
View File
@@ -1,47 +1,5 @@
/* jshint -W097 */ const path = require('path');
/* jshint strict:false */ const { tests } = require('@iobroker/testing');
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
describe('Test package.json and io-package.json', function() { // Validate the package files
it('Test package files', function (done) { tests.packageFiles(path.join(__dirname, '..'));
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();
});
});
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"noEmit": false,
"declaration": false
}
}
+27
View File
@@ -0,0 +1,27 @@
// Root tsconfig to set the settings and power editor support for all TS files
{
"compileOnSave": true,
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true, // Don't report errors in 3rd party definitions
"noEmitOnError": true,
"outDir": "./build",
"esModuleInterop": true,
"resolveJsonModule": true,
"module": "Node16",
"moduleResolution": "node16",
"sourceMap": true, // required for debug
"inlineSources": true, // required for debug
"strict": true,
"target": "es2022",
"inlineSourceMap": false,
"useUnknownInCatchVariables": false,
"types": ["node", "@iobroker/types"]
},
"include": ["src/**/*.*", "src/**/*.d.*"],
"exclude": ["build/**", "node_modules/**", "src-admin/**"]
}