Skip to content

Commit 0dde2cb

Browse files
committed
Rework for hass integration
1 parent 3c30278 commit 0dde2cb

8 files changed

Lines changed: 148 additions & 85 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
.vscode/
55
pykasacloud/__main__.py
66
dist
7+
.DS_Store
8+
pykasacloud/__main__.py
9+
.DS_Store

README.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# PyKasaCloud
22

3+
![GitHub Release](https://img.shields.io/github/v/release/iluvdata/PyKasaCloud)
4+
35
This is a library wrapper that allows you to connect to *some* TPLink/Kasa/Tapo devices via the cloud utilizing the excellent [python-kasa](https://pypi.org/project/python-kasa/) library. Essentially this adds a transport and protocol class to facilitate this. This has not been tested extensively as I only have access to "iot" protocol devices and I'm not sure if other devices utilize passthrough mechanism via the cloud api.
46

57
## Usage
68

79
Rather than use discovery like `python-kasa` you must get connect to the cloud (providing credentials) to obtain a token.
810
```python
9-
cloud: KasaCloud = await KasaCloud.auth(username="username", password="password")
11+
cloud: KasaCloud = await KasaCloud.kasacloud(username="username", password="password")
1012
```
11-
You can then get a dictionary of devices. The deviceId in the cloud will be the keys and the values will be `kasa.Device`s.
13+
You can then get a dictionary of devices. The `deviceId` in the cloud will be the keys and the values will be `kasa.Device`s.
1214
```python
1315
devices: dict[str, Device] = cloud.getDevices()
1416
```
@@ -18,23 +20,30 @@ You can then interact with these devices like python-kasa devices.
1820

1921
To cache tokens to a json file, provide a path.
2022
```python
21-
cloud: KasaCloud = await KasaCloud.auth(username="username", password="password", token_storage_file=".kasacloud.json")
23+
cloud: KasaCloud = await KasaCloud.kasacloud(username="username", password="password", token_storage_file=".kasacloud.json")
2224
```
2325
Subsequent authenication can be accomplished just using the `token_storage_file` parameter.
2426
```python
25-
cloud: KasaCloud = await KasaCloud.auth( token_storage_file=".kasacloud.json")
27+
cloud: KasaCloud = await KasaCloud.kasacloud( token_storage_file=".kasacloud.json")
2628
```
2729
### Refesh Token and Callbacks
28-
If you are storing the token externally, say in a HomeAssistant Config Entry simply pass a `Token` object:
30+
If you are storing the token externally, say in a HomeAssistant Config Entry simply pass a `Token` object (inside `async_setup_entry` in `__init__.py` of a given integration)
2931
```python
3032

31-
def token_update_callback(config_entry: ConfigEntry) -> Callable:
32-
def updated_token(token: Token) -> None:
33-
config_entry["token"] <- token
34-
return update_token
35-
36-
token = Token(**config_entry.get("token"))
37-
cloud: KasaCloud = await KasaCloud.auth(token=token, token_update_callback = token_update_callback(config_entry))
33+
async def update_token(token: Token) -> None:
34+
data = entry.data | {TOKEN: token}
35+
result = hass.config_entries.async_update_entry(
36+
entry=entry, data=data, unique_id=entry.unique_id
37+
)
38+
if not result:
39+
raise TokenUpdateError("Unable to update token in config entry")
40+
41+
try:
42+
cloud: KasaCloud = await KasaCloud.kasacloud(
43+
token=entry.data.get(TOKEN), token_update_callback=update_token
44+
)
45+
except AuthenticationError as err:
46+
raise ConfigEntryAuthFailed(err) from err
3847
```
3948

4049

pykasacloud/__init__.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,4 @@
99

1010
__version__ = version("python-kasa")
1111

12-
__all__ = [
13-
"CloudTransport",
14-
"KasaCloudError",
15-
"CloudProtocol",
16-
"KasaCloud",
17-
"Token"
18-
]
12+
__all__ = ["CloudTransport", "KasaCloudError", "CloudProtocol", "KasaCloud", "Token"]

pykasacloud/const.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
"""Constants for PyKasaCloud integration."""
22

33
API_URL = "https://wap.tplinkcloud.com"
4-
APPTYPE = "Kasa_Android"
4+
APPTYPE = "Tapo_Ios"
55
USERAGENT = "User-Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1; A0001 Build/M4B30X)"
6+
TOKEN = "token"
7+
REFRESH_TOKEN = "refresh_token"
8+
CLIENT_ID = "client_id"
9+
ACCOUNT_ID = "account_id"
10+
APPSERVERURL = "appServerUrl"

pykasacloud/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
"""Extension of python-kasa exceptions for Kasa Cloud integration."""
22

33
from enum import IntEnum
4+
45
from kasa.exceptions import KasaException
56

7+
68
class KasaCloudError(KasaException):
79
"""Exception raised for errors in the Kasa Cloud API interaction."""
810

911
def __init__(self, msg: str) -> None:
1012
"""Initialize the KasaCloudError with a message."""
1113
super().__init__(msg)
1214

15+
1316
class MissingCredentials(KasaCloudError):
1417
"""Exception raised when credentials are missing."""
1518

19+
1620
class CloudErrorCode(IntEnum):
1721
"""Enum for cloud error codes."""
22+
1823
SUCCESS = 0
1924
TOKEN_EXPIRED = -20651
2025
MISSING_METHOD = -20103
2126
MISSING_PARAMETER = -20104
2227
MISSING_REQUEST_DATA = -20573
2328
DEVICE_OFFLINE = -20571
29+
ACCOUNT_NOT_FOUND = -20600

pykasacloud/kasacloud.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""KasaCloud."""
22

3+
from collections.abc import Callable, Coroutine
34
import logging
45
import time
5-
from collections.abc import Callable, Coroutine
66
from typing import Any
77

88
from kasa import Device, DeviceType
@@ -17,20 +17,18 @@
1717
)
1818

1919
from .exceptions import KasaCloudError
20-
2120
from .protocols import CloudProtocol
2221
from .transports import CloudTransport, Token
2322

24-
_GET_DEVICES_QUERY: dict[str, str] = {
25-
"method": "getDeviceList"
26-
}
23+
_GET_DEVICES_QUERY: dict[str, str] = {"method": "getDeviceList"}
2724

2825
GET_SYSINFO_QUERY: dict[str, dict[str, dict]] = {
2926
"system": {"get_sysinfo": {}},
3027
}
3128

3229
_LOGGER = logging.getLogger(__name__)
3330

31+
3432
class KasaCloud:
3533
"""Class to instantiate and get devices."""
3634

@@ -56,13 +54,22 @@ async def kasacloud(
5654
password=password,
5755
token=ctoken,
5856
token_storage_file=token_storage_file,
59-
token_update_callback=token_update_callback
57+
token_update_callback=token_update_callback,
6058
)
6159

6260
return self
6361

64-
async def get_devices(self) -> dict[str, Device]:
65-
"""Get kasa devices from cloud."""
62+
@property
63+
def token(self) -> Token:
64+
"""Return the token associated with this authentication."""
65+
return self._transport.token
66+
67+
async def close(self) -> None:
68+
"""Close the underlying resources."""
69+
await self._transport.close()
70+
71+
async def get_device_list(self) -> dict[str, Any]:
72+
"""Get kasa device ids from cloud."""
6673

6774
protocol: CloudProtocol = CloudProtocol(transport=self._transport)
6875

@@ -73,16 +80,18 @@ async def get_devices(self) -> dict[str, Device]:
7380

7481
device_list: list[dict[str, Any]] = resp["deviceList"]
7582

76-
device_dict: dict[str, Device] = {}
83+
devices: dict[str, Any] = {}
7784
for device in device_list:
7885
if device["status"]:
79-
device_dict[device["deviceId"]] = await self._get_device(device)
86+
devices[device["deviceId"]] = device
8087

81-
return device_dict
88+
return devices
8289

83-
async def _get_device(self, device_dict: dict[str, Any]) -> Device:
84-
"""Initantiate and populate the device.
85-
Taken from device_factory.py in python-kasa."""
90+
async def get_device(self, device_dict: dict[str, Any]) -> Device:
91+
"""Initantiate and populate the device.
92+
93+
Taken from device_factory.py in python-kasa.
94+
"""
8695
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
8796
if debug_enabled:
8897
start_time = time.perf_counter()
@@ -116,9 +125,10 @@ def _perf_log(has_params: bool, perf_type: str) -> None:
116125
_perf_log(True, "update")
117126
return device
118127

128+
119129
def _get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
120130
"""Find SmartDevice subclass for device described by passed data."""
121-
TYPE_TO_CLASS = { # pylint: disable=invalid-name
131+
TYPE_TO_CLASS = { # pylint: disable=invalid-name
122132
DeviceType.Bulb: IotBulb,
123133
DeviceType.Plug: IotPlug,
124134
DeviceType.Dimmer: IotDimmer,
@@ -128,4 +138,4 @@ def _get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
128138
# Disabled until properly implemented
129139
# DeviceType.Camera: IotCamera,
130140
}
131-
return TYPE_TO_CLASS[IotDevice._get_device_type_from_sys_info(sysinfo)] # pylint: disable=protected-access
141+
return TYPE_TO_CLASS[IotDevice._get_device_type_from_sys_info(sysinfo)] # pylint: disable=protected-access # noqa: SLF001

pykasacloud/protocols/cloudprotocol.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
from kasa.protocols.protocol import redact_data
1111
from yarl import URL
1212

13-
from ..transports import CloudTransport
13+
from pykasacloud.transports import CloudTransport
1414

1515
_LOGGER = logging.getLogger(__name__)
1616

17+
1718
class CloudProtocol(IotProtocol):
1819
"""Cloud Protocol Class."""
1920

@@ -36,7 +37,9 @@ async def _execute_query(self, request: str, retry_count: int) -> dict:
3637
)
3738
transport: CloudTransport = cast(CloudTransport, self._transport)
3839

39-
resp = await transport.send_request(json_loads(request), self._device_id, self._url)
40+
resp = await transport.send_request(
41+
json_loads(request), self._device_id, self._url
42+
)
4043

4144
if debug_enabled:
4245
data = redact_data(resp, REDACTORS) if self._redact_data else resp

0 commit comments

Comments
 (0)