From 4e616c4bdb20328b1610048199fb9de5d2f65af7 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:23:59 -0500 Subject: [PATCH 01/20] Premier essai MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ça marche pas encore tout à fait... j'arrive à avoir l'URL du devicehub mais pas du challengehub je comprends pas pourquoi. Si quelqu'un veut s'éssayer avec ça --- pyhilo/api.py | 18 +++++- pyhilo/const.py | 2 + pyhilo/websocket.py | 138 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 4 deletions(-) mode change 100644 => 100755 pyhilo/const.py diff --git a/pyhilo/api.py b/pyhilo/api.py index 761e23f..abf0f6d 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -27,6 +27,7 @@ API_NOTIFICATIONS_ENDPOINT, API_REGISTRATION_ENDPOINT, API_REGISTRATION_HEADERS, + AUTOMATION_CHALLENGE_ENDPOINT, AUTOMATION_DEVICEHUB_ENDPOINT, DEFAULT_STATE_FILE, DEFAULT_USER_AGENT, @@ -51,7 +52,7 @@ get_state, set_state, ) -from pyhilo.websocket import WebsocketClient +from pyhilo.websocket import WebsocketClient, WebsocketConfig, WebsocketManager class API: @@ -355,7 +356,20 @@ async def _async_post_init(self) -> None: await self._get_fid() await self._get_device_token() await self.refresh_ws_token() - self.websocket = WebsocketClient(self) + #self.websocket = WebsocketClient(self) + + #Initialize WebsocketManager ic-dev21 + self.websocket_manager = WebsocketManager( + self.session, + self.async_request, + self._state_yaml, + set_state + ) + await self.websocket_manager.initialize_websockets() + + #Create both websocket clients + self.websocket = WebsocketClient(self, self.websocket_manager.devicehub) + self.websocket2 = WebsocketClient(self, self.websocket_manager.challengehub) async def refresh_ws_token(self) -> None: (self.ws_url, self.ws_token) = await self.post_devicehub_negociate() diff --git a/pyhilo/const.py b/pyhilo/const.py old mode 100644 new mode 100755 index 6441f6d..a53edb1 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -42,6 +42,8 @@ # Automation server constant AUTOMATION_DEVICEHUB_ENDPOINT: Final = "/DeviceHub" +AUTOMATION_CHALLENGE_ENDPOINT: Final = "/ChallengeHub" + # Request constants DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION} HomeAssistant/{homeassistant.core.__version__} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}" diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index ee2b6fc..a44791e 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -8,8 +8,9 @@ import json from os import environ from typing import TYPE_CHECKING, Any, Callable, Dict +from urllib import parse -from aiohttp import ClientWebSocketResponse, WSMsgType +from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType from aiohttp.client_exceptions import ( ClientError, ServerDisconnectedError, @@ -17,7 +18,13 @@ ) from yarl import URL -from pyhilo.const import DEFAULT_USER_AGENT, LOG +from pyhilo.const import ( + DEFAULT_USER_AGENT, + LOG, + AUTOMATION_CHALLENGE_ENDPOINT, + AUTOMATION_DEVICEHUB_ENDPOINT, +) + from pyhilo.exceptions import ( CannotConnectError, ConnectionClosedError, @@ -376,3 +383,130 @@ async def async_invoke( "type": inv_type, } ) + + +@dataclass +class WebsocketConfig: + """Configuration for a websocket connection""" + endpoint: str + url: Optional[str] = None + token: Optional[str] = None + connection_id: Optional[str] = None + full_url: Optional[str] = None + + +class WebsocketManager: + """Manages multiple websocket connections for the Hilo API""" + + def __init__( + self, + session: ClientSession, + async_request, + state_yaml: str, + set_state_callback + ) -> None: + """Initialize the websocket manager. + + Args: + session: The aiohttp client session + async_request: The async request method from the API class + state_yaml: Path to the state file + set_state_callback: Callback to save state + """ + self.session = session + self.async_request = async_request + self._state_yaml = state_yaml + self._set_state = set_state_callback + self._shared_token = None #ic-dev21 need to share the token + + # Initialize websocket configurations + self.devicehub = WebsocketConfig(endpoint=AUTOMATION_DEVICEHUB_ENDPOINT) + self.challengehub = WebsocketConfig(endpoint=AUTOMATION_CHALLENGE_ENDPOINT) + + async def initialize_websockets(self) -> None: + """Initialize both websocket connections""" + # ic-dev21 get token from device hub + await self.refresh_token(self.devicehub, get_new_token=True) + # ic-dev21 reuse it for challenge hub + await self.refresh_token(self.challengehub, get_new_token=False) + + async def refresh_token(self, config: WebsocketConfig, get_new_token: bool = True) -> None: + """Refresh token for a specific websocket configuration. + + Args: + config: The websocket configuration to refresh + """ + if get_new_token: + config.url, self._shared_token = await self._negotiate(config) + config.token = self._shared_token + else: + # ic-dev21 reuse existing token but get new URL + config.url, _ = await self._negotiate(config) + config.token = self._shared_token + + await self._get_websocket_params(config) + + async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: + """Negotiate websocket connection and get URL and token. + + Args: + config: The websocket configuration to negotiate + + Returns: + Tuple containing the websocket URL and access token + """ + LOG.debug(f"Getting websocket url for {config.endpoint}") + url = f"{config.endpoint}/negotiate" + LOG.debug(f"Negotiate URL is {url}") + + resp = await self.async_request("post", url) + ws_url = resp.get("url") + ws_token = resp.get("accessToken") if self._shared_token is None else self._shared_token + + # Save state + state_key = "websocket" if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" else "websocket2" + await self._set_state( + self._state_yaml, + state_key, + { + "url": ws_url, + "token": ws_token, + }, + ) + + return ws_url, ws_token + + async def _get_websocket_params(self, config: WebsocketConfig) -> None: + """Get websocket parameters including connection ID. + + Args: + config: The websocket configuration to get parameters for + """ + uri = parse.urlparse(config.url) + LOG.debug(f"Getting websocket params for {config.endpoint}") + LOG.debug(f"Getting uri {uri}") + + resp = await self.async_request( + "post", + f"{uri.path}negotiate?{uri.query}", + host=uri.netloc, + headers={ + "authorization": f"Bearer {config.token}", + }, + ) + + config.connection_id = resp.get("connectionId", "") + config.full_url = f"{config.url}&id={config.connection_id}&access_token={config.token}" + LOG.debug(f"Getting full ws URL {config.full_url}") + + transport_dict = resp.get("availableTransports", []) + websocket_dict = { + "connection_id": config.connection_id, + "available_transports": transport_dict, + "full_ws_url": config.full_url, + } + + # Save state + state_key = "websocket" if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" else "websocket2" + LOG.debug(f"Calling set_state {state_key}_params") + await self._set_state(self._state_yaml, state_key, websocket_dict) \ No newline at end of file From 08e40ceff70367a13bdf22c8238c2cb5a990bd7d Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:28:35 -0500 Subject: [PATCH 02/20] Linting --- pyhilo/api.py | 11 ++++------- pyhilo/websocket.py | 44 ++++++++++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index abf0f6d..0143e53 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -356,18 +356,15 @@ async def _async_post_init(self) -> None: await self._get_fid() await self._get_device_token() await self.refresh_ws_token() - #self.websocket = WebsocketClient(self) + # self.websocket = WebsocketClient(self) - #Initialize WebsocketManager ic-dev21 + # Initialize WebsocketManager ic-dev21 self.websocket_manager = WebsocketManager( - self.session, - self.async_request, - self._state_yaml, - set_state + self.session, self.async_request, self._state_yaml, set_state ) await self.websocket_manager.initialize_websockets() - #Create both websocket clients + # Create both websocket clients self.websocket = WebsocketClient(self, self.websocket_manager.devicehub) self.websocket2 = WebsocketClient(self, self.websocket_manager.challengehub) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index a44791e..6a571b9 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -19,12 +19,11 @@ from yarl import URL from pyhilo.const import ( - DEFAULT_USER_AGENT, - LOG, AUTOMATION_CHALLENGE_ENDPOINT, AUTOMATION_DEVICEHUB_ENDPOINT, + DEFAULT_USER_AGENT, + LOG, ) - from pyhilo.exceptions import ( CannotConnectError, ConnectionClosedError, @@ -388,6 +387,7 @@ async def async_invoke( @dataclass class WebsocketConfig: """Configuration for a websocket connection""" + endpoint: str url: Optional[str] = None token: Optional[str] = None @@ -399,11 +399,7 @@ class WebsocketManager: """Manages multiple websocket connections for the Hilo API""" def __init__( - self, - session: ClientSession, - async_request, - state_yaml: str, - set_state_callback + self, session: ClientSession, async_request, state_yaml: str, set_state_callback ) -> None: """Initialize the websocket manager. @@ -417,7 +413,7 @@ def __init__( self.async_request = async_request self._state_yaml = state_yaml self._set_state = set_state_callback - self._shared_token = None #ic-dev21 need to share the token + self._shared_token = None # ic-dev21 need to share the token # Initialize websocket configurations self.devicehub = WebsocketConfig(endpoint=AUTOMATION_DEVICEHUB_ENDPOINT) @@ -430,7 +426,9 @@ async def initialize_websockets(self) -> None: # ic-dev21 reuse it for challenge hub await self.refresh_token(self.challengehub, get_new_token=False) - async def refresh_token(self, config: WebsocketConfig, get_new_token: bool = True) -> None: + async def refresh_token( + self, config: WebsocketConfig, get_new_token: bool = True + ) -> None: """Refresh token for a specific websocket configuration. Args: @@ -443,7 +441,7 @@ async def refresh_token(self, config: WebsocketConfig, get_new_token: bool = Tru # ic-dev21 reuse existing token but get new URL config.url, _ = await self._negotiate(config) config.token = self._shared_token - + await self._get_websocket_params(config) async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: @@ -461,10 +459,18 @@ async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: resp = await self.async_request("post", url) ws_url = resp.get("url") - ws_token = resp.get("accessToken") if self._shared_token is None else self._shared_token + ws_token = ( + resp.get("accessToken") + if self._shared_token is None + else self._shared_token + ) # Save state - state_key = "websocket" if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" else "websocket2" + state_key = ( + "websocket" + if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" + else "websocket2" + ) await self._set_state( self._state_yaml, state_key, @@ -496,7 +502,9 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: ) config.connection_id = resp.get("connectionId", "") - config.full_url = f"{config.url}&id={config.connection_id}&access_token={config.token}" + config.full_url = ( + f"{config.url}&id={config.connection_id}&access_token={config.token}" + ) LOG.debug(f"Getting full ws URL {config.full_url}") transport_dict = resp.get("availableTransports", []) @@ -507,6 +515,10 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: } # Save state - state_key = "websocket" if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" else "websocket2" + state_key = ( + "websocket" + if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" + else "websocket2" + ) LOG.debug(f"Calling set_state {state_key}_params") - await self._set_state(self._state_yaml, state_key, websocket_dict) \ No newline at end of file + await self._set_state(self._state_yaml, state_key, websocket_dict) From be41a7bb23a74239581e8f0d8bbad13c8b58506a Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:31:44 -0500 Subject: [PATCH 03/20] =?UTF-8?q?quelques=20progr=C3=A8s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J'arrive à la négociation du challengehub, chose qui ne se produisait pas avant, merci à @Leicas pour la suggestion. J'ai sorti le token du if pour y avoir accès dans la fonction au complet. Ça marche toujours pas, mais je pense que c'est un pas dans la bonne direction --- pyhilo/api.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 0143e53..9020b14 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -217,6 +217,8 @@ async def _async_request( :rtype: dict[str, Any] """ kwargs.setdefault("headers", self.headers) + access_token = await self.async_get_access_token() + if endpoint.startswith(API_REGISTRATION_ENDPOINT): kwargs["headers"] = {**kwargs["headers"], **API_REGISTRATION_HEADERS} if endpoint.startswith(FB_INSTALL_ENDPOINT): @@ -224,10 +226,16 @@ async def _async_request( if endpoint.startswith(ANDROID_CLIENT_ENDPOINT): kwargs["headers"] = {**kwargs["headers"], **ANDROID_CLIENT_HEADERS} if host == API_HOSTNAME: - access_token = await self.async_get_access_token() kwargs["headers"]["authorization"] = f"Bearer {access_token}" kwargs["headers"]["Host"] = host + # ic-dev21 trying Leicas suggestion + if endpoint.startswith(AUTOMATION_CHALLENGE_ENDPOINT): + # remove Ocp-Apim-Subscription-Key header to avoid 401 error + kwargs["headers"].pop("Ocp-Apim-Subscription-Key", None) + kwargs["headers"]["authorization"] = f"Bearer {access_token}" + + data: dict[str, Any] = {} url = parse.urljoin(f"https://{host}", endpoint) if self.log_traces: From 9246a21c94da41fcff2e18449448e07aa2600758 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:33:49 -0500 Subject: [PATCH 04/20] =?UTF-8?q?Linting,=20imports=20oubli=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyhilo/api.py | 1 - pyhilo/websocket.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 9020b14..26507cf 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -235,7 +235,6 @@ async def _async_request( kwargs["headers"].pop("Ocp-Apim-Subscription-Key", None) kwargs["headers"]["authorization"] = f"Bearer {access_token}" - data: dict[str, Any] = {} url = parse.urljoin(f"https://{host}", endpoint) if self.log_traces: diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 6a571b9..28ac55a 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -7,7 +7,7 @@ from enum import IntEnum import json from os import environ -from typing import TYPE_CHECKING, Any, Callable, Dict +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple from urllib import parse from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType From 97a8ca7a4ece7d62d89896f8acb08ac41d5fa8b2 Mon Sep 17 00:00:00 2001 From: Antoine Weill--Duflos Date: Sun, 24 Nov 2024 22:36:34 -0500 Subject: [PATCH 05/20] Keeping the right tokens for every websocket connection --- pyhilo/api.py | 10 ++++++---- pyhilo/websocket.py | 9 +++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 26507cf..84f7214 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -311,8 +311,9 @@ async def _async_handle_on_backoff(self, _: dict[str, Any]) -> None: LOG.info( "401 detected on websocket, refreshing websocket token. Old url: {self.ws_url} Old Token: {self.ws_token}" ) + LOG.info(f"401 detected on {err.request_info.url}") async with self._backoff_refresh_lock_ws: - (self.ws_url, self.ws_token) = await self.post_devicehub_negociate() + await self.refresh_ws_token() await self.get_websocket_params() return @@ -362,7 +363,7 @@ async def _async_post_init(self) -> None: LOG.debug("Websocket postinit") await self._get_fid() await self._get_device_token() - await self.refresh_ws_token() + # await self.refresh_ws_token() # self.websocket = WebsocketClient(self) # Initialize WebsocketManager ic-dev21 @@ -376,8 +377,9 @@ async def _async_post_init(self) -> None: self.websocket2 = WebsocketClient(self, self.websocket_manager.challengehub) async def refresh_ws_token(self) -> None: - (self.ws_url, self.ws_token) = await self.post_devicehub_negociate() - await self.get_websocket_params() + """Refresh the websocket token.""" + await self.websocket_manager.refresh_token(self.websocket_manager.devicehub) + await self.websocket_manager.refresh_token(self.websocket_manager.challengehub) async def post_devicehub_negociate(self) -> tuple[str, str]: LOG.debug("Getting websocket url") diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 28ac55a..82e49e3 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -424,7 +424,7 @@ async def initialize_websockets(self) -> None: # ic-dev21 get token from device hub await self.refresh_token(self.devicehub, get_new_token=True) # ic-dev21 reuse it for challenge hub - await self.refresh_token(self.challengehub, get_new_token=False) + await self.refresh_token(self.challengehub, get_new_token=True) async def refresh_token( self, config: WebsocketConfig, get_new_token: bool = True @@ -459,11 +459,8 @@ async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: resp = await self.async_request("post", url) ws_url = resp.get("url") - ws_token = ( - resp.get("accessToken") - if self._shared_token is None - else self._shared_token - ) + ws_token = resp.get("accessToken") + # Save state state_key = ( From 9dbeadf0629583e8d7be5e262ee7fb3519f1e86a Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:42:48 -0500 Subject: [PATCH 06/20] Too many arguments --- pyhilo/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 84f7214..1c9cc2c 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -373,8 +373,8 @@ async def _async_post_init(self) -> None: await self.websocket_manager.initialize_websockets() # Create both websocket clients - self.websocket = WebsocketClient(self, self.websocket_manager.devicehub) - self.websocket2 = WebsocketClient(self, self.websocket_manager.challengehub) + self.websocket = WebsocketClient(self.websocket_manager.devicehub) + self.websocket2 = WebsocketClient(self.websocket_manager.challengehub) async def refresh_ws_token(self) -> None: """Refresh the websocket token.""" From 561bd1b7f8b887c0ae1bc356623e695d6f166ffb Mon Sep 17 00:00:00 2001 From: Antoine Weill--Duflos Date: Mon, 25 Nov 2024 20:56:58 -0500 Subject: [PATCH 07/20] =?UTF-8?q?=C3=A7a=20fonctionne=20comme=20=C3=A7a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyhilo/websocket.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 82e49e3..e9aebb9 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -214,7 +214,7 @@ async def _async_send_json(self, payload: dict[str, Any]) -> None: if self._api.log_traces: LOG.debug( - f"[TRACE] Sending data to websocket server: {json.dumps(payload)}" + f"[TRACE] Sending data to websocket {self._api.endpoint} : {json.dumps(payload)}" ) # Hilo added a control character (chr(30)) at the end of each payload they send. # They also expect this char to be there at the end of every payload we send them. @@ -269,7 +269,7 @@ async def async_connect(self) -> None: LOG.info("Websocket: Connecting to server") if self._api.log_traces: - LOG.debug(f"[TRACE] Websocket URL: {self._api.full_ws_url}") + LOG.debug(f"[TRACE] Websocket URL: {self._api.full_url}") headers = { "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", "Pragma": "no-cache", @@ -287,7 +287,7 @@ async def async_connect(self) -> None: try: self._client = await self._api.session.ws_connect( URL( - self._api.full_ws_url, + self._api.full_url, encoded=True, ), heartbeat=55, @@ -393,6 +393,8 @@ class WebsocketConfig: token: Optional[str] = None connection_id: Optional[str] = None full_url: Optional[str] = None + log_traces: bool = True + session: ClientSession | None = None class WebsocketManager: @@ -416,8 +418,8 @@ def __init__( self._shared_token = None # ic-dev21 need to share the token # Initialize websocket configurations - self.devicehub = WebsocketConfig(endpoint=AUTOMATION_DEVICEHUB_ENDPOINT) - self.challengehub = WebsocketConfig(endpoint=AUTOMATION_CHALLENGE_ENDPOINT) + self.devicehub = WebsocketConfig(endpoint=AUTOMATION_DEVICEHUB_ENDPOINT, session=session) + self.challengehub = WebsocketConfig(endpoint=AUTOMATION_CHALLENGE_ENDPOINT, session=session) async def initialize_websockets(self) -> None: """Initialize both websocket connections""" @@ -508,7 +510,7 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: websocket_dict = { "connection_id": config.connection_id, "available_transports": transport_dict, - "full_ws_url": config.full_url, + "full_url": config.full_url, } # Save state From dfc49f1a8f3aaa62275080c09ac795f97693f3a8 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:31:02 -0500 Subject: [PATCH 08/20] Some Linting Respect Black, remove unused imports --- pyhilo/api.py | 2 +- pyhilo/websocket.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 1c9cc2c..7683b44 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -52,7 +52,7 @@ get_state, set_state, ) -from pyhilo.websocket import WebsocketClient, WebsocketConfig, WebsocketManager +from pyhilo.websocket import WebsocketClient, WebsocketManager class API: diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index e9aebb9..0b2db73 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -418,8 +418,12 @@ def __init__( self._shared_token = None # ic-dev21 need to share the token # Initialize websocket configurations - self.devicehub = WebsocketConfig(endpoint=AUTOMATION_DEVICEHUB_ENDPOINT, session=session) - self.challengehub = WebsocketConfig(endpoint=AUTOMATION_CHALLENGE_ENDPOINT, session=session) + self.devicehub = WebsocketConfig( + endpoint=AUTOMATION_DEVICEHUB_ENDPOINT, session=session + ) + self.challengehub = WebsocketConfig( + endpoint=AUTOMATION_CHALLENGE_ENDPOINT, session=session + ) async def initialize_websockets(self) -> None: """Initialize both websocket connections""" @@ -461,8 +465,7 @@ async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: resp = await self.async_request("post", url) ws_url = resp.get("url") - ws_token = resp.get("accessToken") - + ws_token = resp.get("accessToken") # Save state state_key = ( From 465e2ffbbdfafdf57838cb817753f1506da0068f Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:54:14 -0500 Subject: [PATCH 09/20] Some more linting --- pyhilo/websocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 0b2db73..7cf0467 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -269,7 +269,7 @@ async def async_connect(self) -> None: LOG.info("Websocket: Connecting to server") if self._api.log_traces: - LOG.debug(f"[TRACE] Websocket URL: {self._api.full_url}") + LOG.debug(f"[TRACE] Websocket URL: {self._api.full_ws_url}") headers = { "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", "Pragma": "no-cache", @@ -287,7 +287,7 @@ async def async_connect(self) -> None: try: self._client = await self._api.session.ws_connect( URL( - self._api.full_url, + self._api.full_ws_url, encoded=True, ), heartbeat=55, From 8bfd6c016153fd404c6829685a02fc16e368eb14 Mon Sep 17 00:00:00 2001 From: Antoine Weill--Duflos Date: Mon, 25 Nov 2024 22:29:55 -0500 Subject: [PATCH 10/20] better logs to see which ws is doing what. --- pyhilo/websocket.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index e9aebb9..58c7792 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -223,7 +223,7 @@ async def _async_send_json(self, payload: dict[str, Any]) -> None: def _parse_message(self, msg: dict[str, Any]) -> None: """Parse an incoming message.""" if self._api.log_traces: - LOG.debug(f"[TRACE] Received message from websocket: {msg}") + LOG.debug(f"[TRACE] Received message on websocket {self._api.endpoint}: {msg}") if msg.get("type") == SignalRMsgType.PING: schedule_callback(self._async_pong) return @@ -267,7 +267,7 @@ async def async_connect(self) -> None: LOG.debug("Websocket: async_connect() called but already connected") return - LOG.info("Websocket: Connecting to server") + LOG.info("Websocket: Connecting to server %s", self._api.endpoint) if self._api.log_traces: LOG.debug(f"[TRACE] Websocket URL: {self._api.full_url}") headers = { @@ -302,7 +302,7 @@ async def async_connect(self) -> None: LOG.error(f"Unable to connect to WS server {err}") raise CannotConnectError(err) from err - LOG.info("Connected to websocket server") + LOG.info(f"Connected to websocket server {self._api.endpoint}") self._watchdog.trigger() for callback in self._connect_callbacks: schedule_callback(callback) From fb35d8711e19e00bdfa00ac1ad42929154048b7b Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:43:12 -0500 Subject: [PATCH 11/20] Revert "Some more linting" This reverts commit 465e2ffbbdfafdf57838cb817753f1506da0068f. --- pyhilo/websocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 12dcae5..ea0aee0 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -269,7 +269,7 @@ async def async_connect(self) -> None: LOG.info("Websocket: Connecting to server %s", self._api.endpoint) if self._api.log_traces: - LOG.debug(f"[TRACE] Websocket URL: {self._api.full_ws_url}") + LOG.debug(f"[TRACE] Websocket URL: {self._api.full_url}") headers = { "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", "Pragma": "no-cache", @@ -287,7 +287,7 @@ async def async_connect(self) -> None: try: self._client = await self._api.session.ws_connect( URL( - self._api.full_ws_url, + self._api.full_url, encoded=True, ), heartbeat=55, From 70cc6bb9012a3307344b1ee8c0033af869debb6d Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:55:39 -0500 Subject: [PATCH 12/20] More linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revenu à full_ws_url pour garder le même nom que le code original --- pyhilo/websocket.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index ea0aee0..418ad59 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -223,7 +223,9 @@ async def _async_send_json(self, payload: dict[str, Any]) -> None: def _parse_message(self, msg: dict[str, Any]) -> None: """Parse an incoming message.""" if self._api.log_traces: - LOG.debug(f"[TRACE] Received message on websocket {self._api.endpoint}: {msg}") + LOG.debug( + f"[TRACE] Received message on websocket {self._api.endpoint}: {msg}" + ) if msg.get("type") == SignalRMsgType.PING: schedule_callback(self._async_pong) return @@ -269,7 +271,7 @@ async def async_connect(self) -> None: LOG.info("Websocket: Connecting to server %s", self._api.endpoint) if self._api.log_traces: - LOG.debug(f"[TRACE] Websocket URL: {self._api.full_url}") + LOG.debug(f"[TRACE] Websocket URL: {self._api.full_ws_url}") headers = { "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", "Pragma": "no-cache", @@ -287,7 +289,7 @@ async def async_connect(self) -> None: try: self._client = await self._api.session.ws_connect( URL( - self._api.full_url, + self._api.full_ws_url, encoded=True, ), heartbeat=55, @@ -392,7 +394,7 @@ class WebsocketConfig: url: Optional[str] = None token: Optional[str] = None connection_id: Optional[str] = None - full_url: Optional[str] = None + full_ws_url: Optional[str] = None log_traces: bool = True session: ClientSession | None = None @@ -504,16 +506,16 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: ) config.connection_id = resp.get("connectionId", "") - config.full_url = ( + config.full_ws_url = ( f"{config.url}&id={config.connection_id}&access_token={config.token}" ) - LOG.debug(f"Getting full ws URL {config.full_url}") + LOG.debug(f"Getting full ws URL {config.full_ws_url}") transport_dict = resp.get("availableTransports", []) websocket_dict = { "connection_id": config.connection_id, "available_transports": transport_dict, - "full_url": config.full_url, + "full_url": config.full_ws_url, } # Save state From 5659b41deac6205f991cd342a854cb0f45b31620 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:10:10 -0500 Subject: [PATCH 13/20] Fix hilo_state.yaml bug Il y avait un string literal donc websocket ne se faisait pas remplir, websocket2 oui. Retrait et modification de quelques commentaires --- pyhilo/api.py | 6 ++++-- pyhilo/websocket.py | 11 +++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 7683b44..267f8a6 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -363,8 +363,6 @@ async def _async_post_init(self) -> None: LOG.debug("Websocket postinit") await self._get_fid() await self._get_device_token() - # await self.refresh_ws_token() - # self.websocket = WebsocketClient(self) # Initialize WebsocketManager ic-dev21 self.websocket_manager = WebsocketManager( @@ -373,6 +371,8 @@ async def _async_post_init(self) -> None: await self.websocket_manager.initialize_websockets() # Create both websocket clients + # ic-dev21 need to work on this as it can't lint as is, may need to + # instanciate differently self.websocket = WebsocketClient(self.websocket_manager.devicehub) self.websocket2 = WebsocketClient(self.websocket_manager.challengehub) @@ -381,6 +381,8 @@ async def refresh_ws_token(self) -> None: await self.websocket_manager.refresh_token(self.websocket_manager.devicehub) await self.websocket_manager.refresh_token(self.websocket_manager.challengehub) + + # ic-dev21 not sure this is still needed? See websocket.py _async_negotiate async def post_devicehub_negociate(self) -> tuple[str, str]: LOG.debug("Getting websocket url") url = f"{AUTOMATION_DEVICEHUB_ENDPOINT}/negotiate" diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 418ad59..b7b7180 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -417,9 +417,9 @@ def __init__( self.async_request = async_request self._state_yaml = state_yaml self._set_state = set_state_callback - self._shared_token = None # ic-dev21 need to share the token + self._shared_token = None - # Initialize websocket configurations + # Initialize websocket configurations, more can be added here self.devicehub = WebsocketConfig( endpoint=AUTOMATION_DEVICEHUB_ENDPOINT, session=session ) @@ -431,7 +431,7 @@ async def initialize_websockets(self) -> None: """Initialize both websocket connections""" # ic-dev21 get token from device hub await self.refresh_token(self.devicehub, get_new_token=True) - # ic-dev21 reuse it for challenge hub + # ic-dev21 get token from challenge hub await self.refresh_token(self.challengehub, get_new_token=True) async def refresh_token( @@ -446,7 +446,6 @@ async def refresh_token( config.url, self._shared_token = await self._negotiate(config) config.token = self._shared_token else: - # ic-dev21 reuse existing token but get new URL config.url, _ = await self._negotiate(config) config.token = self._shared_token @@ -472,7 +471,7 @@ async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: # Save state state_key = ( "websocket" - if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" + if config.endpoint == AUTOMATION_DEVICEHUB_ENDPOINT else "websocket2" ) await self._set_state( @@ -521,7 +520,7 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: # Save state state_key = ( "websocket" - if config.endpoint == "AUTOMATION_DEVICEHUB_ENDPOINT" + if config.endpoint == AUTOMATION_DEVICEHUB_ENDPOINT else "websocket2" ) LOG.debug(f"Calling set_state {state_key}_params") From 03380186c66c692a69ac9397e4b3e6314975826f Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:38:17 -0500 Subject: [PATCH 14/20] Remove uneeded function Le post se fait maintenant dans websocket.py, dead code --- pyhilo/api.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 267f8a6..33a4d4d 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -372,7 +372,7 @@ async def _async_post_init(self) -> None: # Create both websocket clients # ic-dev21 need to work on this as it can't lint as is, may need to - # instanciate differently + # instantiate differently self.websocket = WebsocketClient(self.websocket_manager.devicehub) self.websocket2 = WebsocketClient(self.websocket_manager.challengehub) @@ -381,26 +381,6 @@ async def refresh_ws_token(self) -> None: await self.websocket_manager.refresh_token(self.websocket_manager.devicehub) await self.websocket_manager.refresh_token(self.websocket_manager.challengehub) - - # ic-dev21 not sure this is still needed? See websocket.py _async_negotiate - async def post_devicehub_negociate(self) -> tuple[str, str]: - LOG.debug("Getting websocket url") - url = f"{AUTOMATION_DEVICEHUB_ENDPOINT}/negotiate" - LOG.debug(f"devicehub URL is {url}") - resp = await self.async_request("post", url) - ws_url = resp.get("url") - ws_token = resp.get("accessToken") - LOG.debug("Calling set_state devicehub_negotiate") - await set_state( - self._state_yaml, - "websocket", - { - "url": ws_url, - "token": ws_token, - }, - ) - return (ws_url, ws_token) - async def get_websocket_params(self) -> None: uri = parse.urlparse(self.ws_url) LOG.debug("Getting websocket params") From 15923f5982d173b23e9d976f1c14d04bbd12146c Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:27:05 -0500 Subject: [PATCH 15/20] Remove unused import --- pyhilo/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 33a4d4d..0598926 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -28,7 +28,6 @@ API_REGISTRATION_ENDPOINT, API_REGISTRATION_HEADERS, AUTOMATION_CHALLENGE_ENDPOINT, - AUTOMATION_DEVICEHUB_ENDPOINT, DEFAULT_STATE_FILE, DEFAULT_USER_AGENT, FB_APP_ID, From 4e6ab3d04222ea411f33c8a381b73a480da08873 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:37:23 -0500 Subject: [PATCH 16/20] Linting Adding typing to WebsocketManager class. type : ignore sur la ligne 504 je comprends pas pourquoi le linter chiale. --- pyhilo/websocket.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index b7b7180..e4ca0d1 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -403,7 +403,11 @@ class WebsocketManager: """Manages multiple websocket connections for the Hilo API""" def __init__( - self, session: ClientSession, async_request, state_yaml: str, set_state_callback + self, + session: ClientSession, + async_request: Callable[..., Any], + state_yaml: str, + set_state_callback: Callable[..., Any], ) -> None: """Initialize the websocket manager. @@ -497,7 +501,7 @@ async def _get_websocket_params(self, config: WebsocketConfig) -> None: resp = await self.async_request( "post", - f"{uri.path}negotiate?{uri.query}", + f"{uri.path}negotiate?{uri.query}", # type: ignore host=uri.netloc, headers={ "authorization": f"Bearer {config.token}", From cc86936da859236f1e5f118efd6d7510df142975 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:54:53 -0500 Subject: [PATCH 17/20] Linting Initialize values. --- pyhilo/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyhilo/api.py b/pyhilo/api.py index 0598926..6cf75a3 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -84,6 +84,9 @@ def __init__( self.websocket: WebsocketClient self.log_traces = log_traces self._get_device_callbacks: list[Callable[..., Any]] = [] + self.ws_url: str = "" + self.ws_token: str = "" + self.endpoint: str = "" @classmethod async def async_create( From 83c3ad60ad920827f5038c40ea4781699328f43b Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:30:39 -0500 Subject: [PATCH 18/20] Some more linting Pour faire changement --- pyhilo/websocket.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index e4ca0d1..784b898 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -255,7 +255,7 @@ def add_disconnect_callback( return self._add_callback(self._disconnect_callbacks, callback) def add_event_callback(self, callback: Callable[..., Any]) -> Callable[..., None]: - """Add a callback callback to be called upon receiving an event. + """Add a callback to be called upon receiving an event. Note that callbacks should expect to receive a WebsocketEvent object as a parameter. :param callback: The method to call after receiving an event. @@ -421,8 +421,7 @@ def __init__( self.async_request = async_request self._state_yaml = state_yaml self._set_state = set_state_callback - self._shared_token = None - + self._shared_token: Optional[str] = None # Initialize websocket configurations, more can be added here self.devicehub = WebsocketConfig( endpoint=AUTOMATION_DEVICEHUB_ENDPOINT, session=session @@ -442,7 +441,6 @@ async def refresh_token( self, config: WebsocketConfig, get_new_token: bool = True ) -> None: """Refresh token for a specific websocket configuration. - Args: config: The websocket configuration to refresh """ @@ -457,10 +455,8 @@ async def refresh_token( async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]: """Negotiate websocket connection and get URL and token. - Args: config: The websocket configuration to negotiate - Returns: Tuple containing the websocket URL and access token """ From 414b256ebc7f479427d9b9d05a98af244152b01d Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:21:27 -0500 Subject: [PATCH 19/20] Update api.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renommé pour être plus facile à suivre. --- pyhilo/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index 6cf75a3..348109c 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -81,7 +81,7 @@ def __init__( self.device_attributes = get_device_attributes() self.session: ClientSession = session self._oauth_session = oauth_session - self.websocket: WebsocketClient + self.websocket_devices: WebsocketClient self.log_traces = log_traces self._get_device_callbacks: list[Callable[..., Any]] = [] self.ws_url: str = "" @@ -375,8 +375,8 @@ async def _async_post_init(self) -> None: # Create both websocket clients # ic-dev21 need to work on this as it can't lint as is, may need to # instantiate differently - self.websocket = WebsocketClient(self.websocket_manager.devicehub) - self.websocket2 = WebsocketClient(self.websocket_manager.challengehub) + self.websocket_devices = WebsocketClient(self.websocket_manager.devicehub) + self.websocket_challenges = WebsocketClient(self.websocket_manager.challengehub) async def refresh_ws_token(self) -> None: """Refresh the websocket token.""" From 0c9fca95f7fa7cfff5ee966d57699230fc36f89b Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:46:14 -0500 Subject: [PATCH 20/20] Update api.py Clarifier un peu --- pyhilo/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyhilo/api.py b/pyhilo/api.py index 348109c..6708ba0 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -82,6 +82,7 @@ def __init__( self.session: ClientSession = session self._oauth_session = oauth_session self.websocket_devices: WebsocketClient + self.websocket_challenges: WebsocketClient self.log_traces = log_traces self._get_device_callbacks: list[Callable[..., Any]] = [] self.ws_url: str = ""