├── .releaseconfig.json ├── .vscode ├── extensions.json └── settings.json ├── admin ├── fronius-wattpilot.png ├── tsconfig.json ├── style.css ├── admin.d.ts ├── index_m.html └── words.js ├── test ├── tsconfig.json ├── package.js ├── mocharc.custom.json ├── unit.js ├── integration.js └── mocha.setup.js ├── tsconfig.check.json ├── .gitignore ├── lib └── adapter-config.d.ts ├── eslint.config.mjs ├── main.test.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test-and-release.yml ├── .create-adapter.json ├── LICENSE ├── tsconfig.json ├── package.json ├── README.md ├── README_DE.md ├── io-package.json ├── examples └── example-Blockly.xml └── main.js /.releaseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["iobroker", "license"] 3 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /admin/fronius-wattpilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim2zg/ioBroker.fronius-wattpilot/HEAD/admin/fronius-wattpilot.png -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false 5 | }, 6 | "include": [ 7 | "./**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /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/mocharc.custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "test/mocha.setup.js" 4 | ], 5 | "watch-files": [ 6 | "!(node_modules|test)/**/*.test.js", 7 | "*.test.js", 8 | "test/**/test!(PackageFiles|Startup).js" 9 | ] 10 | } -------------------------------------------------------------------------------- /admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./admin.d.ts", 5 | "./**/*.js", 6 | // include the adapter-config definition if it exists 7 | "../src/lib/adapter-config.d.ts", 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 | -------------------------------------------------------------------------------- /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, "..")); -------------------------------------------------------------------------------- /tsconfig.check.json: -------------------------------------------------------------------------------- 1 | // Specialized tsconfig for type-checking js files 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": {}, 5 | "include": [ 6 | "**/*.js", 7 | "**/*.d.ts" 8 | ], 9 | "exclude": [ 10 | "**/build", 11 | "node_modules/", 12 | "widgets/" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # No dot-directories except github/vscode 2 | .*/ 3 | !.vscode/ 4 | !.github/ 5 | 6 | *.code-workspace 7 | node_modules 8 | nbproject 9 | 10 | # npm package files 11 | iobroker.*.tgz 12 | 13 | Thumbs.db 14 | 15 | # i18n intermediate files 16 | admin/i18n/flat.txt 17 | admin/i18n/*/flat.txt 18 | #ignore .commitinfo created by ioBroker release script 19 | .commitinfo 20 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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 | } 17 | 18 | // this is required so the above AdapterConfig is found by TypeScript / type checking 19 | export {}; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "json.schemas": [ 4 | { 5 | "fileMatch": [ 6 | "io-package.json" 7 | ], 8 | "url": "https://raw.githubusercontent.com/ioBroker/ioBroker.js-controller/master/schemas/io-package.json" 9 | }, 10 | { 11 | "fileMatch": [ 12 | "admin/jsonConfig.json", 13 | "admin/jsonCustom.json", 14 | "admin/jsonTab.json", 15 | "admin/jsonConfig.json5", 16 | "admin/jsonCustom.json5", 17 | "admin/jsonTab.json5" 18 | ], 19 | "url": "https://raw.githubusercontent.com/ioBroker/ioBroker.admin/master/packages/jsonConfig/schemas/jsonConfig.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /admin/style.css: -------------------------------------------------------------------------------- 1 | /* You can delete those if you want. I just found them very helpful */ 2 | * { 3 | box-sizing: border-box 4 | } 5 | .m { 6 | /* Don't cut off dropdowns! */ 7 | overflow: initial; 8 | } 9 | .m.adapter-container, 10 | .m.adapter-container > div.App { 11 | /* Fix layout/scrolling issues with tabs */ 12 | height: 100%; 13 | width: 100%; 14 | position: relative; 15 | } 16 | .m .select-wrapper + label { 17 | /* The positioning for dropdown labels is messed up */ 18 | transform: none !important; 19 | } 20 | 21 | label > i[title] { 22 | /* Display the help cursor for the tooltip icons and fix their positioning */ 23 | cursor: help; 24 | margin-left: 0.25em; 25 | } 26 | 27 | .dropdown-content { 28 | /* Don't wrap text in dropdowns */ 29 | white-space: nowrap; 30 | } 31 | 32 | /* Add your styles here */ 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // ioBroker eslint template configuration file for js and ts files 2 | // Please note that esm or react based modules need additional modules loaded. 3 | import config from '@iobroker/eslint-config'; 4 | 5 | export default [ 6 | ...config, 7 | 8 | { 9 | // specify files to exclude from linting here 10 | ignores: [ 11 | '.dev-server/', 12 | '.vscode/', 13 | '*.test.js', 14 | 'test/**/*.js', 15 | '*.config.mjs', 16 | 'build', 17 | 'admin/build', 18 | 'admin/words.js', 19 | 'admin/admin.d.ts', 20 | '**/adapter-config.d.ts' 21 | ] 22 | }, 23 | 24 | { 25 | // you may disable some 'jsdoc' warnings - but using jsdoc is highly recommended 26 | // as this improves maintainability. jsdoc warnings will not block buiuld process. 27 | rules: { 28 | // 'jsdoc/require-jsdoc': 'off', 29 | }, 30 | }, 31 | 32 | ]; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.create-adapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": true, 3 | "target": "directory", 4 | "adapterName": "fronius-wattpilot", 5 | "title": "Fronius Wattpilot", 6 | "contributors": [ 7 | "tim2zg" 8 | ], 9 | "expert": "yes", 10 | "features": [ 11 | "adapter" 12 | ], 13 | "adminFeatures": [], 14 | "type": "communication", 15 | "startMode": "daemon", 16 | "connectionType": "local", 17 | "dataSource": "poll", 18 | "connectionIndicator": "yes", 19 | "language": "JavaScript", 20 | "adminReact": "no", 21 | "tools": [ 22 | "ESLint", 23 | "type checking" 24 | ], 25 | "i18n": "words.js", 26 | "releaseScript": "no", 27 | "devServer": "yes", 28 | "devServerPort": 8081, 29 | "indentation": "Tab", 30 | "quotes": "double", 31 | "es6class": "yes", 32 | "authorName": "tim2zg", 33 | "authorGithub": "tim2zg", 34 | "authorEmail": "tim2zg@protonmail.com", 35 | "gitRemoteProtocol": "HTTPS", 36 | "gitCommit": "no", 37 | "defaultBranch": "main", 38 | "license": "MIT License", 39 | "dependabot": "no", 40 | "creatorVersion": "2.1.1" 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 tim2zg 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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // Root tsconfig to set the settings and power editor support for all TS files 2 | { 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | // do not compile anything, this file is just to configure type checking 6 | "noEmit": true, 7 | 8 | // check JS files 9 | "allowJs": true, 10 | "checkJs": true, 11 | 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | // this is necessary for the automatic typing of the adapter config 16 | "resolveJsonModule": true, 17 | 18 | // Set this to false if you want to disable the very strict rules (not recommended) 19 | "strict": true, 20 | // Or enable some of those features for more fine-grained control 21 | // "strictNullChecks": true, 22 | // "strictPropertyInitialization": true, 23 | // "strictBindCallApply": true, 24 | "noImplicitAny": false, 25 | // "noUnusedLocals": true, 26 | // "noUnusedParameters": true, 27 | "useUnknownInCatchVariables": false, 28 | 29 | // Consider targetting es2019 or higher if you only support Node.js 12+ 30 | "target": "es2018", 31 | 32 | }, 33 | "include": [ 34 | "**/*.js", 35 | "**/*.d.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules/**" 39 | ] 40 | } -------------------------------------------------------------------------------- /admin/admin.d.ts: -------------------------------------------------------------------------------- 1 | declare let systemDictionary: Record>; 2 | 3 | declare let load: (settings: Record, onChange: (hasChanges: boolean) => void) => void; 4 | declare let save: (callback: (settings: Record) => void) => void; 5 | 6 | // make load and save exist on the window object 7 | interface Window { 8 | load: typeof load; 9 | save: typeof save; 10 | } 11 | 12 | declare const instance: number; 13 | declare const adapter: string; 14 | /** Translates text */ 15 | declare function _(text: string, arg1?: string, arg2?: string, arg3?: string): string; 16 | declare const socket: ioBrokerSocket; 17 | declare function sendTo( 18 | instance: any | null, 19 | command: string, 20 | message: any, 21 | callback: (result: SendToResult) => void | Promise, 22 | ): void; 23 | 24 | interface SendToResult { 25 | error?: string | Error; 26 | result?: any; 27 | } 28 | 29 | // tslint:disable-next-line:class-name 30 | interface ioBrokerSocket { 31 | io(): any; 32 | emit( 33 | command: "subscribeObjects", 34 | pattern: string, 35 | callback?: (err?: string) => void | Promise, 36 | ): void; 37 | emit( 38 | command: "subscribeStates", 39 | pattern: string, 40 | callback?: (err?: string) => void | Promise, 41 | ): void; 42 | emit( 43 | command: "unsubscribeObjects", 44 | pattern: string, 45 | callback?: (err?: string) => void | Promise, 46 | ): void; 47 | emit( 48 | command: "unsubscribeStates", 49 | pattern: string, 50 | callback?: (err?: string) => void | Promise, 51 | ): void; 52 | 53 | emit( 54 | event: "getObjectView", 55 | view: "system", 56 | type: "device", 57 | options: ioBroker.GetObjectViewParams, 58 | callback: ( 59 | err: string | undefined, 60 | result?: any, 61 | ) => void | Promise, 62 | ): void; 63 | emit( 64 | event: "getStates", 65 | callback: ( 66 | err: string | undefined, 67 | result?: Record, 68 | ) => void, 69 | ): void; 70 | emit( 71 | event: "getState", 72 | id: string, 73 | callback: (err: string | undefined, result?: ioBroker.State) => void, 74 | ): void; 75 | emit( 76 | event: "setState", 77 | id: string, 78 | state: unknown, 79 | callback: (err: string | undefined, result?: any) => void, 80 | ): void; 81 | 82 | on(event: "objectChange", handler: ioBroker.ObjectChangeHandler): void; 83 | on(event: "stateChange", handler: ioBroker.StateChangeHandler): void; 84 | removeEventHandler( 85 | event: "objectChange", 86 | handler: ioBroker.ObjectChangeHandler, 87 | ): void; 88 | removeEventHandler( 89 | event: "stateChange", 90 | handler: ioBroker.StateChangeHandler, 91 | ): void; 92 | 93 | // TODO: other events 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">=18.0.0" 4 | }, 5 | "name": "iobroker.fronius-wattpilot", 6 | "version": "4.8.0", 7 | "description": "fronius-wattpilot", 8 | "author": { 9 | "name": "tim2zg", 10 | "email": "tim2zg@protonmail.com" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "tim2zg" 15 | }, 16 | { 17 | "name": "SebastianHanz" 18 | }, 19 | { 20 | "name": "derHaubi" 21 | } 22 | ], 23 | "homepage": "https://github.com/tim2zg/ioBroker.fronius-wattpilot", 24 | "license": "MIT", 25 | "keywords": [ 26 | "template", 27 | "home automation", 28 | "ioBroker", 29 | "wattpilot" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/tim2zg/ioBroker.fronius-wattpilot" 34 | }, 35 | "dependencies": { 36 | "@iobroker/adapter-core": "^3.3.2", 37 | "bcryptjs": "^3.0.3", 38 | "ws": "^8.18.3" 39 | }, 40 | "devDependencies": { 41 | "@alcalzone/release-script": "^3.8.0", 42 | "@alcalzone/release-script-plugin-iobroker": "^3.7.2", 43 | "@alcalzone/release-script-plugin-license": "^3.7.0", 44 | "@iobroker/adapter-dev": "^1.3.0", 45 | "@iobroker/eslint-config": "2.2.0", 46 | "@iobroker/testing": "^4.1.3", 47 | "@types/chai": "^4.3.1", 48 | "@types/chai-as-promised": "^7.1.5", 49 | "@types/mocha": "^9.1.1", 50 | "@types/node": "^14.18.18", 51 | "@types/proxyquire": "^1.3.28", 52 | "@types/sinon": "^10.0.11", 53 | "@types/sinon-chai": "^3.2.8", 54 | "@typescript-eslint/eslint-plugin": "^8.44.1", 55 | "@typescript-eslint/parser": "^8.44.1", 56 | "chai": "^4.3.6", 57 | "chai-as-promised": "^7.1.1", 58 | "eslint": "^9.14.0", 59 | "mocha": "^10.1.0", 60 | "proxyquire": "^2.1.3", 61 | "sinon": "^14.0.0", 62 | "sinon-chai": "^3.7.0", 63 | "typescript": "^5.6.3" 64 | }, 65 | "main": "main.js", 66 | "files": [ 67 | "admin{,/!(src)/**}/!(tsconfig|tsconfig.*).json", 68 | "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}", 69 | "lib/", 70 | "www/", 71 | "io-package.json", 72 | "LICENSE", 73 | "main.js" 74 | ], 75 | "scripts": { 76 | "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"", 77 | "test:package": "mocha test/package --exit", 78 | "test:unit": "mocha test/unit --exit", 79 | "test:integration": "mocha test/integration --exit", 80 | "test": "npm run test:js && npm run test:package", 81 | "check": "tsc --noEmit -p tsconfig.check.json", 82 | "lint": "eslint -c eslint.config.mjs .", 83 | "translate": "translate-adapter", 84 | "release": "release-script" 85 | }, 86 | "bugs": { 87 | "url": "https://github.com/tim2zg/ioBroker.fronius-wattpilot/issues" 88 | }, 89 | "readmeFilename": "README.md" 90 | } 91 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | # Run this job on all pushes and pull requests 4 | # as well as tags with a semantic version 5 | on: 6 | push: 7 | branches: 8 | - '*' 9 | tags: 10 | # normal versions 11 | - 'v[0-9]+.[0-9]+.[0-9]+' 12 | # pre-releases 13 | - 'v[0-9]+.[0-9]+.[0-9]+-**' 14 | pull_request: {} 15 | 16 | # Cancel previous PR/branch runs when a new commit is pushed 17 | concurrency: 18 | group: ${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | # Performs quick checks before the expensive test runs 23 | check-and-lint: 24 | if: contains(github.event.head_commit.message, '[skip ci]') == false 25 | 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: ioBroker/testing-action-check@v1 30 | with: 31 | node-version: '22.x' 32 | # Uncomment the following line if your adapter cannot be installed using 'npm ci' 33 | # install-command: 'npm install' 34 | lint: true 35 | 36 | # Runs adapter tests on all supported node versions and OSes 37 | adapter-tests: 38 | if: contains(github.event.head_commit.message, '[skip ci]') == false 39 | 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | matrix: 43 | node-version: [20.x, 22.x, 24.x] 44 | os: [ubuntu-latest, windows-latest, macos-latest] 45 | 46 | steps: 47 | - uses: ioBroker/testing-action-adapter@v1 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | os: ${{ matrix.os }} 51 | # Uncomment the following line if your adapter cannot be installed using 'npm ci' 52 | # install-command: 'npm install' 53 | 54 | # TODO: To enable automatic npm releases, create a token on npmjs.org 55 | # Enter this token as a GitHub secret (with name NPM_TOKEN) in the repository options 56 | # Then uncomment the following block: 57 | 58 | # # Deploys the final package to NPM 59 | # deploy: 60 | # needs: [check-and-lint, adapter-tests] 61 | # 62 | # # Trigger this step only when a commit on any branch is tagged with a version number 63 | # if: | 64 | # contains(github.event.head_commit.message, '[skip ci]') == false && 65 | # github.event_name == 'push' && 66 | # startsWith(github.ref, 'refs/tags/v') 67 | # 68 | # runs-on: ubuntu-latest 69 | # 70 | # # Write permissions are required to create Github releases 71 | # permissions: 72 | # contents: write 73 | # 74 | # steps: 75 | # - uses: ioBroker/testing-action-deploy@v1 76 | # with: 77 | # node-version: '18.x' 78 | # # Uncomment the following line if your adapter cannot be installed using 'npm ci' 79 | # # install-command: 'npm install' 80 | # npm-token: ${{ secrets.NPM_TOKEN }} 81 | # github-token: ${{ secrets.GITHUB_TOKEN }} 82 | # 83 | # # When using Sentry for error reporting, Sentry can be informed about new releases 84 | # # To enable create a API-Token in Sentry (User settings, API keys) 85 | # # Enter this token as a GitHub secret (with name SENTRY_AUTH_TOKEN) in the repository options 86 | # # Then uncomment and customize the following block: 87 | ## sentry: true 88 | ## sentry-token: ${{ secrets.SENTRY_AUTH_TOKEN }} 89 | ## sentry-project: "iobroker-pid" 90 | ## sentry-version-prefix: "iobroker.pid" 91 | ## # If your sentry project is linked to a GitHub repository, you can enable the following option 92 | ## # sentry-github-integration: true 93 | -------------------------------------------------------------------------------- /admin/index_m.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | 73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 | 85 |
86 | 87 | 88 |
89 | 90 |
91 | 92 | 93 |
94 | 95 |
96 | 97 |
98 | Enable only if your device/firmware requires bcrypt
99 |
100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 | 108 |
109 | Uncheck to get any data available from API
110 |
111 | 112 |
113 | 114 |
115 | Not recommended in a local environment
116 |
117 | 118 |
119 | 120 |
121 | only needed if Only read most... is checked
122 |
123 |
124 | 125 |
126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /admin/words.js: -------------------------------------------------------------------------------- 1 | /*global systemDictionary:true */ 2 | "use strict"; 3 | 4 | systemDictionary = { 5 | "fronius-wattpilot adapter settings": { 6 | "en": "Adapter settings for fronius-wattpilot", 7 | "de": "Adaptereinstellungen für fronius-wattpilot", 8 | "ru": "Настройки адаптера для fronius-wattpilot", 9 | "pt": "Configurações do adaptador para fronius-wattpilot", 10 | "nl": "Adapterinstellingen voor fronius-wattpilot", 11 | "fr": "Paramètres d'adaptateur pour fronius-wattpilot", 12 | "it": "Impostazioni dell'adattatore per fronius-wattpilot", 13 | "es": "Ajustes del adaptador para fronius-wattpilot", 14 | "pl": "Ustawienia adaptera dla fronius-wattpilot", 15 | "zh-cn": "fronius-wattpilot的适配器设置" 16 | }, 17 | "IP-Address (If you use Cloud leave blank)": { 18 | "en": "IP-Address (If you use Cloud leave blank)", 19 | "de": "IP-Adresse (Bei Verwendung der Cloud, hier nichts eintragen)", 20 | "ru": "IP-адрес (Если вы используете облако оставить пустым)", 21 | "pt": "Endereço IP (Se você usar Nuvem deixa em branco)", 22 | "nl": "IP-Addres Als je Cloud gebruikt, laat dan los", 23 | "fr": "IP-Address (Si vous utilisez Cloud leave blank)", 24 | "it": "Indirizzo IP (Se si utilizza Cloud lasciare vuoto)", 25 | "es": "IP-Address (Si usas Cloud Leave blank)", 26 | "pl": "IP-Address (Jeśli używamy pustego rzutu)", 27 | "zh-cn": "IP-Address (请你使用Cloud 空白)" 28 | }, 29 | "Not recommended in a local environment": { 30 | "en": "Not recommended in a local environment", 31 | "de": "Nicht empfohlen in einer lokalen Umgebung", 32 | "ru": "Не рекомендуется в локальной среде", 33 | "pt": "Não recomendado em um ambiente local", 34 | "nl": "Niet aanbevolen in een lokale omgeving", 35 | "fr": "Non recommandé dans un environnement local", 36 | "it": "Non raccomandato in un ambiente locale", 37 | "es": "No recomendado en un entorno local", 38 | "pl": "Nie rekomendowany w środowisku lokalnym", 39 | "zh-cn": "地方环境建议" 40 | }, 41 | "Only read most common data-points": { 42 | "en": "Only read most common data-points", 43 | "de": "Nur die gängigsten Datenpunkte lesen", 44 | "ru": "Только читайте наиболее распространенные данные", 45 | "pt": "Apenas leia os datapoints mais comuns", 46 | "nl": "Lees alleen de meeste gemeenschappelijke datapoints", 47 | "fr": "Seulement lire les points de données les plus courants", 48 | "it": "Leggere solo i punti di dati più comuni", 49 | "es": "Sólo leen los puntos de datos más comunes", 50 | "pl": "Przeczytany jedynie na podstawie danych", 51 | "zh-cn": "只有阅读最常见的数据点" 52 | }, 53 | "Password": { 54 | "en": "Password", 55 | "de": "Passwort", 56 | "ru": "Пароль", 57 | "pt": "Senha", 58 | "nl": "Wachtwoord", 59 | "fr": "Mot de passe", 60 | "it": "Password", 61 | "es": "Contraseña", 62 | "pl": "Password", 63 | "zh-cn": "护照" 64 | }, 65 | "Serial Number (Only in Cloud mode)": { 66 | "en": "Serial Number (Only in Cloud mode)", 67 | "de": "Seriennummer (nur im Cloud-Modus)", 68 | "ru": "Серийный номер (только в облачном режиме)", 69 | "pt": "Número de série (apenas no modo Cloud)", 70 | "nl": "Serienummer (Only in Cloud mode)", 71 | "fr": "Numéro de série (uniquement en mode Cloud)", 72 | "it": "Numero seriale (solo in modalità Cloud)", 73 | "es": "Número de serie (sólo en modo Cloud)", 74 | "pl": "Serial Number (ang.)", 75 | "zh-cn": "数量(在Cloud模式)" 76 | }, 77 | "Uncheck to get any data available from API": { 78 | "en": "Uncheck to get any data available from API", 79 | "de": "Deaktivieren um alle verfügbaren Daten der API auszulesen", 80 | "ru": "Не проверять, чтобы получить какие-либо данные, доступные из API", 81 | "pt": "Desmarque para obter quaisquer dados disponíveis a partir da API", 82 | "nl": "Oncontroleer alle gegevens van API", 83 | "fr": "Décochez pour obtenir toutes les données disponibles de l'API", 84 | "it": "Deselezionare per ottenere qualsiasi dato disponibile da API", 85 | "es": "Desmarque para obtener los datos disponibles en la API", 86 | "pl": "Nie sprawdzaj czy dane dostępne są z API", 87 | "zh-cn": "核对获得非专利计划提供的任何数据" 88 | }, 89 | "Use cloud to get data from the Pilot": { 90 | "en": "Use cloud to get data from the Pilot", 91 | "de": "Cloud verwenden, um Daten vom Pilot zu erhalten", 92 | "ru": "Используйте облако, чтобы получить данные с пилота", 93 | "pt": "Use nuvem para obter dados do piloto", 94 | "nl": "Gebruik wolk om Data van de Pilot te halen", 95 | "fr": "Utilisez le nuage pour obtenir des données du pilote", 96 | "it": "Usa cloud per ottenere i dati dal pilota", 97 | "es": "Utilice la nube para obtener datos del Piloto", 98 | "pl": "Użycie chmur dostarczenia danych z pilota", 99 | "zh-cn": "利用云雾器从试验中获得数据" 100 | }, 101 | "only needed if Only read most... is checked": { 102 | "en": "only needed if \"Only read most...\" is checked", 103 | "de": "nur benötigt, wenn \"nur am meisten lesen\". wird geprüft", 104 | "ru": "только нужно, если \"Только прочитайте больше\". проверяется", 105 | "pt": "só precisava se \"Somente ler mais\". é verificado", 106 | "nl": "alleen nodig als \"lezen het meest\". wordt gecontroleerd", 107 | "fr": "seulement nécessaire si \"Only read most\". est vérifiée", 108 | "it": "solo bisogno se \"Solo leggere la maggior parte\". è controllato", 109 | "es": "sólo se necesita si \"sólo leer la mayoría\". Se ha comprobado", 110 | "pl": "zdecydowano tylko wtedy, gdy „Only read most”. sprawdzić", 111 | "uk": "тільки потрібно, якщо \"Тільки прочитати більшість\". перевіряється", 112 | "zh-cn": "只需要“最阅读”。 检查" 113 | }, 114 | "Use bcrypt for authentication (instead of PBKDF2)": { 115 | "en": "Use bcrypt for authentication (instead of PBKDF2)", 116 | "de": "Bcrypt für die Authentifizierung verwenden (statt PBKDF2)", 117 | "ru": "Использовать bcrypt для аутентификации (вместо PBKDF2)", 118 | "pt": "Use bcrypt para autenticação (em vez de PBKDF2)", 119 | "nl": "Gebruik bcrypt voor authenticatie (in plaats van PBKDF2)", 120 | "fr": "Utiliser bcrypt pour l'authentification (au lieu de PBKDF2)", 121 | "it": "Usa bcrypt per l'autenticazione (invece di PBKDF2)", 122 | "es": "Usar bcrypt para autenticación (en lugar de PBKDF2)", 123 | "pl": "Użyj bcrypt do uwierzytelniania (zamiast PBKDF2)", 124 | "zh-cn": "使用 bcrypt 进行身份验证(代替 PBKDF2)" 125 | }, 126 | "Enable only if your device/firmware requires bcrypt": { 127 | "en": "Enable only if your device/firmware requires bcrypt", 128 | "de": "Nur aktivieren, wenn Ihr Gerät/Firmware bcrypt benötigt", 129 | "ru": "Включайте только если ваше устройство/прошивка требует bcrypt", 130 | "pt": "Ativar apenas se seu dispositivo/firmware exigir bcrypt", 131 | "nl": "Alleen inschakelen als uw apparaat/firmware bcrypt vereist", 132 | "fr": "Activer uniquement si votre appareil/firmware nécessite bcrypt", 133 | "it": "Abilita solo se il tuo dispositivo/firmware richiede bcrypt", 134 | "es": "Habilitar solo si su dispositivo/firmware requiere bcrypt", 135 | "pl": "Włącz tylko, jeśli urządzenie/firmware wymaga bcrypt", 136 | "zh-cn": "仅在设备/固件需要 bcrypt 时启用" 137 | } 138 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/fronius-wattpilot.png) 2 | # ioBroker.fronius-wattpilot 3 | 4 | [![NPM version](https://img.shields.io/npm/v/iobroker.fronius-wattpilot.svg)](https://www.npmjs.com/package/iobroker.fronius-wattpilot) 5 | [![Downloads](https://img.shields.io/npm/dm/iobroker.fronius-wattpilot.svg)](https://www.npmjs.com/package/iobroker.fronius-wattpilot) 6 | ![Number of Installations](https://iobroker.live/badges/fronius-wattpilot-installed.svg) 7 | ![Current version in stable repository](https://iobroker.live/badges/fronius-wattpilot-stable.svg) 8 | 9 | [![NPM](https://nodei.co/npm/iobroker.fronius-wattpilot.png?downloads=true)](https://nodei.co/npm/iobroker.fronius-wattpilot/) 10 | 11 | **Tests:** ![Test and Release](https://github.com/tim2zg/ioBroker.fronius-wattpilot/workflows/Test%20and%20Release/badge.svg) 12 | 13 | [Zur deutschen Version der Dokumentation](README_DE.md) 14 | 15 | ## What is this adapter? 16 | 17 | This adapter integrates your Fronius Wattpilot EV charger with ioBroker, allowing you to monitor and control your charging station. The Wattpilot is an intelligent electric vehicle charging solution that can be integrated into your smart home system. 18 | 19 | **🌟 Key Features:** 20 | - Real-time monitoring of charging status 21 | - Remote control of charging parameters 22 | - Cloud and local connection support 23 | 24 | ## Installation and Setup 25 | 26 | ### Prerequisites 27 | 28 | Before installing the adapter, you need to set up your Wattpilot: 29 | 30 | 1. **Complete Wattpilot Setup**: Finish the initial setup using the official Fronius Wattpilot app and **remember your password** 31 | 2. **Connect to WiFi**: In the app, go to the "Internet" tab and connect your Wattpilot to your WiFi network 32 | 3. **Find IP Address**: You'll need your Wattpilot's IP address using one of these methods: 33 | - **Router Method**: Check your router's web interface for connected devices 34 | - **App Method**: In the Wattpilot app, tap on the WiFi name after connection. You'll see network details including the IP address 35 | 36 | > 💡 **Important**: It's highly recommended to assign a static IP address to your Wattpilot in your router settings to prevent connection issues. 37 | 38 | ### Adapter Installation 39 | 40 | 1. Install the adapter from the ioBroker "Adapters" page 41 | 2. Create a new instance of the fronius-wattpilot adapter 42 | 3. In the instance configuration: 43 | - Enter your Wattpilot's **IP address** 44 | - Enter your Wattpilot **password** 45 | - Configure other settings as needed 46 | 4. Save the configuration 47 | 48 | If everything is configured correctly, the adapter will connect and start creating data points. 49 | 50 | ## How to Use the Adapter 51 | 52 | ### Reading Data 53 | 54 | The adapter automatically creates data points for all Wattpilot values. You can use these like any other data points in ioBroker for: 55 | - Visualization in VIS or other frontends 56 | - Logic in scripts and Blockly 57 | - Automation rules 58 | 59 | **Data Modes:** 60 | - **Key Points Only** (default): Shows only the most important values 61 | - **All Values**: Uncheck the "Key points only" option to see all available API data 62 | 63 | 📖 Full API documentation: [Wattpilot API Documentation](https://github.com/joscha82/wattpilot/blob/main/API.md) (Thanks to joscha82) 64 | 65 | ### Controlling Your Wattpilot 66 | 67 | #### Direct State Control (NEW!) 68 | 69 | You can now directly control important Wattpilot functions by writing to the states. 70 | 71 | #### Advanced Control via set_state 72 | 73 | For more advanced control, use the `set_state` data point with this format: 74 | ``` 75 | stateName;value 76 | ``` 77 | 78 | **Available states:** 79 | - **amp**: `6-16` (charging current in Amperes) 80 | - **cae**: `true` or `false` (⚠️ disables cloud functionality - may require restart) 81 | 82 | **Examples:** 83 | ``` 84 | amp;10 // Set charging current to 10A 85 | ``` 86 | 87 | ## Examples and Use Cases 88 | 89 | ### Solar Integration Example 90 | 91 | Check out our [Blockly example](https://github.com/tim2zg/ioBroker.fronius-wattpilot/blob/main/examples/example-Blockly.xml) that shows how to: 92 | - Monitor your solar power production 93 | - Automatically adjust Wattpilot charging current based on excess solar power 94 | 95 | **How to use the example:** 96 | 1. Copy the content from the example file 97 | 2. In ioBroker Blockly, click the "Import blocks" icon (upper right corner) 98 | 3. Paste the content and adapt it to your setup 99 | 100 | ### Common Automations 101 | 102 | - **Time-based charging**: Start charging during off-peak hours 103 | - **Solar surplus charging**: Charge only when excess solar power is available 104 | - **Presence detection**: Start/stop charging based on car presence 105 | - **Load balancing**: Adjust charging current based on household power consumption 106 | 107 | ## Technical Details 108 | 109 | The adapter connects to the Wattpilot's WebSocket interface and converts incoming data into ioBroker data points. It supports both local WiFi connections and cloud-based connections. 110 | 111 | **Connection Types:** 112 | - **Local WiFi** (recommended): Direct connection to your Wattpilot 113 | - **Cloud**: Connection via Fronius cloud services 114 | 115 | ## Troubleshooting 116 | 117 | **Common Issues:** 118 | - **Connection failed**: Check IP address and password 119 | - **Frequent disconnections**: Assign a static IP to your Wattpilot 120 | - **Missing data points**: Try enabling "All Values" mode 121 | - **Cloud connection issues**: Verify the `cae` setting 122 | 123 | **⚠️ Disclaimer:** This adapter uses unofficial APIs. Use at your own risk and be careful when modifying settings that could affect your device's operation. 124 | 125 | ## Developers 126 | 127 | - [SebastianHanz](https://github.com/SebastianHanz) 128 | - [tim2zg](https://github.com/tim2zg) 129 | - [derHaubi](https://github.com/derHaubi) 130 | 131 | ## Changelog 132 | 133 | 137 | ### 4.8.0 (2025-11-29) 138 | - Integrated working bcrypt algorithm 139 | 140 | ### 4.7.0 (2025-06-19) 141 | - Rewrite of the adapter 142 | - Added ability to set states directly 143 | - Added ability to set common states directly 144 | - Fix all issues 145 | 146 | ### 4.6.3 (2023-12-24) 147 | - Fixed a bug where the adapter would use a undefined variable 148 | - Fixed bug #44 149 | - Fixed bug #43 150 | 151 | ### 4.6.2 (2023-08-15) 152 | - Thanks to Norb1204 for fixing a few bugs that I missed. More in Issue #40 153 | 154 | ### 4.6.1 (2023-08-15) 155 | - Fixed Issue #39 (set_state not working) 156 | 157 | ### 4.6.0 (2023-07-15) 158 | - Fixed timeout issue in normal parser mode (#36), still exist in dynamic parser mode --> use no timeout (0) 159 | - Fixed a number of issues concerning the static parser mode 160 | - Quality of life improvements --> you can now set the common states directly! (set_power, set_mode) are still available for compatibility reasons and for the dynamic parser mode 161 | 162 | ### 4.5.1 (2023-03-02) 163 | - Fixed issue #29 (custom states not working) 164 | 165 | ### 4.5.0 (2023-02-19) 166 | - Fixed random log messages 167 | - Fixed a type conflict at the set_state state 168 | - Commits from now on should be signed 169 | 170 | ### 4.4.0 (2023-02-16) 171 | - known states will now be updated even if the dynamic parser is enabled 172 | 173 | ### 4.3.0 (2023-01-14) 174 | - dependency updates 175 | - state updates 176 | 177 | ### 4.2.1 (2023-01-05) 178 | - Fixed bug in the all values mode / parser 179 | 180 | ### 4.2.0 (2023-01-01) 181 | - Some QoL improvements 182 | 183 | ### 4.1.0 (2022-12-30) 184 | - Added the possibility to add states manually via the instance-settings 185 | - Fixed the bug where the adapter didn't set the correct value types 186 | - Added some quality of life improvements 187 | 188 | ### 4.0.0 (2022-11-30) 189 | - Fixed timing issue 190 | - Added set_power and set_mode states 191 | 192 | ### 3.3.1 (2022-11-17) 193 | - Fixed a bug where set_state was not writable 194 | 195 | ### 3.3.0 (2022-11-17) 196 | - Fixed a bug where the adapter would not set the correct labels for the states 197 | - Performance improvements 198 | - Fixed dependencies 199 | 200 | ### 3.2.5 (2022-10-14) 201 | - Small changes to the package.json and io-package.json 202 | 203 | ### 3.2.4 (2022-10-11) 204 | - Fiexed cool down timer for normal values 205 | 206 | ### 3.2.3 (2022-10-08) 207 | - Bug fixed where the adapter would not respect the timout timer and would try to constantly reconnect to the WattPilot 208 | - Bug fixed where the adapter would send a wrong disconnect message to the WattPilot 209 | 210 | ### 3.2.2 (2022-10-06) 211 | - Fixed reconnecting frequency 212 | - Fixed multiple Websocket connections 213 | - Added frequency handler 214 | 215 | ### 3.2.1 (2022-10-02) 216 | - Fixed reconnecting to the WebSocket 217 | - Restructured the code 218 | 219 | ### 3.2.0 (2022-09-29) 220 | - Implemented reconnecting 221 | - Shrank code down 222 | 223 | ### 3.1.0 (2022-09-07) 224 | - Added the option to use the cloud as a datasource 225 | - Updated GitHub workflows 226 | 227 | ### 3.0.0 (2022-09-04) 228 | - Updated README.md 229 | - Created "examples"-directory for sample applications 230 | - Added some translations 231 | - Renamed checkbox "Parser" to something more intuitive 232 | - Fixxed #4: Datapoint "map" now gets created correctly 233 | - Fixxed #5: Password-characters are no longer visible 234 | - Fixxed type conflict of cableType 235 | 236 | ### 2.2.4 (2022-09-01) 237 | - SebastianHanz fixed infinite RAM-usage 238 | - added some description 239 | 240 | ### 2.2.3 (2022-08-30) 241 | - SebastianHanz fixed type-conflicts. Thank you! 242 | 243 | ### 2.2.2 (2022-08-25) 244 | - Bug fixes 245 | 246 | ### 2.2.1 (2022-08-22) 247 | - Bug fixes 248 | 249 | ### 2.2.0 (2022-08-21) 250 | - Fixed Bugs 251 | 252 | ### 2.1.0 (2022-08-19) 253 | - Min Node Version 16 254 | 255 | ### 2.0.3 (2022-07-20) 256 | - Updated Readme 257 | 258 | ### 2.0.2 (2022-07-12) 259 | - Bug fixed 260 | 261 | ### 2.0.1 (2022-07-10) 262 | - Added a how to install. Not to detail because currently not in stable repo. 263 | 264 | ### 2.0.0 (2022-07-10) 265 | - Fixed NPM Versions hopefully 266 | 267 | ### 1.1.0 (2022-07-10) 268 | - Added UselessPV and TimeStamp Parser, did some testing. 269 | 270 | ### 1.0.1 (2022-06-02) 271 | - Tests 272 | 273 | ### 1.0.0 (2022-06-02) 274 | 275 | - Did some changes 276 | - Did some more changes 277 | 278 | ### 0.0.5 (2020-01-01) 279 | - Better Code 280 | 281 | ### 0.0.4 (2020-01-01) 282 | - Parser option added 283 | 284 | ### 0.0.3 (2020-01-01) 285 | - Parser added 286 | 287 | ### 0.0.2 (2020-01-01) 288 | - Bug fixed 289 | 290 | ### 0.0.1 (2020-01-01) 291 | - Initial release 292 | 293 | ## License 294 | MIT License 295 | 296 | Copyright (c) 2025 tim2zg 297 | 298 | Permission is hereby granted, free of charge, to any person obtaining a copy 299 | of this software and associated documentation files (the "Software"), to deal 300 | in the Software without restriction, including without limitation the rights 301 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 302 | copies of the Software, and to permit persons to whom the Software is 303 | furnished to do so, subject to the following conditions: 304 | 305 | The above copyright notice and this permission notice shall be included in all 306 | copies or substantial portions of the Software. 307 | 308 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 309 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 310 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 311 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 312 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 313 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 314 | SOFTWARE. 315 | -------------------------------------------------------------------------------- /README_DE.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/fronius-wattpilot.png) 2 | # ioBroker.fronius-wattpilot 3 | 4 | [![NPM version](https://img.shields.io/npm/v/iobroker.fronius-wattpilot.svg)](https://www.npmjs.com/package/iobroker.fronius-wattpilot) 5 | [![Downloads](https://img.shields.io/npm/dm/iobroker.fronius-wattpilot.svg)](https://www.npmjs.com/package/iobroker.fronius-wattpilot) 6 | ![Number of Installations](https://iobroker.live/badges/fronius-wattpilot-installed.svg) 7 | ![Current version in stable repository](https://iobroker.live/badges/fronius-wattpilot-stable.svg) 8 | 9 | [![NPM](https://nodei.co/npm/iobroker.fronius-wattpilot.png?downloads=true)](https://nodei.co/npm/iobroker.fronius-wattpilot/) 10 | 11 | **Tests:** ![Test and Release](https://github.com/tim2zg/ioBroker.fronius-wattpilot/workflows/Test%20and%20Release/badge.svg) 12 | 13 | [To the english version of the README](README_DE.md) 14 | 15 | ## Was ist dieser Adapter? 16 | 17 | Dieser Adapter integriert Ihren Fronius Wattpilot EV-Ladegerät mit ioBroker und ermöglicht es Ihnen, Ihre Ladestation zu überwachen und zu steuern. Der Wattpilot ist eine intelligente Ladelösung für Elektrofahrzeuge, die in Ihr Smart-Home-System integriert werden kann. 18 | 19 | **🌟 Hauptfunktionen:** 20 | - Echtzeitüberwachung des Ladestatus 21 | - Fernsteuerung der Ladeparameter 22 | - Unterstützung für Cloud- und lokale Verbindungen 23 | 24 | ## Installation und Einrichtung 25 | 26 | ### Voraussetzungen 27 | 28 | Vor der Installation des Adapters müssen Sie Ihren Wattpilot einrichten: 29 | 30 | 1. **Wattpilot-Einrichtung abschließen**: Beenden Sie die Ersteinrichtung mit der offiziellen Fronius Wattpilot App und **merken Sie sich Ihr Passwort** 31 | 2. **Mit WiFi verbinden**: Gehen Sie in der App zum "Internet"-Tab und verbinden Sie Ihren Wattpilot mit Ihrem WiFi-Netzwerk 32 | 3. **IP-Adresse finden**: Sie benötigen die IP-Adresse Ihres Wattpilot mit einer dieser Methoden: 33 | - **Router-Methode**: Prüfen Sie die Weboberfläche Ihres Routers für verbundene Geräte 34 | - **App-Methode**: Tippen Sie in der Wattpilot App nach der Verbindung auf den WiFi-Namen. Sie sehen dann die Netzwerkdetails einschließlich der IP-Adresse 35 | 36 | > 💡 **Wichtig**: Es wird dringend empfohlen, eine statische IP-Adresse für Ihren Wattpilot in den Router-Einstellungen zu vergeben, um Verbindungsprobleme zu vermeiden. 37 | 38 | ### Adapter-Installation 39 | 40 | 1. Installieren Sie den Adapter von der ioBroker "Adapter"-Seite 41 | 2. Erstellen Sie eine neue Instanz des fronius-wattpilot Adapters 42 | 3. In der Instanzkonfiguration: 43 | - Geben Sie die **IP-Adresse** Ihres Wattpilot ein 44 | - Geben Sie Ihr Wattpilot **Passwort** ein 45 | - Konfigurieren Sie andere Einstellungen nach Bedarf 46 | 4. Speichern Sie die Konfiguration 47 | 48 | Wenn alles korrekt konfiguriert ist, wird sich der Adapter verbinden und beginnen, Datenpunkte zu erstellen. 49 | 50 | ## Wie Sie den Adapter verwenden 51 | 52 | ### Daten lesen 53 | 54 | Der Adapter erstellt automatisch Datenpunkte für alle Wattpilot-Werte. Sie können diese wie alle anderen Datenpunkte in ioBroker verwenden für: 55 | - Visualisierung in VIS oder anderen Frontends 56 | - Logik in Skripten und Blockly 57 | - Automatisierungsregeln 58 | 59 | **Datenmodi:** 60 | - **Nur Schlüsselpunkte** (Standard): Zeigt nur die wichtigsten Werte 61 | - **Alle Werte**: Deaktivieren Sie die Option "Nur Schlüsselpunkte", um alle verfügbaren API-Daten zu sehen 62 | 63 | 📖 Vollständige API-Dokumentation: [Wattpilot API-Dokumentation](https://github.com/joscha82/wattpilot/blob/main/API.md) (Dank an joscha82) 64 | 65 | ### Steuerung Ihres Wattpilot 66 | 67 | #### Direkte Zustandssteuerung (NEU!) 68 | 69 | Sie können jetzt wichtige Wattpilot-Funktionen direkt steuern, indem Sie in die Zustände schreiben. 70 | 71 | #### Erweiterte Steuerung über set_state 72 | 73 | Für erweiterte Steuerung verwenden Sie den `set_state` Datenpunkt mit diesem Format: 74 | ``` 75 | zustandsName;wert 76 | ``` 77 | 78 | **Verfügbare Zustände:** 79 | - **amp**: `6-16` (Ladestrom in Ampere) 80 | - **cae**: `true` oder `false` (⚠️ deaktiviert Cloud-Funktionalität - kann Neustart erfordern) 81 | 82 | **Beispiele:** 83 | ``` 84 | amp;10 // Ladestrom auf 10A setzen 85 | ``` 86 | 87 | ## Beispiele und Anwendungsfälle 88 | 89 | ### Solar-Integrations-Beispiel 90 | 91 | Schauen Sie sich unser [Blockly-Beispiel](https://github.com/tim2zg/ioBroker.fronius-wattpilot/blob/main/examples/example-Blockly.xml) an, das zeigt, wie Sie: 92 | - Ihre Solarstromerzeugung überwachen 93 | - Den Wattpilot-Ladestrom automatisch basierend auf überschüssiger Solarenergie anpassen 94 | 95 | **So verwenden Sie das Beispiel:** 96 | 1. Kopieren Sie den Inhalt aus der Beispieldatei 97 | 2. Klicken Sie in ioBroker Blockly auf das "Blöcke importieren"-Symbol (obere rechte Ecke) 98 | 3. Fügen Sie den Inhalt ein und passen Sie ihn an Ihr Setup an 99 | 100 | ### Häufige Automatisierungen 101 | 102 | - **Zeitbasiertes Laden**: Laden während Schwachlastzeiten starten 103 | - **Solar-Überschuss-Laden**: Nur laden, wenn überschüssige Solarenergie verfügbar ist 104 | - **Anwesenheitserkennung**: Laden basierend auf Auto-Anwesenheit starten/stoppen 105 | - **Lastausgleich**: Ladestrom basierend auf Haushalts-Stromverbrauch anpassen 106 | 107 | ## Technische Details 108 | 109 | Der Adapter verbindet sich mit der WebSocket-Schnittstelle des Wattpilot und konvertiert eingehende Daten in ioBroker-Datenpunkte. Er unterstützt sowohl lokale WiFi-Verbindungen als auch Cloud-basierte Verbindungen. 110 | 111 | **Verbindungstypen:** 112 | - **Lokales WiFi** (empfohlen): Direkte Verbindung zu Ihrem Wattpilot 113 | - **Cloud**: Verbindung über Fronius Cloud-Services 114 | 115 | ## Fehlerbehebung 116 | 117 | **Häufige Probleme:** 118 | - **Verbindung fehlgeschlagen**: Prüfen Sie IP-Adresse und Passwort 119 | - **Häufige Verbindungsabbrüche**: Weisen Sie Ihrem Wattpilot eine statische IP zu 120 | - **Fehlende Datenpunkte**: Versuchen Sie den "Alle Werte"-Modus zu aktivieren 121 | - **Cloud-Verbindungsprobleme**: Überprüfen Sie die `cae`-Einstellung 122 | 123 | **⚠️ Haftungsausschluss:** Dieser Adapter verwendet inoffizielle APIs. Verwenden Sie ihn auf eigene Gefahr und seien Sie vorsichtig beim Ändern von Einstellungen, die den Betrieb Ihres Geräts beeinträchtigen könnten. 124 | 125 | ## Entwickler 126 | 127 | - [SebastianHanz](https://github.com/SebastianHanz) 128 | - [tim2zg](https://github.com/tim2zg) 129 | - [derHaubi](https://github.com/derHaubi) 130 | 131 | ## Changelog 132 | 133 | 137 | 138 | ### 4.7.0 (2025-06-19) 139 | - Neuschreibung des Adapters 140 | - Hinzugefügte Möglichkeit, Zustände direkt zu setzen 141 | - Hinzugefügte Möglichkeit, allgemeine Zustände direkt zu setzen 142 | - Alle Probleme behoben 143 | 144 | ### 4.6.3 (2023-12-24) 145 | - Einen Fehler behoben, bei dem der Adapter eine undefinierte Variable verwenden würde 146 | - Fehler #44 behoben 147 | - Fehler #43 behoben 148 | 149 | ### 4.6.2 (2023-08-15) 150 | - Dank an Norb1204 für die Behebung einiger Fehler, die ich übersehen hatte. Mehr in Issue #40 151 | 152 | ### 4.6.1 (2023-08-15) 153 | - Issue #39 behoben (set_state funktioniert nicht) 154 | 155 | ### 4.6.0 (2023-07-15) 156 | - Timeout-Problem im normalen Parser-Modus behoben (#36), existiert noch im dynamischen Parser-Modus --> verwenden Sie kein Timeout (0) 157 | - Eine Reihe von Problemen bezüglich des statischen Parser-Modus behoben 158 | - Verbesserungen der Lebensqualität --> Sie können jetzt die allgemeinen Zustände direkt setzen! (set_power, set_mode) sind aus Kompatibilitätsgründen und für den dynamischen Parser-Modus weiterhin verfügbar 159 | 160 | ### 4.5.1 (2023-03-02) 161 | - Problem #29 behoben (benutzerdefinierte Zustände funktionieren nicht) 162 | 163 | ### 4.5.0 (2023-02-19) 164 | - Zufällige Log-Nachrichten behoben 165 | - Einen Typkonflikt beim set_state Zustand behoben 166 | - Commits sollten ab sofort signiert sein 167 | 168 | ### 4.4.0 (2023-02-16) 169 | - Bekannte Zustände werden jetzt aktualisiert, auch wenn der dynamische Parser aktiviert ist 170 | 171 | ### 4.3.0 (2023-01-14) 172 | - Abhängigkeits-Updates 173 | - Zustands-Updates 174 | 175 | ### 4.2.1 (2023-01-05) 176 | - Fehler im Alle-Werte-Modus / Parser behoben 177 | 178 | ### 4.2.0 (2023-01-01) 179 | - Einige QoL-Verbesserungen 180 | 181 | ### 4.1.0 (2022-12-30) 182 | - Möglichkeit hinzugefügt, Zustände manuell über die Instanz-Einstellungen hinzuzufügen 183 | - Den Fehler behoben, bei dem der Adapter nicht die korrekten Werttypen setzte 184 | - Einige Verbesserungen der Lebensqualität hinzugefügt 185 | 186 | ### 4.0.0 (2022-11-30) 187 | - Timing-Problem behoben 188 | - set_power und set_mode Zustände hinzugefügt 189 | 190 | ### 3.3.1 (2022-11-17) 191 | - Einen Fehler behoben, bei dem set_state nicht beschreibbar war 192 | 193 | ### 3.3.0 (2022-11-17) 194 | - Einen Fehler behoben, bei dem der Adapter nicht die korrekten Labels für die Zustände setzte 195 | - Performance-Verbesserungen 196 | - Abhängigkeiten behoben 197 | 198 | ### 3.2.5 (2022-10-14) 199 | - Kleine Änderungen an package.json und io-package.json 200 | 201 | ### 3.2.4 (2022-10-11) 202 | - Abkühlungszeittimer für normale Werte behoben 203 | 204 | ### 3.2.3 (2022-10-08) 205 | - Fehler behoben, bei dem der Adapter den Timeout-Timer nicht respektierte und ständig versuchen würde, sich mit dem WattPilot zu verbinden 206 | - Fehler behoben, bei dem der Adapter eine falsche Trennnachricht an den WattPilot senden würde 207 | 208 | ### 3.2.2 (2022-10-06) 209 | - Wiederverbindungsfrequenz behoben 210 | - Mehrere WebSocket-Verbindungen behoben 211 | - Frequenz-Handler hinzugefügt 212 | 213 | ### 3.2.1 (2022-10-02) 214 | - Wiederverbindung zum WebSocket behoben 215 | - Code umstrukturiert 216 | 217 | ### 3.2.0 (2022-09-29) 218 | - Wiederverbindung implementiert 219 | - Code verkleinert 220 | 221 | ### 3.1.0 (2022-09-07) 222 | - Option hinzugefügt, die Cloud als Datenquelle zu verwenden 223 | - GitHub-Workflows aktualisiert 224 | 225 | ### 3.0.0 (2022-09-04) 226 | - README.md aktualisiert 227 | - "Beispiele"-Verzeichnis für Beispielanwendungen erstellt 228 | - Einige Übersetzungen hinzugefügt 229 | - Checkbox "Parser" zu etwas Intuitiverem umbenannt 230 | - #4 behoben: Datenpunkt "map" wird jetzt korrekt erstellt 231 | - #5 behoben: Passwort-Zeichen sind nicht mehr sichtbar 232 | - Typkonflikt von cableType behoben 233 | 234 | ### 2.2.4 (2022-09-01) 235 | - SebastianHanz behob unendlichen RAM-Verbrauch 236 | - etwas Beschreibung hinzugefügt 237 | 238 | ### 2.2.3 (2022-08-30) 239 | - SebastianHanz behob Typ-Konflikte. Vielen Dank! 240 | 241 | ### 2.2.2 (2022-08-25) 242 | - Fehlerbehebungen 243 | 244 | ### 2.2.1 (2022-08-22) 245 | - Fehlerbehebungen 246 | 247 | ### 2.2.0 (2022-08-21) 248 | - Fehler behoben 249 | 250 | ### 2.1.0 (2022-08-19) 251 | - Min Node Version 16 252 | 253 | ### 2.0.3 (2022-07-20) 254 | - Readme aktualisiert 255 | 256 | ### 2.0.2 (2022-07-12) 257 | - Fehler behoben 258 | 259 | ### 2.0.1 (2022-07-10) 260 | - Eine Installationsanleitung hinzugefügt. Nicht zu detailliert, da derzeit nicht im stabilen Repository. 261 | 262 | ### 2.0.0 (2022-07-10) 263 | - NPM-Versionen hoffentlich behoben 264 | 265 | ### 1.1.0 (2022-07-10) 266 | - UselessPV und TimeStamp Parser hinzugefügt, einige Tests durchgeführt. 267 | 268 | ### 1.0.1 (2022-06-02) 269 | - Tests 270 | 271 | ### 1.0.0 (2022-06-02) 272 | - Einige Änderungen vorgenommen 273 | - Einige weitere Änderungen vorgenommen 274 | 275 | ### 0.0.5 (2020-01-01) 276 | - Besserer Code 277 | 278 | ### 0.0.4 (2020-01-01) 279 | - Parser-Option hinzugefügt 280 | 281 | ### 0.0.3 (2020-01-01) 282 | - Parser hinzugefügt 283 | 284 | ### 0.0.2 (2020-01-01) 285 | - Fehler behoben 286 | 287 | ### 0.0.1 (2020-01-01) 288 | - Erste Veröffentlichung 289 | 290 | ## Lizenz 291 | MIT Lizenz 292 | 293 | Copyright (c) 2024 tim2zg 294 | 295 | Hiermit wird unentgeltlich jeder Person, die eine Kopie der Software und der zugehörigen Dokumentationen (die "Software") erhält, die Erlaubnis erteilt, sie uneingeschränkt zu nutzen, inklusive und ohne Ausnahme mit dem Recht, sie zu verwenden, zu kopieren, zu modifizieren, zu fusionieren, zu publizieren, zu verbreiten, zu unterlizenzieren und/oder zu verkaufen, und Personen, denen diese Software überlassen wird, diese Rechte zu verschaffen, unter den folgenden Bedingungen: 296 | 297 | Der obige Urheberrechtsvermerk und dieser Erlaubnisvermerk sind in allen Kopien oder Teilkopien der Software beizulegen. 298 | 299 | DIE SOFTWARE WIRD OHNE JEDE AUSDRÜCKLICHE ODER IMPLIZIERTE GARANTIE BEREITGESTELLT, EINSCHLIESSLICH DER GARANTIE ZUR BENUTZUNG FÜR DEN VORGESEHENEN ODER EINEN BESTIMMTEN ZWECK SOWIE JEGLICHER RECHTSVERLETZUNG, JEDOCH NICHT DARAUF BESCHRÄNKT. IN KEINEM FALL SIND DIE AUTOREN ODER COPYRIGHTINHABER FÜR JEGLICHEN SCHADEN ODER SONSTIGE ANSPRÜCHE HAFTBAR ZU MACHEN, OB INFOLGE DER ERFÜLLUNG EINES VERTRAGES, EINES DELIKTES ODER ANDERS IM ZUSAMMENHANG MIT DER SOFTWARE ODER SONSTIGER VERWENDUNG DER SOFTWARE ENTSTANDEN. -------------------------------------------------------------------------------- /io-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "name": "fronius-wattpilot", 4 | "version": "4.8.0", 5 | "news": { 6 | "4.8.0": { 7 | "en": "Integrated working bcrypt algorithm", 8 | "de": "Integrierter bcrypt-Algorithmus", 9 | "ru": "Интегрированный рабочий алгоритм Bcrypt", 10 | "pt": "Algoritmo de bcrypt de trabalho integrado", 11 | "nl": "Geïntegreerd werkbcrypt-algoritme", 12 | "fr": "Algorithme bcrypt intégré de travail", 13 | "it": "Algoritmo di bcrypt di lavoro integrato", 14 | "es": "Algoritmo de bcriptación de trabajo integrado", 15 | "pl": "Zintegrowany algorytm pracy bcrypt", 16 | "uk": "Інтегрований алгоритм обробки Bcrypt", 17 | "zh-cn": "综合工作 bcrypt 算法" 18 | }, 19 | "4.7.0": { 20 | "en": "Rewrite of the adapter\nAdded ability to set states directly\nAdded ability to set common states directly\nFix all issues", 21 | "de": "Rewrite des Adapters\nZusätzlich Fähigkeit, Zustände direkt einzustellen\nZusätzlich Fähigkeit, gemeinsame Staaten direkt zu setzen\nAlle Probleme beheben", 22 | "ru": "Переписать адаптер\nВозможность устанавливать состояния напрямую\nСпособность устанавливать общие состояния напрямую\nИсправить все вопросы", 23 | "pt": "Reescrever o adaptador\nAdicionada capacidade de definir estados diretamente\nAdicionada capacidade de definir estados comuns diretamente\nCorrigir todos os problemas", 24 | "nl": "Herschrijven van de adapter\nToegevoegd vermogen om toestanden direct in te stellen\nToegevoegd vermogen om gemeenschappelijke staten direct in te stellen\nAlle problemen oplossen", 25 | "fr": "Réécriture de l'adaptateur\nAjout de la capacité de définir les états directement\nCapacité accrue de définir directement des états communs\nCorrection de tous les problèmes", 26 | "it": "Riscrittura dell'adattatore\nAggiunta la capacità di impostare gli stati direttamente\nAggiunta la capacità di impostare stati comuni direttamente\nRisolvi tutti i problemi", 27 | "es": "Reescritura del adaptador\nPosibilidad de establecer estados directamente\nPosibilidad de establecer estados comunes directamente\nArreglar todos os problemas", 28 | "pl": "Przepisanie adaptera\nDodano możliwość bezpośredniego ustawiania stanów\nDodano możliwość bezpośredniego ustalania wspólnych państw\nNapraw wszystkie problemy", 29 | "uk": "Перезапис адаптера\nДодана можливість встановлювати стани безпосередньо\nДодана можливість встановлювати загальні стани безпосередньо\nВиправлення всіх питань", 30 | "zh-cn": "重写适配器\n添加直接设置状态的能力\n添加直接设定共同状态的能力\n解决所有问题" 31 | }, 32 | "4.6.3": { 33 | "en": "Fixed a bug where the adapter would use a undefined variable\nFixed bug #44\nFixed bug #43", 34 | "de": "Fehler behoben, bei dem der Adapter eine undefinierte Variable verwenden würde\nFehler behoben #44\nFehler behoben #43", 35 | "ru": "Исправлена ошибка, где адаптер будет использовать неопределенную переменную\nИсправлена ошибка #44\nИсправлена ошибка #43", 36 | "pt": "Corrigido um bug onde o adaptador usaria uma variável indefinida\nCorrigido o erro #44\nCorrigido o erro #43", 37 | "nl": "Een insect waar de ontvoerder een onbepaalde variabele\nQuality over Quantity (QoQ) Releases Vertaling:\nQuality over Quantity (QoQ) Releases Vertaling:", 38 | "fr": "Correction d'un bug où l'adaptateur utiliserait une variable undefined\nCorrection du bug #44\nCorrection du bug #43", 39 | "it": "Risolto un bug in cui l'adattatore avrebbe usato una variabile non definita\nCorretto bug #44\nCorretto bug #43", 40 | "es": "Arreglado un fallo donde el adaptador utilizaría uma variável indefinida\nError fijo #44\nError fijo #43", 41 | "pl": "Błękitnym błędem, w którym adapter użył nie określonej zmiennej\nFixed bug #44 (ang.)\nFixed bug #43 (ang.)", 42 | "uk": "Виправлено помилку, де адаптер використовуватиме не визначену змінну\nВиправлена помилка #44\nВиправлена помилка #43", 43 | "zh-cn": "确定一个配制,使适应者使用一个未界定的变量。\n九. 固定的乙ug #44\n九、导 言.. 43" 44 | }, 45 | "4.6.2": { 46 | "en": "Thanks to Norb1204 for fixing a few bugs that I missed. More in Issue #40", 47 | "de": "Dank Norb1204 für die Befestigung ein paar Fehler, die ich verpasst. Mehr in Ausgabe #40", 48 | "ru": "Спасибо Norb1204 за исправление нескольких ошибок, которые я пропустил. Подробнее в выпуске #40", 49 | "pt": "Graças a Norb1204 para corrigir alguns bugs que eu perdi. Mais na edição #40", 50 | "nl": "Dankzij Norb1204 voor het repareren van een paar insecten die ik gemist heb. Meer in Issue #40", 51 | "fr": "Merci à Norb1204 pour avoir corrigé quelques bugs que j'ai ratés. Plus en édition #40", 52 | "it": "Grazie a Norb1204 per aver risolto alcuni bug che ho perso. Altro in Numero 40", 53 | "es": "Gracias a Norb1204 por arreglar algunos errores que perdí. Más en Edición #40", 54 | "pl": "Dzięki Norb1204 za naprawę kilku błędów. W: #40", 55 | "uk": "Завдяки Norb1204 для фіксації декількох помилок, які я пропустив. Детальніше про випуск #40", 56 | "zh-cn": "由于诺卜1204年,我错过了几块黑暗。 第40号 章" 57 | }, 58 | "4.6.1": { 59 | "en": "Fixed Issue #39 (set_state not working)", 60 | "de": "Fixed Issue #39 (set_state not working)", 61 | "ru": "Фиксированный номер #39 (set_state не работает)", 62 | "pt": "Problema fixo #39 (set_state não funciona)", 63 | "nl": "Gerepareerd Issue 39 (setstate niet werken)", 64 | "fr": "Correction #39 (set_state not working)", 65 | "it": "Problema fisso #39 (set_state non funzionante)", 66 | "es": "Edición fija #39 (set_state no funciona)", 67 | "pl": "Numer #39 (set_state not working)", 68 | "uk": "Виправлено проблему #39 (set_state не працює)", 69 | "zh-cn": "九. 固定问题编号:国家不工作" 70 | }, 71 | "4.6.0": { 72 | "en": "Fixed timeout issue in normal parser mode (#36), still exist in dynamic parser mode --> use no timeout (0)\nFixed a number of issues concerning the static parser mode\nQuality of life improvements --> you can now set the common states directly! (set_power, set_mode) are still available for compatibility reasons and for the dynamic parser mode", 73 | "de": "Feste Timeout-Ausgabe im normalen Parser-Modus #(36), gibt es noch im dynamischen Parser-Modus --> keine Timeout (0)\nBehoben mehrere Probleme im statischen Parser-Modus\nQualität der Lebensverbesserungen --> Sie können jetzt die gemeinsamen Staaten direkt einstellen! (set_power, set_mode) sind noch aus Kompatibilitätsgründen und für den dynamischen Parser-Modus verfügbar", 74 | "ru": "Исправлена проблема тайм-аут в нормальном парсерском режиме #(36), все еще существует в динамическом парсерском режиме --> не используйте timeout (0)\nИсправлен ряд вопросов относительно статического парсерского режима\nКачество улучшений жизни --> вы можете теперь установить общие состояния сразу! (set_power, set_mode) все еще доступны по соображениям совместимости и для динамического режима парсера", 75 | "pt": "Problema de tempo limite fixo no modo de parser normal #(36), ainda existe no modo de parser dinâmico --> não use tempoout (0)\nCorrigido um número de questões relativas ao modo de parser estático\nMelhorias de qualidade de vida --> você agora pode definir os estados comuns diretamente! (set_power, set_mode) ainda estão disponíveis por razões de compatibilidade e pelo modo de parser dinâmico", 76 | "nl": "Quality over Quantity (QoQ) Releases Vertaling:\nEen aantal problemen met de statische parochie\nQuality of life verbetering -- Je kunt nu de gemeenschappelijke staten rechtzetten! (setpower, zet ) zijn nog steeds beschikbaar voor compatibiliteit redenen en voor de dynamische parser mode", 77 | "fr": "Problème de timeout fixe en mode parser normal #(36), existe toujours en mode parser dynamique -- Cancer n'utilise pas de timeout (0)\nCorrection d'un certain nombre de questions concernant le mode de parseur statique\nAmélioration de la qualité de vie -- Cancer vous pouvez maintenant définir les états communs directement! (set_power, set_mode) sont toujours disponibles pour des raisons de compatibilité et pour le mode de parser dynamique", 78 | "it": "Emissione di timeout fissa in modalità parser normale #(36), esiste ancora in modalità parser dinamica --> non utilizzare timeout (0)\nRisolto un certo numero di problemi relativi alla modalità parser statico\nQualità dei miglioramenti della vita --> ora è possibile impostare gli stati comuni direttamente! (set_power, set_mode) sono ancora disponibili per motivi di compatibilità e per la modalità di parser dinamica", 79 | "es": "Número de tiempo fijo en el modo de parser normal #(36), todavía existen en el modo de parser dinámico -- usuario no use timeout (0)\nArreglado una serie de cuestiones relativas al modo de parser estático\nCalidad des mejoras de la vida -- usuario ahora puede establecer los estados comunes directamente! (set_power, set_mode) todavía están disponibles por razones de compatibilidad y para el modo de parser dinámico", 80 | "pl": "Kwestia czasoprzestrzeni w normalnym trybie parser #(36), wciąż istnieje w dynamicznym trybie parser – > nie używa czasu (0)\nZmieniło wiele problemów dotyczących trybu statycznego parsera\nJakość ulepszeń życia – -> Możesz teraz założyć stany powszechne. (set_power, set_mode) są nadal dostępne z powodów kompatybilności i dynamicznego trybu parsera", 81 | "uk": "Виправлено випуск часу в режимі нормального парсера #(36), як і раніше існують в режимі динамічного парсера --> використовувати без часу (0)\nВиправлено ряд питань щодо статичного режиму парсера\nЯкість поліпшення життя --> ви можете встановити загальні стани прямо! (set_power, set_mode) все ще доступні для причин сумісності і для динамічного режиму parser", 82 | "zh-cn": "正常包装形式(36)中固定的时间问题仍然存在于动态的序号—— 未使用(0)\n确定与静态投机方式有关的若干问题\n改善生活质量——你现在可以直接建立共同国家! (集群:能力,成套,因兼容和动态配件模式而仍然可用。)" 83 | }, 84 | "4.5.1": { 85 | "en": "Fixed issue #29 (custom states not working)", 86 | "de": "Fixed Ausgabe #29 (Kundenstaaten nicht arbeiten)", 87 | "ru": "Исправлена проблема #29 (таможенные государства не работают)", 88 | "pt": "Problema fixo #29 (estados personalizados não funcionam)", 89 | "nl": "Quality over Quantity (QoQ) Releases Vertaling:", 90 | "fr": "Numéro fixe #29 (les états du client ne fonctionnent pas)", 91 | "it": "Risolto numero #29 (Stati personalizzati non funziona)", 92 | "es": "Cuestión fija #29 (la mayoría de los estados no funcionan)", 93 | "pl": "Numer #29 (poziomy nie działają)", 94 | "uk": "Виправлено проблему #29 (податкові стани не працюють)", 95 | "zh-cn": "固定问题编号:第29号(习惯国家没有工作)" 96 | } 97 | }, 98 | "titleLang": { 99 | "en": "Fronius Wattpilot", 100 | "de": "Fronius Wattpilot", 101 | "ru": "Фрониус Ваттпилот", 102 | "pt": "Fronius Wattpilot", 103 | "nl": "Fronius Wattpilot", 104 | "fr": "Fronius Wattpilot", 105 | "it": "Fronius Wattpilot", 106 | "es": "Fronius Wattpilot", 107 | "pl": "Fronius Wattpilot (ang.)", 108 | "uk": "Фроній Ватпілот", 109 | "zh-cn": "Fronius Wattot" 110 | }, 111 | "desc": { 112 | "en": "A adapter to read and write states from and to the Fronius wattpilot", 113 | "de": "Ein Adapter zum Lesen und Schreiben von und zum Fronius Wattpilot", 114 | "ru": "Адаптер для чтения и записи штатов от и до Фрониус Ваттпилот", 115 | "pt": "Um adaptador para ler e escrever estados de e para o Fronius wattpilot", 116 | "nl": "Een adapter om te lezen en staat te schrijven van de Fronius Wattpilot", 117 | "fr": "Un adaptateur pour lire et écrire les états de et vers le Wattpilot Fronius", 118 | "it": "Un adattatore per leggere e scrivere stati da e per il pilota di watt Fronius", 119 | "es": "Un adaptador para leer y escribir estados desde y hacia el Fronius watpilot", 120 | "pl": "Adaptator do czytania i pisania stanów od i do Fronius wattpilot", 121 | "uk": "Перехідник для зчитування та запису станів з Фроніуса ватпілота", 122 | "zh-cn": "A. 适应者从Fronius 支柱处阅读和写信" 123 | }, 124 | "authors": [ 125 | "tim2zg ", 126 | "SebastianHanz <>", 127 | "derHaubi <>" 128 | ], 129 | "keywords": [ 130 | "template", 131 | "home automation" 132 | ], 133 | "licenseInformation": { 134 | "type": "free", 135 | "license": "MIT" 136 | }, 137 | "platform": "Javascript/Node.js", 138 | "icon": "fronius-wattpilot.png", 139 | "enabled": true, 140 | "extIcon": "https://raw.githubusercontent.com/tim2zg/ioBroker.fronius-wattpilot/main/admin/fronius-wattpilot.png", 141 | "readme": "https://github.com/tim2zg/ioBroker.fronius-wattpilot/blob/main/README.md", 142 | "loglevel": "info", 143 | "tier": 2, 144 | "mode": "daemon", 145 | "type": "vehicle", 146 | "compact": true, 147 | "connectionType": "local", 148 | "dataSource": "poll", 149 | "adminUI": { 150 | "config": "materialize" 151 | }, 152 | "globalDependencies": [ 153 | { 154 | "admin": ">=7.6.17" 155 | } 156 | ], 157 | "dependencies": [ 158 | { 159 | "js-controller": ">=6.0.11" 160 | } 161 | ] 162 | }, 163 | "native": { 164 | "ip-host": "IP-Address des WattPilots", 165 | "pass": "", 166 | "freq": 1, 167 | "serial-number": "XXXXXXXX", 168 | "parser": true, 169 | "cloud": false, 170 | "addParam": "", 171 | "useBcrypt": false 172 | }, 173 | "objects": [], 174 | "protectedNative": [ 175 | "pass" 176 | ], 177 | "encryptedNative": [ 178 | "pass" 179 | ], 180 | "instanceObjects": [ 181 | { 182 | "_id": "info", 183 | "type": "channel", 184 | "common": { 185 | "name": "Information" 186 | }, 187 | "native": {} 188 | }, 189 | { 190 | "_id": "info.connection", 191 | "type": "state", 192 | "common": { 193 | "role": "indicator.connected", 194 | "name": "Device or service connected", 195 | "type": "boolean", 196 | "read": true, 197 | "write": false, 198 | "def": false 199 | }, 200 | "native": {} 201 | } 202 | ] 203 | } 204 | -------------------------------------------------------------------------------- /examples/example-Blockly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | any 5 | 6 | 7 | 8 | fronius.0.powerflow.P_Grid 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | GTE 17 | 18 | 19 | -11000 20 | 21 | 22 | 23 | 24 | val 25 | fronius.0.powerflow.P_Grid 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | NEQ 35 | 36 | 37 | 16 38 | 39 | 40 | 41 | 42 | val 43 | fronius-wattpilot.1.amp 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | fronius-wattpilot.1.set_power 52 | FALSE 53 | 54 | 55 | 16 56 | 57 | 58 | 59 | 60 | 61 | fronius-wattpilot.1.set_state 62 | FALSE 63 | 64 | 65 | frc;0 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | GTE 77 | 78 | 79 | -9700 80 | 81 | 82 | 83 | 84 | val 85 | fronius.0.powerflow.P_Grid 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | NEQ 95 | 96 | 97 | 14 98 | 99 | 100 | 101 | 102 | val 103 | fronius-wattpilot.1.amp 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | fronius-wattpilot.1.set_power 112 | FALSE 113 | 114 | 115 | 14 116 | 117 | 118 | 119 | 120 | 121 | fronius-wattpilot.1.set_state 122 | FALSE 123 | 124 | 125 | frc;0 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | GTE 137 | 138 | 139 | -8300 140 | 141 | 142 | 143 | 144 | val 145 | fronius.0.powerflow.P_Grid 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | NEQ 155 | 156 | 157 | 12 158 | 159 | 160 | 161 | 162 | val 163 | fronius-wattpilot.1.amp 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | fronius-wattpilot.1.set_power 172 | FALSE 173 | 174 | 175 | 12 176 | 177 | 178 | 179 | 180 | 181 | fronius-wattpilot.1.set_state 182 | FALSE 183 | 184 | 185 | frc;0 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | GTE 197 | 198 | 199 | -6900 200 | 201 | 202 | 203 | 204 | val 205 | fronius.0.powerflow.P_Grid 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | NEQ 215 | 216 | 217 | 10 218 | 219 | 220 | 221 | 222 | val 223 | fronius-wattpilot.1.amp 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | fronius-wattpilot.1.set_power 232 | FALSE 233 | 234 | 235 | 10 236 | 237 | 238 | 239 | 240 | 241 | fronius-wattpilot.1.set_state 242 | FALSE 243 | 244 | 245 | frc;0 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | GT 257 | 258 | 259 | -4100 260 | 261 | 262 | 263 | 264 | val 265 | fronius.0.powerflow.P_Grid 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | AND 275 | 276 | 277 | NEQ 278 | 279 | 280 | 6 281 | 282 | 283 | 284 | 285 | val 286 | fronius-wattpilot.1.amp 287 | 288 | 289 | 290 | 291 | 292 | 293 | NEQ 294 | 295 | 296 | frc;0 297 | 298 | 299 | 300 | 301 | val 302 | fronius-wattpilot.1.set_state 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | fronius-wattpilot.1.set_power 313 | FALSE 314 | 315 | 316 | 6 317 | 318 | 319 | 320 | 321 | 322 | fronius-wattpilot.1.set_state 323 | FALSE 324 | 325 | 326 | frc;0 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | NEQ 340 | 341 | 342 | frc;1 343 | 344 | 345 | 346 | 347 | val 348 | fronius-wattpilot.1.set_state 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | fronius-wattpilot.1.set_state 357 | FALSE 358 | 359 | 360 | frc;1 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("@iobroker/adapter-core"); 4 | const WebSocket = require("ws"); 5 | const { createHash, createHmac, pbkdf2Sync } = require("crypto"); 6 | const bcrypt = require("bcryptjs"); 7 | 8 | // --- Constants --- 9 | const ADAPTER_NAME = "fronius-wattpilot"; 10 | 11 | const MESSAGE_TYPE = { 12 | RESPONSE: "response", 13 | HELLO: "hello", 14 | AUTH_REQUIRED: "authRequired", 15 | AUTH_SUCCESS: "authSuccess", 16 | AUTH_ERROR: "authError", 17 | SET_VALUE: "setValue", 18 | SECURED_MSG: "securedMsg", 19 | CLEAR_SMIPS: "clearSmips", 20 | CLEAR_INVERTERS: "clearInverters", 21 | UPDATE_INVERTER: "updateInverter", 22 | }; 23 | 24 | const DEFAULT_HOST_IP = "ws://IP-Address des WattPilots/ws"; 25 | const DEFAULT_HOST_CLOUD_PREFIX = "wss://app.wattpilot.io/app/"; 26 | const DEFAULT_HOST_CLOUD_SERIAL = "XXXXXXXX"; // Placeholder for comparison 27 | const DEFAULT_HOST_CLOUD_SUFFIX = "?version=1.2.9"; // Example version 28 | const DEFAULT_PASSWORD_PLACEHOLDER = "Password"; 29 | 30 | const UPTIME_CHECK_INTERVAL_MS = 1000 * 60 * 2.5; // 2.5 minutes 31 | const WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 5000; 32 | 33 | // Mappings for state values 34 | const ACCESS_STATE_MAP_API_TO_VAL = { 0: "Open", 1: "Wait" }; 35 | const ACCESS_STATE_MAP_VAL_TO_API = { open: 0, wait: 1 }; 36 | 37 | const CABLE_LOCK_MODE_MAP_API_TO_VAL = { 38 | 0: "Normal", 39 | 1: "AutoUnlock", 40 | 2: "AlwaysLock", 41 | }; 42 | const CABLE_LOCK_MODE_MAP_VAL_TO_API = { 43 | normal: 0, 44 | autounlock: 1, 45 | alwayslock: 2, 46 | }; 47 | 48 | const CHARGING_MODE_MAP_API_TO_VAL = { 3: "Default", 4: "Eco", 5: "Next Trip" }; 49 | const CHARGING_MODE_MAP_VAL_TO_API = { default: 3, eco: 4, "next trip": 5 }; 50 | 51 | const CAR_STATE_MAP = { 52 | 0: "Unknown/Error", 53 | 1: "Idle", 54 | 2: "Charging", 55 | 3: "WaitCar", 56 | 4: "Complete", 57 | 5: "Error", 58 | }; 59 | const ERROR_STATE_MAP = { 60 | 0: "None", 61 | 1: "FiAc", 62 | 2: "FiDc", 63 | 3: "Phase", 64 | 4: "Overvolt", 65 | 5: "Overamp", 66 | 6: "Diode", 67 | 7: "PpInvalid", 68 | 8: "GndInvalid", 69 | 9: "ContactorStuck", 70 | 10: "ContactorMiss", 71 | 11: "FiUnknown", 72 | 12: "Unknown", 73 | 13: "Overtemp", 74 | 14: "NoComm", 75 | 15: "StatusLockStuckOpen", 76 | 16: "StatusLockStuckLocked", 77 | }; 78 | // --- End Constants --- 79 | 80 | class FroniusWattpilot extends utils.Adapter { 81 | constructor(options) { 82 | super({ ...options, name: ADAPTER_NAME }); 83 | 84 | this.ws = null; 85 | this.messageCounter = 0; 86 | this.sseToken = null; 87 | this.hashedPassword = null; 88 | this.lastMessageTime = Date.now(); 89 | this.rateLimitTimeouts = {}; // Stores last update timestamp for rate-limited states 90 | this.connectionUptimeMonitor = null; 91 | this.createdStatesRegistry = new Set(); // Tracks API keys for which states have been created 92 | this.customParamsToParse = []; // Parsed from config.addParam 93 | 94 | this.STATE_DEFINITIONS = this._getStaticStateDefinitions(); 95 | this.STATE_CHANGE_HANDLERS = this._getStaticStateChangeHandlers(); 96 | 97 | this.on("ready", this.onReady.bind(this)); 98 | this.on("stateChange", this.onStateChange.bind(this)); 99 | this.on("unload", this.onUnload.bind(this)); 100 | } 101 | 102 | _getStaticStateDefinitions() { 103 | // Definitions for known API keys from the Wattpilot 104 | // key: API key name 105 | // id: ioBroker state ID (without namespace) 106 | // type: ioBroker state type 107 | // write: boolean, if the state is controllable 108 | // valueMap: object, to map API values to ioBroker values 109 | // valueFactor: number, to multiply numeric API values (e.g., for unit conversion) 110 | // rateLimit: boolean, if updates should be rate-limited by config.freq 111 | // customHandler: function, for special processing (e.g., 'nrg' array) 112 | return { 113 | acs: { 114 | id: "AccessState", 115 | type: "string", 116 | write: true, 117 | valueMap: ACCESS_STATE_MAP_API_TO_VAL, 118 | }, 119 | cbl: { id: "cableType", type: "number", rateLimit: true }, 120 | fhz: { id: "frequency", type: "number", rateLimit: true }, 121 | pha: { id: "phases", type: "string", rateLimit: true }, // Value is an array, store as JSON string 122 | wh: { id: "energyCounterSinceStart", type: "number", rateLimit: true }, 123 | err: { 124 | id: "errorState", 125 | type: "string", 126 | valueMap: ERROR_STATE_MAP, 127 | rateLimit: true, 128 | }, 129 | ust: { 130 | id: "cableLock", 131 | type: "string", 132 | write: true, 133 | valueMap: CABLE_LOCK_MODE_MAP_API_TO_VAL, 134 | }, 135 | eto: { id: "energyCounterTotal", type: "number", rateLimit: true }, 136 | cae: { id: "cae", type: "boolean", write: true }, // Charge Anywhere Enabled? 137 | cak: { id: "cak", type: "string", rateLimit: true }, // Cable Auth Key? 138 | lmo: { 139 | id: "mode", 140 | type: "string", 141 | write: true, 142 | valueMap: CHARGING_MODE_MAP_API_TO_VAL, 143 | }, 144 | car: { 145 | id: "carConnected", 146 | type: "string", 147 | valueMap: CAR_STATE_MAP, 148 | rateLimit: true, 149 | }, 150 | alw: { id: "AllowCharging", type: "boolean", rateLimit: true }, 151 | nrg: { 152 | id: "nrgData", 153 | type: "object", 154 | rateLimit: true, 155 | customHandler: this._handleNrgData.bind(this), 156 | }, 157 | amp: { id: "amp", type: "number", write: true }, 158 | version: { id: "version", type: "string", rateLimit: true }, // API Version? 159 | fwv: { id: "firmware", type: "string", rateLimit: true }, 160 | wss: { id: "WifiSSID", type: "string", rateLimit: true }, 161 | upd: { 162 | id: "updateAvailable", 163 | type: "boolean", 164 | valueMap: { 0: false, 1: true }, 165 | rateLimit: true, 166 | }, 167 | fna: { id: "hostname", type: "string", rateLimit: true }, 168 | ffna: { id: "serial", type: "string", rateLimit: true }, // Full Friendly Name (Serial) 169 | utc: { id: "TimeStamp", type: "string", rateLimit: true }, 170 | pvopt_averagePGrid: { 171 | id: "PVUselessPower", 172 | type: "number", 173 | rateLimit: true, 174 | }, 175 | lpsc: { id: "lpsc", type: "number", write: true }, 176 | awp: { 177 | id: "awp", 178 | type: "number", 179 | write: true, 180 | rateLimit: true, 181 | }, 182 | }; 183 | } 184 | 185 | _getStaticStateChangeHandlers() { 186 | // Maps ioBroker state IDs (full path) to handler methods for sending commands 187 | return { 188 | [`${this.namespace}.set_power`]: (state) => 189 | this._sendSecureCommand("amp", parseInt(state.val)), 190 | [`${this.namespace}.set_mode`]: (state) => 191 | this._sendSecureCommand("lmo", parseInt(state.val)), 192 | [`${this.namespace}.set_state`]: this._handleSetGenericStateCommand, 193 | [`${this.namespace}.amp`]: (state) => 194 | this._sendSecureCommand("amp", parseInt(state.val)), 195 | [`${this.namespace}.cae`]: (state) => 196 | this._sendSecureCommand( 197 | "cae", 198 | state.val === true || state.val === "true", 199 | ), 200 | [`${this.namespace}.AccessState`]: (state) => { 201 | const apiVal = 202 | ACCESS_STATE_MAP_VAL_TO_API[state.val.toString().toLowerCase()]; 203 | if (apiVal !== undefined) { 204 | this._sendSecureCommand("acs", apiVal); 205 | } else { 206 | this.log.warn(`Invalid AccessState value: ${state.val}`); 207 | } 208 | }, 209 | [`${this.namespace}.cableLock`]: (state) => { 210 | const apiVal = 211 | CABLE_LOCK_MODE_MAP_VAL_TO_API[state.val.toString().toLowerCase()]; 212 | if (apiVal !== undefined) { 213 | this._sendSecureCommand("ust", apiVal); 214 | } else { 215 | this.log.warn(`Invalid cableLock value: ${state.val}`); 216 | } 217 | }, 218 | [`${this.namespace}.mode`]: (state) => { 219 | const apiVal = 220 | CHARGING_MODE_MAP_VAL_TO_API[state.val.toString().toLowerCase()]; 221 | if (apiVal !== undefined) { 222 | this._sendSecureCommand("lmo", apiVal); 223 | } else { 224 | this.log.warn(`Invalid mode value: ${state.val}`); 225 | } 226 | }, 227 | }; 228 | } 229 | 230 | async onReady() { 231 | this.setState("info.connection", false, true); 232 | 233 | if (!this._validateConfig()) { 234 | return; // Stop if config is invalid 235 | } 236 | 237 | if (this.config.addParam) { 238 | this.customParamsToParse = this.config.addParam 239 | .split(";") 240 | .map((p) => p.trim()) 241 | .filter((p) => p); 242 | } 243 | 244 | await this._initializeControlStates(); 245 | this.connectionUptimeMonitor = setInterval( 246 | this._checkUptime.bind(this), 247 | UPTIME_CHECK_INTERVAL_MS, 248 | ); 249 | 250 | this._createWsConnection(); 251 | } 252 | 253 | _getWebSocketUrl() { 254 | if (this.config.cloud) { 255 | const serial = this.config["serial-number"] || DEFAULT_HOST_CLOUD_SERIAL; 256 | return `${DEFAULT_HOST_CLOUD_PREFIX}${serial}${DEFAULT_HOST_CLOUD_SUFFIX}`; 257 | } 258 | return `ws://${this.config["ip-host"] || "localhost"}/ws`; 259 | } 260 | 261 | _validateConfig() { 262 | const hostToConnect = this._getWebSocketUrl(); 263 | const password = this.config.pass; 264 | 265 | let isValid = true; 266 | if (!password || password === DEFAULT_PASSWORD_PLACEHOLDER) { 267 | this.log.error( 268 | "Password is not configured or is the default placeholder.", 269 | ); 270 | isValid = false; 271 | } 272 | if (this.config.cloud) { 273 | if ( 274 | !this.config["serial-number"] || 275 | this.config["serial-number"] === DEFAULT_HOST_CLOUD_SERIAL 276 | ) { 277 | this.log.error( 278 | "Cloud connection selected, but serial number is missing or is the default placeholder.", 279 | ); 280 | isValid = false; 281 | } 282 | } else { 283 | if (!this.config["ip-host"] || hostToConnect === DEFAULT_HOST_IP) { 284 | this.log.error( 285 | "Local connection selected, but IP address/hostname is missing or is the default placeholder.", 286 | ); 287 | isValid = false; 288 | } 289 | } 290 | 291 | if (isValid) { 292 | this.log.info(`Attempting to connect to: ${hostToConnect}`); 293 | } 294 | return isValid; 295 | } 296 | 297 | async _initializeControlStates() { 298 | await this._ensureObjectExists("set_power", "value", "number", true, true); 299 | this.subscribeStates("set_power"); 300 | 301 | await this._ensureObjectExists("set_mode", "value", "number", true, true); 302 | this.subscribeStates("set_mode"); 303 | 304 | await this._ensureObjectExists("set_state", "value", "string", true, true); 305 | this.subscribeStates("set_state"); 306 | } 307 | 308 | _createWsConnection() { 309 | if ( 310 | this.ws && 311 | (this.ws.readyState === WebSocket.OPEN || 312 | this.ws.readyState === WebSocket.CONNECTING) 313 | ) { 314 | this.log.debug( 315 | "WebSocket connection attempt skipped, already open or connecting.", 316 | ); 317 | return; 318 | } 319 | if (this.ws) { 320 | this.ws.removeAllListeners(); // Clean up old listeners 321 | this.ws.terminate(); // Force close if exists 322 | } 323 | 324 | const hostToConnect = this._getWebSocketUrl(); 325 | this.log.info(`Creating WebSocket connection to ${hostToConnect}`); 326 | this.ws = new WebSocket(hostToConnect, { 327 | handshakeTimeout: WEBSOCKET_HANDSHAKE_TIMEOUT_MS, 328 | }); 329 | this.messageCounter = 0; // Reset counter for new connection 330 | 331 | this.ws.on("open", () => { 332 | this.log.debug("WebSocket connection opened. Waiting for messages."); 333 | // Connection state will be set to true upon successful authentication 334 | }); 335 | 336 | this.ws.on("message", (data) => { 337 | this.lastMessageTime = Date.now(); 338 | try { 339 | const messageString = data.toString(); 340 | const messageData = JSON.parse(messageString); 341 | this._handleWebSocketMessage(messageData); 342 | } catch (e) { 343 | this.log.error( 344 | `Error parsing JSON message: ${e.message}. Data: ${data.toString()}`, 345 | ); 346 | } 347 | }); 348 | 349 | this.ws.on("error", (err) => { 350 | this.log.error(`WebSocket error: ${err.message}.`); 351 | this.setState("info.connection", false, true); 352 | // Reconnect attempt will be handled by _checkUptime or implicitly on next scheduled call if needed 353 | }); 354 | 355 | this.ws.on("close", (code, reason) => { 356 | this.log.info( 357 | `WebSocket connection closed. Code: ${code}, Reason: ${reason ? reason.toString() : "N/A"}`, 358 | ); 359 | this.setState("info.connection", false, true); 360 | this.hashedPassword = null; // Invalidate hash on disconnect 361 | // Reconnect logic is handled by _checkUptime 362 | }); 363 | } 364 | 365 | async _handleWebSocketMessage(message) { 366 | this.log.debug(`Received message: ${JSON.stringify(message)}`); 367 | 368 | switch (message.type) { 369 | case MESSAGE_TYPE.RESPONSE: 370 | this._handleResponseMessage(message); 371 | break; 372 | case MESSAGE_TYPE.HELLO: 373 | this.sseToken = message.serial; 374 | this.log.info(`Received HELLO, SSE token: ${this.sseToken}`); 375 | break; 376 | case MESSAGE_TYPE.AUTH_REQUIRED: 377 | await this._handleAuthRequiredMessage(message); 378 | break; 379 | case MESSAGE_TYPE.AUTH_SUCCESS: 380 | await this.setState("info.connection", true, true); 381 | this.log.info("Authentication successful. Connected to Wattpilot."); 382 | break; 383 | case MESSAGE_TYPE.AUTH_ERROR: 384 | this.log.error("Authentication failed. Please check your password."); 385 | await this.setState("info.connection", false, true); 386 | if (this.ws) { 387 | this.ws.close(); 388 | } // Close connection on auth error 389 | break; 390 | case MESSAGE_TYPE.CLEAR_SMIPS: 391 | break; 392 | case MESSAGE_TYPE.CLEAR_INVERTERS: 393 | break; 394 | case MESSAGE_TYPE.UPDATE_INVERTER: 395 | break; // Not used in this adapter 396 | default: 397 | // Assume it's a status update if it has a 'status' property 398 | if (message.status && typeof message.status === "object") { 399 | await this._parseStatusMessage(message.status); 400 | } else { 401 | this.log.warn( 402 | `Received unhandled message type: ${message.type || "Unknown"}`, 403 | ); 404 | } 405 | } 406 | } 407 | 408 | _handleResponseMessage(message) { 409 | if (message.success) { 410 | this.log.debug(`Command successful: ${JSON.stringify(message.status)}`); 411 | // Update corresponding 'set_...' states if needed, though usually status messages provide this 412 | if (message.status && message.status.amp !== undefined) { 413 | this.setState("set_power", message.status.amp, true); 414 | } else if (message.status && message.status.lmo !== undefined) { 415 | this.setState("set_mode", message.status.lmo, true); 416 | } else { 417 | this.setState("set_state", "", true); // Clear after generic command 418 | } 419 | } else { 420 | this.log.error( 421 | `Command failed: ${message.message || "No error message provided."}`, 422 | ); 423 | } 424 | } 425 | 426 | async _handleAuthRequiredMessage(message) { 427 | if (!this.sseToken) { 428 | this.log.error( 429 | "Authentication required, but SSE token (from HELLO) is missing.", 430 | ); 431 | return; 432 | } 433 | if (!this.config.pass) { 434 | this.log.error( 435 | "Authentication required, but password is not configured.", 436 | ); 437 | return; 438 | } 439 | 440 | try { 441 | // === Python: ran = random.randrange(10**80) 442 | const ran = this.__randomBigInt(80); 443 | // === Python: "%064x" % ran 444 | let token3 = this.__formatHex(ran).slice(0, 32); 445 | let hashedPassword; 446 | 447 | if (message.hash === "pbkdf2") { 448 | const iterations = 100000; 449 | const keylen = 256; 450 | const digest = "sha512"; 451 | const derivedKey = pbkdf2Sync( 452 | this.config.pass, 453 | this.sseToken, 454 | iterations, 455 | keylen, 456 | digest, 457 | ); 458 | this.hashedPassword = derivedKey.toString("base64").substring(0, 32); 459 | hashedPassword = this.hashedPassword; 460 | } else { 461 | const passwordHashSha256 = createHash("sha256") 462 | .update(this.config.pass, "utf8") 463 | .digest("hex"); 464 | 465 | const serial = String(this.sseToken || ""); 466 | const serialB64 = this.__bcryptjs_encodeBase64(serial, 16); 467 | 468 | const iterations = 8; 469 | let salt = "$2a$"; 470 | if (iterations < 10) { 471 | salt += "0"; 472 | } 473 | salt += `${iterations}$${serialB64}`; 474 | 475 | const pwhash = bcrypt.hashSync(passwordHashSha256, salt); 476 | this.hashedPassword = pwhash.slice(salt.length); 477 | hashedPassword = this.hashedPassword; 478 | } 479 | 480 | // === Python: hash1 = sha256(token1 + hashedPassword) 481 | const hash1 = createHash("sha256") 482 | .update(message.token1 + hashedPassword) 483 | .digest("hex"); 484 | 485 | // === Python: hash = sha256(token3 + token2 + hash1) 486 | const finalHash = createHash("sha256") 487 | .update(token3 + message.token2 + hash1) 488 | .digest("hex"); 489 | 490 | const authResponse = { 491 | type: "auth", 492 | token3, 493 | hash: finalHash, 494 | }; 495 | 496 | this.log.debug("Sending authentication response (Python-compatible)."); 497 | if (this.ws && this.ws.readyState === WebSocket.OPEN) { 498 | this.ws.send(JSON.stringify(authResponse)); 499 | } 500 | } catch (err) { 501 | this.log.error(`Error during authentication process: ${err.message}`); 502 | this.setState("info.connection", false, true); 503 | } 504 | } 505 | 506 | async _parseStatusMessage(statusData) { 507 | const enableDynamic = this.config.parser === false; // 'parser' in config means 'strict', so false means dynamic 508 | 509 | for (const apiKey in statusData) { 510 | if (!Object.prototype.hasOwnProperty.call(statusData, apiKey)) { 511 | continue; 512 | } 513 | 514 | const apiValue = statusData[apiKey]; 515 | const stateDef = this.STATE_DEFINITIONS[apiKey]; 516 | 517 | if (stateDef) { 518 | await this._processDefinedState(apiKey, apiValue, stateDef); 519 | } else if (this.customParamsToParse.includes(apiKey)) { 520 | await this._processDynamicOrCustomState(apiKey, apiValue, true); 521 | } else if (enableDynamic) { 522 | await this._processDynamicOrCustomState(apiKey, apiValue, false); 523 | } 524 | } 525 | } 526 | 527 | async _processDefinedState(apiKey, apiValue, stateDef) { 528 | let processedValue = apiValue; 529 | 530 | if (stateDef.valueMap && stateDef.valueMap[apiValue] !== undefined) { 531 | processedValue = stateDef.valueMap[apiValue]; 532 | } else if (typeof apiValue === "number" && stateDef.valueFactor) { 533 | // Korrekte Behandlung von Float-Werten bei der Anwendung eines Faktors 534 | processedValue = parseFloat((apiValue * stateDef.valueFactor).toFixed(6)); 535 | } else if ( 536 | typeof apiValue === "string" && 537 | !isNaN(parseFloat(apiValue)) && 538 | stateDef.type === "number" 539 | ) { 540 | // Strings, die Zahlen repräsentieren, in echte Zahlen umwandeln 541 | processedValue = parseFloat(apiValue); 542 | if (stateDef.valueFactor) { 543 | processedValue = parseFloat( 544 | (processedValue * stateDef.valueFactor).toFixed(6), 545 | ); 546 | } 547 | } else if ( 548 | (Array.isArray(apiValue) || typeof apiValue === "object") && 549 | stateDef.type === "string" 550 | ) { 551 | processedValue = JSON.stringify(apiValue); 552 | } 553 | 554 | if (!this.createdStatesRegistry.has(apiKey)) { 555 | await this._ensureObjectExists( 556 | stateDef.id, 557 | "value", 558 | stateDef.type, 559 | true, 560 | stateDef.write || false, 561 | ); 562 | if (stateDef.write) { 563 | this.subscribeStates(stateDef.id); 564 | } 565 | this.createdStatesRegistry.add(apiKey); 566 | } 567 | 568 | if (stateDef.rateLimit && !this._shouldUpdateByRateLimit(apiKey)) { 569 | return; // Skip update due to rate limit 570 | } 571 | 572 | if (stateDef.customHandler) { 573 | await stateDef.customHandler(apiKey, apiValue, stateDef); 574 | } else { 575 | await this.setStateAsync(stateDef.id, { val: processedValue, ack: true }); 576 | } 577 | 578 | if (stateDef.rateLimit) { 579 | this._updateRateLimitTimestamp(apiKey); 580 | } 581 | } 582 | 583 | async _handleNrgData(apiKey, apiValueArray) { 584 | // apiValueArray is like [V1, V2, V3, VN, A1, A2, A3, P1, P2, P3, PN, PTotal] 585 | // Power values are in W, convert to kW for consistency if desired (original code used 0.001 for kW) 586 | const nrgStates = [ 587 | { id: "voltage1", value: apiValueArray[0] }, 588 | { id: "voltage2", value: apiValueArray[1] }, 589 | { id: "voltage3", value: apiValueArray[2] }, 590 | { id: "voltageN", value: apiValueArray[3] }, 591 | { id: "amps1", value: apiValueArray[4] }, 592 | { id: "amps2", value: apiValueArray[5] }, 593 | { id: "amps3", value: apiValueArray[6] }, 594 | { id: "power1", value: apiValueArray[7] * 0.001 }, 595 | { id: "power2", value: apiValueArray[8] * 0.001 }, 596 | { id: "power3", value: apiValueArray[9] * 0.001 }, 597 | { id: "powerN", value: apiValueArray[10] * 0.001 }, 598 | { id: "power", value: apiValueArray[11] * 0.001 }, // Total power 599 | ]; 600 | 601 | for (const nrgState of nrgStates) { 602 | if ( 603 | apiValueArray.length > nrgStates.indexOf(nrgState) && 604 | nrgState.value !== undefined 605 | ) { 606 | // Check if value exists in array 607 | const fullStateId = `${apiKey}_${nrgState.id}`; // e.g., nrgData_voltage1 608 | if (!this.createdStatesRegistry.has(fullStateId)) { 609 | await this._ensureObjectExists( 610 | nrgState.id, 611 | "value", 612 | "number", 613 | true, 614 | false, 615 | ); // Assuming nrg states are read-only 616 | this.createdStatesRegistry.add(fullStateId); // Use a unique key for registry 617 | } 618 | await this.setStateAsync(nrgState.id, { 619 | val: nrgState.value, 620 | ack: true, 621 | }); 622 | } 623 | } 624 | } 625 | 626 | async _processDynamicOrCustomState(apiKey, apiValue, isCustomViaConfig) { 627 | if (apiValue === null || apiValue === undefined) { 628 | return; 629 | } 630 | 631 | if ( 632 | isCustomViaConfig && 633 | this.rateLimitTimeouts[apiKey] && 634 | !this._shouldUpdateByRateLimit(apiKey) 635 | ) { 636 | return; // Apply rate limit for custom params if they were already created 637 | } 638 | 639 | let type = "string"; 640 | let valueToSet = apiValue; 641 | 642 | if (typeof apiValue === "number") { 643 | type = "number"; 644 | valueToSet = apiValue; 645 | } else if (typeof apiValue === "boolean") { 646 | type = "boolean"; 647 | } else if (Array.isArray(apiValue) || typeof apiValue === "object") { 648 | type = "string"; 649 | valueToSet = JSON.stringify(apiValue); 650 | } else if (typeof apiValue === "string") { 651 | if (!isNaN(parseFloat(apiValue))) { 652 | type = "number"; 653 | valueToSet = parseFloat(apiValue); 654 | } 655 | } 656 | 657 | if (apiKey === "rcd" && typeof apiValue !== "number") { 658 | type = "number"; 659 | } 660 | 661 | if (!this.createdStatesRegistry.has(apiKey)) { 662 | await this._ensureObjectExists(apiKey, "value", type, true, true); // Hier write=true gesetzt 663 | this.subscribeStates(apiKey); // State abonnieren, da schreibbar 664 | this.createdStatesRegistry.add(apiKey); 665 | } 666 | 667 | await this.setStateAsync(apiKey, { val: valueToSet, ack: true }); 668 | if (isCustomViaConfig) { 669 | this._updateRateLimitTimestamp(apiKey); 670 | } 671 | } 672 | 673 | _shouldUpdateByRateLimit(apiKey) { 674 | const freqMillis = (this.config.freq || 10) * 1000; // Default to 10s if not set 675 | return !( 676 | this.rateLimitTimeouts[apiKey] && 677 | this.rateLimitTimeouts[apiKey] + freqMillis > Date.now() 678 | ); 679 | } 680 | 681 | _updateRateLimitTimestamp(apiKey) { 682 | this.rateLimitTimeouts[apiKey] = Date.now(); 683 | } 684 | 685 | _checkUptime() { 686 | this.log.debug("Checking Wattpilot connection uptime..."); 687 | if (Date.now() - this.lastMessageTime > UPTIME_CHECK_INTERVAL_MS) { 688 | this.log.warn( 689 | `No message received for over ${UPTIME_CHECK_INTERVAL_MS / 1000 / 60} minutes. Attempting to reconnect.`, 690 | ); 691 | this.setState("info.connection", false, true); 692 | if (this.ws) { 693 | this.ws.terminate(); // Force close existing connection 694 | } 695 | // Explicitly set ws to null so _createWsConnection doesn't think it's still connecting 696 | this.ws = null; 697 | this._createWsConnection(); 698 | } else { 699 | // Send a ping-like request if protocol supports it or just keep connection alive 700 | // For Wattpilot, regular status updates should keep it alive. If not, consider a periodic 'getAllValues' if available. 701 | // For now, assume activity means connection is fine. 702 | this.log.debug("Connection seems active."); 703 | } 704 | } 705 | 706 | async _ensureObjectExists(id, role, type, read = true, write = false) { 707 | try { 708 | const obj = await this.getObjectAsync(id); 709 | if ( 710 | !obj || 711 | obj.common.type !== type || 712 | obj.common.role !== role || 713 | obj.common.read !== read || 714 | obj.common.write !== write 715 | ) { 716 | await this.extendObjectAsync(id, { 717 | type: "state", 718 | common: { 719 | name: id, 720 | role, 721 | type, 722 | read, 723 | write, 724 | def: type === "number" ? 0 : type === "boolean" ? false : "", 725 | }, 726 | native: {}, 727 | }); 728 | this.log.debug(`Object ${this.namespace}.${id} created/updated.`); 729 | } 730 | } catch (error) { 731 | this.log.error(`Error ensuring object ${id}: ${error}`); 732 | await this.setObjectNotExistsAsync(id, { 733 | type: "state", 734 | common: { 735 | name: id, 736 | role, 737 | type, 738 | read, 739 | write, 740 | def: type === "number" ? 0 : type === "boolean" ? false : "", 741 | }, 742 | native: {}, 743 | }); 744 | this.log.debug(`Object ${this.namespace}.${id} created (fallback).`); 745 | } 746 | } 747 | 748 | onUnload(callback) { 749 | try { 750 | this.log.info("Shutting down adapter..."); 751 | if (this.connectionUptimeMonitor) { 752 | clearInterval(this.connectionUptimeMonitor); 753 | this.connectionUptimeMonitor = null; 754 | } 755 | if (this.ws) { 756 | this.ws.removeAllListeners(); 757 | this.ws.close(); 758 | this.ws = null; 759 | } 760 | this.setState("info.connection", false, true); 761 | this.log.info("Cleanup complete. Adapter stopped."); 762 | callback(); 763 | } catch (e) { 764 | this.log.error(`Error during onUnload: ${e.message}`); 765 | callback(); 766 | } 767 | } 768 | 769 | onStateChange(id, state) { 770 | if (state && !state.ack) { 771 | this.log.debug( 772 | `State change command received for ${id}: ${JSON.stringify(state)}`, 773 | ); 774 | 775 | if (!this.hashedPassword) { 776 | this.log.warn( 777 | `Cannot send command for ${id}: not authenticated (hashedPassword missing).`, 778 | ); 779 | return; 780 | } 781 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { 782 | this.log.warn(`Cannot send command for ${id}: WebSocket not open.`); 783 | return; 784 | } 785 | 786 | const handler = this.STATE_CHANGE_HANDLERS[id]; 787 | if (handler) { 788 | try { 789 | handler.call(this, state); 790 | } catch (e) { 791 | this.log.error( 792 | `Error processing state change for ${id}: ${e.message}`, 793 | ); 794 | } 795 | } else { 796 | // Generischer Handler für alle anderen States 797 | this._handleGenericStateChange(id, state); 798 | } 799 | } 800 | } 801 | 802 | // Neue generische Handler-Methode 803 | _handleGenericStateChange(id, state) { 804 | const idParts = id.split("."); 805 | const stateName = idParts[idParts.length - 1]; 806 | 807 | // Zuerst prüfen, ob ein Eintrag in STATE_DEFINITIONS existiert 808 | for (const apiKey in this.STATE_DEFINITIONS) { 809 | if (this.STATE_DEFINITIONS[apiKey].id === stateName) { 810 | let value = state.val; 811 | 812 | // Umkehrung der valueMap verwenden, falls vorhanden 813 | const stateDef = this.STATE_DEFINITIONS[apiKey]; 814 | if (stateDef.valueMap) { 815 | const reverseMap = Object.entries(stateDef.valueMap).reduce( 816 | (acc, [key, val]) => { 817 | acc[val.toString().toLowerCase()] = key; 818 | return acc; 819 | }, 820 | {}, 821 | ); 822 | 823 | if (reverseMap[state.val.toString().toLowerCase()] !== undefined) { 824 | value = reverseMap[state.val.toString().toLowerCase()]; 825 | // Wenn der Wert in der valueMap numerisch ist, konvertieren 826 | if (!isNaN(parseFloat(value))) { 827 | value = parseFloat(value); 828 | } 829 | } 830 | } 831 | 832 | this.log.info(`Sending command for ${stateName} (${apiKey}): ${value}`); 833 | this._sendSecureCommand(apiKey, value); 834 | return; 835 | } 836 | } 837 | 838 | // Falls kein Eintrag in STATE_DEFINITIONS gefunden wurde, versuchen wir es als dynamischen State 839 | this.log.info(`Sending dynamic command for ${stateName}: ${state.val}`); 840 | this._sendSecureCommand(stateName, state.val); 841 | } 842 | 843 | _handleSetGenericStateCommand(state) { 844 | // Expected format for set_state: "apiKey;value" 845 | if (typeof state.val !== "string" || !state.val.includes(";")) { 846 | this.log.error( 847 | `Invalid value for set_state: "${state.val}". Expected format "key;value".`, 848 | ); 849 | return; 850 | } 851 | const [key, valueStr] = state.val.split(";", 2); 852 | let value; 853 | if (valueStr.toLowerCase() === "true") { 854 | value = true; 855 | } else if (valueStr.toLowerCase() === "false") { 856 | value = false; 857 | } else if (!isNaN(parseFloat(valueStr)) && isFinite(valueStr)) { 858 | value = parseFloat(valueStr); 859 | } else if ( 860 | !isNaN(parseInt(valueStr, 10)) && 861 | parseInt(valueStr, 10).toString() === valueStr 862 | ) { 863 | value = parseInt(valueStr, 10); 864 | } else { 865 | value = valueStr; 866 | } // Treat as string if not boolean or number 867 | 868 | this._sendSecureCommand(key, value); 869 | } 870 | 871 | async _sendSecureCommand(apiKey, apiValue) { 872 | if ( 873 | !this.hashedPassword || 874 | !this.ws || 875 | this.ws.readyState !== WebSocket.OPEN 876 | ) { 877 | this.log.warn( 878 | `Cannot send secure command for ${apiKey}: Not ready or authenticated.`, 879 | ); 880 | return; 881 | } 882 | this.messageCounter++; 883 | const payload = { 884 | type: MESSAGE_TYPE.SET_VALUE, 885 | requestId: this.messageCounter, 886 | key: apiKey, 887 | value: apiValue, 888 | }; 889 | const payloadString = JSON.stringify(payload); 890 | 891 | const hmac = createHmac("sha256", this.hashedPassword) 892 | .update(payloadString) 893 | .digest("hex"); 894 | 895 | const messageToSend = { 896 | type: MESSAGE_TYPE.SECURED_MSG, 897 | data: payloadString, 898 | requestId: `${this.messageCounter}sm`, 899 | hmac: hmac, 900 | }; 901 | 902 | this.log.debug(`Sending secure command: ${JSON.stringify(messageToSend)}`); 903 | this.ws.send(JSON.stringify(messageToSend)); 904 | } 905 | 906 | // --- Helper functions for bcrypt authentication --- 907 | 908 | __randomBigInt(digits) { 909 | let result = ""; 910 | const digitsNum = typeof digits === "bigint" ? Number(digits) : digits; 911 | for (let i = 0; i < digitsNum; i++) { 912 | let digit = 913 | i === 0 914 | ? Math.floor(Math.random() * 9) + 1 915 | : Math.floor(Math.random() * 10); 916 | result += digit.toString(); 917 | } 918 | return BigInt(result); 919 | } 920 | 921 | __formatHex(bigint) { 922 | let hex = bigint.toString(16); 923 | return hex.padStart(64, "0"); 924 | } 925 | 926 | __bcryptjs_encodeBase64(s, length) { 927 | if (/^\d+$/.test(s)) { 928 | const vals = Array.from(s).map((ch) => ch.charCodeAt(0) - 48); 929 | const b = Buffer.concat([ 930 | Buffer.alloc(length - vals.length, 0), 931 | Buffer.from(vals), 932 | ]); 933 | return this.__bcryptjs_base64_encode(b, length); 934 | } 935 | this.log.error( 936 | `__bcryptjs_encodeBase64: check serial string - should be digits only: ${s}`, 937 | ); 938 | throw new Error(`Check serial string - should be digits only: ${s}`); 939 | } 940 | 941 | __bcryptjs_base64_encode(b, length) { 942 | const BASE64_CODE = 943 | "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 944 | let off = 0; 945 | let rs = []; 946 | 947 | if (length <= 0 || length > b.length) { 948 | throw new Error(`Illegal len: ${length}`); 949 | } 950 | 951 | while (off < length) { 952 | let c1 = b[off++] & 0xff; 953 | rs.push(BASE64_CODE[(c1 >> 2) & 0x3f]); 954 | c1 = (c1 & 0x03) << 4; 955 | if (off >= length) { 956 | rs.push(BASE64_CODE[c1 & 0x3f]); 957 | break; 958 | } 959 | 960 | let c2 = b[off++] & 0xff; 961 | c1 |= (c2 >> 4) & 0x0f; 962 | rs.push(BASE64_CODE[c1 & 0x3f]); 963 | c1 = (c2 & 0x0f) << 2; 964 | if (off >= length) { 965 | rs.push(BASE64_CODE[c1 & 0x3f]); 966 | break; 967 | } 968 | 969 | c2 = b[off++] & 0xff; 970 | c1 |= (c2 >> 6) & 0x03; 971 | rs.push(BASE64_CODE[c1 & 0x3f]); 972 | rs.push(BASE64_CODE[c2 & 0x3f]); 973 | } 974 | 975 | return rs.join(""); 976 | } 977 | } 978 | 979 | if (require.main !== module) { 980 | module.exports = (options) => new FroniusWattpilot(options); 981 | } else { 982 | (() => new FroniusWattpilot())(); 983 | } 984 | --------------------------------------------------------------------------------