aiokwikset 0.1.0b1__tar.gz → 0.2.2a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiokwikset might be problematic. Click here for more details.
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/PKG-INFO +13 -3
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/README.md +12 -2
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/api.py +38 -8
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/aws_kwikset.py +41 -29
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/const.py +2 -1
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/device.py +26 -5
- aiokwikset-0.2.2a1/aiokwikset/errors.py +19 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/exceptions.py +2 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/PKG-INFO +13 -3
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/setup.py +1 -1
- aiokwikset-0.1.0b1/aiokwikset/errors.py +0 -13
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/LICENSE +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/__init__.py +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/client.py +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset/user.py +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/SOURCES.txt +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/dependency_links.txt +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/requires.txt +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/top_level.txt +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/aiokwikset.egg-info/zip-safe +0 -0
- {aiokwikset-0.1.0b1 → aiokwikset-0.2.2a1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aiokwikset
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2a1
|
|
4
4
|
Summary: Python interface for Kwikset Smart Locks
|
|
5
5
|
Home-page: https://github.com/explosivo22/aiokwikset
|
|
6
6
|
Author: Brad Barbour
|
|
@@ -53,7 +53,8 @@ async def main() -> None:
|
|
|
53
53
|
api = API("<EMAIL>")
|
|
54
54
|
|
|
55
55
|
#start auth
|
|
56
|
-
|
|
56
|
+
#<CODE_TYPE> = [phone, email]
|
|
57
|
+
pre_auth = await api.authenticate('<PASSWORD>','<CODE_TYPE>')
|
|
57
58
|
|
|
58
59
|
#MFA verification
|
|
59
60
|
await api.verify_user(pre_auth, input("Code:"))
|
|
@@ -71,7 +72,16 @@ async def main() -> None:
|
|
|
71
72
|
device_info = await api.device.get_device_info(devices[0]['deviceid'])
|
|
72
73
|
|
|
73
74
|
# Lock the specific device
|
|
74
|
-
lock = await api.device.lock_device(
|
|
75
|
+
lock = await api.device.lock_device(device_info, user_info)
|
|
76
|
+
|
|
77
|
+
# Set led status
|
|
78
|
+
led = await api.device.set_ledstatus(device_info, "false")
|
|
79
|
+
|
|
80
|
+
# Set audio status
|
|
81
|
+
audio = await api.device.set_audiostatus(device_info, "false")
|
|
82
|
+
|
|
83
|
+
# Set secure screen status
|
|
84
|
+
screen = await api.device.set_securescreenstatus(device_info, "false")
|
|
75
85
|
|
|
76
86
|
|
|
77
87
|
asyncio.run(main())
|
|
@@ -37,7 +37,8 @@ async def main() -> None:
|
|
|
37
37
|
api = API("<EMAIL>")
|
|
38
38
|
|
|
39
39
|
#start auth
|
|
40
|
-
|
|
40
|
+
#<CODE_TYPE> = [phone, email]
|
|
41
|
+
pre_auth = await api.authenticate('<PASSWORD>','<CODE_TYPE>')
|
|
41
42
|
|
|
42
43
|
#MFA verification
|
|
43
44
|
await api.verify_user(pre_auth, input("Code:"))
|
|
@@ -55,7 +56,16 @@ async def main() -> None:
|
|
|
55
56
|
device_info = await api.device.get_device_info(devices[0]['deviceid'])
|
|
56
57
|
|
|
57
58
|
# Lock the specific device
|
|
58
|
-
lock = await api.device.lock_device(
|
|
59
|
+
lock = await api.device.lock_device(device_info, user_info)
|
|
60
|
+
|
|
61
|
+
# Set led status
|
|
62
|
+
led = await api.device.set_ledstatus(device_info, "false")
|
|
63
|
+
|
|
64
|
+
# Set audio status
|
|
65
|
+
audio = await api.device.set_audiostatus(device_info, "false")
|
|
66
|
+
|
|
67
|
+
# Set secure screen status
|
|
68
|
+
screen = await api.device.set_securescreenstatus(device_info, "false")
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
asyncio.run(main())
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import aioboto3
|
|
3
|
+
import botocore.exceptions
|
|
3
4
|
import attr
|
|
4
5
|
import datetime
|
|
5
6
|
|
|
@@ -17,7 +18,7 @@ from aiohttp.client_exceptions import (
|
|
|
17
18
|
ClientPayloadError
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
from .errors import RequestError
|
|
21
|
+
from .errors import RequestError, NotAuthorized
|
|
21
22
|
from .device import Device
|
|
22
23
|
from .user import User
|
|
23
24
|
from .exceptions import TokenVerificationException
|
|
@@ -169,7 +170,7 @@ class API(object):
|
|
|
169
170
|
if not self.access_token:
|
|
170
171
|
raise AttributeError('Access Token Required to Check Token')
|
|
171
172
|
now = datetime.datetime.now()
|
|
172
|
-
dec_access_token = jwt.get_unverified_claims(self.
|
|
173
|
+
dec_access_token = jwt.get_unverified_claims(self.id_token)
|
|
173
174
|
|
|
174
175
|
if now > datetime.datetime.fromtimestamp(dec_access_token['exp']):
|
|
175
176
|
expired = True
|
|
@@ -185,12 +186,13 @@ class API(object):
|
|
|
185
186
|
"""
|
|
186
187
|
auth_params = {'SECRET_HASH': '', 'REFRESH_TOKEN': self.refresh_token}
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
try:
|
|
190
|
+
async with self.get_client() as client:
|
|
191
|
+
refresh_response = await client.initiate_auth(
|
|
192
|
+
ClientId=self.client_id,
|
|
193
|
+
AuthFlow='REFRESH_TOKEN_AUTH',
|
|
194
|
+
AuthParameters=auth_params,
|
|
195
|
+
)
|
|
194
196
|
|
|
195
197
|
self._set_attributes(
|
|
196
198
|
refresh_response,
|
|
@@ -209,6 +211,11 @@ class API(object):
|
|
|
209
211
|
|
|
210
212
|
if not self.user:
|
|
211
213
|
self.user = User(self._request)
|
|
214
|
+
|
|
215
|
+
#attempt to catch the NotAuthorizedException
|
|
216
|
+
except botocore.exceptions.ClientError as err:
|
|
217
|
+
if err.response['Error']['Code'] == 'NotAuthorizedException':
|
|
218
|
+
raise NotAuthorized("Refresh Token has been revoked.")
|
|
212
219
|
|
|
213
220
|
def _set_attributes(self, response, attribute_dict):
|
|
214
221
|
"""
|
|
@@ -236,7 +243,29 @@ class API(object):
|
|
|
236
243
|
client_id=self.client_id, client=self.get_client(),
|
|
237
244
|
client_secret=self.client_secret)
|
|
238
245
|
|
|
246
|
+
self.code_type = code_type
|
|
247
|
+
|
|
239
248
|
pre_verification = await self.aws.authenticate_user()
|
|
249
|
+
|
|
250
|
+
if pre_verification.get('AuthenticationResult'):
|
|
251
|
+
print("2-step verification disabled")
|
|
252
|
+
await self.verify_token(pre_verification['AuthenticationResult']['IdToken'],
|
|
253
|
+
'id_token', 'id')
|
|
254
|
+
self.refresh_token = pre_verification['AuthenticationResult']['RefreshToken']
|
|
255
|
+
await self.verify_token(pre_verification['AuthenticationResult']['AccessToken'],
|
|
256
|
+
'access_token', 'access')
|
|
257
|
+
self.token_type = pre_verification['AuthenticationResult']['TokenType']
|
|
258
|
+
|
|
259
|
+
if not self.device:
|
|
260
|
+
self.device = Device(self._request)
|
|
261
|
+
|
|
262
|
+
if not self.user:
|
|
263
|
+
self.user = User(self._request)
|
|
264
|
+
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
print("2-step verification enabled")
|
|
268
|
+
|
|
240
269
|
return pre_verification
|
|
241
270
|
|
|
242
271
|
async def verify_user(self, pre_verification, code):
|
|
@@ -249,6 +278,7 @@ class API(object):
|
|
|
249
278
|
Session=pre_verification['Session'],
|
|
250
279
|
ChallengeResponses=challenge_responses)
|
|
251
280
|
|
|
281
|
+
|
|
252
282
|
await self.verify_token(tokens['AuthenticationResult']['IdToken'],
|
|
253
283
|
'id_token', 'id')
|
|
254
284
|
self.refresh_token = tokens['AuthenticationResult']['RefreshToken']
|
|
@@ -6,9 +6,12 @@ import hmac
|
|
|
6
6
|
import re
|
|
7
7
|
|
|
8
8
|
import aioboto3
|
|
9
|
+
import botocore.exceptions
|
|
9
10
|
import os
|
|
10
11
|
import six
|
|
11
12
|
|
|
13
|
+
from .errors import NotAuthorized
|
|
14
|
+
|
|
12
15
|
# https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
|
|
13
16
|
n_hex = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1' + \
|
|
14
17
|
'29024E088A67CC74020BBEA63B139B22514A08798E3404DD' + \
|
|
@@ -240,35 +243,44 @@ class AWSKWIKSET(object):
|
|
|
240
243
|
boto_client = self.client or client
|
|
241
244
|
auth_params = self.get_auth_params()
|
|
242
245
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
|
|
256
|
-
Session=response['Session'],
|
|
257
|
-
ChallengeResponses=challenge_response)
|
|
258
|
-
|
|
259
|
-
if custom_challenge['ChallengeName'] == self.CUSTOM_VERIFIER_CHALLENGE:
|
|
260
|
-
custom_challenge_response = self.process_custom_challenge()
|
|
261
|
-
|
|
262
|
-
custom_challenge_code = await client.respond_to_auth_challenge(
|
|
246
|
+
try:
|
|
247
|
+
async with boto_client as client:
|
|
248
|
+
response = await client.initiate_auth(
|
|
249
|
+
AuthFlow='CUSTOM_AUTH',
|
|
250
|
+
AuthParameters=auth_params,
|
|
251
|
+
ClientId=self.client_id
|
|
252
|
+
)
|
|
253
|
+
if response['ChallengeName'] == self.PASSWORD_VERIFIER_CHALLENGE:
|
|
254
|
+
challenge_response = self.process_challenge(
|
|
255
|
+
response['ChallengeParameters'])
|
|
256
|
+
|
|
257
|
+
custom_challenge = await client.respond_to_auth_challenge(
|
|
263
258
|
ClientId=self.client_id,
|
|
264
|
-
ChallengeName=self.
|
|
265
|
-
Session=
|
|
266
|
-
ChallengeResponses=
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
|
|
260
|
+
Session=response['Session'],
|
|
261
|
+
ChallengeResponses=challenge_response)
|
|
262
|
+
|
|
263
|
+
#2-step verification is disabled
|
|
264
|
+
if custom_challenge.get('AuthenticationResult'):
|
|
265
|
+
return custom_challenge
|
|
266
|
+
|
|
267
|
+
#2-step verification is enabled
|
|
268
|
+
if custom_challenge['ChallengeName'] == self.CUSTOM_VERIFIER_CHALLENGE:
|
|
269
|
+
custom_challenge_response = self.process_custom_challenge()
|
|
270
|
+
|
|
271
|
+
custom_challenge_code = await client.respond_to_auth_challenge(
|
|
272
|
+
ClientId=self.client_id,
|
|
273
|
+
ChallengeName=self.CUSTOM_VERIFIER_CHALLENGE,
|
|
274
|
+
Session=custom_challenge['Session'],
|
|
275
|
+
ChallengeResponses=custom_challenge_response)
|
|
276
|
+
|
|
277
|
+
return custom_challenge_code
|
|
278
|
+
else:
|
|
279
|
+
raise NotImplementedError('The %s challenge is not supported'
|
|
280
|
+
% response['ChallengeName'])
|
|
269
281
|
else:
|
|
270
282
|
raise NotImplementedError('The %s challenge is not supported'
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
283
|
+
% response['ChallengeName'])
|
|
284
|
+
except botocore.exceptions.ClientError as err:
|
|
285
|
+
if err.response['Error']['Code'] == 'NotAuthorizedException':
|
|
286
|
+
raise NotAuthorized("Refresh Token has been revoked")
|
|
@@ -13,4 +13,5 @@ GET_HOMES_URL = 'https://ynk95r1v52.execute-api.us-east-1.amazonaws.com/prod_v1/
|
|
|
13
13
|
GET_HOME_DEVICES_URL = 'https://ynk95r1v52.execute-api.us-east-1.amazonaws.com/prod_v1/homes/%s/devices'
|
|
14
14
|
GET_DEVICE_URL = 'https://ynk95r1v52.execute-api.us-east-1.amazonaws.com/prod_v1/devices_v2/%s'
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
LOCK_COMMAND_URL = 'https://ynk95r1v52.execute-api.us-east-1.amazonaws.com/prod_v1/devices/%s/status'
|
|
17
|
+
ACCESSORY_COMMAND_URL = 'https://ynk95r1v52.execute-api.us-east-1.amazonaws.com/prod_v1/devices/%s/%s'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from typing import Awaitable, Callable
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
from .const import
|
|
5
|
+
from .const import LOCK_COMMAND_URL, ACCESSORY_COMMAND_URL, GET_HOME_DEVICES_URL, GET_DEVICE_URL
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Device(): # pylint: disable=too-few-public-methods
|
|
@@ -39,7 +39,7 @@ class Device(): # pylint: disable=too-few-public-methods
|
|
|
39
39
|
for items in device_info['data']:
|
|
40
40
|
return items
|
|
41
41
|
|
|
42
|
-
async def
|
|
42
|
+
async def _lock_action(self, dev, user, action: str):
|
|
43
43
|
data = json.dumps({
|
|
44
44
|
"action": action,
|
|
45
45
|
"source": "{\"name\":\"%s\",\"device\":\"%s\"}"
|
|
@@ -47,13 +47,34 @@ class Device(): # pylint: disable=too-few-public-methods
|
|
|
47
47
|
|
|
48
48
|
r = await self._request(
|
|
49
49
|
'patch',
|
|
50
|
-
|
|
50
|
+
LOCK_COMMAND_URL % (dev['serialnumber']),
|
|
51
|
+
data=data,
|
|
52
|
+
)
|
|
53
|
+
return r['data']
|
|
54
|
+
|
|
55
|
+
async def _set_action(self, dev, action, status):
|
|
56
|
+
data = json.dumps({
|
|
57
|
+
action: status,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
r = await self._request(
|
|
61
|
+
'patch',
|
|
62
|
+
ACCESSORY_COMMAND_URL % (dev['serialnumber'], action),
|
|
51
63
|
data=data,
|
|
52
64
|
)
|
|
53
65
|
return r['data']
|
|
54
66
|
|
|
55
67
|
async def lock_device(self, dev, user):
|
|
56
|
-
return await self.
|
|
68
|
+
return await self._lock_action(dev, user, 'lock')
|
|
57
69
|
|
|
58
70
|
async def unlock_device(self, dev, user):
|
|
59
|
-
return await self.
|
|
71
|
+
return await self._lock_action(dev, user, 'unlock')
|
|
72
|
+
|
|
73
|
+
async def set_ledstatus(self, dev, status):
|
|
74
|
+
return await self._set_action(dev, 'ledstatus', status)
|
|
75
|
+
|
|
76
|
+
async def set_audiostatus(self, dev, status):
|
|
77
|
+
return await self._set_action(dev, 'audiostatus', status)
|
|
78
|
+
|
|
79
|
+
async def set_securescreenstatus(self, dev, status):
|
|
80
|
+
return await self._set_action(dev, 'securescreenstatus', status)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Define package errors."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class KwiksetError(Exception):
|
|
5
|
+
"""Define a base error."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RequestError(KwiksetError):
|
|
11
|
+
"""Define an error related to invalid requests."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class NotAuthorized(Exception):
|
|
16
|
+
"""Raised when the refresh token has been revoked"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args: Any) -> None:
|
|
19
|
+
Exception.__init__(self, *args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aiokwikset
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2a1
|
|
4
4
|
Summary: Python interface for Kwikset Smart Locks
|
|
5
5
|
Home-page: https://github.com/explosivo22/aiokwikset
|
|
6
6
|
Author: Brad Barbour
|
|
@@ -53,7 +53,8 @@ async def main() -> None:
|
|
|
53
53
|
api = API("<EMAIL>")
|
|
54
54
|
|
|
55
55
|
#start auth
|
|
56
|
-
|
|
56
|
+
#<CODE_TYPE> = [phone, email]
|
|
57
|
+
pre_auth = await api.authenticate('<PASSWORD>','<CODE_TYPE>')
|
|
57
58
|
|
|
58
59
|
#MFA verification
|
|
59
60
|
await api.verify_user(pre_auth, input("Code:"))
|
|
@@ -71,7 +72,16 @@ async def main() -> None:
|
|
|
71
72
|
device_info = await api.device.get_device_info(devices[0]['deviceid'])
|
|
72
73
|
|
|
73
74
|
# Lock the specific device
|
|
74
|
-
lock = await api.device.lock_device(
|
|
75
|
+
lock = await api.device.lock_device(device_info, user_info)
|
|
76
|
+
|
|
77
|
+
# Set led status
|
|
78
|
+
led = await api.device.set_ledstatus(device_info, "false")
|
|
79
|
+
|
|
80
|
+
# Set audio status
|
|
81
|
+
audio = await api.device.set_audiostatus(device_info, "false")
|
|
82
|
+
|
|
83
|
+
# Set secure screen status
|
|
84
|
+
screen = await api.device.set_securescreenstatus(device_info, "false")
|
|
75
85
|
|
|
76
86
|
|
|
77
87
|
asyncio.run(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|