Repository navigation
Description
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:
- async-dependency — The library must use an async HTTP client (
aiohttp) instead of synchronousrequests. - inject-websession — The library must accept an externally-provided
aiohttp.ClientSessionso 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 headers4. 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.ClientResponseErrorrequests.exceptions.ConnectionError->aiohttp.ClientConnectionErrorrequests.exceptions.Timeout->asyncio.TimeoutErrorrequests.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:
- Major version bump (3.0.0) with async-only API
- 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
indykoning commentedon Jun 1, 2026
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?
johanzander commentedon Jun 1, 2026
After researching the HA requirements and evaluating migration approaches in depth, we've opened #149 with a revised design that uses
httpxinstead ofaiohttp. The key differences:GrowattApikeeps the same constructor, methods, and return typeshttpxhandles both sync (httpx.Client) and async (httpx.AsyncClient), no need forrequests+aiohttphttpxequally viaget_async_client(hass); the quality scale rules don't mandateaiohttp#149 has the full design and rationale. This issue could be closed in favor of that one.
johanzander commentedon Jun 3, 2026
Closing this issue, continuing discussions in #149 instead.