|
@@ -1,23 +1,20 @@
|
|
|
"""Library to handle connection with Switchbot Lock."""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
-import base64
|
|
|
-import hashlib
|
|
|
-import hmac
|
|
|
-import json
|
|
|
+import asyncio
|
|
|
import logging
|
|
|
import time
|
|
|
from typing import Any
|
|
|
|
|
|
-import boto3
|
|
|
-import requests
|
|
|
+import aiohttp
|
|
|
from bleak.backends.device import BLEDevice
|
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
|
|
-from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_COGNITO_POOL
|
|
|
+from ..api_config import SWITCHBOT_APP_API_BASE_URL, SWITCHBOT_APP_CLIENT_ID
|
|
|
from ..const import (
|
|
|
LockStatus,
|
|
|
SwitchbotAccountConnectionError,
|
|
|
+ SwitchbotApiError,
|
|
|
SwitchbotAuthenticationError,
|
|
|
)
|
|
|
from .device import SwitchbotDevice, SwitchbotOperationError
|
|
@@ -86,77 +83,97 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
|
|
|
return lock_info is not None
|
|
|
|
|
|
+ @staticmethod
|
|
|
+ async def api_request(
|
|
|
+ session: aiohttp.ClientSession,
|
|
|
+ subdomain: str,
|
|
|
+ path: str,
|
|
|
+ data: dict = None,
|
|
|
+ headers: dict = None,
|
|
|
+ ) -> dict:
|
|
|
+ url = f"https://{subdomain}.{SWITCHBOT_APP_API_BASE_URL}/{path}"
|
|
|
+ async with session.post(url, json=data, headers=headers) as result:
|
|
|
+ if result.status > 299:
|
|
|
+ raise SwitchbotApiError(
|
|
|
+ f"Unexpected status code returned by SwitchBot API: {result.status}"
|
|
|
+ )
|
|
|
+
|
|
|
+ response = await result.json()
|
|
|
+ if response["statusCode"] != 100:
|
|
|
+ raise SwitchbotApiError(
|
|
|
+ f"{response['message']}, status code: {response['statusCode']}"
|
|
|
+ )
|
|
|
+
|
|
|
+ return response["body"]
|
|
|
+
|
|
|
+ # Old non-async method preserved for backwards compatibility
|
|
|
@staticmethod
|
|
|
def retrieve_encryption_key(device_mac: str, username: str, password: str):
|
|
|
+ async def async_fn():
|
|
|
+ async with aiohttp.ClientSession() as session:
|
|
|
+ return await SwitchbotLock.async_retrieve_encryption_key(
|
|
|
+ session, device_mac, username, password
|
|
|
+ )
|
|
|
+
|
|
|
+ return asyncio.run(async_fn())
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ async def async_retrieve_encryption_key(
|
|
|
+ session: aiohttp.ClientSession, device_mac: str, username: str, password: str
|
|
|
+ ) -> dict:
|
|
|
"""Retrieve lock key from internal SwitchBot API."""
|
|
|
device_mac = device_mac.replace(":", "").replace("-", "").upper()
|
|
|
- msg = bytes(username + SWITCHBOT_APP_COGNITO_POOL["AppClientId"], "utf-8")
|
|
|
- secret_hash = base64.b64encode(
|
|
|
- hmac.new(
|
|
|
- SWITCHBOT_APP_COGNITO_POOL["AppClientSecret"].encode(),
|
|
|
- msg,
|
|
|
- digestmod=hashlib.sha256,
|
|
|
- ).digest()
|
|
|
- ).decode()
|
|
|
-
|
|
|
- cognito_idp_client = boto3.client(
|
|
|
- "cognito-idp", region_name=SWITCHBOT_APP_COGNITO_POOL["Region"]
|
|
|
- )
|
|
|
+
|
|
|
try:
|
|
|
- auth_response = cognito_idp_client.initiate_auth(
|
|
|
- ClientId=SWITCHBOT_APP_COGNITO_POOL["AppClientId"],
|
|
|
- AuthFlow="USER_PASSWORD_AUTH",
|
|
|
- AuthParameters={
|
|
|
- "USERNAME": username,
|
|
|
- "PASSWORD": password,
|
|
|
- "SECRET_HASH": secret_hash,
|
|
|
+ auth_result = await SwitchbotLock.api_request(
|
|
|
+ session,
|
|
|
+ "account",
|
|
|
+ "account/api/v1/user/login",
|
|
|
+ {
|
|
|
+ "clientId": SWITCHBOT_APP_CLIENT_ID,
|
|
|
+ "username": username,
|
|
|
+ "password": password,
|
|
|
+ "grantType": "password",
|
|
|
+ "verifyCode": "",
|
|
|
},
|
|
|
)
|
|
|
- except cognito_idp_client.exceptions.NotAuthorizedException as err:
|
|
|
- raise SwitchbotAuthenticationError(
|
|
|
- f"Failed to authenticate: {err}"
|
|
|
- ) from err
|
|
|
+ auth_headers = {"authorization": auth_result["access_token"]}
|
|
|
except Exception as err:
|
|
|
- raise SwitchbotAuthenticationError(
|
|
|
- f"Unexpected error during authentication: {err}"
|
|
|
- ) from err
|
|
|
+ raise SwitchbotAuthenticationError(f"Authentication failed: {err}") from err
|
|
|
|
|
|
- if (
|
|
|
- auth_response is None
|
|
|
- or "AuthenticationResult" not in auth_response
|
|
|
- or "AccessToken" not in auth_response["AuthenticationResult"]
|
|
|
- ):
|
|
|
- raise SwitchbotAuthenticationError("Unexpected authentication response")
|
|
|
+ try:
|
|
|
+ userinfo = await SwitchbotLock.api_request(
|
|
|
+ session, "account", "account/api/v1/user/userinfo", {}, auth_headers
|
|
|
+ )
|
|
|
+ if "botRegion" in userinfo and userinfo["botRegion"] != "":
|
|
|
+ region = userinfo["botRegion"]
|
|
|
+ else:
|
|
|
+ region = "us"
|
|
|
+ except Exception as err:
|
|
|
+ raise SwitchbotAccountConnectionError(
|
|
|
+ f"Failed to retrieve SwitchBot Account user details: {err}"
|
|
|
+ ) from err
|
|
|
|
|
|
- access_token = auth_response["AuthenticationResult"]["AccessToken"]
|
|
|
try:
|
|
|
- key_response = requests.post(
|
|
|
- url=SWITCHBOT_APP_API_BASE_URL + "/developStage/keys/v1/communicate",
|
|
|
- headers={"authorization": access_token},
|
|
|
- json={
|
|
|
+ device_info = await SwitchbotLock.api_request(
|
|
|
+ session,
|
|
|
+ f"wonderlabs.{region}",
|
|
|
+ "wonder/keys/v1/communicate",
|
|
|
+ {
|
|
|
"device_mac": device_mac,
|
|
|
"keyType": "user",
|
|
|
},
|
|
|
- timeout=10,
|
|
|
+ auth_headers,
|
|
|
)
|
|
|
- except requests.exceptions.RequestException as err:
|
|
|
+
|
|
|
+ return {
|
|
|
+ "key_id": device_info["communicationKey"]["keyId"],
|
|
|
+ "encryption_key": device_info["communicationKey"]["key"],
|
|
|
+ }
|
|
|
+ except Exception as err:
|
|
|
raise SwitchbotAccountConnectionError(
|
|
|
f"Failed to retrieve encryption key from SwitchBot Account: {err}"
|
|
|
) from err
|
|
|
- if key_response.status_code > 299:
|
|
|
- raise SwitchbotAuthenticationError(
|
|
|
- f"Unexpected status code returned by SwitchBot Account API: {key_response.status_code}"
|
|
|
- )
|
|
|
- key_response_content = json.loads(key_response.content)
|
|
|
- if key_response_content["statusCode"] != 100:
|
|
|
- raise SwitchbotAuthenticationError(
|
|
|
- f"Unexpected status code returned by SwitchBot API: {key_response_content['statusCode']}"
|
|
|
- )
|
|
|
-
|
|
|
- return {
|
|
|
- "key_id": key_response_content["body"]["communicationKey"]["keyId"],
|
|
|
- "encryption_key": key_response_content["body"]["communicationKey"]["key"],
|
|
|
- }
|
|
|
|
|
|
async def lock(self) -> bool:
|
|
|
"""Send lock command."""
|