wyzeapy.wyze_auth_lib API documentation

# wyzeapy.wyze_auth_lib

View Source

      1#  Copyright (c) 2021. Mulliken, LLC - All Rights Reserved
      2#  You may use, distribute and modify this code under the terms
      3#  of the attached license. You should have received a copy of
      4#  the license with this file. If not, please write to:
      5#  katie@mulliken.net to receive a copy
      6import asyncio
      7import logging
      8import time
      9from typing import Dict, Any, Optional
     10
     11from aiohttp import TCPConnector, ClientSession, ContentTypeError
     12
     13from .const import API_KEY, PHONE_ID, APP_NAME, APP_VERSION, SC, SV, PHONE_SYSTEM_TYPE, APP_VER, APP_INFO
     14from .exceptions import (
     15    UnknownApiError,
     16    TwoFactorAuthenticationEnabled,
     17    AccessTokenError,
     18)
     19from .utils import create_password, check_for_errors_standard
     20
     21_LOGGER = logging.getLogger(__name__)
     22
     23
     24class Token:
     25    # Token is apparently good for 24 hours, so refresh after 23
     26    REFRESH_INTERVAL = 82800
     27
     28    def __init__(self, access_token, refresh_token, refresh_time: float = None):
     29        self._access_token: str = access_token
     30        self._refresh_token: str = refresh_token
     31        self.expired = False
     32        if refresh_time:
     33            self._refresh_time: float = refresh_time
     34        else:
     35            self._refresh_time: float = time.time() + Token.REFRESH_INTERVAL
     36
     37    @property
     38    def access_token(self):
     39        return self._access_token
     40
     41    @access_token.setter
     42    def access_token(self, access_token):
     43        self._access_token = access_token
     44        self._refresh_time = time.time() + Token.REFRESH_INTERVAL
     45
     46    @property
     47    def refresh_token(self):
     48        return self._refresh_token
     49
     50    @refresh_token.setter
     51    def refresh_token(self, refresh_token):
     52        self._refresh_token = refresh_token
     53
     54    @property
     55    def refresh_time(self):
     56        return self._refresh_time
     57
     58
     59class WyzeAuthLib:
     60    token: Optional[Token] = None
     61    SANITIZE_FIELDS = [
     62        "email",
     63        "password",
     64        "access_token",
     65        "accessToken",
     66        "refresh_token",
     67        "lat",
     68        "lon",
     69        "address",
     70    ]
     71    SANITIZE_STRING = "**Sanitized**"
     72
     73    def __init__(
     74        self,
     75        username=None,
     76        password=None,
     77        key_id=None,
     78        api_key=None,
     79        token: Optional[Token] = None,
     80        token_callback=None,
     81    ):
     82        self._username = username
     83        self._password = password
     84        self._key_id = key_id
     85        self._api_key = api_key
     86        self.token = token
     87        self.session_id = ""
     88        self.verification_id = ""
     89        self.two_factor_type = None
     90        self.refresh_lock = asyncio.Lock()
     91        self.token_callback = token_callback
     92
     93    @classmethod
     94    async def create(
     95        cls,
     96        username=None,
     97        password=None,
     98        key_id=None,
     99        api_key=None,
    100        token: Optional[Token] = None,
    101        token_callback=None,
    102    ):
    103        self = cls(
    104            username=username,
    105            password=password,
    106            key_id=key_id,
    107            api_key=api_key,
    108            token=token,
    109            token_callback=token_callback,
    110        )
    111
    112        if self._username is None and self._password is None and self.token is None:
    113            raise AttributeError("Must provide a username, password or token")
    114        elif self.token is None and self._username is not None and self._password is not None:
    115            assert self._username != ""
    116            assert self._password != ""
    117
    118        return self
    119
    120    async def get_token_with_username_password(
    121        self, username, password, key_id, api_key
    122    ) -> Token:
    123        self._username = username
    124        self._password = create_password(password)
    125        self._key_id = key_id
    126        self._api_key = api_key
    127        login_payload = {"email": self._username, "password": self._password}
    128
    129        headers = {
    130            "keyid": key_id,
    131            "apikey": api_key,
    132            "User-Agent": "wyzeapy",
    133        }
    134
    135        response_json = await self.post(
    136            "https://auth-prod.api.wyze.com/api/user/login",
    137            headers=headers,
    138            json=login_payload,
    139        )
    140
    141        if response_json.get('errorCode') is not None:
    142            _LOGGER.error(f"Unable to login with response from Wyze: {response_json}")
    143            if response_json["errorCode"] == 1000:
    144                raise AccessTokenError
    145            raise UnknownApiError(response_json)
    146
    147        if response_json.get('mfa_options') is not None:
    148            # Store the TOTP verification setting in the token and raise exception
    149            if "TotpVerificationCode" in response_json.get("mfa_options"):
    150                self.two_factor_type = "TOTP"
    151                # Store the verification_id from the response, it's needed for the 2fa payload.
    152                self.verification_id = response_json["mfa_details"]["totp_apps"][0]["app_id"]
    153                raise TwoFactorAuthenticationEnabled
    154                # 2fa using SMS, store sms as 2fa method in token, send the code then raise exception
    155            if "PrimaryPhone" in response_json.get("mfa_options"):
    156                self.two_factor_type = "SMS"
    157                params = {
    158                    'mfaPhoneType': 'Primary',
    159                    'sessionId': response_json.get("sms_session_id"),
    160                    'userId': response_json['user_id'],
    161                }
    162                response_json = await self.post('https://auth-prod.api.wyze.com/user/login/sendSmsCode',
    163                                                headers=headers, data=params)
    164                # Store the session_id from this response, it's needed for the 2fa payload.
    165                self.session_id = response_json['session_id']
    166                raise TwoFactorAuthenticationEnabled
    167
    168        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    169        await self.token_callback(self.token)
    170        return self.token
    171
    172    async def get_token_with_2fa(self, verification_code) -> Token:
    173        headers = {
    174            'Phone-Id': PHONE_ID,
    175            'User-Agent': APP_INFO,
    176            'X-API-Key': API_KEY,
    177        }
    178        # TOTP Payload
    179        if self.two_factor_type == "TOTP":
    180            payload = {
    181                "email": self._username,
    182                "password": self._password,
    183                "mfa_type": "TotpVerificationCode",
    184                "verification_id": self.verification_id,
    185                "verification_code": verification_code
    186            }
    187        # SMS Payload
    188        else:
    189            payload = {
    190                "email": self._username,
    191                "password": self._password,
    192                "mfa_type": "PrimaryPhone",
    193                "verification_id": self.session_id,
    194                "verification_code": verification_code
    195            }
    196
    197        response_json = await self.post(
    198            'https://auth-prod.api.wyze.com/user/login',
    199            headers=headers, json=payload)
    200
    201        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    202        await self.token_callback(self.token)
    203        return self.token
    204
    205    @property
    206    def should_refresh(self) -> bool:
    207        return time.time() >= self.token.refresh_time
    208
    209    async def refresh_if_should(self):
    210        if self.should_refresh or self.token.expired:
    211            async with self.refresh_lock:
    212                if self.should_refresh or self.token.expired:
    213                    _LOGGER.debug("Should refresh. Refreshing...")
    214                    await self.refresh()
    215
    216    async def refresh(self) -> None:
    217        payload = {
    218            "phone_id": PHONE_ID,
    219            "app_name": APP_NAME,
    220            "app_version": APP_VERSION,
    221            "sc": SC,
    222            "sv": SV,
    223            "phone_system_type": PHONE_SYSTEM_TYPE,
    224            "app_ver": APP_VER,
    225            "ts": int(time.time()),
    226            "refresh_token": self.token.refresh_token
    227        }
    228
    229        headers = {
    230            "X-API-Key": API_KEY
    231        }
    232
    233        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    234            response = await _session.post("https://api.wyzecam.com/app/user/refresh_token", headers=headers,
    235                                           json=payload)
    236        response_json = await response.json()
    237        check_for_errors_standard(self, response_json)
    238
    239        self.token.access_token = response_json['data']['access_token']
    240        self.token.refresh_token = response_json['data']['refresh_token']
    241        await self.token_callback(self.token)
    242        self.token.expired = False
    243
    244    def sanitize(self, data):
    245        if data and type(data) is dict:
    246            # value is unused, but it prevents us from having to split the tuple to check against SANITIZE_FIELDS
    247            for key, value in data.items():
    248                if type(value) is dict:
    249                    data[key] = self.sanitize(value)
    250                if key in self.SANITIZE_FIELDS:
    251                    data[key] = self.SANITIZE_STRING
    252        return data
    253
    254    async def post(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    255        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    256            response = await _session.post(url, json=json, headers=headers, data=data)
    257            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    258            _LOGGER.debug("Request:")
    259            _LOGGER.debug(f"url: {url}")
    260            _LOGGER.debug(f"json: {self.sanitize(json)}")
    261            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    262            _LOGGER.debug(f"data: {self.sanitize(data)}")
    263            # Log the response.json() if it exists, if not log the response.
    264            try:
    265                response_json = await response.json()
    266                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    267            except ContentTypeError:
    268                _LOGGER.debug(f"Response: {response}")
    269            return await response.json()
    270
    271    async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    272        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    273            response = await _session.put(url, json=json, headers=headers, data=data)
    274            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    275            _LOGGER.debug("Request:")
    276            _LOGGER.debug(f"url: {url}")
    277            _LOGGER.debug(f"json: {self.sanitize(json)}")
    278            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    279            _LOGGER.debug(f"data: {self.sanitize(data)}")
    280            # Log the response.json() if it exists, if not log the response.
    281            try:
    282                response_json = await response.json()
    283                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    284            except ContentTypeError:
    285                _LOGGER.debug(f"Response: {response}")
    286            return await response.json()
    287
    288    async def get(self, url, headers=None, params=None) -> Dict[Any, Any]:
    289        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    290            response = await _session.get(url, params=params, headers=headers)
    291            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    292            _LOGGER.debug("Request:")
    293            _LOGGER.debug(f"url: {url}")
    294            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    295            _LOGGER.debug(f"params: {self.sanitize(params)}")
    296            # Log the response.json() if it exists, if not log the response.
    297            try:
    298                response_json = await response.json()
    299                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    300            except ContentTypeError:
    301                _LOGGER.debug(f"Response: {response}")
    302            return await response.json()
    303
    304    async def patch(self, url, headers=None, params=None, json=None) -> Dict[Any, Any]:
    305        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    306            response = await _session.patch(url, headers=headers, params=params, json=json)
    307            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    308            _LOGGER.debug("Request:")
    309            _LOGGER.debug(f"url: {url}")
    310            _LOGGER.debug(f"json: {self.sanitize(json)}")
    311            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    312            _LOGGER.debug(f"params: {self.sanitize(params)}")
    313            # Log the response.json() if it exists, if not log the response.
    314            try:
    315                response_json = await response.json()
    316                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    317            except ContentTypeError:
    318                _LOGGER.debug(f"Response: {response}")
    319            return await response.json()
    320
    321    async def delete(self, url, headers=None, json=None) -> Dict[Any, Any]:
    322        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    323            response = await _session.delete(url, headers=headers, json=json)
    324            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    325            _LOGGER.debug("Request:")
    326            _LOGGER.debug(f"url: {url}")
    327            _LOGGER.debug(f"json: {self.sanitize(json)}")
    328            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    329            # Log the response.json() if it exists, if not log the response.
    330            try:
    331                response_json = await response.json()
    332                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    333            except ContentTypeError:
    334                _LOGGER.debug(f"Response: {response}")
    335            return await response.json()

class Token: View Source

    25class Token:
    26    # Token is apparently good for 24 hours, so refresh after 23
    27    REFRESH_INTERVAL = 82800
    28
    29    def __init__(self, access_token, refresh_token, refresh_time: float = None):
    30        self._access_token: str = access_token
    31        self._refresh_token: str = refresh_token
    32        self.expired = False
    33        if refresh_time:
    34            self._refresh_time: float = refresh_time
    35        else:
    36            self._refresh_time: float = time.time() + Token.REFRESH_INTERVAL
    37
    38    @property
    39    def access_token(self):
    40        return self._access_token
    41
    42    @access_token.setter
    43    def access_token(self, access_token):
    44        self._access_token = access_token
    45        self._refresh_time = time.time() + Token.REFRESH_INTERVAL
    46
    47    @property
    48    def refresh_token(self):
    49        return self._refresh_token
    50
    51    @refresh_token.setter
    52    def refresh_token(self, refresh_token):
    53        self._refresh_token = refresh_token
    54
    55    @property
    56    def refresh_time(self):
    57        return self._refresh_time

Token(access_token, refresh_token, refresh_time: float = None) View Source

    29    def __init__(self, access_token, refresh_token, refresh_time: float = None):
    30        self._access_token: str = access_token
    31        self._refresh_token: str = refresh_token
    32        self.expired = False
    33        if refresh_time:
    34            self._refresh_time: float = refresh_time
    35        else:
    36            self._refresh_time: float = time.time() + Token.REFRESH_INTERVAL

REFRESH_INTERVAL = 82800
expired
access_token View Source

    38    @property
    39    def access_token(self):
    40        return self._access_token

refresh_token View Source

    47    @property
    48    def refresh_token(self):
    49        return self._refresh_token

refresh_time View Source

    55    @property
    56    def refresh_time(self):
    57        return self._refresh_time

class WyzeAuthLib: View Source

     60class WyzeAuthLib:
     61    token: Optional[Token] = None
     62    SANITIZE_FIELDS = [
     63        "email",
     64        "password",
     65        "access_token",
     66        "accessToken",
     67        "refresh_token",
     68        "lat",
     69        "lon",
     70        "address",
     71    ]
     72    SANITIZE_STRING = "**Sanitized**"
     73
     74    def __init__(
     75        self,
     76        username=None,
     77        password=None,
     78        key_id=None,
     79        api_key=None,
     80        token: Optional[Token] = None,
     81        token_callback=None,
     82    ):
     83        self._username = username
     84        self._password = password
     85        self._key_id = key_id
     86        self._api_key = api_key
     87        self.token = token
     88        self.session_id = ""
     89        self.verification_id = ""
     90        self.two_factor_type = None
     91        self.refresh_lock = asyncio.Lock()
     92        self.token_callback = token_callback
     93
     94    @classmethod
     95    async def create(
     96        cls,
     97        username=None,
     98        password=None,
     99        key_id=None,
    100        api_key=None,
    101        token: Optional[Token] = None,
    102        token_callback=None,
    103    ):
    104        self = cls(
    105            username=username,
    106            password=password,
    107            key_id=key_id,
    108            api_key=api_key,
    109            token=token,
    110            token_callback=token_callback,
    111        )
    112
    113        if self._username is None and self._password is None and self.token is None:
    114            raise AttributeError("Must provide a username, password or token")
    115        elif self.token is None and self._username is not None and self._password is not None:
    116            assert self._username != ""
    117            assert self._password != ""
    118
    119        return self
    120
    121    async def get_token_with_username_password(
    122        self, username, password, key_id, api_key
    123    ) -> Token:
    124        self._username = username
    125        self._password = create_password(password)
    126        self._key_id = key_id
    127        self._api_key = api_key
    128        login_payload = {"email": self._username, "password": self._password}
    129
    130        headers = {
    131            "keyid": key_id,
    132            "apikey": api_key,
    133            "User-Agent": "wyzeapy",
    134        }
    135
    136        response_json = await self.post(
    137            "https://auth-prod.api.wyze.com/api/user/login",
    138            headers=headers,
    139            json=login_payload,
    140        )
    141
    142        if response_json.get('errorCode') is not None:
    143            _LOGGER.error(f"Unable to login with response from Wyze: {response_json}")
    144            if response_json["errorCode"] == 1000:
    145                raise AccessTokenError
    146            raise UnknownApiError(response_json)
    147
    148        if response_json.get('mfa_options') is not None:
    149            # Store the TOTP verification setting in the token and raise exception
    150            if "TotpVerificationCode" in response_json.get("mfa_options"):
    151                self.two_factor_type = "TOTP"
    152                # Store the verification_id from the response, it's needed for the 2fa payload.
    153                self.verification_id = response_json["mfa_details"]["totp_apps"][0]["app_id"]
    154                raise TwoFactorAuthenticationEnabled
    155                # 2fa using SMS, store sms as 2fa method in token, send the code then raise exception
    156            if "PrimaryPhone" in response_json.get("mfa_options"):
    157                self.two_factor_type = "SMS"
    158                params = {
    159                    'mfaPhoneType': 'Primary',
    160                    'sessionId': response_json.get("sms_session_id"),
    161                    'userId': response_json['user_id'],
    162                }
    163                response_json = await self.post('https://auth-prod.api.wyze.com/user/login/sendSmsCode',
    164                                                headers=headers, data=params)
    165                # Store the session_id from this response, it's needed for the 2fa payload.
    166                self.session_id = response_json['session_id']
    167                raise TwoFactorAuthenticationEnabled
    168
    169        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    170        await self.token_callback(self.token)
    171        return self.token
    172
    173    async def get_token_with_2fa(self, verification_code) -> Token:
    174        headers = {
    175            'Phone-Id': PHONE_ID,
    176            'User-Agent': APP_INFO,
    177            'X-API-Key': API_KEY,
    178        }
    179        # TOTP Payload
    180        if self.two_factor_type == "TOTP":
    181            payload = {
    182                "email": self._username,
    183                "password": self._password,
    184                "mfa_type": "TotpVerificationCode",
    185                "verification_id": self.verification_id,
    186                "verification_code": verification_code
    187            }
    188        # SMS Payload
    189        else:
    190            payload = {
    191                "email": self._username,
    192                "password": self._password,
    193                "mfa_type": "PrimaryPhone",
    194                "verification_id": self.session_id,
    195                "verification_code": verification_code
    196            }
    197
    198        response_json = await self.post(
    199            'https://auth-prod.api.wyze.com/user/login',
    200            headers=headers, json=payload)
    201
    202        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    203        await self.token_callback(self.token)
    204        return self.token
    205
    206    @property
    207    def should_refresh(self) -> bool:
    208        return time.time() >= self.token.refresh_time
    209
    210    async def refresh_if_should(self):
    211        if self.should_refresh or self.token.expired:
    212            async with self.refresh_lock:
    213                if self.should_refresh or self.token.expired:
    214                    _LOGGER.debug("Should refresh. Refreshing...")
    215                    await self.refresh()
    216
    217    async def refresh(self) -> None:
    218        payload = {
    219            "phone_id": PHONE_ID,
    220            "app_name": APP_NAME,
    221            "app_version": APP_VERSION,
    222            "sc": SC,
    223            "sv": SV,
    224            "phone_system_type": PHONE_SYSTEM_TYPE,
    225            "app_ver": APP_VER,
    226            "ts": int(time.time()),
    227            "refresh_token": self.token.refresh_token
    228        }
    229
    230        headers = {
    231            "X-API-Key": API_KEY
    232        }
    233
    234        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    235            response = await _session.post("https://api.wyzecam.com/app/user/refresh_token", headers=headers,
    236                                           json=payload)
    237        response_json = await response.json()
    238        check_for_errors_standard(self, response_json)
    239
    240        self.token.access_token = response_json['data']['access_token']
    241        self.token.refresh_token = response_json['data']['refresh_token']
    242        await self.token_callback(self.token)
    243        self.token.expired = False
    244
    245    def sanitize(self, data):
    246        if data and type(data) is dict:
    247            # value is unused, but it prevents us from having to split the tuple to check against SANITIZE_FIELDS
    248            for key, value in data.items():
    249                if type(value) is dict:
    250                    data[key] = self.sanitize(value)
    251                if key in self.SANITIZE_FIELDS:
    252                    data[key] = self.SANITIZE_STRING
    253        return data
    254
    255    async def post(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    256        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    257            response = await _session.post(url, json=json, headers=headers, data=data)
    258            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    259            _LOGGER.debug("Request:")
    260            _LOGGER.debug(f"url: {url}")
    261            _LOGGER.debug(f"json: {self.sanitize(json)}")
    262            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    263            _LOGGER.debug(f"data: {self.sanitize(data)}")
    264            # Log the response.json() if it exists, if not log the response.
    265            try:
    266                response_json = await response.json()
    267                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    268            except ContentTypeError:
    269                _LOGGER.debug(f"Response: {response}")
    270            return await response.json()
    271
    272    async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    273        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    274            response = await _session.put(url, json=json, headers=headers, data=data)
    275            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    276            _LOGGER.debug("Request:")
    277            _LOGGER.debug(f"url: {url}")
    278            _LOGGER.debug(f"json: {self.sanitize(json)}")
    279            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    280            _LOGGER.debug(f"data: {self.sanitize(data)}")
    281            # Log the response.json() if it exists, if not log the response.
    282            try:
    283                response_json = await response.json()
    284                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    285            except ContentTypeError:
    286                _LOGGER.debug(f"Response: {response}")
    287            return await response.json()
    288
    289    async def get(self, url, headers=None, params=None) -> Dict[Any, Any]:
    290        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    291            response = await _session.get(url, params=params, headers=headers)
    292            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    293            _LOGGER.debug("Request:")
    294            _LOGGER.debug(f"url: {url}")
    295            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    296            _LOGGER.debug(f"params: {self.sanitize(params)}")
    297            # Log the response.json() if it exists, if not log the response.
    298            try:
    299                response_json = await response.json()
    300                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    301            except ContentTypeError:
    302                _LOGGER.debug(f"Response: {response}")
    303            return await response.json()
    304
    305    async def patch(self, url, headers=None, params=None, json=None) -> Dict[Any, Any]:
    306        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    307            response = await _session.patch(url, headers=headers, params=params, json=json)
    308            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    309            _LOGGER.debug("Request:")
    310            _LOGGER.debug(f"url: {url}")
    311            _LOGGER.debug(f"json: {self.sanitize(json)}")
    312            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    313            _LOGGER.debug(f"params: {self.sanitize(params)}")
    314            # Log the response.json() if it exists, if not log the response.
    315            try:
    316                response_json = await response.json()
    317                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    318            except ContentTypeError:
    319                _LOGGER.debug(f"Response: {response}")
    320            return await response.json()
    321
    322    async def delete(self, url, headers=None, json=None) -> Dict[Any, Any]:
    323        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    324            response = await _session.delete(url, headers=headers, json=json)
    325            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    326            _LOGGER.debug("Request:")
    327            _LOGGER.debug(f"url: {url}")
    328            _LOGGER.debug(f"json: {self.sanitize(json)}")
    329            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    330            # Log the response.json() if it exists, if not log the response.
    331            try:
    332                response_json = await response.json()
    333                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    334            except ContentTypeError:
    335                _LOGGER.debug(f"Response: {response}")
    336            return await response.json()

WyzeAuthLib( username=None, password=None, key_id=None, api_key=None, token: Optional[Token] = None, token_callback=None) View Source

    74    def __init__(
    75        self,
    76        username=None,
    77        password=None,
    78        key_id=None,
    79        api_key=None,
    80        token: Optional[Token] = None,
    81        token_callback=None,
    82    ):
    83        self._username = username
    84        self._password = password
    85        self._key_id = key_id
    86        self._api_key = api_key
    87        self.token = token
    88        self.session_id = ""
    89        self.verification_id = ""
    90        self.two_factor_type = None
    91        self.refresh_lock = asyncio.Lock()
    92        self.token_callback = token_callback

token: Optional[Token] = None
SANITIZE_FIELDS = ['email', 'password', 'access_token', 'accessToken', 'refresh_token', 'lat', 'lon', 'address']
SANITIZE_STRING = '**Sanitized**'
session_id
verification_id
two_factor_type
refresh_lock
token_callback
@classmethod
async def create( cls, username=None, password=None, key_id=None, api_key=None, token: Optional[Token] = None, token_callback=None): View Source

     94    @classmethod
     95    async def create(
     96        cls,
     97        username=None,
     98        password=None,
     99        key_id=None,
    100        api_key=None,
    101        token: Optional[Token] = None,
    102        token_callback=None,
    103    ):
    104        self = cls(
    105            username=username,
    106            password=password,
    107            key_id=key_id,
    108            api_key=api_key,
    109            token=token,
    110            token_callback=token_callback,
    111        )
    112
    113        if self._username is None and self._password is None and self.token is None:
    114            raise AttributeError("Must provide a username, password or token")
    115        elif self.token is None and self._username is not None and self._password is not None:
    116            assert self._username != ""
    117            assert self._password != ""
    118
    119        return self

async def get_token_with_username_password(self, username, password, key_id, api_key) -> Token: View Source

    121    async def get_token_with_username_password(
    122        self, username, password, key_id, api_key
    123    ) -> Token:
    124        self._username = username
    125        self._password = create_password(password)
    126        self._key_id = key_id
    127        self._api_key = api_key
    128        login_payload = {"email": self._username, "password": self._password}
    129
    130        headers = {
    131            "keyid": key_id,
    132            "apikey": api_key,
    133            "User-Agent": "wyzeapy",
    134        }
    135
    136        response_json = await self.post(
    137            "https://auth-prod.api.wyze.com/api/user/login",
    138            headers=headers,
    139            json=login_payload,
    140        )
    141
    142        if response_json.get('errorCode') is not None:
    143            _LOGGER.error(f"Unable to login with response from Wyze: {response_json}")
    144            if response_json["errorCode"] == 1000:
    145                raise AccessTokenError
    146            raise UnknownApiError(response_json)
    147
    148        if response_json.get('mfa_options') is not None:
    149            # Store the TOTP verification setting in the token and raise exception
    150            if "TotpVerificationCode" in response_json.get("mfa_options"):
    151                self.two_factor_type = "TOTP"
    152                # Store the verification_id from the response, it's needed for the 2fa payload.
    153                self.verification_id = response_json["mfa_details"]["totp_apps"][0]["app_id"]
    154                raise TwoFactorAuthenticationEnabled
    155                # 2fa using SMS, store sms as 2fa method in token, send the code then raise exception
    156            if "PrimaryPhone" in response_json.get("mfa_options"):
    157                self.two_factor_type = "SMS"
    158                params = {
    159                    'mfaPhoneType': 'Primary',
    160                    'sessionId': response_json.get("sms_session_id"),
    161                    'userId': response_json['user_id'],
    162                }
    163                response_json = await self.post('https://auth-prod.api.wyze.com/user/login/sendSmsCode',
    164                                                headers=headers, data=params)
    165                # Store the session_id from this response, it's needed for the 2fa payload.
    166                self.session_id = response_json['session_id']
    167                raise TwoFactorAuthenticationEnabled
    168
    169        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    170        await self.token_callback(self.token)
    171        return self.token

async def get_token_with_2fa(self, verification_code) -> Token: View Source

    173    async def get_token_with_2fa(self, verification_code) -> Token:
    174        headers = {
    175            'Phone-Id': PHONE_ID,
    176            'User-Agent': APP_INFO,
    177            'X-API-Key': API_KEY,
    178        }
    179        # TOTP Payload
    180        if self.two_factor_type == "TOTP":
    181            payload = {
    182                "email": self._username,
    183                "password": self._password,
    184                "mfa_type": "TotpVerificationCode",
    185                "verification_id": self.verification_id,
    186                "verification_code": verification_code
    187            }
    188        # SMS Payload
    189        else:
    190            payload = {
    191                "email": self._username,
    192                "password": self._password,
    193                "mfa_type": "PrimaryPhone",
    194                "verification_id": self.session_id,
    195                "verification_code": verification_code
    196            }
    197
    198        response_json = await self.post(
    199            'https://auth-prod.api.wyze.com/user/login',
    200            headers=headers, json=payload)
    201
    202        self.token = Token(response_json['access_token'], response_json['refresh_token'])
    203        await self.token_callback(self.token)
    204        return self.token

should_refresh: bool View Source

    206    @property
    207    def should_refresh(self) -> bool:
    208        return time.time() >= self.token.refresh_time

async def refresh_if_should(self): View Source

    210    async def refresh_if_should(self):
    211        if self.should_refresh or self.token.expired:
    212            async with self.refresh_lock:
    213                if self.should_refresh or self.token.expired:
    214                    _LOGGER.debug("Should refresh. Refreshing...")
    215                    await self.refresh()

async def refresh(self) -> None: View Source

    217    async def refresh(self) -> None:
    218        payload = {
    219            "phone_id": PHONE_ID,
    220            "app_name": APP_NAME,
    221            "app_version": APP_VERSION,
    222            "sc": SC,
    223            "sv": SV,
    224            "phone_system_type": PHONE_SYSTEM_TYPE,
    225            "app_ver": APP_VER,
    226            "ts": int(time.time()),
    227            "refresh_token": self.token.refresh_token
    228        }
    229
    230        headers = {
    231            "X-API-Key": API_KEY
    232        }
    233
    234        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    235            response = await _session.post("https://api.wyzecam.com/app/user/refresh_token", headers=headers,
    236                                           json=payload)
    237        response_json = await response.json()
    238        check_for_errors_standard(self, response_json)
    239
    240        self.token.access_token = response_json['data']['access_token']
    241        self.token.refresh_token = response_json['data']['refresh_token']
    242        await self.token_callback(self.token)
    243        self.token.expired = False

def sanitize(self, data): View Source

    245    def sanitize(self, data):
    246        if data and type(data) is dict:
    247            # value is unused, but it prevents us from having to split the tuple to check against SANITIZE_FIELDS
    248            for key, value in data.items():
    249                if type(value) is dict:
    250                    data[key] = self.sanitize(value)
    251                if key in self.SANITIZE_FIELDS:
    252                    data[key] = self.SANITIZE_STRING
    253        return data

async def post(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]: View Source

    255    async def post(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    256        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    257            response = await _session.post(url, json=json, headers=headers, data=data)
    258            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    259            _LOGGER.debug("Request:")
    260            _LOGGER.debug(f"url: {url}")
    261            _LOGGER.debug(f"json: {self.sanitize(json)}")
    262            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    263            _LOGGER.debug(f"data: {self.sanitize(data)}")
    264            # Log the response.json() if it exists, if not log the response.
    265            try:
    266                response_json = await response.json()
    267                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    268            except ContentTypeError:
    269                _LOGGER.debug(f"Response: {response}")
    270            return await response.json()

async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]: View Source

    272    async def put(self, url, json=None, headers=None, data=None) -> Dict[Any, Any]:
    273        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    274            response = await _session.put(url, json=json, headers=headers, data=data)
    275            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    276            _LOGGER.debug("Request:")
    277            _LOGGER.debug(f"url: {url}")
    278            _LOGGER.debug(f"json: {self.sanitize(json)}")
    279            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    280            _LOGGER.debug(f"data: {self.sanitize(data)}")
    281            # Log the response.json() if it exists, if not log the response.
    282            try:
    283                response_json = await response.json()
    284                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    285            except ContentTypeError:
    286                _LOGGER.debug(f"Response: {response}")
    287            return await response.json()

async def get(self, url, headers=None, params=None) -> Dict[Any, Any]: View Source

    289    async def get(self, url, headers=None, params=None) -> Dict[Any, Any]:
    290        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    291            response = await _session.get(url, params=params, headers=headers)
    292            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    293            _LOGGER.debug("Request:")
    294            _LOGGER.debug(f"url: {url}")
    295            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    296            _LOGGER.debug(f"params: {self.sanitize(params)}")
    297            # Log the response.json() if it exists, if not log the response.
    298            try:
    299                response_json = await response.json()
    300                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    301            except ContentTypeError:
    302                _LOGGER.debug(f"Response: {response}")
    303            return await response.json()

async def patch(self, url, headers=None, params=None, json=None) -> Dict[Any, Any]: View Source

    305    async def patch(self, url, headers=None, params=None, json=None) -> Dict[Any, Any]:
    306        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    307            response = await _session.patch(url, headers=headers, params=params, json=json)
    308            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    309            _LOGGER.debug("Request:")
    310            _LOGGER.debug(f"url: {url}")
    311            _LOGGER.debug(f"json: {self.sanitize(json)}")
    312            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    313            _LOGGER.debug(f"params: {self.sanitize(params)}")
    314            # Log the response.json() if it exists, if not log the response.
    315            try:
    316                response_json = await response.json()
    317                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    318            except ContentTypeError:
    319                _LOGGER.debug(f"Response: {response}")
    320            return await response.json()

async def delete(self, url, headers=None, json=None) -> Dict[Any, Any]: View Source

    322    async def delete(self, url, headers=None, json=None) -> Dict[Any, Any]:
    323        async with ClientSession(connector=TCPConnector(ttl_dns_cache=(30 * 60))) as _session:
    324            response = await _session.delete(url, headers=headers, json=json)
    325            # Relocated these below as the sanitization seems to modify the data before it goes to the post.
    326            _LOGGER.debug("Request:")
    327            _LOGGER.debug(f"url: {url}")
    328            _LOGGER.debug(f"json: {self.sanitize(json)}")
    329            _LOGGER.debug(f"headers: {self.sanitize(headers)}")
    330            # Log the response.json() if it exists, if not log the response.
    331            try:
    332                response_json = await response.json()
    333                _LOGGER.debug(f"Response Json: {self.sanitize(response_json)}")
    334            except ContentTypeError:
    335                _LOGGER.debug(f"Response: {response}")
    336            return await response.json()