From ace52f3e020ed2ce88d1e1a7e266c2d8e3cd01c8 Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Thu, 8 Jan 2026 21:58:02 +0100 Subject: [PATCH 1/5] Add V1 API support for SPH devices Implements complete V1 API support for SPH (type 5) hybrid inverters, parallel to existing MIN device support. Changes: - Add DeviceType enum to distinguish device types - Implement 10 SPH methods in OpenApiV1: * sph_detail() - Get device details * sph_energy() - Get current energy data * sph_energy_history() - Get historical data (7-day max) * sph_settings() - Get all inverter settings * sph_read_parameter() - Read specific parameters * sph_write_parameter() - Write parameters * sph_write_ac_charge_time() - Configure AC charge periods (1-3) * sph_write_ac_discharge_time() - Configure AC discharge periods (1-3) * sph_read_ac_charge_times() - Read all charge periods * sph_read_ac_discharge_times() - Read all discharge periods - Add documentation to docs/openapiv1.md - Include working example script (examples/sph_example.py) Technical details: - SPH devices use 'mix' endpoints internally (device/mix/*) - AC charge/discharge periods support 3 time windows each - Methods follow same patterns as existing MIN implementation - All endpoints match official Growatt V1 API documentation --- README.md | 2 +- docs/openapiv1.md | 12 + docs/openapiv1/sph_settings.md | 54 +++ examples/sph_example.py | 135 ++++++++ growattServer/__init__.py | 4 +- growattServer/open_api_v1.py | 592 ++++++++++++++++++++++++++++++--- 6 files changed, 756 insertions(+), 43 deletions(-) create mode 100644 docs/openapiv1/sph_settings.md create mode 100644 examples/sph_example.py diff --git a/README.md b/README.md index da42e85..b0a4896 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Package to retrieve PV information from the growatt server. Special thanks to [Sjoerd Langkemper](https://github.com/Sjord) who has provided a strong base to start off from https://github.com/Sjord/growatt_api_client That project has since ben archived. -This library now supports both the legacy password-based authentication and the V1 API with token-based authentication for MIN systems (TLX are identified as MIN system in the public API). Certain endpoints are not supported anymore by openapi.growatt.com. For example `api.min_write_parameter()` should be used instead of old `api.update_tlx_inverter_setting()`. +This library now supports both the legacy password-based authentication and the V1 API with token-based authentication for MIN and SPH systems. MIN devices (type 7) correspond to MIN/MID series inverters, while SPH devices (type 5) are hybrid inverters. The V1 API is officially supported by Growatt and recommended over legacy authentication. Certain endpoints are not supported anymore by openapi.growatt.com. For example `api.min_write_parameter()` should be used instead of old `api.update_tlx_inverter_setting()`. ## Usage diff --git a/docs/openapiv1.md b/docs/openapiv1.md index 61f49d1..1f56bfc 100644 --- a/docs/openapiv1.md +++ b/docs/openapiv1.md @@ -2,6 +2,8 @@ This version of the API follows the newer [OpenAPI V1 API](https://www.showdoc.com.cn/262556420217021/0) Growatt has made available. +Currently supports MIN (type 7) and SPH (type 5) devices. MIN devices correspond to MIN/MID series inverters (using classic TLX endpoints), while SPH devices are hybrid inverters (using classic MIX endpoints). The V1 API is officially supported by Growatt and offers better security, more features, and relaxed rate limiting compared to the legacy API. + It extends our ["Legacy" ShinePhone](./shinephone.md) so methods from [there](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. ## Usage @@ -38,6 +40,16 @@ Any methods that may be useful. | `api.min_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on a min inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_write_time_segment(device_sn, segment_id, batt_mode, start_time, end_time, enabled=True)` | device_sn: String, segment_id: Int, batt_mode: Int <0=load priority, 1=battery priority, 2=grid priority>, start_time: Time, end_time: Time, enabled: Bool | Update a specific time segment for a min inverter. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_read_time_segments(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all time segments from a MIN inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/min_tlx_settings.md) | +| `api.sph_detail(device_sn)` | device_sn: String | Get detailed data for an SPH hybrid inverter. | +| `api.sph_energy(device_sn)` | device_sn: String | Get current energy data for an SPH inverter, including power and energy values. | +| `api.sph_energy_history(device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None)` | device_sn: String, start_date: Date, end_date: Date, timezone: String, page: Int, limit: Int | Get energy history data for an SPH inverter (7-day max range). | +| `api.sph_settings(device_sn)` | device_sn: String | Get all settings for an SPH inverter. | +| `api.sph_read_parameter(device_sn, parameter_id, start_address=None, end_address=None)` | device_sn: String, parameter_id: String, start_address: Int, end_address: Int | Read a specific setting for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on an SPH inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_ac_charge_time(device_sn, period_id, charge_power, charge_stop_soc, start_time, end_time, mains_enabled=True, enabled=True)` | device_sn: String, period_id: Int (1-3), charge_power: Int (0-100), charge_stop_soc: Int (0-100), start_time: Time, end_time: Time, mains_enabled: Bool, enabled: Bool | Configure an AC charge time period for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_ac_discharge_time(device_sn, period_id, discharge_power, discharge_stop_soc, start_time, end_time, enabled=True)` | device_sn: String, period_id: Int (1-3), discharge_power: Int (0-100), discharge_stop_soc: Int (0-100), start_time: Time, end_time: Time, enabled: Bool | Configure an AC discharge time period for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_charge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC charge time periods from an SPH inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_discharge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC discharge time periods from an SPH inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | Methods from [here](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. There is no guarantee those methods will work, or remain stable through updates. diff --git a/docs/openapiv1/sph_settings.md b/docs/openapiv1/sph_settings.md new file mode 100644 index 0000000..71af73a --- /dev/null +++ b/docs/openapiv1/sph_settings.md @@ -0,0 +1,54 @@ +# SPH Inverter Settings + +This is part of the [OpenAPI V1 doc](../openapiv1.md). + +For SPH (hybrid inverter) systems, the public V1 API provides methods to read and write inverter settings. SPH inverters have different time period configurations compared to MIN inverters: + +* **Read Parameter** + * function: `api.sph_read_parameter` + * parameters: + * `device_sn`: The device serial number + * `parameter_id`: Parameter ID to read (e.g., "discharge_power") + * `start_address`, `end_address`: Optional, for reading registers by address + +* **Write Parameter** + * function: `api.sph_write_parameter` + * parameters: + * `device_sn`: The device serial number + * `parameter_id`: Parameter ID to write (e.g., "ac_charge") + * `parameter_values`: Value to set (single value, list, or dictionary) + +* **AC Charge Time Periods** + * function: `api.sph_write_ac_charge_time` + * parameters: + * `device_sn`: The device serial number + * `period_id`: Period number (1-3) + * `charge_power`: Charging power percentage (0-100) + * `charge_stop_soc`: Stop charging at this SOC percentage (0-100) + * `start_time`: Datetime.time object for period start + * `end_time`: Datetime.time object for period end + * `mains_enabled`: Boolean to enable/disable grid charging (default: True) + * `enabled`: Boolean to enable/disable period (default: True) + +* **AC Discharge Time Periods** + * function: `api.sph_write_ac_discharge_time` + * parameters: + * `device_sn`: The device serial number + * `period_id`: Period number (1-3) + * `discharge_power`: Discharge power percentage (0-100) + * `discharge_stop_soc`: Stop discharging at this SOC percentage (0-100) + * `start_time`: Datetime.time object for period start + * `end_time`: Datetime.time object for period end + * `enabled`: Boolean to enable/disable period (default: True) + +* **Read AC Charge Time Periods** + * function: `api.sph_read_ac_charge_times` + * parameters: + * `device_sn`: The device serial number + * `settings_data`: Optional settings data to avoid redundant API calls + +* **Read AC Discharge Time Periods** + * function: `api.sph_read_ac_discharge_times` + * parameters: + * `device_sn`: The device serial number + * `settings_data`: Optional settings data to avoid redundant API calls diff --git a/examples/sph_example.py b/examples/sph_example.py new file mode 100644 index 0000000..bea3ee7 --- /dev/null +++ b/examples/sph_example.py @@ -0,0 +1,135 @@ +""" +Example script for SPH devices using the OpenAPI V1. + +This script demonstrates controlling SPH interface devices (device type 5) +such as hybrid inverter systems. +You can obtain an API token from the Growatt API documentation or developer portal. +""" + +import datetime +import json +from pathlib import Path + +import requests + +from . import growattServer + +# Get the API token from user input or environment variable +# api_token = os.environ.get("GROWATT_API_TOKEN") or input("Enter your Growatt API token: ") + +# test token from official API docs https://www.showdoc.com.cn/262556420217021/1494053950115877 +api_token = "6eb6f069523055a339d71e5b1f6c88cc" # noqa: S105 + +try: + # Initialize the API with token instead of using login + api = growattServer.OpenApiV1(token=api_token) + + # Plant info + plants = api.plant_list() + print(f"Plants: Found {plants['count']} plants") # noqa: T201 + plant_id = plants["plants"][0]["plant_id"] + + # Devices + devices = api.device_list(plant_id) + + for device in devices["devices"]: + print(device) # noqa: T201 + if device["type"] == growattServer.DeviceType.SPH.value: + inverter_sn = device["device_sn"] + print(f"Processing SPH device: {inverter_sn}") # noqa: T201 + + # Get device details + inverter_data = api.sph_detail( + device_sn=inverter_sn, + ) + print("Saving inverter data to inverter_data.json") # noqa: T201 + with Path("inverter_data.json").open("w") as f: + json.dump(inverter_data, f, indent=4, sort_keys=True) + + # Get energy data + energy_data = api.sph_energy( + device_sn=inverter_sn, + ) + print("Saving energy data to energy_data.json") # noqa: T201 + with Path("energy_data.json").open("w") as f: + json.dump(energy_data, f, indent=4, sort_keys=True) + + # Get energy history + energy_history_data = api.sph_energy_history( + device_sn=inverter_sn, + ) + print("Saving energy history data to energy_history.json") # noqa: T201 + with Path("energy_history.json").open("w") as f: + json.dump( + energy_history_data.get("datas", []), + f, + indent=4, + sort_keys=True, + ) + + # Get settings + settings_data = api.sph_settings( + device_sn=inverter_sn, + ) + print("Saving settings data to settings_data.json") # noqa: T201 + with Path("settings_data.json").open("w") as f: + json.dump(settings_data, f, indent=4, sort_keys=True) + + # Read AC charge time periods + charge_times = api.sph_read_ac_charge_times( + device_sn=inverter_sn, + settings_data=settings_data, + ) + print("AC Charge Time Periods:") # noqa: T201 + print(json.dumps(charge_times, indent=4)) # noqa: T201 + + # Read AC discharge time periods + discharge_times = api.sph_read_ac_discharge_times( + device_sn=inverter_sn, + settings_data=settings_data, + ) + print("AC Discharge Time Periods:") # noqa: T201 + print(json.dumps(discharge_times, indent=4)) # noqa: T201 + + # Read discharge power + discharge_power = api.sph_read_parameter( + device_sn=inverter_sn, + parameter_id="discharge_power", + ) + print(f"Current discharge power: {discharge_power}%") # noqa: T201 + + # Write examples - Uncomment to test + + # Set AC charge time period 1: charge at 50% power to 95% SOC between 00:00-06:00 + # api.sph_write_ac_charge_time( + # device_sn=inverter_sn, + # period_id=1, + # charge_power=50, # 50% charging power + # charge_stop_soc=95, # Stop at 95% SOC + # start_time=datetime.time(0, 0), + # end_time=datetime.time(6, 0), + # mains_enabled=True, # Enable grid charging + # enabled=True + # ) + # print("AC charge period 1 updated successfully") + + # Set AC discharge time period 1: discharge at 100% power to 20% SOC between 17:00-22:00 + # api.sph_write_ac_discharge_time( + # device_sn=inverter_sn, + # period_id=1, + # discharge_power=100, # 100% discharge power + # discharge_stop_soc=20, # Stop at 20% SOC + # start_time=datetime.time(17, 0), + # end_time=datetime.time(22, 0), + # enabled=True + # ) + # print("AC discharge period 1 updated successfully") + +except growattServer.GrowattV1ApiError as e: + print(f"API Error: {e} (Code: {e.error_code}, Message: {e.error_msg})") # noqa: T201 +except growattServer.GrowattParameterError as e: + print(f"Parameter Error: {e}") # noqa: T201 +except requests.exceptions.RequestException as e: + print(f"Network Error: {e}") # noqa: T201 +except Exception as e: # noqa: BLE001 + print(f"Unexpected error: {e}") # noqa: T201 diff --git a/growattServer/__init__.py b/growattServer/__init__.py index 927f010..75698ac 100755 --- a/growattServer/__init__.py +++ b/growattServer/__init__.py @@ -1,7 +1,7 @@ # Import everything from base_api to ensure backward compatibility from .base_api import * -# Import the V1 API class -from .open_api_v1 import OpenApiV1 +# Import the V1 API class and DeviceType enum +from .open_api_v1 import OpenApiV1, DeviceType # Import exceptions from .exceptions import GrowattError, GrowattParameterError, GrowattV1ApiError diff --git a/growattServer/open_api_v1.py b/growattServer/open_api_v1.py index 02ae3cc..3446fa6 100644 --- a/growattServer/open_api_v1.py +++ b/growattServer/open_api_v1.py @@ -1,14 +1,30 @@ import warnings from datetime import date, timedelta +from enum import Enum from . import GrowattApi import platform from .exceptions import GrowattParameterError, GrowattV1ApiError +class DeviceType(Enum): + """Enumeration of Growatt device types.""" + + INVERTER = 1 + STORAGE = 2 + OTHER = 3 + MAX = 4 + SPH = 5 # (MIX) + SPA = 6 + MIN = 7 + PCS = 8 + HPS = 9 + PBD = 10 + + class OpenApiV1(GrowattApi): """ Extended Growatt API client with V1 API support. - This class extends the base GrowattApi class with methods for MIN inverters using + This class extends the base GrowattApi class with methods for MIN and SPH devices using the public V1 API described here: https://www.showdoc.com.cn/262556420217021/0 """ @@ -136,45 +152,6 @@ def plant_energy_overview(self, plant_id): return self._process_response(response.json(), "getting plant energy overview") - def plant_power_overview(self, plant_id: int, day: str | date = None) -> dict: - """ - Obtain power data of a certain power station. - Get the frequency once every 5 minutes - - Args: - plant_id (int): Power Station ID - day (date): Date - defaults to today - - Returns: - dict: A dictionary containing the plants power data. - .. code-block:: python - - { - 'count': int, # Total number of records - 'powers': list[dict], # List of power data entries - # Each entry in 'powers' is a dictionary with: - # 'time': str, # Time of the power reading - # 'power': float | None # Power value in Watts (can be None) - } - Raises: - GrowattV1ApiError: If the API returns an error response. - requests.exceptions.RequestException: If there is an issue with the HTTP request. - - API-Doc: https://www.showdoc.com.cn/262556420217021/1494062656174173 - """ - if day is None: - day = date.today() - - response = self.session.get( - self._get_url('plant/power'), - params={ - 'plant_id': plant_id, - 'date': day, - } - ) - - return self._process_response(response.json(), "getting plant power overview") - def plant_energy_history(self, plant_id, start_date=None, end_date=None, time_unit="day", page=None, perpage=None): """ Retrieve plant energy data for multiple days/months/years. @@ -681,3 +658,538 @@ def min_read_time_segments(self, device_sn, settings_data=None): segments.append(segment) return segments + + # SPH Device Methods (Device Type 5) + + def sph_detail(self, device_sn): + """ + Get detailed data for an SPH inverter. + + Args: + device_sn (str): The serial number of the SPH inverter. + + Returns: + dict: A dictionary containing the SPH inverter details. + + Raises: + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + response = self.session.get( + self._get_url('device/mix/mix_data_info'), + params={ + 'device_sn': device_sn + } + ) + + return self._process_response(response.json(), "getting SPH inverter details") + + def sph_energy(self, device_sn): + """ + Get energy data for an SPH inverter. + + Args: + device_sn (str): The serial number of the SPH inverter. + + Returns: + dict: A dictionary containing the SPH inverter energy data. + + Raises: + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + response = self.session.post( + url=self._get_url("device/mix/mix_last_data"), + data={ + "mix_sn": device_sn, + }, + ) + + return self._process_response(response.json(), "getting SPH inverter energy data") + + def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None): + """ + Get SPH inverter data history. + + Args: + device_sn (str): The ID of the SPH inverter. + start_date (date, optional): Start date. Defaults to today. + end_date (date, optional): End date. Defaults to today. + timezone (str, optional): Timezone ID. + page (int, optional): Page number. + limit (int, optional): Results per page. + + Returns: + dict: A dictionary containing the SPH inverter history data. + + Raises: + GrowattParameterError: If date interval is invalid (exceeds 7 days). + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + if start_date is None and end_date is None: + start_date = date.today() + end_date = date.today() + elif start_date is None: + start_date = end_date + elif end_date is None: + end_date = start_date + + # check interval validity + if end_date - start_date > timedelta(days=7): + raise GrowattParameterError("date interval must not exceed 7 days") + + response = self.session.post( + url=self._get_url('device/mix/mix_data'), + data={ + "mix_sn": device_sn, + "start_date": start_date.strftime("%Y-%m-%d"), + "end_date": end_date.strftime("%Y-%m-%d"), + "timezone_id": timezone, + "page": page, + "perpage": limit, + } + ) + + return self._process_response(response.json(), "getting SPH inverter energy history") + + def sph_settings(self, device_sn): + """ + Get settings for an SPH inverter. + + Args: + device_sn (str): The serial number of the SPH inverter. + + Returns: + dict: A dictionary containing the SPH inverter settings. + + Raises: + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + response = self.session.get( + self._get_url('device/mix/mix_data_info'), + params={ + 'device_sn': device_sn + } + ) + + return self._process_response(response.json(), "getting SPH inverter settings") + + def sph_read_parameter(self, device_sn, parameter_id, start_address=None, end_address=None): + """ + Read setting from SPH inverter. + + Args: + device_sn (str): The ID of the SPH inverter. + parameter_id (str): Parameter ID to read. Don't use start_address and end_address if this is set. + start_address (int, optional): Register start address (for set_any_reg). Don't use parameter_id if this is set. + end_address (int, optional): Register end address (for set_any_reg). Don't use parameter_id if this is set. + + Returns: + dict: A dictionary containing the setting value. + + Raises: + GrowattParameterError: If parameters are invalid. + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + if parameter_id is None and start_address is None: + raise GrowattParameterError( + "specify either parameter_id or start_address/end_address") + elif parameter_id is not None and start_address is not None: + raise GrowattParameterError( + "specify either parameter_id or start_address/end_address - not both." + ) + elif parameter_id is not None: + # named parameter + start_address = 0 + end_address = 0 + else: + # address range + parameter_id = "set_any_reg" + + response = self.session.post( + self._get_url('readMixParam'), + data={ + "mix_sn": device_sn, + "type": parameter_id, + "param1": start_address, + "param2": end_address + } + ) + + return self._process_response(response.json(), f"reading parameter {parameter_id}") + + def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): + """ + Set parameters on an SPH inverter. + + Args: + device_sn (str): Serial number of the inverter + parameter_id (str): Setting type to be configured + parameter_values: Parameter values to be sent to the system. + Can be a single string (for param1 only), + a list of strings (for sequential params), + or a dictionary mapping param positions to values + + Returns: + dict: JSON response from the server + + Raises: + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + # Initialize all parameters as empty strings + parameters = {i: "" for i in range(1, 20)} + + # Process parameter values based on type + if parameter_values is not None: + if isinstance(parameter_values, (str, int, float, bool)): + # Single value goes to param1 + parameters[1] = str(parameter_values) + elif isinstance(parameter_values, list): + # List of values go to sequential params + for i, value in enumerate(parameter_values, 1): + if i <= 19: # Only use up to 19 parameters + parameters[i] = str(value) + elif isinstance(parameter_values, dict): + # Dict maps param positions to values + for pos, value in parameter_values.items(): + pos = int(pos) if not isinstance(pos, int) else pos + if 1 <= pos <= 19: # Validate parameter positions + parameters[pos] = str(value) + + # IMPORTANT: Create a data dictionary with ALL parameters explicitly included + request_data = { + "mix_sn": device_sn, + "type": parameter_id + } + + # Add all 19 parameters to the request + for i in range(1, 20): + request_data[f"param{i}"] = str(parameters[i]) + + # Send the request + response = self.session.post( + self._get_url('mixSet'), + data=request_data + ) + + return self._process_response(response.json(), f"writing parameter {parameter_id}") + + def sph_write_ac_charge_time(self, device_sn, period_id, charge_power, charge_stop_soc, + start_time, end_time, mains_enabled=True, enabled=True): + """ + Set an AC charge time period for an SPH inverter. + + Args: + device_sn (str): The serial number of the inverter. + period_id (int): Period ID (1-3). + charge_power (int): Charging power percentage (0-100). + charge_stop_soc (int): Stop charging at this SOC percentage (0-100). + start_time (datetime.time): Start time for the period. + end_time (datetime.time): End time for the period. + mains_enabled (bool): Enable grid charging. Default True. + enabled (bool): Whether this period is enabled. Default True. + + Returns: + dict: The server response. + + Raises: + GrowattParameterError: If parameters are invalid. + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + if not 1 <= period_id <= 3: + raise GrowattParameterError("period_id must be between 1 and 3") + + if not 0 <= charge_power <= 100: + raise GrowattParameterError("charge_power must be between 0 and 100") + + if not 0 <= charge_stop_soc <= 100: + raise GrowattParameterError("charge_stop_soc must be between 0 and 100") + + # Initialize ALL 19 parameters as empty strings + all_params = { + "mix_sn": device_sn, + "type": "mix_ac_charge_time_period" + } + + # Period-specific parameter offsets + base_param = (period_id - 1) * 5 + + # Set parameters according to SPH AC charge format + all_params["param1"] = str(charge_power) + all_params["param2"] = str(charge_stop_soc) + all_params["param3"] = "1" if mains_enabled else "0" + all_params[f"param{base_param + 4}"] = str(start_time.hour) + all_params[f"param{base_param + 5}"] = str(start_time.minute) + all_params[f"param{base_param + 6}"] = str(end_time.hour) + all_params[f"param{base_param + 7}"] = str(end_time.minute) + all_params[f"param{base_param + 8}"] = "1" if enabled else "0" + + # Add empty strings for all other parameters + for i in range(1, 20): + if f"param{i}" not in all_params: + all_params[f"param{i}"] = "" + + # Send the request + response = self.session.post( + self._get_url('mixSet'), + data=all_params + ) + + return self._process_response(response.json(), f"writing AC charge period {period_id}") + + def sph_write_ac_discharge_time(self, device_sn, period_id, discharge_power, discharge_stop_soc, + start_time, end_time, enabled=True): + """ + Set an AC discharge time period for an SPH inverter. + + Args: + device_sn (str): The serial number of the inverter. + period_id (int): Period ID (1-3). + discharge_power (int): Discharging power percentage (0-100). + discharge_stop_soc (int): Stop discharging at this SOC percentage (0-100). + start_time (datetime.time): Start time for the period. + end_time (datetime.time): End time for the period. + enabled (bool): Whether this period is enabled. Default True. + + Returns: + dict: The server response. + + Raises: + GrowattParameterError: If parameters are invalid. + GrowattV1ApiError: If the API returns an error response. + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + if not 1 <= period_id <= 3: + raise GrowattParameterError("period_id must be between 1 and 3") + + if not 0 <= discharge_power <= 100: + raise GrowattParameterError("discharge_power must be between 0 and 100") + + if not 0 <= discharge_stop_soc <= 100: + raise GrowattParameterError("discharge_stop_soc must be between 0 and 100") + + # Initialize ALL 19 parameters as empty strings + all_params = { + "mix_sn": device_sn, + "type": "mix_ac_discharge_time_period" + } + + # Period-specific parameter offsets + base_param = (period_id - 1) * 5 + + # Set parameters according to SPH AC discharge format + all_params["param1"] = str(discharge_power) + all_params["param2"] = str(discharge_stop_soc) + all_params[f"param{base_param + 3}"] = str(start_time.hour) + all_params[f"param{base_param + 4}"] = str(start_time.minute) + all_params[f"param{base_param + 5}"] = str(end_time.hour) + all_params[f"param{base_param + 6}"] = str(end_time.minute) + all_params[f"param{base_param + 7}"] = "1" if enabled else "0" + + # Add empty strings for all other parameters + for i in range(1, 20): + if f"param{i}" not in all_params: + all_params[f"param{i}"] = "" + + # Send the request + response = self.session.post( + self._get_url('mixSet'), + data=all_params + ) + + return self._process_response(response.json(), f"writing AC discharge period {period_id}") + + def sph_read_ac_charge_times(self, device_sn, settings_data=None): + """ + Read AC charge time periods from an SPH inverter. + + Retrieves all 3 AC charge time periods from an SPH inverter and + parses them into a structured format. + + Note that this function uses sph_settings() internally to get the settings data. + To avoid endpoint rate limit, you can pass the settings_data parameter + with the data returned from sph_settings(). + + Args: + device_sn (str): The device serial number of the inverter + settings_data (dict, optional): Settings data from sph_settings call to avoid repeated API calls. + + Returns: + list: A list of dictionaries, each containing details for one time period: + - period_id (int): The period number (1-3) + - start_time (str): Start time in format "HH:MM" + - end_time (str): End time in format "HH:MM" + - enabled (bool): Whether the period is enabled + + Example: + # Option 1: Make a single call + charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER") + + # Option 2: Reuse existing settings data + settings_response = api.sph_settings("DEVICE_SERIAL_NUMBER") + charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER", settings_response) + + Raises: + GrowattV1ApiError: If the API request fails + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + # Process the settings data + if settings_data is None: + settings_data = self.sph_settings(device_sn=device_sn) + + periods = [] + + # Process each time period (1-3 for SPH) + for i in range(1, 4): + # Get raw time values + start_time_raw = settings_data.get(f'forcedChargeTimeStart{i}', "0:0") + end_time_raw = settings_data.get(f'forcedChargeTimeStop{i}', "0:0") + enabled_raw = settings_data.get(f'forcedChargeStopSwitch{i}', 0) + + # Handle 'null' string values + if start_time_raw == 'null' or not start_time_raw: + start_time_raw = "0:0" + if end_time_raw == 'null' or not end_time_raw: + end_time_raw = "0:0" + + # Format times with leading zeros (HH:MM) + try: + start_parts = start_time_raw.split(":") + start_hour = int(start_parts[0]) + start_min = int(start_parts[1]) + start_time = f"{start_hour:02d}:{start_min:02d}" + except (ValueError, IndexError): + start_time = "00:00" + + try: + end_parts = end_time_raw.split(":") + end_hour = int(end_parts[0]) + end_min = int(end_parts[1]) + end_time = f"{end_hour:02d}:{end_min:02d}" + except (ValueError, IndexError): + end_time = "00:00" + + # Get the enabled status + if enabled_raw == 'null' or enabled_raw is None: + enabled = False + else: + try: + enabled = int(enabled_raw) == 1 + except (ValueError, TypeError): + enabled = False + + period = { + 'period_id': i, + 'start_time': start_time, + 'end_time': end_time, + 'enabled': enabled + } + + periods.append(period) + + return periods + + def sph_read_ac_discharge_times(self, device_sn, settings_data=None): + """ + Read AC discharge time periods from an SPH inverter. + + Retrieves all 3 AC discharge time periods from an SPH inverter and + parses them into a structured format. + + Note that this function uses sph_settings() internally to get the settings data. + To avoid endpoint rate limit, you can pass the settings_data parameter + with the data returned from sph_settings(). + + Args: + device_sn (str): The device serial number of the inverter + settings_data (dict, optional): Settings data from sph_settings call to avoid repeated API calls. + + Returns: + list: A list of dictionaries, each containing details for one time period: + - period_id (int): The period number (1-3) + - start_time (str): Start time in format "HH:MM" + - end_time (str): End time in format "HH:MM" + - enabled (bool): Whether the period is enabled + + Example: + # Option 1: Make a single call + discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER") + + # Option 2: Reuse existing settings data + settings_response = api.sph_settings("DEVICE_SERIAL_NUMBER") + discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER", settings_response) + + Raises: + GrowattV1ApiError: If the API request fails + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + + # Process the settings data + if settings_data is None: + settings_data = self.sph_settings(device_sn=device_sn) + + periods = [] + + # Process each time period (1-3 for SPH) + for i in range(1, 4): + # Get raw time values + start_time_raw = settings_data.get(f'forcedDischargeTimeStart{i}', "0:0") + end_time_raw = settings_data.get(f'forcedDischargeTimeStop{i}', "0:0") + enabled_raw = settings_data.get(f'forcedDischargeStopSwitch{i}', 0) + + # Handle 'null' string values + if start_time_raw == 'null' or not start_time_raw: + start_time_raw = "0:0" + if end_time_raw == 'null' or not end_time_raw: + end_time_raw = "0:0" + + # Format times with leading zeros (HH:MM) + try: + start_parts = start_time_raw.split(":") + start_hour = int(start_parts[0]) + start_min = int(start_parts[1]) + start_time = f"{start_hour:02d}:{start_min:02d}" + except (ValueError, IndexError): + start_time = "00:00" + + try: + end_parts = end_time_raw.split(":") + end_hour = int(end_parts[0]) + end_min = int(end_parts[1]) + end_time = f"{end_hour:02d}:{end_min:02d}" + except (ValueError, IndexError): + end_time = "00:00" + + # Get the enabled status + if enabled_raw == 'null' or enabled_raw is None: + enabled = False + else: + try: + enabled = int(enabled_raw) == 1 + except (ValueError, TypeError): + enabled = False + + period = { + 'period_id': i, + 'start_time': start_time, + 'end_time': end_time, + 'enabled': enabled + } + + periods.append(period) + + return periods From f1c771c7d70742ab12ec53e9fce78f07176d2e2f Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Thu, 8 Jan 2026 22:31:34 +0100 Subject: [PATCH 2/5] Update README and OpenAPI documentation to clarify support for MIN and SPH devices with V1 API --- README.md | 2 +- docs/openapiv1.md | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b0a4896..6e143ee 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Package to retrieve PV information from the growatt server. Special thanks to [Sjoerd Langkemper](https://github.com/Sjord) who has provided a strong base to start off from https://github.com/Sjord/growatt_api_client That project has since ben archived. -This library now supports both the legacy password-based authentication and the V1 API with token-based authentication for MIN and SPH systems. MIN devices (type 7) correspond to MIN/MID series inverters, while SPH devices (type 5) are hybrid inverters. The V1 API is officially supported by Growatt and recommended over legacy authentication. Certain endpoints are not supported anymore by openapi.growatt.com. For example `api.min_write_parameter()` should be used instead of old `api.update_tlx_inverter_setting()`. +This library supports both the classic password-based API and the token-based V1 API, officially supported by Growatt. Currently, the V1 API implementation supports MIN and SPH devices, where MIN broadly corresponds to classic TLX and SPH to classic MIX. If your inverter supports the V1 API, it is encouraged to use this over the classic API, as it offers better security, more features, and more relaxed rate limiting. ## Usage diff --git a/docs/openapiv1.md b/docs/openapiv1.md index 1f56bfc..c2c52b9 100644 --- a/docs/openapiv1.md +++ b/docs/openapiv1.md @@ -2,8 +2,6 @@ This version of the API follows the newer [OpenAPI V1 API](https://www.showdoc.com.cn/262556420217021/0) Growatt has made available. -Currently supports MIN (type 7) and SPH (type 5) devices. MIN devices correspond to MIN/MID series inverters (using classic TLX endpoints), while SPH devices are hybrid inverters (using classic MIX endpoints). The V1 API is officially supported by Growatt and offers better security, more features, and relaxed rate limiting compared to the legacy API. - It extends our ["Legacy" ShinePhone](./shinephone.md) so methods from [there](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. ## Usage @@ -23,7 +21,9 @@ print(plants) ### Methods -Any methods that may be useful. +#### Generic Methods + +Methods that work across all device types. | Method | Arguments | Description | |:---|:---|:---| @@ -32,6 +32,13 @@ Any methods that may be useful. | `api.plant_energy_overview(plant_id)` | plant_id: String | Get energy overview data for a plant. | | `api.plant_energy_history(plant_id, start_date, end_date, time_unit, page, perpage)` | plant_id: String, start_date: Date, end_date: Date, time_unit: String, page: Int, perpage: Int | Get historical energy data for a plant for multiple days/months/years. | | `api.device_list(plant_id)` | plant_id: String | Get a list of devices in specified plant. | + +#### MIN Methods + +Methods for MIN devices (type 7). + +| Method | Arguments | Description | +|:---|:---|:---| | `api.min_energy(device_sn)` | device_sn: String | Get current energy data for a min inverter, including power and energy values. | | `api.min_detail(device_sn)` | device_sn: String | Get detailed data for a min inverter. | | `api.min_energy_history(device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None)` | device_sn: String, start_date: Date, end_date: Date, timezone: String, page: Int, limit: Int | Get energy history data for a min inverter (7-day max range). | @@ -40,6 +47,13 @@ Any methods that may be useful. | `api.min_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on a min inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_write_time_segment(device_sn, segment_id, batt_mode, start_time, end_time, enabled=True)` | device_sn: String, segment_id: Int, batt_mode: Int <0=load priority, 1=battery priority, 2=grid priority>, start_time: Time, end_time: Time, enabled: Bool | Update a specific time segment for a min inverter. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_read_time_segments(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all time segments from a MIN inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/min_tlx_settings.md) | + +#### SPH Methods + +Methods for SPH devices (type 5). + +| Method | Arguments | Description | +|:---|:---|:---| | `api.sph_detail(device_sn)` | device_sn: String | Get detailed data for an SPH hybrid inverter. | | `api.sph_energy(device_sn)` | device_sn: String | Get current energy data for an SPH inverter, including power and energy values. | | `api.sph_energy_history(device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None)` | device_sn: String, start_date: Date, end_date: Date, timezone: String, page: Int, limit: Int | Get energy history data for an SPH inverter (7-day max range). | From 288e28c6229333832da9f817e8060f23ce17e12e Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Tue, 13 Jan 2026 22:25:19 +0100 Subject: [PATCH 3/5] Adress review comments --- docs/openapiv1.md | 13 +- docs/openapiv1/sph_settings.md | 22 +-- examples/sph_example.py | 54 +++--- growattServer/open_api_v1.py | 308 ++++++++++++++------------------- 4 files changed, 174 insertions(+), 223 deletions(-) diff --git a/docs/openapiv1.md b/docs/openapiv1.md index c2c52b9..be7ca9b 100644 --- a/docs/openapiv1.md +++ b/docs/openapiv1.md @@ -45,7 +45,7 @@ Methods for MIN devices (type 7). | `api.min_settings(device_sn)` | device_sn: String | Get all settings for a min inverter. | | `api.min_read_parameter(device_sn, parameter_id, start_address=None, end_address=None)` | device_sn: String, parameter_id: String, start_address: Int, end_address: Int | Read a specific setting for a min inverter. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on a min inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/min_tlx_settings.md) | -| `api.min_write_time_segment(device_sn, segment_id, batt_mode, start_time, end_time, enabled=True)` | device_sn: String, segment_id: Int, batt_mode: Int <0=load priority, 1=battery priority, 2=grid priority>, start_time: Time, end_time: Time, enabled: Bool | Update a specific time segment for a min inverter. see: [details](./openapiv1/min_tlx_settings.md) | +| `api.min_write_time_segment(device_sn, segment_id, batt_mode, start_time, end_time, enabled=True)` | device_sn: String, segment_id: Int, batt_mode: Int <0=load priority, 1=battery priority, 2=grid priority>, start_time: datetime.time, end_time: datetime.time, enabled: Bool | Update a specific time segment for a min inverter. see: [details](./openapiv1/min_tlx_settings.md) | | `api.min_read_time_segments(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all time segments from a MIN inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/min_tlx_settings.md) | #### SPH Methods @@ -54,16 +54,15 @@ Methods for SPH devices (type 5). | Method | Arguments | Description | |:---|:---|:---| -| `api.sph_detail(device_sn)` | device_sn: String | Get detailed data for an SPH hybrid inverter. | +| `api.sph_detail(device_sn)` | device_sn: String | Get detailed data and settings for an SPH hybrid inverter. | | `api.sph_energy(device_sn)` | device_sn: String | Get current energy data for an SPH inverter, including power and energy values. | | `api.sph_energy_history(device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None)` | device_sn: String, start_date: Date, end_date: Date, timezone: String, page: Int, limit: Int | Get energy history data for an SPH inverter (7-day max range). | -| `api.sph_settings(device_sn)` | device_sn: String | Get all settings for an SPH inverter. | | `api.sph_read_parameter(device_sn, parameter_id, start_address=None, end_address=None)` | device_sn: String, parameter_id: String, start_address: Int, end_address: Int | Read a specific setting for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | | `api.sph_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on an SPH inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_write_ac_charge_time(device_sn, period_id, charge_power, charge_stop_soc, start_time, end_time, mains_enabled=True, enabled=True)` | device_sn: String, period_id: Int (1-3), charge_power: Int (0-100), charge_stop_soc: Int (0-100), start_time: Time, end_time: Time, mains_enabled: Bool, enabled: Bool | Configure an AC charge time period for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_write_ac_discharge_time(device_sn, period_id, discharge_power, discharge_stop_soc, start_time, end_time, enabled=True)` | device_sn: String, period_id: Int (1-3), discharge_power: Int (0-100), discharge_stop_soc: Int (0-100), start_time: Time, end_time: Time, enabled: Bool | Configure an AC discharge time period for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_charge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC charge time periods from an SPH inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_discharge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC discharge time periods from an SPH inverter. Optionally pass settings_data to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_ac_charge_times(device_sn, charge_power, charge_stop_soc, mains_enabled, periods)` | device_sn: String, charge_power: Int (0-100), charge_stop_soc: Int (0-100), mains_enabled: Bool, periods: List of 3 dicts with start_time, end_time, enabled | Configure AC charge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_ac_discharge_times(device_sn, discharge_power, discharge_stop_soc, periods)` | device_sn: String, discharge_power: Int (0-100), discharge_stop_soc: Int (0-100), periods: List of 3 dicts with start_time, end_time, enabled | Configure AC discharge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_charge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC charge time periods from an SPH inverter. Optionally pass settings_data from sph_detail() to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_discharge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC discharge time periods from an SPH inverter. Optionally pass settings_data from sph_detail() to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | Methods from [here](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. There is no guarantee those methods will work, or remain stable through updates. diff --git a/docs/openapiv1/sph_settings.md b/docs/openapiv1/sph_settings.md index 71af73a..bf86d38 100644 --- a/docs/openapiv1/sph_settings.md +++ b/docs/openapiv1/sph_settings.md @@ -19,27 +19,27 @@ For SPH (hybrid inverter) systems, the public V1 API provides methods to read an * `parameter_values`: Value to set (single value, list, or dictionary) * **AC Charge Time Periods** - * function: `api.sph_write_ac_charge_time` + * function: `api.sph_write_ac_charge_times` * parameters: * `device_sn`: The device serial number - * `period_id`: Period number (1-3) * `charge_power`: Charging power percentage (0-100) * `charge_stop_soc`: Stop charging at this SOC percentage (0-100) - * `start_time`: Datetime.time object for period start - * `end_time`: Datetime.time object for period end - * `mains_enabled`: Boolean to enable/disable grid charging (default: True) - * `enabled`: Boolean to enable/disable period (default: True) + * `mains_enabled`: Boolean to enable/disable grid charging + * `periods`: List of 3 period dicts, each with: + * `start_time`: datetime.time object for period start + * `end_time`: datetime.time object for period end + * `enabled`: Boolean to enable/disable period * **AC Discharge Time Periods** - * function: `api.sph_write_ac_discharge_time` + * function: `api.sph_write_ac_discharge_times` * parameters: * `device_sn`: The device serial number - * `period_id`: Period number (1-3) * `discharge_power`: Discharge power percentage (0-100) * `discharge_stop_soc`: Stop discharging at this SOC percentage (0-100) - * `start_time`: Datetime.time object for period start - * `end_time`: Datetime.time object for period end - * `enabled`: Boolean to enable/disable period (default: True) + * `periods`: List of 3 period dicts, each with: + * `start_time`: datetime.time object for period start + * `end_time`: datetime.time object for period end + * `enabled`: Boolean to enable/disable period * **Read AC Charge Time Periods** * function: `api.sph_read_ac_charge_times` diff --git a/examples/sph_example.py b/examples/sph_example.py index bea3ee7..d1580b2 100644 --- a/examples/sph_example.py +++ b/examples/sph_example.py @@ -6,13 +6,11 @@ You can obtain an API token from the Growatt API documentation or developer portal. """ -import datetime import json -from pathlib import Path import requests -from . import growattServer +import growattServer # Get the API token from user input or environment variable # api_token = os.environ.get("GROWATT_API_TOKEN") or input("Enter your Growatt API token: ") @@ -43,7 +41,7 @@ device_sn=inverter_sn, ) print("Saving inverter data to inverter_data.json") # noqa: T201 - with Path("inverter_data.json").open("w") as f: + with open("inverter_data.json", "w") as f: json.dump(inverter_data, f, indent=4, sort_keys=True) # Get energy data @@ -51,7 +49,7 @@ device_sn=inverter_sn, ) print("Saving energy data to energy_data.json") # noqa: T201 - with Path("energy_data.json").open("w") as f: + with open("energy_data.json", "w") as f: json.dump(energy_data, f, indent=4, sort_keys=True) # Get energy history @@ -59,7 +57,7 @@ device_sn=inverter_sn, ) print("Saving energy history data to energy_history.json") # noqa: T201 - with Path("energy_history.json").open("w") as f: + with open("energy_history.json", "w") as f: json.dump( energy_history_data.get("datas", []), f, @@ -67,18 +65,18 @@ sort_keys=True, ) - # Get settings - settings_data = api.sph_settings( + # Get details (includes settings data) + detail_data = api.sph_detail( device_sn=inverter_sn, ) - print("Saving settings data to settings_data.json") # noqa: T201 - with Path("settings_data.json").open("w") as f: - json.dump(settings_data, f, indent=4, sort_keys=True) + print("Saving detail data to settings_data.json") # noqa: T201 + with open("settings_data.json", "w") as f: + json.dump(detail_data, f, indent=4, sort_keys=True) # Read AC charge time periods charge_times = api.sph_read_ac_charge_times( device_sn=inverter_sn, - settings_data=settings_data, + settings_data=detail_data, ) print("AC Charge Time Periods:") # noqa: T201 print(json.dumps(charge_times, indent=4)) # noqa: T201 @@ -86,7 +84,7 @@ # Read AC discharge time periods discharge_times = api.sph_read_ac_discharge_times( device_sn=inverter_sn, - settings_data=settings_data, + settings_data=detail_data, ) print("AC Discharge Time Periods:") # noqa: T201 print(json.dumps(discharge_times, indent=4)) # noqa: T201 @@ -100,30 +98,32 @@ # Write examples - Uncomment to test - # Set AC charge time period 1: charge at 50% power to 95% SOC between 00:00-06:00 - # api.sph_write_ac_charge_time( + # Set AC charge time periods: charge at 50% power to 95% SOC + # api.sph_write_ac_charge_times( # device_sn=inverter_sn, - # period_id=1, # charge_power=50, # 50% charging power # charge_stop_soc=95, # Stop at 95% SOC - # start_time=datetime.time(0, 0), - # end_time=datetime.time(6, 0), # mains_enabled=True, # Enable grid charging - # enabled=True + # periods=[ + # {"start_time": datetime.time(0, 0), "end_time": datetime.time(6, 0), "enabled": True}, + # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, + # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, + # ] # ) - # print("AC charge period 1 updated successfully") + # print("AC charge periods updated successfully") - # Set AC discharge time period 1: discharge at 100% power to 20% SOC between 17:00-22:00 - # api.sph_write_ac_discharge_time( + # Set AC discharge time periods: discharge at 100% power to 20% SOC + # api.sph_write_ac_discharge_times( # device_sn=inverter_sn, - # period_id=1, # discharge_power=100, # 100% discharge power # discharge_stop_soc=20, # Stop at 20% SOC - # start_time=datetime.time(17, 0), - # end_time=datetime.time(22, 0), - # enabled=True + # periods=[ + # {"start_time": datetime.time(17, 0), "end_time": datetime.time(22, 0), "enabled": True}, + # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, + # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, + # ] # ) - # print("AC discharge period 1 updated successfully") + # print("AC discharge periods updated successfully") except growattServer.GrowattV1ApiError as e: print(f"API Error: {e} (Code: {e.error_code}, Message: {e.error_msg})") # noqa: T201 diff --git a/growattServer/open_api_v1.py b/growattServer/open_api_v1.py index 3446fa6..c0a15c4 100644 --- a/growattServer/open_api_v1.py +++ b/growattServer/open_api_v1.py @@ -15,7 +15,7 @@ class DeviceType(Enum): MAX = 4 SPH = 5 # (MIX) SPA = 6 - MIN = 7 + MIN = 7 PCS = 8 HPS = 9 PBD = 10 @@ -756,30 +756,6 @@ def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone return self._process_response(response.json(), "getting SPH inverter energy history") - def sph_settings(self, device_sn): - """ - Get settings for an SPH inverter. - - Args: - device_sn (str): The serial number of the SPH inverter. - - Returns: - dict: A dictionary containing the SPH inverter settings. - - Raises: - GrowattV1ApiError: If the API returns an error response. - requests.exceptions.RequestException: If there is an issue with the HTTP request. - """ - - response = self.session.get( - self._get_url('device/mix/mix_data_info'), - params={ - 'device_sn': device_sn - } - ) - - return self._process_response(response.json(), "getting SPH inverter settings") - def sph_read_parameter(self, device_sn, parameter_id, start_address=None, end_address=None): """ Read setting from SPH inverter. @@ -884,148 +860,154 @@ def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): return self._process_response(response.json(), f"writing parameter {parameter_id}") - def sph_write_ac_charge_time(self, device_sn, period_id, charge_power, charge_stop_soc, - start_time, end_time, mains_enabled=True, enabled=True): + def sph_write_ac_charge_times(self, device_sn, charge_power, charge_stop_soc, mains_enabled, periods): """ - Set an AC charge time period for an SPH inverter. + Set AC charge time periods for an SPH inverter. Args: device_sn (str): The serial number of the inverter. - period_id (int): Period ID (1-3). charge_power (int): Charging power percentage (0-100). charge_stop_soc (int): Stop charging at this SOC percentage (0-100). - start_time (datetime.time): Start time for the period. - end_time (datetime.time): End time for the period. - mains_enabled (bool): Enable grid charging. Default True. - enabled (bool): Whether this period is enabled. Default True. + mains_enabled (bool): Enable grid charging. + periods (list): List of 3 period dicts, each with keys: + - start_time (datetime.time): Start time for the period + - end_time (datetime.time): End time for the period + - enabled (bool): Whether this period is enabled Returns: dict: The server response. + Example: + from datetime import time + + api.sph_write_ac_charge_times( + device_sn="ABC123", + charge_power=100, + charge_stop_soc=100, + mains_enabled=True, + periods=[ + {"start_time": time(1, 0), "end_time": time(5, 0), "enabled": True}, + {"start_time": time(0, 0), "end_time": time(0, 0), "enabled": False}, + {"start_time": time(0, 0), "end_time": time(0, 0), "enabled": False}, + ] + ) + Raises: GrowattParameterError: If parameters are invalid. GrowattV1ApiError: If the API returns an error response. requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - - if not 1 <= period_id <= 3: - raise GrowattParameterError("period_id must be between 1 and 3") - if not 0 <= charge_power <= 100: raise GrowattParameterError("charge_power must be between 0 and 100") if not 0 <= charge_stop_soc <= 100: raise GrowattParameterError("charge_stop_soc must be between 0 and 100") - # Initialize ALL 19 parameters as empty strings - all_params = { + if len(periods) != 3: + raise GrowattParameterError("periods must contain exactly 3 period definitions") + + # Build request data + request_data = { "mix_sn": device_sn, - "type": "mix_ac_charge_time_period" + "type": "mix_ac_charge_time_period", + "param1": str(charge_power), + "param2": str(charge_stop_soc), + "param3": "1" if mains_enabled else "0", } - # Period-specific parameter offsets - base_param = (period_id - 1) * 5 + # Add period parameters (param4-18) + for i, period in enumerate(periods): + base = i * 5 + 4 + request_data[f"param{base}"] = str(period["start_time"].hour) + request_data[f"param{base + 1}"] = str(period["start_time"].minute) + request_data[f"param{base + 2}"] = str(period["end_time"].hour) + request_data[f"param{base + 3}"] = str(period["end_time"].minute) + request_data[f"param{base + 4}"] = "1" if period["enabled"] else "0" - # Set parameters according to SPH AC charge format - all_params["param1"] = str(charge_power) - all_params["param2"] = str(charge_stop_soc) - all_params["param3"] = "1" if mains_enabled else "0" - all_params[f"param{base_param + 4}"] = str(start_time.hour) - all_params[f"param{base_param + 5}"] = str(start_time.minute) - all_params[f"param{base_param + 6}"] = str(end_time.hour) - all_params[f"param{base_param + 7}"] = str(end_time.minute) - all_params[f"param{base_param + 8}"] = "1" if enabled else "0" - - # Add empty strings for all other parameters - for i in range(1, 20): - if f"param{i}" not in all_params: - all_params[f"param{i}"] = "" - - # Send the request response = self.session.post( self._get_url('mixSet'), - data=all_params + data=request_data ) - return self._process_response(response.json(), f"writing AC charge period {period_id}") + return self._process_response(response.json(), "writing AC charge time periods") - def sph_write_ac_discharge_time(self, device_sn, period_id, discharge_power, discharge_stop_soc, - start_time, end_time, enabled=True): + def sph_write_ac_discharge_times(self, device_sn, discharge_power, discharge_stop_soc, periods): """ - Set an AC discharge time period for an SPH inverter. + Set AC discharge time periods for an SPH inverter. Args: device_sn (str): The serial number of the inverter. - period_id (int): Period ID (1-3). discharge_power (int): Discharging power percentage (0-100). discharge_stop_soc (int): Stop discharging at this SOC percentage (0-100). - start_time (datetime.time): Start time for the period. - end_time (datetime.time): End time for the period. - enabled (bool): Whether this period is enabled. Default True. + periods (list): List of 3 period dicts, each with keys: + - start_time (datetime.time): Start time for the period + - end_time (datetime.time): End time for the period + - enabled (bool): Whether this period is enabled Returns: dict: The server response. + Example: + from datetime import time + + api.sph_write_ac_discharge_times( + device_sn="ABC123", + discharge_power=100, + discharge_stop_soc=10, + periods=[ + {"start_time": time(17, 0), "end_time": time(21, 0), "enabled": True}, + {"start_time": time(0, 0), "end_time": time(0, 0), "enabled": False}, + {"start_time": time(0, 0), "end_time": time(0, 0), "enabled": False}, + ] + ) + Raises: GrowattParameterError: If parameters are invalid. GrowattV1ApiError: If the API returns an error response. requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - - if not 1 <= period_id <= 3: - raise GrowattParameterError("period_id must be between 1 and 3") - if not 0 <= discharge_power <= 100: raise GrowattParameterError("discharge_power must be between 0 and 100") if not 0 <= discharge_stop_soc <= 100: raise GrowattParameterError("discharge_stop_soc must be between 0 and 100") - # Initialize ALL 19 parameters as empty strings - all_params = { + if len(periods) != 3: + raise GrowattParameterError("periods must contain exactly 3 period definitions") + + # Build request data + request_data = { "mix_sn": device_sn, - "type": "mix_ac_discharge_time_period" + "type": "mix_ac_discharge_time_period", + "param1": str(discharge_power), + "param2": str(discharge_stop_soc), } - # Period-specific parameter offsets - base_param = (period_id - 1) * 5 + # Add period parameters (param3-17) + for i, period in enumerate(periods): + base = i * 5 + 3 + request_data[f"param{base}"] = str(period["start_time"].hour) + request_data[f"param{base + 1}"] = str(period["start_time"].minute) + request_data[f"param{base + 2}"] = str(period["end_time"].hour) + request_data[f"param{base + 3}"] = str(period["end_time"].minute) + request_data[f"param{base + 4}"] = "1" if period["enabled"] else "0" - # Set parameters according to SPH AC discharge format - all_params["param1"] = str(discharge_power) - all_params["param2"] = str(discharge_stop_soc) - all_params[f"param{base_param + 3}"] = str(start_time.hour) - all_params[f"param{base_param + 4}"] = str(start_time.minute) - all_params[f"param{base_param + 5}"] = str(end_time.hour) - all_params[f"param{base_param + 6}"] = str(end_time.minute) - all_params[f"param{base_param + 7}"] = "1" if enabled else "0" - - # Add empty strings for all other parameters - for i in range(1, 20): - if f"param{i}" not in all_params: - all_params[f"param{i}"] = "" - - # Send the request response = self.session.post( self._get_url('mixSet'), - data=all_params + data=request_data ) - return self._process_response(response.json(), f"writing AC discharge period {period_id}") + return self._process_response(response.json(), "writing AC discharge time periods") - def sph_read_ac_charge_times(self, device_sn, settings_data=None): + def _parse_time_periods(self, settings_data, time_type): """ - Read AC charge time periods from an SPH inverter. + Parse time periods from settings data. - Retrieves all 3 AC charge time periods from an SPH inverter and - parses them into a structured format. - - Note that this function uses sph_settings() internally to get the settings data. - To avoid endpoint rate limit, you can pass the settings_data parameter - with the data returned from sph_settings(). + Internal helper method to extract and format time period data from SPH settings. Args: - device_sn (str): The device serial number of the inverter - settings_data (dict, optional): Settings data from sph_settings call to avoid repeated API calls. + settings_data (dict): Settings data from sph_detail call. + time_type (str): Either "Charge" or "Discharge" to specify which periods to parse. Returns: list: A list of dictionaries, each containing details for one time period: @@ -1033,32 +1015,15 @@ def sph_read_ac_charge_times(self, device_sn, settings_data=None): - start_time (str): Start time in format "HH:MM" - end_time (str): End time in format "HH:MM" - enabled (bool): Whether the period is enabled - - Example: - # Option 1: Make a single call - charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER") - - # Option 2: Reuse existing settings data - settings_response = api.sph_settings("DEVICE_SERIAL_NUMBER") - charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER", settings_response) - - Raises: - GrowattV1ApiError: If the API request fails - requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - - # Process the settings data - if settings_data is None: - settings_data = self.sph_settings(device_sn=device_sn) - periods = [] # Process each time period (1-3 for SPH) for i in range(1, 4): # Get raw time values - start_time_raw = settings_data.get(f'forcedChargeTimeStart{i}', "0:0") - end_time_raw = settings_data.get(f'forcedChargeTimeStop{i}', "0:0") - enabled_raw = settings_data.get(f'forcedChargeStopSwitch{i}', 0) + start_time_raw = settings_data.get(f'forced{time_type}TimeStart{i}', "0:0") + end_time_raw = settings_data.get(f'forced{time_type}TimeStop{i}', "0:0") + enabled_raw = settings_data.get(f'forced{time_type}StopSwitch{i}', 0) # Handle 'null' string values if start_time_raw == 'null' or not start_time_raw: @@ -1103,20 +1068,20 @@ def sph_read_ac_charge_times(self, device_sn, settings_data=None): return periods - def sph_read_ac_discharge_times(self, device_sn, settings_data=None): + def sph_read_ac_charge_times(self, device_sn, settings_data=None): """ - Read AC discharge time periods from an SPH inverter. + Read AC charge time periods from an SPH inverter. - Retrieves all 3 AC discharge time periods from an SPH inverter and + Retrieves all 3 AC charge time periods from an SPH inverter and parses them into a structured format. - Note that this function uses sph_settings() internally to get the settings data. + Note that this function uses sph_detail() internally to get the settings data. To avoid endpoint rate limit, you can pass the settings_data parameter - with the data returned from sph_settings(). + with the data returned from sph_detail(). Args: device_sn (str): The device serial number of the inverter - settings_data (dict, optional): Settings data from sph_settings call to avoid repeated API calls. + settings_data (dict, optional): Settings data from sph_detail call to avoid repeated API calls. Returns: list: A list of dictionaries, each containing details for one time period: @@ -1127,69 +1092,56 @@ def sph_read_ac_discharge_times(self, device_sn, settings_data=None): Example: # Option 1: Make a single call - discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER") + charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER") # Option 2: Reuse existing settings data - settings_response = api.sph_settings("DEVICE_SERIAL_NUMBER") - discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER", settings_response) + settings_response = api.sph_detail("DEVICE_SERIAL_NUMBER") + charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER", settings_response) Raises: GrowattV1ApiError: If the API request fails requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - - # Process the settings data if settings_data is None: - settings_data = self.sph_settings(device_sn=device_sn) + settings_data = self.sph_detail(device_sn=device_sn) - periods = [] + return self._parse_time_periods(settings_data, "Charge") - # Process each time period (1-3 for SPH) - for i in range(1, 4): - # Get raw time values - start_time_raw = settings_data.get(f'forcedDischargeTimeStart{i}', "0:0") - end_time_raw = settings_data.get(f'forcedDischargeTimeStop{i}', "0:0") - enabled_raw = settings_data.get(f'forcedDischargeStopSwitch{i}', 0) + def sph_read_ac_discharge_times(self, device_sn, settings_data=None): + """ + Read AC discharge time periods from an SPH inverter. - # Handle 'null' string values - if start_time_raw == 'null' or not start_time_raw: - start_time_raw = "0:0" - if end_time_raw == 'null' or not end_time_raw: - end_time_raw = "0:0" + Retrieves all 3 AC discharge time periods from an SPH inverter and + parses them into a structured format. - # Format times with leading zeros (HH:MM) - try: - start_parts = start_time_raw.split(":") - start_hour = int(start_parts[0]) - start_min = int(start_parts[1]) - start_time = f"{start_hour:02d}:{start_min:02d}" - except (ValueError, IndexError): - start_time = "00:00" + Note that this function uses sph_detail() internally to get the settings data. + To avoid endpoint rate limit, you can pass the settings_data parameter + with the data returned from sph_detail(). - try: - end_parts = end_time_raw.split(":") - end_hour = int(end_parts[0]) - end_min = int(end_parts[1]) - end_time = f"{end_hour:02d}:{end_min:02d}" - except (ValueError, IndexError): - end_time = "00:00" + Args: + device_sn (str): The device serial number of the inverter + settings_data (dict, optional): Settings data from sph_detail call to avoid repeated API calls. - # Get the enabled status - if enabled_raw == 'null' or enabled_raw is None: - enabled = False - else: - try: - enabled = int(enabled_raw) == 1 - except (ValueError, TypeError): - enabled = False + Returns: + list: A list of dictionaries, each containing details for one time period: + - period_id (int): The period number (1-3) + - start_time (str): Start time in format "HH:MM" + - end_time (str): End time in format "HH:MM" + - enabled (bool): Whether the period is enabled - period = { - 'period_id': i, - 'start_time': start_time, - 'end_time': end_time, - 'enabled': enabled - } + Example: + # Option 1: Make a single call + discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER") - periods.append(period) + # Option 2: Reuse existing settings data + settings_response = api.sph_detail("DEVICE_SERIAL_NUMBER") + discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER", settings_response) - return periods + Raises: + GrowattV1ApiError: If the API request fails + requests.exceptions.RequestException: If there is an issue with the HTTP request. + """ + if settings_data is None: + settings_data = self.sph_detail(device_sn=device_sn) + + return self._parse_time_periods(settings_data, "Discharge") From baace1861ff7bd73b7e17a34b5f4c2deee9ccc8b Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Thu, 15 Jan 2026 23:33:41 +0100 Subject: [PATCH 4/5] Enhance SPH read methods with global settings and optional device_sn - sph_read_ac_charge_times now returns dict with charge_power, charge_stop_soc, mains_enabled, and periods - sph_read_ac_discharge_times now returns dict with discharge_power, discharge_stop_soc, and periods - Made device_sn optional when settings_data is provided (avoids redundant API calls) - Updated example to demonstrate new return structure - Updated documentation with new signatures and return types --- docs/openapiv1.md | 4 +- docs/openapiv1/sph_settings.md | 12 ++-- examples/sph_example.py | 48 ++++++------- growattServer/open_api_v1.py | 119 ++++++++++++++++++++++++--------- 4 files changed, 119 insertions(+), 64 deletions(-) diff --git a/docs/openapiv1.md b/docs/openapiv1.md index be7ca9b..1c5983f 100644 --- a/docs/openapiv1.md +++ b/docs/openapiv1.md @@ -61,8 +61,8 @@ Methods for SPH devices (type 5). | `api.sph_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on an SPH inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/sph_settings.md) | | `api.sph_write_ac_charge_times(device_sn, charge_power, charge_stop_soc, mains_enabled, periods)` | device_sn: String, charge_power: Int (0-100), charge_stop_soc: Int (0-100), mains_enabled: Bool, periods: List of 3 dicts with start_time, end_time, enabled | Configure AC charge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | | `api.sph_write_ac_discharge_times(device_sn, discharge_power, discharge_stop_soc, periods)` | device_sn: String, discharge_power: Int (0-100), discharge_stop_soc: Int (0-100), periods: List of 3 dicts with start_time, end_time, enabled | Configure AC discharge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_charge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC charge time periods from an SPH inverter. Optionally pass settings_data from sph_detail() to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_discharge_times(device_sn, settings_data=None)` | device_sn: String, settings_data: Dict | Read all AC discharge time periods from an SPH inverter. Optionally pass settings_data from sph_detail() to avoid redundant API calls. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_charge_times(device_sn=None, settings_data=None)` | device_sn: String (optional if settings_data provided), settings_data: Dict | Read AC charge configuration including charge_power, charge_stop_soc, mains_enabled, and time periods. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_discharge_times(device_sn=None, settings_data=None)` | device_sn: String (optional if settings_data provided), settings_data: Dict | Read AC discharge configuration including discharge_power, discharge_stop_soc, and time periods. see: [details](./openapiv1/sph_settings.md) | Methods from [here](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. There is no guarantee those methods will work, or remain stable through updates. diff --git a/docs/openapiv1/sph_settings.md b/docs/openapiv1/sph_settings.md index bf86d38..60d0e65 100644 --- a/docs/openapiv1/sph_settings.md +++ b/docs/openapiv1/sph_settings.md @@ -44,11 +44,15 @@ For SPH (hybrid inverter) systems, the public V1 API provides methods to read an * **Read AC Charge Time Periods** * function: `api.sph_read_ac_charge_times` * parameters: - * `device_sn`: The device serial number - * `settings_data`: Optional settings data to avoid redundant API calls + * `device_sn`: The device serial number (optional if settings_data is provided) + * `settings_data`: Settings data from sph_detail() (optional if device_sn is provided) + * note: Either `device_sn` or `settings_data` must be provided + * returns: Dict with `charge_power`, `charge_stop_soc`, `mains_enabled`, and `periods` list * **Read AC Discharge Time Periods** * function: `api.sph_read_ac_discharge_times` * parameters: - * `device_sn`: The device serial number - * `settings_data`: Optional settings data to avoid redundant API calls + * `device_sn`: The device serial number (optional if settings_data is provided) + * `settings_data`: Settings data from sph_detail() (optional if device_sn is provided) + * note: Either `device_sn` or `settings_data` must be provided + * returns: Dict with `discharge_power`, `discharge_stop_soc`, and `periods` list diff --git a/examples/sph_example.py b/examples/sph_example.py index d1580b2..3d6b731 100644 --- a/examples/sph_example.py +++ b/examples/sph_example.py @@ -7,16 +7,17 @@ """ import json +import os import requests import growattServer -# Get the API token from user input or environment variable -# api_token = os.environ.get("GROWATT_API_TOKEN") or input("Enter your Growatt API token: ") - -# test token from official API docs https://www.showdoc.com.cn/262556420217021/1494053950115877 -api_token = "6eb6f069523055a339d71e5b1f6c88cc" # noqa: S105 +# Get the API token from environment variable or use test token +api_token = os.environ.get("GROWATT_API_TOKEN") +if not api_token: + # test token from official API docs https://www.showdoc.com.cn/262556420217021/1494053950115877 + api_token = "6eb6f069523055a339d71e5b1f6c88cc" # noqa: S105 try: # Initialize the API with token instead of using login @@ -65,29 +66,24 @@ sort_keys=True, ) - # Get details (includes settings data) - detail_data = api.sph_detail( - device_sn=inverter_sn, + # Read AC charge time periods (reuse inverter_data, no device_sn needed) + charge_config = api.sph_read_ac_charge_times( + settings_data=inverter_data, ) - print("Saving detail data to settings_data.json") # noqa: T201 - with open("settings_data.json", "w") as f: - json.dump(detail_data, f, indent=4, sort_keys=True) - - # Read AC charge time periods - charge_times = api.sph_read_ac_charge_times( - device_sn=inverter_sn, - settings_data=detail_data, - ) - print("AC Charge Time Periods:") # noqa: T201 - print(json.dumps(charge_times, indent=4)) # noqa: T201 - - # Read AC discharge time periods - discharge_times = api.sph_read_ac_discharge_times( - device_sn=inverter_sn, - settings_data=detail_data, + print("AC Charge Configuration:") # noqa: T201 + print(f" Charge Power: {charge_config['charge_power']}%") # noqa: T201 + print(f" Stop SOC: {charge_config['charge_stop_soc']}%") # noqa: T201 + print(f" Mains Enabled: {charge_config['mains_enabled']}") # noqa: T201 + print(f" Periods: {json.dumps(charge_config['periods'], indent=4)}") # noqa: T201 + + # Read AC discharge time periods (reuse inverter_data, no device_sn needed) + discharge_config = api.sph_read_ac_discharge_times( + settings_data=inverter_data, ) - print("AC Discharge Time Periods:") # noqa: T201 - print(json.dumps(discharge_times, indent=4)) # noqa: T201 + print("AC Discharge Configuration:") # noqa: T201 + print(f" Discharge Power: {discharge_config['discharge_power']}%") # noqa: T201 + print(f" Stop SOC: {discharge_config['discharge_stop_soc']}%") # noqa: T201 + print(f" Periods: {json.dumps(discharge_config['periods'], indent=4)}") # noqa: T201 # Read discharge power discharge_power = api.sph_read_parameter( diff --git a/growattServer/open_api_v1.py b/growattServer/open_api_v1.py index c0a15c4..b9136a5 100644 --- a/growattServer/open_api_v1.py +++ b/growattServer/open_api_v1.py @@ -1068,80 +1068,135 @@ def _parse_time_periods(self, settings_data, time_type): return periods - def sph_read_ac_charge_times(self, device_sn, settings_data=None): + def sph_read_ac_charge_times(self, device_sn=None, settings_data=None): """ - Read AC charge time periods from an SPH inverter. + Read AC charge time periods and settings from an SPH inverter. - Retrieves all 3 AC charge time periods from an SPH inverter and - parses them into a structured format. + Retrieves all 3 AC charge time periods plus global charge settings + (power, stop SOC, mains enabled) from an SPH inverter. Note that this function uses sph_detail() internally to get the settings data. To avoid endpoint rate limit, you can pass the settings_data parameter with the data returned from sph_detail(). Args: - device_sn (str): The device serial number of the inverter + device_sn (str, optional): The device serial number of the inverter. + Required if settings_data is not provided. settings_data (dict, optional): Settings data from sph_detail call to avoid repeated API calls. + If provided, device_sn is not required. Returns: - list: A list of dictionaries, each containing details for one time period: - - period_id (int): The period number (1-3) - - start_time (str): Start time in format "HH:MM" - - end_time (str): End time in format "HH:MM" - - enabled (bool): Whether the period is enabled + dict: A dictionary containing: + - charge_power (int): Charging power percentage (0-100) + - charge_stop_soc (int): Stop charging at this SOC percentage (0-100) + - mains_enabled (bool): Whether grid/mains charging is enabled + - periods (list): List of 3 period dicts, each with: + - period_id (int): The period number (1-3) + - start_time (str): Start time in format "HH:MM" + - end_time (str): End time in format "HH:MM" + - enabled (bool): Whether the period is enabled Example: - # Option 1: Make a single call - charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER") + # Option 1: Fetch settings automatically + charge_config = api.sph_read_ac_charge_times(device_sn="DEVICE_SERIAL_NUMBER") + print(f"Charge power: {charge_config['charge_power']}%") + print(f"Periods: {charge_config['periods']}") - # Option 2: Reuse existing settings data + # Option 2: Reuse existing settings data (no device_sn needed) settings_response = api.sph_detail("DEVICE_SERIAL_NUMBER") - charge_times = api.sph_read_ac_charge_times("DEVICE_SERIAL_NUMBER", settings_response) + charge_config = api.sph_read_ac_charge_times(settings_data=settings_response) Raises: - GrowattV1ApiError: If the API request fails + GrowattParameterError: If neither device_sn nor settings_data is provided. + GrowattV1ApiError: If the API request fails. requests.exceptions.RequestException: If there is an issue with the HTTP request. """ if settings_data is None: + if device_sn is None: + raise GrowattParameterError("Either device_sn or settings_data must be provided") settings_data = self.sph_detail(device_sn=device_sn) - return self._parse_time_periods(settings_data, "Charge") + # Extract global charge settings + charge_power = settings_data.get('chargePowerCommand', 0) + charge_stop_soc = settings_data.get('wchargeSOCLowLimit', 100) + mains_enabled_raw = settings_data.get('acChargeEnable', 0) + + # Handle null/empty values + if charge_power == 'null' or charge_power is None or charge_power == '': + charge_power = 0 + if charge_stop_soc == 'null' or charge_stop_soc is None or charge_stop_soc == '': + charge_stop_soc = 100 + if mains_enabled_raw == 'null' or mains_enabled_raw is None or mains_enabled_raw == '': + mains_enabled = False + else: + mains_enabled = int(mains_enabled_raw) == 1 + + return { + 'charge_power': int(charge_power), + 'charge_stop_soc': int(charge_stop_soc), + 'mains_enabled': mains_enabled, + 'periods': self._parse_time_periods(settings_data, "Charge") + } - def sph_read_ac_discharge_times(self, device_sn, settings_data=None): + def sph_read_ac_discharge_times(self, device_sn=None, settings_data=None): """ - Read AC discharge time periods from an SPH inverter. + Read AC discharge time periods and settings from an SPH inverter. - Retrieves all 3 AC discharge time periods from an SPH inverter and - parses them into a structured format. + Retrieves all 3 AC discharge time periods plus global discharge settings + (power, stop SOC) from an SPH inverter. Note that this function uses sph_detail() internally to get the settings data. To avoid endpoint rate limit, you can pass the settings_data parameter with the data returned from sph_detail(). Args: - device_sn (str): The device serial number of the inverter + device_sn (str, optional): The device serial number of the inverter. + Required if settings_data is not provided. settings_data (dict, optional): Settings data from sph_detail call to avoid repeated API calls. + If provided, device_sn is not required. Returns: - list: A list of dictionaries, each containing details for one time period: - - period_id (int): The period number (1-3) - - start_time (str): Start time in format "HH:MM" - - end_time (str): End time in format "HH:MM" - - enabled (bool): Whether the period is enabled + dict: A dictionary containing: + - discharge_power (int): Discharging power percentage (0-100) + - discharge_stop_soc (int): Stop discharging at this SOC percentage (0-100) + - periods (list): List of 3 period dicts, each with: + - period_id (int): The period number (1-3) + - start_time (str): Start time in format "HH:MM" + - end_time (str): End time in format "HH:MM" + - enabled (bool): Whether the period is enabled Example: - # Option 1: Make a single call - discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER") + # Option 1: Fetch settings automatically + discharge_config = api.sph_read_ac_discharge_times(device_sn="DEVICE_SERIAL_NUMBER") + print(f"Discharge power: {discharge_config['discharge_power']}%") + print(f"Stop SOC: {discharge_config['discharge_stop_soc']}%") - # Option 2: Reuse existing settings data + # Option 2: Reuse existing settings data (no device_sn needed) settings_response = api.sph_detail("DEVICE_SERIAL_NUMBER") - discharge_times = api.sph_read_ac_discharge_times("DEVICE_SERIAL_NUMBER", settings_response) + discharge_config = api.sph_read_ac_discharge_times(settings_data=settings_response) Raises: - GrowattV1ApiError: If the API request fails + GrowattParameterError: If neither device_sn nor settings_data is provided. + GrowattV1ApiError: If the API request fails. requests.exceptions.RequestException: If there is an issue with the HTTP request. """ if settings_data is None: + if device_sn is None: + raise GrowattParameterError("Either device_sn or settings_data must be provided") settings_data = self.sph_detail(device_sn=device_sn) - return self._parse_time_periods(settings_data, "Discharge") + # Extract global discharge settings + discharge_power = settings_data.get('disChargePowerCommand', 0) + discharge_stop_soc = settings_data.get('wdisChargeSOCLowLimit', 10) + + # Handle null/empty values + if discharge_power == 'null' or discharge_power is None or discharge_power == '': + discharge_power = 0 + if discharge_stop_soc == 'null' or discharge_stop_soc is None or discharge_stop_soc == '': + discharge_stop_soc = 10 + + return { + 'discharge_power': int(discharge_power), + 'discharge_stop_soc': int(discharge_stop_soc), + 'periods': self._parse_time_periods(settings_data, "Discharge") + } From 8b9a67ca3a1a95fc4aea366758a0f55f877fcbdc Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Sat, 17 Jan 2026 11:56:12 +0100 Subject: [PATCH 5/5] docs and API: clarify SPH helper methods, harmonize with MIN V1, improve docs and examples --- docs/openapiv1.md | 27 ++-- docs/openapiv1/sph_settings.md | 261 ++++++++++++++++++++++++++------- examples/sph_example.py | 68 ++++++--- growattServer/open_api_v1.py | 34 +++-- 4 files changed, 289 insertions(+), 101 deletions(-) diff --git a/docs/openapiv1.md b/docs/openapiv1.md index 1c5983f..da21723 100644 --- a/docs/openapiv1.md +++ b/docs/openapiv1.md @@ -54,17 +54,26 @@ Methods for SPH devices (type 5). | Method | Arguments | Description | |:---|:---|:---| -| `api.sph_detail(device_sn)` | device_sn: String | Get detailed data and settings for an SPH hybrid inverter. | +| `api.sph_detail(device_sn)` | device_sn: String | Get detailed data and settings for an SPH hybrid inverter. see: [details](./openapiv1/sph_settings.md) | | `api.sph_energy(device_sn)` | device_sn: String | Get current energy data for an SPH inverter, including power and energy values. | | `api.sph_energy_history(device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None)` | device_sn: String, start_date: Date, end_date: Date, timezone: String, page: Int, limit: Int | Get energy history data for an SPH inverter (7-day max range). | -| `api.sph_read_parameter(device_sn, parameter_id, start_address=None, end_address=None)` | device_sn: String, parameter_id: String, start_address: Int, end_address: Int | Read a specific setting for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on an SPH inverter. Parameter values can be a single value, a list, or a dictionary. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_write_ac_charge_times(device_sn, charge_power, charge_stop_soc, mains_enabled, periods)` | device_sn: String, charge_power: Int (0-100), charge_stop_soc: Int (0-100), mains_enabled: Bool, periods: List of 3 dicts with start_time, end_time, enabled | Configure AC charge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_write_ac_discharge_times(device_sn, discharge_power, discharge_stop_soc, periods)` | device_sn: String, discharge_power: Int (0-100), discharge_stop_soc: Int (0-100), periods: List of 3 dicts with start_time, end_time, enabled | Configure AC discharge time periods for an SPH inverter. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_charge_times(device_sn=None, settings_data=None)` | device_sn: String (optional if settings_data provided), settings_data: Dict | Read AC charge configuration including charge_power, charge_stop_soc, mains_enabled, and time periods. see: [details](./openapiv1/sph_settings.md) | -| `api.sph_read_ac_discharge_times(device_sn=None, settings_data=None)` | device_sn: String (optional if settings_data provided), settings_data: Dict | Read AC discharge configuration including discharge_power, discharge_stop_soc, and time periods. see: [details](./openapiv1/sph_settings.md) | - -Methods from [here](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this file where possible. There is no guarantee those methods will work, or remain stable through updates. +| `api.sph_read_parameter(device_sn, parameter_id=None, start_address=None, end_address=None)` | device_sn: String, parameter_id: String (optional), start_address: Int (optional), end_address: Int (optional) | Read a specific parameter (only pv_on_off supported). see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_parameter(device_sn, parameter_id, parameter_values)` | device_sn: String, parameter_id: String, parameter_values: Dict/Array | Set parameters on an SPH inverter. see: [details](./openapiv1/sph_settings.md) | + +#### SPH Helper Methods + +Convenience methods that wrap the core SPH methods above for common use cases. + +| Method | Arguments | Description | +|:---|:---|:---| +| `api.sph_write_ac_charge_times(...)` | device_sn, charge_power, charge_stop_soc, mains_enabled, periods | Helper: wraps `sph_write_parameter()` with type `mix_ac_charge_time_period`. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_write_ac_discharge_times(...)` | device_sn, discharge_power, discharge_stop_soc, periods | Helper: wraps `sph_write_parameter()` with type `mix_ac_discharge_time_period`. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_charge_times(...)` | device_sn (optional), settings_data (optional) | Helper: parses charge config from `sph_detail()` response. see: [details](./openapiv1/sph_settings.md) | +| `api.sph_read_ac_discharge_times(...)` | device_sn (optional), settings_data (optional) | Helper: parses discharge config from `sph_detail()` response. see: [details](./openapiv1/sph_settings.md) | + +#### Classic methods + +Methods from [classic API](./shinephone.md#methods) should be available, but it's safer to rely on the functions described in this section where possible. There is no guarantee that the classic API methods will work, or remain stable through updates. ### Variables diff --git a/docs/openapiv1/sph_settings.md b/docs/openapiv1/sph_settings.md index 60d0e65..6d46df1 100644 --- a/docs/openapiv1/sph_settings.md +++ b/docs/openapiv1/sph_settings.md @@ -2,57 +2,210 @@ This is part of the [OpenAPI V1 doc](../openapiv1.md). -For SPH (hybrid inverter) systems, the public V1 API provides methods to read and write inverter settings. SPH inverters have different time period configurations compared to MIN inverters: - -* **Read Parameter** - * function: `api.sph_read_parameter` - * parameters: - * `device_sn`: The device serial number - * `parameter_id`: Parameter ID to read (e.g., "discharge_power") - * `start_address`, `end_address`: Optional, for reading registers by address - -* **Write Parameter** - * function: `api.sph_write_parameter` - * parameters: - * `device_sn`: The device serial number - * `parameter_id`: Parameter ID to write (e.g., "ac_charge") - * `parameter_values`: Value to set (single value, list, or dictionary) - -* **AC Charge Time Periods** - * function: `api.sph_write_ac_charge_times` - * parameters: - * `device_sn`: The device serial number - * `charge_power`: Charging power percentage (0-100) - * `charge_stop_soc`: Stop charging at this SOC percentage (0-100) - * `mains_enabled`: Boolean to enable/disable grid charging - * `periods`: List of 3 period dicts, each with: - * `start_time`: datetime.time object for period start - * `end_time`: datetime.time object for period end - * `enabled`: Boolean to enable/disable period - -* **AC Discharge Time Periods** - * function: `api.sph_write_ac_discharge_times` - * parameters: - * `device_sn`: The device serial number - * `discharge_power`: Discharge power percentage (0-100) - * `discharge_stop_soc`: Stop discharging at this SOC percentage (0-100) - * `periods`: List of 3 period dicts, each with: - * `start_time`: datetime.time object for period start - * `end_time`: datetime.time object for period end - * `enabled`: Boolean to enable/disable period - -* **Read AC Charge Time Periods** - * function: `api.sph_read_ac_charge_times` - * parameters: - * `device_sn`: The device serial number (optional if settings_data is provided) - * `settings_data`: Settings data from sph_detail() (optional if device_sn is provided) - * note: Either `device_sn` or `settings_data` must be provided - * returns: Dict with `charge_power`, `charge_stop_soc`, `mains_enabled`, and `periods` list - -* **Read AC Discharge Time Periods** - * function: `api.sph_read_ac_discharge_times` - * parameters: - * `device_sn`: The device serial number (optional if settings_data is provided) - * `settings_data`: Settings data from sph_detail() (optional if device_sn is provided) - * note: Either `device_sn` or `settings_data` must be provided - * returns: Dict with `discharge_power`, `discharge_stop_soc`, and `periods` list +For SPH (hybrid inverter) systems, the public V1 API provides methods to read and write inverter settings. + +**Source:** [Official Growatt API Documentation](https://www.showdoc.com.cn/262556420217021/6129763571291058) + +## Read All Settings + +* function: `api.sph_detail` +* parameters: + * `device_sn`: The device serial number +* returns: Dict containing all device data and settings + +**Return parameter description:** + +| Field | Type | Description | +|-------|------|-------------| +| serialNum | String | Serial Number | +| portName | String | Communication port information Communication port type and address | +| dataLogSn | String | DataLog serial number | +| groupId | int | Inverter group | +| alias | String | Alias | +| location | String | Location | +| addr | int | Inverter address | +| fwVersion | String | Firmware version | +| model | long | Model | +| innerVersion | String | Internal version number | +| lost | boolean | Whether communication is lost | +| status | int | Mix Status 0: waiting mode, 1: self-check mode, 3: failure mode, 4: upgrading, 5, 6, 7, 8: normal mode | +| tcpServerIp | String | TCP server IP address | +| lastUpdateTime | Date | Last update time | +| sysTime | Calendar | System Time | +| deviceType | int | 0: Mix6k, 1: Mix4-10k | +| communicationVersion | String | Communication version number | +| onOff | int | Switch machine | +| pmax | int | Rated power | +| vnormal | float | Rated PV voltage | +| lcdLanguage | int | LCD language | +| countrySelected | int | Country selection | +| wselectBaudrate | int | Baud rate selection | +| comAddress | int | Mailing address | +| manufacturer | String | Manufacturer Code | +| dtc | int | Device code | +| modbusVersion | int | MODBUS version | +| floatChargeCurrentLimit | float | Float charge current limit | +| vbatWarning | float | Low battery voltage alarm point | +| vbatWarnClr | float | Low battery voltage recovery point | +| vbatStopForDischarge | float | Battery discharge stop voltage | +| vbatStopForCharge | float | Battery charging stop voltage | +| vbatStartForDischarge | float | Lower limit of battery discharge voltage | +| vbatStartforCharge | float | Battery charging upper limit voltage | +| batTempLowerLimitD | float | Lower limit of battery discharge temperature | +| batTempUpperLimitD | float | Upper limit of battery discharge temperature | +| batTempLowerLimitC | float | Lower limit of battery charging temperature | +| batTempUpperLimitC | float | Upper limit of battery charging temperature | +| forcedDischargeTimeStart1 | String | Discharge 1 start time | +| forcedDischargeTimeStart2 | String | Discharge 2 start time | +| forcedDischargeTimeStart3 | String | Discharge 3 start time | +| forcedDischargeTimeStop1 | String | Discharge 1 stop time | +| forcedDischargeTimeStop2 | String | Discharge 2 stop time | +| forcedDischargeTimeStop3 | String | Discharge 3 stop time | +| forcedChargeTimeStart1 | String | Charge 1 start time | +| forcedChargeTimeStart2 | String | Charge 2 start time | +| forcedChargeTimeStart3 | String | Charge 3 start time | +| forcedChargeTimeStop1 | String | Charge 1 stop time | +| forcedChargeTimeStop2 | String | Charge 2 stop time | +| forcedChargeTimeStop3 | String | Charge 3 stop time | +| bctMode | int | Sensor type (2:METER;1:cWirelessCT;0:cWiredCT) | +| bctAdjust | int | Sensor adjustment enable | +| wdisChargeSOCLowLimit1 | int | Discharge in load priority mode | +| wdisChargeSOCLowLimit2 | int | Grid priority mode discharge | +| wchargeSOCLowLimit1 | int | Load priority mode charging | +| wchargeSOCLowLimit2 | int | Battery priority mode charging | +| acChargeEnable | int | AC charging enable | +| priorityChoose | int | Energy priority selection | +| chargePowerCommand | int | Charging power setting | +| disChargePowerCommand | int | Discharge power setting | +| bagingTestStep | int | Battery self-test | +| batteryType | int | Battery type selection | +| epsFunEn | int | Emergency power enable | +| epsVoltSet | int | Emergency power supply voltage | +| epsFreqSet | int | Emergency power frequency | +| forcedDischargeStopSwitch1 | int | Discharge 1 enable bit | +| forcedDischargeStopSwitch2 | int | Discharge 2 enable bit | +| forcedDischargeStopSwitch3 | int | Discharge 3 enable bit | +| forcedChargeStopSwitch1 | int | Charge 1 enable bit | +| forcedChargeStopSwitch2 | int | Charge 2 enable bit | +| forcedChargeStopSwitch3 | int | Charge 3 enable bit | +| voltageHighLimit | float | Mains voltage upper limit | +| voltageLowLimit | float | Mains voltage lower limit | +| buckUpsFunEn | int | Off-grid enable | +| uspFreqSet | int | Off-grid frequency | +| buckUPSVoltSet | int | Off-grid voltage | +| pvPfCmdMemoryState | int | Does the inverter store the following commands | +| activeRate | int | Active power | +| reactiveRate | int | Reactive power | +| underExcited | int | Capacitive or Perceptual | +| exportLimit | int | Backflow prevention enable | +| exportLimitPowerRate | float | Backflow prevention | +| powerFactor | float | PF value | +| pv_on_off | String | Switch | +| pf_sys_year | String | Set time | +| pv_grid_voltage_high | String | Mains voltage upper limit | +| pv_grid_voltage_low | String | Mains voltage lower limit | +| mix_off_grid_enable | String | Off-grid enable | +| mix_ac_discharge_frequency | String | Off-grid frequency | +| mix_ac_discharge_voltage | String | Off-grid voltage | +| pv_pf_cmd_memory_state | String | Set whether to store the following PF commands | +| pv_active_p_rate | String | Set active power | +| pv_reactive_p_rate | String | Set reactive power | +| pv_reactive_p_rate_two | String | No power capacity/inductive | +| backflow_setting | String | Backflow prevention setting | +| pv_power_factor | String | Set PF value | +| batSeriesNum | int | Number of cells in series | +| batParallelNum | int | Number of parallel cells | +| error_code | string | 0: normal return, 10001: system error | +| error_msg | string | Error message prompt | + +## Read Parameter + +* function: `api.sph_read_parameter` +* parameters: + * `device_sn`: The device serial number + * `parameter_id`: Parameter ID to read (optional) + * `start_address`, `end_address`: Optional, for reading registers by address + +**Supported parameter types for reading:** + +| parameter_id | Description | Return value | +|--------------|-------------|--------------| +| `pv_on_off` | Switch | 0 (off), 1 (on) | + +## Write Parameter + +* function: `api.sph_write_parameter` +* parameters: + * `device_sn`: The device serial number + * `parameter_id`: Parameter ID to write + * `parameter_values`: Value to set (single value, list, or dictionary) + +**Supported parameter types for writing:** + +| parameter_id | Description | Values | +|--------------|-------------|--------| +| **Device Control** ||| +| `pv_on_off` | Switch | "0" (off), "1" (on) | +| `pf_sys_year` | Set time | hour:min format | +| **Charge Settings** ||| +| `mix_ac_charge_time_period` | Charge time periods | charge power, stop SOC, mains enable, time periods... | +| **Discharge Settings** ||| +| `mix_ac_discharge_time_period` | Discharge time periods | discharge power, stop SOC, time periods... | +| **Grid Settings** ||| +| `pv_grid_voltage_high` | Mains voltage upper limit | e.g. "270" | +| `pv_grid_voltage_low` | Mains voltage lower limit | e.g. "180" | +| `pv_active_p_rate` | Set active power | 0-100 | +| `pv_reactive_p_rate` | Set reactive power | value | +| `pv_reactive_p_rate_two` | No power capacity/inductive | value | +| `pv_pf_cmd_memory_state` | Set whether to store PF commands | "0" (no), "1" (yes) | +| `pv_power_factor` | Set PF value | 0-100 | +| `backflow_setting` | Backflow prevention setting | "1" (on), "0" (off), power % | +| **Off-Grid/EPS Settings** ||| +| `mix_off_grid_enable` | Off-grid enable | "1" (enabled), "0" (disabled) | +| `mix_ac_discharge_frequency` | Off-grid frequency | "0" (50Hz), "1" (60Hz) | +| `mix_ac_discharge_voltage` | Off-grid voltage | "0" (230V), "1" (208V), "2" (240V) | + +> **Note:** For time period settings, it's recommended to use the dedicated helper functions `sph_write_ac_charge_times()` and `sph_write_ac_discharge_times()` instead of calling `sph_write_parameter()` directly. + +## AC Charge Time Periods + +### Write: `api.sph_write_ac_charge_times` + +* parameters: + * `device_sn`: The device serial number + * `charge_power`: Charging power percentage (0-100) + * `charge_stop_soc`: Stop charging at this SOC percentage (0-100) + * `mains_enabled`: Boolean to enable/disable grid charging + * `periods`: List of 3 period dicts, each with: + * `start_time`: datetime.time object for period start + * `end_time`: datetime.time object for period end + * `enabled`: Boolean to enable/disable period + +### Read: `api.sph_read_ac_charge_times` + +* parameters: + * `device_sn`: The device serial number (not used if settings_data is provided) + * `settings_data`: Settings data from sph_detail() (not used if device_sn is provided) +* note: Either `device_sn` or `settings_data` must be provided +* returns: Dict with `charge_power`, `charge_stop_soc`, `mains_enabled`, and `periods` list + +## AC Discharge Time Periods + +### Write: `api.sph_write_ac_discharge_times` + +* parameters: + * `device_sn`: The device serial number + * `discharge_power`: Discharge power percentage (0-100) + * `discharge_stop_soc`: Stop discharging at this SOC percentage (0-100) + * `periods`: List of 3 period dicts, each with: + * `start_time`: datetime.time object for period start + * `end_time`: datetime.time object for period end + * `enabled`: Boolean to enable/disable period + +### Read: `api.sph_read_ac_discharge_times` + +* parameters: + * `device_sn`: The device serial number (not used if settings_data is provided) + * `settings_data`: Settings data from sph_detail() (not used if device_sn is provided) +* note: Either `device_sn` or `settings_data` must be provided +* returns: Dict with `discharge_power`, `discharge_stop_soc`, and `periods` list diff --git a/examples/sph_example.py b/examples/sph_example.py index 3d6b731..b8ee926 100644 --- a/examples/sph_example.py +++ b/examples/sph_example.py @@ -6,6 +6,7 @@ You can obtain an API token from the Growatt API documentation or developer portal. """ +import datetime import json import os @@ -37,14 +38,6 @@ inverter_sn = device["device_sn"] print(f"Processing SPH device: {inverter_sn}") # noqa: T201 - # Get device details - inverter_data = api.sph_detail( - device_sn=inverter_sn, - ) - print("Saving inverter data to inverter_data.json") # noqa: T201 - with open("inverter_data.json", "w") as f: - json.dump(inverter_data, f, indent=4, sort_keys=True) - # Get energy data energy_data = api.sph_energy( device_sn=inverter_sn, @@ -66,7 +59,23 @@ sort_keys=True, ) - # Read AC charge time periods (reuse inverter_data, no device_sn needed) + # Get device details + inverter_data = api.sph_detail( + device_sn=inverter_sn, + ) + print("Saving inverter data to inverter_data.json") # noqa: T201 + with open("inverter_data.json", "w") as f: + json.dump(inverter_data, f, indent=4, sort_keys=True) + + # Read some settings directly from inverter_data (from sph_detail) + # See docs/openapiv1/sph_settings.md for all available fields + print("Device Settings:") # noqa: T201 + print(f" Device status: {inverter_data.get('status', 'N/A')}") # noqa: T201 + print(f" Battery type: {inverter_data.get('batteryType', 'N/A')}") # noqa: T201 + print(f" EPS enabled: {inverter_data.get('epsFunEn', 'N/A')}") # noqa: T201 + print(f" Export limit: {inverter_data.get('exportLimitPowerRate', 'N/A')}%") # noqa: T201 + + # Read AC charge time periods using helper function and inverter_data to avoid rate limiting charge_config = api.sph_read_ac_charge_times( settings_data=inverter_data, ) @@ -76,7 +85,7 @@ print(f" Mains Enabled: {charge_config['mains_enabled']}") # noqa: T201 print(f" Periods: {json.dumps(charge_config['periods'], indent=4)}") # noqa: T201 - # Read AC discharge time periods (reuse inverter_data, no device_sn needed) + # Read AC discharge time periods using helper function and inverter_data to avoid rate limiting discharge_config = api.sph_read_ac_discharge_times( settings_data=inverter_data, ) @@ -85,21 +94,15 @@ print(f" Stop SOC: {discharge_config['discharge_stop_soc']}%") # noqa: T201 print(f" Periods: {json.dumps(discharge_config['periods'], indent=4)}") # noqa: T201 - # Read discharge power - discharge_power = api.sph_read_parameter( - device_sn=inverter_sn, - parameter_id="discharge_power", - ) - print(f"Current discharge power: {discharge_power}%") # noqa: T201 - # Write examples - Uncomment to test - # Set AC charge time periods: charge at 50% power to 95% SOC + # Example 1: Set AC charge time periods + # Charge at 50% power, stop at 95% SOC, grid charging enabled # api.sph_write_ac_charge_times( # device_sn=inverter_sn, - # charge_power=50, # 50% charging power - # charge_stop_soc=95, # Stop at 95% SOC - # mains_enabled=True, # Enable grid charging + # charge_power=50, + # charge_stop_soc=95, + # mains_enabled=True, # periods=[ # {"start_time": datetime.time(0, 0), "end_time": datetime.time(6, 0), "enabled": True}, # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, @@ -108,11 +111,12 @@ # ) # print("AC charge periods updated successfully") - # Set AC discharge time periods: discharge at 100% power to 20% SOC + # Example 2: Set AC discharge time periods + # Discharge at 100% power, stop at 20% SOC # api.sph_write_ac_discharge_times( # device_sn=inverter_sn, - # discharge_power=100, # 100% discharge power - # discharge_stop_soc=20, # Stop at 20% SOC + # discharge_power=100, + # discharge_stop_soc=20, # periods=[ # {"start_time": datetime.time(17, 0), "end_time": datetime.time(22, 0), "enabled": True}, # {"start_time": datetime.time(0, 0), "end_time": datetime.time(0, 0), "enabled": False}, @@ -121,6 +125,22 @@ # ) # print("AC discharge periods updated successfully") + # Example 3: Turn device on/off + # api.sph_write_parameter(inverter_sn, "pv_on_off", "1") # Turn on + # api.sph_write_parameter(inverter_sn, "pv_on_off", "0") # Turn off + + # Example 4: Set grid voltage limits + # api.sph_write_parameter(inverter_sn, "pv_grid_voltage_high", "270") + # api.sph_write_parameter(inverter_sn, "pv_grid_voltage_low", "180") + + # Example 5: Configure off-grid/EPS settings + # api.sph_write_parameter(inverter_sn, "mix_off_grid_enable", "1") # Enable + # api.sph_write_parameter(inverter_sn, "mix_ac_discharge_frequency", "0") # 50Hz + # api.sph_write_parameter(inverter_sn, "mix_ac_discharge_voltage", "0") # 230V + + # Example 6: Set anti-backflow (export limit) + # api.sph_write_parameter(inverter_sn, "backflow_setting", ["1", "50"]) # On, 50% + except growattServer.GrowattV1ApiError as e: print(f"API Error: {e} (Code: {e.error_code}, Message: {e.error_msg})") # noqa: T201 except growattServer.GrowattParameterError as e: diff --git a/growattServer/open_api_v1.py b/growattServer/open_api_v1.py index b9136a5..b06cd69 100644 --- a/growattServer/open_api_v1.py +++ b/growattServer/open_api_v1.py @@ -676,6 +676,7 @@ def sph_detail(self, device_sn): requests.exceptions.RequestException: If there is an issue with the HTTP request. """ + # API: https://www.showdoc.com.cn/262556420217021/6129763571291058 response = self.session.get( self._get_url('device/mix/mix_data_info'), params={ @@ -700,6 +701,7 @@ def sph_energy(self, device_sn): requests.exceptions.RequestException: If there is an issue with the HTTP request. """ + # API: https://www.showdoc.com.cn/262556420217021/6129764475556048 response = self.session.post( url=self._get_url("device/mix/mix_last_data"), data={ @@ -742,6 +744,7 @@ def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone if end_date - start_date > timedelta(days=7): raise GrowattParameterError("date interval must not exceed 7 days") + # API: https://www.showdoc.com.cn/262556420217021/6129765461123058 response = self.session.post( url=self._get_url('device/mix/mix_data'), data={ @@ -756,13 +759,13 @@ def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone return self._process_response(response.json(), "getting SPH inverter energy history") - def sph_read_parameter(self, device_sn, parameter_id, start_address=None, end_address=None): + def sph_read_parameter(self, device_sn, parameter_id=None, start_address=None, end_address=None): """ Read setting from SPH inverter. Args: device_sn (str): The ID of the SPH inverter. - parameter_id (str): Parameter ID to read. Don't use start_address and end_address if this is set. + parameter_id (str, optional): Parameter ID to read. Don't use start_address and end_address if this is set. start_address (int, optional): Register start address (for set_any_reg). Don't use parameter_id if this is set. end_address (int, optional): Register end address (for set_any_reg). Don't use parameter_id if this is set. @@ -790,13 +793,14 @@ def sph_read_parameter(self, device_sn, parameter_id, start_address=None, end_ad # address range parameter_id = "set_any_reg" + # API: https://www.showdoc.com.cn/262556420217021/6129766954561259 response = self.session.post( self._get_url('readMixParam'), data={ - "mix_sn": device_sn, - "type": parameter_id, - "param1": start_address, - "param2": end_address + "device_sn": device_sn, + "paramId": parameter_id, + "startAddr": start_address, + "endAddr": end_address } ) @@ -822,8 +826,8 @@ def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - # Initialize all parameters as empty strings - parameters = {i: "" for i in range(1, 20)} + # Initialize all parameters as empty strings (API uses param1-param18) + parameters = {i: "" for i in range(1, 19)} # Process parameter values based on type if parameter_values is not None: @@ -833,26 +837,26 @@ def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): elif isinstance(parameter_values, list): # List of values go to sequential params for i, value in enumerate(parameter_values, 1): - if i <= 19: # Only use up to 19 parameters + if i <= 18: # Only use up to 18 parameters parameters[i] = str(value) elif isinstance(parameter_values, dict): # Dict maps param positions to values for pos, value in parameter_values.items(): pos = int(pos) if not isinstance(pos, int) else pos - if 1 <= pos <= 19: # Validate parameter positions + if 1 <= pos <= 18: # Validate parameter positions parameters[pos] = str(value) - # IMPORTANT: Create a data dictionary with ALL parameters explicitly included + # Create a data dictionary with ALL parameters explicitly included request_data = { "mix_sn": device_sn, "type": parameter_id } - # Add all 19 parameters to the request - for i in range(1, 20): + # Add all 18 parameters to the request + for i in range(1, 19): request_data[f"param{i}"] = str(parameters[i]) - # Send the request + # API: https://www.showdoc.com.cn/262556420217021/6129761750718760 response = self.session.post( self._get_url('mixSet'), data=request_data @@ -924,6 +928,7 @@ def sph_write_ac_charge_times(self, device_sn, charge_power, charge_stop_soc, ma request_data[f"param{base + 3}"] = str(period["end_time"].minute) request_data[f"param{base + 4}"] = "1" if period["enabled"] else "0" + # API: https://www.showdoc.com.cn/262556420217021/6129761750718760 response = self.session.post( self._get_url('mixSet'), data=request_data @@ -992,6 +997,7 @@ def sph_write_ac_discharge_times(self, device_sn, discharge_power, discharge_sto request_data[f"param{base + 3}"] = str(period["end_time"].minute) request_data[f"param{base + 4}"] = "1" if period["enabled"] else "0" + # API: https://www.showdoc.com.cn/262556420217021/6129761750718760 response = self.session.post( self._get_url('mixSet'), data=request_data