From fb8cdb75e9263ab85ca1d33f63295ea91f3d59ec Mon Sep 17 00:00:00 2001 From: Antoine Boucher Date: Tue, 5 Mar 2024 20:49:15 -0500 Subject: [PATCH] 3.0.0 --- README.md | 2 - api/app.py | 116 ++++++++++++++------- custom_components/renpho/api_renpho.py | 94 ++++++++--------- custom_components/renpho/coordinator.py | 3 - custom_components/renpho/sensor.py | 1 - custom_components/renpho/sensor_configs.py | 50 +++------ 6 files changed, 134 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 2c72982..91e88aa 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Renpho Weight Home Assistant Component -# NOT Working for now - ![Version](https://img.shields.io/badge/version-v2.0.2.1-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Build Status](https://img.shields.io/badge/build-passing-brightgreen) diff --git a/api/app.py b/api/app.py index 96475ed..3db2523 100644 --- a/api/app.py +++ b/api/app.py @@ -339,6 +339,43 @@ def get(self, key, default=None): return getattr(self, key, default) +import asyncio +import datetime +import logging +import time +from base64 import b64encode +from typing import Callable, Dict, Final, List, Optional, Union + +import aiohttp +from Crypto.Cipher import PKCS1_v1_5 +from Crypto.PublicKey import RSA + +from .const import CONF_PUBLIC_KEY + +METRIC_TYPE_WEIGHT: Final = "weight" +METRIC_TYPE_GROWTH_RECORD: Final = "growth_record" +METRIC_TYPE_GIRTH: Final = "girth" +METRIC_TYPE_GIRTH_GOAL: Final = "girth_goals" + +from .api_object import UserResponse, DeviceBind, MeasurementDetail, Users, GirthGoal, GirthGoalsResponse, Girth, GirthResponse, MeasurementResponse + +# Initialize logging +_LOGGER = logging.getLogger(__name__) + +# API Endpoints +API_AUTH_URL = "https://renpho.qnclouds.com/api/v3/users/sign_in.json?app_id=Renpho" # Authentication Post +API_SCALE_USERS_URL = "https://renpho.qnclouds.com/api/v3/scale_users/list_scale_user" # Scale users +API_MEASUREMENTS_URL = "https://renpho.qnclouds.com/api/v2/measurements/list.json" # Measurements +DEVICE_INFO_URL = "https://renpho.qnclouds.com/api/v2/device_binds/get_device.json" # Device info +LATEST_MODEL_URL = "https://renpho.qnclouds.com/api/v3/devices/list_lastest_model.json" # Latest model +GIRTH_URL = "https://renpho.qnclouds.com/api/v3/girths/list_girth.json" # Girth +GIRTH_GOAL_URL = "https://renpho.qnclouds.com/api/v3/girth_goals/list_girth_goal.json" # Girth goal +GROWTH_RECORD_URL = "https://renpho.qnclouds.com/api/v3/growth_records/list_growth_record.json" # Growth record +MESSAGE_LIST_URL = "https://renpho.qnclouds.com/api/v2/messages/list.json" # message to support +USER_REQUEST_URL = "https://renpho.qnclouds.com/api/v2/users/request_user.json" # error +USERS_REACH_GOAL = "https://renpho.qnclouds.com/api/v3/users/reach_goal.json" # error 404 + + class RenphoWeight: """ A class to interact with Renpho's weight scale API. @@ -412,7 +449,6 @@ async def open_session(self): ) - 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. @@ -427,42 +463,43 @@ 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 + 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 retries < 1: - _LOGGER.error("Max retries exceeded for API request.") - raise APIError("Max retries exceeded for API request.") - - 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 self.token and not url.endswith("sign_in.json"): - 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": - pass - 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() + 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 + 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() @staticmethod def encrypt_password(public_key_str, password): @@ -609,7 +646,7 @@ async def get_measurements(self): if measurements := parsed["last_ary"]: self.weight_history = [MeasurementDetail(**measurement) for measurement in measurements] self.weight_info = self.weight_history[0] if self.weight_history else None - self.weight = self.weight_history[0].weight if self.weight_info else None + self.weight = self.weight_info.weight if self.weight_info else None self.time_stamp = self.weight_info.time_stamp if self.weight_info else None self._last_updated_weight = time.time() return self.weight_info @@ -652,8 +689,9 @@ async def get_device_info(self): # Check for successful response code if parsed.get("status_code") == "20000" and "device_binds_ary" in parsed: - self.device_info = parsed["device_binds_ary"] - return self.device_info + device_info = [DeviceBind(**device) for device in parsed["device_binds_ary"]] + self.device_info = device_info + return device_info else: # Handling different error scenarios if "status_code" not in parsed: diff --git a/custom_components/renpho/api_renpho.py b/custom_components/renpho/api_renpho.py index 3abd111..70d05b4 100755 --- a/custom_components/renpho/api_renpho.py +++ b/custom_components/renpho/api_renpho.py @@ -108,7 +108,6 @@ async def open_session(self): ) - 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. @@ -123,42 +122,43 @@ 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 + 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 retries < 1: - _LOGGER.error("Max retries exceeded for API request.") - raise APIError("Max retries exceeded for API request.") - - 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 self.token and not url.endswith("sign_in.json"): - 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": - pass - 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() + 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 + 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() @staticmethod def encrypt_password(public_key_str, password): @@ -537,22 +537,16 @@ async def get_specific_metric(self, metric_type: str, metric: str, user_id: Opti 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: - last_measurement = ( - self.girth_info[0] - if self.girth_info - else None - ) - return last_measurement.get(metric, None) if last_measurement else None - return self.girth_info[0].get(metric, None) if self.girth_info else 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) elif metric_type == METRIC_TYPE_GIRTH_GOAL: - last_goal = next( - (goal for goal in self.girth_goal if goal.girth_type == metric), - None - ) if self._last_updated_girth_goal is None or time.time() - self._last_updated_girth_goal > self.refresh: - return last_goal.get('goal_value', None) - else: - return last_goal.get('goal_value', 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 = ( diff --git a/custom_components/renpho/coordinator.py b/custom_components/renpho/coordinator.py index 20d333d..f2b563e 100644 --- a/custom_components/renpho/coordinator.py +++ b/custom_components/renpho/coordinator.py @@ -50,6 +50,3 @@ async def _async_update_data(self): @property def last_updated(self): return self._last_updated - - async def get_specific_metric(self, metric_type: str, metric: str): - return await self.api.get_specific_metric(metric_type = metric_type, metric = metric, user_id = self._user_id) diff --git a/custom_components/renpho/sensor.py b/custom_components/renpho/sensor.py index c775269..3f1f6e3 100755 --- a/custom_components/renpho/sensor.py +++ b/custom_components/renpho/sensor.py @@ -185,7 +185,6 @@ def state(self): async def async_update(self): """Request an immediate update of the coordinator data.""" try: - await coordinator.async_refresh() metric_value = await self.coordinator.api.get_specific_metric( metric_type=self._metric, metric=self._id, diff --git a/custom_components/renpho/sensor_configs.py b/custom_components/renpho/sensor_configs.py index e489a89..6d207bf 100644 --- a/custom_components/renpho/sensor_configs.py +++ b/custom_components/renpho/sensor_configs.py @@ -440,7 +440,7 @@ }, # Girth Measurements { - "id": "neck_value", + "id": "neck", "name": "Neck Value", "unit": "cm", "category": "Measurements", @@ -448,7 +448,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "shoulder_value", + "id": "shoulder", "name": "Shoulder Value", "unit": "cm", "category": "Measurements", @@ -456,7 +456,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "left_arm_value", + "id": "left_arm", "name": "Left Arm Value", "unit": "cm", "category": "Measurements", @@ -464,7 +464,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "right_arm_value", + "id": "right_arm", "name": "Right Arm Value", "unit": "cm", "category": "Measurements", @@ -472,7 +472,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "chest_value", + "id": "chest", "name": "Chest Value", "unit": "cm", "category": "Measurements", @@ -480,7 +480,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "waist_value", + "id": "waist", "name": "Waist Value", "unit": "cm", "category": "Measurements", @@ -488,7 +488,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "hip_value", + "id": "hip", "name": "Hip Value", "unit": "cm", "category": "Measurements", @@ -496,7 +496,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "left_thigh_value", + "id": "left_thigh", "name": "Left Thigh Value", "unit": "cm", "category": "Measurements", @@ -504,7 +504,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "right_thigh_value", + "id": "right_thigh", "name": "Right Thigh Value", "unit": "cm", "category": "Measurements", @@ -512,7 +512,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "left_calf_value", + "id": "left_calf", "name": "Left Calf Value", "unit": "cm", "category": "Measurements", @@ -520,7 +520,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "right_calf_value", + "id": "right_calf", "name": "Right Calf Value", "unit": "cm", "category": "Measurements", @@ -528,7 +528,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "whr_value", + "id": "whr", "name": "WHR Value", "unit": "ratio", "category": "Measurements", @@ -536,7 +536,7 @@ "metric": METRIC_TYPE_GIRTH, }, { - "id": "abdomen_value", + "id": "abdomen", "name": "Abdomen Value", "unit": "cm", "category": "Measurements", @@ -560,14 +560,6 @@ "label": "Girth Goals", "metric": METRIC_TYPE_GIRTH_GOAL, }, - { - "id": "arm", - "name": "Arm Goal Value", - "unit": "cm", - "category": "Goals", - "label": "Girth Goals", - "metric": METRIC_TYPE_GIRTH_GOAL, - }, { "id": "chest", "name": "Chest Goal Value", @@ -592,22 +584,6 @@ "label": "Girth Goals", "metric": METRIC_TYPE_GIRTH_GOAL, }, - { - "id": "thigh", - "name": "Thigh Goal Value", - "unit": "cm", - "category": "Goals", - "label": "Girth Goals", - "metric": METRIC_TYPE_GIRTH_GOAL, - }, - { - "id": "calf", - "name": "Calf Goal Value", - "unit": "cm", - "category": "Goals", - "label": "Girth Goals", - "metric": METRIC_TYPE_GIRTH_GOAL, - }, { "id": "left_arm", "name": "Left Arm Goal Value",