Skip to content

Commit

Permalink
Rename and deprecate exception classes (#739)
Browse files Browse the repository at this point in the history
# 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)
  • Loading branch information
sdb9696 committed Feb 21, 2024
1 parent 4beff22 commit 8c39e81
Show file tree
Hide file tree
Showing 44 changed files with 390 additions and 358 deletions.
17 changes: 7 additions & 10 deletions devtools/dump_devinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@

from devtools.helpers.smartrequests import SmartRequest, get_component_requests
from kasa import (
AuthenticationException,
AuthenticationError,
Credentials,
Device,
Discover,
SmartDeviceException,
TimeoutException,
KasaException,
TimeoutError,
)
from kasa.discover import DiscoveryResult
from kasa.exceptions import SmartErrorCode
Expand Down Expand Up @@ -303,19 +303,16 @@ async def _make_requests_or_exit(
for method, result in responses.items():
final[method] = result
return final
except AuthenticationException as ex:
except AuthenticationError as ex:
_echo_error(
f"Unable to query the device due to an authentication error: {ex}",
)
exit(1)
except SmartDeviceException as ex:
except KasaException as ex:
_echo_error(
f"Unable to query {name} at once: {ex}",
)
if (
isinstance(ex, TimeoutException)
or ex.error_code == SmartErrorCode.SESSION_TIMEOUT_ERROR
):
if isinstance(ex, TimeoutError):
_echo_error(
"Timeout, try reducing the batch size via --batch-size option.",
)
Expand Down Expand Up @@ -400,7 +397,7 @@ async def get_smart_fixture(device: SmartDevice, batch_size: int):
response = await device.protocol.query(
SmartRequest._create_request_dict(test_call.request)
)
except AuthenticationException as ex:
except AuthenticationError as ex:
_echo_error(
f"Unable to query the device due to an authentication error: {ex}",
)
Expand Down
34 changes: 34 additions & 0 deletions docs/source/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ The classes providing this functionality are:
- :class:`KlapTransport <kasa.klaptransport.KlapTransport>`
- :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
*****************************
Expand Down Expand Up @@ -154,3 +165,26 @@ API documentation for protocols and transports
:members:
:inherited-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:
14 changes: 1 addition & 13 deletions docs/source/smartdevice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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:

Expand Down Expand Up @@ -154,15 +154,3 @@ API documentation
.. autoclass:: Credentials
:members:
:undoc-members:

.. autoclass:: SmartDeviceException
:members:
:undoc-members:

.. autoclass:: AuthenticationException
:members:
:undoc-members:

.. autoclass:: UnsupportedDeviceException
:members:
:undoc-members:
36 changes: 27 additions & 9 deletions kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip`
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.
"""
from importlib.metadata import version
Expand All @@ -28,10 +28,11 @@
from kasa.discover import Discover
from kasa.emeterstatus import EmeterStatus
from kasa.exceptions import (
AuthenticationException,
SmartDeviceException,
TimeoutException,
UnsupportedDeviceException,
AuthenticationError,
DeviceError,
KasaException,
TimeoutError,
UnsupportedDeviceError,
)
from kasa.feature import Feature, FeatureType
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
Expand Down Expand Up @@ -61,10 +62,11 @@
"Device",
"Bulb",
"Plug",
"SmartDeviceException",
"AuthenticationException",
"UnsupportedDeviceException",
"TimeoutException",
"KasaException",
"AuthenticationError",
"DeviceError",
"UnsupportedDeviceError",
"TimeoutError",
"Credentials",
"DeviceConfig",
"ConnectionType",
Expand All @@ -84,6 +86,12 @@
"SmartDimmer": iot.IotDimmer,
"SmartBulbPreset": BulbPreset,
}
deprecated_exceptions = {
"SmartDeviceException": KasaException,
"UnsupportedDeviceException": UnsupportedDeviceError,
"AuthenticationException": AuthenticationError,
"TimeoutException": TimeoutError,
}


def __getattr__(name):
Expand All @@ -101,6 +109,11 @@ def __getattr__(name):
stacklevel=1,
)
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}")


Expand All @@ -112,6 +125,11 @@ def __getattr__(name):
SmartStrip = iot.IotStrip
SmartDimmer = iot.IotDimmer
SmartBulbPreset = BulbPreset

SmartDeviceException = KasaException
UnsupportedDeviceException = UnsupportedDeviceError
AuthenticationException = AuthenticationError
TimeoutException = TimeoutError
# Instanstiate all classes so the type checkers catch abstract issues
from . import smart

Expand Down
31 changes: 14 additions & 17 deletions kasa/aestransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@
from .exceptions import (
SMART_AUTHENTICATION_ERRORS,
SMART_RETRYABLE_ERRORS,
SMART_TIMEOUT_ERRORS,
AuthenticationException,
RetryableException,
SmartDeviceException,
AuthenticationError,
DeviceError,
KasaException,
SmartErrorCode,
TimeoutException,
_RetryableError,
)
from .httpclient import HttpClient
from .json import dumps as json_dumps
Expand Down Expand Up @@ -141,14 +140,12 @@ def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None:
if error_code == SmartErrorCode.SUCCESS:
return
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:
raise RetryableException(msg, error_code=error_code)
raise _RetryableError(msg, error_code=error_code)
if error_code in SMART_AUTHENTICATION_ERRORS:
self._state = TransportState.HANDSHAKE_REQUIRED
raise AuthenticationException(msg, error_code=error_code)
raise SmartDeviceException(msg, error_code=error_code)
raise AuthenticationError(msg, error_code=error_code)
raise DeviceError(msg, error_code=error_code)

async def send_secure_passthrough(self, request: str) -> Dict[str, Any]:
"""Send encrypted message as passthrough."""
Expand All @@ -171,7 +168,7 @@ async def send_secure_passthrough(self, request: str) -> Dict[str, Any]:
# _LOGGER.debug(f"secure_passthrough response is {status_code}: {resp_dict}")

if status_code != 200:
raise SmartDeviceException(
raise KasaException(
f"{self._host} responded with an unexpected "
+ f"status code {status_code} to passthrough"
)
Expand All @@ -197,7 +194,7 @@ async def send_secure_passthrough(self, request: str) -> Dict[str, Any]:
self._host,
)
except Exception:
raise SmartDeviceException(
raise KasaException(
f"Unable to decrypt response from {self._host}, "
+ f"error: {ex}, response: {raw_response}",
ex,
Expand All @@ -208,7 +205,7 @@ async def perform_login(self):
"""Login to the device."""
try:
await self.try_login(self._login_params)
except AuthenticationException as aex:
except AuthenticationError as aex:
try:
if aex.error_code is not SmartErrorCode.LOGIN_ERROR:
raise aex
Expand All @@ -223,10 +220,10 @@ async def perform_login(self):
"%s: logged in with default credentials",
self._host,
)
except AuthenticationException:
except AuthenticationError:
raise
except Exception as ex:
raise SmartDeviceException(
raise KasaException(
"Unable to login and trying default "
+ f"login raised another exception: {ex}",
ex,
Expand Down Expand Up @@ -292,7 +289,7 @@ async def perform_handshake(self) -> None:
_LOGGER.debug("Device responded with: %s", resp_dict)

if status_code != 200:
raise SmartDeviceException(
raise KasaException(
f"{self._host} responded with an unexpected "
+ f"status code {status_code} to handshake"
)
Expand Down Expand Up @@ -347,7 +344,7 @@ async def send(self, request: str) -> Dict[str, Any]:
await self.perform_login()
# After a login failure handshake needs to
# be redone or a 9999 error is received.
except AuthenticationException as ex:
except AuthenticationError as ex:
self._state = TransportState.HANDSHAKE_REQUIRED
raise ex

Expand Down
12 changes: 6 additions & 6 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import asyncclick as click

from kasa import (
AuthenticationException,
AuthenticationError,
Bulb,
ConnectionType,
Credentials,
Expand All @@ -22,8 +22,8 @@
DeviceFamilyType,
Discover,
EncryptType,
SmartDeviceException,
UnsupportedDeviceException,
KasaException,
UnsupportedDeviceError,
)
from kasa.discover import DiscoveryResult
from kasa.iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
Expand Down Expand Up @@ -458,7 +458,7 @@ async def discover(ctx):
unsupported = []
auth_failed = []

async def print_unsupported(unsupported_exception: UnsupportedDeviceException):
async def print_unsupported(unsupported_exception: UnsupportedDeviceError):
unsupported.append(unsupported_exception)
async with sem:
if unsupported_exception.discovery_result:
Expand All @@ -476,7 +476,7 @@ async def print_discovered(dev: Device):
async with sem:
try:
await dev.update()
except AuthenticationException:
except AuthenticationError:
auth_failed.append(dev._discovery_info)
echo("== Authentication failed for device ==")
_echo_discovery_info(dev._discovery_info)
Expand Down Expand Up @@ -677,7 +677,7 @@ async def cmd_command(dev: Device, module, command, parameters):
elif isinstance(dev, SmartDevice):
res = await dev._query_helper(command, parameters)
else:
raise SmartDeviceException("Unexpected device type %s.", dev)
raise KasaException("Unexpected device type %s.", dev)
echo(json.dumps(res))
return res

Expand Down
8 changes: 4 additions & 4 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .device_type import DeviceType
from .deviceconfig import DeviceConfig
from .emeterstatus import EmeterStatus
from .exceptions import SmartDeviceException
from .exceptions import KasaException
from .feature import Feature
from .iotprotocol import IotProtocol
from .protocol import BaseProtocol
Expand Down Expand Up @@ -242,12 +242,12 @@ def get_plug_by_name(self, name: str) -> "Device":
if p.alias == name:
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":
"""Return child device for the given index."""
if index + 1 > len(self.children) or index < 0:
raise SmartDeviceException(
raise KasaException(
f"Invalid index {index}, device has {len(self.children)} plugs"
)
return self.children[index]
Expand Down Expand Up @@ -306,7 +306,7 @@ def _add_feature(self, feature: Feature):
"""Add a new feature to the device."""
desc_name = feature.name.lower().replace(" ", "_")
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

@property
Expand Down

0 comments on commit 8c39e81

Please sign in to comment.