SonnenladenGmbH / APsystems-EZ1-API

The APsystems EZ1 Python library offers a streamlined interface for interacting with the local API of APsystems EZ1 Microinverters.
MIT License
54 stars 7 forks source link

:sparkles: Added error handling when inverter is not replying with success #19

Closed mawoka-myblock closed 2 months ago

mawoka-myblock commented 3 months ago

Hey @ccmatrix coould you be so kind and test this new version as I won't be able to do that before next week?

ccMatrix commented 3 months ago

I'll try to somehow test it tomorrow. I hope to just patch the init.py in my Home Assistant docker container. I don't have a dedicated debugging environment setup yet. I'll let you know

mawoka-myblock commented 3 months ago

Yea, I mean there's no handling of the error in the HA integration, but if you get this error instead of the TypeError, it's working. Thank you already for your effort!

ccMatrix commented 2 months ago

The error is thrown as expected:

This error originated from a custom integration.

Logger: custom_components.apsystemsapi_local
Source: custom_components/apsystemsapi_local/__init__.py:103
integration: APsystems Local API
First occurred: 12:40:53 (4 occurrences)
Last logged: 15:15:53

Unexpected error fetching APSystems Data data
Traceback (most recent call last):
  File "/config/custom_components/apsystemsapi_local/__init__.py", line 103, in _async_refresh
    self.data = await self._async_update_data()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/apsystemsapi_local/__init__.py", line 77, in _async_update_data
    data = await self.api.get_output_data()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/APsystemsEZ1/__init__.py", line 168, in get_output_data
    response = await self._request("getOutputData")
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/APsystemsEZ1/__init__.py", line 79, in _request
    raise InverterReturnedError
APsystemsEZ1.InverterReturnedError

I did check some other custom components and how they handle something like this and one pattern I liked was this:

  1. The request method has a parameter retry that is set to True or not set (default value)
  2. If the request fails and retry is True the method does one recursive call to itself with the same request parameters and retry set to false.
  3. If the request fails again then retry is false and the error is raised.

This way, there is always a single retry for an error and since the error happens infrequently it would recover without crashing through. If you want I can try to refactor the request method for this pattern and test it out.

ccMatrix commented 2 months ago

I'm testing this code for _request to see how that goes:

    async def _request(self, endpoint: str, retry: bool = True) -> dict | None:
        """
        A private method to send HTTP requests to the specified endpoint of the microinverter.
        This method is used internally by other class methods to perform GET or POST requests.

        :param endpoint: The API endpoint to make the request to.

        :return: The JSON response from the microinverter as a dictionary.
        :raises: Prints an error message if the HTTP request fails for any reason.
        """
        url = f"{self.base_url}/{endpoint}"
        async with ClientSession() as ses, ses.get(url, timeout=self.timeout) as resp:
            if resp.status != 200:
                raise HttpBadRequest(f"HTTP Error: {resp.status}")
            data = await resp.json()
            if data["message"] == "SUCCESS":
                return data
            if retry:
                return await self._request(endpoint, retry=False)
            raise InverterReturnedError
mawoka-myblock commented 2 months ago

That sounds like a good idea. Will add that and thanks for taking your time helping me!