├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── validate.yml ├── .gitignore ├── FAQ.md ├── LICENSE ├── README.md ├── TODO.md ├── custom_components └── growatt_server_api │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── sensor.py │ ├── sensor_types │ ├── __init__.py │ ├── inverter.py │ ├── mix.py │ ├── sensor_entity_description.py │ ├── storage.py │ ├── tlx.py │ └── total.py │ ├── strings.json │ └── translations │ ├── bg.json │ ├── ca.json │ ├── cs.json │ ├── de.json │ ├── el.json │ ├── en.json │ ├── es.json │ ├── et.json │ ├── fr.json │ ├── he.json │ ├── hu.json │ ├── id.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── nb.json │ ├── nl.json │ ├── no.json │ ├── pl.json │ ├── pt-BR.json │ ├── pt.json │ ├── ru.json │ ├── sk.json │ ├── sv.json │ ├── tr.json │ ├── zh-Hans.json │ └── zh-Hant.json ├── hacs.json └── info.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Log a bug against the integration 4 | title: BUG - [Insert Bug Title Here] 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Describe the bug** 17 | 18 | 19 | **To Reproduce** 20 | 26 | 27 | **Growatt Device Type** 28 | 29 | Device(s): 30 | 31 | **Home Assistant information** 32 | - Home Assistant version: 33 | - Growatt Server API Integration version: 34 | 35 | **Additional context** 36 | 37 | N/A 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: ENCHANCEMENT - [TITLE GOES HERE] 5 | labels: enhancement, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | hassfest: 11 | runs-on: "ubuntu-latest" 12 | name: Hassfest 13 | steps: 14 | - name: Check out the repository 15 | uses: "actions/checkout@v3" 16 | - name: Hassfest validation 17 | uses: "home-assistant/actions/hassfest@master" 18 | 19 | hacs: 20 | runs-on: "ubuntu-latest" 21 | name: HACS 22 | steps: 23 | - name: Check out the repository 24 | uses: "actions/checkout@v3" 25 | - name: HACS validation 26 | uses: "hacs/action@main" 27 | with: 28 | category: "integration" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | This page is intended to cover FAQs associated with the Growatt Integration for Home Assistant, not Home Assistant iteself 3 | 4 | #### Table of Contents 5 | - [How do I increase the update interval to be more than every 5 minutes](#how-do-i-increase-the-update-interval-to-be-more-than-every-5-minutes) 6 | - [How do I configure my ShineLink/Wifi Dongle to update more often](#how-do-i-configure-my-shinelinkwifi-dongle-to-update-more-often) 7 | - [How do I share my credentials with you for testing/adding new features for my system type](#how-do-i-share-my-credentials-with-you-for-testingadding-new-features-for-my-system-type) 8 | 9 | ## How do I increase the update interval to be more than every 5 minutes 10 | Firstly you need to ensure your ShineLink/Wifi Dongle is configured to push updates to the server more regularly, see [here](#how-do-i-configure-my-shinelinkwifi-dongle-to-update-more-often) 11 | 12 | Once you have configured your system to push updates to the server more frequently follow these steps: 13 | 1. Navigate to the 'automations' in area in your Home Assistant instance 14 | ![image](https://user-images.githubusercontent.com/10612068/212285861-6028a930-c09d-436f-a695-64a300fd7064.png) 15 | 1. Click on `Create Automation`: 16 | ![image](https://user-images.githubusercontent.com/10612068/212546882-c54321e6-af4d-473f-b64a-5acff19d5560.png) 17 | 1. Click `Start with an empty automation`: 18 | ![image](https://user-images.githubusercontent.com/10612068/212547087-36131043-012e-4103-8e2e-693c545a6aef.png) 19 | 1. In `Triggers` select `Add Tigger` 20 | 1. Select `Time pattern` 21 | 1. Enter the frequency in minutes that you wish the Integration to pull updates. Ensure you put a `/` before the number e.g `/1` for 1 minute. 22 | **NOTE - The integration has a throttle of 1 minute, therefore any more frequent than that is pointless** 23 | ![image](https://user-images.githubusercontent.com/10612068/212547381-d53da02a-da56-4daf-a812-463503200233.png) 24 | 1. No values are required in `Conditions` 25 | 1. In Actions select `Add Action` 26 | 1. Select `Call Service`: 27 | ![image](https://user-images.githubusercontent.com/10612068/212547619-d72ea220-121a-40a1-98df-130e74355075.png) 28 | 1. In the box that appears type `update` and then select `Home Assistant Core Integration: Update entity`: 29 | ![image](https://user-images.githubusercontent.com/10612068/212547909-27629a04-00fd-4972-b0e3-bc4ef86e1751.png) 30 | 1. In the `Targets` section of the resulting box select `Choose Device`: 31 | ![image](https://user-images.githubusercontent.com/10612068/212548125-0c5234c2-37be-423f-992f-4533ee91111b.png) 32 | 1. Then select your Growatt devices, there will be 2, one for `Totals` and one for your Inverter, make sure you add them both: 33 | ![image](https://user-images.githubusercontent.com/10612068/212548187-a7257f60-ed21-404f-a051-dc0227c2789d.png) 34 | You'll end up with something like this: 35 | ![image](https://user-images.githubusercontent.com/10612068/212548329-c32a7949-602f-481e-bfd6-e3589d81428d.png) 36 | 1. **DO NOT SKIP THIS STEP** You must expand the two devices into their full entity list by clicking the `<>` image on each of them: 37 | ![image](https://user-images.githubusercontent.com/10612068/212548877-32e2946c-436f-49b8-9428-9d114c1ea63d.png) 38 | 1. Once you've expanded all entities you'll see something like this (yes, it's messy, but it works): 39 | ![image](https://user-images.githubusercontent.com/10612068/212548913-541990b8-f4d0-459e-8d8a-6b5d1873dd68.png) 40 | 1. The whole automation should look something like this (when everything is minimised): 41 | ![image](https://user-images.githubusercontent.com/10612068/212548983-16e06ac4-3398-40e0-83f2-6ae6d0ce6bb1.png) 42 | 1. Click `Save` and give the automation a useful name and it will then trigger every X minutes based on your selection 43 | 44 | 45 | ## How do I configure my ShineLink/Wifi Dongle to update more often 46 | You need to know the IP address of your ShineLink/Wifi Dongle, this will vary depending on your home network setup and isn't something I can help you with. 47 | 48 | Typically looking on your router's configuration or DHCP server you will find something that looks like it. 49 | 50 | Once you know its IP address enter it into your browser and you'll find something like this: 51 | ![image](https://user-images.githubusercontent.com/10612068/212528884-f7d7c44e-6a98-47f7-931c-fc035c3a4f5d.png) 52 | 53 | Enter the credentials (admin/admin by default), and you'll be shown the Datalogger information page, from here click on `Network Setting`: 54 | ![image](https://user-images.githubusercontent.com/10612068/212528973-f2a5f248-2211-4154-b5aa-f272b5967985.png) 55 | 56 | On the Network Settings page change the `Data Transfer interval` to one of your choosing (lowest is 1 minute), then click `Save` 57 | ![image](https://user-images.githubusercontent.com/10612068/212528966-c131c5aa-b478-4753-9648-4d0cbc381169.png) 58 | 59 | After this your ShineLink/Wifi Dongle will start pushing data more frequently to the Growatt Servers 60 | 61 | ## How do I share my credentials with you for testing/adding new features for my system type 62 | Firstly - You have to acknowledge that you are willing to provide access to a complete stranger, that requires trust, if you're uncomfortable with this, stop reading now. 63 | If you're still reading, follow the steps below: 64 | 1. Log in to the [growatt website](https://server.growatt.com) 65 | 1. Click 'Setting': 66 | ![image](https://user-images.githubusercontent.com/10612068/212376925-18a13715-0503-40b1-99fe-4cca209fd6cb.png) 67 | 1. Click 'Browse Account', then 'Add' 68 | ![image](https://user-images.githubusercontent.com/10612068/212377188-206541b5-b4fa-4cec-935d-d985845d0c22.png) 69 | 1. Fill out the Form with the required information 70 | 1. **User Name** - Must be unique - Multiple people have already sent ones based on my username which is difficult to get alternatives for, instead pick something completely random 71 | 1. **Email** - Please use my email address: c.m.straffon@gmail.com (I will get an email when the account is created then) 72 | 1. **Password** - A Password of your choice 73 | 1. **Again** - The password again 74 | ![image](https://user-images.githubusercontent.com/10612068/212377862-c38a0e5e-c8d4-426a-a968-2703362bc680.png) 75 | 1. Click 'Yes' 76 | 77 | At any point if you become uncomfortable with me having access you can simply delete the user and my access is revoked 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Straffon 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 | [![validate_badge](https://github.com/muppet3000/homeassistant-growatt_server_api/actions/workflows/validate.yml/badge.svg)](https://github.com/muppet3000/homeassistant-growatt_server_api/actions) 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?logo=homeassistantcommunitystore)](https://github.com/hacs/integration) 3 | [![bmab_badge](https://img.shields.io/badge/Buy_Me-A_Beer-FFDD00.svg?logo=buymeacoffee)](https://www.buymeacoffee.com/muppet3000) 4 | [![paypal_badge](https://img.shields.io/badge/PayPal-Beer_Fund-blue.svg?&logo=paypal)](https://www.paypal.com/paypalme/muppet3000) 5 | 6 | # Growatt Server API Home Assistant Custom Component (growatt_server_api) 7 | This custom component (installable via HACS) is an upstream version of [growatt_server](https://www.home-assistant.io/integrations/growatt_server/) integration that is part of the [Home Assistant Core](https://github.com/home-assistant/core/tree/dev/homeassistant/components/growatt_server) repository. 8 | 9 | The integration has been through a period of instability due to various issues with the authentication mechanism for Growatt (Growatt keep blocking the python library), using the HACS system will allow for more dynamic responses to the changes made by Growatt. 10 | 11 | The changes made in this repository will be rolled up monthly and then submitted as a Pull-Request to the main Home Assistant Core repository for release as an 'official' Integration. 12 | 13 | Note - This Integration works not only with Growatt systems but also Jinko Solar systems (as they appear to be exactly the same systems & API). 14 | 15 | # !IMPORTANT NOTICE! 16 | Since 07/02/2023 (7th Feb) Growatt have started implementing rate limiting and blocking to user accounts that make excessive API calls i.e. this integration. There is an extended discussion around it [here](https://github.com/muppet3000/homeassistant-growatt_server_api/issues/15). 17 | 18 | If you're interested in the short version read these posts: [first](https://github.com/muppet3000/homeassistant-growatt_server_api/issues/15#issuecomment-1423787751), [second](https://github.com/muppet3000/homeassistant-growatt_server_api/issues/15#issuecomment-1424810061), [third](https://github.com/muppet3000/homeassistant-growatt_server_api/issues/15#issuecomment-1427052222) 19 | 20 | The recent developments throw into question the long-term viability of this Integration, please see the 'third' link above for the proposed plan. 21 | 22 | # Implementation 23 | On initial release this plugin is an identical copy of the original integration that was part of the Home Assistant release. Going forward, this will be the upstream release of the Integration and the first place that bugs are fixed & new features are added. 24 | 25 | # Installation 26 | This integration can be installed via HACS for Home Assistant 27 | 1. [Install HACS](https://hacs.xyz/docs/setup/prerequisites) (Follow all the way through to the 'Configuration' page) 28 | 1. In HACS install this integration (this will be simplified once this repository has been added to the default HACS repos via [this](https://github.com/hacs/default/pull/1660) PR): 29 | 1. Click 'HACS' on the left menu 30 | 1. Click 'Integrations' 31 | 32 | 1. Click the three dots in the top-right corner 33 | 1. Click 'Custom repositories' 34 | 1. In the 'Repository' field enter: `https://github.com/muppet3000/homeassistant-growatt_server_api` 35 | 1. In the 'Category' field select 'Integration' 36 | 1. Click 'Add' 37 | 1. Once the repository is added click on `Growatt Server API` 38 | 1. On the next page select 'Download' in the bottom-right corner 39 | 1. Choose the latest released version and click 'DOWNLOAD' 40 | 1. Click the 'HACS' button in the menu and restart Home Assistant when prompted 41 | 46 | 1. Once the plugin is installed via HACS configure it just like a normal Home Assistant Integration i.e. from Settings -> Devices & Settings -> Add Integration -> Search for Growatt Server API 47 | 48 | or (if you don't want to use HACS) 49 | 50 | 1. Download the latest release to your custom_components folder inside the Home Assistant configuration directory and unpack it 51 | 52 | # More Info 53 | Please read the info.md file [here](https://github.com/muppet3000/homeassistant-growatt_server_api/blob/main/info.md) for more information, including the roadmap for the integration. 54 | 55 | There is also an [FAQ](FAQ.md) 56 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Correct values in manifest.json based on here: https://hacs.xyz/docs/publish/integration 2 | - Add FAQ page with: 3 | - How to set scan interval 4 | - Move/copy all bugs to this repo 5 | - Create new bugs/issues for new features to be added 6 | 7 | 8 | -------------------------------------------------------------------------------- /custom_components/growatt_server_api/__init__.py: -------------------------------------------------------------------------------- 1 | """The Growatt server PV inverter sensor integration.""" 2 | from homeassistant import config_entries 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.core import HomeAssistant 5 | 6 | from .const import PLATFORMS 7 | 8 | 9 | async def async_setup_entry( 10 | hass: HomeAssistant, entry: config_entries.ConfigEntry 11 | ) -> bool: 12 | """Load the saved entities.""" 13 | 14 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 15 | return True 16 | 17 | 18 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 19 | """Unload a config entry.""" 20 | return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 21 | -------------------------------------------------------------------------------- /custom_components/growatt_server_api/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for growatt server integration.""" 2 | import growattServer 3 | import voluptuous as vol 4 | 5 | from homeassistant import config_entries 6 | from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME 7 | from homeassistant.core import callback 8 | 9 | from .const import ( 10 | CONF_PLANT_ID, 11 | DEFAULT_URL, 12 | DOMAIN, 13 | LOGIN_INVALID_AUTH_CODE, 14 | SERVER_URLS, 15 | ) 16 | 17 | 18 | class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 19 | """Config flow class.""" 20 | 21 | VERSION = 1 22 | 23 | def __init__(self): 24 | """Initialise growatt server flow.""" 25 | self.api = None 26 | self.user_id = None 27 | self.data = {} 28 | 29 | @callback 30 | def _async_show_user_form(self, errors=None): 31 | """Show the form to the user.""" 32 | data_schema = vol.Schema( 33 | { 34 | vol.Required(CONF_USERNAME): str, 35 | vol.Required(CONF_PASSWORD): str, 36 | vol.Required(CONF_URL, default=DEFAULT_URL): vol.In(SERVER_URLS), 37 | } 38 | ) 39 | 40 | return self.async_show_form( 41 | step_id="user", data_schema=data_schema, errors=errors 42 | ) 43 | 44 | async def async_step_user(self, user_input=None): 45 | """Handle the start of the config flow.""" 46 | if not user_input: 47 | return self._async_show_user_form() 48 | 49 | # Initialise the library with the username & a random id each time it is started 50 | self.api = growattServer.GrowattApi( 51 | add_random_user_id=True, agent_identifier=user_input[CONF_USERNAME] 52 | ) 53 | self.api.server_url = user_input[CONF_URL] 54 | login_response = await self.hass.async_add_executor_job( 55 | self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] 56 | ) 57 | 58 | if ( 59 | not login_response["success"] 60 | and login_response["msg"] == LOGIN_INVALID_AUTH_CODE 61 | ): 62 | return self._async_show_user_form({"base": "invalid_auth"}) 63 | self.user_id = login_response["user"]["id"] 64 | 65 | self.data = user_input 66 | return await self.async_step_plant() 67 | 68 | async def async_step_plant(self, user_input=None): 69 | """Handle adding a "plant" to Home Assistant.""" 70 | plant_info = await self.hass.async_add_executor_job( 71 | self.api.plant_list, self.user_id 72 | ) 73 | 74 | if not plant_info["data"]: 75 | return self.async_abort(reason="no_plants") 76 | 77 | plants = {plant["plantId"]: plant["plantName"] for plant in plant_info["data"]} 78 | 79 | if user_input is None and len(plant_info["data"]) > 1: 80 | data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)}) 81 | 82 | return self.async_show_form(step_id="plant", data_schema=data_schema) 83 | 84 | if user_input is None and len(plant_info["data"]) == 1: 85 | user_input = {CONF_PLANT_ID: plant_info["data"][0]["plantId"]} 86 | 87 | user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]] 88 | await self.async_set_unique_id(user_input[CONF_PLANT_ID]) 89 | self._abort_if_unique_id_configured() 90 | self.data.update(user_input) 91 | return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) 92 | -------------------------------------------------------------------------------- /custom_components/growatt_server_api/const.py: -------------------------------------------------------------------------------- 1 | """Define constants for the Growatt Server component.""" 2 | from homeassistant.const import Platform 3 | 4 | CONF_PLANT_ID = "plant_id" 5 | 6 | DEFAULT_PLANT_ID = "0" 7 | 8 | DEFAULT_NAME = "Growatt" 9 | 10 | SERVER_URLS = [ 11 | "https://openapi.growatt.com/", 12 | "https://server-api.growatt.com/", 13 | "https://server.growatt.com/", 14 | "https://server-us.growatt.com/", 15 | "https://server.pvbutler.com/", 16 | "http://server.smten.com/", 17 | "https://pvplusanz.jinkosolar.com/", 18 | ] 19 | 20 | DEPRECATED_URLS: list[str] = [] 21 | 22 | DEFAULT_URL = SERVER_URLS[0] 23 | 24 | DOMAIN = "growatt_server_api" 25 | 26 | PLATFORMS = [Platform.SENSOR] 27 | 28 | LOGIN_INVALID_AUTH_CODE = "502" 29 | -------------------------------------------------------------------------------- /custom_components/growatt_server_api/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "growatt_server_api", 3 | "name": "Growatt Server API", 4 | "codeowners": ["@muppet3000"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/muppet3000/homeassistant-growatt_server_api", 7 | "iot_class": "cloud_polling", 8 | "issue_tracker": "https://github.com/muppet3000/homeassistant-growatt_server_api/issues", 9 | "loggers": ["growattServerApi"], 10 | "requirements": ["growattServer==1.3.0"], 11 | "version": "1.0.4" 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/growatt_server_api/sensor.py: -------------------------------------------------------------------------------- 1 | """Read status of growatt inverters.""" 2 | from __future__ import annotations 3 | 4 | import datetime 5 | import json 6 | import logging 7 | 8 | import growattServer 9 | 10 | from homeassistant.components.sensor import SensorEntity 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME 13 | from homeassistant.core import HomeAssistant 14 | from homeassistant.helpers.entity import DeviceInfo 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | from homeassistant.util import Throttle, dt 17 | 18 | from .const import ( 19 | CONF_PLANT_ID, 20 | DEFAULT_PLANT_ID, 21 | DEFAULT_URL, 22 | DEPRECATED_URLS, 23 | DOMAIN, 24 | LOGIN_INVALID_AUTH_CODE, 25 | ) 26 | from .sensor_types.inverter import INVERTER_SENSOR_TYPES 27 | from .sensor_types.mix import MIX_SENSOR_TYPES 28 | from .sensor_types.sensor_entity_description import GrowattSensorEntityDescription 29 | from .sensor_types.storage import STORAGE_SENSOR_TYPES 30 | from .sensor_types.tlx import TLX_SENSOR_TYPES 31 | from .sensor_types.total import TOTAL_SENSOR_TYPES 32 | 33 | _LOGGER = logging.getLogger(__name__) 34 | 35 | SCAN_INTERVAL = datetime.timedelta(minutes=5) 36 | THROTTLE = datetime.timedelta(minutes=1) 37 | 38 | 39 | def get_device_list(api, config): 40 | """Retrieve the device list for the selected plant.""" 41 | plant_id = config[CONF_PLANT_ID] 42 | 43 | # Log in to api and fetch first plant if no plant id is defined. 44 | login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD]) 45 | if ( 46 | not login_response["success"] 47 | and login_response["msg"] == LOGIN_INVALID_AUTH_CODE 48 | ): 49 | _LOGGER.error("Username, Password or URL may be incorrect!") 50 | return 51 | user_id = login_response["user"]["id"] 52 | if plant_id == DEFAULT_PLANT_ID: 53 | plant_info = api.plant_list(user_id) 54 | plant_id = plant_info["data"][0]["plantId"] 55 | 56 | # Get a list of devices for specified plant to add sensors for. 57 | devices = api.device_list(plant_id) 58 | return [devices, plant_id] 59 | 60 | 61 | async def async_setup_entry( 62 | hass: HomeAssistant, 63 | config_entry: ConfigEntry, 64 | async_add_entities: AddEntitiesCallback, 65 | ) -> None: 66 | """Set up the Growatt sensor.""" 67 | config = {**config_entry.data} 68 | username = config[CONF_USERNAME] 69 | password = config[CONF_PASSWORD] 70 | url = config.get(CONF_URL, DEFAULT_URL) 71 | name = config[CONF_NAME] 72 | 73 | # If the URL has been deprecated then change to the default instead 74 | if url in DEPRECATED_URLS: 75 | _LOGGER.info( 76 | "URL: %s has been deprecated, migrating to the latest default: %s", 77 | url, 78 | DEFAULT_URL, 79 | ) 80 | url = DEFAULT_URL 81 | config[CONF_URL] = url 82 | hass.config_entries.async_update_entry(config_entry, data=config) 83 | 84 | # Initialise the library with the username & a random id each time it is started 85 | api = growattServer.GrowattApi(add_random_user_id=True, agent_identifier=username) 86 | api.server_url = url 87 | 88 | devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) 89 | 90 | probe = GrowattData(api, username, password, plant_id, "total") 91 | entities = [ 92 | GrowattInverter( 93 | probe, 94 | name=f"{name} Total", 95 | unique_id=f"{plant_id}-{description.key}", 96 | description=description, 97 | ) 98 | for description in TOTAL_SENSOR_TYPES 99 | ] 100 | 101 | # Add sensors for each device in the specified plant. 102 | for device in devices: 103 | probe = GrowattData( 104 | api, username, password, device["deviceSn"], device["deviceType"] 105 | ) 106 | sensor_descriptions: tuple[GrowattSensorEntityDescription, ...] = () 107 | if device["deviceType"] == "inverter": 108 | sensor_descriptions = INVERTER_SENSOR_TYPES 109 | elif device["deviceType"] == "tlx": 110 | probe.plant_id = plant_id 111 | sensor_descriptions = TLX_SENSOR_TYPES 112 | elif device["deviceType"] == "storage": 113 | probe.plant_id = plant_id 114 | sensor_descriptions = STORAGE_SENSOR_TYPES 115 | elif device["deviceType"] == "mix": 116 | probe.plant_id = plant_id 117 | sensor_descriptions = MIX_SENSOR_TYPES 118 | else: 119 | _LOGGER.debug( 120 | "Device type %s was found but is not supported right now", 121 | device["deviceType"], 122 | ) 123 | 124 | entities.extend( 125 | [ 126 | GrowattInverter( 127 | probe, 128 | name=f"{device['deviceAilas']}", 129 | unique_id=f"{device['deviceSn']}-{description.key}", 130 | description=description, 131 | ) 132 | for description in sensor_descriptions 133 | ] 134 | ) 135 | 136 | async_add_entities(entities, True) 137 | 138 | 139 | class GrowattInverter(SensorEntity): 140 | """Representation of a Growatt Sensor.""" 141 | 142 | entity_description: GrowattSensorEntityDescription 143 | 144 | def __init__( 145 | self, probe, name, unique_id, description: GrowattSensorEntityDescription 146 | ) -> None: 147 | """Initialize a PVOutput sensor.""" 148 | self.probe = probe 149 | self.entity_description = description 150 | 151 | self._attr_name = f"{name} {description.name}" 152 | self._attr_unique_id = unique_id 153 | self._attr_icon = "mdi:solar-power" 154 | 155 | self._attr_device_info = DeviceInfo( 156 | identifiers={(DOMAIN, probe.device_id)}, 157 | manufacturer="Growatt", 158 | name=name, 159 | ) 160 | 161 | @property 162 | def native_value(self): 163 | """Return the state of the sensor.""" 164 | result = self.probe.get_data(self.entity_description) 165 | if self.entity_description.precision is not None: 166 | result = round(result, self.entity_description.precision) 167 | return result 168 | 169 | @property 170 | def native_unit_of_measurement(self) -> str | None: 171 | """Return the unit of measurement of the sensor, if any.""" 172 | if self.entity_description.currency: 173 | return self.probe.get_currency() 174 | return super().native_unit_of_measurement 175 | 176 | def update(self) -> None: 177 | """Get the latest data from the Growat API and updates the state.""" 178 | self.probe.update() 179 | 180 | 181 | def get_pac_value_from_chart(api_pac_value, chart_data): 182 | """Return the correct pac (Output Power) value based on chart data.""" 183 | pac_value = api_pac_value 184 | # If there are no datapoints in the graph set the value to zero. 185 | if len(chart_data) == 0: 186 | _LOGGER.debug("Chart data missing, setting pac to 0") 187 | pac_value = 0 188 | else: 189 | # Establish the last data point in the graph, if the last point 190 | # in the graph is before 'now - 10 minutes' then set the value 191 | # to zero. 192 | sorted_chart_data = sorted(chart_data) 193 | last_updated_dt = dt.parse_datetime(str(sorted_chart_data[-1])) 194 | last_updated = datetime.datetime.combine( 195 | last_updated_dt.date(), last_updated_dt.time(), dt.DEFAULT_TIME_ZONE 196 | ) 197 | now = dt.now() 198 | time_diff = now - last_updated 199 | if time_diff > datetime.timedelta(minutes=10): 200 | # If last updated time is < now - 10 minutes, then set the value to zero. 201 | _LOGGER.debug( 202 | "Chart data has no new entry for 10 minutes, setting pac to 0" 203 | ) 204 | pac_value = 0 205 | else: 206 | _LOGGER.debug("Chart data has recent entries, leaving pac untouched") 207 | return pac_value 208 | 209 | 210 | class GrowattData: 211 | """The class for handling data retrieval.""" 212 | 213 | def __init__(self, api, username, password, device_id, growatt_type): 214 | """Initialize the probe.""" 215 | 216 | self.growatt_type = growatt_type 217 | self.api = api 218 | self.device_id = device_id 219 | self.plant_id = None 220 | self.data = {} 221 | self.previous_values = {} 222 | self.username = username 223 | self.password = password 224 | 225 | @Throttle(THROTTLE) 226 | def update(self): 227 | """Update probe data.""" 228 | self.api.login(self.username, self.password) 229 | _LOGGER.debug("Updating data for %s (%s)", self.device_id, self.growatt_type) 230 | try: 231 | if self.growatt_type == "total": 232 | total_info = self.api.plant_info(self.device_id) 233 | del total_info["deviceList"] 234 | # PlantMoneyText comes in as "3.1/€" split between value and currency 235 | plant_money_text, currency = total_info["plantMoneyText"].split("/") 236 | total_info["plantMoneyText"] = plant_money_text 237 | total_info["currency"] = currency 238 | self.data = total_info 239 | elif self.growatt_type == "inverter": 240 | inverter_info = self.api.inverter_detail(self.device_id) 241 | self.data = inverter_info 242 | elif self.growatt_type == "tlx": 243 | tlx_info = self.api.tlx_detail(self.device_id) 244 | tlx_data = self.api.tlx_data(self.device_id) 245 | tlx_chart_data = tlx_data["invPacData"] 246 | 247 | tlx_info["data"]["pac"] = get_pac_value_from_chart( 248 | tlx_info["data"]["pac"], tlx_chart_data 249 | ) 250 | self.data = tlx_info["data"] 251 | elif self.growatt_type == "storage": 252 | storage_info_detail = self.api.storage_params(self.device_id)[ 253 | "storageDetailBean" 254 | ] 255 | storage_energy_overview = self.api.storage_energy_overview( 256 | self.plant_id, self.device_id 257 | ) 258 | self.data = {**storage_info_detail, **storage_energy_overview} 259 | elif self.growatt_type == "mix": 260 | mix_info = self.api.mix_info(self.device_id) 261 | mix_totals = self.api.mix_totals(self.device_id, self.plant_id) 262 | mix_system_status = self.api.mix_system_status( 263 | self.device_id, self.plant_id 264 | ) 265 | 266 | mix_detail = self.api.mix_detail(self.device_id, self.plant_id) 267 | # Get the chart data and work out the time of the last entry, use this 268 | # as the last time data was published to the Growatt Server 269 | mix_chart_entries = mix_detail["chartData"] 270 | sorted_keys = sorted(mix_chart_entries) 271 | 272 | # Create datetime from the latest entry 273 | date_now = dt.now().date() 274 | last_updated_time = dt.parse_time(str(sorted_keys[-1])) 275 | mix_detail["lastdataupdate"] = datetime.datetime.combine( 276 | date_now, last_updated_time, dt.DEFAULT_TIME_ZONE 277 | ) 278 | 279 | # We calculate this value dynamically based on the returned chart data. 280 | # There is no value available on the API that provides the combined 281 | # value of: charging + load consumption 282 | # For each time entry convert it's wattage into kWh, this assumes that 283 | # the wattage value is the same for the whole X minute window (it's the 284 | # only assumption we can make) 285 | # We Multiply the wattage by