From 5c6330f08c66f8426a41a11cca7ae055f7258814 Mon Sep 17 00:00:00 2001 From: Johan Zander Date: Mon, 6 Apr 2026 00:04:27 +0200 Subject: [PATCH] Improve error codes, fix exception handling and default dates New: - Add GrowattV1ApiErrorCode IntEnum with generic API error codes, exported from the package so callers can use GrowattV1ApiErrorCode.NO_PRIVILEGE instead of hardcoding magic numbers Bug fixes: - Fix base_api.py: legacy TLX method incorrectly raised GrowattV1ApiError (a V1-specific exception); changed to GrowattError (base class) - Fix default date handling: use local system timezone instead of UTC so Growatt API receives the correct calendar date for the plant location - Fix GrowattV1ApiError.__init__: error_code and error_msg were typed as optional but all V1 raise sites always provide them; made required Docs: - Add endpoint-specific error codes to all API method docstrings - Add References sections with API doc URLs to all API methods - Standardise GrowattV1ApiError docstring to reference GrowattV1ApiErrorCode Co-Authored-By: Claude Sonnet 4.6 --- growattServer/__init__.py | 8 +- growattServer/base_api.py | 4 +- growattServer/exceptions.py | 36 +++- growattServer/open_api_v1/__init__.py | 186 +++++++++++++----- growattServer/open_api_v1/devices/min.py | 177 ++++++++++------- growattServer/open_api_v1/devices/sph.py | 230 ++++++++++++++++------- 6 files changed, 446 insertions(+), 195 deletions(-) diff --git a/growattServer/__init__.py b/growattServer/__init__.py index 41d839a..fc799dc 100755 --- a/growattServer/__init__.py +++ b/growattServer/__init__.py @@ -2,7 +2,12 @@ """growattServer package exports.""" from .base_api import GrowattApi, Timespan, hash_password -from .exceptions import GrowattError, GrowattParameterError, GrowattV1ApiError +from .exceptions import ( + GrowattError, + GrowattParameterError, + GrowattV1ApiError, + GrowattV1ApiErrorCode, +) from .open_api_v1 import DeviceType, OpenApiV1 # Package name @@ -14,6 +19,7 @@ "GrowattError", "GrowattParameterError", "GrowattV1ApiError", + "GrowattV1ApiErrorCode", "OpenApiV1", "Timespan", "hash_password", diff --git a/growattServer/base_api.py b/growattServer/base_api.py index 85f7005..e690406 100644 --- a/growattServer/base_api.py +++ b/growattServer/base_api.py @@ -10,7 +10,7 @@ import requests -from .exceptions import GrowattV1ApiError +from .exceptions import GrowattError name = "growattServer" @@ -1169,7 +1169,7 @@ def update_tlx_inverter_time_segment(self, serial_number, segment_id, batt_mode, if not result.get("success", False): msg = f"Failed to update TLX inverter time segment: {result.get('msg', 'Unknown error')}" - raise GrowattV1ApiError(msg) + raise GrowattError(msg) return result diff --git a/growattServer/exceptions.py b/growattServer/exceptions.py index 776afa2..ecf5fd8 100644 --- a/growattServer/exceptions.py +++ b/growattServer/exceptions.py @@ -1,5 +1,5 @@ """ -Exception classes for the growattServer library. +Exception classes and error code constants for the growattServer library. Note that in addition to these custom exceptions, methods may also raise exceptions from the underlying requests library (requests.exceptions.RequestException and its @@ -13,28 +13,52 @@ - requests.exceptions.RequestException: The base exception for all requests exceptions """ +from enum import IntEnum + + +class GrowattV1ApiErrorCode(IntEnum): + """ + Generic error codes returned by the Growatt V1 (OpenAPI) endpoints. + + These codes are common across all endpoints. Individual endpoints may also + return additional endpoint-specific error codes — see the docstrings of the + respective methods for details. + + Reference: https://www.showdoc.com.cn/262556420217021/1494055648380019 + """ + + SUCCESS = 0 # Normal (General) + NO_PRIVILEGE = 10011 # No privilege access (generic) + RATE_LIMITED = 10012 # Access Frequency Limitation of 5 Minutes/Time (Universal) + PAGE_SIZE_TOO_LARGE = ( + 10013 # The number per page cannot be greater than 100 (general) + ) + PAGE_COUNT_TOO_LARGE = ( + 10014 # The number of pages cannot be greater than 250 pages (general) + ) + WRONG_DOMAIN = -1 # Please use the new domain name to access + class GrowattError(Exception): """Base exception class for all Growatt API related errors.""" - class GrowattParameterError(GrowattError): """Raised when invalid parameters are provided to API methods.""" - class GrowattV1ApiError(GrowattError): """Raised when a Growatt V1 API request fails or returns an error.""" - def __init__(self, message: str, error_code: int | None = None, error_msg: str | None = None) -> None: + def __init__(self, message: str, error_code: int, error_msg: str) -> None: """ Initialize the GrowattV1ApiError. Args: message: Human readable error message. - error_code: Optional numeric error code returned by the API. - error_msg: Optional detailed error message from the API. + error_code: Numeric error code returned by the API. + See :class:`GrowattV1ApiErrorCode` for known generic codes. + error_msg: Error message returned by the API. """ super().__init__(message) diff --git a/growattServer/open_api_v1/__init__.py b/growattServer/open_api_v1/__init__.py index 4735d5d..5cc6468 100644 --- a/growattServer/open_api_v1/__init__.py +++ b/growattServer/open_api_v1/__init__.py @@ -1,4 +1,5 @@ """OpenApi V1 extensions for Growatt API client.""" + import platform import warnings from datetime import UTC, date, datetime @@ -17,7 +18,7 @@ class DeviceType(Enum): STORAGE = 2 OTHER = 3 MAX = 4 - SPH = Sph.DEVICE_TYPE_ID # (MIX) + SPH = Sph.DEVICE_TYPE_ID # (MIX) SPA = 6 MIN = Min.DEVICE_TYPE_ID PCS = 8 @@ -77,8 +78,8 @@ def process_response(self, response, operation_name="API operation"): msg = f"Error during {operation_name}" raise GrowattV1ApiError( msg, - error_code=response.get("error_code"), - error_msg=response.get("error_msg", "Unknown error") + error_code=response["error_code"], + error_msg=response.get("error_msg", "Unknown error"), ) return response.get("data") @@ -94,23 +95,24 @@ def plant_list(self): dict: A dictionary containing plants information with 'count' and 'plants' keys. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/1494058730404880 + """ # Prepare request data request_data = { "page": "", "perpage": "", "search_type": "", - "search_keyword": "" + "search_keyword": "", } # Make the request - response = self.session.get( - url=self.get_url("plant/list"), - data=request_data - ) + response = self.session.get(url=self.get_url("plant/list"), data=request_data) return self.process_response(response.json(), "getting plant list") @@ -125,13 +127,19 @@ def plant_details(self, plant_id): dict: A dictionary containing the plant details. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Power station does not exist + 10003 - Power station ID is empty + 10004 - User does not exist requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/1494060394238679 + """ response = self.session.get( - self.get_url("plant/details"), - params={"plant_id": plant_id} + self.get_url("plant/details"), params={"plant_id": plant_id} ) return self.process_response(response.json(), "getting plant details") @@ -147,18 +155,25 @@ def plant_energy_overview(self, plant_id): dict: A dictionary containing the plant energy overview. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Power station does not exist + 10003 - Power station ID is empty requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/1494061093808613 + """ response = self.session.get( - self.get_url("plant/data"), - params={"plant_id": plant_id} + self.get_url("plant/data"), params={"plant_id": plant_id} ) return self.process_response(response.json(), "getting plant energy overview") - def plant_power_overview(self, plant_id: int, day: str | date | None = None) -> dict: + def plant_power_overview( + self, plant_id: int, day: str | date | None = None + ) -> dict: """ Obtain power data of a certain power station. @@ -166,7 +181,7 @@ def plant_power_overview(self, plant_id: int, day: str | date | None = None) -> Args: plant_id (int): Power Station ID - day (date): Date - defaults to today + day (date): Date - defaults to today in the local system timezone Returns: dict: A dictionary containing the plants power data. @@ -180,33 +195,45 @@ def plant_power_overview(self, plant_id: int, day: str | date | None = None) -> }. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Power station does not exist + 10003 - Power station ID is empty or time format is incorrect requests.exceptions.RequestException: If there is an issue with the HTTP request. - API-Doc: https://www.showdoc.com.cn/262556420217021/1494062656174173 + References: + https://www.showdoc.com.cn/262556420217021/1494062656174173 """ if day is None: - day = datetime.now(UTC).date() + day = datetime.now(tz=UTC).astimezone().date() 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): + 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. Args: plant_id (int): Power Station ID - start_date (date, optional): Start Date - defaults to today - end_date (date, optional): End Date - defaults to today + start_date (date, optional): Start Date - defaults to today in the local system timezone + end_date (date, optional): End Date - defaults to today in the local system timezone time_unit (str, optional): Time unit ('day', 'month', 'year') - defaults to 'day' page (int, optional): Page number - defaults to 1 perpage (int, optional): Number of items per page - defaults to 20, max 100 @@ -221,16 +248,23 @@ def plant_energy_history(self, plant_id, start_date=None, end_date=None, time_un Raises: GrowattParameterError: If date parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Power station does not exist + 10003 - Power station ID is empty + 10004 - Time format is incorrect requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/1494061730868556 + """ max_day_interval = 7 max_year_interval = 20 if start_date is None and end_date is None: - start_date = datetime.now(UTC).date() - end_date = datetime.now(UTC).date() + start_date = datetime.now(tz=UTC).astimezone().date() + end_date = datetime.now(tz=UTC).astimezone().date() elif start_date is None: start_date = end_date elif end_date is None: @@ -239,13 +273,24 @@ def plant_energy_history(self, plant_id, start_date=None, end_date=None, time_un # Validate date ranges based on time_unit if time_unit == "day" and (end_date - start_date).days > max_day_interval: warnings.warn( - "Date interval must not exceed 7 days in 'day' mode.", RuntimeWarning, stacklevel=2) + "Date interval must not exceed 7 days in 'day' mode.", + RuntimeWarning, + stacklevel=2, + ) elif time_unit == "month" and (end_date.year - start_date.year > 1): warnings.warn( - "Start date must be within same or previous year in 'month' mode.", RuntimeWarning, stacklevel=2) - elif time_unit == "year" and (end_date.year - start_date.year > max_year_interval): + "Start date must be within same or previous year in 'month' mode.", + RuntimeWarning, + stacklevel=2, + ) + elif time_unit == "year" and ( + end_date.year - start_date.year > max_year_interval + ): warnings.warn( - "Date interval must not exceed 20 years in 'year' mode.", RuntimeWarning, stacklevel=2) + "Date interval must not exceed 20 years in 'year' mode.", + RuntimeWarning, + stacklevel=2, + ) response = self.session.get( self.get_url("plant/energy"), @@ -255,8 +300,8 @@ def plant_energy_history(self, plant_id, start_date=None, end_date=None, time_un "end_date": end_date.strftime("%Y-%m-%d"), "time_unit": time_unit, "page": page, - "perpage": perpage - } + "perpage": perpage, + }, ) return self.process_response(response.json(), "getting plant energy history") @@ -316,7 +361,7 @@ def device_list(self, plant_id): ) return self.process_response(response.json(), "getting device list") - def get_device(self, device_sn: str, device_type: int) -> AbstractDevice|None: + def get_device(self, device_sn: str, device_type: int) -> AbstractDevice | None: """Get the device class by serial number and device_type id.""" match device_type: case Sph.DEVICE_TYPE_ID: @@ -324,7 +369,10 @@ def get_device(self, device_sn: str, device_type: int) -> AbstractDevice|None: case Min.DEVICE_TYPE_ID: return Min(self, device_sn) case _: - warnings.warn(f"Device for type id: {device_type} has not been implemented yet.", stacklevel=2) + warnings.warn( + f"Device for type id: {device_type} has not been implemented yet.", + stacklevel=2, + ) return None def min_detail(self, device_sn): @@ -361,7 +409,15 @@ def min_energy(self, device_sn): """ return Min(self, device_sn).energy() - def min_energy_history(self, device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None): + def min_energy_history( + self, + device_sn, + start_date=None, + end_date=None, + timezone=None, + page=None, + limit=None, + ): """ Get MIN inverter data history. @@ -382,7 +438,9 @@ def min_energy_history(self, device_sn, start_date=None, end_date=None, timezone requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Min(self, device_sn).energy_history(start_date, end_date, timezone, page, limit) + return Min(self, device_sn).energy_history( + start_date, end_date, timezone, page, limit + ) def min_settings(self, device_sn): """ @@ -401,7 +459,9 @@ def min_settings(self, device_sn): """ return Min(self, device_sn).settings() - def min_read_parameter(self, device_sn, parameter_id, start_address=None, end_address=None): + def min_read_parameter( + self, device_sn, parameter_id, start_address=None, end_address=None + ): """ Read setting from MIN inverter. @@ -420,7 +480,9 @@ def min_read_parameter(self, device_sn, parameter_id, start_address=None, end_ad requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Min(self, device_sn).read_parameter(parameter_id, start_address, end_address) + return Min(self, device_sn).read_parameter( + parameter_id, start_address, end_address + ) def min_write_parameter(self, device_sn, parameter_id, parameter_values=None): """ @@ -444,7 +506,9 @@ def min_write_parameter(self, device_sn, parameter_id, parameter_values=None): """ return Min(self, device_sn).write_parameter(parameter_id, parameter_values) - def min_write_time_segment(self, device_sn, segment_id, batt_mode, start_time, end_time, enabled=True): + def min_write_time_segment( + self, device_sn, segment_id, batt_mode, start_time, end_time, enabled=True + ): """ Set a time segment for a MIN inverter. @@ -465,7 +529,9 @@ def min_write_time_segment(self, device_sn, segment_id, batt_mode, start_time, e requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Min(self, device_sn).write_time_segment(segment_id, batt_mode, start_time, end_time, enabled) + return Min(self, device_sn).write_time_segment( + segment_id, batt_mode, start_time, end_time, enabled + ) def min_read_time_segments(self, device_sn, settings_data=None): """ @@ -543,7 +609,15 @@ def sph_energy(self, device_sn): """ return Sph(self, device_sn).energy() - def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone=None, page=None, limit=None): + def sph_energy_history( + self, + device_sn, + start_date=None, + end_date=None, + timezone=None, + page=None, + limit=None, + ): """ Get SPH inverter data history. @@ -564,9 +638,13 @@ def sph_energy_history(self, device_sn, start_date=None, end_date=None, timezone requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Sph(self, device_sn).energy_history(start_date, end_date, timezone, page, limit) + return Sph(self, device_sn).energy_history( + start_date, end_date, timezone, page, limit + ) - def sph_read_parameter(self, device_sn, parameter_id=None, 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. @@ -585,7 +663,9 @@ def sph_read_parameter(self, device_sn, parameter_id=None, start_address=None, e requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Sph(self, device_sn).read_parameter(parameter_id, start_address, end_address) + return Sph(self, device_sn).read_parameter( + parameter_id, start_address, end_address + ) def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): """ @@ -609,7 +689,9 @@ def sph_write_parameter(self, device_sn, parameter_id, parameter_values=None): """ return Sph(self, device_sn).write_parameter(parameter_id, parameter_values) - def sph_write_ac_charge_times(self, device_sn, charge_power, charge_stop_soc, mains_enabled, periods): + def sph_write_ac_charge_times( + self, device_sn, charge_power, charge_stop_soc, mains_enabled, periods + ): """ Set AC charge time periods for an SPH inverter. @@ -647,9 +729,13 @@ def sph_write_ac_charge_times(self, device_sn, charge_power, charge_stop_soc, ma requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Sph(self, device_sn).write_ac_charge_times(charge_power, charge_stop_soc, mains_enabled, periods) + return Sph(self, device_sn).write_ac_charge_times( + charge_power, charge_stop_soc, mains_enabled, periods + ) - def sph_write_ac_discharge_times(self, device_sn, discharge_power, discharge_stop_soc, periods): + def sph_write_ac_discharge_times( + self, device_sn, discharge_power, discharge_stop_soc, periods + ): """ Set AC discharge time periods for an SPH inverter. @@ -685,7 +771,9 @@ def sph_write_ac_discharge_times(self, device_sn, discharge_power, discharge_sto requests.exceptions.RequestException: If there is an issue with the HTTP request. """ - return Sph(self, device_sn).write_ac_discharge_times(discharge_power, discharge_stop_soc, periods) + return Sph(self, device_sn).write_ac_discharge_times( + discharge_power, discharge_stop_soc, periods + ) def sph_read_ac_charge_times(self, device_sn, settings_data=None): """ diff --git a/growattServer/open_api_v1/devices/min.py b/growattServer/open_api_v1/devices/min.py index ca1737b..702cb6c 100644 --- a/growattServer/open_api_v1/devices/min.py +++ b/growattServer/open_api_v1/devices/min.py @@ -1,4 +1,5 @@ """Min/TLX device file.""" + from datetime import UTC, datetime, timedelta from typing import Any @@ -16,42 +17,44 @@ def detail(self) -> dict: """ Get detailed data for a MIN inverter. - See the API doc: https://www.showdoc.com.cn/262556420217021/6129816412127075. - - Args: - device_sn (str): The serial number of the MIN inverter. - Returns: dict: A dictionary containing the MIN inverter details. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129816412127075 + """ response = self.api.session.get( self.api.get_url("device/tlx/tlx_data_info"), - params={ - "device_sn": self.device_sn - } + params={"device_sn": self.device_sn}, ) - return self.api.process_response(response.json(), "getting MIN inverter details") + return self.api.process_response( + response.json(), "getting MIN inverter details" + ) def energy(self) -> dict: """ Get energy data for a MIN inverter. - Args: - device_sn (str): The serial number of the MIN inverter. - Returns: dict: A dictionary containing the MIN inverter energy data. Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Min does not exist + 10003 - Device SN error requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129822090975531 + """ response = self.api.session.post( url=self.api.get_url("device/tlx/tlx_last_data"), @@ -60,16 +63,20 @@ def energy(self) -> dict: }, ) - return self.api.process_response(response.json(), "getting MIN inverter energy data") + return self.api.process_response( + response.json(), "getting MIN inverter energy data" + ) - def energy_history(self, start_date=None, end_date=None, timezone=None, page=None, limit=None) -> dict: + def energy_history( + self, start_date=None, end_date=None, timezone=None, page=None, limit=None + ) -> dict: """ Get MIN inverter data history. Args: device_sn (str): The ID of the MIN inverter. - start_date (date, optional): Start date. Defaults to today. - end_date (date, optional): End date. Defaults to today. + start_date (date, optional): Start date. Defaults to today in the local system timezone. + end_date (date, optional): End date. Defaults to today in the local system timezone. timezone (str, optional): Timezone ID. page (int, optional): Page number. limit (int, optional): Results per page. @@ -79,13 +86,22 @@ def energy_history(self, start_date=None, end_date=None, timezone=None, page=Non Raises: GrowattParameterError: If date interval is invalid (exceeds 7 days). - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Serial number is empty + 10003 - Start date is wrong + 10004 - Start date interval has exceeded seven days + 10005 - Min does not exist + 10011 - Permission is not satisfied requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129764475556048 + """ if start_date is None and end_date is None: - start_date = datetime.now(UTC).date() - end_date = datetime.now(UTC).date() + start_date = datetime.now(tz=UTC).astimezone().date() + end_date = datetime.now(tz=UTC).astimezone().date() elif start_date is None: start_date = end_date elif end_date is None: @@ -104,18 +120,17 @@ def energy_history(self, start_date=None, end_date=None, timezone=None, page=Non "timezone_id": timezone, "page": page, "perpage": limit, - } + }, ) - return self.api.process_response(response.json(), "getting MIN inverter energy history") + return self.api.process_response( + response.json(), "getting MIN inverter energy history" + ) def settings(self) -> dict: """ Get settings for a MIN inverter. - Args: - device_sn (str): The serial number of the MIN inverter. - Returns: dict: A dictionary containing the MIN inverter settings. @@ -123,22 +138,26 @@ def settings(self) -> dict: GrowattV1ApiError: If the API returns an error response. requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/8696815667375182 + """ response = self.api.session.get( self.api.get_url("device/tlx/tlx_set_info"), - params={ - "device_sn": self.device_sn - } + params={"device_sn": self.device_sn}, ) - return self.api.process_response(response.json(), "getting MIN inverter settings") + return self.api.process_response( + response.json(), "getting MIN inverter settings" + ) - def read_parameter(self, parameter_id, start_address=None, end_address=None) -> dict: + def read_parameter( + self, parameter_id, start_address=None, end_address=None + ) -> dict: """ Read setting from MIN inverter. Args: - device_sn (str): The ID of the TLX 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. @@ -148,9 +167,21 @@ def read_parameter(self, parameter_id, start_address=None, end_address=None) -> Raises: GrowattParameterError: If parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - Reading failed + 10002 - Device does not exist + 10003 - Device offline + 10004 - Collector serial number is empty + 10005 - Collector offline + 10006 - Collector type does not support reading Get function + 10007 - The collector version does not support the reading function + 10008 - The collector connects to the server error, please restart and try again + 10009 - The read setting parameter type does not exist requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129828239577315 + """ self.validate_read_parameter_input(parameter_id, start_address, end_address) @@ -173,17 +204,18 @@ def read_parameter(self, parameter_id, start_address=None, end_address=None) -> "paramId": parameter_id, "startAddr": start_address, "endAddr": end_address, - } + }, ) - return self.api.process_response(response.json(), f"reading parameter {parameter_id}") + return self.api.process_response( + response.json(), f"reading parameter {parameter_id}" + ) def write_parameter(self, parameter_id, parameter_values=None) -> dict: """ Set parameters on a MIN 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), @@ -194,9 +226,23 @@ def write_parameter(self, parameter_id, parameter_values=None) -> dict: dict: JSON response from the server Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Min server error + 10003 - Min offline + 10004 - Min serial number is empty + 10005 - Collector offline + 10006 - Setting parameter type does not exist + 10007 - The parameter value is empty + 10008 - The parameter value is not within the range + 10009 - The date and time format is wrong + 10012 - Min does not exist + 10013 - The end time cannot be less than the start time requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129826876191828 + """ # Initialize all parameters as empty strings max_min_params = 19 @@ -220,29 +266,26 @@ def write_parameter(self, parameter_id, parameter_values=None) -> dict: parameters[pos] = str(value) # IMPORTANT: Create a data dictionary with ALL parameters explicitly included - request_data = { - "tlx_sn": self.device_sn, - "type": parameter_id - } + request_data = {"tlx_sn": self.device_sn, "type": parameter_id} # Add all MIN parameters to the request for i in range(1, max_min_params + 1): request_data[f"param{i}"] = str(parameters[i]) # Send the request - response = self.api.session.post( - self.api.get_url("tlxSet"), - data=request_data - ) + response = self.api.session.post(self.api.get_url("tlxSet"), data=request_data) - return self.api.process_response(response.json(), f"writing parameter {parameter_id}") + return self.api.process_response( + response.json(), f"writing parameter {parameter_id}" + ) - def write_time_segment(self, segment_id, batt_mode, start_time, end_time, enabled=True) -> dict: + def write_time_segment( + self, segment_id, batt_mode, start_time, end_time, enabled=True + ) -> dict: """ Set a time segment for a MIN inverter. Args: - device_sn (str): The serial number of the inverter. segment_id (int): Time segment ID (1-9). batt_mode (int): 0=load priority, 1=battery priority, 2=grid priority. start_time (datetime.time): Start time for the segment. @@ -254,9 +297,23 @@ def write_time_segment(self, segment_id, batt_mode, start_time, end_time, enable Raises: GrowattParameterError: If parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Min server error + 10003 - Min offline + 10004 - Min serial number is empty + 10005 - Collector offline + 10006 - Setting parameter type does not exist + 10007 - The parameter value is empty + 10008 - The parameter value is not within the range + 10009 - The date and time format is wrong + 10012 - Min does not exist + 10013 - The end time cannot be less than the start time requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129826876191828 + """ max_min_params = 19 max_min_segments = 9 @@ -271,10 +328,7 @@ def write_time_segment(self, segment_id, batt_mode, start_time, end_time, enable raise GrowattParameterError(msg) # Initialize ALL 19 parameters as empty strings, not just the ones we need - all_params = { - "tlx_sn": self.device_sn, - "type": f"time_segment{segment_id}" - } + all_params = {"tlx_sn": self.device_sn, "type": f"time_segment{segment_id}"} # Add param1 through param19, setting the values we need all_params["param1"] = str(batt_mode) @@ -289,12 +343,11 @@ def write_time_segment(self, segment_id, batt_mode, start_time, end_time, enable all_params[f"param{i}"] = "" # Send the request - response = self.api.session.post( - self.api.get_url("tlxSet"), - data=all_params - ) + response = self.api.session.post(self.api.get_url("tlxSet"), data=all_params) - return self.api.process_response(response.json(), f"writing time segment {segment_id}") + return self.api.process_response( + response.json(), f"writing time segment {segment_id}" + ) def read_time_segments(self, settings_data=None) -> list[dict[str, Any]]: """ @@ -340,11 +393,7 @@ def read_time_segments(self, settings_data=None) -> list[dict[str, Any]]: settings_data = self.settings() # Define mode names - mode_names = { - 0: "Load First", - 1: "Battery First", - 2: "Grid First" - } + mode_names = {0: "Load First", 1: "Battery First", 2: "Grid First"} segments = [] @@ -403,7 +452,7 @@ def read_time_segments(self, settings_data=None) -> list[dict[str, Any]]: "mode_name": mode_names.get(batt_mode, "Unknown"), "start_time": start_time, "end_time": end_time, - "enabled": enabled + "enabled": enabled, } segments.append(segment) diff --git a/growattServer/open_api_v1/devices/sph.py b/growattServer/open_api_v1/devices/sph.py index 5bdf24e..515f1e7 100644 --- a/growattServer/open_api_v1/devices/sph.py +++ b/growattServer/open_api_v1/devices/sph.py @@ -1,4 +1,5 @@ """SPH/MIX device file.""" + from datetime import UTC, datetime, timedelta from growattServer.exceptions import GrowattParameterError @@ -15,43 +16,45 @@ def detail(self): """ 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. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129763571291058 + """ - # API: https://www.showdoc.com.cn/262556420217021/6129763571291058 response = self.api.session.get( self.api.get_url("device/mix/mix_data_info"), - params={ - "device_sn": self.device_sn - } + params={"device_sn": self.device_sn}, ) - return self.api.process_response(response.json(), "getting SPH inverter details") + return self.api.process_response( + response.json(), "getting SPH inverter details" + ) def energy(self): """ 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. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Mix does not exist + 10003 - Device SN error requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129764475556048 + """ - # API: https://www.showdoc.com.cn/262556420217021/6129764475556048 response = self.api.session.post( url=self.api.get_url("device/mix/mix_last_data"), data={ @@ -59,16 +62,20 @@ def energy(self): }, ) - return self.api.process_response(response.json(), "getting SPH inverter energy data") + return self.api.process_response( + response.json(), "getting SPH inverter energy data" + ) - def energy_history(self, start_date=None, end_date=None, timezone=None, page=None, limit=None): + def energy_history( + self, 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. + start_date (date, optional): Start date. Defaults to today in the local system timezone. + end_date (date, optional): End date. Defaults to today in the local system timezone. timezone (str, optional): Timezone ID. page (int, optional): Page number. limit (int, optional): Results per page. @@ -78,13 +85,21 @@ def energy_history(self, start_date=None, end_date=None, timezone=None, page=Non Raises: GrowattParameterError: If date interval is invalid (exceeds 7 days). - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Device serial number error + 10003 - Date format error + 10004 - Date interval exceeds seven days + 10005 - Mix does not exist requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129765461123058 + """ if start_date is None and end_date is None: - start_date = datetime.now(UTC).date() - end_date = datetime.now(UTC).date() + start_date = datetime.now(tz=UTC).astimezone().date() + end_date = datetime.now(tz=UTC).astimezone().date() elif start_date is None: start_date = end_date elif end_date is None: @@ -94,7 +109,6 @@ def energy_history(self, start_date=None, end_date=None, timezone=None, page=Non 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.api.session.post( url=self.api.get_url("device/mix/mix_data"), data={ @@ -104,17 +118,18 @@ def energy_history(self, start_date=None, end_date=None, timezone=None, page=Non "timezone_id": timezone, "page": page, "perpage": limit, - } + }, ) - return self.api.process_response(response.json(), "getting SPH inverter energy history") + return self.api.process_response( + response.json(), "getting SPH inverter energy history" + ) def read_parameter(self, 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, 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. @@ -124,13 +139,27 @@ def read_parameter(self, parameter_id=None, start_address=None, end_address=None Raises: GrowattParameterError: If parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - Reading failed + 10002 - Device does not exist + 10003 - Device offline + 10004 - Collector serial number is empty + 10005 - Collector offline + 10006 - Collector type does not support reading Get function + 10007 - The collector version does not support the reading function + 10008 - The collector connects to the server error, please restart and try again + 10009 - The read setting parameter type does not exist requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129766954561259 + https://www.showdoc.com.cn/262556420217021/6129828239577315 + """ if parameter_id is None and start_address is None: raise GrowattParameterError( - "specify either parameter_id or start_address/end_address") + "specify either parameter_id or start_address/end_address" + ) if parameter_id is not None and start_address is not None: raise GrowattParameterError( "specify either parameter_id or start_address/end_address - not both." @@ -143,25 +172,25 @@ def read_parameter(self, parameter_id=None, start_address=None, end_address=None # address range parameter_id = "set_any_reg" - # API: https://www.showdoc.com.cn/262556420217021/6129766954561259 response = self.api.session.post( self.api.get_url("readMixParam"), data={ "device_sn": self.device_sn, "paramId": parameter_id, "startAddr": start_address, - "endAddr": end_address - } + "endAddr": end_address, + }, ) - return self.api.process_response(response.json(), f"reading parameter {parameter_id}") + return self.api.process_response( + response.json(), f"reading parameter {parameter_id}" + ) def write_parameter(self, 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), @@ -172,9 +201,23 @@ def write_parameter(self, parameter_id, parameter_values=None): dict: JSON response from the server Raises: - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Server error of the mixed storage integrated machine + 10003 - Mixed storage integrated machine offline + 10004 - Mixed storage integrated machine serial number is empty + 10005 - Collector offline + 10006 - Setting parameter type does not exist + 10007 - Parameter value is empty + 10008 - Parameter value is out of range + 10009 - Date and time format is wrong + 10012 - Hybrid storage integrated machine does not exist + 10013 - End time cannot be less than start time requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129761750718760 + """ # Initialize all parameters as empty strings (API uses param1-param18) max_sph_params = 18 @@ -198,24 +241,21 @@ def write_parameter(self, parameter_id, parameter_values=None): parameters[pos] = str(value) # Create a data dictionary with ALL parameters explicitly included - request_data = { - "mix_sn": self.device_sn, - "type": parameter_id - } + request_data = {"mix_sn": self.device_sn, "type": parameter_id} # Add all SPH parameters to the request for i in range(1, max_sph_params + 1): request_data[f"param{i}"] = str(parameters[i]) - # API: https://www.showdoc.com.cn/262556420217021/6129761750718760 - response = self.api.session.post( - self.api.get_url("mixSet"), - data=request_data - ) + response = self.api.session.post(self.api.get_url("mixSet"), data=request_data) - return self.api.process_response(response.json(), f"writing parameter {parameter_id}") + return self.api.process_response( + response.json(), f"writing parameter {parameter_id}" + ) - def write_ac_charge_times(self, charge_power, charge_stop_soc, mains_enabled, periods): + def write_ac_charge_times( + self, charge_power, charge_stop_soc, mains_enabled, periods + ): """ Set AC charge time periods for an SPH inverter. @@ -249,18 +289,34 @@ def write_ac_charge_times(self, charge_power, charge_stop_soc, mains_enabled, pe Raises: GrowattParameterError: If parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Server error of the mixed storage integrated machine + 10003 - Mixed storage integrated machine offline + 10004 - Mixed storage integrated machine serial number is empty + 10005 - Collector offline + 10006 - Setting parameter type does not exist + 10007 - Parameter value is empty + 10008 - Parameter value is out of range + 10009 - Date and time format is wrong + 10012 - Hybrid storage integrated machine does not exist + 10013 - End time cannot be less than start time requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129761750718760 + """ - if not 0 <= charge_power <= 100: # noqa: PLR2004 + if not 0 <= charge_power <= 100: # noqa: PLR2004 raise GrowattParameterError("charge_power must be between 0 and 100") - if not 0 <= charge_stop_soc <= 100: # noqa: PLR2004 + if not 0 <= charge_stop_soc <= 100: # noqa: PLR2004 raise GrowattParameterError("charge_stop_soc must be between 0 and 100") - if len(periods) != 3: # noqa: PLR2004 - raise GrowattParameterError("periods must contain exactly 3 period definitions") + if len(periods) != 3: # noqa: PLR2004 + raise GrowattParameterError( + "periods must contain exactly 3 period definitions" + ) # Build request data request_data = { @@ -280,13 +336,11 @@ def write_ac_charge_times(self, charge_power, charge_stop_soc, mains_enabled, pe 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.api.session.post( - self.api.get_url("mixSet"), - data=request_data - ) + response = self.api.session.post(self.api.get_url("mixSet"), data=request_data) - return self.api.process_response(response.json(), "writing AC charge time periods") + return self.api.process_response( + response.json(), "writing AC charge time periods" + ) def write_ac_discharge_times(self, discharge_power, discharge_stop_soc, periods): """ @@ -320,18 +374,34 @@ def write_ac_discharge_times(self, discharge_power, discharge_stop_soc, periods) Raises: GrowattParameterError: If parameters are invalid. - GrowattV1ApiError: If the API returns an error response. + GrowattV1ApiError: If the API returns an error response. Endpoint-specific error codes: + 10001 - System error + 10002 - Server error of the mixed storage integrated machine + 10003 - Mixed storage integrated machine offline + 10004 - Mixed storage integrated machine serial number is empty + 10005 - Collector offline + 10006 - Setting parameter type does not exist + 10007 - Parameter value is empty + 10008 - Parameter value is out of range + 10009 - Date and time format is wrong + 10012 - Hybrid storage integrated machine does not exist + 10013 - End time cannot be less than start time requests.exceptions.RequestException: If there is an issue with the HTTP request. + References: + https://www.showdoc.com.cn/262556420217021/6129761750718760 + """ - if not 0 <= discharge_power <= 100: # noqa: PLR2004 + if not 0 <= discharge_power <= 100: # noqa: PLR2004 raise GrowattParameterError("discharge_power must be between 0 and 100") - if not 0 <= discharge_stop_soc <= 100: # noqa: PLR2004 + if not 0 <= discharge_stop_soc <= 100: # noqa: PLR2004 raise GrowattParameterError("discharge_stop_soc must be between 0 and 100") - if len(periods) != 3: # noqa: PLR2004 - raise GrowattParameterError("periods must contain exactly 3 period definitions") + if len(periods) != 3: # noqa: PLR2004 + raise GrowattParameterError( + "periods must contain exactly 3 period definitions" + ) # Build request data request_data = { @@ -350,13 +420,11 @@ def write_ac_discharge_times(self, discharge_power, discharge_stop_soc, periods) 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.api.session.post( - self.api.get_url("mixSet"), - data=request_data - ) + response = self.api.session.post(self.api.get_url("mixSet"), data=request_data) - return self.api.process_response(response.json(), "writing AC discharge time periods") + return self.api.process_response( + response.json(), "writing AC discharge time periods" + ) def _parse_time_periods(self, settings_data, time_type): """ @@ -421,7 +489,7 @@ def _parse_time_periods(self, settings_data, time_type): "period_id": i, "start_time": start_time, "end_time": end_time, - "enabled": enabled + "enabled": enabled, } periods.append(period) @@ -481,9 +549,17 @@ def read_ac_charge_times(self, settings_data=None): # 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 == "": + 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 == "": + 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 @@ -492,7 +568,7 @@ def read_ac_charge_times(self, settings_data=None): "charge_power": int(charge_power), "charge_stop_soc": int(charge_stop_soc), "mains_enabled": mains_enabled, - "periods": self._parse_time_periods(settings_data, "Charge") + "periods": self._parse_time_periods(settings_data, "Charge"), } def read_ac_discharge_times(self, settings_data=None): @@ -544,13 +620,21 @@ def read_ac_discharge_times(self, settings_data=None): discharge_stop_soc = settings_data.get("wdisChargeSOCLowLimit", 10) # Handle null/empty values - if discharge_power == "null" or discharge_power is None or discharge_power == "": + 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 == "": + 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") + "periods": self._parse_time_periods(settings_data, "Discharge"), }