22
33from __future__ import annotations
44
5- import asyncio
65import logging
76import time
87from typing import Any
98
10- import aiohttp
119from bleak .backends .device import BLEDevice
1210from cryptography .hazmat .primitives .ciphers import Cipher , algorithms , modes
1311
14- from ..api_config import SWITCHBOT_APP_API_BASE_URL , SWITCHBOT_APP_CLIENT_ID
15- from ..const import (
16- LockStatus ,
17- SwitchbotAccountConnectionError ,
18- SwitchbotApiError ,
19- SwitchbotAuthenticationError ,
20- SwitchbotModel ,
21- )
22- from .device import SwitchbotDevice , SwitchbotOperationError
12+ from ..const import LockStatus , SwitchbotModel
13+ from .device import SwitchbotEncryptedDevice
2314
2415COMMAND_HEADER = "57"
2516COMMAND_GET_CK_IV = f"{ COMMAND_HEADER } 0f2103"
5445# The return value of the command is 6 when the command is successful but the battery is low.
5546
5647
57- class SwitchbotLock (SwitchbotDevice ):
48+ class SwitchbotLock (SwitchbotEncryptedDevice ):
5849 """Representation of a Switchbot Lock."""
5950
6051 def __init__ (
@@ -66,141 +57,23 @@ def __init__(
6657 model : SwitchbotModel = SwitchbotModel .LOCK ,
6758 ** kwargs : Any ,
6859 ) -> None :
69- if len (key_id ) == 0 :
70- raise ValueError ("key_id is missing" )
71- elif len (key_id ) != 2 :
72- raise ValueError ("key_id is invalid" )
73- if len (encryption_key ) == 0 :
74- raise ValueError ("encryption_key is missing" )
75- elif len (encryption_key ) != 32 :
76- raise ValueError ("encryption_key is invalid" )
7760 if model not in (SwitchbotModel .LOCK , SwitchbotModel .LOCK_PRO ):
7861 raise ValueError ("initializing SwitchbotLock with a non-lock model" )
79- self ._iv = None
80- self ._cipher = None
81- self ._key_id = key_id
82- self ._encryption_key = bytearray .fromhex (encryption_key )
8362 self ._notifications_enabled : bool = False
84- self ._model : SwitchbotModel = model
85- super ().__init__ (device , None , interface , ** kwargs )
63+ super ().__init__ (device , key_id , encryption_key , model , interface , ** kwargs )
8664
87- @staticmethod
65+ @classmethod
8866 async def verify_encryption_key (
67+ cls ,
8968 device : BLEDevice ,
9069 key_id : str ,
9170 encryption_key : str ,
9271 model : SwitchbotModel = SwitchbotModel .LOCK ,
9372 ** kwargs : Any ,
9473 ) -> bool :
95- try :
96- lock = SwitchbotLock (
97- device , key_id = key_id , encryption_key = encryption_key , model = model
98- )
99- except ValueError :
100- return False
101- try :
102- lock_info = await lock .get_basic_info ()
103- except SwitchbotOperationError :
104- return False
105-
106- return lock_info is not None
107-
108- @staticmethod
109- async def api_request (
110- session : aiohttp .ClientSession ,
111- subdomain : str ,
112- path : str ,
113- data : dict = None ,
114- headers : dict = None ,
115- ) -> dict :
116- url = f"https://{ subdomain } .{ SWITCHBOT_APP_API_BASE_URL } /{ path } "
117- async with session .post (
118- url ,
119- json = data ,
120- headers = headers ,
121- timeout = aiohttp .ClientTimeout (total = 10 ),
122- ) as result :
123- if result .status > 299 :
124- raise SwitchbotApiError (
125- f"Unexpected status code returned by SwitchBot API: { result .status } "
126- )
127-
128- response = await result .json ()
129- if response ["statusCode" ] != 100 :
130- raise SwitchbotApiError (
131- f"{ response ['message' ]} , status code: { response ['statusCode' ]} "
132- )
133-
134- return response ["body" ]
135-
136- # Old non-async method preserved for backwards compatibility
137- @staticmethod
138- def retrieve_encryption_key (device_mac : str , username : str , password : str ):
139- async def async_fn ():
140- async with aiohttp .ClientSession () as session :
141- return await SwitchbotLock .async_retrieve_encryption_key (
142- session , device_mac , username , password
143- )
144-
145- return asyncio .run (async_fn ())
146-
147- @staticmethod
148- async def async_retrieve_encryption_key (
149- session : aiohttp .ClientSession , device_mac : str , username : str , password : str
150- ) -> dict :
151- """Retrieve lock key from internal SwitchBot API."""
152- device_mac = device_mac .replace (":" , "" ).replace ("-" , "" ).upper ()
153-
154- try :
155- auth_result = await SwitchbotLock .api_request (
156- session ,
157- "account" ,
158- "account/api/v1/user/login" ,
159- {
160- "clientId" : SWITCHBOT_APP_CLIENT_ID ,
161- "username" : username ,
162- "password" : password ,
163- "grantType" : "password" ,
164- "verifyCode" : "" ,
165- },
166- )
167- auth_headers = {"authorization" : auth_result ["access_token" ]}
168- except Exception as err :
169- raise SwitchbotAuthenticationError (f"Authentication failed: { err } " ) from err
170-
171- try :
172- userinfo = await SwitchbotLock .api_request (
173- session , "account" , "account/api/v1/user/userinfo" , {}, auth_headers
174- )
175- if "botRegion" in userinfo and userinfo ["botRegion" ] != "" :
176- region = userinfo ["botRegion" ]
177- else :
178- region = "us"
179- except Exception as err :
180- raise SwitchbotAccountConnectionError (
181- f"Failed to retrieve SwitchBot Account user details: { err } "
182- ) from err
183-
184- try :
185- device_info = await SwitchbotLock .api_request (
186- session ,
187- f"wonderlabs.{ region } " ,
188- "wonder/keys/v1/communicate" ,
189- {
190- "device_mac" : device_mac ,
191- "keyType" : "user" ,
192- },
193- auth_headers ,
194- )
195-
196- return {
197- "key_id" : device_info ["communicationKey" ]["keyId" ],
198- "encryption_key" : device_info ["communicationKey" ]["key" ],
199- }
200- except Exception as err :
201- raise SwitchbotAccountConnectionError (
202- f"Failed to retrieve encryption key from SwitchBot Account: { err } "
203- ) from err
74+ return super ().verify_encryption_key (
75+ device , key_id , encryption_key , model , ** kwargs
76+ )
20477
20578 async def lock (self ) -> bool :
20679 """Send lock command."""
0 commit comments