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
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
SANITIZE_FIELDS =
['email', 'password', 'access_token', 'accessToken', 'refresh_token', 'lat', 'lon', 'address']
@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
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
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
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()