Skip to content

Commit

Permalink
Add retry on protocol errors
Browse files Browse the repository at this point in the history
Some tv's disconnect lingering connections at write time,
leading to an exception. Retry the request once in those
cases.
  • Loading branch information
elupus committed May 28, 2024
1 parent 21caf6f commit d8d4201
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 30 deletions.
71 changes: 41 additions & 30 deletions haphilipsjs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from urllib.parse import quote
from secrets import token_bytes, token_hex
from base64 import b64decode, b64encode
from functools import wraps


from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
Expand Down Expand Up @@ -200,6 +202,27 @@ def __init__(self, data):
T = TypeVar("T")


def handle_httpx_exceptions(f):
"""Wrap up httpx exceptions in our wanted variants."""
@wraps(f)
async def wrapper(*args, **kwds):
try:
try:
return await f(*args, **kwds)
except httpx.RemoteProtocolError as err:
LOG.warning("%r. We retry once, could be a reused session that was closed", err)
return await f(*args, **kwds)

except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except (httpx.ProtocolError, httpx.ReadError) as err:
raise ProtocolFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err

return wrapper


class PhilipsTV(object):

channels: ChannelsType
Expand Down Expand Up @@ -569,38 +592,32 @@ def _url(self, path, protocol = None):

return f"{protocol}://{self._host}:{port}/{self.api_version}/{path}"

@handle_httpx_exceptions
async def getReq(self, path, protocol = None) -> Optional[Dict]:
try:
resp = await self.session.get(self._url(path, protocol = protocol))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")
resp = await self.session.get(self._url(path, protocol = protocol))

if resp.status_code != 200:
LOG.debug("Get failed: %s -> %d %s", path, resp.status_code, resp.text)
return None
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")

LOG.debug("Get succeded: %s -> %s", path, resp.text)
return decode_xtv_response(resp)
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err
if resp.status_code != 200:
LOG.debug("Get failed: %s -> %d %s", path, resp.status_code, resp.text)
return None

LOG.debug("Get succeded: %s -> %s", path, resp.text)
return decode_xtv_response(resp)

@handle_httpx_exceptions
async def _getBinary(self, path: str) -> Tuple[Optional[bytes], Optional[str]]:

try:
resp = await self.session.get(self._url(path))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")
resp = await self.session.get(self._url(path))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")

if resp.status_code != 200:
return None, None
return resp.content, resp.headers.get("content-type")
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err
if resp.status_code != 200:
return None, None
return resp.content, resp.headers.get("content-type")

@handle_httpx_exceptions
async def postReq(self, path: str, data: Any, timeout=None, protocol=None) -> Optional[Dict]:
try:
resp = await self.session.post(self._url(path, protocol), json=data, timeout=timeout)
Expand All @@ -616,12 +633,6 @@ async def postReq(self, path: str, data: Any, timeout=None, protocol=None) -> Op
except httpx.ReadTimeout:
LOG.debug("Read time out on postReq", exc_info=True)
return None
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except (httpx.ProtocolError, httpx.ReadError) as err:
raise ProtocolFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err

async def pairRequest(
self,
Expand Down
9 changes: 9 additions & 0 deletions tests/test_v6.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ async def test_send_key_off(client_mock, param: Param):
await client_mock.sendKey("Standby")


async def test_send_key_retry(client_mock, param: Param):
route = respx.post(f"{param.base}/input/key").mock(side_effect=httpx.RemoteProtocolError)

with pytest.raises(haphilipsjs.ProtocolFailure):
await client_mock.sendKey("Standby")

assert route.call_count == 2


async def test_ambilight_mode(client_mock, param):
await client_mock.getSystem()

Expand Down

0 comments on commit d8d4201

Please sign in to comment.