Rename and deprecate exception classes (#739)

# Public #
SmartDeviceException -> KasaException
UnsupportedDeviceException(SmartDeviceException) -> UnsupportedDeviceError(KasaException)
TimeoutException(SmartDeviceException, asyncio.TimeoutError) -> TimeoutError(KasaException, asyncio.TimeoutError)

Add new exception for error codes -> DeviceError(KasaException)
AuthenticationException(SmartDeviceException) -> AuthenticationError(DeviceError)

# Internal #
RetryableException(SmartDeviceException) -> _RetryableError(DeviceError)
ConnectionException(SmartDeviceException) -> _ConnectionError(KasaException)
This commit is contained in:
Steven B 2024-02-21 15:52:55 +00:00 committed by GitHub
parent 4beff228c9
commit 8c39e81a40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 393 additions and 361 deletions

View File

@ -22,12 +22,12 @@ import asyncclick as click
from devtools.helpers.smartrequests import SmartRequest, get_component_requests from devtools.helpers.smartrequests import SmartRequest, get_component_requests
from kasa import ( from kasa import (
AuthenticationException, AuthenticationError,
Credentials, Credentials,
Device, Device,
Discover, Discover,
SmartDeviceException, KasaException,
TimeoutException, TimeoutError,
) )
from kasa.discover import DiscoveryResult from kasa.discover import DiscoveryResult
from kasa.exceptions import SmartErrorCode from kasa.exceptions import SmartErrorCode
@ -303,19 +303,16 @@ async def _make_requests_or_exit(
for method, result in responses.items(): for method, result in responses.items():
final[method] = result final[method] = result
return final return final
except AuthenticationException as ex: except AuthenticationError as ex:
_echo_error( _echo_error(
f"Unable to query the device due to an authentication error: {ex}", f"Unable to query the device due to an authentication error: {ex}",
) )
exit(1) exit(1)
except SmartDeviceException as ex: except KasaException as ex:
_echo_error( _echo_error(
f"Unable to query {name} at once: {ex}", f"Unable to query {name} at once: {ex}",
) )
if ( if isinstance(ex, TimeoutError):
isinstance(ex, TimeoutException)
or ex.error_code == SmartErrorCode.SESSION_TIMEOUT_ERROR
):
_echo_error( _echo_error(
"Timeout, try reducing the batch size via --batch-size option.", "Timeout, try reducing the batch size via --batch-size option.",
) )
@ -400,7 +397,7 @@ async def get_smart_fixture(device: SmartDevice, batch_size: int):
response = await device.protocol.query( response = await device.protocol.query(
SmartRequest._create_request_dict(test_call.request) SmartRequest._create_request_dict(test_call.request)
) )
except AuthenticationException as ex: except AuthenticationError as ex:
_echo_error( _echo_error(
f"Unable to query the device due to an authentication error: {ex}", f"Unable to query the device due to an authentication error: {ex}",
) )

View File

@ -100,6 +100,17 @@ The classes providing this functionality are:
- :class:`KlapTransport <kasa.klaptransport.KlapTransport>` - :class:`KlapTransport <kasa.klaptransport.KlapTransport>`
- :class:`KlapTransportV2 <kasa.klaptransport.KlapTransportV2>` - :class:`KlapTransportV2 <kasa.klaptransport.KlapTransportV2>`
Errors and Exceptions
*********************
The base exception for all library errors is :class:`KasaException <kasa.exceptions.KasaException>`.
- If the device returns an error the library raises a :class:`DeviceError <kasa.exceptions.DeviceError>` which will usually contain an ``error_code`` with the detail.
- If the device fails to authenticate the library raises an :class:`AuthenticationError <kasa.exceptions.AuthenticationError>` which is derived
from :class:`DeviceError <kasa.exceptions.DeviceError>` and could contain an ``error_code`` depending on the type of failure.
- If the library encounters and unsupported deviceit raises an :class:`UnsupportedDeviceError <kasa.exceptions.UnsupportedDeviceError>`.
- If the device fails to respond within a timeout the library raises a :class:`TimeoutError <kasa.exceptions.TimeoutError>`.
- All other failures will raise the base :class:`KasaException <kasa.exceptions.KasaException>` class.
API documentation for modules API documentation for modules
***************************** *****************************
@ -154,3 +165,26 @@ API documentation for protocols and transports
:members: :members:
:inherited-members: :inherited-members:
:undoc-members: :undoc-members:
API documentation for errors and exceptions
*******************************************
.. autoclass:: kasa.exceptions.KasaException
:members:
:undoc-members:
.. autoclass:: kasa.exceptions.DeviceError
:members:
:undoc-members:
.. autoclass:: kasa.exceptions.AuthenticationError
:members:
:undoc-members:
.. autoclass:: kasa.exceptions.UnsupportedDeviceError
:members:
:undoc-members:
.. autoclass:: kasa.exceptions.TimeoutError
:members:
:undoc-members:

View File

@ -24,7 +24,7 @@ Methods changing the state of the device do not invalidate the cache (i.e., ther
You can assume that the operation has succeeded if no exception is raised. You can assume that the operation has succeeded if no exception is raised.
These methods will return the device response, which can be useful for some use cases. These methods will return the device response, which can be useful for some use cases.
Errors are raised as :class:`SmartDeviceException` instances for the library user to handle. Errors are raised as :class:`KasaException` instances for the library user to handle.
Simple example script showing some functionality for legacy devices: Simple example script showing some functionality for legacy devices:
@ -154,15 +154,3 @@ API documentation
.. autoclass:: Credentials .. autoclass:: Credentials
:members: :members:
:undoc-members: :undoc-members:
.. autoclass:: SmartDeviceException
:members:
:undoc-members:
.. autoclass:: AuthenticationException
:members:
:undoc-members:
.. autoclass:: UnsupportedDeviceException
:members:
:undoc-members:

View File

@ -8,7 +8,7 @@ All common, shared functionalities are available through `SmartDevice` class::
For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip` For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip`
should be used instead. should be used instead.
Module-specific errors are raised as `SmartDeviceException` and are expected Module-specific errors are raised as `KasaException` and are expected
to be handled by the user of the library. to be handled by the user of the library.
""" """
from importlib.metadata import version from importlib.metadata import version
@ -28,10 +28,11 @@ from kasa.deviceconfig import (
from kasa.discover import Discover from kasa.discover import Discover
from kasa.emeterstatus import EmeterStatus from kasa.emeterstatus import EmeterStatus
from kasa.exceptions import ( from kasa.exceptions import (
AuthenticationException, AuthenticationError,
SmartDeviceException, DeviceError,
TimeoutException, KasaException,
UnsupportedDeviceException, TimeoutError,
UnsupportedDeviceError,
) )
from kasa.feature import Feature, FeatureType from kasa.feature import Feature, FeatureType
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
@ -61,10 +62,11 @@ __all__ = [
"Device", "Device",
"Bulb", "Bulb",
"Plug", "Plug",
"SmartDeviceException", "KasaException",
"AuthenticationException", "AuthenticationError",
"UnsupportedDeviceException", "DeviceError",
"TimeoutException", "UnsupportedDeviceError",
"TimeoutError",
"Credentials", "Credentials",
"DeviceConfig", "DeviceConfig",
"ConnectionType", "ConnectionType",
@ -84,6 +86,12 @@ deprecated_smart_devices = {
"SmartDimmer": iot.IotDimmer, "SmartDimmer": iot.IotDimmer,
"SmartBulbPreset": BulbPreset, "SmartBulbPreset": BulbPreset,
} }
deprecated_exceptions = {
"SmartDeviceException": KasaException,
"UnsupportedDeviceException": UnsupportedDeviceError,
"AuthenticationException": AuthenticationError,
"TimeoutException": TimeoutError,
}
def __getattr__(name): def __getattr__(name):
@ -101,6 +109,11 @@ def __getattr__(name):
stacklevel=1, stacklevel=1,
) )
return new_class return new_class
if name in deprecated_exceptions:
new_class = deprecated_exceptions[name]
msg = f"{name} is deprecated, use {new_class.__name__} instead"
warn(msg, DeprecationWarning, stacklevel=1)
return new_class
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@ -112,6 +125,11 @@ if TYPE_CHECKING:
SmartStrip = iot.IotStrip SmartStrip = iot.IotStrip
SmartDimmer = iot.IotDimmer SmartDimmer = iot.IotDimmer
SmartBulbPreset = BulbPreset SmartBulbPreset = BulbPreset
SmartDeviceException = KasaException
UnsupportedDeviceException = UnsupportedDeviceError
AuthenticationException = AuthenticationError
TimeoutException = TimeoutError
# Instanstiate all classes so the type checkers catch abstract issues # Instanstiate all classes so the type checkers catch abstract issues
from . import smart from . import smart

View File

@ -22,12 +22,11 @@ from .deviceconfig import DeviceConfig
from .exceptions import ( from .exceptions import (
SMART_AUTHENTICATION_ERRORS, SMART_AUTHENTICATION_ERRORS,
SMART_RETRYABLE_ERRORS, SMART_RETRYABLE_ERRORS,
SMART_TIMEOUT_ERRORS, AuthenticationError,
AuthenticationException, DeviceError,
RetryableException, KasaException,
SmartDeviceException,
SmartErrorCode, SmartErrorCode,
TimeoutException, _RetryableError,
) )
from .httpclient import HttpClient from .httpclient import HttpClient
from .json import dumps as json_dumps from .json import dumps as json_dumps
@ -141,14 +140,12 @@ class AesTransport(BaseTransport):
if error_code == SmartErrorCode.SUCCESS: if error_code == SmartErrorCode.SUCCESS:
return return
msg = f"{msg}: {self._host}: {error_code.name}({error_code.value})" msg = f"{msg}: {self._host}: {error_code.name}({error_code.value})"
if error_code in SMART_TIMEOUT_ERRORS:
raise TimeoutException(msg, error_code=error_code)
if error_code in SMART_RETRYABLE_ERRORS: if error_code in SMART_RETRYABLE_ERRORS:
raise RetryableException(msg, error_code=error_code) raise _RetryableError(msg, error_code=error_code)
if error_code in SMART_AUTHENTICATION_ERRORS: if error_code in SMART_AUTHENTICATION_ERRORS:
self._state = TransportState.HANDSHAKE_REQUIRED self._state = TransportState.HANDSHAKE_REQUIRED
raise AuthenticationException(msg, error_code=error_code) raise AuthenticationError(msg, error_code=error_code)
raise SmartDeviceException(msg, error_code=error_code) raise DeviceError(msg, error_code=error_code)
async def send_secure_passthrough(self, request: str) -> Dict[str, Any]: async def send_secure_passthrough(self, request: str) -> Dict[str, Any]:
"""Send encrypted message as passthrough.""" """Send encrypted message as passthrough."""
@ -171,7 +168,7 @@ class AesTransport(BaseTransport):
# _LOGGER.debug(f"secure_passthrough response is {status_code}: {resp_dict}") # _LOGGER.debug(f"secure_passthrough response is {status_code}: {resp_dict}")
if status_code != 200: if status_code != 200:
raise SmartDeviceException( raise KasaException(
f"{self._host} responded with an unexpected " f"{self._host} responded with an unexpected "
+ f"status code {status_code} to passthrough" + f"status code {status_code} to passthrough"
) )
@ -197,7 +194,7 @@ class AesTransport(BaseTransport):
self._host, self._host,
) )
except Exception: except Exception:
raise SmartDeviceException( raise KasaException(
f"Unable to decrypt response from {self._host}, " f"Unable to decrypt response from {self._host}, "
+ f"error: {ex}, response: {raw_response}", + f"error: {ex}, response: {raw_response}",
ex, ex,
@ -208,7 +205,7 @@ class AesTransport(BaseTransport):
"""Login to the device.""" """Login to the device."""
try: try:
await self.try_login(self._login_params) await self.try_login(self._login_params)
except AuthenticationException as aex: except AuthenticationError as aex:
try: try:
if aex.error_code is not SmartErrorCode.LOGIN_ERROR: if aex.error_code is not SmartErrorCode.LOGIN_ERROR:
raise aex raise aex
@ -223,10 +220,10 @@ class AesTransport(BaseTransport):
"%s: logged in with default credentials", "%s: logged in with default credentials",
self._host, self._host,
) )
except AuthenticationException: except AuthenticationError:
raise raise
except Exception as ex: except Exception as ex:
raise SmartDeviceException( raise KasaException(
"Unable to login and trying default " "Unable to login and trying default "
+ f"login raised another exception: {ex}", + f"login raised another exception: {ex}",
ex, ex,
@ -292,7 +289,7 @@ class AesTransport(BaseTransport):
_LOGGER.debug("Device responded with: %s", resp_dict) _LOGGER.debug("Device responded with: %s", resp_dict)
if status_code != 200: if status_code != 200:
raise SmartDeviceException( raise KasaException(
f"{self._host} responded with an unexpected " f"{self._host} responded with an unexpected "
+ f"status code {status_code} to handshake" + f"status code {status_code} to handshake"
) )
@ -347,7 +344,7 @@ class AesTransport(BaseTransport):
await self.perform_login() await self.perform_login()
# After a login failure handshake needs to # After a login failure handshake needs to
# be redone or a 9999 error is received. # be redone or a 9999 error is received.
except AuthenticationException as ex: except AuthenticationError as ex:
self._state = TransportState.HANDSHAKE_REQUIRED self._state = TransportState.HANDSHAKE_REQUIRED
raise ex raise ex

View File

@ -13,7 +13,7 @@ from typing import Any, Dict, cast
import asyncclick as click import asyncclick as click
from kasa import ( from kasa import (
AuthenticationException, AuthenticationError,
Bulb, Bulb,
ConnectionType, ConnectionType,
Credentials, Credentials,
@ -22,8 +22,8 @@ from kasa import (
DeviceFamilyType, DeviceFamilyType,
Discover, Discover,
EncryptType, EncryptType,
SmartDeviceException, KasaException,
UnsupportedDeviceException, UnsupportedDeviceError,
) )
from kasa.discover import DiscoveryResult from kasa.discover import DiscoveryResult
from kasa.iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip from kasa.iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
@ -458,7 +458,7 @@ async def discover(ctx):
unsupported = [] unsupported = []
auth_failed = [] auth_failed = []
async def print_unsupported(unsupported_exception: UnsupportedDeviceException): async def print_unsupported(unsupported_exception: UnsupportedDeviceError):
unsupported.append(unsupported_exception) unsupported.append(unsupported_exception)
async with sem: async with sem:
if unsupported_exception.discovery_result: if unsupported_exception.discovery_result:
@ -476,7 +476,7 @@ async def discover(ctx):
async with sem: async with sem:
try: try:
await dev.update() await dev.update()
except AuthenticationException: except AuthenticationError:
auth_failed.append(dev._discovery_info) auth_failed.append(dev._discovery_info)
echo("== Authentication failed for device ==") echo("== Authentication failed for device ==")
_echo_discovery_info(dev._discovery_info) _echo_discovery_info(dev._discovery_info)
@ -677,7 +677,7 @@ async def cmd_command(dev: Device, module, command, parameters):
elif isinstance(dev, SmartDevice): elif isinstance(dev, SmartDevice):
res = await dev._query_helper(command, parameters) res = await dev._query_helper(command, parameters)
else: else:
raise SmartDeviceException("Unexpected device type %s.", dev) raise KasaException("Unexpected device type %s.", dev)
echo(json.dumps(res)) echo(json.dumps(res))
return res return res

View File

@ -9,7 +9,7 @@ from .credentials import Credentials
from .device_type import DeviceType from .device_type import DeviceType
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .emeterstatus import EmeterStatus from .emeterstatus import EmeterStatus
from .exceptions import SmartDeviceException from .exceptions import KasaException
from .feature import Feature from .feature import Feature
from .iotprotocol import IotProtocol from .iotprotocol import IotProtocol
from .protocol import BaseProtocol from .protocol import BaseProtocol
@ -242,12 +242,12 @@ class Device(ABC):
if p.alias == name: if p.alias == name:
return p return p
raise SmartDeviceException(f"Device has no child with {name}") raise KasaException(f"Device has no child with {name}")
def get_plug_by_index(self, index: int) -> "Device": def get_plug_by_index(self, index: int) -> "Device":
"""Return child device for the given index.""" """Return child device for the given index."""
if index + 1 > len(self.children) or index < 0: if index + 1 > len(self.children) or index < 0:
raise SmartDeviceException( raise KasaException(
f"Invalid index {index}, device has {len(self.children)} plugs" f"Invalid index {index}, device has {len(self.children)} plugs"
) )
return self.children[index] return self.children[index]
@ -306,7 +306,7 @@ class Device(ABC):
"""Add a new feature to the device.""" """Add a new feature to the device."""
desc_name = feature.name.lower().replace(" ", "_") desc_name = feature.name.lower().replace(" ", "_")
if desc_name in self._features: if desc_name in self._features:
raise SmartDeviceException("Duplicate feature name %s" % desc_name) raise KasaException("Duplicate feature name %s" % desc_name)
self._features[desc_name] = feature self._features[desc_name] = feature
@property @property

View File

@ -6,7 +6,7 @@ from typing import Any, Dict, Optional, Tuple, Type
from .aestransport import AesTransport from .aestransport import AesTransport
from .device import Device from .device import Device
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import SmartDeviceException, UnsupportedDeviceException from .exceptions import KasaException, UnsupportedDeviceError
from .iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip from .iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
from .iotprotocol import IotProtocol from .iotprotocol import IotProtocol
from .klaptransport import KlapTransport, KlapTransportV2 from .klaptransport import KlapTransport, KlapTransportV2
@ -45,12 +45,12 @@ async def connect(*, host: Optional[str] = None, config: DeviceConfig) -> "Devic
:return: Object for querying/controlling found device. :return: Object for querying/controlling found device.
""" """
if host and config or (not host and not config): if host and config or (not host and not config):
raise SmartDeviceException("One of host or config must be provded and not both") raise KasaException("One of host or config must be provded and not both")
if host: if host:
config = DeviceConfig(host=host) config = DeviceConfig(host=host)
if (protocol := get_protocol(config=config)) is None: if (protocol := get_protocol(config=config)) is None:
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unsupported device for {config.host}: " f"Unsupported device for {config.host}: "
+ f"{config.connection_type.device_family.value}" + f"{config.connection_type.device_family.value}"
) )
@ -99,7 +99,7 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
_perf_log(True, "update") _perf_log(True, "update")
return device return device
else: else:
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unsupported device for {config.host}: " f"Unsupported device for {config.host}: "
+ f"{config.connection_type.device_family.value}" + f"{config.connection_type.device_family.value}"
) )
@ -108,12 +108,12 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]: def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
"""Find SmartDevice subclass for device described by passed data.""" """Find SmartDevice subclass for device described by passed data."""
if "system" not in info or "get_sysinfo" not in info["system"]: if "system" not in info or "get_sysinfo" not in info["system"]:
raise SmartDeviceException("No 'system' or 'get_sysinfo' in response") raise KasaException("No 'system' or 'get_sysinfo' in response")
sysinfo: Dict[str, Any] = info["system"]["get_sysinfo"] sysinfo: Dict[str, Any] = info["system"]["get_sysinfo"]
type_: Optional[str] = sysinfo.get("type", sysinfo.get("mic_type")) type_: Optional[str] = sysinfo.get("type", sysinfo.get("mic_type"))
if type_ is None: if type_ is None:
raise SmartDeviceException("Unable to find the device type field!") raise KasaException("Unable to find the device type field!")
if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]: if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]:
return IotDimmer return IotDimmer
@ -129,7 +129,7 @@ def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
return IotLightStrip return IotLightStrip
return IotBulb return IotBulb
raise UnsupportedDeviceException("Unknown device type: %s" % type_) raise UnsupportedDeviceError("Unknown device type: %s" % type_)
def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]: def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]:

