pysolarcloud 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ Copyright 2025 Tore Green
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is furnished
8
+ to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ exclude tests/*
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.2
2
+ Name: pysolarcloud
3
+ Version: 0.1.0
4
+ Summary: A library to interact with Sungrow's iSolarCloud API
5
+ Author-email: Tore Green <bugjam@e-dreams.dk>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bugjam/pysolarcloud
8
+ Project-URL: Issues, https://github.com/bugjam/pysolarcloud/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Topic :: Home Automation
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: aiohttp
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-asyncio; extra == "dev"
20
+ Dynamic: provides-extra
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+
24
+ # pysolarcloud
25
+
26
+ A Python package to interact with the [iSolarCloud API](https://developer-api.isolarcloud.com/) by Sungrow.
27
+
28
+ The current version has only very basic functionality:
29
+ * OAuth2 authentication
30
+ * Getting a list plants
31
+ * Getting details of a plant
32
+ * Getting "real-time" data of a plant (Data is updated every 5 minutes according to Sungrow's documentation)
33
+
34
+ ## Quirks
35
+ The iSolarCloud API is quite new and not very mature. Some tips:
36
+ * The authorisation flow is based on OAuth2 but doesn't work exactly as you would expect
37
+ * The `state` parameter is not passed back after to the authorisation step. This makes it more tricky to resume the flow in a client application.
38
+ * User is asked to approve the authorisation if the flow is invoked again, e.g. in case the tokens have expired - unlike many OAuth2 implementations who will perform a "silent" authorisation if the user has already approved the access.
39
+ * The API documentation lists a lot of data points which do not seem to be returned from my inverter, it probably varies between models.
40
+ * There are different iSolarCloud servers for different regions, see the `pysolarcloud.Server` enum
41
+ * API endpoints accept a language code but respond with Chinese text when when English is requested
42
+
43
+ # Usage
44
+
45
+ ## Installation
46
+
47
+ ```
48
+ pip install pysolarcloud
49
+ ```
50
+
51
+ ## Register your app
52
+ 1. Create an account in the [iSolarCloud Developer Portal](https://developer-api.isolarcloud.com/)
53
+ 2. Create an app in the developer portal
54
+ * Answer "Yes" to authorize with OAuth2.0
55
+ * Enter a Redirect URL for your app (this can be changed later)
56
+ 3. Wait for approval by Sungrow
57
+ 4. Find the needed configuration details in the developer portal. You will need:
58
+ * Appkey
59
+ * Secret Key
60
+ * Application Id (This is shown as a query parameter within the Authorize URL in the developer portal)
61
+
62
+ ## Example
63
+
64
+ ```python
65
+ from pysolarcloud import Auth, Server
66
+ from pysolarcloud.plants import Plants
67
+
68
+ app_key = "your app key"
69
+ secret_key = "your secret key"
70
+ app_id = "your app id"
71
+ redirect_uri = "your redirect uri"
72
+
73
+ auth = Auth(Server.Europe, app_key, secret_key, app_id)
74
+ url = Auth.auth_url(redirect_uri)
75
+ ```
76
+ 1. Redirect user to `url`
77
+ 2. User selects plant(s) and grants authorisation
78
+ 3. iSolarCloud will redirect the user to `redirect_uri` with query parameter `code`
79
+ ```python
80
+ await auth.async_authorize(code, redirect_uri)
81
+ plants_api = Plants(auth)
82
+ plant_list = await plants_api.async_get_plants()
83
+ ```
84
+
85
+ The `Auth` class keeps the access between calls and refreshes it when needed. If you prefer to manage this state yourself, you can create your own subclass of `AbstractAuth`.
86
+
87
+ # Contributions
88
+ Ideas or contributions are welcome. I am not afiliated with Sungrow, I'm just another user of the API. My main use case will be a HomeAssistant integration based on this package.
89
+
90
+ I don't currently have a need for the Grid Control APIs and I might not be able to test them on my own plant[^1] - but let me know if you are interested.
91
+
92
+ [^1]: because it's controlled by [Heartbeat](https://1komma5.com/en/offer/energymanager-heartbeat/)
93
+
94
+ Enjoy!
@@ -0,0 +1,71 @@
1
+ # pysolarcloud
2
+
3
+ A Python package to interact with the [iSolarCloud API](https://developer-api.isolarcloud.com/) by Sungrow.
4
+
5
+ The current version has only very basic functionality:
6
+ * OAuth2 authentication
7
+ * Getting a list plants
8
+ * Getting details of a plant
9
+ * Getting "real-time" data of a plant (Data is updated every 5 minutes according to Sungrow's documentation)
10
+
11
+ ## Quirks
12
+ The iSolarCloud API is quite new and not very mature. Some tips:
13
+ * The authorisation flow is based on OAuth2 but doesn't work exactly as you would expect
14
+ * The `state` parameter is not passed back after to the authorisation step. This makes it more tricky to resume the flow in a client application.
15
+ * User is asked to approve the authorisation if the flow is invoked again, e.g. in case the tokens have expired - unlike many OAuth2 implementations who will perform a "silent" authorisation if the user has already approved the access.
16
+ * The API documentation lists a lot of data points which do not seem to be returned from my inverter, it probably varies between models.
17
+ * There are different iSolarCloud servers for different regions, see the `pysolarcloud.Server` enum
18
+ * API endpoints accept a language code but respond with Chinese text when when English is requested
19
+
20
+ # Usage
21
+
22
+ ## Installation
23
+
24
+ ```
25
+ pip install pysolarcloud
26
+ ```
27
+
28
+ ## Register your app
29
+ 1. Create an account in the [iSolarCloud Developer Portal](https://developer-api.isolarcloud.com/)
30
+ 2. Create an app in the developer portal
31
+ * Answer "Yes" to authorize with OAuth2.0
32
+ * Enter a Redirect URL for your app (this can be changed later)
33
+ 3. Wait for approval by Sungrow
34
+ 4. Find the needed configuration details in the developer portal. You will need:
35
+ * Appkey
36
+ * Secret Key
37
+ * Application Id (This is shown as a query parameter within the Authorize URL in the developer portal)
38
+
39
+ ## Example
40
+
41
+ ```python
42
+ from pysolarcloud import Auth, Server
43
+ from pysolarcloud.plants import Plants
44
+
45
+ app_key = "your app key"
46
+ secret_key = "your secret key"
47
+ app_id = "your app id"
48
+ redirect_uri = "your redirect uri"
49
+
50
+ auth = Auth(Server.Europe, app_key, secret_key, app_id)
51
+ url = Auth.auth_url(redirect_uri)
52
+ ```
53
+ 1. Redirect user to `url`
54
+ 2. User selects plant(s) and grants authorisation
55
+ 3. iSolarCloud will redirect the user to `redirect_uri` with query parameter `code`
56
+ ```python
57
+ await auth.async_authorize(code, redirect_uri)
58
+ plants_api = Plants(auth)
59
+ plant_list = await plants_api.async_get_plants()
60
+ ```
61
+
62
+ The `Auth` class keeps the access between calls and refreshes it when needed. If you prefer to manage this state yourself, you can create your own subclass of `AbstractAuth`.
63
+
64
+ # Contributions
65
+ Ideas or contributions are welcome. I am not afiliated with Sungrow, I'm just another user of the API. My main use case will be a HomeAssistant integration based on this package.
66
+
67
+ I don't currently have a need for the Grid Control APIs and I might not be able to test them on my own plant[^1] - but let me know if you are interested.
68
+
69
+ [^1]: because it's controlled by [Heartbeat](https://1komma5.com/en/offer/energymanager-heartbeat/)
70
+
71
+ Enjoy!
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "pysolarcloud"
3
+ version = "0.1.0"
4
+ authors = [
5
+ { name="Tore Green", email="bugjam@e-dreams.dk" },
6
+ ]
7
+ description = "A library to interact with Sungrow's iSolarCloud API"
8
+ readme = "README.md"
9
+ requires-python = ">=3.7"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ "Framework :: AsyncIO",
14
+ "Topic :: Home Automation",
15
+ ]
16
+ license = { text = "MIT" }
17
+ dynamic = [
18
+ "dependencies",
19
+ "optional-dependencies"
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["setuptools"]
24
+ build-backend = "setuptools.build_meta"
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/bugjam/pysolarcloud"
28
+ Issues = "https://github.com/bugjam/pysolarcloud/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="pysolarcloud",
5
+ version="0.1.0",
6
+ packages=find_packages(where="src"),
7
+ package_dir={"": "src"},
8
+ install_requires=[
9
+ "aiohttp",
10
+ ],
11
+ extras_require={
12
+ "dev": [
13
+ "pytest",
14
+ "pytest-asyncio",
15
+ ],
16
+ },
17
+ entry_points={
18
+ "console_scripts": [
19
+ # Add any command-line scripts here
20
+ ],
21
+ },
22
+ classifiers=[
23
+ "Programming Language :: Python :: 3",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ ],
27
+ python_requires=">=3.7",
28
+ )
@@ -0,0 +1,140 @@
1
+ """A Python library to interact with Sungrow's iSolarCloud API."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import StrEnum
5
+ import logging
6
+ import time
7
+
8
+ from aiohttp import ClientResponse, ClientSession
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+ class Server(StrEnum):
13
+ """Enum of iSolarCloud servers."""
14
+ China = "https://gateway.isolarcloud.com"
15
+ International = "https://gateway.isolarcloud.com.hk"
16
+ Europe = "https://gateway.isolarcloud.eu"
17
+ Australia = "https://augateway.isolarcloud.com"
18
+
19
+ class AbstractAuth(ABC):
20
+ """Abstract class to make authenticated requests.
21
+
22
+ Subclasses must implement the async_get_access_token method
23
+ and may call async_fetch_tokens and async_refresh_token.
24
+ """
25
+
26
+ def __init__(self, websession: ClientSession, server: Server | str, client_id: str, client_secret: str, app_id: str):
27
+ """Initialize the authorization session."""
28
+ self.websession = websession
29
+ self.host = server.value if isinstance(server, Server) else server
30
+ self.appkey = client_id
31
+ self.access_key = client_secret
32
+ self.app_id = app_id
33
+
34
+ def auth_url(self, redirect_uri: str) -> str:
35
+ """Return the URL to authorize the user."""
36
+ match self.host:
37
+ case Server.China.value:
38
+ cloud_id = 1
39
+ case Server.International.value:
40
+ cloud_id = 2
41
+ case Server.Europe.value:
42
+ cloud_id = 3
43
+ case Server.Australia.value:
44
+ cloud_id = 4
45
+ return f"https://web3.isolarcloud.eu/#/authorized-app?cloudId={cloud_id}&applicationId={self.app_id}&redirectUrl={redirect_uri}"
46
+
47
+ @abstractmethod
48
+ async def async_get_access_token(self) -> str:
49
+ """Return a valid access token."""
50
+
51
+ async def request(self, path, data, *, lang="_en_US", **kwargs) -> ClientResponse:
52
+ """Make a request to iSolarCloud.
53
+
54
+ Parameters:
55
+ path -- the path to request
56
+ data -- the data to send
57
+ lang -- the language to use (default "_en_US", supported languages are "_en_US", "_zh_CN", "_ja_JP", "_es_ES", "_de_DE", "_pt_BR", "_fr_FR", "_it_IT", "_ko_KR", "_nl_NL", "_pl_PL", "_vi_VN", "_zh_TW"
58
+ **kwargs -- additional arguments to pass to the request
59
+ """
60
+ if not path.startswith("/"):
61
+ path = f"/{path}"
62
+ if headers := kwargs.pop("headers", {}):
63
+ headers = dict(headers)
64
+ access_token = await self.async_get_access_token()
65
+ headers = {**headers, "x-access-key": self.access_key, "Authorization": f"Bearer {access_token}"}
66
+ body = {**data, "appkey": self.appkey, "lang": lang}
67
+ return await self.websession.request(
68
+ "post", f"{self.host}{path}", json=body, **kwargs, headers=headers,
69
+ )
70
+
71
+ async def async_fetch_tokens(self, code, redirect_uri, **kwargs) -> ClientResponse:
72
+ """Fetch the access and refresh tokens."""
73
+ if headers := kwargs.pop("headers", {}):
74
+ headers = dict(headers)
75
+ headers = {**headers, "x-access-key": self.access_key, "Content-type": "application/json"}
76
+ body = {
77
+ "appkey": self.appkey,
78
+ "code": code,
79
+ "grant_type": "authorization_code",
80
+ "redirect_uri": redirect_uri
81
+ }
82
+ response = await self.websession.request("post", f"{self.host}/openapi/apiManage/token", json=body, headers=headers, **kwargs)
83
+ return await response.json()
84
+
85
+ async def async_refresh_tokens(self, refresh_token, **kwargs) -> ClientResponse:
86
+ """Refresh the access token."""
87
+ if headers := kwargs.pop("headers", {}):
88
+ headers = dict(headers)
89
+ headers = {**headers, "x-access-key": self.access_key}
90
+ body = {
91
+ "appkey": self.appkey,
92
+ "refresh_token": refresh_token
93
+ }
94
+ response = await self.websession.request("post", f"{self.host}/openapi/apiManage/refreshToken", json=body, **kwargs, headers=headers)
95
+ return await response.json()
96
+
97
+ class Auth(AbstractAuth):
98
+ """Class to authenticate with the SolarCloud API."""
99
+
100
+ def __init__(self, host: str, appkey: str, access_key: str, app_id: str, *, websession: ClientSession = None):
101
+ """Initialize the auth."""
102
+ if websession is None:
103
+ websession = ClientSession(raise_for_status=True)
104
+ super().__init__(websession, host, appkey, access_key, app_id)
105
+ self.tokens = None
106
+
107
+ async def async_authorize(self, code, redirect_uri):
108
+ """Authorize the user."""
109
+ ts = await self.async_fetch_tokens(code, redirect_uri)
110
+ print(ts)
111
+ if "access_token" not in ts:
112
+ _LOGGER.error("Authorization failed: %s", str(ts))
113
+ return
114
+ self.tokens = {
115
+ "access_token": ts["access_token"],
116
+ "refresh_token": ts["refresh_token"],
117
+ "expires_at": int(time.time()) + ts["expires_in"] - 20,
118
+ }
119
+ _LOGGER.debug("Authorization succesful")
120
+
121
+ async def async_get_access_token(self) -> str:
122
+ """Return a valid access token."""
123
+ if self.tokens is None:
124
+ raise PySolarCloudException({"error": "auth_not_initialised", "error_description": "You must authorize first."})
125
+ if self.tokens["expires_at"] < int(time.time()):
126
+ ts = await self.async_refresh_tokens(self.tokens["refresh_token"])
127
+ self.tokens = {
128
+ "access_token": ts["access_token"],
129
+ "refresh_token": ts["refresh_token"],
130
+ "expires_at": int(time.time()) + ts["expires_in"] - 20,
131
+ }
132
+ return self.tokens["access_token"]
133
+
134
+ class PySolarCloudException(Exception):
135
+ """Exception class raised by PySolarCloud when communication with the iSolarCloud service fails."""
136
+ def __init__(self, err: dict):
137
+ super().__init__(err["error"])
138
+ self.error = err["error"]
139
+ self.error_description = err["error_description"]
140
+ self.req_serial_num = err.get("req_serial_num", None)
@@ -0,0 +1,170 @@
1
+ from . import AbstractAuth, PySolarCloudException, _LOGGER
2
+
3
+ class Plants:
4
+ """Class to interact with the plants API."""
5
+
6
+ def __init__(self, auth: AbstractAuth, *, lang: str = "_en_US"):
7
+ """Initialize the plants."""
8
+ self.auth = auth
9
+ self.lang = lang
10
+
11
+ async def async_get_plants(self) -> list[dict]:
12
+ """Return the list of plants accessible to the user."""
13
+ uri = "/openapi/platform/queryPowerStationList"
14
+ res = await self.auth.request(uri, {"page": 1, "size": 100})
15
+ res.raise_for_status()
16
+ data = await res.json()
17
+ if "error" in data:
18
+ _LOGGER.error("Error response from %s: %s", uri, data)
19
+ raise PySolarCloudException(res)
20
+ plants = [plant for plant in data["result_data"]["pageList"]]
21
+ _LOGGER.debug("async_get_plants: %s", plants)
22
+ return plants
23
+
24
+ async def async_get_plant_details(self, plant_id: str | list[str]) -> list[dict]:
25
+ """Return details about one or more plants."""
26
+ if isinstance(plant_id, list):
27
+ ps = ",".join(plant_id)
28
+ else:
29
+ ps = plant_id
30
+ uri = "/openapi/platform/getPowerStationDetail"
31
+ res = await self.auth.request(uri, {"ps_ids": ps})
32
+ data = await res.json()
33
+ if "error" in data:
34
+ _LOGGER.error("Error response from %s: %s", uri, res)
35
+ raise PySolarCloudException(res)
36
+ plants = data["result_data"]["data_list"]
37
+ _LOGGER.debug("async_get_plant_details: %s", plants)
38
+ return plants
39
+
40
+ async def async_get_realtime_data(self, plant_id: str | list[str], *, measure_points=None) -> dict:
41
+ """Return the latest realtime data from one or more plants.
42
+
43
+ plant_id: str | list[str] - The ID of the plant or a list of plant IDs.
44
+ measure_points: list[str] - A list of measure points to return. If None, all measure points are returned.
45
+ Data is returned as a dictionary of dictionaries:
46
+ {
47
+ plant_id: {
48
+ measure_point_code: {
49
+ "id": str, # Numerical identifier of the measure point
50
+ "code": str, # Readable code of the measure point (see measure_points dict)
51
+ "value": float | str,
52
+ "unit": str,
53
+ "name": str, # Name of the measure point (in the specified language)
54
+ }
55
+ }
56
+ }
57
+ iSolarCloud data is updated every 5 minutes so polling more frequently than that is not useful.
58
+ """
59
+ if isinstance(plant_id, list):
60
+ ps = plant_id
61
+ else:
62
+ ps = [plant_id]
63
+ if measure_points is None:
64
+ ms = list(self.measure_points.keys())
65
+ else:
66
+ measure_points_map = {v: k for k, v in self.measure_points.items()}
67
+ ms = [m if m.isdigit() else measure_points_map[m] for m in measure_points]
68
+ uri = "/openapi/platform/getPowerStationRealTimeData"
69
+ res = await self.auth.request(uri, {"ps_id_list": ps, "point_id_list": ms, "is_get_point_dict": "1"}, lang=self.lang)
70
+ res = await res.json()
71
+ if "error" in res:
72
+ _LOGGER.error("Error response from %s: %s", uri, res)
73
+ raise PySolarCloudException(res)
74
+ point_dict = dict([(str(point["point_id"]), point) for point in res["result_data"]["point_dict"]])
75
+ plants = {}
76
+ for plant in res["result_data"]["device_point_list"]:
77
+ data = [self._format_measure_point(k[1:], v, point_dict) for k,v in plant.items() if k[0]=='p' and k[1:].isdigit()]
78
+ data_as_dict = {d["code"]: d for d in data}
79
+ plants[str(plant["ps_id"])] = data_as_dict
80
+ _LOGGER.debug("async_get_realtime_data: %s", plants)
81
+ return plants
82
+
83
+ def _format_measure_point(self, point_id: str, point_value: str, point_dict: dict) -> dict:
84
+ try:
85
+ v = float(point_value) if point_value is not None else None
86
+ except ValueError:
87
+ v = point_value
88
+ return {
89
+ "id": point_id,
90
+ "code": self.measure_points.get(point_id, point_id),
91
+ "value": v,
92
+ "unit": point_dict.get(point_id, {}).get("point_unit", None),
93
+ "name": point_dict.get(point_id, {}).get("point_name", None),
94
+ }
95
+
96
+ measure_points = {
97
+ "83022": "daily_yield", # Wh
98
+ "83024": "total_yield", # Wh
99
+ "83033": "power", # W
100
+ "83019": "power_fraction", # Plant Power/Installed Power of Plant
101
+ "83006": "meter_daily_yield", # Wh
102
+ "83020": "meter_total_yield", # Wh
103
+ "83011": "meter_e_daily_consumption", # Wh
104
+ "83021": "accumulative_power_consumption_by_meter", # Wh
105
+ "83032": "meter_ac_power", # W
106
+ "83007": "meter_pr", #
107
+ "83002": "inverter_ac_power", # W
108
+ "83009": "inverter_daily_yield", # Wh
109
+ "83004": "inverter_total_yield", # Wh
110
+ "83012": "p_radiation_h", # W/㎡
111
+ "83013": "daily_irradiation", # Wh/㎡
112
+ "83023": "plant_pr", #
113
+ "83005": "daily_equivalent_hours", # h
114
+ "83025": "plant_equivalent_hours", # h
115
+ "83018": "daily_yield_theoretical", # Wh
116
+ "83001": "inverter_ac_power_normalization", # W/Wp
117
+ "83008": "daily_equivalent_hours_of_inverter", # h
118
+ "83010": "inverter_pr", #
119
+ "83016": "plant_ambient_temperature", # ℃
120
+ "83017": "plant_module_temperature", # ℃
121
+ "83046": "pcs_total_active_power", # W
122
+ "83052": "total_load_active_power", # W
123
+ "83067": "total_active_power_of_pv", # W
124
+ "83097": "daily_direct_energy_consumption", # Wh
125
+ "83100": "total_direct_energy_consumption", # Wh
126
+ "83102": "energy_purchased_today", # Wh
127
+ "83105": "total_purchased_energy", # Wh
128
+ "83106": "load_power", # W
129
+ "83118": "daily_load_consumption", # Wh
130
+ "83124": "total_load_consumption", # Wh
131
+ "83119": "daily_feed_in_energy_pv", # Wh
132
+ "83072": "feed_in_energy_today", # Wh
133
+ "83075": "feed_in_energy_total", # Wh
134
+ "83252": "battery_level_soc", #
135
+ "83129": "battery_soc", #
136
+ "83232": "total_field_soc", #
137
+ "83233": "total_field_maximum_rechargeable_power", # W
138
+ "83234": "total_field_maximum_dischargeable_power", # W
139
+ "83235": "total_field_chargeable_energy", # Wh
140
+ "83236": "total_field_dischargeable_energy", # Wh
141
+ "83237": "total_field_energy_storage_maximum_reactive_power", # W
142
+ "83238": "total_field_energy_storage_active_power", # W
143
+ "83239": "total_field_reactive_power", # var
144
+ "83240": "total_field_power_factor", #
145
+ "83243": "daily_field_charge_capacity", # Wh
146
+ "83241": "total_field_charge_capacity", # Wh
147
+ "83244": "daily_field_discharge_capacity", # Wh
148
+ "83242": "total_field_discharge_capacity", # Wh
149
+ "83548": "total_number_of_charge_discharge", #
150
+ "83549": "grid_active_power", # W
151
+ "83419": "daily_highest_inverter_power_inverter_installed_capacity", #
152
+ "83317": "power_forecast", # W
153
+ "83318": "planned_es_charging_discharging_power", # W
154
+ "83319": "planned_es_soc", #
155
+ "83320": "planned_charging_power", # Wh
156
+ "83321": "planned_discharging_power", # Wh
157
+ "83322": "ess_daily_charge_ems", # Wh
158
+ "83324": "energy_storage_cumulative_charge", # Wh
159
+ "83323": "ess_daily_discharge_ems", # Wh
160
+ "83325": "cumulative_discharge", # Wh
161
+ "83327": "energy_storage_remaining_charge", # Wh
162
+ "83326": "energy_storage_active_power_ems", # W
163
+ "83328": "grid_active_power_ems", # W
164
+ "83329": "pv_active_power_ems", # W
165
+ "83330": "load_active_power_ems", # W
166
+ "83331": "daily_pv_yield_ems", # Wh
167
+ "83332": "total_pv_yield", # Wh
168
+ "83334": "energy_storage_soc_ems", #
169
+ "83335": "energy_storage_remaining_charge_ems", # Wh
170
+ }
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.2
2
+ Name: pysolarcloud
3
+ Version: 0.1.0
4
+ Summary: A library to interact with Sungrow's iSolarCloud API
5
+ Author-email: Tore Green <bugjam@e-dreams.dk>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bugjam/pysolarcloud
8
+ Project-URL: Issues, https://github.com/bugjam/pysolarcloud/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Topic :: Home Automation
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: aiohttp
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-asyncio; extra == "dev"
20
+ Dynamic: provides-extra
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+
24
+ # pysolarcloud
25
+
26
+ A Python package to interact with the [iSolarCloud API](https://developer-api.isolarcloud.com/) by Sungrow.
27
+
28
+ The current version has only very basic functionality:
29
+ * OAuth2 authentication
30
+ * Getting a list plants
31
+ * Getting details of a plant
32
+ * Getting "real-time" data of a plant (Data is updated every 5 minutes according to Sungrow's documentation)
33
+
34
+ ## Quirks
35
+ The iSolarCloud API is quite new and not very mature. Some tips:
36
+ * The authorisation flow is based on OAuth2 but doesn't work exactly as you would expect
37
+ * The `state` parameter is not passed back after to the authorisation step. This makes it more tricky to resume the flow in a client application.
38
+ * User is asked to approve the authorisation if the flow is invoked again, e.g. in case the tokens have expired - unlike many OAuth2 implementations who will perform a "silent" authorisation if the user has already approved the access.
39
+ * The API documentation lists a lot of data points which do not seem to be returned from my inverter, it probably varies between models.
40
+ * There are different iSolarCloud servers for different regions, see the `pysolarcloud.Server` enum
41
+ * API endpoints accept a language code but respond with Chinese text when when English is requested
42
+
43
+ # Usage
44
+
45
+ ## Installation
46
+
47
+ ```
48
+ pip install pysolarcloud
49
+ ```
50
+
51
+ ## Register your app
52
+ 1. Create an account in the [iSolarCloud Developer Portal](https://developer-api.isolarcloud.com/)
53
+ 2. Create an app in the developer portal
54
+ * Answer "Yes" to authorize with OAuth2.0
55
+ * Enter a Redirect URL for your app (this can be changed later)
56
+ 3. Wait for approval by Sungrow
57
+ 4. Find the needed configuration details in the developer portal. You will need:
58
+ * Appkey
59
+ * Secret Key
60
+ * Application Id (This is shown as a query parameter within the Authorize URL in the developer portal)
61
+
62
+ ## Example
63
+
64
+ ```python
65
+ from pysolarcloud import Auth, Server
66
+ from pysolarcloud.plants import Plants
67
+
68
+ app_key = "your app key"
69
+ secret_key = "your secret key"
70
+ app_id = "your app id"
71
+ redirect_uri = "your redirect uri"
72
+
73
+ auth = Auth(Server.Europe, app_key, secret_key, app_id)
74
+ url = Auth.auth_url(redirect_uri)
75
+ ```
76
+ 1. Redirect user to `url`
77
+ 2. User selects plant(s) and grants authorisation
78
+ 3. iSolarCloud will redirect the user to `redirect_uri` with query parameter `code`
79
+ ```python
80
+ await auth.async_authorize(code, redirect_uri)
81
+ plants_api = Plants(auth)
82
+ plant_list = await plants_api.async_get_plants()
83
+ ```
84
+
85
+ The `Auth` class keeps the access between calls and refreshes it when needed. If you prefer to manage this state yourself, you can create your own subclass of `AbstractAuth`.
86
+
87
+ # Contributions
88
+ Ideas or contributions are welcome. I am not afiliated with Sungrow, I'm just another user of the API. My main use case will be a HomeAssistant integration based on this package.
89
+
90
+ I don't currently have a need for the Grid Control APIs and I might not be able to test them on my own plant[^1] - but let me know if you are interested.
91
+
92
+ [^1]: because it's controlled by [Heartbeat](https://1komma5.com/en/offer/energymanager-heartbeat/)
93
+
94
+ Enjoy!
@@ -0,0 +1,12 @@
1
+ LICENSE.txt
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ src/pysolarcloud/__init__.py
7
+ src/pysolarcloud/plants.py
8
+ src/pysolarcloud.egg-info/PKG-INFO
9
+ src/pysolarcloud.egg-info/SOURCES.txt
10
+ src/pysolarcloud.egg-info/dependency_links.txt
11
+ src/pysolarcloud.egg-info/requires.txt
12
+ src/pysolarcloud.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ aiohttp
2
+
3
+ [dev]
4
+ pytest
5
+ pytest-asyncio
@@ -0,0 +1 @@
1
+ pysolarcloud