Skip to content

Commit 218600f

Browse files
committed
Switch to aiohttp and cryptography library
1 parent e6cb8ab commit 218600f

File tree

6 files changed

+117
-86
lines changed

6 files changed

+117
-86
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ Commands:
3636
```python
3737
from karcher.karcher import KarcherHome
3838

39-
kh = KarcherHome()
40-
kh.login("user@email", "password")
41-
devices = hk.get_devices()
39+
kh = await KarcherHome.create()
40+
await kh.login("user@email", "password")
41+
devices = await hk.get_devices()
4242
```
4343

4444
## License

karcher/cli.py

+29-15
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# SPDX-License-Identifier: MIT
44
# -----------------------------------------------------------
55

6+
import asyncio
67
import click
78
import dataclasses
89
import json
910
import logging
11+
from functools import wraps
1012

1113
from karcher.exception import KarcherHomeException
1214
from karcher.karcher import KarcherHome
@@ -18,6 +20,14 @@
1820
echo = click.echo
1921

2022

23+
def coro(f):
24+
@wraps(f)
25+
def wrapper(*args, **kwargs):
26+
return asyncio.run(f(*args, **kwargs))
27+
28+
return wrapper
29+
30+
2131
class EnhancedJSONEncoder(json.JSONEncoder):
2232
def default(self, o):
2333
if dataclasses.is_dataclass(o):
@@ -84,11 +94,12 @@ def safe_cli():
8494

8595
@cli.command()
8696
@click.pass_context
87-
def urls(ctx: click.Context):
97+
@coro
98+
async def urls(ctx: click.Context):
8899
"""Get region information."""
89100

90-
kh = KarcherHome(region=ctx.obj.region)
91-
d = kh.get_urls()
101+
kh = await KarcherHome.create(region=ctx.obj.region)
102+
d = await kh.get_urls()
92103

93104
ctx.obj.print(d)
94105

@@ -97,10 +108,11 @@ def urls(ctx: click.Context):
97108
@click.option('--username', '-u', help='Username to login with.')
98109
@click.option('--password', '-p', help='Password to login with.')
99110
@click.pass_context
100-
def login(ctx: click.Context, username: str, password: str):
111+
@coro
112+
async def login(ctx: click.Context, username: str, password: str):
101113
"""Get user session tokens."""
102114

103-
kh = KarcherHome(region=ctx.obj.region)
115+
kh = await KarcherHome.create(region=ctx.obj.region)
104116
ctx.obj.print(kh.login(username, password))
105117

106118

@@ -109,23 +121,24 @@ def login(ctx: click.Context, username: str, password: str):
109121
@click.option('--password', '-p', default=None, help='Password to login with.')
110122
@click.option('--auth-token', '-t', default=None, help='Authorization token.')
111123
@click.pass_context
112-
def devices(ctx: click.Context, username: str, password: str, auth_token: str):
124+
@coro
125+
async def devices(ctx: click.Context, username: str, password: str, auth_token: str):
113126
"""List all devices."""
114127

115-
kh = KarcherHome(region=ctx.obj.region)
128+
kh = await KarcherHome.create(region=ctx.obj.region)
116129
if auth_token is not None:
117130
kh.login_token(auth_token, '')
118131
elif username is not None and password is not None:
119-
kh.login(username, password)
132+
await kh.login(username, password)
120133
else:
121134
raise click.BadParameter(
122135
'Must provide either token or username and password.')
123136

124-
devices = kh.get_devices()
137+
devices = await kh.get_devices()
125138

126139
# Logout if we used a username and password
127140
if auth_token is None:
128-
kh.logout()
141+
await kh.logout()
129142

130143
ctx.obj.print(devices)
131144

