From db87e02f8b95c71223cfff439a13ba2e151db992 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Mon, 11 Mar 2024 19:40:10 -0400 Subject: [PATCH 01/30] small convert --- custom_components/renpho/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/renpho/const.py b/custom_components/renpho/const.py index a4aa494..0ef30e0 100755 --- a/custom_components/renpho/const.py +++ b/custom_components/renpho/const.py @@ -24,7 +24,7 @@ ) CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" -KG_TO_LBS: Final = 2.20462 +KG_TO_LBS: Final = 2.2046226218 CM_TO_INCH: Final = 0.393701 # General Information Metrics From dc744330ddf45fdc37df6e0c52cd8b355f0824f9 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Mon, 11 Mar 2024 21:17:17 -0400 Subject: [PATCH 02/30] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 869aed1..cc27a9b 100755 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ https://github.com/antoinebou12/hass_renpho/assets/13888068/0bf0e48f-0582-462a-b Use http proxy for the app here a list https://hidemy.io/en/proxy-list/?type=s#list +## Install config +![image](https://github.com/antoinebou12/hass_renpho/assets/13888068/b1a90a12-9f57-42ad-adf8-008a6de92880) + ### Weight ![Weight Sensor](docs/images/weight.png) From be0e20524ca0a78c499834d4c4623243d1e0931c Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Tue, 12 Mar 2024 12:14:41 -0400 Subject: [PATCH 03/30] 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)) From c8b23ea69feb923ebd97b274b07bcedd96d992a6 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Tue, 12 Mar 2024 12:17:27 -0400 Subject: [PATCH 04/30] Update app.py --- api/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/app.py b/api/app.py index 95ba184..0a13df5 100644 --- a/api/app.py +++ b/api/app.py @@ -15,12 +15,28 @@ from contextlib import asynccontextmanager import aiohttp +from aiohttp import ClientTimeout +from aiohttp_socks import ProxyConnector from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from pydantic import BaseModel import logging +import asyncio +import datetime +import logging +import time +from base64 import b64encode +from threading import Timer +from typing import Callable, Dict, Final, List, Optional, Union, Any +from contextlib import asynccontextmanager +import aiohttp +from Crypto.Cipher import PKCS1_v1_5 +from Crypto.PublicKey import RSA + +from pydantic import BaseModel +import logging METRIC_TYPE_WEIGHT: Final = "weight" METRIC_TYPE_GROWTH_RECORD: Final = "growth_record" From 42f209de7248be73b3ac06fa7ce0f0f90d0373ff Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Tue, 12 Mar 2024 12:17:37 -0400 Subject: [PATCH 05/30] Update requirements.txt --- api/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/api/requirements.txt b/api/requirements.txt index 1906cf3..d7765e7 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -13,3 +13,4 @@ pyjwt python-dotenv pycryptodome starlette +aiohttp_socks From 5baf1490b64b48cdfa4bf66eb80106382acf52be Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Mon, 25 Mar 2024 17:11:54 -0400 Subject: [PATCH 06/30] Update api_renpho.py --- custom_components/renpho/api_renpho.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/custom_components/renpho/api_renpho.py b/custom_components/renpho/api_renpho.py index da68fc9..dbbab6c 100755 --- a/custom_components/renpho/api_renpho.py +++ b/custom_components/renpho/api_renpho.py @@ -79,6 +79,8 @@ def __init__(self, email, password, user_id=None, refresh=60, proxy=None): self.is_polling_active = False self.proxy = proxy + _LOGGER.info(f"Initializing RenphoWeight instance. Proxy is {'enabled: ' + proxy if proxy else 'disabled.'}") + @staticmethod def get_timestamp() -> int: start_date = datetime.date(1998, 1, 1) @@ -112,14 +114,25 @@ async def open_session(self): async def check_proxy(self): """ - Checks if the proxy is working by making a request to httpbin.org. + Checks if the proxy is working by making a request to a Renpho API endpoint. """ test_url = 'https://renpho.qnclouds.com/api/v3/girths/list_girth.json?app_id=Renpho&terminal_user_session_key=' + + if not self.proxy: + _LOGGER.info("No proxy configured. Proceeding without proxy.") + else: + _LOGGER.info(f"Checking proxy connectivity using proxy: {self.proxy}") + 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 + if response.status == 200: + _LOGGER.info("Proxy check successful." if self.proxy else "Direct connection successful.") + return True + else: + _LOGGER.error(f"Failed to connect using {'proxy' if self.proxy else 'direct connection'}. HTTP Status: {response.status}") + return False except Exception as e: _LOGGER.error(f"Proxy connection failed: {e}") return False @@ -198,11 +211,13 @@ async def validate_credentials(self): Validate the current credentials by attempting to authenticate. Returns True if authentication succeeds, False otherwise. """ + _LOGGER.debug("Validating credentials for user: %s", self.email) try: return await self.auth() except Exception as e: - _LOGGER.error(f"Validation failed: {e}") - return False + _LOGGER.error("Failed to validate credentials for user: %s. Error: %s", self.email, e) + raise AuthenticationError(f"Invalid credentials for user {self.email}. Error details: {e}") from e + async def auth(self): """Authenticate with the Renpho API.""" @@ -665,4 +680,4 @@ class APIError(Exception): class ClientSSLError(Exception): - pass \ No newline at end of file + pass From df64bc1421d8ba8985e47432d93133dbca8ce61f Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Mon, 25 Mar 2024 17:14:46 -0400 Subject: [PATCH 07/30] Update config_flow.py --- custom_components/renpho/config_flow.py | 39 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/custom_components/renpho/config_flow.py b/custom_components/renpho/config_flow.py index afee581..4cf9b22 100644 --- a/custom_components/renpho/config_flow.py +++ b/custom_components/renpho/config_flow.py @@ -34,34 +34,55 @@ async def async_validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect.""" - _LOGGER.debug("Starting to validate input: %s", data) + _LOGGER.debug("Starting to validate input for Renpho integration: %s", data) + + # Initialize RenphoWeight instance renpho = RenphoWeight( email=data[CONF_EMAIL], password=data[CONF_PASSWORD], refresh=data.get(CONF_REFRESH, 60), proxy=data.get("proxy", None) ) + + # Check if a proxy is set and validate it + if renpho.proxy: + _LOGGER.info(f"Proxy is configured, checking proxy: {renpho.proxy}") + proxy_is_valid = await renpho.check_proxy() + if not proxy_is_valid: + _LOGGER.error(f"Proxy check failed for proxy: {renpho.proxy}") + raise CannotConnect(reason="Proxy check failed", details={"proxy": renpho.proxy}) + else: + _LOGGER.info("Proxy check passed successfully.") + else: + _LOGGER.info("No proxy configured, skipping proxy check.") + + _LOGGER.info(f"Attempting to validate credentials for {data[CONF_EMAIL]}") + + # Validate credentials is_valid = await renpho.validate_credentials() if not is_valid: + _LOGGER.error(f"Failed to validate credentials for user: {data[CONF_EMAIL]}. Invalid credentials.") raise CannotConnect( reason="Invalid credentials", - details={ - "email": data[CONF_EMAIL], - }, + details={"email": data[CONF_EMAIL]}, ) + else: + _LOGGER.info(f"Credentials validated successfully for {data[CONF_EMAIL]}") + # Fetch and validate scale users + _LOGGER.info("Fetching scale users associated with the account.") await renpho.get_scale_users() - - user_ids = [ - user.get("user_id", None) - for user in renpho.users - ] + user_ids = [user.user_id for user in renpho.users if user.user_id is not None] if not user_ids: + _LOGGER.error(f"No users found associated with the account {data[CONF_EMAIL]}") raise CannotConnect(reason="No users found", details={"email": data[CONF_EMAIL]}) + else: + _LOGGER.info(f"Found users with IDs: {user_ids} for the account {data[CONF_EMAIL]}") return {"title": data[CONF_EMAIL], "user_ids": user_ids, "renpho_instance": renpho} + class RenphoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL From 24cd2f01d76fe4f2d3e521272e4363228e921751 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Thu, 4 Apr 2024 18:42:58 -0400 Subject: [PATCH 08/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc27a9b..1d47c23 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Renpho Weight Home Assistant Component -# IN DEVELOPPEMENT +# IN DEVELOPPEMENT (Not Working) ![Version](https://img.shields.io/badge/version-v3.0.1-blue) ![License](https://img.shields.io/badge/license-MIT-green) From a9b5505dc73b6fb7d844a4e3b770c7930fd0d746 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 16:23:04 -0400 Subject: [PATCH 09/30] small changes to proxy test --- custom_components/renpho/api_renpho.py | 23 +++++++++++++---------- custom_components/renpho/config_flow.py | 2 +- custom_components/renpho/manifest.json | 10 +++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/custom_components/renpho/api_renpho.py b/custom_components/renpho/api_renpho.py index dbbab6c..3e515e5 100755 --- a/custom_components/renpho/api_renpho.py +++ b/custom_components/renpho/api_renpho.py @@ -116,7 +116,7 @@ async def check_proxy(self): """ Checks if the proxy is working by making a request to a Renpho API endpoint. """ - test_url = 'https://renpho.qnclouds.com/api/v3/girths/list_girth.json?app_id=Renpho&terminal_user_session_key=' + test_url = 'http://httpbin.org/get' if not self.proxy: _LOGGER.info("No proxy configured. Proceeding without proxy.") @@ -125,17 +125,20 @@ async def check_proxy(self): 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: - if response.status == 200: - _LOGGER.info("Proxy check successful." if self.proxy else "Direct connection successful.") - return True - else: - _LOGGER.error(f"Failed to connect using {'proxy' if self.proxy else 'direct connection'}. HTTP Status: {response.status}") - return False + session = aiohttp.ClientSession(connector=connector) + async with session.get(test_url) as response: + if response.status == 200: + _LOGGER.info("Proxy check successful." if self.proxy else "Direct connection successful.") + return True + else: + _LOGGER.error(f"Failed to connect using {'proxy' if self.proxy else 'direct connection'}. HTTP Status: {response.status}") + return False except Exception as e: _LOGGER.error(f"Proxy connection failed: {e}") return False + finally: + await session.close() + async def _request(self, method: str, url: str, retries: int = 3, skip_auth=False, **kwargs): @@ -680,4 +683,4 @@ class APIError(Exception): class ClientSSLError(Exception): - pass + pass \ No newline at end of file diff --git a/custom_components/renpho/config_flow.py b/custom_components/renpho/config_flow.py index 4cf9b22..d8475fa 100644 --- a/custom_components/renpho/config_flow.py +++ b/custom_components/renpho/config_flow.py @@ -143,4 +143,4 @@ def __str__(self): return f"CannotConnect: {self.reason} - {self.details}" def get_details(self): - return self.details + return self.details \ No newline at end of file diff --git a/custom_components/renpho/manifest.json b/custom_components/renpho/manifest.json index 89dcd12..c0d5e48 100755 --- a/custom_components/renpho/manifest.json +++ b/custom_components/renpho/manifest.json @@ -6,14 +6,14 @@ "dependencies": [], "codeowners": ["@neilzilla", "@antoinebou12"], "requirements": [ - "pycryptodome>=3.3.1", - "requests>=2.25.0", - "aiohttp>=3.6.1", - "voluptuous>=0.11.7", + "pycryptodome", + "requests", + "aiohttp", + "voluptuous", "pydantic", "aiohttp_socks" ], "iot_class": "cloud_polling", - "version": "3.0.1", + "version": "3.0.0", "config_flow": true } From fc4b5d025d0b08afb18cdc9d1c64f1535cbc0385 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 17:00:37 -0400 Subject: [PATCH 10/30] changes api with api_key --- api/app.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/api/app.py b/api/app.py index 0a13df5..9747413 100644 --- a/api/app.py +++ b/api/app.py @@ -1021,6 +1021,23 @@ class ClientSSLError(Exception): from starlette.responses import Response import httpx +from datetime import datetime +import hashlib +import os +from Crypto.PublicKey import RSA +from Crypto.Signature import pkcs1_15 +from Crypto.Hash import SHA256 +from fastapi import HTTPException, Security +from fastapi.security.api_key import APIKeyHeader + +security_basic = HTTPBasic() +API_KEY_NAME = "access_token" +api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True) + +# Load RSA keys from environment variables +PRIVATE_KEY = RSA.import_key(os.getenv("RSA_PRIVATE_KEY")) +PUBLIC_KEY = RSA.import_key(os.getenv("RSA_PUBLIC_KEY")) + # Initialize FastAPI and Jinja2 app = FastAPI(docs_url="/docs", redoc_url=None) @@ -1036,8 +1053,6 @@ class ClientSSLError(Exception): allow_headers=["*"], ) -security = HTTPBasic() - class APIResponse(BaseModel): status: str @@ -1045,15 +1060,51 @@ class APIResponse(BaseModel): data: Optional[Any] = None -async def get_current_user(credentials: HTTPBasicCredentials = Depends(security)): +def generate_api_key(email: str, password: str) -> str: + """Generate a signed API key.""" + timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") + salt = os.urandom(16).hex() + payload = f"{email}:{password}:{timestamp}:{salt}" + api_key = hashlib.sha256(payload.encode()).hexdigest() + + # Sign the API key + key_hash = SHA256.new(api_key.encode()) + signature = pkcs1_15.new(PRIVATE_KEY).sign(key_hash) + signed_api_key = api_key + ":" + signature.hex() + + return signed_api_key + +def decrypt_api_key(api_key: str) -> Optional[Dict[str, str]]: + """Decrypt API key to extract the email and password.""" try: + api_key, signature = api_key.rsplit(':', 1) + key_hash = SHA256.new(api_key.encode()) + # Verify the signature + pkcs1_15.new(PUBLIC_KEY).verify(key_hash, bytes.fromhex(signature)) + # If verification is successful, decode the email and password + decoded_data = hashlib.sha256().new(bytes.fromhex(api_key)).hexdigest().split(":") + return {"email": decoded_data[0], "password": decoded_data[1]} + except (ValueError, pkcs1_15.PKCS115_SigSchemeError) as e: + return None + +async def get_api_key(api_key: str = Depends(api_key_header)): + """Dependency that validates the API key.""" + user_credentials = decrypt_api_key(api_key) + if user_credentials: + return user_credentials + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid API key") + +async def get_current_user(credentials: Optional[HTTPBasicCredentials] = Depends(security_basic), api_key: Optional[str] = Depends(get_api_key)): + """Dependency that authenticates user either by basic auth or API key.""" + if credentials: user = RenphoWeight(email=credentials.username, password=credentials.password) - await user.auth() # Ensure that user can authenticate - return user - except Exception as e: - _LOGGER.error(f"Authentication failed: {e}") - raise HTTPException(status_code=401, detail="Authentication failed") from e - + if await user.auth(): + return user + elif api_key: + user = RenphoWeight(email=api_key["email"], password=api_key["password"]) + if await user.auth(): + return user + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials or API key") @app.get("/") def read_root(request: Request): @@ -1064,6 +1115,20 @@ async def auth(renpho: RenphoWeight = Depends(get_current_user)): # If this point is reached, authentication was successful return APIResponse(status="success", message="Authentication successful.") +@app.get("/auth/apikey", response_model=APIResponse) +async def auth_api_key(api_key: str = Depends(get_api_key)): + """Endpoint to authenticate using an API key.""" + return APIResponse(status="success", message="Authenticated using API key.") + +@app.get("/generate_api_key", response_model=APIResponse) +def generate_key(request: Request, email: str, password: str): + """Generate API key for a user.""" + try: + api_key = generate_api_key(email, password) + return APIResponse(status="success", message="API key generated.", data={"api_key": api_key}) + except Exception as e: + return APIResponse(status="error", message="Failed to generate API key.", data=str(e)) + @app.get("/info", response_model=APIResponse) async def get_info(renpho: RenphoWeight = Depends(get_current_user)): try: From 34e5608872fc1a047b53c48defe7257d6ee9cc11 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 17:15:25 -0400 Subject: [PATCH 11/30] test optional for api key --- api/app.py | 104 +++++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/api/app.py b/api/app.py index 9747413..ec03592 100644 --- a/api/app.py +++ b/api/app.py @@ -21,22 +21,6 @@ from Crypto.PublicKey import RSA from pydantic import BaseModel -import logging -import asyncio -import datetime -import logging -import time -from base64 import b64encode -from threading import Timer -from typing import Callable, Dict, Final, List, Optional, Union, Any -from contextlib import asynccontextmanager - -import aiohttp -from Crypto.Cipher import PKCS1_v1_5 -from Crypto.PublicKey import RSA - -from pydantic import BaseModel -import logging METRIC_TYPE_WEIGHT: Final = "weight" METRIC_TYPE_GROWTH_RECORD: Final = "growth_record" @@ -1021,23 +1005,23 @@ class ClientSSLError(Exception): from starlette.responses import Response import httpx +from fastapi import FastAPI, HTTPException, Depends, Request +from fastapi.security import HTTPBasic, HTTPBasicCredentials, APIKeyHeader +from fastapi.responses import JSONResponse +from starlette.middleware.cors import CORSMiddleware +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND from datetime import datetime import hashlib import os from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 -from fastapi import HTTPException, Security -from fastapi.security.api_key import APIKeyHeader +from pydantic import BaseModel +from typing import Optional security_basic = HTTPBasic() API_KEY_NAME = "access_token" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True) - -# Load RSA keys from environment variables -PRIVATE_KEY = RSA.import_key(os.getenv("RSA_PRIVATE_KEY")) -PUBLIC_KEY = RSA.import_key(os.getenv("RSA_PUBLIC_KEY")) - +api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) # Initialize FastAPI and Jinja2 app = FastAPI(docs_url="/docs", redoc_url=None) @@ -1059,6 +1043,17 @@ class APIResponse(BaseModel): message: str data: Optional[Any] = None +def load_rsa_keys(): + try: + private_key = RSA.import_key(os.getenv("RSA_PRIVATE_KEY")) + public_key = RSA.import_key(os.getenv("RSA_PUBLIC_KEY")) + return private_key, public_key + except Exception as e: + print(f"Error loading RSA keys: {e}") + raise HTTPException(status_code=500, detail="Failed to load RSA keys") + +PRIVATE_KEY, PUBLIC_KEY = load_rsa_keys() + def generate_api_key(email: str, password: str) -> str: """Generate a signed API key.""" @@ -1074,37 +1069,46 @@ def generate_api_key(email: str, password: str) -> str: return signed_api_key -def decrypt_api_key(api_key: str) -> Optional[Dict[str, str]]: +def decrypt_api_key(api_key: str): """Decrypt API key to extract the email and password.""" + try: + api_key_part, signature = api_key.rsplit(':', 1) + key_hash = SHA256.new(api_key_part.encode()) + # Verify the signature + pkcs1_15.new(PUBLIC_KEY).verify(key_hash, bytes.fromhex(signature)) + + # Decode the base string + decoded_bytes = bytes.fromhex(api_key_part) + decoded_string = decoded_bytes.decode('utf-8') # Assuming the input was UTF-8-encoded + email, password, timestamp, salt = decoded_string.split(':') + + return {"email": email, "password": password} + except ValueError: # Catches all errors related to cryptographic operations + raise HTTPException(status_code=403, detail="Invalid API key") + +def verify_api_key(api_key: str) -> bool: try: api_key, signature = api_key.rsplit(':', 1) key_hash = SHA256.new(api_key.encode()) - # Verify the signature pkcs1_15.new(PUBLIC_KEY).verify(key_hash, bytes.fromhex(signature)) - # If verification is successful, decode the email and password - decoded_data = hashlib.sha256().new(bytes.fromhex(api_key)).hexdigest().split(":") - return {"email": decoded_data[0], "password": decoded_data[1]} - except (ValueError, pkcs1_15.PKCS115_SigSchemeError) as e: - return None - -async def get_api_key(api_key: str = Depends(api_key_header)): - """Dependency that validates the API key.""" - user_credentials = decrypt_api_key(api_key) - if user_credentials: - return user_credentials - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid API key") - -async def get_current_user(credentials: Optional[HTTPBasicCredentials] = Depends(security_basic), api_key: Optional[str] = Depends(get_api_key)): - """Dependency that authenticates user either by basic auth or API key.""" - if credentials: - user = RenphoWeight(email=credentials.username, password=credentials.password) - if await user.auth(): - return user - elif api_key: - user = RenphoWeight(email=api_key["email"], password=api_key["password"]) - if await user.auth(): - return user - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials or API key") + return True + except Exception: + return False + +def get_api_key(api_key: str = Depends(api_key_header)): + if verify_api_key(api_key): + return api_key + else: + raise HTTPException(status_code=403, detail="Invalid API key") + + +def get_current_user(credentials: Optional[HTTPBasicCredentials] = Depends(security_basic), api_key: Optional[str] = Depends(get_api_key)): + email = credentials.username if credentials else decrypt_api_key(api_key)['email'] + password = credentials.password if credentials else decrypt_api_key(api_key)['password'] + user = RenphoWeight(email=email, password=password) + if not user.auth(): + raise HTTPException(status_code=403, detail="Invalid credentials") + return user @app.get("/") def read_root(request: Request): From 82e8cb2d0a484fd6002eb1a2ed3fe864706f926c Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 17:26:22 -0400 Subject: [PATCH 12/30] apikey or auth --- api/app.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/api/app.py b/api/app.py index ec03592..d979377 100644 --- a/api/app.py +++ b/api/app.py @@ -1095,19 +1095,32 @@ def verify_api_key(api_key: str) -> bool: except Exception: return False -def get_api_key(api_key: str = Depends(api_key_header)): - if verify_api_key(api_key): - return api_key +def get_credentials_or_api_key(credentials: Optional[HTTPBasicCredentials] = Depends(security_basic), api_key: Optional[str] = Depends(api_key_header)): + if api_key: + if verify_api_key(api_key): + return api_key + else: + raise HTTPException(status_code=403, detail="Invalid API key") + elif credentials: + if credentials.username and credentials.password: + return credentials + else: + raise HTTPException(status_code=403, detail="Invalid HTTP Basic credentials") else: - raise HTTPException(status_code=403, detail="Invalid API key") - + raise HTTPException(status_code=401, detail="Authentication credentials were not provided") + +async def get_current_user(auth: Union[HTTPBasicCredentials, str] = Depends(get_credentials_or_api_key)) -> RenphoWeight: + if isinstance(auth, HTTPBasicCredentials): + email, password = auth.username, auth.password + elif isinstance(auth, str): # auth is an API key + user_info = decrypt_api_key(auth) + email, password = user_info['email'], user_info['password'] + else: + raise HTTPException(status_code=401, detail="Invalid authentication method") -def get_current_user(credentials: Optional[HTTPBasicCredentials] = Depends(security_basic), api_key: Optional[str] = Depends(get_api_key)): - email = credentials.username if credentials else decrypt_api_key(api_key)['email'] - password = credentials.password if credentials else decrypt_api_key(api_key)['password'] user = RenphoWeight(email=email, password=password) - if not user.auth(): - raise HTTPException(status_code=403, detail="Invalid credentials") + if not await user.auth(): + raise HTTPException(status_code=403, detail="Invalid email or password") return user @app.get("/") @@ -1119,11 +1132,6 @@ async def auth(renpho: RenphoWeight = Depends(get_current_user)): # If this point is reached, authentication was successful return APIResponse(status="success", message="Authentication successful.") -@app.get("/auth/apikey", response_model=APIResponse) -async def auth_api_key(api_key: str = Depends(get_api_key)): - """Endpoint to authenticate using an API key.""" - return APIResponse(status="success", message="Authenticated using API key.") - @app.get("/generate_api_key", response_model=APIResponse) def generate_key(request: Request, email: str, password: str): """Generate API key for a user.""" From 03a757393f391dc7713293fecfbdb507bbb31836 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 17:34:46 -0400 Subject: [PATCH 13/30] test --- api/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/app.py b/api/app.py index d979377..92df910 100644 --- a/api/app.py +++ b/api/app.py @@ -1083,6 +1083,7 @@ def decrypt_api_key(api_key: str): email, password, timestamp, salt = decoded_string.split(':') return {"email": email, "password": password} + raise HTTPException(status_code=403, detail=f"{email} {password}") except ValueError: # Catches all errors related to cryptographic operations raise HTTPException(status_code=403, detail="Invalid API key") From 777c688f39d69f617b53c2d84a40db817d6af455 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 17:39:23 -0400 Subject: [PATCH 14/30] decrypt --- api/app.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/app.py b/api/app.py index 92df910..92e4e75 100644 --- a/api/app.py +++ b/api/app.py @@ -1083,7 +1083,6 @@ def decrypt_api_key(api_key: str): email, password, timestamp, salt = decoded_string.split(':') return {"email": email, "password": password} - raise HTTPException(status_code=403, detail=f"{email} {password}") except ValueError: # Catches all errors related to cryptographic operations raise HTTPException(status_code=403, detail="Invalid API key") @@ -1142,6 +1141,17 @@ def generate_key(request: Request, email: str, password: str): except Exception as e: return APIResponse(status="error", message="Failed to generate API key.", data=str(e)) +@app.get("/decrypt_api_key", response_model=dict) +def decrypt_key(api_key: str = Depends(api_key_header)): + """ + Decrypts an API key to extract the email and password. + This endpoint should be secured and limited to administrative use. + """ + if not api_key or not verify_api_key(api_key): + raise HTTPException(status_code=403, detail="Invalid or missing API key") + + return decrypt_api_key(api_key) + @app.get("/info", response_model=APIResponse) async def get_info(renpho: RenphoWeight = Depends(get_current_user)): try: From b5e2b5515ac86b21e58123a034d610aa2f86b07c Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:08:13 -0400 Subject: [PATCH 15/30] new encryption --- api/app.py | 82 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/api/app.py b/api/app.py index 92e4e75..ec4e1c3 100644 --- a/api/app.py +++ b/api/app.py @@ -1016,6 +1016,10 @@ class ClientSSLError(Exception): from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 +from Crypto.Cipher import PKCS1_OAEP +import binascii +from base64 import b64encode, b64decode + from pydantic import BaseModel from typing import Optional @@ -1055,42 +1059,62 @@ def load_rsa_keys(): PRIVATE_KEY, PUBLIC_KEY = load_rsa_keys() -def generate_api_key(email: str, password: str) -> str: - """Generate a signed API key.""" - timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") - salt = os.urandom(16).hex() - payload = f"{email}:{password}:{timestamp}:{salt}" - api_key = hashlib.sha256(payload.encode()).hexdigest() - - # Sign the API key - key_hash = SHA256.new(api_key.encode()) - signature = pkcs1_15.new(PRIVATE_KEY).sign(key_hash) - signed_api_key = api_key + ":" + signature.hex() +def load_rsa_keys(): + try: + # Environment variables should be set securely + private_key = RSA.import_key(os.environ["PRIVATE_RSA_KEY"]) + public_key = RSA.import_key(os.environ["PUBLIC_RSA_KEY"]) + return private_key, public_key + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to load RSA keys: {str(e)}") - return signed_api_key +PRIVATE_KEY, PUBLIC_KEY = load_rsa_keys() -def decrypt_api_key(api_key: str): - """Decrypt API key to extract the email and password.""" +def generate_api_key(email: str, password: str) -> str: + """Generate a signed and encrypted API key.""" + timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S%f") + payload = f"{email}:{password}:{timestamp}" + # Encrypt the payload + cipher = PKCS1_OAEP.new(PUBLIC_KEY) + encrypted_payload = cipher.encrypt(payload.encode()) + encrypted_hex = binascii.hexlify(encrypted_payload).decode() + + # Sign the encrypted payload + hash_obj = SHA256.new(encrypted_payload) + signature = pkcs1_15.new(PRIVATE_KEY).sign(hash_obj) + signature_hex = binascii.hexlify(signature).decode() + + return f"{encrypted_hex}:{signature_hex}" + +def decrypt_api_key(api_key: str) -> dict: + """Verify and decrypt an API key.""" try: - api_key_part, signature = api_key.rsplit(':', 1) - key_hash = SHA256.new(api_key_part.encode()) + encrypted_payload_hex, signature_hex = api_key.split(':') + encrypted_payload = binascii.unhexlify(encrypted_payload_hex) + signature = binascii.unhexlify(signature_hex) + # Verify the signature - pkcs1_15.new(PUBLIC_KEY).verify(key_hash, bytes.fromhex(signature)) - - # Decode the base string - decoded_bytes = bytes.fromhex(api_key_part) - decoded_string = decoded_bytes.decode('utf-8') # Assuming the input was UTF-8-encoded - email, password, timestamp, salt = decoded_string.split(':') - - return {"email": email, "password": password} - except ValueError: # Catches all errors related to cryptographic operations + hash_obj = SHA256.new(encrypted_payload) + pkcs1_15.new(PUBLIC_KEY).verify(hash_obj, signature) + + # Decrypt the payload + cipher = PKCS1_OAEP.new(PRIVATE_KEY) + payload = cipher.decrypt(encrypted_payload) + email, password, timestamp = payload.decode().split(':') + + return {"email": email, "password": password, "timestamp": timestamp} + except (ValueError, IndexError, TypeError, binascii.Error) as e: raise HTTPException(status_code=403, detail="Invalid API key") def verify_api_key(api_key: str) -> bool: try: - api_key, signature = api_key.rsplit(':', 1) - key_hash = SHA256.new(api_key.encode()) - pkcs1_15.new(PUBLIC_KEY).verify(key_hash, bytes.fromhex(signature)) + encrypted_payload_hex, signature_hex = api_key.split(':') + encrypted_payload = binascii.unhexlify(encrypted_payload_hex) + signature = binascii.unhexlify(signature_hex) + + # Verify the signature + hash_obj = SHA256.new(encrypted_payload) + pkcs1_15.new(PUBLIC_KEY).verify(hash_obj, signature) return True except Exception: return False @@ -1133,7 +1157,7 @@ async def auth(renpho: RenphoWeight = Depends(get_current_user)): return APIResponse(status="success", message="Authentication successful.") @app.get("/generate_api_key", response_model=APIResponse) -def generate_key(request: Request, email: str, password: str): +def generate_key(request: Request, email: str, password: str, renpho: RenphoWeight = Depends(get_current_user)): """Generate API key for a user.""" try: api_key = generate_api_key(email, password) From cff56e1c959eb05ac4a1bd7f06c45d4f34224cbf Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:11:42 -0400 Subject: [PATCH 16/30] remove error --- api/app.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/api/app.py b/api/app.py index ec4e1c3..e7f336a 100644 --- a/api/app.py +++ b/api/app.py @@ -1059,17 +1059,6 @@ def load_rsa_keys(): PRIVATE_KEY, PUBLIC_KEY = load_rsa_keys() -def load_rsa_keys(): - try: - # Environment variables should be set securely - private_key = RSA.import_key(os.environ["PRIVATE_RSA_KEY"]) - public_key = RSA.import_key(os.environ["PUBLIC_RSA_KEY"]) - return private_key, public_key - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to load RSA keys: {str(e)}") - -PRIVATE_KEY, PUBLIC_KEY = load_rsa_keys() - def generate_api_key(email: str, password: str) -> str: """Generate a signed and encrypted API key.""" timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S%f") From a0c272b29e7fc75f495dec8f9382366e62265254 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:21:37 -0400 Subject: [PATCH 17/30] Update app.py --- api/app.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/api/app.py b/api/app.py index e7f336a..93bcf58 100644 --- a/api/app.py +++ b/api/app.py @@ -53,8 +53,6 @@ from dataclasses import dataclass -from typing import List, Optional - from pydantic import BaseModel class DeviceBind(BaseModel): @@ -1012,7 +1010,6 @@ class ClientSSLError(Exception): from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND from datetime import datetime import hashlib -import os from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 @@ -1020,9 +1017,6 @@ class ClientSSLError(Exception): import binascii from base64 import b64encode, b64decode -from pydantic import BaseModel -from typing import Optional - security_basic = HTTPBasic() API_KEY_NAME = "access_token" api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) @@ -1154,17 +1148,6 @@ def generate_key(request: Request, email: str, password: str, renpho: RenphoWeig except Exception as e: return APIResponse(status="error", message="Failed to generate API key.", data=str(e)) -@app.get("/decrypt_api_key", response_model=dict) -def decrypt_key(api_key: str = Depends(api_key_header)): - """ - Decrypts an API key to extract the email and password. - This endpoint should be secured and limited to administrative use. - """ - if not api_key or not verify_api_key(api_key): - raise HTTPException(status_code=403, detail="Invalid or missing API key") - - return decrypt_api_key(api_key) - @app.get("/info", response_model=APIResponse) async def get_info(renpho: RenphoWeight = Depends(get_current_user)): try: From f23c4a5b80df825681fc45b1118717d4a028d977 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:28:57 -0400 Subject: [PATCH 18/30] Update manifest.json --- custom_components/renpho/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/renpho/manifest.json b/custom_components/renpho/manifest.json index c0d5e48..098f6db 100755 --- a/custom_components/renpho/manifest.json +++ b/custom_components/renpho/manifest.json @@ -14,6 +14,6 @@ "aiohttp_socks" ], "iot_class": "cloud_polling", - "version": "3.0.0", + "version": "3.0.2", "config_flow": true } From 247dcfcb6dca29c1b762442afcc70192d10cb83c Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:29:13 -0400 Subject: [PATCH 19/30] Delete custom_components/renpho/renpho.png --- custom_components/renpho/renpho.png | Bin 14773 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 custom_components/renpho/renpho.png diff --git a/custom_components/renpho/renpho.png b/custom_components/renpho/renpho.png deleted file mode 100644 index 26cf6aca6d562616cfd2289af95b6f217b309eca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14773 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 From 7ce0c39eef074e76dbd32fa5aa737aa825f8a394 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:30:32 -0400 Subject: [PATCH 20/30] Update api_renpho.py --- custom_components/renpho/api_renpho.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/renpho/api_renpho.py b/custom_components/renpho/api_renpho.py index 3e515e5..004876a 100755 --- a/custom_components/renpho/api_renpho.py +++ b/custom_components/renpho/api_renpho.py @@ -139,8 +139,6 @@ async def check_proxy(self): finally: await session.close() - - async def _request(self, method: str, url: str, retries: int = 3, skip_auth=False, **kwargs): """ Perform an API request and return the parsed JSON response. @@ -683,4 +681,4 @@ class APIError(Exception): class ClientSSLError(Exception): - pass \ No newline at end of file + pass From f951429d5ae2c807fe54eafd3158a7fcdd7001a3 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:31:24 -0400 Subject: [PATCH 21/30] Create fr.json --- custom_components/renpho/translations/fr.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/fr.json diff --git a/custom_components/renpho/translations/fr.json b/custom_components/renpho/translations/fr.json new file mode 100644 index 0000000..b52206b --- /dev/null +++ b/custom_components/renpho/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Entrez votre adresse e-mail de compte Renpho obsolète.", + "password": "Entrez votre mot de passe de compte Renpho.", + "refresh": "Définissez le taux de rafraîchissement des données en secondes. La valeur par défaut est de 60 secondes.", + "unit_of_measurement": "Choisissez l'unité de mesure pour le poids. Les options sont kilogrammes (kg) ou livres (lbs).", + "proxy": "Optionnel : Spécifiez un serveur proxy à utiliser (par exemple, 'http://127.0.0.1:8080')." + } + } + }, + "error": { + "cannot_connect": "Impossible de se connecter au service", + "invalid_auth": "Authentification invalide", + "unknown": "Une erreur inconnue s'est produite" + }, + "abort": { + "already_configured": "L'appareil est déjà configuré" + } + } +} From b7733a869b6277717ae34e1c4f07e5c317a09056 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:32:03 -0400 Subject: [PATCH 22/30] Create es.json --- custom_components/renpho/translations/es.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/es.json diff --git a/custom_components/renpho/translations/es.json b/custom_components/renpho/translations/es.json new file mode 100644 index 0000000..8f81bc0 --- /dev/null +++ b/custom_components/renpho/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Introduzca la dirección de correo electrónico de su cuenta Renpho obsoleta.", + "password": "Introduzca la contraseña de su cuenta Renpho.", + "refresh": "Establezca la tasa de refresco de datos en segundos. El valor por defecto es 60 segundos.", + "unit_of_measurement": "Elija la unidad de medida para el peso. Las opciones son kilogramos (kg) o libras (lbs).", + "proxy": "Opcional: Especifique un servidor proxy para usar (por ejemplo, 'http://127.0.0.1:8080')." + } + } + }, + "error": { + "cannot_connect": "No se puede conectar al servicio", + "invalid_auth": "Autenticación inválida", + "unknown": "Ocurrió un error desconocido" + }, + "abort": { + "already_configured": "El dispositivo ya está configurado" + } + } +} From 04e8f76dfc1f83f348ea01d107197af881848e17 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:33:17 -0400 Subject: [PATCH 23/30] Create zh.json --- custom_components/renpho/translations/zh.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/zh.json diff --git a/custom_components/renpho/translations/zh.json b/custom_components/renpho/translations/zh.json new file mode 100644 index 0000000..41f6c51 --- /dev/null +++ b/custom_components/renpho/translations/zh.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "输入您的Renpho过时账户电子邮件地址。", + "password": "输入您的Renpho账户密码。", + "refresh": "设置数据刷新率,单位为秒。默认值为60秒。", + "unit_of_measurement": "选择重量的计量单位。选项为千克(kg)或磅(lbs)。", + "proxy": "可选:指定要使用的代理服务器(例如,'http://127.0.0.1:8080')。" + } + } + }, + "error": { + "cannot_connect": "无法连接到服务", + "invalid_auth": "认证无效", + "unknown": "发生未知错误" + }, + "abort": { + "already_configured": "设备已配置" + } + } +} From a9230b1967fead2d0a0415ca474b8ae2ab2962fe Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:33:42 -0400 Subject: [PATCH 24/30] Create de.json --- custom_components/renpho/translations/de.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/de.json diff --git a/custom_components/renpho/translations/de.json b/custom_components/renpho/translations/de.json new file mode 100644 index 0000000..f039fa0 --- /dev/null +++ b/custom_components/renpho/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Geben Sie Ihre E-Mail-Adresse für Ihr veraltetes Renpho-Konto ein.", + "password": "Geben Sie Ihr Renpho-Kontopasswort ein.", + "refresh": "Stellen Sie die Datenaktualisierungsrate in Sekunden ein. Standard ist 60 Sekunden.", + "unit_of_measurement": "Wählen Sie die Maßeinheit für das Gewicht. Die Optionen sind Kilogramm (kg) oder Pfund (lbs).", + "proxy": "Optional: Geben Sie einen Proxy-Server an (z.B. 'http://127.0.0.1:8080')." + } + } + }, + "error": { + "cannot_connect": "Verbindung zum Dienst kann nicht hergestellt werden", + "invalid_auth": "Ungültige Authentifizierung", + "unknown": "Ein unbekannter Fehler ist aufgetreten" + }, + "abort": { + "already_configured": "Gerät ist bereits konfiguriert" + } + } +} From ab3e94c4698db58d53900e6414f313b53932ee4f Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:34:08 -0400 Subject: [PATCH 25/30] Create ja.json --- custom_components/renpho/translations/ja.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/ja.json diff --git a/custom_components/renpho/translations/ja.json b/custom_components/renpho/translations/ja.json new file mode 100644 index 0000000..eedddd3 --- /dev/null +++ b/custom_components/renpho/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "古いRenphoアカウントのメールアドレスを入力してください。", + "password": "Renphoアカウントのパスワードを入力してください。", + "refresh": "データ更新レートを秒単位で設定します。デフォルトは60秒です。", + "unit_of_measurement": "体重の単位を選択してください。オプションはキログラム(kg)またはポンド(lbs)です。", + "proxy": "オプション:使用するプロキシサーバーを指定します(例:'http://127.0.0.1:8080')。" + } + } + }, + "error": { + "cannot_connect": "サービスに接続できません", + "invalid_auth": "認証が無効です", + "unknown": "不明なエラーが発生しました" + }, + "abort": { + "already_configured": "デバイスは既に設定されています" + } + } +} From 5b670c4e26c2a2347da7dd7042564f5a4d07105e Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:34:47 -0400 Subject: [PATCH 26/30] Create pt.json --- custom_components/renpho/translations/pt.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/pt.json diff --git a/custom_components/renpho/translations/pt.json b/custom_components/renpho/translations/pt.json new file mode 100644 index 0000000..00f592c --- /dev/null +++ b/custom_components/renpho/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Digite o e-mail da sua conta Renpho desatualizada.", + "password": "Digite a senha da sua conta Renpho.", + "refresh": "Defina a taxa de atualização de dados em segundos. O padrão é 60 segundos.", + "unit_of_measurement": "Escolha a unidade de medida para o peso. As opções são quilogramas (kg) ou libras (lbs).", + "proxy": "Opcional: Especifique um servidor proxy para usar (exemplo: 'http://127.0.0.1:8080')." + } + } + }, + "error": { + "cannot_connect": "Não é possível conectar ao serviço", + "invalid_auth": "Autenticação inválida", + "unknown": "Ocorreu um erro desconhecido" + }, + "abort": { + "already_configured": "Dispositivo já está configurado" + } + } +} From da4ec83d535d2685491fc73af7fd3549445df381 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:35:05 -0400 Subject: [PATCH 27/30] Create ru.json --- custom_components/renpho/translations/ru.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 custom_components/renpho/translations/ru.json diff --git a/custom_components/renpho/translations/ru.json b/custom_components/renpho/translations/ru.json new file mode 100644 index 0000000..ebb23ee --- /dev/null +++ b/custom_components/renpho/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Введите адрес электронной почты вашей устаревшей учетной записи Renpho.", + "password": "Введите пароль от вашей учетной записи Renpho.", + "refresh": "Установите частоту обновления данных в секундах. По умолчанию 60 секунд.", + "unit_of_measurement": "Выберите единицу измерения веса. Опции: килограммы (кг) или фунты (lbs).", + "proxy": "Необязательно: укажите прокси-сервер для использования (например, 'http://127.0.0.1:8080')." + } + } + }, + "error": { + "cannot_connect": "Невозможно подключиться к сервису", + "invalid_auth": "Неверная аутентификация", + "unknown": "Произошла неизвестная ошибка" + }, + "abort": { + "already_configured": "Устройство уже настроено" + } + } +} From 9c599e032364462c68e766ac0f76001e12ed02b5 Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:40:06 -0400 Subject: [PATCH 28/30] Update string.json --- custom_components/renpho/string.json | 67 +++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/custom_components/renpho/string.json b/custom_components/renpho/string.json index 55f72b9..960cb24 100644 --- a/custom_components/renpho/string.json +++ b/custom_components/renpho/string.json @@ -19,5 +19,70 @@ "abort": { "already_configured": "Device is already configured" } + }, + "sensor": { + "weight": "Weight", + "bmi": "BMI", + "muscle": "Muscle Mass", + "bone": "Bone Mass", + "waistline": "Waistline", + "hip": "Hip", + "stature": "Stature", + "bodyfat": "Body Fat", + "water": "Water Content", + "subfat": "Subcutaneous Fat", + "visfat": "Visceral Fat", + "bmr": "Basal Metabolic Rate", + "protein": "Protein Content", + "bodyage": "Body Age", + "neck": "Neck", + "shoulder": "Shoulder", + "left_arm": "Left Arm", + "right_arm": "Right Arm", + "chest": "Chest", + "waist": "Waist", + "hip": "Hip", + "left_thigh": "Left Thigh", + "right_thigh": "Right Thigh", + "left_calf": "Left Calf", + "right_calf": "Right Calf", + "whr": "Waist-Hip Ratio", + "abdomen": "Abdomen", + "unit": { + "kg": "kg", + "percent": "%", + "cm": "cm", + "bpm": "bpm" + }, + "category": { + "measurements": "Measurements", + "body_composition": "Body Composition", + "metabolic_metrics": "Metabolic Metrics", + "age_metrics": "Age Metrics", + "device_information": "Device Information", + "miscellaneous": "Miscellaneous", + "meta_information": "Meta Information", + "user_profile": "User Profile", + "electrical_measurements": "Electrical Measurements", + "cardiovascular_metrics": "Cardiovascular Metrics", + "other_metrics": "Other Metrics", + "girth_measurements": "Girth Measurements", + "girth_goals": "Girth Goals" + }, + "label": { + "physical_metrics": "Physical Metrics", + "body_composition": "Body Composition", + "metabolic_metrics": "Metabolic Metrics", + "age_metrics": "Age Metrics", + "device_information": "Device Information", + "miscellaneous": "Miscellaneous", + "meta_information": "Meta Information", + "user_profile": "User Profile", + "electrical_measurements": "Electrical Measurements", + "cardiovascular_metrics": "Cardiovascular Metrics", + "other_metrics": "Other Metrics", + "girth_measurements": "Girth Measurements", + "girth_goals": "Girth Goals" + } } -} \ No newline at end of file +} From 65f0f366898c2c3b313259e3a735781df38aa6bb Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:42:08 -0400 Subject: [PATCH 29/30] Update fr.json --- custom_components/renpho/translations/fr.json | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/custom_components/renpho/translations/fr.json b/custom_components/renpho/translations/fr.json index b52206b..9be1b27 100644 --- a/custom_components/renpho/translations/fr.json +++ b/custom_components/renpho/translations/fr.json @@ -3,11 +3,11 @@ "step": { "user": { "data": { - "email": "Entrez votre adresse e-mail de compte Renpho obsolète.", - "password": "Entrez votre mot de passe de compte Renpho.", + "email": "Entrez votre adresse e-mail du compte Renpho Obsolète.", + "password": "Entrez le mot de passe de votre compte Renpho.", "refresh": "Définissez le taux de rafraîchissement des données en secondes. La valeur par défaut est de 60 secondes.", "unit_of_measurement": "Choisissez l'unité de mesure pour le poids. Les options sont kilogrammes (kg) ou livres (lbs).", - "proxy": "Optionnel : Spécifiez un serveur proxy à utiliser (par exemple, 'http://127.0.0.1:8080')." + "proxy": "Facultatif : Spécifiez un serveur proxy à utiliser (par exemple, 'http://127.0.0.1:8080')." } } }, @@ -17,7 +17,72 @@ "unknown": "Une erreur inconnue s'est produite" }, "abort": { - "already_configured": "L'appareil est déjà configuré" + "already_configured": "Le périphérique est déjà configuré" + } + }, + "sensor": { + "weight": "Poids", + "bmi": "IMC", + "muscle": "Masse Musculaire", + "bone": "Masse Osseuse", + "waistline": "Tour de Taille", + "hip": "Hanche", + "stature": "Stature", + "bodyfat": "Masse Grasse", + "water": "Contenu en Eau", + "subfat": "Graisse Sous-cutanée", + "visfat": "Graisse Viscérale", + "bmr": "Taux Métabolique de Base", + "protein": "Teneur en Protéines", + "bodyage": "Âge Corporel", + "neck": "Cou", + "shoulder": "Épaule", + "left_arm": "Bras Gauche", + "right_arm": "Bras Droit", + "chest": "Poitrine", + "waist": "Taille", + "hip": "Hanche", + "left_thigh": "Cuisse Gauche", + "right_thigh": "Cuisse Droite", + "left_calf": "Mollet Gauche", + "right_calf": "Mollet Droit", + "whr": "Ratio Taille-Hanche", + "abdomen": "Abdomen", + "unit": { + "kg": "kg", + "percent": "%", + "cm": "cm", + "bpm": "bpm" + }, + "category": { + "measurements": "Mesures", + "body_composition": "Composition Corporelle", + "metabolic_metrics": "Métriques Métaboliques", + "age_metrics": "Métriques d'Âge", + "device_information": "Informations sur le Dispositif", + "miscellaneous": "Divers", + "meta_information": "Méta-informations", + "user_profile": "Profil Utilisateur", + "electrical_measurements": "Mesures Électriques", + "cardiovascular_metrics": "Métriques Cardiovasculaires", + "other_metrics": "Autres Métriques", + "girth_measurements": "Mesures de Circonférence", + "girth_goals": "Objectifs de Circonférence" + }, + "label": { + "physical_metrics": "Métriques Physiques", + "body_composition": "Composition Corporelle", + "metabolic_metrics": "Métriques Métaboliques", + "age_metrics": "Métriques d'Âge", + "device_information": "Informations sur le Dispositif", + "miscellaneous": "Divers", + "meta_information": "Méta-informations", + "user_profile": "Profil Utilisateur", + "electrical_measurements": "Mesures Électriques", + "cardiovascular_metrics": "Métriques Cardiovasculaires", + "other_metrics": "Autres Métriques", + "girth_measurements": "Mesures de Circonférence", + "girth_goals": "Objectifs de Circonférence" } } } From e53faf1e15f5eb38192447ae7d7dfe2707b1bd2b Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Fri, 19 Apr 2024 18:46:40 -0400 Subject: [PATCH 30/30] Update es.json --- custom_components/renpho/translations/es.json | 75 +++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/custom_components/renpho/translations/es.json b/custom_components/renpho/translations/es.json index 8f81bc0..0808089 100644 --- a/custom_components/renpho/translations/es.json +++ b/custom_components/renpho/translations/es.json @@ -3,10 +3,10 @@ "step": { "user": { "data": { - "email": "Introduzca la dirección de correo electrónico de su cuenta Renpho obsoleta.", - "password": "Introduzca la contraseña de su cuenta Renpho.", - "refresh": "Establezca la tasa de refresco de datos en segundos. El valor por defecto es 60 segundos.", - "unit_of_measurement": "Elija la unidad de medida para el peso. Las opciones son kilogramos (kg) o libras (lbs).", + "email": "Ingrese su dirección de correo electrónico de la cuenta Renpho Obsoleta.", + "password": "Ingrese la contraseña de su cuenta Renpho.", + "refresh": "Establezca la frecuencia de actualización de los datos en segundos. El valor predeterminado es de 60 segundos.", + "unit_of_measurement": "Seleccione la unidad de medida para el peso. Las opciones son kilogramos (kg) o libras (lbs).", "proxy": "Opcional: Especifique un servidor proxy para usar (por ejemplo, 'http://127.0.0.1:8080')." } } @@ -14,10 +14,75 @@ "error": { "cannot_connect": "No se puede conectar al servicio", "invalid_auth": "Autenticación inválida", - "unknown": "Ocurrió un error desconocido" + "unknown": "Se produjo un error desconocido" }, "abort": { "already_configured": "El dispositivo ya está configurado" } + }, + "sensor": { + "weight": "Peso", + "bmi": "IMC", + "muscle": "Masa Muscular", + "bone": "Masa Ósea", + "waistline": "Circunferencia de la Cintura", + "hip": "Cadera", + "stature": "Estatura", + "bodyfat": "Grasa Corporal", + "water": "Contenido de Agua", + "subfat": "Grasa Subcutánea", + "visfat": "Grasa Visceral", + "bmr": "Tasa Metabólica Basal", + "protein": "Contenido de Proteína", + "bodyage": "Edad Corporal", + "neck": "Cuello", + "shoulder": "Hombro", + "left_arm": "Brazo Izquierdo", + "right_arm": "Brazo Derecho", + "chest": "Pecho", + "waist": "Cintura", + "hip": "Cadera", + "left_thigh": "Muslo Izquierdo", + "right_thigh": "Muslo Derecho", + "left_calf": "Pantorrilla Izquierda", + "right_calf": "Pantorrilla Derecha", + "whr": "Ratio Cintura-Cadera", + "abdomen": "Abdomen", + "unit": { + "kg": "kg", + "percent": "%", + "cm": "cm", + "bpm": "bpm" + }, + "category": { + "measurements": "Mediciones", + "body_composition": "Composición Corporal", + "metabolic_metrics": "Métricas Metabólicas", + "age_metrics": "Métricas de Edad", + "device_information": "Información del Dispositivo", + "miscellaneous": "Misceláneo", + "meta_information": "Meta Información", + "user_profile": "Perfil de Usuario", + "electrical_measurements": "Mediciones Eléctricas", + "cardiovascular_metrics": "Métricas Cardiovasculares", + "other_metrics": "Otras Métricas", + "girth_measurements": "Mediciones de Circunferencia", + "girth_goals": "Objetivos de Circunferencia" + }, + "label": { + "physical_metrics": "Métricas Físicas", + "body_composition": "Composición Corporal", + "metabolic_metrics": "Métricas Metabólicas", + "age_metrics": "Métricas de Edad", + "device_information": "Información del Dispositivo", + "miscellaneous": "Misceláneo", + "meta_information": "Meta Información", + "user_profile": "Perfil de Usuario", + "electrical_measurements": "Mediciones Eléctricas", + "cardiovascular_metrics": "Métricas Cardiovasculares", + "other_metrics": "Otras Métricas", + "girth_measurements": "Mediciones de Circunferencia", + "girth_goals": "Objetivos de Circunferencia" + } } }