|
1 | 1 | """Library to handle connection with Switchbot Lock.""" |
2 | 2 | from __future__ import annotations |
3 | 3 |
|
4 | | -import base64 |
5 | | -import hashlib |
6 | | -import hmac |
7 | | -import json |
| 4 | +import asyncio |
8 | 5 | import logging |
9 | 6 | import time |
10 | 7 | from typing import Any |
11 | 8 |
|
12 | | -import boto3 |
13 | | -import requests |
| 9 | +import aiohttp |
14 | 10 | from bleak.backends.device import BLEDevice |
15 | 11 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
16 | 12 |
|
17 | | -from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_COGNITO_POOL |
| 13 | +from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID |
18 | 14 | from ..const import ( |
19 | 15 | LockStatus, |
20 | 16 | SwitchbotAccountConnectionError, |
| 17 | + SwitchbotApiError, |
21 | 18 | SwitchbotAuthenticationError, |
22 | 19 | ) |
23 | 20 | from .device import SwitchbotDevice, SwitchbotOperationError |
@@ -86,77 +83,97 @@ async def verify_encryption_key( |
86 | 83 |
|
87 | 84 | return lock_info is not None |
88 | 85 |
|
| 86 | + @staticmethod |
| 87 | + async def api_request( |
| 88 | + session: aiohttp.ClientSession, |
| 89 | + subdomain: str, |
| 90 | + path: str, |
| 91 | + data: dict = None, |
| 92 | + headers: dict = None, |
| 93 | + ) -> dict: |
| 94 | + url = f"https://{subdomain}.{SWITCHBOT_APP_API_BASE_URL}/{path}" |
| 95 | + async with session.post(url, json=data, headers=headers) as result: |
| 96 | + if result.status > 299: |
| 97 | + raise SwitchbotApiError( |
| 98 | + f"Unexpected status code returned by SwitchBot API: {result.status}" |
| 99 | + ) |
| 100 | + |
| 101 | + response = await result.json() |
| 102 | + if response["statusCode"] != 100: |
| 103 | + raise SwitchbotApiError( |
| 104 | + f"{response['message']}, status code: {response['statusCode']}" |
| 105 | + ) |
| 106 | + |
| 107 | + return response["body"] |
| 108 | + |
| 109 | + # Old non-async method preserved for backwards compatibility |
89 | 110 | @staticmethod |
90 | 111 | def retrieve_encryption_key(device_mac: str, username: str, password: str): |
| 112 | + async def async_fn(): |
| 113 | + async with aiohttp.ClientSession() as session: |
| 114 | + return await SwitchbotLock.async_retrieve_encryption_key( |
| 115 | + session, device_mac, username, password |
| 116 | + ) |
| 117 | + |
| 118 | + return asyncio.run(async_fn()) |
| 119 | + |
| 120 | + @staticmethod |
| 121 | + async def async_retrieve_encryption_key( |
| 122 | + session: aiohttp.ClientSession, device_mac: str, username: str, password: str |
| 123 | + ) -> dict: |
91 | 124 | """Retrieve lock key from internal SwitchBot API.""" |
92 | 125 | device_mac = device_mac.replace(":", "").replace("-", "").upper() |
93 | | - msg = bytes(username + SWITCHBOT_APP_COGNITO_POOL["AppClientId"], "utf-8") |
94 | | - secret_hash = base64.b64encode( |
95 | | - hmac.new( |
96 | | - SWITCHBOT_APP_COGNITO_POOL["AppClientSecret"].encode(), |
97 | | - msg, |
98 | | - digestmod=hashlib.sha256, |
99 | | - ).digest() |
100 | | - ).decode() |
101 | | - |
102 | | - cognito_idp_client = boto3.client( |
103 | | - "cognito-idp", region_name=SWITCHBOT_APP_COGNITO_POOL["Region"] |
104 | | - ) |
| 126 | + |
105 | 127 | try: |
106 | | - auth_response = cognito_idp_client.initiate_auth( |
107 | | - ClientId=SWITCHBOT_APP_COGNITO_POOL["AppClientId"], |
108 | | - AuthFlow="USER_PASSWORD_AUTH", |
109 | | - AuthParameters={ |
110 | | - "USERNAME": username, |
111 | | - "PASSWORD": password, |
112 | | - "SECRET_HASH": secret_hash, |
| 128 | + auth_result = await SwitchbotLock.api_request( |
| 129 | + session, |
| 130 | + "account", |
| 131 | + "account/api/v1/user/login", |
| 132 | + { |
| 133 | + "clientId": SWITCHBOT_APP_CLIENT_ID, |
| 134 | + "username": username, |
| 135 | + "password": password, |
| 136 | + "grantType": "password", |
| 137 | + "verifyCode": "", |
113 | 138 | }, |
114 | 139 | ) |
115 | | - except cognito_idp_client.exceptions.NotAuthorizedException as err: |
116 | | - raise SwitchbotAuthenticationError( |
117 | | - f"Failed to authenticate: {err}" |
118 | | - ) from err |
| 140 | + auth_headers = {"authorization": auth_result["access_token"]} |
119 | 141 | except Exception as err: |
120 | | - raise SwitchbotAuthenticationError( |
121 | | - f"Unexpected error during authentication: {err}" |
122 | | - ) from err |
| 142 | + raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err |
123 | 143 |
|
124 | | - if ( |
125 | | - auth_response is None |
126 | | - or "AuthenticationResult" not in auth_response |
127 | | - or "AccessToken" not in auth_response["AuthenticationResult"] |
128 | | - ): |
129 | | - raise SwitchbotAuthenticationError("Unexpected authentication response") |
| 144 | + try: |
| 145 | + userinfo = await SwitchbotLock.api_request( |
| 146 | + session, "account", "account/api/v1/user/userinfo", {}, auth_headers |
| 147 | + ) |
| 148 | + if "botRegion" in userinfo and userinfo["botRegion"] != "": |
| 149 | + region = userinfo["botRegion"] |
| 150 | + else: |
| 151 | + region = "us" |
| 152 | + except Exception as err: |
| 153 | + raise SwitchbotAccountConnectionError( |
| 154 | + f"Failed to retrieve SwitchBot Account user details: {err}" |
| 155 | + ) from err |
130 | 156 |
|
131 | | - access_token = auth_response["AuthenticationResult"]["AccessToken"] |
132 | 157 | try: |
133 | | - key_response = requests.post( |
134 | | - url=SWITCHBOT_APP_API_BASE_URL + "/developStage/keys/v1/communicate", |
135 | | - headers={"authorization": access_token}, |
136 | | - json={ |
| 158 | + device_info = await SwitchbotLock.api_request( |
| 159 | + session, |
| 160 | + f"wonderlabs.{region}", |
| 161 | + "wonder/keys/v1/communicate", |
| 162 | + { |
137 | 163 | "device_mac": device_mac, |
138 | 164 | "keyType": "user", |
139 | 165 | }, |
140 | | - timeout=10, |
| 166 | + auth_headers, |
141 | 167 | ) |
142 | | - except requests.exceptions.RequestException as err: |
| 168 | + |
| 169 | + return { |
| 170 | + "key_id": device_info["communicationKey"]["keyId"], |
| 171 | + "encryption_key": device_info["communicationKey"]["key"], |
| 172 | + } |
| 173 | + except Exception as err: |
143 | 174 | raise SwitchbotAccountConnectionError( |
144 | 175 | f"Failed to retrieve encryption key from SwitchBot Account: {err}" |
145 | 176 | ) from err |
146 | | - if key_response.status_code > 299: |
147 | | - raise SwitchbotAuthenticationError( |
148 | | - f"Unexpected status code returned by SwitchBot Account API: {key_response.status_code}" |
149 | | - ) |
150 | | - key_response_content = json.loads(key_response.content) |
151 | | - if key_response_content["statusCode"] != 100: |
152 | | - raise SwitchbotAuthenticationError( |
153 | | - f"Unexpected status code returned by SwitchBot API: {key_response_content['statusCode']}" |
154 | | - ) |
155 | | - |
156 | | - return { |
157 | | - "key_id": key_response_content["body"]["communicationKey"]["keyId"], |
158 | | - "encryption_key": key_response_content["body"]["communicationKey"]["key"], |
159 | | - } |
160 | 177 |
|
161 | 178 | async def lock(self) -> bool: |
162 | 179 | """Send lock command.""" |
|
0 commit comments