View File

@ -5,7 +5,7 @@ from enum import Enum
from typing import TYPE_CHECKING, Dict, Optional, Union from typing import TYPE_CHECKING, Dict, Optional, Union
from .credentials import Credentials from .credentials import Credentials
from .exceptions import SmartDeviceException from .exceptions import KasaException
if TYPE_CHECKING: if TYPE_CHECKING:
from aiohttp import ClientSession from aiohttp import ClientSession
@ -46,7 +46,7 @@ def _dataclass_from_dict(klass, in_val):
fieldtypes[dict_key], in_val[dict_key] fieldtypes[dict_key], in_val[dict_key]
) )
else: else:
raise SmartDeviceException( raise KasaException(
f"Cannot create dataclass from dict, unknown key: {dict_key}" f"Cannot create dataclass from dict, unknown key: {dict_key}"
) )
return klass(**val) return klass(**val)
@ -92,7 +92,7 @@ class ConnectionType:
login_version, login_version,
) )
except (ValueError, TypeError) as ex: except (ValueError, TypeError) as ex:
raise SmartDeviceException( raise KasaException(
f"Invalid connection parameters for {device_family}." f"Invalid connection parameters for {device_family}."
+ f"{encryption_type}.{login_version}" + f"{encryption_type}.{login_version}"
) from ex ) from ex
@ -113,9 +113,7 @@ class ConnectionType:
login_version, # type: ignore[arg-type] login_version, # type: ignore[arg-type]
) )
raise SmartDeviceException( raise KasaException(f"Invalid connection type data for {connection_type_dict}")
f"Invalid connection type data for {connection_type_dict}"
)
def to_dict(self) -> Dict[str, Union[str, int]]: def to_dict(self) -> Dict[str, Union[str, int]]:
"""Convert connection params to dict.""" """Convert connection params to dict."""
@ -185,4 +183,4 @@ class DeviceConfig:
"""Return device config from dict.""" """Return device config from dict."""
if isinstance(config_dict, dict): if isinstance(config_dict, dict):
return _dataclass_from_dict(DeviceConfig, config_dict) return _dataclass_from_dict(DeviceConfig, config_dict)
raise SmartDeviceException(f"Invalid device config data: {config_dict}") raise KasaException(f"Invalid device config data: {config_dict}")

View File

