From a36b6280f0e8e174174012aee64473b2bce89ae3 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 22:06:00 -0800 Subject: [PATCH 1/8] Support for a client factory --- franklinwh/client.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index 83e5539..ef950b7 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -291,8 +291,18 @@ class DeviceTimeoutException(Exception): class GatewayOfflineException(Exception): """raised when the gateway is offline.""" +class HttpClientFactory: + # If you store a function in an attribute, it becomes a bound method + factory = (lambda: httpx.AsyncClient(http2=True),) -class TokenFetcher: + @classmethod + def set_client_factory(cls, factory): + cls.factory = (factory,) + + def get_client(self): + return self.client_factory[0]() + +class TokenFetcher(HttpClientFactory): """Fetches and refreshes authentication tokens for FranklinWH API.""" def __init__(self, username: str, password: str) -> None: @@ -301,6 +311,9 @@ def __init__(self, username: str, password: str) -> None: self.password = password self.info: dict | None = None + def get_client(self): + return self.get_client() + async def get_token(self): """Fetch a new authentication token using the stored credentials. @@ -349,7 +362,7 @@ async def retry(func, filter, refresh_func): return await func() -class Client: +class Client(HttpClientFactory): """Client for interacting with FranklinWH gateway API.""" def __init__( @@ -361,7 +374,7 @@ def __init__( self.url_base = url_base self.token = "" self.snno = 0 - self.session = httpx.AsyncClient(http2=True) + self.session = self.get_client() # to enable detailed logging add this to configuration.yaml: # logger: From be024a4876f550e638e0d97926f65fb3dedea389 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 22:39:17 -0800 Subject: [PATCH 2/8] Export the client factory --- franklinwh/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/franklinwh/__init__.py b/franklinwh/__init__.py index 1e4ea85..a5d0edd 100644 --- a/franklinwh/__init__.py +++ b/franklinwh/__init__.py @@ -2,7 +2,7 @@ from .api import DEFAULT_URL_BASE from .caching_thread import CachingThread -from .client import AccessoryType, Client, GridStatus, Mode, Stats, TokenFetcher +from .client import AccessoryType, Client, GridStatus, Mode, Stats, TokenFetcher, HttpClientFactory __all__ = [ "DEFAULT_URL_BASE", @@ -10,6 +10,7 @@ "CachingThread", "Client", "GridStatus", + "HttpClientfactory" "Mode", "Stats", "TokenFetcher", From 5b505ab4e325973110aaebc64d971f26bf4a69e8 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 22:42:04 -0800 Subject: [PATCH 3/8] Helps if you use the same method name everywhere --- franklinwh/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index ef950b7..595d02c 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -300,7 +300,7 @@ def set_client_factory(cls, factory): cls.factory = (factory,) def get_client(self): - return self.client_factory[0]() + return self.factory[0]() class TokenFetcher(HttpClientFactory): """Fetches and refreshes authentication tokens for FranklinWH API.""" From cf630bd981ddff4e3e6edee51f7cb17f69fa95a8 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 22:46:33 -0800 Subject: [PATCH 4/8] Start shotgun debugging --- franklinwh/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/franklinwh/client.py b/franklinwh/client.py index 595d02c..c12f470 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -382,6 +382,7 @@ def __init__( # franklinwh: debug logger = logging.getLogger("franklinwh") + logger.warning("Session class: %s" % type(self.session)) if logger.isEnabledFor(logging.DEBUG): async def debug_request(request: httpx.Request): From f942ef91409ed613643ee1679448b662a7d8a030 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 22:49:56 -0800 Subject: [PATCH 5/8] Helps if you don't override the client --- franklinwh/client.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index c12f470..7687700 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -412,13 +412,6 @@ async def debug_response(response: httpx.Response): return response self.logger = logger - self.session = httpx.AsyncClient( - http2=True, - event_hooks={ - "request": [debug_request], - "response": [debug_response], - }, - ) # TODO(richo) Setup timeouts and deal with them gracefully. async def _post(self, url, payload, params: dict | None = None): From 00cd545842e445bc5260e68b1c4777156d1ce6ae Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Tue, 30 Dec 2025 23:07:16 -0800 Subject: [PATCH 6/8] More buckshot --- franklinwh/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index 7687700..46c1eae 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -383,6 +383,7 @@ def __init__( logger = logging.getLogger("franklinwh") logger.warning("Session class: %s" % type(self.session)) + self.logger = logger if logger.isEnabledFor(logging.DEBUG): async def debug_request(request: httpx.Request): @@ -411,7 +412,6 @@ async def debug_response(response: httpx.Response): ) return response - self.logger = logger # TODO(richo) Setup timeouts and deal with them gracefully. async def _post(self, url, payload, params: dict | None = None): @@ -579,6 +579,7 @@ async def get_stats(self) -> Stats: This includes instantaneous measurements for current power, as well as totals for today (in local time) """ + self.logger.warning("get_stats: Session class: %s" % type(self.session)) data = await self._status() grid_status: GridStatus = GridStatus.NORMAL if "offgridreason" in data: From 57b60eb8000af65cd5edc5762521ea30c50475ca Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Wed, 31 Dec 2025 12:06:35 -0800 Subject: [PATCH 7/8] Use our factory --- franklinwh/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index 46c1eae..e4992e6 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -311,9 +311,6 @@ def __init__(self, username: str, password: str) -> None: self.password = password self.info: dict | None = None - def get_client(self): - return self.get_client() - async def get_token(self): """Fetch a new authentication token using the stored credentials. @@ -339,7 +336,7 @@ async def _login(username: str, password: str) -> dict: "lang": "en_US", "type": 1, } - async with httpx.AsyncClient(http2=True) as client: + async with TokenFetcher.get_client() as client: res = await client.post(url, data=form, timeout=10) res.raise_for_status() js = res.json() From a3be7847cac5ab6b335077774bcc157de905c521 Mon Sep 17 00:00:00 2001 From: Richo Healey Date: Wed, 31 Dec 2025 12:16:51 -0800 Subject: [PATCH 8/8] Always just create a class so we can access the factory --- franklinwh/client.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/franklinwh/client.py b/franklinwh/client.py index e4992e6..c8967c6 100644 --- a/franklinwh/client.py +++ b/franklinwh/client.py @@ -316,27 +316,30 @@ async def get_token(self): Store the intermediate account information in self.info. """ - self.info = await TokenFetcher._login(self.username, self.password) + self.info = await self.fetch_token() return self.info["token"] @staticmethod async def login(username: str, password: str): """Log in to the FranklinWH API and retrieve an authentication token.""" - return (await TokenFetcher._login(username, password))["token"] + await TokenFetcher(username, password).get_token() @staticmethod async def _login(username: str, password: str) -> dict: + await TokenFetcher(username, password).get_token() + + async def fetch_token(self): """Log in to the FranklinWH API and retrieve account information.""" url = ( DEFAULT_URL_BASE + "hes-gateway/terminal/initialize/appUserOrInstallerLogin" ) form = { - "account": username, - "password": hashlib.md5(bytes(password, "ascii")).hexdigest(), + "account": self.username, + "password": hashlib.md5(bytes(self.password, "ascii")).hexdigest(), "lang": "en_US", "type": 1, } - async with TokenFetcher.get_client() as client: + async with self.get_client() as client: res = await client.post(url, data=form, timeout=10) res.raise_for_status() js = res.json()