Metadata-Version: 2.4
Name: sungrow-isolarcloud
Version: 0.7.0
Summary: A library to interact with Sungrow's iSolarCloud API (KRoperUK fork with battery, EV charger and dispatch extensions)
Author: KRoperUK
Author-email: Tore Green <bugjam@e-dreams.dk>
License: MIT
Project-URL: Homepage, https://github.com/KRoperUK/pysolarcloud
Project-URL: Issues, https://github.com/KRoperUK/pysolarcloud/issues
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Framework :: AsyncIO
Classifier: Topic :: Home Automation
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: aiohttp
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist

# sungrow-isolarcloud

A maintained fork of the [pysolarcloud](https://github.com/bugjam/pysolarcloud) library for interacting with Sungrow's [iSolarCloud API](https://developer-api.isolarcloud.com/).

Install from PyPI:

```
pip install sungrow-isolarcloud
```

This fork adds:
* Support for requesting **additional / custom measure points** without modifying the upstream point map (useful for battery charge/discharge power fields that vary by inverter model).
* A best-effort **per-device realtime** helper for devices such as EV chargers (`Plants.async_get_device_realtime`).
* A **heartbeat** helper for External EMS dispatch mode (`Control.async_heartbeat` / `Control.heartbeat_loop`).
* Convenience constants for dispatch command value sets (`Control.CHARGE_DISCHARGE_COMMANDS`, `Control.FORCED_CHARGING`).

The package supports the following functionality:
* OAuth2 authentication
* Getting a list plants
* Getting details of a plant
* Getting devices of a plant
* Getting "real-time" data of a plant (Data is updated every 5 minutes according to Sungrow's documentation)
* Getting historical data
* Getting and updating grid control settings

## Quirks
The iSolarCloud API is quite new and not very mature. Some tips:
* The authorisation flow is based on OAuth2 but doesn't work exactly as you would expect
* 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.
* 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.
* The API documentation lists a lot of data points which do not seem to be returned from my inverter, it probably varies between models.
* There are different iSolarCloud servers for different regions, see the `pysolarcloud.Server` enum
* API endpoints accept a language code but respond with Chinese text when when English is requested

# Usage

## Register your app
1. Create an account in the [iSolarCloud Developer Portal](https://developer-api.isolarcloud.com/)
2. Create an app in the developer portal
   * Answer "Yes" to authorize with OAuth2.0
   * Enter a Redirect URL for your app (this can be changed later)
3. Wait for approval by Sungrow
4. Find the needed configuration details in the developer portal. You will need:
   * Appkey
   * Secret Key
   * Application Id (This is shown as a query parameter within the Authorize URL in the developer portal)

## Example

```python
from pysolarcloud import Auth, Server
from pysolarcloud.plants import Plants

app_key = "your app key"
secret_key = "your secret key"
app_id = "your app id"
redirect_uri = "your redirect uri"

auth = Auth(Server.Europe, app_key, secret_key, app_id)
url = auth.auth_url(redirect_uri)
```
1. Redirect user to `url`
2. User selects plant(s) and grants authorisation
3. iSolarCloud will redirect the user to `redirect_uri` with query parameter `code`
```python
await auth.async_authorize(code, redirect_uri)
plants_api = Plants(auth)
plant_list = await plants_api.async_get_plants()
if plant_list:
   print(f"{len(plant_list)} plants found:")
   for plant in plant_list:
         print(f"Plant ID: {plant["ps_id"]}, Name: {plant["ps_name"]}")
else:
   print("No plants found.")
   return

print("\nFetching detailed information for each plant...\n")
plant_ids = [str(plant["ps_id"]) for plant in plant_list]
plant_details = await plants_api.async_get_plant_details(plant_ids)
for plant in plant_details:
   print(f"Details for Plant ID {plant["ps_id"]}: {plant}")

print("\nFetching real-time data for each plant...\n")
real_time_data = await plants_api.async_get_realtime_data(plant_ids)
for plant_id, data in real_time_data.items():
   # Print only the data points where value is not None
   data_values = {k: v for k, v in data.items() if v and v.get("value") is not None}
   print(f"Real-time data for Plant ID {plant_id}: {data_values}")
```

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`.

## Grid Control

The `Control` class enables retrieving and updating grid control settings. Parameters and value sets are documented in the iSolarCloud Developer portal.

### Example

```python
from pysolarcloud.control import Control
from pysolarcloud.plants import DeviceType

devices = await plants_api.async_get_plant_devices(plant_id, device_types=[DeviceType.ENERGY_STORAGE_SYSTEM])
device_uuid = devices[0]["uuid"]
control_api = Control(auth)
# Fetch current config
current_settings = await control_api.async_read_parameters(device_uuid)
print(current_settings)
# Make an update using the canonical command values
await control_api.async_update_parameters(
    device_uuid,
    {
        "charge_discharge_command": Control.CHARGE_DISCHARGE_COMMANDS["charge"],
        "charge_discharge_power": "2500",
    },
)

# When using External EMS mode, send a heartbeat periodically to keep the inverter in dispatch mode.
# 10017 = external_ems_heartbeat, value is the heartbeat interval in seconds (1-1000).
await control_api.async_heartbeat(device_uuid, interval_seconds=60)
```

# Contributions
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.

Enjoy!
