Skip to content

Add async support (aiohttp) and accept external session for Home Assistant integration #148

Closed

Description

@johanzander

Context

The Home Assistant growatt_server integration uses this library and is working toward Platinum quality scale. Two of the remaining requirements are blocked on upstream library changes:

  1. async-dependency — The library must use an async HTTP client (aiohttp) instead of synchronous requests.
  2. inject-websession — The library must accept an externally-provided aiohttp.ClientSession so Home Assistant can pass its managed session.

Currently, every API call in the HA integration is wrapped in hass.async_add_executor_job(...) (~15 calls) because the library blocks the event loop.

What needs to change

1. Replace requests with aiohttp

Every method that makes HTTP calls needs to become async. The library currently uses a single requests.Session per instance (good pattern), which maps cleanly to aiohttp.ClientSession.

Before (base_api.py):

import requests

class GrowattApi:
    def __init__(self, add_random_user_id=False, agent_identifier=None):
        self.session = requests.Session()
        self.session.hooks = {"response": _raise_for_status}
        self.session.headers.update({"User-Agent": self.agent_identifier})

    def login(self, username, password, is_password_hashed=False):
        if not is_password_hashed:
            password = hash_password(password)
        response = self.session.post(self.get_url("newTwoLoginAPI.do"), data={...})
        data = response.json()["back"]
        return data

    def plant_list(self, user_id):
        response = self.session.get(self.get_url("PlantListAPI.do"), params={...})
        return response.json().get("back", [])

After:

import aiohttp

class GrowattApi:
    def __init__(self, session: aiohttp.ClientSession | None = None,
                 add_random_user_id=False, agent_identifier=None):
        self._external_session = session is not None
        self.session = session or aiohttp.ClientSession()
        # Headers are passed per-request or via session default headers

    async def close(self):
        """Close the session if we created it."""
        if not self._external_session:
            await self.session.close()

    async def login(self, username, password, is_password_hashed=False):
        if not is_password_hashed:
            password = hash_password(password)
        async with self.session.post(self.get_url("newTwoLoginAPI.do"), data={...}) as resp:
            resp.raise_for_status()
            data = (await resp.json())["back"]
        return data

    async def plant_list(self, user_id):
        async with self.session.get(self.get_url("PlantListAPI.do"), params={...}) as resp:
            resp.raise_for_status()
            return (await resp.json()).get("back", [])

2. Accept external aiohttp.ClientSession (for HA inject-websession)

The constructor should accept an optional session parameter. When provided, the library uses it as-is and does NOT close it. When not provided, the library creates its own session and manages its lifecycle.

This lets Home Assistant pass async_get_clientsession(hass) so sessions are shared across integrations.

3. OpenApiV1 changes

Same pattern — accept session, pass to super, add token header:

class OpenApiV1(GrowattApi):
    def __init__(self, token, session: aiohttp.ClientSession | None = None):
        super().__init__(session=session, agent_identifier=self._create_user_agent())
        # Token passed per-request or via default headers

4. Device classes (Min, Sph)

These already use self.api.session for requests — they just need async/await added to their methods. No structural changes needed.

5. Error handling

Replace requests.exceptions.* with aiohttp.ClientError:

  • requests.exceptions.HTTPError -> aiohttp.ClientResponseError
  • requests.exceptions.ConnectionError -> aiohttp.ClientConnectionError
  • requests.exceptions.Timeout -> asyncio.TimeoutError
  • requests.exceptions.RequestException -> aiohttp.ClientError (base)

Update the docstrings in exceptions.py accordingly.

6. Dependencies

In setup.py:

install_requires=[
    "aiohttp",   # replaces "requests"
],

Scope

Files that need changes:

File Lines Changes
base_api.py 1405 All ~40 methods -> async, requests -> aiohttp
open_api_v1/__init__.py 864 All ~25 methods -> async
open_api_v1/devices/min.py 461 All ~8 methods -> async
open_api_v1/devices/sph.py 641 All ~10 methods -> async
open_api_v1/devices/abstract_device.py 52 No HTTP changes needed
exceptions.py 67 Update docstring references
setup.py ~20 Replace requests -> aiohttp

Breaking change

This is a breaking change (sync -> async). Options:

  1. Major version bump (3.0.0) with async-only API
  2. Dual support: keep sync methods and add async variants (more complex, not recommended)

I'd recommend option 1 — the sync -> async migration is straightforward for callers, and the main consumer (Home Assistant) needs async.

I am happy to help implement this

I can submit a PR with the full implementation if you are open to this direction. Just wanted to align on the approach first.

Activity

  1. indykoning commented on Jun 1, 2026

    @indykoning
    Owner

    That would indeed be quite a major breaking change, and i agree I/O should be done asynchronously.
    Passing the sessions along looks good, though we should probably add it as the last argument in case people currently pass the arguments in order instead of by name.

    What would the migration path look like for people currently using the package for their synchronous code?

  2. johanzander commented on Jun 1, 2026

    @johanzander
    CollaboratorAuthor

    After researching the HA requirements and evaluating migration approaches in depth, we've opened #149 with a revised design that uses httpx instead of aiohttp. The key differences:

    • Non-breaking for existing sync users — GrowattApi keeps the same constructor, methods, and return types
    • Single dependency — httpx handles both sync (httpx.Client) and async (httpx.AsyncClient), no need for requests + aiohttp
    • HA-compatible — HA supports httpx equally via get_async_client(hass); the quality scale rules don't mandate aiohttp
    • Proven pattern — same approach as OpenAI, Anthropic, and other modern Python SDKs

    #149 has the full design and rationale. This issue could be closed in favor of that one.

  3. johanzander commented on Jun 3, 2026

    @johanzander
    CollaboratorAuthor

    Closing this issue, continuing discussions in #149 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions