├── .DS_Store ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BugReport.md │ ├── Enhancement.md │ └── bug_report.md ├── auto-merge.yml ├── dependabot.yml └── workflows │ ├── dependabot-auto-merge.yml │ └── test-and-release.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── admin ├── .DS_Store ├── Mbit.png ├── Mbyte.png ├── admin.d.ts ├── button.png ├── states.png ├── tsconfig.json ├── web-speedy-original.png ├── web-speedy.png └── words.js ├── gulpfile.js ├── io-package.json ├── lib ├── adapter-config.d.ts ├── state_attr.js ├── tools.js └── web-speedy.Grafana_Dashboard.json ├── main.js ├── main.test.js ├── package-lock.json ├── package.json ├── test ├── integration.js ├── mocha.custom.opts ├── mocha.setup.js ├── package.js ├── tsconfig.json └── unit.js └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "indent": [ 10 | "error", 11 | "tab", 12 | { 13 | "SwitchCase": 1 14 | } 15 | ], 16 | "no-console": "off", 17 | "no-var": "error", 18 | "prefer-const": "error", 19 | "quotes": [ 20 | "error", 21 | "single", 22 | { 23 | "avoidEscape": true, 24 | "allowTemplateLiterals": true 25 | } 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ] 31 | }, 32 | "parserOptions": { 33 | "ecmaVersion": 2018 34 | } 35 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['http://paypal.me/DutchmanNL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BugReport.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is not working as it should 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'DutchmanNL' 7 | --- 8 | 9 | **!!! Before you start !!!** 10 | 1. Verify if there is not already an issue requesting the same 11 | 2. Is this really a bug of current code, or an enhancement request ? 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '...' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots & Logfiles** 27 | If applicable, add screenshots and logfiles to help explain your problem. 28 | 29 | **Versions:** 30 | - Adapter version: 31 | - JS-Controller version: 32 | - Node version: 33 | - Operating system: 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Request new functionality 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'DutchmanNL' 7 | --- 8 | 9 | **!!! Before you start !!!!** 10 | Verify if there is not already an issue requesting the same Enhancement 11 | 12 | **Describe wanted Enhancement !** 13 | A clear description of the wanted functionality 14 | 15 | **Why should we put effort in it ?** 16 | Please add some additional information why this Enhancement should be integrated 17 | 18 | **Additional context** 19 | Add any other context about the problem here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is not working as it should 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '...' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots & Logfiles** 23 | If applicable, add screenshots and logfiles to help explain your problem. 24 | 25 | **Versions:** 26 | - Adapter version: 27 | - JS-Controller version: 28 | - Node version: 29 | - Operating system: 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configure here which dependency updates should be merged automatically. 2 | # The recommended configuration is the following: 3 | - match: 4 | # Only merge patches for production dependencies 5 | dependency_type: production 6 | update_type: "semver:patch" 7 | - match: 8 | # Except for security fixes, here we allow minor patches 9 | dependency_type: production 10 | update_type: "security:minor" 11 | - match: 12 | # and development dependencies can have a minor update, too 13 | dependency_type: development 14 | update_type: "semver:minor" 15 | 16 | # The syntax is based on the legacy dependabot v1 automerged_updates syntax, see: 17 | # https://dependabot.com/docs/config-file/#automerged_updates 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "dependencyAutoUpdate" 8 | open-pull-requests-limit: 30 9 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Automatically merge Dependabot PRs when version comparison is within the range 2 | # that is configured in .github/auto-merge.yml 3 | 4 | name: Auto-Merge Dependabot PRs 5 | 6 | on: 7 | pull_request_target: 8 | 9 | jobs: 10 | auto-merge: 11 | if: github.actor == 'dependabot[bot]' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 15 | with: 16 | target: minor 17 | github-token: ${{ secrets.AUTOMERGER }} -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | 2 | # Run this job on all pushes and pull requests 3 | # as well as tags with a semantic version 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | # normal versions 10 | - "v[0-9]+.[0-9]+.[0-9]+" 11 | # pre-releases 12 | - "v[0-9]+.[0-9]+.[0-9]+-**" 13 | pull_request: {} 14 | 15 | jobs: 16 | # Performs quick checks before the expensive test runs 17 | check-and-lint: 18 | if: contains(github.event.head_commit.message, '[skip ci]') == false 19 | 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | node-version: [14.x] 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install Dependencies 36 | run: npm ci 37 | 38 | - name: Lint source code 39 | run: npm run lint 40 | - name: Test package files 41 | run: npm run test:package 42 | 43 | # Runs adapter tests on all supported node versions and OSes 44 | adapter-tests: 45 | if: contains(github.event.head_commit.message, '[skip ci]') == false 46 | 47 | needs: [check-and-lint] 48 | 49 | runs-on: ${{ matrix.os }} 50 | strategy: 51 | matrix: 52 | node-version: [12.x, 14.x, 16.x] 53 | os: [ubuntu-latest, windows-latest, macos-latest] 54 | 55 | steps: 56 | - name: Checkout code 57 | uses: actions/checkout@v2 58 | 59 | - name: Use Node.js ${{ matrix.node-version }} 60 | uses: actions/setup-node@v1 61 | with: 62 | node-version: ${{ matrix.node-version }} 63 | 64 | - name: Install Dependencies 65 | run: npm ci 66 | 67 | - name: Run unit tests 68 | run: npm run test:unit 69 | 70 | # - name: Run integration tests (unix only) 71 | # if: startsWith(runner.OS, 'windows') == false 72 | # run: DEBUG=testing:* npm run test:integration 73 | # 74 | # - name: Run integration tests (windows only) 75 | # if: startsWith(runner.OS, 'windows') 76 | # run: set DEBUG=testing:* & npm run test:integration 77 | 78 | # Deploys the final package to NPM 79 | deploy: 80 | needs: [adapter-tests] 81 | 82 | # Trigger this step only when a commit on any branch is tagged with a version number 83 | if: | 84 | contains(github.event.head_commit.message, '[skip ci]') == false && 85 | github.event_name == 'push' && 86 | startsWith(github.ref, 'refs/tags/v') 87 | 88 | runs-on: ubuntu-latest 89 | strategy: 90 | matrix: 91 | node-version: [14.x] 92 | 93 | steps: 94 | - name: Checkout code 95 | uses: actions/checkout@v2 96 | 97 | - name: Use Node.js ${{ matrix.node-version }} 98 | uses: actions/setup-node@v1 99 | with: 100 | node-version: ${{ matrix.node-version }} 101 | 102 | - name: Extract the version and commit body from the tag 103 | id: extract_release 104 | # The body may be multiline, therefore newlines and % need to be escaped 105 | run: | 106 | VERSION="${{ github.ref }}" 107 | VERSION=${VERSION##*/v} 108 | echo "::set-output name=VERSION::$VERSION" 109 | BODY=$(git show -s --format=%b) 110 | BODY="${BODY//'%'/'%25'}" 111 | BODY="${BODY//$'\n'/'%0A'}" 112 | BODY="${BODY//$'\r'/'%0D'}" 113 | echo "::set-output name=BODY::$BODY" 114 | 115 | - name: Publish package to npm 116 | run: | 117 | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 118 | npm whoami 119 | npm publish 120 | 121 | - name: Create Github Release 122 | uses: actions/create-release@v1 123 | env: 124 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 125 | with: 126 | tag_name: ${{ github.ref }} 127 | release_name: Release v${{ steps.extract_release.outputs.VERSION }} 128 | draft: false 129 | # Prerelease versions create prereleases on Github 130 | prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} 131 | body: 132 | 133 | - name: Notify Sentry.io about the release 134 | run: | 135 | npm i -g @sentry/cli 136 | export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 137 | export SENTRY_URL=https://sentry.iobroker.net 138 | export SENTRY_ORG=iobroker 139 | export SENTRY_PROJECT=iobroker-web-speedy 140 | export SENTRY_VERSION=iobroker.web-speedy @${{ env.VERSION }} 141 | sentry-cli releases new $SENTRY_VERSION 142 | sentry-cli releases finalize $SENTRY_VERSION 143 | # Add the following line BEFORE finalize if repositories are connected in Sentry 144 | # sentry-cli releases set-commits $SENTRY_VERSION --auto 145 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | *.code-workspace 4 | node_modules 5 | nbproject 6 | 7 | # npm package files 8 | iobroker.*.tgz 9 | 10 | Thumbs.db 11 | 12 | # i18n intermediate files 13 | admin/i18n/flat.txt 14 | admin/i18n/*/flat.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | node_modules/ 4 | nbproject/ 5 | .vs*/ 6 | *.code-workspace 7 | Thumbs.db 8 | gulpfile.js 9 | 10 | # CI test files 11 | test/ 12 | travis/ 13 | .travis.yml 14 | appveyor.yml 15 | .travis.yaml 16 | appveyor.yaml 17 | 18 | # Type checking configuration 19 | tsconfig.json 20 | tsconfig.*.json 21 | 22 | # ESLint configuration 23 | .eslintrc.json 24 | .eslintrc.js 25 | 26 | # npm package files 27 | iobroker.*.tgz 28 | package-lock.json 29 | 30 | # i18n intermediate files 31 | admin/i18n 32 | 33 | # maintenance scripts 34 | maintenance/** -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | 6 | language: node_js 7 | node_js: 8 | - '8' 9 | - '10' 10 | - '12' 11 | 12 | env: 13 | - CXX=g++-4.8 14 | addons: 15 | apt: 16 | sources: 17 | - ubuntu-toolchain-r-test 18 | packages: 19 | - g++-4.9 20 | 21 | before_install: 22 | - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then CC=gcc-4.9; fi' 23 | - 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then CC=g++-4.9; fi' 24 | before_script: 25 | - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) 26 | - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm; fi' 27 | - npm -v 28 | script: 29 | - npm run test:package 30 | - npm run test:unit 31 | - export DEBUG=testing:* 32 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DutchmanNL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/web-speedy.png) 2 | # ioBroker.web-speedy 3 | 4 | [![NPM version](http://img.shields.io/npm/v/iobroker.web-speedy.svg)](https://www.npmjs.com/package/iobroker.web-speedy) 5 | [![Downloads](https://img.shields.io/npm/dm/iobroker.web-speedy.svg)](https://www.npmjs.com/package/iobroker.web-speedy) 6 | ![Number of Installations (latest)](http://iobroker.live/badges/web-speedy-installed.svg) 7 | ![Number of Installations (stable)](http://iobroker.live/badges/web-speedy-stable.svg) 8 | [![Dependency Status](https://img.shields.io/david/DrozmotiX/iobroker.web-speedy.svg)](https://david-dm.org/DrozmotiX/iobroker.web-speedy) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/DrozmotiX/ioBroker.web-speedy/badge.svg)](https://snyk.io/test/github/DrozmotiX/ioBroker.web-speedy) 10 | 11 | [![NPM](https://nodei.co/npm/iobroker.web-speedy.png?downloads=true)](https://nodei.co/npm/iobroker.web-speedy/) 12 | 13 | **Tests:**: [![Travis-CI](http://img.shields.io/travis/DrozmotiX/ioBroker.web-speedy/master.svg)](https://travis-ci.org/DrozmotiX/ioBroker.web-speedy) 14 | 15 | ## web-speedy adapter for ioBroker 16 | 17 | Web-Speedy enables you to test your internet connection on a regular base and store results in ioBroker ! 18 | 19 | ### How to use this adapter 20 | 21 | At first startup it will retrieve best-servers nearby based on ping results and run the first test. 22 | 23 | Web-Speedy is build in a way all execution is handled automatically, meaning you don't have a configuration page. 24 | However, you still can influance some things (see datapoints): 25 | 26 | | State | Description | 27 | |---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 28 | | test_auto_modus | Server mode to run automatically tests with | 29 | | test_auto_intervall | Intervall time for automated test-execution (default = 30, if set to 0 no automated test will run !) | 30 | | test_best | Run test now on best-server based on last ping results | 31 | | test_by_id | Run test on specific server ID now ! | 32 | | test_by_url | Run test on specific server URL now ! | 33 | | test_duration | The maximum length (in seconds) of a single test run (upload or download) | 34 | | test_specific | Use the dropdown list to choose one of the top 5 servers found in previous scan | 35 | | test_specific_id | Enter specific server ID [Please find a server ID here](https://c.speedtest.net/speedtest-servers-static.php?fbclid=IwAR3mLi2N9mwp1zG4Xu96cn4h1Zql6NG26p6GDjctjMftq0YzKKwPk-wme8A) | 36 | | test_specific_url | Enter specific server URL [Please find a server URL here](https://c.speedtest.net/speedtest-servers-static.php?fbclid=IwAR3mLi2N9mwp1zG4Xu96cn4h1Zql6NG26p6GDjctjMftq0YzKKwPk-wme8A) | 37 | 38 | 39 | ![Mbyte](https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/master/admin/Mbyte.png) 40 | ![Mbit](https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/master/admin/Mbit.png) 41 | ![States](https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/master/admin/states.png) 42 | 43 | ## Support me 44 | If you like my work, please feel free to provide a personal donation 45 | (this is an personal Donate link for DutchmanNL, no relation to the ioBroker Project !) 46 | [![Donate](https://raw.githubusercontent.com/DrozmotiX/ioBroker.wled/master/admin/button.png)](http://paypal.me/DutchmanNL) 47 | 48 | ## Changelog 49 | 50 | ### 0.2.1 bugfix 51 | * (Xenon-s) bugfix: [issue #162](https://github.com/DrozmotiX/ioBroker.web-speedy/issues/162) Warning messages in the log 52 | * (Xenon-s) bugfix: [issue #169](https://github.com/DrozmotiX/ioBroker.web-speedy/issues/169) Warning messages in the log 53 | 54 | ### 0.2.0 Initial release 55 | * (DutchmanNL) Attention : Delete all objects beforer adapter start if previous version installed ! 56 | * (DutchmanNL) Implement [ test_auto_modus ] Server mode to run automatically tests with 57 | * (DutchmanNL) Fix issues with running specific server tests 58 | * (DutchmanNL) Upload speed calculation issue fixed 59 | 60 | ### 0.1.5 New settings possibilities & Code improvements 61 | * (DutchmanNL) Implemented states for progress in % 62 | * (DutchmanNL) No automated scan if test_auto_intervall set zo 0 63 | * (DutchmanNL) Ensure propper running state reset at adapter start 64 | * (DutchmanNL) Improve code performance and avoid multiple running instances 65 | * (DutchmanNL) Implemented adjustable duration time for scan by (increase if you see strange test results, like to 20 secons) 66 | * (DutchmanNL) Implemented state to run test by id or URL at specific server [Please find a server id here](https://c.speedtest.net/speedtest-servers-static.php?fbclid=IwAR3mLi2N9mwp1zG4Xu96cn4h1Zql6NG26p6GDjctjMftq0YzKKwPk-wme8A) 67 | 68 | ### 0.1.1 MegaByte to Megabit calculation and current test speeds implemented 69 | * (DutchmanNL) Fix wrong status "test runnig" 70 | * (DutchmanNL) Implement byte to bit calculation for test - results 71 | * (DutchmanNL) implement current speeds in kb/s during download 72 | 73 | ### 0.1.0 Beta release for public testing 74 | * (DutchmanNL) Beta release for public testing 75 | 76 | ## License 77 | MIT License 78 | 79 | Copyright (c) 2020 DutchmanNL 80 | 81 | Permission is hereby granted, free of charge, to any person obtaining a copy 82 | of this software and associated documentation files (the "Software"), to deal 83 | in the Software without restriction, including without limitation the rights 84 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | copies of the Software, and to permit persons to whom the Software is 86 | furnished to do so, subject to the following conditions: 87 | 88 | The above copyright notice and this permission notice shall be included in all 89 | copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 94 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 96 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 97 | SOFTWARE. 98 | -------------------------------------------------------------------------------- /admin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/.DS_Store -------------------------------------------------------------------------------- /admin/Mbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/Mbit.png -------------------------------------------------------------------------------- /admin/Mbyte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/Mbyte.png -------------------------------------------------------------------------------- /admin/admin.d.ts: -------------------------------------------------------------------------------- 1 | declare let systemDictionary: Record>; 2 | -------------------------------------------------------------------------------- /admin/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/button.png -------------------------------------------------------------------------------- /admin/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/states.png -------------------------------------------------------------------------------- /admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./**/*.d.ts", 5 | "./**/*.js" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /admin/web-speedy-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/web-speedy-original.png -------------------------------------------------------------------------------- /admin/web-speedy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/d9e4f2ee05759d3ee681f7ddd2b1d33770fd91c3/admin/web-speedy.png -------------------------------------------------------------------------------- /admin/words.js: -------------------------------------------------------------------------------- 1 | /*global systemDictionary:true */ 2 | 'use strict'; 3 | 4 | systemDictionary = { 5 | 'web-speedy adapter settings': { 6 | 'en': 'Adapter settings for web-speedy', 7 | 'de': 'Adaptereinstellungen für web-speedy', 8 | 'ru': 'Настройки адаптера для web-speedy', 9 | 'pt': 'Configurações do adaptador para web-speedy', 10 | 'nl': 'Adapterinstellingen voor web-speedy', 11 | 'fr': "Paramètres d'adaptateur pour web-speedy", 12 | 'it': "Impostazioni dell'adattatore per web-speedy", 13 | 'es': 'Ajustes del adaptador para web-speedy', 14 | 'pl': 'Ustawienia adaptera dla web-speedy', 15 | 'zh-cn': 'web-speedy的适配器设置' 16 | }, 17 | 'option1': { 18 | 'en': 'option1', 19 | 'de': 'Option 1', 20 | 'ru': 'Опция 1', 21 | 'pt': 'Opção 1', 22 | 'nl': 'Optie 1', 23 | 'fr': 'Option 1', 24 | 'it': 'opzione 1', 25 | 'es': 'Opción 1', 26 | 'pl': 'opcja 1', 27 | 'zh-cn': '选项1' 28 | }, 29 | 'option2': { 30 | 'en': 'option2', 31 | 'de': 'Option 2', 32 | 'ru': 'option2', 33 | 'pt': 'opção 2', 34 | 'nl': 'Optie 2', 35 | 'fr': 'Option 2', 36 | 'it': 'opzione 2', 37 | 'es': 'opcion 2', 38 | 'pl': 'Opcja 2', 39 | 'zh-cn': '选项2' 40 | } 41 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ioBroker gulpfile 3 | * Date: 2019-01-28 4 | */ 5 | 'use strict'; 6 | 7 | const gulp = require('gulp'); 8 | const fs = require('fs'); 9 | const pkg = require('./package.json'); 10 | const iopackage = require('./io-package.json'); 11 | const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; 12 | const fileName = 'words.js'; 13 | const EMPTY = ''; 14 | const translate = require('./lib/tools').translateText; 15 | const languages = { 16 | en: {}, 17 | de: {}, 18 | ru: {}, 19 | pt: {}, 20 | nl: {}, 21 | fr: {}, 22 | it: {}, 23 | es: {}, 24 | pl: {}, 25 | 'zh-cn': {} 26 | }; 27 | 28 | function lang2data(lang, isFlat) { 29 | let str = isFlat ? '' : '{\n'; 30 | let count = 0; 31 | for (const w in lang) { 32 | if (lang.hasOwnProperty(w)) { 33 | count++; 34 | if (isFlat) { 35 | str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; 36 | } else { 37 | const key = ' "' + w.replace(/"/g, '\\"') + '": '; 38 | str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; 39 | } 40 | } 41 | } 42 | if (!count) 43 | return isFlat ? '' : '{\n}'; 44 | if (isFlat) { 45 | return str; 46 | } else { 47 | return str.substring(0, str.length - 2) + '\n}'; 48 | } 49 | } 50 | 51 | function readWordJs(src) { 52 | try { 53 | let words; 54 | if (fs.existsSync(src + 'js/' + fileName)) { 55 | words = fs.readFileSync(src + 'js/' + fileName).toString(); 56 | } else { 57 | words = fs.readFileSync(src + fileName).toString(); 58 | } 59 | words = words.substring(words.indexOf('{'), words.length); 60 | words = words.substring(0, words.lastIndexOf(';')); 61 | 62 | const resultFunc = new Function('return ' + words + ';'); 63 | 64 | return resultFunc(); 65 | } catch (e) { 66 | return null; 67 | } 68 | } 69 | 70 | function padRight(text, totalLength) { 71 | return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); 72 | } 73 | 74 | function writeWordJs(data, src) { 75 | let text = ''; 76 | text += '/*global systemDictionary:true */\n'; 77 | text += "'use strict';\n\n"; 78 | text += 'systemDictionary = {\n'; 79 | for (const word in data) { 80 | if (data.hasOwnProperty(word)) { 81 | text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); 82 | let line = ''; 83 | for (const lang in data[word]) { 84 | if (data[word].hasOwnProperty(lang)) { 85 | line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; 86 | } 87 | } 88 | if (line) { 89 | line = line.trim(); 90 | line = line.substring(0, line.length - 1); 91 | } 92 | text += line + '},\n'; 93 | } 94 | } 95 | text += '};'; 96 | if (fs.existsSync(src + 'js/' + fileName)) { 97 | fs.writeFileSync(src + 'js/' + fileName, text); 98 | } else { 99 | fs.writeFileSync(src + '' + fileName, text); 100 | } 101 | } 102 | 103 | function words2languages(src) { 104 | const langs = Object.assign({}, languages); 105 | const data = readWordJs(src); 106 | if (data) { 107 | for (const word in data) { 108 | if (data.hasOwnProperty(word)) { 109 | for (const lang in data[word]) { 110 | if (data[word].hasOwnProperty(lang)) { 111 | langs[lang][word] = data[word][lang]; 112 | // pre-fill all other languages 113 | for (const j in langs) { 114 | if (langs.hasOwnProperty(j)) { 115 | langs[j][word] = langs[j][word] || EMPTY; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | if (!fs.existsSync(src + 'i18n/')) { 123 | fs.mkdirSync(src + 'i18n/'); 124 | } 125 | for (const l in langs) { 126 | if (!langs.hasOwnProperty(l)) 127 | continue; 128 | const keys = Object.keys(langs[l]); 129 | keys.sort(); 130 | const obj = {}; 131 | for (let k = 0; k < keys.length; k++) { 132 | obj[keys[k]] = langs[l][keys[k]]; 133 | } 134 | if (!fs.existsSync(src + 'i18n/' + l)) { 135 | fs.mkdirSync(src + 'i18n/' + l); 136 | } 137 | 138 | fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); 139 | } 140 | } else { 141 | console.error('Cannot read or parse ' + fileName); 142 | } 143 | } 144 | 145 | function words2languagesFlat(src) { 146 | const langs = Object.assign({}, languages); 147 | const data = readWordJs(src); 148 | if (data) { 149 | for (const word in data) { 150 | if (data.hasOwnProperty(word)) { 151 | for (const lang in data[word]) { 152 | if (data[word].hasOwnProperty(lang)) { 153 | langs[lang][word] = data[word][lang]; 154 | // pre-fill all other languages 155 | for (const j in langs) { 156 | if (langs.hasOwnProperty(j)) { 157 | langs[j][word] = langs[j][word] || EMPTY; 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | const keys = Object.keys(langs.en); 165 | keys.sort(); 166 | for (const l in langs) { 167 | if (!langs.hasOwnProperty(l)) 168 | continue; 169 | const obj = {}; 170 | for (let k = 0; k < keys.length; k++) { 171 | obj[keys[k]] = langs[l][keys[k]]; 172 | } 173 | langs[l] = obj; 174 | } 175 | if (!fs.existsSync(src + 'i18n/')) { 176 | fs.mkdirSync(src + 'i18n/'); 177 | } 178 | for (const ll in langs) { 179 | if (!langs.hasOwnProperty(ll)) 180 | continue; 181 | if (!fs.existsSync(src + 'i18n/' + ll)) { 182 | fs.mkdirSync(src + 'i18n/' + ll); 183 | } 184 | 185 | fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); 186 | } 187 | fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); 188 | } else { 189 | console.error('Cannot read or parse ' + fileName); 190 | } 191 | } 192 | 193 | function languagesFlat2words(src) { 194 | const dirs = fs.readdirSync(src + 'i18n/'); 195 | const langs = {}; 196 | const bigOne = {}; 197 | const order = Object.keys(languages); 198 | dirs.sort(function (a, b) { 199 | const posA = order.indexOf(a); 200 | const posB = order.indexOf(b); 201 | if (posA === -1 && posB === -1) { 202 | if (a > b) 203 | return 1; 204 | if (a < b) 205 | return -1; 206 | return 0; 207 | } else if (posA === -1) { 208 | return -1; 209 | } else if (posB === -1) { 210 | return 1; 211 | } else { 212 | if (posA > posB) 213 | return 1; 214 | if (posA < posB) 215 | return -1; 216 | return 0; 217 | } 218 | }); 219 | const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); 220 | 221 | for (const lang of dirs) { 222 | if (lang === 'flat.txt') 223 | continue; 224 | const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); 225 | langs[lang] = {}; 226 | keys.forEach(function (word, i) { 227 | langs[lang][word] = values[i]; 228 | }); 229 | 230 | const words = langs[lang]; 231 | for (const word in words) { 232 | if (words.hasOwnProperty(word)) { 233 | bigOne[word] = bigOne[word] || {}; 234 | if (words[word] !== EMPTY) { 235 | bigOne[word][lang] = words[word]; 236 | } 237 | } 238 | } 239 | } 240 | // read actual words.js 241 | const aWords = readWordJs(); 242 | 243 | const temporaryIgnore = ['flat.txt']; 244 | if (aWords) { 245 | // Merge words together 246 | for (const w in aWords) { 247 | if (aWords.hasOwnProperty(w)) { 248 | if (!bigOne[w]) { 249 | console.warn('Take from actual words.js: ' + w); 250 | bigOne[w] = aWords[w]; 251 | } 252 | dirs.forEach(function (lang) { 253 | if (temporaryIgnore.indexOf(lang) !== -1) 254 | return; 255 | if (!bigOne[w][lang]) { 256 | console.warn('Missing "' + lang + '": ' + w); 257 | } 258 | }); 259 | } 260 | } 261 | 262 | } 263 | 264 | writeWordJs(bigOne, src); 265 | } 266 | 267 | function languages2words(src) { 268 | const dirs = fs.readdirSync(src + 'i18n/'); 269 | const langs = {}; 270 | const bigOne = {}; 271 | const order = Object.keys(languages); 272 | dirs.sort(function (a, b) { 273 | const posA = order.indexOf(a); 274 | const posB = order.indexOf(b); 275 | if (posA === -1 && posB === -1) { 276 | if (a > b) 277 | return 1; 278 | if (a < b) 279 | return -1; 280 | return 0; 281 | } else if (posA === -1) { 282 | return -1; 283 | } else if (posB === -1) { 284 | return 1; 285 | } else { 286 | if (posA > posB) 287 | return 1; 288 | if (posA < posB) 289 | return -1; 290 | return 0; 291 | } 292 | }); 293 | for (const lang of dirs) { 294 | if (lang === 'flat.txt') 295 | continue; 296 | langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); 297 | langs[lang] = JSON.parse(langs[lang]); 298 | const words = langs[lang]; 299 | for (const word in words) { 300 | if (words.hasOwnProperty(word)) { 301 | bigOne[word] = bigOne[word] || {}; 302 | if (words[word] !== EMPTY) { 303 | bigOne[word][lang] = words[word]; 304 | } 305 | } 306 | } 307 | } 308 | // read actual words.js 309 | const aWords = readWordJs(); 310 | 311 | const temporaryIgnore = ['flat.txt']; 312 | if (aWords) { 313 | // Merge words together 314 | for (const w in aWords) { 315 | if (aWords.hasOwnProperty(w)) { 316 | if (!bigOne[w]) { 317 | console.warn('Take from actual words.js: ' + w); 318 | bigOne[w] = aWords[w]; 319 | } 320 | dirs.forEach(function (lang) { 321 | if (temporaryIgnore.indexOf(lang) !== -1) 322 | return; 323 | if (!bigOne[w][lang]) { 324 | console.warn('Missing "' + lang + '": ' + w); 325 | } 326 | }); 327 | } 328 | } 329 | 330 | } 331 | 332 | writeWordJs(bigOne, src); 333 | } 334 | 335 | async function translateNotExisting(obj, baseText, yandex) { 336 | let t = obj['en']; 337 | if (!t) { 338 | t = baseText; 339 | } 340 | 341 | if (t) { 342 | for (let l in languages) { 343 | if (!obj[l]) { 344 | const time = new Date().getTime(); 345 | obj[l] = await translate(t, l, yandex); 346 | console.log('en -> ' + l + ' ' + (new Date().getTime() - time) + ' ms'); 347 | } 348 | } 349 | } 350 | } 351 | 352 | //TASKS 353 | 354 | gulp.task('adminWords2languages', function (done) { 355 | words2languages('./admin/'); 356 | done(); 357 | }); 358 | 359 | gulp.task('adminWords2languagesFlat', function (done) { 360 | words2languagesFlat('./admin/'); 361 | done(); 362 | }); 363 | 364 | gulp.task('adminLanguagesFlat2words', function (done) { 365 | languagesFlat2words('./admin/'); 366 | done(); 367 | }); 368 | 369 | gulp.task('adminLanguages2words', function (done) { 370 | languages2words('./admin/'); 371 | done(); 372 | }); 373 | 374 | gulp.task('updatePackages', function (done) { 375 | iopackage.common.version = pkg.version; 376 | iopackage.common.news = iopackage.common.news || {}; 377 | if (!iopackage.common.news[pkg.version]) { 378 | const news = iopackage.common.news; 379 | const newNews = {}; 380 | 381 | newNews[pkg.version] = { 382 | en: 'news', 383 | de: 'neues', 384 | ru: 'новое', 385 | pt: 'novidades', 386 | nl: 'nieuws', 387 | fr: 'nouvelles', 388 | it: 'notizie', 389 | es: 'noticias', 390 | pl: 'nowości', 391 | 'zh-cn': '新' 392 | }; 393 | iopackage.common.news = Object.assign(newNews, news); 394 | } 395 | fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); 396 | done(); 397 | }); 398 | 399 | gulp.task('updateReadme', function (done) { 400 | const readme = fs.readFileSync('README.md').toString(); 401 | const pos = readme.indexOf('## Changelog\n'); 402 | if (pos !== -1) { 403 | const readmeStart = readme.substring(0, pos + '## Changelog\n'.length); 404 | const readmeEnd = readme.substring(pos + '## Changelog\n'.length); 405 | 406 | if (readme.indexOf(version) === -1) { 407 | const timestamp = new Date(); 408 | const date = timestamp.getFullYear() + '-' + 409 | ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + 410 | ('0' + (timestamp.getDate()).toString(10)).slice(-2); 411 | 412 | let news = ''; 413 | if (iopackage.common.news && iopackage.common.news[pkg.version]) { 414 | news += '* ' + iopackage.common.news[pkg.version].en; 415 | } 416 | 417 | fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); 418 | } 419 | } 420 | done(); 421 | }); 422 | 423 | gulp.task('translate', async function (done) { 424 | 425 | let yandex; 426 | const i = process.argv.indexOf('--yandex'); 427 | if (i > -1) { 428 | yandex = process.argv[i + 1]; 429 | } 430 | 431 | if (iopackage && iopackage.common) { 432 | if (iopackage.common.news) { 433 | console.log('Translate News'); 434 | for (let k in iopackage.common.news) { 435 | console.log('News: ' + k); 436 | let nw = iopackage.common.news[k]; 437 | await translateNotExisting(nw, null, yandex); 438 | } 439 | } 440 | if (iopackage.common.titleLang) { 441 | console.log('Translate Title'); 442 | await translateNotExisting(iopackage.common.titleLang, iopackage.common.title, yandex); 443 | } 444 | if (iopackage.common.desc) { 445 | console.log('Translate Description'); 446 | await translateNotExisting(iopackage.common.desc, null, yandex); 447 | } 448 | 449 | if (fs.existsSync('./admin/i18n/en/translations.json')) { 450 | let enTranslations = require('./admin/i18n/en/translations.json'); 451 | for (let l in languages) { 452 | console.log('Translate Text: ' + l); 453 | let existing = {}; 454 | if (fs.existsSync('./admin/i18n/' + l + '/translations.json')) { 455 | existing = require('./admin/i18n/' + l + '/translations.json'); 456 | } 457 | for (let t in enTranslations) { 458 | if (!existing[t]) { 459 | existing[t] = await translate(enTranslations[t], l, yandex); 460 | } 461 | } 462 | if (!fs.existsSync('./admin/i18n/' + l + '/')) { 463 | fs.mkdirSync('./admin/i18n/' + l + '/'); 464 | } 465 | fs.writeFileSync('./admin/i18n/' + l + '/translations.json', JSON.stringify(existing, null, 4)); 466 | } 467 | } 468 | 469 | } 470 | fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); 471 | }); 472 | 473 | gulp.task('translateAndUpdateWordsJS', gulp.series('translate', 'adminLanguages2words', 'adminWords2languages')); 474 | 475 | gulp.task('default', gulp.series('updatePackages', 'updateReadme')); -------------------------------------------------------------------------------- /io-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "name": "web-speedy", 4 | "version": "0.2.0", 5 | "news": { 6 | "0.2.0": { 7 | "en": "Initial release", 8 | "de": "Erstveröffentlichung", 9 | "ru": "Первый выпуск", 10 | "pt": "lançamento inicial", 11 | "nl": "Eerste uitgave", 12 | "fr": "Première version", 13 | "it": "Versione iniziale", 14 | "es": "Versión inicial", 15 | "pl": "Pierwsze wydanie", 16 | "zh-cn": "初始发行" 17 | }, 18 | "0.1.5": { 19 | "en": "New settings possibilities & Code improvements", 20 | "de": "Neue Einstellungsmöglichkeiten & Codeverbesserungen", 21 | "ru": "Новые возможности настройки и улучшения кода", 22 | "pt": "Novas possibilidades de configurações e melhorias de código", 23 | "nl": "Nieuwe instellingsmogelijkheden & codeverbeteringen", 24 | "fr": "Nouvelles possibilités de réglages et améliorations du code", 25 | "it": "Nuove possibilità di impostazione e miglioramenti del codice", 26 | "es": "Nuevas posibilidades de configuración y mejoras de código", 27 | "pl": "Nowe możliwości ustawień i ulepszenia kodu", 28 | "zh-cn": "新的设置可能性和代码改进" 29 | }, 30 | "0.1.1": { 31 | "en": "MegaByte to Megabit calculation and current test speeds implemented", 32 | "de": "MegaByte zu Megabit Berechnung und aktuelle Testgeschwindigkeiten implementiert", 33 | "ru": "Реализован расчет от MegaByte до Megabit и текущие скорости тестирования", 34 | "pt": "Cálculo de MegaByte para Megabit e velocidades de teste atuais implementadas", 35 | "nl": "MegaByte naar Megabit-berekening en huidige testsnelheden geïmplementeerd", 36 | "fr": "Calcul de MegaByte à Megabit et vitesses de test actuelles implémentées", 37 | "it": "Calcolo da MegaByte a Megabit e velocità di test correnti implementate", 38 | "es": "Cálculo de MegaByte a Megabit y velocidades de prueba actuales implementadas", 39 | "pl": "Wprowadzono obliczenia od MegaByte do Megabit i wdrożono bieżące prędkości testowe", 40 | "zh-cn": "兆字节到兆比特的计算和当前测试速度的实现" 41 | }, 42 | "0.1.0": { 43 | "en": "Beta release for public testing", 44 | "de": "Beta-Version für öffentliche Tests", 45 | "ru": "Бета-версия для публичного тестирования", 46 | "pt": "Versão beta para testes públicos", 47 | "nl": "Beta-release voor openbare tests", 48 | "fr": "Version bêta pour les tests publics", 49 | "it": "Versione beta per test pubblici", 50 | "es": "Versión beta para pruebas públicas", 51 | "pl": "Wersja beta do testów publicznych", 52 | "zh-cn": "Beta版以供公共测试" 53 | } 54 | }, 55 | "title": "Web Speedy", 56 | "titleLang": { 57 | "en": "Web Speedy", 58 | "de": "Web Speedy", 59 | "ru": "Web Speedy", 60 | "pt": "Web Speedy", 61 | "nl": "Web Speedy", 62 | "fr": "Web Speedy", 63 | "it": "Web veloce", 64 | "es": "Web Speedy", 65 | "pl": "Szybka sieć", 66 | "zh-cn": "网络快速" 67 | }, 68 | "desc": { 69 | "en": "Web-Speedy enables you to test your internet connection on a regular base and store results in ioBroker !", 70 | "de": "Mit Web-Speedy können Sie Ihre Internetverbindung regelmäßig testen und die Ergebnisse in ioBroker speichern!", 71 | "ru": "Web-Speedy позволяет вам регулярно проверять ваше интернет-соединение и сохранять результаты в ioBroker!", 72 | "pt": "O Web-Speedy permite que você teste sua conexão com a Internet regularmente e armazene os resultados no ioBroker!", 73 | "nl": "Met Web-Speedy kunt u uw internetverbinding regelmatig testen en resultaten opslaan in ioBroker!", 74 | "fr": "Web-Speedy vous permet de tester votre connexion Internet sur une base régulière et de stocker les résultats dans ioBroker!", 75 | "it": "Web-Speedy ti consente di testare la tua connessione Internet su base regolare e di archiviare i risultati in ioBroker!", 76 | "es": "¡Web-Speedy le permite probar su conexión a Internet de forma regular y almacenar resultados en ioBroker!", 77 | "pl": "Web-Speedy umożliwia regularne testowanie połączenia internetowego i zapisywanie wyników w ioBroker!", 78 | "zh-cn": "Web-Speedy使您可以定期测试Internet连接并将结果存储在ioBroker中!" 79 | }, 80 | "authors": [ 81 | "DutchmanNL " 82 | ], 83 | "keywords": [ 84 | "speed", 85 | "internet", 86 | "web", 87 | "iobroker" 88 | ], 89 | "license": "MIT", 90 | "noConfig": true, 91 | "platform": "Javascript/Node.js", 92 | "main": "main.js", 93 | "icon": "web-speedy.png", 94 | "enabled": true, 95 | "extIcon": "https://raw.githubusercontent.com/DrozmotiX/ioBroker.web-speedy/master/admin/web-speedy.png", 96 | "readme": "https://github.com/DrozmotiX/ioBroker.web-speedy/blob/master/README.md", 97 | "loglevel": "info", 98 | "mode": "daemon", 99 | "type": "network", 100 | "compact": false, 101 | "connectionType": "cloud", 102 | "dataSource": "poll", 103 | "materialize": true, 104 | "plugins": { 105 | "sentry": { 106 | "dsn": "https://240d1b3f0b33449c9ef66dd9fc7255ea@sentry.iobroker.net/25" 107 | } 108 | }, 109 | "dependencies": [ 110 | { 111 | "js-controller": ">=1.4.2" 112 | } 113 | ] 114 | }, 115 | "native": { 116 | "option1": true, 117 | "option2": "42" 118 | }, 119 | "objects": [], 120 | "instanceObjects": [ 121 | { 122 | "_id": "info", 123 | "type": "channel", 124 | "common": { 125 | "name": "Information" 126 | }, 127 | "native": {} 128 | }, 129 | { 130 | "_id": "info.connection", 131 | "type": "state", 132 | "common": { 133 | "role": "indicator.connected", 134 | "name": "Device or service connected", 135 | "type": "boolean", 136 | "read": true, 137 | "write": false, 138 | "def": false 139 | }, 140 | "native": {} 141 | }, 142 | { 143 | "_id": "running", 144 | "type": "state", 145 | "common": { 146 | "role": "indicator.working", 147 | "name": "Speed test currenlty in progress ?", 148 | "type": "boolean", 149 | "read": true, 150 | "write": false, 151 | "def": false 152 | }, 153 | "native": {} 154 | }, 155 | { 156 | "_id": "test_auto_intervall", 157 | "type": "state", 158 | "common": { 159 | "role": "value.interval", 160 | "name": "Intervall to run automatically tests with best-server", 161 | "type": "number", 162 | "read": true, 163 | "write": true, 164 | "def": 30, 165 | "unit": "minutes" 166 | }, 167 | "native": {} 168 | }, 169 | { 170 | "_id": "test_auto_modus", 171 | "type": "state", 172 | "common": { 173 | "role": "switch.mode", 174 | "name": "Server mode to run automatically tests with", 175 | "type": "number", 176 | "read": true, 177 | "write": true, 178 | "states": { 179 | "0" : "Best_Server", 180 | "1" : "By_ID", 181 | "2" : "By_URL" 182 | } 183 | }, 184 | "native": {} 185 | }, 186 | { 187 | "_id": "test_best", 188 | "type": "state", 189 | "common": { 190 | "name": "Run test on Best Server now !", 191 | "type": "boolean", 192 | "read": true, 193 | "role": "button", 194 | "write": true, 195 | "def": false 196 | }, 197 | "native": {} 198 | }, 199 | { 200 | "_id": "test_by_ID", 201 | "type": "state", 202 | "common": { 203 | "name": "Run test on specific ID now !", 204 | "type": "boolean", 205 | "read": true, 206 | "role": "button", 207 | "write": true, 208 | "def": false 209 | }, 210 | "native": {} 211 | }, 212 | { 213 | "_id": "test_by_URL", 214 | "type": "state", 215 | "common": { 216 | "name": "Run test on specific URL now !", 217 | "type": "boolean", 218 | "read": true, 219 | "role": "button", 220 | "write": true, 221 | "def": false 222 | }, 223 | "native": {} 224 | }, 225 | { 226 | "_id": "test_duration", 227 | "type": "state", 228 | "common": { 229 | "role": "state", 230 | "name": "The maximum length (in seconds) of a single test run (upload or download)", 231 | "type": "number", 232 | "read": true, 233 | "write": true, 234 | "def": 5, 235 | "unit": "seconds" 236 | }, 237 | "native": {} 238 | }, 239 | { 240 | "_id": "test_specific_id", 241 | "type": "state", 242 | "common": { 243 | "role": "state", 244 | "name": "Run test on specific server ID (List : https://c.speedtest.net/speedtest-servers-static.php?fbclid=IwAR3mLi2N9mwp1zG4Xu96cn4h1Zql6NG26p6GDjctjMftq0YzKKwPk-wme8A )", 245 | "type": "number", 246 | "read": true, 247 | "write": true 248 | }, 249 | "native": {} 250 | }, 251 | { 252 | "_id": "test_specific_url", 253 | "type": "state", 254 | "common": { 255 | "role": "state", 256 | "name": "Run test on specific server URL (List : https://c.speedtest.net/speedtest-servers-static.php?fbclid=IwAR3mLi2N9mwp1zG4Xu96cn4h1Zql6NG26p6GDjctjMftq0YzKKwPk-wme8A )", 257 | "type": "mixed", 258 | "read": true, 259 | "write": true 260 | }, 261 | "native": {} 262 | } 263 | ] 264 | } -------------------------------------------------------------------------------- /lib/adapter-config.d.ts: -------------------------------------------------------------------------------- 1 | // This file extends the AdapterConfig type from "@types/iobroker" 2 | // using the actual properties present in io-package.json 3 | // in order to provide typings for adapter.config properties 4 | 5 | import { native } from "../io-package.json"; 6 | 7 | type _AdapterConfig = typeof native; 8 | 9 | // Augment the globally declared type ioBroker.AdapterConfig 10 | declare global { 11 | namespace ioBroker { 12 | interface AdapterConfig extends _AdapterConfig { 13 | // Do not enter anything here! 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /lib/state_attr.js: -------------------------------------------------------------------------------- 1 | // Classification of all state attributes possible 2 | const state_attrb = { 3 | // Speed results 4 | 'running_download': { 5 | name: 'Currently download test in progress ?', 6 | type: 'boolean', 7 | role: 'info.status', 8 | }, 9 | 'running_download_speed': { 10 | name: 'Current download kb/s of test', 11 | type: 'number', 12 | role: 'info.status', 13 | unit: 'kb/s', 14 | }, 15 | 'running_download_progress': { 16 | name: 'Current download progress in %', 17 | type: 'number', 18 | role: 'info.status', 19 | unit: '%', 20 | }, 21 | 'running_upload': { 22 | name: 'Currently download test in progress ?', 23 | type: 'boolean', 24 | role: 'info.status', 25 | }, 26 | 'running_upload_speed': { 27 | name: 'Current upload kb/s of test', 28 | type: 'number', 29 | role: 'info.status', 30 | unit: 'kb/s', 31 | }, 32 | 'running_upload_progress': { 33 | name: 'Current upload progress in %', 34 | type: 'number', 35 | role: 'info.status', 36 | unit: '%', 37 | }, 38 | 'running': { 39 | name: 'Currently Speed test in progress ?', 40 | type: 'boolean', 41 | role: 'info.status', 42 | }, 43 | 'download_MB': { 44 | name: 'Download bandwidth in MegaBytes per second', 45 | type: 'number', 46 | role: 'state', 47 | unit: 'MB/s', 48 | }, 49 | 'download_Mb': { 50 | name: 'Download bandwidth in Megabits per second', 51 | type: 'number', 52 | role: 'state', 53 | unit: 'Mb/s', 54 | }, 55 | 'upload_MB': { 56 | name: 'Upload bandwidth in MegaBytes per second', 57 | type: 'number', 58 | role: 'state', 59 | unit: 'MB/s', 60 | }, 61 | 'upload_Mb': { 62 | name: 'Upload bandwidth in Megabits per second', 63 | type: 'number', 64 | role: 'state', 65 | unit: 'Mb/s', 66 | }, 67 | 'originalDownload': { 68 | name: 'Unadjusted downloadh bandwidth in bytes per second', 69 | type: 'number', 70 | role: 'state', 71 | unit: 'b/s', 72 | }, 73 | 'originalUpload': { 74 | name: 'Unadjusted upload bandwidth in bytes per second', 75 | type: 'number', 76 | role: 'state', 77 | unit: 'b/s', 78 | }, 79 | 80 | // Client details 81 | 'ip': { 82 | name: 'Ip of client', 83 | type: 'string', 84 | role: 'info.ip', 85 | }, 86 | 'lat': { 87 | name: 'Latitude of location', 88 | type: 'string', 89 | role: 'value.gps.latitude ', 90 | unit: '°', 91 | }, 92 | 'lon': { 93 | name: 'Longtitude of location', 94 | type: 'string', 95 | role: 'value.gps.longtitude ', 96 | unit: '°', 97 | }, 98 | 'isp': { 99 | name: 'Clients ISP', 100 | type: 'mixed', 101 | role: 'state', 102 | }, 103 | 'isprating': { 104 | name: 'Some kind of rating', 105 | type: 'number', 106 | role: 'state', 107 | }, 108 | 'rating': { 109 | name: 'Another rating, which is always 0 it seems', 110 | type: 'number', 111 | role: 'state', 112 | }, 113 | 'ispdlavg': { 114 | name: 'Avg download speed by all users of this isp in Mbps', 115 | type: 'number', 116 | role: 'state', 117 | unit: 'Mb/s', 118 | }, 119 | 'ispulavg': { 120 | name: 'Avg upload speed by all users of this isp in Mbps', 121 | type: 'number', 122 | role: 'state', 123 | unit: 'Mb/s', 124 | }, 125 | 126 | // Server details 127 | 'url': { 128 | name: 'Test server URL', 129 | type: 'mixed', 130 | role: 'state', 131 | }, 132 | 'url2': { 133 | name: 'Test server URL', 134 | type: 'mixed', 135 | role: 'state', 136 | }, 137 | 'host': { 138 | name: 'Test server hostname', 139 | type: 'mixed', 140 | role: 'state', 141 | }, 142 | 'location': { 143 | name: 'Name of the location', 144 | type: 'mixed', 145 | role: 'state', 146 | }, 147 | 'name': { 148 | name: 'Name of the location', 149 | type: 'mixed', 150 | role: 'state', 151 | }, 152 | 'country': { 153 | name: 'Name of the country', 154 | type: 'mixed', 155 | role: 'state', 156 | }, 157 | 'cc': { 158 | name: 'Country code', 159 | type: 'mixed', 160 | role: 'state', 161 | }, 162 | 'sponsor': { 163 | name: 'Who pays for the test server', 164 | type: 'mixed', 165 | role: 'state', 166 | }, 167 | 'dist': { 168 | name: 'Distance from client to server (SI)', 169 | type: 'number', 170 | role: 'state', 171 | unit: 'km', 172 | }, 173 | 'distMi': { 174 | name: 'Distance from client to server (Imperial)', 175 | type: 'number', 176 | role: 'state', 177 | unit: 'mile', 178 | }, 179 | 'distance': { 180 | name: 'Distance from client to server (SI)', 181 | type: 'number', 182 | role: 'state', 183 | unit: 'km', 184 | }, 185 | 'distanceMi': { 186 | name: 'Distance from client to server (Imperial)', 187 | type: 'number', 188 | role: 'state', 189 | unit: 'mile', 190 | }, 191 | 'bestPing': { 192 | name: 'Best result to download a small file from the server, in ms', 193 | type: 'number', 194 | role: 'state', 195 | unit: 'ms', 196 | }, 197 | 'ping': { 198 | name: 'How long it took to download a small file from the server, in ms', 199 | type: 'number', 200 | role: 'state', 201 | unit: 'ms', 202 | }, 203 | 'id': { 204 | name: 'ID of the server', 205 | type: 'string', 206 | role: 'state', 207 | }, 208 | 'test_duration': { 209 | name: 'The maximum length (in ms) of a single test run (upload or download)', 210 | type: 'number', 211 | role: 'state', 212 | write: true, 213 | unit: 'seconds', 214 | }, 215 | 'Last_Run_Timestamp': { 216 | name: 'Timestamp of last test-execution', 217 | type: 'string', 218 | role: 'value.time', 219 | }, 220 | }; 221 | 222 | module.exports = state_attrb; 223 | -------------------------------------------------------------------------------- /lib/tools.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * Tests whether the given variable is a real object and not an Array 5 | * @param {any} it The variable to test 6 | * @returns {it is Record} 7 | */ 8 | function isObject(it) { 9 | // This is necessary because: 10 | // typeof null === 'object' 11 | // typeof [] === 'object' 12 | // [] instanceof Object === true 13 | return Object.prototype.toString.call(it) === '[object Object]'; 14 | } 15 | 16 | /** 17 | * Tests whether the given variable is really an Array 18 | * @param {any} it The variable to test 19 | * @returns {it is any[]} 20 | */ 21 | function isArray(it) { 22 | if (typeof Array.isArray === 'function') return Array.isArray(it); 23 | return Object.prototype.toString.call(it) === '[object Array]'; 24 | } 25 | 26 | /** 27 | * Translates text to the target language. Automatically chooses the right translation API. 28 | * @param {string} text The text to translate 29 | * @param {string} targetLang The target languate 30 | * @param {string} [yandexApiKey] The yandex API key. You can create one for free at https://translate.yandex.com/developers 31 | * @returns {Promise} 32 | */ 33 | async function translateText(text, targetLang, yandexApiKey) { 34 | if (targetLang === 'en') { 35 | return text; 36 | } 37 | if (yandexApiKey) { 38 | return await translateYandex(text, targetLang, yandexApiKey); 39 | } else { 40 | return await translateGoogle(text, targetLang); 41 | } 42 | } 43 | 44 | /** 45 | * Translates text with Yandex API 46 | * @param {string} text The text to translate 47 | * @param {string} targetLang The target languate 48 | * @param {string} [apiKey] The yandex API key. You can create one for free at https://translate.yandex.com/developers 49 | * @returns {Promise} 50 | */ 51 | async function translateYandex(text, targetLang, apiKey) { 52 | if (targetLang === 'zh-cn') { 53 | targetLang = 'zh'; 54 | } 55 | try { 56 | const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`; 57 | const response = await axios({url, timeout: 15000}); 58 | if (response.data && response.data['text']) { 59 | return response.data['text'][0]; 60 | } 61 | throw new Error('Invalid response for translate request'); 62 | } catch (e) { 63 | throw new Error(`Could not translate to "${targetLang}": ${e}`); 64 | } 65 | } 66 | 67 | /** 68 | * Translates text with Google API 69 | * @param {string} text The text to translate 70 | * @param {string} targetLang The target languate 71 | * @returns {Promise} 72 | */ 73 | async function translateGoogle(text, targetLang) { 74 | try { 75 | const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}&ie=UTF-8&oe=UTF-8`; 76 | const response = await axios({url, timeout: 15000}); 77 | if (isArray(response.data)) { 78 | // we got a valid response 79 | return response.data[0][0][0]; 80 | } 81 | throw new Error('Invalid response for translate request'); 82 | } catch (e) { 83 | throw new Error(`Could not translate to "${targetLang}": ${e}`); 84 | } 85 | } 86 | 87 | module.exports = { 88 | isArray, 89 | isObject, 90 | translateText 91 | }; 92 | -------------------------------------------------------------------------------- /lib/web-speedy.Grafana_Dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 10, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "backgroundColor": "rgba(128,128,128,0.1)", 23 | "colorMaps": [ 24 | { 25 | "color": "#629e51", 26 | "text": "true" 27 | }, 28 | { 29 | "color": "#bf1b00", 30 | "text": "false" 31 | } 32 | ], 33 | "crosshairColor": "#8F070C", 34 | "datasource": "iobroker", 35 | "display": "timeline", 36 | "expandFromQueryS": 0, 37 | "extendLastValue": true, 38 | "gridPos": { 39 | "h": 5, 40 | "w": 24, 41 | "x": 0, 42 | "y": 0 43 | }, 44 | "highlightOnMouseover": true, 45 | "id": 8, 46 | "legendSortBy": "-ms", 47 | "lineColor": "rgba(0,0,0,0.1)", 48 | "links": [], 49 | "metricNameColor": "#000000", 50 | "rangeMaps": [ 51 | { 52 | "from": "null", 53 | "text": "N/A", 54 | "to": "null" 55 | } 56 | ], 57 | "rowHeight": 50, 58 | "showLegend": true, 59 | "showLegendNames": true, 60 | "showLegendPercent": true, 61 | "showLegendValues": true, 62 | "showTimeAxis": true, 63 | "targets": [ 64 | { 65 | "groupBy": [ 66 | { 67 | "params": [ 68 | "$__interval" 69 | ], 70 | "type": "time" 71 | }, 72 | { 73 | "params": [ 74 | "none" 75 | ], 76 | "type": "fill" 77 | } 78 | ], 79 | "measurement": "web-speedy.0.running", 80 | "orderByTime": "ASC", 81 | "policy": "default", 82 | "refId": "A", 83 | "resultFormat": "time_series", 84 | "select": [ 85 | [ 86 | { 87 | "params": [ 88 | "value" 89 | ], 90 | "type": "field" 91 | }, 92 | { 93 | "params": [], 94 | "type": "max" 95 | } 96 | ] 97 | ], 98 | "tags": [] 99 | } 100 | ], 101 | "textSize": 24, 102 | "textSizeTime": 12, 103 | "timeTextColor": "#d8d9da", 104 | "title": "Running Test", 105 | "type": "natel-discrete-panel", 106 | "units": "short", 107 | "valueMaps": [ 108 | { 109 | "op": "=", 110 | "text": "N/A", 111 | "value": "null" 112 | } 113 | ], 114 | "valueTextColor": "#000000", 115 | "writeAllValues": false, 116 | "writeLastValue": true, 117 | "writeMetricNames": false 118 | }, 119 | { 120 | "aliasColors": {}, 121 | "bars": false, 122 | "dashLength": 10, 123 | "dashes": false, 124 | "datasource": "iobroker", 125 | "fill": 1, 126 | "gridPos": { 127 | "h": 7, 128 | "w": 24, 129 | "x": 0, 130 | "y": 5 131 | }, 132 | "id": 6, 133 | "legend": { 134 | "avg": true, 135 | "current": true, 136 | "max": true, 137 | "min": true, 138 | "show": true, 139 | "total": false, 140 | "values": true 141 | }, 142 | "lines": true, 143 | "linewidth": 1, 144 | "links": [], 145 | "nullPointMode": "null", 146 | "percentage": false, 147 | "pointradius": 5, 148 | "points": false, 149 | "renderer": "flot", 150 | "seriesOverrides": [ 151 | { 152 | "alias": "Upload", 153 | "yaxis": 1 154 | }, 155 | { 156 | "alias": "Download", 157 | "yaxis": 1 158 | } 159 | ], 160 | "spaceLength": 10, 161 | "stack": false, 162 | "steppedLine": false, 163 | "targets": [ 164 | { 165 | "alias": "Download", 166 | "groupBy": [ 167 | { 168 | "params": [ 169 | "$__interval" 170 | ], 171 | "type": "time" 172 | }, 173 | { 174 | "params": [ 175 | "previous" 176 | ], 177 | "type": "fill" 178 | } 179 | ], 180 | "measurement": "web-speedy.0.Results.speeds.download_Mb", 181 | "orderByTime": "ASC", 182 | "policy": "default", 183 | "refId": "A", 184 | "resultFormat": "time_series", 185 | "select": [ 186 | [ 187 | { 188 | "params": [ 189 | "value" 190 | ], 191 | "type": "field" 192 | }, 193 | { 194 | "params": [], 195 | "type": "max" 196 | } 197 | ] 198 | ], 199 | "tags": [] 200 | }, 201 | { 202 | "alias": "Upload", 203 | "groupBy": [ 204 | { 205 | "params": [ 206 | "$__interval" 207 | ], 208 | "type": "time" 209 | }, 210 | { 211 | "params": [ 212 | "previous" 213 | ], 214 | "type": "fill" 215 | } 216 | ], 217 | "measurement": "web-speedy.0.Results.speeds.upload_Mb", 218 | "orderByTime": "ASC", 219 | "policy": "default", 220 | "refId": "B", 221 | "resultFormat": "time_series", 222 | "select": [ 223 | [ 224 | { 225 | "params": [ 226 | "value" 227 | ], 228 | "type": "field" 229 | }, 230 | { 231 | "params": [], 232 | "type": "max" 233 | } 234 | ] 235 | ], 236 | "tags": [] 237 | } 238 | ], 239 | "thresholds": [], 240 | "timeFrom": null, 241 | "timeShift": null, 242 | "title": "Web-Speedy Mbit", 243 | "tooltip": { 244 | "shared": true, 245 | "sort": 0, 246 | "value_type": "individual" 247 | }, 248 | "type": "graph", 249 | "xaxis": { 250 | "buckets": null, 251 | "mode": "time", 252 | "name": null, 253 | "show": true, 254 | "values": [] 255 | }, 256 | "yaxes": [ 257 | { 258 | "$$hashKey": "object:749", 259 | "decimals": null, 260 | "format": "Mbits", 261 | "label": null, 262 | "logBase": 1, 263 | "max": null, 264 | "min": null, 265 | "show": true 266 | }, 267 | { 268 | "$$hashKey": "object:750", 269 | "format": "Mbits", 270 | "label": null, 271 | "logBase": 1, 272 | "max": null, 273 | "min": null, 274 | "show": false 275 | } 276 | ], 277 | "yaxis": { 278 | "align": false, 279 | "alignLevel": null 280 | } 281 | }, 282 | { 283 | "aliasColors": {}, 284 | "bars": false, 285 | "dashLength": 10, 286 | "dashes": false, 287 | "datasource": "iobroker", 288 | "fill": 1, 289 | "gridPos": { 290 | "h": 7, 291 | "w": 24, 292 | "x": 0, 293 | "y": 12 294 | }, 295 | "id": 9, 296 | "legend": { 297 | "avg": true, 298 | "current": true, 299 | "max": true, 300 | "min": true, 301 | "show": true, 302 | "total": false, 303 | "values": true 304 | }, 305 | "lines": true, 306 | "linewidth": 1, 307 | "links": [], 308 | "nullPointMode": "null", 309 | "percentage": false, 310 | "pointradius": 5, 311 | "points": false, 312 | "renderer": "flot", 313 | "seriesOverrides": [], 314 | "spaceLength": 10, 315 | "stack": false, 316 | "steppedLine": false, 317 | "targets": [ 318 | { 319 | "alias": "Download", 320 | "groupBy": [ 321 | { 322 | "params": [ 323 | "$__interval" 324 | ], 325 | "type": "time" 326 | }, 327 | { 328 | "params": [ 329 | "previous" 330 | ], 331 | "type": "fill" 332 | } 333 | ], 334 | "measurement": "web-speedy.0.Results.speeds.download_MB", 335 | "orderByTime": "ASC", 336 | "policy": "default", 337 | "refId": "A", 338 | "resultFormat": "time_series", 339 | "select": [ 340 | [ 341 | { 342 | "params": [ 343 | "value" 344 | ], 345 | "type": "field" 346 | }, 347 | { 348 | "params": [], 349 | "type": "max" 350 | } 351 | ] 352 | ], 353 | "tags": [] 354 | }, 355 | { 356 | "alias": "Upload", 357 | "groupBy": [ 358 | { 359 | "params": [ 360 | "$__interval" 361 | ], 362 | "type": "time" 363 | }, 364 | { 365 | "params": [ 366 | "previous" 367 | ], 368 | "type": "fill" 369 | } 370 | ], 371 | "measurement": "web-speedy.0.Results.speeds.upload_MB", 372 | "orderByTime": "ASC", 373 | "policy": "default", 374 | "refId": "B", 375 | "resultFormat": "time_series", 376 | "select": [ 377 | [ 378 | { 379 | "params": [ 380 | "value" 381 | ], 382 | "type": "field" 383 | }, 384 | { 385 | "params": [], 386 | "type": "max" 387 | } 388 | ] 389 | ], 390 | "tags": [] 391 | } 392 | ], 393 | "thresholds": [], 394 | "timeFrom": null, 395 | "timeShift": null, 396 | "title": "Web-Speedy Mbyte", 397 | "tooltip": { 398 | "shared": true, 399 | "sort": 0, 400 | "value_type": "individual" 401 | }, 402 | "type": "graph", 403 | "xaxis": { 404 | "buckets": null, 405 | "mode": "time", 406 | "name": null, 407 | "show": true, 408 | "values": [] 409 | }, 410 | "yaxes": [ 411 | { 412 | "decimals": null, 413 | "format": "MBs", 414 | "label": null, 415 | "logBase": 1, 416 | "max": null, 417 | "min": null, 418 | "show": true 419 | }, 420 | { 421 | "format": "short", 422 | "label": null, 423 | "logBase": 1, 424 | "max": null, 425 | "min": null, 426 | "show": true 427 | } 428 | ], 429 | "yaxis": { 430 | "align": false, 431 | "alignLevel": null 432 | } 433 | }, 434 | { 435 | "aliasColors": {}, 436 | "bars": false, 437 | "dashLength": 10, 438 | "dashes": false, 439 | "datasource": "iobroker", 440 | "fill": 1, 441 | "gridPos": { 442 | "h": 8, 443 | "w": 24, 444 | "x": 0, 445 | "y": 19 446 | }, 447 | "id": 2, 448 | "legend": { 449 | "avg": false, 450 | "current": false, 451 | "max": false, 452 | "min": false, 453 | "show": true, 454 | "total": false, 455 | "values": false 456 | }, 457 | "lines": true, 458 | "linewidth": 1, 459 | "links": [], 460 | "nullPointMode": "null", 461 | "percentage": false, 462 | "pointradius": 5, 463 | "points": false, 464 | "renderer": "flot", 465 | "seriesOverrides": [], 466 | "spaceLength": 10, 467 | "stack": false, 468 | "steppedLine": false, 469 | "targets": [ 470 | { 471 | "groupBy": [ 472 | { 473 | "params": [ 474 | "$__interval" 475 | ], 476 | "type": "time" 477 | }, 478 | { 479 | "params": [ 480 | "previous" 481 | ], 482 | "type": "fill" 483 | } 484 | ], 485 | "measurement": "web-speedy.0.running_download_speed", 486 | "orderByTime": "ASC", 487 | "policy": "default", 488 | "refId": "A", 489 | "resultFormat": "time_series", 490 | "select": [ 491 | [ 492 | { 493 | "params": [ 494 | "value" 495 | ], 496 | "type": "field" 497 | }, 498 | { 499 | "params": [], 500 | "type": "max" 501 | } 502 | ] 503 | ], 504 | "tags": [] 505 | }, 506 | { 507 | "groupBy": [ 508 | { 509 | "params": [ 510 | "$__interval" 511 | ], 512 | "type": "time" 513 | }, 514 | { 515 | "params": [ 516 | "previous" 517 | ], 518 | "type": "fill" 519 | } 520 | ], 521 | "measurement": "web-speedy.0.running_upload_speed", 522 | "orderByTime": "ASC", 523 | "policy": "default", 524 | "refId": "B", 525 | "resultFormat": "time_series", 526 | "select": [ 527 | [ 528 | { 529 | "params": [ 530 | "value" 531 | ], 532 | "type": "field" 533 | }, 534 | { 535 | "params": [], 536 | "type": "max" 537 | } 538 | ] 539 | ], 540 | "tags": [] 541 | } 542 | ], 543 | "thresholds": [], 544 | "timeFrom": "45m", 545 | "timeShift": null, 546 | "title": "Life Measurement", 547 | "tooltip": { 548 | "shared": true, 549 | "sort": 0, 550 | "value_type": "individual" 551 | }, 552 | "type": "graph", 553 | "xaxis": { 554 | "buckets": null, 555 | "mode": "time", 556 | "name": null, 557 | "show": true, 558 | "values": [] 559 | }, 560 | "yaxes": [ 561 | { 562 | "format": "Kbits", 563 | "label": null, 564 | "logBase": 1, 565 | "max": null, 566 | "min": null, 567 | "show": true 568 | }, 569 | { 570 | "format": "short", 571 | "label": null, 572 | "logBase": 1, 573 | "max": null, 574 | "min": null, 575 | "show": true 576 | } 577 | ], 578 | "yaxis": { 579 | "align": false, 580 | "alignLevel": null 581 | } 582 | } 583 | ], 584 | "refresh": "10s", 585 | "schemaVersion": 16, 586 | "style": "dark", 587 | "tags": [], 588 | "templating": { 589 | "list": [] 590 | }, 591 | "time": { 592 | "from": "now-24h", 593 | "to": "now" 594 | }, 595 | "timepicker": { 596 | "refresh_intervals": [ 597 | "5s", 598 | "10s", 599 | "30s", 600 | "1m", 601 | "5m", 602 | "15m", 603 | "30m", 604 | "1h", 605 | "2h", 606 | "1d" 607 | ], 608 | "time_options": [ 609 | "5m", 610 | "15m", 611 | "1h", 612 | "6h", 613 | "12h", 614 | "24h", 615 | "2d", 616 | "7d", 617 | "30d" 618 | ] 619 | }, 620 | "timezone": "", 621 | "title": "Weeb-Speedy", 622 | "uid": "j5z1aRwZz", 623 | "version": 9 624 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Created with @iobroker/create-adapter v1.21.1 5 | */ 6 | 7 | // The adapter-core module gives you access to the core ioBroker functions 8 | // you need to create an adapter 9 | const utils = require('@iobroker/adapter-core'); 10 | const { type } = require('os'); 11 | 12 | // Load speedtest-net thank you @ddsol ! 13 | const state_attr = require(__dirname + '/lib/state_attr.js'); 14 | const speedTest = require('speedtest-net'); 15 | 16 | // Declare used varaibles 17 | let run_test = null, test_running = false, down_ready = null, up_ready = null; 18 | let intervall_time = null, timer = null, stop_timer = null, test_duration = null; 19 | let test_server_id = null, test_server_url = null, running_mode; 20 | let server_id = null, server_url = null; 21 | 22 | // const fs = require("fs"); 23 | 24 | class WebSpeedy extends utils.Adapter { 25 | 26 | /** 27 | * @param {Partial} [options={}] 28 | */ 29 | constructor(options) { 30 | super({ 31 | ...options, 32 | name: 'web-speedy', 33 | }); 34 | this.on('ready', this.onReady.bind(this)); 35 | this.on('stateChange', this.onStateChange.bind(this)); 36 | this.on('unload', this.onUnload.bind(this)); 37 | } 38 | 39 | /** 40 | * Is called when databases are connected and adapter received configuration. 41 | */ 42 | async onReady() { 43 | // Reset the connection & statuus indicators during startup 44 | this.setState('info.connection', false, true); 45 | this.setNotRunning(true); 46 | 47 | // Create state to manually run test & sped indicators 48 | this.create_state('running_download', 'running_download', false); 49 | this.create_state('running_upload', 'running_upload', false); 50 | 51 | // Subscribe to configuration states 52 | this.subscribeStates('test_best'); 53 | this.subscribeStates('test_by_ID'); 54 | this.subscribeStates('test_by_URL'); 55 | this.subscribeStates('test_duration'); 56 | this.subscribeStates('test_auto_modus'); 57 | this.subscribeStates('test_specific_id'); 58 | this.subscribeStates('test_specific_url'); 59 | this.subscribeStates('test_auto_intervall'); 60 | 61 | // Shedule automated execution 62 | await this.intervall_runner(); 63 | 64 | // Initial run to get best-servers and run a first test creating all wanted data-points 65 | // Ignore if no automated intervall time is set 66 | if (intervall_time !== 0) { 67 | this.log.info('Web Speedy startet, getting list of closest servers '); 68 | await this.test_run(0); // Initial run on best Server 69 | this.log.info('Automated scan every : ' + intervall_time + ' minutes :-)'); 70 | } 71 | } 72 | 73 | async test_run(run_type, best_id) { 74 | 75 | // Get configuration of max duration time for scans 76 | const duration_time = await this.getStateAsync('test_duration'); 77 | if (!duration_time || (duration_time.val * 1000) < 5000) { 78 | this.log.warn('Invalid value set for test duration, ignoring value and set to default : 15 seconds'); 79 | test_duration = 15000; 80 | await this.setStateAsync('test_duration', { val: 15, ack: true }); 81 | } else { 82 | test_duration = duration_time.val * 1000; 83 | } 84 | 85 | // Select running mode, run default in case of error during selection 86 | 87 | try { 88 | 89 | switch (run_type) { 90 | 91 | case (0): 92 | // Run test on Best Server found 93 | this.log.info('Run test on on Best Server found'); 94 | run_test = speedTest({ maxTime: test_duration }); 95 | break; 96 | 97 | case (1): 98 | // Get configuration of pecific server id if configured for scan 99 | server_id = await this.getStateAsync('test_specific_id'); 100 | if (server_id !== null && server_id !== undefined) { 101 | test_server_id = server_id.val; 102 | // Run test on configured server by id 103 | 104 | if (best_id) { test_server_id = best_id; } 105 | this.log.info('Run test on configured server by id : ' + test_server_id); 106 | run_test = speedTest({ maxTime: test_duration, serverId: test_server_id.toString() }); 107 | 108 | // run_test = speedTest({maxTime: test_duration, serverId: test_server_id}); 109 | } else { 110 | this.log.warn('Error Case 1 selecting specific server, running Best_Server mode'); 111 | run_test = speedTest({ maxTime: test_duration }); 112 | } 113 | 114 | break; 115 | 116 | case (2): 117 | // Get configuration of pecific server id if configured for scan 118 | server_url = await this.getStateAsync('test_specific_url'); 119 | if (server_url !== null && server_url !== undefined) { 120 | test_server_url = server_url.val; 121 | // Run test on configured server by url 122 | this.log.info('Run test on configured server by url : ' + test_server_url); 123 | run_test = speedTest({ maxTime: test_duration, serversUrl: test_server_url.toString() }); 124 | } else { 125 | this.log.warn('Error Case 2 selecting specific server, running Best_Server mode'); 126 | run_test = speedTest({ maxTime: test_duration }); 127 | } 128 | break; 129 | 130 | default: 131 | this.log.warn('Error No Case selecting specific server, running Best_Server mode'); 132 | run_test = speedTest({ maxTime: test_duration }); 133 | } 134 | 135 | 136 | } catch (error) { 137 | this.log.warn('Error selecting specific server, running Best_Server mode : ' + error); 138 | run_test = speedTest({ maxTime: test_duration }); 139 | 140 | } 141 | 142 | this.log.info('The speed test has been started and will take at maximum ' + (test_duration / 1000) + ' seconds for a single test run'); 143 | 144 | // Fired when an error occurs. The error is written to log when received. 145 | run_test.on('error', err => { 146 | this.log.error(err); 147 | 148 | // Reset all states to non-running state 149 | this.setState('info.connection', false, true); 150 | this.setNotRunning(); // Reset all status indicators to not-running 151 | }); 152 | 153 | // Fired when module has been triggered providing configuration 154 | run_test.on('config', config => { 155 | this.log.debug('Configuration info : ' + JSON.stringify(config)); 156 | 157 | // Set all states to running state 158 | this.setRunning(); 159 | }); 160 | 161 | // Monitor download progress % 162 | run_test.on('downloadprogress', progress => { 163 | this.log.debug('Download progress : ' + progress); 164 | this.create_state('running_download_progress', 'running_download_progress', progress); 165 | 166 | }); 167 | 168 | // Monitor upload progress % 169 | run_test.on('uploadprogress', progress => { 170 | this.log.debug('Upload progress : ' + progress); 171 | this.create_state('running_upload_progress', 'running_upload_progress', progress); 172 | }); 173 | 174 | // Fired when download is finished 175 | run_test.on('downloadspeed', speed => { 176 | this.setState('running_download', false, true); 177 | this.setState('running_download_speed', 0, true); 178 | this.log.debug('Download speed : ' + (speed * 125).toFixed(2)); 179 | }); 180 | 181 | // Fired when upload is finished 182 | run_test.on('uploadspeed', speed => { 183 | this.setState('running_upload', false, true); 184 | this.setState('running_upload_speed', 0, true); 185 | this.log.debug('Upload speed : ' + (speed * 125).toFixed(2)); 186 | }); 187 | 188 | // Monitor download speed kb/s 189 | run_test.on('downloadspeedprogress', speed => { 190 | 191 | this.log.debug('Download speed (in progress) : ' + (speed * 125).toFixed(2) + ' kb/s'); 192 | if (down_ready !== true) { 193 | down_ready = true; 194 | this.setState('running_download', true, true); 195 | } 196 | this.create_state('running_download_speed', 'running_upload_speed', parseFloat((speed * 125).toFixed(2))); 197 | }); 198 | 199 | // Monitor upload speed kb/s 200 | run_test.on('uploadspeedprogress', speed => { 201 | this.log.debug('Upload speed (in progress) : ' + (speed * 125).toFixed(2) + ' kb/s'); 202 | if (up_ready !== true) { 203 | up_ready = true; 204 | this.setState('running_upload', true, true); 205 | } 206 | this.create_state('running_upload_speed', 'running_upload_speed', parseFloat((speed * 125).toFixed(2))); 207 | }); 208 | 209 | // Execute when data is publish at test-end (not working) 210 | // run_test.on('result', url => { 211 | // if (!url) { 212 | // this.log.error('Could not successfully post test results.'); 213 | // } else { 214 | // this.log.info('Test result url:', url); 215 | // } 216 | // }); 217 | 218 | // Get best server results and write to states and create drop-down menu to run specific test 219 | run_test.on('bestservers', serverlist => { 220 | this.setRunning(); 221 | this.log.debug('Closest servers : ' + JSON.stringify(serverlist)); 222 | 223 | // Create channel for list of closest servers with all details 224 | this.extendObject('Closest_servers', { 225 | type: 'channel', 226 | common: { 227 | name: 'Closest servers pinged at last test', 228 | }, 229 | native: {}, 230 | }); 231 | 232 | // Write Server details to states 233 | try { 234 | 235 | // Loop array of server 236 | for (const i in serverlist) { 237 | this.log.debug('Closest server : ' + i + ' : ' + JSON.stringify(serverlist[i])); 238 | 239 | // Create Server Channels 240 | this.extendObject('Closest_servers.' + i, { 241 | type: 'channel', 242 | common: { 243 | name: 'Closest server ' + i, 244 | }, 245 | native: {}, 246 | }); 247 | 248 | // Create and write information to states 249 | for (const x in serverlist[i]) { 250 | this.create_state('Closest_servers.' + i + '.' + x, x, serverlist[i][x]); 251 | } 252 | } 253 | 254 | // Create state to run test on specific server with drop-down menu(top 5 retrieved from scan) 255 | this.extendObject('test_specific', { 256 | type: 'state', 257 | common: { 258 | name: 'Run test on selected server', 259 | type: 'mixed', 260 | role: 'state', 261 | write: true, 262 | states: { 263 | [serverlist[0]['id']]: 'name : ' + serverlist[0]['sponsor'] + ' Last Ping : ' + serverlist[0]['bestPing'], 264 | [serverlist[1]['id']]: 'name : ' + serverlist[1]['sponsor'] + ' Last Ping : ' + serverlist[1]['bestPing'], 265 | [serverlist[2]['id']]: 'name : ' + serverlist[2]['sponsor'] + ' Last Ping : ' + serverlist[2]['bestPing'], 266 | [serverlist[3]['id']]: 'name : ' + serverlist[3]['sponsor'] + ' Last Ping : ' + serverlist[3]['bestPing'], 267 | [serverlist[4]['id']]: 'name : ' + serverlist[4]['sponsor'] + ' Last Ping : ' + serverlist[4]['bestPing'], 268 | }, 269 | }, 270 | native: {}, 271 | }); 272 | 273 | this.setState('test_specific', { val: '', ack: true }); 274 | this.subscribeStates('test_specific'); 275 | 276 | } catch (error) { 277 | this.log.error(error); 278 | } 279 | 280 | this.log.info('Closest server found, running test'); 281 | 282 | }); 283 | 284 | // Get all test-result data (to much information, ignoring results) 285 | // eslint-disable-next-line no-unused-vars 286 | run_test.on('done', dataOverload => { 287 | // this.log.info('Speed test finished, result : ' + JSON.stringify(dataOverload)); 288 | this.log.info('The speed test has been completed successfully.'); 289 | this.setNotRunning(); // Reset all status indicators to not-running 290 | }); 291 | 292 | // Fired at test end providing JSON-array with all data 293 | run_test.on('data', data => { 294 | this.log.debug('Test Result Data : ' + JSON.stringify(data)); 295 | // Create channle for test-results 296 | this.extendObject('Results', { 297 | type: 'channel', 298 | common: { 299 | name: 'Test results of latest run', 300 | }, 301 | native: {}, 302 | }); 303 | 304 | // Write Speed-test results to state 305 | this.log.debug('Test results : ' + JSON.stringify(data)); 306 | this.create_state('Results.Last_Run', 'Last_Run_Timestamp', new Date().toString()); 307 | 308 | for (const i in data) { 309 | 310 | // Create channel for each cathegorie 311 | this.extendObject('Results.' + i, { 312 | type: 'channel', 313 | common: { 314 | name: i, 315 | }, 316 | native: {}, 317 | }); 318 | 319 | // Loop data-array and write values to states 320 | for (const x in data[i]) { 321 | 322 | switch (x) { 323 | 324 | // Make propper calculation for download speed in Mbit and MByte 325 | case ('download'): 326 | this.create_state('Results.' + i + '.download_MB', 'download_MB', (data[i][x] / 8)); 327 | this.create_state('Results.' + i + '.download_Mb', 'download_Mb', data[i][x]); 328 | break; 329 | 330 | case ('upload'): 331 | this.create_state('Results.' + i + '.upload_MB', 'upload_MB', (data[i][x] / 8)); 332 | this.create_state('Results.' + i + '.upload_Mb', 'upload_Mb', data[i][x]); 333 | break; 334 | 335 | default: 336 | // Write alle regular states without conversion 337 | if (x === 'lon' || x === 'lat') { // latitude and longtitude == type string! 338 | this.create_state('Results.' + i + '.' + x, x, (data[i][x]).toString()); 339 | } else { 340 | this.create_state('Results.' + i + '.' + x, x, data[i][x]); 341 | }; 342 | 343 | } 344 | 345 | } 346 | } 347 | this.setNotRunning(); // Reset all status indicators to not-running 348 | }); 349 | } 350 | 351 | // Intervall timer to run automated tests 352 | async intervall_runner() { 353 | 354 | // Get intervall time configuration 355 | intervall_time = await this.getStateAsync('test_auto_intervall'); 356 | 357 | // Propper handling of shedule, if NULL set to default (30 minutes) 358 | if (!intervall_time || (intervall_time && intervall_time.val === null)) { 359 | await this.setStateAsync('test_auto_intervall', { val: 30, ack: true }); 360 | intervall_time = 30; 361 | } else if (intervall_time && intervall_time.val !== null) { 362 | intervall_time = intervall_time.val; 363 | } 364 | 365 | // Get intervall running mode configuration 366 | const test_auto_modus = await this.getStateAsync('test_auto_modus'); 367 | 368 | // Propper handling of shedule, if NULL set to default (Best Server) 369 | if (!test_auto_modus) { 370 | this.log.warn('Invalid value set for auto modus, ignoring value and set to default'); 371 | running_mode = 0; // Run on best server found 372 | await this.setStateAsync('test_auto_modus', { val: 0, ack: true }); 373 | } else { 374 | running_mode = test_auto_modus.val; 375 | } 376 | 377 | this.log.debug('Start timer with : ' + (intervall_time * 60000)); 378 | 379 | // Disable time if test_auto_intervall is set to 0 380 | if (intervall_time !== 0) { 381 | // Reset timer (if running) and start new one 382 | if (timer) { clearTimeout(timer); timer = null; } 383 | timer = setTimeout(() => { 384 | this.log.info('Execute timer with : ' + (intervall_time * 60000) + ' Currently running : ' + test_running); 385 | if (!test_running) { 386 | this.test_run(running_mode); 387 | } 388 | // Restart intervall at run 389 | this.intervall_runner(); 390 | 391 | // Set timer, minutes to milliseconds 392 | }, (intervall_time * 60000)); 393 | 394 | } else { 395 | // 0 intervall time configured, disabling auto shedule 396 | if (timer) { clearTimeout(timer); timer = null; } 397 | this.log.warn('!!! Automated test disabled !!!'); 398 | } 399 | } 400 | 401 | // Handle configuration changes and test-start trigger 402 | onStateChange(id, state) { 403 | this.log.debug('State Change event : ' + id + ' value : ' + JSON.stringify(state)); 404 | 405 | // Check if valid state change is received with not-acknowledged value 406 | if (state && state.ack === false) { 407 | 408 | // Get state name 409 | const deviceId = id.split('.'); 410 | const device = deviceId[2]; 411 | 412 | // Only handle cases for different states if not test is currenlty running 413 | if (!test_running) { 414 | 415 | switch (device) { 416 | 417 | case ('test_best'): 418 | this.test_run(0); 419 | this.setRunning(); 420 | this.setState('test_best', { ack: true }); 421 | this.log.info('Manuel test startet on best server'); 422 | break; 423 | 424 | case ('test_by_ID'): 425 | this.test_run(1); 426 | this.setRunning(); 427 | this.log.info('Manuel test startet for specific server ID'); 428 | this.setState('test_by_ID', { ack: true }); 429 | break; 430 | 431 | case ('test_by_URL'): 432 | this.test_run(2); 433 | this.setRunning(); 434 | this.log.info('Manuel test startet for specific server URL'); 435 | this.setState('test_by_ID', { ack: true }); 436 | break; 437 | 438 | case ('test_specific'): 439 | this.test_run(1, state.val); 440 | this.setRunning(); 441 | this.log.info('Manuel test startet for best avaiable server ID : ' + state.val); 442 | this.setState('test_specific', { val: null, ack: true }); 443 | break; 444 | 445 | case ('test_specific_id'): 446 | // Acknowledge ID change 447 | this.log.info('Test pecific server ID changed'); 448 | this.setState('test_specific_id', { ack: true }); 449 | break; 450 | 451 | case ('test_specific_url'): 452 | // Acknowledge ID change 453 | this.log.info('Test pecific server URL changed'); 454 | this.setState('test_specific_id', { ack: true }); 455 | break; 456 | 457 | case ('test_auto_intervall'): 458 | this.intervall_runner(); 459 | this.setState('test_auto_intervall', { ack: true }); 460 | this.log.info('Automated scan changed to every : ' + state.val + ' minutes'); 461 | break; 462 | 463 | case ('test_auto_modus'): 464 | this.intervall_runner(); 465 | this.setState('test_auto_modus', { ack: true }); 466 | this.log.info('Automated scan mode changed.'); 467 | break; 468 | 469 | case ('test_duration'): 470 | this.setState('test_duration', { ack: true }); 471 | this.log.info('Maximum test duration changed to : ' + state.val + ' seconds'); 472 | break; 473 | 474 | default: 475 | } 476 | } 477 | } 478 | } 479 | 480 | // Set all status indicators to running state 481 | setRunning() { 482 | if (!test_running) { 483 | this.setState('info.connection', true, true); 484 | this.setState('running', true, true); 485 | test_running = true; 486 | } 487 | } 488 | 489 | // Set all status indicators to not-running state 490 | setNotRunning(start) { 491 | 492 | // Different delay times for adapter start and running mode 493 | let delay_time = null; 494 | if (start === true) { 495 | delay_time = 10; 496 | } else { 497 | delay_time = 5000; 498 | } 499 | 500 | // a little delay to ensure all backend processes are finished 501 | if (stop_timer) { clearTimeout(stop_timer); stop_timer = null; } 502 | if (start !== true) { 503 | stop_timer = setTimeout(() => { 504 | this.setState('running', false, true); 505 | this.setState('running_download_speed', 0, true); 506 | this.setState('running_upload_speed', 0, true); 507 | this.setState('running_download', false, true); 508 | this.setState('running_upload', false, true); 509 | down_ready = false; 510 | up_ready = false; 511 | test_running = false; 512 | }, delay_time); 513 | } 514 | } 515 | 516 | async create_state(state, name, value) { 517 | this.log.debug('Create_state called for : ' + state + ' with value : ' + value); 518 | 519 | try { 520 | // Try to get details from state lib, if not use defaults. throw warning if states is not known in attribute list 521 | if ((state_attr[name] === undefined)) { this.log.warn('State attribute definition missing for + ' + name); } 522 | const writable = (state_attr[name] !== undefined) ? state_attr[name].write || false : false; 523 | const state_name = (state_attr[name] !== undefined) ? state_attr[name].name || name : name; 524 | const role = (state_attr[name] !== undefined) ? state_attr[name].role || 'state' : 'state'; 525 | const type = (state_attr[name] !== undefined) ? state_attr[name].type || 'mixed' : 'mixed'; 526 | const unit = (state_attr[name] !== undefined) ? state_attr[name].unit || '' : ''; 527 | this.log.debug('Write value : ' + writable); 528 | 529 | await this.extendObjectAsync(state, { 530 | type: 'state', 531 | common: { 532 | name: state_name, 533 | role: role, 534 | type: type, 535 | unit: unit, 536 | write: writable 537 | }, 538 | native: {}, 539 | }); 540 | 541 | // Only set value if input != null 542 | if (value !== null) { await this.setState(state, { val: value, ack: true }); } 543 | 544 | // Subscribe on state changes if writable 545 | if (writable === true) { this.subscribeStates(state); } 546 | 547 | } catch (error) { 548 | this.log.error('Create state error = ' + error); 549 | } 550 | } 551 | 552 | /** 553 | * Is called when adapter shuts down - callback has to be called under any circumstances! 554 | * @param {() => void} callback 555 | */ 556 | onUnload(callback) { 557 | try { 558 | this.log.info('cleaned everything up...'); 559 | // clear running timers 560 | if (timer) { clearTimeout(timer); timer = null; } 561 | if (stop_timer) { clearTimeout(stop_timer); stop_timer = null; } 562 | 563 | callback(); 564 | } catch (e) { 565 | callback(); 566 | } 567 | } 568 | 569 | } 570 | 571 | // @ts-ignore parent is a valid property on module 572 | if (module.parent) { 573 | // Export the constructor in compact mode 574 | /** 575 | * @param {Partial} [options={}] 576 | */ 577 | module.exports = (options) => new WebSpeedy(options); 578 | } else { 579 | // otherwise start the instance directly 580 | new WebSpeedy(); 581 | } 582 | -------------------------------------------------------------------------------- /main.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This is a dummy TypeScript test file using chai and mocha 5 | * 6 | * It's automatically excluded from npm and its build output is excluded from both git and npm. 7 | * It is advised to test all your modules with accompanying *.test.js-files 8 | */ 9 | 10 | // tslint:disable:no-unused-expression 11 | 12 | const { expect } = require('chai'); 13 | // import { functionToTest } from "./moduleToTest"; 14 | 15 | describe('module to test => function to test', () => { 16 | // initializing logic 17 | const expected = 5; 18 | 19 | it(`should return ${expected}`, () => { 20 | const result = 5; 21 | // assign result a value from functionToTest 22 | expect(result).to.equal(expected); 23 | // or using the should() syntax 24 | result.should.equal(expected); 25 | }); 26 | // ... more tests => it 27 | 28 | }); 29 | 30 | // ... more test suites => describe 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iobroker.web-speedy", 3 | "version": "0.2.0", 4 | "description": "Web-Speedy enables you to test your internet connection on a regular base and store results in ioBroker !", 5 | "author": { 6 | "name": "DutchmanNL", 7 | "email": "rdrozda86@gmail.com" 8 | }, 9 | "homepage": "https://github.com/DrozmotiX/ioBroker.web-speedy", 10 | "license": "MIT", 11 | "keywords": [ 12 | "speed", 13 | "internet", 14 | "web", 15 | "iobroker" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/DrozmotiX/ioBroker.web-speedy" 20 | }, 21 | "dependencies": { 22 | "@iobroker/adapter-core": "^2.2.1", 23 | "speedtest-net": "^1.6.2" 24 | }, 25 | "devDependencies": { 26 | "@iobroker/testing": "^2.0.2", 27 | "@types/chai": "^4.2.9", 28 | "@types/chai-as-promised": "^7.1.2", 29 | "@types/gulp": "^4.0.6", 30 | "@types/mocha": "^7.0.1", 31 | "@types/node": "^10.17.15", 32 | "@types/proxyquire": "^1.3.28", 33 | "@types/sinon": "^7.5.1", 34 | "@types/sinon-chai": "^3.2.3", 35 | "axios": "^0.21.2", 36 | "chai": "^4.2.0", 37 | "chai-as-promised": "^7.1.1", 38 | "eslint": "^6.8.0", 39 | "gulp": "^4.0.2", 40 | "mocha": "^7.0.1", 41 | "proxyquire": "^2.1.3", 42 | "sinon": "^8.1.1", 43 | "sinon-chai": "^3.4.0" 44 | }, 45 | "main": "main.js", 46 | "scripts": { 47 | "test:js": "mocha --opts test/mocha.custom.opts", 48 | "test:package": "mocha test/package --exit", 49 | "test:unit": "mocha test/unit --exit", 50 | "test:integration": "mocha test/integration --exit", 51 | "test": "npm run test:js && npm run test:package", 52 | "lint": "eslint" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/DrozmotiX/ioBroker.web-speedy/issues" 56 | }, 57 | "readmeFilename": "README.md" 58 | } 59 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Run integration tests - See https://github.com/ioBroker/testing for a detailed explanation and further options 5 | tests.integration(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/mocha.custom.opts: -------------------------------------------------------------------------------- 1 | --require test/mocha.setup.js 2 | {!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js} -------------------------------------------------------------------------------- /test/mocha.setup.js: -------------------------------------------------------------------------------- 1 | // Don't silently swallow unhandled rejections 2 | process.on('unhandledRejection', (e) => { 3 | throw e; 4 | }); 5 | 6 | // enable the should interface with sinon 7 | // and load chai-as-promised and sinon-chai by default 8 | const sinonChai = require('sinon-chai'); 9 | const chaiAsPromised = require('chai-as-promised'); 10 | const { should, use } = require('chai'); 11 | 12 | should(); 13 | use(sinonChai); 14 | use(chaiAsPromised); -------------------------------------------------------------------------------- /test/package.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Validate the package files 5 | tests.packageFiles(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false 5 | }, 6 | "include": [ 7 | "./**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Run unit tests - See https://github.com/ioBroker/testing for a detailed explanation and further options 5 | tests.unit(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | // do not compile anything, this file is just to configure type checking 5 | "noEmit": true, 6 | 7 | // check JS files 8 | "allowJs": true, 9 | "checkJs": true, 10 | 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | // this is necessary for the automatic typing of the adapter config 14 | "resolveJsonModule": true, 15 | 16 | // Set this to false if you want to disable the very strict rules (not recommended) 17 | "strict": true, 18 | // Or enable some of those features for more fine-grained control 19 | // "strictNullChecks": true, 20 | // "strictPropertyInitialization": true, 21 | // "strictBindCallApply": true, 22 | "noImplicitAny": false, 23 | // "noUnusedLocals": true, 24 | // "noUnusedParameters": true, 25 | 26 | // Consider targetting es2017 or higher if you require the new NodeJS 8+ features 27 | "target": "es2015", 28 | 29 | }, 30 | "include": [ 31 | "**/*.js", 32 | "**/*.d.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules/**", 36 | "admin/**" 37 | ] 38 | } --------------------------------------------------------------------------------