├── .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 | 
15 | 1. Click on `Create Automation`:
16 | 
17 | 1. Click `Start with an empty automation`:
18 | 
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 | 
24 | 1. No values are required in `Conditions`
25 | 1. In Actions select `Add Action`
26 | 1. Select `Call Service`:
27 | 
28 | 1. In the box that appears type `update` and then select `Home Assistant Core Integration: Update entity`:
29 | 
30 | 1. In the `Targets` section of the resulting box select `Choose Device`:
31 | 
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 | 
34 | You'll end up with something like this:
35 | 
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 | 
38 | 1. Once you've expanded all entities you'll see something like this (yes, it's messy, but it works):
39 | 
40 | 1. The whole automation should look something like this (when everything is minimised):
41 | 
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 | 
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 | 
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 | 
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 | 
67 | 1. Click 'Browse Account', then 'Add'
68 | 
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 | 
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 | [](https://github.com/muppet3000/homeassistant-growatt_server_api/actions)
2 | [](https://github.com/hacs/integration)
3 | [](https://www.buymeacoffee.com/muppet3000)
4 | [](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