@ -24,9 +24,9 @@ from kasa.device_factory import (
) )
from kasa.deviceconfig import ConnectionType, DeviceConfig, EncryptType from kasa.deviceconfig import ConnectionType, DeviceConfig, EncryptType
from kasa.exceptions import ( from kasa.exceptions import (
SmartDeviceException, KasaException,
TimeoutException, TimeoutError,
UnsupportedDeviceException, UnsupportedDeviceError,
) )
from kasa.iot.iotdevice import IotDevice from kasa.iot.iotdevice import IotDevice
from kasa.json import dumps as json_dumps from kasa.json import dumps as json_dumps
@ -59,7 +59,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
discovery_timeout: int = 5, discovery_timeout: int = 5,
interface: Optional[str] = None, interface: Optional[str] = None,
on_unsupported: Optional[ on_unsupported: Optional[
Callable[[UnsupportedDeviceException], Awaitable[None]] Callable[[UnsupportedDeviceError], Awaitable[None]]
] = None, ] = None,
port: Optional[int] = None, port: Optional[int] = None,
credentials: Optional[Credentials] = None, credentials: Optional[Credentials] = None,
@ -162,14 +162,14 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
device = Discover._get_device_instance(data, config) device = Discover._get_device_instance(data, config)
else: else:
return return
except UnsupportedDeviceException as udex: except UnsupportedDeviceError as udex:
_LOGGER.debug("Unsupported device found at %s << %s", ip, udex) _LOGGER.debug("Unsupported device found at %s << %s", ip, udex)
self.unsupported_device_exceptions[ip] = udex self.unsupported_device_exceptions[ip] = udex
if self.on_unsupported is not None: if self.on_unsupported is not None:
self._run_callback_task(self.on_unsupported(udex)) self._run_callback_task(self.on_unsupported(udex))
self._handle_discovered_event() self._handle_discovered_event()
return return
except SmartDeviceException as ex: except KasaException as ex:
_LOGGER.debug(f"[DISCOVERY] Unable to find device type for {ip}: {ex}") _LOGGER.debug(f"[DISCOVERY] Unable to find device type for {ip}: {ex}")
self.invalid_device_exceptions[ip] = ex self.invalid_device_exceptions[ip] = ex
self._handle_discovered_event() self._handle_discovered_event()
@ -311,7 +311,7 @@ class Discover:
try: try:
_LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout) _LOGGER.debug("Waiting %s seconds for responses...", discovery_timeout)
await protocol.wait_for_discovery_to_complete() await protocol.wait_for_discovery_to_complete()
except SmartDeviceException as ex: except KasaException as ex:
for device in protocol.discovered_devices.values(): for device in protocol.discovered_devices.values():
await device.protocol.close() await device.protocol.close()
raise ex raise ex
@ -368,9 +368,7 @@ class Discover:
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo # https://docs.python.org/3/library/socket.html#socket.getaddrinfo
ip = adrrinfo[0][4][0] ip = adrrinfo[0][4][0]
except socket.gaierror as gex: except socket.gaierror as gex:
raise SmartDeviceException( raise KasaException(f"Could not resolve hostname {host}") from gex
f"Could not resolve hostname {host}"
) from gex
transport, protocol = await loop.create_datagram_endpoint( transport, protocol = await loop.create_datagram_endpoint(
lambda: _DiscoverProtocol( lambda: _DiscoverProtocol(
@ -401,7 +399,7 @@ class Discover:
elif ip in protocol.invalid_device_exceptions: elif ip in protocol.invalid_device_exceptions:
raise protocol.invalid_device_exceptions[ip] raise protocol.invalid_device_exceptions[ip]
else: else:
raise TimeoutException(f"Timed out getting discovery response for {host}") raise TimeoutError(f"Timed out getting discovery response for {host}")
@staticmethod @staticmethod
def _get_device_class(info: dict) -> Type[Device]: def _get_device_class(info: dict) -> Type[Device]:
@ -410,7 +408,7 @@ class Discover:
discovery_result = DiscoveryResult(**info["result"]) discovery_result = DiscoveryResult(**info["result"])
dev_class = get_device_class_from_family(discovery_result.device_type) dev_class = get_device_class_from_family(discovery_result.device_type)
if not dev_class: if not dev_class:
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
"Unknown device type: %s" % discovery_result.device_type, "Unknown device type: %s" % discovery_result.device_type,
discovery_result=info, discovery_result=info,
) )
@ -424,7 +422,7 @@ class Discover:
try: try:
info = json_loads(XorEncryption.decrypt(data)) info = json_loads(XorEncryption.decrypt(data))
except Exception as ex: except Exception as ex:
raise SmartDeviceException( raise KasaException(
f"Unable to read response from device: {config.host}: {ex}" f"Unable to read response from device: {config.host}: {ex}"
) from ex ) from ex
@ -451,7 +449,7 @@ class Discover:
info = json_loads(data[16:]) info = json_loads(data[16:])
except Exception as ex: except Exception as ex:
_LOGGER.debug("Got invalid response from device %s: %s", config.host, data) _LOGGER.debug("Got invalid response from device %s: %s", config.host, data)
raise SmartDeviceException( raise KasaException(
f"Unable to read response from device: {config.host}: {ex}" f"Unable to read response from device: {config.host}: {ex}"
) from ex ) from ex
try: try:
@ -460,7 +458,7 @@ class Discover:
_LOGGER.debug( _LOGGER.debug(
"Unable to parse discovery from device %s: %s", config.host, info "Unable to parse discovery from device %s: %s", config.host, info
) )
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unable to parse discovery from device: {config.host}: {ex}" f"Unable to parse discovery from device: {config.host}: {ex}"
) from ex ) from ex
@ -472,15 +470,15 @@ class Discover:
discovery_result.mgt_encrypt_schm.encrypt_type, discovery_result.mgt_encrypt_schm.encrypt_type,
discovery_result.mgt_encrypt_schm.lv, discovery_result.mgt_encrypt_schm.lv,
) )
except SmartDeviceException as ex: except KasaException as ex:
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unsupported device {config.host} of type {type_} " f"Unsupported device {config.host} of type {type_} "
+ f"with encrypt_type {discovery_result.mgt_encrypt_schm.encrypt_type}", + f"with encrypt_type {discovery_result.mgt_encrypt_schm.encrypt_type}",
discovery_result=discovery_result.get_dict(), discovery_result=discovery_result.get_dict(),
) from ex ) from ex
if (device_class := get_device_class_from_family(type_)) is None: if (device_class := get_device_class_from_family(type_)) is None:
_LOGGER.warning("Got unsupported device type: %s", type_) _LOGGER.warning("Got unsupported device type: %s", type_)
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unsupported device {config.host} of type {type_}: {info}", f"Unsupported device {config.host} of type {type_}: {info}",
discovery_result=discovery_result.get_dict(), discovery_result=discovery_result.get_dict(),
) )
@ -488,7 +486,7 @@ class Discover:
_LOGGER.warning( _LOGGER.warning(
"Got unsupported connection type: %s", config.connection_type.to_dict() "Got unsupported connection type: %s", config.connection_type.to_dict()
) )
raise UnsupportedDeviceException( raise UnsupportedDeviceError(
f"Unsupported encryption scheme {config.host} of " f"Unsupported encryption scheme {config.host} of "
+ f"type {config.connection_type.to_dict()}: {info}", + f"type {config.connection_type.to_dict()}: {info}",
discovery_result=discovery_result.get_dict(), discovery_result=discovery_result.get_dict(),

View File

@ -1,47 +1,59 @@
"""python-kasa exceptions.""" """python-kasa exceptions."""
from asyncio import TimeoutError from asyncio import TimeoutError as _asyncioTimeoutError
from enum import IntEnum from enum import IntEnum
from typing import Any, Optional from typing import Any, Optional
class SmartDeviceException(Exception): class KasaException(Exception):
"""Base exception for library errors."""
class TimeoutError(KasaException, _asyncioTimeoutError):
"""Timeout exception for device errors."""
def __repr__(self):
return KasaException.__repr__(self)
def __str__(self):
return KasaException.__str__(self)
class _ConnectionError(KasaException):
"""Connection exception for device errors."""
class UnsupportedDeviceError(KasaException):
"""Exception for trying to connect to unsupported devices."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.discovery_result = kwargs.get("discovery_result")
super().__init__(*args)
class DeviceError(KasaException):
"""Base exception for device errors.""" """Base exception for device errors."""
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self.error_code: Optional["SmartErrorCode"] = kwargs.get("error_code", None) self.error_code: Optional["SmartErrorCode"] = kwargs.get("error_code", None)
super().__init__(*args) super().__init__(*args)
def __repr__(self):
err_code = self.error_code.__repr__() if self.error_code else ""
return f"{self.__class__.__name__}({err_code})"
class UnsupportedDeviceException(SmartDeviceException): def __str__(self):
"""Exception for trying to connect to unsupported devices.""" err_code = f" (error_code={self.error_code.name})" if self.error_code else ""
return super().__str__() + err_code
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.discovery_result = kwargs.get("discovery_result")
super().__init__(*args, **kwargs)
class AuthenticationException(SmartDeviceException): class AuthenticationError(DeviceError):
"""Base exception for device authentication errors.""" """Base exception for device authentication errors."""
class RetryableException(SmartDeviceException): class _RetryableError(DeviceError):
"""Retryable exception for device errors.""" """Retryable exception for device errors."""
class TimeoutException(SmartDeviceException, TimeoutError):
"""Timeout exception for device errors."""
def __repr__(self):
return SmartDeviceException.__repr__(self)
def __str__(self):
return SmartDeviceException.__str__(self)
class ConnectionException(SmartDeviceException):
"""Connection exception for device errors."""
class SmartErrorCode(IntEnum): class SmartErrorCode(IntEnum):
"""Enum for SMART Error Codes.""" """Enum for SMART Error Codes."""
@ -109,6 +121,7 @@ SMART_RETRYABLE_ERRORS = [
SmartErrorCode.TRANSPORT_NOT_AVAILABLE_ERROR, SmartErrorCode.TRANSPORT_NOT_AVAILABLE_ERROR,
SmartErrorCode.HTTP_TRANSPORT_FAILED_ERROR, SmartErrorCode.HTTP_TRANSPORT_FAILED_ERROR,
SmartErrorCode.UNSPECIFIC_ERROR, SmartErrorCode.UNSPECIFIC_ERROR,
SmartErrorCode.SESSION_TIMEOUT_ERROR,
] ]
SMART_AUTHENTICATION_ERRORS = [ SMART_AUTHENTICATION_ERRORS = [
@ -118,7 +131,3 @@ SMART_AUTHENTICATION_ERRORS = [
SmartErrorCode.HAND_SHAKE_FAILED_ERROR, SmartErrorCode.HAND_SHAKE_FAILED_ERROR,
SmartErrorCode.TRANSPORT_UNKNOWN_CREDENTIALS_ERROR, SmartErrorCode.TRANSPORT_UNKNOWN_CREDENTIALS_ERROR,
] ]
SMART_TIMEOUT_ERRORS = [
SmartErrorCode.SESSION_TIMEOUT_ERROR,
]

View File

@ -8,9 +8,9 @@ from yarl import URL
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import ( from .exceptions import (
ConnectionException, KasaException,
SmartDeviceException, TimeoutError,
TimeoutException, _ConnectionError,
) )
from .json import loads as json_loads from .json import loads as json_loads
@ -86,17 +86,17 @@ class HttpClient:
response_data = json_loads(response_data.decode()) response_data = json_loads(response_data.decode())
except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex: except (aiohttp.ServerDisconnectedError, aiohttp.ClientOSError) as ex:
raise ConnectionException( raise _ConnectionError(
f"Device connection error: {self._config.host}: {ex}", ex f"Device connection error: {self._config.host}: {ex}", ex
) from ex ) from ex
except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as ex: except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as ex:
raise TimeoutException( raise TimeoutError(
"Unable to query the device, " "Unable to query the device, "
+ f"timed out: {self._config.host}: {ex}", + f"timed out: {self._config.host}: {ex}",
ex, ex,
) from ex ) from ex
except Exception as ex: except Exception as ex:
raise SmartDeviceException( raise KasaException(
f"Unable to query the device: {self._config.host}: {ex}", ex f"Unable to query the device: {self._config.host}: {ex}", ex
) from ex ) from ex

View File

@ -13,7 +13,7 @@ from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import IotDevice, SmartDeviceException, requires_update from .iotdevice import IotDevice, KasaException, requires_update
from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage
@ -97,7 +97,7 @@ class IotBulb(IotDevice, Bulb):
so you must await :func:`update()` to fetch updates values from the device. so you must await :func:`update()` to fetch updates values from the device.
Errors reported by the device are raised as Errors reported by the device are raised as
:class:`SmartDeviceExceptions <kasa.exceptions.SmartDeviceException>`, :class:`KasaException <kasa.exceptions.KasaException>`,
and should be handled by the user of the library. and should be handled by the user of the library.
Examples: Examples:
@ -233,7 +233,7 @@ class IotBulb(IotDevice, Bulb):
:return: White temperature range in Kelvin (minimum, maximum) :return: White temperature range in Kelvin (minimum, maximum)
""" """
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Color temperature not supported") raise KasaException("Color temperature not supported")
for model, temp_range in TPLINK_KELVIN.items(): for model, temp_range in TPLINK_KELVIN.items():
sys_info = self.sys_info sys_info = self.sys_info
@ -249,7 +249,7 @@ class IotBulb(IotDevice, Bulb):
"""Query the light state.""" """Query the light state."""
light_state = self.sys_info["light_state"] light_state = self.sys_info["light_state"]
if light_state is None: if light_state is None:
raise SmartDeviceException( raise KasaException(
"The device has no light_state or you have not called update()" "The device has no light_state or you have not called update()"
) )
@ -333,7 +333,7 @@ class IotBulb(IotDevice, Bulb):
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
""" """
if not self.is_color: if not self.is_color:
raise SmartDeviceException("Bulb does not support color.") raise KasaException("Bulb does not support color.")
light_state = cast(dict, self.light_state) light_state = cast(dict, self.light_state)
@ -360,7 +360,7 @@ class IotBulb(IotDevice, Bulb):
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
if not self.is_color: if not self.is_color:
raise SmartDeviceException("Bulb does not support color.") raise KasaException("Bulb does not support color.")
if not isinstance(hue, int) or not (0 <= hue <= 360): if not isinstance(hue, int) or not (0 <= hue <= 360):
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)") raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
@ -387,7 +387,7 @@ class IotBulb(IotDevice, Bulb):
def color_temp(self) -> int: def color_temp(self) -> int:
"""Return color temperature of the device in kelvin.""" """Return color temperature of the device in kelvin."""
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.") raise KasaException("Bulb does not support colortemp.")
light_state = self.light_state light_state = self.light_state
return int(light_state["color_temp"]) return int(light_state["color_temp"])
@ -402,7 +402,7 @@ class IotBulb(IotDevice, Bulb):
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.") raise KasaException("Bulb does not support colortemp.")
valid_temperature_range = self.valid_temperature_range valid_temperature_range = self.valid_temperature_range
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]: if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
@ -423,7 +423,7 @@ class IotBulb(IotDevice, Bulb):
def brightness(self) -> int: def brightness(self) -> int:
"""Return the current brightness in percentage.""" """Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.") raise KasaException("Bulb is not dimmable.")
light_state = self.light_state light_state = self.light_state
return int(light_state["brightness"]) return int(light_state["brightness"])
@ -438,7 +438,7 @@ class IotBulb(IotDevice, Bulb):
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
if not self.is_dimmable: # pragma: no cover if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.") raise KasaException("Bulb is not dimmable.")
self._raise_for_invalid_brightness(brightness) self._raise_for_invalid_brightness(brightness)
@ -511,10 +511,10 @@ class IotBulb(IotDevice, Bulb):
obtained using :func:`presets`. obtained using :func:`presets`.
""" """
if len(self.presets) == 0: if len(self.presets) == 0:
raise SmartDeviceException("Device does not supported saving presets") raise KasaException("Device does not supported saving presets")
if preset.index >= len(self.presets): if preset.index >= len(self.presets):
raise SmartDeviceException("Invalid preset index") raise KasaException("Invalid preset index")
return await self._query_helper( return await self._query_helper(
self.LIGHT_SERVICE, "set_preferred_state", preset.dict(exclude_none=True) self.LIGHT_SERVICE, "set_preferred_state", preset.dict(exclude_none=True)

View File

@ -21,7 +21,7 @@ from typing import Any, Dict, List, Optional, Sequence, Set
from ..device import Device, WifiNetwork from ..device import Device, WifiNetwork
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..emeterstatus import EmeterStatus from ..emeterstatus import EmeterStatus
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..feature import Feature from ..feature import Feature
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotmodule import IotModule from .iotmodule import IotModule
@ -48,9 +48,7 @@ def requires_update(f):
async def wrapped(*args, **kwargs): async def wrapped(*args, **kwargs):
self = args[0] self = args[0]
if self._last_update is None and f.__name__ not in self._sys_info: if self._last_update is None and f.__name__ not in self._sys_info:
raise SmartDeviceException( raise KasaException("You need to await update() to access the data")
"You need to await update() to access the data"
)
return await f(*args, **kwargs) return await f(*args, **kwargs)
else: else:
@ -59,9 +57,7 @@ def requires_update(f):
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
self = args[0] self = args[0]
if self._last_update is None and f.__name__ not in self._sys_info: if self._last_update is None and f.__name__ not in self._sys_info:
raise SmartDeviceException( raise KasaException("You need to await update() to access the data")
"You need to await update() to access the data"
)
return f(*args, **kwargs) return f(*args, **kwargs)
f.requires_update = True f.requires_update = True
@ -92,7 +88,8 @@ class IotDevice(Device):
All changes to the device are done using awaitable methods, All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await update() separately. which will not change the cached values, but you must await update() separately.
Errors reported by the device are raised as SmartDeviceExceptions, Errors reported by the device are raised as
:class:`KasaException <kasa.exceptions.KasaException>`,
and should be handled by the user of the library. and should be handled by the user of the library.
Examples: Examples:
@ -221,9 +218,9 @@ class IotDevice(Device):
def _verify_emeter(self) -> None: def _verify_emeter(self) -> None:
"""Raise an exception if there is no emeter.""" """Raise an exception if there is no emeter."""
if not self.has_emeter: if not self.has_emeter:
raise SmartDeviceException("Device has no emeter") raise KasaException("Device has no emeter")
if self.emeter_type not in self._last_update: if self.emeter_type not in self._last_update:
raise SmartDeviceException("update() required prior accessing emeter") raise KasaException("update() required prior accessing emeter")
async def _query_helper( async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
@ -241,20 +238,20 @@ class IotDevice(Device):
try: try:
response = await self._raw_query(request=request) response = await self._raw_query(request=request)
except Exception as ex: except Exception as ex:
raise SmartDeviceException(f"Communication error on {target}:{cmd}") from ex raise KasaException(f"Communication error on {target}:{cmd}") from ex
if target not in response: if target not in response:
raise SmartDeviceException(f"No required {target} in response: {response}") raise KasaException(f"No required {target} in response: {response}")
result = response[target] result = response[target]
if "err_code" in result and result["err_code"] != 0: if "err_code" in result and result["err_code"] != 0:
raise SmartDeviceException(f"Error on {target}.{cmd}: {result}") raise KasaException(f"Error on {target}.{cmd}: {result}")
if cmd not in result: if cmd not in result:
raise SmartDeviceException(f"No command in response: {response}") raise KasaException(f"No command in response: {response}")
result = result[cmd] result = result[cmd]
if "err_code" in result and result["err_code"] != 0: if "err_code" in result and result["err_code"] != 0:
raise SmartDeviceException(f"Error on {target} {cmd}: {result}") raise KasaException(f"Error on {target} {cmd}: {result}")
if "err_code" in result: if "err_code" in result:
del result["err_code"] del result["err_code"]
@ -513,7 +510,7 @@ class IotDevice(Device):
sys_info = self._sys_info sys_info = self._sys_info
mac = sys_info.get("mac", sys_info.get("mic_mac")) mac = sys_info.get("mac", sys_info.get("mic_mac"))
if not mac: if not mac:
raise SmartDeviceException( raise KasaException(
"Unknown mac, please submit a bug report with sys_info output." "Unknown mac, please submit a bug report with sys_info output."
) )
mac = mac.replace("-", ":") mac = mac.replace("-", ":")
@ -656,14 +653,14 @@ class IotDevice(Device):
try: try:
info = await _scan("netif") info = await _scan("netif")
except SmartDeviceException as ex: except KasaException as ex:
_LOGGER.debug( _LOGGER.debug(
"Unable to scan using 'netif', retrying with 'softaponboarding': %s", ex "Unable to scan using 'netif', retrying with 'softaponboarding': %s", ex
) )
info = await _scan("smartlife.iot.common.softaponboarding") info = await _scan("smartlife.iot.common.softaponboarding")
if "ap_list" not in info: if "ap_list" not in info:
raise SmartDeviceException("Invalid response for wifi scan: %s" % info) raise KasaException("Invalid response for wifi scan: %s" % info)
return [WifiNetwork(**x) for x in info["ap_list"]] return [WifiNetwork(**x) for x in info["ap_list"]]
@ -679,7 +676,7 @@ class IotDevice(Device):
payload = {"ssid": ssid, "password": password, "key_type": int(keytype)} payload = {"ssid": ssid, "password": password, "key_type": int(keytype)}
try: try:
return await _join("netif", payload) return await _join("netif", payload)
except SmartDeviceException as ex: except KasaException as ex:
_LOGGER.debug( _LOGGER.debug(
"Unable to join using 'netif', retrying with 'softaponboarding': %s", ex "Unable to join using 'netif', retrying with 'softaponboarding': %s", ex
) )

View File

@ -5,7 +5,7 @@ from typing import Any, Dict, Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import SmartDeviceException, requires_update from .iotdevice import KasaException, requires_update
from .iotplug import IotPlug from .iotplug import IotPlug
from .modules import AmbientLight, Motion from .modules import AmbientLight, Motion
@ -46,7 +46,7 @@ class IotDimmer(IotPlug):
which will not change the cached values, which will not change the cached values,
but you must await :func:`update()` separately. but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s, Errors reported by the device are raised as :class:`KasaException`\s,
and should be handled by the user of the library. and should be handled by the user of the library.
Examples: Examples:
@ -88,7 +88,7 @@ class IotDimmer(IotPlug):
Will return a range between 0 - 100. Will return a range between 0 - 100.
""" """
if not self.is_dimmable: if not self.is_dimmable:
raise SmartDeviceException("Device is not dimmable.") raise KasaException("Device is not dimmable.")
sys_info = self.sys_info sys_info = self.sys_info
return int(sys_info["brightness"]) return int(sys_info["brightness"])
@ -103,7 +103,7 @@ class IotDimmer(IotPlug):
Using a transition will cause the dimmer to turn on. Using a transition will cause the dimmer to turn on.
""" """
if not self.is_dimmable: if not self.is_dimmable:
raise SmartDeviceException("Device is not dimmable.") raise KasaException("Device is not dimmable.")
if not isinstance(brightness, int): if not isinstance(brightness, int):
raise ValueError( raise ValueError(

View File

@ -6,7 +6,7 @@ from ..deviceconfig import DeviceConfig
from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1 from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotbulb import IotBulb from .iotbulb import IotBulb
from .iotdevice import SmartDeviceException, requires_update from .iotdevice import KasaException, requires_update
class IotLightStrip(IotBulb): class IotLightStrip(IotBulb):
@ -117,7 +117,7 @@ class IotLightStrip(IotBulb):
:param int transition: The wanted transition time :param int transition: The wanted transition time
""" """
if effect not in EFFECT_MAPPING_V1: if effect not in EFFECT_MAPPING_V1:
raise SmartDeviceException(f"The effect {effect} is not a built in effect.") raise KasaException(f"The effect {effect} is not a built in effect.")
effect_dict = EFFECT_MAPPING_V1[effect] effect_dict = EFFECT_MAPPING_V1[effect]
if brightness is not None: if brightness is not None:
effect_dict["brightness"] = brightness effect_dict["brightness"] = brightness
@ -136,7 +136,7 @@ class IotLightStrip(IotBulb):
:param str effect_dict: The custom effect dict to set :param str effect_dict: The custom effect dict to set
""" """
if not self.has_effects: if not self.has_effects:
raise SmartDeviceException("Bulb does not support effects.") raise KasaException("Bulb does not support effects.")
await self._query_helper( await self._query_helper(
"smartlife.iot.lighting_effect", "smartlife.iot.lighting_effect",
"set_lighting_effect", "set_lighting_effect",

View File

@ -2,7 +2,7 @@
import collections import collections
import logging import logging
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..module import Module from ..module import Module
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,7 +43,7 @@ class IotModule(Module):
def data(self): def data(self):
"""Return the module specific raw data from the last update.""" """Return the module specific raw data from the last update."""
if self._module not in self._device._last_update: if self._module not in self._device._last_update:
raise SmartDeviceException( raise KasaException(
f"You need to call update() prior accessing module data" f"You need to call update() prior accessing module data"
f" for '{self._module}'" f" for '{self._module}'"
) )

View File

@ -22,7 +22,7 @@ class IotPlug(IotDevice):
which will not change the cached values, which will not change the cached values,
but you must await :func:`update()` separately. but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s, Errors reported by the device are raised as :class:`KasaException`\s,
and should be handled by the user of the library. and should be handled by the user of the library.
Examples: Examples:

View File

@ -6,7 +6,7 @@ from typing import Any, DefaultDict, Dict, Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import ( from .iotdevice import (
EmeterStatus, EmeterStatus,
@ -43,7 +43,7 @@ class IotStrip(IotDevice):
which will not change the cached values, which will not change the cached values,
but you must await :func:`update()` separately. but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s, Errors reported by the device are raised as :class:`KasaException`\s,
and should be handled by the user of the library. and should be handled by the user of the library.
Examples: Examples:
@ -375,4 +375,4 @@ class IotStripPlug(IotPlug):
if plug["id"] == self.child_id: if plug["id"] == self.child_id:
return plug return plug
raise SmartDeviceException(f"Unable to find children {self.child_id}") raise KasaException(f"Unable to find children {self.child_id}")

View File

@ -2,7 +2,7 @@
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from ...exceptions import SmartDeviceException from ...exceptions import KasaException
from ..iotmodule import IotModule from ..iotmodule import IotModule
@ -54,9 +54,7 @@ class Motion(IotModule):
elif range is not None: elif range is not None:
payload = {"index": range.value} payload = {"index": range.value}
else: else:
raise SmartDeviceException( raise KasaException("Either range or custom_range need to be defined")
"Either range or custom_range need to be defined"
)
return await self.call("set_trigger_sens", payload) return await self.call("set_trigger_sens", payload)

View File

@ -1,7 +1,7 @@
"""Provides the current time and timezone information.""" """Provides the current time and timezone information."""
from datetime import datetime from datetime import datetime
from ...exceptions import SmartDeviceException from ...exceptions import KasaException
from ..iotmodule import IotModule, merge from ..iotmodule import IotModule, merge
@ -46,7 +46,7 @@ class Time(IotModule):
res["min"], res["min"],
res["sec"], res["sec"],
) )
except SmartDeviceException: except KasaException:
return None return None
async def get_timezone(self): async def get_timezone(self):

View File

@ -5,11 +5,11 @@ from typing import Dict, Optional, Union
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import ( from .exceptions import (
AuthenticationException, AuthenticationError,
ConnectionException, KasaException,
RetryableException, TimeoutError,
SmartDeviceException, _ConnectionError,
TimeoutException, _RetryableError,
) )
from .json import dumps as json_dumps from .json import dumps as json_dumps
from .protocol import BaseProtocol, BaseTransport from .protocol import BaseProtocol, BaseTransport
@ -46,31 +46,31 @@ class IotProtocol(BaseProtocol):
for retry in range(retry_count + 1): for retry in range(retry_count + 1):
try: try:
return await self._execute_query(request, retry) return await self._execute_query(request, retry)
except ConnectionException as sdex: except _ConnectionError as sdex:
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise sdex raise sdex
continue continue
except AuthenticationException as auex: except AuthenticationError as auex:
await self._transport.reset() await self._transport.reset()
_LOGGER.debug( _LOGGER.debug(
"Unable to authenticate with %s, not retrying", self._host "Unable to authenticate with %s, not retrying", self._host
) )
raise auex raise auex
except RetryableException as ex: except _RetryableError as ex:
await self._transport.reset() await self._transport.reset()
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise ex raise ex
continue continue
except TimeoutException as ex: except TimeoutError as ex:
await self._transport.reset() await self._transport.reset()
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise ex raise ex
await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT) await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT)
continue continue
except SmartDeviceException as ex: except KasaException as ex:
await self._transport.reset() await self._transport.reset()
_LOGGER.debug( _LOGGER.debug(
"Unable to query the device: %s, not retrying: %s", "Unable to query the device: %s, not retrying: %s",
@ -80,7 +80,7 @@ class IotProtocol(BaseProtocol):
raise ex raise ex
# make mypy happy, this should never be reached.. # make mypy happy, this should never be reached..
raise SmartDeviceException("Query reached somehow to unreachable") raise KasaException("Query reached somehow to unreachable")
async def _execute_query(self, request: str, retry_count: int) -> Dict: async def _execute_query(self, request: str, retry_count: int) -> Dict:
return await self._transport.send(request) return await self._transport.send(request)
@ -101,7 +101,7 @@ class _deprecated_TPLinkSmartHomeProtocol(IotProtocol):
) -> None: ) -> None:
"""Create a protocol object.""" """Create a protocol object."""
if not host and not transport: if not host and not transport:
raise SmartDeviceException("host or transport must be supplied") raise KasaException("host or transport must be supplied")
if not transport: if not transport:
config = DeviceConfig( config = DeviceConfig(
host=host, # type: ignore[arg-type] host=host, # type: ignore[arg-type]

View File

@ -57,7 +57,7 @@ from yarl import URL
from .credentials import Credentials from .credentials import Credentials
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import AuthenticationException, SmartDeviceException from .exceptions import AuthenticationError, KasaException
from .httpclient import HttpClient from .httpclient import HttpClient
from .json import loads as json_loads from .json import loads as json_loads
from .protocol import DEFAULT_CREDENTIALS, BaseTransport, get_default_credentials, md5 from .protocol import DEFAULT_CREDENTIALS, BaseTransport, get_default_credentials, md5
@ -159,7 +159,7 @@ class KlapTransport(BaseTransport):
) )
if response_status != 200: if response_status != 200:
raise SmartDeviceException( raise KasaException(
f"Device {self._host} responded with {response_status} to handshake1" f"Device {self._host} responded with {response_status} to handshake1"
) )
@ -168,7 +168,7 @@ class KlapTransport(BaseTransport):
server_hash = response_data[16:] server_hash = response_data[16:]
if len(server_hash) != 32: if len(server_hash) != 32:
raise SmartDeviceException( raise KasaException(
f"Device {self._host} responded with unexpected klap response " f"Device {self._host} responded with unexpected klap response "
+ f"{response_data!r} to handshake1" + f"{response_data!r} to handshake1"
) )
@ -236,7 +236,7 @@ class KlapTransport(BaseTransport):
msg = f"Server response doesn't match our challenge on ip {self._host}" msg = f"Server response doesn't match our challenge on ip {self._host}"
_LOGGER.debug(msg) _LOGGER.debug(msg)
raise AuthenticationException(msg) raise AuthenticationError(msg)
async def perform_handshake2( async def perform_handshake2(
self, local_seed, remote_seed, auth_hash self, local_seed, remote_seed, auth_hash
@ -267,8 +267,8 @@ class KlapTransport(BaseTransport):
if response_status != 200: if response_status != 200:
# This shouldn't be caused by incorrect # This shouldn't be caused by incorrect
# credentials so don't raise AuthenticationException # credentials so don't raise AuthenticationError
raise SmartDeviceException( raise KasaException(
f"Device {self._host} responded with {response_status} to handshake2" f"Device {self._host} responded with {response_status} to handshake2"
) )
@ -337,12 +337,12 @@ class KlapTransport(BaseTransport):
# If we failed with a security error, force a new handshake next time. # If we failed with a security error, force a new handshake next time.
if response_status == 403: if response_status == 403:
self._handshake_done = False self._handshake_done = False
raise AuthenticationException( raise AuthenticationError(
f"Got a security error from {self._host} after handshake " f"Got a security error from {self._host} after handshake "
+ "completed" + "completed"
) )
else: else:
raise SmartDeviceException( raise KasaException(
f"Device {self._host} responded with {response_status} to" f"Device {self._host} responded with {response_status} to"
+ f"request with seq {seq}" + f"request with seq {seq}"
) )

View File

@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from typing import Dict from typing import Dict
from .device import Device from .device import Device
from .exceptions import SmartDeviceException from .exceptions import KasaException
from .feature import Feature from .feature import Feature
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,7 +39,7 @@ class Module(ABC):
"""Add module feature.""" """Add module feature."""
feat_name = f"{self._module}_{feature.name}" feat_name = f"{self._module}_{feature.name}"
if feat_name in self._module_features: if feat_name in self._module_features:
raise SmartDeviceException("Duplicate name detected %s" % feat_name) raise KasaException("Duplicate name detected %s" % feat_name)
self._module_features[feat_name] = feature self._module_features[feat_name] = feature
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional
from ..bulb import Bulb from ..bulb import Bulb
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..iot.iotbulb import HSV, BulbPreset, ColorTempRange from ..iot.iotbulb import HSV, BulbPreset, ColorTempRange
from ..smartprotocol import SmartProtocol from ..smartprotocol import SmartProtocol
from .smartdevice import SmartDevice from .smartdevice import SmartDevice
@ -57,7 +57,7 @@ class SmartBulb(SmartDevice, Bulb):
:return: White temperature range in Kelvin (minimum, maximum) :return: White temperature range in Kelvin (minimum, maximum)
""" """
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Color temperature not supported") raise KasaException("Color temperature not supported")
ct_range = self._info.get("color_temp_range", [0, 0]) ct_range = self._info.get("color_temp_range", [0, 0])
return ColorTempRange(min=ct_range[0], max=ct_range[1]) return ColorTempRange(min=ct_range[0], max=ct_range[1])
@ -107,7 +107,7 @@ class SmartBulb(SmartDevice, Bulb):
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
""" """
if not self.is_color: if not self.is_color:
raise SmartDeviceException("Bulb does not support color.") raise KasaException("Bulb does not support color.")
h, s, v = ( h, s, v = (
self._info.get("hue", 0), self._info.get("hue", 0),
@ -121,7 +121,7 @@ class SmartBulb(SmartDevice, Bulb):
def color_temp(self) -> int: def color_temp(self) -> int:
"""Whether the bulb supports color temperature changes.""" """Whether the bulb supports color temperature changes."""
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.") raise KasaException("Bulb does not support colortemp.")
return self._info.get("color_temp", -1) return self._info.get("color_temp", -1)
@ -129,7 +129,7 @@ class SmartBulb(SmartDevice, Bulb):
def brightness(self) -> int: def brightness(self) -> int:
"""Return the current brightness in percentage.""" """Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.") raise KasaException("Bulb is not dimmable.")
return self._info.get("brightness", -1) return self._info.get("brightness", -1)
@ -151,7 +151,7 @@ class SmartBulb(SmartDevice, Bulb):
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
if not self.is_color: if not self.is_color:
raise SmartDeviceException("Bulb does not support color.") raise KasaException("Bulb does not support color.")
if not isinstance(hue, int) or not (0 <= hue <= 360): if not isinstance(hue, int) or not (0 <= hue <= 360):
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)") raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
@ -188,7 +188,7 @@ class SmartBulb(SmartDevice, Bulb):
# TODO: Note, trying to set brightness at the same time # TODO: Note, trying to set brightness at the same time
# with color_temp causes error -1008 # with color_temp causes error -1008
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.") raise KasaException("Bulb does not support colortemp.")
valid_temperature_range = self.valid_temperature_range valid_temperature_range = self.valid_temperature_range
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]: if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
@ -211,7 +211,7 @@ class SmartBulb(SmartDevice, Bulb):
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
if not self.is_dimmable: # pragma: no cover if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.") raise KasaException("Bulb is not dimmable.")
return await self.protocol.query( return await self.protocol.query(
{"set_device_info": {"brightness": brightness}} {"set_device_info": {"brightness": brightness}}

View File

@ -9,7 +9,7 @@ from ..device import Device, WifiNetwork
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..emeterstatus import EmeterStatus from ..emeterstatus import EmeterStatus
from ..exceptions import AuthenticationException, SmartDeviceException, SmartErrorCode from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
from ..feature import Feature, FeatureType from ..feature import Feature, FeatureType
from ..smartprotocol import SmartProtocol from ..smartprotocol import SmartProtocol
from .modules import ( # noqa: F401 from .modules import ( # noqa: F401
@ -85,7 +85,7 @@ class SmartDevice(Device):
return response return response
if default is not None: if default is not None:
return default return default
raise SmartDeviceException( raise KasaException(
f"{request} not found in {responses} for device {self.host}" f"{request} not found in {responses} for device {self.host}"
) )
@ -100,7 +100,7 @@ class SmartDevice(Device):
async def update(self, update_children: bool = True): async def update(self, update_children: bool = True):
"""Update the device.""" """Update the device."""
if self.credentials is None and self.credentials_hash is None: if self.credentials is None and self.credentials_hash is None:
raise AuthenticationException("Tapo plug requires authentication.") raise AuthenticationError("Tapo plug requires authentication.")
if self._components_raw is None: if self._components_raw is None:
await self._negotiate() await self._negotiate()
@ -341,7 +341,7 @@ class SmartDevice(Device):
"""Retrieve current energy readings.""" """Retrieve current energy readings."""
_LOGGER.warning("Deprecated, use `emeter_realtime`.") _LOGGER.warning("Deprecated, use `emeter_realtime`.")
if not self.has_emeter: if not self.has_emeter:
raise SmartDeviceException("Device has no emeter") raise KasaException("Device has no emeter")
return self.emeter_realtime return self.emeter_realtime
@property @property
@ -421,7 +421,7 @@ class SmartDevice(Device):
after some delay. after some delay.
""" """
if not self.credentials: if not self.credentials:
raise AuthenticationException("Device requires authentication.") raise AuthenticationError("Device requires authentication.")
payload = { payload = {
"account": { "account": {
@ -445,10 +445,9 @@ class SmartDevice(Device):
# Thus, We limit retries and suppress the raised exception as useless. # Thus, We limit retries and suppress the raised exception as useless.
try: try:
return await self.protocol.query({"set_qs_info": payload}, retry_count=0) return await self.protocol.query({"set_qs_info": payload}, retry_count=0)
except SmartDeviceException as ex: except DeviceError:
if ex.error_code: # Re-raise on device-reported errors raise # Re-raise on device-reported errors
raise except KasaException:
_LOGGER.debug("Received an expected for wifi join, but this is expected") _LOGGER.debug("Received an expected for wifi join, but this is expected")
async def update_credentials(self, username: str, password: str): async def update_credentials(self, username: str, password: str):

View File

@ -2,7 +2,7 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, Type from typing import TYPE_CHECKING, Dict, Type
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..module import Module from ..module import Module
if TYPE_CHECKING: if TYPE_CHECKING:
@ -59,7 +59,7 @@ class SmartModule(Module):
q_keys = list(q.keys()) q_keys = list(q.keys())
# TODO: hacky way to check if update has been called. # TODO: hacky way to check if update has been called.
if q_keys[0] not in self._device._last_update: if q_keys[0] not in self._device._last_update:
raise SmartDeviceException( raise KasaException(
f"You need to call update() prior accessing module data" f"You need to call update() prior accessing module data"
f" for '{self._module}'" f" for '{self._module}'"
) )

View File

@ -15,13 +15,13 @@ from typing import Any, Dict, Union
from .exceptions import ( from .exceptions import (
SMART_AUTHENTICATION_ERRORS, SMART_AUTHENTICATION_ERRORS,
SMART_RETRYABLE_ERRORS, SMART_RETRYABLE_ERRORS,
SMART_TIMEOUT_ERRORS, AuthenticationError,
AuthenticationException, DeviceError,
ConnectionException, KasaException,
RetryableException,
SmartDeviceException,
SmartErrorCode, SmartErrorCode,
TimeoutException, TimeoutError,
_ConnectionError,
_RetryableError,
) )
from .json import dumps as json_dumps from .json import dumps as json_dumps
from .protocol import BaseProtocol, BaseTransport, md5 from .protocol import BaseProtocol, BaseTransport, md5
@ -66,32 +66,32 @@ class SmartProtocol(BaseProtocol):
for retry in range(retry_count + 1): for retry in range(retry_count + 1):
try: try:
return await self._execute_query(request, retry) return await self._execute_query(request, retry)
except ConnectionException as sdex: except _ConnectionError as sdex:
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise sdex raise sdex
continue continue
except AuthenticationException as auex: except AuthenticationError as auex:
await self._transport.reset() await self._transport.reset()
_LOGGER.debug( _LOGGER.debug(
"Unable to authenticate with %s, not retrying", self._host "Unable to authenticate with %s, not retrying", self._host
) )
raise auex raise auex
except RetryableException as ex: except _RetryableError as ex:
await self._transport.reset() await self._transport.reset()
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise ex raise ex
await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT) await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT)
continue continue
except TimeoutException as ex: except TimeoutError as ex:
await self._transport.reset() await self._transport.reset()
if retry >= retry_count: if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self._host, retry) _LOGGER.debug("Giving up on %s after %s retries", self._host, retry)
raise ex raise ex
await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT) await asyncio.sleep(self.BACKOFF_SECONDS_AFTER_TIMEOUT)
continue continue
except SmartDeviceException as ex: except KasaException as ex:
await self._transport.reset() await self._transport.reset()
_LOGGER.debug( _LOGGER.debug(
"Unable to query the device: %s, not retrying: %s", "Unable to query the device: %s, not retrying: %s",
@ -101,7 +101,7 @@ class SmartProtocol(BaseProtocol):
raise ex raise ex
# make mypy happy, this should never be reached.. # make mypy happy, this should never be reached..
raise SmartDeviceException("Query reached somehow to unreachable") raise KasaException("Query reached somehow to unreachable")
async def _execute_multiple_query(self, request: Dict, retry_count: int) -> Dict: async def _execute_multiple_query(self, request: Dict, retry_count: int) -> Dict:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
@ -193,13 +193,11 @@ class SmartProtocol(BaseProtocol):
+ f"{error_code.name}({error_code.value})" + f"{error_code.name}({error_code.value})"
+ f" for method: {method}" + f" for method: {method}"
) )
if error_code in SMART_TIMEOUT_ERRORS:
raise TimeoutException(msg, error_code=error_code)
if error_code in SMART_RETRYABLE_ERRORS: if error_code in SMART_RETRYABLE_ERRORS:
raise RetryableException(msg, error_code=error_code) raise _RetryableError(msg, error_code=error_code)
if error_code in SMART_AUTHENTICATION_ERRORS: if error_code in SMART_AUTHENTICATION_ERRORS:
raise AuthenticationException(msg, error_code=error_code) raise AuthenticationError(msg, error_code=error_code)
raise SmartDeviceException(msg, error_code=error_code) raise DeviceError(msg, error_code=error_code)
async def close(self) -> None: async def close(self) -> None:
"""Close the underlying transport.""" """Close the underlying transport."""

View File

@ -1,7 +1,7 @@
import warnings import warnings
from json import loads as json_loads from json import loads as json_loads
from kasa import Credentials, DeviceConfig, SmartDeviceException, SmartProtocol from kasa import Credentials, DeviceConfig, KasaException, SmartProtocol
from kasa.protocol import BaseTransport from kasa.protocol import BaseTransport
@ -144,7 +144,7 @@ class FakeSmartTransport(BaseTransport):
) )
return {"result": missing_result[1], "error_code": 0} return {"result": missing_result[1], "error_code": 0}
else: else:
raise SmartDeviceException(f"Fixture doesn't support {method}") raise KasaException(f"Fixture doesn't support {method}")
elif method == "set_qs_info": elif method == "set_qs_info":
return {"error_code": 0} return {"error_code": 0}
elif method[:4] == "set_": elif method[:4] == "set_":

View File

@ -19,8 +19,8 @@ from ..aestransport import AesEncyptionSession, AesTransport, TransportState
from ..credentials import Credentials from ..credentials import Credentials
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import ( from ..exceptions import (
AuthenticationException, AuthenticationError,
SmartDeviceException, KasaException,
SmartErrorCode, SmartErrorCode,
) )
from ..httpclient import HttpClient from ..httpclient import HttpClient
@ -49,8 +49,8 @@ status_parameters = pytest.mark.parametrize(
"status_code, error_code, inner_error_code, expectation", "status_code, error_code, inner_error_code, expectation",
[ [
(200, 0, 0, does_not_raise()), (200, 0, 0, does_not_raise()),
(400, 0, 0, pytest.raises(SmartDeviceException)), (400, 0, 0, pytest.raises(KasaException)),
(200, -1, 0, pytest.raises(SmartDeviceException)), (200, -1, 0, pytest.raises(KasaException)),
], ],
ids=("success", "status_code", "error_code"), ids=("success", "status_code", "error_code"),
) )
@ -101,17 +101,17 @@ async def test_login(mocker, status_code, error_code, inner_error_code, expectat
([SmartErrorCode.LOGIN_ERROR, 0, 0, 0], does_not_raise(), 4), ([SmartErrorCode.LOGIN_ERROR, 0, 0, 0], does_not_raise(), 4),
( (
[SmartErrorCode.LOGIN_ERROR, SmartErrorCode.LOGIN_ERROR], [SmartErrorCode.LOGIN_ERROR, SmartErrorCode.LOGIN_ERROR],
pytest.raises(AuthenticationException), pytest.raises(AuthenticationError),
3, 3,
), ),
( (
[SmartErrorCode.LOGIN_FAILED_ERROR], [SmartErrorCode.LOGIN_FAILED_ERROR],
pytest.raises(AuthenticationException), pytest.raises(AuthenticationError),
1, 1,
), ),
( (
[SmartErrorCode.LOGIN_ERROR, SmartErrorCode.SESSION_TIMEOUT_ERROR], [SmartErrorCode.LOGIN_ERROR, SmartErrorCode.SESSION_TIMEOUT_ERROR],
pytest.raises(SmartDeviceException), pytest.raises(KasaException),
3, 3,
), ),
], ],
@ -238,7 +238,7 @@ async def test_unencrypted_response_invalid_json(mocker, caplog):
} }
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
msg = f"Unable to decrypt response from {host}, error: Incorrect padding, response: Foobar" msg = f"Unable to decrypt response from {host}, error: Incorrect padding, response: Foobar"
with pytest.raises(SmartDeviceException, match=msg): with pytest.raises(KasaException, match=msg):
await transport.send(json_dumps(request)) await transport.send(json_dumps(request))
@ -267,7 +267,7 @@ async def test_passthrough_errors(mocker, error_code):
"requestID": 1, "requestID": 1,
"terminal_uuid": "foobar", "terminal_uuid": "foobar",
} }
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await transport.send(json_dumps(request)) await transport.send(json_dumps(request))

View File

@ -7,7 +7,7 @@ from voluptuous import (
Schema, Schema,
) )
from kasa import Bulb, BulbPreset, DeviceType, SmartDeviceException from kasa import Bulb, BulbPreset, DeviceType, KasaException
from kasa.iot import IotBulb from kasa.iot import IotBulb
from .conftest import ( from .conftest import (
@ -51,7 +51,7 @@ async def test_state_attributes(dev: Bulb):
@bulb_iot @bulb_iot
async def test_light_state_without_update(dev: IotBulb, monkeypatch): async def test_light_state_without_update(dev: IotBulb, monkeypatch):
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
monkeypatch.setitem( monkeypatch.setitem(
dev._last_update["system"]["get_sysinfo"], "light_state", None dev._last_update["system"]["get_sysinfo"], "light_state", None
) )
@ -123,9 +123,9 @@ async def test_color_state_information(dev: Bulb):
async def test_hsv_on_non_color(dev: Bulb): async def test_hsv_on_non_color(dev: Bulb):
assert not dev.is_color assert not dev.is_color
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.set_hsv(0, 0, 0) await dev.set_hsv(0, 0, 0)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
print(dev.hsv) print(dev.hsv)
@ -175,13 +175,13 @@ async def test_out_of_range_temperature(dev: Bulb):
@non_variable_temp @non_variable_temp
async def test_non_variable_temp(dev: Bulb): async def test_non_variable_temp(dev: Bulb):
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.set_color_temp(2700) await dev.set_color_temp(2700)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
print(dev.valid_temperature_range) print(dev.valid_temperature_range)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
print(dev.color_temp) print(dev.color_temp)
@ -238,9 +238,9 @@ async def test_invalid_brightness(dev: Bulb):
async def test_non_dimmable(dev: Bulb): async def test_non_dimmable(dev: Bulb):
assert not dev.is_dimmable assert not dev.is_dimmable
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
assert dev.brightness == 0 assert dev.brightness == 0
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.set_brightness(100) await dev.set_brightness(100)
@ -296,7 +296,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
await dev.save_preset(preset) await dev.save_preset(preset)
assert dev.presets[0].brightness == 10 assert dev.presets[0].brightness == 10
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.save_preset( await dev.save_preset(
BulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) BulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
) )

View File

@ -6,12 +6,13 @@ import pytest
from asyncclick.testing import CliRunner from asyncclick.testing import CliRunner
from kasa import ( from kasa import (
AuthenticationException, AuthenticationError,
Credentials, Credentials,
Device, Device,
DeviceError,
EmeterStatus, EmeterStatus,
SmartDeviceException, KasaException,
UnsupportedDeviceException, UnsupportedDeviceError,
) )
from kasa.cli import ( from kasa.cli import (
TYPE_TO_CLASS, TYPE_TO_CLASS,
@ -188,15 +189,13 @@ async def test_wifi_join_no_creds(dev):
) )
assert res.exit_code != 0 assert res.exit_code != 0
assert isinstance(res.exception, AuthenticationException) assert isinstance(res.exception, AuthenticationError)
@device_smart @device_smart
async def test_wifi_join_exception(dev, mocker): async def test_wifi_join_exception(dev, mocker):
runner = CliRunner() runner = CliRunner()
mocker.patch.object( mocker.patch.object(dev.protocol, "query", side_effect=DeviceError(error_code=9999))
dev.protocol, "query", side_effect=SmartDeviceException(error_code=9999)
)
res = await runner.invoke( res = await runner.invoke(
wifi, wifi,
["join", "FOOBAR", "--keytype", "wpa_psk", "--password", "foobar"], ["join", "FOOBAR", "--keytype", "wpa_psk", "--password", "foobar"],
@ -204,7 +203,7 @@ async def test_wifi_join_exception(dev, mocker):
) )
assert res.exit_code != 0 assert res.exit_code != 0
assert isinstance(res.exception, SmartDeviceException) assert isinstance(res.exception, KasaException)
@device_smart @device_smart
@ -509,7 +508,7 @@ async def test_host_unsupported(unsupported_device_info):
) )
assert res.exit_code != 0 assert res.exit_code != 0
assert isinstance(res.exception, UnsupportedDeviceException) assert isinstance(res.exception, UnsupportedDeviceError)
@new_discovery @new_discovery
@ -522,7 +521,7 @@ async def test_discover_auth_failed(discovery_mock, mocker):
mocker.patch.object( mocker.patch.object(
device_class, device_class,
"update", "update",
side_effect=AuthenticationException("Failed to authenticate"), side_effect=AuthenticationError("Failed to authenticate"),
) )
res = await runner.invoke( res = await runner.invoke(
cli, cli,
@ -553,7 +552,7 @@ async def test_host_auth_failed(discovery_mock, mocker):
mocker.patch.object( mocker.patch.object(
device_class, device_class,
"update", "update",
side_effect=AuthenticationException("Failed to authenticate"), side_effect=AuthenticationError("Failed to authenticate"),
) )
res = await runner.invoke( res = await runner.invoke(
cli, cli,
@ -569,7 +568,7 @@ async def test_host_auth_failed(discovery_mock, mocker):
) )
assert res.exit_code != 0 assert res.exit_code != 0
assert isinstance(res.exception, AuthenticationException) assert isinstance(res.exception, AuthenticationError)
@pytest.mark.parametrize("device_type", list(TYPE_TO_CLASS)) @pytest.mark.parametrize("device_type", list(TYPE_TO_CLASS))
@ -616,7 +615,7 @@ async def test_shell(dev: Device, mocker):
async def test_errors(mocker): async def test_errors(mocker):
runner = CliRunner() runner = CliRunner()
err = SmartDeviceException("Foobar") err = KasaException("Foobar")
# Test masking # Test masking
mocker.patch("kasa.Discover.discover", side_effect=err) mocker.patch("kasa.Discover.discover", side_effect=err)

View File

@ -8,7 +8,7 @@ from kasa import (
Credentials, Credentials,
Device, Device,
Discover, Discover,
SmartDeviceException, KasaException,
) )
from kasa.device_factory import connect, get_protocol from kasa.device_factory import connect, get_protocol
from kasa.deviceconfig import ( from kasa.deviceconfig import (
@ -110,8 +110,8 @@ async def test_connect_logs_connect_time(
async def test_connect_query_fails(all_fixture_data: dict, mocker): async def test_connect_query_fails(all_fixture_data: dict, mocker):
"""Make sure that connect fails when query fails.""" """Make sure that connect fails when query fails."""
host = "127.0.0.1" host = "127.0.0.1"
mocker.patch("kasa.IotProtocol.query", side_effect=SmartDeviceException) mocker.patch("kasa.IotProtocol.query", side_effect=KasaException)
mocker.patch("kasa.SmartProtocol.query", side_effect=SmartDeviceException) mocker.patch("kasa.SmartProtocol.query", side_effect=KasaException)
ctype, _ = _get_connection_type_device_class(all_fixture_data) ctype, _ = _get_connection_type_device_class(all_fixture_data)
config = DeviceConfig( config = DeviceConfig(
@ -120,7 +120,7 @@ async def test_connect_query_fails(all_fixture_data: dict, mocker):
protocol_class = get_protocol(config).__class__ protocol_class = get_protocol(config).__class__
close_mock = mocker.patch.object(protocol_class, "close") close_mock = mocker.patch.object(protocol_class, "close")
assert close_mock.call_count == 0 assert close_mock.call_count == 0
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await connect(config=config) await connect(config=config)
assert close_mock.call_count == 1 assert close_mock.call_count == 1

View File

@ -8,7 +8,7 @@ from kasa.credentials import Credentials
from kasa.deviceconfig import ( from kasa.deviceconfig import (
DeviceConfig, DeviceConfig,
) )
from kasa.exceptions import SmartDeviceException from kasa.exceptions import KasaException
async def test_serialization(): async def test_serialization():
@ -29,7 +29,7 @@ async def test_serialization():
ids=["invalid-dict", "not-dict"], ids=["invalid-dict", "not-dict"],
) )
def test_deserialization_errors(input_value, expected_msg): def test_deserialization_errors(input_value, expected_msg):
with pytest.raises(SmartDeviceException, match=expected_msg): with pytest.raises(KasaException, match=expected_msg):
DeviceConfig.from_dict(input_value) DeviceConfig.from_dict(input_value)

View File

@ -13,14 +13,14 @@ from kasa import (
Device, Device,
DeviceType, DeviceType,
Discover, Discover,
SmartDeviceException, KasaException,
) )
from kasa.deviceconfig import ( from kasa.deviceconfig import (
ConnectionType, ConnectionType,
DeviceConfig, DeviceConfig,
) )
from kasa.discover import DiscoveryResult, _DiscoverProtocol, json_dumps from kasa.discover import DiscoveryResult, _DiscoverProtocol, json_dumps
from kasa.exceptions import AuthenticationException, UnsupportedDeviceException from kasa.exceptions import AuthenticationError, UnsupportedDeviceError
from kasa.iot import IotDevice from kasa.iot import IotDevice
from kasa.xortransport import XorEncryption from kasa.xortransport import XorEncryption
@ -94,7 +94,7 @@ async def test_type_detection_lightstrip(dev: Device):
async def test_type_unknown(): async def test_type_unknown():
invalid_info = {"system": {"get_sysinfo": {"type": "nosuchtype"}}} invalid_info = {"system": {"get_sysinfo": {"type": "nosuchtype"}}}
with pytest.raises(UnsupportedDeviceException): with pytest.raises(UnsupportedDeviceError):
Discover._get_device_class(invalid_info) Discover._get_device_class(invalid_info)
@ -151,7 +151,7 @@ async def test_discover_single_hostname(discovery_mock, mocker):
assert update_mock.call_count == 0 assert update_mock.call_count == 0
mocker.patch("socket.getaddrinfo", side_effect=socket.gaierror()) mocker.patch("socket.getaddrinfo", side_effect=socket.gaierror())
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
x = await Discover.discover_single(host, credentials=Credentials()) x = await Discover.discover_single(host, credentials=Credentials())
@ -161,7 +161,7 @@ async def test_discover_single_unsupported(unsupported_device_info, mocker):
# Test with a valid unsupported response # Test with a valid unsupported response
with pytest.raises( with pytest.raises(
UnsupportedDeviceException, UnsupportedDeviceError,
): ):
await Discover.discover_single(host) await Discover.discover_single(host)
@ -171,7 +171,7 @@ async def test_discover_single_no_response(mocker):
host = "127.0.0.1" host = "127.0.0.1"
mocker.patch.object(_DiscoverProtocol, "do_discover") mocker.patch.object(_DiscoverProtocol, "do_discover")
with pytest.raises( with pytest.raises(
SmartDeviceException, match=f"Timed out getting discovery response for {host}" KasaException, match=f"Timed out getting discovery response for {host}"
): ):
await Discover.discover_single(host, discovery_timeout=0) await Discover.discover_single(host, discovery_timeout=0)
@ -198,7 +198,7 @@ async def test_discover_invalid_info(msg, data, mocker):
mocker.patch.object(_DiscoverProtocol, "do_discover", mock_discover) mocker.patch.object(_DiscoverProtocol, "do_discover", mock_discover)
with pytest.raises(SmartDeviceException, match=msg): with pytest.raises(KasaException, match=msg):
await Discover.discover_single(host) await Discover.discover_single(host)
@ -280,11 +280,11 @@ async def test_discover_single_authentication(discovery_mock, mocker):
mocker.patch.object( mocker.patch.object(
device_class, device_class,
"update", "update",
side_effect=AuthenticationException("Failed to authenticate"), side_effect=AuthenticationError("Failed to authenticate"),
) )
with pytest.raises( with pytest.raises(
AuthenticationException, AuthenticationError,
match="Failed to authenticate", match="Failed to authenticate",
): ):
device = await Discover.discover_single( device = await Discover.discover_single(
@ -315,7 +315,7 @@ async def test_device_update_from_new_discovery_info(discovery_data):
# TODO implement requires_update for SmartDevice # TODO implement requires_update for SmartDevice
if isinstance(device, IotDevice): if isinstance(device, IotDevice):
with pytest.raises( with pytest.raises(
SmartDeviceException, KasaException,
match=re.escape("You need to await update() to access the data"), match=re.escape("You need to await update() to access the data"),
): ):
assert device.supported_modules assert device.supported_modules
@ -456,9 +456,9 @@ async def test_discover_propogates_task_exceptions(discovery_mock):
discovery_timeout = 0 discovery_timeout = 0
async def on_discovered(dev): async def on_discovered(dev):
raise SmartDeviceException("Dummy exception") raise KasaException("Dummy exception")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await Discover.discover( await Discover.discover(
discovery_timeout=discovery_timeout, on_discovered=on_discovered discovery_timeout=discovery_timeout, on_discovered=on_discovered
) )

View File

@ -10,7 +10,7 @@ from voluptuous import (
Schema, Schema,
) )
from kasa import EmeterStatus, SmartDeviceException from kasa import EmeterStatus, KasaException
from kasa.iot import IotDevice from kasa.iot import IotDevice
from kasa.iot.modules.emeter import Emeter from kasa.iot.modules.emeter import Emeter
@ -38,16 +38,16 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
async def test_no_emeter(dev): async def test_no_emeter(dev):
assert not dev.has_emeter assert not dev.has_emeter
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.get_emeter_realtime() await dev.get_emeter_realtime()
# Only iot devices support the historical stats so other # Only iot devices support the historical stats so other
# devices will not implement the methods below # devices will not implement the methods below
if isinstance(dev, IotDevice): if isinstance(dev, IotDevice):
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.get_emeter_daily() await dev.get_emeter_daily()
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.get_emeter_monthly() await dev.get_emeter_monthly()
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.erase_emeter_stats() await dev.erase_emeter_stats()

View File

@ -6,9 +6,9 @@ import pytest
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import ( from ..exceptions import (
ConnectionException, KasaException,
SmartDeviceException, TimeoutError,
TimeoutException, _ConnectionError,
) )
from ..httpclient import HttpClient from ..httpclient import HttpClient
@ -18,28 +18,28 @@ from ..httpclient import HttpClient
[ [
( (
aiohttp.ServerDisconnectedError(), aiohttp.ServerDisconnectedError(),
ConnectionException, _ConnectionError,
"Device connection error: ", "Device connection error: ",
), ),
( (
aiohttp.ClientOSError(), aiohttp.ClientOSError(),
ConnectionException, _ConnectionError,
"Device connection error: ", "Device connection error: ",
), ),
( (
aiohttp.ServerTimeoutError(), aiohttp.ServerTimeoutError(),
TimeoutException, TimeoutError,
"Unable to query the device, timed out: ", "Unable to query the device, timed out: ",
), ),
( (
asyncio.TimeoutError(), asyncio.TimeoutError(),
TimeoutException, TimeoutError,
"Unable to query the device, timed out: ", "Unable to query the device, timed out: ",
), ),
(Exception(), SmartDeviceException, "Unable to query the device: "), (Exception(), KasaException, "Unable to query the device: "),
( (
aiohttp.ServerFingerprintMismatch("exp", "got", "host", 1), aiohttp.ServerFingerprintMismatch("exp", "got", "host", 1),
SmartDeviceException, KasaException,
"Unable to query the device: ", "Unable to query the device: ",
), ),
], ],

View File

@ -12,11 +12,11 @@ from ..aestransport import AesTransport
from ..credentials import Credentials from ..credentials import Credentials
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import ( from ..exceptions import (
AuthenticationException, AuthenticationError,
ConnectionException, KasaException,
RetryableException, TimeoutError,
SmartDeviceException, _ConnectionError,
TimeoutException, _RetryableError,
) )
from ..httpclient import HttpClient from ..httpclient import HttpClient
from ..iotprotocol import IotProtocol from ..iotprotocol import IotProtocol
@ -68,7 +68,7 @@ async def test_protocol_retries_via_client_session(
mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0) mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0)
config = DeviceConfig(host) config = DeviceConfig(host)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
DUMMY_QUERY, retry_count=retry_count DUMMY_QUERY, retry_count=retry_count
) )
@ -80,11 +80,11 @@ async def test_protocol_retries_via_client_session(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error, retry_expectation", "error, retry_expectation",
[ [
(SmartDeviceException("dummy exception"), False), (KasaException("dummy exception"), False),
(RetryableException("dummy exception"), True), (_RetryableError("dummy exception"), True),
(TimeoutException("dummy exception"), True), (TimeoutError("dummy exception"), True),
], ],
ids=("SmartDeviceException", "RetryableException", "TimeoutException"), ids=("KasaException", "_RetryableError", "TimeoutError"),
) )
@pytest.mark.parametrize("transport_class", [AesTransport, KlapTransport]) @pytest.mark.parametrize("transport_class", [AesTransport, KlapTransport])
@pytest.mark.parametrize("protocol_class", [IotProtocol, SmartProtocol]) @pytest.mark.parametrize("protocol_class", [IotProtocol, SmartProtocol])
@ -97,7 +97,7 @@ async def test_protocol_retries_via_httpclient(
mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0) mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0)
config = DeviceConfig(host) config = DeviceConfig(host)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
DUMMY_QUERY, retry_count=retry_count DUMMY_QUERY, retry_count=retry_count
) )
@ -115,11 +115,11 @@ async def test_protocol_no_retry_on_connection_error(
conn = mocker.patch.object( conn = mocker.patch.object(
aiohttp.ClientSession, aiohttp.ClientSession,
"post", "post",
side_effect=AuthenticationException("foo"), side_effect=AuthenticationError("foo"),
) )
mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0) mocker.patch.object(protocol_class, "BACKOFF_SECONDS_AFTER_TIMEOUT", 0)
config = DeviceConfig(host) config = DeviceConfig(host)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
DUMMY_QUERY, retry_count=5 DUMMY_QUERY, retry_count=5
) )
@ -139,7 +139,7 @@ async def test_protocol_retry_recoverable_error(
side_effect=aiohttp.ClientOSError("foo"), side_effect=aiohttp.ClientOSError("foo"),
) )
config = DeviceConfig(host) config = DeviceConfig(host)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
DUMMY_QUERY, retry_count=5 DUMMY_QUERY, retry_count=5
) )
@ -159,7 +159,7 @@ async def test_protocol_reconnect(mocker, retry_count, protocol_class, transport
nonlocal remaining nonlocal remaining
remaining -= 1 remaining -= 1
if remaining: if remaining:
raise ConnectionException("Simulated connection failure") raise _ConnectionError("Simulated connection failure")
return mock_response return mock_response
@ -249,7 +249,7 @@ def test_encrypt_unicode():
), ),
( (
Credentials("shouldfail", "shouldfail"), Credentials("shouldfail", "shouldfail"),
pytest.raises(AuthenticationException), pytest.raises(AuthenticationError),
), ),
], ],
ids=("client", "blank", "kasa_setup", "shouldfail"), ids=("client", "blank", "kasa_setup", "shouldfail"),
@ -350,7 +350,7 @@ async def test_handshake(
assert protocol._transport._handshake_done is True assert protocol._transport._handshake_done is True
response_status = 403 response_status = 403
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol._transport.perform_handshake() await protocol._transport.perform_handshake()
assert protocol._transport._handshake_done is False assert protocol._transport._handshake_done is False
await protocol.close() await protocol.close()
@ -405,37 +405,37 @@ async def test_query(mocker):
pytest.param( pytest.param(
(403, 403, 403), (403, 403, 403),
True, True,
pytest.raises(SmartDeviceException), pytest.raises(KasaException),
id="handshake1-403-status", id="handshake1-403-status",
), ),
pytest.param( pytest.param(
(200, 403, 403), (200, 403, 403),
True, True,
pytest.raises(SmartDeviceException), pytest.raises(KasaException),
id="handshake2-403-status", id="handshake2-403-status",
), ),
pytest.param( pytest.param(
(200, 200, 403), (200, 200, 403),
True, True,
pytest.raises(AuthenticationException), pytest.raises(AuthenticationError),
id="request-403-status", id="request-403-status",
), ),
pytest.param( pytest.param(
(200, 200, 400), (200, 200, 400),
True, True,
pytest.raises(SmartDeviceException), pytest.raises(KasaException),
id="request-400-status", id="request-400-status",
), ),
pytest.param( pytest.param(
(200, 200, 200), (200, 200, 200),
False, False,
pytest.raises(AuthenticationException), pytest.raises(AuthenticationError),
id="handshake1-wrong-auth", id="handshake1-wrong-auth",
), ),
pytest.param( pytest.param(
(200, 200, 200), (200, 200, 200),
secrets.token_bytes(16), secrets.token_bytes(16),
pytest.raises(SmartDeviceException), pytest.raises(KasaException),
id="handshake1-bad-auth-length", id="handshake1-bad-auth-length",
), ),
], ],

View File

@ -1,7 +1,7 @@
import pytest import pytest
from kasa import DeviceType from kasa import DeviceType
from kasa.exceptions import SmartDeviceException from kasa.exceptions import KasaException
from kasa.iot import IotLightStrip from kasa.iot import IotLightStrip
from .conftest import lightstrip from .conftest import lightstrip
@ -23,7 +23,7 @@ async def test_lightstrip_effect(dev: IotLightStrip):
@lightstrip @lightstrip
async def test_effects_lightstrip_set_effect(dev: IotLightStrip): async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev.set_effect("Not real") await dev.set_effect("Not real")
await dev.set_effect("Candy Cane") await dev.set_effect("Candy Cane")

View File

@ -14,7 +14,7 @@ import pytest
from ..aestransport import AesTransport from ..aestransport import AesTransport
from ..credentials import Credentials from ..credentials import Credentials
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import SmartDeviceException from ..exceptions import KasaException
from ..iotprotocol import IotProtocol, _deprecated_TPLinkSmartHomeProtocol from ..iotprotocol import IotProtocol, _deprecated_TPLinkSmartHomeProtocol
from ..klaptransport import KlapTransport, KlapTransportV2 from ..klaptransport import KlapTransport, KlapTransportV2
from ..protocol import ( from ..protocol import (
@ -46,7 +46,7 @@ async def test_protocol_retries(mocker, retry_count, protocol_class, transport_c
conn = mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer) conn = mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=retry_count {}, retry_count=retry_count
) )
@ -70,7 +70,7 @@ async def test_protocol_no_retry_on_unreachable(
side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), side_effect=OSError(errno.EHOSTUNREACH, "No route to host"),
) )
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=5 {}, retry_count=5
) )
@ -94,7 +94,7 @@ async def test_protocol_no_retry_connection_refused(
side_effect=ConnectionRefusedError, side_effect=ConnectionRefusedError,
) )
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=5 {}, retry_count=5
) )
@ -118,7 +118,7 @@ async def test_protocol_retry_recoverable_error(
side_effect=OSError(errno.ECONNRESET, "Connection reset by peer"), side_effect=OSError(errno.ECONNRESET, "Connection reset by peer"),
) )
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=5 {}, retry_count=5
) )
@ -553,7 +553,7 @@ async def test_protocol_will_retry_on_connect(
retry_count = 2 retry_count = 2
conn = mocker.patch("asyncio.open_connection", side_effect=error) conn = mocker.patch("asyncio.open_connection", side_effect=error)
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=retry_count {}, retry_count=retry_count
) )
@ -595,7 +595,7 @@ async def test_protocol_will_retry_on_write(
conn = mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer) conn = mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
write_mock = mocker.patch("asyncio.StreamWriter.write", side_effect=error) write_mock = mocker.patch("asyncio.StreamWriter.write", side_effect=error)
config = DeviceConfig("127.0.0.1") config = DeviceConfig("127.0.0.1")
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await protocol_class(transport=transport_class(config=config)).query( await protocol_class(transport=transport_class(config=config)).query(
{}, retry_count=retry_count {}, retry_count=retry_count
) )
@ -609,9 +609,7 @@ def test_deprecated_protocol():
with pytest.deprecated_call(): with pytest.deprecated_call():
from kasa import TPLinkSmartHomeProtocol from kasa import TPLinkSmartHomeProtocol
with pytest.raises( with pytest.raises(KasaException, match="host or transport must be supplied"):
SmartDeviceException, match="host or transport must be supplied"
):
proto = TPLinkSmartHomeProtocol() proto = TPLinkSmartHomeProtocol()
host = "127.0.0.1" host = "127.0.0.1"
proto = TPLinkSmartHomeProtocol(host=host) proto = TPLinkSmartHomeProtocol(host=host)

View File

@ -21,7 +21,7 @@ from voluptuous import (
) )
import kasa import kasa
from kasa import Credentials, Device, DeviceConfig, SmartDeviceException from kasa import Credentials, Device, DeviceConfig, KasaException
from kasa.exceptions import SmartErrorCode from kasa.exceptions import SmartErrorCode
from kasa.iot import IotDevice from kasa.iot import IotDevice
from kasa.smart import SmartChildDevice, SmartDevice from kasa.smart import SmartChildDevice, SmartDevice
@ -67,8 +67,8 @@ async def test_state_info(dev):
@device_iot @device_iot
async def test_invalid_connection(dev): async def test_invalid_connection(dev):
with patch.object( with patch.object(
FakeIotProtocol, "query", side_effect=SmartDeviceException FakeIotProtocol, "query", side_effect=KasaException
), pytest.raises(SmartDeviceException): ), pytest.raises(KasaException):
await dev.update() await dev.update()
@ -98,7 +98,7 @@ async def test_initial_update_no_emeter(dev, mocker):
@device_iot @device_iot
async def test_query_helper(dev): async def test_query_helper(dev):
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dev._query_helper("test", "testcmd", {}) await dev._query_helper("test", "testcmd", {})
# TODO check for unwrapping? # TODO check for unwrapping?
@ -328,7 +328,7 @@ async def test_update_no_device_info(dev: SmartDevice):
} }
msg = f"get_device_info not found in {mock_response} for device 127.0.0.123" msg = f"get_device_info not found in {mock_response} for device 127.0.0.123"
with patch.object(dev.protocol, "query", return_value=mock_response), pytest.raises( with patch.object(dev.protocol, "query", return_value=mock_response), pytest.raises(
SmartDeviceException, match=msg KasaException, match=msg
): ):
await dev.update() await dev.update()
@ -348,6 +348,16 @@ def test_deprecated_devices(device_class, use_class):
getattr(module, use_class.__name__) getattr(module, use_class.__name__)
@pytest.mark.parametrize(
"exceptions_class, use_class", kasa.deprecated_exceptions.items()
)
def test_deprecated_exceptions(exceptions_class, use_class):
msg = f"{exceptions_class} is deprecated, use {use_class.__name__} instead"
with pytest.deprecated_call(match=msg):
getattr(kasa, exceptions_class)
getattr(kasa, use_class.__name__)
def check_mac(x): def check_mac(x):
if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", x.lower()): if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", x.lower()):
return x return x

View File

@ -1,13 +1,10 @@
from itertools import chain
import pytest import pytest
from ..credentials import Credentials from ..credentials import Credentials
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..exceptions import ( from ..exceptions import (
SMART_RETRYABLE_ERRORS, SMART_RETRYABLE_ERRORS,
SMART_TIMEOUT_ERRORS, KasaException,
SmartDeviceException,
SmartErrorCode, SmartErrorCode,
) )
from ..smartprotocol import _ChildProtocolWrapper from ..smartprotocol import _ChildProtocolWrapper
@ -28,13 +25,10 @@ async def test_smart_device_errors(dummy_protocol, mocker, error_code):
dummy_protocol._transport, "send", return_value=mock_response dummy_protocol._transport, "send", return_value=mock_response
) )
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dummy_protocol.query(DUMMY_QUERY, retry_count=2) await dummy_protocol.query(DUMMY_QUERY, retry_count=2)
if error_code in chain(SMART_TIMEOUT_ERRORS, SMART_RETRYABLE_ERRORS): expected_calls = 3 if error_code in SMART_RETRYABLE_ERRORS else 1
expected_calls = 3
else:
expected_calls = 1
assert send_mock.call_count == expected_calls assert send_mock.call_count == expected_calls
@ -124,7 +118,7 @@ async def test_childdevicewrapper_error(dummy_protocol, mocker):
mock_response = {"error_code": 0, "result": {"responseData": {"error_code": -1001}}} mock_response = {"error_code": 0, "result": {"responseData": {"error_code": -1001}}}
mocker.patch.object(wrapped_protocol._transport, "send", return_value=mock_response) mocker.patch.object(wrapped_protocol._transport, "send", return_value=mock_response)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await wrapped_protocol.query(DUMMY_QUERY) await wrapped_protocol.query(DUMMY_QUERY)
@ -180,5 +174,5 @@ async def test_childdevicewrapper_multiplerequest_error(dummy_protocol, mocker):
} }
mocker.patch.object(dummy_protocol._transport, "send", return_value=mock_response) mocker.patch.object(dummy_protocol._transport, "send", return_value=mock_response)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
await dummy_protocol.query(DUMMY_QUERY) await dummy_protocol.query(DUMMY_QUERY)

View File

@ -2,7 +2,7 @@ from datetime import datetime
import pytest import pytest
from kasa import SmartDeviceException from kasa import KasaException
from kasa.iot import IotStrip from kasa.iot import IotStrip
from .conftest import handle_turn_on, strip, turn_on from .conftest import handle_turn_on, strip, turn_on
@ -73,7 +73,7 @@ async def test_get_plug_by_name(dev: IotStrip):
name = dev.children[0].alias name = dev.children[0].alias
assert dev.get_plug_by_name(name) == dev.children[0] # type: ignore[arg-type] assert dev.get_plug_by_name(name) == dev.children[0] # type: ignore[arg-type]
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
dev.get_plug_by_name("NONEXISTING NAME") dev.get_plug_by_name("NONEXISTING NAME")
@ -81,10 +81,10 @@ async def test_get_plug_by_name(dev: IotStrip):
async def test_get_plug_by_index(dev: IotStrip): async def test_get_plug_by_index(dev: IotStrip):
assert dev.get_plug_by_index(0) == dev.children[0] assert dev.get_plug_by_index(0) == dev.children[0]
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
dev.get_plug_by_index(-1) dev.get_plug_by_index(-1)
with pytest.raises(SmartDeviceException): with pytest.raises(KasaException):
dev.get_plug_by_index(len(dev.children)) dev.get_plug_by_index(len(dev.children))

View File

@ -23,7 +23,7 @@ from typing import Dict, Generator, Optional
from async_timeout import timeout as asyncio_timeout from async_timeout import timeout as asyncio_timeout
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import RetryableException, SmartDeviceException from .exceptions import KasaException, _RetryableError
from .json import loads as json_loads from .json import loads as json_loads
from .protocol import BaseTransport from .protocol import BaseTransport
@ -129,24 +129,24 @@ class XorTransport(BaseTransport):
await self._connect(self._timeout) await self._connect(self._timeout)
except ConnectionRefusedError as ex: except ConnectionRefusedError as ex:
await self.reset() await self.reset()
raise SmartDeviceException( raise KasaException(
f"Unable to connect to the device: {self._host}:{self._port}: {ex}" f"Unable to connect to the device: {self._host}:{self._port}: {ex}"
) from ex ) from ex
except OSError as ex: except OSError as ex:
await self.reset() await self.reset()
if ex.errno in _NO_RETRY_ERRORS: if ex.errno in _NO_RETRY_ERRORS:
raise SmartDeviceException( raise KasaException(
f"Unable to connect to the device:" f"Unable to connect to the device:"
f" {self._host}:{self._port}: {ex}" f" {self._host}:{self._port}: {ex}"
) from ex ) from ex
else: else:
raise RetryableException( raise _RetryableError(
f"Unable to connect to the device:" f"Unable to connect to the device:"
f" {self._host}:{self._port}: {ex}" f" {self._host}:{self._port}: {ex}"
) from ex ) from ex
except Exception as ex: except Exception as ex:
await self.reset() await self.reset()
raise RetryableException( raise _RetryableError(
f"Unable to connect to the device:" f" {self._host}:{self._port}: {ex}" f"Unable to connect to the device:" f" {self._host}:{self._port}: {ex}"
) from ex ) from ex
except BaseException: except BaseException:
@ -162,7 +162,7 @@ class XorTransport(BaseTransport):
return await self._execute_send(request) return await self._execute_send(request)
except Exception as ex: except Exception as ex:
await self.reset() await self.reset()
raise RetryableException( raise _RetryableError(
f"Unable to query the device {self._host}:{self._port}: {ex}" f"Unable to query the device {self._host}:{self._port}: {ex}"
) from ex ) from ex
except BaseException: except BaseException: