wyzeapy.wyze_auth_lib

  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:
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)
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
38    @property
39    def access_token(self):
40        return self._access_token
refresh_token
47    @property
48    def refresh_token(self):
49        return self._refresh_token
refresh_time
55    @property
56    def refresh_time(self):
57        return self._refresh_time
class WyzeAuthLib:
 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)
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):
 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:
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:
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
206    @property
207    def should_refresh(self) -> bool:
208        return time.time() >= self.token.refresh_time
async def refresh_if_should(self):
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:
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):
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]:
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]:
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]:
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]:
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]:
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()