From be0e20524ca0a78c499834d4c4623243d1e0931c Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Tue, 12 Mar 2024 12:14:41 -0400 Subject: [PATCH] Update app.py --- api/app.py | 240 +++++++++++++++++++++++++++++------------------------ 1 file changed, 133 insertions(+), 107 deletions(-) diff --git a/api/app.py b/api/app.py index 78d0410..95ba184 100644 --- a/api/app.py +++ b/api/app.py @@ -349,7 +349,7 @@ class RenphoWeight: user_id (str, optional): The ID of the user for whom weight data should be fetched. """ - def __init__(self, email, password, user_id=None, refresh=60): + def __init__(self, email, password, user_id=None, refresh=60, proxy=None): """Initialize a new RenphoWeight instance.""" self.public_key: str = CONF_PUBLIC_KEY self.email: str = email @@ -379,6 +379,7 @@ def __init__(self, email, password, user_id=None, refresh=60): self._last_updated_growth_record = None self.auth_in_progress = False self.is_polling_active = False + self.proxy = proxy @staticmethod def get_timestamp() -> int: @@ -411,6 +412,20 @@ async def open_session(self): headers={"Content-Type": "application/json", "Accept": "application/json"}, ) + async def check_proxy(self): + """ + Checks if the proxy is working by making a request to httpbin.org. + """ + test_url = 'https://renpho.qnclouds.com/api/v3/girths/list_girth.json?app_id=Renpho&terminal_user_session_key=' + try: + connector = ProxyConnector.from_url(self.proxy) if self.proxy else None + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get(test_url) as response: + return True + except Exception as e: + _LOGGER.error(f"Proxy connection failed: {e}") + return False + async def _request(self, method: str, url: str, retries: int = 3, skip_auth=False, **kwargs): """ @@ -426,44 +441,45 @@ async def _request(self, method: str, url: str, retries: int = 3, skip_auth=Fals Returns: Union[Dict, List]: The parsed JSON response from the API request. """ - token = self.token + if not await self.check_proxy(): + _LOGGER.error("Proxy check failed. Aborting authentication.") + raise APIError("Proxy check failed. Aborting authentication.") while retries > 0: - session = aiohttp.ClientSession( - headers={"Content-Type": "application/json", "Accept": "application/json", - "User-Agent": "Renpho/2.1.0 (iPhone; iOS 14.4; Scale/2.1.0; en-US)" - } - ) - - if not token and not url.endswith("sign_in.json") and not skip_auth: - auth_success = await self.auth() - token = self.token - if not auth_success: - raise AuthenticationError("Authentication failed. Unable to proceed with the request.") - - kwargs = self.prepare_data(kwargs) - - try: - async with session.request(method, url, **kwargs) as response: - response.raise_for_status() - parsed_response = await response.json() - - if parsed_response.get("status_code") == "40302": - token = None - skip_auth = False - retries -= 1 - await session.close() - continue # Retry the request - if parsed_response.get("status_code") == "50000": - raise APIError(f"Internal server error: {parsed_response.get('status_message')}") - if parsed_response.get("status_code") == "20000" and parsed_response.get("status_message") == "ok": - return parsed_response - else: - raise APIError(f"API request failed {method} {url}: {parsed_response.get('status_message')}") - except (aiohttp.ClientResponseError, aiohttp.ClientConnectionError) as e: - _LOGGER.error(f"Client error: {e}") - raise APIError(f"API request failed {method} {url}") from e - finally: - await session.close() + connector = ProxyConnector.from_url(self.proxy) if self.proxy else None + async with aiohttp.ClientSession(connector=connector, headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Renpho/2.1.0 (iPhone; iOS 14.4; Scale/2.1.0; en-US)" + }, timeout=ClientTimeout(total=60)) as session: + + if not self.token and not url.endswith("sign_in.json") or not skip_auth: + auth_success = await self.auth() + if not auth_success: + raise AuthenticationError("Authentication failed. Unable to proceed with the request.") + + kwargs = self.prepare_data(kwargs) + + try: + async with session.request(method, url, **kwargs) as response: + response.raise_for_status() + parsed_response = await response.json() + + if parsed_response.get("status_code") == "40302": + skip_auth = False + auth_success = await self.auth() + if not auth_success: + raise AuthenticationError("Authentication failed. Unable to proceed with the request.") + retries -= 1 + continue # Retry the request + if parsed_response.get("status_code") == "50000": + raise APIError(f"Internal server error: {parsed_response.get('status_message')}") + if parsed_response.get("status_code") == "20000" and parsed_response.get("status_message") == "ok": + return parsed_response + else: + raise APIError(f"API request failed {method} {url}: {parsed_response.get('status_message')}") + except (aiohttp.ClientResponseError, aiohttp.ClientConnectionError) as e: + _LOGGER.error(f"Client error: {e}") + raise APIError(f"API request failed {method} {url}") from e @staticmethod def encrypt_password(public_key_str, password): @@ -510,53 +526,64 @@ async def auth(self): data = self.prepare_data({"secure_flag": "1", "email": self.email, "password": encrypted_password}) - try: - self.token = None - session = aiohttp.ClientSession( - headers={"Content-Type": "application/json", "Accept": "application/json", "User-Agent": "Renpho/2.1.0 (iPhone; iOS 14.4; Scale/2.1.0; en-US)"}, - ) - - async with session.request("POST", API_AUTH_URL, json=data) as response: - response.raise_for_status() - parsed = await response.json() - - if parsed is None: - _LOGGER.error("Authentication failed. No response received.") - raise AuthenticationError("Authentication failed. No response received.") - - if parsed.get("status_code") == "50000" and parsed.get("status_message") == "Email was not registered": - _LOGGER.warning("Email was not registered.") - raise AuthenticationError("Email was not registered.") - - if parsed.get("status_code") == "500" and parsed.get("status_message") == "Internal Server Error": - _LOGGER.warning("Bad Password or Internal Server Error.") - raise AuthenticationError("Bad Password or Internal Server Error.") - - if "terminal_user_session_key" not in parsed: - _LOGGER.error( - "'terminal_user_session_key' not found in parsed object.") - raise AuthenticationError(f"Authentication failed: {parsed}") - - if parsed.get("status_code") == "20000" and parsed.get("status_message") == "ok": - if 'terminal_user_session_key' in parsed: - self.token = parsed["terminal_user_session_key"] - else: - self.token = None - raise AuthenticationError("Session key not found in response.") - if 'device_binds_ary' in parsed: - parsed['device_binds_ary'] = [DeviceBind(**device) for device in parsed['device_binds_ary']] - else: - parsed['device_binds_ary'] = [] - self.login_data = UserResponse(**parsed) - if self.user_id is None: - self.user_id = self.login_data.get("id", None) - return True - except Exception as e: - _LOGGER.error(f"Authentication failed: {e}") - raise AuthenticationError("Authentication failed due to an error. {e}") from e - finally: - self.auth_in_progress = False - await session.close() + for attempt in range(3): + try: + self.token = None + if not await self.check_proxy(): + _LOGGER.error("Proxy check failed. Aborting authentication.") + raise APIError("Proxy check failed. Aborting authentication.") + + connector = ProxyConnector.from_url(self.proxy) if self.proxy else None + async with aiohttp.ClientSession(connector=connector, headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Renpho/2.1.0 (iPhone; iOS 14.4; Scale/2.1.0; en-US)" + }, timeout=ClientTimeout(total=60)) as session: + + async with session.request("POST", API_AUTH_URL, json=data) as response: + response.raise_for_status() + parsed = await response.json() + + if parsed is None: + _LOGGER.error("Authentication failed. No response received.") + raise AuthenticationError("Authentication failed. No response received.") + + if parsed.get("status_code") == "50000" and parsed.get("status_message") == "Email was not registered": + _LOGGER.warning("Email was not registered.") + raise AuthenticationError("Email was not registered.") + + if parsed.get("status_code") == "500" and parsed.get("status_message") == "Internal Server Error": + _LOGGER.warning("Bad Password or Internal Server Error.") + raise AuthenticationError("Bad Password or Internal Server Error.") + + if "terminal_user_session_key" not in parsed: + _LOGGER.error( + "'terminal_user_session_key' not found in parsed object.") + raise AuthenticationError(f"Authentication failed: {parsed}") + + if parsed.get("status_code") == "20000" and parsed.get("status_message") == "ok": + if 'terminal_user_session_key' in parsed: + self.token = parsed["terminal_user_session_key"] + else: + self.token = None + raise AuthenticationError("Session key not found in response.") + if 'device_binds_ary' in parsed: + parsed['device_binds_ary'] = [DeviceBind(**device) for device in parsed['device_binds_ary']] + else: + parsed['device_binds_ary'] = [] + self.login_data = UserResponse(**parsed) + self.token = parsed["terminal_user_session_key"] + if self.user_id is None: + self.user_id = self.login_data.get("id", None) + return True + except (aiohttp.ClientResponseError, aiohttp.ClientConnectionError) as e: + _LOGGER.error(f"Authentication failed: {e}") + if attempt < 3 - 1: + await asyncio.sleep(5) # Wait before retrying + else: + raise AuthenticationError(f"Authentication failed after retries. {e}") from e + finally: + self.auth_in_progress = False async def get_scale_users(self): """ @@ -629,7 +656,6 @@ async def get_measurements(self): _LOGGER.error(f"Failed to fetch weight measurements: {e}") return None - async def get_measurements_history(self): """ Fetch the most recent weight measurements_history for the user. @@ -735,6 +761,7 @@ async def list_girth(self): if "status_code" in parsed and parsed["status_code"] == "20000": response = GirthResponse(**parsed) + self._last_updated_girth = time.time() self.girth_info = response.girths return self.girth_info else: @@ -870,31 +897,30 @@ async def get_specific_metric(self, metric_type: str, metric: str, user_id: Opti try: if metric_type == METRIC_TYPE_WEIGHT: - if self._last_updated_weight is None or time.time() - self._last_updated_weight > self.refresh: - last_measurement = await self.get_weight() - if last_measurement and self.weight is not None: - return last_measurement[1].get(metric, None) if last_measurement[1] else None + if self._last_updated_weight is None or self.weight: + if self.weight_info is not None: + return self.weight_info.get(metric, None) return self.weight_info.get(metric, None) if self.weight_info else None elif metric_type == METRIC_TYPE_GIRTH: - if self._last_updated_girth is None or time.time() - self._last_updated_girth > self.refresh: + if self._last_updated_girth is None or self.girth_info is None: await self.list_girth() - for girth_entry in self.girth_info: - if hasattr(girth_entry, f"{metric}_value"): - return getattr(girth_entry, f"{metric}_value", None) + if self.girth_info: + valid_girths = sorted([g for g in self.girth_info if getattr(g, f"{metric}_value", 0) not in (None, 0.0)], key=lambda x: x.time_stamp, reverse=True) + for girth in valid_girths: + value = getattr(girth, f"{metric}_value", None) + if value not in (None, 0.0): + return value + return None elif metric_type == METRIC_TYPE_GIRTH_GOAL: - if self._last_updated_girth_goal is None or time.time() - self._last_updated_girth_goal > self.refresh: + if self._last_updated_girth_goal is None or self.girth_goal is None: await self.list_girth_goal() - for goal in self.girth_goal: - if goal.girth_type == metric: - return goal.goal_value - elif metric_type == METRIC_TYPE_GROWTH_RECORD: - if self._last_updated_growth_record is None or time.time() - self._last_updated_growth_record > self.refresh: - last_measurement = ( - self.growth_record.get("growths", [])[0] - if self.growth_record.get("growths") - else None - ) - return last_measurement.get(metric, None) if last_measurement else None + if self.girth_goal: + valid_goals = sorted([g for g in self.girth_goal if g.girth_type == metric and g.goal_value not in (None, 0.0)], key=lambda x: x.setup_goal_at, reverse=True) + # Iterate to find the first valid goal + for goal in valid_goals: + if goal.goal_value not in (None, 0.0): + return goal.goal_value + return None else: _LOGGER.error(f"Invalid metric type: {metric_type}") return None @@ -1152,4 +1178,4 @@ async def message_list(request: Request, renpho: RenphoWeight = Depends(get_curr raise HTTPException(status_code=404, detail="Message list not found") except Exception as e: _LOGGER.error(f"Error fetching message list: {e}") - return APIResponse(status="error", message=str(e)) \ No newline at end of file + return APIResponse(status="error", message=str(e))