├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── admin ├── stiebel-isg.png ├── i18n │ ├── zh-cn │ │ └── translations.json │ ├── en │ │ └── translations.json │ ├── uk │ │ └── translations.json │ ├── pl │ │ └── translations.json │ ├── ru │ │ └── translations.json │ ├── de │ │ └── translations.json │ ├── it │ │ └── translations.json │ ├── nl │ │ └── translations.json │ ├── pt │ │ └── translations.json │ ├── fr │ │ └── translations.json │ └── es │ │ └── translations.json └── jsonConfig.json ├── .releaseconfig.json ├── test ├── tsconfig.json ├── package.js ├── mocharc.custom.json ├── integration.js └── mocha.setup.js ├── prettier.config.mjs ├── tsconfig.check.json ├── .gitignore ├── .github ├── dependabot.yml ├── auto-merge.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── workflows │ ├── dependabot-automerge.yml │ ├── codeql-analysis.yml │ ├── test-and-release.yml │ └── check-copilot-template.yml └── copilot-instructions.md ├── lib └── adapter-config.d.ts ├── CHANGELOG_OLD.md ├── main.test.js ├── .create-adapter.json ├── tsconfig.json ├── LICENSE ├── eslint.config.mjs ├── package.json ├── README.md ├── io-package.json └── main.js /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /admin/stiebel-isg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iobroker-community-adapters/ioBroker.stiebel-isg/HEAD/admin/stiebel-isg.png -------------------------------------------------------------------------------- /.releaseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "iobroker", 4 | "license", 5 | "manual-review" 6 | ] 7 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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, "..")); -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | // iobroker prettier configuration file 2 | import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs'; 3 | 4 | export default { 5 | ...prettierConfig, 6 | // uncomment next line if you prefer double quotes 7 | // singleQuote: false, 8 | } -------------------------------------------------------------------------------- /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 | "gulpfile.js" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | *.code-workspace 4 | node_modules 5 | nbproject 6 | 7 | # npm package files 8 | iobroker.*.tgz 9 | 10 | Thumbs.db 11 | 12 | # i18n intermediate files 13 | admin/i18n/flat.txt 14 | admin/i18n/*/flat.txt 15 | 16 | #ignore .commitinfo created by ioBroker release script 17 | .commitinfo 18 | 19 | 20 | # ioBroker dev-server 21 | .dev-server/ 22 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot will run on day 26 of each month at 02:10 (Europe/Berlin timezone) 2 | version: 2 3 | updates: 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "cron" 8 | timezone: "Europe/Berlin" 9 | cronjob: "10 2 26 * *" 10 | open-pull-requests-limit: 15 11 | versioning-strategy: "increase" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "cron" 16 | timezone: "Europe/Berlin" 17 | cronjob: "10 2 26 * *" 18 | open-pull-requests-limit: 15 19 | -------------------------------------------------------------------------------- /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 {}; -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configure here which dependency updates should be merged automatically. 2 | # The recommended configuration is the following: 3 | - match: 4 | # Only merge patches for production dependencies 5 | dependency_type: production 6 | update_type: "semver:patch" 7 | - match: 8 | # Except for security fixes, here we allow minor patches 9 | dependency_type: production 10 | update_type: "security:minor" 11 | - match: 12 | # and development dependencies can have a minor update, too 13 | dependency_type: development 14 | update_type: "semver:minor" 15 | 16 | # The syntax is based on the legacy dependabot v1 automerged_updates syntax, see: 17 | # https://dependabot.com/docs/config-file/#automerged_updates -------------------------------------------------------------------------------- /CHANGELOG_OLD.md: -------------------------------------------------------------------------------- 1 | # Older changes 2 | ## 1.7.5 3 | 4 | * security enhancements 5 | 6 | ## 1.7.4 7 | 8 | * security enhancements 9 | 10 | ## 1.7.3 11 | 12 | * bugfix 13 | 14 | ## 1.7.2 15 | 16 | * ready for Admin 5 and NodeJS 16 17 | 18 | ## 1.7.1 19 | 20 | * bugfix for translation 21 | 22 | ## 1.7.0 23 | 24 | * new adapter structure, bugfixes for new js-controller 25 | 26 | ## 1.6.1 27 | 28 | * new values for isg-version 12 implemented 29 | 30 | ## 1.6.0 31 | 32 | * isg-sites to read values from, can now be select by the user 33 | 34 | ## 1.5.3 35 | 36 | * bugfix for latest_value added in statistics for database 37 | 38 | ## 1.5.2 39 | 40 | * latest_value added in statistics for database 41 | 42 | ## 1.5.1 43 | 44 | * new adapter testing and security update 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "eslint.enable": true, 5 | "json.schemas": [ 6 | { 7 | "fileMatch": [ 8 | "io-package.json" 9 | ], 10 | "url": "https://raw.githubusercontent.com/ioBroker/ioBroker.js-controller/master/schemas/io-package.json" 11 | }, 12 | { 13 | "fileMatch": [ 14 | "admin/jsonConfig.json", 15 | "admin/jsonCustom.json", 16 | "admin/jsonTab.json", 17 | "admin/jsonConfig.json5", 18 | "admin/jsonCustom.json5", 19 | "admin/jsonTab.json5" 20 | ], 21 | "url": "https://raw.githubusercontent.com/ioBroker/ioBroker.admin/master/packages/jsonConfig/schemas/jsonConfig.json" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /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 | "adapterName": "stiebel-isg", 4 | "title": "Stiebel-ISG", 5 | "description": "This adapter is a ment to read values from stiebel-eltron/tecalor internet service gateways (ISG) and control the device.", 6 | "keywords": [ 7 | "Stiebel-Eltron/Tecalor", 8 | "Internet Service Gateway", 9 | "ISG" 10 | ], 11 | "expert": "no", 12 | "features": [ 13 | "adapter" 14 | ], 15 | "adminFeatures": [], 16 | "type": "climate-control", 17 | "startMode": "daemon", 18 | "connectionType": "local", 19 | "dataSource": "poll", 20 | "connectionIndicator": "no", 21 | "language": "JavaScript", 22 | "adminReact": "no", 23 | "tools": [ 24 | "ESLint", 25 | "type checking" 26 | ], 27 | "indentation": "Tab", 28 | "quotes": "double", 29 | "es6class": "yes", 30 | "authorName": "Michael Schuster", 31 | "authorGithub": "unltdnetworx", 32 | "authorEmail": "development@unltd-networx.de", 33 | "gitRemoteProtocol": "HTTPS", 34 | "gitCommit": "no", 35 | "license": "MIT License", 36 | "ci": "gh-actions", 37 | "dependabot": "no", 38 | "creatorVersion": "1.33.0" 39 | } -------------------------------------------------------------------------------- /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 | 28 | // Consider targetting es2019 or higher if you only support Node.js 12+ 29 | "target": "es2018", 30 | 31 | }, 32 | "include": [ 33 | "**/*.js", 34 | "**/*.d.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules/**" 38 | ] 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 iobroker-community-adapters 4 | Copyright (c) 2018 - 2023 Michael Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /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 | // specify files to exclude from linting here 9 | ignores: [ 10 | '.dev-server/', 11 | '.vscode/', 12 | '*.test.js', 13 | 'test/**/*.js', 14 | '*.config.mjs', 15 | 'build', 16 | 'dist', 17 | 'admin/build', 18 | 'admin/words.js', 19 | 'admin/admin.d.ts', 20 | 'admin/blockly.js', 21 | '**/adapter-config.d.ts', 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 | // 'jsdoc/require-param': 'off', 30 | // 'jsdoc/require-param-description': 'off', 31 | // 'jsdoc/require-returns-description': 'off', 32 | // 'jsdoc/require-returns-check': 'off', 33 | }, 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # Automatically merge Dependabot PRs when version comparison is within the range 2 | # that is configured in .github/auto-merge.yml 3 | 4 | name: Auto-Merge Dependabot PRs 5 | 6 | on: 7 | # WARNING: This needs to be run in the PR base, DO NOT build untrusted code in this action 8 | # details under https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/ 9 | pull_request_target: 10 | 11 | jobs: 12 | auto-merge: 13 | if: github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v6 18 | 19 | - name: Check if PR should be auto-merged 20 | uses: ahmadnassri/action-dependabot-auto-merge@v2 21 | with: 22 | # In order to use this, you need to go to https://github.com/settings/tokens and 23 | # create a Personal Access Token with the permission "public_repo". 24 | # Enter this token in your repository settings under "Secrets" and name it AUTO_MERGE_TOKEN 25 | github-token: ${{ secrets.AUTO_MERGE_TOKEN }} 26 | # By default, squash and merge, so Github chooses nice commit messages 27 | command: squash and merge 28 | -------------------------------------------------------------------------------- /admin/i18n/zh-cn/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "IP地址或域名", 3 | "IP address or domain_help": "ISG的IP地址或域名", 4 | "IP address or domain_tips": "示例 - IP:192.168.178.188 或 FQDN:servicewelt.fritz.box", 5 | "ISGReboot": "ISG重启", 6 | "Main settings": "主要设置", 7 | "URLs": "网址", 8 | "command paths": "命令路径", 9 | "command paths_help": "定义 ISG 页面上设置的路径。", 10 | "command paths_tips": "ISG 设置页面“/?s=”后面的 URL 结尾,以“;”分隔", 11 | "enable expert values": "实现专家价值", 12 | "enable expert values_help": "启用后显示额外的专家值", 13 | "enable expert values_tips": "允许读取/写入专家值。", 14 | "expert paths": "专家路径", 15 | "expert paths_help": "定义 ISG 页面上专家值的路径。", 16 | "expert paths_tips": "专家值 ISG 页面“/?s=”后面的 URL 结尾,以“;”分隔", 17 | "info": "信息", 18 | "intervall (commands) (s)": "间隔(命令)(s)", 19 | "intervall (commands) (s)_help": "拉取命令的时间间隔(以秒为单位)", 20 | "intervall (s)": "间隔(秒)", 21 | "intervall (s)_help": "拉取值的时间间隔(以秒为单位)", 22 | "isg password": "密码", 23 | "isg password_help": "ISG 的密码", 24 | "isg password_tips": "密码区分大小写,如果 ISG 中未设置则留空", 25 | "no": "不", 26 | "settings": "设置", 27 | "start": "开始", 28 | "status paths": "状态路径", 29 | "status paths_help": "定义 ISG 页面上状态的路径。", 30 | "status paths_tips": "ISG 状态页面“/?s=”后面的 URL 结尾,以“;”分隔", 31 | "umlauts active": "元音变音活跃", 32 | "umlauts active_help": "启用元音变音处理", 33 | "umlauts active_tips": "激活对象中的变音符号。警告:可能会导致历史适配器出现问题。", 34 | "username": "用户名", 35 | "username_help": "ISG 的用户名", 36 | "username_tips": "用户名区分大小写,如果ISG中未设置则留空", 37 | "value paths": "价值路径", 38 | "value paths_help": "定义 ISG 页面上的值的路径。", 39 | "value paths_tips": "ISG 值页面“/?s=”后面的 URL 结尾,以“;”分隔", 40 | "yes": "是的" 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Verwendet IntelliSense zum Ermitteln möglicher Attribute. 3 | // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. 4 | // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch normal", 9 | "program": "${workspaceFolder}/build/main.js", 10 | "args": ["--instance", "0", "--force", "--logs", "--debug"], 11 | "request": "launch", 12 | "stopOnEntry": true, 13 | "console": "internalConsole", 14 | "outputCapture": "std", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "node", 19 | "env": {"NODE_PATH":"${workspaceFolder}/.dev-server/default/node_modules"} 20 | }, 21 | 22 | { 23 | "name": "Launch install", 24 | "program": "${workspaceFolder}/build/main.js", 25 | "args": ["--instance", "0", "--force", "--logs", "--debug", "--install"], 26 | "request": "launch", 27 | "stopOnEntry": true, 28 | "console": "internalConsole", 29 | "outputCapture": "std", 30 | "skipFiles": [ 31 | "/**" 32 | ], 33 | "type": "node" 34 | }, 35 | 36 | 37 | { 38 | "name": "Attach by Process ID", 39 | "processId": "${command:PickProcess}", 40 | "request": "attach", 41 | "skipFiles": [ 42 | "/**" 43 | ], 44 | "type": "node" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /admin/i18n/en/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main settings": "Main settings", 3 | "URLs": "URLs", 4 | "IP address or domain": "IP address or domain", 5 | "IP address or domain_help": "IP address or domain name of the ISG", 6 | "IP address or domain_tips": "Examples - IP: 192.168.178.188 or FQDN: servicewelt.fritz.box", 7 | "username": "Username", 8 | "username_help": "Username for the ISG", 9 | "username_tips": "Username is case-sensitive, leave empty if not set in ISG", 10 | "isg password": "Password", 11 | "isg password_help": "Password for the ISG", 12 | "isg password_tips": "Password is case-sensitive, leave empty if not set in ISG", 13 | "umlauts active": "Umlauts active", 14 | "umlauts active_help": "Enables handling of umlauts", 15 | "umlauts active_tips": "Activates umlauts in objects. Warning: could cause problems in history-adapter.", 16 | "enable expert values": "Enables expert values", 17 | "enable expert values_help": "Shows additional expert values when enabled", 18 | "enable expert values_tips": "Enables reading/writing of expert values.", 19 | "intervall (s)": "Interval (s)", 20 | "intervall (s)_help": "Interval for values to pull (in seconds)", 21 | "intervall (commands) (s)": "Interval (commands) (s)", 22 | "intervall (commands) (s)_help": "Interval for commands to pull (in seconds)", 23 | "command paths": "Command paths", 24 | "command paths_tips": "Ending of URLs behind \"/?s=\" of ISG pages for settings, separated by \";\"", 25 | "command paths_help": "Defines the paths for settings on the ISG pages.", 26 | "value paths": "Value paths", 27 | "value paths_tips": "Ending of URLs behind \"/?s=\" of ISG pages for values, separated by \";\"", 28 | "value paths_help": "Defines the paths for values on the ISG pages.", 29 | "status paths": "Status paths", 30 | "status paths_tips": "Ending of URLs behind \"/?s=\" of ISG pages for status, separated by \";\"", 31 | "status paths_help": "Defines the paths for status on the ISG pages.", 32 | "expert paths": "Expert paths", 33 | "expert paths_tips": "Ending of URLs behind \"/?s=\" of ISG pages for expert values, separated by \";\"", 34 | "expert paths_help": "Defines the paths for expert values on the ISG pages.", 35 | "settings": "Settings", 36 | "info": "Info", 37 | "start": "Start", 38 | "yes": "Yes", 39 | "no": "No", 40 | "ISGReboot": "ISG Reboot" 41 | } -------------------------------------------------------------------------------- /admin/i18n/uk/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "IP-адреса або домен", 3 | "IP address or domain_help": "IP-адреса або доменне ім’я ISG", 4 | "IP address or domain_tips": "Приклади - IP: 192.168.178.188 або FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "Перезавантаження ISG", 6 | "Main settings": "Основні налаштування", 7 | "URLs": "URL-адреси", 8 | "command paths": "Командні шляхи", 9 | "command paths_help": "Визначає шляхи для налаштувань на сторінках ISG.", 10 | "command paths_tips": "Закінчення URL-адрес за \"/?s=\" сторінок ISG для налаштувань, розділених \";\"", 11 | "enable expert values": "Вмикає експертні значення", 12 | "enable expert values_help": "Показує додаткові експертні значення, якщо ввімкнено", 13 | "enable expert values_tips": "Дозволяє читати/записувати експертні значення.", 14 | "expert paths": "Експертні шляхи", 15 | "expert paths_help": "Визначає шляхи експертних значень на сторінках ISG.", 16 | "expert paths_tips": "Закінчення URL-адрес за \"/?s=\" сторінок ISG для експертних значень, розділених \";\"", 17 | "info": "Інформація", 18 | "intervall (commands) (s)": "Інтервал (команди) (s)", 19 | "intervall (commands) (s)_help": "Інтервал для отримання команд (у секундах)", 20 | "intervall (s)": "Інтервал (s)", 21 | "intervall (s)_help": "Інтервал для отримання значень (у секундах)", 22 | "isg password": "Пароль", 23 | "isg password_help": "Пароль для ISG", 24 | "isg password_tips": "Пароль чутливий до регістру, залиште порожнім, якщо не встановлено в ISG", 25 | "no": "немає", 26 | "settings": "Налаштування", 27 | "start": "старт", 28 | "status paths": "Шляхи стану", 29 | "status paths_help": "Визначає шляхи для статусу на сторінках ISG.", 30 | "status paths_tips": "Закінчення URL-адрес за \"/?s=\" сторінок ISG для статусу, розділених \";\"", 31 | "umlauts active": "Умляути активні", 32 | "umlauts active_help": "Вмикає обробку умляутів", 33 | "umlauts active_tips": "Активує умляути в об'єктах. Попередження: можуть виникнути проблеми в адаптері історії.", 34 | "username": "Ім'я користувача", 35 | "username_help": "Ім'я користувача для ISG", 36 | "username_tips": "Ім’я користувача чутливе до регістру, залиште порожнім, якщо не встановлено в ISG", 37 | "value paths": "Значні шляхи", 38 | "value paths_help": "Визначає шляхи для значень на сторінках ISG.", 39 | "value paths_tips": "Закінчення URL-адрес за \"/?s=\" сторінок ISG для значень, розділених \";\"", 40 | "yes": "так" 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iobroker.stiebel-isg", 3 | "version": "2.0.2", 4 | "description": "stiebel/tecalor internet service gateway", 5 | "author": { 6 | "name": "Michael Schuster", 7 | "email": "development@unltd-networx.de" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "iobroker-community-adapters", 12 | "email": "iobroker-community-adapters@gmx.de" 13 | }, 14 | { 15 | "name": "mcm1957", 16 | "email": "mcm57@gmx.at" 17 | } 18 | ], 19 | "homepage": "https://github.com/iobroker-community-adapters/ioBroker.stiebel-isg", 20 | "license": "MIT", 21 | "keywords": [ 22 | "ioBroker", 23 | "Stiebel-Eltron/Tecalor", 24 | "Internet Service Gateway", 25 | "ISG" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/iobroker-community-adapters/ioBroker.stiebel-isg" 30 | }, 31 | "engines": { 32 | "node": ">= 20" 33 | }, 34 | "dependencies": { 35 | "@iobroker/adapter-core": "^3.3.2", 36 | "cheerio": "^1.0.0-rc.10", 37 | "fetch-cookie": "^3.1.0", 38 | "tough-cookie": "^6.0.0" 39 | }, 40 | "devDependencies": { 41 | "@alcalzone/release-script": "^5.0.0", 42 | "@alcalzone/release-script-plugin-iobroker": "^4.0.0", 43 | "@alcalzone/release-script-plugin-license": "^4.0.0", 44 | "@alcalzone/release-script-plugin-manual-review": "^4.0.0", 45 | "@iobroker/adapter-dev": "^1.5.0", 46 | "@iobroker/eslint-config": "^2.2.0", 47 | "@iobroker/testing": "^5.2.2", 48 | "@types/node": "^24.10.1", 49 | "typescript": "^5.9.3" 50 | }, 51 | "main": "main.js", 52 | "files": [ 53 | "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).json", 54 | "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}", 55 | "lib/", 56 | "www/", 57 | "io-package.json", 58 | "LICENSE", 59 | "main.js" 60 | ], 61 | "scripts": { 62 | "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"", 63 | "test:package": "mocha test/package --exit", 64 | "test:integration": "mocha test/integration --exit", 65 | "test": "npm run test:js && npm run test:package", 66 | "check": "tsc --noEmit -p tsconfig.check.json", 67 | "lint": "eslint -c eslint.config.mjs .", 68 | "translate": "translate-adapter", 69 | "release": "release-script" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/iobroker-community-adapters/ioBroker.stiebel-isg/issues" 73 | }, 74 | "readmeFilename": "README.md" 75 | } 76 | -------------------------------------------------------------------------------- /admin/i18n/pl/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "Adres IP lub domena", 3 | "IP address or domain_help": "Adres IP lub nazwa domeny ISG", 4 | "IP address or domain_tips": "Przykłady - IP: 192.168.178.188 lub FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "Ponowne uruchomienie ISG", 6 | "Main settings": "Główne ustawienia", 7 | "URLs": "Adresy URL", 8 | "command paths": "Ścieżki poleceń", 9 | "command paths_help": "Definiuje ścieżki ustawień na stronach ISG.", 10 | "command paths_tips": "Zakończenie adresów URL za „/?s=\" stron ISG z ustawieniami, oddzielonych znakami „;”", 11 | "enable expert values": "Włącza wartości eksperckie", 12 | "enable expert values_help": "Po włączeniu wyświetla dodatkowe wartości eksperckie", 13 | "enable expert values_tips": "Umożliwia odczyt/zapis wartości eksperckich.", 14 | "expert paths": "Ścieżki eksperckie", 15 | "expert paths_help": "Definiuje ścieżki wartości eksperckich na stronach ISG.", 16 | "expert paths_tips": "Zakończenie adresów URL za „/?s=\" stron ISG z wartościami eksperckimi, oddzielone znakami „;”", 17 | "info": "Informacje", 18 | "intervall (commands) (s)": "Interwał (polecenia) (s)", 19 | "intervall (commands) (s)_help": "Interwał poleceń do ściągnięcia (w sekundach)", 20 | "intervall (s)": "Interwał (y)", 21 | "intervall (s)_help": "Interwał pobierania wartości (w sekundach)", 22 | "isg password": "Hasło", 23 | "isg password_help": "Hasło do ISG", 24 | "isg password_tips": "W haśle rozróżniana jest wielkość liter. Jeśli nie ustawiono w ISG, pozostaw puste", 25 | "no": "NIE", 26 | "settings": "Ustawienia", 27 | "start": "Start", 28 | "status paths": "Ścieżki stanu", 29 | "status paths_help": "Definiuje ścieżki statusu na stronach ISG.", 30 | "status paths_tips": "Zakończenie adresów URL znajdujących się za „/?s=\" stron ISG dla statusu, oddzielonych znakami „;”", 31 | "umlauts active": "Przegłosy aktywne", 32 | "umlauts active_help": "Umożliwia obsługę umlautów", 33 | "umlauts active_tips": "Aktywuje umlauty w obiektach. Ostrzeżenie: może powodować problemy z adapterem historii.", 34 | "username": "Nazwa użytkownika", 35 | "username_help": "Nazwa użytkownika dla ISG", 36 | "username_tips": "W nazwie użytkownika rozróżniana jest wielkość liter. Pozostaw puste, jeśli nie jest ustawione w ISG", 37 | "value paths": "Ścieżki wartości", 38 | "value paths_help": "Definiuje ścieżki wartości na stronach ISG.", 39 | "value paths_tips": "Zakończenie adresów URL za „/?s=\" stron ISG dla wartości, oddzielonych znakami „;”", 40 | "yes": "Tak" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/ru/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "IP-адрес или домен", 3 | "IP address or domain_help": "IP-адрес или доменное имя ISG", 4 | "IP address or domain_tips": "Примеры: IP: 192.168.178.188 или полное доменное имя: servicewelt.fritz.box.", 5 | "ISGReboot": "Перезагрузка ISG", 6 | "Main settings": "Основные настройки", 7 | "URLs": "URL-адреса", 8 | "command paths": "Пути команд", 9 | "command paths_help": "Определяет пути к настройкам на страницах ISG.", 10 | "command paths_tips": "Окончание URL-адресов после \"/?s=\" страниц ISG для настроек, разделенных знаком \";\"", 11 | "enable expert values": "Включает экспертные значения", 12 | "enable expert values_help": "Показывает дополнительные экспертные значения при включении", 13 | "enable expert values_tips": "Позволяет читать/записывать экспертные значения.", 14 | "expert paths": "Экспертные пути", 15 | "expert paths_help": "Определяет пути к экспертным значениям на страницах ISG.", 16 | "expert paths_tips": "Окончание URL-адресов после \"/?s=\" на страницах ISG для экспертных значений, разделенных знаком \";\"", 17 | "info": "Информация", 18 | "intervall (commands) (s)": "Интервал (команды) (с)", 19 | "intervall (commands) (s)_help": "Интервал для получения команд (в секундах)", 20 | "intervall (s)": "Интервал (с)", 21 | "intervall (s)_help": "Интервал получения значений (в секундах)", 22 | "isg password": "Пароль", 23 | "isg password_help": "Пароль для ISG", 24 | "isg password_tips": "Пароль чувствителен к регистру, оставьте пустым, если он не установлен в ISG.", 25 | "no": "Нет", 26 | "settings": "Настройки", 27 | "start": "Начинать", 28 | "status paths": "Пути статуса", 29 | "status paths_help": "Определяет пути к статусу на страницах ISG.", 30 | "status paths_tips": "Окончание URL-адресов после \"/?s=\" страниц ISG для статуса, разделенных знаком \";\"", 31 | "umlauts active": "Умлауты активны", 32 | "umlauts active_help": "Включает обработку умляутов", 33 | "umlauts active_tips": "Активирует умлауты в объектах. Внимание: могут возникнуть проблемы в адаптере истории.", 34 | "username": "Имя пользователя", 35 | "username_help": "Имя пользователя для ISG", 36 | "username_tips": "Имя пользователя чувствительно к регистру, оставьте пустым, если оно не установлено в ISG.", 37 | "value paths": "Пути создания ценности", 38 | "value paths_help": "Определяет пути к значениям на страницах ISG.", 39 | "value paths_tips": "Окончание URL-адресов после \"/?s=\" страниц ISG для значений, разделенных знаком \";\"", 40 | "yes": "Да" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/de/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "IP-Adresse oder Domäne", 3 | "IP address or domain_help": "IP-Adresse oder Domänenname des ISG", 4 | "IP address or domain_tips": "Beispiele - IP: 192.168.178.188 oder FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "ISG-Neustart", 6 | "Main settings": "Haupteinstellungen", 7 | "URLs": "URLs", 8 | "command paths": "Befehlspfade", 9 | "command paths_help": "Definiert die Pfade für Einstellungen auf den ISG-Seiten.", 10 | "command paths_tips": "Ende der URLs hinter „/?s=\" von ISG-Seiten für Einstellungen, getrennt durch „;“", 11 | "enable expert values": "Aktiviert Expertenwerte", 12 | "enable expert values_help": "Zeigt bei Aktivierung zusätzliche Expertenwerte an", 13 | "enable expert values_tips": "Ermöglicht das Lesen/Schreiben von Expertenwerten.", 14 | "expert paths": "Expertenpfade", 15 | "expert paths_help": "Definiert die Pfade für Expertenwerte auf den ISG-Seiten.", 16 | "expert paths_tips": "Ende der URLs hinter „/?s=\" von ISG-Seiten für Expertenwerte, getrennt durch „;“", 17 | "info": "Info", 18 | "intervall (commands) (s)": "Intervall (Befehle) (s)", 19 | "intervall (commands) (s)_help": "Intervall für Befehle zum Abrufen (in Sekunden)", 20 | "intervall (s)": "Intervall(e)", 21 | "intervall (s)_help": "Intervall für abzurufende Werte (in Sekunden)", 22 | "isg password": "Passwort", 23 | "isg password_help": "Passwort für die ISG", 24 | "isg password_tips": "Beim Passwort muss die Groß-/Kleinschreibung beachtet werden. Lassen Sie es leer, wenn es nicht in ISG festgelegt ist", 25 | "no": "NEIN", 26 | "settings": "Einstellungen", 27 | "start": "Start", 28 | "status paths": "Statuspfade", 29 | "status paths_help": "Definiert die Pfade für den Status auf den ISG-Seiten.", 30 | "status paths_tips": "Ende der URLs hinter „/?s=\" von ISG-Seiten für den Status, getrennt durch „;“", 31 | "umlauts active": "Umlaute aktiv", 32 | "umlauts active_help": "Ermöglicht die Handhabung von Umlauten", 33 | "umlauts active_tips": "Aktiviert Umlaute in Objekten. Warnung: könnte Probleme im History-Adapter verursachen.", 34 | "username": "Benutzername", 35 | "username_help": "Benutzername für die ISG", 36 | "username_tips": "Beim Benutzernamen muss die Groß-/Kleinschreibung beachtet werden. Lassen Sie das Feld leer, wenn es nicht in ISG festgelegt ist", 37 | "value paths": "Wertpfade", 38 | "value paths_help": "Definiert die Pfade für Werte auf den ISG-Seiten.", 39 | "value paths_tips": "Ende der URLs hinter „/?s=\" von ISG-Seiten für Werte, getrennt durch „;“", 40 | "yes": "Ja" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/it/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "Indirizzo IP o dominio", 3 | "IP address or domain_help": "Indirizzo IP o nome di dominio dell'ISG", 4 | "IP address or domain_tips": "Esempi - IP: 192.168.178.188 o FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "Riavvio dell'ISG", 6 | "Main settings": "Impostazioni principali", 7 | "URLs": "URL", 8 | "command paths": "Percorsi di comando", 9 | "command paths_help": "Definisce i percorsi per le impostazioni nelle pagine ISG.", 10 | "command paths_tips": "Fine degli URL dietro \"/?s=\" delle pagine ISG per le impostazioni, separati da \";\"", 11 | "enable expert values": "Abilita valori esperti", 12 | "enable expert values_help": "Mostra ulteriori valori esperti quando abilitato", 13 | "enable expert values_tips": "Abilita la lettura/scrittura dei valori esperti.", 14 | "expert paths": "Percorsi esperti", 15 | "expert paths_help": "Definisce i percorsi per i valori esperti nelle pagine ISG.", 16 | "expert paths_tips": "Fine degli URL dietro \"/?s=\" delle pagine ISG per valori esperti, separati da \";\"", 17 | "info": "Informazioni", 18 | "intervall (commands) (s)": "Intervallo (comandi) (s)", 19 | "intervall (commands) (s)_help": "Intervallo per i comandi da eseguire (in secondi)", 20 | "intervall (s)": "Intervallo (i)", 21 | "intervall (s)_help": "Intervallo per i valori da estrarre (in secondi)", 22 | "isg password": "Password", 23 | "isg password_help": "Password per l'ISG", 24 | "isg password_tips": "La password fa distinzione tra maiuscole e minuscole, lascia vuota se non impostata in ISG", 25 | "no": "NO", 26 | "settings": "Impostazioni", 27 | "start": "Inizio", 28 | "status paths": "Percorsi di stato", 29 | "status paths_help": "Definisce i percorsi per lo stato nelle pagine ISG.", 30 | "status paths_tips": "Fine degli URL dietro \"/?s=\" delle pagine ISG per lo stato, separati da \";\"", 31 | "umlauts active": "Umlaut attivi", 32 | "umlauts active_help": "Abilita la gestione delle dieresi", 33 | "umlauts active_tips": "Attiva le dieresi negli oggetti. Attenzione: potrebbe causare problemi nell'adattatore della cronologia.", 34 | "username": "Nome utente", 35 | "username_help": "Nome utente per l'ISG", 36 | "username_tips": "Il nome utente fa distinzione tra maiuscole e minuscole, lascia vuoto se non impostato in ISG", 37 | "value paths": "Percorsi di valore", 38 | "value paths_help": "Definisce i percorsi per i valori nelle pagine ISG.", 39 | "value paths_tips": "Fine degli URL dietro \"/?s=\" delle pagine ISG per i valori, separati da \";\"", 40 | "yes": "SÌ" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/nl/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "IP-adres of domein", 3 | "IP address or domain_help": "IP-adres of domeinnaam van de ISG", 4 | "IP address or domain_tips": "Voorbeelden - IP: 192.168.178.188 of FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "ISG opnieuw opstarten", 6 | "Main settings": "Belangrijkste instellingen", 7 | "URLs": "URL's", 8 | "command paths": "Commandopaden", 9 | "command paths_help": "Definieert de paden voor instellingen op de ISG-pagina's.", 10 | "command paths_tips": "Einde van URL's achter \"/?s=\" van ISG-pagina's voor instellingen, gescheiden door \";\"", 11 | "enable expert values": "Maakt expertwaarden mogelijk", 12 | "enable expert values_help": "Toont aanvullende expertwaarden indien ingeschakeld", 13 | "enable expert values_tips": "Maakt het lezen/schrijven van expertwaarden mogelijk.", 14 | "expert paths": "Deskundige paden", 15 | "expert paths_help": "Definieert de paden voor expertwaarden op de ISG-pagina's.", 16 | "expert paths_tips": "Einde van URL's achter \"/?s=\" van ISG-pagina's voor expertwaarden, gescheiden door \";\"", 17 | "info": "Info", 18 | "intervall (commands) (s)": "Interval (opdrachten) (s)", 19 | "intervall (commands) (s)_help": "Interval voor het trekken van commando's (in seconden)", 20 | "intervall (s)": "Interval(len)", 21 | "intervall (s)_help": "Interval voor het ophalen van waarden (in seconden)", 22 | "isg password": "Wachtwoord", 23 | "isg password_help": "Wachtwoord voor de ISG", 24 | "isg password_tips": "Het wachtwoord is hoofdlettergevoelig. Laat het leeg als het niet is ingesteld in ISG", 25 | "no": "Nee", 26 | "settings": "Instellingen", 27 | "start": "Begin", 28 | "status paths": "Statuspaden", 29 | "status paths_help": "Definieert de paden voor de status op de ISG-pagina's.", 30 | "status paths_tips": "Einde van URL's achter \"/?s=\" van ISG-pagina's voor status, gescheiden door \";\"", 31 | "umlauts active": "Umlauten actief", 32 | "umlauts active_help": "Maakt de verwerking van umlauten mogelijk", 33 | "umlauts active_tips": "Activeert umlauten in objecten. Waarschuwing: kan problemen veroorzaken in de history-adapter.", 34 | "username": "Gebruikersnaam", 35 | "username_help": "Gebruikersnaam voor de ISG", 36 | "username_tips": "De gebruikersnaam is hoofdlettergevoelig. Laat leeg als deze niet is ingesteld in ISG", 37 | "value paths": "Waardepaden", 38 | "value paths_help": "Definieert de paden voor waarden op de ISG-pagina's.", 39 | "value paths_tips": "Einde van URL's achter \"/?s=\" van ISG-pagina's voor waarden, gescheiden door \";\"", 40 | "yes": "Ja" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/pt/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "Endereço IP ou domínio", 3 | "IP address or domain_help": "Endereço IP ou nome de domínio do ISG", 4 | "IP address or domain_tips": "Exemplos - IP: 192.168.178.188 ou FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "Reinicialização do ISG", 6 | "Main settings": "Configurações principais", 7 | "URLs": "URLs", 8 | "command paths": "Caminhos de comando", 9 | "command paths_help": "Define os caminhos para configurações nas páginas ISG.", 10 | "command paths_tips": "Fim das URLs atrás de \"/?s=\" das páginas ISG para configurações, separadas por \";\"", 11 | "enable expert values": "Permite valores especializados", 12 | "enable expert values_help": "Mostra valores de especialistas adicionais quando ativado", 13 | "enable expert values_tips": "Permite leitura/gravação de valores especialistas.", 14 | "expert paths": "Caminhos de especialistas", 15 | "expert paths_help": "Define os caminhos para valores especialistas nas páginas ISG.", 16 | "expert paths_tips": "Finalização de URLs atrás de \"/?s=\" das páginas ISG para valores de especialistas, separados por \";\"", 17 | "info": "Informações", 18 | "intervall (commands) (s)": "Intervalo (comandos)(s)", 19 | "intervall (commands) (s)_help": "Intervalo para comandos serem puxados (em segundos)", 20 | "intervall (s)": "Intervalo(s)", 21 | "intervall (s)_help": "Intervalo para extrair valores (em segundos)", 22 | "isg password": "Senha", 23 | "isg password_help": "Senha do ISG", 24 | "isg password_tips": "A senha diferencia maiúsculas de minúsculas, deixe em branco se não for definida no ISG", 25 | "no": "Não", 26 | "settings": "Configurações", 27 | "start": "Começar", 28 | "status paths": "Caminhos de status", 29 | "status paths_help": "Define os caminhos para status nas páginas ISG.", 30 | "status paths_tips": "Fim das URLs atrás de \"/?s=\" das páginas ISG para status, separadas por \";\"", 31 | "umlauts active": "Tremas ativos", 32 | "umlauts active_help": "Permite o tratamento de tremas", 33 | "umlauts active_tips": "Ativa tremas em objetos. Aviso: pode causar problemas no adaptador de histórico.", 34 | "username": "Nome de usuário", 35 | "username_help": "Nome de usuário do ISG", 36 | "username_tips": "O nome de usuário diferencia maiúsculas de minúsculas, deixe em branco se não estiver definido no ISG", 37 | "value paths": "Caminhos de valor", 38 | "value paths_help": "Define os caminhos para valores nas páginas ISG.", 39 | "value paths_tips": "Finalização de URLs atrás de \"/?s=\" das páginas ISG para valores, separados por \";\"", 40 | "yes": "Sim" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/fr/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "Adresse IP ou domaine", 3 | "IP address or domain_help": "Adresse IP ou nom de domaine de l'ISG", 4 | "IP address or domain_tips": "Exemples - IP : 192.168.178.188 ou FQDN : servicewelt.fritz.box", 5 | "ISGReboot": "Redémarrage d'ISG", 6 | "Main settings": "Paramètres principaux", 7 | "URLs": "URL", 8 | "command paths": "Chemins de commande", 9 | "command paths_help": "Définit les chemins d'accès aux paramètres sur les pages ISG.", 10 | "command paths_tips": "Fin des URL derrière \"/?s=\" des pages ISG pour les paramètres, séparées par \";\"", 11 | "enable expert values": "Permet des valeurs d'expert", 12 | "enable expert values_help": "Affiche des valeurs expertes supplémentaires lorsqu'elles sont activées", 13 | "enable expert values_tips": "Permet la lecture/écriture de valeurs expertes.", 14 | "expert paths": "Parcours experts", 15 | "expert paths_help": "Définit les chemins pour les valeurs expertes sur les pages ISG.", 16 | "expert paths_tips": "Fin des URL derrière \"/?s=\" des pages ISG pour les valeurs expertes, séparées par \";\"", 17 | "info": "Informations", 18 | "intervall (commands) (s)": "Intervalle (commandes) (s)", 19 | "intervall (commands) (s)_help": "Intervalle d'extraction des commandes (en secondes)", 20 | "intervall (s)": "Intervalle(s)", 21 | "intervall (s)_help": "Intervalle d'extraction des valeurs (en secondes)", 22 | "isg password": "Mot de passe", 23 | "isg password_help": "Mot de passe pour l'ISG", 24 | "isg password_tips": "Le mot de passe est sensible à la casse, laissez vide s'il n'est pas défini dans ISG", 25 | "no": "Non", 26 | "settings": "Paramètres", 27 | "start": "Commencer", 28 | "status paths": "Chemins d'état", 29 | "status paths_help": "Définit les chemins d'accès au statut sur les pages ISG.", 30 | "status paths_tips": "Fin des URL derrière \"/?s=\" des pages ISG pour le statut, séparées par \";\"", 31 | "umlauts active": "Trémas actifs", 32 | "umlauts active_help": "Permet la gestion des trémas", 33 | "umlauts active_tips": "Active les trémas dans les objets. Attention : cela pourrait provoquer des problèmes dans l'adaptateur d'historique.", 34 | "username": "Nom d'utilisateur", 35 | "username_help": "Nom d'utilisateur pour l'ISG", 36 | "username_tips": "Le nom d'utilisateur est sensible à la casse, laissez vide s'il n'est pas défini dans ISG", 37 | "value paths": "Chemins de valeur", 38 | "value paths_help": "Définit les chemins des valeurs sur les pages ISG.", 39 | "value paths_tips": "Fin des URL derrière \"/?s=\" des pages ISG pour les valeurs, séparées par \";\"", 40 | "yes": "Oui" 41 | } 42 | -------------------------------------------------------------------------------- /admin/i18n/es/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "IP address or domain": "dirección IP o dominio", 3 | "IP address or domain_help": "Dirección IP o nombre de dominio del ISG", 4 | "IP address or domain_tips": "Ejemplos: IP: 192.168.178.188 o FQDN: servicewelt.fritz.box", 5 | "ISGReboot": "Reinicio del ISG", 6 | "Main settings": "Configuraciones principales", 7 | "URLs": "URL", 8 | "command paths": "Rutas de comando", 9 | "command paths_help": "Define las rutas para la configuración en las páginas ISG.", 10 | "command paths_tips": "Final de las URL detrás de \"/?s=\" de las páginas ISG para configuración, separadas por \";\"", 11 | "enable expert values": "Permite valores expertos", 12 | "enable expert values_help": "Muestra valores expertos adicionales cuando está habilitado", 13 | "enable expert values_tips": "Permite la lectura/escritura de valores expertos.", 14 | "expert paths": "Caminos expertos", 15 | "expert paths_help": "Define las rutas para los valores expertos en las páginas ISG.", 16 | "expert paths_tips": "Final de las URL detrás de \"/?s=\" de las páginas ISG para valores de expertos, separadas por \";\"", 17 | "info": "Información", 18 | "intervall (commands) (s)": "Intervalo (comandos) (s)", 19 | "intervall (commands) (s)_help": "Intervalo para que se extraigan los comandos (en segundos)", 20 | "intervall (s)": "Intervalo(s)", 21 | "intervall (s)_help": "Intervalo para que se extraigan los valores (en segundos)", 22 | "isg password": "Contraseña", 23 | "isg password_help": "Contraseña para el ISG", 24 | "isg password_tips": "La contraseña distingue entre mayúsculas y minúsculas, déjela en blanco si no está configurada en ISG", 25 | "no": "No", 26 | "settings": "Ajustes", 27 | "start": "Comenzar", 28 | "status paths": "Rutas de estado", 29 | "status paths_help": "Define las rutas para el estado en las páginas ISG.", 30 | "status paths_tips": "Final de las URL detrás de \"/?s=\" de las páginas ISG para el estado, separadas por \";\"", 31 | "umlauts active": "Diéresis activas", 32 | "umlauts active_help": "Permite el manejo de diéresis.", 33 | "umlauts active_tips": "Activa diéresis en objetos. Advertencia: podría causar problemas en el adaptador de historial.", 34 | "username": "Nombre de usuario", 35 | "username_help": "Nombre de usuario para el ISG", 36 | "username_tips": "El nombre de usuario distingue entre mayúsculas y minúsculas, déjelo vacío si no está configurado en ISG", 37 | "value paths": "Caminos de valor", 38 | "value paths_help": "Define las rutas de los valores en las páginas ISG.", 39 | "value paths_tips": "Final de las URL detrás de \"/?s=\" de las páginas ISG para valores, separadas por \";\"", 40 | "yes": "Sí" 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '39 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v4 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v4 73 | -------------------------------------------------------------------------------- /.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 | - "master" 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 | jobs: 17 | # Performs quick checks before the expensive test runs 18 | check-and-lint: 19 | if: contains(github.event.head_commit.message, '[skip ci]') == false 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: ioBroker/testing-action-check@v1 25 | with: 26 | node-version: '22.x' 27 | # Uncomment the following line if your adapter cannot be installed using 'npm ci' 28 | # install-command: 'npm install' 29 | lint: true 30 | 31 | # Runs adapter tests on all supported node versions and OSes 32 | adapter-tests: 33 | needs: [check-and-lint] 34 | 35 | if: contains(github.event.head_commit.message, '[skip ci]') == false 36 | 37 | runs-on: ${{ matrix.os }} 38 | strategy: 39 | matrix: 40 | node-version: [20.x, 22.x, 24.x] 41 | os: [ubuntu-latest, windows-latest, macos-latest] 42 | 43 | 44 | steps: 45 | - uses: ioBroker/testing-action-adapter@v1 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | os: ${{ matrix.os }} 49 | # Uncomment the following line if your adapter cannot be installed using 'npm ci' 50 | # install-command: 'npm install' 51 | 52 | # TODO: To enable automatic npm releases, create a token on npmjs.org 53 | # Enter this token as a GitHub secret (with name NPM_TOKEN) in the repository options 54 | # Then uncomment the following block: 55 | 56 | # Deploys the final package to NPM 57 | deploy: 58 | needs: [check-and-lint, adapter-tests] 59 | 60 | # Permissions are required to create Github releases and npm trusted publishing 61 | permissions: 62 | contents: write 63 | id-token: write 64 | 65 | # Trigger this step only when a commit on any branch is tagged with a version number 66 | if: | 67 | contains(github.event.head_commit.message, '[skip ci]') == false && 68 | github.event_name == 'push' && 69 | startsWith(github.ref, 'refs/tags/v') 70 | 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - uses: ioBroker/testing-action-deploy@v1 75 | with: 76 | node-version: '22.x' 77 | # Uncomment the following line if your adapter cannot be installed using 'npm ci' 78 | # install-command: 'npm install' 79 | # npm-token: ${{ secrets.NPM_TOKEN }} 80 | github-token: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | # When using Sentry for error reporting, Sentry can be informed about new releases 83 | # To enable create a API-Token in Sentry (User settings, API keys) 84 | # Enter this token as a GitHub secret (with name SENTRY_AUTH_TOKEN) in the repository options 85 | # Then uncomment and customize the following block: 86 | # sentry: true 87 | # sentry-token: ${{ secrets.SENTRY_AUTH_TOKEN }} 88 | # sentry-project: "iobroker-oekofen-json" 89 | # sentry-version-prefix: "iobroker.oekofen-json" 90 | # # If your sentry project is linked to a GitHub repository, you can enable the following option 91 | # # sentry-github-integration: true 92 | -------------------------------------------------------------------------------- /admin/jsonConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": true, 3 | "type": "tabs", 4 | "tabsStyle": { 5 | "width": "calc(100% - 100px)" 6 | }, 7 | "items": { 8 | "tab_main": { 9 | "type": "panel", 10 | "label": "Main settings", 11 | "items": { 12 | "isgAddress": { 13 | "type": "text", 14 | "label": "IP address or domain", 15 | "help": "IP address or domain_help", 16 | "tooltip": "IP address or domain_tips", 17 | "default": "", 18 | "xs": 12, 19 | "sm": 12, 20 | "md": 6, 21 | "lg": 4, 22 | "xl": 4 23 | }, 24 | "isgUser": { 25 | "type": "text", 26 | "label": "username", 27 | "help": "username_help", 28 | "tooltip": "username_tips", 29 | "default": "", 30 | "xs": 12, 31 | "sm": 12, 32 | "md": 6, 33 | "lg": 4, 34 | "xl": 4 35 | }, 36 | "isgPassword": { 37 | "type": "password", 38 | "repeat": false, 39 | "visible": true, 40 | "label": "isg password", 41 | "help": "isg password_help", 42 | "tooltip": "isg password_tips", 43 | "default": "", 44 | "xs": 12, 45 | "sm": 12, 46 | "md": 6, 47 | "lg": 4, 48 | "xl": 4 49 | }, 50 | "isgUmlauts": { 51 | "type": "select", 52 | "label": "umlauts active", 53 | "help": "umlauts active_help", 54 | "tooltip": "umlauts active_tips", 55 | "default": "no", 56 | "options": [ 57 | { "value": "yes", "label": "yes" }, 58 | { "value": "no", "label": "no" } 59 | ], 60 | "xs": 12, 61 | "sm": 12, 62 | "md": 6, 63 | "lg": 4, 64 | "xl": 4 65 | }, 66 | "isgExpert": { 67 | "type": "checkbox", 68 | "label": "enable expert values", 69 | "help": "enable expert values_help", 70 | "tooltip": "enable expert values_tips", 71 | "default": false, 72 | "xs": 12, 73 | "sm": 12, 74 | "md": 6, 75 | "lg": 4, 76 | "xl": 4 77 | }, 78 | "isgIntervall": { 79 | "type": "number", 80 | "label": "intervall (s)", 81 | "help": "intervall (s)_help", 82 | "default": 60, 83 | "min": 1, 84 | "xs": 12, 85 | "sm": 12, 86 | "md": 6, 87 | "lg": 4, 88 | "xl": 4 89 | }, 90 | "isgCommandIntervall": { 91 | "type": "number", 92 | "label": "intervall (commands) (s)", 93 | "help": "intervall (commands) (s)_help", 94 | "default": 3600, 95 | "min": 1, 96 | "xs": 12, 97 | "sm": 12, 98 | "md": 6, 99 | "lg": 4, 100 | "xl": 4 101 | } 102 | } 103 | }, 104 | "tab_urls": { 105 | "type": "panel", 106 | "label": "URLs", 107 | "items": { 108 | "isgCommandPaths": { 109 | "type": "text", 110 | "label": "command paths", 111 | "help": "command paths_help", 112 | "tooltip": "command paths_tips", 113 | "default": "0;4,0,0;4,0,1;4,0,2;4,0,3;4,0,4;4,0,5;4,1,0;4,1,1;4,2,0;4,2,1;4,2,2;4,2,4;4,2,6;4,2,3;4,2,5;4,2,7;4,3;4,3,0;4,3,1;4,3,2;4,3,3;4,3,4", 114 | "xs": 12, 115 | "sm": 12, 116 | "md": 12, 117 | "lg": 12, 118 | "xl": 12 119 | }, 120 | "isgValuePaths": { 121 | "type": "text", 122 | "label": "value paths", 123 | "help": "value paths_help", 124 | "tooltip": "value paths_tips", 125 | "default": "1,0;1,1;1,2;2,3;2,13", 126 | "xs": 12, 127 | "sm": 12, 128 | "md": 12, 129 | "lg": 12, 130 | "xl": 12 131 | }, 132 | "isgStatusPaths": { 133 | "type": "text", 134 | "label": "status paths", 135 | "help": "status paths_help", 136 | "tooltip": "status paths_tips", 137 | "default": "1,0;2,0", 138 | "xs": 12, 139 | "sm": 12, 140 | "md": 12, 141 | "lg": 12, 142 | "xl": 12 143 | }, 144 | "isgExpertPaths": { 145 | "type": "text", 146 | "label": "expert paths", 147 | "help": "expert paths_help", 148 | "tooltip": "expert paths_tips", 149 | "default": "4,9,0;4,9,1;4,9,2;4,9,3;4,9,4;4,9,5;4,7;4,7,1;4,7,2;4,7,3;4,7,4;4,7,5;4,7,6;4,7,7;4,7,8", 150 | "xs": 12, 151 | "sm": 12, 152 | "md": 12, 153 | "lg": 12, 154 | "xl": 12 155 | } 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/stiebel-isg.png) 2 | 3 | # ioBroker.stiebel-isg 4 | 5 | [![NPM version](https://img.shields.io/npm/v/iobroker.stiebel-isg.svg)](https://www.npmjs.com/package/iobroker.stiebel-isg) 6 | ![Version (stable)](https://iobroker.live/badges/stiebel-isg-stable.svg) 7 | [![Downloads](https://img.shields.io/npm/dm/iobroker.stiebel-isg.svg)](https://www.npmjs.com/package/iobroker.stiebel-isg) 8 | ![Number of Installations (latest)](https://iobroker.live/badges/stiebel-isg-installed.svg) 9 | 10 | **Tests:** ![Test and Release](https://github.com/iobroker-community-adapters/ioBroker.stiebel-isg/workflows/Test%20and%20Release/badge.svg) 11 | 12 | [![NPM](https://nodei.co/npm/iobroker.stiebel-isg.svg?data=d,s)](https://www.npmjs.com/package/iobroker.stiebel-isg) 13 | 14 | ## ioBroker adapter for STIEBEL ELTRON/Tecalor Internet Service Gateways (ISG) 15 | 16 | This adapter reads values from STIEBEL ELTRON/Tecalor Internet Service Gateways (ISG) web pages and can send commands to control the device. 17 | 18 | **NOTE:** This adapter has been tested with legacy ISG devices only. (ISG Plus and ISG Web). Whether it works with the current ISG Connect device is to be determined yet. 19 | 20 | **NOTE:** This adapter has been transferred to iobroker-community-adapters for maintenance. Only important bug fixes and dependency updates will be released in the future. However PRs with bug fixes or feature enhancements are always welcome. 21 | 22 | **Credits:** This adapter would not have been possible without the great work of Michael Schuster (unltdnetworx) , who created previous releases of this adapter. 23 | 24 | ## Release Notes 25 | 26 | **Caution:** Version 2.0.x includes some Breaking Changes: 27 | 28 | * node.js >= 20, js-controller >= 6.0.11 and admin >= 7.6.17 is required 29 | Upgrade your ioBroker to at least this software level, if you want to use this adapter 30 | 31 | * Password and username encryption in config UI 32 | If you update this adapter from a previous version instead of a new installation, the adapter may not start, even if your password and username in your config is correct and has not been changed. To fix this, simply enter the same previous password and username once more in the config UI and store and close the config UI to restart the adapter. This of course is only neccessary once after the first start after the update. 33 | 34 | * The type and/or name of some objects in the object tab has changed 35 | If you update this adapter from a previous version instead of a new installation, you may possibly find warnings in the ioBroker log or object values and/or names are not updated correctly. To prevent this from happening, the most simple solution is to stop the adapter in the instances tab of ioBroker, completely delete the object tree in the objects tab and then restart the adapter. However, this is only neccessary once after the update and is not required if you do a clean new installation. 36 | **CAUTION:** Deleting the object tree will erase any user-defined settings e.g. links to other adapters like history or statistics. You will have to recreate them manually, so make sure to remember the details of the settings. 37 | 38 | ## Installation 39 | 40 | 1. You need a fully configured and running STIEBEL ELTRON or Tecalor Internet Service Gateway (ISG Web or ISG Plus) in the same network as your ioBroker server. 41 | 2. Install the adapter on your ioBroker server and create an instance 42 | 43 | ## Configuration 44 | 45 | 1. Configure the instance by entering the IP-address or domain name of the ISG and if configured in the ISG, the user name and password. 46 | 2. The other settings and the the list of the web pages of the ISG on tab URLs may be left at their default values. 47 | 3. You can improve performance and reduce the load on the ISG if you remove any paths from the URLs tab which do not exist in you ISG Web GUI or which you are not interested in. You can easily identify the URLs by opening the ISG SERVICEWELT Web page and open the various navigation tabs one by one. The URL of the respective page is shown in your browser e.g is the value path to INFO/ANLAGE. 48 | 49 | ## Changelog 50 | 51 | 55 | ### **WORK IN PROGRESS** 56 | 57 | * (pdbjjens) **Fixed**: Cleanup some eslint issues 58 | 59 | ### 2.0.2 (2025-11-23) 60 | 61 | * (pdbjjens) **Fixed**: Adapter hangup on wrong credentials. (fixes #127) 62 | 63 | ### 2.0.1 (2025-11-12) 64 | 65 | * (pdbjjens) **Fixed**: ioBroker warnings are avoided by clamping any values exceeding min/max to the min value before setting. (fixes #53 & #65) 66 | 67 | ### 2.0.0 (2025-10-27) 68 | 69 | * (mcm1957) Change: Adapter has been migrated to iobroker-community-adapters organisation 70 | * (mcm1957) Change: Adapter requires node.js >= 20, js-controller >= 6.0.11 and admin >= 7.6.17 now 71 | * (mcm1957) Fix: Dependencies have been updated 72 | * (pdbjjens) Change: remove .npmignore 73 | * (pdbjjens) Change: migrate adapter configuration to jsonConfig 74 | * (pdbjjens) Change: migrate from deprecated "request" http client to native fetch API 75 | * (pdbjjens) Fix: min/max handling 76 | 77 | ### 1.7.7 78 | 79 | * security- and compatibility update 80 | 81 | ### 1.7.6 82 | 83 | * fix error with controller v5 84 | 85 | ## Legal Notices 86 | 87 | STIEBEL ELTRON, TECALOR, ISG and associated logos are trademarks or registered trademarks of STIEBEL ELTRON GmbH & Co KG [https://www.stiebel-eltron.com](https://www.stiebel-eltron.com) 88 | 89 | All other trademarks are the property of their respective owners. 90 | 91 | The authors are in no way endorsed by or affiliated with STIEBEL ELTRON GmbH & Co KG, or any associated subsidiaries, logos or trademarks. 92 | 93 | ## License 94 | 95 | MIT License 96 | 97 | Copyright (c) 2025 iobroker-community-adapters 98 | Copyright (c) 2018-2023 Michael Schuster 99 | 100 | Permission is hereby granted, free of charge, to any person obtaining a copy 101 | of this software and associated documentation files (the "Software"), to deal 102 | in the Software without restriction, including without limitation the rights 103 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 104 | copies of the Software, and to permit persons to whom the Software is 105 | furnished to do so, subject to the following conditions: 106 | 107 | The above copyright notice and this permission notice shall be included in all 108 | copies or substantial portions of the Software. 109 | 110 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 111 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 112 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 113 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 114 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 115 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 116 | SOFTWARE. 117 | -------------------------------------------------------------------------------- /.github/workflows/check-copilot-template.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action: Automated Version Check and Update for ioBroker Copilot Instructions 2 | # Version: 0.4.0 3 | # 4 | # This action automatically checks for template updates and creates issues when updates are available 5 | 6 | name: Check ioBroker Copilot Template Version 7 | 8 | on: 9 | schedule: 10 | - cron: '23 3 * * 0' # Weekly check optimized for off-peak hours (3:23 AM UTC Sunday) 11 | workflow_dispatch: # Allow manual triggering 12 | 13 | jobs: 14 | check-template: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | contents: read 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v6 23 | 24 | - name: Dynamic template version check 25 | id: version-check 26 | run: | 27 | echo "🔍 Starting dynamic ioBroker Copilot template version check..." 28 | 29 | # Get current version from local copilot instructions 30 | if [ -f ".github/copilot-instructions.md" ]; then 31 | CURRENT_VERSION=$(awk '/Version:|Template Version:/ {match($0, /([0-9]+(\.[0-9]+)*)/, arr); if (arr[1] != "") print arr[1]}' .github/copilot-instructions.md | head -1) 32 | if [ -z "$CURRENT_VERSION" ]; then CURRENT_VERSION="unknown"; fi 33 | echo "📋 Current local version: $CURRENT_VERSION" 34 | else 35 | CURRENT_VERSION="none" 36 | echo "❌ No .github/copilot-instructions.md file found" 37 | fi 38 | 39 | # Get latest version from centralized metadata 40 | echo "🌐 Fetching latest template version from centralized config..." 41 | LATEST_VERSION=$(curl -s https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/config/metadata.json | jq -r '.version' 2>/dev/null || echo "unknown") 42 | if [ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" = "null" ]; then 43 | LATEST_VERSION="unknown" 44 | fi 45 | echo "📋 Latest available version: $LATEST_VERSION" 46 | 47 | # Determine repository status 48 | COPILOT_INITIALIZED="false" 49 | UPDATE_NEEDED="false" 50 | SETUP_NEEDED="false" 51 | 52 | if [ "$CURRENT_VERSION" = "none" ]; then 53 | SETUP_NEEDED="true" 54 | echo "🆕 Status: Setup needed - no copilot instructions found" 55 | elif [ "$CURRENT_VERSION" = "unknown" ] || [ "$LATEST_VERSION" = "unknown" ]; then 56 | echo "❓ Status: Cannot determine versions - manual check required" 57 | else 58 | # Compare versions (simple string comparison for now) 59 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then 60 | UPDATE_NEEDED="true" 61 | COPILOT_INITIALIZED="true" 62 | echo "📈 Status: Update needed - $CURRENT_VERSION → $LATEST_VERSION" 63 | else 64 | COPILOT_INITIALIZED="true" 65 | echo "✅ Status: Up to date - version $CURRENT_VERSION" 66 | fi 67 | fi 68 | 69 | # Set outputs for later steps 70 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 71 | echo "latest_version=$LATEST_VERSION" >> $GITHUB_OUTPUT 72 | echo "copilot_initialized=$COPILOT_INITIALIZED" >> $GITHUB_OUTPUT 73 | echo "update_needed=$UPDATE_NEEDED" >> $GITHUB_OUTPUT 74 | echo "setup_needed=$SETUP_NEEDED" >> $GITHUB_OUTPUT 75 | 76 | - name: Check for existing issues 77 | id: issue-check 78 | uses: actions/github-script@v8 79 | with: 80 | script: | 81 | const { data: issues } = await github.rest.issues.listForRepo({ 82 | owner: context.repo.owner, 83 | repo: context.repo.repo, 84 | state: 'open', 85 | labels: 'copilot-setup,automation' 86 | }); 87 | 88 | const setupIssues = issues.filter(issue => 89 | issue.title.includes('Setup ioBroker') || 90 | issue.title.includes('Update ioBroker') || 91 | issue.title.includes('Copilot') 92 | ); 93 | 94 | core.setOutput('has_existing_issue', setupIssues.length > 0); 95 | core.setOutput('existing_count', setupIssues.length); 96 | 97 | if (setupIssues.length > 0) { 98 | console.log(`Found ${setupIssues.length} existing copilot setup/update issue(s)`); 99 | } 100 | 101 | - name: Create initial setup issue 102 | if: steps.version-check.outputs.setup_needed == 'true' && steps.issue-check.outputs.has_existing_issue == 'false' 103 | uses: actions/github-script@v8 104 | with: 105 | script: | 106 | const title = '🤖 Setup ioBroker GitHub Copilot Instructions'; 107 | const body = `# Automated ioBroker Copilot Instructions Setup 108 | 109 | **GitHub Copilot**: Please help me set up GitHub Copilot instructions for my ioBroker adapter repository with full automation and validation. 110 | 111 | ## 🚨 Important Notice - Ignore GitHub Copilot's Own Setup Suggestions 112 | 113 | **GitHub Copilot**: If you see suggestions from GitHub Copilot about setting up copilot instructions (like creating \`.github/copilot-instructions.md\` through GitHub's interface), **IGNORE THEM**. This issue handles the complete setup process with ioBroker-specific optimizations. 114 | 115 | ## Setup Process 116 | 117 | This repository needs initial setup of GitHub Copilot instructions. Please: 118 | 119 | 1. **Initialize with Adapter-Specific Context**: Create \`.github/copilot-instructions.md\` with basic ioBroker adapter context 120 | 2. **Enhance with Full Template**: Merge with comprehensive template from https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/template.md 121 | 3. **Customize Sections**: Populate [CUSTOMIZE] sections with adapter-specific content only 122 | 123 | **Template Version**: ${{ steps.version-check.outputs.latest_version }} 124 | **Detected Status**: No copilot instructions found 125 | 126 | ## Reference Templates 127 | 128 | - **Initial Setup Action**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/templates/ghAction-InitialSetup.yml 129 | - **Full Template**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/template.md 130 | - **Version Check Action**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/templates/ghAction-AutomatedVersionCheckAndUpdate.yml`; 131 | 132 | await github.rest.issues.create({ 133 | owner: context.repo.owner, 134 | repo: context.repo.repo, 135 | title: title, 136 | body: body, 137 | labels: ['copilot-setup', 'automation'] 138 | }); 139 | 140 | - name: Create update issue 141 | if: steps.version-check.outputs.update_needed == 'true' && steps.issue-check.outputs.has_existing_issue == 'false' 142 | uses: actions/github-script@v8 143 | with: 144 | script: | 145 | const title = '🤖 Update ioBroker Copilot Instructions Template'; 146 | const body = `# Automated ioBroker Copilot Template Update Request 147 | 148 | **GitHub Copilot**: Please help me validate and update my ioBroker adapter's Copilot instructions template while preserving all custom sections. 149 | 150 | ## Update Required 151 | 152 | - **Current Version**: ${{ steps.version-check.outputs.current_version }} 153 | - **Latest Version**: ${{ steps.version-check.outputs.latest_version }} 154 | - **Status**: Update needed 155 | 156 | ## Update Process 157 | 158 | Please merge the latest ioBroker template while preserving all customizations: 159 | 160 | \`\`\` 161 | Merge the ioBroker template from https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/template.md with my existing .github/copilot-instructions.md. 162 | 163 | CRITICAL REQUIREMENTS: 164 | 1. Preserve ALL [CUSTOMIZE] sections and their content exactly as they are 165 | 2. Maintain any project-specific context not already covered in the template 166 | 3. Add the latest ioBroker best practices from the new template 167 | 4. Update the version number to ${{ steps.version-check.outputs.latest_version }} 168 | 5. Keep the Template Source reference up-to-date 169 | 6. Ensure no custom content is lost during the merge 170 | 7. REMOVE any duplicate content from [CUSTOMIZE] sections that already exists in the standard template 171 | \`\`\` 172 | 173 | ## Reference Templates 174 | 175 | - **Latest Template**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/template.md 176 | - **Update Template**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/templates/automated-template-update.md 177 | - **Version Check Action**: https://raw.githubusercontent.com/DrozmotiX/ioBroker-Copilot-Instructions/main/templates/ghAction-AutomatedVersionCheckAndUpdate.yml`; 178 | 179 | await github.rest.issues.create({ 180 | owner: context.repo.owner, 181 | repo: context.repo.repo, 182 | title: title, 183 | body: body, 184 | labels: ['template-update', 'automation'] 185 | }); 186 | 187 | - name: Summary 188 | run: | 189 | echo "🎯 Template Version Check Complete" 190 | echo " Current: ${{ steps.version-check.outputs.current_version }}" 191 | echo " Latest: ${{ steps.version-check.outputs.latest_version }}" 192 | echo " Setup needed: ${{ steps.version-check.outputs.setup_needed }}" 193 | echo " Update needed: ${{ steps.version-check.outputs.update_needed }}" 194 | echo " Existing issues: ${{ steps.issue-check.outputs.existing_count }}" 195 | -------------------------------------------------------------------------------- /io-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "name": "stiebel-isg", 4 | "version": "2.0.2", 5 | "news": { 6 | "2.0.2": { 7 | "en": "**Fixed**: Adapter hangup on wrong credentials. (fixes #127)", 8 | "de": "**Fixed**: Adapter hängt bei falschen Anmeldeinformationen. (fixt #127)", 9 | "ru": "** Исправлено**: Адаптер зависает на неправильных учетных данных. (Исправления #127)", 10 | "pt": "**Resolvido**: Desligamento do adaptador com credenciais erradas. (fixos #127)", 11 | "nl": "**Fixed**: Adapter hangup op verkeerde referenties. (fixeert #127)", 12 | "fr": "**Fixé**: Adaptateur suspendu sur de mauvaises références. (fixe #127)", 13 | "it": "**Fixed**: Hangup adattatore su credenziali sbagliate. (fissi n. 127)", 14 | "es": "**Fixed**: Colgador de Adaptador en credenciales equivocadas. (fixes #127)", 15 | "pl": "* *Naprawiono* *: Rozłączanie adapterów na złych referencjach. (poprawki # 127) ", 16 | "uk": "**Фіксований**: Переміщення адаптера на невірних облікових записах. (фікси #127)", 17 | "zh-cn": "** Fixed**:适配器挂在错误的证书上. (编号 #127)" 18 | }, 19 | "2.0.1": { 20 | "en": "**Fixed**: ioBroker warnings are avoided by clamping any values exceeding min/max to the min value before setting. (fixes #53 & #65)", 21 | "de": "**Fixed**: ioBroker Warnungen werden vermieden, indem Werte größer als min/max vor dem Setzen auf den min-Wert gesetzt werden. (Fixes #53 & #65)", 22 | "ru": "** Фиксированный**: предостережения ioBroker избегают, зажимая любые значения, превышающие мин/макс, до значения мин перед установкой. (Исправления #53 и #65)", 23 | "pt": "**Resolvido**: as advertências de ioBroker são evitadas através da fixação de quaisquer valores superiores a min/máx ao valor min antes da regulação. (fixos #53 & #65)", 24 | "nl": "**Fixed**: ioBroker-waarschuwingen worden vermeden door waarden boven min/max aan de min-waarde te bevestigen voordat ze worden ingesteld. (fixeert #53 & #65)", 25 | "fr": "**Fixé**: les avertissements ioBroker sont évités en resserrant toute valeur dépassant min/max jusqu'à la valeur min avant réglage. (fixe #53 & #65)", 26 | "it": "**Fixed**: gli avvisi ioBroker vengono evitati bloccando i valori superiori a min/max al valore minimo prima dell'impostazione. (fissi n. 53 e n.65)", 27 | "es": "**Fixed**: las advertencias de ioBroker se evitan mediante la fijación de valores superiores a min/max al valor min antes de establecer. (fixes #53 > 65)", 28 | "pl": "* *Naprawiono* *: przed ustawieniem ostrzeżeń jOBrokera należy unikać zaciskania wartości przekraczających min / max do wartości min. (poprawki # 53 i # 65) ", 29 | "uk": "**Фіксований**: попередження ioBroker уникнути, затискаючи будь-які значення, що перевищують min/max до мінімального значення перед встановленням. (фікси #53 та #65)", 30 | "zh-cn": "** Fixed**: ioBroker 警告通过在设定前将任何超过分钟/最大值的值夹住到分钟值来避免。 (编号#53 & 65)" 31 | }, 32 | "2.0.0": { 33 | "en": "Change: Adapter has been migrated to iobroker-community-adapters organisation\nChange: Adapter requires node.js >= 20, js-controller >= 6.0.11 and admin >= 7.6.17 now\nFix: Dependencies have been updated\nChange: remove .npmignore\nChange: migrate adapter configuration to jsonConfig\nChange: migrate from deprecated \"request\" http client to native fetch API\nFix: min/max handling", 34 | "de": "Änderung: Adapter wurde auf iobroker-community-Adapter-Organisation migriert\nÄnderung: Adapter erfordert node.js >= 20, js-controller >= 6.0.11 und admin >= 7.6.17\nFix: Abhängigkeiten wurden aktualisiert\nÄnderung: .npmignore entfernt\nÄnderung: Adapterkonfiguration auf jsonConfig migriert\nÄnderung: http-Client zu nativer fetch API migriert\nFix: min/max Handhabung", 35 | "ru": "Изменение: Адаптер был перемещен в организацию адаптации сообщества йоброкеров\nАдаптер требует node.js >= 20, js-контроллер >= 6.0.11 и админ >= 7.6.17\nИсправление: Зависимости были обновлены\nИзменение: удалить .npmignore\nИзменение: перенос конфигурации адаптера в jsonConfig\nИзменение: переход от устаревшего http-клиента «запрос» к нативному API fetch\nИсправление: min/max обработка", 36 | "pt": "Mudança: O adaptador foi migrado para a organização de adaptadores de comunidades iobroker\nAlteração: O adaptador necessita de nodo.js >= 20, js-controlador >= 6.0.11 e administrador >= 7.6.17 agora\nCorrigir: Dependências foram atualizadas\nAlteração: remover .npmignore\nAlterar: migrar a configuração do adaptador para o jsonConfig\nAlteração: migrar do \"requisito\" obsoleto do cliente http para a API de busca nativa\nFixação: manipulação min/max", 37 | "nl": "Verandering: Adapter is gemigreerd naar iobroker-community-adapters organisatie\nVoor de toepassing van deze onderverdeling wordt verstaan onder:\nFix: Afhankelijkheden zijn bijgewerkt\nWijzigen: verwijderen .npmignore\nWijzigen: adapterconfiguratie migreren naar jsonConfig\nVerandering: migreren van verouderde \"verzoek\" http client naar native apport API\nFix: min/max hantering", 38 | "fr": "Changement: l'adaptateur a été transféré vers l'organisation iobroker-community-adapters\nChangement: Adapter nécessite node.js >= 20, js-controller >= 6.0.11 et admin >= 7.6.17 maintenant\nCorrection : Les dépendances ont été mises à jour\nChangement: supprimer .npmignore\nChangement : migrer la configuration de l'adaptateur vers jsonConfig\nChangement : migrer de la \"requête\" obsolète http client vers native fetch API\nFixe: manipulation min/max", 39 | "it": "Modifica: L'adattatore è stato migrato all'organizzazione di adattatori della comunità-iobroker\nModifica: Adattatore richiede node.js >= 20, js-controller >= 6.0.11 e admin >= 7.6.17 ora\nFisso: Le dipendenze sono state aggiornate\nCambio: rimuovere .npmignore\nModifica: migrare la configurazione dell'adattatore a jsonConfig\nCambiamento: migrare da deprecated \"request\" http client to native fetch API\nFisso: min/max movimentazione", 40 | "es": "Cambio: Adaptador ha sido migrado a la organización de ibroker-community-adapters\nCambio: Adaptador requiere node.js >= 20, js-controller √= 6.0.11 y admin= 7.6.17 ahora\nFijación: Se han actualizado las dependencias\nCambio: retirar .npmignore\nCambio: configuración del adaptador de migrar a jsonConfig\nCambio: migrar desde el cliente http de \"request\" deprecatado a la API de lotes nativos\nFijación: min/max manipulación", 41 | "pl": "Zmiana: Adapter został przeniesiony do organizacji adapterów społeczności iobrokerowej\nZmiana: Adapter wymaga node.js > = 20, kontroler js- i admin > = 7.6.17\nFix: Zależności zostały zaktualizowane\nZmiana: usunąć .npmignore\nZmiana: migrate konfiguracji adaptera do jsonConfig\nZmiana: migrate z \"request\" http client to native fetch API\nFix: min / max handling", 42 | "uk": "Зміна: Перехідник мігрований до організації iobroker-community-adapters\nЗміна: Адаптер вимагає node.js >= 20, js-controller >=6.0.11 і admin >= 7.6.17 тепер\nВиправлення: В залежності було оновлено\nЗміна: видалення .npmignore\nЗміна: налаштування адаптера migrate до jsonConfig\nЗміна: migrate від deprecated \"request\"\nФіксація: обробка хв/max", 43 | "zh-cn": "变化:适应者已迁移到职业经纪人-社区适应者组织\n更改:适配器需要节点.js >= 20,js控制器 >= 6.0.11和管理员 >= 7.6.17 现在\n修正: 已更新依赖性\n更改: 删除.npmignore\n更改: 将适配器配置迁移到 jsonConfig\n更改: 从已贬值的“ 请求” http客户端迁移到本地获取 API\n固定:分钟/最大处理" 44 | }, 45 | "2.0.0-alpha.1": { 46 | "en": "Adapter has been migrated to iobroker-communita-adapters organisation\nAdapter requires node.js >= 20, js-controller >= 6.0.11 and admin >= 7.6.17 now\nDependencies have been updated", 47 | "de": "Adapter wurde auf iobroker-communita-adapter Organisation migriert\nAdapter benötigt node.js >= 20, js-controller >= 6.0.11 und admin >= 7.6.17 jetzt\nAbhängigkeiten wurden aktualisiert", 48 | "ru": "Адаптер мигрировал в организацию iobroker-communita-adapters\nАдаптер требует node.js >= 20, js-контроллер >= 6.0.11 и админ >= 7.6.17 сейчас\nЗависимости были обновлены", 49 | "pt": "Adaptador foi migrado para iobroker-communita-adaptadores organização\nO adaptador necessita de nodo.js >= 20, js-controlador >= 6.0.11 e administrador >= 7.6.17 agora\nAs dependências foram atualizadas", 50 | "nl": "Adapter is gemigreerd naar iobroker-communita-adapters organisatie\nVoor adapters zijn node.js < 20, js-controller <= 6,0.11 en admin <= 7.6.17 nu vereist\nAfhankelijkheden zijn bijgewerkt", 51 | "fr": "Adapter a été migré vers iobroker-communita-adaptateurs organisation\nAdapter nécessite node.js >= 20, js-controller >= 6.0.11 et admin >= 7.6.17 maintenant\nLes dépendances ont été actualisées", 52 | "it": "L'adattatore è stato migrato all'organizzazione iobroker-communita-adapters\nAdattatore richiede node.js >= 20, js-controller >= 6.0.11 e admin >= 7.6.17 ora\nLe dipendenze sono state aggiornate", 53 | "es": "Adapter ha sido migrado a iobroker-communita-adapters organisation\nAdaptador requiere node.js ю= 20, js-controller √= 6.0.11 y admin= 7.6.17 ahora\nSe han actualizado las dependencias", 54 | "pl": "Adapter został przeniesiony do organizacji adapterów iobroker- communita\nAdapter wymaga node.js > = 20, kontroler js- i admin > = 7.6.17\nZaktualizowano zależności", 55 | "uk": "Перехідник мігрований до організації iobroker-communita-adapters\nАдаптер вимагає node.js >= 20, js-controller >= 6.0.11 і admin >= 7.6.17 тепер\nЗалежність було оновлено", 56 | "zh-cn": "适应者已迁移到职业中介-社区适应者组织\n适配器需要节点.js >= 20,js控制器 >= 6.0.11和admin >= 7.6.17 现在\n依赖关系已更新" 57 | }, 58 | "1.7.7": { 59 | "en": "code enhancements for js-controller v5", 60 | "de": "Codeerweiterungen für js-controller v5", 61 | "ru": "улучшения кода для js-controller v5", 62 | "pt": "melhorias de código para js-controller v5", 63 | "nl": "codeverhoging voor js-controller v5", 64 | "fr": "améliorations de code pour js-controller v5", 65 | "it": "miglioramenti del codice per js-controller v5", 66 | "es": "mejoras de código para js-controller v5", 67 | "pl": "rozszerzenia kodu dla js-controller v5", 68 | "uk": "javascript licenses api веб-сайт go1.13.8", 69 | "zh-cn": "b. 加强对控制的容忍 v 5" 70 | }, 71 | "1.7.6": { 72 | "en": "fix for js-controller v5", 73 | "de": "Anpassungen für js-controller v5", 74 | "ru": "фикс для js-controller v5", 75 | "pt": "correção para js-controller v5", 76 | "nl": "vertaling:", 77 | "fr": "fixation pour js-controller v5", 78 | "it": "correzione per js-controller v5", 79 | "es": "fijado para js-controller v5", 80 | "pl": "zastosowanie js-controller v5", 81 | "uk": "фіксація для js-controller v5", 82 | "zh-cn": "fix 控方诉5" 83 | }, 84 | "1.7.5": { 85 | "en": "security improvements", 86 | "de": "Verbesserung der Sicherheit", 87 | "ru": "улучшения безопасности", 88 | "pt": "melhorias de segurança", 89 | "nl": "beveiliging verbetert", 90 | "fr": "amélioration de la sécurité", 91 | "it": "miglioramento della sicurezza", 92 | "es": "mejoras de seguridad", 93 | "pl": "poprawa bezpieczeństwa", 94 | "uk": "підвищення безпеки", 95 | "zh-cn": "安全改善" 96 | } 97 | }, 98 | "titleLang": { 99 | "en": "Stiebel-ISG/Tecalor-ISG", 100 | "de": "Stiebel-ISG/Tecalor-ISG", 101 | "ru": "Stiebel-ISG/Текалор-ISG", 102 | "pt": "Stiebel-ISG/Tecalor-ISG", 103 | "nl": "Stiebel-ISG/Tecalor-ISG", 104 | "fr": "Stiebel-ISG/Tecalor-ISG", 105 | "it": "Stiebel-ISG/Tecalor-ISG", 106 | "es": "Stiebel-ISG/Tecalor-ISG", 107 | "pl": "Stiebel-ISG/Tecalor-ISG", 108 | "uk": "Стібел-ІСГ/Текалор-ІСГ", 109 | "zh-cn": "Stiebel-ISG/Tecalor-ISG" 110 | }, 111 | "desc": { 112 | "en": "This adapter is a ment to read values from stiebel-eltron/tecalor internet service gateways (ISG) and control the device.", 113 | "de": "Dieser Adapter dient zum Auslesen der Werten des Internetdienst-Gateways (ISG) von Stiebel-Eltron / Tecalor und zum Steuern des Geräts.", 114 | "ru": "Этот адаптер является ментом для чтения значений от stiebel-eltron/tecalor internet service Gateways (ISG) и управления устройством.", 115 | "pt": "Este adaptador é um complemento para ler valores de gateways de serviço de Internet esteibel-eltron/tecalor (ISG) e controlar o dispositivo.", 116 | "nl": "Deze adapter is een ment om waarden te lezen van stiebel-eltron/tecalor internetbediening en controleer het apparaat.", 117 | "fr": "Cet adaptateur est un ment pour lire les valeurs des passerelles de service Internet stiebel-eltron/tecalor (ISG) et contrôler l'appareil.", 118 | "it": "Questo adattatore è un'azione per leggere i valori dai gateway di servizio internet di stiebel-eltron/tecalor (ISG) e controllare il dispositivo.", 119 | "es": "Este adaptador es un ment para leer valores de los portales de servicio stiebel-eltron/tecalor de Internet (ISG) y controlar el dispositivo.", 120 | "pl": "Ta adapter jest mentem do czytania wartości od stiebel-eltron/tecalorowych portali serwisowych (ISG) i kontroli urządzenia.", 121 | "uk": "Цей адаптер є домом для зчитування значень від stiebel-eltron/tecalor Інтернет-додатків (ISG) та керування пристроєм.", 122 | "zh-cn": "这种适应者是从半贝尔-客运/保护者互联网服务网(ISG)和控制该装置的价值观。." 123 | }, 124 | "authors": [ 125 | "Michael Schuster ", 126 | "iobroker-community-adapters ", 127 | "mcm1957 " 128 | ], 129 | "keywords": [ 130 | "Stiebel-Eltron/Tecalor", 131 | "Internet Service Gateway", 132 | "ISG" 133 | ], 134 | "tier": 2, 135 | "licenseInformation": { 136 | "license": "MIT", 137 | "type": "free" 138 | }, 139 | "platform": "Javascript/Node.js", 140 | "icon": "stiebel-isg.png", 141 | "enabled": true, 142 | "extIcon": "https://raw.githubusercontent.com/iobroker-community-adapters/ioBroker.stiebel-isg/master/admin/stiebel-isg.png", 143 | "readme": "https://github.com/iobroker-community-adapters/ioBroker.stiebel-isg/blob/master/README.md", 144 | "loglevel": "info", 145 | "mode": "daemon", 146 | "type": "climate-control", 147 | "compact": true, 148 | "connectionType": "local", 149 | "dataSource": "poll", 150 | "adminUI": { 151 | "config": "json" 152 | }, 153 | "dependencies": [ 154 | { 155 | "js-controller": ">=6.0.11" 156 | } 157 | ], 158 | "globalDependencies": [ 159 | { 160 | "admin": ">=7.6.17" 161 | } 162 | ] 163 | }, 164 | "protectedNative": [ 165 | "isgUser", 166 | "isgPassword" 167 | ], 168 | "encryptedNative": [ 169 | "isgUser", 170 | "isgPassword" 171 | ], 172 | "native": { 173 | "isgUser": "", 174 | "isgPassword": "", 175 | "isgAddress": "", 176 | "isgIntervall": 60, 177 | "isgCommandIntervall": 3600, 178 | "isgUmlauts": "no", 179 | "isgExpert": false, 180 | "isgCommandPaths": "0;4,0,0;4,0,1;4,0,2;4,0,3;4,0,4;4,0,5;4,1,0;4,1,1;4,2,0;4,2,1;4,2,2;4,2,4;4,2,6;4,2,3;4,2,5;4,2,7;4,3;4,3,0;4,3,1;4,3,2;4,3,3;4,3,4", 181 | "isgValuePaths": "1,0;1,1;1,2;2,3;2,13", 182 | "isgStatusPaths": "1,0;2,0", 183 | "isgExpertPaths": "4,9,0;4,9,1;4,9,2;4,9,3;4,9,4;4,9,5;4,7;4,7,1;4,7,2;4,7,3;4,7,4;4,7,5;4,7,6;4,7,7;4,7,8" 184 | }, 185 | "objects": [], 186 | "instanceObjects": [ 187 | { 188 | "_id": "info", 189 | "type": "channel", 190 | "common": { 191 | "name": "Information", 192 | "type": "string" 193 | }, 194 | "native": {} 195 | }, 196 | { 197 | "_id": "info.connection", 198 | "type": "state", 199 | "common": { 200 | "role": "indicator.connected", 201 | "name": "ISG device connected", 202 | "type": "boolean", 203 | "read": true, 204 | "write": false, 205 | "def": false 206 | }, 207 | "native": {} 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | 2 | # ioBroker Adapter Development with GitHub Copilot 3 | 4 | **Version:** 0.4.2 5 | **Template Source:** https://github.com/DrozmotiX/ioBroker-Copilot-Instructions 6 | 7 | This file contains instructions and best practices for GitHub Copilot when working on ioBroker adapter development. 8 | 9 | ## Project Context 10 | 11 | You are working on an ioBroker adapter. ioBroker is an integration platform for the Internet of Things, focused on building smart home and industrial IoT solutions. Adapters are plugins that connect ioBroker to external systems, devices, or services. 12 | 13 | ### Adapter-Specific Context: Stiebel-ISG 14 | 15 | This adapter interfaces with **Stiebel-Eltron/Tecalor Internet Service Gateway (ISG)** devices. The ISG is a web-based gateway that provides access to heat pump systems and enables remote monitoring and control. 16 | 17 | #### Key Features: 18 | - **Device Communication**: Communicates with ISG devices via HTTP/HTTPS requests 19 | - **Web Scraping**: Uses Cheerio to parse HTML responses from the ISG web interface 20 | - **Cookie Management**: Maintains session cookies using tough-cookie for authenticated requests 21 | - **Polling**: Regularly polls the ISG for status updates and values 22 | - **Command Execution**: Allows sending commands to control the heat pump system 23 | - **Multi-language Support**: Supports multiple languages through i18n translations 24 | 25 | #### Configuration Parameters: 26 | - `isgUser`: Username for ISG authentication 27 | - `isgPassword`: Password for ISG authentication 28 | - `isgAddress`: IP address or hostname of the ISG device 29 | - `isgIntervall`: Polling interval for reading values (in seconds) 30 | - `isgCommandIntervall`: Interval for reading command paths (in seconds) 31 | - `isgUmlauts`: Handling of German umlauts in state names 32 | - `isgExpert`: Enable expert mode for additional parameters 33 | - `isgCommandPaths`: Semicolon-separated list of command paths to monitor 34 | - `isgValuePaths`: Semicolon-separated list of value paths to read 35 | - `isgStatusPaths`: Semicolon-separated list of status paths to monitor 36 | - `isgExpertPaths`: Additional paths for expert mode 37 | 38 | #### Technical Stack: 39 | - **HTTP Client**: Uses the native `fetch` API for HTTP communication 40 | - **HTML Parsing**: Uses Cheerio for parsing ISG web interface responses 41 | - **Cookie Management**: Uses tough-cookie for session management 42 | - **Testing**: Uses Mocha for unit and integration tests 43 | 44 | #### Development Considerations: 45 | - The ISG device may not be publicly accessible, so tests should be designed to work with mock data 46 | - Authentication is handled via web forms and session cookies 47 | - The adapter implements a reboot command for the ISG device 48 | - State changes trigger command execution on the ISG device 49 | - Resource cleanup is critical - ensure all intervals are cleared in the unload method 50 | 51 | ## Testing 52 | 53 | ### Unit Testing 54 | - Use Jest as the primary testing framework for ioBroker adapters 55 | - Create tests for all adapter main functions and helper methods 56 | - Test error handling scenarios and edge cases 57 | - Mock external API calls and hardware dependencies 58 | - For adapters connecting to APIs/devices not reachable by internet, provide example data files to allow testing of functionality without live connections 59 | - Example test structure: 60 | ```javascript 61 | describe('AdapterName', () => { 62 | let adapter; 63 | 64 | beforeEach(() => { 65 | // Setup test adapter instance 66 | }); 67 | 68 | test('should initialize correctly', () => { 69 | // Test adapter initialization 70 | }); 71 | }); 72 | ``` 73 | 74 | ### Integration Testing 75 | 76 | **IMPORTANT**: Use the official `@iobroker/testing` framework for all integration tests. This is the ONLY correct way to test ioBroker adapters. 77 | 78 | **Official Documentation**: https://github.com/ioBroker/testing 79 | 80 | #### Framework Structure 81 | Integration tests MUST follow this exact pattern: 82 | 83 | ```javascript 84 | const path = require('path'); 85 | const { tests } = require('@iobroker/testing'); 86 | 87 | // Define test coordinates or configuration 88 | const TEST_COORDINATES = '52.520008,13.404954'; // Berlin 89 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 90 | 91 | // Use tests.integration() with defineAdditionalTests 92 | tests.integration(path.join(__dirname, '..'), { 93 | defineAdditionalTests({ suite }) { 94 | suite('Test adapter with specific configuration', (getHarness) => { 95 | let harness; 96 | 97 | before(() => { 98 | harness = getHarness(); 99 | }); 100 | 101 | it('should configure and start adapter', function () { 102 | return new Promise(async (resolve, reject) => { 103 | try { 104 | harness = getHarness(); 105 | 106 | // Get adapter object using promisified pattern 107 | const obj = await new Promise((res, rej) => { 108 | harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => { 109 | if (err) return rej(err); 110 | res(o); 111 | }); 112 | }); 113 | 114 | if (!obj) { 115 | return reject(new Error('Adapter object not found')); 116 | } 117 | 118 | // Configure adapter properties 119 | Object.assign(obj.native, { 120 | position: TEST_COORDINATES, 121 | createCurrently: true, 122 | createHourly: true, 123 | createDaily: true, 124 | // Add other configuration as needed 125 | }); 126 | 127 | // Set the updated configuration 128 | harness.objects.setObject(obj._id, obj); 129 | 130 | console.log('✅ Step 1: Configuration written, starting adapter...'); 131 | 132 | // Start adapter and wait 133 | await harness.startAdapterAndWait(); 134 | 135 | console.log('✅ Step 2: Adapter started'); 136 | 137 | // Wait for adapter to process data 138 | const waitMs = 15000; 139 | await wait(waitMs); 140 | 141 | console.log('🔍 Step 3: Checking states after adapter run...'); 142 | 143 | // Get all states created by adapter 144 | const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*'); 145 | 146 | console.log(`📊 Found ${stateIds.length} states`); 147 | 148 | if (stateIds.length > 0) { 149 | console.log('✅ Adapter successfully created states'); 150 | 151 | // Show sample of created states 152 | const allStates = await new Promise((res, rej) => { 153 | harness.states.getStates(stateIds, (err, states) => { 154 | if (err) return rej(err); 155 | res(states || []); 156 | }); 157 | }); 158 | 159 | console.log('📋 Sample states created:'); 160 | stateIds.slice(0, 5).forEach((stateId, index) => { 161 | const state = allStates[index]; 162 | console.log(` ${stateId}: ${state && state.val !== undefined ? state.val : 'undefined'}`); 163 | }); 164 | 165 | await harness.stopAdapter(); 166 | resolve(true); 167 | } else { 168 | console.log('❌ No states were created by the adapter'); 169 | reject(new Error('Adapter did not create any states')); 170 | } 171 | } catch (error) { 172 | reject(error); 173 | } 174 | }); 175 | }).timeout(40000); 176 | }); 177 | } 178 | }); 179 | ``` 180 | 181 | #### Testing Both Success AND Failure Scenarios 182 | 183 | **IMPORTANT**: For every "it works" test, implement corresponding "it doesn't work and fails" tests. This ensures proper error handling and validates that your adapter fails gracefully when expected. 184 | 185 | ```javascript 186 | // Example: Testing successful configuration 187 | it('should configure and start adapter with valid configuration', function () { 188 | return new Promise(async (resolve, reject) => { 189 | // ... successful configuration test as shown above 190 | }); 191 | }).timeout(40000); 192 | 193 | // Example: Testing failure scenarios 194 | it('should NOT create daily states when daily is disabled', function () { 195 | return new Promise(async (resolve, reject) => { 196 | try { 197 | harness = getHarness(); 198 | 199 | console.log('🔍 Step 1: Fetching adapter object...'); 200 | const obj = await new Promise((res, rej) => { 201 | harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => { 202 | if (err) return rej(err); 203 | res(o); 204 | }); 205 | }); 206 | 207 | if (!obj) return reject(new Error('Adapter object not found')); 208 | console.log('✅ Step 1.5: Adapter object loaded'); 209 | 210 | console.log('🔍 Step 2: Updating adapter config...'); 211 | Object.assign(obj.native, { 212 | position: TEST_COORDINATES, 213 | createCurrently: false, 214 | createHourly: true, 215 | createDaily: false, // Daily disabled for this test 216 | }); 217 | 218 | await new Promise((res, rej) => { 219 | harness.objects.setObject(obj._id, obj, (err) => { 220 | if (err) return rej(err); 221 | console.log('✅ Step 2.5: Adapter object updated'); 222 | res(undefined); 223 | }); 224 | }); 225 | 226 | console.log('🔍 Step 3: Starting adapter...'); 227 | await harness.startAdapterAndWait(); 228 | console.log('✅ Step 4: Adapter started'); 229 | 230 | console.log('⏳ Step 5: Waiting 20 seconds for states...'); 231 | await new Promise((res) => setTimeout(res, 20000)); 232 | 233 | console.log('🔍 Step 6: Fetching state IDs...'); 234 | const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*'); 235 | 236 | console.log(`📊 Step 7: Found ${stateIds.length} total states`); 237 | 238 | const hourlyStates = stateIds.filter((key) => key.includes('hourly')); 239 | if (hourlyStates.length > 0) { 240 | console.log(`✅ Step 8: Correctly ${hourlyStates.length} hourly weather states created`); 241 | } else { 242 | console.log('❌ Step 8: No hourly states created (test failed)'); 243 | return reject(new Error('Expected hourly states but found none')); 244 | } 245 | 246 | // Check daily states should NOT be present 247 | const dailyStates = stateIds.filter((key) => key.includes('daily')); 248 | if (dailyStates.length === 0) { 249 | console.log(`✅ Step 9: No daily states found as expected`); 250 | } else { 251 | console.log(`❌ Step 9: Daily states present (${dailyStates.length}) (test failed)`); 252 | return reject(new Error('Expected no daily states but found some')); 253 | } 254 | 255 | await harness.stopAdapter(); 256 | console.log('🛑 Step 10: Adapter stopped'); 257 | 258 | resolve(true); 259 | } catch (error) { 260 | reject(error); 261 | } 262 | }); 263 | }).timeout(40000); 264 | 265 | // Example: Testing missing required configuration 266 | it('should handle missing required configuration properly', function () { 267 | return new Promise(async (resolve, reject) => { 268 | try { 269 | harness = getHarness(); 270 | 271 | console.log('🔍 Step 1: Fetching adapter object...'); 272 | const obj = await new Promise((res, rej) => { 273 | harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => { 274 | if (err) return rej(err); 275 | res(o); 276 | }); 277 | }); 278 | 279 | if (!obj) return reject(new Error('Adapter object not found')); 280 | 281 | console.log('🔍 Step 2: Removing required configuration...'); 282 | // Remove required configuration to test failure handling 283 | delete obj.native.position; // This should cause failure or graceful handling 284 | 285 | await new Promise((res, rej) => { 286 | harness.objects.setObject(obj._id, obj, (err) => { 287 | if (err) return rej(err); 288 | res(undefined); 289 | }); 290 | }); 291 | 292 | console.log('🔍 Step 3: Starting adapter...'); 293 | await harness.startAdapterAndWait(); 294 | 295 | console.log('⏳ Step 4: Waiting for adapter to process...'); 296 | await new Promise((res) => setTimeout(res, 10000)); 297 | 298 | console.log('🔍 Step 5: Checking adapter behavior...'); 299 | const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*'); 300 | 301 | // Check if adapter handled missing configuration gracefully 302 | if (stateIds.length === 0) { 303 | console.log('✅ Adapter properly handled missing configuration - no invalid states created'); 304 | resolve(true); 305 | } else { 306 | // If states were created, check if they're in error state 307 | const connectionState = await new Promise((res, rej) => { 308 | harness.states.getState('your-adapter.0.info.connection', (err, state) => { 309 | if (err) return rej(err); 310 | res(state); 311 | }); 312 | }); 313 | 314 | if (!connectionState || connectionState.val === false) { 315 | console.log('✅ Adapter properly failed with missing configuration'); 316 | resolve(true); 317 | } else { 318 | console.log('❌ Adapter should have failed or handled missing config gracefully'); 319 | reject(new Error('Adapter should have handled missing configuration')); 320 | } 321 | } 322 | 323 | await harness.stopAdapter(); 324 | } catch (error) { 325 | console.log('✅ Adapter correctly threw error with missing configuration:', error.message); 326 | resolve(true); 327 | } 328 | }); 329 | }).timeout(40000); 330 | ``` 331 | 332 | #### Advanced State Access Patterns 333 | 334 | For testing adapters that create multiple states, use bulk state access methods to efficiently verify large numbers of states: 335 | 336 | ```javascript 337 | it('should create and verify multiple states', () => new Promise(async (resolve, reject) => { 338 | // Configure and start adapter first... 339 | harness.objects.getObject('system.adapter.tagesschau.0', async (err, obj) => { 340 | if (err) { 341 | console.error('Error getting adapter object:', err); 342 | reject(err); 343 | return; 344 | } 345 | 346 | // Configure adapter as needed 347 | obj.native.someConfig = 'test-value'; 348 | harness.objects.setObject(obj._id, obj); 349 | 350 | await harness.startAdapterAndWait(); 351 | 352 | // Wait for adapter to create states 353 | setTimeout(() => { 354 | // Access bulk states using pattern matching 355 | harness.dbConnection.getStateIDs('tagesschau.0.*').then(stateIds => { 356 | if (stateIds && stateIds.length > 0) { 357 | harness.states.getStates(stateIds, (err, allStates) => { 358 | if (err) { 359 | console.error('❌ Error getting states:', err); 360 | reject(err); // Properly fail the test instead of just resolving 361 | return; 362 | } 363 | 364 | // Verify states were created and have expected values 365 | const expectedStates = ['tagesschau.0.info.connection', 'tagesschau.0.articles.0.title']; 366 | let foundStates = 0; 367 | 368 | for (const stateId of expectedStates) { 369 | if (allStates[stateId]) { 370 | foundStates++; 371 | console.log(`✅ Found expected state: ${stateId}`); 372 | } else { 373 | console.log(`❌ Missing expected state: ${stateId}`); 374 | } 375 | } 376 | 377 | if (foundStates === expectedStates.length) { 378 | console.log('✅ All expected states were created successfully'); 379 | resolve(); 380 | } else { 381 | reject(new Error(`Only ${foundStates}/${expectedStates.length} expected states were found`)); 382 | } 383 | }); 384 | } else { 385 | reject(new Error('No states found matching pattern tagesschau.0.*')); 386 | } 387 | }).catch(reject); 388 | }, 20000); // Allow more time for multiple state creation 389 | }); 390 | })).timeout(45000); 391 | ``` 392 | 393 | #### Key Integration Testing Rules 394 | 395 | 1. **NEVER test API URLs directly** - Let the adapter handle API calls 396 | 2. **ALWAYS use the harness** - `getHarness()` provides the testing environment 397 | 3. **Configure via objects** - Use `harness.objects.setObject()` to set adapter configuration 398 | 4. **Start properly** - Use `harness.startAdapterAndWait()` to start the adapter 399 | 5. **Check states** - Use `harness.states.getState()` to verify results 400 | 6. **Use timeouts** - Allow time for async operations with appropriate timeouts 401 | 7. **Test real workflow** - Initialize → Configure → Start → Verify States 402 | 403 | #### Workflow Dependencies 404 | Integration tests should run ONLY after lint and adapter tests pass: 405 | 406 | ```yaml 407 | integration-tests: 408 | needs: [check-and-lint, adapter-tests] 409 | runs-on: ubuntu-latest 410 | steps: 411 | - name: Run integration tests 412 | run: npx mocha test/integration-*.js --exit 413 | ``` 414 | 415 | #### What NOT to Do 416 | ❌ Direct API testing: `axios.get('https://api.example.com')` 417 | ❌ Mock adapters: `new MockAdapter()` 418 | ❌ Direct internet calls in tests 419 | ❌ Bypassing the harness system 420 | 421 | #### What TO Do 422 | ✅ Use `@iobroker/testing` framework 423 | ✅ Configure via `harness.objects.setObject()` 424 | ✅ Start via `harness.startAdapterAndWait()` 425 | ✅ Test complete adapter lifecycle 426 | ✅ Verify states via `harness.states.getState()` 427 | ✅ Allow proper timeouts for async operations 428 | 429 | ### API Testing with Credentials 430 | For adapters that connect to external APIs requiring authentication, implement comprehensive credential testing: 431 | 432 | #### Password Encryption for Integration Tests 433 | When creating integration tests that need encrypted passwords (like those marked as `encryptedNative` in io-package.json): 434 | 435 | 1. **Read system secret**: Use `harness.objects.getObjectAsync("system.config")` to get `obj.native.secret` 436 | 2. **Apply XOR encryption**: Implement the encryption algorithm: 437 | ```javascript 438 | async function encryptPassword(harness, password) { 439 | const systemConfig = await harness.objects.getObjectAsync("system.config"); 440 | if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { 441 | throw new Error("Could not retrieve system secret for password encryption"); 442 | } 443 | 444 | const secret = systemConfig.native.secret; 445 | let result = ''; 446 | for (let i = 0; i < password.length; ++i) { 447 | result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ password.charCodeAt(i)); 448 | } 449 | return result; 450 | } 451 | ``` 452 | 3. **Store encrypted password**: Set the encrypted result in adapter config, not the plain text 453 | 4. **Result**: Adapter will properly decrypt and use credentials, enabling full API connectivity testing 454 | 455 | #### Demo Credentials Testing Pattern 456 | - Use provider demo credentials when available (e.g., `demo@api-provider.com` / `demo`) 457 | - Create separate test file (e.g., `test/integration-demo.js`) for credential-based tests 458 | - Add npm script: `"test:integration-demo": "mocha test/integration-demo --exit"` 459 | - Implement clear success/failure criteria with recognizable log messages 460 | - Expected success pattern: Look for specific adapter initialization messages 461 | - Test should fail clearly with actionable error messages for debugging 462 | 463 | #### Enhanced Test Failure Handling 464 | ```javascript 465 | it("Should connect to API with demo credentials", async () => { 466 | // ... setup and encryption logic ... 467 | 468 | const connectionState = await harness.states.getStateAsync("adapter.0.info.connection"); 469 | 470 | if (connectionState && connectionState.val === true) { 471 | console.log("✅ SUCCESS: API connection established"); 472 | return true; 473 | } else { 474 | throw new Error("API Test Failed: Expected API connection to be established with demo credentials. " + 475 | "Check logs above for specific API errors (DNS resolution, 401 Unauthorized, network issues, etc.)"); 476 | } 477 | }).timeout(120000); // Extended timeout for API calls 478 | ``` 479 | 480 | ## README Updates 481 | 482 | ### Required Sections 483 | When updating README.md files, ensure these sections are present and well-documented: 484 | 485 | 1. **Installation** - Clear npm/ioBroker admin installation steps 486 | 2. **Configuration** - Detailed configuration options with examples 487 | 3. **Usage** - Practical examples and use cases 488 | 4. **Changelog** - Version history and changes (use "## **WORK IN PROGRESS**" section for ongoing changes following AlCalzone release-script standard) 489 | 5. **License** - License information (typically MIT for ioBroker adapters) 490 | 6. **Support** - Links to issues, discussions, and community support 491 | 492 | ### Documentation Standards 493 | - Use clear, concise language 494 | - Include code examples for configuration 495 | - Add screenshots for admin interface when applicable 496 | - Maintain multilingual support (at minimum English and German) 497 | - When creating PRs, add entries to README under "## **WORK IN PROGRESS**" section following ioBroker release script standard 498 | - Always reference related issues in commits and PR descriptions (e.g., "solves #xx" or "fixes #xx") 499 | 500 | ### Mandatory README Updates for PRs 501 | For **every PR or new feature**, always add a user-friendly entry to README.md: 502 | 503 | - Add entries under `## **WORK IN PROGRESS**` section before committing 504 | - Use format: `* (author) **TYPE**: Description of user-visible change` 505 | - Types: **NEW** (features), **FIXED** (bugs), **ENHANCED** (improvements), **TESTING** (test additions), **CI/CD** (automation) 506 | - Focus on user impact, not technical implementation details 507 | - Example: `* (DutchmanNL) **FIXED**: Adapter now properly validates login credentials instead of always showing "credentials missing"` 508 | 509 | ### Documentation Workflow Standards 510 | - **Mandatory README updates**: Establish requirement to update README.md for every PR/feature 511 | - **Standardized documentation**: Create consistent format and categories for changelog entries 512 | - **Enhanced development workflow**: Integrate documentation requirements into standard development process 513 | 514 | ### Changelog Management with AlCalzone Release-Script 515 | Follow the [AlCalzone release-script](https://github.com/AlCalzone/release-script) standard for changelog management: 516 | 517 | #### Format Requirements 518 | - Always use `## **WORK IN PROGRESS**` as the placeholder for new changes 519 | - Add all PR/commit changes under this section until ready for release 520 | - Never modify version numbers manually - only when merging to main branch 521 | - Maintain this format in README.md or CHANGELOG.md: 522 | 523 | ```markdown 524 | # Changelog 525 | 526 | 527 | ## **WORK IN PROGRESS** 528 | 529 | - Did some changes 530 | - Did some more changes 531 | 532 | ## v0.1.0 (2023-01-01) 533 | Initial release 534 | ``` 535 | 536 | #### Workflow Process 537 | - **During Development**: All changes go under `## **WORK IN PROGRESS**` 538 | - **For Every PR**: Add user-facing changes to the WORK IN PROGRESS section 539 | - **Before Merge**: Version number and date are only added when merging to main 540 | - **Release Process**: The release-script automatically converts the placeholder to the actual version 541 | 542 | #### Change Entry Format 543 | Use this consistent format for changelog entries: 544 | - `- (author) **TYPE**: User-friendly description of the change` 545 | - Types: **NEW** (features), **FIXED** (bugs), **ENHANCED** (improvements) 546 | - Focus on user impact, not technical implementation details 547 | - Reference related issues: "fixes #XX" or "solves #XX" 548 | 549 | #### Example Entry 550 | ```markdown 551 | ## **WORK IN PROGRESS** 552 | 553 | - (DutchmanNL) **FIXED**: Adapter now properly validates login credentials instead of always showing "credentials missing" (fixes #25) 554 | - (DutchmanNL) **NEW**: Added support for device discovery to simplify initial setup 555 | ``` 556 | 557 | ## Dependency Updates 558 | 559 | ### Package Management 560 | - Always use `npm` for dependency management in ioBroker adapters 561 | - When working on new features in a repository with an existing package-lock.json file, use `npm ci` to install dependencies. Use `npm install` only when adding or updating dependencies. 562 | - Keep dependencies minimal and focused 563 | - Only update dependencies to latest stable versions when necessary or in separate Pull Requests. Avoid updating dependencies when adding features that don't require these updates. 564 | - When you modify `package.json`: 565 | 1. Run `npm install` to update and sync `package-lock.json`. 566 | 2. If `package-lock.json` was updated, commit both `package.json` and `package-lock.json`. 567 | 568 | ### Dependency Best Practices 569 | - Prefer built-in Node.js modules when possible 570 | - Use `@iobroker/adapter-core` for adapter base functionality 571 | - Avoid deprecated packages 572 | - Document any specific version requirements 573 | 574 | ## JSON-Config Admin Instructions 575 | 576 | ### Configuration Schema 577 | When creating admin configuration interfaces: 578 | 579 | - Use JSON-Config format for modern ioBroker admin interfaces 580 | - Provide clear labels and help text for all configuration options 581 | - Include input validation and error messages 582 | - Group related settings logically 583 | - Example structure: 584 | ```json 585 | { 586 | "type": "panel", 587 | "items": { 588 | "host": { 589 | "type": "text", 590 | "label": "Host address", 591 | "help": "IP address or hostname of the device" 592 | } 593 | } 594 | } 595 | ``` 596 | 597 | ### Admin Interface Guidelines 598 | - Use consistent naming conventions 599 | - Provide sensible default values 600 | - Include validation for required fields 601 | - Add tooltips for complex configuration options 602 | - Ensure translations are available for all supported languages (minimum English and German) 603 | - Write end-user friendly labels and descriptions, avoiding technical jargon where possible 604 | 605 | ## Best Practices for Dependencies 606 | 607 | ### HTTP Client Libraries 608 | - **Preferred:** Use native `fetch` API (Node.js 20+ required for adapters; built-in since Node.js 18) 609 | - **Avoid:** `axios` unless specific features are required (reduces bundle size) 610 | 611 | ### Example with fetch: 612 | ```javascript 613 | try { 614 | const response = await fetch('https://api.example.com/data'); 615 | if (!response.ok) { 616 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 617 | } 618 | const data = await response.json(); 619 | } catch (error) { 620 | this.log.error(`API request failed: ${error.message}`); 621 | } 622 | ``` 623 | 624 | ### Other Dependency Recommendations 625 | - **Logging:** Use adapter built-in logging (`this.log.*`) 626 | - **Scheduling:** Use adapter built-in timers and intervals 627 | - **File operations:** Use Node.js `fs/promises` for async file operations 628 | - **Configuration:** Use adapter config system rather than external config libraries 629 | 630 | ## Error Handling 631 | 632 | ### Adapter Error Patterns 633 | - Always catch and log errors appropriately 634 | - Use adapter log levels (error, warn, info, debug) 635 | - Provide meaningful, user-friendly error messages that help users understand what went wrong 636 | - Handle network failures gracefully 637 | - Implement retry mechanisms where appropriate 638 | - Always clean up timers, intervals, and other resources in the `unload()` method 639 | 640 | ### Example Error Handling: 641 | ```javascript 642 | try { 643 | await this.connectToDevice(); 644 | } catch (error) { 645 | this.log.error(`Failed to connect to device: ${error.message}`); 646 | this.setState('info.connection', false, true); 647 | // Implement retry logic if needed 648 | } 649 | ``` 650 | 651 | ### Timer and Resource Cleanup: 652 | ```javascript 653 | // In your adapter class 654 | private connectionTimer?: NodeJS.Timeout; 655 | 656 | async onReady() { 657 | this.connectionTimer = setInterval(() => { 658 | this.checkConnection(); 659 | }, 30000); 660 | } 661 | 662 | onUnload(callback) { 663 | try { 664 | // Clean up timers and intervals 665 | if (this.connectionTimer) { 666 | clearInterval(this.connectionTimer); 667 | this.connectionTimer = undefined; 668 | } 669 | // Close connections, clean up resources 670 | callback(); 671 | } catch (e) { 672 | callback(); 673 | } 674 | } 675 | ``` 676 | 677 | ## Code Style and Standards 678 | 679 | - Follow JavaScript/TypeScript best practices 680 | - Use async/await for asynchronous operations 681 | - Implement proper resource cleanup in `unload()` method 682 | - Use semantic versioning for adapter releases 683 | - Include proper JSDoc comments for public methods 684 | 685 | ## CI/CD and Testing Integration 686 | 687 | ### GitHub Actions for API Testing 688 | For adapters with external API dependencies, implement separate CI/CD jobs: 689 | 690 | ```yaml 691 | # Tests API connectivity with demo credentials (runs separately) 692 | demo-api-tests: 693 | if: contains(github.event.head_commit.message, '[skip ci]') == false 694 | 695 | runs-on: ubuntu-22.04 696 | 697 | steps: 698 | - name: Checkout code 699 | uses: actions/checkout@v4 700 | 701 | - name: Use Node.js 20.x 702 | uses: actions/setup-node@v4 703 | with: 704 | node-version: 20.x 705 | cache: 'npm' 706 | 707 | - name: Install dependencies 708 | run: npm ci 709 | 710 | - name: Run demo API tests 711 | run: npm run test:integration-demo 712 | ``` 713 | 714 | ### CI/CD Best Practices 715 | - Run credential tests separately from main test suite 716 | - Use ubuntu-22.04 for consistency 717 | - Don't make credential tests required for deployment 718 | - Provide clear failure messages for API connectivity issues 719 | - Use appropriate timeouts for external API calls (120+ seconds) 720 | 721 | ### Package.json Script Integration 722 | Add dedicated script for credential testing: 723 | ```json 724 | { 725 | "scripts": { 726 | "test:integration-demo": "mocha test/integration-demo --exit" 727 | } 728 | } 729 | ``` 730 | 731 | ### Practical Example: Complete API Testing Implementation 732 | Here's a complete example based on lessons learned from the Discovergy adapter: 733 | 734 | #### test/integration-demo.js 735 | ```javascript 736 | const path = require("path"); 737 | const { tests } = require("@iobroker/testing"); 738 | 739 | // Helper function to encrypt password using ioBroker's encryption method 740 | async function encryptPassword(harness, password) { 741 | const systemConfig = await harness.objects.getObjectAsync("system.config"); 742 | 743 | if (!systemConfig || !systemConfig.native || !systemConfig.native.secret) { 744 | throw new Error("Could not retrieve system secret for password encryption"); 745 | } 746 | 747 | const secret = systemConfig.native.secret; 748 | let result = ''; 749 | for (let i = 0; i < password.length; ++i) { 750 | result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ password.charCodeAt(i)); 751 | } 752 | 753 | return result; 754 | } 755 | 756 | // Run integration tests with demo credentials 757 | tests.integration(path.join(__dirname, ".."), { 758 | defineAdditionalTests({ suite }) { 759 | suite("API Testing with Demo Credentials", (getHarness) => { 760 | let harness; 761 | 762 | before(() => { 763 | harness = getHarness(); 764 | }); 765 | 766 | it("Should connect to API and initialize with demo credentials", async () => { 767 | console.log("Setting up demo credentials..."); 768 | 769 | if (harness.isAdapterRunning()) { 770 | await harness.stopAdapter(); 771 | } 772 | 773 | const encryptedPassword = await encryptPassword(harness, "demo_password"); 774 | 775 | await harness.changeAdapterConfig("your-adapter", { 776 | native: { 777 | username: "demo@provider.com", 778 | password: encryptedPassword, 779 | // other config options 780 | } 781 | }); 782 | 783 | console.log("Starting adapter with demo credentials..."); 784 | await harness.startAdapter(); 785 | 786 | // Wait for API calls and initialization 787 | await new Promise(resolve => setTimeout(resolve, 60000)); 788 | 789 | const connectionState = await harness.states.getStateAsync("your-adapter.0.info.connection"); 790 | 791 | if (connectionState && connectionState.val === true) { 792 | console.log("✅ SUCCESS: API connection established"); 793 | return true; 794 | } else { 795 | throw new Error("API Test Failed: Expected API connection to be established with demo credentials. " + 796 | "Check logs above for specific API errors (DNS resolution, 401 Unauthorized, network issues, etc.)"); 797 | } 798 | }).timeout(120000); 799 | }); 800 | } 801 | }); 802 | ``` 803 | 804 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * stiebel-eltron / tecalor isg adapter - main.js v13 5 | * 6 | * Changes in v13: 7 | * - Fixes min/max handling and updates existing objects that were created previously with incorrect min/max = 0. 8 | * - Adds diagnostic debug logs for encountered valMin/valMax (as before). 9 | * - Concurrency limiter, native fetch + fetch-cookie + tough-cookie, and timeout handling preserved. 10 | * 11 | * Requirements: 12 | * - Node >= 18 (global fetch + AbortController) 13 | * - npm i --save fetch-cookie tough-cookie cheerio 14 | * - optionally: npm i --save undici 15 | */ 16 | 17 | const utils = require('@iobroker/adapter-core'); 18 | const querystring = require('querystring'); 19 | const cheerio = require('cheerio'); 20 | const tough = require('tough-cookie'); 21 | 22 | const fetchCookieModule = (() => { 23 | try { 24 | return require('fetch-cookie'); 25 | } catch { 26 | return null; 27 | } 28 | })(); 29 | 30 | let undiciDispatcher = null; 31 | try { 32 | const undici = require('undici'); 33 | if (typeof undici.Agent === 'function') { 34 | undiciDispatcher = new undici.Agent({ keepAliveTimeout: 60000, connections: 6 }); 35 | } else { 36 | undiciDispatcher = null; 37 | } 38 | } catch { 39 | undiciDispatcher = null; 40 | } 41 | 42 | let adapter; 43 | let systemLanguage; 44 | let nameTranslation; 45 | let isgIntervall; 46 | let isgCommandIntervall; 47 | let commands = []; 48 | let CommandTimeout; 49 | let jar; 50 | let host; 51 | let commandPaths = []; 52 | let valuePaths = []; 53 | let statusPaths = []; 54 | let minmaxlogging = false; // enable detailed min/max logging 55 | 56 | /* ------------------------- 57 | Concurrency queue (simple, dependency-free) 58 | ------------------------- */ 59 | 60 | let queue = []; 61 | let running = 0; 62 | let maxConcurrentFetches = 3; // default; updated from adapter.config in ready() 63 | 64 | function schedule(fn) { 65 | return new Promise((resolve, reject) => { 66 | queue.push({ fn, resolve, reject }); 67 | runQueue(); 68 | }); 69 | } 70 | 71 | function runQueue() { 72 | while (running < maxConcurrentFetches && queue.length > 0) { 73 | const item = queue.shift(); 74 | running++; 75 | Promise.resolve() 76 | .then(() => item.fn()) 77 | .then(res => item.resolve(res)) 78 | .catch(err => item.reject(err)) 79 | .finally(() => { 80 | running--; 81 | setImmediate(runQueue); 82 | }); 83 | } 84 | } 85 | 86 | /* ------------------------- 87 | Cookie jar & fetch helpers 88 | ------------------------- */ 89 | 90 | function setJar(j) { 91 | jar = j; 92 | } 93 | 94 | function getJar() { 95 | if (jar) { 96 | return jar; 97 | } 98 | jar = new tough.CookieJar(); 99 | return jar; 100 | } 101 | 102 | function ensureNativeFetch() { 103 | if (typeof globalThis.fetch !== 'function') { 104 | const msg = 105 | 'Global fetch() is not available. This adapter requires Node.js >= 18 for native fetch. ' + 106 | 'Install Node >= 18 or request a node-fetch fallback.'; 107 | if (adapter && adapter.log) { 108 | adapter.log.error(msg); 109 | } 110 | throw new Error(msg); 111 | } 112 | } 113 | 114 | function getFetchFactory() { 115 | if (!fetchCookieModule) { 116 | const msg = 117 | 'fetch-cookie is not installed. Please install fetch-cookie and tough-cookie: npm i --save fetch-cookie tough-cookie'; 118 | if (adapter && adapter.log) { 119 | adapter.log.error(msg); 120 | } 121 | throw new Error(msg); 122 | } 123 | if (typeof fetchCookieModule === 'function') { 124 | return fetchCookieModule; 125 | } 126 | if (fetchCookieModule && typeof fetchCookieModule.default === 'function') { 127 | return fetchCookieModule.default; 128 | } 129 | const msg = 'fetch-cookie export shape is unexpected. Ensure fetch-cookie is installed and compatible.'; 130 | if (adapter && adapter.log) { 131 | adapter.log.error(msg); 132 | } 133 | throw new Error(msg); 134 | } 135 | 136 | function getFetch() { 137 | ensureNativeFetch(); 138 | const fetchFactory = getFetchFactory(); 139 | const jarInst = getJar(); 140 | try { 141 | return fetchFactory(globalThis.fetch, jarInst); 142 | } catch (err) { 143 | try { 144 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 145 | // @ts-expect-error 146 | return fetchFactory(jarInst, globalThis.fetch); 147 | } catch (err2) { 148 | const errMsg = err instanceof Error ? err.message : String(err); 149 | const err2Msg = err2 instanceof Error ? err2.message : String(err2); 150 | const msg = `Failed to create cookie-wrapped fetch: ${errMsg}; ${err2Msg}`; 151 | if (adapter && adapter.log) { 152 | adapter.log.error(msg); 153 | } 154 | throw new Error(msg); 155 | } 156 | } 157 | } 158 | 159 | /* 160 | * Build fetch options and timeout/abort logic. 161 | * Returns { options, clearTimeout }. 162 | */ 163 | function buildFetchOptions(url, extra = {}) { 164 | let configuredTimeout = 60000; 165 | try { 166 | if (adapter && typeof adapter.config !== 'undefined' && adapter.config.requestTimeout != null) { 167 | const num = Number(adapter.config.requestTimeout); 168 | if (!isNaN(num)) { 169 | configuredTimeout = num; 170 | } 171 | } 172 | } catch { 173 | configuredTimeout = 60000; 174 | } 175 | const timeout = Number(configuredTimeout) || 0; 176 | 177 | const controller = new AbortController(); 178 | let timer = null; 179 | if (timeout > 0) { 180 | timer = setTimeout(() => { 181 | try { 182 | controller.abort(); 183 | } catch { 184 | /* ignore */ 185 | } 186 | }, timeout); 187 | } 188 | 189 | const opts = Object.assign( 190 | { 191 | signal: controller.signal, 192 | // eslint-disable-next-line jsdoc/check-tag-names 193 | credentials: /** @type {RequestCredentials} */ ('include'), 194 | }, 195 | extra, 196 | ); 197 | 198 | if (undiciDispatcher) { 199 | // @ts-expect-error: dispatcher is Undici-specific and not in standard fetch options 200 | opts.dispatcher = undiciDispatcher; 201 | } 202 | 203 | return { 204 | options: opts, 205 | clearTimeout: () => { 206 | if (timer) { 207 | clearTimeout(timer); 208 | timer = null; 209 | } 210 | }, 211 | }; 212 | } 213 | 214 | /* ------------------------- 215 | Translation / helpers 216 | ------------------------- */ 217 | 218 | function translateName(strName, intType) { 219 | if (typeof intType === 'undefined') { 220 | intType = 0; 221 | } 222 | 223 | switch (intType) { 224 | case 1: 225 | if (nameTranslation[strName]) { 226 | return nameTranslation[strName][1]; 227 | } 228 | return strName; 229 | 230 | case 0: 231 | default: 232 | if (nameTranslation[strName]) { 233 | return nameTranslation[strName]; 234 | } 235 | return strName; 236 | } 237 | } 238 | 239 | function Umlauts(text_string) { 240 | if (!text_string) { 241 | return text_string; 242 | } 243 | return text_string 244 | .replace(/[\u00c4]+/g, 'AE') 245 | .replace(/[\u00d6]+/g, 'OE') 246 | .replace(/[\u00dc]+/g, 'UE'); 247 | } 248 | 249 | /* 250 | * safeSetState: wrapper around adapter.setState that: 251 | * - If the object is not a valNN object write it directly. 252 | * - If the object has common.min/common.max, clamp values to avoid ioBroker warnings. 253 | * - Accepts optional expire param. 254 | */ 255 | function safeSetState(id, val, ack = true, expire) { 256 | // If it's not a valNN state, just set it 257 | const last = id.split('.').pop(); 258 | const m = last && last.match(/^val(\d+)$/); 259 | if (!m) { 260 | // Not a valNN => write directly 261 | try { 262 | if (typeof expire !== 'undefined') { 263 | adapter.setState(id, { val: val, ack: ack, expire: expire }); 264 | } else { 265 | adapter.setState(id, { val: val, ack: ack }); 266 | } 267 | } catch (err) { 268 | adapter.log && adapter.log.warn && adapter.log.warn(`safeSetState: setState failed for ${id}: ${err}`); 269 | } 270 | return; 271 | } 272 | 273 | // Otherwise write, but first clamp to min/max if the object defines them 274 | adapter.getObject(id, (err2, obj) => { 275 | let finalVal = val; 276 | try { 277 | if (!err2 && obj && obj.common) { 278 | const { min, max } = obj.common; 279 | if (typeof min !== 'undefined' && !isNaN(Number(min)) && !isNaN(Number(finalVal))) { 280 | if (Number(finalVal) < Number(min)) { 281 | adapter.log.debug( 282 | `safeSetState: clamping ${id} value ${finalVal} -> min ${Number(min)} to avoid warning`, 283 | ); 284 | finalVal = Number(min); 285 | } 286 | } 287 | if (typeof max !== 'undefined' && !isNaN(Number(max)) && !isNaN(Number(finalVal))) { 288 | if (Number(finalVal) > Number(max)) { 289 | adapter.log.debug( 290 | `safeSetState: clamping ${id} value ${finalVal} -> min ${Number(min)} to avoid warning`, 291 | ); 292 | // set finalVal to min to mimic behaviour of ISG widget pre7 293 | finalVal = Number(min); 294 | } 295 | } 296 | } 297 | } catch (e) { 298 | adapter.log.debug && 299 | adapter.log.debug( 300 | `safeSetState: clamp check error for ${id}: ${e instanceof Error ? e.message : String(e)}`, 301 | ); 302 | } 303 | 304 | try { 305 | if (typeof expire !== 'undefined') { 306 | adapter.setState(id, { val: finalVal, ack: ack, expire: expire }); 307 | } else { 308 | adapter.setState(id, { val: finalVal, ack: ack }); 309 | } 310 | } catch (e) { 311 | adapter.log && adapter.log.warn && adapter.log.warn(`safeSetState: setState failed for ${id}: ${e}`); 312 | } 313 | }); 314 | } 315 | 316 | function updateState(strGroup, valTag, valTagLang, valType, valUnit, valRole, valValue) { 317 | if (valTag == null) { 318 | return; 319 | } 320 | 321 | let ValueExpire = null; 322 | 323 | if ( 324 | strGroup.startsWith(translateName('settings')) || 325 | strGroup.startsWith(`${translateName('info')}.ANLAGE.STATISTIK`) 326 | ) { 327 | ValueExpire = adapter.config.isgCommandIntervall * 2; 328 | } else { 329 | ValueExpire = adapter.config.isgIntervall * 2; 330 | } 331 | 332 | if (adapter.config.isgUmlauts == 'no') { 333 | valTag = Umlauts(valTag); 334 | strGroup = Umlauts(strGroup); 335 | } 336 | 337 | valTag = valTag.replace(/[*]+/g, '_'); 338 | 339 | adapter.setObjectNotExists( 340 | `${strGroup}.${valTag}`, 341 | { 342 | type: 'state', 343 | common: { 344 | name: valTagLang, 345 | type: valType, 346 | read: true, 347 | write: false, 348 | unit: valUnit, 349 | role: valRole, 350 | }, 351 | native: {}, 352 | }, 353 | function () { 354 | //adapter.setState(`${strGroup}.${valTag}`, { val: valValue, ack: true, expire: ValueExpire }); 355 | safeSetState(`${strGroup}.${valTag}`, valValue, true, ValueExpire); 356 | }, 357 | ); 358 | } 359 | 360 | /* ------------------------- 361 | HTTP helpers: getHTML/getIsgStatus/getIsgValues 362 | ------------------------- */ 363 | 364 | async function getHTML(sidePath) { 365 | const strURL = `${host}/?s=${sidePath}`; 366 | 367 | const payload = querystring.stringify({ 368 | user: adapter.config.isgUser, 369 | pass: adapter.config.isgPassword, 370 | }); 371 | 372 | const fetch = getFetch(); 373 | const built = buildFetchOptions(strURL, { 374 | method: 'POST', 375 | headers: { 376 | 'Content-Type': 'application/x-www-form-urlencoded', 377 | Connection: 'keep-alive', 378 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 379 | }, 380 | body: payload, 381 | }); 382 | 383 | try { 384 | const res = await fetch(strURL, built.options); 385 | built.clearTimeout(); 386 | 387 | const status = res && typeof res.status !== 'undefined' ? res.status : null; 388 | if (status === 200) { 389 | // @ts-expect-error: .res.text() exists 390 | const text = await res.text(); 391 | adapter.setState('info.connection', true, true); 392 | return cheerio.load(text); 393 | } 394 | adapter.setState('info.connection', false, true); 395 | throw new Error(`HTTP ${status}`); 396 | } catch (error) { 397 | built.clearTimeout(); 398 | if ( 399 | (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') || 400 | String(error).toLowerCase().includes('aborted') 401 | ) { 402 | adapter.log.debug( 403 | `getHTML(${sidePath}) aborted: ${error instanceof Error ? error.message : String(error)}`, 404 | ); 405 | } else { 406 | adapter.log.error( 407 | `Error: ${typeof error === 'object' && error !== null && 'message' in error ? error.message : String(error)} to ${strURL} - Check ISG Address!`, 408 | ); 409 | } 410 | adapter.setState('info.connection', false, true); 411 | throw error; 412 | } 413 | } 414 | 415 | async function getIsgStatus(sidePath) { 416 | try { 417 | const $ = await getHTML(sidePath); 418 | if ($) { 419 | const submenu = $('#sub_nav') 420 | .children() 421 | .first() 422 | .text() 423 | .replace(/[-/]+/g, '_') 424 | .replace(/[ .]+/g, '') 425 | .replace(/[\u00df]+/g, 'SS'); 426 | 427 | $('.info').each((_i, el) => { 428 | let group = $(el) 429 | .find('.round-top') 430 | .text() 431 | .replace(/[ -]+/g, '_') 432 | .replace(/[.]+/g, '') 433 | .replace(/[\u00df]+/, 'SS'); 434 | 435 | group = `${submenu}.${group}`; 436 | 437 | $(el) 438 | .find('tr') 439 | .each(function () { 440 | const valueName = $(this).find('.key').text(); 441 | 442 | const key = $(this) 443 | .find('.key') 444 | .text() 445 | .replace(/[ -]+/g, '_') 446 | .replace(/[.]+/g, '') 447 | .replace(/[\u00df]+/, 'SS'); 448 | 449 | const param = $(this).find('.value').html(); 450 | let value; 451 | if (param !== null) { 452 | if (param.search('symbol_an') > -1) { 453 | value = true; 454 | } 455 | } 456 | 457 | const valType = typeof value; 458 | let valThisType = 'state'; 459 | if (valType !== null) { 460 | if (value === true || value === false) { 461 | valThisType = 'boolean'; 462 | } else { 463 | valThisType = 'state'; 464 | } 465 | } 466 | 467 | if (value === true) { 468 | updateState( 469 | `${translateName('info')}.${group}`, 470 | key, 471 | translateName(valueName), 472 | valThisType, 473 | '', 474 | 'indicator.state', 475 | value, 476 | ); 477 | } 478 | }); 479 | }); 480 | } 481 | } catch (e) { 482 | adapter.log.debug(`getIsgStatus(${sidePath}) error: ${e instanceof Error ? e.message : String(e)}`); 483 | } 484 | } 485 | 486 | async function getIsgValues(sidePath) { 487 | try { 488 | const $ = await getHTML(sidePath); 489 | if ($) { 490 | const submenu = $('#sub_nav') 491 | .children() 492 | .first() 493 | .text() 494 | .replace(/[-/]+/g, '_') 495 | .replace(/[ .]+/g, '') 496 | .replace(/[\u00df]+/g, 'SS'); 497 | 498 | $('.info').each((_i, el) => { 499 | let group = $(el) 500 | .find('.round-top') 501 | .text() 502 | .replace(/[ -]+/g, '_') 503 | .replace(/[.]+/g, '') 504 | .replace(/[\u00df]+/, 'SS'); 505 | 506 | group = `${submenu}.${group}`; 507 | 508 | $(el) 509 | .find('tr') 510 | .each(function () { 511 | const valueName = $(this).find('.key').text(); 512 | 513 | const key = $(this) 514 | .find('.key') 515 | .text() 516 | .replace(/[ -]+/g, '_') 517 | .replace(/[.]+/g, '') 518 | .replace(/[\u00df]+/, 'SS'); 519 | 520 | const param = $(this).find('.value').text().replace(/,/, '.'); 521 | 522 | const value = parseFloat(param); 523 | const unit = (param || '') 524 | .replace(/[ ]{0,2}/, '') 525 | .replace(/ /g, '') 526 | .replace(String(value), '') 527 | .replace(/([.0][0]){1}?/, '') 528 | .replace(/^0+/, ''); 529 | 530 | let valueRole; 531 | if ( 532 | key.search('TEMP') > -1 || 533 | key.search('FROST') > -1 || 534 | key.search('SOLLWERT_HK') == 0 || 535 | key.search('ISTWERT_HK') == 0 536 | ) { 537 | valueRole = 'value.temperature'; 538 | } else if (key.search('DRUCK') > -1) { 539 | valueRole = 'value.pressure'; 540 | } else if (key.search('P_') == 0) { 541 | valueRole = 'value.power.consumption'; 542 | } else if (key.search('FEUCHTE') > -1) { 543 | valueRole = 'value.humidity'; 544 | } else { 545 | valueRole = 'value'; 546 | } 547 | 548 | if (key && value != null && !isNaN(value)) { 549 | updateState( 550 | `${translateName('info')}.${group}`, 551 | key, 552 | translateName(valueName), 553 | typeof value, 554 | unit, 555 | valueRole, 556 | value, 557 | ); 558 | } 559 | }); 560 | }); 561 | } 562 | } catch (e) { 563 | const errorMessage = e instanceof Error ? e.message : String(e); 564 | adapter.log.debug(`getIsgValues(${sidePath}) error: ${errorMessage}`); 565 | } 566 | } 567 | 568 | /* ------------------------- 569 | Commands creation & parsing (v13: correct min/max + object update) 570 | ------------------------- */ 571 | 572 | function createISGCommands( 573 | strGroup, 574 | valTag, 575 | valTagLang, 576 | valType, 577 | valUnit, 578 | valRole, 579 | valValue, 580 | valStates, 581 | valMin, 582 | valMax, 583 | ) { 584 | if (valTag == null) { 585 | return; 586 | } 587 | 588 | if (adapter.config.isgUmlauts == 'no') { 589 | valTag = Umlauts(valTag); 590 | strGroup = Umlauts(strGroup); 591 | } 592 | 593 | valTag = valTag.replace(/[*]+/g, '_'); 594 | valUnit = (valUnit || '').replace(/ +0+/g, ''); 595 | 596 | // Build desired common object (what we want the state's common to contain) 597 | const desiredCommon = { 598 | name: valTagLang, 599 | type: valType, 600 | read: true, 601 | write: true, 602 | unit: valUnit, 603 | role: valRole, 604 | }; 605 | 606 | // Diagnostic debug: print the raw valMin/valMax encountered (debug-only) 607 | try { 608 | minmaxlogging && 609 | adapter.log.debug( 610 | `createISGCommands: encountered valMin="${valMin}" valMax="${valMax}" for ${strGroup}.${valTag}`, 611 | ); 612 | } catch { 613 | // ignore logging errors 614 | } 615 | 616 | // Only include min/max if explicitly provided and parseable as finite number 617 | if (typeof valMin !== 'undefined' && valMin !== null) { 618 | const sMin = String(valMin).trim(); 619 | if (sMin !== '') { 620 | const minNum = Number(sMin); 621 | if (Number.isFinite(minNum)) { 622 | desiredCommon.min = minNum; 623 | } else { 624 | minmaxlogging && 625 | adapter.log.debug(`createISGCommands: invalid min "${valMin}" for ${strGroup}.${valTag} - ignored`); 626 | } 627 | } else { 628 | minmaxlogging && adapter.log.debug(`createISGCommands: empty min for ${strGroup}.${valTag} - ignored`); 629 | } 630 | } 631 | 632 | if (typeof valMax !== 'undefined' && valMax !== null) { 633 | const sMax = String(valMax).trim(); 634 | if (sMax !== '') { 635 | const maxNum = Number(sMax); 636 | if (Number.isFinite(maxNum)) { 637 | desiredCommon.max = maxNum; 638 | } else { 639 | minmaxlogging && 640 | adapter.log.debug(`createISGCommands: invalid max "${valMax}" for ${strGroup}.${valTag} - ignored`); 641 | } 642 | } else { 643 | minmaxlogging && adapter.log.debug(`createISGCommands: empty max for ${strGroup}.${valTag} - ignored`); 644 | } 645 | } 646 | 647 | // Ensure states is an object (js-controller expects object) 648 | if (valStates) { 649 | if (typeof valStates === 'object') { 650 | desiredCommon.states = valStates; 651 | } else if (typeof valStates === 'string') { 652 | const s = valStates.trim(); 653 | if (s.startsWith('{') || s.startsWith('[')) { 654 | try { 655 | desiredCommon.states = JSON.parse(s); 656 | } catch { 657 | try { 658 | desiredCommon.states = JSON.parse(s.replace(/'/g, '"')); 659 | } catch { 660 | adapter.log && 661 | adapter.log.warn && 662 | adapter.log.warn( 663 | `createISGCommands: could not parse states for ${valTag}. states will be ignored.`, 664 | ); 665 | } 666 | } 667 | } else { 668 | // try "0:Off,1:On" style 669 | try { 670 | const obj = {}; 671 | s.split(',').forEach(pair => { 672 | const parts = pair.split(':'); 673 | if (parts.length >= 2) { 674 | const k = parts[0].trim(); 675 | const v = parts.slice(1).join(':').trim(); 676 | if (k) { 677 | obj[k] = v; 678 | } 679 | } 680 | }); 681 | if (Object.keys(obj).length) { 682 | desiredCommon.states = obj; 683 | } 684 | } catch { 685 | // ignore 686 | } 687 | } 688 | } 689 | } 690 | 691 | const id = `${strGroup}.${valTag}`; 692 | 693 | // First check whether object exists; if not: create it with desiredCommon. 694 | adapter.getObject(id, (err, obj) => { 695 | if (err) { 696 | adapter.log && 697 | adapter.log.error && 698 | adapter.log.error(`createISGCommands: getObject error for ${id}: ${err}`); 699 | return; 700 | } 701 | 702 | if (!obj) { 703 | // object doesn't exist -> create with desiredCommon 704 | adapter.setObjectNotExists( 705 | id, 706 | { 707 | type: 'state', 708 | common: desiredCommon, 709 | native: {}, 710 | }, 711 | function () { 712 | //adapter.setState(id, { val: valValue, ack: true }); 713 | safeSetState(id, valValue, true); 714 | }, 715 | ); 716 | } else { 717 | // object exists -> we may need to update common.min/common.max/states/name/unit/role if changed 718 | const existingCommon = obj.common || {}; 719 | const newCommon = Object.assign({}, existingCommon); 720 | 721 | // update basic fields from desiredCommon 722 | newCommon.name = desiredCommon.name; 723 | newCommon.type = desiredCommon.type; 724 | newCommon.read = desiredCommon.read; 725 | newCommon.write = desiredCommon.write; 726 | newCommon.unit = desiredCommon.unit; 727 | newCommon.role = desiredCommon.role; 728 | 729 | // min: if desiredCommon has min -> set, otherwise ensure it's removed if previously present but now no min 730 | if (Object.prototype.hasOwnProperty.call(desiredCommon, 'min')) { 731 | newCommon.min = desiredCommon.min; 732 | } else { 733 | if (Object.prototype.hasOwnProperty.call(newCommon, 'min')) { 734 | // remove erroneous min 735 | delete newCommon.min; 736 | } 737 | } 738 | 739 | // max: same logic 740 | if (Object.prototype.hasOwnProperty.call(desiredCommon, 'max')) { 741 | newCommon.max = desiredCommon.max; 742 | } else { 743 | if (Object.prototype.hasOwnProperty.call(newCommon, 'max')) { 744 | delete newCommon.max; 745 | } 746 | } 747 | 748 | // states 749 | if (Object.prototype.hasOwnProperty.call(desiredCommon, 'states')) { 750 | newCommon.states = desiredCommon.states; 751 | } else { 752 | // if desiredCommon has no states, do nothing: keep existing states if any 753 | } 754 | 755 | // Only update the object if newCommon differs from existingCommon. 756 | // Minimal deep-check for properties we care about: 757 | let needUpdate = false; 758 | const keysToCheck = ['name', 'type', 'read', 'write', 'unit', 'role', 'min', 'max', 'states']; 759 | for (const k of keysToCheck) { 760 | const a = existingCommon[k]; 761 | const b = newCommon[k]; 762 | const same = 763 | typeof a === 'object' && typeof b === 'object' 764 | ? JSON.stringify(a) === JSON.stringify(b) 765 | : String(a) === String(b); 766 | if (!same) { 767 | needUpdate = true; 768 | break; 769 | } 770 | } 771 | 772 | if (needUpdate) { 773 | // Use extendObject to update only the common part 774 | adapter.extendObject(id, { common: newCommon }, err2 => { 775 | if (err2) { 776 | adapter.log && 777 | adapter.log.error && 778 | adapter.log.error(`createISGCommands: extendObject failed for ${id}: ${err2}`); 779 | } 780 | // Always set the state value after ensuring object exists/updated 781 | safeSetState(id, valValue, true); 782 | //adapter.setState(id, { val: valValue, ack: true }); 783 | }); 784 | } else { 785 | // No update required; just set the state value 786 | safeSetState(id, valValue, true); 787 | // adapter.setState(id, { val: valValue, ack: true }); 788 | } 789 | } 790 | }); 791 | } 792 | 793 | /* ------------------------- 794 | Get & parse commands page 795 | ------------------------- */ 796 | 797 | async function getIsgCommands(sidePath) { 798 | try { 799 | const $ = await getHTML(sidePath); 800 | if ($) { 801 | let group; 802 | try { 803 | group = $('#sub_nav') 804 | .children() 805 | .first() 806 | .text() 807 | .replace(/[-/]+/g, '_') 808 | .replace(/[ .]+/g, '') 809 | .replace(/[\u00df]+/g, 'SS'); 810 | } catch (e) { 811 | adapter.log.error('#sub_nav error:'); 812 | adapter.log.error(e); 813 | group = 'Allgemein'; 814 | } 815 | 816 | const submenu = $.html().match(/#subnavactivename"\)\.html\('(.*?)'/); 817 | let submenupath = ''; 818 | 819 | if (String(sidePath) === '0') { 820 | // parse infographics on start page 821 | let scriptValues = null; 822 | try { 823 | // @ts-expect-error: .data may be missing 824 | scriptValues = $('#buehne').next().next().get()[0].children[0].data; 825 | } catch { 826 | try { 827 | // @ts-expect-error: .data may be missing 828 | scriptValues = $('#buehne').next().next().next().get()[0].children[0].data; 829 | } catch { 830 | scriptValues = null; 831 | } 832 | } 833 | if (scriptValues) { 834 | const regexp = /charts\[(\d)\]\['([\w]*)'\]\s*= [[]{1}(.*?)\];{1}/gm; 835 | let graphsValues; 836 | const group = 'ANLAGE.STATISTIK'; 837 | do { 838 | graphsValues = regexp.exec(scriptValues); 839 | if (graphsValues) { 840 | const tabs = $(`#tab${graphsValues[1]}`).find('h3').text().split('in '); 841 | const valueName = tabs[0]; 842 | const graphUnit = tabs[1]; 843 | const valThisType = 'number'; 844 | let valueArray = []; 845 | let values = graphsValues[3].substring(1, graphsValues[3].length - 1); 846 | valueArray = values.split('],['); 847 | let prevValue; 848 | valueArray.forEach(function (item) { 849 | const valueact = item.split(','); 850 | const key = `${valueName}_${graphsValues[2]}`; 851 | const value = parseInt(valueact[1]); 852 | 853 | let valueRole; 854 | if (key.search('temperatur') > -1 || key.search('frost') > -1) { 855 | valueRole = 'value.temperature'; 856 | } else if (key.search('energie') > -1) { 857 | valueRole = 'value.power.consumption'; 858 | } else { 859 | valueRole = 'indicator.state'; 860 | } 861 | 862 | if (prevValue !== valueact[0]) { 863 | updateState( 864 | `${translateName('info')}.${group}.${valueName.toLocaleUpperCase()}.LATEST_VALUE`, 865 | key.toLocaleUpperCase(), 866 | valueName, 867 | valThisType, 868 | graphUnit, 869 | valueRole, 870 | value, 871 | ); 872 | } 873 | prevValue = valueact[0]; 874 | 875 | updateState( 876 | `${translateName('info')}.${group}.${valueName.toLocaleUpperCase()}.${valueact[0] 877 | .slice(1, valueact[0].length - 1) 878 | .toLocaleUpperCase()}`, 879 | key.toLocaleUpperCase(), 880 | valueName, 881 | valThisType, 882 | graphUnit, 883 | valueRole, 884 | value, 885 | ); 886 | }); 887 | } 888 | } while (graphsValues); 889 | } 890 | } 891 | 892 | // parse inputs and command widgets 893 | $('#werte') 894 | .find('input') 895 | .each(function (_i, el) { 896 | try { 897 | if (String(sidePath) === '0') { 898 | let statesCommand = ''; 899 | const nameCommand = $(el).parent().parent().find('h3').text(); 900 | if (nameCommand == 'Betriebsart') { 901 | const idStartCommand = $(el).attr('name'); 902 | if (idStartCommand && idStartCommand.match(/aval/)) { 903 | statesCommand = '{'; 904 | let idCommand; 905 | let valCommand = undefined; // Always define valCommand 906 | $(el) 907 | .parent() 908 | .parent() 909 | .parent() 910 | .parent() 911 | .find('div.values') 912 | .each(function (_j, ele) { 913 | $(ele) 914 | .find('input') 915 | .each(function (_k, elem) { 916 | idCommand = $(elem).attr('name'); 917 | if ( 918 | idCommand && 919 | !(idCommand.match(/aval/) || idCommand.match(/info/)) 920 | ) { 921 | if (idCommand.match(/[0-9]s/)) { 922 | if (statesCommand !== '{') { 923 | statesCommand += ','; 924 | } 925 | statesCommand += `"${$(elem).attr('value')}":"${$(elem).next().text()}"`; 926 | } else { 927 | let tempVal = $(elem).attr('value'); 928 | valCommand = 929 | tempVal !== undefined && 930 | tempVal !== null && 931 | typeof tempVal === 'string' 932 | ? parseFloat( 933 | tempVal.replace(',', '.').replace(' ', ''), 934 | ) 935 | : undefined; 936 | } 937 | } 938 | }); 939 | }); 940 | statesCommand += '}'; 941 | createISGCommands( 942 | translateName('start'), 943 | idCommand, 944 | nameCommand, 945 | 'number', 946 | '', 947 | 'level', 948 | valCommand, 949 | statesCommand, 950 | '', 951 | '', 952 | ); 953 | } 954 | } 955 | } else if ($(this).parent().find('div.black').html()) { 956 | $(this) 957 | .parent() 958 | .find('div.black') 959 | .each(function (_j, ele) { 960 | const nameCommand = $(ele).parent().parent().parent().find('h3').text(); 961 | const idCommand = $(ele).find('input').attr('name'); 962 | let valCommand; 963 | 964 | let statesCommand = '{'; 965 | $(ele) 966 | .find('input') 967 | .each(function (_j, el) { 968 | if (statesCommand !== '{') { 969 | statesCommand += ','; 970 | } 971 | statesCommand += `"${$(el).attr('value')}":"${$(el).attr('alt')}"`; 972 | 973 | if ($(el).attr('checked') == 'checked') { 974 | let tempVal = $(el).attr('value'); 975 | valCommand = 976 | tempVal !== undefined && 977 | tempVal !== null && 978 | typeof tempVal === 'string' 979 | ? parseFloat(tempVal.replace(',', '.').replace(' ', '')) 980 | : undefined; 981 | } 982 | }); 983 | statesCommand += '}'; 984 | if (submenu) { 985 | submenupath = ''; 986 | submenupath += `.${submenu[1]}`; 987 | } 988 | createISGCommands( 989 | `${translateName('settings')}.${group}${submenupath}`, 990 | idCommand, 991 | nameCommand, 992 | 'number', 993 | '', 994 | 'level', 995 | valCommand, 996 | statesCommand, 997 | '', 998 | '', 999 | ); 1000 | }); 1001 | } else { 1002 | const parentsClass = $(el).parent().attr('class'); 1003 | let scriptValues; 1004 | 1005 | if (parentsClass == 'current') { 1006 | $(el) 1007 | .parent() 1008 | .parent() 1009 | .find('div.black') 1010 | .each(function (_j, ele) { 1011 | const nameCommand = $(ele) 1012 | .parent() 1013 | .parent() 1014 | .parent() 1015 | .parent() 1016 | .find('h3') 1017 | .text(); 1018 | const idCommand = $(ele).parent().find('input').attr('id'); 1019 | let valCommand; 1020 | 1021 | $(ele) 1022 | .parent() 1023 | .find('input') 1024 | .each(function (_j, inp) { 1025 | if ($(inp).attr('checked') == 'checked') { 1026 | let tempVal = $(inp).attr('value'); 1027 | valCommand = 1028 | tempVal !== undefined && 1029 | tempVal !== null && 1030 | typeof tempVal === 'string' 1031 | ? parseFloat(tempVal.replace(',', '.').replace(' ', '')) 1032 | : undefined; 1033 | } 1034 | }); 1035 | if (submenu) { 1036 | submenupath = ''; 1037 | submenupath += `.${submenu[1]}`; 1038 | } 1039 | updateState( 1040 | `${translateName('settings')}.${group}${submenupath}`, 1041 | idCommand, 1042 | translateName(nameCommand), 1043 | 'number', 1044 | '', 1045 | 'level', 1046 | valCommand, 1047 | ); 1048 | }); 1049 | } else { 1050 | let parentsID = $(el).parent().attr('id') || ''; 1051 | if (parentsID === undefined) { 1052 | parentsID = ''; 1053 | } 1054 | 1055 | if (parentsID.includes('chval')) { 1056 | try { 1057 | // @ts-expect-error: .data may be missing 1058 | // eslint-disable-next-line prettier/prettier 1059 | scriptValues = $(el).parent().parent().next().next().next().get()[0].children[0].data; 1060 | } catch { 1061 | try { 1062 | scriptValues = $(el).parent().parent().next().next().next().text(); 1063 | } catch { 1064 | scriptValues = null; 1065 | } 1066 | } 1067 | 1068 | if (scriptValues) { 1069 | const nameCommand = $(el).parent().parent().parent().find('h3').text(); 1070 | const minCommand = scriptValues.match(/\['min'] = '(.*?)'/); 1071 | const maxCommand = scriptValues.match(/\['max'] = '(.*?)'/); 1072 | const valCommandMatch = scriptValues.match(/\['val']='(.*?)'/); 1073 | const idCommand = scriptValues.match(/\['id']='(.*?)'/); 1074 | const unitCommand = $(el).parent().parent().parent().find('.append-1').text(); 1075 | 1076 | if (idCommand) { 1077 | if (submenu) { 1078 | submenupath = ''; 1079 | submenupath += `.${submenu[1]}`; 1080 | } 1081 | createISGCommands( 1082 | `${translateName('settings')}.${group}${submenupath}`, 1083 | idCommand[1], 1084 | nameCommand, 1085 | 'number', 1086 | unitCommand, 1087 | 'state', 1088 | parseFloat( 1089 | valCommandMatch 1090 | ? String(valCommandMatch[1]).replace(',', '.').replace(' ', '') 1091 | : 'NaN', 1092 | ), 1093 | '', 1094 | parseFloat( 1095 | String( 1096 | minCommand 1097 | ? minCommand[1].replace(',', '.').replace(' ', '') 1098 | : NaN, 1099 | ), 1100 | ), 1101 | parseFloat( 1102 | String( 1103 | maxCommand 1104 | ? maxCommand[1].replace(',', '.').replace(' ', '') 1105 | : NaN, 1106 | ), 1107 | ), 1108 | ); 1109 | } 1110 | } 1111 | } else { 1112 | try { 1113 | const nextNode = $(el).next().get()[0]; 1114 | if ( 1115 | nextNode && 1116 | nextNode.children && 1117 | nextNode.children[0] && 1118 | 'data' in nextNode.children[0] 1119 | ) { 1120 | scriptValues = nextNode.children[0].data; 1121 | } else { 1122 | scriptValues = undefined; 1123 | } 1124 | } catch { 1125 | try { 1126 | scriptValues = $(el).next().text(); 1127 | } catch { 1128 | scriptValues = null; 1129 | } 1130 | } 1131 | 1132 | if (scriptValues) { 1133 | const nameCommand = $(el).parent().parent().find('h3').text(); 1134 | 1135 | const minCommand = scriptValues.match(/\['min'] = '(.*?)'/); 1136 | const maxCommand = scriptValues.match(/\['max'] = '(.*?)'/); 1137 | const valCommandMatch = scriptValues.match(/\['val']='(.*?)'/); 1138 | const idCommand = scriptValues.match(/\['id']='(.*?)'/); 1139 | const unitCommand = $(el).parent().parent().find('.append-1').text(); 1140 | 1141 | if (idCommand) { 1142 | if (submenu) { 1143 | submenupath = ''; 1144 | submenupath += `.${submenu[1]}`; 1145 | } 1146 | createISGCommands( 1147 | `${translateName('settings')}.${group}${submenupath}`, 1148 | idCommand[1], 1149 | nameCommand, 1150 | 'number', 1151 | unitCommand, 1152 | 'state', 1153 | parseFloat( 1154 | valCommandMatch 1155 | ? valCommandMatch[1].replace(',', '.').replace(' ', '') 1156 | : 'NaN', 1157 | ), 1158 | '', 1159 | parseFloat( 1160 | String( 1161 | minCommand 1162 | ? minCommand[1].replace(',', '.').replace(' ', '') 1163 | : NaN, 1164 | ), 1165 | ), 1166 | parseFloat( 1167 | String( 1168 | maxCommand 1169 | ? maxCommand[1].replace(',', '.').replace(' ', '') 1170 | : NaN, 1171 | ), 1172 | ), 1173 | ); 1174 | } 1175 | } 1176 | } 1177 | } 1178 | } 1179 | } catch (errInner) { 1180 | const errorMessage = errInner instanceof Error ? errInner.message : String(errInner); 1181 | adapter.log.debug(`getIsgCommands input-parse error: ${errorMessage}`); 1182 | } 1183 | }); 1184 | } 1185 | } catch (e) { 1186 | const errorMessage = e instanceof Error ? e.message : String(e); 1187 | adapter.log.debug(`getIsgCommands(${sidePath}) error: ${errorMessage}`); 1188 | } 1189 | } 1190 | 1191 | /* ------------------------- 1192 | Sending queued commands (debounced) 1193 | ------------------------- */ 1194 | 1195 | function setIsgCommands(strKey, strValue) { 1196 | const newCommand = { name: strKey, value: strValue }; 1197 | commands.push(newCommand); 1198 | 1199 | const payload = querystring.stringify({ 1200 | user: adapter.config.isgUser, 1201 | pass: adapter.config.isgPassword, 1202 | data: JSON.stringify(commands), 1203 | }); 1204 | 1205 | clearTimeout(CommandTimeout); 1206 | CommandTimeout = setTimeout(async function () { 1207 | const fetch = getFetch(); 1208 | const built = buildFetchOptions(`${host}/save.php`, { 1209 | method: 'POST', 1210 | headers: { 1211 | 'Content-Type': 'application/x-www-form-urlencoded', 1212 | Accept: '*/*', 1213 | }, 1214 | body: payload, 1215 | }); 1216 | 1217 | try { 1218 | const res = await fetch(`${host}/save.php`, built.options); 1219 | built.clearTimeout(); 1220 | if (res && res.status == 200) { 1221 | commandPaths.forEach(function (item) { 1222 | schedule(() => getIsgCommands(item)); 1223 | }); 1224 | } else { 1225 | adapter.log.error(`statusCode: ${res ? res.status : 'no response'}`); 1226 | adapter.log.error(`statusText: ${res && 'statusText' in res ? res.statusText : ''}`); 1227 | } 1228 | } catch (error) { 1229 | built.clearTimeout(); 1230 | const errorMessage = error instanceof Error ? error.message : String(error); 1231 | if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') { 1232 | adapter.log.debug(`setIsgCommands aborted: ${errorMessage}`); 1233 | } else if (String(error).toLowerCase().includes('aborted')) { 1234 | adapter.log.debug(`setIsgCommands aborted: ${errorMessage}`); 1235 | } else { 1236 | adapter.log.error(`Error: ${errorMessage}`); 1237 | } 1238 | } 1239 | commands = []; 1240 | }, 5000); 1241 | } 1242 | 1243 | /* ------------------------- 1244 | Reboot / main loop 1245 | ------------------------- */ 1246 | 1247 | function rebootISG() { 1248 | const url = `${host}/reboot.php`; 1249 | const fetch = getFetch(); 1250 | const built = buildFetchOptions(url, { method: 'GET' }); 1251 | 1252 | fetch(url, built.options) 1253 | .then(() => { 1254 | built.clearTimeout(); 1255 | adapter.log.info('Reboot request sent to ISG.'); 1256 | }) 1257 | .catch(err => { 1258 | built.clearTimeout(); 1259 | if ( 1260 | err && 1261 | ((err instanceof Error && err.name === 'AbortError') || String(err).toLowerCase().includes('aborted')) 1262 | ) { 1263 | adapter.log.debug(`rebootISG aborted: ${err instanceof Error ? err.message : err}`); 1264 | } else { 1265 | adapter.log.error(`Reboot request failed: ${err instanceof Error ? err.message : err}`); 1266 | } 1267 | }); 1268 | } 1269 | 1270 | async function main() { 1271 | adapter.setObjectNotExists( 1272 | 'ISGReboot', 1273 | { 1274 | type: 'state', 1275 | common: { 1276 | name: translateName('ISGReboot'), 1277 | type: 'boolean', 1278 | role: 'button', 1279 | read: true, 1280 | write: true, 1281 | }, 1282 | native: {}, 1283 | }, 1284 | () => adapter.subscribeStates('ISGReboot'), 1285 | ); 1286 | 1287 | const ipformat = 1288 | /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 1289 | /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 1290 | const fqdnformat = /^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$/; 1291 | 1292 | if (!adapter.config.isgAddress || adapter.config.isgAddress.trim() === '') { 1293 | adapter.log.error('Invalid configuration - isgAddress not set.'); 1294 | adapter.setState('info.connection', false, true); 1295 | return; 1296 | } else if (!adapter.config.isgAddress.match(ipformat) && !adapter.config.isgAddress.match(fqdnformat)) { 1297 | adapter.log.error( 1298 | `ISG Address ${adapter.config.isgAddress} format not valid. Should be e.g. 192.168.123.123 or servicewelt.fritz.box`, 1299 | ); 1300 | return; 1301 | } 1302 | 1303 | host = adapter.config.isgAddress.trim(); 1304 | if (!/^\s*https?:\/\//i.test(host)) { 1305 | host = `http://${host}`; 1306 | } 1307 | adapter.log.info(`Connecting to ISG: ${host} ...`); 1308 | 1309 | // remove trailing slashes 1310 | // host = host.replace(/\/+$/, ''); 1311 | 1312 | adapter.subscribeStates('*'); 1313 | 1314 | // check username and password 1315 | try { 1316 | const $ = await getHTML('1,0'); 1317 | if ($) { 1318 | let loginPage; 1319 | try { 1320 | loginPage = $('#main').attr('class'); 1321 | } catch (e) { 1322 | const errorMessage = e instanceof Error ? e.message : String(e); 1323 | adapter.log.error(`#main error: ${errorMessage}`); 1324 | } 1325 | if (loginPage && loginPage != null && loginPage != undefined && String(loginPage) === 'login') { 1326 | adapter.log.error('ISG Login failed - please check your username and password!'); 1327 | adapter.setState('info.connection', false, true); 1328 | return; 1329 | } 1330 | adapter.log.info('Connected to ISG successfully.'); 1331 | adapter.setState('info.connection', true, true); 1332 | } 1333 | } catch (e) { 1334 | const errorMessage = e instanceof Error ? e.message : String(e); 1335 | adapter.log.error(`checkIsgCredentials error: ${errorMessage}`); 1336 | } 1337 | 1338 | // schedule initial fetches with concurrency control 1339 | statusPaths.forEach(function (item) { 1340 | schedule(() => getIsgStatus(item)); 1341 | }); 1342 | 1343 | valuePaths.forEach(function (item) { 1344 | schedule(() => getIsgValues(item)); 1345 | }); 1346 | 1347 | commandPaths.forEach(function (item) { 1348 | schedule(() => getIsgCommands(item)); 1349 | }); 1350 | 1351 | if (isgIntervall) { 1352 | clearInterval(isgIntervall); 1353 | } 1354 | if (isgCommandIntervall) { 1355 | clearInterval(isgCommandIntervall); 1356 | } 1357 | 1358 | isgIntervall = setInterval( 1359 | function () { 1360 | valuePaths.forEach(function (item) { 1361 | schedule(() => getIsgValues(item)); 1362 | }); 1363 | statusPaths.forEach(function (item) { 1364 | schedule(() => getIsgStatus(item)); 1365 | }); 1366 | }, 1367 | Math.max(1, Number(adapter.config.isgIntervall) || 60) * 1000, 1368 | ); 1369 | 1370 | isgCommandIntervall = setInterval( 1371 | function () { 1372 | commandPaths.forEach(function (item) { 1373 | schedule(() => getIsgCommands(item)); 1374 | }); 1375 | }, 1376 | Math.max(1, Number(adapter.config.isgCommandIntervall) || 60) * 1000, 1377 | ); 1378 | } 1379 | 1380 | /* ------------------------- 1381 | Adapter lifecycle 1382 | ------------------------- */ 1383 | 1384 | function startAdapter(options) { 1385 | options = options || {}; 1386 | Object.assign(options, { 1387 | name: 'stiebel-isg', 1388 | stateChange: function (id, state) { 1389 | const command = id.split('.').pop(); 1390 | if (!state || state.ack) { 1391 | return; 1392 | } 1393 | 1394 | if (command == 'ISGReboot') { 1395 | adapter.log.info('ISG rebooting'); 1396 | rebootISG(); 1397 | setTimeout(main, 60000); 1398 | return; 1399 | } 1400 | 1401 | setIsgCommands(command, state.val); 1402 | }, 1403 | ready: function () { 1404 | adapter.getForeignObject('system.config', function (err, obj) { 1405 | if (err) { 1406 | adapter.log.error(err); 1407 | if (obj) { 1408 | adapter.log.error(`statusCode: ${obj.statusCode}`); 1409 | adapter.log.error(`statusText: ${obj.statusText}`); 1410 | } 1411 | return; 1412 | } else if (obj) { 1413 | if (!obj.common.language) { 1414 | adapter.log.info('Language not set. English set therefore.'); 1415 | nameTranslation = require('./admin/i18n/en/translations.json'); 1416 | } else { 1417 | systemLanguage = obj.common.language; 1418 | try { 1419 | nameTranslation = require(`./admin/i18n/${systemLanguage}/translations.json`); 1420 | } catch { 1421 | adapter.log.warn(`Translations for ${systemLanguage} not found, falling back to English.`); 1422 | nameTranslation = require('./admin/i18n/en/translations.json'); 1423 | } 1424 | } 1425 | 1426 | // set cookie jar 1427 | setJar(new tough.CookieJar()); 1428 | // Reset the connection indicator during startup 1429 | adapter.setState('info.connection', false, true); 1430 | 1431 | // read concurrency configuration (default 3) 1432 | try { 1433 | const cfgVal = Number(adapter.config.maxConcurrentFetches); 1434 | if (!isNaN(cfgVal) && cfgVal > 0) { 1435 | maxConcurrentFetches = cfgVal; 1436 | } else { 1437 | maxConcurrentFetches = 3; 1438 | } 1439 | } catch { 1440 | maxConcurrentFetches = 3; 1441 | } 1442 | 1443 | main(); 1444 | } 1445 | }); 1446 | 1447 | commandPaths = (adapter.config.isgCommandPaths || '').split(';').filter(Boolean); 1448 | valuePaths = (adapter.config.isgValuePaths || '').split(';').filter(Boolean); 1449 | statusPaths = (adapter.config.isgStatusPaths || '').split(';').filter(Boolean); 1450 | 1451 | if (adapter.config.isgExpert === true && adapter.config.isgExpertPaths) { 1452 | commandPaths = commandPaths.concat(adapter.config.isgExpertPaths.split(';').filter(Boolean)); 1453 | } 1454 | }, 1455 | unload: function (callback) { 1456 | try { 1457 | if (isgIntervall) { 1458 | clearInterval(isgIntervall); 1459 | } 1460 | if (isgCommandIntervall) { 1461 | clearInterval(isgCommandIntervall); 1462 | } 1463 | if (CommandTimeout) { 1464 | clearTimeout(CommandTimeout); 1465 | } 1466 | adapter.log.info('cleaned everything up...'); 1467 | callback(); 1468 | } catch { 1469 | callback(); 1470 | } 1471 | }, 1472 | }); 1473 | 1474 | adapter = new utils.Adapter(options); 1475 | return adapter; 1476 | } 1477 | 1478 | /* ------------------------- 1479 | Export / start 1480 | ------------------------- */ 1481 | 1482 | // @ts-expect-error: module is defined in adapter-core 1483 | if (module && module.parent) { 1484 | module.exports = startAdapter; 1485 | } else { 1486 | startAdapter(); 1487 | } 1488 | --------------------------------------------------------------------------------