diff --git a/README.md b/README.md index c1169fd..0d7bee7 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,23 @@ client = Client(email=os.environ['WYZE_EMAIL'], password=os.environ['WYZE_PASSWO ... ``` +##### Wyze API Key/ID Support + +Visit the Wyze developer API portal to generate an API ID/KEY: https://developer-api-console.wyze.com/#/apikey/view + +```python +import os +from wyze_sdk import Client + +response = Client().login( + email=os.environ['WYZE_EMAIL'], + password=os.environ['WYZE_PASSWORD'], + key_id=os.environ['WYZE_KEY_ID'], + api_key=os.environ['WYZE_API_KEY'] +) +... +``` + ##### Multi-Factor Authentication (2FA) Support If your Wyze account has multi-factor authentication (2FA) enabled, you may be prompted for your 2FA code when authenticating via either supported method described above. If you wish to automate the MFA interaction, both the `Client` constructor and the `login()` method accept `totp_key` as input. If the TOTP key is provided, the MFA prompt should not appear. diff --git a/wyze_sdk/api/client.py b/wyze_sdk/api/client.py index dd0e10f..186453c 100644 --- a/wyze_sdk/api/client.py +++ b/wyze_sdk/api/client.py @@ -43,6 +43,8 @@ def __init__( refresh_token: Optional[str] = None, email: Optional[str] = None, password: Optional[str] = None, + key_id: Optional[str] = None, + api_key: Optional[str] = None, totp_key: Optional[str] = None, base_url: Optional[str] = None, timeout: int = 30, @@ -55,6 +57,10 @@ def __init__( self._email = None if email is None else email.strip() #: An unencrypted string specifying the account password. self._password = None if password is None else password.strip() + # A string used for API-based requests + self._key_id = key_id.strip() if key_id else None + # A string used for API-based requests + self._api_key = api_key.strip() if api_key else None #: An unencrypted string specifying the TOTP Key for automatic TOTP 2FA verification code generation. self._totp_key = None if totp_key is None else totp_key.strip() #: An optional string representing the API base URL. **This should not be used except for when running tests.** @@ -136,8 +142,10 @@ def login( self, email: str = None, password: str = None, + key_id: str = None, + api_key: str = None, totp_key: Optional[str] = None, - ) -> WyzeResponse: + ) -> WyzeResponse: """ Exchanges email and password for an ``access_token`` and a ``refresh_token``, which are stored in this client. The tokens will be used for all subsequent requests @@ -156,13 +164,35 @@ def login( self._email = email.strip() if password is not None: self._password = password.strip() + if key_id is not None: + self._key_id = key_id.strip() + if api_key is not None: + self._api_key = api_key.strip() if totp_key is not None: self._totp_key = totp_key.strip() if self._email is None or self._password is None: raise WyzeClientConfigurationError("must provide email and password") - self._logger.debug(f"access token not provided, attempting to login as {self._email}") - response = self._auth_client().user_login(email=self._email, password=self._password, totp_key=self._totp_key) - self._update_session(access_token=response["access_token"], refresh_token=response["refresh_token"], user_id=response["user_id"]) + if self._key_id is None or self._api_key is None: + raise WyzeClientConfigurationError( + "Must provide a Wyze API key and id.\n\n" + + "As of July 2023, users must provide an api key and key id to create an access token. " + + "For more information, please visit https://support.wyze.com/hc/en-us/articles/16129834216731." + ) + self._logger.debug( + f"access token not provided, attempting to login as {self._email}" + ) + response = self._auth_client().user_login( + email=self._email, + password=self._password, + key_id=self._key_id, + api_key=self._api_key, + totp_key=self._totp_key, + ) + self._update_session( + access_token=response["access_token"], + refresh_token=response["refresh_token"], + user_id=response["user_id"], + ) return response def refresh_token(self) -> WyzeResponse: diff --git a/wyze_sdk/service/auth_service.py b/wyze_sdk/service/auth_service.py index 315d226..587463b 100644 --- a/wyze_sdk/service/auth_service.py +++ b/wyze_sdk/service/auth_service.py @@ -3,6 +3,8 @@ from typing import Dict, Optional from mintotp import totp + +from wyze_sdk import version from wyze_sdk.signature import RequestVerifier from .base import ExServiceClient, WyzeResponse @@ -62,18 +64,32 @@ def api_call( nonce=nonce, ) - def user_login(self, *, email: str, password: str, totp_key: Optional[str] = None, **kwargs) -> WyzeResponse: + def user_login( + self, + *, + email: str, + password: str, + key_id: Optional[str] = None, + api_key: Optional[str] = None, + totp_key: Optional[str] = None, + **kwargs, + ) -> WyzeResponse: nonce = self.request_verifier.clock.nonce() password = self.request_verifier.md5_string( self.request_verifier.md5_string(self.request_verifier.md5_string(password)) ) - kwargs.update({ - 'nonce': str(nonce), - 'email': email, - 'password': password - }) - response = self.api_call('/user/login', json=kwargs, nonce=nonce) - if response['access_token']: + kwargs.update({"nonce": str(nonce), "email": email, "password": password}) + api_headers = { + "keyid": key_id, + "apikey": api_key, + "user-agent": f"wyze-sdk-{version.__version__}", + } + + response = self.api_call( + '/api/user/login', json=kwargs, request_specific_headers=api_headers, nonce=nonce + ) + + if response["access_token"]: return response if 'TotpVerificationCode' in response.get('mfa_options'):