### 1.0.0 (2020-12-13)

* (bluefox) added the support of compact mode
This commit is contained in:
GermanBluefox
2020-12-13 17:50:27 +01:00
parent 09436c0929
commit d7adbddcc0
14 changed files with 658 additions and 407 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: Bug report
about: Something is not working as it should
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '...'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots & Logfiles**
If applicable, add screenshots and logfiles to help explain your problem.
**Versions:**
- Adapter version: <adapter-version>
- JS-Controller version: <js-controller-version> <!-- determine this with `iobroker -v` on the console -->
- Node version: <node-version> <!-- determine this with `node -v` on the console -->
- Operating system: <os-name>
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+78
View File
@@ -0,0 +1,78 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- enhancement
- security
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: wontfix
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs within the next 7 days.
Please check if the issue is still relevant in the most current version of the adapter
and tell us. Also check that all relevant details, logs and reproduction steps
are included and update them if needed.
Thank you for your contributions.
Dieses Problem wurde automatisch als veraltet markiert, da es in letzter Zeit keine Aktivitäten gab.
Es wird geschlossen, wenn nicht innerhalb der nächsten 7 Tage weitere Aktivitäten stattfinden.
Bitte überprüft, ob das Problem auch in der aktuellsten Version des Adapters noch relevant ist,
und teilt uns dies mit. Überprüft auch, ob alle relevanten Details, Logs und Reproduktionsschritte
enthalten sind bzw. aktualisiert diese.
Vielen Dank für Eure Unterstützung.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed because of inactivity. Please open a new
issue if still relevant and make sure to include all relevant details, logs and
reproduction steps.
Thank you for your contributions.
Dieses Problem wurde aufgrund von Inaktivität automatisch geschlossen. Bitte öffnet ein
neues Issue, falls dies noch relevant ist und stellt sicher das alle relevanten Details,
Logs und Reproduktionsschritte enthalten sind.
Vielen Dank für Eure Unterstützung.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed
+159
View File
@@ -0,0 +1,159 @@
# 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
# as well as tags with a semantic version
on:
push:
branches:
- '*'
tags:
# normal versions
- "v?[0-9]+.[0-9]+.[0-9]+"
# pre-releases
- "v?[0-9]+.[0-9]+.[0-9]+-**"
pull_request: {}
jobs:
# Performs quick checks before the expensive test runs
check-and-lint:
if: contains(github.event.head_commit.message, '[skip ci]') == false
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- 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]
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- 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]
# 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' &&
github.event.base_ref == 'refs/heads/master' &&
startsWith(github.ref, 'refs/tags/')
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@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 finalize $SENTRY_VERSION
# Add the following line BEFORE finalize if repositories are connected in Sentry
# sentry-cli releases set-commits $SENTRY_VERSION --auto
# Add the following line BEFORE finalize if sourcemap uploads are needed
# sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/
+11 -8
View File
@@ -1,9 +1,12 @@
/**/*
/gulpfile.js
gulpfile.js
tasks
tmp
test
.travis.yml
appveyor.yml
admin/i18n
iob_npm.done
package-lock.json
!/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)
Copyright (c) 2018 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2020 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
+20 -5
View File
@@ -1,23 +1,38 @@
![Logo](admin/hass.png)
# ioBroker.hass
=================
[![NPM version](http://img.shields.io/npm/v/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass)
[![Downloads](https://img.shields.io/npm/dm/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass)
[![NPM](https://nodei.co/npm/iobroker.hass.png?downloads=true)](https://nodei.co/npm/iobroker.hass/)
This adapter allows the connecting of Home Assistant to ioBroker.
This adapter allows connect Home Assistant to ioBroker.
## Usage
Create a long term token in HASS and use it as PW (copy it also in the repeat field).
## Configuration
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)**
<!--
Placeholder for the next version (at the beginning of the line):
### __WORK IN PROGRESS__
-->
## Changelog
#### 0.1.0
### 1.0.0 (2020-12-13)
* (bluefox) added the support of compact mode
### 0.1.0
* (bluefox) initial release
## License
The MIT License (MIT)
Copyright (c) 2018 bluefox <dogafox@gmail.com>
Copyright (c) 2018-2020 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
+1 -2
View File
@@ -56,12 +56,11 @@
callback(obj);
}
</script>
</head>
<body>
<div id="adapter-container">
<table><tr>
<td><img src="hass.png"/></td>
<td><img src="hass.png" alt="logo"/></td>
<td><h3 class="translate">Home assistant adapter settings</h3></td>
</tr></table>
<table>
+88 -86
View File
@@ -1,10 +1,10 @@
'use strict';
var gulp = require('gulp');
var fs = require('fs');
var pkg = require('./package.json');
var iopackage = require('./io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
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;
/*var appName = getAppName();
function getAppName() {
@@ -13,7 +13,7 @@ function getAppName() {
}
*/
const fileName = 'words.js';
var languages = {
let languages = {
en: {},
de: {},
ru: {},
@@ -22,24 +22,27 @@ var languages = {
fr: {},
it: {},
es: {},
pl: {}
pl: {},
'zh-cn': {}
};
function lang2data(lang, isFlat) {
var str = isFlat ? '' : '{\n';
var count = 0;
for (var w in lang) {
let str = isFlat ? '' : '{\n';
let count = 0;
for (const w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
var key = ' "' + w.replace(/"/g, '\\"') + '": ';
const key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
}
if (!count) return isFlat ? '' : '{\n}';
if (!count) {
return isFlat ? '' : '{\n}';
}
if (isFlat) {
return str;
} else {
@@ -49,15 +52,15 @@ function lang2data(lang, isFlat) {
function readWordJs(src) {
try {
var words;
let words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
var lines = words.split(/\r\n|\r|\n/g);
var i = 0;
const lines = words.split(/\r\n|\r|\n/g);
let i = 0;
while (!lines[i].match(/^systemDictionary = {/)) {
i++;
}
@@ -75,7 +78,7 @@ function readWordJs(src) {
lines[0] = lines[0].replace('systemDictionary = ', '');
lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}');
words = lines.join('\n');
var resultFunc = new Function('return ' + words + ';');
const resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
@@ -86,16 +89,16 @@ function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (var word in data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
var line = '';
for (var lang in data[word]) {
let line = '';
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
@@ -120,16 +123,16 @@ function writeWordJs(data, src) {
const EMPTY = '';
function words2languages(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
const langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (var word in data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
@@ -141,12 +144,12 @@ function words2languages(src) {
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var l in langs) {
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var keys = Object.keys(langs[l]);
const keys = Object.keys(langs[l]);
//keys.sort();
var obj = {};
for (var k = 0; k < keys.length; k++) {
let obj = {};
for (let k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
@@ -160,16 +163,16 @@ function words2languages(src) {
}
}
function words2languagesFlat(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
let langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (var word in data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
@@ -178,12 +181,12 @@ function words2languagesFlat(src) {
}
}
}
var keys = Object.keys(langs.en);
const keys = Object.keys(langs.en);
//keys.sort();
for (var l in langs) {
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var obj = {};
for (var k = 0; k < keys.length; k++) {
let obj = {};
for (let k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
langs[l] = obj;
@@ -191,7 +194,7 @@ function words2languagesFlat(src) {
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var ll in langs) {
for (const ll in langs) {
if (!langs.hasOwnProperty(ll)) continue;
if (!fs.existsSync(src + 'i18n/' + ll)) {
fs.mkdirSync(src + 'i18n/' + ll);
@@ -205,13 +208,13 @@ function words2languagesFlat(src) {
}
}
function languagesFlat2words(src) {
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
const dirs = fs.readdirSync(src + 'i18n/');
let langs = {};
let bigOne = {};
const order = Object.keys(languages);
dirs.sort((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;
@@ -226,19 +229,19 @@ function languagesFlat2words(src) {
return 0;
}
});
var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (var l = 0; l < dirs.length; l++) {
for (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
let lang = dirs[l];
const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i].replace(/<\/ i>/g, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').replace(/% s/g, ' %s');
});
var words = langs[lang];
for (var word in words) {
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
@@ -248,12 +251,12 @@ function languagesFlat2words(src) {
}
}
// read actual words.js
var aWords = readWordJs();
const aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
if (aWords) {
// Merge words together
for (var w in aWords) {
for (const w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
@@ -273,13 +276,13 @@ function languagesFlat2words(src) {
writeWordJs(bigOne, src);
}
function languages2words(src) {
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
const dirs = fs.readdirSync(src + 'i18n/');
let langs = {};
let bigOne = {};
const order = Object.keys(languages);
dirs.sort((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;
@@ -294,13 +297,13 @@ function languages2words(src) {
return 0;
}
});
for (var l = 0; l < dirs.length; l++) {
for (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
const lang = dirs[l];
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
var words = langs[lang];
for (var word in words) {
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
@@ -310,12 +313,12 @@ function languages2words(src) {
}
}
// read actual words.js
var aWords = readWordJs();
const aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
const temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
if (aWords) {
// Merge words together
for (var w in aWords) {
for (const w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
@@ -335,33 +338,32 @@ function languages2words(src) {
writeWordJs(bigOne, src);
}
gulp.task('adminWords2languages', function (done) {
gulp.task('admin (words.js => json)', done => {
words2languages('./admin/');
done();
});
gulp.task('adminWords2languagesFlat', function (done) {
gulp.task('admin (words.js => flat)', done => {
words2languagesFlat('./admin/');
done();
});
gulp.task('adminLanguagesFlat2words', function (done) {
gulp.task('admin (flat => words.js)', done => {
languagesFlat2words('./admin/');
done();
});
gulp.task('adminLanguages2words', function (done) {
gulp.task('admin (json => words.js)', done => {
languages2words('./admin/');
done();
});
gulp.task('updatePackages', function (done) {
gulp.task('updatePackages', done => {
iopackage.common.version = pkg.version;
iopackage.common.news = iopackage.common.news || {};
if (!iopackage.common.news[pkg.version]) {
var news = iopackage.common.news;
var newNews = {};
const news = iopackage.common.news;
let newNews = {};
newNews[pkg.version] = {
en: 'news',
@@ -374,20 +376,20 @@ gulp.task('updatePackages', function (done) {
done();
});
gulp.task('updateReadme', function (done) {
var readme = fs.readFileSync('README.md').toString();
var pos = readme.indexOf('## Changelog\n');
gulp.task('updateReadme', done => {
const readme = fs.readFileSync('README.md').toString();
const pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
var readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
var readmeEnd = readme.substring(pos + '## Changelog\n'.length);
let readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
let readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
var timestamp = new Date();
var date = timestamp.getFullYear() + '-' +
const timestamp = new Date();
const date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
var news = '';
let news = '';
if (iopackage.common.news && iopackage.common.news[pkg.version]) {
news += '* ' + iopackage.common.news[pkg.version].en;
}
@@ -398,4 +400,4 @@ gulp.task('updateReadme', function (done) {
done();
});
gulp.task('default', ['updatePackages', 'updateReadme']);
gulp.task('default', gulp.series('updatePackages', 'updateReadme'));
+21 -3
View File
@@ -1,7 +1,7 @@
{
"common": {
"name": "hass",
"version": "0.1.0",
"version": "1.0.0",
"title": "Home Assistant",
"desc": {
"en": "Home Assistant connection for ioBroker",
@@ -15,6 +15,18 @@
"pl": "Połączenie Home Assistant dla ioBroker"
},
"news": {
"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",
@@ -32,8 +44,15 @@
"mode": "daemon",
"icon": "hass.png",
"enabled": true,
"compact": true,
"materialize": true,
"license": "MIT",
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png",
"keywords": ["HASS", "Home", "assistant"],
"keywords": [
"HASS",
"Home",
"assistant"
],
"readme": "https://github.com/ioBroker/ioBroker.hass/blob/master/README.md",
"loglevel": "info",
"type": "iot-systems",
@@ -48,7 +67,6 @@
"secure": false
},
"objects": [
],
"instanceObjects": [
{
+37 -50
View File
@@ -1,30 +1,32 @@
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocket = require('ws');
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);
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;
var ERRORS = {
const ERRORS = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST'
};
this.socket = null;
var that = this;
var currentId = 1;
var requests = {};
var connected;
var connectTimeout = null;
const that = this;
let currentId = 1;
const requests = {};
let connected;
let connectTimeout = null;
function subscribeEvents(socket, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'subscribe_events', ts: Date.now(), cb: callback};
socket.send(JSON.stringify({
@@ -39,7 +41,7 @@ function HASS(options, log) {
function getConfig(socket, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'get_config', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
@@ -52,7 +54,7 @@ function HASS(options, log) {
function getStates(socket, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'get_states', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
@@ -65,7 +67,7 @@ function HASS(options, log) {
function getPanels(socket, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'get_panels', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
@@ -78,7 +80,7 @@ function HASS(options, log) {
function getServices(socket, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'get_services', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
@@ -91,7 +93,7 @@ function HASS(options, log) {
function callService(socket, service, domain, serviceData, callback) {
if (socket && typeof socket.send === 'function') {
var id = currentId++;
const id = currentId++;
requests[id] = {type: 'call_service', cb: callback, ts: Date.now()};
socket.send(JSON.stringify({
id: id,
@@ -115,10 +117,10 @@ function HASS(options, log) {
}
function initSocket(socket) {
socket.on('message', function (msg) {
socket.on('message', msg => {
log.silly(msg);
var response = JSON.parse(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') {
@@ -138,25 +140,22 @@ function HASS(options, log) {
that.emit('error', 'Password required. Connection closed');
socket.terminate();
} else {
setTimeout(function () {
sendAuth(socket, options.password);
}, 50);
setTimeout(() => sendAuth(socket, options.password), 50);
}
} else
if (response.type === 'auth_ok') {
setImmediate(function () {
subscribeEvents(socket, function (err) {
setImmediate(() =>
subscribeEvents(socket, err => {
if (!err) {
connected = true;
that.emit('connected');
}
});
});
}));
} else if (response.id === undefined) {
log.error('Invalid answer: ' + msg);
log.error(`Invalid answer: ${msg}`);
} else {
if (response.type === 'result' && requests[response.id]) {
log.debug('got answer for ' + requests[response.id].type);
log.debug(`got answer for ${requests[response.id].type}`);
if (typeof requests[response.id].cb === 'function') {
requests[response.id].cb(!response.success, response.result);
delete requests[response.id];
@@ -165,7 +164,7 @@ function HASS(options, log) {
}
});
socket.on('error', function (err) {
socket.on('error', err => {
socket = null;
if (err && err.message.indexOf('RSV2 and RSV3 must be clear') !== -1) {
// ignore deflate error
@@ -173,12 +172,12 @@ function HASS(options, log) {
log.error(err);
}
});
socket.on('open', function () {
socket.on('open', () => {
if (!connected) {
}
});
socket.on('close', function () {
socket.on('close', () => {
that.socket = null;
if (connected) {
connected = false;
@@ -186,7 +185,7 @@ function HASS(options, log) {
}
if (!connectTimeout) {
setTimeout(function () {
setTimeout(() => {
connectTimeout = null;
that.connect();
}, 3000);
@@ -194,15 +193,11 @@ function HASS(options, log) {
});
}
this.isConnected = function () {
return connected;
};
this.isConnected = () => connected;
this.getConfig = function (callback) {
if (!connected) {
if (typeof callback === 'function') {
callback('not connected');
}
typeof callback === 'function' && callback('not connected');
} else {
getConfig(this.socket, callback);
}
@@ -210,9 +205,7 @@ function HASS(options, log) {
this.getStates = function (callback) {
if (!connected) {
if (typeof callback === 'function') {
callback('not connected');
}
typeof callback === 'function' && callback('not connected');
} else {
getStates(this.socket, callback);
}
@@ -220,9 +213,7 @@ function HASS(options, log) {
this.getServices = function (callback) {
if (!connected) {
if (typeof callback === 'function') {
callback('not connected');
}
typeof callback === 'function' && callback('not connected');
} else {
getServices(this.socket, callback);
}
@@ -230,9 +221,7 @@ function HASS(options, log) {
this.getPanels = function (callback) {
if (!connected) {
if (typeof callback === 'function') {
callback('not connected');
}
typeof callback === 'function' && callback('not connected');
} else {
getPanels(this.socket, callback);
}
@@ -240,9 +229,7 @@ function HASS(options, log) {
this.callService = function (service, domain, serviceData, callback) {
if (!connected) {
if (typeof callback === 'function') {
callback('not connected');
}
typeof callback === 'function' && callback('not connected');
} else {
callService(this.socket, service, domain, serviceData, callback);
}
@@ -254,7 +241,7 @@ function HASS(options, log) {
connectTimeout = null;
}
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
});
-64
View File
@@ -1,64 +0,0 @@
var controllerDir;
var appName;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
// Get js-controller directory to load libs
function getControllerDir(isInstall) {
var fs = require('fs');
// Find the js-controller location
var controllerDir = __dirname.replace(/\\/g, '/');
controllerDir = controllerDir.split('/');
if (controllerDir[controllerDir.length - 3] === 'adapter') {
controllerDir.splice(controllerDir.length - 3, 3);
controllerDir = controllerDir.join('/');
} else if (controllerDir[controllerDir.length - 3] === 'node_modules') {
controllerDir.splice(controllerDir.length - 3, 3);
controllerDir = controllerDir.join('/');
if (fs.existsSync(controllerDir + '/node_modules/' + appName + '.js-controller')) {
controllerDir += '/node_modules/' + appName + '.js-controller';
} else if (fs.existsSync(controllerDir + '/node_modules/' + appName.toLowerCase() + '.js-controller')) {
controllerDir += '/node_modules/' + appName.toLowerCase() + '.js-controller';
} else if (!fs.existsSync(controllerDir + '/controller.js')) {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
} else if (fs.existsSync(__dirname + '/../../node_modules/' + appName.toLowerCase() + '.js-controller')) {
controllerDir.splice(controllerDir.length - 2, 2);
return controllerDir.join('/') + '/node_modules/' + appName.toLowerCase() + '.js-controller';
} else {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
return controllerDir;
}
// Read controller configuration file
function getConfig() {
var fs = require('fs');
if (fs.existsSync(controllerDir + '/conf/' + appName + '.json')) {
return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName + '.json'));
} else if (fs.existsSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json')) {
return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json'));
} else {
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
}
}
appName = getAppName();
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
exports.controllerDir = controllerDir;
exports.getConfig = getConfig;
exports.Adapter = require(controllerDir + '/lib/adapter.js');
exports.appName = appName;
+103 -104
View File
@@ -4,22 +4,26 @@
'use strict';
var utils = require(__dirname + '/lib/utils');
var HASS = require(__dirname + '/lib/hass');
const utils = require('@iobroker/adapter-core');
const HASS = require('./lib/hass');
const adapterName = require('./package.json').name.split('.').pop();
var adapter = utils.Adapter('hass');
let connected = false;
let hass;
let adapter;
const hassObjects = {};
var connected = false;
var hass;
var hassObjects = {};
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', function (id, state) {
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) {
adapter.log.warn('Cannot send command to "' + id + '", because not connected');
return;
return adapter.log.warn('Cannot send command to "' + id + '", because not connected');
}
/*if (id === adapter.namespace + '.' + '.info.resync') {
queue.push({command: 'resync'});
@@ -29,11 +33,13 @@ adapter.on('stateChange', function (id, state) {
if (!hassObjects[id].common.write) {
adapter.log.warn('Object ' + id + ' is not writable!');
} else {
var serviceData = {};
var fields = hassObjects[id].native.fields;
const serviceData = {};
const fields = hassObjects[id].native.fields;
for (var field in fields) {
if (!fields.hasOwnProperty(field)) continue;
for (const field in fields) {
if (!fields.hasOwnProperty(field)) {
continue;
}
if (field === 'entity_id') {
serviceData.entity_id = hassObjects[id].native.entity_id
@@ -42,25 +48,9 @@ adapter.on('stateChange', function (id, state) {
}
}
hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, function (err) {
if (err) {
adapter.log.error('Cannot control ' + id + ': ' + err);
hass.callService(hassObjects[id].native.attr, hassObjects[id].native.domain || hassObjects[id].native.type, serviceData, err =>
err && adapter.log.error('Cannot control ' + id + ': ' + err));
}
});
}
}
}
});
// Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ...
adapter.on('message', function (obj) {
if (typeof obj === 'object' && obj.message) {
if (obj.command === 'send') {
// e.g. send email or pushover or whatever
console.log('send command');
// Send response in callback if required
if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback);
}
}
});
@@ -69,6 +59,9 @@ adapter.on('message', function (obj) {
// start here!
adapter.on('ready', main);
return adapter;
}
function getUnit(name) {
name = name.toLowerCase();
if (name.indexOf('temperature') !== -1) {
@@ -87,37 +80,34 @@ function getUnit(name) {
function syncStates(states, cb) {
if (!states || !states.length) {
cb();
return;
return cb();
}
var state = states.shift();
var id = state.id;
const state = states.shift();
const id = state.id;
delete state.id;
adapter.setForeignState(id, state, function (err) {
if (err) adapter.log.error(err);
adapter.setForeignState(id, state, err => {
err && adapter.log.error(err);
setImmediate(syncStates, states, cb);
});
}
function syncObjects(objects, cb) {
if (!objects || !objects.length) {
cb();
return;
return cb();
}
var obj = objects.shift();
const obj = objects.shift();
hassObjects[obj._id] = obj;
adapter.getForeignObject(obj._id, function (err, oldObj) {
adapter.getForeignObject(obj._id, (err, oldObj) => {
if (err) adapter.log.error(err);
err && adapter.log.error(err);
if (!oldObj) {
adapter.log.debug('Create "' + obj._id + '"');
hassObjects[obj._id] = obj;
adapter.setForeignObject(obj._id, obj, function (err) {
if (err) adapter.log.error(err);
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
setImmediate(syncObjects, objects, cb);
});
} else {
@@ -126,8 +116,8 @@ function syncObjects(objects, cb) {
oldObj.native = obj.native;
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, oldObj, function (err) {
if (err) adapter.log.error(err);
adapter.setForeignObject(obj._id, oldObj, err => {
err => adapter.log.error(err);
setImmediate(syncObjects, objects, cb);
});
} else {
@@ -138,7 +128,7 @@ function syncObjects(objects, cb) {
}
function syncRoom(room, members, cb) {
adapter.getForeignObject('enum.rooms.' + room, function (err, obj) {
adapter.getForeignObject('enum.rooms.' + room, (err, obj) => {
if (!obj) {
obj = {
_id: 'enum.rooms.' + room,
@@ -150,15 +140,15 @@ function syncRoom(room, members, cb) {
native: {}
};
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, function (err) {
if (err) adapter.log.error(err);
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
obj.common = obj.common || {};
obj.common.members = obj.common.members || [];
var changed = false;
for (var m = 0; m < members.length; m++) {
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]);
@@ -166,8 +156,8 @@ function syncRoom(room, members, cb) {
}
if (changed) {
adapter.log.debug('Update "' + obj._id + '"');
adapter.setForeignObject(obj._id, obj, function (err) {
if (err) adapter.log.error(err);
adapter.setForeignObject(obj._id, obj, err => {
err && adapter.log.error(err);
cb();
});
} else {
@@ -177,41 +167,41 @@ function syncRoom(room, members, cb) {
});
}
var knownAttributes = {
const knownAttributes = {
azimuth: {write: false, read: true, unit: '°'},
elevation: {write: false, read: true, unit: '°'}
};
var ERRORS = {
const ERRORS = {
1: 'ERR_CANNOT_CONNECT',
2: 'ERR_INVALID_AUTH',
3: 'ERR_CONNECTION_LOST'
};
var mapTypes = {
const mapTypes = {
'string': 'string',
'number': 'number',
'object': 'mixed',
'boolean': 'boolean'
};
var skipServices = [
const skipServices = [
'persistent_notification'
];
function parseStates(entities, services, callback) {
var objs = [];
var states = [];
var obj;
var channel;
for (var e = 0; e < entities.length; e++) {
var entity = entities[e];
const objs = [];
const states = [];
let obj;
let channel;
for (let e = 0; e < entities.length; e++) {
const entity = entities[e];
if (!entity) continue;
var name = entity.name || (entity.attributes && entity.attributes.friendly_name ? entity.attributes.friendly_name : entity.entity_id);
var desc = entity.attributes && entity.attributes.attribution ? entity.attributes.attribution : undefined;
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,
_id: `${adapter.namespace}.entities.${entity.entity_id}`,
common: {
name: name
},
@@ -224,15 +214,15 @@ function parseStates(entities, services, callback) {
if (desc) channel.common.desc = desc;
objs.push(channel);
var lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
var ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
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',
_id: `${adapter.namespace}.entities.${entity.entity_id}.state`,
type: 'state',
common: {
name: name + ' STATE',
name: `${name} STATE`,
read: true,
write: false
},
@@ -250,11 +240,13 @@ function parseStates(entities, services, callback) {
}
if (entity.attributes) {
for (var attr in entity.attributes) {
for (const attr in entity.attributes) {
if (entity.attributes.hasOwnProperty(attr)) {
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') continue;
if (attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
var common;
let common;
if (knownAttributes[attr]) {
common = Object.assign({}, knownAttributes[attr]);
} else {
@@ -262,7 +254,7 @@ function parseStates(entities, services, callback) {
}
obj = {
_id: adapter.namespace + '.entities.' + entity.entity_id + '.' + attr,
_id: `${adapter.namespace}.entities.${entity.entity_id}.${attr}`,
type: 'state',
common: common,
native: {
@@ -291,14 +283,14 @@ function parseStates(entities, services, callback) {
}
}
var serviceType = entity.entity_id.split('.')[0];
const serviceType = entity.entity_id.split('.')[0];
if (services[serviceType] && skipServices.indexOf(serviceType) === -1) {
var service = services[serviceType];
for (var s in service) {
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,
_id: `${adapter.namespace}.entities.${entity.entity_id}.${s}`,
type: 'state',
common: {
desc: service[s].description,
@@ -320,9 +312,8 @@ function parseStates(entities, services, callback) {
}
}
syncObjects(objs, function () {
syncStates(states, callback);
});
syncObjects(objs, () =>
syncStates(states, callback));
}
function main() {
@@ -336,60 +327,60 @@ function main() {
hass = new HASS(adapter.config, adapter.log);
hass.on('error', function (err) {
adapter.log.error(err);
});
hass.on('state_changed', function (entity) {
var id = adapter.namespace + '.entities.' + entity.entity_id + '.';
var lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
var ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
hass.on('error', err =>
adapter.log.error(err));
hass.on('state_changed', entity => {
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 (var attr in entity.attributes) {
if (!entity.attributes.hasOwnProperty(attr) || attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') continue;
for (const attr in entity.attributes) {
if (!entity.attributes.hasOwnProperty(attr) || attr === 'friendly_name' || attr === 'unit_of_measurement' || attr === 'icon') {
continue;
}
adapter.setForeignState(id + attr, {val: entity.attributes[attr], ack: true, lc: lc, ts: ts});
}
}
});
hass.on('connected', function () {
hass.on('connected', () => {
if (!connected) {
adapter.log.debug('Connected');
connected = true;
adapter.setState('info.connection', true, true);
hass.getConfig(function (err, config) {
hass.getConfig((err, config) => {
if (err) {
adapter.log.error('Cannot read config: ' + err);
return;
}
//adapter.log.debug(JSON.stringify(config));
setTimeout(function () {
hass.getStates(function (err, states) {
setTimeout(() =>
hass.getStates((err, states) => {
if (err) {
return adapter.log.error('Cannot read states: ' + err);
}
//adapter.log.debug(JSON.stringify(states));
setTimeout(function () {
hass.getServices(function (err, services) {
setTimeout(() =>
hass.getServices((err, services) => {
if (err) {
adapter.log.error('Cannot read states: ' + err);
} else {
//adapter.log.debug(JSON.stringify(services));
parseStates(states, services, function () {
parseStates(states, services, () => {
});
}
});
}, 100);
});
}, 100);
}), 100);
}), 100);
});
}
});
hass.on('disconnected', function () {
hass.on('disconnected', () => {
if (connected) {
adapter.log.debug('Disconnected');
connected = false;
@@ -399,3 +390,11 @@ function main() {
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();
}
+10 -7
View File
@@ -1,6 +1,6 @@
{
"name": "iobroker.hass",
"version": "0.1.0",
"version": "1.0.0",
"description": "Home Assistant",
"author": {
"name": "bluefox",
@@ -26,17 +26,20 @@
},
"dependencies": {
"home-assistant-js-websocket": "^1.1.4",
"websocket": "^1.0.25",
"ws": "^5.0.0"
"websocket": "^1.0.33",
"ws": "^5.0.0",
"@iobroker/adapter-core": "^2.4.0"
},
"devDependencies": {
"gulp": "^3.9.1",
"mocha": "^4.1.0",
"chai": "^4.1.2"
"@alcalzone/release-script": "^1.8.1",
"gulp": "^4.0.2",
"mocha": "^8.2.1",
"chai": "^4.2.0"
},
"main": "main.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
"test": "node node_modules/mocha/bin/mocha --exit",
"release": "node node_modules/@alcalzone/release-script/bin/release.js"
},
"bugs": {
"url": "https://github.com/ioBroker/ioBroker.hass/issues"