4545)
4646from pyhilo .device import DeviceAttribute , HiloDevice , get_device_attributes
4747from pyhilo .exceptions import InvalidCredentialsError , RequestError
48- from pyhilo .util .state import (
49- StateDict ,
50- WebsocketDict ,
51- WebsocketTransportsDict ,
52- get_state ,
53- set_state ,
54- )
55- from pyhilo .websocket import WebsocketClient , WebsocketManager
48+ from pyhilo .signalr import SignalRHub , SignalRManager
49+ from pyhilo .util .state import AndroidDeviceDict , StateDict , get_state , set_state
5650
5751
5852class API :
@@ -74,27 +68,19 @@ def __init__(
7468 ) -> None :
7569 """Initialize"""
7670 self ._backoff_refresh_lock_api = asyncio .Lock ()
77- self ._backoff_refresh_lock_ws = asyncio .Lock ()
7871 self ._request_retries = request_retries
7972 self ._state_yaml : str = DEFAULT_STATE_FILE
8073 self .state : StateDict = {}
8174 self .async_request = self ._wrap_request_method (self ._request_retries )
8275 self .device_attributes = get_device_attributes ()
8376 self .session : ClientSession = session
8477 self ._oauth_session = oauth_session
85- self .websocket_devices : WebsocketClient
86- # Backward compatibility during transition to websocket for challenges. Currently the HA Hilo integration
87- # uses the .websocket attribute. Re-added this attribute and point to the same object as websocket_devices.
88- # Should be removed once the transition to the challenge websocket is completed everywhere.
89- self .websocket : WebsocketClient
90- self .websocket_challenges : WebsocketClient
78+ self .signalr_devices : SignalRHub
79+ self .signalr_challenges : SignalRHub
9180 self .log_traces = log_traces
9281 self ._get_device_callbacks : list [Callable [..., Any ]] = []
93- self .ws_url : str = ""
94- self .ws_token : str = ""
95- self .endpoint : str = ""
9682 self ._urn : str | None = None
97- # Device cache from websocket DeviceListInitialValuesReceived
83+ # Device cache from SignalR DeviceListInitialValuesReceived
9884 self ._device_cache : list [dict [str , Any ]] = []
9985 self ._device_cache_event : asyncio .Event = asyncio .Event ()
10086
@@ -146,7 +132,7 @@ async def async_get_access_token(self) -> str:
146132 await self ._oauth_session .async_ensure_token_valid ()
147133
148134 access_token = str (self ._oauth_session .token ["access_token" ])
149- LOG .debug ("Websocket access token is %s" , access_token )
135+ LOG .debug ("SignalR access token is %s" , access_token )
150136
151137 urn = self .urn
152138 LOG .debug ("Extracted URN: %s" , urn )
@@ -356,19 +342,7 @@ async def _async_handle_on_backoff(self, _: dict[str, Any]) -> None:
356342 err : ClientResponseError = err_info [1 ].with_traceback (err_info [2 ]) # type: ignore
357343
358344 if err .status in (401 , 403 ):
359- LOG .warning ("Refreshing websocket token %s" , err .request_info .url )
360- if (
361- "client/negotiate" in str (err .request_info .url )
362- and err .request_info .method == "POST"
363- ):
364- LOG .info (
365- "401 detected on websocket, refreshing websocket token. Old url: {self.ws_url} Old Token: {self.ws_token}"
366- )
367- LOG .info ("401 detected on %s" , err .request_info .url )
368- async with self ._backoff_refresh_lock_ws :
369- await self .refresh_ws_token ()
370- await self .get_websocket_params ()
371- return
345+ LOG .warning ("Refreshing API token on %s" , err .request_info .url )
372346
373347 @staticmethod
374348 def _handle_on_giveup (_ : dict [str , Any ]) -> None :
@@ -413,57 +387,13 @@ def enable_request_retries(self) -> None:
413387
414388 async def _async_post_init (self ) -> None :
415389 """Perform some post-init actions."""
416- LOG .debug ("Websocket _async_post_init running" )
390+ LOG .debug ("SignalR _async_post_init running" )
417391 await self ._get_fid ()
418392 await self ._get_device_token ()
419393
420- # Initialize WebsocketManager ic-dev21
421- self .websocket_manager = WebsocketManager (
422- self .session , self .async_request , self ._state_yaml , set_state
423- )
424- await self .websocket_manager .initialize_websockets ()
425-
426- # Create both websocket clients
427- # ic-dev21 need to work on this as it can't lint as is, may need to
428- # instantiate differently
429- # TODO: fix type ignore after refactor
430- self .websocket_devices = WebsocketClient (self .websocket_manager .devicehub ) # type: ignore
431-
432- # For backward compatibility during the transition to challengehub websocket
433- self .websocket = self .websocket_devices
434- self .websocket_challenges = WebsocketClient (self .websocket_manager .challengehub ) # type: ignore
435-
436- async def refresh_ws_token (self ) -> None :
437- """Refresh the websocket token."""
438- await self .websocket_manager .refresh_token (self .websocket_manager .devicehub )
439- await self .websocket_manager .refresh_token (self .websocket_manager .challengehub )
440-
441- async def get_websocket_params (self ) -> None :
442- """Retrieves and constructs WebSocket connection parameters from the negotiation endpoint."""
443- uri = parse .urlparse (self .ws_url )
444- LOG .debug ("Getting websocket params" )
445- LOG .debug ("Getting uri %s" , uri )
446- resp : dict [str , Any ] = await self .async_request (
447- "post" ,
448- f"{ uri .path } negotiate?{ uri .query } " ,
449- host = uri .netloc ,
450- headers = {
451- "authorization" : f"Bearer { self .ws_token } " ,
452- },
453- )
454- conn_id : str = resp .get ("connectionId" , "" )
455- self .full_ws_url = f"{ self .ws_url } &id={ conn_id } &access_token={ self .ws_token } "
456- LOG .debug ("Getting full ws URL %s" , self .full_ws_url )
457- transport_dict : list [WebsocketTransportsDict ] = resp .get (
458- "availableTransports" , []
459- )
460- websocket_dict : WebsocketDict = {
461- "connection_id" : conn_id ,
462- "available_transports" : transport_dict ,
463- "full_ws_url" : self .full_ws_url ,
464- }
465- LOG .debug ("Calling set_state from get_websocket_params" )
466- await set_state (self ._state_yaml , "websocket" , websocket_dict )
394+ signalr_manager = SignalRManager (self .async_request )
395+ self .signalr_devices = signalr_manager .build_hub ("/DeviceHub" )
396+ self .signalr_challenges = signalr_manager .build_hub ("/ChallengeHub" )
467397
468398 async def fb_install (self , fb_id : str ) -> None :
469399 """Registers a Firebase installation and stores the authentication token state."""
@@ -535,9 +465,7 @@ async def android_register(self) -> None:
535465 await set_state (
536466 self ._state_yaml ,
537467 "android" ,
538- {
539- "token" : token ,
540- },
468+ cast (AndroidDeviceDict , {"token" : token }),
541469 )
542470
543471 async def get_location_ids (self ) -> tuple [int , str ]:
@@ -548,26 +476,26 @@ async def get_location_ids(self) -> tuple[int, str]:
548476 return (req [0 ]["id" ], req [0 ]["locationHiloId" ])
549477
550478 def set_device_cache (self , devices : list [dict [str , Any ]]) -> None :
551- """Store devices received from websocket DeviceListInitialValuesReceived.
479+ """Store devices received from SignalR DeviceListInitialValuesReceived.
552480
553- This replaces the old REST API get_devices call. The websocket sends
481+ This replaces the old REST API get_devices call. SignalR sends
554482 device data with list-type attributes (supportedAttributesList, etc.)
555483 which need to be converted to comma-separated strings to match the
556484 format that HiloDevice.update() expects.
557485 """
558486 self ._device_cache = [self ._convert_ws_device (device ) for device in devices ]
559487 LOG .debug (
560- "Device cache populated with %d devices from websocket " ,
488+ "Device cache populated with %d devices from SignalR " ,
561489 len (self ._device_cache ),
562490 )
563491 self ._device_cache_event .set ()
564492
565493 @staticmethod
566494 def _convert_ws_device (ws_device : dict [str , Any ]) -> dict [str , Any ]:
567- """Convert a websocket device dict to the format generate_device expects.
495+ """Convert a SignalR device dict to the format generate_device expects.
568496
569497 The REST API returned supportedAttributes/settableAttributes as
570- comma-separated strings. The websocket returns supportedAttributesList/
498+ comma-separated strings. SignalR returns supportedAttributesList/
571499 settableAttributesList/supportedParametersList as Python lists.
572500 We convert to the old format so HiloDevice.update() works unchanged.
573501 """
@@ -590,25 +518,25 @@ def _convert_ws_device(ws_device: dict[str, Any]) -> dict[str, Any]:
590518 return device
591519
592520 async def wait_for_device_cache (self , timeout : float = 30.0 ) -> None :
593- """Wait for the device cache to be populated from websocket .
521+ """Wait for the device cache to be populated from SignalR .
594522
595523 :param timeout: Maximum time to wait in seconds
596524 :raises TimeoutError: If the device cache is not populated in time
597525 """
598526 if self ._device_cache_event .is_set ():
599527 return
600- LOG .debug ("Waiting for device cache from websocket (timeout=%ss)" , timeout )
528+ LOG .debug ("Waiting for device cache from SignalR (timeout=%ss)" , timeout )
601529 try :
602530 await asyncio .wait_for (self ._device_cache_event .wait (), timeout = timeout )
603531 except asyncio .TimeoutError :
604532 LOG .error (
605- "Timed out waiting for device list from websocket after %ss" ,
533+ "Timed out waiting for device list from SignalR after %ss" ,
606534 timeout ,
607535 )
608536 raise
609537
610538 def get_device_cache (self , location_id : int ) -> list [dict [str , Any ]]:
611- """Return cached devices from websocket .
539+ """Return cached devices from SignalR .
612540
613541 :param location_id: Hilo location id (unused but kept for interface compat)
614542 :return: List of device dicts ready for generate_device()
@@ -618,7 +546,7 @@ def get_device_cache(self, location_id: int) -> list[dict[str, Any]]:
618546 def add_to_device_cache (self , devices : list [dict [str , Any ]]) -> None :
619547 """Append new devices to the existing cache (e.g. from DeviceAdded).
620548
621- Converts websocket format and adds to the cache without replacing
549+ Converts SignalR format and adds to the cache without replacing
622550 existing entries. Skips devices already in cache (by id).
623551 """
624552 existing_ids = {d .get ("id" ) for d in self ._device_cache }
0 commit comments