Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve discovery times by decoupling http clients from inverters #119

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 42 additions & 60 deletions solax/discovery.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import logging
import typing
from solax.http_client import all_variations

from solax.inverter import Inverter, InverterError
from solax.inverters import (
Expand Down Expand Up @@ -33,69 +34,50 @@
]


logging.basicConfig(level=logging.INFO)


class DiscoveryState:
_discovered_inverter: typing.Optional[Inverter]
_tasks: typing.Set[asyncio.Task]
_failures: list

def __init__(self):
self._discovered_inverter = None
self._tasks = set()
self._failures = []

def get_discovered_inverter(self):
return self._discovered_inverter

def _task_handler(self, task):
try:
self._tasks.remove(task)
result = task.result()
self._discovered_inverter = result
for a_task in self._tasks:
a_task.cancel()
except asyncio.CancelledError:
logging.debug("task %s canceled", task.get_name())
except InverterError as ex:
self._failures.append(ex)

@classmethod
async def _discovery_task(cls, i) -> Inverter:
logging.info("Trying inverter %s", i)
await i.get_data()
return i

async def discover(self, host, port, pwd="") -> Inverter:
for inverter in REGISTRY:
for i in inverter.build_all_variants(host, port, pwd):
task = asyncio.create_task(self._discovery_task(i), name=f"{i}")
task.add_done_callback(self._task_handler)
self._tasks.add(task)

while len(self._tasks) > 0:
logging.debug("%d discovery tasks are still running...", len(self._tasks))
await asyncio.sleep(0.5)

if self._discovered_inverter is not None:
logging.info("Discovered inverter: %s", self._discovered_inverter)
return self._discovered_inverter

msg = (
"Unable to connect to the inverter at "
f"host={host} port={port}, or your inverter is not supported yet.\n"
"Please see https://github.com/squishykid/solax/wiki/DiscoveryError\n"
f"Failures={str(self._failures)}"
)
raise DiscoveryError(msg)


class DiscoveryError(Exception):
"""Raised when unable to discover inverter"""


async def discover(host, port, pwd="") -> Inverter:
discover_state = DiscoveryState()
await discover_state.discover(host, port, pwd)
return discover_state.get_discovered_inverter()
failures: list = []
clients = all_variations(host, port, pwd)
for client_name, client in clients.items():
try:
response = await client.request()
except InverterError as ex:
failures.append(
(
client_name,
ex,
)
)
continue
for inverter_class in REGISTRY:
try:
inverter = inverter_class(client)
if inverter.identify(response):
return inverter
else:
failures.append(
(
client_name,
inverter_class.__name__,
"did not identify",
)
)
except InverterError as ex:
failures.append(
(
client_name,
inverter_class.__name__,
ex,
)
)
msg = (
"Unable to connect to the inverter at "
f"host={host} port={port}, or your inverter is not supported yet.\n"
"Please see https://github.com/squishykid/solax/wiki/DiscoveryError\n"
f"Failures={str(failures)}"
)
raise DiscoveryError(msg)
36 changes: 32 additions & 4 deletions solax/inverter_http_client.py → solax/http_client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from enum import Enum

import aiohttp
from solax.inverter_error import InverterError

from solax.utils import to_url


class Method(Enum):
GET = 1
POST = 2


class InverterHttpClient:
class HttpClient:
def __init__(self, url, method: Method = Method.POST, pwd=""):
"""Initialize the Http client."""
self.url = url
Expand Down Expand Up @@ -51,9 +54,13 @@ def with_default_query(self):
return self.with_query(query)

async def request(self):
if self.method is Method.POST:
return await self.post()
return await self.get()
try:
if self.method is Method.POST:
return await self.post()
return await self.get()
except aiohttp.ClientError as ex:
msg = "Could not connect to inverter endpoint"
raise InverterError(msg, str(self.__class__.__name__)) from ex

async def get(self):
url = self.url + "?" + self.query if self.query else self.url
Expand All @@ -75,3 +82,24 @@ async def post(self):
def __str__(self) -> str:
using = "query in url" if self.query else "data in the body"
return f"{self.url} using {using}"


def all_variations(host, port, pwd=""):
url = to_url(host, port)
get = HttpClient.build_w_url(
f"http://{host}:{port}/api/realTimeData.htm", Method.GET
)
post = HttpClient(url, Method.POST, pwd)
headers = {"X-Forwarded-For": "5.8.8.8"}
post_query = (
HttpClient(url, Method.POST, pwd).with_default_query().with_headers(headers)
)
post_data = (
HttpClient(url, Method.POST, pwd).with_default_data().with_headers(headers)
)
return {
"get": get,
"post": post,
"post_query": post_query,
"post_data": post_data,
}
Loading