@@ -137,7 +150,8 @@ def devices(ctx: click.Context, username: str, password: str, auth_token: str):
137150
@click.option('--mqtt-token', '-m', default=None, help='MQTT authorization token.')
138151
@click.option('--device-id', '-d', required=True, help='Device ID.')
139152
@click.pass_context
140-
def device_properties(
153+
@coro
154+
async def device_properties(
141155
ctx: click.Context,
142156
username: str,
143157
password: str,
@@ -146,17 +160,17 @@ def device_properties(
146160
device_id: str):
147161
"""Get device properties."""
148162

149-
kh = KarcherHome(region=ctx.obj.region)
163+
kh = await KarcherHome.create(region=ctx.obj.region)
150164
if auth_token is not None:
151165
kh.login_token(auth_token, mqtt_token)
152166
elif username is not None and password is not None:
153-
kh.login(username, password)
167+
await kh.login(username, password)
154168
else:
155169
raise click.BadParameter(
156170
'Must provide either token or username and password.')
157171

158172
dev = None
159-
for device in kh.get_devices():
173+
for device in await kh.get_devices():
160174
if device.device_id == device_id:
161175
dev = device
162176
break
@@ -168,6 +182,6 @@ def device_properties(
168182

169183
# Logout if we used a username and password
170184
if auth_token is None:
171-
kh.logout()
185+
await kh.logout()
172186

173187
ctx.obj.print(props)

karcher/karcher.py

+66-50
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,66 @@
77
import json
88
import threading
99
from typing import List, Any
10-
import requests
10+
import aiohttp
1111
import urllib.parse
1212

1313

1414
from .auth import Domains, Session
1515
from .countries import get_country_code, get_region_by_country
16-
from .consts import APP_VERSION_CODE, APP_VERSION_NAME, PROJECT_TYPE, \
17-
PROTOCOL_VERSION, REGION_URLS, ROBOT_PROPERTIES, TENANT_ID, \
18-
Language
16+
from .consts import (
17+
APP_VERSION_CODE, APP_VERSION_NAME, PROJECT_TYPE,
18+
PROTOCOL_VERSION, REGION_URLS, ROBOT_PROPERTIES, TENANT_ID,
19+
Language, Region
20+
)
1921
from .device import Device, DeviceProperties
2022
from .exception import KarcherHomeAccessDenied, KarcherHomeException, handle_error_code
2123
from .map import Map
2224
from .mqtt import MqttClient, get_device_topic_property_get_reply, get_device_topics
23-
from .utils import decrypt, decrypt_map, encrypt, get_nonce, get_random_string, \
25+
from .utils import (
26+
decrypt, decrypt_map, encrypt, get_nonce, get_random_string,
2427
get_timestamp, get_timestamp_ms, is_email, md5
28+
)
2529

2630

2731
class KarcherHome:
2832
"""Main class to access Karcher Home Robots API"""
2933

30-
def __init__(self, country: str = 'GB', language: Language = Language.EN):
31-
"""Initialize Karcher Home Robots API"""
34+
@classmethod
35+
async def create(cls, country: str = 'GB', language: Language = Language.EN):
36+
"""Create Karcher Home Robots API instance"""
3237

33-
super().__init__()
38+
self = KarcherHome()
3439
self._country = country.upper()
3540
self._base_url = REGION_URLS[get_region_by_country(self._country)]
36-
self._mqtt_url = None
3741
self._language = language
38-
self._session = None
39-
self._mqtt = None
40-
self._device_props = {}
41-
self._wait_events = {}
4242

43-
d = self.get_urls()
43+
d = await self.get_urls()
4444
# Update base URLs
4545
if d.app_api != '':
4646
self._base_url = d.app_api
4747
if d.mqtt != '':
4848
self._mqtt_url = d.mqtt
4949

50-
def _request(self, method: str, url: str, **kwargs) -> requests.Response:
51-
session = requests.Session()
50+
return self
51+
52+
def __init__(self):
53+
"""Initialize Karcher Home Robots API"""
54+
55+
super().__init__()
56+
self._country = 'US'
57+
self._base_url = REGION_URLS[Region.US]
58+
self._mqtt_url = None
59+
self._language = Language.EN
60+
self._session = None
61+
self._mqtt = None
62+
self._device_props = {}
63+
self._wait_events = {}
64+
65+
async def _request(self, method: str, url: str, **kwargs) -> aiohttp.ClientResponse:
66+
session = aiohttp.ClientSession()
5267
# TODO: Fix SSL
53-
requests.packages.urllib3.disable_warnings()
54-
session.verify = False
68+
# requests.packages.urllib3.disable_warnings()
69+
# session.skip = False
5570

5671
headers = {}
5772
if kwargs.get('headers') is not None:
@@ -98,26 +113,27 @@ def _request(self, method: str, url: str, **kwargs) -> requests.Response:
98113
headers['nonce'] = nonce
99114

100115
kwargs['headers'] = headers
101-
return session.request(method, self._base_url + url, **kwargs)
116+
kwargs['verify_ssl'] = False
117+
return await session.request(method, self._base_url + url, **kwargs)
102118

103-
def _download(self, url) -> bytes:
104-
session = requests.Session()
119+
async def _download(self, url) -> bytes:
120+
session = aiohttp.ClientSession()
105121
headers = {
106122
'User-Agent': 'Android_' + TENANT_ID,
107123
}
108124

109-
resp = session.get(url, headers=headers)
110-
if resp.status_code != 200:
125+
resp = await session.get(url, headers=headers)
126+
if resp.status != 200:
111127
raise KarcherHomeException(-1,
112128
'HTTP error: ' + str(resp.status_code))
113129

114-
return resp.content
130+
return await resp.content.read(-1)
115131

116-
def _process_response(self, resp, prop=None) -> Any:
117-
if resp.status_code != 200:
132+
async def _process_response(self, resp: aiohttp.ClientResponse, prop=None) -> Any:
133+
if resp.status != 200:
118134
raise KarcherHomeException(-1,
119-
'HTTP error: ' + str(resp.status_code))
120-
data = resp.json()
135+
'HTTP error: ' + str(resp.status))
136+
data = await resp.json()
121137
# Check for error response
122138
if data['code'] != 0:
123139
handle_error_code(data['code'], data['msg'])
@@ -161,19 +177,19 @@ def _mqtt_connect(self, wait_for_connect=False):
161177
event.wait()
162178
self._mqtt.on_connect = None
163179

164-
def get_urls(self) -> Domains:
180+
async def get_urls(self) -> Domains:
165181
"""Get URLs for API and MQTT."""
166182

167-
resp = self._request('GET', '/network-service/domains/list', params={
183+
resp = await self._request('GET', '/network-service/domains/list', params={
168184
'tenantId': TENANT_ID,
169185
'productModeCode': PROJECT_TYPE,
170186
'version': PROTOCOL_VERSION,
171187
})
172188

173-
d = self._process_response(resp, 'domain')
189+
d = await self._process_response(resp, 'domain')
174190
return Domains(**d)
175191

176-
def login(self, username, password, register_id=None) -> Session:
192+
async def login(self, username, password, register_id=None) -> Session:
177193
"""Login using provided credentials."""
178194

179195
if register_id is None or register_id == '':
@@ -182,7 +198,7 @@ def login(self, username, password, register_id=None) -> Session:
182198
if not is_email(username):
183199
username = '86-' + username
184200

185-
resp = self._request('POST', '/user-center/auth/login', json={
201+
resp = await self._request('POST', '/user-center/auth/login', json={
186202
'tenantId': TENANT_ID,
187203
'lang': str(self._language),
188204
'token': None,
@@ -201,7 +217,7 @@ def login(self, username, password, register_id=None) -> Session:
201217
},
202218
})
203219

204-
d = self._process_response(resp)
220+
d = await self._process_response(resp)
205221
self._session = Session(**d)
206222
self._session.register_id = register_id
207223

@@ -222,7 +238,7 @@ def login_token(
222238

223239
return self._session
224240

225-
def logout(self):
241+
async def logout(self):
226242
"""End current session.
227243
228244
This will also reset the session object.
@@ -232,49 +248,49 @@ def logout(self):
232248
self._session = None
233249
return
234250

235-
self._process_response(self._request(
251+
await self._process_response(await self._request(
236252
'POST', '/user-center/auth/logout'))
237253
self._session = None
238254

239255
if self._mqtt is not None:
240256
self._mqtt.disconnect()
241257
self._mqtt = None
242258

243-
def get_devices(self) -> List[Device]:
259+
async def get_devices(self) -> List[Device]:
244260
"""Get all user devices."""
245261

246262
if self._session is None \
247263
or self._session.auth_token == '' or self._session.user_id == '':
248264
raise KarcherHomeAccessDenied('Not authorized')
249265

250-
resp = self._request(
266+
resp = await self._request(
251267
'GET',
252268
'/smart-home-service/smartHome/user/getDeviceInfoByUserId/'
253269
+ self._session.user_id)
254270

255-
return [Device(**d) for d in self._process_response(resp)]
271+
return [Device(**d) for d in await self._process_response(resp)]
256272

257-
def get_map_data(self, dev: Device, map: int = 1):
273+
async def get_map_data(self, dev: Device, map: int = 1):
258274
# <tenantId>/<modeType>/<deviceSn>/01-01-2022/map/temp/0046690461_<deviceSn>_1
259275
mapDir = TENANT_ID + '/' + dev.product_mode_code + '/' +\
260276
dev.sn + '/01-01-2022/map/temp/0046690461_' + \
261277
dev.sn + '_' + str(map)
262278

263-
resp = self._request('POST',
264-
'/storage-management/storage/aws/getAccessUrl',
265-
json={
266-
'dir': mapDir,
267-
'countryCode': get_country_code(self._country),
268-
'serviceType': 2,
269-
'tenantId': TENANT_ID,
270-
})
279+
resp = await self._request('POST',
280+
'/storage-management/storage/aws/getAccessUrl',
281+
json={
282+
'dir': mapDir,
283+
'countryCode': get_country_code(self._country),
284+
'serviceType': 2,
285+
'tenantId': TENANT_ID,
286+
})
271287

272-
d = self._process_response(resp)
288+
d = await self._process_response(resp)
273289
downloadUrl = d['url']
274290
if 'cdnDomain' in d and d['cdnDomain'] != '':
275291
downloadUrl = 'https://' + d['cdnDomain'] + '/' + d['dir']
276292

277-
d = self._download(downloadUrl)
293+
d = await self._download(downloadUrl)
278294
data = decrypt_map(dev.sn, dev.mac, dev.product_id, d)
279295
if map == 1 or map == 2:
280296
return Map.parse(data)

0 commit comments

Comments
 (0)