diff --git a/.github/auto-merge.yml b/.github/auto-merge.yml index 3f5fbe3..1c01864 100644 --- a/.github/auto-merge.yml +++ b/.github/auto-merge.yml @@ -3,15 +3,14 @@ - match: # Only merge patches for production dependencies dependency_type: production - update_type: "semver:patch" + update_type: 'semver:patch' - match: # Except for security fixes, here we allow minor patches dependency_type: production - update_type: "security:minor" + update_type: 'security:minor' - match: # and development dependencies can have a minor update, too dependency_type: development - update_type: "semver:minor" - + update_type: 'semver:minor' # The syntax is based on the legacy dependabot v1 automerged_updates syntax, see: -# https://dependabot.com/docs/config-file/#automerged_updates \ No newline at end of file +# https://dependabot.com/docs/config-file/#automerged_updates diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 346fe51..1d6be8a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,17 @@ version: 2 updates: - # Maintain dependencies for GitHub Actions - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - time: "04:00" - timezone: Europe/Berlin - - package-ecosystem: npm - directory: "/" - schedule: - interval: monthly - time: "04:00" - timezone: Europe/Berlin - open-pull-requests-limit: 5 - versioning-strategy: increase + # Maintain dependencies for GitHub Actions + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: monthly + time: '04:00' + timezone: Europe/Berlin + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + time: '04:00' + timezone: Europe/Berlin + open-pull-requests-limit: 5 + versioning-strategy: increase diff --git a/.github/stale.yml b/.github/stale.yml index 8a1bc21..c8cf3a3 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -12,9 +12,9 @@ onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - - enhancement - - security - - bug + - enhancement + - security + - bug # Set to true to ignore issues in a project (defaults to false) exemptProjects: true @@ -30,19 +30,19 @@ 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. + 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: > @@ -50,22 +50,21 @@ markComment: > # 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. + 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. + 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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4af443e..88360a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,41 +1,41 @@ -name: "CodeQL" +name: 'CodeQL' on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: "52 4 * * 3" + push: + branches: ['master'] + pull_request: + branches: ['master'] + schedule: + - cron: '52 4 * * 3' jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write - strategy: - fail-fast: false - matrix: - language: [ javascript ] + strategy: + fail-fast: false + matrix: + language: [javascript] - steps: - - name: Checkout - uses: actions/checkout@v4 + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{ matrix.language }}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index dfddd2c..be71036 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -4,19 +4,19 @@ name: Auto-Merge Dependabot PRs on: - pull_request_target: + pull_request_target: jobs: - auto-merge: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + auto-merge: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Check if PR should be auto-merged - uses: ahmadnassri/action-dependabot-auto-merge@v2 - with: - # This must be a personal access token with push access - github-token: ${{ secrets.AUTO_MERGE_TOKEN }} - # By default, squash and merge, so Github chooses nice commit messages - command: squash and merge \ No newline at end of file + - name: Check if PR should be auto-merged + uses: ahmadnassri/action-dependabot-auto-merge@v2 + with: + # This must be a personal access token with push access + github-token: ${{ secrets.AUTO_MERGE_TOKEN }} + # By default, squash and merge, so Github chooses nice commit messages + command: squash and merge diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 97f3667..814b86b 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -6,148 +6,153 @@ 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: {} + push: + branches: + - '*' + tags: + # normal versions + - 'v?[0-9]+.[0-9]+.[0-9]+' + # pre-releases + - 'v?[0-9]+.[0-9]+.[0-9]+-**' + pull_request: {} # Cancel previous PR/branch runs when a new commit is pushed concurrency: - group: ${{ github.ref }} - cancel-in-progress: true + group: ${{ github.ref }} + cancel-in-progress: true 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 - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - name: Install Dependencies - run: npm i - -# - 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: [18.x, 20.x, 22.x] - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Dependencies - run: npm i - - - 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' && - startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - 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 i - -# - 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 pre-releases 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-simple-api - export SENTRY_VERSION=iobroker.simple-api@${{ 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/ + # 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 + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Install Dependencies + run: npm i + + # - 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: [18.x, 20.x, 22.x] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm i + + - name: Build + run: npm run build + + - 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' && + startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + + - 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 i + + - name: Build + run: npm run build + + # - 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 pre-releases 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-simple-api + export SENTRY_VERSION=iobroker.simple-api@${{ 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/ diff --git a/.mocharc.json b/.mocharc.json index 89a1352..6a2d8be 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,5 +1,3 @@ { - "require": [ - "./test/mocha.setup.js" - ] + "require": ["./test/mocha.setup.js"] } diff --git a/.releaseconfig.json b/.releaseconfig.json index 6f9d0f5..65cb56b 100644 --- a/.releaseconfig.json +++ b/.releaseconfig.json @@ -1,3 +1,6 @@ { - "plugins": ["iobroker", "license"] + "plugins": ["iobroker", "license"], + "exec": { + "before_commit": "npm run build" + } } diff --git a/LICENSE b/LICENSE index bbcf998..89d9d6d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright (c) 2015-2024 bluefox - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +The MIT License (MIT) + +Copyright (c) 2015-2025 bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index a4c2d65..afe6ba2 100644 --- a/README.md +++ b/README.md @@ -1,774 +1,772 @@ -![Logo](admin/simple-api.png) -# Simple-api - -![Number of Installations](http://iobroker.live/badges/simple-api-installed.svg) -![Number of Installations](http://iobroker.live/badges/simple-api-stable.svg) -[![NPM version](http://img.shields.io/npm/v/iobroker.simple-api.svg)](https://www.npmjs.com/package/iobroker.simple-api) - -![Test and Release](https://github.com/ioBroker/ioBroker.simple-api/workflows/Test%20and%20Release/badge.svg) -[![Translation status](https://weblate.iobroker.net/widgets/adapters/-/simple-api/svg-badge.svg)](https://weblate.iobroker.net/engage/adapters/?utm_source=widget) -[![Downloads](https://img.shields.io/npm/dm/iobroker.simple-api.svg)](https://www.npmjs.com/package/iobroker.simple-api) - -This is a RESTFul interface to read the objects and states from ioBroker and to write/control the states over HTTP Get/Post requests. - -**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0. - -## Usage -Call in browser `http://ipaddress:8087/help` to get the help about API. The result is: - -``` -{ - "getPlainValue": "http://ipaddress:8087/getPlainValue/stateID", - "getPlainValue": "http://ipaddress:8087/getPlainValue/stateID?json", - "get": "http://ipaddress:8087/get/stateID/?prettyPrint", - "getBulk": "http://ipaddress:8087/getBulk/stateID1,stateID2/?prettyPrint", - "set": "http://ipaddress:8087/set/stateID?value=1&prettyPrint", - "toggle": "http://ipaddress:8087/toggle/stateID?prettyPrint", - "setBulk": "http://ipaddress:8087/setBulk?stateID1=0.7&stateID2=0&prettyPrint", - "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&prettyPrint", - "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&type=adapter&prettyPrint", - "states": "http://ipaddress:8087/states?pattern=system.adapter.admin.0*&prettyPrint" - "search": "http://ipaddress:8087/search?pattern=system.adapter.admin.0*&prettyPrint", - "query": "http://ipaddress:8087/query/stateID1,stateID2/?prettyPrint" - "query": "http://ipaddress:8087/query/stateID1,stateID2/?noHistory=true&prettyPrint" - "query": "http://ipaddress:8087/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&d&prettyPrint" - "query": "http://ipaddress:8087/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&dateTo=2019-06-06T12:00:00.000Z&aggregate=minmax&count=2000&prettyPrint" -} -``` - -### getPlainValue -Call e.g.: - -`http://ipaddress:8087/getPlainValue/system.adapter.admin.0.alive` - -Result: - -`true` - -Additionally, you can use query key `json` to force the parsing of the stored value: - -`http://ipaddress:8087/getPlainValue/javascript.0.value?json` - -Result: - -`{"a":1}` - -And without `json` flag the result would be - -`"{\"a\": 1}"` - -One more useful flag could be used too, `noStringify`: - -`http://ipaddress:8087/getPlainValue/javascript.0.stringValue?noStringify` - -Result: - -`VALUETEXT` - -And without `noStringify` flag the result would be - -`"VALUETEXT"` - -### get -Call e.g.: -`http://ipaddress:8087/get/system.adapter.admin.0.alive` - -Result: -``` -{"val":true,"ack":true,"ts":1442432193,"from":"system.adapter.admin.0","lc":1442431190,"expire":23437,"_id":"system.adapter.admin.0.alive","type":"state","common":{"name":"admin.0.alive","type":"boolean","role":"indicator.state"},"native":{}} -``` - -or call e.g.: -``` -http://ipaddress:8087/get/system.adapter.admin.0.alive?prettyPrint -``` - -Result: -``` -{ - "val": true, - "ack": true, - "ts": 1442432238, - "from": "system.adapter.admin.0", - "lc": 1442431190, - "expire": 28494, - "_id": "system.adapter.admin.0.alive", - "type": "state", - "common": { - "name": "admin.0.alive", - "type": "boolean", - "role": "indicator.state" - }, - "native": {} -} -``` - -### getBulk -Get many states with one request, returned as array of objects in order of list in request and id/val/ts as sub-object - -### set -Call e.g.: -``` -http://ipaddress:8087/set/javascript.0.test?value=1 -``` -Result: -``` -{"id":"javascript.0.test","value":1} -``` -or call e.g.: -``` -http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint -``` -Result: -``` -{ - "id": "javascript.0.test", - "value": 1 -} -``` -Of course the data point `javascript.0.test` must exist. - -Additionally, the type of value could be defined: - -``` -http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint&type=string -``` - -and ack flag could be defined too: - -``` -http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint&ack=true -``` - -### toggle -Toggles value: - -- boolean: true => false, false => true -- number without limits: x => 100-x -- number with limits: x => max - (x - min) - -### setBulk -Set many states with one request. This request supports POST method too, for POST data should be in body and not URL. - -### setValueFromBody -Allows to set the value of a given State be set by the POST body content. - -Call e.g.: -`http://ipaddress:8087/setValueFromBody/0_userdata.0.example_state` -with body `hello` where `0_userdata.0.example_state` is the ID of the state. - -### objects -Read objects of defined type from DB. - -Call e.g.: -`http://ipaddress:8087/objects?pattern=enum.*&type=enum` - to read all enums - -or - -`http://ipaddress:8087/objects?pattern=system.adapter.admin.0.*` - to read all states in branch `system.adapter.admin.0` - -### states - -### search -Is a data source (History, SQL) in the configuration is set, then only the data points known to the data source are listed. -If the option 'List all data points' has been activated or no data source has been specified, all data points will be listed. -This command is needed for the Grafana JSON / SimpleJSON Plugin. - -### query -If a data source (History, SQL) has been specified in the instance configuration, the data from the specified data points are read out for the specified period, otherwise only the current value is read out. -This command is needed for the Grafana JSON / SimpleJSON Plugin. - -### help -Gives [this](#usage) output back - - -## Usage -Assume, we have no security and the server runs on default port 8087. - -For all queries the name or id of the state can be specified. - -For every request that returns JSON you can set parameter `prettyPrint` to get the output in human-readable form. - -If authentication is enabled, two other fields are mandatory: `?user=admin&pass=iobroker` - -### getPlainValue -Read state value as text. You can specify more ids divided by semicolon - -```http://ip:8087/getPlainValue/admin.0.memHeapTotal``` - -``` - 31.19 -``` - -```http://ip:8087/getPlainValue/admin.0.memHeapTotal, admin.0.memHeapUsed``` -``` - 31.19 - 17.52 -``` - -### get -Read state and object data of state as json. You can specify more ids divided by semicolon. -If more than one ID requested, the JSON array will be returned. - -```http://localhost:8087/get/admin.0.memHeapTotal/?prettyPrint``` - -``` - { - "val": 31.19, - "ack": true, - "ts": 1423154619, - "from": "system.adapter.admin.0", - "lc": 1423153989, - "_id": "system.adapter.admin.0.memHeapTotal", - "type": "state", - "common": { - "name": "admin.0.memHeapTotal", - "type": "number", - "role": "indicator.state", - "unit": "MB", - "history": { - "enabled": true, - "changesOnly": true, - "minLength": 480, - "maxLength": 960, - "retention": 604800, - "debounce": 10000 - } - }, - "native": {} - } -``` - -```http://ip:8087/get/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint``` -``` - [ - { - "val": 31.19, - "ack": true, - "ts": 1423154544, - "from": "system.adapter.admin.0", - "lc": 1423153989, - "_id": "system.adapter.admin.0.memHeapTotal", - "type": "state", - "common": { - "name": "admin.0.memHeapTotal", - "type": "number", - "role": "indicator.state", - "unit": "MB", - "history": { - "enabled": true, - "changesOnly": true, - "minLength": 480, - "maxLength": 960, - "retention": 604800, - "debounce": 10000 - } - }, - "native": {} - }, - { - "val": 16.25, - "ack": true, - "ts": 1423154544, - "from": "system.adapter.admin.0", - "lc": 1423154544, - "_id": "system.adapter.admin.0.memHeapUsed", - "type": "state", - "common": { - "name": "admin.0.memHeapUsed", - "type": "number", - "role": "indicator.state", - "unit": "MB", - "history": { - "enabled": true, - "changesOnly": true, - "minLength": 480, - "maxLength": 960, - "retention": 604800, - "debounce": 10000 - } - }, - "native": {} - } - ] -``` - -### getBulk -Read the states of more IDs with timestamp. You can specify more ids divided by semicolon. -The JSON array will be returned always. - -```http://ip:8087/getBulk/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint``` - -``` - { - "admin.0.memHeapTotal": { - "val": 31.19, - "ts": 1423154754 - }, - "admin.0.memHeapUsed": { - "val": 15.6, - "ts": 1423154754 - } - } -``` - -### set -Write the states with specified IDs. You can specify *wait* option in milliseconds to wait for answer from driver. - -```http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&prettyPrint``` -```{ - "id": "hm-rpc.0.IEQ12345.LEVEL", - "value": 1 - } -``` - -```http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&wait=5000&prettyPrint``` -```{ - "val": 1, - "ack": true, - "ts": 1423155399, - "from": "hm-rpc.0.IEQ12345.LEVEL", - "lc": 1423155399 - } -``` - -If no answer will be received in specified time, the `null` value will be returned. -In the first case the answer will be returned immediately and `ack` is false. In the second case `ack` is true. That means it was response from driver. - -### setBulk -- write bulk of IDs in one request. - -```http://ip:8087/setBulk?hm-rpc.0.FEQ1234567:1.LEVEL=0.7&Anwesenheit=0&prettyPrint``` -``` - [ - { - "id": "hm-rpc.0.FEQ1234567:1.LEVEL", - "val": "0.7" - }, - { - "error": "error: datapoint \"Anwesenheit\" not found" - } - ] -``` -You can send this request as POST too. - -### objects -Get the list of all objects for pattern. If no pattern specified all objects as JSON array will be returned. - -```http://ip:8087/objects?prettyPrint``` -``` - { - "system.adapter.admin.0.uptime": { - "_id": "system.adapter.admin.0.uptime", - "type": "state", - "common": { - "name": "admin.0.uptime", - "type": "number", - "role": "indicator.state", - "unit": "seconds" - }, - "native": {} - }, - "system.adapter.admin.0.memRss": { - "_id": "system.adapter.admin.0.memRss", - "type": "state", - "common": { - "name": "admin.0.memRss", - "desc": "Resident set size", - "type": "number", - "role": "indicator.state", - "unit": "MB", - "history": { - "enabled": true, - "changesOnly": true, - "minLength": 480, - "maxLength": 960, - "retention": 604800, - "debounce": 10000 - } - }, - "native": {} - }, - ... -``` - -Get all control objects of adapter system.adapter.admin.0: -```http://ip:8087/objects?pattern=system.adapter.admin.0*&prettyPrint``` -``` - { - "system.adapter.admin.0.uptime": { - "_id": "system.adapter.admin.0.uptime", - "type": "state", - "common": { - "name": "admin.0.uptime", - "type": "number", - "role": "indicator.state", - "unit": "seconds" - }, - "native": {} - }, - ... - -``` - -### states -Get the list of all states for pattern. If no pattern specified all states as JSON array will be returned. - -```http://ip:8087/states?prettyPrint``` -``` - { - "system.adapter.admin.0.uptime": { - "val": 32176, - "ack": true, - "ts": 1423156164, - "from": "system.adapter.admin.0", - "lc": 1423156164 - }, - "system.adapter.admin.0.memRss": { - "val": 41.14, - "ack": true, - "ts": 1423156164, - "from": "system.adapter.admin.0", - "lc": 1423156119 - }, - "system.adapter.admin.0.memHeapTotal": { - "val": 31.19, - "ack": true, - "ts": 1423156164, - "from": "system.adapter.admin.0", - "lc": 1423155084 - }, - ... -``` - -Get all control objects of adapter system.adapter.admin.0: - - ```http://ip:8087/states?pattern=system.adapter.admin.0*&prettyPrint``` -``` - { - "system.adapter.admin.0.uptime": { - "val": 32161, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423156149 - }, - "system.adapter.admin.0.memRss": { - "val": 41.14, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423156119 - }, - "system.adapter.admin.0.memHeapTotal": { - "val": 31.19, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423155084 - }, - "system.adapter.admin.0.memHeapUsed": { - "val": 19.07, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423156149 - }, - "system.adapter.admin.0.connected": { - "val": true, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423128324, - "expire": 28100 - }, - "system.adapter.admin.0.alive": { - "val": true, - "ack": true, - "ts": 1423156149, - "from": "system.adapter.admin.0", - "lc": 1423128324, - "expire": 28115 - } - } -``` - -### search -Is a data source (History, SQL) in the configuration is set, then only the data points known to the data source are listed. -If the option 'List all data points' has been activated or no data source has been specified, all data points will be listed. - -``` -http://ip:8087/search?pattern=system.adapter.admin.0*&prettyPrint -``` -``` - { - "system.adapter.admin.0.outputCount", - "system.adapter.admin.0.inputCount", - "system.adapter.admin.0.uptime", - "system.adapter.admin.0.memRss", - "system.adapter.admin.0.memHeapTotal", - "system.adapter.admin.0.memHeapUsed", - "system.adapter.admin.0.cputime", - "system.adapter.admin.0.cpu", - "system.adapter.admin.0.connected", - "system.adapter.admin.0.alive" - } -``` - -### query -If a data source (History, SQL) has been specified, the data from the specified data points are read out for the specified period. - -```http://ip:8087/query/system.host.iobroker-dev.load,system.host.iobroker-dev.memHeapUsed/?prettyPrint&dateFrom=2019-06-08T01:00:00.000Z&dateTo=2019-06-08T01:00:10.000Z``` -``` - [ - { - "target": "system.host.iobroker-dev.load", - "datapoints": [ - [ - 0.12, - 1559955600000 - ], - [ - 0.46, - 1559955601975 - ], - [ - 0.44, - 1559955610000 - ] - ] - }, - { - "target": "system.host.iobroker-dev.memHeapUsed", - "datapoints": [ - [ - 23.01, - 1559955600000 - ], - [ - 22.66, - 1559955601975 - ], - [ - 22.69, - 1559955610000 - ] - ] - } - ] -``` - -If no data source was specified or the noHistory parameter is passed, then only the current value of the data point is read out. - -```http://ip:8087/query/system.host.iobroker-dev.load,system.host.iobroker-dev.memHeapUsed/?prettyPrint&noHistory=true``` -``` - [ - { - "target": "system.host.iobroker-dev.load", - "datapoints": [ - [ - 0.58, - 1559970500342 - ] - ] - }, - { - "target": "system.host.iobroker-dev.memHeapUsed", - "datapoints": [ - [ - 21.53, - 1559970500342 - ] - ] - } - ] -``` - -## Changelog -### **WORK IN PROGRESS** -* (bluefox) Updated packages - -### 2.8.0 (2024-05-23) -* (foxriver76) ported to `@iobroker/webserver` - -### 2.7.2 (2022-10-08) -* (Apollon77) Prepare for future js-controller versions - -### 2.7.1 (2022-08-29) -* (bluefox) Check if the port is occupied only on defined interface -* (bluefox) Added JSON config - -### 2.7.0 (2022-05-31) -* (crycode-de) Allow use of ack flag for setBulk post requests -* (Apollon77) Return ack flag too on getBulk - -### 2.6.5 (2022-04-14) -* Added support aggregate and count for queries - -### 2.6.4 (2022-03-17) -* (Apollon77) Optimize performance, especially when using names instead of object ids - -### 2.6.3 (2022-02-19) -* (Apollon77) Optimize error message for multi-language objects -* (Apollon77) Do not overwrite state properties by object properties - -### 2.6.2 (2021-11-12) -* (bluefox) Support of new flags for `getPlainValue`: `json` and `noStringify` - -### 2.6.1 (2021-05-13) -* (Apollon77) Catch error in request parsing when malformed (Sentry IOBROKER-SIMPLE-API-16) - -### 2.6.0 (2021-05-09) -* (Apollon77) Also URL-Decode the path parts like state ids -* (Apollon77) Optimize for js-controller 3.3 - -### 2.5.3 (2021-01-25) -* (Apollon77) Make sure that delayed answers are not crashing (Sentry IOBROKER-SIMPLE-API-Z) - -### 2.5.2 (2021-01-09) -* (bluefox) Support of new Let's Encrypt (only with js-controller 3.2.x) - -### 2.4.8 (2020-09-17) -* (Apollon77) Make sure missing favico file locally is not throwing exceptions (Sentry IOBROKER-SIMPLE-API-G) - -### 2.4.7 (2020-08-17) -* (Apollon77) check that targets are an array for "query" requests (Sentry IOBROKER-SIMPLE-API-F) - -### 2.4.6 (2020-06-11) -* (Apollon77) Make sure adapter is showing correct error when webserver can not be initialized (Sentry IOBROKER-SIMPLE-API-7) - -### 2.4.5 (2020-05-04) -* (Apollon77) webserver initialization optimized again to prevent errors with imvalid certificates - -### 2.4.4 (2020-05-02) -* (Apollon77) Make sure Permission errors do not crash adapter (Sentry IOBROKER-SIMPLE-API-3) - -### 2.4.3 (2020-04-30) -* (Apollon77) Optimize web server error handling - -### 2.4.1 (2020-04-23) -* (bluefox) Caught the web server errors - -### 2.4.0 (2020-04-12) -* (Apollon77) Add Sentry support with js-controller 3.0 -* (Apollon77) fix potential crash - -### 2.3.3 (2019-11-16) -* (bluefox) Added response code for unknown commands - -### 2.3.2 (2019-10-18) -* (Apollon77) Fix Admin 3 support - -### 2.3.1 (2019-10-12) -* (bluefox) Admin 3 is now supported -* (bluefox) NPM packages were updated - -### 2.2.0 (2019-09-10) -* (bluefox) New flags are supported: ack and type -* (bluefox) Return error codes as JSON if no pretty print defined - -### 2.1.2 (2019-09-05) -* (Apollon77) fix compact mode - -### 2.1.0 (2019-07-05) -* (Marco.K) Added command set for the Grafana plugins JSON / SimpleJSON. Usage see https://forum.iobroker.net/topic/23033/aufruf-modifikation-simpleapi-adapter-iobroker-als-datenquelle-f%C3%BCr-grafana - -### 2.0.5 (2019-06-26) -* (Apollon77) remove logging - -### 2.0.4 (2019-06-23) -* (Apollon77) fix usage as web extension - -### 2.0.2 (2018-12-17) -* (Apollon77) fix decoding for state Ids with # in it - -### 2.0.0 (2018-06-29) -* (Giermann) BREAKING CHANGE: getBulk is returning data in a different structure - -### 1.6.3 (2018-04-15) -* (Apollon77) Return used character encoding (UTF-8) - -### 1.6.2 (2017-11-27) -* (Apollon77) Fix decoding problems - -### 1.6.1 (2017-09-25) -* (Apollon77) Fix statuscode for setBulk and optimize permission errors - -### 1.6.0 (2017-07-10) -* (Apollon77) Fix handling of URL-encoded values, they are now decoded properly -* (Apollon77) Optimize Permission handling -* (Apollon77) add possibility to only allow access to states where user is also owner, finally works correct with js-controller 1.1.1! - -### 1.5.0 (2017-03-10) -* (greyhound) Add new POST method setValueFromBody - -### 1.4.0 (2017-01-05) -* (bluefox) new web server plugin support - -### 1.3.0 (2016-08-30) -* (bluefox) compatible only with new admin - -### 1.2.0 (2016-08-27) -* (bluefox) support of letsencrypt certificates - -### 1.1.1 (2016-07-06) -* (bluefox) support of chained certificates - -### 1.1.0 (2016-02-09) -* (bluefox) fix toggle, objects, states, setBulk, POST -* (bluefox) add tests - -### 1.0.0 (2015-09-30) -* (bluefox) stop adapter before update - -### 0.1.2 (2015-06-28) -* (bluefox) add description in readme.md -* (bluefox) change "toggle" for boolean and numbers - -### 0.1.1 (2015-06-28) -* (bluefox) change setForeignState api -* (bluefox) add type to io-package.json -* (bluefox) enable run from "web" -* (bluefox) add default user - -### 0.1.0 (2015-06-10) -* (bluefox) change setForeignState api -* (bluefox) support of user permissions - -### 0.0.4 (2015-03-11) -* (bluefox) remove socket.io from file - -### 0.0.3 (2015-02-13) -* (bluefox) remove socket.io from dependencies - -### 0.0.2 (2015-02-12) -* (bluefox) enable be a part of "web" - -### 0.0.1 (2015-02-06) -* (bluefox) initial commit - -## License -The MIT License (MIT) - -Copyright (c) 2015-2024 bluefox - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +![Logo](admin/simple-api.png) +# Simple-api + +![Number of Installations](http://iobroker.live/badges/simple-api-installed.svg) +![Number of Installations](http://iobroker.live/badges/simple-api-stable.svg) +[![NPM version](http://img.shields.io/npm/v/iobroker.simple-api.svg)](https://www.npmjs.com/package/iobroker.simple-api) + +![Test and Release](https://github.com/ioBroker/ioBroker.simple-api/workflows/Test%20and%20Release/badge.svg) +[![Translation status](https://weblate.iobroker.net/widgets/adapters/-/simple-api/svg-badge.svg)](https://weblate.iobroker.net/engage/adapters/?utm_source=widget) +[![Downloads](https://img.shields.io/npm/dm/iobroker.simple-api.svg)](https://www.npmjs.com/package/iobroker.simple-api) + +This is a RESTFul interface to read the objects and states from ioBroker and to write/control the states over HTTP Get/Post requests. + +**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0. + +**Use better [`ioBroker.rest-api`](https://github.com/ioBroker/ioBroker.rest-api) instead of this adapter.** + +## Usage +Call in browser `http://ipaddress:8087/help` to get the help about API. The result is: + +```json +{ + "getPlainValue": "http://ipaddress:8087/getPlainValue/stateID", + "getPlainValue": "http://ipaddress:8087/getPlainValue/stateID?json", + "get": "http://ipaddress:8087/get/stateID/?prettyPrint", + "getBulk": "http://ipaddress:8087/getBulk/stateID1,stateID2/?prettyPrint", + "set": "http://ipaddress:8087/set/stateID?value=1&prettyPrint", + "toggle": "http://ipaddress:8087/toggle/stateID?prettyPrint", + "setBulk": "http://ipaddress:8087/setBulk?stateID1=0.7&stateID2=0&prettyPrint", + "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&prettyPrint", + "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&type=adapter&prettyPrint", + "states": "http://ipaddress:8087/states?pattern=system.adapter.admin.0*&prettyPrint" + "search": "http://ipaddress:8087/search?pattern=system.adapter.admin.0*&prettyPrint", + "query": "http://ipaddress:8087/query/stateID1,stateID2/?prettyPrint" + "query": "http://ipaddress:8087/query/stateID1,stateID2/?noHistory=true&prettyPrint" + "query": "http://ipaddress:8087/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&d&prettyPrint" + "query": "http://ipaddress:8087/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&dateTo=2019-06-06T12:00:00.000Z&aggregate=minmax&count=2000&prettyPrint" +} +``` + +### getPlainValue +Call e.g.: + +`http://ipaddress:8087/getPlainValue/system.adapter.admin.0.alive` + +Result: + +`true` + +Additionally, you can use query key `json` to force the parsing of the stored value: + +`http://ipaddress:8087/getPlainValue/javascript.0.value?json` + +Result: + +`{"a":1}` + +And without `json` flag the result would be + +`"{\"a\": 1}"` + +One more useful flag could be used too, `noStringify`: + +`http://ipaddress:8087/getPlainValue/javascript.0.stringValue?noStringify` + +Result: + +`VALUETEXT` + +And without `noStringify` flag the result would be + +`"VALUETEXT"` + +### get +Call e.g.: +`http://ipaddress:8087/get/system.adapter.admin.0.alive` + +Result: +```json +{"val":true,"ack":true,"ts":1442432193,"from":"system.adapter.admin.0","lc":1442431190,"expire":23437,"_id":"system.adapter.admin.0.alive","type":"state","common":{"name":"admin.0.alive","type":"boolean","role":"indicator.state"},"native":{}} +``` + +or call e.g.: +``` +http://ipaddress:8087/get/system.adapter.admin.0.alive?prettyPrint +``` + +Result: +```json +{ + "val": true, + "ack": true, + "ts": 1442432238, + "from": "system.adapter.admin.0", + "lc": 1442431190, + "expire": 28494, + "_id": "system.adapter.admin.0.alive", + "type": "state", + "common": { + "name": "admin.0.alive", + "type": "boolean", + "role": "indicator.state" + }, + "native": {} +} +``` + +### getBulk +Get many states with one request, returned as array of objects in order of list in request and id/val/ts as sub-object + +### set +Call e.g.: `http://ipaddress:8087/set/javascript.0.test?value=1` + +Result: +```json +{"id":"javascript.0.test","value":1} +``` +or call e.g.: `http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint` + +Result: +```json +{ + "id": "javascript.0.test", + "value": 1 +} +``` +Of course the data point `javascript.0.test` must exist. + +Additionally, the type of value could be defined: `http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint&type=string` + +and an acknowledgment flag could be defined too: `http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint&ack=true` + +### toggle +Toggles value: + +- boolean: true => false, false => true +- number without limits: x => 100-x +- number with limits: x => max - (x - min) + +### setBulk +Set many states with one request. This request supports POST method too, for POST data should be in body and not URL. + +### setValueFromBody +This command allows setting the value of a given state to be set by the POST body content. + +Call e.g.: +`http://ipaddress:8087/setValueFromBody/0_userdata.0.example_state` +with body `hello` where `0_userdata.0.example_state` is the ID of the state. + +### objects +Read objects of a defined type from DB. + +Call e.g.: +`http://ipaddress:8087/objects?pattern=enum.*&type=enum` - to read all enums + +or + +`http://ipaddress:8087/objects?pattern=system.adapter.admin.0.*` - to read all states in branch `system.adapter.admin.0` + +### states + +### search +If a data source (History, SQL) in the configuration is set, then only the data points known to the data source are listed. +If the option 'List all data points' has been activated or no data source has been specified, all data points will be listed. +This command is needed for the Grafana JSON / SimpleJSON Plugin. + +### query +If a data source (History, SQL) has been specified in the instance configuration, the data from the specified data points are read out for the specified period, otherwise only the current value is read out. +This command is needed for the Grafana JSON / SimpleJSON Plugin. + +### help +Gives [this](#usage) output back + +## Usage +Assume, we have no security and the server runs on default port 8087. + +For all queries, the name or id of the state can be specified. + +For every request that returns JSON you can set parameter `prettyPrint` to get the output in human-readable form. + +If authentication is enabled, two other fields are mandatory: `?user=admin&pass=iobroker` + +### getPlainValue +Read state value as text. You can specify more ids divided by semicolon + +`http://ip:8087/getPlainValue/admin.0.memHeapTotal` => `31.19` + +`http://ip:8087/getPlainValue/admin.0.memHeapTotal, admin.0.memHeapUsed` => + +``` + 31.19 + 17.52 +``` + +### get +Read state and object data of state as JSON. You can specify more ids divided by semicolon. +If more than one ID is requested, the JSON array will be returned. + +`http://localhost:8087/get/admin.0.memHeapTotal/?prettyPrint` => + +```json + { + "val": 31.19, + "ack": true, + "ts": 1423154619, + "from": "system.adapter.admin.0", + "lc": 1423153989, + "_id": "system.adapter.admin.0.memHeapTotal", + "type": "state", + "common": { + "name": "admin.0.memHeapTotal", + "type": "number", + "role": "indicator.state", + "unit": "MB", + "history": { + "enabled": true, + "changesOnly": true, + "minLength": 480, + "maxLength": 960, + "retention": 604800, + "debounce": 10000 + } + }, + "native": {} + } +``` + +`http://ip:8087/get/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint` => + +```json + [ + { + "val": 31.19, + "ack": true, + "ts": 1423154544, + "from": "system.adapter.admin.0", + "lc": 1423153989, + "_id": "system.adapter.admin.0.memHeapTotal", + "type": "state", + "common": { + "name": "admin.0.memHeapTotal", + "type": "number", + "role": "indicator.state", + "unit": "MB", + "history": { + "enabled": true, + "changesOnly": true, + "minLength": 480, + "maxLength": 960, + "retention": 604800, + "debounce": 10000 + } + }, + "native": {} + }, + { + "val": 16.25, + "ack": true, + "ts": 1423154544, + "from": "system.adapter.admin.0", + "lc": 1423154544, + "_id": "system.adapter.admin.0.memHeapUsed", + "type": "state", + "common": { + "name": "admin.0.memHeapUsed", + "type": "number", + "role": "indicator.state", + "unit": "MB", + "history": { + "enabled": true, + "changesOnly": true, + "minLength": 480, + "maxLength": 960, + "retention": 604800, + "debounce": 10000 + } + }, + "native": {} + } + ] +``` + +### getBulk +Read the states of more IDs with timestamp. You can specify more ids divided by semicolon. +The JSON array will be returned always. + +`http://ip:8087/getBulk/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint` => + +```json + { + "admin.0.memHeapTotal": { + "val": 31.19, + "ts": 1423154754 + }, + "admin.0.memHeapUsed": { + "val": 15.6, + "ts": 1423154754 + } + } +``` + +### set +Write the states with specified IDs. You can specify *wait* option in milliseconds to wait for answer from driver. + +`http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&prettyPrint` => + +```json +{ + "id": "hm-rpc.0.IEQ12345.LEVEL", + "value": 1 + } +``` + +`http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&wait=5000&prettyPrint` => + +```json +{ + "val": 1, + "ack": true, + "ts": 1423155399, + "from": "hm-rpc.0.IEQ12345.LEVEL", + "lc": 1423155399 + } +``` + +If no answer is received in specified time, the `null` value will be returned. +In the first case the answer will be returned immediately and `ack` is false. In the second case `ack` is true. That means it was response from driver. + +### setBulk +- write the bulk of IDs in one request. + +`http://ip:8087/setBulk?hm-rpc.0.FEQ1234567:1.LEVEL=0.7&Anwesenheit=0&prettyPrint` => + +```json + [ + { + "id": "hm-rpc.0.FEQ1234567:1.LEVEL", + "val": "0.7" + }, + { + "error": "error: datapoint \"Anwesenheit\" not found" + } + ] +``` +You can send this request as POST too. + +### objects +Get the list of all objects for pattern. If no pattern specified all objects as JSON array will be returned. + +`http://ip:8087/objects?prettyPrint` => + +```json + { + "system.adapter.admin.0.uptime": { + "_id": "system.adapter.admin.0.uptime", + "type": "state", + "common": { + "name": "admin.0.uptime", + "type": "number", + "role": "indicator.state", + "unit": "seconds" + }, + "native": {} + }, + "system.adapter.admin.0.memRss": { + "_id": "system.adapter.admin.0.memRss", + "type": "state", + "common": { + "name": "admin.0.memRss", + "desc": "Resident set size", + "type": "number", + "role": "indicator.state", + "unit": "MB", + "history": { + "enabled": true, + "changesOnly": true, + "minLength": 480, + "maxLength": 960, + "retention": 604800, + "debounce": 10000 + } + }, + "native": {} + }, + ... +``` + +Get all control objects of adapter system.adapter.admin.0: +`http://ip:8087/objects?pattern=system.adapter.admin.0*&prettyPrint` => + +```json + { + "system.adapter.admin.0.uptime": { + "_id": "system.adapter.admin.0.uptime", + "type": "state", + "common": { + "name": "admin.0.uptime", + "type": "number", + "role": "indicator.state", + "unit": "seconds" + }, + "native": {} + }, + ... + +``` + +### states +Get the list of all states for pattern. If no pattern is specified, all states as JSON array will be returned. + +`http://ip:8087/states?prettyPrint` => + +```json + { + "system.adapter.admin.0.uptime": { + "val": 32176, + "ack": true, + "ts": 1423156164, + "from": "system.adapter.admin.0", + "lc": 1423156164 + }, + "system.adapter.admin.0.memRss": { + "val": 41.14, + "ack": true, + "ts": 1423156164, + "from": "system.adapter.admin.0", + "lc": 1423156119 + }, + "system.adapter.admin.0.memHeapTotal": { + "val": 31.19, + "ack": true, + "ts": 1423156164, + "from": "system.adapter.admin.0", + "lc": 1423155084 + }, + ... +``` + +Get all control objects of adapter system.adapter.admin.0: + +`http://ip:8087/states?pattern=system.adapter.admin.0*&prettyPrint` => + +```json + { + "system.adapter.admin.0.uptime": { + "val": 32161, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423156149 + }, + "system.adapter.admin.0.memRss": { + "val": 41.14, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423156119 + }, + "system.adapter.admin.0.memHeapTotal": { + "val": 31.19, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423155084 + }, + "system.adapter.admin.0.memHeapUsed": { + "val": 19.07, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423156149 + }, + "system.adapter.admin.0.connected": { + "val": true, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423128324, + "expire": 28100 + }, + "system.adapter.admin.0.alive": { + "val": true, + "ack": true, + "ts": 1423156149, + "from": "system.adapter.admin.0", + "lc": 1423128324, + "expire": 28115 + } + } +``` + +### search +If a data source (History, SQL) in the configuration is set, then only the data points known to the data source are listed. +If the option 'List all data points' has been activated or no data source has been specified, all data points will be listed. + +`http://ip:8087/search?pattern=system.adapter.admin.0*&prettyPrint` => + +```json + { + "system.adapter.admin.0.outputCount", + "system.adapter.admin.0.inputCount", + "system.adapter.admin.0.uptime", + "system.adapter.admin.0.memRss", + "system.adapter.admin.0.memHeapTotal", + "system.adapter.admin.0.memHeapUsed", + "system.adapter.admin.0.cputime", + "system.adapter.admin.0.cpu", + "system.adapter.admin.0.connected", + "system.adapter.admin.0.alive" + } +``` + +### query +If a data source (History, SQL) has been specified, the data from the specified data points are read out for the specified period. + +`http://ip:8087/query/system.host.iobroker-dev.load,system.host.iobroker-dev.memHeapUsed/?prettyPrint&dateFrom=2019-06-08T01:00:00.000Z&dateTo=2019-06-08T01:00:10.000Z` => + +```json + [ + { + "target": "system.host.iobroker-dev.load", + "datapoints": [ + [ + 0.12, + 1559955600000 + ], + [ + 0.46, + 1559955601975 + ], + [ + 0.44, + 1559955610000 + ] + ] + }, + { + "target": "system.host.iobroker-dev.memHeapUsed", + "datapoints": [ + [ + 23.01, + 1559955600000 + ], + [ + 22.66, + 1559955601975 + ], + [ + 22.69, + 1559955610000 + ] + ] + } + ] +``` + +If no data source was specified or the noHistory parameter is passed, then only the current value of the data point is read out. + +`http://ip:8087/query/system.host.iobroker-dev.load,system.host.iobroker-dev.memHeapUsed/?prettyPrint&noHistory=true` => + +```json + [ + { + "target": "system.host.iobroker-dev.load", + "datapoints": [ + [ + 0.58, + 1559970500342 + ] + ] + }, + { + "target": "system.host.iobroker-dev.memHeapUsed", + "datapoints": [ + [ + 21.53, + 1559970500342 + ] + ] + } + ] +``` + +## Changelog +### **WORK IN PROGRESS** +* (bluefox) Updated packages +* (bluefox) Migrated to TypeScript + +### 2.8.0 (2024-05-23) +* (foxriver76) ported to `@iobroker/webserver` + +### 2.7.2 (2022-10-08) +* (Apollon77) Prepare for future js-controller versions + +### 2.7.1 (2022-08-29) +* (bluefox) Check if the port is occupied only on defined interface +* (bluefox) Added JSON config + +### 2.7.0 (2022-05-31) +* (crycode-de) Allow use of ack flag for setBulk post requests +* (Apollon77) Return ack flag too on getBulk + +### 2.6.5 (2022-04-14) +* Added support aggregate and count for queries + +### 2.6.4 (2022-03-17) +* (Apollon77) Optimize performance, especially when using names instead of object ids + +### 2.6.3 (2022-02-19) +* (Apollon77) Optimize error message for multi-language objects +* (Apollon77) Do not overwrite state properties by object properties + +### 2.6.2 (2021-11-12) +* (bluefox) Support of new flags for `getPlainValue`: `json` and `noStringify` + +### 2.6.1 (2021-05-13) +* (Apollon77) Catch error in request parsing when malformed (Sentry IOBROKER-SIMPLE-API-16) + +### 2.6.0 (2021-05-09) +* (Apollon77) Also URL-Decode the path parts like state ids +* (Apollon77) Optimize for js-controller 3.3 + +### 2.5.3 (2021-01-25) +* (Apollon77) Make sure that delayed answers are not crashing (Sentry IOBROKER-SIMPLE-API-Z) + +### 2.5.2 (2021-01-09) +* (bluefox) Support of new Let's Encrypt (only with js-controller 3.2.x) + +### 2.4.8 (2020-09-17) +* (Apollon77) Make sure missing favico file locally is not throwing exceptions (Sentry IOBROKER-SIMPLE-API-G) + +### 2.4.7 (2020-08-17) +* (Apollon77) check that targets are an array for "query" requests (Sentry IOBROKER-SIMPLE-API-F) + +### 2.4.6 (2020-06-11) +* (Apollon77) Make sure adapter is showing correct error when webserver can not be initialized (Sentry IOBROKER-SIMPLE-API-7) + +### 2.4.5 (2020-05-04) +* (Apollon77) webserver initialization optimized again to prevent errors with imvalid certificates + +### 2.4.4 (2020-05-02) +* (Apollon77) Make sure Permission errors do not crash adapter (Sentry IOBROKER-SIMPLE-API-3) + +### 2.4.3 (2020-04-30) +* (Apollon77) Optimize web server error handling + +### 2.4.1 (2020-04-23) +* (bluefox) Caught the web server errors + +### 2.4.0 (2020-04-12) +* (Apollon77) Add Sentry support with js-controller 3.0 +* (Apollon77) fix potential crash + +### 2.3.3 (2019-11-16) +* (bluefox) Added response code for unknown commands + +### 2.3.2 (2019-10-18) +* (Apollon77) Fix Admin 3 support + +### 2.3.1 (2019-10-12) +* (bluefox) Admin 3 is now supported +* (bluefox) NPM packages were updated + +### 2.2.0 (2019-09-10) +* (bluefox) New flags are supported: ack and type +* (bluefox) Return error codes as JSON if no pretty print defined + +### 2.1.2 (2019-09-05) +* (Apollon77) fix compact mode + +### 2.1.0 (2019-07-05) +* (Marco.K) Added command set for the Grafana plugins JSON / SimpleJSON. Usage see https://forum.iobroker.net/topic/23033/aufruf-modifikation-simpleapi-adapter-iobroker-als-datenquelle-f%C3%BCr-grafana + +### 2.0.5 (2019-06-26) +* (Apollon77) remove logging + +### 2.0.4 (2019-06-23) +* (Apollon77) fix usage as web extension + +### 2.0.2 (2018-12-17) +* (Apollon77) fix decoding for state Ids with # in it + +### 2.0.0 (2018-06-29) +* (Giermann) BREAKING CHANGE: getBulk is returning data in a different structure + +### 1.6.3 (2018-04-15) +* (Apollon77) Return used character encoding (UTF-8) + +### 1.6.2 (2017-11-27) +* (Apollon77) Fix decoding problems + +### 1.6.1 (2017-09-25) +* (Apollon77) Fix statuscode for setBulk and optimize permission errors + +### 1.6.0 (2017-07-10) +* (Apollon77) Fix handling of URL-encoded values, they are now decoded properly +* (Apollon77) Optimize Permission handling +* (Apollon77) add possibility to only allow access to states where user is also owner, finally works correct with js-controller 1.1.1! + +### 1.5.0 (2017-03-10) +* (greyhound) Add new POST method setValueFromBody + +### 1.4.0 (2017-01-05) +* (bluefox) new web server plugin support + +### 1.3.0 (2016-08-30) +* (bluefox) compatible only with new admin + +### 1.2.0 (2016-08-27) +* (bluefox) support of letsencrypt certificates + +### 1.1.1 (2016-07-06) +* (bluefox) support of chained certificates + +### 1.1.0 (2016-02-09) +* (bluefox) fix toggle, objects, states, setBulk, POST +* (bluefox) add tests + +### 1.0.0 (2015-09-30) +* (bluefox) stop adapter before update + +### 0.1.2 (2015-06-28) +* (bluefox) add description in readme.md +* (bluefox) change "toggle" for boolean and numbers + +### 0.1.1 (2015-06-28) +* (bluefox) change setForeignState api +* (bluefox) add type to io-package.json +* (bluefox) enable run from "web" +* (bluefox) add default user + +### 0.1.0 (2015-06-10) +* (bluefox) change setForeignState api +* (bluefox) support of user permissions + +### 0.0.4 (2015-03-11) +* (bluefox) remove socket.io from file + +### 0.0.3 (2015-02-13) +* (bluefox) remove socket.io from dependencies + +### 0.0.2 (2015-02-12) +* (bluefox) enable be a part of "web" + +### 0.0.1 (2015-02-06) +* (bluefox) initial commit + +## License +The MIT License (MIT) + +Copyright (c) 2015-2025 bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/admin/i18n/de/translations.json b/admin/i18n/de.json similarity index 100% rename from admin/i18n/de/translations.json rename to admin/i18n/de.json diff --git a/admin/i18n/en/translations.json b/admin/i18n/en.json similarity index 100% rename from admin/i18n/en/translations.json rename to admin/i18n/en.json diff --git a/admin/i18n/es/translations.json b/admin/i18n/es.json similarity index 100% rename from admin/i18n/es/translations.json rename to admin/i18n/es.json diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr.json similarity index 100% rename from admin/i18n/fr/translations.json rename to admin/i18n/fr.json diff --git a/admin/i18n/it/translations.json b/admin/i18n/it.json similarity index 100% rename from admin/i18n/it/translations.json rename to admin/i18n/it.json diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl.json similarity index 100% rename from admin/i18n/nl/translations.json rename to admin/i18n/nl.json diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl.json similarity index 100% rename from admin/i18n/pl/translations.json rename to admin/i18n/pl.json diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt.json similarity index 100% rename from admin/i18n/pt/translations.json rename to admin/i18n/pt.json diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru.json similarity index 100% rename from admin/i18n/ru/translations.json rename to admin/i18n/ru.json diff --git a/admin/i18n/zh-cn/translations.json b/admin/i18n/zh-cn.json similarity index 100% rename from admin/i18n/zh-cn/translations.json rename to admin/i18n/zh-cn.json diff --git a/admin/index.html b/admin/index.html deleted file mode 100644 index acabec2..0000000 --- a/admin/index.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - - - - - - - - - - - -
- -

