mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Merge branch 'master' into janitor/smartcam_referer
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from functools import singledispatch, update_wrapper, wraps
|
||||
from gettext import gettext
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import asyncclick as click
|
||||
@@ -238,4 +240,19 @@ def CatchAllExceptions(cls):
|
||||
except Exception as exc:
|
||||
_handle_exception(self._debug, exc)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Run the coroutine in the event loop and print any exceptions.
|
||||
|
||||
python click catches KeyboardInterrupt in main, raises Abort()
|
||||
and does sys.exit. asyncclick doesn't properly handle a coroutine
|
||||
receiving CancelledError on a KeyboardInterrupt, so we catch the
|
||||
KeyboardInterrupt here once asyncio.run has re-raised it. This
|
||||
avoids large stacktraces when a user presses Ctrl-C.
|
||||
"""
|
||||
try:
|
||||
asyncio.run(self.main(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
click.echo(gettext("\nAborted!"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return _CommandCls
|
||||
|
@@ -498,7 +498,7 @@ class Discover:
|
||||
try:
|
||||
_LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout)
|
||||
await protocol.wait_for_discovery_to_complete()
|
||||
except KasaException as ex:
|
||||
except (KasaException, asyncio.CancelledError) as ex:
|
||||
for device in protocol.discovered_devices.values():
|
||||
await device.protocol.close()
|
||||
raise ex
|
||||
|
@@ -113,10 +113,23 @@ class HttpClient:
|
||||
ssl=ssl,
|
||||
)
|
||||
async with resp:
|
||||
if resp.status == 200:
|
||||
response_data = await resp.read()
|
||||
if return_json:
|
||||
response_data = await resp.read()
|
||||
|
||||
if resp.status == 200:
|
||||
if return_json:
|
||||
response_data = json_loads(response_data.decode())
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Device %s received status code %s with response %s",
|
||||
self._config.host,
|
||||
resp.status,
|
||||
str(response_data),
|
||||
)
|
||||
if response_data and return_json:
|
||||
try:
|
||||
response_data = json_loads(response_data.decode())
|
||||
except Exception:
|
||||
_LOGGER.debug("Device %s response could not be parsed as json")
|
||||
|
||||
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex:
|
||||
if not self._wait_between_requests:
|
||||
|
@@ -9,6 +9,7 @@ import logging
|
||||
import secrets
|
||||
import socket
|
||||
import ssl
|
||||
from contextlib import suppress
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
@@ -156,6 +157,19 @@ class SslAesTransport(BaseTransport):
|
||||
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
|
||||
return error_code
|
||||
|
||||
def _get_response_inner_error(self, resp_dict: Any) -> SmartErrorCode | None:
|
||||
error_code_raw = resp_dict.get("data", {}).get("code")
|
||||
if error_code_raw is None:
|
||||
return None
|
||||
try:
|
||||
error_code = SmartErrorCode.from_int(error_code_raw)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Device %s received unknown error code: %s", self._host, error_code_raw
|
||||
)
|
||||
error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR
|
||||
return error_code
|
||||
|
||||
def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
|
||||
error_code = self._get_response_error(resp_dict)
|
||||
if error_code is SmartErrorCode.SUCCESS:
|
||||
@@ -240,6 +254,31 @@ class SslAesTransport(BaseTransport):
|
||||
ssl=await self._get_ssl_context(),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self._encryption_session is not None
|
||||
|
||||
# Devices can respond with 500 if another session is created from
|
||||
# the same host. Decryption may not succeed after that
|
||||
if status_code == 500:
|
||||
msg = (
|
||||
f"Device {self._host} replied with status 500 after handshake, "
|
||||
f"response: "
|
||||
)
|
||||
decrypted = None
|
||||
if isinstance(resp_dict, dict) and (
|
||||
response := resp_dict.get("result", {}).get("response")
|
||||
):
|
||||
with suppress(Exception):
|
||||
decrypted = self._encryption_session.decrypt(response.encode())
|
||||
|
||||
if decrypted:
|
||||
msg += decrypted
|
||||
else:
|
||||
msg += str(resp_dict)
|
||||
|
||||
_LOGGER.debug(msg)
|
||||
raise _RetryableError(msg)
|
||||
|
||||
if status_code != 200:
|
||||
raise KasaException(
|
||||
f"{self._host} responded with an unexpected "
|
||||
@@ -252,7 +291,6 @@ class SslAesTransport(BaseTransport):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
resp_dict = cast(dict[str, Any], resp_dict)
|
||||
assert self._encryption_session is not None
|
||||
|
||||
if "result" in resp_dict and "response" in resp_dict["result"]:
|
||||
raw_response: str = resp_dict["result"]["response"]
|
||||
@@ -407,13 +445,29 @@ class SslAesTransport(BaseTransport):
|
||||
error_code = default_error_code
|
||||
resp_dict = default_resp_dict
|
||||
|
||||
# If the default login worked it's ok not to provide credentials but if
|
||||
# it didn't raise auth error here.
|
||||
if not self._username:
|
||||
raise AuthenticationError(
|
||||
f"Credentials must be supplied to connect to {self._host}"
|
||||
)
|
||||
|
||||
# Device responds with INVALID_NONCE and a "nonce" to indicate ready
|
||||
# for secure login. Otherwise error.
|
||||
if error_code is not SmartErrorCode.INVALID_NONCE or (
|
||||
resp_dict and "nonce" not in resp_dict["result"].get("data", {})
|
||||
resp_dict and "nonce" not in resp_dict.get("result", {}).get("data", {})
|
||||
):
|
||||
if (
|
||||
resp_dict
|
||||
and self._get_response_inner_error(resp_dict)
|
||||
is SmartErrorCode.DEVICE_BLOCKED
|
||||
):
|
||||
sec_left = resp_dict.get("data", {}).get("sec_left")
|
||||
msg = "Device blocked" + (
|
||||
f" for {sec_left} seconds" if sec_left else ""
|
||||
)
|
||||
raise DeviceError(msg, error_code=SmartErrorCode.DEVICE_BLOCKED)
|
||||
|
||||
raise AuthenticationError(f"Error trying handshake1: {resp_dict}")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
Reference in New Issue
Block a user