├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test-and-release.yml ├── .gitignore ├── .releaseconfig.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── admin ├── glogo.png ├── growatt.png ├── index_m.html ├── style.css └── words.js ├── eslint.config.mjs ├── growattMain.js ├── io-package.json ├── lib └── env │ └── tools.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── test ├── integration.js └── package.js └── update.cmd /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | max_line_length = 180 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.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 your inverter** 10 | 11 | Typ, modell, ... 12 | The enviroment (panels, accu etc.) 13 | The modell of the logger 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 21 | 1. Go to '...' 22 | 2. Click on '...' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Screenshots & Logfiles** 30 | If applicable, add screenshots and logfiles to help explain your problem. 31 | 32 | **Versions:** 33 | 34 | - Adapter version: 35 | - JS-Controller version: 36 | - Node version: 37 | - Operating system: 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | # Run this job on all pushes and pull requests 4 | # as well as tags with a semantic version 5 | on: 6 | push: 7 | branches: 8 | - '*' 9 | tags: 10 | # normal versions 11 | - 'v[0-9]+.[0-9]+.[0-9]+' 12 | # pre-releases 13 | - 'v[0-9]+.[0-9]+.[0-9]+-**' 14 | 15 | pull_request: 16 | branches: 17 | - '*' 18 | 19 | jobs: 20 | # Performs quick checks before the expensive test runs 21 | check-and-lint: 22 | if: contains(github.event.head_commit.message, '[skip ci]') == false 23 | 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | node-version: [18.x, 20.x, 22.x] 29 | 30 | steps: 31 | - name: Checkout source Code 32 | uses: actions/checkout@v4 33 | 34 | - name: Use Node.js ${{ matrix.node-version }} 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | 39 | - name: Install Dependencies 40 | uses: bahmutov/npm-install@v1 41 | 42 | - name: Lint source code 43 | run: npm run lint 44 | 45 | - name: Test package files 46 | run: npm run test:package 47 | 48 | # Runs adapter tests on all supported node versions and OSes 49 | adapter-tests: 50 | needs: [check-and-lint] 51 | if: contains(github.event.head_commit.message, '[skip ci]') == false 52 | 53 | strategy: 54 | matrix: 55 | node-version: [18.x, 20.x, 22.x] 56 | os: [ubuntu-latest, windows-latest, macos-latest] 57 | 58 | runs-on: ${{ matrix.os }} 59 | 60 | steps: 61 | - name: Checkout source Code 62 | uses: actions/checkout@v4 63 | 64 | - name: Use Node.js ${{ matrix.node-version }} 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: ${{ matrix.node-version }} 68 | 69 | - name: Install Dependencies 70 | uses: bahmutov/npm-install@v1 71 | 72 | - name: Run integration tests (unix only) 73 | if: startsWith(runner.OS, 'windows') == false 74 | run: DEBUG=testing:* npm run test:integration 75 | 76 | - name: Run integration tests (windows only) 77 | if: startsWith(runner.OS, 'windows') 78 | run: set DEBUG=testing:* & npm run test:integration 79 | 80 | # Deploys the final package to NPM 81 | deploy: 82 | needs: [adapter-tests] 83 | # Trigger this step only when a commit on master is tagged with a version number 84 | if: | 85 | contains(github.event.head_commit.message, '[skip ci]') == false && 86 | github.event_name == 'push' && 87 | startsWith(github.ref, 'refs/tags/v') 88 | strategy: 89 | matrix: 90 | node-version: [22.x] 91 | os: [ubuntu-latest] 92 | 93 | runs-on: ${{ matrix.os }} 94 | steps: 95 | - name: Checkout source Code 96 | uses: actions/checkout@v4 97 | 98 | - name: Use Node.js ${{ matrix.node-version }} 99 | uses: actions/setup-node@v4 100 | with: 101 | node-version: ${{ matrix.node-version }} 102 | 103 | - name: Install Dependencies 104 | uses: bahmutov/npm-install@v1 105 | 106 | - name: Publish package to npm 107 | uses: JS-DevTools/npm-publish@v3 108 | with: 109 | token: ${{ secrets.npm }} 110 | 111 | # Dummy job for skipped builds - without this, github reports the build as failed 112 | skip-ci: 113 | if: contains(github.event.head_commit.message, '[skip ci]') 114 | runs-on: ubuntu-latest 115 | steps: 116 | - name: Skip build 117 | run: echo "Build skipped!" 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | test.json -------------------------------------------------------------------------------- /.releaseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["iobroker", "license", "manual-review"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": ["io-package.json"], 5 | "url": "https://json.schemastore.org/io-package" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 PLCHome 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](admin/glogo.png) 2 | 3 | ## ioBroker.growatt 4 | 5 | ![NPM version](http://img.shields.io/npm/v/iobroker.growatt.svg) 6 | ![Downloads](https://img.shields.io/npm/dm/iobroker.growatt.svg) 7 | ![Number of Installations (latest)](https://iobroker.live/badges/growatt-installed.svg) 8 | ![Number of Installations (stable)](https://iobroker.live/badges/growatt-stable.svg) 9 | 10 | [![NPM](https://nodei.co/npm/iobroker.growatt.png?downloads=true)](https://nodei.co/npm/iobroker.growatt/) 11 | 12 | **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0. 13 | 14 | This adapter works through Growatt's cloud servers. There is also the [Grott project](https://github.com/johanmeijer/grott) that intercepts the data from the communication. 15 | 16 | --- 17 | 18 | ### growatt adapter for ioBroker 19 | 20 | ioBroker Growatt Adapter to communiacte with Growatt Shine Server. 21 | I'm not affiliated. 22 | Usually, the data is sent from the data logger to the cloud every 5 minutes. 23 | You can change it, see below. 24 | 25 | Not all plant types are implemented. 26 | 27 | Currently only data can be read, writing parameters or changing parameters is not possible. 28 | 29 | ### Can I spend a coffee? 30 | 31 | Of course if you like my work via Paypal to PLChome _at_ fontanestr _dot_ de 32 | 33 | --- 34 | 35 | ## Adapter admin page 36 | 37 | ### Main Settings 38 | 39 | #### User and Password 40 | 41 | Please enter the name and password that you also use in the Shine app or in the web portal. 42 | 43 | #### Login with shared key 44 | 45 | On the Growatt website under energy, plant management, operating tools you can send yourself a key by e-mail. 46 | 47 | #### Read plant data 48 | 49 | This data record contains the stored master data 50 | 51 | #### Read last history data 52 | 53 | Reads the last data record from the history of the data logger. 54 | This function supports minute intervals for the data logger. 55 | 56 | #### Read status data 57 | 58 | These data are not available for all plants (not INV/MAX/TLX). This dataset contains live data. 59 | This function supports minute intervals for the data logger. 60 | 61 | #### Read total data 62 | 63 | This data record contains aggregation data. 64 | 65 | #### Read device data 66 | 67 | This data record contains some data from the device. Some data are also available in the other categories. 68 | 69 | #### Read weather 70 | 71 | This data set contains the weather forecast. 72 | 73 | #### Read fault log entries 74 | 75 | Reads the entries in the fault log of the current year and creates objects with the messages for this. Only the first page with the most current reports is read. 76 | 77 | #### Write inverter settings 78 | 79 | If this is activated, some settings can be edited for some inverters. 80 | 81 | Objects are created below the inverter serial number element for the settings. A channel is created for each setting. 82 | 83 | Below the objects are "read", "write", "msg" and and the node values. Below the values are parameters. 84 | 85 | If the values of the parameters could be read, they are written with ACK=true. "read" is set to true on successful reading with ack. If reading fails, "Read" is set to false ack=true. Writing to "Read" from "true" without ACK triggers a read operation. If a new connection to the cloud is established, the settings are also read out. 86 | 87 | To write the settings, the parameters must first be set. Then "write" is set to true with ack=false. 88 | If the data was written successfully, "write" is set to "true" ack=true, if the inverter reported an error, "write" is set to "false" ack=true. In addition, the return message of the inverter is written to the "msg" status. 89 | 90 | If writing was successful, reading is automatically triggered. 91 | 92 | The inverter can only change one setting at a time and the transmission path is from ioBroker via the cloud to the WLAN adapter and then to the inverter. The settings are processed one after the other via a queue. A session time that is too short can lead to problems. 93 | 94 | The writing of the settings was developed to the best of our knowledge. However, the author does not assume liability for errors contained in or for damages arising from the use of the software. 95 | 96 | #### Select it if your Growatt page is a black C&I page 97 | 98 | Select it if your Growatt page is a C&I Plant page with indexbC or plantDo in the Path of the Growatt webinterface. 99 | 100 | The black C&I pages (commercial and industrial) have an other path to the objects but it semms to work if this Checkbox is on. It Changed index to indexbC in the webpath. 101 | 102 | #### Timeout in seconds 103 | 104 | The default timeout for HTTP requests. The default value 60 seconds, as with web browsers 105 | 106 | #### Process timeout in seconds 107 | 108 | This timeout monitors the collection of data from the Growatt server. If the server does not process all of the data within this time, an error is reported, the session is ended and a new cycle timer is started. The default value is 600 seconds. 109 | If the value is 0, this check function is not executed. 110 | 111 | #### Keep web session 112 | 113 | The adapter only logs in once and not with every data request from the Growatt server. By default it is on. 114 | 115 | #### Session time in minutes 116 | 117 | Here you can set when the adapter logs out of the server and logs in again. A 0 means never log out. Default value is 0=infinity. 118 | 119 | #### Cycle time in seconds 120 | 121 | The interval at which the data is requested from the server. The time required for the data query is then deducted from the next one. If the query lasts longer than the waiting time, the adapter only sleeps 100ms. The default value is 30 seconds. 122 | 123 | #### Error cycle time in seconds 124 | 125 | If an error occurs when querying the values at the Growatt server, this time is used instead of the cycle time. The default value is 120 seconds 126 | 127 | #### Growatt server 128 | 129 | Another url can be entered here, for example to use the US domain. But it must start with "https://". The default is blank, so https://server.growatt.com is used. 130 | 131 | ### Manage Objects 132 | 133 | Here you can define what should happen to each value (object) that is picked up by the inverter. 134 | There are a lot of values that do not belong to your inverter. These can be removed here. 135 | Since there is no event with which the object list can be reloaded when saving. The update button must be used when save is pressed. 136 | 137 | #### Normal 138 | 139 | The object remains, the value is updated. 140 | 141 | #### Delete 142 | 143 | The object is deleted and the value loaded by the inverter is discarded. 144 | After the update, only the ID and the action are displayed because the object no longer exists. If you select normally, the object will be created again after saving. 145 | 146 | #### No update 147 | 148 | The object remains, the values from the inverter are discarded. 149 | 150 | ### Manage Loggers 151 | 152 | The instance must be running and logged in to the server. Then the settings of the data logger can be called up via the refresh button in this tab. 153 | The data is not requested automatically, the request can only be made via the button. 154 | The fields displayed for the data logger cannot be changed. It is only about retrieved data. 155 | Buttons are displayed for each logger. Settings can be edited with the button. 156 | _When using GROTT, changing settings in the INI must be enabled._ 157 | Please do not use the settings if a value appears that you did not expect. 158 | Attention this is based on reingeneering. I am not liable for damage to the device. 159 | 160 | #### Button interval 161 | 162 | The current interval in minutes is read from the data logger and an input form appears in which the value can be adjusted. 163 | If you get a successful response, the data logger should be restarted to activate the settings. 164 | 165 | #### Button server ip 166 | 167 | The server for data transmission on the logger can be set here. When using Grott or US, the local or US IP can be specified here. 168 | If you get a successful response, the data logger should be restarted to activate the settings. 169 | 170 | #### Button server port 171 | 172 | The port on the server for data transmission on the logger can be set here. 173 | If you get a successful response, the data logger should be restarted to activate the settings. 174 | 175 | #### Button check firmware 176 | 177 | It will be asked whether the firmware of the data logger is up to date. 178 | The update must be done on the growatt page. 179 | 180 | #### Button restart datalogger 181 | 182 | Every boot is good. 183 | The settings are accepted. 184 | 185 | --- 186 | 187 | ## sendTo for scripts 188 | 189 | It is possible to send a command to the instance via sendTo. The adapter then responds. 190 | The following commands are implemented. 191 | The return value is returned depending on the parameter transfer. If the parameters are passed as a JSON string, a JSON is returned. If the parameters are passed as an object, an object is returned. 192 | 193 | ### getHistory 194 | 195 | This command lists the history. It can be used, for example, to supplement data in a database. 196 | Regardless of the time range, Growatt always seems to return 80 records. If the interval is set to 1 minute and more than 80 minutes are needed, the command must be executed several times and the start from 0 must be increased more and more. 197 | 198 | | Parameter | Type | Description | 199 | | --------- | ------- | ------------------------------------------------------------------------------------------------------------ | 200 | | type | String | The type of inverter can be found in object "growatt. - instance - . - nr - .devices. - sn - .growattType". | 201 | | sn | String | The serialnumber of inverter can be found in object path "growatt. - instance - . - nr - .devices. - sn - ". | 202 | | startDate | Date | The atart | 203 | | endDate | Date | The end mast be grater then start | 204 | | start | Integer | 0 is the start page for the call with the most recent data first | 205 | 206 | Example call: 207 | 208 | ``` 209 | sendTo('growatt.0','getHistory',{"type":"","sn":"","startDate":new Date((new Date()).getTime()- 60*60*1000),"endDate":new Date() , "start":0},(res)=>{console.log(res)}) 210 | ``` 211 | 212 | Example code: 213 | 214 | ``` 215 | const sn = " your sn "; //your inverter sn 216 | const inType = " your typ "; // the invertertyp 217 | const hist = 'growatt.0. your nr .devices. your sn .historyLast.'; // the Path to history 218 | const storeField =['accChargePower','etoGridToday']; //the fields to store 219 | const history = "influx.0" //your History sql.0 or influx.0 or history.0 ... 220 | const min = 10 // größer 10 min auffüllen.... 221 | 222 | on({id: hist+'calendar', change: "ne"},(obj)=>{ 223 | if ((obj.state.val - obj.oldState.val) > min*60000) { 224 | console.log(obj.state.val - obj.oldState.val); 225 | function fillup(res) { 226 | res.forEach((r)=>{ 227 | const ti = (new Date(r.calendar)).getTime(); 228 | if (ti > obj.oldState.val && ti < obj.state.val) { 229 | function store(n) { 230 | sendTo(history, 'storeState', { 231 | id: hist+n, 232 | state: {ts: ti, val: r[n], ack: true} 233 | }, result => {console.log(`added ${hist+n} ${new Date(ti)} ${r[n]}`)}); 234 | } 235 | storeField.forEach((f) => {store(f)}); 236 | } 237 | }) 238 | } 239 | sendTo('growatt.0','getHistory',{"type":inType,"sn":sn,"startDate":obj.oldState.val,"endDate":obj.state.val, "start":0},fillup) 240 | sendTo('growatt.0','getHistory',{"type":inType,"sn":sn,"startDate":obj.oldState.val,"endDate":obj.state.val, "start":1},fillup) 241 | sendTo('growatt.0','getHistory',{"type":inType,"sn":sn,"startDate":obj.oldState.val,"endDate":obj.state.val, "start":2},fillup) 242 | sendTo('growatt.0','getHistory',{"type":inType,"sn":sn,"startDate":obj.oldState.val,"endDate":obj.state.val, "start":3},fillup) 243 | } 244 | }); 245 | ``` 246 | 247 | ### getDatalogger 248 | 249 | It gives you information about the dataloggers. 250 | This function has no parameters. Either "{}" or {} must be passed. 251 | The return is an array of object. 252 | 253 | | Parameter | Type | Description | 254 | | --------- | ---- | ----------- | 255 | 256 | ### getDataLoggerIntervalRegister 257 | 258 | It reads out the interval and returns it. the return value is an OBJ. The interval is in msg. 259 | 260 | | Parameter | Type | Description | 261 | | --------- | ------ | ------------------------------------------------------------- | 262 | | sn | string | The serial number of the logger is returned by getDatalogger. | 263 | 264 | ### setDataLoggerIntervalRegister 265 | 266 | Writes the interval at which the logger sends the data. 267 | 268 | | Parameter | Type | Description | 269 | | --------- | ------- | ------------------------------------------------------------- | 270 | | sn | string | The serial number of the logger is returned by getDatalogger. | 271 | | value | integer | The new value in minutes | 272 | 273 | An object is returned with a message. 274 | 275 | ### getDataLoggerIpRegister 276 | 277 | It reads the IP to which the logger sends the data and returns it. The return value is an OBJ. The IP is in msg. 278 | 279 | | Parameter | Type | Description | 280 | | --------- | ------ | ------------------------------------------------------------- | 281 | | sn | string | The serial number of the logger is returned by getDatalogger. | 282 | 283 | ### setDataLoggerIp 284 | 285 | It writes the IP to which the logger sends the data. It's useful for the Grott project. The return value is an object that says what happened. 286 | 287 | | Parameter | Type | Description | 288 | | --------- | ------- | ------------------------------------------------------------- | 289 | | sn | string | The serial number of the logger is returned by getDatalogger. | 290 | | value | integer | The new value in minutes | 291 | 292 | An object is returned with a message. 293 | 294 | ### getDataLoggerPortRegister 295 | 296 | It reads the port to which the logger sends the data and returns it. The return value is an OBJ. The IP is in msg. 297 | 298 | | Parameter | Type | Description | 299 | | --------- | ------ | ------------------------------------------------------------- | 300 | | sn | string | The serial number of the logger is returned by getDatalogger. | 301 | 302 | ### setDataLoggerPort 303 | 304 | It writes the port to which the logger sends the data. It's useful for the Grott project. The return value is an object that says what happened. 305 | 306 | | Parameter | Type | Description | 307 | | --------- | ------- | ------------------------------------------------------------- | 308 | | sn | string | The serial number of the logger is returned by getDatalogger. | 309 | | value | integer | The new value in minutes | 310 | 311 | An object is returned with a message. 312 | 313 | ### checkLoggerFirmware 314 | 315 | Calls up the firmware check from the logger. If an update is necessary, you can see it in the answer. 316 | 317 | | Parameter | Type | Description | 318 | | --------- | ------ | ------------------------------------------------------------- | 319 | | sn | string | The serial number of the logger is returned by getDatalogger. | 320 | 321 | ### restartDatalogger 322 | 323 | Causes a warm start of the data logger. 324 | 325 | | Parameter | Type | Description | 326 | | --------- | ------ | ------------------------------------------------------------- | 327 | | sn | string | The serial number of the logger is returned by getDatalogger. | 328 | 329 | --- 330 | 331 | ## Speedup data interval internal method 332 | 333 | Have a look at Manage Loggers and Button Interval 334 | 335 | ## Speedup data interval external app method 336 | 337 | - Open the ShinePhone app 338 | - Click on attachment below 339 | - Top right +, then list data loggers 340 | - Click on existing data logger 341 | - Configure data logger 342 | - Wireless hotspot mode 343 | - Put the stick in AP mode 344 | - Connect to Wifi hotspot, PW 123456789 ? <- check again 345 | - Continue 346 | - Advanced 347 | - Time setting 348 | - Interval to 1 349 | - Enter password growattYYYYMMDD (e.g.growatt20220209) 350 | - Unlock 351 | - Click and apply changes 352 | - Exit hotspot mode 353 | 354 | ## Speedup data interval external old method 355 | 356 | In hotspot mode it is only possible to change the interval on the old firmware. 357 | Growatt has removed the website from the firmware. 358 | Therefore, the description has also been removed. 359 | 360 | **There is no change to the charts on growatt side. There you can only see a change in the data from the datalogger.** 361 | 362 | -\*- 363 | 364 | ## Changelog 365 | 366 | 370 | 371 | ### 3.3.1 (2024-10-26) 372 | 373 | - (PLCHome) Added ac charge for TLXH. Thanx to olli0815! 374 | - (PLCHome) Added time slots for TLXH. Thanks to olli0815 for debugging and support. 375 | - (PLCHome) Added Inverter On Off for TLX und TLXH. Thanks to olli0815 for debugging and support. 376 | 377 | ### 3.3.0 (2024-10-25) 378 | 379 | - (PLCHome) Added time slots for TLXH. Thanks to olli0815 for debugging and support. 380 | - (PLCHome) Added Inverter On Off for TLX und TLXH. Thanks to olli0815 for debugging and support. 381 | 382 | ### 3.2.5 (2024-08-13) 383 | 384 | - (PLCHome) Solved the problem that no inverter list but result 2 was returned in NOAH. 385 | - (PLCHome) Added NOAH. 386 | 387 | ### 3.2.4 (2024-07-03) 388 | 389 | - (PLCHome) Configure this adapter to use the release script. 390 | - (PLCHome) no connection can be established password must now be transferred as MD5 hash. 391 | - (PLCHome) cookie issue 392 | 393 | ### 3.2.3 (27.01.2024) 394 | 395 | - (PLCHome) In Multiple Backflow the objects in Total Data and Status Data were swapped. Please delete the objects below Total Data and Status Data and restart the adapter after the update. 396 | 397 | ### 3.2.2 (27.01.2024) 398 | 399 | - (PLCHome) Catching of the fault log messages is now possible (Thanx to ZioCain for the code) 400 | - (PLCHome) Setting active power for MAX inverter (Thanx to sefina for testing) 401 | 402 | ### 3.2.1 (08.09.2023) 403 | 404 | - (PLCHome) Additionally query the status information via the Plant List. 405 | 406 | ### 3.2.0 (01.09.2023) 407 | 408 | - (PLCHome) Added inverter typ singleBackflow and multipleBackflow 409 | 410 | ### 3.1.2 (16.08.2023) 411 | 412 | - (PLCHome) sendTo now also possible with objects as message data 413 | - (PLCHome) Added message getHistory 414 | 415 | ### 3.1.1 (03.07.2023) 416 | 417 | - (PLCHome) Added support for Growatt page when Plant is a C&I Plant page with indexbC or plantDo in Path of the Growatt web interface. Thanks to Denn281 418 | 419 | ### 3.0.4 (03.07.2023) 420 | 421 | - (PLCHome) No retrieval of the other parameters value possible after parameter error 422 | - (PLCHome) Grid first and Battery first setting on MIX may not work 423 | 424 | ### 3.0.3 (27.06.2023) 425 | 426 | - (PLCHome) setting for tlx/tlxh time improved 427 | 428 | ### 3.0.2 (08.06.2023) 429 | 430 | - (PLCHome) Write inverter settings, it can be activated via the config 431 | 432 | - mix 433 | - Time 434 | - Grid first 435 | - Battery first 436 | - Inverter On/Off 437 | - LoadFirst 438 | - Failsafe 439 | - PV active power rate 440 | - Backflow setting 441 | - Backflow setting power 442 | - EPSOn 443 | - tlx/tlxh 444 | - Time 445 | - PV active power rate 446 | 447 | ### 2.1.1 (17.04.2023) 448 | 449 | - (PLCHome) Calendar structure was not always changed to timestamp 450 | - (PLCHome) Improvement in the internal handling of objects without considering their case. 451 | 452 | ### 2.1.0 (14.04.2023) 453 | 454 | - (PLCHome) Status data now also from TLX/TLXH 455 | - (PLCHome) TLX Hybrid is now working 456 | - (PLCHome) If there are different inverters, these are now shown 457 | 458 | ### 2.0.0 (06.10.2022) 459 | 460 | - (PLCHome) Data logger settings can be called up and changed. 461 | - (PLCHome) The server url can be changed. 462 | 463 | ### 1.2.1 (21.09.2022) 464 | 465 | - (PLCHome) Added an offset to numeric values. My inverter reset te total quantity by itself. Now the total quantity can be corrected. 466 | 467 | ### 1.1.19 (30.08.2022) 468 | 469 | - (PLCHome) HTML Header 470 | 471 | ### 1.1.17 (10.08.2022) 472 | 473 | - (PLCHome) JSON Loopkiller 474 | 475 | ### 1.1.16 (10.08.2022) 476 | 477 | - (PLCHome) https rejectUnauthorized false 478 | 479 | ### 1.1.15 (28.04.2022) 480 | 481 | - (PLCHome) Apple devices cannot open the adapter's config page with Safari, all values are empty 482 | 483 | ### 1.1.14 (26.04.2022) 484 | 485 | - (PLCHome) Restart loop when exception 486 | 487 | ### 1.1.13 (08.04.2022) 488 | 489 | - (PLCHome) total data and history data missing for type inv 490 | 491 | ### 1.1.12 (06.04.2022) 492 | 493 | - (PLCHome) api maintance 494 | 495 | ### 1.1.11 (02.04.2022) 496 | 497 | - (PLCHome) fixed readme 498 | - (PLCHome) fixed version 499 | 500 | ### 1.1.10 (02.04.2022) 501 | 502 | - (PLCHome) suppressed the warning for the Firsttime: /error.do?errorMess=errorNoLogin 503 | 504 | ### 1.1.9 (27.03.2022) 505 | 506 | - (PLCHome) Make the source a little prettier 507 | - (PLCHome) Make the readme prettier 508 | - (PLCHome) Added Test and Release 509 | - (PLCHome) Improvement: used i in inner and outer loop 510 | - (PLCHome) Improvement triggered by "Sentry" issues: undefined object 511 | - (PLCHome) Improvement: no disconnect to "Sentry" 512 | 513 | ### 1.1.8 (16.03.2022) 514 | 515 | - (PLCHome) Improvement triggered by "Sentry" issues 516 | 517 | ### 1.1.7 (13.02.2022) 518 | 519 | - (PLCHome) "Sentry" was added 520 | 521 | ### 1.1.6 (12.02.2022) 522 | 523 | - (PLCHome) Read me 524 | 525 | ### 1.1.2 (12.02.2022) 526 | 527 | - (PLCHome) Timeouts made maintainable and adjusted. Request timout is now 60 second like chrome 528 | - (PLCHome) Server request improved and additionally secured against dying 529 | - (PLCHome) Calculate sleep to next request to keep cycle. Minimum sleep is 100ms 530 | - (PLCHome) Error output: if the key has expired, requests are forwarded with an error code, which is now in the log 531 | - (PLCHome) Improved error handling 532 | - (PLCHome) Improved debugging 533 | - (PLCHome) Update the includes 534 | 535 | ### 1.1.1 (27.05.2021) 536 | 537 | - (PLCHome) The web request timeout was increased due to login issues 538 | 539 | ### 1.1.0 (24.05.2021) 540 | 541 | - (PLCHome) Improvement of the connection via Axios Session 542 | 543 | ### 1.0.1 (05.03.2021) 544 | 545 | - (PLCHome) Duplicate keys are transmitted, I try to filter them out. 546 | 547 | ### 1.0.0 (24.02.2021) 548 | 549 | - (PLCHome) Read me 550 | - (PLCHome) fix: Create a date from the time or calendar structure for last history data for all devices sometimes not working 551 | 552 | ### 0.0.20 (09.02.2021) 553 | 554 | - (PLCHome) Create a date from the time or calendar structure for last history data for all devices 555 | 556 | ### 0.0.19 (05.02.2021) 557 | 558 | - (PLCHome) The data from the chart is removed. These were only available in a 5-minute grid. The performance can now be queried via the history. 559 | - (PLCHome) Objects of unselected data areas are now deleted. 560 | - (PLCHome) You can choose objects to be ignored or deleted. 561 | - (PLCHome) A link to the Growatt page was added, so the adapter now also appears in the overview. 562 | - (PLCHome) Recently, Growatt has changed the spelling of values, which letters are uppercase and lowercase. For this reason, the objects are now handled internally Case Insensively. If a warning is written in the log after the update when starting, you have to delete one of the two objects. If a warning is written in the log after the update when starting, you have to delete one of the two objects. And then restart the adapter so that it definitely uses the remaining object to store the value. 563 | 564 | ### 0.0.18 (23.01.2021) 565 | 566 | - (PLCHome) wrong version info. 567 | 568 | ### 0.0.17 (21.01.2021) 569 | 570 | - (PLCHome) fixes a date issue on inverter history data. 571 | 572 | ### 0.0.16 (20.01.2021) 573 | 574 | - (PLCHome) npm package version update 575 | - (PLCHome) add last history for all plants. Special thanks to magix for the key, so i can test the inverter history function. 576 | 577 | ### 0.0.15 (04.12.2020) 578 | 579 | - (PLCHome) npm package version update 580 | 581 | ### 0.0.14 (01.12.2020) 582 | 583 | - (PLCHome) improvement for objects not returned from Growatt website 584 | 585 | ### 0.0.12 (27.11.2020) 586 | 587 | - (PLCHome) wrong initialization for shared key: string instead of boolean 588 | 589 | ### 0.0.11 (27.11.2020) 590 | 591 | - (PLCHome) Read me 592 | 593 | ### 0.0.10 (26.11.2020) 594 | 595 | - (PLCHome) Shared key login 596 | - (PLCHome) Last value of the graph if there are no live data. 597 | - (PLCHome) Change of the polling interval 598 | 599 | ### 0.0.9 (05.10.2020) 600 | 601 | - (PLCHome) fix no feature 'ADAPTER_AUTO_DECRYPT_NATIVE' 602 | 603 | ### 0.0.8 (05.10.2020) 604 | 605 | - (PLCHome) fix io-package 606 | 607 | ### 0.0.7 (05.10.2020) 608 | 609 | - (PLCHome) with "@iobroker/adapter-core": "^2.4.0", the js-controller dep needs to be >=2.0.0! 610 | - (PLCHome) io-package native defined 5 values, admin sets 7 611 | - (PLCHome) store password encrypted 612 | 613 | ### 0.0.6 (31.08.2020) 614 | 615 | - (PLCHome) translation with ioBroker tool. 616 | 617 | ### 0.0.5 618 | 619 | - (PLCHome) initial release. 620 | 621 | ### 0.0.1 622 | 623 | - (PLCHome) initial release. 624 | 625 | -\*- 626 | 627 | ## License 628 | 629 | The MIT License (MIT) 630 | 631 | Copyright (c) 2024 PLCHome 632 | 633 | Permission is hereby granted, free of charge, to any person obtaining a copy 634 | of this software and associated documentation files (the "Software"), to deal 635 | in the Software without restriction, including without limitation the rights 636 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 637 | copies of the Software, and to permit persons to whom the Software is 638 | furnished to do so, subject to the following conditions: 639 | 640 | The above copyright notice and this permission notice shall be included in all 641 | copies or substantial portions of the Software. 642 | 643 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 644 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 645 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 646 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 647 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 648 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 649 | SOFTWARE. 650 | -------------------------------------------------------------------------------- /admin/glogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCHome/ioBroker.growatt/271c565491a1ba0047070cb4be8d2134d6a2e094/admin/glogo.png -------------------------------------------------------------------------------- /admin/growatt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PLCHome/ioBroker.growatt/271c565491a1ba0047070cb4be8d2134d6a2e094/admin/growatt.png -------------------------------------------------------------------------------- /admin/index_m.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 526 | 527 | 528 | 529 |
530 |
531 |
532 | 533 |
534 |
535 |
536 |
537 | 548 |
549 | 550 |
551 |
552 |
553 | 554 | 555 |
556 |
557 |
558 |
559 | 560 | 561 |
562 |
563 | 564 | 565 |
566 |
567 | 568 | 569 |
570 |
571 |
572 |
573 | 574 | 575 |
576 |
577 | 578 | 579 |
580 |
581 | 582 | 583 |
584 |
585 | 586 | 587 |
588 |
589 | 590 | 591 |
592 |
593 | 594 | 595 |
596 |
597 | 598 | 599 |
600 |
601 | 602 | 603 |
604 |
605 | 606 | 607 |
608 |
609 |
610 |
611 |   612 |
613 |
614 |
615 |
616 | 617 | 618 |
619 |
620 | 621 | 622 |
623 |
624 |
625 |
626 | 627 | 628 |
629 |
630 | 631 | 632 |
633 |
634 |
635 |
636 | 637 | 638 |
639 |
640 | 641 | 642 |
643 |
644 |
645 |
646 | 647 | 648 |
649 |
650 |
651 | 652 |
653 |
654 | 659 |
660 |
661 |
662 |
663 | 664 | 665 | 666 | 667 | 670 | 673 | 676 | 679 | 682 | 685 | 686 | 687 |
668 | Action 669 | 671 | Offset 672 | 674 | ID 675 | 677 | Name 678 | 680 | Type 681 | 683 | Created 684 |
688 |
689 |
690 |
691 |
692 | 693 | 710 |
711 |
712 | 713 | 714 | -------------------------------------------------------------------------------- /admin/style.css: -------------------------------------------------------------------------------- 1 | /* You can delete those if you want. I just found them very helpful */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | .m { 6 | /* Don't cut off dropdowns! */ 7 | overflow: initial; 8 | } 9 | /* Style for small Screens */ 10 | @media screen and (max-width: 768px) { 11 | .m { 12 | overflow: hidden; 13 | } 14 | } 15 | 16 | /* Style for very small Screens */ 17 | @media screen and (max-width: 600px) { 18 | .m .dropdown-menu { 19 | position: fixed; 20 | } 21 | 22 | .m .page { 23 | height: calc(100% - 110px); 24 | } 25 | } 26 | /* Add your styles here */ 27 | -------------------------------------------------------------------------------- /admin/words.js: -------------------------------------------------------------------------------- 1 | /*global systemDictionary:true */ 2 | 'use strict'; 3 | 4 | systemDictionary = { 5 | 'growatt adapter settings': { 6 | en: 'Adapter settings for growatt', 7 | de: 'Adaptereinstellungen für growatt', 8 | ru: 'Настройки адаптера для growatt', 9 | pt: 'Configurações do adaptador para growatt', 10 | nl: 'Adapterinstellingen voor growatt', 11 | fr: "Paramètres d'adaptateur pour growatt", 12 | it: "Impostazioni dell'adattatore per growatt", 13 | es: 'Ajustes del adaptador para growatt', 14 | pl: 'Ustawienia adaptera dla growatt', 15 | 'zh-cn': 'growatt的适配器设置', 16 | }, 17 | 'Login with shared key': { 18 | en: 'Login with shared key', 19 | de: 'Melden Sie sich mit dem freigegebenen Schlüssel an', 20 | ru: 'Войти с общим ключом', 21 | pt: 'Login com chave compartilhada', 22 | nl: 'Login met gedeelde sleutel', 23 | fr: 'Connexion avec clé partagée', 24 | it: 'Accedi con chiave condivisa', 25 | es: 'Iniciar sesión con clave compartida', 26 | pl: 'Zaloguj się za pomocą klucza wspólnego', 27 | 'zh-cn': '使用共享密钥登录', 28 | }, 29 | User: { 30 | en: 'User', 31 | de: 'Benutzer', 32 | ru: 'Пользователь', 33 | pt: 'Do utilizador', 34 | nl: 'Gebruiker', 35 | fr: 'Utilisateur', 36 | it: 'Utente', 37 | es: 'Usuario', 38 | pl: 'Użytkownik', 39 | 'zh-cn': '用户', 40 | }, 41 | Password: { 42 | en: 'Password', 43 | de: 'Passwort', 44 | ru: 'пароль', 45 | pt: 'Senha', 46 | nl: 'Wachtwoord', 47 | fr: 'Mot de passe', 48 | it: "Parola d'ordine", 49 | es: 'Contraseña', 50 | pl: 'Hasło', 51 | 'zh-cn': '密码', 52 | }, 53 | 'The key from shared URL': { 54 | en: 'The key from shared URL', 55 | de: 'Der Schlüssel von der freigegebenen URL', 56 | ru: 'Ключ из общего URL', 57 | pt: 'A chave do URL compartilhado', 58 | nl: 'De sleutel van gedeelde URL', 59 | fr: "La clé de l'URL partagée", 60 | it: "La chiave dall'URL condiviso", 61 | es: 'La clave de la URL compartida', 62 | pl: 'Klucz z udostępnionego adresu URL', 63 | 'zh-cn': '共享URL中的密钥', 64 | }, 65 | 'Read weather': { 66 | en: 'Read weather', 67 | de: 'Wetter lesen', 68 | ru: 'Читать погоду', 69 | pt: 'Leia o tempo', 70 | nl: 'Lees het weer', 71 | fr: 'Lire la météo', 72 | it: 'Leggi il meteo', 73 | es: 'Leer el tiempo', 74 | pl: 'Czytaj pogodę', 75 | 'zh-cn': '阅读天气', 76 | }, 77 | 'Read fault log entries': { 78 | en: 'Read fault log entries', 79 | de: 'Fehlerprotokolleinträge lesen', 80 | ru: 'Запись журнала ошибок', 81 | pt: 'Leia entradas de log de falhas', 82 | nl: 'Foutlog-items lezen', 83 | fr: 'Lire les entrées du journal des défauts', 84 | it: 'Leggi le voci di registro dei guasti', 85 | es: 'Lea las entradas del registro de fallas', 86 | pl: 'Odczytywanie wpisów dziennika błędów', 87 | uk: 'Читання записів журналу несправностей', 88 | 'zh-cn': '读取错误日志条目', 89 | }, 90 | 'Read device data': { 91 | en: 'Read device data', 92 | de: 'Gerätedaten lesen', 93 | ru: 'Прочитать данные устройства', 94 | pt: 'Leia os dados do dispositivo', 95 | nl: 'Lees apparaatgegevens', 96 | fr: "Lire les données de l'appareil", 97 | it: 'Leggi i dati del dispositivo', 98 | es: 'Leer datos del dispositivo', 99 | pl: 'Czytaj dane urządzenia', 100 | 'zh-cn': '读取设备数据', 101 | }, 102 | 'Read total data': { 103 | en: 'Read total data', 104 | de: 'Gesamtdaten lesen', 105 | ru: 'Прочитать общие данные', 106 | pt: 'Leia os dados totais', 107 | nl: 'Lees de totale gegevens', 108 | fr: 'Lire les données totales', 109 | it: 'Leggi i dati totali', 110 | es: 'Leer datos totales', 111 | pl: 'Przeczytaj wszystkie dane', 112 | 'zh-cn': '读取总数据', 113 | }, 114 | 'Read status data (not INV/MAX/TLX)': { 115 | en: 'Read status data (not INV/MAX/TLX)', 116 | de: 'Statusdaten lesen (nicht INV/MAX/TLX)', 117 | ru: 'Считывание данных состояния (не INV/MAX/TLX)', 118 | pt: 'Ler dados de status (não INV/MAX/TLX)', 119 | nl: 'Lees statusgegevens (niet INV/MAX/TLX)', 120 | fr: "Lire les données d'état (pas INV/MAX/TLX)", 121 | it: 'Leggere i dati di stato (non INV/MAX/TLX)', 122 | es: 'Leer datos de estado (no INV/MAX/TLX)', 123 | pl: 'Czytaj dane o stanie (nie INV/MAX/TLX)', 124 | 'zh-cn': '读取状态数据(非INV/MAX/TLX)', 125 | }, 126 | 'Read last history data': { 127 | en: 'Read last history data', 128 | de: 'Lesen Sie die letzten Verlaufsdaten', 129 | ru: 'Прочитать последние данные истории', 130 | pt: 'Leia os últimos dados do histórico', 131 | nl: 'Lees de laatste geschiedenisgegevens', 132 | fr: "Lire les dernières données d'historique", 133 | it: 'Leggi gli ultimi dati della cronologia', 134 | es: 'Leer los últimos datos del historial', 135 | pl: 'Przeczytaj ostatnie dane historyczne', 136 | 'zh-cn': '读取最近的历史数据', 137 | }, 138 | 'Read last data of chart (only INV/MAX/TLX) [deprecated]': { 139 | en: 'Read last data of chart (only INV/MAX/TLX) [deprecated]', 140 | de: 'Letzte Daten des Diagramms lesen (nur INV / MAX / TLX) [veraltet]', 141 | ru: 'Чтение последних данных диаграммы (только INV / MAX / TLX) [устарело]', 142 | pt: 'Ler os últimos dados do gráfico (apenas INV / MAX / TLX) [obsoleto]', 143 | nl: 'Laatste gegevens van diagram lezen (alleen INV / MAX / TLX) [verouderd]', 144 | fr: 'Lire les dernières données du graphique (uniquement INV / MAX / TLX) [obsolète]', 145 | it: 'Leggi gli ultimi dati del grafico (solo INV / MAX / TLX) [obsoleto]', 146 | es: 'Leer los últimos datos del gráfico (solo INV / MAX / TLX) [obsoleto]', 147 | pl: 'Przeczytaj ostatnie dane wykresu (tylko INV / MAX / TLX) [wycofane]', 148 | 'zh-cn': '读取图表的最后数据(仅INV / MAX / TLX)[不建议使用]', 149 | }, 150 | 'Read data of chart (requires read last data of chart) [deprecated]': { 151 | en: 'Read data of chart (requires read last data of chart) [deprecated]', 152 | de: 'Daten des Diagramms lesen (erfordert das Lesen der letzten Daten des Diagramms) [veraltet]', 153 | ru: 'Чтение данных диаграммы (требуется чтение последних данных диаграммы) [устарело]', 154 | pt: 'Ler os dados do gráfico (requer a leitura dos últimos dados do gráfico) [obsoleto]', 155 | nl: 'Gegevens van diagram lezen (vereist laatste gegevens van diagram lezen) [verouderd]', 156 | fr: 'Lire les données du graphique (nécessite la lecture des dernières données du graphique) [obsolète]', 157 | it: 'Leggi i dati del grafico (richiede la lettura degli ultimi dati del grafico) [obsoleto]', 158 | es: 'Leer datos del gráfico (requiere leer los últimos datos del gráfico) [obsoleto]', 159 | pl: 'Odczytaj dane wykresu (wymaga odczytu ostatnich danych wykresu) [przestarzałe]', 160 | 'zh-cn': '读取图表数据(需要读取图表的最后数据)[不建议使用]', 161 | }, 162 | 'Read plant data': { 163 | en: 'Read plant data', 164 | de: 'Anlagendaten lesen', 165 | ru: 'Прочитать данные о заводе', 166 | pt: 'Leia os dados da planta', 167 | nl: 'Lees installatiegegevens', 168 | fr: "Lire les données de l'installation", 169 | it: "Leggere i dati dell'impianto", 170 | es: 'Leer datos de la planta', 171 | pl: 'Przeczytaj dane zakładu', 172 | 'zh-cn': '读取工厂数据', 173 | }, 174 | normal: { 175 | en: 'normal', 176 | de: 'normal', 177 | ru: 'нормальный', 178 | pt: 'normal', 179 | nl: 'normaal', 180 | fr: 'Ordinaire', 181 | it: 'normale', 182 | es: 'normal', 183 | pl: 'normalna', 184 | 'zh-cn': '正常', 185 | }, 186 | delete: { 187 | en: 'delete', 188 | de: 'löschen', 189 | ru: 'Удалить', 190 | pt: 'excluir', 191 | nl: 'verwijderen', 192 | fr: 'effacer', 193 | it: 'Elimina', 194 | es: 'Eliminar', 195 | pl: 'usunąć', 196 | 'zh-cn': '删除', 197 | }, 198 | noupdate: { 199 | en: 'no update', 200 | de: 'kein Update', 201 | ru: 'нет обновления', 202 | pt: 'sem atualização', 203 | nl: 'geen update', 204 | fr: 'Pas de mise à jour', 205 | it: 'nessun aggiornamento', 206 | es: 'ninguna actualización', 207 | pl: 'brak aktualizacji', 208 | 'zh-cn': '没有更新', 209 | }, 210 | 'Main Settings': { 211 | en: 'Main Settings', 212 | de: 'Haupteinstellungen', 213 | ru: 'Основные параметры', 214 | pt: 'Configurações principais', 215 | nl: 'Belangrijkste instellingen', 216 | fr: 'Réglages principaux', 217 | it: 'Impostazioni principali', 218 | es: 'Ajustes principales', 219 | pl: 'Ustawienia główne', 220 | 'zh-cn': '主要设定', 221 | }, 222 | 'Manage Objects': { 223 | en: 'Manage Objects', 224 | de: 'Objekte verwalten', 225 | ru: 'Управлять объектами', 226 | pt: 'Gerenciar objetos', 227 | nl: 'Beheer objecten', 228 | fr: 'Gérer les objets', 229 | it: 'Gestisci oggetti', 230 | es: 'Administrar objetos', 231 | pl: 'Zarządzaj obiektami', 232 | 'zh-cn': '管理物件', 233 | }, 234 | Created: { 235 | en: 'Created', 236 | de: 'Erstellt', 237 | ru: 'Создано', 238 | pt: 'Criado', 239 | nl: 'Gemaakt', 240 | fr: 'Créé', 241 | it: 'Creato', 242 | es: 'Creado', 243 | pl: 'Utworzony', 244 | 'zh-cn': '已建立', 245 | }, 246 | Action: { 247 | en: 'Action', 248 | de: 'Aktion', 249 | ru: 'Действие', 250 | pt: 'Açao', 251 | nl: 'Actie', 252 | fr: 'action', 253 | it: 'Azione', 254 | es: 'Acción', 255 | pl: 'Akcja', 256 | 'zh-cn': '行动', 257 | }, 258 | Name: { 259 | en: 'Name', 260 | de: 'Name', 261 | ru: 'имя', 262 | pt: 'Nome', 263 | nl: 'Naam', 264 | fr: 'Nom', 265 | it: 'Nome', 266 | es: 'Nombre', 267 | pl: 'Nazwa', 268 | 'zh-cn': '名称', 269 | }, 270 | Type: { 271 | en: 'Type', 272 | de: 'Typ', 273 | ru: 'Тип', 274 | pt: 'Tipo', 275 | nl: 'Type', 276 | fr: 'Type', 277 | it: 'genere', 278 | es: 'Tipo', 279 | pl: 'Rodzaj', 280 | 'zh-cn': '类型', 281 | }, 282 | 'Process timeout in seconds': { 283 | en: 'Process timeout in seconds', 284 | de: 'Prozess-Timeout in Sekunden', 285 | ru: 'Время ожидания процесса в секундах', 286 | pt: 'Tempo limite do processo em segundos', 287 | nl: 'Verwerkingstime-out in seconden', 288 | fr: "Délai d'attente du processus en secondes", 289 | it: 'Timeout del processo in secondi', 290 | es: 'Tiempo de espera del proceso en segundos', 291 | pl: 'Limit czasu procesu w sekundach', 292 | 'zh-cn': '处理超时(以秒为单位)', 293 | }, 294 | 'Timeout in seconds': { 295 | en: 'Timeout in seconds', 296 | de: 'Zeitüberschreitung in Sekunden', 297 | ru: 'Время ожидания в секундах', 298 | pt: 'Tempo limite em segundos', 299 | nl: 'Time-out in seconden', 300 | fr: "Délai d'attente en secondes", 301 | it: 'Timeout in secondi', 302 | es: 'Tiempo de espera en segundos', 303 | pl: 'Limit czasu w sekundach', 304 | 'zh-cn': '以秒为单位的超时', 305 | }, 306 | 'Keep web session': { 307 | en: 'Keep web session', 308 | de: 'Websitzung beibehalten', 309 | ru: 'Сохранить веб-сессию', 310 | pt: 'Manter sessão da web', 311 | nl: 'Websessie behouden', 312 | fr: 'Conserver la session Web', 313 | it: 'Mantieni la sessione web', 314 | es: 'Mantener sesión web', 315 | pl: 'Zachowaj sesję internetową', 316 | 'zh-cn': '保持网络会话', 317 | }, 318 | 'Session time in minutes': { 319 | en: 'Session time in minutes', 320 | de: 'Sitzungszeit in Minuten', 321 | ru: 'Время сеанса в минутах', 322 | pt: 'Tempo de sessão em minutos', 323 | nl: 'Sessietijd in minuten', 324 | fr: 'Durée de la session en minutes', 325 | it: 'Durata della sessione in minuti', 326 | es: 'Tiempo de sesión en minutos', 327 | pl: 'Czas sesji w minutach', 328 | 'zh-cn': '会话时间(分钟)', 329 | }, 330 | 'Cycle time in seconds': { 331 | en: 'Cycle time in seconds', 332 | de: 'Zykluszeit in Sekunden', 333 | ru: 'Время цикла в секундах', 334 | pt: 'Tempo de ciclo em segundos', 335 | nl: 'Cyclustijd in seconden', 336 | fr: 'Temps de cycle en secondes', 337 | it: 'Tempo di ciclo in secondi', 338 | es: 'Tiempo de ciclo en segundos', 339 | pl: 'Czas cyklu w sekundach', 340 | 'zh-cn': '以秒为单位的周期时间', 341 | }, 342 | 'Error cycle time in seconds': { 343 | en: 'Error cycle time in seconds', 344 | de: 'Fehlerzykluszeit in Sekunden', 345 | ru: 'Время цикла ошибки в секундах', 346 | pt: 'Tempo de ciclo de erro em segundos', 347 | nl: 'Foutcyclustijd in seconden', 348 | fr: "Temps de cycle d'erreur en secondes", 349 | it: "Tempo di ciclo dell'errore in secondi", 350 | es: 'Tiempo de ciclo de error en segundos', 351 | pl: 'Czas cyklu błędu w sekundach', 352 | 'zh-cn': '错误循环时间(以秒为单位)', 353 | }, 354 | Datalogger: { 355 | en: 'Datalogger', 356 | de: 'Datenlogger', 357 | ru: 'Datalogger', 358 | pt: 'Datalogger', 359 | nl: 'Datalogger', 360 | fr: 'Datalogger', 361 | it: 'Datalogger', 362 | es: 'Datalogger', 363 | pl: 'Datalogger', 364 | 'zh-cn': '数据摘要', 365 | }, 366 | 'Manage Dataloggers': { 367 | en: 'Manage Dataloggers', 368 | de: 'Datenlogger verwalten', 369 | ru: 'Управление Dataloggers', 370 | pt: 'Gerenciar Dataloggers', 371 | nl: 'Manage Dataloggers', 372 | fr: 'Gérer les enregistreurs de données', 373 | it: 'Gestione dei Datalogger', 374 | es: 'Manage Dataloggers', 375 | pl: 'Manage Datalog', 376 | 'zh-cn': 'B. 管理数据目录', 377 | }, 378 | 'PV plant id': { 379 | en: 'PV plant id', 380 | de: 'PV-Anlage', 381 | ru: 'PV завод ид', 382 | pt: 'Planta de PV', 383 | nl: 'PV plant', 384 | fr: 'PV plant id', 385 | it: 'Impianti fotovoltaici', 386 | es: 'PV planta id', 387 | pl: 'Plant id (ang.)', 388 | 'zh-cn': '电厂', 389 | }, 390 | 'PV plant name': { 391 | en: 'PV plant name', 392 | de: 'PV-Anlagenname', 393 | ru: 'PV название растения', 394 | pt: 'Nome da planta PV', 395 | nl: 'PV plant', 396 | fr: 'Nom de la plante PV', 397 | it: 'Nome impianto fotovoltaico', 398 | es: 'Nombre de planta PV', 399 | pl: 'Nazwa rośliny PV', 400 | 'zh-cn': '电厂名称', 401 | }, 402 | 'Account name': { 403 | en: 'Account name', 404 | de: 'Name des Kontos', 405 | ru: 'Имя аккаунта', 406 | pt: 'Nome da conta', 407 | nl: 'Account naam', 408 | fr: 'Nom du compte', 409 | it: 'Nome account', 410 | es: 'Nombre de la cuenta', 411 | pl: 'Nazwa', 412 | 'zh-cn': '名称', 413 | }, 414 | 'Serial number': { 415 | en: 'Serial number', 416 | de: 'Seriennummer', 417 | ru: 'Серийный номер', 418 | pt: 'Número de série', 419 | nl: 'Serienummer', 420 | fr: 'Numéro de série', 421 | it: 'Numero di serie', 422 | es: 'Número de serie', 423 | pl: 'Numer szeregowy', 424 | 'zh-cn': '数量', 425 | }, 426 | Alias: { 427 | en: 'Alias', 428 | de: 'Alias', 429 | ru: 'Алиас', 430 | pt: 'Alias', 431 | nl: 'Alias', 432 | fr: 'Alias', 433 | it: 'Alias', 434 | es: 'Alias', 435 | pl: 'Alias', 436 | 'zh-cn': 'Aliasasas', 437 | }, 438 | 'Firmware version': { 439 | en: 'Firmware version', 440 | de: 'Firmware-Version', 441 | ru: 'Версия прошивки', 442 | pt: 'Versão de firmware', 443 | nl: 'Firmware', 444 | fr: 'Firmware version', 445 | it: 'Versione firmware', 446 | es: 'Versión de firmware', 447 | pl: 'Okładka', 448 | 'zh-cn': '导 言', 449 | }, 450 | 'Device type index': { 451 | en: 'Device type index', 452 | de: 'Gerätetyp Index', 453 | ru: 'Тип устройства индекс', 454 | pt: 'Índice do tipo de dispositivo', 455 | nl: 'Vernietigingstype', 456 | fr: 'Indice de type de dispositif', 457 | it: 'Indice del tipo di dispositivo', 458 | es: 'Índice de tipo de dispositivo', 459 | pl: 'Indeks typowy', 460 | 'zh-cn': '传单', 461 | }, 462 | 'Device type': { 463 | en: 'Device type', 464 | de: 'Gerätetyp', 465 | ru: 'Тип устройства', 466 | pt: 'Tipo de dispositivo', 467 | nl: 'Device type', 468 | fr: 'Type de dispositif', 469 | it: 'Tipo di dispositivo', 470 | es: 'Tipo de dispositivo', 471 | pl: 'Device type', 472 | 'zh-cn': '2. 证人类型', 473 | }, 474 | 'Wifi model': { 475 | en: 'Wifi model', 476 | de: 'Wifi Modell', 477 | ru: 'Модель Wifi', 478 | pt: 'Modelo Wifi', 479 | nl: 'Wifi model', 480 | fr: 'Modèle Wifi', 481 | it: 'Modello Wifi', 482 | es: 'Modelo Wifi', 483 | pl: 'Wifi model', 484 | 'zh-cn': '菲菲菲图', 485 | }, 486 | 'Comunication lost': { 487 | en: 'Comunication lost', 488 | de: 'Komunikation verloren', 489 | ru: 'Взаимодействие потеряно', 490 | pt: 'Comunidade perdida', 491 | nl: 'Comunicatie verloren', 492 | fr: 'Comunication lost', 493 | it: 'Comunicato perso', 494 | es: 'Comunication lost', 495 | pl: 'Comunicja przegrała', 496 | 'zh-cn': '损失', 497 | }, 498 | 'Wifi signal level': { 499 | en: 'Wifi signal level', 500 | de: 'Wifi Signalpegel', 501 | ru: 'Уровень сигнала Wifi', 502 | pt: 'Nível de sinal Wifi', 503 | nl: 'Wifi signaal', 504 | fr: 'Niveau du signal Wifi', 505 | it: 'Livello del segnale Wifi', 506 | es: 'Nivel de señal Wifi', 507 | pl: 'Poziom Wifi', 508 | 'zh-cn': '维菲信号水平', 509 | }, 510 | 'IP and port': { 511 | en: 'IP and port', 512 | de: 'IP und Port', 513 | ru: 'IP и порт', 514 | pt: 'IP e porta', 515 | nl: 'IP en haven', 516 | fr: 'IP et port', 517 | it: 'IP e porta', 518 | es: 'IP y puerto', 519 | pl: 'IP i port', 520 | 'zh-cn': 'IP和港口', 521 | }, 522 | Interval: { 523 | en: 'Interval', 524 | de: 'Intervall', 525 | ru: 'интервал', 526 | pt: 'Intervalo', 527 | nl: 'Interval', 528 | fr: 'Intervalle', 529 | it: 'Intervallo', 530 | es: 'Intervalo', 531 | pl: 'długość', 532 | 'zh-cn': '间隔', 533 | }, 534 | 'Last update timepoint': { 535 | en: 'Last update timepoint', 536 | de: 'Letzter Update-Zeitpunkt', 537 | ru: 'Последнее обновление timepoint', 538 | pt: 'Última atualização', 539 | nl: 'Laatste update', 540 | fr: 'Dernière mise à jour', 541 | it: 'Ultimo aggiornamento', 542 | es: 'Último punto de tiempo de actualización', 543 | pl: 'Czasoprzestrzeń', 544 | 'zh-cn': '上次更新时间', 545 | }, 546 | 'Server ip': { 547 | en: 'Server ip', 548 | de: 'Server ip', 549 | ru: 'Сервер ip', 550 | pt: 'Ip do servidor', 551 | nl: 'Server ip', 552 | fr: 'Server ip', 553 | it: 'Server ip', 554 | es: 'Servidor ip', 555 | pl: 'Serwer', 556 | 'zh-cn': '导 言', 557 | }, 558 | 'Server port': { 559 | en: 'Server port', 560 | de: 'Serverport', 561 | ru: 'Порт сервера', 562 | pt: 'Porta do servidor', 563 | nl: 'Server haven', 564 | fr: 'Port de serveur', 565 | it: 'Porta server', 566 | es: 'Puerto servidor', 567 | pl: 'Porter', 568 | 'zh-cn': '服务港口', 569 | }, 570 | 'Check firmware': { 571 | en: 'Check firmware', 572 | de: 'Firmware überprüfen', 573 | ru: 'Проверить прошивку', 574 | pt: 'Verificar firmware', 575 | nl: 'Controleer firmaware', 576 | fr: 'Vérifiez le firmware', 577 | it: 'Controllare il firmware', 578 | es: 'Comprobar firmware', 579 | pl: 'Oprogramowanie Check', 580 | 'zh-cn': '检查公司', 581 | }, 582 | 'Restart datalogger': { 583 | en: 'Restart datalogger', 584 | de: 'Restart Datenlogger', 585 | ru: 'Перезапустите datalogger', 586 | pt: 'Reinicie o datalogger', 587 | nl: 'Herstart datalog', 588 | fr: 'Restart datalogger', 589 | it: 'Riavviare il datalogger', 590 | es: 'Restart datalogger', 591 | pl: 'Restart datalog', 592 | 'zh-cn': '重开数据', 593 | }, 594 | 'Please enter new interval in minutes': { 595 | en: 'Please enter new interval in minutes', 596 | de: 'Bitte geben Sie ein neues Intervall in Minuten ein', 597 | ru: 'Пожалуйста, введите новый интервал за считанные минуты', 598 | pt: 'Digite um novo intervalo em minutos', 599 | nl: 'Kom binnen enkele minuten een nieuwe interval binnen', 600 | fr: 'Veuillez entrer un nouvel intervalle en quelques minutes', 601 | it: 'Si prega di inserire un nuovo intervallo in minuti', 602 | es: 'Por favor, introduzca el intervalo nuevo en minutos', 603 | pl: 'Przyjmuje się do nowej przerwy', 604 | 'zh-cn': '请以新的分钟内间隔发言', 605 | }, 606 | 'Please enter new IP for server': { 607 | en: 'Please enter new IP for server', 608 | de: 'Bitte geben Sie eine neue IP für den Server ein', 609 | ru: 'Пожалуйста, введите новый IP для сервера', 610 | pt: 'Digite novo IP para servidor', 611 | nl: 'Betreed nieuwe IP voor server', 612 | fr: 'Veuillez saisir une nouvelle IP pour serveur', 613 | it: 'Inserisci il nuovo IP per il server', 614 | es: 'Por favor, introduzca IP nueva para servidor', 615 | pl: 'Wstęp do nowego serwera', 616 | 'zh-cn': '请提供新的服务对象。', 617 | }, 618 | 'Please enter new port for server': { 619 | en: 'Please enter new port for server', 620 | de: 'Bitte geben Sie einen neuen Port für den Server ein', 621 | ru: 'Пожалуйста, введите новый порт для сервера', 622 | pt: 'Digite nova porta para servidor', 623 | nl: 'Kom binnen voor een server', 624 | fr: 'Veuillez entrer un nouveau port pour serveur', 625 | it: 'Inserisci una nuova porta per il server', 626 | es: 'Por favor introduzca nuevo puerto para servidor', 627 | pl: 'Wstęp do nowego portu dla serwera', 628 | 'zh-cn': '请进入新的服务器港口', 629 | }, 630 | "The server's response": { 631 | en: "The server's response", 632 | de: 'Antwort des Servers', 633 | ru: 'Ответ сервера', 634 | pt: 'Resposta do servidor', 635 | nl: 'De reactie van de server', 636 | fr: 'Réponse du serveur', 637 | it: 'La risposta del server', 638 | es: 'Respuesta del servidor', 639 | pl: 'Odpowiedź serwera', 640 | 'zh-cn': '服务器的答复', 641 | }, 642 | 'The server did not respond to the request!': { 643 | en: 'The server did not respond to the request!', 644 | de: 'Der Server reagierte nicht auf die Anfrage!', 645 | ru: 'Сервер не ответил на запрос!', 646 | pt: 'O servidor não respondeu à solicitação!', 647 | nl: 'De server reageerde niet op het verzoek!', 648 | fr: "Le serveur n'a pas répondu à la demande !", 649 | it: 'Il server non ha risposto alla richiesta!', 650 | es: '¡El servidor no respondió a la solicitud!', 651 | pl: 'Serwer nie odpowiedział na prośbę!', 652 | 'zh-cn': '服务器没有对请求作出反应!', 653 | }, 654 | 'The instance must be running and logged in to the server. Then the settings of the data logger can be called up via the refresh button in this tab.': 655 | { 656 | en: 'The instance must be running and logged in to the server. Then the settings of the data logger can be called up via the refresh button in this tab.', 657 | de: 'Die Instanz muss laufen und am Server eingeloggt sein. Dann können die Einstellungen des Datenloggers über den Refresh-Button in dieser Registerkarte aufgerufen werden.', 658 | ru: 'Например, необходимо запустить и войти на сервер. Затем настройки логгера данных можно назвать через кнопку обновления в этой вкладке.', 659 | pt: 'A instância deve ser executada e registrada no servidor. Em seguida, as configurações do registrador de dados podem ser chamadas através do botão de atualização nesta guia.', 660 | nl: 'De instance moet worden gerend en ingelogd bij de server. Dan kunnen de gegevens logger worden opgeroepen via de verfrissende knop in deze rekening.', 661 | fr: "L'instance doit être activée et connectée au serveur. Ensuite, les paramètres du journal de données peuvent être appelés via le bouton de rafraîchissement dans cet onglet.", 662 | it: "L'istanza deve essere in esecuzione e l'accesso al server. Quindi le impostazioni del data logger possono essere richiamate tramite il pulsante di aggiornamento in questa scheda.", 663 | es: 'La instancia debe estar corriendo y conectado al servidor. A continuación, la configuración del registrador de datos se puede llamar a través del botón de actualización en esta pestaña.', 664 | pl: 'Przykład musi być uruchomiony i wylogowany w serwerze. Wtedy ustawienia loggera danych mogą być nazywane przez odświeżoną przycisk w tabeli.', 665 | 'zh-cn': '例子必须是向服务器操作和标志。 数据标志的环境中可以通过这一表格的制冷剂来予以命名。.', 666 | }, 667 | Offset: { 668 | en: 'Offset', 669 | de: 'Offset', 670 | ru: 'Офсет', 671 | pt: 'Desligado', 672 | nl: 'Vertaling:', 673 | fr: 'Offset', 674 | it: 'Offset', 675 | es: 'Offset', 676 | pl: 'Offset', 677 | 'zh-cn': 'B. 排 排 排', 678 | }, 679 | ID: { 680 | en: 'ID', 681 | de: 'ID', 682 | ru: 'КОД', 683 | pt: 'ID', 684 | nl: 'ID', 685 | fr: 'ID', 686 | it: 'ID', 687 | es: 'ID', 688 | pl: 'ID', 689 | 'zh-cn': 'ID ID', 690 | }, 691 | success: { 692 | en: 'success', 693 | de: 'erfolg', 694 | ru: 'успех', 695 | pt: 'sucesso', 696 | nl: 'succes', 697 | fr: 'succès', 698 | it: 'successo', 699 | es: 'éxito', 700 | pl: 'sukces', 701 | 'zh-cn': '成功', 702 | }, 703 | 'no success': { 704 | en: 'no success', 705 | de: 'kein erfolg', 706 | ru: 'без успеха', 707 | pt: 'sem sucesso', 708 | nl: 'geen succes', 709 | fr: 'pas de succès', 710 | it: 'nessun successo', 711 | es: 'sin éxito', 712 | pl: 'bez powodzenia', 713 | 'zh-cn': '没有成功', 714 | }, 715 | 'Growatt server default https://server.growatt.com': { 716 | en: 'Growatt server default https://server.growatt.com', 717 | de: 'Growatt Server Standard https://server.growatt.com', 718 | ru: 'Growatt сервер по умолчанию https://server.growatt.com', 719 | pt: 'Padrão do servidor Growatt https://server.growatt.com', 720 | nl: 'Growatt server default https://server.growatt.com', 721 | fr: 'Par défaut de serveur Growatt https://server.growatt.com', 722 | it: 'Predefinito server https://server.growatt.com', 723 | es: 'Por defecto del servidor Growatt https://server.growatt.com', 724 | pl: 'Serwer Growatny https://server.growatt.com', 725 | 'zh-cn': 'Growatt服务器 https://server.growatt.com', 726 | }, 727 | 'Write inverter settings (only mix/tlx/tlxh)': { 728 | en: 'Write inverter settings (only mix/tlx/tlxh)', 729 | de: 'Invertereinstellungen schreiben (nur mix/tlx/tlxh)', 730 | ru: 'Напишите настройки инвертора (только mix/tlx/tlxh)', 731 | pt: 'Escreva configurações do inversor (somente mistura/tlx/tlxh)', 732 | nl: 'Schrijf inverter settings (only mix/tlx/tlxh)', 733 | fr: "Écrire des paramètres d'invertisseur (seulement mix/tlx/tlxh)", 734 | it: 'Scrivi impostazioni inverter (solo mix/tlx/tlxh)', 735 | es: 'Escribir configuración de inversor (sólo mix/tlx/tlxh)', 736 | pl: 'Write inverter (mix/tlx/tlxh)', 737 | uk: 'Настроювання інвертора (mix/tlx/tlxh)', 738 | 'zh-cn': '不同环境的标志(mix/tlx/tlxh)', 739 | }, 740 | 'Reading and writing the inverter settings is at your own risk. It can damage the inverter. The functions have been tested to the best of our knowledge.': 741 | { 742 | en: 'Reading and writing the inverter settings is at your own risk. It can damage the inverter. The functions have been tested to the best of our knowledge.', 743 | de: 'Das Lesen und Schreiben der Wechselrichtereinstellungen erfolgt auf eigene Gefahr. Es kann den Wechselrichter beschädigen. Die Funktionen wurden nach bestem Wissen getestet.', 744 | ru: 'Чтение и написание настроек инвертора на вашем собственном риске. Он может повредить инвертор. Функции были проверены на лучшее из наших знаний.', 745 | pt: 'Ler e escrever as configurações do inversor está em seu próprio risco. Pode danificar o inversor. As funções foram testadas para o melhor de nosso conhecimento.', 746 | nl: 'Lezen en schrijven van de inverter settings is in je eigen risico. Het kan de inverter schaden. De functies zijn getest op het beste van onze kennis.', 747 | fr: "La lecture et l'écriture des paramètres de l'inverté sont à votre propre risque. Il peut endommager l'invertateur. Les fonctions ont été testées au mieux de nos connaissances.", 748 | it: "Leggere e scrivere le impostazioni dell'inverter è a proprio rischio. Può danneggiare l'inverter. Le funzioni sono state testate al meglio delle nostre conoscenze.", 749 | es: 'Leer y escribir la configuración del inversor está en su propio riesgo. Puede dañar el inversor. Las funciones han sido probadas al mejor de nuestros conocimientos.', 750 | pl: 'Czytanie i pisanie ustawienia inwerterów znajduje się na własnym ryzyku. Może uszkodzić inwersatora. Funkcje te zostały przetestowane na najlepszą wiedzę.', 751 | uk: 'Читання та написання параметрів інвертора на власний ризик. Це може пошкодити інвертор. Наші функції протестовані в кращих знаннях.', 752 | 'zh-cn': '阅读和写信的环境中面临你本身的风险。 它会破坏多样性。 这些职能是对我们知识进行最佳测试的。.', 753 | }, 754 | 'Select it if your Growatt page is a black C and I page': { 755 | en: 'Select it if your Growatt page is a black C&I page', 756 | de: 'Wählen Sie es aus, wenn Ihre Growatt-Seite eine schwarze C&I-Seite ist', 757 | ru: 'Выберите его, если ваша страница Growatt является черной C & I страницы', 758 | pt: 'Selecioná-lo se sua página Growatt é uma página C&I preta', 759 | nl: 'Verkoop het als je Growat pagina een zwarte pagina is', 760 | fr: 'Sélectionnez-le si votre page Growatt est une page Cautorisation noire', 761 | it: 'Selezionalo se la pagina Growatt è una pagina C&I nera', 762 | es: 'Seleccione si su página de Growatt es una página de CENTE negro', 763 | pl: 'Wybiera go, jeśli twój profil jest czarnym stroną C&I', 764 | uk: 'Виберіть його, якщо ваша сторінка Growatt є чорною C&I сторінкою', 765 | 'zh-cn': '如果你的Growatt页是黑白的C&I页。', 766 | }, 767 | }; 768 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import js from '@eslint/js'; 5 | import { FlatCompat } from '@eslint/eslintrc'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }); 14 | 15 | export default [ 16 | { 17 | ignores: ['**/admin', '**/test', 'lib/env/tools.js', '**/gulpfile.js', 'eslint.config.mjs'], 18 | }, 19 | ...compat.extends('eslint:recommended', 'plugin:prettier/recommended'), 20 | { 21 | languageOptions: { 22 | globals: { 23 | ...globals.browser, 24 | ...globals.node, 25 | ...globals.mocha, 26 | }, 27 | 28 | ecmaVersion: 2018, 29 | sourceType: 'module', 30 | 31 | parserOptions: { 32 | ecmaFeatures: { 33 | impliedStrict: true, 34 | }, 35 | }, 36 | }, 37 | 38 | rules: { 39 | 'no-use-before-define': [ 40 | 'error', 41 | { 42 | functions: false, 43 | }, 44 | ], 45 | 46 | 'no-continue': 'off', 47 | 48 | 'no-param-reassign': [ 49 | 'error', 50 | { 51 | props: false, 52 | }, 53 | ], 54 | }, 55 | }, 56 | ]; 57 | -------------------------------------------------------------------------------- /growattMain.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created with @iobroker/create-adapter v1.26.0 3 | */ 4 | 5 | // The adapter-core module gives you access to the core ioBroker functions 6 | // you need to create an adapter 7 | const API = require('growatt'); 8 | const utils = require('@iobroker/adapter-core'); 9 | 10 | const growartyp = { 11 | INUM_0_100: { type: 'number', role: 'value', min: 0, max: 100, step: 1, read: true, write: true }, 12 | INUM_0_24: { type: 'number', role: 'value', min: 0, max: 24, step: 1, read: true, write: true }, 13 | INUM_0_60: { type: 'number', role: 'value', min: 0, max: 60, step: 1, read: true, write: true }, 14 | BOOL: { type: 'boolean', role: 'value', read: true, write: true }, 15 | STIME_H_MIN: { type: 'string', role: 'value', read: true, write: true }, 16 | DATETIME: { type: 'number', role: 'value.time', read: true, write: true }, 17 | INUM_0_1: { type: 'number', role: 'value', min: 0, max: 1, step: 1, read: true, write: true }, 18 | INUM_0_2: { type: 'number', role: 'value', min: 0, max: 2, step: 1, read: true, write: true }, 19 | }; 20 | const SETTINGS = 'settings'; 21 | 22 | // Load your modules here, e.g.: 23 | // const fs = require("fs"); 24 | 25 | function getTime() { 26 | return new Date().getTime(); 27 | } 28 | function getTimeDiff(start) { 29 | return getTime() - start; 30 | } 31 | const getJSONCircularReplacer = () => { 32 | const seen = new WeakMap(); 33 | return (key, val) => { 34 | const value = val; 35 | if (typeof value === 'object' && value !== null) { 36 | if (seen.has(value)) { 37 | return `loop on ${seen.get(value)}`; 38 | } 39 | seen.set(value, key); 40 | } 41 | return value; 42 | }; 43 | }; 44 | 45 | /** 46 | * Is called to decrypt the Password 47 | * @param {key} the secret 48 | * @param {value} the encrypted password 49 | * */ 50 | function decrypt(key, value) { 51 | let result = ''; 52 | for (let i = 0; i < value.length; i += 1) { 53 | result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); 54 | } 55 | return result; 56 | } 57 | 58 | class Growatt extends utils.Adapter { 59 | /** 60 | * @param {Partial} [options={}] 61 | */ 62 | constructor(options) { 63 | super({ 64 | ...options, 65 | name: 'growatt', 66 | }); 67 | this.callTimeout = null; 68 | this.processTimeout = null; 69 | this.objNames = {}; 70 | this.on('ready', this.onReady.bind(this)); 71 | this.on('stateChange', this.onStateChange.bind(this)); 72 | this.on('unload', this.onUnload.bind(this)); 73 | this.on('message', this.onMessage.bind(this)); 74 | } 75 | 76 | /** 77 | * Is called when databases are connected and adapter received configuration. 78 | */ 79 | async onReady() { 80 | this.getForeignObject('system.config', (errFO, obj) => { 81 | this.config.objUpdate = this.config.objUpdate || {}; 82 | this.config.objOffset = this.config.objOffset || {}; 83 | // ! for stateChange 84 | this.subscribeStates('*.read'); 85 | this.subscribeStates('*.write'); 86 | 87 | if (!this.supportsFeature || !this.supportsFeature('ADAPTER_AUTO_DECRYPT_NATIVE')) { 88 | if (obj && obj.native && obj.native.secret) { 89 | this.config.password = decrypt(obj.native.secret, this.config.password); 90 | this.config.shareKey = decrypt(obj.native.secret, this.config.shareKey); 91 | } else { 92 | this.config.password = decrypt('Zgfr56gFe87jJOM', this.config.password); 93 | this.config.shareKey = decrypt('Zgfr56gFe87jJOM', this.config.shareKey); 94 | } 95 | } 96 | 97 | if (typeof this.config.webTimeout === 'undefined' || this.config.webTimeout === '') this.config.webTimeout = 60; 98 | if (typeof this.config.processTimeout === 'undefined' || this.config.processTimeout === '') this.config.processTimeout = 600; 99 | if (typeof this.config.sessionHold === 'undefined' || this.config.sessionHold === '') this.config.sessionHold = true; 100 | if (typeof this.config.sessionTime === 'undefined' || this.config.sessionTime === '') this.config.sessionTime = 0; 101 | if (typeof this.config.cycleTime === 'undefined' || this.config.cycleTime === '') this.config.cycleTime = 30; 102 | if (typeof this.config.errorCycleTime === 'undefined' || this.config.errorCycleTime === '') this.config.errorCycleTime = 120; 103 | if (typeof this.config.indexCandI === 'undefined' || this.config.indexCandI === '') this.config.indexCandI = false; 104 | 105 | this.getStates(`${this.name}.${this.instance}.*`, (errGS, states) => { 106 | Object.keys(states).forEach(id => { 107 | const ebene = id.toString().split('.'); 108 | ebene.shift(); 109 | ebene.shift(); 110 | if (ebene[0] !== 'info' && ebene[3] !== SETTINGS && ebene.length > 1) { 111 | const ownID = ebene.join('.'); 112 | const ownIDsearch = ownID.toLowerCase(); 113 | if (this.config.objUpdate[ownIDsearch] && this.config.objUpdate[ownIDsearch].action === 'delete') { 114 | this.delObject(ownID); 115 | this.log.info(`deleted: ${ownID}`); 116 | } else if ( 117 | (!this.config.weather && ebene.length > 1 && ebene[1].toLowerCase() === 'weather') || 118 | (!this.config.faultlog && ebene.length > 1 && ebene[1].toLowerCase() === 'faultlog') || 119 | (!this.config.totalData && ebene.length > 3 && ebene[3].toLowerCase() === 'totaldata') || 120 | (!this.config.statusData && ebene.length > 3 && ebene[3].toLowerCase() === 'statusdata') || 121 | (!this.config.plantData && ebene.length > 1 && ebene[1].toLowerCase() === 'plantdata') || 122 | (!this.config.deviceData && ebene.length > 3 && ebene[3].toLowerCase() === 'devicedata') || 123 | (!this.config.historyLast && ebene.length > 3 && ebene[3].toLowerCase() === 'historylast') || 124 | (!this.config.chartLast && ebene.length > 3 && ebene[3].toLowerCase() === 'chart') 125 | ) { 126 | this.delObject(ownID); 127 | this.log.info(`deleted: ${ownID}`); 128 | } else if (this.objNames[ownIDsearch]) { 129 | this.log.warn(`${this.objNames[ownIDsearch]} exists twice: ${ownID}`); 130 | } else if ( 131 | ebene.length > 5 && 132 | ebene[3].toLowerCase() === 'historylast' && 133 | (ebene[4] === 'calendar' || ebene[4] === 'time') && 134 | (ebene[5] === 'year' || 135 | ebene[5] === 'month' || 136 | ebene[5] === 'dayOfMonth' || 137 | ebene[5] === 'hourOfDay' || 138 | ebene[5] === 'minute' || 139 | ebene[5] === 'second') 140 | ) { 141 | this.delObject(ownID); 142 | this.log.info(`deleted: ${ownID}`); 143 | } else { 144 | this.objNames[ownIDsearch] = ownID; 145 | } 146 | } else if (!this.config.settings && ebene.length > 1 && ebene[3] === SETTINGS) { 147 | const ownID = ebene.join('.'); 148 | this.delObject(ownID); 149 | this.log.info(`deleted: ${ownID}`); 150 | } 151 | }); 152 | this.callRun = true; 153 | this.growattData(); 154 | }); 155 | }); 156 | } 157 | 158 | /** 159 | * Is called when adapter shuts down - callback has to be called under any circumstances! 160 | * @param {() => void} callback 161 | */ 162 | onUnload(callback) { 163 | try { 164 | this.callRun = false; 165 | clearTimeout(this.processTimeout); 166 | clearTimeout(this.callTimeout); 167 | this.growattLogout(); 168 | this.setState('info.connection', { val: false, ack: true }); 169 | 170 | callback(); 171 | // eslint-disable-next-line no-unused-vars 172 | } catch (e) { 173 | callback(); 174 | } 175 | } 176 | 177 | /** 178 | * Parses the data from the website into objects. Is called recrusively. 179 | * @param {object} plantData 180 | * @param {path} path to object 181 | * @param {key} the key in the object 182 | */ 183 | async storeData(plantData, path, key) { 184 | const ele = path + key; 185 | const eleSearch = ele.toLowerCase(); 186 | this.log.silly(`storeData for ${ele}`); 187 | let data = plantData[key]; 188 | if (typeof data === 'object') { 189 | this.parseData(data, `${ele}.`); 190 | } else { 191 | if (!(typeof this.config.objUpdate[eleSearch] === 'undefined') && this.config.objUpdate[eleSearch].action !== 'normal') { 192 | return; 193 | } 194 | let objType = 'string'; 195 | let objRole = 'value'; 196 | if (key.toLowerCase().includes('name'.toLowerCase())) { 197 | data = data.toString(); 198 | } 199 | if (typeof data === 'number') { 200 | objType = 'number'; 201 | } else { 202 | data = data.toString(); 203 | // Date: yyyy-mm-dd hh:mi:ss 204 | if ( 205 | data.match('^\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d$') || 206 | data.match('^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d\\d\\dZ$') 207 | ) { 208 | data = new Date(data).getTime(); 209 | objType = 'number'; 210 | objRole = 'value.time'; 211 | // Date: yyyy-mm-dd hh:mi 212 | } else if (data.match('^\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d$')) { 213 | data = new Date(`${data}:00`).getTime(); 214 | objType = 'number'; 215 | objRole = 'value.time'; 216 | // Date: yyyy-mm-dd 217 | } else if (data.match('^\\d\\d\\d\\d-\\d\\d-\\d\\d$')) { 218 | data = new Date(data).getTime(); 219 | objType = 'number'; 220 | objRole = 'date'; 221 | // number: -123 or +123.45 222 | } else if (data.match('^(\\+|\\-)?\\d+(\\.\\d*)?$')) { 223 | data = parseFloat(data); 224 | objType = 'number'; 225 | // json: {...} or [...] 226 | } else if (data.match('^({.*}|\\[.*\\])$')) { 227 | objRole = 'json'; 228 | // boolean: true or false 229 | } else if (data.match('^(true)|(false)$')) { 230 | data = data === 'true'; 231 | objType = 'boolean'; 232 | } 233 | } 234 | if (objType === 'number' && !(typeof this.config.objOffset[eleSearch] === 'undefined') && this.config.objOffset[eleSearch].offset) { 235 | data += this.config.objOffset[eleSearch].offset; 236 | } 237 | if (typeof this.objNames[eleSearch] === 'undefined') { 238 | this.log.silly(`Create object not exists ${ele} type:${objType} role:${objRole}`); 239 | await this.setObjectNotExistsAsync(ele, { 240 | type: 'state', 241 | common: { 242 | name: key, 243 | type: objType, 244 | role: objRole, 245 | read: true, 246 | write: false, 247 | }, 248 | native: {}, 249 | }).catch(e => { 250 | this.log.error(`setObjectNotExists:${e}`); 251 | }); 252 | this.log.info(`added: ${ele}`); 253 | this.objNames[eleSearch] = ele; 254 | } 255 | this.log.silly(`Set value ${this.objNames[eleSearch]} type ${objType} : ${data}`); 256 | this.setState(this.objNames[eleSearch], { val: data, ack: true }); 257 | } 258 | } 259 | 260 | /** 261 | * 262 | loads the settings of the inverter and pastes the settings. 263 | * @param {string} path to id 264 | * @param {string} growattType 265 | * @param {string} setting 266 | * @param {string} sn 267 | */ 268 | async readSetting(path, growattType, setting, sn) { 269 | if (this.growatt) { 270 | this.growatt 271 | .getInverterSetting(growattType, setting, sn) 272 | .then(r => { 273 | this.log.debug(`Read inverter setting ${setting} : ${JSON.stringify(r, getJSONCircularReplacer())}`); 274 | if (r.success) { 275 | const params = Object.keys(r); 276 | params.forEach(p => { 277 | if (p.startsWith('param')) { 278 | this.setState(`${path}.values.${p}`, { val: r[p], ack: true }); 279 | } 280 | }); 281 | } 282 | this.setState(`${path}.read`, { val: r.success, ack: true }); 283 | }) 284 | .catch(e => { 285 | this.log.warn(`Read inverter settings ${setting}:${e}`); 286 | }); 287 | } 288 | } 289 | 290 | /** 291 | * 292 | writes the settings to the inverter. 293 | * @param {string} path to id 294 | * @param {string} growattType 295 | * @param {string} setting 296 | * @param {string} sn 297 | * @param {object param} set 298 | */ 299 | async writeSetting(path, growattType, setting, sn, set) { 300 | if (this.growatt && set && set.param) { 301 | const runState = []; 302 | const values = {}; 303 | const paramKeys = Object.keys(set.param); 304 | paramKeys.forEach(param => { 305 | runState.push( 306 | this.getStateAsync(`${path}.values.${param}`).then(s => { 307 | values[param] = s.val; 308 | }) 309 | ); 310 | }); 311 | await Promise.all(runState); 312 | this.growatt 313 | .setInverterSetting(growattType, setting, sn, values) 314 | .then(a => { 315 | this.setState(`${path}.write`, { val: a.success, ack: true }); 316 | this.setState(`${path}.msg`, { val: a.msg, ack: true }); 317 | this.log.debug(`${typeof a === 'object' ? JSON.stringify(a, getJSONCircularReplacer()) : a}`); 318 | if (a.success) { 319 | this.readSetting(path, growattType, setting, sn); 320 | } 321 | }) 322 | .catch(e => { 323 | this.setState(`${path}.write`, { val: false, ack: true }); 324 | this.setState(`${path}.msg`, { val: `${e}`, ack: true }); 325 | }); 326 | this.log.debug(`write inverter setting ${growattType}, ${setting}, ${sn}, ${JSON.stringify(values, getJSONCircularReplacer())}`); 327 | } 328 | } 329 | 330 | /** 331 | * Called when a subscribed status changes 332 | * @param {string} id 333 | * @param {ioBroker.State | null | undefined} state 334 | */ 335 | async onStateChange(id, state) { 336 | if (state) { 337 | if (!state.ack && state.val === true) { 338 | const obj = await this.getObjectAsync(id); 339 | const splitid = id.split('.'); 340 | splitid.pop(); 341 | const path = splitid.join('.'); 342 | if (obj.native && obj.native.action === 'read') { 343 | this.readSetting(path, obj.native.growattType, obj.native.setting, obj.native.sn); 344 | if (obj.native.set && obj.native.set.subRead) { 345 | obj.native.set.subRead.forEach(read => { 346 | this.readSetting(path, obj.native.growattType, read, obj.native.sn); 347 | }); 348 | } 349 | } else if (obj.native && obj.native.action === 'write') { 350 | this.writeSetting(path, obj.native.growattType, obj.native.setting, obj.native.sn, obj.native.set); 351 | } 352 | } 353 | } 354 | } 355 | 356 | /** 357 | * 358 | loads the settings of the inverter and pastes the settings. 359 | * @param {object} plantData 360 | */ 361 | async loadSettings(plantDatas) { 362 | /** 363 | Creates an iobroker state or update the properties 364 | * @param {this} t 365 | * @param {string} ele 366 | * @param {string} name 367 | * @param {object} common 368 | * @param {any} def 369 | * @param {object} native 370 | */ 371 | function createS(t, ele, name, common, def, native) { 372 | t.log.silly(`Create object not exists ${ele} type:${common} def:${def} native:${native}`); 373 | const o = { 374 | type: 'state', 375 | common: { 376 | name, 377 | }, 378 | native: {}, 379 | }; 380 | Object.assign(o.common, common); 381 | if (typeof def !== 'undefined' && def !== null) { 382 | o.common.def = def; 383 | } 384 | if (typeof native !== 'undefined' && native !== null) { 385 | Object.assign(o.native, native); 386 | } 387 | t.setObjectNotExists(ele, o); 388 | t.setObject(ele, o); 389 | } 390 | /** 391 | Creates an iobroker channel or update the properties 392 | * @param {this} t 393 | * @param {string} ele 394 | * @param {string} name 395 | */ 396 | function createC(t, ele, name) { 397 | t.log.silly(`Create object not exists ${ele} `); 398 | const o = { 399 | type: 'channel', 400 | common: { 401 | name, 402 | }, 403 | native: {}, 404 | }; 405 | t.setObjectNotExists(ele, o); 406 | t.setObject(ele, o); 407 | } 408 | if (plantDatas) { 409 | const plantDataKeys = Object.keys(plantDatas); 410 | plantDataKeys.forEach(plantDataKey => { 411 | const plantData = plantDatas[plantDataKey]; 412 | if (plantData.devices) { 413 | const snKeys = Object.keys(plantData.devices); 414 | if (snKeys) { 415 | snKeys.forEach(sn => { 416 | if (plantData.devices[sn].growattType) { 417 | const { growattType } = plantData.devices[sn]; 418 | const path = `${plantDataKey}.devices.${sn}.${SETTINGS}.`; 419 | if (this.growatt) { 420 | const com = this.growatt.getInverterCommunication(growattType); 421 | if (com) { 422 | const sets = Object.keys(com); 423 | sets.forEach(setting => { 424 | const set = com[setting]; 425 | this.log.silly(`getInverterCommunication ${path} answers ${setting} ${JSON.stringify(set, getJSONCircularReplacer())}`); 426 | if (!set.isSubread) { 427 | createC(this, path + setting, set.name); 428 | createS( 429 | this, 430 | `${path + setting}.write`, 431 | 'Write to the inverter', 432 | { type: 'boolean', role: 'value', read: true, write: true }, 433 | false, 434 | { 435 | set, 436 | sn, 437 | growattType, 438 | setting, 439 | action: 'write', 440 | } 441 | ); 442 | createS( 443 | this, 444 | `${path + setting}.read`, 445 | 'read from the inverter', 446 | { type: 'boolean', role: 'value', read: true, write: true }, 447 | false, 448 | { 449 | set, 450 | sn, 451 | growattType, 452 | setting, 453 | action: 'read', 454 | } 455 | ); 456 | createS(this, `${path + setting}.msg`, 'answer for write from the inverter', { 457 | type: 'string', 458 | role: 'value', 459 | read: true, 460 | write: false, 461 | }); 462 | const paramKeys = Object.keys(set.param); 463 | paramKeys.forEach(param => { 464 | const p = set.param[param]; 465 | const t = growartyp[p.type]; 466 | if (t) { 467 | if (p.values) { 468 | t.states = {}; 469 | Object.assign(t.states, p.values); 470 | } 471 | if (p.unit) { 472 | t.unit = p.unit; 473 | } 474 | createS(this, `${path + setting}.values.${param}`, p.name, t, p.def); 475 | } 476 | }); 477 | this.readSetting(path + setting, growattType, setting, sn); 478 | } else { 479 | this.readSetting(path + set.isSubread, growattType, setting, sn); 480 | } 481 | }); 482 | } 483 | } 484 | } 485 | }); 486 | } 487 | } 488 | }); 489 | } 490 | } 491 | 492 | /** 493 | * Parses the data from the website into objects. Is called recrusively. 494 | * @param {object} plantData 495 | * @param {path} path to object 496 | */ 497 | async parseData(plantData, path) { 498 | if (plantData) { 499 | const keys = Object.keys(plantData); 500 | // Duplicate keys are transmitted, we try to filter them here. 501 | const processed = {}; 502 | keys.forEach(key => { 503 | if (typeof processed[key.toLowerCase()] === 'undefined') { 504 | processed[key.toLowerCase()] = true; 505 | this.storeData(plantData, path, key); 506 | } 507 | }); 508 | } 509 | } 510 | 511 | /** 512 | * Is Called to get Data 513 | * @param {bool} ndel no delete 514 | */ 515 | async growattLogout(ndel) { 516 | if (this.log && this.log.debug) this.log.debug('Enter growattLogout'); 517 | const allTimeDiff = getTime(); 518 | delete this.connectTime; 519 | const { growatt } = this; 520 | if (!ndel) delete this.growatt; 521 | if (typeof growatt !== 'undefined') { 522 | if (growatt.isConnected()) { 523 | await growatt.logout().catch({}); 524 | } 525 | } 526 | if (this.log && this.log.debug) this.log.debug(`Leave growattLogout :${getTimeDiff(allTimeDiff)}ms`); 527 | } 528 | 529 | /** 530 | * Is Called to get a lifesign 531 | */ 532 | lifeSignCallback() { 533 | this.log.debug(`Enter lifeSignCallback ${this.config.processTimeout * 1000}ms`); 534 | clearTimeout(this.processTimeout); 535 | if (this.callRun && this.config.processTimeout && this.config.processTimeout > 0) { 536 | this.processTimeout = setTimeout(() => { 537 | this.growattLogout(true); 538 | this.log.warn('Process timeout reached'); 539 | if (this.callRun) { 540 | clearTimeout(this.callTimeout); 541 | this.callTimeout = setTimeout(() => { 542 | this.growattData(); 543 | }, this.config.errorCycleTime * 1000); 544 | } 545 | }, this.config.processTimeout * 1000); 546 | } 547 | } 548 | 549 | /** 550 | * Is Called to get Data 551 | */ 552 | async growattData() { 553 | this.log.debug(`Enter growattData, Param: sessionHold:${this.config.sessionHold}`); 554 | const allTimeDiff = getTime(); 555 | let debugTimeDiff = getTime(); 556 | let afterConnect = false; 557 | let timeout = this.config.errorCycleTime * 1000; 558 | this.lifeSignCallback(); 559 | try { 560 | if (typeof this.growatt === 'undefined') { 561 | this.log.debug('Growatt new API'); 562 | 563 | this.growatt = new API({ 564 | timeout: this.config.webTimeout * 1000, 565 | lifeSignCallback: this.lifeSignCallback.bind(this), 566 | server: this.config.growattServer || '', 567 | indexCandI: this.config.indexCandI, 568 | }); 569 | } 570 | this.log.debug(`Growatt isConnected() : ${this.growatt.isConnected()}`); 571 | if (!this.growatt.isConnected()) { 572 | afterConnect = true; 573 | if (this.config.keyLogin) { 574 | this.log.debug('Growatt share plant login'); 575 | await this.growatt.sharePlantLogin(this.config.shareKey).catch(e => { 576 | this.log.warn(`Login to share plant:${typeof e === 'object' ? JSON.stringify(e, getJSONCircularReplacer()) : e}`); 577 | }); 578 | } else { 579 | this.log.debug('Growatt login with user and password'); 580 | await this.growatt.login(this.config.user, this.config.password).catch(e => { 581 | this.log.warn(`Login:${typeof e === 'object' ? JSON.stringify(e, getJSONCircularReplacer()) : e}`); 582 | }); 583 | } 584 | this.log.debug(`Growatt isConnected() : ${this.growatt.isConnected()}`); 585 | if (this.growatt.isConnected() && this.config.sessionHold) { 586 | this.connectTime = getTime(); 587 | } 588 | this.log.debug(`Growatt time for login : ${getTimeDiff(debugTimeDiff)}ms`); 589 | debugTimeDiff = getTime(); 590 | } 591 | if (this.growatt.isConnected()) { 592 | const allPlantData = await this.growatt.getAllPlantData({ 593 | weather: this.config.weather, 594 | faultlog: this.config.faultlog, 595 | totalData: this.config.totalData, 596 | statusData: this.config.statusData, 597 | plantData: this.config.plantData, 598 | deviceData: this.config.deviceData, 599 | historyLast: this.config.historyLast, 600 | }); 601 | delete this.relogin; 602 | this.log.debug(`Growatt time for allPlantData : ${getTimeDiff(debugTimeDiff)}ms`); 603 | debugTimeDiff = getTime(); 604 | this.parseData(allPlantData, ''); 605 | this.log.debug(`Growatt time for parseData : ${getTimeDiff(debugTimeDiff)}ms`); 606 | if (afterConnect && this.config.settings) { 607 | this.log.debug(`Growatt time for settings : ${getTimeDiff(debugTimeDiff)}ms`); 608 | debugTimeDiff = getTime(); 609 | this.loadSettings(allPlantData); 610 | this.log.debug(`Growatt time for settings : ${getTimeDiff(debugTimeDiff)}ms`); 611 | } 612 | debugTimeDiff = getTime(); 613 | if (this.callRun) { 614 | this.setState('info.connection', { val: true, ack: true }); 615 | timeout = this.config.cycleTime * 1000 - getTimeDiff(allTimeDiff); 616 | if (timeout < 100) { 617 | timeout = 100; 618 | } 619 | } 620 | return; 621 | } 622 | this.log.info('not connected'); 623 | this.setState('info.connection', { val: false, ack: true }); 624 | } catch (e) { 625 | if (e.toString().toLowerCase().includes('errornologin')) { 626 | if (!this.config.sessionHold || this.relogin) { 627 | this.log.warn(`Growatt login: ${e}`); 628 | if (this.config.keyLogin) { 629 | this.log.info('If this message appears continuously, your key has expired. Please generate a new one.'); 630 | } 631 | } else { 632 | this.log.info(`Growatt relogin on session failed: ${e}`); 633 | this.relogin = true; 634 | timeout = 1; 635 | } 636 | } else { 637 | this.log.error(`Growatt exception: ${e}`); 638 | } 639 | this.setState('info.connection', { val: false, ack: true }); 640 | this.growattLogout(); 641 | if (this.supportsFeature && this.supportsFeature('PLUGINS')) { 642 | const sentryInstance = this.getPluginInstance('sentry'); 643 | if (sentryInstance && sentryInstance.getSentryObject() && !e.toString().toLowerCase().includes('errornologin')) { 644 | sentryInstance.getSentryObject().captureException(e); 645 | } 646 | } 647 | } finally { 648 | if (!this.config.sessionHold) { 649 | this.growattLogout(); 650 | } else if ( 651 | typeof this.connectTime !== 'undefined' && 652 | this.config.sessionTime > 0 && 653 | getTimeDiff(this.connectTime) > this.config.sessionTime * 60000 654 | ) { 655 | this.log.debug('Connection time of the session reached'); 656 | this.growattLogout(); 657 | } 658 | clearTimeout(this.processTimeout); 659 | clearTimeout(this.callTimeout); 660 | if (this.callRun) { 661 | this.callTimeout = setTimeout(() => { 662 | this.growattData(); 663 | }, timeout); 664 | } 665 | this.log.debug(`Leave growattData :${getTimeDiff(allTimeDiff)}ms`); 666 | } 667 | } 668 | 669 | confCheckMsgObj(obj, vars) { 670 | let res = true; 671 | let data = obj.message; 672 | if (typeof data === 'string') { 673 | data = JSON.parse(data); 674 | } 675 | if (typeof data !== 'object') { 676 | this.log.error(`message arg ${typeof data} not an object or json string`); 677 | res = false; 678 | return [data, res]; 679 | } 680 | vars.forEach(v => { 681 | if (typeof data[v] === 'undefined') { 682 | this.log.error(`message .${v} is missing`); 683 | res = false; 684 | } 685 | }); 686 | return [data, res]; 687 | } 688 | 689 | /** 690 | * spinoff onMessage, reads a register 691 | * @param {string} sn serielnumber of datalogger 692 | * @param {integer} register to read 693 | * @param {object} obj the messageoject 694 | */ 695 | readLoggerRegister(register, obj) { 696 | if (this.growatt && this.growatt.isConnected()) { 697 | const [data, ok] = this.confCheckMsgObj(obj, ['sn']); 698 | if (!ok) { 699 | return; 700 | } 701 | this.growatt 702 | .getDataLoggerRegister(data.sn, register) 703 | .then(res => { 704 | this.log.debug(`readLoggerRegister: ${JSON.stringify(res, getJSONCircularReplacer())}`); 705 | if (obj.callback && typeof res.success !== 'undefined') { 706 | this.sendTo(obj.from, obj.command, typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, obj.callback); 707 | } 708 | }) 709 | .catch(e => { 710 | this.log.error(e); 711 | }); 712 | } 713 | } 714 | 715 | /** 716 | * spinoff onMessage, writes a register 717 | * @param {string} sn serielnumber of datalogger 718 | * @param {integer} register to write 719 | * @param {string} value to write 720 | * @param {object} obj the messageoject 721 | */ 722 | writeLoggerRegister(register, obj) { 723 | if (this.growatt && this.growatt.isConnected()) { 724 | const [data, ok] = this.confCheckMsgObj(obj, ['sn', 'value']); 725 | if (!ok) { 726 | return; 727 | } 728 | this.growatt 729 | .setDataLoggerRegister(data.sn, register, data.value) 730 | .then(res => { 731 | this.log.debug(`writeLoggerRegister: ${JSON.stringify(res, getJSONCircularReplacer())}`); 732 | if (obj.callback && typeof res.success !== 'undefined') { 733 | this.sendTo(obj.from, obj.command, typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, obj.callback); 734 | } 735 | }) 736 | .catch(e => { 737 | this.log.error(e); 738 | }); 739 | } 740 | } 741 | 742 | /** 743 | * spinoff onMessage, writes with function 744 | * @param {string} sn serielnumber of datalogger 745 | * @param {integer} function to use 746 | * @param {string} value to write 747 | * @param {object} obj the messageoject 748 | */ 749 | writeLoggerFunction(func, obj) { 750 | if (this.growatt && this.growatt.isConnected()) { 751 | const [data, ok] = this.confCheckMsgObj(obj, ['sn', 'value']); 752 | if (!ok) { 753 | return; 754 | } 755 | this.growatt 756 | .setDataLoggerParam(data.sn, func, data.value) 757 | .then(res => { 758 | this.log.debug(`writeLoggerFunction: ${JSON.stringify(res, getJSONCircularReplacer())}`); 759 | if (obj.callback && typeof res.success !== 'undefined') { 760 | this.sendTo(obj.from, obj.command, typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, obj.callback); 761 | } 762 | }) 763 | .catch(e => { 764 | this.log.error(e); 765 | }); 766 | } 767 | } 768 | 769 | /** 770 | * onMessage, from Admin interface 771 | * @param {object} obj the messageoject 772 | */ 773 | onMessage(obj) { 774 | let wait = false; 775 | this.log.debug(JSON.stringify(obj, getJSONCircularReplacer())); 776 | if (obj) { 777 | switch (obj.command) { 778 | case 'getDatalogger': 779 | if (this.growatt && this.growatt.isConnected()) { 780 | wait = true; 781 | this.growatt 782 | .getDataLoggers() 783 | .then(res => { 784 | this.log.debug(`getDatalogger: ${JSON.stringify(res, getJSONCircularReplacer())}`); 785 | if (obj.callback) { 786 | this.sendTo( 787 | obj.from, 788 | obj.command, 789 | typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, 790 | obj.callback 791 | ); 792 | } 793 | }) 794 | .catch(e => { 795 | this.log.error(e); 796 | }); 797 | } 798 | break; 799 | case 'getDataLoggerIntervalRegister': 800 | wait = true; 801 | this.readLoggerRegister(API.LOGGERREGISTER.INTERVAL, obj); 802 | break; 803 | case 'setDataLoggerIntervalRegister': 804 | wait = true; 805 | this.writeLoggerRegister(API.LOGGERREGISTER.INTERVAL, obj); 806 | break; 807 | case 'getDataLoggerIpRegister': 808 | wait = true; 809 | this.readLoggerRegister(API.LOGGERREGISTER.SERVERIP, obj); 810 | break; 811 | case 'setDataLoggerIp': 812 | wait = true; 813 | this.writeLoggerFunction(API.LOGGERFUNCTION.SERVERIP, obj); 814 | break; 815 | case 'getDataLoggerPortRegister': 816 | wait = true; 817 | this.readLoggerRegister(API.LOGGERREGISTER.SERVERPORT, obj); 818 | break; 819 | case 'setDataLoggerPort': 820 | wait = true; 821 | this.writeLoggerFunction(API.LOGGERFUNCTION.SERVERPORT, obj); 822 | break; 823 | case 'checkLoggerFirmware': 824 | if (this.growatt && this.growatt.isConnected()) { 825 | wait = true; 826 | const [data, ok] = this.confCheckMsgObj(obj, ['sn']); 827 | if (!ok) { 828 | return; 829 | } 830 | this.growatt 831 | .checkDataLoggerFirmware(data.type, data.version) 832 | .then(res => { 833 | this.log.debug(`checkDataLoggerFirmware: ${JSON.stringify(res, getJSONCircularReplacer())}`); 834 | if (obj.callback && typeof res.success !== 'undefined') { 835 | this.sendTo( 836 | obj.from, 837 | obj.command, 838 | typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, 839 | obj.callback 840 | ); 841 | } 842 | }) 843 | .catch(e => { 844 | this.log.error(e); 845 | }); 846 | } 847 | break; 848 | case 'restartDatalogger': 849 | if (this.growatt && this.growatt.isConnected()) { 850 | wait = true; 851 | const [data, ok] = this.confCheckMsgObj(obj, ['sn']); 852 | if (!ok) { 853 | return; 854 | } 855 | this.growatt 856 | .setDataLoggerRestart(data.sn) 857 | .then(res => { 858 | if (obj.callback) { 859 | this.sendTo(obj.from, obj.command, res.msg, obj.callback); 860 | } 861 | }) 862 | .catch(e => { 863 | this.log.error(e); 864 | }); 865 | } 866 | break; 867 | case 'getHistory': 868 | if (this.growatt && this.growatt.isConnected()) { 869 | wait = true; 870 | const [data, ok] = this.confCheckMsgObj(obj, ['type', 'sn', 'startDate', 'endDate', 'start']); 871 | if (!ok) { 872 | return; 873 | } 874 | this.growatt 875 | .getHistory(data.type, data.sn, new Date(data.startDate), new Date(data.endDate), data.start, true) 876 | .then(res => { 877 | if (obj.callback) { 878 | this.sendTo( 879 | obj.from, 880 | obj.command, 881 | typeof obj.message === 'string' ? JSON.stringify(res, getJSONCircularReplacer()) : res, 882 | obj.callback 883 | ); 884 | } 885 | }) 886 | .catch(e => { 887 | this.log.error(e); 888 | }); 889 | } 890 | break; 891 | default: 892 | this.log.warn(`Unknown command: ${obj.command}`); 893 | return; 894 | } 895 | } 896 | if (!wait && obj.callback) { 897 | this.sendTo(obj.from, obj.command, obj.message, obj.callback); 898 | } 899 | } 900 | } 901 | 902 | // @ts-ignore parent is a valid property on module 903 | if (module.parent) { 904 | // Export the constructor in compact mode 905 | /** 906 | * @param {Partial} [options={}] 907 | */ 908 | module.exports = options => new Growatt(options); 909 | } else { 910 | // otherwise start the instance directly 911 | 912 | new Growatt(); 913 | } 914 | -------------------------------------------------------------------------------- /io-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "name": "growatt", 4 | "version": "3.3.1", 5 | "news": { 6 | "3.3.1": { 7 | "en": "Added ac charge for TLXH. Thanx to olli0815!\nAdded time slots for TLXH. Thanks to olli0815 for debugging and support.\nAdded Inverter On Off for TLX und TLXH. Thanks to olli0815 for debugging and support.", 8 | "de": "Zuschlag für TLXH. Thanx zu olli0815!\nZeitschlitze für TLXH hinzugefügt. Dank olli0815 für Debugging und Support.\nInverter On Off für TLX und TLXH hinzugefügt. Dank olli0815 für Debugging und Support.", 9 | "ru": "TLXH. Танкс-олли0815!\nДобавлены временные слоты для TLXH. Благодаря olli0815 для отладки и поддержки.\nДобавлено Inverter On Off для TLX und TLXH. Благодаря olli0815 для отладки и поддержки.", 10 | "pt": "Adicionado a carga ac para TLXH. Thanx para olli0815!\nTempo adicionado slots para TLXH. Graças ao olli0815 para depuração e apoio.\nAdicionado Inverter On Off para TLX und TLXH. Graças ao olli0815 para depuração e apoio.", 11 | "nl": "Toegevoegd ac charge voor TLXH. Thanx aan Olli0815!\nToegevoegd tijdslots voor TLXH. Dankzij Olli0815 voor debuggen en ondersteuning.\nToegevoegd Inverter On Off voor TLX und TLXH. Dankzij Olli0815 voor debuggen en ondersteuning.", 12 | "fr": "Ajout de la charge ac pour TLXH. Thanx à olli0815 !\nAjout de créneaux horaires pour TLXH. Merci à olli0815 pour le débogage et le soutien.\nAjouté Inverter On Off pour TLX und TLXH. Merci à olli0815 pour le débogage et le soutien.", 13 | "it": "Aggiunta carica ac per TLXH. Thanx a olli0815!\nAggiunta di slot tempo per TLXH. Grazie a olli0815 per debug e supporto.\nAggiunto Inverter On Off per TLX und TLXH. Grazie a olli0815 per debug e supporto.", 14 | "es": "Se agregó un cargo por TLXH. ¡Tanx a olli0815!\nRanuras de tiempo agregadas para TLXH. Gracias a olli0815 por depurar y apoyar.\nAñadido Inverter on off para TLX und TLXH. Gracias a olli0815 por depurar y apoyar.", 15 | "pl": "Dodano ładunek AC dla TLXH. Thanx do Olli0815!\nDodano szczeliny czasowe dla TLXH. Dzięki olli0815 za debugowanie i wsparcie.\nDodano Inverter On Off dla TLX und TLXH. Dzięki olli0815 za debugowanie i wsparcie.", 16 | "uk": "Додана плата за TLXH. Thanx до olli0815!\nДодано часові слоти для TLXH. Завдяки olli0815 для видалення та підтримки.\nДодано інвертор на вимкнення для TLX und TLXH. Завдяки olli0815 для видалення та підтримки.", 17 | "zh-cn": "增加了TLXH的AC电荷. 丹克斯呼叫奥利0815!\n为 TLXH 添加了时间档 。 感谢olli0815的调试和支持.\n为 TLX und TLXH 添加了反转器 Off. 感谢olli0815的调试和支持." 18 | }, 19 | "3.3.0": { 20 | "en": "Added time slots for TLXH. Thanks to olli0815 for debugging and support.\nAdded Inverter On Off for TLX und TLXH. Thanks to olli0815 for debugging and support.", 21 | "de": "Zeitschlitze für TLXH hinzugefügt. Dank olli0815 für Debugging und Support.\nInverter On Off für TLX und TLXH hinzugefügt. Dank olli0815 für Debugging und Support.", 22 | "ru": "Добавлены временные слоты для TLXH. Благодаря olli0815 для отладки и поддержки.\nДобавлено Inverter On Off для TLX und TLXH. Благодаря olli0815 для отладки и поддержки.", 23 | "pt": "Tempo adicionado slots para TLXH. Graças ao olli0815 para depuração e apoio.\nAdicionado Inverter On Off para TLX und TLXH. Graças ao olli0815 para depuração e apoio.", 24 | "nl": "Toegevoegd tijdslots voor TLXH. Dankzij Olli0815 voor debuggen en ondersteuning.\nToegevoegd Inverter On Off voor TLX und TLXH. Dankzij Olli0815 voor debuggen en ondersteuning.", 25 | "fr": "Ajout de créneaux horaires pour TLXH. Merci à olli0815 pour le débogage et le soutien.\nAjouté Inverter On Off pour TLX und TLXH. Merci à olli0815 pour le débogage et le soutien.", 26 | "it": "Aggiunta di slot tempo per TLXH. Grazie a olli0815 per debug e supporto.\nAggiunto Inverter On Off per TLX und TLXH. Grazie a olli0815 per debug e supporto.", 27 | "es": "Ranuras de tiempo agregadas para TLXH. Gracias a olli0815 por depurar y apoyar.\nAñadido Inverter on off para TLX und TLXH. Gracias a olli0815 por depurar y apoyar.", 28 | "pl": "Dodano szczeliny czasowe dla TLXH. Dzięki olli0815 za debugowanie i wsparcie.\nDodano Inverter On Off dla TLX und TLXH. Dzięki olli0815 za debugowanie i wsparcie.", 29 | "uk": "Додано часові слоти для TLXH. Завдяки olli0815 для видалення та підтримки.\nДодано інвертор на вимкнення для TLX und TLXH. Завдяки olli0815 для видалення та підтримки.", 30 | "zh-cn": "为 TLXH 添加了时间档 。 感谢olli0815的调试和支持.\n为 TLX und TLXH 添加了反转器 Off. 感谢olli0815的调试和支持." 31 | }, 32 | "3.2.5": { 33 | "en": "Solved the problem that no inverter list but result 2 was returned in NOAH.\nAdded NOAH.", 34 | "de": "Lösen Sie das Problem, dass keine Inverterliste, sondern Ergebnis 2 in NOAH zurückgegeben wurde.\nNOAH hinzugefügt.", 35 | "ru": "Решение проблемы, что ни один инверторный список, но результат 2 был возвращен в NOAH.\nДобавлен НОАХ.", 36 | "pt": "Resolvido o problema que nenhuma lista de inversores, mas o resultado 2 foi retornado em NOAH.\nAdicionado NOAH.", 37 | "nl": "Loste het probleem op dat geen inverterlijst maar resultaat 2 werd teruggegeven in NOAH.\nToegevoegd NOAH.", 38 | "fr": "Résoudre le problème qu'aucune liste d'onduleurs mais le résultat 2 a été retourné en NOAH.\nAjout de NOAH.", 39 | "it": "Risolto il problema che nessun elenco inverter ma il risultato 2 è stato restituito in NOAH.\nAggiunta NOAH.", 40 | "es": "Resolvió el problema de que ninguna lista de inversores pero el resultado 2 fue devuelto en NOAH.\nAñadido NOAH.", 41 | "pl": "Rozwiązał problem, zgodnie z którym w NOAH nie została zwrócona żadna lista zwrotna, ale wynik 2.\nDodano NOAH.", 42 | "uk": "Вирішено проблему, яка не перевернула список, але результат 2 був повернений в NOAH.\nДодано NOAH.", 43 | "zh-cn": "解决了在NOAH中没有反向列表但结果2被返回的问题.\n添加了NOAH." 44 | }, 45 | "3.2.4": { 46 | "en": "No connection can be established password must now be transferred as MD5 hash.", 47 | "de": "Keine Verbindung kann hergestellt werden Passwort muss jetzt als MD5 Hash übertragen werden.", 48 | "ru": "Никакое соединение не может быть установлено паролем теперь должно быть передано как MD5 хэш.", 49 | "pt": "Nenhuma conexão pode ser estabelecida senha deve agora ser transferida como hash MD5.", 50 | "nl": "Geen verbinding kan worden ingesteld wachtwoord moet nu worden overgedragen als MD5 hash.", 51 | "fr": "Aucune connexion ne peut être établie mot de passe doit maintenant être transféré comme MD5 hachage.", 52 | "it": "Nessuna connessione può essere stabilita password deve ora essere trasferito come hash MD5.", 53 | "es": "Ninguna conexión puede establecerse contraseña debe ser transferida ahora como MD5 hash.", 54 | "pl": "Hasło nie może zostać ustanowione jako hasz MD5.", 55 | "uk": "Немає підключення може бути встановленим паролем, щоб тепер передаватися як MD5.", 56 | "zh-cn": "无法建立连接密码, 现在必须作为 MD5 散列传输 ." 57 | } 58 | }, 59 | "titleLang": { 60 | "en": "Growatt Shine API", 61 | "de": "Growatt Shine API", 62 | "ru": "Growatt Shine API", 63 | "pt": "API Growatt Shine", 64 | "nl": "Growatt Shine API", 65 | "fr": "API Growatt Shine", 66 | "it": "Growatt Shine API", 67 | "es": "API Growatt Shine", 68 | "pl": "Growatt Shine API", 69 | "uk": "Growatt Shine API", 70 | "zh-cn": "Growatt Shine API" 71 | }, 72 | "desc": { 73 | "en": "ioBroker Growatt Adapter to communiacte with ShineAPI", 74 | "de": "ioBroker Growatt Adapter zur Kommunikation mit ShineAPI", 75 | "ru": "Адаптер ioBroker Growatt для связи с ShineAPI", 76 | "pt": "Adaptador ioBroker Growatt para comunicação com ShineAPI", 77 | "nl": "ioBroker Growatt-adapter voor communicatie met ShineAPI", 78 | "fr": "ioBroker Growatt Adaptateur pour communiquer avec ShineAPI", 79 | "it": "Adattatore per Growatt ioBroker da comunicare con ShineAPI", 80 | "es": "Adaptador ioBroker Growatt para comunicarse con ShineAPI", 81 | "pl": "ioBroker Growatt Adapter do komunikacji z ShineAPI", 82 | "uk": "ioBroker Growatt адаптер для комунікації з ShineAPI", 83 | "zh-cn": "ioBroker Growatt适配器可与ShineAPI通信" 84 | }, 85 | "authors": ["PLCHome"], 86 | "keywords": ["growatt", "shine", "shinephone", "shineapi", "solarenergy", "home automation", "solar power", "solar power plant", "solaranlage"], 87 | "licenseInformation": { 88 | "license": "MIT", 89 | "type": "free" 90 | }, 91 | "platform": "Javascript/Node.js", 92 | "icon": "growatt.png", 93 | "enabled": true, 94 | "extIcon": "https://raw.githubusercontent.com/PLCHome/ioBroker.growatt/master/admin/growatt.png", 95 | "readme": "https://github.com/PLCHome/ioBroker.growatt/blob/master/README.md", 96 | "loglevel": "info", 97 | "mode": "daemon", 98 | "messagebox": true, 99 | "tier": 3, 100 | "type": "energy", 101 | "compact": true, 102 | "connectionType": "cloud", 103 | "dataSource": "poll", 104 | "localLinks": { 105 | "_default": "https://server.growatt.com/" 106 | }, 107 | "adminUI": { 108 | "config": "materialize" 109 | }, 110 | "plugins": { 111 | "sentry": { 112 | "dsn": "https://580e0de8b3414b6caba783785eb4e240@o1143033.ingest.sentry.io/6202057" 113 | } 114 | }, 115 | "dependencies": [ 116 | { 117 | "js-controller": ">=5.0.19" 118 | } 119 | ] 120 | }, 121 | "messages": [ 122 | { 123 | "condition": { 124 | "operand": "and", 125 | "rules": ["oldVersion<3.2.2", "newVersion>=3.2.3"] 126 | }, 127 | "title": { 128 | "en": "Incorrect objects were created in connection with Multiple Backflow.", 129 | "de": "Falsche Objekte wurden in Verbindung mit Multiple Backflow erstellt.", 130 | "ru": "Неправильные объекты были созданы в связи с Множественным Backflow.", 131 | "pt": "Objetos incorretos foram criados em conexão com Vários Backflow.", 132 | "nl": "Onjuiste objecten zijn gemaakt in verband met Multiple Backflow.", 133 | "fr": "Des objets incorrects ont été créés en liaison avec Multiple Backflow.", 134 | "it": "Oggetti non corretti sono stati creati in connessione con Multiple Backflow.", 135 | "es": "Se crearon objetos incorrectos en conexión con Multiple Backflow.", 136 | "pl": "Nieprawidłowe obiekty zostały utworzone w związku z Multiple Backflow.", 137 | "uk": "Некоректні об'єкти були створені у зв'язку з декількома перепадами.", 138 | "zh-cn": "在多个后流中创建了不正确的对象 ." 139 | }, 140 | "text": { 141 | "en": "If you have multiple backflow objects, please delete the objects in Total Data and Status Data. The objects below Total Data and Status Data were swapped.", 142 | "de": "Wenn Sie mehrere Backflow-Objekte haben, löschen Sie bitte die Objekte in Total Data und Status Data. Die Objekte unter Gesamtdaten und Statusdaten wurden vertauscht.", 143 | "ru": "Если у вас есть несколько объектов обратного потока, пожалуйста, удалите объекты в Total Data и Status Data. Объекты ниже Total Data и Status Data были изменены.", 144 | "pt": "Se você tiver vários objetos de backflow, exclua os objetos em Dados totais e dados de status. Os objetos abaixo dos dados totais e dados de status foram trocados.", 145 | "nl": "Als u meerdere backflow objecten hebt, verwijder dan de objecten in Total Data en Status Data. De objecten hieronder Total Data en Status Data werden geruild.", 146 | "fr": "Si vous avez plusieurs objets backflow, veuillez supprimer les objets dans Total Data et Status Data. Les objets ci-dessous ont été échangés.", 147 | "it": "Se si dispone di più oggetti di backflow, si prega di eliminare gli oggetti in Dati totali e Dati di stato. Gli oggetti sottostanti Dati totali e Dati di stato sono stati scambiati.", 148 | "es": "Si tiene múltiples objetos de flujo de respaldo, por favor, eliminar los objetos en Datos Totales y Datos del estado. Los objetos debajo de Total Data and Status Data fueron intercambiados.", 149 | "pl": "Jeśli masz wiele obiektów back flow, usuń obiekty w Total Data i Status Data. Obiekty poniżej Total Data i Status Data zostały zamienione.", 150 | "uk": "Якщо у вас є кілька об'єктів зворотного потоку, будь ласка, видаліть об'єкти в загальній даних та даних стану. Об'єкти, які були заповнені.", 151 | "zh-cn": "如果您有多个回流对象,请删除全部数据和状态数据中的对象。 数据总数和状态数据下的对象已互换." 152 | }, 153 | "link": "https://github.com/PLCHome/ioBroker.growatt/blob/master/README.md", 154 | "level": "warn", 155 | "linkText": { 156 | "en": "Documentation", 157 | "de": "Dokumentation", 158 | "ru": "Документация", 159 | "pt": "Documentação", 160 | "nl": "Document", 161 | "fr": "Documentation", 162 | "it": "Documentazione", 163 | "es": "Documentación", 164 | "pl": "Dokumentacja", 165 | "uk": "Документація", 166 | "zh-cn": "文件" 167 | }, 168 | "buttons": ["agree", "cancel"] 169 | } 170 | ], 171 | "encryptedNative": ["password", "shareKey"], 172 | "protectedNative": ["password", "shareKey"], 173 | "native": { 174 | "keyLogin": false, 175 | "user": "", 176 | "password": "", 177 | "shareKey": "", 178 | "weather": true, 179 | "faultlog": false, 180 | "totalData": true, 181 | "historyLast": true, 182 | "statusData": true, 183 | "plantData": true, 184 | "deviceData": true, 185 | "objUpdate": {}, 186 | "objOffset": {}, 187 | "webTimeout": 60, 188 | "processTimeout": 600, 189 | "sessionHold": true, 190 | "sessionTime": 0, 191 | "cycleTime": 30, 192 | "errorCycleTime": 120, 193 | "growattServer": "", 194 | "indexCandI": false, 195 | "settings": false 196 | }, 197 | "objects": [], 198 | "instanceObjects": [ 199 | { 200 | "_id": "info", 201 | "type": "channel", 202 | "common": { 203 | "name": "Information" 204 | }, 205 | "native": {} 206 | }, 207 | { 208 | "_id": "info.connection", 209 | "type": "state", 210 | "common": { 211 | "role": "indicator.connected", 212 | "name": "If connected to Growatt Server", 213 | "type": "boolean", 214 | "read": true, 215 | "write": false, 216 | "def": false 217 | }, 218 | "native": {} 219 | } 220 | ] 221 | } 222 | -------------------------------------------------------------------------------- /lib/env/tools.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * Tests whether the given variable is a real object and not an Array 5 | * @param {any} it The variable to test 6 | * @returns {it is Record} 7 | */ 8 | function isObject(it) { 9 | // This is necessary because: 10 | // typeof null === 'object' 11 | // typeof [] === 'object' 12 | // [] instanceof Object === true 13 | return Object.prototype.toString.call(it) === '[object Object]'; 14 | } 15 | 16 | /** 17 | * Tests whether the given variable is really an Array 18 | * @param {any} it The variable to test 19 | * @returns {it is any[]} 20 | */ 21 | function isArray(it) { 22 | if (Array.isArray != null) return Array.isArray(it); 23 | 24 | return Object.prototype.toString.call(it) === '[object Array]'; 25 | } 26 | 27 | /** 28 | * Choose the right tranalation API 29 | * @param {string} text The text to translate 30 | * @param {string} targetLang The target languate 31 | * @param {string} yandex api key 32 | * @returns {Promise} 33 | */ 34 | async function translateText(text, targetLang, yandex) { 35 | if (targetLang === 'en') { 36 | return text; 37 | } 38 | if (yandex) { 39 | return await translateYandex(text, targetLang, yandex); 40 | } 41 | 42 | return await translateGoogle(text, targetLang); 43 | } 44 | 45 | /** 46 | * Translates text with Yandex API 47 | * @param {string} text The text to translate 48 | * @param {string} targetLang The target languate 49 | * @param {string} yandex api key 50 | * @returns {Promise} 51 | */ 52 | async function translateYandex(text, targetLang, yandex) { 53 | if (targetLang === 'zh-cn') { 54 | targetLang = 'zh'; 55 | } 56 | try { 57 | const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${yandex}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`; 58 | const response = await axios({ url, timeout: 15000 }); 59 | 60 | if (response.data && response.data.text) { 61 | return response.data.text[0]; 62 | } 63 | throw new Error('Invalid response for translate request'); 64 | } catch (e) { 65 | throw new Error(`Could not translate to "${targetLang}": ${e}`); 66 | } 67 | } 68 | 69 | /** 70 | * Translates text with Google API 71 | * @param {string} text The text to translate 72 | * @param {string} targetLang The target languate 73 | * @returns {Promise} 74 | */ 75 | async function translateGoogle(text, targetLang) { 76 | try { 77 | const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent( 78 | text 79 | )}&ie=UTF-8&oe=UTF-8`; 80 | const response = await axios({ url, timeout: 15000 }); 81 | 82 | if (isArray(response.data)) { 83 | // we got a valid response 84 | return response.data[0][0][0]; 85 | } 86 | throw new Error('Invalid response for translate request'); 87 | } catch (e) { 88 | throw new Error(`Could not translate to "${targetLang}": ${e}`); 89 | } 90 | } 91 | 92 | module.exports = { 93 | isArray, 94 | isObject, 95 | translateText, 96 | }; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iobroker.growatt", 3 | "version": "3.3.1", 4 | "description": "ioBroker Growatt Adapter to communiacte with ShineAPI", 5 | "author": { 6 | "name": "PLCHome" 7 | }, 8 | "homepage": "https://github.com/PLCHome/ioBroker.growatt", 9 | "license": "MIT", 10 | "keywords": [ 11 | "growatt", 12 | "shine", 13 | "shinephone", 14 | "shineapi", 15 | "ioBroker", 16 | "smart home", 17 | "solarenergy", 18 | "home automation", 19 | "solar power", 20 | "solar power plant", 21 | "solaranlage" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/PLCHome/ioBroker.growatt.git" 26 | }, 27 | "engines": { 28 | "node": ">=18" 29 | }, 30 | "dependencies": { 31 | "@iobroker/adapter-core": "^3.2.2", 32 | "axios": "^1.7.7", 33 | "growatt": "^0.7.7" 34 | }, 35 | "devDependencies": { 36 | "@alcalzone/release-script": "^3.8.0", 37 | "@alcalzone/release-script-plugin-iobroker": "^3.7.2", 38 | "@alcalzone/release-script-plugin-license": "^3.7.0", 39 | "@alcalzone/release-script-plugin-manual-review": "^3.7.0", 40 | "@iobroker/adapter-dev": "^1.3.0", 41 | "@eslint/eslintrc": "^3.1.0", 42 | "@eslint/js": "^9.13.0", 43 | "@iobroker/testing": "^5.0.0", 44 | "chai": "^5.1.2", 45 | "eslint": "^9.13.0", 46 | "eslint-config-prettier": "^9.1.0", 47 | "eslint-plugin-prettier": "^5.2.1", 48 | "globals": "^15.11.0", 49 | "mocha": "^10.7.3", 50 | "prettier": "^3.3.3" 51 | }, 52 | "main": "growattMain.js", 53 | "scripts": { 54 | "lint": "eslint . ", 55 | "lint:fix": "eslint . --fix", 56 | "test:package": "mocha test/package --exit", 57 | "test:integration": "mocha test/integration --exit", 58 | "release": "release-script" 59 | }, 60 | "files": [ 61 | "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).json", 62 | "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}", 63 | "lib/", 64 | "media/", 65 | "io-package.json", 66 | "LICENSE", 67 | "growattMain.js", 68 | "README.md" 69 | ], 70 | "bugs": { 71 | "url": "https://github.com/PLCHome/ioBroker.growatt/issues" 72 | }, 73 | "readmeFilename": "README.md" 74 | } 75 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 150, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | bracketSameLine: false, 12 | arrowParens: 'avoid', 13 | endOfLine: 'auto', 14 | }; 15 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tests } = require('@iobroker/testing'); 3 | 4 | // Run integration tests - See https://github.com/ioBroker/testing for a detailed explanation and further options 5 | tests.integration(path.join(__dirname, '..')); 6 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /update.cmd: -------------------------------------------------------------------------------- 1 | call npm install -g npm-check-updates 2 | call ncu -u 3 | call npm outdated 4 | call npm update 5 | call npm install 6 | 7 | 8 | call npx prettier --write . 9 | call npx prettier --check . 10 | 11 | call npm run lint -- --fix 12 | call npm run lint 13 | --------------------------------------------------------------------------------