simpleAPI adapter settings

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 

Let's Encrypt settings

-
- diff --git a/admin/index_m.html b/admin/index_m.html deleted file mode 100644 index 1e455b7..0000000 --- a/admin/index_m.html +++ /dev/null @@ -1,293 +0,0 @@ - - - - - - - - - - - - - - - - - - -
-
- -
-
-
- -
-
-
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
-
-
-   -
-
- - -
-
- - Allow only when User is Owner: -
-
-
-
- - -
-
- - -
-
-
-
-
-
- -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- - diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index c090a54..970bc64 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -1,151 +1,149 @@ { - "type": "tabs", - "i18n": true, - "items": { - "mainTab": { - "type": "panel", - "label": "Main settings", - "items": { - "webInstance": { - "type": "instance", - "label": "Extend WEB adapter", - "all": true, - "sm": 12, - "md": 6, - "lg": 3, - "adapter": "web" + "type": "tabs", + "i18n": true, + "items": { + "mainTab": { + "type": "panel", + "label": "Main settings", + "items": { + "webInstance": { + "type": "instance", + "label": "Extend WEB adapter", + "all": true, + "sm": 12, + "md": 6, + "lg": 3, + "adapter": "web" + }, + "bind": { + "hidden": "!!data.webInstance", + "newLine": true, + "type": "ip", + "listenOnAllPorts": true, + "label": "IP", + "sm": 12, + "md": 8, + "lg": 5 + }, + "port": { + "hidden": "!!data.webInstance", + "type": "number", + "min": 1, + "max": 65565, + "label": "Port", + "sm": 12, + "md": 4, + "lg": 3 + }, + "secure": { + "hidden": "!!data.webInstance", + "newLine": true, + "type": "checkbox", + "label": "Secure(HTTPS)", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certPublic": { + "type": "certificate", + "hidden": "!data.secure || !!data.webInstance", + "certType": "public", + "validator": "!data.secure || data.certPublic", + "label": "Public certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certPrivate": { + "hidden": "!data.secure || !!data.webInstance", + "type": "certificate", + "certType": "private", + "validator": "!data.secure || data.certPrivate", + "label": "Private certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certChained": { + "hidden": "!data.secure || !!data.webInstance", + "type": "certificate", + "certType": "chained", + "label": "Chained certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "auth": { + "newLine": true, + "hidden": "!!data.webInstance", + "type": "checkbox", + "confirm": { + "condition": "!data.secure && data.auth", + "title": "Warning!", + "text": "Unsecure_Auth", + "ok": "Ignore warning", + "cancel": "Disable authentication", + "type": "warning", + "alsoDependsOn": ["secure"] + }, + "label": "Authentication", + "sm": 12, + "md": 6, + "lg": 2 + }, + "defaultUser": { + "hidden": "!!data.auth || !!data.webInstance", + "type": "user", + "label": "Run as", + "sm": 12, + "md": 6, + "lg": 2 + }, + "onlyAllowWhenUserIsOwner": { + "newLine": true, + "type": "checkbox", + "label": "Allow only when User is Owner", + "sm": 12 + }, + "dataSource": { + "newLine": true, + "type": "instance", + "label": "Select data source", + "sm": 12, + "md": 6, + "lg": 3, + "adapter": "_dataSources" + }, + "allDatapoints": { + "type": "checkbox", + "hidden": "!data.dataSource", + "label": "List all datapoints", + "sm": 12, + "md": 6, + "lg": 3 + } + } }, - "bind": { - "hidden": "!!data.webInstance", - "newLine": true, - "type": "ip", - "listenOnAllPorts": true, - "label": "IP", - "sm": 12, - "md": 8, - "lg": 5 - }, - "port": { - "hidden": "!!data.webInstance", - "type": "number", - "min": 1, - "max": 65565, - "label": "Port", - "sm": 12, - "md": 4, - "lg": 3 - }, - "secure": { - "hidden": "!!data.webInstance", - "newLine": true, - "type": "checkbox", - "label": "Secure(HTTPS)", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certPublic": { - "type": "certificate", - "hidden": "!data.secure || !!data.webInstance", - "certType": "public", - "validator": "!data.secure || data.certPublic", - "label": "Public certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certPrivate": { - "hidden": "!data.secure || !!data.webInstance", - "type": "certificate", - "certType": "private", - "validator": "!data.secure || data.certPrivate", - "label": "Private certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certChained": { - "hidden": "!data.secure || !!data.webInstance", - "type": "certificate", - "certType": "chained", - "label": "Chained certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "auth": { - "newLine": true, - "hidden": "!!data.webInstance", - "type": "checkbox", - "confirm": { - "condition": "!data.secure && data.auth", - "title": "Warning!", - "text": "Unsecure_Auth", - "ok": "Ignore warning", - "cancel": "Disable authentication", - "type": "warning", - "alsoDependsOn": [ - "secure" - ] - }, - "label": "Authentication", - "sm": 12, - "md": 6, - "lg": 2 - }, - "defaultUser": { - "hidden": "!!data.auth || !!data.webInstance", - "type": "user", - "label": "Run as", - "sm": 12, - "md": 6, - "lg": 2 - }, - "onlyAllowWhenUserIsOwner": { - "newLine": true, - "type": "checkbox", - "label": "Allow only when User is Owner", - "sm": 12 - }, - "dataSource": { - "newLine": true, - "type": "instance", - "label": "Select data source", - "sm": 12, - "md": 6, - "lg": 3, - "adapter": "_dataSources" - }, - "allDatapoints": { - "type": "checkbox", - "hidden": "!data.dataSource", - "label": "List all datapoints", - "sm": 12, - "md": 6, - "lg": 3 - } - } - }, - "leTab": { - "type": "panel", - "label": "Let's Encrypt SSL", - "disabled": "!data.secure", - "items": { - "_image": { - "type": "staticImage", - "tooltip": "Read about Let's Encrypt certificates", - "href": "https://github.com/ioBroker/ioBroker.admin/blob/master/README.md#lets-encrypt-certificates", - "src": "../../img/le.png", - "style": { - "width": 200, - "height": 59 - } - }, - "_staticText": { - "type": "staticText", - "text": "ra_Use iobroker.acme adapter for letsencrypt certificates" + "leTab": { + "type": "panel", + "label": "Let's Encrypt SSL", + "disabled": "!data.secure", + "items": { + "_image": { + "type": "staticImage", + "tooltip": "Read about Let's Encrypt certificates", + "href": "https://github.com/ioBroker/ioBroker.admin/blob/master/README.md#lets-encrypt-certificates", + "src": "../../img/le.png", + "style": { + "width": 200, + "height": 59 + } + }, + "_staticText": { + "type": "staticText", + "text": "ra_Use iobroker.acme adapter for letsencrypt certificates" + } + } } - } } - } -} \ No newline at end of file +} diff --git a/admin/simple-api.svg b/admin/simple-api.svg new file mode 100644 index 0000000..c0f94a6 --- /dev/null +++ b/admin/simple-api.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/admin/words.js b/admin/words.js deleted file mode 100644 index ce486f9..0000000 --- a/admin/words.js +++ /dev/null @@ -1,53 +0,0 @@ -/*global systemDictionary:true */ -/* -+===================== DO NOT MODIFY ======================+ -| This file was generated by translate-adapter, please use | -| `translate-adapter adminLanguages2words` to update it. | -+===================== DO NOT MODIFY ======================+ -*/ -'use strict'; - -systemDictionary = { - "Allow only when User is Owner": { "en": "Allow only when User is Owner", "de": "Nur erlauben wenn Benutzer auch Besitzer ist", "ru": "Разрешить только когда пользователь является владельцем", "pt": "Permitir apenas quando o usuário é proprietário", "nl": "Alleen toestaan als gebruiker eigenaar is", "fr": "Autoriser uniquement lorsque l'utilisateur est propriétaire", "it": "Consenti solo quando l'utente è proprietario", "es": "Permitir solo cuando el usuario es propietario", "pl": "Zezwalaj tylko wtedy, gdy użytkownik jest właścicielem", "uk": "", "zh-cn": "仅当用户为所有者时才允许"}, - "Authentication was deactivated": { "en": "Authentication was deactivated", "de": "Die Authentifizierung wurde deaktiviert", "ru": "Аутентификация была отключена", "pt": "A autenticação foi desativada", "nl": "Verificatie was gedeactiveerd", "fr": "L'authentification a été désactivée", "it": "L'autenticazione è stata disattivata", "es": "La autenticación fue desactivada", "pl": "Uwierzytelnianie zostało dezaktywowane", "uk": "", "zh-cn": "身份认证机制已停用"}, - "Authentication": { "en": "Authentication", "de": "Authentifizierung", "ru": "Аутентификация", "pt": "Autenticação", "nl": "authenticatie", "fr": "Authentification", "it": "Autenticazione", "es": "Autenticación", "pl": "Poświadczenie", "uk": "", "zh-cn": "认证机制"}, - "Chained certificate": { "en": "Chained certificate", "de": "Verkettetes Zertifikat", "ru": "'Chained' сертификат", "pt": "Certificado acorrentado", "nl": "Geketend certificaat", "fr": "Certificat chaîné", "it": "Certificato incatenato", "es": "Certificado encadenado", "pl": "Przykuty certyfikat", "uk": "", "zh-cn": "链式证书"}, - "Disable authentication": { "en": "Disable authentication", "de": "Authentifizierung deaktivieren", "ru": "Отключить аутентификацию", "pt": "Desativar autenticação", "nl": "Schakel verificatie uit", "fr": "Désactiver l'authentification", "it": "Disabilitare l'autenticazione", "es": "Deshabilitar autenticación", "pl": "Wyłącz uwierzytelnianie", "uk": "", "zh-cn": "禁用身份认证机制"}, - "Extend WEB adapter": { "en": "Use as web adapter extension", "de": "Als Web-Adapter-Erweiterung nutzen", "ru": "Использовать, как расширение web-драйвера", "pt": "Use como uma extensão do adaptador da web", "nl": "WEB-adapter uitbreiden", "fr": "Étendre l'adaptateur WEB", "it": "Estendi l'adattatore WEB", "es": "Amplíe el adaptador WEB", "pl": "Przedłuż adapter WEB", "uk": "", "zh-cn": "扩展WEB适配器"}, - "IP": { "en": "IP", "de": "IP", "ru": "IP", "pt": "IP", "nl": "IK P", "fr": "IP", "it": "IP", "es": "IP", "pl": "IP", "uk": "", "zh-cn": "IP"}, - "Ignore warning": { "en": "Ignore warning", "de": "Warnung ignorieren", "ru": "Игнорировать предупреждение", "pt": "Ignorar aviso", "nl": "Negeer waarschuwing", "fr": "Ignorer l'avertissement", "it": "Ignora l'avviso", "es": "Ignorar advertencia", "pl": "Zignoruj ​​ostrzeżenie", "uk": "", "zh-cn": "忽略警告"}, - "Let's Encrypt settings": { "en": "Let's Encrypt settings", "de": "Let's Encrypt Einstellungen", "ru": "Настройки Let's Encrypt", "pt": "Configurações do Let's Encrypt", "nl": "Laten we de instellingen versleutelen", "fr": "Cryptons les paramètres", "it": "Let's Encrypt settings", "es": "Vamos a cifrar la configuración", "pl": "Zakodujmy ustawienia", "uk": "", "zh-cn": "设置Let's Encrypt"}, - "List all datapoints": { "en": "List all states", "de": "Alle Datenpunkte auflisten", "ru": "Разрешить получение списка всех состояний", "pt": "Listar todos os pontos de dados", "nl": "Lijst alle gegevenspunten", "fr": "Lister tous les points de données", "it": "Elenca tutti i punti dati", "es": "Listar todos los puntos de datos", "pl": "Wyświetl wszystkie punkty danych", "uk": "", "zh-cn": "列出所有数据点"}, - "Listen on all IPs": { "en": "Listen on all IPs", "de": "Zugriff von allen IPs zulassen", "ru": "Ожидать соединения на всех IP адресах", "pt": "Ouça em todos os IPs", "nl": "Luister op alle IP's", "fr": "Écoutez sur toutes les adresses IP", "it": "Ascolta su tutti gli IP", "es": "Escuchar en todas las direcciones IP", "pl": "Posłuchaj na wszystkich IP", "uk": "", "zh-cn": "监听所有IP"}, - "Port to check the domain": { "en": "Port to check the domain", "de": "Port um die Erreichbarkeit der Domain zu prüfen", "ru": "Порт для проверки доменного имени", "pt": "Porta para verificar a acessibilidade do domínio", "nl": "Poort om het domein te controleren", "fr": "Port pour vérifier le domaine", "it": "Porta per controllare il dominio", "es": "Puerto para verificar el dominio", "pl": "Port do sprawdzenia domeny", "uk": "", "zh-cn": "自动更新证书使用的端口"}, - "Port": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Porta", "nl": "Haven", "fr": "Port", "it": "Porta", "es": "Puerto", "pl": "Port", "uk": "", "zh-cn": "端口"}, - "Private certificate": { "en": "Private certificate", "de": "Privates Zertifikat", "ru": "'Private' сертификат", "pt": "Certificado privado", "nl": "Privé certificaat", "fr": "Certificat privé", "it": "Certificato privato", "es": "Certificado privado", "pl": "Prywatny certyfikat", "uk": "", "zh-cn": "私人证书"}, - "Public certificate": { "en": "Public certificate", "de": "Öffentliches Zertifikat", "ru": "'Public' сертификат", "pt": "Certificado público", "nl": "Openbaar certificaat", "fr": "Certificat public", "it": "Certificato pubblico", "es": "Certificado público", "pl": "Certyfikat publiczny", "uk": "", "zh-cn": "公共证书"}, - "Run as": { "en": "Execute as user", "de": "Ausführen als Benutzer", "ru": "Запустить от пользователя", "pt": "Correr como", "nl": "Rennen als", "fr": "Courir comme", "it": "Correre come", "es": "Correr como", "pl": "Uruchom jako", "uk": "", "zh-cn": "运行为"}, - "Secure(HTTPS)": { "en": "Secure (HTTPS)", "de": "Verschlüsselte Verbindung (HTTPS)", "ru": "Шифрование (HTTPS)", "pt": "Seguro (HTTPS)", "nl": "Secure (HTTPS)", "fr": "Sécurisé (HTTPS)", "it": "Sicuro (HTTPS)", "es": "Seguro (HTTPS)", "pl": "Bezpieczne (HTTPS)", "uk": "", "zh-cn": "安全访问(HTTPS)"}, - "Select data source": { "en": "Select data source", "de": "Datenquelle auswählen", "ru": "Выберите источник данных для \"query\" запросов", "pt": "Selecionar fonte de dados", "nl": "Selecteer gegevensbron", "fr": "Sélectionner une source de données", "it": "Seleziona l'origine dati", "es": "Seleccionar fuente de datos", "pl": "Wybierz źródło danych", "uk": "", "zh-cn": "选择数据源"}, - "Set certificates or load it first in the system settings (right top).": {"en": "Set certificates or load it first in the system settings (right top).", "de": "Zertifikate wählen bzw. Zertifikate in den Systemeinstellungen (oben rechts) hinterlegen.", "ru": "Нужно выбрать сертификаты или сначала загрузить их в системных настройках.", "pt": "Defina certificados ou carregue primeiro nas configurações do sistema (parte superior direita).", "nl": "Stel certificaten in of laad het eerst in de systeeminstellingen (rechtsboven).", "fr": "Définissez des certificats ou chargez-les d'abord dans les paramètres du système (en haut à droite).", "it": "Imposta i certificati o caricali prima nelle impostazioni di sistema (in alto a destra).", "es": "Establezca certificados o cárguelos primero en la configuración del sistema (arriba a la derecha).", "pl": "Ustaw certyfikaty lub załaduj najpierw w ustawieniach systemu (prawy górny).", "uk": "", "zh-cn": "首先在系统设置中(右上角)设置或加载证书。"}, - "Unsecure_Auth": { "en": "The password will be sent unencrypted via an unsecure connection. To protect your passwords enable the secure connection (HTTPS)!", "de": "Das Passwort wird über eine unverschlüsselte Verbindung gesendet. Um Ihre Passwörter zu schützen, aktivieren Sie die sichere Verbindung (HTTPS)!", "ru": "Пароль будет отправлен через незащищенное соединение. Для защиты ваших паролей активируйте безопасное соединение (HTTPS)!", "pt": "A senha será enviada por meio de conexão não segura. Para proteger suas senhas, ative a conexão segura (HTTPS)!", "nl": "Het wachtwoord wordt verzonden via onbeveiligde verbinding. Ter beveiliging van uw wachtwoorden schakelt u de beveiligde verbinding (HTTPS) in!", "fr": "Le mot de passe sera envoyé via une connexion non sécurisée. Pour protéger vos mots de passe, activez la connexion sécurisée (HTTPS)!", "it": "La password verrà inviata tramite connessione non protetta. Per proteggere le tue password abilita la connessione sicura (HTTPS)!", "es": "La contraseña se enviará a través de una conexión no segura. Para proteger sus contraseñas, ¡habilite la conexión segura (HTTPS)!", "pl": "Hasło zostanie wysłane przez połączenie bez zabezpieczeń. Aby chronić swoje hasła, włącz bezpieczne połączenie (HTTPS)!", "uk": "", "zh-cn": "密码将通过不安全的连接发送。 要保护您的密码,请启用安全连接(HTTPS)!"}, - "Use Lets Encrypt certificates": { "en": "Use Let's Encrypt certificates", "de": "Let's Encrypt Zertifikate verwenden", "ru": "Использовать сертификаты Let's Encrypt", "pt": "Use certificados do Let's Encrypt", "nl": "Gebruik Let's Encrypt-certificaten", "fr": "Utiliser les certificats Let's Encrypt", "it": "Utilizza Let's Encrypt certificates", "es": "Utilice los certificados Let's Encrypt", "pl": "Użyj Let's Encrypt certificates", "uk": "", "zh-cn": "使用Let's Encrypt证书"}, - "Use this instance for automatic update": { "en": "Use this instance for automatic update", "de": "Diese Instanz für automatische Zertifikat-Updates benutzen", "ru": "Использовать этот экземпляр для обновления сертификатов", "pt": "Use esta instância para atualizações automáticas de certificados", "nl": "Gebruik deze instantie voor automatische update", "fr": "Utilisez cette instance pour la mise à jour automatique", "it": "Utilizza questa istanza per l'aggiornamento automatico", "es": "Use esta instancia para la actualización automática", "pl": "Użyj tej instancji do automatycznej aktualizacji", "uk": "", "zh-cn": "自动更新Let's Encrypt证书"}, - "Warning!": { "en": "Warning!", "de": "Warnung!", "ru": "Внимание!", "pt": "Aviso!", "nl": "Waarschuwing!", "fr": "Attention!", "it": "Avvertimento!", "es": "¡Advertencia!", "pl": "Ostrzeżenie!", "uk": "", "zh-cn": "警告!"}, - "all": { "en": "all", "de": "alle", "ru": "все", "pt": "todos", "nl": "allemaal", "fr": "tout", "it": "tutti", "es": "todas", "pl": "wszystko", "uk": "", "zh-cn": "所有"}, - "none": { "en": "none", "de": "keins", "ru": "никакой", "pt": "Nenhum", "nl": "geen", "fr": "aucun", "it": "nessuna", "es": "ninguna", "pl": "Żaden", "uk": "", "zh-cn": "无"}, - "place here": { "en": "Drop the files here", "de": "Dateien hier ablegen", "ru": "Перетащите файлы сюда", "pt": "coloque os arquivos aqui", "nl": "plaats de bestanden hier", "fr": "Placez les fichiers ici", "it": "posiziona i file qui", "es": "coloca los archivos aquí", "pl": "umieść pliki tutaj", "uk": "", "zh-cn": "将文件拖拽到这里"}, - "Allow only when User is Owner:": { "en": "Allow only when User is Owner", "de": "Nur erlauben wenn Benutzer auch Besitzer ist", "ru": "Разрешить только когда пользователь является владельцем", "pt": "Permitir apenas quando o usuário é proprietário", "nl": "Alleen toestaan als gebruiker eigenaar is", "fr": "Autoriser uniquement lorsque l'utilisateur est propriétaire", "it": "Consenti solo quando l'utente è proprietario", "es": "Permitir solo cuando el usuario es propietario", "pl": "Zezwalaj tylko wtedy, gdy użytkownik jest właścicielem", "zh-cn": "仅当用户为所有者时才允许"}, - "Authentication:": { "en": "Authentication", "de": "Authentifizierung", "ru": "Аутентификация", "pt": "Autenticação", "nl": "authenticatie", "fr": "Authentification", "it": "Autenticazione", "es": "Autenticación", "pl": "Poświadczenie", "zh-cn": "认证机制"}, - "Chained certificate:": { "en": "Chained certificate", "de": "Verkettetes Zertifikat", "ru": "'Chained' сертификат", "pt": "Certificado acorrentado", "nl": "Geketend certificaat", "fr": "Certificat chaîné", "it": "Certificato incatenato", "es": "Certificado encadenado", "pl": "Przykuty certyfikat", "zh-cn": "链式证书"}, - "Extend WEB adapter:": { "en": "Use as web adapter extension", "de": "Als Web-Adapter-Erweiterung nutzen", "ru": "Использовать, как расширение web-драйвера", "pt": "Use como uma extensão do adaptador da web", "nl": "WEB-adapter uitbreiden", "fr": "Étendre l'adaptateur WEB", "it": "Estendi l'adattatore WEB", "es": "Amplíe el adaptador WEB", "pl": "Przedłuż adapter WEB", "zh-cn": "扩展WEB适配器"}, - "IP:": { "en": "IP", "de": "IP", "ru": "IP", "pt": "IP", "nl": "IK P", "fr": "IP", "it": "IP", "es": "IP", "pl": "IP", "zh-cn": "IP"}, - "List all datapoints:": { "en": "List all data points", "de": "Alle Datenpunkte auflisten", "ru": "Разрешить получение списка всех состояний", "pt": "Listar todos os pontos de dados", "nl": "Lijst alle gegevenspunten", "fr": "Lister tous les points de données", "it": "Elenca tutti i punti dati", "es": "Listar todos los puntos de datos", "pl": "Wyświetl wszystkie punkty danych", "zh-cn": "列出所有数据点"}, - "Port to check the domain:": { "en": "Port to check the domain", "de": "Port um die Erreichbarkeit der Domain zu prüfen", "ru": "Порт для проверки доменного имени", "pt": "Porta para verificar a acessibilidade do domínio", "nl": "Poort om het domein te controleren", "fr": "Port pour vérifier le domaine", "it": "Porta per controllare il dominio", "es": "Puerto para verificar el dominio", "pl": "Port do sprawdzenia domeny", "zh-cn": "自动更新证书使用的端口"}, - "Port:": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Porta", "nl": "Haven", "fr": "Port", "it": "Porta", "es": "Puerto", "pl": "Port", "zh-cn": "端口"}, - "Private certificate:": { "en": "Private certificate", "de": "Privates Zertifikat", "ru": "'Private' сертификат", "pt": "Certificado privado", "nl": "Privé certificaat", "fr": "Certificat privé", "it": "Certificato privato", "es": "Certificado privado", "pl": "Prywatny certyfikat", "zh-cn": "私人证书"}, - "Public certificate:": { "en": "Public certificate", "de": "Öffentliches Zertifikat", "ru": "'Public' сертификат", "pt": "Certificado público", "nl": "Openbaar certificaat", "fr": "Certificat public", "it": "Certificato pubblico", "es": "Certificado público", "pl": "Certyfikat publiczny", "zh-cn": "公共证书"}, - "Run as:": { "en": "Execute as user", "de": "Ausführen als Benutzer", "ru": "Запустить от пользователя", "pt": "Correr como", "nl": "Rennen als", "fr": "Courir comme", "it": "Correre come", "es": "Correr como", "pl": "Uruchom jako", "zh-cn": "运行为"}, - "Secure(HTTPS):": { "en": "Secure (HTTPS)", "de": "Verschlüsselung (HTTPS)", "ru": "Шифрование (HTTPS)", "pt": "Seguro (HTTPS)", "nl": "Secure (HTTPS)", "fr": "Sécurisé (HTTPS)", "it": "Sicuro (HTTPS)", "es": "Seguro (HTTPS)", "pl": "Bezpieczne (HTTPS)", "zh-cn": "安全访问(HTTPS)"}, - "Select data source:": { "en": "Select data source", "de": "Datenquelle auswählen", "ru": "Выберите источник данных для \"query\" запросов", "pt": "Selecionar fonte de dados", "nl": "Selecteer gegevensbron", "fr": "Sélectionner une source de données", "it": "Seleziona l'origine dati", "es": "Seleccionar fuente de datos", "pl": "Wybierz źródło danych", "zh-cn": "选择数据源"}, - "Use Lets Encrypt certificates:": { "en": "Use Let's Encrypt certificates", "de": "Let's Encrypt Zertifikate verwenden", "ru": "Использовать сертификаты Let's Encrypt", "pt": "Use certificados do Let's Encrypt", "nl": "Gebruik Let's Encrypt-certificaten", "fr": "Utiliser les certificats Let's Encrypt", "it": "Utilizza Let's Encrypt certificates", "es": "Utilice los certificados Let's Encrypt", "pl": "Użyj Let's Encrypt certificates", "zh-cn": "使用Let's Encrypt证书"}, - "Use this instance for automatic update:": { "en": "Use this instance for automatic update", "de": "Diese Instanz für automatische Zertifikat-Updates benutzen", "ru": "Использовать этот экземпляр для обновления сертификатов", "pt": "Use esta instância para atualizações automáticas de certificados", "nl": "Gebruik deze instantie voor automatische update", "fr": "Utilisez cette instance pour la mise à jour automatique", "it": "Utilizza questa istanza per l'aggiornamento automatico", "es": "Use esta instancia para la actualización automática", "pl": "Użyj tej instancji do automatycznej aktualizacji", "zh-cn": "自动更新Let's Encrypt证书"}, - "simpleAPI adapter settings": { "en": "SimpleAPI instance settings", "de": "simpleAPI Instanz-Einstellungen", "ru": "Настройки драйвера simpleAPI"}, -}; \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..6f14662 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,39 @@ +import config from '@iobroker/eslint-config'; + +export default [ + ...config, + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.mjs'], + }, + tsconfigRootDir: import.meta.dirname, + project: './tsconfig.json', + }, + }, + }, + { + ignores: [ + 'src-admin/**/*', + 'admin/**/*', + 'node_modules/**/*', + 'test/**/*', + 'build/**/*', + 'tasks.js', + 'tmp/**/*', + 'www/**/*', + 'dist/**/*', + '.**/*', + ], + }, + { + // disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc' + rules: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + + '@typescript-eslint/no-require-imports': 'off', + }, + }, +]; diff --git a/io-package.json b/io-package.json index 7de6ff2..3dea077 100644 --- a/io-package.json +++ b/io-package.json @@ -122,7 +122,7 @@ "platform": "Javascript/Node.js", "mode": "daemon", "loglevel": "info", - "icon": "simple-api.png", + "icon": "simple-api.svg", "webExtension": "lib/simpleapi.js", "materialize": true, "readme": "https://github.com/ioBroker/ioBroker.simple-api/blob/master/README.md", @@ -134,10 +134,9 @@ ], "enabled": true, "compact": true, - "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.simple-api/master/admin/simple-api.png", + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.simple-api/master/admin/simple-api.svg", "type": "communication", "stopBeforeUpdate": true, - "localLink": "%protocol%://%ip%:%port%/get/system.adapter.simple-api.%instance%.uptime?prettyPrint", "dependencies": [ { "js-controller": ">=2.0.0" diff --git a/lib/simpleapi.js b/lib/simpleapi.js deleted file mode 100644 index a750a37..0000000 --- a/lib/simpleapi.js +++ /dev/null @@ -1,1577 +0,0 @@ -/* jshint -W097 */ -/* jshint strict: false */ -/* jslint node: true */ -/* jshint -W061 */ -'use strict'; - -// copied from here: https://github.com/component/escape-html/blob/master/index.js -const matchHtmlRegExp = /["'&<>]/; -function escapeHtml (string) { - const str = `${string}`; - const match = matchHtmlRegExp.exec(str); - - if (!match) { - return str; - } - - let escape; - let html = ''; - let index; - let lastIndex = 0; - - for (index = match.index; index < str.length; index++) { - switch (str.charCodeAt(index)) { - case 34: // " - escape = '"'; - break; - case 38: // & - escape = '&'; - break; - case 39: // ' - escape = '''; - break; - case 60: // < - escape = '<'; - break; - case 62: // > - escape = '>'; - break; - default: - continue; - } - - if (lastIndex !== index) { - html += str.substring(lastIndex, index); - } - - lastIndex = index + 1; - html += escape; - } - - return lastIndex !== index - ? html + str.substring(lastIndex, index) - : html; -} - -const ERROR_PERMISSION = 'permissionError'; -const ERROR_UNKNOWN_COMMAND = 'unknownCommand'; - -/** - * SimpleAPI class - * - * From settings used only secure, auth and crossDomain - * - * @class - * @param {object} server http or https node.js object - * @param {object} webSettings settings of the web server, like
{secure: settings.secure, port: settings.port}
- * @param {object} adapter web adapter object - * @param {object} instanceSettings instance object with common and native - * @param {object} app express application - * @return {object} object instance - */ -function SimpleAPI(server, webSettings, adapter, instanceSettings, app) { - if (!(this instanceof SimpleAPI)) { - return new SimpleAPI(server, webSettings, adapter, instanceSettings, app); - } - - //this.server = server; - this.app = app; - this.adapter = adapter; - this.settings = webSettings; - this.config = instanceSettings ? instanceSettings.native : {}; - this.namespace = instanceSettings ? instanceSettings._id.substring('system.adapter.'.length) : 'simple-api'; - - this.cachedNames = new Map(); - this.cachedIds = new Map(); - - this.restApiDelayed = { - timer: null, - responseType: '', - response: null, - waitId: 0 - }; - - const that = this; - - // static information - const commandsPermissions = { - getPlainValue: {type: 'state', operation: 'read'}, - get: {type: 'state', operation: 'read'}, - getBulk: {type: 'state', operation: 'read'}, - set: {type: 'state', operation: 'write'}, - toggle: {type: 'state', operation: 'write'}, - setBulk: {type: 'state', operation: 'write'}, - setValueFromBody: {type: 'state', operation: 'write'}, - getObjects: {type: 'object', operation: 'list'}, - objects: {type: 'object', operation: 'list'}, - states: {type: 'state', operation: 'list'}, - getStates: {type: 'state', operation: 'list'}, - search: {type: 'state', operation: 'list'}, - query: {type: 'state', operation: 'read'}, - annotations: {type: '', operation: ''}, - help: {type: '', operation: ''} - }; - - const __construct = (function () { - that.adapter.log.info(`${that.settings.secure ? 'Secure ' : ''}simpleAPI server listening on port ${that.settings.port}`); - that.adapter.config.defaultUser = that.adapter.config.defaultUser || 'system.user.admin'; - if (!that.adapter.config.defaultUser.match(/^system\.user\./)) { - that.adapter.config.defaultUser = `system.user.${that.adapter.config.defaultUser}`; - } - if (that.adapter.config.onlyAllowWhenUserIsOwner === undefined) { - that.adapter.config.onlyAllowWhenUserIsOwner = false; - } - adapter.log.info(`Allow states only when user is owner: ${that.adapter.config.onlyAllowWhenUserIsOwner}`); - - if (that.app) { - adapter.log.info(`Install extension on /${that.namespace}/`); - - that.app.use(`/${that.namespace}/`, (req, res) => - that.restApi.call(that, req, res)); - - // let it be accessible under old address too - for (const c in commandsPermissions) { - (function (command) { - adapter.log.info(`Install extension on /${command}/`); - that.app.use(`/${command}/`, (req, res) => { - req.url = `/${command}${req.url}`; - that.restApi.call(that, req, res); - }); - })(c); - } - } - - // Subscribe on object changes to manage cache - that.adapter.subscribeForeignObjects('*'); - }.bind(this))(); - - this.isAuthenticated = function (values, callback) { - if (!values.user || !values.pass) { - that.adapter.log.warn('No password or username!'); - callback(false); - } else { - that.adapter.checkPassword(values.user, values.pass, res => { - if (res) { - that.adapter.log.debug(`Logged in: ${values.user}`); - callback(true); - } else { - that.adapter.log.warn(`Invalid password or user name: ${values.user}`); - callback(false); - } - }); - } - }; - - this.stateChange = function (id, state) { - if (that.restApiDelayed.id === id && state && state.ack) { - adapter.unsubscribeForeignStates(id); - that.restApiDelayed.response = state; - setTimeout(restApiDelayedAnswer, 0); - } - }; - - this.objectChange = function (id, _obj) { - // Clear from cache, will be reinitialized on next usage - if (that.cachedIds.has(id)) { - const name = that.cachedIds.get(id).originId; - that.cachedIds.delete(id); - that.cachedNames.delete(name); - } - }; - - function restApiPost(req, res, command, oId, values, ack) { - let status = 500; - const responseType = 'json'; - const headers = {'Access-Control-Allow-Origin': '*'}; - - let body = ''; - req.on('data', data => body += data); - - req.on('end', () => { - switch (command) { - case 'setBulk': - that.adapter.log.debug(`POST-${command}: body = ${body}`); - let arr = []; - if (body) { - arr = body.split('&'); - } - - for (let i = 0; i < arr.length; i++) { - arr[i] = arr[i].split('='); - try { - values[arr[i][0].trim()] = (arr[i][1] === undefined) ? null : decodeURIComponent((`${arr[i][1]}`).replace(/\+/g, '%20')); - } catch (e) { - values[arr[i][0].trim()] = arr[i][1]; - } - } - - if (values.prettyPrint !== undefined) { - if (values.prettyPrint === 'false') { - values.prettyPrint = false; - } - if (values.prettyPrint === null) { - values.prettyPrint = true; - } - } - - if (values.ack !== undefined) { - ack = values.ack === 'true' || values.ack === '1'; - delete values.ack; - } - - let cnt = 0; - let response = []; - that.adapter.log.debug(`POST-${command}: values = ${JSON.stringify(values)}`); - let done = false; - - for (const _id in values) { - if (!values.hasOwnProperty(_id) || _id === 'prettyPrint' || _id === 'user' || _id === 'pass') continue; - const resIndex = cnt; - cnt++; - that.adapter.log.debug(`${resIndex}: "${_id}"`); - - findState(_id, values.user, (err, id, originId) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!done) { - done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else if (!id) { - response[resIndex] = {error: `datapoint "${_id}" not found`}; - if (!--cnt && !done) { - done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } else { - const usedId = (_id === originId ? originId : id); - that.adapter.log.debug(`POST-${command} for id=${id}, oid=${originId}, used=${usedId}, value=${values[usedId]}`); - if (values[usedId] === 'true') { - values[usedId] = true; - } else if (values[usedId] === 'false') { - values[usedId] = false; - } else if (!isNaN(values[usedId]) && values[usedId] == parseFloat(values[usedId])) { - values[usedId] = parseFloat(values[usedId]); - } - - adapter.setForeignState(id, values[usedId], ack, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, id) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!done) { - done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else { - adapter.log.debug(`Add to Response: ${JSON.stringify({ - id: id, - val: values[usedId] - })}`); - - response[resIndex] = {id: id, val: values[usedId]}; - - if (!--cnt && !done) { - status = 200; - done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - }); - } - }); - } - - if (!cnt && !done) { - done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - break; - - case 'setValueFromBody': { - //that.adapter.log.debug('POST-' + command + ': body = ' + JSON.stringify(body)); // "{0123456xx}" - //that.adapter.log.debug('POST-' + command + ': req.url = ' + JSON.stringify(req.url)); // "/setValueFromBody?javascript.0.Nuki.Devices.NukiSL1.NukiBridgeResponse&prettyPrint" - //that.adapter.log.debug('POST-' + command + ': valuesAA = ' + JSON.stringify(values)); // {"javascript.0.Nuki.Devices.NukiSL1.NukiBridgeResponse":null,"prettyPrint":true,"user":"system.user.admin"} - - Object.keys(oId).forEach(id => - values[oId[id]] = body); - - if (values.prettyPrint !== undefined) { - if (values.prettyPrint === 'false') { - values.prettyPrint = false; - } - if (values.prettyPrint === null) { - values.prettyPrint = true; - } - } - - if (values.ack !== undefined) { - ack = values.ack === 'true' || values.ack === '1'; - delete values.ack; - } - - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no object/datapoint given'}, values.prettyPrint); - break; - } - - let response = []; - that.adapter.log.debug(`POST-${command}: values = ${JSON.stringify(values)}`); - let cnt = oId.length; - let _done = false; - - for (let k = 0; k < oId.length; k++) { - const resIndex = k; - that.adapter.log.debug(`${resIndex}: "${oId[k]}"`); - - findState(oId[k], values.user, (err, id, originId) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!_done) { - _done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else if (!id) { - response[resIndex] = {error: `datapoint "${originId}" not found`}; - if (!--cnt && !_done) { - _done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } else { - const usedId = (oId[k] === originId ? originId : id); - that.adapter.log.debug(`POST-${command} for id=${id}, oid=${originId}, used=${usedId}, value=${values[usedId]}`); - if (values[usedId] === 'true') { - values[usedId] = true; - } else if (values[usedId] === 'false') { - values[usedId] = false; - } else if (!isNaN(values[usedId]) && values[usedId] == parseFloat(values[usedId])) { - values[usedId] = parseFloat(values[usedId]); - } - - adapter.setForeignState(id, values[usedId], ack, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, id) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!_done) { - _done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else { - status = 200; - adapter.log.debug(`Add to Response: ${JSON.stringify({ - id: id, - val: values[usedId] - })}`); - response[resIndex] = {id: id, val: values[usedId]}; - if (!--cnt && !_done) { - _done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - }); - } - }); - } - if (!cnt && !_done) { - _done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - break; - - case 'search': - if (adapter.config.dataSource && adapter.config.allDatapoints !== true) { - adapter.sendTo(adapter.config.dataSource, 'getEnabledDPs', {}, result => { - status = 200; - oId = []; - for (const id in result) { - if( result.hasOwnProperty(id) ) { - oId.push(id); - } - } - doResponse(res, responseType, status, headers, oId, values.prettyPrint); - }); - } else { - const target = JSON.parse(body).target || ''; - that.adapter.log.debug(`[SEARCH] target = ${target}`); - - adapter.getForeignStates(values.pattern || `${target}*`, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, list) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else { - status = 200; - oId = []; - for (const id in list) { - if( list.hasOwnProperty(id) ) { - oId.push(id); - } - } - doResponse(res, responseType, status, headers, oId, values.prettyPrint); - } - }); - } - break; - - case 'query': - const targets = JSON.parse(body).targets || []; - const range = JSON.parse(body).range || {}; - let dateFrom = Date.now(); - let dateTo = Date.now(); - - that.adapter.log.debug(`[QUERY] targets = ${JSON.stringify(targets)}`); - that.adapter.log.debug(`[QUERY] range = ${JSON.stringify(range)}`); - - if (range) { - dateFrom = Date.parse(range.from); - dateTo = Date.parse(range.to); - } - const options = { - start: dateFrom, - end: dateTo, - aggregate: values.aggregate || 'onchange', - }; - - if (values.count) { - options.count = parseInt(values.count, 10); - } - if (values.step) { - options.step = parseInt(values.step, 10); - } - - oId = []; - Array.isArray(targets) && targets.forEach(t => oId.push(t.target)); - - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no datapoints given'}, values.prettyPrint); - break; - } - let bcnt = targets.length; - const list = []; - for (let b = 0; b < targets.length; b++) { - if (that.adapter.config.dataSource && !(targets[b].data && targets[b].data.noHistory === true)) { - that.adapter.log.debug(`Read data from: ${that.adapter.config.dataSource}`); - - that.adapter.sendTo(that.adapter.config.dataSource, 'getHistory', { - id: targets[b].target, - options - }, (result, step, error) => { - if (!error) status = 200; - - that.adapter.log.debug(`[QUERY] sendTo result = ${JSON.stringify(result)}`); - - const element = {}; - element.target = targets[b].target; - element.datapoints = []; - - for (let i = 0; i < result.result.length; i++) { - const datapoint = [result.result[i].val, result.result[i].ts]; - element.datapoints.push(datapoint); - } - - list.push(element); - - if (!--bcnt) { - that.adapter.log.debug(`[QUERY] list = ${JSON.stringify(list)}`); - doResponse(res, responseType, status, headers, list, values.prettyPrint); - } - }); - } else { - that.adapter.log.debug('Read last state'); - - getState(targets[b].target, values.user, (err, state, id) => { - const element = {}; - element.target = id; - element.datapoints = []; - - if (err) { - bcnt = 0; - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else { - if (id) status = 200; - state = state || {}; - - element.datapoints = [[state.val, state.ts]]; - - list.push(element); - - if (!--bcnt) { - that.adapter.log.debug(`[QUERY] list = ${JSON.stringify(list)}`); - doResponse(res, responseType, status, headers, list, values.prettyPrint); - } - } - }); - } - } - if (!bcnt) { - that.adapter.log.debug('[QUERY] !bcnt'); - doResponse(res, responseType, status, headers, list, values.prettyPrint); - } - break; - - case 'annotations': - // iobroker does not support annotations - that.adapter.log.debug('[ANNOTATIONS]'); - doResponse(res, responseType, 200, headers, [], values.prettyPrint); - break; - - default: - doResponse(res, responseType, status, headers, {error: `command ${command} unknown`}, values.prettyPrint); - break; - } - }); - } - - function restApiDelayedAnswer() { - if (that.restApiDelayed.timer) { - clearTimeout(that.restApiDelayed.timer); - that.restApiDelayed.timer = null; - - doResponse(that.restApiDelayed.res, that.restApiDelayed.responseType, 200, {'Access-Control-Allow-Origin': '*'}, that.restApiDelayed.response, that.restApiDelayed.prettyPrint); - that.restApiDelayed.id = null; - that.restApiDelayed.res = null; - that.restApiDelayed.response = null; - that.restApiDelayed.prettyPrint = false; - } - } - - function findState(idOrName, user, type, callback) { - if (typeof type === 'function') { - callback = type; - type = null; - } - - if (that.cachedIds.has(idOrName)) { - const data = that.cachedIds.get(idOrName); - return setImmediate(() => callback(null, data.id, data.originId)); - } else if (that.cachedNames.has(idOrName)) { - const data = that.cachedNames.get(idOrName); - return void setImmediate(() => callback(null, data.id, data.originId)); - } - - adapter.findForeignObject(idOrName, type, {user: user, checked: true}, (err, id, originId) => { - if (originId && typeof originId === 'object' && originId.en) { - originId = originId.en; - } - that.cachedIds.set(id, { id, originId}); - if (idOrName !== id) { // search was for a name, so also cache the name - that.cachedNames.set(originId, { id, originId}); - } - callback(err, id, originId); - }); - } - - function getState(idOrName, user, type, callback) { - if (typeof type === 'function') { - callback = type; - type = null; - } - findState(idOrName, user, type, (err, id, originId) => { - if (err) { - callback && callback(err, undefined, null, originId); - } else if (id) { - that.adapter.getForeignState(id, { - user: user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, obj) => { - if (err || !obj) { - obj = undefined; - } - callback && callback(err, obj, id, originId); - }); - } else { - callback && callback(null, undefined, null, originId); - } - }); - } - - function doResponse(res, type, status, _headers, content, pretty) { - status = parseInt(status, 10) || 200; - - if (status !== 200 && type === 'plain') { - content = escapeHtml(content ? content.error || content : 'unknown'); - if (!content.startsWith('error: ')) { - content = `error: ${content}`; - } - } - - if (pretty && typeof content === 'object') { - type = 'plain'; - content = JSON.stringify(content, null, 2); - } - - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); - - switch (type) { - case 'json': - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.statusCode = status; - res.end(JSON.stringify(content), 'utf8'); - break; - - case 'plain': - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.statusCode = status; - if (typeof content === 'object') { - content = JSON.stringify(content); - } - - res.end(content, 'utf8'); - break; - } - } - - this.commands = Object.keys(commandsPermissions); - - // Register api by express (do not delete) - this.checkRequest = function (url) { - const parts = url.split('/', 2); - return (parts[1] && this.commands.indexOf(parts[1]) !== -1); - }; - - this.checkPermissions = function (user, command, callback) { - adapter.calculatePermissions(user, commandsPermissions, acl => { - if (user !== 'system.user.admin') { - // type: file, object, state, other - // operation: create, read, write, list, delete, sendto, execute, sendto - if (commandsPermissions[command]) { - // If permission required - if (commandsPermissions[command].type) { - if (acl[commandsPermissions[command].type] && - acl[commandsPermissions[command].type][commandsPermissions[command].operation]) { - return callback(null); - } else { - that.adapter.log.warn(`No permission for "${user}" to call ${command}`); - callback(ERROR_PERMISSION); - } - } else { - callback(null); - } - } else { - // unknown command - that.adapter.log.warn(`Unknown command from "${user}": ${command}`); - callback(ERROR_UNKNOWN_COMMAND); - } - } else { - callback(null); - } - }); - }; - - this.restApi = async function (req, res, isAuth, isChecked) { - const values = {}; - let oId = []; - let wait = 0; - let responseType = 'json'; - let status = 500; - const headers = {'Access-Control-Allow-Origin': '*'}; - let response; - let ack = false; - - let url; - try { - url = decodeURI(req.url); - } catch (e) { - url = req.url; - that.adapter.log.warn(`Malformed URL encoding for ${req.url}: ${e}`); - } - - const pos = url.indexOf('?'); - - if (pos !== -1) { - const arr = url.substring(pos + 1).split('&'); - url = url.substring(0, pos); - - for (let i = 0; i < arr.length; i++) { - const _parts = arr[i].split('='); - - try { - _parts[0] = decodeURIComponent(_parts[0]).trim().replace(/%23/g, '#'); - _parts[1] = _parts[1] === undefined ? null : decodeURIComponent((`${_parts[1]}`).replace(/\+/g, '%20')); - values[_parts[0]] = _parts[1]; - } catch (e) { - values[_parts[0]] = _parts[1]; - } - //that.adapter.log.debug(' Decode Result ' + i + ': ' + values[arr[i][0].trim()]); - } - if (values.prettyPrint !== undefined) { - if (values.prettyPrint === 'false') { - values.prettyPrint = false; - } - if (values.prettyPrint === null) { - values.prettyPrint = true; - } - } - - if (values.json !== undefined) { - if (values.json === 'false') { - values.json = false; - } - if (values.json === null) { - values.json = true; - } - } - - if (values.noStringify !== undefined) { - if (values.noStringify === 'false') { - values.noStringify = false; - } - if (values.noStringify === null) { - values.noStringify = true; - } - } - - if (values.ack !== undefined) { - ack = values.ack === 'true' || values.ack === '1'; - delete values.ack; - } - - // Default value for wait - if (values.wait === null) { - values.wait = 2000; - } - } - - const parts = url.split('/'); - const command = parts[1]; - - // Analyse system.adapter.socketio.0.uptime,system.adapter.history.0.memRss?value=78&wait=300 - if (parts[2]) { - oId = parts[2].split(','); - for (let j = oId.length - 1; j >= 0; j--) { - //oId[j] = oId[j].trim().replace(/%23/g, '#'); - try { - oId[j] = decodeURIComponent(oId[j]); - } catch (e) { - that.adapter.log.warn(`Malformed URL encoding for "${oId[j]}": ${e}`); - oId[j] = oId[j].trim().replace(/%23/g, '#'); // do old style minimal parsing - } - - if (!oId[j]) { - oId.splice(j, 1); - } - } - } - - // If authentication check is required - if (that.settings.auth) { - if (!isAuth) { - this.isAuthenticated(values, isAuth => { - if (isAuth) { - that.restApi(req, res, true); - } else { - doResponse(res, responseType, 401, headers, {error: `authentication failed. Please write "http${that.settings.secure ? 's' : ''}://${req.headers.host}?user=UserName&pass=Password"`}); - } - }); - return; - } else if (!isChecked) { - if (!values.user.match(/^system\.user\./)) { - values.user = `system.user.${values.user}`; - } - that.checkPermissions(values.user, command, err => { - if (!err) { - that.restApi(req, res, true, true); - } else { - if (err instanceof Error) { - err = err.message; - } - doResponse(res, responseType, 401, headers, {error: err}, values.prettyPrint); - } - }); - return; - } - } else { - req.user = req.user || that.adapter.config.defaultUser; - values.user = req.user; - if (!values.user.match(/^system\.user\./)) { - values.user = `system.user.${values.user}`; - } - - if (!isChecked && command) { - that.checkPermissions(req.user || that.adapter.config.defaultUser, command, err => { - if (!err) { - that.restApi(req, res, true, true); - } else { - if (err instanceof Error) { - err = err.message; - } - doResponse(res, responseType, 401, headers, {error: err}, values.prettyPrint); - } - }); - return; - } - } - if (!values.user.startsWith('system.user.')) { - values.user = `system.user.${values.user}`; - } - - if (req.method === 'POST') { - restApiPost(req, res, command, oId, values, ack); - return; - } - - switch (command) { - case 'getPlainValue': - responseType = 'plain'; - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no datapoint given'}, values.prettyPrint); - break; - } - - let pcnt = oId.length; - let pErrSent = false; - response = []; - for (let g = 0; g < oId.length; g++) { - const resIndex = g; - getState(oId[g], values.user, (err, obj, id, originId) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - pcnt = 0; - !pErrSent && doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - pErrSent = true; - return; - } else if ((!id && originId) || obj === undefined) { - response[resIndex] = `error: datapoint "${oId[g]}" not found`; - } else { - let val = obj.val; - if (values.json) { - try { - val = JSON.parse(val); - } catch (e) { - - } - val = JSON.stringify(val); - } else { - if (values.noStringify) { - val = obj.val === null ? 'null' : (obj.val === undefined ? 'undefined' : obj.val.toString()); - } else { - val = JSON.stringify(obj.val); - } - } - response[resIndex] = val; - status = 200; - } - - !--pcnt && !pErrSent && doResponse(res, (status === 500 ? 'plain' : responseType), status, headers, response.join('\n'), values.prettyPrint); - }); - } - break; - - case 'get': - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no object/datapoint given'}, values.prettyPrint); - break; - } - - let gCnt = oId.length; - let gErrSent = false; - response = []; - for (let k = 0; k < oId.length; k++) { - const resIndex = k; - that.adapter.log.debug(`work for ID ${oId[k]}`); - getState(oId[k], values.user, (err, state, id, originId) => { - that.adapter.log.debug(`return err ${err}`); - that.adapter.log.debug(`return state ${state}`); - that.adapter.log.debug(`return id ${JSON.stringify(id)}`); - that.adapter.log.debug(`return originId ${originId}`); - if (err) { - gCnt = 0; - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - !gErrSent && doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - gErrSent = true; - } else if (!id && originId) { - response[resIndex] = {error: `datapoint "${originId}" not found`}; - !--gCnt && !gErrSent && doResponse(res, responseType, status, headers, response.length === 1 ? response[0] : response, values.prettyPrint); - } else { - const vObj = state || {}; - status = 200; - - that.adapter.getForeignObject(id, (err, obj) => { - if (obj) { - for (const attr in obj) { - if (obj.hasOwnProperty(attr) && vObj[attr] === undefined) { - vObj[attr] = obj[attr]; - } - } - } - - response[resIndex] = vObj; - !--gCnt && !gErrSent && doResponse(res, responseType, status, headers, response.length === 1 ? response[0] : response, values.prettyPrint); - }); - } - }); - } - break; - - case 'getBulk': - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no datapoints given'}, values.prettyPrint); - break; - } - let bcnt = oId.length; - response = []; - let _done = false; - for (let b = 0; b < oId.length; b++) { - const resIndex = b; - getState(oId[b], values.user, (err, state, id, originId) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - bcnt = 0; - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!_done) { - _done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else { - if (id) status = 200; - state = state || {}; - response[resIndex] = {id: id, val: state.val, ts: state.ts, ack: state.ack}; - if (!--bcnt && !_done) { - _done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - }); - } - if (!bcnt && !_done) { - _done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - break; - - case 'set': - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'object/datapoint not given'}, values.prettyPrint); - break; - } - if (values.value === undefined && values.val === undefined) { - doResponse(res, responseType, status, headers, `error: no value found for "${oId[0]}". Use /set/id?value=1 or /set/id?value=1&wait=1000`, values.prettyPrint); - } else { - findState(oId[0], values.user, (err, id, originId) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - wait = 0; - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}); - } else if (id) { - const type = values.type; - - if (!type || (type !== 'boolean' && type !== 'number' && type !== 'string' && type !== 'json' && type !== 'object' && type !== 'array')) { - if (values.val && (values.val[0] === '[' || values.val[0] === '{')) { - try { - values.val = JSON.parse(values.val); - } catch (e) { - // keep it as string - } - } - - wait = values.wait || 0; - if (wait) { - wait = parseInt(wait, 10); - } - if (values.val === undefined) { - values.val = values.value; - } - - if (values.val === 'true') { - values.val = true; - } else if (values.val === 'false') { - values.val = false; - } else if (!isNaN(values.val)) { - values.val = parseFloat(values.val); - } - } else { - if (type === 'boolean') { - values.val = values.val === 'true' || values.val === '1'; - } else if (type === 'number') { - values.val = parseFloat(values.val); - } else if (type === 'json' || type === 'array' || type === 'object') { - try { - values.val = JSON.parse(values.val); - } catch (e) { - status = 500; - doResponse(res, responseType, status, headers, {error: e}, values.prettyPrint); - wait = 0; - return; - } - } - // string must not be formatted - } - - wait && adapter.subscribeForeignStates(id); - - adapter.setForeignState(id, values.val, ack, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, err => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (wait && that.restApiDelayed.id === id && that.restApiDelayed.timer) { - clearTimeout(that.restApiDelayed.timer); - that.restApiDelayed.timer = null; - - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else if (!wait) { - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else if (!wait) { - status = 200; - response = {id: id, value: values.val, val: values.val}; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - }); - - if (wait) { - that.restApiDelayed.responseType = responseType; - that.restApiDelayed.response = null; - that.restApiDelayed.id = id; - that.restApiDelayed.res = res; - that.restApiDelayed.prettyPrint = values.prettyPrint; - that.restApiDelayed.timer = setTimeout(restApiDelayedAnswer, wait); - } - } else { - doResponse(res, responseType, status, headers, `error: datapoint "${originId}" not found`, values.prettyPrint); - } - }); - } - break; - - case 'toggle': - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'state not given'}, values.prettyPrint); - break; - } - - findState(oId[0], values.user, (err, id, originId) => { - if (err) { - doResponse(res, responseType, 500, headers, {error: err}, values.prettyPrint); - wait = 0; - } else if (id) { - wait = values.wait || 0; - if (wait) { - wait = parseInt(wait, 10); - } - - // Read type of object - adapter.getForeignObject(id, {user: values.user, checked: true}, (err, obj) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - wait = 0; - } else { - // Read actual value - adapter.getForeignState(id, {user: values.user, checked: true}, (err, state) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - wait = 0; - } else if (state) { - if (obj && obj.common && obj.common.type) { - if (obj.common.type === 'bool' || obj.common.type === 'boolean') { - if (state.val === 'true') { - state.val = true; - } else if (state.val === 'false') { - state.val = false; - } - state.val = !state.val; - } else if (obj.common.type === 'number') { - state.val = parseFloat(state.val); - if (obj.common.max !== undefined) { - obj.common.min = obj.common.min || 0; - - if (state.val > obj.common.max) { - state.val = obj.common.max; - }else - if (state.val < obj.common.min) { - state.val = obj.common.min; - } - // Invert - state.val = obj.common.max + obj.common.min - state.val; - } else { - // default number is from 0 to 100 - if (state.val > 100) { - state.val = 100; - } - if (state.val < 0) { - state.val = 0; - } - state.val = 100 - state.val; - } - } else { - if (state.val === 'true' || state.val === true) { - state.val = false; - } else if (state.val === 'false' || state.val === false) { - state.val = true; - } else if (parseInt(state.val, 10) == state.val) { - state.val = parseInt(state.val, 10) ? 0 : 1; - } else { - doResponse(res, responseType, status, headers, {error: 'state is neither number nor boolean'}, values.prettyPrint); - return; - } - } - } else { - if (state.val === 'true') { - state.val = true; - } else if (state.val === 'false') { - state.val = false; - } else if (!isNaN(state.val)) { - state.val = parseFloat(state.val); - } - - if (state.val === true) { - state.val = 1; - } - if (state.val === false) { - state.val = 0; - } - state.val = 1 - parseInt(state.val, 10); - } - - wait && adapter.subscribeForeignStates(id); - - adapter.setForeignState(id, state.val, ack, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, err => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - wait = 0; - if (that.restApiDelayed.id === id && that.restApiDelayed.timer) { - clearTimeout(that.restApiDelayed.timer); - that.restApiDelayed.timer = null; - } - } else if (!wait) { - status = 200; - doResponse(res, responseType, status, headers, { - id: id, - value: state.val, - val: state.val - }, values.prettyPrint); - } - }); - - if (wait) { - that.restApiDelayed.responseType = responseType; - that.restApiDelayed.response = null; - that.restApiDelayed.id = id; - that.restApiDelayed.res = res; - that.restApiDelayed.prettyPrint = values.prettyPrint; - that.restApiDelayed.timer = setTimeout(restApiDelayedAnswer, wait); - } - } else { - doResponse(res, responseType, status, headers, {error: 'object has no state'}, values.prettyPrint); - } - }); - } - }); - } else { - doResponse(res, responseType, status, headers, {error: `error: datapoint "${originId}" not found`}, values.prettyPrint); - } - }); - - break; - - // /setBulk?BidCos-RF.FEQ1234567:1.LEVEL=0.7&Licht-Küche/LEVEL=0.7&Anwesenheit=0&950=1 - case 'setBulk': - let cnt = 0; - response = []; - let done = false; - adapter.log.debug(`Values: ${JSON.stringify(values)}`); - for (const _id in values) { - if (!values.hasOwnProperty(_id) || _id === 'prettyPrint' || _id === 'user' || _id === 'pass') continue; - const resIndex = cnt; - cnt++; - findState(_id, values.user, (err, id, originId) => { - // id is "name", originId is the ioBroker-ID of the datapoint - if (err) { - adapter.log.debug(`Error on ID lookup: ${err}`); - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!done) { - done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else if (!id) { - response[resIndex] = {error: `error: datapoint "${originId}" not found`}; - if (!--cnt && !done) { - done = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } else { - const usedId = (_id === originId ? originId : id); - that.adapter.log.debug(`GET-${command} for id=${id}, oid=${originId}used=${usedId}, value=${values[usedId]}`); - if (values[usedId] === 'true') { - values[usedId] = true; - } else if (values[usedId] === 'false') { - values[usedId] = false; - } else if (!isNaN(values[usedId]) && values[usedId] === parseFloat(values[usedId]).toString()) { - values[usedId] = parseFloat(values[usedId]); - } - - adapter.setForeignState(id, values[usedId], ack, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, id) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!done) { - done = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else { - adapter.log.debug(`Add to Response-Get: ${JSON.stringify({ - id: id, - val: values[usedId], - value: values[usedId] - })}`); - response[resIndex] = {id: id, val: values[usedId], value: values[usedId]}; - if (!--cnt && !done) { - done = true; - status = 200; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - }); - } - }); - } - if (!cnt && !done) doResponse(res, responseType, status, headers, response, values.prettyPrint); - break; - - case 'getObjects': - case 'objects': - adapter.getForeignObjects(values.pattern || parts[2] || '*', values.type, { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, list) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else { - status = 200; - doResponse(res, responseType, status, headers, list, values.prettyPrint); - } - }); - break; - - case 'getStates': - case 'states': - adapter.getForeignStates(values.pattern || parts[2] || '*', { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, list) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else { - status = 200; - doResponse(res, responseType, status, headers, list, values.prettyPrint); - } - }); - break; - - case 'search': - if (adapter.config.dataSource && adapter.config.allDatapoints !== true) { - adapter.sendTo(adapter.config.dataSource, 'getEnabledDPs', {}, result => { - status = 200; - oId = []; - for (const id in result) { - if( result.hasOwnProperty(id) ) { - oId.push(id); - } - } - doResponse(res, responseType, status, headers, oId, values.prettyPrint); - }); - } else { - that.adapter.log.debug(`[SEARCH] target = ${parts[2]}`); - - adapter.getForeignStates(values.pattern || parts[2] || '*', { - user: values.user, - limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner - }, (err, list) => { - if (err) { - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } else { - status = 200; - oId = []; - for (const id in list) { - if( list.hasOwnProperty(id) ) { - oId.push(id); - } - } - doResponse(res, responseType, status, headers, oId, values.prettyPrint); - } - }); - } - break; - - case 'query': - that.adapter.log.debug(JSON.stringify(parts)); - that.adapter.log.debug(JSON.stringify(values)); - - let dateFrom = Date.now(); - let dateTo = Date.now(); - - if (values.dateFrom) { - dateFrom = Date.parse(values.dateFrom); - } - if (values.dateTo) { - dateTo = Date.parse(values.dateTo); - } - const options = { - start: dateFrom, - end: dateTo, - aggregate: values.aggregate || 'onchange', - }; - - if (values.count) { - options.count = parseInt(values.count, 10); - } - if (values.step) { - options.step = parseInt(values.step, 10); - } - - if (!oId.length || !oId[0]) { - doResponse(res, responseType, status, headers, {error: 'no datapoints given'}, values.prettyPrint); - break; - } - let tCnt = oId.length; - response = []; - let tDone = false; - - for (let b = 0; b < oId.length; b++) { - const resIndex = b; - if (that.adapter.config.dataSource && !(values.noHistory && values.noHistory === 'true')) { - that.adapter.log.debug(`Read data from: ${that.adapter.config.dataSource}`); - - that.adapter.sendTo(that.adapter.config.dataSource, 'getHistory', { - id: oId[b], - options - }, (result, step, error) => { - if (!error) { - status = 200; - } - - that.adapter.log.debug(`[QUERY] sendTo result = ${JSON.stringify(result)}`); - - const element = {}; - element.target = oId[b]; - element.datapoints = []; - - for (let i = 0; i < result.result.length; i++) { - const datapoint = [result.result[i].val, result.result[i].ts]; - element.datapoints.push(datapoint); - } - - response[resIndex] = element; - - if (!--tCnt && !tDone) { - tDone = true; - that.adapter.log.debug(`[QUERY] response = ${JSON.stringify(response)}`); - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - }); - } else { - that.adapter.log.debug('Read last state'); - - getState(oId[b], values.user, (err, state, id, originId) => { - const element = {}; - element.target = id; - element.datapoints = []; - - if (err) { - tCnt = 0; - if (err instanceof Error) { - err = err.message; - } - status = 500; - if (err.includes(ERROR_PERMISSION)) { - status = 401; - } - if (!tDone) { - tDone = true; - doResponse(res, responseType, status, headers, {error: err}, values.prettyPrint); - } - } else { - if (id) status = 200; - state = state || {}; - - element.datapoints = [[state.val, state.ts]]; - - response[resIndex] = element; - - if (!--tCnt && !tDone) { - that.adapter.log.debug(`[QUERY] response = ${JSON.stringify(response)}`); - tDone = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - } - }); - } - } - if (!tCnt && !tDone) { - tDone = true; - doResponse(res, responseType, status, headers, response, values.prettyPrint); - } - break; - - case 'annotations': - // ioBroker does not support annotations - that.adapter.log.debug('[ANNOTATIONS]'); - doResponse(res, responseType, 200, headers, [], values.prettyPrint); - break; - - case 'help': - // is default behaviour too - default: - status = 200; - - const _obj = command === 'help' ? {} : {error: `command ${command} unknown`}; - let request = `http${that.settings.secure ? 's' : ''}://${req.headers.host}`; - if (this.app) { - request += `/${this.namespace}/`; - } - let auth = ''; - if (that.settings.auth) { - auth = 'user=UserName&pass=Password'; - } - _obj.getPlainValue = `${request}/getPlainValue/stateID${auth ? `?${auth}` : ''}`; - _obj.get = `${request}/get/stateID/?prettyPrint${auth ? `&${auth}` : ''}`; - _obj.getBulk = `${request}/getBulk/stateID1,stateID2/?prettyPrint${auth ? `&${auth}` : ''}`; - _obj.set = `${request}/set/stateID?value=1&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.toggle = `${request}/toggle/stateID&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.setBulk = `${request}/setBulk?stateID1=0.7&stateID2=0&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.setValueFromBody = `${request}/setValueFromBody?stateID1${auth ? `&${auth}` : ''}`; - _obj.objects = `${request}/objects?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.states = `${request}/states?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.search = `${request}/search?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; - _obj.query = `${request}/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&dateTo=2019-06-06T12:00:00.000Z&noHistory=false&aggregate=minmax&count=3000&prettyPrint${auth ? `&${auth}` : ''}`; - - doResponse(res, responseType, status, headers, _obj, true); - break; - } - }; -} - -module.exports = SimpleAPI; diff --git a/main.js b/main.js deleted file mode 100644 index 3861ff4..0000000 --- a/main.js +++ /dev/null @@ -1,204 +0,0 @@ -/* jshint -W097 */ -/* jshint strict: false */ -/* jslint node: true */ -'use strict'; - -const utils = require('@iobroker/adapter-core'); // Get common adapter utils -const SimpleAPI = require('./lib/simpleapi.js'); -const { WebServer } = require('@iobroker/webserver'); -const adapterName = require('./package.json').name.split('.').pop(); - -let webServer = null; -let fs = null; -let adapter; - -function startAdapter(options) { - options = options || {}; - Object.assign(options, { - name: adapterName, - stateChange: (id, state) => webServer && webServer.api && webServer.api.stateChange(id, state), - objectChange: (id, obj) => webServer && webServer.api && webServer.api.objectChange(id, obj), - unload: callback => { - try { - if (webServer && webServer.server) { - adapter.log.info(`terminating http${webServer.settings.secure ? 's' : ''} server on port ${webServer.settings.port}`); - webServer.server.close(); - webServer.server = null; - } - } catch (e) { - // ignore - } - callback && callback(); - }, - ready: main - }); - - adapter = new utils.Adapter(options); - return adapter; -} - -async function main() { - if (adapter.config.webInstance) { - console.log('Adapter runs as a part of web service'); - adapter.log.warn('Adapter runs as a part of web service'); - return adapter.setForeignState(`system.adapter.${adapter.namespace}.alive`, false, true, () => - setTimeout(() => adapter.terminate ? adapter.terminate() : process.exit(), 1000)); - } - - if (adapter.config.secure) { - // Load certificates - adapter.getCertificates(async (err, certificates, leConfig) => { - adapter.config.certificates = certificates; - adapter.config.leConfig = leConfig; - webServer = await initWebServer(adapter.config); - }); - } else { - webServer = await initWebServer(adapter.config); - } -} - -function requestProcessor(req, res) { - if (req.url.indexOf('favicon.ico') !== -1) { - fs = fs || require('fs'); - let stat; - try { - if (fs.existsSync(__dirname + '/img/favicon.ico')) { - stat = fs.statSync(__dirname + '/img/favicon.ico'); - } - } catch (err) { - // no special handling - } - if (stat) { - res.writeHead(200, { - 'Content-Type': 'image/x-icon', - 'Content-Length': stat.size - }); - - const readStream = fs.createReadStream(__dirname + '/img/favicon.ico'); - // We replaced all the event handlers with a simple call to readStream.pipe() - readStream.pipe(res); - } else { - res.writeHead(404, {'Content-Type': 'text/plain'}); - res.write('404 Not Found\n'); - res.end(); - } - - } else { - webServer.api.restApi(req, res); - } -} - -//settings: { -// "port": 8080, -// "auth": false, -// "secure": false, -// "bind": "0.0.0.0", // "::" -// "cache": false -//} -async function initWebServer(settings) { - const server = { - app: null, - server: null, - api: null, - io: null, - settings: settings - }; - - settings.port = parseInt(settings.port, 10); - - if (settings.port) { - if (settings.secure && !adapter.config.certificates) { - return null; - } - - try { - const webserver = new WebServer({ - app: requestProcessor, - adapter, - secure: adapter.config.secure - }); - - server.server = await webserver.init(); - } catch (err) { - adapter.log.error(`Cannot create webserver: ${err}`); - adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - return; - } - if (!server.server) { - adapter.log.error(`Cannot create webserver`); - adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - return; - } - server.server.__server = server; - } else { - adapter.log.error('port missing'); - if (adapter.terminate) { - adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } else { - process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } - return; - } - - if (server.server) { - let serverListening = false; - let serverPort = settings.port; - server.server.on('error', e => { - if (e.toString().includes('EACCES') && serverPort <= 1024) { - adapter.log.error(`node.js process has no rights to start server on the port ${serverPort}.\n` + - `Do you know that on linux you need special permissions for ports under 1024?\n` + - `You can call in shell following scrip to allow it for node.js: "iobroker fix"` - ); - } else { - adapter.log.error(`Cannot start server on ${settings.bind || '0.0.0.0'}:${serverPort}: ${e}`); - } - if (!serverListening) { - adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } - }); - - adapter.getPort(settings.port, (!settings.bind || settings.bind === '0.0.0.0') ? undefined : settings.bind || undefined, port => { - if (port !== settings.port && !adapter.config.findNextPort) { - adapter.log.error(`port ${settings.port} already in use`); - if (adapter.terminate) { - adapter.terminate(1); - } else { - process.exit(1); - } - return; - } - serverPort = port; - - if (server.server) { - // create web server - server.server.listen(port, (!settings.bind || settings.bind === '0.0.0.0') ? undefined : settings.bind || undefined, () => - serverListening = true); - - adapter.log.info(`http${settings.secure ? 's' : ''} server listening on port ${port}`); - } else { - adapter.log.error('server initialization failed'); - if (adapter.terminate) { - adapter.terminate(1); - } else { - process.exit(1); - } - } - }); - } - - server.api = new SimpleAPI(server.server, settings, adapter); - - if (server.server) { - return server; - } else { - return null; - } -} - -// If started as allInOne mode => return function to create instance -if (module.parent) { - module.exports = startAdapter; -} else { - // or start the instance directly - startAdapter(); -} diff --git a/package.json b/package.json index 38fb62e..dda1856 100644 --- a/package.json +++ b/package.json @@ -26,33 +26,39 @@ "url": "https://github.com/ioBroker/ioBroker.simple-api" }, "dependencies": { - "@iobroker/adapter-core": "^3.1.6", - "@iobroker/webserver": "^1.0.3" + "@iobroker/adapter-core": "^3.2.3", + "@iobroker/webserver": "^1.0.8" }, "devDependencies": { - "@iobroker/testing": "^4.1.3", "@alcalzone/release-script": "^3.7.3", "@alcalzone/release-script-plugin-iobroker": "^3.7.2", "@alcalzone/release-script-plugin-license": "^3.7.0", "@iobroker/adapter-dev": "^1.3.0", + "@iobroker/eslint-config": "^1.0.0", + "@iobroker/testing": "^4.1.3", + "@iobroker/types": "^7.0.6", + "@types/express": "^4.17.21", + "@types/node": "^22.13.1", "chai": "^4.4.1", "mocha": "^10.6.0", - "request": "^2.88.2" + "request": "^2.88.2", + "typescript": "^5.7.3" }, "bugs": { "url": "https://github.com/ioBroker/ioBroker.simple-api/issues" }, - "main": "main.js", + "main": "dist/main.js", "files": [ "admin/", "img/", - "lib/", + "dist/", "io-package.json", - "LICENSE", - "main.js" + "LICENSE" ], "scripts": { "test": "node node_modules/mocha/bin/mocha --exit", + "build": "tsc -p tsconfig.build.json && node tasks", + "lint": "eslint -c eslint.config.mjs", "release": "release-script", "release-patch": "release-script patch --yes", "release-minor": "release-script minor --yes", diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..2f00708 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,3 @@ +import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs'; + +export default prettierConfig; diff --git a/src/lib/SimpleAPI.ts b/src/lib/SimpleAPI.ts new file mode 100644 index 0000000..bc85ded --- /dev/null +++ b/src/lib/SimpleAPI.ts @@ -0,0 +1,1352 @@ +import type { Server as HttpServer } from 'node:http'; +import type { Server as HttpsServer } from 'node:https'; +import type { SimpleApiAdapterConfig } from '../types'; +import { Express, type Request, type Response } from 'express'; +import { CommandsPermissionsObject } from '@iobroker/types/build/types'; + +// copied from here: https://github.com/component/escape-html/blob/master/index.js +const matchHtmlRegExp = /["'&<>]/; +function escapeHtml(string: string): string { + const str = `${string}`; + const match = matchHtmlRegExp.exec(str); + + if (!match) { + return str; + } + + let escape; + let html = ''; + let index; + let lastIndex = 0; + + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"'; + break; + case 38: // & + escape = '&'; + break; + case 39: // ' + escape = '''; + break; + case 60: // < + escape = '<'; + break; + case 62: // > + escape = '>'; + break; + default: + continue; + } + + if (lastIndex !== index) { + html += str.substring(lastIndex, index); + } + + lastIndex = index + 1; + html += escape; + } + + return lastIndex !== index ? html + str.substring(lastIndex, index) : html; +} + +const ERROR_PERMISSION = 'permissionError'; +const ERROR_UNKNOWN_COMMAND = 'unknownCommand'; + +export type Server = HttpServer | HttpsServer; + +// static information +const commandsPermissions: { + [operation: string]: { type: 'state' | 'object' | ''; operation: 'read' | 'write' | 'list' | '' }; +} = { + getPlainValue: { type: 'state', operation: 'read' }, + get: { type: 'state', operation: 'read' }, + getBulk: { type: 'state', operation: 'read' }, + set: { type: 'state', operation: 'write' }, + toggle: { type: 'state', operation: 'write' }, + setBulk: { type: 'state', operation: 'write' }, + setValueFromBody: { type: 'state', operation: 'write' }, + getObjects: { type: 'object', operation: 'list' }, + objects: { type: 'object', operation: 'list' }, + states: { type: 'state', operation: 'list' }, + getStates: { type: 'state', operation: 'list' }, + search: { type: 'state', operation: 'list' }, + query: { type: 'state', operation: 'read' }, + annotations: { type: '', operation: '' }, + help: { type: '', operation: '' }, +}; + +type CommandName = keyof typeof commandsPermissions; + +/** + * SimpleAPI class + * + * From settings used only secure, auth and crossDomain + * + * @class + * @param webSettings settings of the web server, like
{secure: settings.secure, port: settings.port}
+ * @param adapter web adapter object + * @param instanceSettings instance object with common and native + * @param app express application + * @return object instance + */ +export class SimpleAPI { + private readonly adapter: ioBroker.Adapter; + private readonly settings: { + secure: boolean; + port: number | string; + defaultUser?: string; + auth?: boolean; + language?: ioBroker.Languages; + }; + private readonly config: SimpleApiAdapterConfig; + private readonly namespace: string; + private readonly app?: Express; + private readonly cachedNames: Map = new Map(); + private readonly cachedIds: Map = new Map(); + + private readonly restApiDelayed: { + id: string; + timer: ioBroker.Timeout | undefined; + resolve: (index: string) => void; + index: string; + value: ioBroker.StateValue; + }[] = []; + + constructor( + _server: Server, + webSettings: { + secure: boolean; + port: number | string; + defaultUser?: string; + auth?: boolean; + language?: ioBroker.Languages; + }, + adapter: ioBroker.Adapter, + instanceSettings: ioBroker.InstanceObject, + app?: Express, + ) { + this.app = app; + this.adapter = adapter; + this.settings = webSettings; + this.config = instanceSettings + ? (instanceSettings.native as SimpleApiAdapterConfig) + : ({} as SimpleApiAdapterConfig); + this.namespace = instanceSettings ? instanceSettings._id.substring('system.adapter.'.length) : 'simple-api'; + + this.adapter.log.info( + `${this.settings.secure ? 'Secure ' : ''}simpleAPI server listening on port ${this.settings.port}`, + ); + this.config.defaultUser = webSettings.defaultUser || this.config.defaultUser || 'system.user.admin'; + if (!this.config.defaultUser.match(/^system\.user\./)) { + this.config.defaultUser = `system.user.${this.config.defaultUser}`; + } + this.config.onlyAllowWhenUserIsOwner = !!this.config.onlyAllowWhenUserIsOwner; + + this.adapter.log.info(`Allow states only when user is owner: ${this.config.onlyAllowWhenUserIsOwner}`); + + if (this.app) { + this.adapter.log.info(`Install extension on /${this.namespace}/`); + + this.app.use(`/${this.namespace}/`, (req: Request, res: Response): void => { + this.restApi(req, res); + }); + + // let it be accessible under old address too + for (const c in commandsPermissions) { + ((command: string): void => { + this.adapter.log.info(`Install extension on /${command}/`); + this.app.use(`/${command}/`, (req: Request, res: Response): void => { + void this.restApi(req, res, `/${command}${req.url}`); + }); + })(c); + } + } + + // Subscribe on object changes to manage cache + this.adapter.subscribeForeignObjects('*'); + } + + async isAuthenticated(query: { user?: string; pass?: string }): Promise { + if (!query.user || !query.pass) { + this.adapter.log.warn('No password or username!'); + return false; + } + + const res = await this.adapter.checkPasswordAsync(query.user, query.pass); + if (res) { + this.adapter.log.debug(`Logged in: ${query.user}`); + } else { + this.adapter.log.warn(`Invalid password or user name: ${query.user}`); + } + return res; + } + + stateChange(id: string, state: ioBroker.State | null | undefined): void { + if (state?.ack) { + for (let i = this.restApiDelayed.length - 1; i >= 0; i--) { + if (this.restApiDelayed[i].id === id) { + if (this.restApiDelayed[i].timer) { + this.restApiDelayed[i].timer = undefined; + this.adapter.clearTimeout(this.restApiDelayed[i].timer); + this.restApiDelayed[i].resolve(this.restApiDelayed[i].index); + } + this.restApiDelayed.splice(i, 1); + } + } + } + } + + objectChange(id: string, _obj: ioBroker.Object | null | undefined): void { + // Clear from cache, will be reinitialized on next usage + if (this.cachedIds.has(id)) { + const name = this.cachedIds.get(id)?.name; + if (name) { + this.cachedIds.delete(id); + this.cachedNames.delete(name); + } + } + } + + static parseQuery( + input: string | undefined, + query: { + user?: string; + pass?: string; + prettyPrint?: boolean; + json?: boolean; + noStringify?: boolean; + wait?: number; + ack: boolean; + }, + values: Record, + ): void { + const parts = (input || '').split('&'); + for (const part of parts) { + const [name, value] = part.split('='); + try { + if (name === 'user') { + query.user = decodeURIComponent(value.trim()); + } else if (name === 'pass') { + query.pass = decodeURIComponent(value); + } else if (name === 'prettyPrint') { + query.prettyPrint = !value ? true : decodeURIComponent(value.trim()) === 'true'; + } else if (name === 'json') { + query.json = !value ? true : decodeURIComponent(value.trim()) === 'true'; + } else if (name === 'noStringify') { + query.noStringify = !value ? true : decodeURIComponent(value.trim()) === 'true'; + } else if (name === 'wait') { + query.wait = !value ? 2000 : parseInt(decodeURIComponent(value.trim()), 10) || 0; + } else if (name === 'ack') { + const val = decodeURIComponent(value.trim()); + query.ack = val === 'true' || val === '1'; + } else { + values[name] = + value === undefined ? null : decodeURIComponent(`${value.trim()}`.replace(/\+/g, '%20')); + } + } catch { + values[name] = value; + } + } + if (query.ack === undefined) { + query.ack = false; + } + } + + async setStates( + values: Record, + query: { + user?: string; + pass?: string; + prettyPrint?: boolean; + json?: boolean; + noStringify?: boolean; + wait?: number; + ack: boolean; + }, + ): Promise<{ id?: string; val?: boolean | string | number; error?: string }[]> { + let response: { id?: string; val?: boolean | string | number; error?: string }[] = []; + const names = Object.keys(values); + + for (let i = 0; i < names.length; i++) { + const stateId = names[i]; + this.adapter.log.debug(`${i}: "${stateId}"`); + + try { + const { id, name } = await this.findState(stateId, query.user as `system.user.${string}`); + if (!id) { + response[i] = { error: `datapoint "${stateId}" not found` }; + } else { + let value: string | number | boolean; + if (values[stateId] === 'true') { + value = true; + } else if (values[stateId] === 'false') { + value = false; + } else { + const f = parseFloat(values[stateId] as string); + if (!isNaN(f) && values[stateId] === f.toString()) { + value = f; + } else { + value = values[stateId] as string; + } + } + + try { + await this.adapter.setForeignState(id, value, !!query.ack, { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + response[i] = { id, val: value }; + this.adapter.log.debug(`Add to Response: ${JSON.stringify(response[i])}`); + } catch (err) { + throw err; + } + } + } catch (err) { + // State isn't found or no permission + if (err.toString().includes(ERROR_PERMISSION)) { + throw err; + } + response[i] = { error: err.toString() }; + } + } + return response; + } + + async restApiPost( + req: Request, + res: Response, + command: CommandName, + oId: string[], + values: Record, + query: { + user?: string; + pass?: string; + prettyPrint?: boolean; + json?: boolean; + noStringify?: boolean; + wait?: number; + ack: boolean; + }, + ): Promise { + let body = ''; + req.on('data', (data: Buffer): void => { + body += data.toString(); + }); + + await new Promise(resolve => req.on('end', resolve)); + + switch (command) { + case 'setBulk': { + this.adapter.log.debug(`POST-${command}: body = ${body}`); + SimpleAPI.parseQuery(body, query, values); + this.adapter.log.debug(`POST-${command}: values = ${JSON.stringify(values)}`); + try { + const response = await this.setStates(values, query); + this.doResponse(res, 'json', response, query.prettyPrint); + } catch (err) { + // State not found + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, 'json', 401, err.toString()); + } else if (err.toString().includes('found')) { + this.doErrorResponse(res, 'json', 404, err.toString()); + } else { + this.doErrorResponse(res, 'json', 500, err.toString()); + } + } + break; + } + + case 'setValueFromBody': { + // all given variables will get the same value + Object.values(oId).forEach(id => { + values[id] = body; + }); + + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, 'json', 422, 'no object/datapoint given'); + return; + } + try { + const response = await this.setStates(values, query); + this.doResponse(res, 'json', response, query.prettyPrint); + } catch (err) { + // State not found + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, 'json', 403, err.toString()); + } else if (err.toString().includes('found')) { + this.doErrorResponse(res, 'json', 404, err.toString()); + } else { + this.doErrorResponse(res, 'json', 500, err.toString()); + } + } + break; + } + + case 'search': + if (this.config.dataSource && this.config.allDatapoints) { + const result = await this.adapter.sendToAsync(this.config.dataSource, 'getEnabledDPs'); + this.doResponse(res, 'json', Object.keys(result as Record), query.prettyPrint); + } else { + try { + const target = JSON.parse(body).target || ''; + this.adapter.log.debug(`[SEARCH] target = ${target}`); + const list = await this.adapter.getForeignStatesAsync(values.pattern || `${target}*`, { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + oId = Object.keys(list); + this.doResponse(res, 'json', oId, query.prettyPrint); + } catch (err) { + if (err.includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, 'json', 401, err); + } else { + this.doErrorResponse(res, 'json', 500, err); + } + } + } + break; + + case 'query': { + let bodyQuery: { + targets: { target: string; data: { noHistory: boolean } }[]; + range: { from: string; to: string }; + }; + + try { + bodyQuery = JSON.parse(body); + let dateFrom = Date.now(); + let dateTo = Date.now(); + + this.adapter.log.debug(`[QUERY] targets = ${JSON.stringify(bodyQuery.targets)}`); + this.adapter.log.debug(`[QUERY] range = ${JSON.stringify(bodyQuery.range)}`); + + if (bodyQuery.range) { + dateFrom = Date.parse(bodyQuery.range.from); + dateTo = Date.parse(bodyQuery.range.to); + } + + const options: ioBroker.GetHistoryOptions = { + start: dateFrom, + end: dateTo, + aggregate: (values.aggregate as ioBroker.GetHistoryOptions['aggregate']) || 'onchange', + }; + + if (values.count) { + options.count = parseInt(values.count, 10); + } + if (values.step) { + options.step = parseInt(values.step, 10); + } + + oId = []; + if (Array.isArray(bodyQuery.targets)) { + bodyQuery.targets.forEach(t => oId.push(t.target)); + } + + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, 'json', 422, 'no datapoints given'); + break; + } + const list: { target: string; datapoints: [ioBroker.StateValue, number | null][] }[] = []; + for (let b = 0; b < bodyQuery.targets.length; b++) { + const element: { target: string; datapoints: [ioBroker.StateValue, number | null][] } = { + target: bodyQuery.targets[b].target, + datapoints: [], + }; + + if ( + this.config.dataSource && + !(bodyQuery.targets[b].data && bodyQuery.targets[b].data.noHistory === true) + ) { + this.adapter.log.debug(`Read data from: ${this.config.dataSource}`); + + const result = await this.adapter.getHistoryAsync( + this.config.dataSource, + bodyQuery.targets[b].target, + options, + ); + + this.adapter.log.debug(`[QUERY] sendTo result = ${JSON.stringify(result)}`); + + for (let i = 0; i < result.result.length; i++) { + element.datapoints.push([result.result[i].val, result.result[i].ts]); + } + + list.push(element); + } else { + this.adapter.log.debug('Read last state'); + + try { + const { state, id } = await this.getState( + bodyQuery.targets[b].target, + query.user as `system.user.${string}`, + ); + element.target = id; + if (state) { + element.datapoints = [[state.val, state.ts]]; + } else { + element.datapoints = [[null, null]]; + } + + list.push(element); + } catch (err) { + if (err.toString().includes('not found')) { + list.push({ target: bodyQuery.targets[b].target, datapoints: [] }); + } else { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, 'json', 401, err); + } else { + this.doErrorResponse(res, 'json', 500, err); + } + return; + } + } + } + } + + this.doResponse(res, 'json', list, query.prettyPrint); + } catch (err) { + if (err.includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, 'json', 403, err); + } else { + this.doErrorResponse(res, 'json', 500, `Cannot parse request: ${body}`); + } + } + + break; + } + + case 'annotations': + // iobroker does not support annotations + this.adapter.log.debug('[ANNOTATIONS]'); + this.doResponse(res, 'json', [], query.prettyPrint); + break; + + default: + this.doErrorResponse(res, 'json', 422, `command "${command}" unknown`); + break; + } + } + + async findState(idOrName: string, user: `system.user.${string}`): Promise<{ id: string; name: string }> { + // By ID + let r = this.cachedIds.get(idOrName); + if (r) { + return r; + } + // By name + r = this.cachedNames.get(idOrName); + if (r) { + return r; + } + + const result: { id: string | undefined; name: ioBroker.StringOrTranslated | undefined } = + // @ts-expect-error fixed in js-controller + await this.adapter.findForeignObjectAsync(idOrName, null, { user, lang: this.settings.language }); + if (result.id) { + let name: string; + if (result.name && typeof result.name === 'object') { + name = result.name[this.settings.language || 'en'] || result.name.en; + } else { + name = (result.name as string) || ''; + } + this.cachedIds.set(result.id, { id: result.id, name }); + if (idOrName !== result.id) { + // search was for a name, so also cache the name + this.cachedNames.set(name, { id: result.id, name }); + } + return { id: result.id, name }; + } + + throw new Error(`datapoint "${idOrName}" not found`); + } + + async getState( + idOrName: string, + user: `system.user.${string}`, + ): Promise<{ state: ioBroker.State | null | undefined; id: string }> { + const result: { id: string; name: string } = await this.findState(idOrName, user); + + return { + state: await this.adapter.getForeignStateAsync(result.id, { + user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }), + id: result.id, + }; + } + + doResponse(res: Response, type: 'json' | 'plain', content?: any, pretty?: boolean): void { + let response: string; + if (pretty && typeof content === 'object') { + type = 'plain'; + response = JSON.stringify(content, null, 2); + } else { + response = JSON.stringify(content); + } + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + + res.setHeader('Content-Type', type === 'json' ? 'application/json; charset=utf-8' : 'text/html; charset=utf-8'); + res.statusCode = 200; + res.end(response, 'utf8'); + } + + doErrorResponse(res: Response, type: 'json' | 'plain', status: 401 | 403 | 404 | 422 | 500, error?: string): void { + let response: string; + response = escapeHtml(error || 'unknown'); + if (!response.startsWith('error: ')) { + response = `error: ${response}`; + } + if (type === 'json') { + response = JSON.stringify({ error: response }); + } + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + res.setHeader('Content-Type', type === 'json' ? 'application/json; charset=utf-8' : 'text/html; charset=utf-8'); + res.statusCode = status; + res.end(response, 'utf8'); + } + + async checkPermissions(user: `system.user.${string}`, command: CommandName): Promise { + const acl = await this.adapter.calculatePermissionsAsync( + user, + commandsPermissions as CommandsPermissionsObject, + ); + if (user !== 'system.user.admin') { + // type: file, object, state, other + // operation: create, read, write, list, delete, sendto, execute, sendto + if (commandsPermissions[command]) { + // If permission required + if (commandsPermissions[command].type) { + if (commandsPermissions[command].type === 'object') { + if (commandsPermissions[command].operation === 'list') { + return !!acl.object?.list; + } + if (commandsPermissions[command].operation === 'read') { + return !!acl.object?.read; + } + return !!acl.object?.write; + } + if (commandsPermissions[command].type === 'state') { + if (commandsPermissions[command].operation === 'list') { + return !!acl.state?.list; + } + if (commandsPermissions[command].operation === 'read') { + return !!acl.state?.read; + } + return !!acl.state?.write; + } + } + + return true; + } + + // unknown command + this.adapter.log.warn(`Unknown command from "${user}": ${command}`); + return false; + } + + return true; + } + + async setValue( + id: string, + value: ioBroker.StateValue, + res: Response, + wait: number, + query: { ack: boolean; user?: string; prettyPrint?: boolean }, + responseType: 'json' | 'plain', + ): Promise { + if (wait) { + await this.adapter.subscribeForeignStatesAsync(id); + } + + try { + await this.adapter.setForeignState(id, value, query.ack, { + user: query.user as `system.user.${string}`, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + } catch (err) { + if (wait && !this.restApiDelayed.find(it => it.id === id)) { + this.adapter.unsubscribeForeignStates(id); + } + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + } else { + this.doErrorResponse(res, responseType, 500, err); + } + return; + } + + if (!wait) { + this.doResponse(res, responseType, { id, value, val: value }, query.prettyPrint); + } else { + // Wait for ack=true + await new Promise((resolve, reject): void => { + let timer: ioBroker.Timeout | undefined; + const index = `${Date.now()}_${Math.round(Math.random() * 1000000)}`; + + timer = this.adapter.setTimeout(() => { + timer = undefined; + reject(new Error(`timeout ${index}`)); + }, wait); + this.restApiDelayed.push({ id, resolve, timer, index, value }); + }) + .then((index: string): void => { + // Delete the timer + for (let i = 0; i < this.restApiDelayed.length; i++) { + if (this.restApiDelayed[i].index === index) { + const id = this.restApiDelayed[i].id; + const value = this.restApiDelayed[i].value; + if (this.restApiDelayed[i].timer) { + this.adapter.clearTimeout(this.restApiDelayed[i].timer); + this.restApiDelayed[i].timer = undefined; + } + this.restApiDelayed.splice(i, 1); + + this.doResponse(res, responseType, { id, value, val: value }, query.prettyPrint); + + // Unsubscribe if no other request is waiting + if (!this.restApiDelayed.find(it => it.id === id)) { + this.adapter.unsubscribeForeignStates(id); + } + + break; + } + } + }) + .catch((err: Error): void => { + const [error, index] = err.toString().split(' '); + + for (let i = 0; i < this.restApiDelayed.length; i++) { + if (this.restApiDelayed[i].index === index) { + const id = this.restApiDelayed[i].id; + if (this.restApiDelayed[i].timer) { + this.adapter.clearTimeout(this.restApiDelayed[i].timer); + this.restApiDelayed[i].timer = undefined; + } + this.restApiDelayed.splice(i, 1); + + if (!this.restApiDelayed.find(it => it.id === id)) { + this.adapter.unsubscribeForeignStates(id); + } + + this.doErrorResponse(res, responseType, 500, error); + + break; + } + } + }); + } + } + + async restApi(req: Request, res: Response, overwriteUrl?: string): Promise { + let url: string = overwriteUrl || req.url || ''; + const values: Record = {}; + const query: { + user?: string; + pass?: string; + prettyPrint?: boolean; + json?: boolean; + noStringify?: boolean; + wait?: number; + ack: boolean; + } = { ack: false }; + let oId: string[] = []; + + try { + url = decodeURI(url); + } catch (e) { + this.adapter.log.warn(`Malformed URL encoding for ${url}: ${e}`); + } + + const pos = url.indexOf('?'); + + if (pos !== -1) { + url = url.substring(0, pos); + SimpleAPI.parseQuery(url.substring(pos + 1), query, values); + } + + const [, _command, varsName] = url.split('/'); + const command = _command as CommandName; + const responseType: 'plain' | 'json' = command === 'getPlainValue' ? 'plain' : 'json'; + + // Analyse system.adapter.socketio.0.uptime,system.adapter.history.0.memRss?value=78&wait=300 + if (varsName) { + oId = varsName.split(','); + for (let j = oId.length - 1; j >= 0; j--) { + try { + oId[j] = decodeURIComponent(oId[j]); + } catch (e) { + this.adapter.log.warn(`Malformed URL encoding for "${oId[j]}": ${e}`); + oId[j] = oId[j].trim().replace(/%23/g, '#'); // do old style minimal parsing + } + } + oId = oId.filter(id => id.trim()); + } + + let user: `system.user.${string}`; + + // If authentication check is required + if (this.settings.auth) { + query.user ||= (req as any).user as string; + const isAuth: boolean = !!(req as any).user || (await this.isAuthenticated(query)); + if (!isAuth) { + this.doErrorResponse( + res, + responseType, + 401, + `authentication failed. Please write "http${this.settings.secure ? 's' : ''}://${req.headers.host}?user=UserName&pass=Password"`, + ); + return; + } + } else { + query.user = ((req as any).user as string | undefined) || this.config.defaultUser; + } + + if (query.user && !query.user.startsWith('system.user.')) { + user = `system.user.${query.user}`; + } else { + user = query.user as `system.user.${string}`; + } + + if (!(await this.checkPermissions(user, command))) { + this.doErrorResponse(res, responseType, 401, `No permission for "${query.user}" to call ${command}`); + return; + } + + if (req.method === 'POST') { + await this.restApiPost(req, res, command, oId, values, query); + return; + } + + switch (command) { + case 'getPlainValue': { + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'no datapoint given'); + break; + } + + const response: string[] = []; + for (let i = 0; i < oId.length; i++) { + try { + const { state, id } = await this.getState(oId[i], user); + if (state) { + let val = state.val; + if (query.json) { + let obj: any; + try { + obj = JSON.parse(typeof val === 'string' ? val : JSON.stringify(val)); + } catch { + // ignore + obj = val; + } + val = JSON.stringify(obj); + } else { + if (query.noStringify) { + val = + state.val === null + ? 'null' + : state.val === undefined + ? 'undefined' + : state.val.toString(); + } else { + val = JSON.stringify(state.val); + } + } + response[i] = val; + } else { + response[i] = `error: cannot read state "${oId[i]}"`; + } + } catch (err) { + if (err.toString().includes('not found')) { + response[i] = `error: datapoint "${oId[i]}" not found`; + } else { + this.adapter.log.error(`Cannot get state: ${err}`); + + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + } else { + this.doErrorResponse(res, responseType, 500, err); + } + return; + } + } + } + this.doResponse(res, responseType, response.join('\n'), query.prettyPrint); + break; + } + + case 'get': { + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'no object/datapoint given'); + break; + } + + const response: any[] = []; + for (let k = 0; k < oId.length; k++) { + this.adapter.log.debug(`work for ID ${oId[k]}`); + try { + const { state, id } = await this.getState(oId[k], query.user as `system.user.${string}`); + if (!id) { + response[k] = { error: `datapoint "${oId[k]}" not found` }; + } else { + const obj = this.adapter.getForeignObjectAsync(id); + response[k] = { ...obj, ...state }; + } + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + return; + } + response[k] = { error: `datapoint "${oId[k]}" not found` }; + } + } + this.doResponse(res, responseType, response.length === 1 ? response[0] : response, query.prettyPrint); + break; + } + case 'getBulk': { + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'no datapoints given'); + break; + } + const response: { + id: string; + val: ioBroker.StateValue | undefined; + ts: number | undefined; + ack: boolean | undefined; + }[] = []; + for (let b = 0; b < oId.length; b++) { + try { + const { id, state } = await this.getState(oId[b], query.user as `system.user.${string}`); + response[b] = { id, val: state?.val, ts: state?.ts, ack: state?.ack }; + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + return; + } + response[b] = { id: oId[b], val: undefined, ts: undefined, ack: undefined }; + } + } + this.doResponse(res, responseType, response, query.prettyPrint); + break; + } + + case 'set': { + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'object/datapoint not given'); + return; + } + if (values.value === undefined && values.val === undefined) { + this.doErrorResponse( + res, + responseType, + 422, + `error: no value found for "${oId[0]}". Use /set/id?value=1 or /set/id?value=1&wait=1000`, + ); + return; + } + try { + const { id } = await this.findState(oId[0], query.user as `system.user.${string}`); + if (!id) { + this.doErrorResponse(res, responseType, 404, `error: datapoint "${oId[0]}" not found`); + return; + } + const type = values.type; + let value: ioBroker.StateValue; + + if (values.val === undefined) { + value = values.value; + } else { + value = values.val; + } + + // Ack=true cannot be awaited + const wait = !query.ack ? query.wait || 0 : 0; + + // If type is not defined or not known + if ( + !type || + (type !== 'boolean' && + type !== 'number' && + type !== 'string' && + type !== 'json' && + type !== 'object' && + type !== 'array') + ) { + // Maybe this is JSON + if (values.val && (values.val[0] === '[' || values.val[0] === '{')) { + try { + values.val = JSON.parse(values.val); + } catch (e) { + // keep it as string + } + } + + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } else if (!isNaN(parseFloat(value as string))) { + value = parseFloat(value as string); + } + } else { + // type is known + if (type === 'boolean') { + value = value === 'true' || value === '1'; + } else if (type === 'number') { + value = parseFloat(value as string); + } else if (type === 'json' || type === 'array' || type === 'object') { + try { + value = JSON.parse(value as string); + } catch (e) { + this.doErrorResponse(res, responseType, 500, e); + return; + } + } + // string must not be formatted + } + + await this.setValue(id, value, res, wait, query, responseType); + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + } else { + this.doErrorResponse(res, responseType, 404, err); + } + } + break; + } + + case 'toggle': + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'state not given'); + return; + } + + try { + const { id } = await this.findState(oId[0], query.user as `system.user.${string}`); + if (id) { + const wait = query.wait || 0; + + // Read a type of object + const obj = await this.adapter.getForeignObjectAsync(id, { + user: query.user as `system.user.${string}`, + }); + if (obj) { + const state = await this.adapter.getForeignStateAsync(id, { + user: query.user as `system.user.${string}`, + }); + if (state) { + let value = state.val; + if (obj.common.type === 'boolean' || (!obj.common.type && typeof value === 'boolean')) { + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } + value = !value; + } else if ( + obj.common.type === 'number' || + (!obj.common.type && typeof value === 'number') + ) { + value = parseFloat(value as string); + if (obj.common.max !== undefined) { + obj.common.min = obj.common.min === undefined ? 0 : obj.common.min; + + if (value > obj.common.max) { + value = obj.common.max; + } else if (value < obj.common.min) { + value = obj.common.min; + } + // Invert + value = obj.common.max + obj.common.min - (value as number); + } else { + // the default number is from 0 to 100 + if (value > 100) { + value = 100; + } + if (value < 0) { + value = 0; + } + value = 100 - value; + } + } else { + if (value === 'true' || value === true) { + value = false; + } else if (value === 'on') { + value = 'off'; + } else if (value === 'off') { + value = 'on'; + } else if (value === 'OFF') { + value = 'ON'; + } else if (value === 'ON') { + value = 'OFF'; + } else if (value === 'false' || value === false) { + value = true; + } else if (parseFloat(value as string).toString() === value?.toString()) { + value = parseFloat(value as string).toString() ? 0 : 1; + } else { + this.doErrorResponse( + res, + responseType, + 422, + 'state is neither number nor boolean', + ); + return; + } + } + + await this.setValue(id, value, res, wait, query, responseType); + return; + } + + this.doErrorResponse( + res, + responseType, + 404, + `error: state "${oId[0]}" does not exist or null`, + ); + return; + } + } + + this.doErrorResponse(res, responseType, 404, `error: datapoint "${oId[0]}" not found`); + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + } else { + this.doErrorResponse(res, responseType, 404, err); + } + } + + break; + + // /setBulk?BidCos-RF.FEQ1234567:1.LEVEL=0.7&Licht-Küche/LEVEL=0.7&Anwesenheit=0&950=1 + case 'setBulk': { + const response: ( + | { error: string } + | { id: string; val: number | string | boolean | null; value: number | string | boolean | null } + )[] = []; + this.adapter.log.debug(`Values: ${JSON.stringify(values)}`); + const names = Object.keys(values); + for (let n = 0; n < names.length; n++) { + try { + const { id, name } = await this.findState(names[n], query.user as `system.user.${string}`); + if (!id) { + response[n] = { error: `error: datapoint "${names[n]}" not found` }; + continue; + } + this.adapter.log.debug( + `GET-${command} for id=${id}, oid=${names[n]}, value=${values[names[n]]}`, + ); + + let value: ioBroker.StateValue = values[names[n]]; + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } else { + const f = parseFloat(value as string); + if (!isNaN(f) && value === f.toString()) { + value = f; + } + } + + try { + await this.adapter.setForeignState(id, value, query.ack, { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + response[n] = { id: id, val: value, value }; + this.adapter.log.debug(`Add to Response-Get: ${JSON.stringify(response[n])}`); + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + return; + } else { + response[n] = { error: err.toString() }; + } + } + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doErrorResponse(res, responseType, 403, err); + return; + } else { + response[n] = { error: err.toString() }; + } + } + } + + this.doResponse(res, responseType, response, query.prettyPrint); + break; + } + + case 'getObjects': + case 'objects': { + try { + const list = await this.adapter.getForeignObjectsAsync( + values.pattern || varsName || '*', + (values.type as ioBroker.ObjectType) || null, + { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }, + ); + this.doResponse(res, responseType, list, query.prettyPrint); + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doResponse(res, responseType, 403, err.toString()); + } else { + this.doResponse(res, responseType, 500, err.toString()); + } + } + break; + } + + case 'getStates': + case 'states': { + try { + const list = await this.adapter.getForeignStatesAsync(values.pattern || varsName || '*', { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + this.doResponse(res, responseType, list, query.prettyPrint); + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doResponse(res, responseType, 403, err.toString()); + } else { + this.doResponse(res, responseType, 500, err.toString()); + } + } + break; + } + + case 'search': + try { + if (this.config.dataSource && this.config.allDatapoints !== true) { + const result = await this.adapter.sendToAsync(this.config.dataSource, 'getEnabledDPs'); + this.doResponse(res, responseType, Object.keys(result || {}), query.prettyPrint); + } else { + this.adapter.log.debug(`[SEARCH] target = ${varsName}`); + + const list = await this.adapter.getForeignStatesAsync(values.pattern || varsName || '*', { + user: query.user, + limitToOwnerRights: this.config.onlyAllowWhenUserIsOwner, + }); + this.doResponse(res, responseType, Object.keys(list), query.prettyPrint); + } + } catch (err) { + if (err.toString().includes(ERROR_PERMISSION)) { + this.doResponse(res, responseType, 403, err.toString()); + } else { + this.doResponse(res, responseType, 500, err.toString()); + } + } + break; + + case 'query': { + this.adapter.log.debug(JSON.stringify(values)); + + let dateFrom = Date.now(); + let dateTo = Date.now(); + + if (values.dateFrom) { + dateFrom = Date.parse(values.dateFrom); + } + if (values.dateTo) { + dateTo = Date.parse(values.dateTo); + } + + const options: ioBroker.GetHistoryOptions = { + start: dateFrom, + end: dateTo, + aggregate: (values.aggregate as ioBroker.GetHistoryOptions['aggregate']) || 'onchange', + }; + + if (values.count) { + options.count = parseInt(values.count, 10); + } + if (values.step) { + options.step = parseInt(values.step, 10); + } + + if (!oId.length || !oId[0]) { + this.doErrorResponse(res, responseType, 422, 'no datapoints given'); + return; + } + const response: { target: string; datapoints: [ioBroker.StateValue, number | null][] }[] = []; + + for (let b = 0; b < oId.length; b++) { + const element: { target: string; datapoints: [ioBroker.StateValue, number | null][] } = { + target: oId[b], + datapoints: [], + }; + + if (this.config.dataSource && !(values.noHistory && values.noHistory === 'true')) { + this.adapter.log.debug(`Read data from: ${this.config.dataSource}`); + + const result = await this.adapter.getHistoryAsync(this.config.dataSource, oId[b], options); + this.adapter.log.debug(`[QUERY] sendTo result = ${JSON.stringify(result)}`); + + for (let i = 0; i < result.result.length; i++) { + element.datapoints.push([result.result[i].val, result.result[i].ts]); + } + + response[b] = element; + } else { + this.adapter.log.debug('Read last state'); + + const { state } = await this.getState(oId[b], query.user as `system.user.${string}`); + if (state) { + element.datapoints = [[state.val, state.ts]]; + } else { + element.datapoints = [[null, null]]; + } + } + } + + this.doResponse(res, responseType, response, query.prettyPrint); + break; + } + + case 'annotations': + // ioBroker does not support annotations + this.adapter.log.debug('[ANNOTATIONS]'); + this.doResponse(res, responseType, [], query.prettyPrint); + break; + + case 'help': + // is default behaviour too + default: + const _obj: Record = command === 'help' ? {} : { error: `command ${command} unknown` }; + let request = `http${this.settings.secure ? 's' : ''}://${req.headers.host}`; + if (this.app) { + request += `/${this.namespace}/`; + } + let auth = ''; + if (this.settings.auth) { + auth = 'user=UserName&pass=Password'; + } + _obj.getPlainValue = `${request}/getPlainValue/stateID${auth ? `?${auth}` : ''}`; + _obj.get = `${request}/get/stateID/?prettyPrint${auth ? `&${auth}` : ''}`; + _obj.getBulk = `${request}/getBulk/stateID1,stateID2/?prettyPrint${auth ? `&${auth}` : ''}`; + _obj.set = `${request}/set/stateID?value=1&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.toggle = `${request}/toggle/stateID&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.setBulk = `${request}/setBulk?stateID1=0.7&stateID2=0&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.setValueFromBody = `${request}/setValueFromBody?stateID1${auth ? `&${auth}` : ''}`; + _obj.objects = `${request}/objects?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.states = `${request}/states?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.search = `${request}/search?pattern=system.adapter.admin.0*&prettyPrint${auth ? `&${auth}` : ''}`; + _obj.query = `${request}/query/stateID1,stateID2/?dateFrom=2019-06-06T12:00:00.000Z&dateTo=2019-06-06T12:00:00.000Z&noHistory=false&aggregate=minmax&count=3000&prettyPrint${auth ? `&${auth}` : ''}`; + + this.doResponse(res, responseType, _obj, true); + break; + } + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..1fbde02 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,226 @@ +import { type Request, type Response } from 'express'; +import { createReadStream, existsSync, type Stats, statSync } from 'node:fs'; +import type { RequestListener } from 'http'; + +import { Adapter, type AdapterOptions, EXIT_CODES } from '@iobroker/adapter-core'; +import { SimpleAPI, type Server } from './lib/SimpleAPI'; +import { WebServer } from '@iobroker/webserver'; +import { SimpleApiAdapterConfig } from './types'; + +interface WebStructure { + server: null | (Server & { __server: WebStructure }); + api: SimpleAPI | null; +} + +export class SimpleApiAdapter extends Adapter { + declare public config: SimpleApiAdapterConfig; + private webServer: WebStructure = { + server: null, + api: null, + }; + private certificates: ioBroker.Certificates | undefined; + + public constructor(options: Partial = {}) { + super({ + ...options, + name: 'simple-api', + unload: callback => this.onUnload(callback), + stateChange: (id, state) => { + this.webServer?.api?.stateChange(id, state); + }, + ready: () => this.main(), + objectChange: (id: string, obj: ioBroker.Object | null | undefined): void => { + this.webServer?.api?.objectChange(id, obj); + }, + }); + + this.config = this.config as SimpleApiAdapterConfig; + } + + onUnload(callback: () => void): void { + try { + if (this.webServer.server) { + this.log.info(`terminating http${this.config.secure ? 's' : ''} server on port ${this.config.port}`); + this.webServer.server.close(); + this.webServer.server = null; + } + } catch { + // ignore + } + if (callback) { + callback(); + } + } + + async main(): Promise { + this.config = this.config as SimpleApiAdapterConfig; + + if (this.config.webInstance) { + console.log('Adapter runs as a part of web service'); + this.log.warn('Adapter runs as a part of web service'); + return this.setForeignState(`system.adapter.${this.namespace}.alive`, false, true, () => + setTimeout(() => (this.terminate ? this.terminate() : process.exit()), 1000), + ); + } + + if (this.config.secure) { + // Load certificates + await new Promise(resolve => + this.getCertificates(undefined, undefined, undefined, (err, certificates, leConfig): void => { + this.certificates = certificates; + resolve(); + }), + ); + } + + await this.initWebServer(); + } + + requestProcessor = (req: Request, res: Response): void => { + if ((req.url || '').includes('favicon.ico')) { + let stat: Stats | undefined; + try { + if (existsSync(`${__dirname}/../img/favicon.ico`)) { + stat = statSync(`${__dirname}/../img/favicon.ico`); + } + } catch { + // no special handling + } + + if (stat) { + res.writeHead(200, { + 'Content-Type': 'image/x-icon', + 'Content-Length': stat.size, + }); + + const readStream = createReadStream(`${__dirname}/../img/favicon.ico`); + // We replaced all the event handlers with a simple call to readStream.pipe() + readStream.pipe(res); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.write('404 Not Found\n'); + res.end(); + } + } else { + this.webServer.api?.restApi(req, res); + } + }; + + async initWebServer() { + this.config.port = parseInt(this.config.port as string, 10); + + if (this.config.port) { + if (this.config.secure && !this.certificates) { + return null; + } + + try { + const webserver = new WebServer({ + app: this.requestProcessor as RequestListener, + adapter: this, + secure: this.config.secure, + }); + + this.webServer.server = (await webserver.init()) as Server & { __server: WebStructure }; + } catch (err) { + this.log.error(`Cannot create webserver: ${err}`); + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + if (!this.webServer.server) { + this.log.error(`Cannot create webserver`); + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + this.webServer.server.__server = this.webServer; + } else { + this.log.error('port missing'); + if (this.terminate) { + this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } else { + process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + return; + } + + if (this.webServer.server) { + let serverListening = false; + let serverPort = this.config.port; + + this.webServer.server.on('error', e => { + if (e.toString().includes('EACCES') && serverPort <= 1024) { + this.log.error( + `node.js process has no rights to start server on the port ${serverPort}.\n` + + `Do you know that on linux you need special permissions for ports under 1024?\n` + + `You can call in shell following scrip to allow it for node.js: "iobroker fix"`, + ); + } else { + this.log.error(`Cannot start server on ${this.config.bind || '0.0.0.0'}:${serverPort}: ${e}`); + } + if (!serverListening) { + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + }); + + this.getPort( + this.config.port, + !this.config.bind || this.config.bind === '0.0.0.0' ? undefined : this.config.bind || undefined, + port => { + if (port !== this.config.port) { + this.log.error(`port ${this.config.port} already in use`); + if (this.terminate) { + this.terminate(1); + } else { + process.exit(1); + } + return; + } + serverPort = port; + + if (this.webServer.server) { + // create web server + this.webServer.server.listen( + port, + !this.config.bind || this.config.bind === '0.0.0.0' + ? undefined + : this.config.bind || undefined, + () => (serverListening = true), + ); + + this.log.info(`http${this.config.secure ? 's' : ''} server listening on port ${port}`); + } else { + this.log.error('server initialization failed'); + if (this.terminate) { + this.terminate(1); + } else { + process.exit(1); + } + } + }, + ); + } + + this.webServer.api = new SimpleAPI(this.webServer.server, this.config, this, { + native: this.config, + _id: `system.adapter.${this.namespace}`, + common: this.common as ioBroker.InstanceCommon, + type: 'instance', + objects: [], + instanceObjects: [], + }); + } +} + +if (require.main !== module) { + // Export the constructor in compact mode + module.exports = (options: Partial | undefined) => new SimpleApiAdapter(options); +} else { + // otherwise start the instance directly + (() => new SimpleApiAdapter())(); +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..f6ec9e4 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,38 @@ +export interface SimpleApiAdapterConfig { + port: number | string; + auth: boolean; + secure: boolean; + bind: string; + certPublic: string; + certPrivate: string; + certChained: string; + defaultUser: string; // without 'system.user.' + onlyAllowWhenUserIsOwner: boolean; + webInstance: string; + leEnabled: boolean; + leUpdate: boolean; + leCheckPort: number | string; + dataSource: string; + allDatapoints: boolean; +} + +declare class ExtAPI { + public waitForReadyTime?: number; + + constructor( + webServer: Server, + settings: { secure: boolean; port: number | string; defaultUser?: string }, + adapter: ioBroker.Adapter, + config?: ioBroker.InstanceObject, + app?: Express, + io?: SocketIO, + ); + + welcomePage?(): LocalMultipleLinkEntry; + fileChange?(id: string, fileName: string, size: number | null): void; + stateChange?(id: string, state: ioBroker.State | null | undefined): void; + objectChange?(id: string, state: ioBroker.Object | null | undefined): void; + /** Give to the extension up to 5 seconds to be loaded */ + waitForReady?(onReady: () => void): void; + unload?(): Promise; +} \ No newline at end of file diff --git a/tasks.js b/tasks.js new file mode 100644 index 0000000..d37ad5d --- /dev/null +++ b/tasks.js @@ -0,0 +1,3 @@ +const { copyFileSync } = require('node:fs'); + +copyFileSync(`${__dirname}/src/types.d.ts`, `${__dirname}/dist/types.d.ts`); diff --git a/test/lib/setup.js b/test/lib/setup.js index 874b5e5..fe4ef45 100644 --- a/test/lib/setup.js +++ b/test/lib/setup.js @@ -2,11 +2,11 @@ /* 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 fs = require('node:fs'); +const path = require('node:path'); +const child_process = require('node:child_process'); +const rootDir = path.normalize(`${__dirname}/../../`); +const pkg = require(`${rootDir}package.json`); const debug = typeof v8debug === 'object'; pkg.main = pkg.main || 'main.js'; @@ -24,14 +24,14 @@ function getAppName() { function loadJSONLDB() { if (!JSONLDB) { const dbPath = require.resolve('@alcalzone/jsonl-db', { - paths: [rootDir + 'tmp/node_modules', rootDir, `${rootDir}tmp/node_modules/${appName}.js-controller`] + paths: [`${rootDir}tmp/node_modules`, rootDir, `${rootDir}tmp/node_modules/${appName}.js-controller`], }); console.log(`JSONLDB path: ${dbPath}`); try { - const {JsonlDB} = require(dbPath); + const { JsonlDB } = require(dbPath); JSONLDB = JsonlDB; } catch (err) { - console.log('Jsonl require error: ' + err); + console.log(`Jsonl require error: ${err}`); } } } @@ -223,8 +223,7 @@ async function checkIsAdapterInstalled(cb, counter, customName) { if (obj) { console.log('checkIsAdapterInstalled: ready!'); - setTimeout(() => - cb && cb(), 100); + setTimeout(() => cb && cb(), 100); return; } else { console.warn('checkIsAdapterInstalled: still not ready'); @@ -232,7 +231,6 @@ async function checkIsAdapterInstalled(cb, counter, customName) { } else { console.error('checkIsAdapterInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { console.log('checkIsAdapterInstalled: catch ' + err); } @@ -242,8 +240,7 @@ async function checkIsAdapterInstalled(cb, counter, customName) { cb && cb('Cannot install'); } else { console.log('checkIsAdapterInstalled: wait...'); - setTimeout(() => - checkIsAdapterInstalled(cb, counter + 1), 1000); + setTimeout(() => checkIsAdapterInstalled(cb, counter + 1), 1000); } } @@ -258,8 +255,7 @@ async function checkIsControllerInstalled(cb, counter) { const objects = JSON.parse(f.toString()); if (objects['system.certificates']) { console.log('checkIsControllerInstalled: installed!'); - setTimeout(() => - cb && cb(), 100); + setTimeout(() => cb && cb(), 100); return; } } else if (fs.existsSync(dataDir + 'objects.jsonl')) { @@ -279,25 +275,20 @@ async function checkIsControllerInstalled(cb, counter) { if (obj) { console.log('checkIsControllerInstalled: installed!'); - setTimeout(() => - cb && cb(), 100); + setTimeout(() => cb && cb(), 100); return; } - } else { console.error('checkIsControllerInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { - - } + } catch (err) {} if (counter > 20) { console.log('checkIsControllerInstalled: Cannot install!'); cb && cb('Cannot install'); } else { console.log('checkIsControllerInstalled: wait...'); - setTimeout(() => - checkIsControllerInstalled(cb, counter + 1), 1000); + setTimeout(() => checkIsControllerInstalled(cb, counter + 1), 1000); } } @@ -313,7 +304,7 @@ function installAdapter(customName, cb) { if (debug) { child_process.execSync(`node ${startFile} add ${customName} --enabled false`, { cwd: rootDir + 'tmp', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); checkIsAdapterInstalled(error => { error && console.error(error); @@ -324,7 +315,7 @@ function installAdapter(customName, cb) { // add controller const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { cwd: rootDir + 'tmp', - stdio: [0, 1, 2, 'ipc'] + stdio: [0, 1, 2, 'ipc'], }); waitForEnd(_pid, async () => { @@ -358,12 +349,16 @@ function waitForEnd(_pid, cb) { function installJsController(cb) { console.log('installJsController...'); - if (!fs.existsSync(`${rootDir}tmp/node_modules/${appName}.js-controller`) || - !fs.existsSync(`${rootDir}tmp/${appName}-data`)) { + 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"`); + 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...'); @@ -372,12 +367,12 @@ function installJsController(cb) { // start controller _pid = child_process.exec(`node ${appName}.js stop`, { cwd: `${rootDir}node_modules/${appName}.js-controller`, - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { _pid = child_process.fork(`${appName}.js`, ['stop'], { cwd: `${rootDir}node_modules/${appName}.js-controller`, - stdio: [0, 1, 2, 'ipc'] + stdio: [0, 1, 2, 'ipc'], }); } @@ -392,7 +387,10 @@ function installJsController(cb) { 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/'); + copyFolderRecursiveSync( + `${rootDir}node_modules/${appName}.js-controller`, + rootDir + 'tmp/node_modules/', + ); } console.log('Setup js-controller...'); @@ -401,12 +399,12 @@ function installJsController(cb) { // start controller _pid = child_process.exec(`node ${appName}.js setup first --console`, { cwd: `${rootDir}tmp/node_modules/${appName}.js-controller`, - stdio: [0, 1, 2] + 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'] + stdio: [0, 1, 2, 'ipc'], }); } waitForEnd(__pid, () => { @@ -419,7 +417,10 @@ function installJsController(cb) { // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(`${rootDir}tmp/${appName}-data/${appName}.json`, JSON.stringify(config, null, 2)); + fs.writeFileSync( + `${rootDir}tmp/${appName}-data/${appName}.json`, + JSON.stringify(config, null, 2), + ); console.log('Setup finished.'); copyAdapterToController(); @@ -434,10 +435,11 @@ function installJsController(cb) { } 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.on('error', () => {}); client.connect(9000, '127.0.0.1', () => { - console.error('Cannot initiate first run of test, because one instance of application is running on this PC. Stop it and repeat.'); + console.error( + 'Cannot initiate first run of test, because one instance of application is running on this PC. Stop it and repeat.', + ); process.exit(0); }); @@ -448,7 +450,7 @@ function installJsController(cb) { child_process.execSync(`npm install ${appName}.js-controller@dev --prefix ./ --production`, { cwd: rootDir + 'tmp/', - stdio: [0, 1, 2] + stdio: [0, 1, 2], }); } else { console.log('Setup js-controller...'); @@ -457,12 +459,12 @@ function installJsController(cb) { // start controller child_process.exec(`node ${appName}.js setup first`, { cwd: `${rootDir}tmp/node_modules/${appName}.js-controller`, - stdio: [0, 1, 2] + 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'] + stdio: [0, 1, 2, 'ipc'], }); } } @@ -474,7 +476,7 @@ function installJsController(cb) { 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'] + stdio: [0, 1, 2, 'ipc'], }); } @@ -487,7 +489,10 @@ function installJsController(cb) { // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(`${rootDir}tmp/${appName}-data/${appName}.json`, JSON.stringify(config, null, 2)); + fs.writeFileSync( + `${rootDir}tmp/${appName}-data/${appName}.json`, + JSON.stringify(config, null, 2), + ); copyAdapterToController(); @@ -510,7 +515,13 @@ function installJsController(cb) { 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']); + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', [ + '.idea', + 'test', + 'tmp', + '.git', + appName + '.js-controller', + ]); console.log('Adapter copied.'); } @@ -592,10 +603,10 @@ function setupController(cb) { objs = JSON.parse(objs); } catch (e) { console.log('ERROR reading/parsing system configuration. Ignore'); - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } systemConfig = objs['system.config']; @@ -636,7 +647,7 @@ async function getSecret() { return null; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } return objs['system.config'].native.secre; @@ -678,13 +689,13 @@ function startAdapter(objects, states, callback) { // start controller pid = child_process.exec(`node node_modules/${pkg.name}/${pkg.main} --console silly`, { cwd: rootDir + 'tmp', - stdio: [0, 1, 2] + 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'] + stdio: [0, 1, 2, 'ipc'], }); } } catch (error) { @@ -722,7 +733,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback // 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`] + paths: [`${rootDir}tmp/node_modules`, rootDir, `${rootDir}tmp/node_modules/${appName}.js-controller`], }); console.log('Objects Path: ' + objPath); const Objects = require(objPath).Server; @@ -734,7 +745,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback user: '', pass: '', noFileCache: false, - connectTimeout: 2000 + connectTimeout: 2000, }, logger: { silly: function (msg) { @@ -751,7 +762,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback }, error: function (msg) { console.error(msg); - } + }, }, connected: () => { isObjectConnected = true; @@ -767,12 +778,12 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } } }, - change: onObjectChange + 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`] + paths: [`${rootDir}tmp/node_modules`, rootDir, `${rootDir}tmp/node_modules/${appName}.js-controller`], }); console.log('States Path: ' + statePath); const States = require(statePath).Server; @@ -783,8 +794,8 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback port: 19000, options: { auth_pass: null, - retry_max_delay: 15000 - } + retry_max_delay: 15000, + }, }, logger: { silly: function (msg) { @@ -801,7 +812,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback }, error: function (msg) { console.log(msg); - } + }, }, connected: () => { isStatesConnected = true; @@ -817,7 +828,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } } }, - change: onStateChange + change: onStateChange, }); } catch (err) { console.log(err); @@ -829,8 +840,7 @@ function stopAdapter(cb) { if (!pid) { console.error('Controller is not running!'); if (cb) { - setTimeout(() => - cb(false), 0); + setTimeout(() => cb(false), 0); } } else { adapterStarted = false; @@ -870,8 +880,8 @@ function stopController(cb) { console.log(`Set system.adapter.${pkg.name}.0`); objects.setObject(`system.adapter.${pkg.name}.0`, { common: { - enabled: false - } + enabled: false, + }, }); } diff --git a/test/mocha.setup.js b/test/mocha.setup.js index 2adcb98..f641040 100644 --- a/test/mocha.setup.js +++ b/test/mocha.setup.js @@ -1 +1,3 @@ -process.on("unhandledRejection", (r) => { throw r; }); +process.on('unhandledRejection', r => { + throw r; +}); diff --git a/test/testApi.js b/test/testApi.js index ab3df62..0e01d29 100644 --- a/test/testApi.js +++ b/test/testApi.js @@ -2,19 +2,19 @@ /* jshint strict: false */ /* jslint node: true */ /* jshint expr: true*/ -const expect = require('chai').expect; -const setup = require('./lib/setup'); +const expect = require('chai').expect; +const setup = require('./lib/setup'); const request = require('request'); let objects = null; -let states = null; +let states = null; const TEST_STATE_ID = 'simple-api.0.testNumber'; process.env.NO_PROXY = '127.0.0.1'; function checkConnectionOfAdapter(cb, counter) { counter = counter || 0; - console.log('Try check #' + counter); + console.log(`Try check #${counter}`); if (counter > 30) { if (cb) cb('Cannot check connection'); return; @@ -31,23 +31,27 @@ function checkConnectionOfAdapter(cb, counter) { } function createTestState(cb) { - objects.setObject(TEST_STATE_ID, { - _id: TEST_STATE_ID, - type: 'state', - common: { - name: 'Test state', - type: 'number', - read: true, - write: false, - role: 'indicator.state', - unit: '%', - def: 0, - desc: 'test state' + objects.setObject( + TEST_STATE_ID, + { + _id: TEST_STATE_ID, + type: 'state', + common: { + name: 'Test state', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'test state', + }, + native: {}, }, - native: {} - }, () => { - states.setState(TEST_STATE_ID, {val: 0, ack: true}, cb && cb); - }); + () => { + states.setState(TEST_STATE_ID, { val: 0, ack: true }, cb && cb); + }, + ); } describe('Test RESTful API', function () { @@ -65,7 +69,7 @@ describe('Test RESTful API', function () { setup.startController((_objects, _states) => { objects = _objects; - states = _states; + states = _states; // give some time to start server setTimeout(() => createTestState(() => _done()), 2000); @@ -77,45 +81,51 @@ describe('Test RESTful API', function () { checkConnectionOfAdapter(res => { res && console.log(res); expect(res).not.to.be.equal('Cannot check connection'); - objects.setObject('javascript.0.test-string', { - common: { - name: 'test', - type: 'string', - role: 'value', - def: '' - }, - native: { + objects.setObject( + 'javascript.0.test-string', + { + common: { + name: 'test', + type: 'string', + role: 'value', + def: '', + }, + native: {}, + type: 'state', }, - type: 'state' - }, err => { - expect(err).to.be.null; - states.setState('javascript.0.test-string', '', err => { + err => { expect(err).to.be.null; - objects.setObject('javascript.0.test-number', { - common: { - name: 'test', - type: 'number', - role: 'value', - def: 0 - }, - native: { - }, - type: 'state' - }, err => { + states.setState('javascript.0.test-string', '', err => { expect(err).to.be.null; - states.setState('javascript.0.test-number', 0, err => { - expect(err).to.be.null; - done(); - }); + objects.setObject( + 'javascript.0.test-number', + { + common: { + name: 'test', + type: 'number', + role: 'value', + def: 0, + }, + native: {}, + type: 'state', + }, + err => { + expect(err).to.be.null; + states.setState('javascript.0.test-number', 0, err => { + expect(err).to.be.null; + done(); + }); + }, + ); }); - }); - }); + }, + ); }); }).timeout(60000); it('Test RESTful API: get - must return value', done => { request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive => ' + body); + console.log(`get/system.adapter.simple-api.0.alive => ${body}`); expect(error).to.be.not.ok; const obj = JSON.parse(body); //{ @@ -142,12 +152,12 @@ describe('Test RESTful API', function () { expect(obj.ack).to.be.true; expect(obj.ts).to.be.ok; //expect(obj.from).to.equal("system.adapter.simple-api.0"); - expect(obj.type).to.equal("state"); - expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); + expect(obj.type).to.equal('state'); + expect(obj._id).to.equal('system.adapter.simple-api.0.alive'); expect(obj.common).to.be.ok; expect(obj.native).to.be.ok; - expect(obj.common.name).to.equal("simple-api.0 alive"); - expect(obj.common.role).to.equal("indicator.state"); + expect(obj.common.name).to.equal('simple-api.0 alive'); + expect(obj.common.role).to.equal('indicator.state'); expect(response.statusCode).to.equal(200); done(); }); @@ -155,7 +165,7 @@ describe('Test RESTful API', function () { it('Test RESTful API: get - must return error', done => { request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive%23test', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive%23test => ' + body); + console.log(`get/system.adapter.simple-api.0.alive%23test => ${body}`); expect(error).to.be.not.ok; expect(body).to.be.equal('{"error":"datapoint \\"system.adapter.simple-api.0.alive#test\\" not found"}'); expect(response.statusCode).to.equal(500); @@ -165,7 +175,7 @@ describe('Test RESTful API', function () { it('Test RESTful API: get - must return error', done => { request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.%23alive%23test', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive#%23test => ' + body); + console.log(`get/system.adapter.simple-api.0.alive#%23test => ${body}`); expect(error).to.be.not.ok; expect(body).to.be.equal('{"error":"datapoint \\"system.adapter.simple-api.0.#alive#test\\" not found"}'); expect(response.statusCode).to.equal(500); @@ -175,7 +185,7 @@ describe('Test RESTful API', function () { it('Test RESTful API: getPlainValue - must return plain value', done => { request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + console.log(`getPlainValue/system.adapter.simple-api.0.alive => ${body}`); expect(error).to.be.not.ok; expect(body).equal('true'); expect(response.statusCode).to.equal(200); @@ -192,19 +202,22 @@ describe('Test RESTful API', function () { expect(obj.val).to.be.false; expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('false'); - expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive => ' + body); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); expect(error).to.be.not.ok; - expect(JSON.parse(body).val).equal(false); + expect(body).equal('false'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(JSON.parse(body).val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }, + ); }); }); @@ -234,28 +247,31 @@ describe('Test RESTful API', function () { }); it('Test RESTful API: set - must set encoded string value', done => { - request('http://127.0.0.1:18183/set/javascript.0.test-string?val=bla%26fasel%2efoo%3Dhummer+hey', (error, response, body) => { - console.log('set/javascript.0.test-string?val=bla%20fasel%2efoo => ' + body); - expect(error).to.be.not.ok; - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj.val).equal('bla&fasel.foo=hummer hey'); - expect(obj.id).to.equal('javascript.0.test-string'); - expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/javascript.0.test-string', (error, response, body) => { - console.log('getPlainValue/javascript.0.test-string => ' + body); + request( + 'http://127.0.0.1:18183/set/javascript.0.test-string?val=bla%26fasel%2efoo%3Dhummer+hey', + (error, response, body) => { + console.log('set/javascript.0.test-string?val=bla%20fasel%2efoo => ' + body); expect(error).to.be.not.ok; - expect(body).equal('"bla&fasel.foo=hummer hey"'); + const obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).equal('bla&fasel.foo=hummer hey'); + expect(obj.id).to.equal('javascript.0.test-string'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/get/javascript.0.test-string', (error, response, body) => { - console.log('get/javascript.0.test-string => ' + body); + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test-string', (error, response, body) => { + console.log('getPlainValue/javascript.0.test-string => ' + body); expect(error).to.be.not.ok; - expect(JSON.parse(body).val).equal('bla&fasel.foo=hummer hey'); + expect(body).equal('"bla&fasel.foo=hummer hey"'); expect(response.statusCode).to.equal(200); - done(); + request('http://127.0.0.1:18183/get/javascript.0.test-string', (error, response, body) => { + console.log('get/javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + expect(JSON.parse(body).val).equal('bla&fasel.foo=hummer hey'); + expect(response.statusCode).to.equal(200); + done(); + }); }); - }); - }); + }, + ); }); it('Test RESTful API: set - must set val', done => { @@ -268,41 +284,47 @@ describe('Test RESTful API', function () { expect(typeof obj.val).to.be.equal('boolean'); expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('true'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); it('Test RESTful API: set - must have ack true', done => { - request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=true&ack=true', (error, response, body) => { - console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); - expect(error).to.be.not.ok; - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj.val).to.be.true; - expect(typeof obj.val).to.be.equal('boolean'); - expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); - expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive => ' + body); - try { - body = JSON.parse(body); - } catch (e) { - expect(e).to.be.false; - } - + request( + 'http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=true&ack=true', + (error, response, body) => { + console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); expect(error).to.be.not.ok; - expect(body.val).equal(true); - expect(body.ack).equal(true); + const obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(typeof obj.val).to.be.equal('boolean'); + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + try { + body = JSON.parse(body); + } catch (e) { + expect(e).to.be.false; + } + + expect(error).to.be.not.ok; + expect(body.val).equal(true); + expect(body.ack).equal(true); + expect(response.statusCode).to.equal(200); + done(); + }); + }, + ); }); it('Test RESTful API: toggle - must toggle boolean value to false', done => { @@ -316,13 +338,16 @@ describe('Test RESTful API', function () { expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('false'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -336,13 +361,16 @@ describe('Test RESTful API', function () { expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('true'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -387,31 +415,37 @@ describe('Test RESTful API', function () { }); it('Test RESTful API: setBulk - must set values', done => { - request(`http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false`, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false => ${body}`); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); + request( + `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false`, + (error, response, body) => { + console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false => ${body}`); + expect(error).to.be.not.ok; - console.log(JSON.stringify(obj, null, 2)); + const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(50); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(response.statusCode).to.equal(200); + console.log(JSON.stringify(obj, null, 2)); - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ${body}`); - expect(error).to.be.not.ok; - const obj = JSON.parse(body); - expect(obj[0].val).equal(50); - expect(obj[1].val).equal(false); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ${body}`); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API: objects - must return objects', done => { @@ -448,93 +482,114 @@ describe('Test RESTful API', function () { }); it('Test RESTful API: setBulk(POST) - must set values', done => { - request({ - uri: 'http://127.0.0.1:18183/setBulk', - method: 'POST', - body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey&ack=true` - }, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey => ${JSON.stringify(body)}`); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(50); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(obj[2].val).to.be.equal('bla&fasel.foo=hummer hey'); - expect(obj[2].id).to.equal('javascript.0.test-string'); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test-string`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test-string => ${body}`); + request( + { + uri: 'http://127.0.0.1:18183/setBulk', + method: 'POST', + body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey&ack=true`, + }, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey => ${JSON.stringify(body)}`, + ); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(50); - expect(obj[0].ack).equal(true); - expect(obj[1].val).equal(false); - expect(obj[1].ack).equal(true); - expect(obj[2].val).equal('bla&fasel.foo=hummer hey'); - expect(obj[2].ack).equal(true); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal('bla&fasel.foo=hummer hey'); + expect(obj[2].id).to.equal('javascript.0.test-string'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test-string`, + (error, response, body) => { + console.log( + `getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test-string => ${body}`, + ); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[0].ack).equal(true); + expect(obj[1].val).equal(false); + expect(obj[1].ack).equal(true); + expect(obj[2].val).equal('bla&fasel.foo=hummer hey'); + expect(obj[2].ack).equal(true); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API: setBulk(POST-GET-Mix) - must set values', done => { - request({ - uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, - method: 'POST', - body: '' - }, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ${JSON.stringify(body)}`); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(51); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ${body}`); + request( + { + uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, + method: 'POST', + body: '', + }, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ${JSON.stringify(body)}`, + ); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(51); - expect(obj[1].val).equal(false); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(51); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ${body}`); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(51); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API: setValueFromBody(POST) - must set one value', done => { - request({ - uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, - method: 'POST', - body: '55' - }, (error, response, body) => { - console.log(`setValueFromBody/?${TEST_STATE_ID} => ${JSON.stringify(body)}`); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(55); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID} => ${body}`); + request( + { + uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, + method: 'POST', + body: '55', + }, + (error, response, body) => { + console.log(`setValueFromBody/?${TEST_STATE_ID} => ${JSON.stringify(body)}`); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(55); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal(TEST_STATE_ID); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID} => ${body}`); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }); + }, + ); }); after('Test RESTful API: Stop js-controller', function (done) { diff --git a/test/testApiAsLimitedUser.js b/test/testApiAsLimitedUser.js index e938851..8581e93 100644 --- a/test/testApiAsLimitedUser.js +++ b/test/testApiAsLimitedUser.js @@ -3,12 +3,12 @@ /* jslint node: true */ /* jshint expr: true*/ -const expect = require('chai').expect; -const setup = require('./lib/setup'); +const expect = require('chai').expect; +const setup = require('./lib/setup'); const request = require('request'); let objects = null; -let states = null; +let states = null; const PORT = 18183; const TEST_STATE_ID = 'simple-api.0.testNumber'; @@ -34,23 +34,27 @@ function checkConnectionOfAdapter(cb, counter) { } function createTestState(cb) { - objects.setObject(TEST_STATE_ID, { - _id: TEST_STATE_ID, - type: 'state', - common: { - name: 'Test state', - type: 'number', - read: true, - write: false, - role: 'indicator.state', - unit: '%', - def: 0, - desc: 'test state' + objects.setObject( + TEST_STATE_ID, + { + _id: TEST_STATE_ID, + type: 'state', + common: { + name: 'Test state', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'test state', + }, + native: {}, }, - native: {} - }, () => { - states.setState(TEST_STATE_ID, {val: 0, ack: true}, cb && cb); - }); + () => { + states.setState(TEST_STATE_ID, { val: 0, ack: true }, cb && cb); + }, + ); } describe('Test RESTful API as Owner-User', function () { @@ -70,7 +74,7 @@ describe('Test RESTful API as Owner-User', function () { setup.startController((_objects, _states) => { objects = _objects; - states = _states; + states = _states; // give some time to start server setTimeout(() => createTestState(() => _done()), 2000); }); @@ -83,116 +87,129 @@ describe('Test RESTful API as Owner-User', function () { expect(res).not.to.be.equal('Cannot check connection'); - objects.setObject('system.group.writer', { - "common": { - "name": "Writer", - "desc": "", - "members": [ - "system.user.myuser" - ], - "acl": { - "object": { - "list": true, - "read": true, - "write": false, - "delete": false - }, - "state": { - "list": false, - "read": true, - "write": true, - "create": false, - "delete": false - }, - "users": { - "write": false, - "create": false, - "delete": false - }, - "other": { - "execute": false, - "http": false, - "sendto": false - }, - "file": { - "list": false, - "read": false, - "write": false, - "create": false, - "delete": false - } - } - }, - "native": {}, - "acl": { - "object": 1638, - "owner": "system.user.admin", - "ownerGroup": "system.group.administrator" - }, - "_id": "system.group.writer", - "type": "group" - }, err => { - expect(err).to.be.null; - - objects.setObject('system.user.myuser', { - "type": "user", - "common": { - "name": "myuser", - "enabled": true, - "groups": [], - "password": "pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585" + objects.setObject( + 'system.group.writer', + { + common: { + name: 'Writer', + desc: '', + members: ['system.user.myuser'], + acl: { + object: { + list: true, + read: true, + write: false, + delete: false, + }, + state: { + list: false, + read: true, + write: true, + create: false, + delete: false, + }, + users: { + write: false, + create: false, + delete: false, + }, + other: { + execute: false, + http: false, + sendto: false, + }, + file: { + list: false, + read: false, + write: false, + create: false, + delete: false, + }, + }, }, - "_id": "system.user.myuser", - "native": {}, - "acl": { - "object": 1638 - } - }, err => { + native: {}, + acl: { + object: 1638, + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', + }, + _id: 'system.group.writer', + type: 'group', + }, + err => { expect(err).to.be.null; - objects.setObject('javascript.0.test', { - common: { - name: 'test', - type: 'number', - role: 'level', - min: -100, - max: 100, - def: 1 - }, - native: { + + objects.setObject( + 'system.user.myuser', + { + type: 'user', + common: { + name: 'myuser', + enabled: true, + groups: [], + password: + 'pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585', + }, + _id: 'system.user.myuser', + native: {}, + acl: { + object: 1638, + }, }, - type: 'state', - acl: { - object: 1638, - owner: "system.user.myuser", - ownerGroup:"system.group.administrator", - state: 1638 - } - }, err => { - expect(err).to.be.null; - states.setState('javascript.0.test',1, err => { - console.log('END javascript.0.test ' + err); + err => { expect(err).to.be.null; - objects.setObject('javascript.0.test-number', { - common: { - name: 'test', - type: 'number', - role: 'value', - def: 0 - }, - native: { + objects.setObject( + 'javascript.0.test', + { + common: { + name: 'test', + type: 'number', + role: 'level', + min: -100, + max: 100, + def: 1, + }, + native: {}, + type: 'state', + acl: { + object: 1638, + owner: 'system.user.myuser', + ownerGroup: 'system.group.administrator', + state: 1638, + }, }, - type: 'state' - }, err => { - expect(err).to.be.null; - states.setState('javascript.0.test-number', 0, err => { + err => { expect(err).to.be.null; - done(); - }); - }); - }); - }); - }); - }); + states.setState('javascript.0.test', 1, err => { + console.log('END javascript.0.test ' + err); + expect(err).to.be.null; + objects.setObject( + 'javascript.0.test-number', + { + common: { + name: 'test', + type: 'number', + role: 'value', + def: 0, + }, + native: {}, + type: 'state', + }, + err => { + expect(err).to.be.null; + states.setState('javascript.0.test-number', 0, err => { + expect(err).to.be.null; + done(); + }); + }, + ); + }); + }, + ); + }, + ); + }, + ); }); }).timeout(60000); @@ -208,12 +225,15 @@ describe('Test RESTful API as Owner-User', function () { }); it('Test RESTful API as Owner-User: getPlainValue - must return plain value', done => { - request('http://127.0.0.1:' + PORT + '/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(body).to.be.equal('error: permissionError'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + 'http://127.0.0.1:' + PORT + '/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); it('Test RESTful API as Owner-User: getPlainValue 4 Test-Endpoint - must return plain value', done => { @@ -246,24 +266,29 @@ describe('Test RESTful API as Owner-User', function () { }); it('Test RESTful API as Owner-User: set - must set value', done => { - request('http://127.0.0.1:' + PORT + '/set/system.adapter.simple-api.0.alive?val=false', (error, response, body) => { - console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + 'http://127.0.0.1:' + PORT + '/set/system.adapter.simple-api.0.alive?val=false', + (error, response, body) => { + console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); it('Test RESTful API as Owner-User: set - must set val', done => { - request('http://127.0.0.1:' + PORT + '/set/system.adapter.simple-api.0.alive?val=true', (error, response, body) => { - console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + 'http://127.0.0.1:' + PORT + '/set/system.adapter.simple-api.0.alive?val=true', + (error, response, body) => { + console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); - it('Test RESTful API as Owner-User: objects - must return objects', done => { request('http://127.0.0.1:' + PORT + '/objects?pattern=system.adapter.*', (error, response, body) => { //console.log('objects?pattern=system.adapter.* => ' + body); @@ -274,13 +299,16 @@ describe('Test RESTful API as Owner-User', function () { }); it('Test RESTful API as Owner-User: objects - must return objects', done => { - request('http://127.0.0.1:' + PORT + '/objects?pattern=system.adapter.*&type=instance', (error, response, body) => { - //console.log('objects?pattern=system.adapter.* => ' + body); - //expect(body).to.be.equal('error: permissionError'); - expect(!!body).to.be.true; - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:' + PORT + '/objects?pattern=system.adapter.*&type=instance', + (error, response, body) => { + //console.log('objects?pattern=system.adapter.* => ' + body); + //expect(body).to.be.equal('error: permissionError'); + expect(!!body).to.be.true; + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); it('Test RESTful API as Owner-User: states - must return states', done => { @@ -293,16 +321,19 @@ describe('Test RESTful API as Owner-User', function () { }); it('Test RESTful API as Owner-User: setValueFromBody(POST) - must set one value', done => { - request({ - uri: `http://127.0.0.1:${PORT}/setValueFromBody/${TEST_STATE_ID}`, - method: 'POST', - body: '55' - }, (error, response, body) => { - console.log(`setValueFromBody/?${TEST_STATE_ID} => ${JSON.stringify(body)}`); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + { + uri: `http://127.0.0.1:${PORT}/setValueFromBody/${TEST_STATE_ID}`, + method: 'POST', + body: '55', + }, + (error, response, body) => { + console.log(`setValueFromBody/?${TEST_STATE_ID} => ${JSON.stringify(body)}`); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); after('Test RESTful API as Owner-User: Stop js-controller', function (done) { diff --git a/test/testApiAsUser.js b/test/testApiAsUser.js index 059eb81..b1b4a4a 100644 --- a/test/testApiAsUser.js +++ b/test/testApiAsUser.js @@ -3,12 +3,12 @@ /* jslint node: true */ /* jshint expr: true*/ -const expect = require('chai').expect; -const setup = require('./lib/setup'); +const expect = require('chai').expect; +const setup = require('./lib/setup'); const request = require('request'); let objects = null; -let states = null; +let states = null; process.env.NO_PROXY = '127.0.0.1'; const TEST_STATE_ID = 'simple-api.0.testNumber'; @@ -34,23 +34,27 @@ function checkConnectionOfAdapter(cb, counter) { } function createTestState(cb) { - objects.setObject(TEST_STATE_ID, { - _id: TEST_STATE_ID, - type: 'state', - common: { - name: 'Test state', - type: 'number', - read: true, - write: false, - role: 'indicator.state', - unit: '%', - def: 0, - desc: 'test state' + objects.setObject( + TEST_STATE_ID, + { + _id: TEST_STATE_ID, + type: 'state', + common: { + name: 'Test state', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'test state', + }, + native: {}, }, - native: {} - }, () => { - states.setState(TEST_STATE_ID, {val: 0, ack: true}, cb && cb); - }); + () => { + states.setState(TEST_STATE_ID, { val: 0, ack: true }, cb && cb); + }, + ); } describe('Test RESTful API as User', function () { @@ -69,7 +73,7 @@ describe('Test RESTful API as User', function () { setup.startController(function (_objects, _states) { objects = _objects; - states = _states; + states = _states; // give some time to start server setTimeout(() => createTestState(() => _done()), 2000); }); @@ -80,117 +84,130 @@ describe('Test RESTful API as User', function () { checkConnectionOfAdapter(function (res) { if (res) console.log(res); expect(res).not.to.be.equal('Cannot check connection'); - objects.setObject('system.group.writer', { - "common": { - "name": "Writer", - "desc": "", - "members": [ - "system.user.myuser" - ], - "acl": { - "object": { - "list": true, - "read": true, - "write": false, - "delete": false - }, - "state": { - "list": true, - "read": true, - "write": true, - "create": false, - "delete": false - }, - "users": { - "write": false, - "create": false, - "delete": false - }, - "other": { - "execute": false, - "http": false, - "sendto": false - }, - "file": { - "list": false, - "read": false, - "write": false, - "create": false, - "delete": false - } - } - }, - "native": {}, - "acl": { - "object": 1638, - "owner": "system.user.admin", - "ownerGroup": "system.group.administrator" - }, - "_id": "system.group.writer", - "type": "group" - }, function (err) { - expect(err).to.be.null; - - objects.setObject('system.user.myuser', { - "type": "user", - "common": { - "name": "myuser", - "enabled": true, - "groups": [], - "password": "pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585" + objects.setObject( + 'system.group.writer', + { + common: { + name: 'Writer', + desc: '', + members: ['system.user.myuser'], + acl: { + object: { + list: true, + read: true, + write: false, + delete: false, + }, + state: { + list: true, + read: true, + write: true, + create: false, + delete: false, + }, + users: { + write: false, + create: false, + delete: false, + }, + other: { + execute: false, + http: false, + sendto: false, + }, + file: { + list: false, + read: false, + write: false, + create: false, + delete: false, + }, + }, + }, + native: {}, + acl: { + object: 1638, + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', }, - "_id": "system.user.myuser", - "native": {}, - "acl": { - "object": 1638 - } - }, function (err) { + _id: 'system.group.writer', + type: 'group', + }, + function (err) { expect(err).to.be.null; - objects.setObject('javascript.0.test', { - common: { - name: 'test', - type: 'number', - role: 'level', - min: -100, - max: 100, - def: 1 - }, - native: { + + objects.setObject( + 'system.user.myuser', + { + type: 'user', + common: { + name: 'myuser', + enabled: true, + groups: [], + password: + 'pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585', + }, + _id: 'system.user.myuser', + native: {}, + acl: { + object: 1638, + }, }, - type: 'state', - acl: { - object: 1638, - owner: "system.user.myuser", - ownerGroup:"system.group.administrator", - state: 1638 - } - }, function (err) { - expect(err).to.be.null; - states.setState('javascript.0.test',1, function(err) { + function (err) { expect(err).to.be.null; - objects.setObject('javascript.0.test-number', { - common: { - name: 'test', - type: 'number', - role: 'value', - def: 0 - }, - native: { + objects.setObject( + 'javascript.0.test', + { + common: { + name: 'test', + type: 'number', + role: 'level', + min: -100, + max: 100, + def: 1, + }, + native: {}, + type: 'state', + acl: { + object: 1638, + owner: 'system.user.myuser', + ownerGroup: 'system.group.administrator', + state: 1638, + }, }, - type: 'state' - }, err => { - expect(err).to.be.null; - states.setState('javascript.0.test-number', 0, err => { + function (err) { expect(err).to.be.null; - done(); - }); - }); - }); - }); - }); - }); + states.setState('javascript.0.test', 1, function (err) { + expect(err).to.be.null; + objects.setObject( + 'javascript.0.test-number', + { + common: { + name: 'test', + type: 'number', + role: 'value', + def: 0, + }, + native: {}, + type: 'state', + }, + err => { + expect(err).to.be.null; + states.setState('javascript.0.test-number', 0, err => { + expect(err).to.be.null; + done(); + }); + }, + ); + }); + }, + ); + }, + ); + }, + ); }); - }).timeout(60000);; + }).timeout(60000); it('Test RESTful API as User: get - must return value', done => { request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', (error, response, body) => { @@ -221,12 +238,12 @@ describe('Test RESTful API as User', function () { expect(obj.ack).to.be.true; expect(obj.ts).to.be.ok; //expect(obj.from).to.equal("system.adapter.simple-api.0"); - expect(obj.type).to.equal("state"); - expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); + expect(obj.type).to.equal('state'); + expect(obj._id).to.equal('system.adapter.simple-api.0.alive'); expect(obj.common).to.be.ok; expect(obj.native).to.be.ok; - expect(obj.common.name).to.equal("simple-api.0 alive"); - expect(obj.common.role).to.equal("indicator.state"); + expect(obj.common.name).to.equal('simple-api.0 alive'); + expect(obj.common.role).to.equal('indicator.state'); expect(response.statusCode).to.equal(200); done(); }); @@ -280,13 +297,16 @@ describe('Test RESTful API as User', function () { expect(obj.val).to.be.false; expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('false'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -299,13 +319,16 @@ describe('Test RESTful API as User', function () { expect(obj.val).to.be.true; expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('true'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -319,13 +342,16 @@ describe('Test RESTful API as User', function () { expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('false'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -339,13 +365,16 @@ describe('Test RESTful API as User', function () { expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(body).equal('true'); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); }); @@ -389,31 +418,42 @@ describe('Test RESTful API as User', function () { }); it('Test RESTful API as User: setBulk - must set values', done => { - request(`http://127.0.0.1:18183/setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3`, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3 => ` + body); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(50); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(obj[2].val).to.be.equal(3); - expect(obj[2].id).to.equal('javascript.0.test'); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive&javascript.0.test => ` + body); + request( + `http://127.0.0.1:18183/setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3`, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3 => ` + + body, + ); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(50); - expect(obj[1].val).equal(false); - expect(obj[2].val).equal(3); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal(3); + expect(obj[2].id).to.equal('javascript.0.test'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, + (error, response, body) => { + console.log( + `getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive&javascript.0.test => ` + body, + ); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(obj[2].val).equal(3); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API as User: objects - must return objects', done => { @@ -444,91 +484,113 @@ describe('Test RESTful API as User', function () { }); it('Test RESTful API as User: setBulk(POST) - must set values', done => { - request({ - uri: 'http://127.0.0.1:18183/setBulk', - method: 'POST', - body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4` - }, function(error, response, body) { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4 => ` + JSON.stringify(body)); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(50); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(obj[2].val).to.be.equal(4); - expect(obj[2].id).to.equal('javascript.0.test'); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test => ` + body); + request( + { + uri: 'http://127.0.0.1:18183/setBulk', + method: 'POST', + body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4`, + }, + function (error, response, body) { + console.log( + `setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4 => ` + + JSON.stringify(body), + ); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(50); - expect(obj[1].val).equal(false); - expect(obj[2].val).equal(4); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal(4); + expect(obj[2].id).to.equal('javascript.0.test'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, + (error, response, body) => { + console.log( + `getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test => ` + body, + ); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(obj[2].val).equal(4); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API as User: setBulk(POST-GET-Mix) - must set values', done => { - request({ - uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, - method: 'POST', - body: '' - }, function(error, response, body) { - console.log(`setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body)); - expect(error).to.be.not.ok; - expect(response.statusCode).to.equal(200); - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(51); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + request( + { + uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, + method: 'POST', + body: '', + }, + function (error, response, body) { + console.log( + `setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body), + ); expect(error).to.be.not.ok; + expect(response.statusCode).to.equal(200); + const obj = JSON.parse(body); - expect(obj[0].val).equal(51); - expect(obj[1].val).equal(false); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(51); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(51); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API as User: setValueFromBody(POST) - must set one value', done => { - request({ - uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, - method: 'POST', - body: '55' - }, (error, response, body) => { - console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(55); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(response.statusCode).to.equal(200); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID} => ` + body); + request( + { + uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, + method: 'POST', + body: '55', + }, + (error, response, body) => { + console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(55); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal(TEST_STATE_ID); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID} => ` + body); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }); + }, + ); }); after('Test RESTful API as User: Stop js-controller', function (done) { diff --git a/test/testApiAsUserNoRights.js b/test/testApiAsUserNoRights.js index bcea252..055b318 100644 --- a/test/testApiAsUserNoRights.js +++ b/test/testApiAsUserNoRights.js @@ -1,19 +1,19 @@ -/* jshint -W097 */// jshint strict:false +/* jshint -W097 */ // jshint strict:false /*jslint node: true */ /*jshint expr: true*/ -const expect = require('chai').expect; -const setup = require(__dirname + '/lib/setup'); +const expect = require('chai').expect; +const setup = require(`${__dirname}/lib/setup`); const request = require('request'); let objects = null; -let states = null; +let states = null; const TEST_STATE_ID = 'simple-api.0.testNumber'; process.env.NO_PROXY = '127.0.0.1'; function checkConnectionOfAdapter(cb, counter) { counter = counter || 0; - console.log('Try check #' + counter); + console.log(`Try check #${counter}`); if (counter > 30) { if (cb) cb('Cannot check connection'); return; @@ -32,26 +32,30 @@ function checkConnectionOfAdapter(cb, counter) { } function createTestState(cb) { - objects.setObject(TEST_STATE_ID, { - _id: TEST_STATE_ID, - type: 'state', - common: { - name: 'Test state', - type: 'number', - read: true, - write: false, - role: 'indicator.state', - unit: '%', - def: 0, - desc: 'test state' + objects.setObject( + TEST_STATE_ID, + { + _id: TEST_STATE_ID, + type: 'state', + common: { + name: 'Test state', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'test state', + }, + native: {}, }, - native: {} - }, () => { - states.setState(TEST_STATE_ID, {val: 0, ack: true}, cb && cb); - }); + () => { + states.setState(TEST_STATE_ID, { val: 0, ack: true }, cb && cb); + }, + ); } -describe('Test RESTful API as User(No rights)', function() { +describe('Test RESTful API as User(No rights)', function () { before('Test RESTful API as User:(No rights) Start js-controller', function (_done) { this.timeout(600000); // because of first install from npm setup.adapterStarted = false; @@ -67,7 +71,7 @@ describe('Test RESTful API as User(No rights)', function() { setup.startController(function (_objects, _states) { objects = _objects; - states = _states; + states = _states; // give some time to start server setTimeout(() => createTestState(() => _done()), 2000); }); @@ -79,99 +83,109 @@ describe('Test RESTful API as User(No rights)', function() { checkConnectionOfAdapter(function (res) { if (res) console.log(res); expect(res).not.to.be.equal('Cannot check connection'); - objects.setObject('system.group.norights', { - "common": { - "name": "norights", - "desc": "", - "members": [ - "system.user.norights" - ], - "acl": { - "object": { - "list": false, - "read": false, - "write": false, - "delete": false - }, - "state": { - "list": true, - "read": true, - "write": true, - "create": false, - "delete": false - }, - "users": { - "write": false, - "create": false, - "delete": false - }, - "other": { - "execute": false, - "http": false, - "sendto": false - }, - "file": { - "list": false, - "read": false, - "write": false, - "create": false, - "delete": false - } - } - }, - "native": {}, - "acl": { - "object": 1638, - "owner": "system.user.admin", - "ownerGroup": "system.group.administrator" - }, - "_id": "system.group.norights", - "type": "group" - }, function (err) { - expect(err).to.be.null; - - objects.setObject('system.user.norights', { - "type": "user", - "common": { - "name": "norights", - "enabled": true, - "groups": [], - "password": "pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585" + objects.setObject( + 'system.group.norights', + { + common: { + name: 'norights', + desc: '', + members: ['system.user.norights'], + acl: { + object: { + list: false, + read: false, + write: false, + delete: false, + }, + state: { + list: true, + read: true, + write: true, + create: false, + delete: false, + }, + users: { + write: false, + create: false, + delete: false, + }, + other: { + execute: false, + http: false, + sendto: false, + }, + file: { + list: false, + read: false, + write: false, + create: false, + delete: false, + }, + }, + }, + native: {}, + acl: { + object: 1638, + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', }, - "_id": "system.user.norights", - "native": {}, - "acl": { - "object": 1638 - } - }, function (err) { + _id: 'system.group.norights', + type: 'group', + }, + function (err) { expect(err).to.be.null; - objects.setObject('javascript.0.test', { - common: { - name: 'test', - type: 'number', - role: 'level', - min: -100, - max: 100, - def: 1 - }, - native: { + + objects.setObject( + 'system.user.norights', + { + type: 'user', + common: { + name: 'norights', + enabled: true, + groups: [], + password: + 'pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585', + }, + _id: 'system.user.norights', + native: {}, + acl: { + object: 1638, + }, }, - type: 'state', - acl: { - object: 1638, - owner: "system.user.norights", - ownerGroup:"system.group.administrator", - state: 1638 - } - }, function (err) { - expect(err).to.be.null; - states.setState('javascript.0.test',1, function(err) { + function (err) { expect(err).to.be.null; - done(); - }); - }); - }); - }); + objects.setObject( + 'javascript.0.test', + { + common: { + name: 'test', + type: 'number', + role: 'level', + min: -100, + max: 100, + def: 1, + }, + native: {}, + type: 'state', + acl: { + object: 1638, + owner: 'system.user.norights', + ownerGroup: 'system.group.administrator', + state: 1638, + }, + }, + function (err) { + expect(err).to.be.null; + states.setState('javascript.0.test', 1, function (err) { + expect(err).to.be.null; + done(); + }); + }, + ); + }, + ); + }, + ); }); }); @@ -221,12 +235,15 @@ describe('Test RESTful API as User(No rights)', function() { console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); expect(body).to.be.equal('{"error":"permissionError"}'); expect(response.statusCode).to.equal(401); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(body).to.be.equal('error: permissionError'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); }); @@ -235,12 +252,15 @@ describe('Test RESTful API as User(No rights)', function() { console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); expect(body).to.be.equal('{"error":"permissionError"}'); expect(response.statusCode).to.equal(401); - request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', (error, response, body) => { - console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); - expect(body).to.be.equal('error: permissionError'); - expect(response.statusCode).to.equal(401); - done(); - }); + request( + 'http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', + (error, response, body) => { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); }); }); @@ -272,66 +292,86 @@ describe('Test RESTful API as User(No rights)', function() { }); it('Test RESTful API as User:(No rights) setBulk(POST) - must set values', done => { - request({ - uri: 'http://127.0.0.1:18183/setBulk', - method: 'POST', - body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4` - }, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4 => ${JSON.stringify(body)}`); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test => ${body}`); + request( + { + uri: 'http://127.0.0.1:18183/setBulk', + method: 'POST', + body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4`, + }, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4 => ${JSON.stringify(body)}`, + ); expect(body).to.be.equal('{"error":"permissionError"}'); expect(response.statusCode).to.equal(401); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test`, + (error, response, body) => { + console.log( + `getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive,javascript.0.test => ${body}`, + ); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); + }, + ); }); it('Test RESTful API as User:(No rights) setBulk(POST-GET-Mix) - must set values', done => { - - request({ - uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, - method: 'POST', - body: '' - }, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body)); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + request( + { + uri: `http://127.0.0.1:18183/setBulk?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false`, + method: 'POST', + body: '', + }, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=51&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body), + ); expect(body).to.be.equal('{"error":"permissionError"}'); expect(response.statusCode).to.equal(401); - done(); - }); - }); + + request( + `http://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }, + ); + }, + ); }); it('Test RESTful API as User:(No rights) setValueFromBody(POST) - must set one value', done => { - request({ - uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, - method: 'POST', - body: '55' - }, (error, response, body) => { - console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); - expect(body).to.be.equal('{"error":"permissionError"}'); - expect(response.statusCode).to.equal(401); - - request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID} => ` + body); + request( + { + uri: `http://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}`, + method: 'POST', + body: '55', + }, + (error, response, body) => { + console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); expect(body).to.be.equal('{"error":"permissionError"}'); expect(response.statusCode).to.equal(401); - done(); - }); - }); + + request(`http://127.0.0.1:18183/getBulk/${TEST_STATE_ID}`, (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID} => ` + body); + expect(body).to.be.equal('{"error":"permissionError"}'); + expect(response.statusCode).to.equal(401); + done(); + }); + }, + ); }); after('Test RESTful API as User:(No rights) Stop js-controller', function (done) { this.timeout(9000); - setup.stopController((normalTerminated) => { + setup.stopController(normalTerminated => { console.log('Adapter normal terminated: ' + normalTerminated); setTimeout(done, 3000); }); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js index ae1fcce..415f519 100644 --- a/test/testPackageFiles.js +++ b/test/testPackageFiles.js @@ -1,5 +1,5 @@ -const path = require('path'); +const path = require('node:path'); const { tests } = require('@iobroker/testing'); // Validate the package files -tests.packageFiles(path.join(__dirname, '..')); \ No newline at end of file +tests.packageFiles(path.join(__dirname, '..')); diff --git a/test/testSsl.js b/test/testSsl.js index 37f6ecc..6600b0d 100644 --- a/test/testSsl.js +++ b/test/testSsl.js @@ -3,12 +3,12 @@ /* jslint node: true */ /* jshint expr: true*/ -const expect = require('chai').expect; -const setup = require('./lib/setup'); +const expect = require('chai').expect; +const setup = require('./lib/setup'); const request = require('request'); let objects = null; -let states = null; +let states = null; process.env.NO_PROXY = '127.0.0.1'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -33,23 +33,27 @@ function checkConnectionOfAdapter(cb, counter) { } function createTestState(cb) { - objects.setObject(TEST_STATE_ID, { - _id: TEST_STATE_ID, - type: 'state', - common: { - name: 'Test state', - type: 'number', - read: true, - write: false, - role: 'indicator.state', - unit: '%', - def: 0, - desc: 'test state' + objects.setObject( + TEST_STATE_ID, + { + _id: TEST_STATE_ID, + type: 'state', + common: { + name: 'Test state', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'test state', + }, + native: {}, }, - native: {} - }, () => { - states.setState(TEST_STATE_ID, {val: 0, ack: true}, cb && cb); - }); + () => { + states.setState(TEST_STATE_ID, { val: 0, ack: true }, cb && cb); + }, + ); } describe('Test RESTful API SSL', function () { @@ -57,7 +61,7 @@ describe('Test RESTful API SSL', function () { this.timeout(600000); // because of first install from npm setup.adapterStarted = false; - const brokerStarted = false; + const brokerStarted = false; setup.setupController(async () => { const config = await setup.getAdapterConfig(); // enable adapter @@ -73,7 +77,7 @@ describe('Test RESTful API SSL', function () { setup.startController((_objects, _states) => { objects = _objects; - states = _states; + states = _states; // give some time to start server setTimeout(() => createTestState(() => _done()), 2000); }); @@ -86,73 +90,82 @@ describe('Test RESTful API SSL', function () { expect(res).not.to.be.equal('Cannot check connection'); - objects.setObject('javascript.0.test-number', { - common: { - name: 'test', - type: 'number', - role: 'value', - def: 0 - }, - native: { + objects.setObject( + 'javascript.0.test-number', + { + common: { + name: 'test', + type: 'number', + role: 'value', + def: 0, + }, + native: {}, + type: 'state', }, - type: 'state' - }, err => { - expect(err).to.be.null; - states.setState('javascript.0.test-number', 0, err => { + err => { expect(err).to.be.null; - done(); - }); - }); + states.setState('javascript.0.test-number', 0, err => { + expect(err).to.be.null; + done(); + }); + }, + ); }); }).timeout(60000); it('Test RESTful API SSL: get - must return value', done => { - request('https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=iobroker', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - const obj = JSON.parse(body); - //{ - // "val" : true, - // "ack" : true, - // "ts" : 1455009717, - // "q" : 0, - // "from" : "system.adapter.simple-api.0", - // "lc" : 1455009717, - // "expire" : 30000, - // "_id" : "system.adapter.simple-api.0.alive", - // "type" : "state", - // "common" : { - // "name" : "simple-api.0.alive", - // "type" : "boolean", - // "role" : "indicator.state" - // }, - // "native" : {} - // - //} - - expect(obj).to.be.ok; - expect(obj.val).to.be.true; - expect(obj.ack).to.be.true; - expect(obj.ts).to.be.ok; - //expect(obj.from).to.equal("system.adapter.simple-api.0"); - expect(obj.type).to.equal("state"); - expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); - expect(obj.common).to.be.ok; - expect(obj.native).to.be.ok; - expect(obj.common.name).to.equal("simple-api.0 alive"); - expect(obj.common.role).to.equal("indicator.state"); - expect(response.statusCode).to.equal(200); - done(); - }); + request( + 'https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=iobroker', + (error, response, body) => { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + //{ + // "val" : true, + // "ack" : true, + // "ts" : 1455009717, + // "q" : 0, + // "from" : "system.adapter.simple-api.0", + // "lc" : 1455009717, + // "expire" : 30000, + // "_id" : "system.adapter.simple-api.0.alive", + // "type" : "state", + // "common" : { + // "name" : "simple-api.0.alive", + // "type" : "boolean", + // "role" : "indicator.state" + // }, + // "native" : {} + // + //} + + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.ack).to.be.true; + expect(obj.ts).to.be.ok; + //expect(obj.from).to.equal("system.adapter.simple-api.0"); + expect(obj.type).to.equal('state'); + expect(obj._id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj.common).to.be.ok; + expect(obj.native).to.be.ok; + expect(obj.common.name).to.equal('simple-api.0 alive'); + expect(obj.common.role).to.equal('indicator.state'); + expect(response.statusCode).to.equal(200); + done(); + }, + ); }); it('Test RESTful API SSL: get with no credentials', done => { - request('https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=io', (error, response, body) => { - console.log('get/system.adapter.simple-api.0.alive => ' + body); - expect(error).to.be.not.ok; - expect(response.statusCode).to.be.equal(401); - done(); - }); + request( + 'https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=io', + (error, response, body) => { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(response.statusCode).to.be.equal(401); + done(); + }, + ); }); it('Test RESTful API SSL: get with wrong credentials', done => { @@ -165,61 +178,74 @@ describe('Test RESTful API SSL', function () { }); it('Test RESTful API SSL: setBulk(POST) - must set values', done => { - - request({ - uri: 'https://127.0.0.1:18183/setBulk?user=admin&pass=iobroker', - method: 'POST', - body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false` - }, (error, response, body) => { - console.log(`setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body)); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(50); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(obj[1].val).to.be.equal(false); - expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); - expect(response.statusCode).to.equal(200); - - request(`https://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive?user=admin&pass=iobroker`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + request( + { + uri: 'https://127.0.0.1:18183/setBulk?user=admin&pass=iobroker', + method: 'POST', + body: `${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false`, + }, + (error, response, body) => { + console.log( + `setBulk/?${TEST_STATE_ID}=50&system.adapter.simple-api.0.alive=false => ` + JSON.stringify(body), + ); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(50); - expect(obj[1].val).equal(false); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal(TEST_STATE_ID); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); expect(response.statusCode).to.equal(200); - done(); - }); - }); + + request( + `https://127.0.0.1:18183/getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive?user=admin&pass=iobroker`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID},system.adapter.simple-api.0.alive => ` + body); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); }); it('Test RESTful API SSL: setValueFromBody(POST) - must set values', done => { - request({ - uri: `https://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}?user=admin&pass=iobroker&`, - method: 'POST', - body: '55' - }, (error, response, body) => { - console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); - expect(error).to.be.not.ok; - - const obj = JSON.parse(body); - expect(obj).to.be.ok; - expect(obj[0].val).to.be.equal(55); - expect(obj[0].id).to.equal(TEST_STATE_ID); - expect(response.statusCode).to.equal(200); - - body = ''; - request(`https://127.0.0.1:18183/getBulk/${TEST_STATE_ID}?user=admin&pass=iobroker`, (error, response, body) => { - console.log(`getBulk/${TEST_STATE_ID} => ` + body); + request( + { + uri: `https://127.0.0.1:18183/setValueFromBody/${TEST_STATE_ID}?user=admin&pass=iobroker&`, + method: 'POST', + body: '55', + }, + (error, response, body) => { + console.log(`setValueFromBody/?${TEST_STATE_ID} => ` + JSON.stringify(body)); expect(error).to.be.not.ok; + const obj = JSON.parse(body); - expect(obj[0].val).equal(55); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal(TEST_STATE_ID); expect(response.statusCode).to.equal(200); - done(); - }); - }); -}); + + body = ''; + request( + `https://127.0.0.1:18183/getBulk/${TEST_STATE_ID}?user=admin&pass=iobroker`, + (error, response, body) => { + console.log(`getBulk/${TEST_STATE_ID} => ` + body); + expect(error).to.be.not.ok; + const obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }, + ); + }, + ); + }); after('Test RESTful API SSL: Stop js-controller', function (done) { this.timeout(9000); setup.stopController(normalTerminated => { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..66c7731 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "noEmit": false, + }, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..44462ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +// Root tsconfig to set the settings and power editor support for all TS files +{ + "compileOnSave": true, + "compilerOptions": { + // do not compile anything; this file is just to configure type checking + // the compilation is configured in tsconfig.build.json + "noEmit": true, + // check JS files, but do not compile them => tsconfig.build.json + "allowJs": true, + "checkJs": true, + "skipLibCheck": true, // Don't report errors in 3rd party definitions + "noEmitOnError": true, + "declaration": true, + "outDir": "./dist/", + "removeComments": false, + "module": "Node16", + "moduleResolution": "node16", + "esModuleInterop": true, + // this is necessary for the automatic typing of the adapter config + "resolveJsonModule": true, + "strict": true, + "target": "es2022", + "sourceMap": true, + "inlineSourceMap": false, + "useUnknownInCatchVariables": false, + "types": ["@iobroker/types", "node"] + }, + "include": ["src/**/*.ts", "src/*.d.ts"], + "exclude": ["dist/**", "node_modules/**", "eslint.config.mjs", "www/**/*"] +}