From f3fc384f159b02fdb1fcc327acec9ffb3d066758 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sun, 23 Jun 2024 13:52:20 +0300 Subject: [PATCH 01/12] handle timeouts as managed failure instead of general failure --- CHANGELOG.md | 2 ++ custom_components/mydolphin_plus/managers/rest_api.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f0f24..200fbb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Remove startup blocking call - Improve reconnect process (cool-down between attempts) +- Handle timeouts as managed failure instead of general failure +- Ignore update request when the connection is not established ## v1.0.14 diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index b453a1e..a84a1d3 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -188,6 +188,9 @@ async def _async_post(self, url, headers: dict, request_data: str | dict | None) elif crex.status in [404, 405]: self._set_status(ConnectivityStatus.NotFound) + except TimeoutError: + _LOGGER.error(f"Failed to post JSON to {url} due to timeout") + except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno @@ -398,7 +401,6 @@ async def _generate_token(self): async def _load_details(self): if self._status != ConnectivityStatus.Connected: - self._set_status(ConnectivityStatus.Failed) return try: From f3df5c563f4c69bb04cec6dfe3cf49b9ef92febc Mon Sep 17 00:00:00 2001 From: Elad Bar <3207137+elad-bar@users.noreply.github.com> Date: Sun, 23 Jun 2024 13:59:11 +0300 Subject: [PATCH 02/12] handle timeouts as managed failure instead of general failure (#206) --- CHANGELOG.md | 2 ++ custom_components/mydolphin_plus/managers/rest_api.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f0f24..200fbb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Remove startup blocking call - Improve reconnect process (cool-down between attempts) +- Handle timeouts as managed failure instead of general failure +- Ignore update request when the connection is not established ## v1.0.14 diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index b453a1e..a84a1d3 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -188,6 +188,9 @@ async def _async_post(self, url, headers: dict, request_data: str | dict | None) elif crex.status in [404, 405]: self._set_status(ConnectivityStatus.NotFound) + except TimeoutError: + _LOGGER.error(f"Failed to post JSON to {url} due to timeout") + except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno @@ -398,7 +401,6 @@ async def _generate_token(self): async def _load_details(self): if self._status != ConnectivityStatus.Connected: - self._set_status(ConnectivityStatus.Failed) return try: From ee01117361afa12b1d376fe3c23bf657cb446846 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Mon, 24 Jun 2024 09:31:38 +0300 Subject: [PATCH 03/12] improved log messages of status changes --- CHANGELOG.md | 1 + .../mydolphin_plus/managers/aws_client.py | 72 ++++---- .../mydolphin_plus/managers/rest_api.py | 170 ++++++++++-------- 3 files changed, 141 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200fbb0..d1d492a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Improve reconnect process (cool-down between attempts) - Handle timeouts as managed failure instead of general failure - Ignore update request when the connection is not established +- Improved log messages of status changes ## v1.0.14 diff --git a/custom_components/mydolphin_plus/managers/aws_client.py b/custom_components/mydolphin_plus/managers/aws_client.py index 377be48..e32322c 100644 --- a/custom_components/mydolphin_plus/managers/aws_client.py +++ b/custom_components/mydolphin_plus/managers/aws_client.py @@ -180,14 +180,13 @@ def _on_terminate_future_completed(future): self._awsiot_client = None - self._set_status(ConnectivityStatus.Disconnected) - _LOGGER.debug("AWS Client is disconnected") + self._set_status(ConnectivityStatus.Disconnected, "terminate requested") async def initialize(self): try: - _LOGGER.info("Initializing MyDolphin AWS IOT WS") - - self._set_status(ConnectivityStatus.Connecting) + self._set_status( + ConnectivityStatus.Connecting, "Initializing MyDolphin AWS IOT WS" + ) aws_token = self._api_data.get(API_RESPONSE_DATA_TOKEN) aws_key = self._api_data.get(API_RESPONSE_DATA_ACCESS_KEY_ID) @@ -246,11 +245,9 @@ def _on_connect_future_completed(future): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to initialize MyDolphin Plus WS, error: {ex}, line: {line_number}" - ) + message = f"Failed to initialize MyDolphin Plus WS, error: {ex}, line: {line_number}" - self._set_status(ConnectivityStatus.Failed) + self._set_status(ConnectivityStatus.Failed, message) def _subscribe(self): _LOGGER.debug(f"Subscribing topics: {self._topic_data.subscribe}") @@ -333,23 +330,26 @@ def _on_connection_failure(self, connection, callback_data): if connection is not None and isinstance( callback_data, mqtt.OnConnectionFailureData ): - _LOGGER.error(f"AWS IoT connection failed, Error: {callback_data.error}") + message = f"AWS IoT connection failed, Error: {callback_data.error}" - self._set_status(ConnectivityStatus.Failed) + self._set_status(ConnectivityStatus.Failed, message) def _on_connection_closed(self, connection, callback_data): if connection is not None and isinstance( callback_data, mqtt.OnConnectionClosedData ): - _LOGGER.debug("AWS IoT connection was closed") + message = "AWS IoT connection was closed" - self._set_status(ConnectivityStatus.Disconnected) + self._set_status(ConnectivityStatus.Disconnected, message) def _on_connection_interrupted(self, connection, error, **_kwargs): - _LOGGER.error(f"AWS IoT connection interrupted, Error: {error}") + message = f"AWS IoT connection interrupted, Error: {error}" - if connection is not None: - self._set_status(ConnectivityStatus.Failed) + if connection is None: + _LOGGER.error(message) + + else: + self._set_status(ConnectivityStatus.Failed, message) def _on_connection_resumed( self, connection, return_code, session_present, **_kwargs @@ -641,27 +641,39 @@ def _get_led_settings(self, key, value): return data - def _set_status(self, status: ConnectivityStatus): + def _set_status(self, status: ConnectivityStatus, message: str | None = None): + log_level = ConnectivityStatus.get_log_level(status) + if status != self._status: ignored_transitions = IGNORED_TRANSITIONS.get(self._status, []) + should_perform_action = status not in ignored_transitions - if status in ignored_transitions: - return + log_message = f"Status update {self._status} --> {status}" - log_level = ConnectivityStatus.get_log_level(status) + if not should_perform_action: + log_message = f"{log_message}, no action will be performed" - _LOGGER.log( - log_level, - f"Status changed from '{self._status}' to '{status}'", - ) + if message is None: + log_message = f"{log_message}, {message}" - self._status = status + _LOGGER.log(log_level, log_message) - self._async_dispatcher_send( - SIGNAL_AWS_CLIENT_STATUS, - self._config_manager.entry_id, - status, - ) + if should_perform_action: + self._status = status + + self._async_dispatcher_send( + SIGNAL_AWS_CLIENT_STATUS, + self._config_manager.entry_id, + status, + ) + + else: + log_message = f"Status is {status}" + + if message is None: + log_message = f"{log_message}, {message}" + + _LOGGER.log(log_level, log_message) def set_local_async_dispatcher_send(self, callback): self._local_async_dispatcher_send = callback diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index a84a1d3..6509748 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -8,6 +8,7 @@ from typing import Any from aiohttp import ClientResponseError, ClientSession +from aiohttp.hdrs import METH_GET, METH_POST from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -136,7 +137,7 @@ async def terminate(self): if self._session is not None: await self._session.close() - self._set_status(ConnectivityStatus.Disconnected) + self._set_status(ConnectivityStatus.Disconnected, "terminate requested") async def _initialize_session(self): try: @@ -150,11 +151,11 @@ async def _initialize_session(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.warning( + message = ( f"Failed to initialize session, Error: {str(ex)}, Line: {line_number}" ) - self._set_status(ConnectivityStatus.Failed) + self._set_status(ConnectivityStatus.Failed, message) async def validate(self): await self._initialize_session() @@ -178,28 +179,13 @@ async def _async_post(self, url, headers: dict, request_data: str | dict | None) ) except ClientResponseError as crex: - _LOGGER.error( - f"Failed to post JSON to {url}, HTTP Status: {crex.message} ({crex.status})" - ) - - if crex.status in [401, 403]: - self._set_status(ConnectivityStatus.Failed) - - elif crex.status in [404, 405]: - self._set_status(ConnectivityStatus.NotFound) + self._handle_client_error(url, METH_POST, crex) except TimeoutError: - _LOGGER.error(f"Failed to post JSON to {url} due to timeout") + self._handle_server_timeout(url, METH_POST) except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to post JSON to {url}, Error: {ex}, Line: {line_number}" - ) - - self._set_status(ConnectivityStatus.Failed) + self._handle_general_request_failure(url, METH_POST, ex) return result @@ -219,22 +205,13 @@ async def _async_get(self, url, headers: dict): ) except ClientResponseError as crex: - _LOGGER.error( - f"Failed to get data from {url}, HTTP Status: {crex.message} ({crex.status})" - ) + self._handle_client_error(url, METH_GET, crex) - if crex.status in [404, 405]: - self._set_status(ConnectivityStatus.NotFound) + except TimeoutError: + self._handle_server_timeout(url, METH_GET) except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to get data from {url}, Error: {ex}, Line: {line_number}" - ) - - self._set_status(ConnectivityStatus.Failed) + self._handle_general_request_failure(url, METH_GET, ex) return result @@ -262,7 +239,7 @@ async def _login(self): return else: - self._set_status(ConnectivityStatus.Failed) + self._set_status(ConnectivityStatus.Failed, "general failure of login") async def _service_login(self): try: @@ -276,31 +253,35 @@ async def _service_login(self): payload = await self._async_post(LOGIN_URL, LOGIN_HEADERS, request_data) if payload is None: - payload = {} + self._set_status(ConnectivityStatus.Failed, "empty response of login") - data = payload.get(API_RESPONSE_DATA, {}) - if data: - _LOGGER.info(f"Logged in to user {username}") + else: + data = payload.get(API_RESPONSE_DATA) - motor_unit_serial = data.get(API_REQUEST_SERIAL_NUMBER) - token = data.get(API_REQUEST_HEADER_TOKEN) + if data is None: + self._set_status( + ConnectivityStatus.InvalidCredentials, + "empty response payload of login", + ) - self.data[API_DATA_SERIAL_NUMBER] = motor_unit_serial - self.data[API_DATA_LOGIN_TOKEN] = token + else: + _LOGGER.info(f"Logged in to user {username}") - await self._set_actual_motor_unit_serial() + motor_unit_serial = data.get(API_REQUEST_SERIAL_NUMBER) + token = data.get(API_REQUEST_HEADER_TOKEN) - else: - self._set_status(ConnectivityStatus.InvalidCredentials) + self.data[API_DATA_SERIAL_NUMBER] = motor_unit_serial + self.data[API_DATA_LOGIN_TOKEN] = token + + await self._set_actual_motor_unit_serial() except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" - ) - self._set_status(ConnectivityStatus.Failed) + message = f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" + + self._set_status(ConnectivityStatus.Failed, message) async def _set_actual_motor_unit_serial(self): try: @@ -323,24 +304,21 @@ async def _set_actual_motor_unit_serial(self): data: dict = payload.get(API_RESPONSE_DATA, {}) if data is not None: - _LOGGER.info( - f"Successfully retrieved details for device {serial_serial}" - ) + message = f"Successfully retrieved details for device {serial_serial}" self.data[API_DATA_MOTOR_UNIT_SERIAL] = data.get( API_RESPONSE_UNIT_SERIAL_NUMBER ) - self._set_status(ConnectivityStatus.TemporaryConnected) + self._set_status(ConnectivityStatus.TemporaryConnected, message) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" - ) - self._set_status(ConnectivityStatus.Failed) + message = f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" + + self._set_status(ConnectivityStatus.Failed, message) async def _generate_token(self): try: @@ -382,11 +360,9 @@ async def _generate_token(self): self.data[STORAGE_DATA_AWS_TOKEN_ENCRYPTED_KEY] = None if get_token_attempts + 1 >= MAXIMUM_ATTEMPTS_GET_AWS_TOKEN: - _LOGGER.error( - f"Failed to retrieve AWS token after {get_token_attempts} attempts, Error: {alert}" - ) + message = f"Failed to retrieve AWS token after {get_token_attempts} attempts, Error: {alert}" - self._set_status(ConnectivityStatus.Failed) + self._set_status(ConnectivityStatus.Failed, message) get_token_attempts += 1 @@ -394,10 +370,9 @@ async def _generate_token(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to retrieve AWS token from service, Error: {str(ex)}, Line: {line_number}" - ) - self._set_status(ConnectivityStatus.Failed) + message = f"Failed to retrieve AWS token from service, Error: {str(ex)}, Line: {line_number}" + + self._set_status(ConnectivityStatus.Failed, message) async def _load_details(self): if self._status != ConnectivityStatus.Connected: @@ -484,14 +459,16 @@ def _get_aes_key(self): return encryption_key - def _set_status(self, status: ConnectivityStatus): + def _set_status(self, status: ConnectivityStatus, message: str | None = None): + log_level = ConnectivityStatus.get_log_level(status) + if status != self._status: - log_level = ConnectivityStatus.get_log_level(status) + log_message = f"Status update {self._status} --> {status}" - _LOGGER.log( - log_level, - f"Status changed from '{self._status}' to '{status}'", - ) + if message is None: + log_message = f"{log_message}, {message}" + + _LOGGER.log(log_level, log_message) self._status = status @@ -499,6 +476,55 @@ def _set_status(self, status: ConnectivityStatus): SIGNAL_API_STATUS, self._config_manager.entry_id, status ) + else: + log_message = f"Status is {status}" + + if message is None: + log_message = f"{log_message}, {message}" + + _LOGGER.log(log_level, log_message) + + def _handle_client_error( + self, endpoint: str, method: str, crex: ClientResponseError + ): + message = ( + "Failed to send HTTP request, " + f"Endpoint: {endpoint}, " + f"Method: {method}, " + f"HTTP Status: {crex.message} ({crex.status})" + ) + + if crex.status in [404, 405]: + self._set_status(ConnectivityStatus.NotFound, message) + + else: + self._set_status(ConnectivityStatus.Failed, message) + + def _handle_server_timeout(self, endpoint: str, method: str): + message = ( + "Failed to send HTTP request due to timeout, " + f"Endpoint: {endpoint}, " + f"Method: {method}" + ) + + self._set_status(ConnectivityStatus.Failed, message) + + def _handle_general_request_failure( + self, endpoint: str, method: str, ex: Exception + ): + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + + message = ( + "Failed to send HTTP request, " + f"Endpoint: {endpoint}, " + f"Method: {method}, " + f"Error: {ex}, " + f"Line: {line_number}" + ) + + self._set_status(ConnectivityStatus.Failed, message) + def set_local_async_dispatcher_send(self, callback): self._local_async_dispatcher_send = callback From 3f1d792667db82305adc371a018fa9992c714796 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Mon, 24 Jun 2024 10:06:33 +0300 Subject: [PATCH 04/12] fix if statement for logging --- custom_components/mydolphin_plus/managers/aws_client.py | 2 +- custom_components/mydolphin_plus/managers/rest_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/mydolphin_plus/managers/aws_client.py b/custom_components/mydolphin_plus/managers/aws_client.py index e32322c..8e881e5 100644 --- a/custom_components/mydolphin_plus/managers/aws_client.py +++ b/custom_components/mydolphin_plus/managers/aws_client.py @@ -653,7 +653,7 @@ def _set_status(self, status: ConnectivityStatus, message: str | None = None): if not should_perform_action: log_message = f"{log_message}, no action will be performed" - if message is None: + if message is not None: log_message = f"{log_message}, {message}" _LOGGER.log(log_level, log_message) diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index 6509748..95abf6c 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -465,7 +465,7 @@ def _set_status(self, status: ConnectivityStatus, message: str | None = None): if status != self._status: log_message = f"Status update {self._status} --> {status}" - if message is None: + if message is not None: log_message = f"{log_message}, {message}" _LOGGER.log(log_level, log_message) From 64754ff6b830b6945ff983b507b16cc29b72ba10 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 5 Jul 2024 14:20:22 +0300 Subject: [PATCH 05/12] refactor states --- CHANGELOG.md | 29 ++ .../mydolphin_plus/common/calculated_state.py | 23 ++ .../mydolphin_plus/common/consts.py | 116 ++---- .../common/power_supply_state.py | 10 + .../mydolphin_plus/common/robot_state.py | 10 + .../mydolphin_plus/managers/aws_client.py | 7 +- .../mydolphin_plus/managers/coordinator.py | 187 +++------- .../mydolphin_plus/managers/rest_api.py | 3 - .../mydolphin_plus/manifest.json | 2 +- .../mydolphin_plus/models/system_details.py | 136 +++++++ custom_components/mydolphin_plus/strings.json | 6 +- custom_components/mydolphin_plus/vacuum.py | 16 - requirements.txt | 14 +- tests/indicators_test.py | 333 +++++------------- 14 files changed, 376 insertions(+), 516 deletions(-) create mode 100644 custom_components/mydolphin_plus/common/calculated_state.py create mode 100644 custom_components/mydolphin_plus/common/power_supply_state.py create mode 100644 custom_components/mydolphin_plus/common/robot_state.py create mode 100644 custom_components/mydolphin_plus/models/system_details.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d492a..7e48115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## v1.0.16 + +- Improved log messages of status changes +- Removed vacuum actions + - Turn on - not supported + - Turn off - not supported + - Pause - acts as stop, calls stop, no need for duplicate functionality + - Toggle - Non turn on / off, no need +- Clean unused constants +- Refactor calculated status + - Move to dedicated class + - Adjust tests + - Remove on state, instead introduce idle state, off state remain + +| Power Supply State | Robot State | Calculated State | +| ------------------ | --------------------------------------------------- | ---------------- | +| error | \* | error | +| \* | fault | error | +| holdDelay | notConnected, programming, init. scanning, finished | hold_delay | +| holdWeekly | notConnected, programming, init. scanning, finished | hold_weekly | +| on | init | init | +| on | programming, scanning | cleaning | +| programming | notConnected, init, scanning | cleaning | +| programming | programming | programming | +| programming | finished | off | +| off | \* (but fault) | off | + +Unmatched matching, will be treated as off. + ## v1.0.15 - Remove startup blocking call diff --git a/custom_components/mydolphin_plus/common/calculated_state.py b/custom_components/mydolphin_plus/common/calculated_state.py new file mode 100644 index 0000000..e263919 --- /dev/null +++ b/custom_components/mydolphin_plus/common/calculated_state.py @@ -0,0 +1,23 @@ +from enum import StrEnum + + +class CalculatedState(StrEnum): + OFF = "off" + PROGRAMMING = "programming" + ERROR = "error" + CLEANING = "cleaning" + INIT = "init" + HOLD_DELAY = "hold_delay" + HOLD_WEEKLY = "hold_weekly" + + @staticmethod + def is_on_state(value) -> bool: + is_on = value in [ + CalculatedState.INIT, + CalculatedState.CLEANING, + CalculatedState.PROGRAMMING, + CalculatedState.HOLD_WEEKLY, + CalculatedState.HOLD_DELAY, + ] + + return is_on diff --git a/custom_components/mydolphin_plus/common/consts.py b/custom_components/mydolphin_plus/common/consts.py index c2fb0ee..b3e477d 100644 --- a/custom_components/mydolphin_plus/common/consts.py +++ b/custom_components/mydolphin_plus/common/consts.py @@ -6,7 +6,6 @@ MANUFACTURER = "Maytronics" DEFAULT_NAME = "MyDolphin Plus" DOMAIN = "mydolphin_plus" -DATA = f"{DOMAIN}_DATA" LEGACY_KEY_FILE = f"{DOMAIN}.key" CONFIGURATION_FILE = f"{DOMAIN}.config.json" @@ -29,37 +28,25 @@ Platform.NUMBER, ] -ATTR_EVENT = "Error" ATTR_IS_ON = "is_on" -ATTR_FRIENDLY_NAME = "friendly_name" ATTR_START_TIME = "start_time" ATTR_STATUS = "status" ATTR_RESET_FBI = "reset_fbi" -ATTR_RSSI = "RSSI" -ATTR_NETWORK_NAME = "network_name" -ATTR_INTENSITY = "intensity" ATTR_EXPECTED_END_TIME = "expected_end_time" -ATTR_BATTERY_LEVEL = "battery_level" -ATTR_AWS_IOT_BROKER_STATUS = "aws_iot_broker_status" - -ATTR_CALCULATED_STATUS = "calculated_status" -ATTR_PWS_STATUS = "pws_status" -ATTR_ROBOT_STATUS = "robot_status" -ATTR_ROBOT_TYPE = "robot_type" -ATTR_IS_BUSY = "busy" -ATTR_TURN_ON_COUNT = "turn_on_count" -ATTR_TIME_ZONE = "time_zone" - -ATTR_ENABLE = "enable" -ATTR_DISABLED = "disabled" +ATTR_CALCULATED_STATUS = "Calculated State" +ATTR_POWER_SUPPLY_STATE = "Power Supply State" +ATTR_ROBOT_STATE = "Robot State" +ATTR_ROBOT_TYPE = "Robot Type" +ATTR_IS_BUSY = "Busy" +ATTR_TURN_ON_COUNT = "Turn On Count" +ATTR_TIME_ZONE = "Time Zone" DYNAMIC_TYPE = "type" DYNAMIC_DESCRIPTION = "description" DYNAMIC_DESCRIPTION_JOYSTICK = "joystick" DYNAMIC_DESCRIPTION_TEMPERATURE = "temperature" DYNAMIC_TYPE_PWS_REQUEST = "pwsRequest" -DYNAMIC_TYPE_PWS_RESPONSE = "pwsResponse" DYNAMIC_TYPE_IOT_RESPONSE = "iotResponse" DYNAMIC_CONTENT = "content" DYNAMIC_CONTENT_SERIAL_NUMBER = "robotSerial" @@ -84,9 +71,6 @@ DATA_SECTION_WIFI = "wifi" DATA_SECTION_CYCLE_INFO = "cycleInfo" DATA_SECTION_FILTER_BAG_INDICATION = "filterBagIndication" -DATA_SECTION_WEEKLY_SETTINGS = "weeklySettings" -DATA_SECTION_DELAY = "delay" -DATA_SECTION_FEATURE = "featureEn" DATA_SECTION_SYSTEM_STATE = "systemState" DATA_SECTION_ROBOT_ERROR = "robotError" DATA_SECTION_PWS_ERROR = "pwsError" @@ -102,14 +86,11 @@ DATA_SYSTEM_STATE_TIME_ZONE = "timeZone" DATA_SYSTEM_STATE_TIME_ZONE_NAME = "timeZoneName" -DATA_FEATURE_WEEKLY_TIMER = "weeklyTimer" - DATA_SCHEDULE_IS_ENABLED = "isEnabled" DATA_SCHEDULE_CLEANING_MODE = "cleaningMode" DATA_SCHEDULE_TIME = "time" DATA_SCHEDULE_TIME_HOURS = "hours" DATA_SCHEDULE_TIME_MINUTES = "minutes" -DATA_SCHEDULE_TRIGGERED_BY = "triggeredBy" DATA_FILTER_BAG_INDICATION_RESET_FBI = "resetFBI" DATA_FILTER_BAG_INDICATION_RESET_FBI_COMMAND = "resetFbi" @@ -131,11 +112,9 @@ DEFAULT_ENABLE = False DEFAULT_TIME_ZONE_NAME = "UTC" DEFAULT_TIME_PART = 255 -DEFAULT_BATTERY_LEVEL = "NA" UPDATE_API_INTERVAL = timedelta(seconds=60) UPDATE_ENTITIES_INTERVAL = timedelta(seconds=5) -LOCATE_OFF_INTERVAL_SECONDS = timedelta(seconds=10) API_RECONNECT_INTERVAL = timedelta(seconds=30) WS_RECONNECT_INTERVAL = timedelta(minutes=1) @@ -177,9 +156,6 @@ BLOCK_SIZE = 16 -MQTT_QOS_0 = 0 -MQTT_QOS_1 = 1 - MQTT_MESSAGE_ENCODING = "utf-8" AWS_REGION = "eu-west-1" @@ -220,7 +196,6 @@ } ATTR_ERROR_DESCRIPTIONS = "Description" -ATTR_LED_MODE = "led_mode" ATTR_ATTRIBUTES = "attributes" ATTR_ACTIONS = "actions" ATTR_INSTRUCTIONS = "instructions" @@ -256,56 +231,21 @@ JOYSTICK_LEFT, ] -CLOCK_HOURS_ICONS = { - 0: "mdi:clock-time-twelve", - 1: "mdi:clock-time-one", - 2: "mdi:clock-time-two", - 3: "mdi:clock-time-three", - 4: "mdi:clock-time-four", - 5: "mdi:clock-time-five", - 6: "mdi:clock-time-six", - 7: "mdi:clock-time-seven", - 8: "mdi:clock-time-eight", - 9: "mdi:clock-time-nine", - 10: "mdi:clock-time-ten", - 11: "mdi:clock-time-eleven", - 12: "mdi:clock-time-twelve", - 13: "mdi:clock-time-one", - 14: "mdi:clock-time-two", - 15: "mdi:clock-time-three", - 16: "mdi:clock-time-four", - 17: "mdi:clock-time-five", - 18: "mdi:clock-time-six", - 19: "mdi:clock-time-seven", - 20: "mdi:clock-time-eight", - 21: "mdi:clock-time-nine", - 22: "mdi:clock-time-ten", - 23: "mdi:clock-time-eleven", -} - -PWS_STATE_ON = "on" -PWS_STATE_OFF = "off" -PWS_STATE_HOLD_DELAY = "holddelay" -PWS_STATE_HOLD_WEEKLY = "holdweekly" -PWS_STATE_PROGRAMMING = "programming" -PWS_STATE_ERROR = "error" -PWS_STATE_CLEANING = "cleaning" - -ROBOT_STATE_FINISHED = "finished" -ROBOT_STATE_FAULT = "fault" -ROBOT_STATE_NOT_CONNECTED = "notConnected" -ROBOT_STATE_PROGRAMMING = "programming" -ROBOT_STATE_INIT = "init" -ROBOT_STATE_SCANNING = "scanning" - -CONSIDERED_POWER_STATE = { - PWS_STATE_OFF: False, - PWS_STATE_ERROR: False, - PWS_STATE_ON: True, - PWS_STATE_CLEANING: True, - PWS_STATE_PROGRAMMING: True, - ROBOT_STATE_INIT: True, -} +CLOCK_HOURS_ICON = "mdi:clock-time-" +CLOCK_HOURS_TEXT = [ + "twelve", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", +] FILTER_BAG_STATUS = { "unknown": (-1, -1), @@ -336,9 +276,6 @@ | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.START | VacuumEntityFeature.STOP - | VacuumEntityFeature.PAUSE - | VacuumEntityFeature.TURN_ON - | VacuumEntityFeature.TURN_OFF | VacuumEntityFeature.LOCATE ) @@ -346,12 +283,6 @@ STORAGE_DATA_LOCATING = "locating" STORAGE_DATA_AWS_TOKEN_ENCRYPTED_KEY = "aws-token-encrypted-key" -STORAGE_DATA_FILE_CONFIG = "config" - -STORAGE_DATA_FILES = [STORAGE_DATA_FILE_CONFIG] - -DATA_KEYS = [CONF_USERNAME, CONF_PASSWORD] - DATA_KEY_STATUS = "Status" DATA_KEY_VACUUM = "Vacuum" DATA_KEY_LED_MODE = "LED Mode" @@ -361,7 +292,6 @@ DATA_KEY_CYCLE_TIME = "Cycle Time" DATA_KEY_CYCLE_TIME_LEFT = "Cycle Time Left" DATA_KEY_AWS_BROKER = "AWS Broker" -DATA_KEY_WEEKLY_SCHEDULER = "Weekly Scheduler" DATA_KEY_SCHEDULE = "Schedule" DATA_KEY_RSSI = "RSSI" DATA_KEY_NETWORK_NAME = "Network Name" @@ -378,10 +308,8 @@ ACTION_ENTITY_SET_FAN_SPEED = "set_fan_speed" ACTION_ENTITY_START = "start" ACTION_ENTITY_STOP = "stop" -ACTION_ENTITY_PAUSE = "stop" ACTION_ENTITY_TURN_ON = "turn_on" ACTION_ENTITY_TURN_OFF = "turn_off" -ACTION_ENTITY_TOGGLE = "toggle" ACTION_ENTITY_SEND_COMMAND = "send_command" ACTION_ENTITY_LOCATE = "locate" ACTION_ENTITY_SELECT_OPTION = "select_option" diff --git a/custom_components/mydolphin_plus/common/power_supply_state.py b/custom_components/mydolphin_plus/common/power_supply_state.py new file mode 100644 index 0000000..3f76230 --- /dev/null +++ b/custom_components/mydolphin_plus/common/power_supply_state.py @@ -0,0 +1,10 @@ +from enum import StrEnum + + +class PowerSupplyState(StrEnum): + ON = "on" + OFF = "off" + HOLD_DELAY = "holdDelay" + HOLD_WEEKLY = "holdWeekly" + PROGRAMMING = "programming" + ERROR = "error" diff --git a/custom_components/mydolphin_plus/common/robot_state.py b/custom_components/mydolphin_plus/common/robot_state.py new file mode 100644 index 0000000..a9b79fc --- /dev/null +++ b/custom_components/mydolphin_plus/common/robot_state.py @@ -0,0 +1,10 @@ +from enum import StrEnum + + +class RobotState(StrEnum): + FAULT = "fault" + NOT_CONNECTED = "notConnected" + PROGRAMMING = "programming" + INIT = "init" + SCANNING = "scanning" + FINISHED = "finished" diff --git a/custom_components/mydolphin_plus/managers/aws_client.py b/custom_components/mydolphin_plus/managers/aws_client.py index 8e881e5..c3d4088 100644 --- a/custom_components/mydolphin_plus/managers/aws_client.py +++ b/custom_components/mydolphin_plus/managers/aws_client.py @@ -71,8 +71,6 @@ JOYSTICK_SPEED, LED_MODE_BLINKING, MQTT_MESSAGE_ENCODING, - PWS_STATE_OFF, - PWS_STATE_ON, SIGNAL_AWS_CLIENT_STATUS, TOPIC_CALLBACK_ACCEPTED, TOPIC_CALLBACK_REJECTED, @@ -82,6 +80,7 @@ WS_DATA_VERSION, WS_LAST_UPDATE, ) +from ..common.power_supply_state import PowerSupplyState from ..common.robot_family import RobotFamily from ..models.topic_data import TopicData from .config_manager import ConfigManager @@ -586,10 +585,10 @@ def _read_temperature_and_in_water_details(self): def pickup(self): self.set_cleaning_mode(CleanModes.PICKUP) - def set_power_state(self, is_on: bool): + def power_off(self): request_data = { DATA_SECTION_SYSTEM_STATE: { - DATA_SYSTEM_STATE_PWS_STATE: PWS_STATE_ON if is_on else PWS_STATE_OFF + DATA_SYSTEM_STATE_PWS_STATE: PowerSupplyState.OFF.value } } diff --git a/custom_components/mydolphin_plus/managers/coordinator.py b/custom_components/mydolphin_plus/managers/coordinator.py index a12ea5f..73d14e3 100644 --- a/custom_components/mydolphin_plus/managers/coordinator.py +++ b/custom_components/mydolphin_plus/managers/coordinator.py @@ -13,11 +13,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify +from ..common.calculated_state import CalculatedState from ..common.clean_modes import CleanModes, get_clean_mode_cycle_time_key from ..common.connectivity_status import ConnectivityStatus from ..common.consts import ( ACTION_ENTITY_LOCATE, - ACTION_ENTITY_PAUSE, ACTION_ENTITY_RETURN_TO_BASE, ACTION_ENTITY_SELECT_OPTION, ACTION_ENTITY_SEND_COMMAND, @@ -25,29 +25,21 @@ ACTION_ENTITY_SET_NATIVE_VALUE, ACTION_ENTITY_START, ACTION_ENTITY_STOP, - ACTION_ENTITY_TOGGLE, ACTION_ENTITY_TURN_OFF, ACTION_ENTITY_TURN_ON, API_DATA_SERIAL_NUMBER, API_RECONNECT_INTERVAL, ATTR_ACTIONS, ATTR_ATTRIBUTES, - ATTR_CALCULATED_STATUS, ATTR_EXPECTED_END_TIME, - ATTR_IS_BUSY, ATTR_IS_ON, - ATTR_PWS_STATUS, ATTR_RESET_FBI, - ATTR_ROBOT_STATUS, - ATTR_ROBOT_TYPE, ATTR_START_TIME, ATTR_STATUS, - ATTR_TIME_ZONE, - ATTR_TURN_ON_COUNT, - CLOCK_HOURS_ICONS, + CLOCK_HOURS_ICON, + CLOCK_HOURS_TEXT, CONF_DIRECTION, CONFIGURATION_URL, - CONSIDERED_POWER_STATE, DATA_CYCLE_INFO_CLEANING_MODE, DATA_CYCLE_INFO_CLEANING_MODE_DURATION, DATA_CYCLE_INFO_CLEANING_MODE_START_TIME, @@ -87,18 +79,11 @@ DATA_SECTION_ROBOT_ERROR, DATA_SECTION_SYSTEM_STATE, DATA_SECTION_WIFI, - DATA_SYSTEM_STATE_IS_BUSY, - DATA_SYSTEM_STATE_PWS_STATE, - DATA_SYSTEM_STATE_ROBOT_STATE, - DATA_SYSTEM_STATE_ROBOT_TYPE, - DATA_SYSTEM_STATE_TIME_ZONE, - DATA_SYSTEM_STATE_TIME_ZONE_NAME, DATA_SYSTEM_STATE_TURN_ON_COUNT, DATA_WIFI_NETWORK_NAME, DEFAULT_ENABLE, DEFAULT_LED_INTENSITY, DEFAULT_NAME, - DEFAULT_TIME_ZONE_NAME, DOMAIN, DYNAMIC_DESCRIPTION_TEMPERATURE, DYNAMIC_TYPE_IOT_RESPONSE, @@ -110,17 +95,6 @@ LED_MODE_ICON_DEFAULT, MANUFACTURER, PLATFORMS, - PWS_STATE_CLEANING, - PWS_STATE_ERROR, - PWS_STATE_HOLD_DELAY, - PWS_STATE_HOLD_WEEKLY, - PWS_STATE_OFF, - PWS_STATE_ON, - PWS_STATE_PROGRAMMING, - ROBOT_STATE_FAULT, - ROBOT_STATE_INIT, - ROBOT_STATE_NOT_CONNECTED, - ROBOT_STATE_SCANNING, SIGNAL_API_STATUS, SIGNAL_AWS_CLIENT_STATUS, UPDATE_API_INTERVAL, @@ -131,6 +105,7 @@ SERVICE_NAVIGATE, SERVICE_VALIDATION, ) +from ..models.system_details import SystemDetails from .aws_client import AWSClient from .config_manager import ConfigManager from .rest_api import RestAPI @@ -145,7 +120,7 @@ class MyDolphinPlusCoordinator(DataUpdateCoordinator): _aws_client: AWSClient | None _data_mapping: dict[str, Callable[[EntityDescription], dict | None]] | None - _system_status_details: dict | None + _system_details: SystemDetails _last_update: float @@ -165,7 +140,7 @@ def __init__(self, hass, config_manager: ConfigManager): self._config_manager = config_manager self._data_mapping = None - self._system_status_details = None + self._system_details = SystemDetails() self._last_update = 0 @@ -414,7 +389,7 @@ def get_data(self, entity_description: EntityDescription) -> dict | None: ) else: - if self._system_status_details is not None: + if self._system_details.is_updated: result = handler(entity_description) except Exception as ex: @@ -437,11 +412,11 @@ def get_device_action( return async_action def _get_status_data(self, _entity_description) -> dict | None: - state = self._system_status_details.get(ATTR_CALCULATED_STATUS) + state = self._system_details.calculated_state result = { ATTR_STATE: None if state is None else state.lower(), - ATTR_ATTRIBUTES: self._system_status_details, + ATTR_ATTRIBUTES: self._system_details.data, } return result @@ -485,35 +460,35 @@ def _get_clean_mode_data(self, _entity_description) -> dict | None: return result def _get_power_supply_status_data(self, _entity_description) -> dict | None: - state = self._system_status_details.get(ATTR_PWS_STATUS) + state = self._system_details.power_unit_state.lower() - result = {ATTR_STATE: state} + result = {ATTR_STATE: None if state is None else state.lower()} return result def _get_robot_status_data(self, _entity_description) -> dict | None: - state = self._system_status_details.get(ATTR_ROBOT_STATUS) + state = self._system_details.robot_state.lower() result = {ATTR_STATE: None if state is None else state.lower()} return result def _get_robot_type_data(self, _entity_description) -> dict | None: - state = self._system_status_details.get(ATTR_ROBOT_TYPE) + state = self._system_details.robot_type result = {ATTR_STATE: state} return result def _get_busy_data(self, _entity_description) -> dict | None: - is_on = self._system_status_details.get(ATTR_IS_BUSY) + is_on = self._system_details.is_busy result = {ATTR_IS_ON: is_on} return result def _get_cycle_count_data(self, _entity_description) -> dict | None: - state = self._system_status_details.get(ATTR_TURN_ON_COUNT) + state = self._system_details.turn_on_count result = {ATTR_STATE: state} @@ -524,18 +499,14 @@ def _get_vacuum_data(self, _entity_description) -> dict | None: cleaning_mode = cycle_info.get(DATA_CYCLE_INFO_CLEANING_MODE, {}) mode = cleaning_mode.get(ATTR_MODE, CleanModes.REGULAR) - state = self._system_status_details.get(ATTR_CALCULATED_STATUS) + state = self._system_details.calculated_state.lower() result = { ATTR_STATE: state, ATTR_ATTRIBUTES: {ATTR_MODE: mode}, ATTR_ACTIONS: { - ACTION_ENTITY_TURN_ON: self._vacuum_turn_on, - ACTION_ENTITY_TURN_OFF: self._vacuum_turn_off, - ACTION_ENTITY_TOGGLE: self._vacuum_toggle, ACTION_ENTITY_START: self._vacuum_start, ACTION_ENTITY_STOP: self._vacuum_stop, - ACTION_ENTITY_PAUSE: self._vacuum_pause, ACTION_ENTITY_SET_FAN_SPEED: self._set_cleaning_mode, ACTION_ENTITY_LOCATE: self._vacuum_locate, ACTION_ENTITY_SEND_COMMAND: self._send_command, @@ -640,26 +611,27 @@ def _get_cycle_time_data(self, _entity_description) -> dict | None: DATA_CYCLE_INFO_CLEANING_MODE_DURATION, 0 ) cycle_time = timedelta(minutes=cycle_time_minutes) - cycle_time_hours = cycle_time / timedelta(hours=1) + cycle_time_hours = int(cycle_time / timedelta(hours=1)) cycle_start_time_ts = cycle_info.get( DATA_CYCLE_INFO_CLEANING_MODE_START_TIME, 0 ) cycle_start_time = self._get_date_time_from_timestamp(cycle_start_time_ts) + icon = self._get_hour_icon(cycle_time_hours) + result = { ATTR_STATE: cycle_time_minutes, ATTR_ATTRIBUTES: { ATTR_START_TIME: cycle_start_time, }, - ATTR_ICON: CLOCK_HOURS_ICONS.get(cycle_time_hours, "mdi:clock-time-twelve"), + ATTR_ICON: icon, } return result def _get_cycle_time_left_data(self, _entity_description) -> dict | None: - system_details = self._system_status_details - calculated_state = system_details.get(ATTR_CALCULATED_STATUS) + calculated_state = self._system_details.calculated_state cycle_info = self.aws_data.get(DATA_SECTION_CYCLE_INFO, {}) cleaning_mode = cycle_info.get(DATA_CYCLE_INFO_CLEANING_MODE, {}) @@ -682,7 +654,7 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: seconds_left = 0 if ( - calculated_state in [PWS_STATE_ON, PWS_STATE_CLEANING] + calculated_state == CalculatedState.CLEANING and expected_cycle_end_time_ts > now_ts ): seconds_left = expected_cycle_end_time_ts - now_ts @@ -690,13 +662,15 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: state = timedelta(seconds=seconds_left).total_seconds() state_hours = int((expected_cycle_end_time - now) / timedelta(hours=1)) + icon = self._get_hour_icon(state_hours) + result = { ATTR_STATE: state, ATTR_ATTRIBUTES: { ATTR_START_TIME: cycle_start_time, ATTR_EXPECTED_END_TIME: expected_cycle_end_time, }, - ATTR_ICON: CLOCK_HOURS_ICONS.get(state_hours, "mdi:clock-time-twelve"), + ATTR_ICON: icon, } return result @@ -793,28 +767,6 @@ async def _pickup(self, _entity_description: EntityDescription): self._aws_client.pickup() - async def _switch_power(self, state: str, desired_state: bool): - considered_state = CONSIDERED_POWER_STATE.get(state, False) - _LOGGER.debug(f"Set vacuum power state, State: {state}, Power: {desired_state}") - - if considered_state != desired_state: - self._aws_client.set_power_state(desired_state) - - async def _vacuum_turn_on(self, _entity_description: EntityDescription, _state): - data = self._get_vacuum_data(None) - attributes = data.get(ATTR_ATTRIBUTES) - mode = attributes.get(ATTR_MODE, CleanModes.REGULAR) - - self._aws_client.set_cleaning_mode(mode) - - async def _vacuum_turn_off(self, _entity_description: EntityDescription, state): - await self._switch_power(state, False) - - async def _vacuum_toggle(self, _entity_description: EntityDescription, state): - considered_state = CONSIDERED_POWER_STATE.get(state, False) - - await self._switch_power(state, not considered_state) - async def _vacuum_start(self, _entity_description: EntityDescription, _state): data = self._get_vacuum_data(None) attributes = data.get(ATTR_ATTRIBUTES) @@ -823,10 +775,11 @@ async def _vacuum_start(self, _entity_description: EntityDescription, _state): self._aws_client.set_cleaning_mode(mode) async def _vacuum_stop(self, _entity_description: EntityDescription, state): - await self._switch_power(state, False) + is_on_state = CalculatedState.is_on_state(state) + _LOGGER.debug(f"Set vacuum power state, State: {state}, Power: {is_on_state}") - async def _vacuum_pause(self, _entity_description: EntityDescription, state): - await self._switch_power(state, False) + if is_on_state: + self._aws_client.power_off() async def _vacuum_locate(self, entity_description: EntityDescription): led_light_entity = self._get_led_data(None) @@ -883,79 +836,33 @@ async def _service_navigate(self, data: dict[str, Any] | list[Any] | None): def _set_system_status_details(self): data = self.aws_data - system_state = data.get(DATA_SECTION_SYSTEM_STATE, {}) - pws_state = system_state.get(DATA_SYSTEM_STATE_PWS_STATE, PWS_STATE_OFF) - robot_state = system_state.get( - DATA_SYSTEM_STATE_ROBOT_STATE, ROBOT_STATE_NOT_CONNECTED - ) - robot_type = system_state.get(DATA_SYSTEM_STATE_ROBOT_TYPE) - is_busy = system_state.get(DATA_SYSTEM_STATE_IS_BUSY, False) - turn_on_count = system_state.get(DATA_SYSTEM_STATE_TURN_ON_COUNT, 0) - time_zone = system_state.get(DATA_SYSTEM_STATE_TIME_ZONE, 0) - time_zone_name = system_state.get( - DATA_SYSTEM_STATE_TIME_ZONE_NAME, DEFAULT_TIME_ZONE_NAME - ) - - calculated_state = PWS_STATE_OFF - - pws_on = pws_state.lower() in [ - PWS_STATE_ON, - PWS_STATE_HOLD_DELAY, - PWS_STATE_HOLD_WEEKLY, - PWS_STATE_PROGRAMMING, - ] - pws_error = pws_state in [PWS_STATE_ERROR] - pws_cleaning = pws_state in [PWS_STATE_ON, ROBOT_STATE_SCANNING] - pws_programming = pws_state == PWS_STATE_PROGRAMMING - - robot_error = robot_state in [ROBOT_STATE_FAULT] - robot_cleaning = robot_state not in [ROBOT_STATE_NOT_CONNECTED] - - robot_programming = robot_state == PWS_STATE_PROGRAMMING - - if pws_error or robot_error: - calculated_state = PWS_STATE_ERROR - - elif pws_programming and robot_programming: - calculated_state = PWS_STATE_PROGRAMMING - - elif pws_on: - if (pws_cleaning and robot_cleaning) or ( - pws_programming and not robot_programming - ): - calculated_state = ( - ROBOT_STATE_INIT - if robot_state == ROBOT_STATE_INIT - else PWS_STATE_CLEANING - ) - - else: - calculated_state = PWS_STATE_OFF - - result = { - ATTR_CALCULATED_STATUS: calculated_state, - ATTR_PWS_STATUS: pws_state, - ATTR_ROBOT_STATUS: robot_state, - ATTR_ROBOT_TYPE: robot_type, - ATTR_IS_BUSY: is_busy, - ATTR_TURN_ON_COUNT: turn_on_count, - ATTR_TIME_ZONE: f"{time_zone_name} ({time_zone})", - } + updated = self._system_details.update(data) - if self._system_status_details != result: + if updated: self._can_load_components = True _LOGGER.debug( f"System status recalculated, " - f"Calculated State: {calculated_state}, " - f"Main Unit State: {pws_state}, " - f"Robot State: {robot_state}" + f"Calculated State: {self._system_details.calculated_state}, " + f"Main Unit State: {self._system_details.power_unit_state}, " + f"Robot State: {self._system_details.robot_state}" ) - self._system_status_details = result - @staticmethod def _get_date_time_from_timestamp(timestamp): result = datetime.fromtimestamp(timestamp) return result + + @staticmethod + def _get_hour_icon(current_hour: int) -> str: + if current_hour > 11: + current_hour = current_hour - 12 + + if current_hour >= len(CLOCK_HOURS_TEXT): + current_hour = 0 + + hour_text = CLOCK_HOURS_TEXT[current_hour] + icon = "".join([CLOCK_HOURS_ICON, hour_text]) + + return icon diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index 24a86c0..95abf6c 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -184,9 +184,6 @@ async def _async_post(self, url, headers: dict, request_data: str | dict | None) except TimeoutError: self._handle_server_timeout(url, METH_POST) - except TimeoutError: - _LOGGER.error(f"Failed to post JSON to {url} due to timeout") - except Exception as ex: self._handle_general_request_failure(url, METH_POST, ex) diff --git a/custom_components/mydolphin_plus/manifest.json b/custom_components/mydolphin_plus/manifest.json index ec43d99..41abcb6 100644 --- a/custom_components/mydolphin_plus/manifest.json +++ b/custom_components/mydolphin_plus/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "issue_tracker": "https://github.com/sh00t2kill/dolphin-robot/issues", "requirements": ["awsiotsdk"], - "version": "1.0.15" + "version": "1.0.16" } diff --git a/custom_components/mydolphin_plus/models/system_details.py b/custom_components/mydolphin_plus/models/system_details.py new file mode 100644 index 0000000..d48f90f --- /dev/null +++ b/custom_components/mydolphin_plus/models/system_details.py @@ -0,0 +1,136 @@ +from custom_components.mydolphin_plus.common.calculated_state import CalculatedState +from custom_components.mydolphin_plus.common.consts import ( + ATTR_CALCULATED_STATUS, + ATTR_IS_BUSY, + ATTR_POWER_SUPPLY_STATE, + ATTR_ROBOT_STATE, + ATTR_ROBOT_TYPE, + ATTR_TIME_ZONE, + ATTR_TURN_ON_COUNT, + DATA_SECTION_SYSTEM_STATE, + DATA_SYSTEM_STATE_IS_BUSY, + DATA_SYSTEM_STATE_PWS_STATE, + DATA_SYSTEM_STATE_ROBOT_STATE, + DATA_SYSTEM_STATE_ROBOT_TYPE, + DATA_SYSTEM_STATE_TIME_ZONE, + DATA_SYSTEM_STATE_TIME_ZONE_NAME, + DATA_SYSTEM_STATE_TURN_ON_COUNT, + DEFAULT_TIME_ZONE_NAME, +) +from custom_components.mydolphin_plus.common.power_supply_state import PowerSupplyState +from custom_components.mydolphin_plus.common.robot_state import RobotState + + +class SystemDetails: + _is_updated: bool + _data: dict + + def __init__(self): + self._is_updated = False + self._data = {} + + @property + def is_updated(self) -> bool: + return self._is_updated + + @property + def data(self) -> dict: + return self._data + + @property + def calculated_state(self) -> CalculatedState: + return self._data.get(ATTR_CALCULATED_STATUS, CalculatedState.OFF) + + @property + def power_unit_state(self) -> PowerSupplyState: + return self._data.get(ATTR_POWER_SUPPLY_STATE, PowerSupplyState.OFF) + + @property + def robot_state(self) -> RobotState: + return self._data.get(ATTR_ROBOT_STATE, RobotState.NOT_CONNECTED) + + @property + def robot_type(self) -> str | None: + return self._data.get(ATTR_ROBOT_TYPE) + + @property + def is_busy(self) -> bool | None: + return self._data.get(ATTR_IS_BUSY) + + @property + def turn_on_count(self) -> int: + return self._data.get(ATTR_TURN_ON_COUNT, 0) + + @property + def time_zone(self) -> str | None: + return self._data.get(ATTR_TIME_ZONE) + + def update(self, data: dict) -> bool: + new_data = self._get_updated_data(data) + + changed_keys = [key for key in new_data if new_data[key] != self._data.get(key)] + + was_changed = len(changed_keys) > 0 + + if was_changed: + self._is_updated = True + self._data = new_data + + return was_changed + + @staticmethod + def _get_updated_data(data: dict): + system_state = data.get(DATA_SECTION_SYSTEM_STATE, {}) + power_supply_state = system_state.get( + DATA_SYSTEM_STATE_PWS_STATE, PowerSupplyState.OFF.value + ) + robot_state = system_state.get( + DATA_SYSTEM_STATE_ROBOT_STATE, RobotState.NOT_CONNECTED.value + ) + robot_type = system_state.get(DATA_SYSTEM_STATE_ROBOT_TYPE) + is_busy = system_state.get(DATA_SYSTEM_STATE_IS_BUSY, False) + turn_on_count = system_state.get(DATA_SYSTEM_STATE_TURN_ON_COUNT, 0) + time_zone = system_state.get(DATA_SYSTEM_STATE_TIME_ZONE, 0) + time_zone_name = system_state.get( + DATA_SYSTEM_STATE_TIME_ZONE_NAME, DEFAULT_TIME_ZONE_NAME + ) + + calculated_state = CalculatedState.OFF + + if power_supply_state == PowerSupplyState.ERROR: + calculated_state = CalculatedState.ERROR + + elif robot_state == RobotState.FAULT: + calculated_state = CalculatedState.ERROR + + elif power_supply_state == PowerSupplyState.PROGRAMMING: + if robot_state == RobotState.PROGRAMMING: + calculated_state = CalculatedState.PROGRAMMING + + elif robot_state != RobotState.FINISHED: + calculated_state = CalculatedState.CLEANING + + elif power_supply_state == PowerSupplyState.ON: + if robot_state == RobotState.INIT: + calculated_state = CalculatedState.INIT + + elif robot_state not in [RobotState.NOT_CONNECTED, RobotState.FINISHED]: + calculated_state = CalculatedState.CLEANING + + elif power_supply_state == PowerSupplyState.HOLD_DELAY: + calculated_state = CalculatedState.HOLD_DELAY + + elif power_supply_state == PowerSupplyState.HOLD_WEEKLY: + calculated_state = CalculatedState.HOLD_WEEKLY + + result = { + ATTR_CALCULATED_STATUS: calculated_state, + ATTR_POWER_SUPPLY_STATE: power_supply_state, + ATTR_ROBOT_STATE: robot_state, + ATTR_ROBOT_TYPE: robot_type, + ATTR_IS_BUSY: is_busy, + ATTR_TURN_ON_COUNT: turn_on_count, + ATTR_TIME_ZONE: f"{time_zone_name} ({time_zone})", + } + + return result diff --git a/custom_components/mydolphin_plus/strings.json b/custom_components/mydolphin_plus/strings.json index 84b0e83..11b21c2 100644 --- a/custom_components/mydolphin_plus/strings.json +++ b/custom_components/mydolphin_plus/strings.json @@ -130,10 +130,10 @@ "state": { "on": "On", "off": "Off", - "holddelay": "Idle (Delay)", - "holdweekly": "Idle (Weekly)", + "hold_delay": "Idle (Delay)", + "hold_weekly": "Idle (Weekly)", "programming": "Programming", - "notconnected": "Disconnected", + "not_connected": "Disconnected", "cleaning": "Cleaning", "init": "Checking Environment" } diff --git a/custom_components/mydolphin_plus/vacuum.py b/custom_components/mydolphin_plus/vacuum.py index 8e51451..5dd7444 100644 --- a/custom_components/mydolphin_plus/vacuum.py +++ b/custom_components/mydolphin_plus/vacuum.py @@ -12,15 +12,11 @@ from .common.base_entity import MyDolphinPlusBaseEntity, async_setup_entities from .common.consts import ( ACTION_ENTITY_LOCATE, - ACTION_ENTITY_PAUSE, ACTION_ENTITY_RETURN_TO_BASE, ACTION_ENTITY_SEND_COMMAND, ACTION_ENTITY_SET_FAN_SPEED, ACTION_ENTITY_START, ACTION_ENTITY_STOP, - ACTION_ENTITY_TOGGLE, - ACTION_ENTITY_TURN_OFF, - ACTION_ENTITY_TURN_ON, ATTR_ATTRIBUTES, SIGNAL_DEVICE_NEW, ) @@ -83,18 +79,6 @@ async def async_start(self) -> None: async def async_stop(self, **kwargs: Any) -> None: await self.async_execute_device_action(ACTION_ENTITY_STOP, self.state) - async def async_pause(self) -> None: - await self.async_execute_device_action(ACTION_ENTITY_PAUSE, self.state) - - async def async_turn_on(self, **kwargs: Any) -> None: - await self.async_execute_device_action(ACTION_ENTITY_TURN_ON, self.state) - - async def async_turn_off(self, **kwargs: Any) -> None: - await self.async_execute_device_action(ACTION_ENTITY_TURN_OFF, self.state) - - async def async_toggle(self, **kwargs: Any) -> None: - await self.async_execute_device_action(ACTION_ENTITY_TOGGLE, self.state) - async def async_send_command( self, command: str, diff --git a/requirements.txt b/requirements.txt index 9e21776..ae6621d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ pre-commit -homeassistant~=2024.2.0 -aiohttp~=3.9.3 -cryptography~=42.0.4 -voluptuous~=0.13.1 +homeassistant +aiohttp +cryptography +voluptuous python-slugify -awsiotsdk~=1.21.0 -awscrt~=0.20.2 +awsiotsdk +awscrt -awsiot~=0.1.3 +awsiot diff --git a/tests/indicators_test.py b/tests/indicators_test.py index c468a56..9ac9ca9 100644 --- a/tests/indicators_test.py +++ b/tests/indicators_test.py @@ -1,263 +1,100 @@ """Test file for indicators.""" +from copy import copy + from custom_components.mydolphin_plus.common.consts import ( - PWS_STATE_CLEANING, - PWS_STATE_ERROR, - PWS_STATE_HOLD_DELAY, - PWS_STATE_HOLD_WEEKLY, - PWS_STATE_OFF, - PWS_STATE_ON, - PWS_STATE_PROGRAMMING, - ROBOT_STATE_FAULT, - ROBOT_STATE_FINISHED, - ROBOT_STATE_INIT, - ROBOT_STATE_NOT_CONNECTED, - ROBOT_STATE_PROGRAMMING, - ROBOT_STATE_SCANNING, + DATA_SECTION_SYSTEM_STATE, + DATA_SYSTEM_STATE_IS_BUSY, + DATA_SYSTEM_STATE_PWS_STATE, + DATA_SYSTEM_STATE_ROBOT_STATE, + DATA_SYSTEM_STATE_ROBOT_TYPE, + DATA_SYSTEM_STATE_TIME_ZONE, + DATA_SYSTEM_STATE_TIME_ZONE_NAME, + DATA_SYSTEM_STATE_TURN_ON_COUNT, + DEFAULT_TIME_ZONE_NAME, ) - -IS_ROBOT_BUSY = "isBusy" - - -class IndicatorType: - """Indicator types.""" - - ROBOT_STATE_NOT_CONNECTED = "ROBOT_STATE_NOT_CONNECTED" - ROBOT_STATE_PROGRAMMING = "ROBOT_STATE_PROGRAMMING" - GENERAL_ERROR_FALLBACK_ROBOT_OFF = "GENERAL_ERROR_FALLBACK_ROBOT_OFF" - GENERAL_ERROR_FALLBACK_ROBOT_ON = "GENERAL_ERROR_FALLBACK_ROBOT_ON" - ROBOT_IS_BUSY = "ROBOT_IS_BUSY" - PWS_NOT_CONNECTED_TO_CLOUD = "PWS_NOT_CONNECTED_TO_CLOUD" - - -changeWifiToBleProcess = False -CONNECTED_TO_WIFI = "wifi" -available_states = [] - - -def indicator( - pws_state: str, - robot_state: str, - _pws_cloud_state: str, - _is_busy: bool, - _connectivity_type: str, -): - """Assumption: indicator to present failed to connect.""" - result = IndicatorType.ROBOT_STATE_NOT_CONNECTED - if pws_state == "on" and robot_state == ROBOT_STATE_NOT_CONNECTED: - result = None - - print(result) - - -def indicator2( - pws_state: str, - robot_state: str, - _pws_cloud_state: str, - _is_busy: bool, - _connectivity_type: str, -): - """Assumption: Is in programming (setup?) state.""" - result = None - if pws_state == ROBOT_STATE_PROGRAMMING and robot_state == ROBOT_STATE_PROGRAMMING: - result = IndicatorType.ROBOT_STATE_PROGRAMMING - - print(result) - - -def indicator3( - pws_state: str, - robot_state: str, - _pws_cloud_state: str, - _is_busy: bool, - _connectivity_type: str, -): - """Assumption: Off.""" - result = None - if pws_state in [ - "off", - PWS_STATE_HOLD_DELAY, - PWS_STATE_HOLD_WEEKLY, - ] and robot_state not in [ - ROBOT_STATE_FINISHED, - "fault", - "ROBOT_STATE_NOT_CONNECTED", - ]: - result = IndicatorType.GENERAL_ERROR_FALLBACK_ROBOT_OFF - - print(result) - - -def indicator4( - pws_state: str, - robot_state: str, - _pws_cloud_state: str, - _is_busy: bool, - _connectivity_type: str, -): - """Assumption: On.""" - result = None - if pws_state in ["on"] and robot_state not in [ - ROBOT_STATE_INIT, - ROBOT_STATE_SCANNING, - ROBOT_STATE_NOT_CONNECTED, - ]: - result = IndicatorType.GENERAL_ERROR_FALLBACK_ROBOT_ON - - if pws_state in [ROBOT_STATE_PROGRAMMING] and robot_state not in [ - ROBOT_STATE_PROGRAMMING - ]: - result = IndicatorType.GENERAL_ERROR_FALLBACK_ROBOT_ON - - print(result) - - -def indicator5( - _pws_state: str, - _robot_state: str, - _pws_cloud_state: str, - is_busy: bool, - connectivity_type: str, -): - """Assumption: Busy.""" - result = None - if ( - is_busy - and connectivity_type in [CONNECTED_TO_WIFI] - and not changeWifiToBleProcess - ): - result = IndicatorType.ROBOT_IS_BUSY - - print(result) - - -def indicator6( - _pws_state: str, - _robot_state: str, - pws_cloud_state: str, - _is_busy: bool, - connectivity_type: str, -): - """Assumption: Not connected to cloud.""" - result = None - if pws_cloud_state in [ROBOT_STATE_NOT_CONNECTED] and connectivity_type in [ - CONNECTED_TO_WIFI - ]: - result = IndicatorType.PWS_NOT_CONNECTED_TO_CLOUD - - print(result) - - -def run(pws_state: str, robot_state: str): - """Simulate calculation.""" - calculated_status = PWS_STATE_OFF - - pws_on = pws_state in [ - PWS_STATE_ON, - PWS_STATE_HOLD_DELAY, - PWS_STATE_HOLD_WEEKLY, - PWS_STATE_PROGRAMMING, - ] - pws_error = pws_state in [ROBOT_STATE_NOT_CONNECTED] - pws_cleaning = pws_state in [PWS_STATE_ON] - pws_programming = pws_state == PWS_STATE_PROGRAMMING - - robot_error = robot_state in [ROBOT_STATE_FAULT, ROBOT_STATE_NOT_CONNECTED] - robot_cleaning = robot_state not in [ - ROBOT_STATE_INIT, - ROBOT_STATE_SCANNING, - ROBOT_STATE_NOT_CONNECTED, - ] - robot_programming = robot_state == PWS_STATE_PROGRAMMING - - if pws_error or robot_error: - calculated_status = PWS_STATE_ERROR - - elif pws_programming and robot_programming: - calculated_status = PWS_STATE_PROGRAMMING - - elif pws_on: - if (pws_cleaning and robot_cleaning) or ( - pws_programming and not robot_programming - ): - calculated_status = PWS_STATE_CLEANING - - else: - calculated_status = PWS_STATE_ON - - capabilities = [] - - if calculated_status in [PWS_STATE_OFF, PWS_STATE_ERROR]: - capabilities.append("Turn On") - - if calculated_status in [PWS_STATE_ON, PWS_STATE_CLEANING, PWS_STATE_PROGRAMMING]: - capabilities.append("Turn Off") - - if calculated_status in [PWS_STATE_ON]: - capabilities.append("Start") - - if calculated_status in [PWS_STATE_CLEANING]: - capabilities.append("Stop") - - actions = ", ".join(capabilities) - - print( - f"| {calculated_status.capitalize().ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {pws_state.ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {robot_state.ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {actions.ljust(16, ' ')} |" - ) - - if calculated_status.capitalize() not in available_states: - available_states.append(calculated_status.capitalize()) +from custom_components.mydolphin_plus.common.power_supply_state import PowerSupplyState +from custom_components.mydolphin_plus.common.robot_state import RobotState +from custom_components.mydolphin_plus.models.system_details import SystemDetails + +DEVICE_DATA = { + DATA_SECTION_SYSTEM_STATE: { + DATA_SYSTEM_STATE_PWS_STATE: PowerSupplyState.OFF, + DATA_SYSTEM_STATE_ROBOT_STATE: RobotState.NOT_CONNECTED, + DATA_SYSTEM_STATE_ROBOT_TYPE: None, + DATA_SYSTEM_STATE_IS_BUSY: False, + DATA_SYSTEM_STATE_TURN_ON_COUNT: 0, + DATA_SYSTEM_STATE_TIME_ZONE: 0, + DATA_SYSTEM_STATE_TIME_ZONE_NAME: DEFAULT_TIME_ZONE_NAME + } +} + +ASSERTS = [ + "Power Supply: on, Robot: fault, Result: error", + "Power Supply: on, Robot: notConnected, Result: idle", + "Power Supply: on, Robot: programming, Result: cleaning", + "Power Supply: on, Robot: init, Result: init", + "Power Supply: on, Robot: scanning, Result: cleaning", + "Power Supply: on, Robot: finished, Result: idle", + "Power Supply: off, Robot: fault, Result: error", + "Power Supply: off, Robot: notConnected, Result: off", + "Power Supply: off, Robot: programming, Result: off", + "Power Supply: off, Robot: init, Result: off", + "Power Supply: off, Robot: scanning, Result: off", + "Power Supply: off, Robot: finished, Result: off", + "Power Supply: holdDelay, Robot: fault, Result: error", + "Power Supply: holdDelay, Robot: notConnected, Result: idle", + "Power Supply: holdDelay, Robot: programming, Result: idle", + "Power Supply: holdDelay, Robot: init, Result: idle", + "Power Supply: holdDelay, Robot: scanning, Result: idle", + "Power Supply: holdDelay, Robot: finished, Result: idle", + "Power Supply: holdWeekly, Robot: fault, Result: error", + "Power Supply: holdWeekly, Robot: notConnected, Result: idle", + "Power Supply: holdWeekly, Robot: programming, Result: idle", + "Power Supply: holdWeekly, Robot: init, Result: idle", + "Power Supply: holdWeekly, Robot: scanning, Result: idle", + "Power Supply: holdWeekly, Robot: finished, Result: idle", + "Power Supply: programming, Robot: fault, Result: error", + "Power Supply: programming, Robot: notConnected, Result: cleaning", + "Power Supply: programming, Robot: programming, Result: programming", + "Power Supply: programming, Robot: init, Result: cleaning", + "Power Supply: programming, Robot: scanning, Result: cleaning", + "Power Supply: programming, Robot: finished, Result: idle", + "Power Supply: error, Robot: fault, Result: error", + "Power Supply: error, Robot: notConnected, Result: error", + "Power Supply: error, Robot: programming, Result: error", + "Power Supply: error, Robot: init, Result: error", + "Power Supply: error, Robot: scanning, Result: error", + "Power Supply: error, Robot: finished, Result: error" +] + +system_details = SystemDetails() +device_data = copy(DEVICE_DATA) print( - f"| {'State'.ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {'PWS'.ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {'Robot'.ljust(len(PWS_STATE_PROGRAMMING) + 1, ' ')} " - f"| {'Actions'.ljust(16, ' ')} |" + f"| Power Supply State " + f"| Robot State " + f"| Calculated State |" ) print( - f"| {''.ljust(len(PWS_STATE_PROGRAMMING) + 1, '-')} " - f"| {''.ljust(len(PWS_STATE_PROGRAMMING) + 1, '-')} " - f"| {''.ljust(len(PWS_STATE_PROGRAMMING) + 1, '-')} " - f"| {''.ljust(16, '-')} |" + f"| ------------------ " + f"| ----------- " + f"| ---------------- |" ) -run(PWS_STATE_OFF, ROBOT_STATE_NOT_CONNECTED) -run(PWS_STATE_OFF, ROBOT_STATE_FAULT) -run(PWS_STATE_OFF, PWS_STATE_PROGRAMMING) -run(PWS_STATE_OFF, ROBOT_STATE_FINISHED) -run(PWS_STATE_OFF, ROBOT_STATE_INIT) -run(PWS_STATE_OFF, ROBOT_STATE_SCANNING) - -run(PWS_STATE_ON, ROBOT_STATE_NOT_CONNECTED) -run(PWS_STATE_ON, ROBOT_STATE_FAULT) -run(PWS_STATE_ON, PWS_STATE_PROGRAMMING) -run(PWS_STATE_ON, ROBOT_STATE_FINISHED) -run(PWS_STATE_ON, ROBOT_STATE_INIT) -run(PWS_STATE_ON, ROBOT_STATE_SCANNING) +for power_supply_state in list(PowerSupplyState): + device_data[DATA_SECTION_SYSTEM_STATE][DATA_SYSTEM_STATE_PWS_STATE] = PowerSupplyState(power_supply_state) -run(PWS_STATE_HOLD_DELAY, ROBOT_STATE_NOT_CONNECTED) -run(PWS_STATE_HOLD_DELAY, ROBOT_STATE_FAULT) -run(PWS_STATE_HOLD_DELAY, PWS_STATE_PROGRAMMING) -run(PWS_STATE_HOLD_DELAY, ROBOT_STATE_FINISHED) -run(PWS_STATE_HOLD_DELAY, ROBOT_STATE_INIT) -run(PWS_STATE_HOLD_DELAY, ROBOT_STATE_SCANNING) + for robot_state in list(RobotState): + device_data[DATA_SECTION_SYSTEM_STATE][DATA_SYSTEM_STATE_ROBOT_STATE] = RobotState(robot_state) -run(PWS_STATE_HOLD_WEEKLY, ROBOT_STATE_NOT_CONNECTED) -run(PWS_STATE_HOLD_WEEKLY, ROBOT_STATE_FAULT) -run(PWS_STATE_HOLD_WEEKLY, PWS_STATE_PROGRAMMING) -run(PWS_STATE_HOLD_WEEKLY, ROBOT_STATE_FINISHED) -run(PWS_STATE_HOLD_WEEKLY, ROBOT_STATE_INIT) -run(PWS_STATE_HOLD_WEEKLY, ROBOT_STATE_SCANNING) + system_details.update(device_data) -run(PWS_STATE_PROGRAMMING, ROBOT_STATE_NOT_CONNECTED) -run(PWS_STATE_PROGRAMMING, ROBOT_STATE_FAULT) -run(PWS_STATE_PROGRAMMING, PWS_STATE_PROGRAMMING) -run(PWS_STATE_PROGRAMMING, ROBOT_STATE_FINISHED) -run(PWS_STATE_PROGRAMMING, ROBOT_STATE_INIT) -run(PWS_STATE_PROGRAMMING, ROBOT_STATE_SCANNING) + result = ( + f"| {power_supply_state} " + f"| {robot_state} " + f"| {system_details.calculated_state} |" + ) -print(f"Available states: {', '.join(available_states)}") + print(result) From 10abf1ffa0a1fbb40e0f8d2361fe7a9f24802a48 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 5 Jul 2024 14:24:27 +0300 Subject: [PATCH 06/12] fix typo --- CHANGELOG.md | 4 ++-- custom_components/mydolphin_plus/common/calculated_state.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e48115..fd69a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ | ------------------ | --------------------------------------------------- | ---------------- | | error | \* | error | | \* | fault | error | -| holdDelay | notConnected, programming, init. scanning, finished | hold_delay | -| holdWeekly | notConnected, programming, init. scanning, finished | hold_weekly | +| holdDelay | notConnected, programming, init. scanning, finished | holddelay | +| holdWeekly | notConnected, programming, init. scanning, finished | holdweekly | | on | init | init | | on | programming, scanning | cleaning | | programming | notConnected, init, scanning | cleaning | diff --git a/custom_components/mydolphin_plus/common/calculated_state.py b/custom_components/mydolphin_plus/common/calculated_state.py index e263919..af57c82 100644 --- a/custom_components/mydolphin_plus/common/calculated_state.py +++ b/custom_components/mydolphin_plus/common/calculated_state.py @@ -7,8 +7,8 @@ class CalculatedState(StrEnum): ERROR = "error" CLEANING = "cleaning" INIT = "init" - HOLD_DELAY = "hold_delay" - HOLD_WEEKLY = "hold_weekly" + HOLD_DELAY = "holddelay" + HOLD_WEEKLY = "holdweekly" @staticmethod def is_on_state(value) -> bool: From b21984fa67a51968b4a7971c48d3e6936596b902 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 5 Jul 2024 14:32:20 +0300 Subject: [PATCH 07/12] refactor new client initialization process to non-blocking call --- CHANGELOG.md | 1 + .../mydolphin_plus/managers/aws_client.py | 63 ++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd69a69..e8856e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v1.0.16 +- Refactor new client initialization process to non-blocking call - Improved log messages of status changes - Removed vacuum actions - Turn on - not supported diff --git a/custom_components/mydolphin_plus/managers/aws_client.py b/custom_components/mydolphin_plus/managers/aws_client.py index c3d4088..cf606a1 100644 --- a/custom_components/mydolphin_plus/managers/aws_client.py +++ b/custom_components/mydolphin_plus/managers/aws_client.py @@ -199,36 +199,10 @@ async def initialize(self): self._topic_data = TopicData(motor_unit_serial) - credentials_provider = auth.AwsCredentialsProvider.new_static( - aws_key, aws_secret, aws_token - ) - ca_content = await self._get_certificate() - client = mqtt_connection_builder.websockets_with_default_aws_signing( - endpoint=AWS_IOT_URL, - port=AWS_IOT_PORT, - region=AWS_REGION, - ca_bytes=ca_content, - credentials_provider=credentials_provider, - client_id=self._awsiot_id, - clean_session=False, - keep_alive_secs=30, - on_connection_success=self._connection_callbacks.get( - ConnectionCallbacks.SUCCESS - ), - on_connection_failure=self._connection_callbacks.get( - ConnectionCallbacks.FAILURE - ), - on_connection_closed=self._connection_callbacks.get( - ConnectionCallbacks.CLOSED - ), - on_connection_interrupted=self._connection_callbacks.get( - ConnectionCallbacks.INTERRUPTED - ), - on_connection_resumed=self._connection_callbacks.get( - ConnectionCallbacks.RESUMED - ), + client = await self._hass.async_add_executor_job( + self._get_client, aws_key, aws_secret, aws_token, ca_content ) def _on_connect_future_completed(future): @@ -248,6 +222,39 @@ def _on_connect_future_completed(future): self._set_status(ConnectivityStatus.Failed, message) + def _get_client(self, aws_key, aws_secret, aws_token, ca_content): + credentials_provider = auth.AwsCredentialsProvider.new_static( + aws_key, aws_secret, aws_token + ) + + client = mqtt_connection_builder.websockets_with_default_aws_signing( + endpoint=AWS_IOT_URL, + port=AWS_IOT_PORT, + region=AWS_REGION, + ca_bytes=ca_content, + credentials_provider=credentials_provider, + client_id=self._awsiot_id, + clean_session=False, + keep_alive_secs=30, + on_connection_success=self._connection_callbacks.get( + ConnectionCallbacks.SUCCESS + ), + on_connection_failure=self._connection_callbacks.get( + ConnectionCallbacks.FAILURE + ), + on_connection_closed=self._connection_callbacks.get( + ConnectionCallbacks.CLOSED + ), + on_connection_interrupted=self._connection_callbacks.get( + ConnectionCallbacks.INTERRUPTED + ), + on_connection_resumed=self._connection_callbacks.get( + ConnectionCallbacks.RESUMED + ), + ) + + return client + def _subscribe(self): _LOGGER.debug(f"Subscribing topics: {self._topic_data.subscribe}") From ea0c9a714d52397a02a5794bdd52f5aea1ae7f8a Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 5 Jul 2024 15:52:57 +0300 Subject: [PATCH 08/12] Add reset account password flow from setup or configure --- CHANGELOG.md | 2 + .../common/connectivity_status.py | 40 +- .../mydolphin_plus/common/consts.py | 5 + .../mydolphin_plus/diagnostics.py | 2 +- .../mydolphin_plus/managers/aws_client.py | 20 +- .../mydolphin_plus/managers/coordinator.py | 16 +- .../mydolphin_plus/managers/flow_manager.py | 66 +-- .../mydolphin_plus/managers/rest_api.py | 114 ++++- .../mydolphin_plus/models/config_data.py | 5 +- custom_components/mydolphin_plus/strings.json | 20 +- .../mydolphin_plus/translations/en.json | 296 ++++++------- .../mydolphin_plus/translations/it.json | 392 +++++++++--------- requirements.txt | 7 + tests/api_test.py | 10 +- tests/translation_compare.py | 203 ++++++--- 15 files changed, 694 insertions(+), 504 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8856e3..b2484ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v1.0.16 +- Add email validation on setup and every startup +- Add reset account password flow from setup or configure (when integration already connected but OTP is required) - Refactor new client initialization process to non-blocking call - Improved log messages of status changes - Removed vacuum actions diff --git a/custom_components/mydolphin_plus/common/connectivity_status.py b/custom_components/mydolphin_plus/common/connectivity_status.py index c21f479..856b9bb 100644 --- a/custom_components/mydolphin_plus/common/connectivity_status.py +++ b/custom_components/mydolphin_plus/common/connectivity_status.py @@ -3,26 +3,27 @@ class ConnectivityStatus(StrEnum): - NotConnected = "Not connected" - Connecting = "Establishing connection to API" - Connected = "Connected to the API" - TemporaryConnected = "Connected with temporary API key" - Failed = "Failed to access API" - InvalidCredentials = "Invalid credentials" - MissingAPIKey = "Permanent API Key was not found" - Disconnected = "Disconnected by the system" - NotFound = "API Not found" + NOT_CONNECTED = "Not connected" + CONNECTING = "Establishing connection to API" + CONNECTED = "Connected to the API" + TEMPORARY_CONNECTED = "Connected with temporary API key" + FAILED = "Failed to access API" + INVALID_CREDENTIALS = "Invalid credentials" + MISSING_API_KEY = "Permanent API Key was not found" + DISCONNECTED = "Disconnected by the system" + API_NOT_FOUND = "API Not found" + INVALID_ACCOUNT = "Invalid account" @staticmethod def get_log_level(status: StrEnum) -> int: if status in [ - ConnectivityStatus.Connected, - ConnectivityStatus.Connecting, - ConnectivityStatus.Disconnected, - ConnectivityStatus.TemporaryConnected, + ConnectivityStatus.CONNECTED, + ConnectivityStatus.CONNECTING, + ConnectivityStatus.DISCONNECTED, + ConnectivityStatus.TEMPORARY_CONNECTED, ]: return logging.INFO - elif status in [ConnectivityStatus.NotConnected]: + elif status in [ConnectivityStatus.NOT_CONNECTED]: return logging.WARNING else: return logging.ERROR @@ -30,10 +31,11 @@ def get_log_level(status: StrEnum) -> int: @staticmethod def get_ha_error(status: str) -> str | None: errors = { - str(ConnectivityStatus.InvalidCredentials): "invalid_admin_credentials", - str(ConnectivityStatus.MissingAPIKey): "missing_permanent_api_key", - str(ConnectivityStatus.Failed): "invalid_server_details", - str(ConnectivityStatus.NotFound): "invalid_server_details", + str(ConnectivityStatus.INVALID_CREDENTIALS): "invalid_credentials", + str(ConnectivityStatus.INVALID_ACCOUNT): "invalid_account", + str(ConnectivityStatus.MISSING_API_KEY): "missing_permanent_api_key", + str(ConnectivityStatus.FAILED): "invalid_server_details", + str(ConnectivityStatus.API_NOT_FOUND): "invalid_server_details", } error_id = errors.get(status) @@ -41,4 +43,4 @@ def get_ha_error(status: str) -> str | None: return error_id -IGNORED_TRANSITIONS = {ConnectivityStatus.Disconnected: [ConnectivityStatus.Failed]} +IGNORED_TRANSITIONS = {ConnectivityStatus.DISCONNECTED: [ConnectivityStatus.FAILED]} diff --git a/custom_components/mydolphin_plus/common/consts.py b/custom_components/mydolphin_plus/common/consts.py index b3e477d..525ed7f 100644 --- a/custom_components/mydolphin_plus/common/consts.py +++ b/custom_components/mydolphin_plus/common/consts.py @@ -12,6 +12,7 @@ INVALID_TOKEN_SECTION = "https://github.com/sh00t2kill/dolphin-robot#invalid-token" CONF_TITLE = "title" +CONF_RESET_PASSWORD = "reset_password" SIGNAL_DEVICE_NEW = f"{DOMAIN}_NEW_DEVICE_SIGNAL" SIGNAL_AWS_CLIENT_STATUS = f"{DOMAIN}_AWS_CLIENT_STATUS_SIGNAL" @@ -122,6 +123,8 @@ BASE_API = "https://mbapp18.maytronics.com/api" LOGIN_URL = f"{BASE_API}/users/Login/" +EMAIL_VALIDATION_URL = f"{BASE_API}/users/isEmailExists/" +FORGOT_PASSWORD_URL = f"{BASE_API}/users/ForgotPassword/" TOKEN_URL = f"{BASE_API}/IOT/getToken_DecryptSN/" ROBOT_DETAILS_URL = f"{BASE_API}/serialnumbers/getrobotdetailsbymusn/" ROBOT_DETAILS_BY_SN_URL = f"{BASE_API}/serialnumbers/getrobotdetailsbyrobotsn/" @@ -140,6 +143,8 @@ API_RESPONSE_STATUS_SUCCESS = "1" API_RESPONSE_UNIT_SERIAL_NUMBER = "eSERNUM" +API_RESPONSE_IS_EMAIL_EXISTS = "isEmailExists" + API_RESPONSE_DATA_TOKEN = "Token" API_RESPONSE_DATA_ACCESS_KEY_ID = "AccessKeyId" API_RESPONSE_DATA_SECRET_ACCESS_KEY = "SecretAccessKey" diff --git a/custom_components/mydolphin_plus/diagnostics.py b/custom_components/mydolphin_plus/diagnostics.py index 2032dd2..fd66e16 100644 --- a/custom_components/mydolphin_plus/diagnostics.py +++ b/custom_components/mydolphin_plus/diagnostics.py @@ -1,4 +1,4 @@ -"""Diagnostics support for Tuya.""" +"""Diagnostics support for MyDolphin.""" from __future__ import annotations import logging diff --git a/custom_components/mydolphin_plus/managers/aws_client.py b/custom_components/mydolphin_plus/managers/aws_client.py index cf606a1..f2a13b8 100644 --- a/custom_components/mydolphin_plus/managers/aws_client.py +++ b/custom_components/mydolphin_plus/managers/aws_client.py @@ -179,12 +179,12 @@ def _on_terminate_future_completed(future): self._awsiot_client = None - self._set_status(ConnectivityStatus.Disconnected, "terminate requested") + self._set_status(ConnectivityStatus.DISCONNECTED, "terminate requested") async def initialize(self): try: self._set_status( - ConnectivityStatus.Connecting, "Initializing MyDolphin AWS IOT WS" + ConnectivityStatus.CONNECTING, "Initializing MyDolphin AWS IOT WS" ) aws_token = self._api_data.get(API_RESPONSE_DATA_TOKEN) @@ -220,7 +220,7 @@ def _on_connect_future_completed(future): message = f"Failed to initialize MyDolphin Plus WS, error: {ex}, line: {line_number}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def _get_client(self, aws_key, aws_secret, aws_token, ca_content): credentials_provider = auth.AwsCredentialsProvider.new_static( @@ -299,7 +299,7 @@ async def update_api_data(self, api_data: dict): self._robot_family = RobotFamily.from_string(robot_family_str) async def update(self): - if self._status == ConnectivityStatus.Connected: + if self._status == ConnectivityStatus.CONNECTED: _LOGGER.debug("Connected. Refresh details") await self._refresh_details() @@ -330,7 +330,7 @@ def _on_connection_success(self, connection, callback_data): self._subscribe() - self._set_status(ConnectivityStatus.Connected) + self._set_status(ConnectivityStatus.CONNECTED) def _on_connection_failure(self, connection, callback_data): if connection is not None and isinstance( @@ -338,7 +338,7 @@ def _on_connection_failure(self, connection, callback_data): ): message = f"AWS IoT connection failed, Error: {callback_data.error}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def _on_connection_closed(self, connection, callback_data): if connection is not None and isinstance( @@ -346,7 +346,7 @@ def _on_connection_closed(self, connection, callback_data): ): message = "AWS IoT connection was closed" - self._set_status(ConnectivityStatus.Disconnected, message) + self._set_status(ConnectivityStatus.DISCONNECTED, message) def _on_connection_interrupted(self, connection, error, **_kwargs): message = f"AWS IoT connection interrupted, Error: {error}" @@ -355,7 +355,7 @@ def _on_connection_interrupted(self, connection, error, **_kwargs): _LOGGER.error(message) else: - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def _on_connection_resumed( self, connection, return_code, session_present, **_kwargs @@ -372,7 +372,7 @@ def _on_connection_resumed( resubscribe_future.add_done_callback(self._on_resubscribe_complete) - self._set_status(ConnectivityStatus.Connected) + self._set_status(ConnectivityStatus.CONNECTED) @staticmethod def _on_resubscribe_complete(resubscribe_future): @@ -481,7 +481,7 @@ def _publish(self, topic: str, data: dict | None): payload = json.dumps(data) - if self._status == ConnectivityStatus.Connected: + if self._status == ConnectivityStatus.CONNECTED: try: if self._awsiot_client is not None: publish_future, packet_id = self._awsiot_client.publish( diff --git a/custom_components/mydolphin_plus/managers/coordinator.py b/custom_components/mydolphin_plus/managers/coordinator.py index 73d14e3..44e7077 100644 --- a/custom_components/mydolphin_plus/managers/coordinator.py +++ b/custom_components/mydolphin_plus/managers/coordinator.py @@ -280,7 +280,7 @@ async def _on_api_status_changed(self, entry_id: str, status: ConnectivityStatus if entry_id != self._config_manager.entry_id: return - if status == ConnectivityStatus.Connected: + if status == ConnectivityStatus.CONNECTED: await self._set_aws_token_encrypted_key() await self._api.update() @@ -290,8 +290,8 @@ async def _on_api_status_changed(self, entry_id: str, status: ConnectivityStatus await self._aws_client.initialize() elif status in [ - ConnectivityStatus.Failed, - ConnectivityStatus.InvalidCredentials, + ConnectivityStatus.FAILED, + ConnectivityStatus.INVALID_CREDENTIALS, ]: await self._handle_connection_failure() @@ -301,10 +301,10 @@ async def _on_aws_client_status_changed( if entry_id != self._config_manager.entry_id: return - if status == ConnectivityStatus.Connected: + if status == ConnectivityStatus.CONNECTED: await self._aws_client.update() - if status in [ConnectivityStatus.Failed, ConnectivityStatus.NotConnected]: + if status in [ConnectivityStatus.FAILED, ConnectivityStatus.NOT_CONNECTED]: await self._handle_connection_failure() async def _handle_connection_failure(self): @@ -321,9 +321,9 @@ async def _async_update_data(self): so entities can quickly look up their parameters. """ try: - api_connected = self._api.status == ConnectivityStatus.Connected + api_connected = self._api.status == ConnectivityStatus.CONNECTED aws_client_connected = ( - self._aws_client.status == ConnectivityStatus.Connected + self._aws_client.status == ConnectivityStatus.CONNECTED ) is_ready = api_connected and aws_client_connected @@ -676,7 +676,7 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: return result def _get_aws_broker_data(self, _entity_description) -> dict | None: - is_on = self._aws_client.status == ConnectivityStatus.Connected + is_on = self._aws_client.status == ConnectivityStatus.CONNECTED result = { ATTR_IS_ON: is_on, diff --git a/custom_components/mydolphin_plus/managers/flow_manager.py b/custom_components/mydolphin_plus/managers/flow_manager.py index b580055..cd43624 100644 --- a/custom_components/mydolphin_plus/managers/flow_manager.py +++ b/custom_components/mydolphin_plus/managers/flow_manager.py @@ -12,7 +12,7 @@ from homeassistant.data_entry_flow import FlowHandler from ..common.connectivity_status import ConnectivityStatus -from ..common.consts import CONF_TITLE, DEFAULT_NAME +from ..common.consts import CONF_RESET_PASSWORD, CONF_TITLE, DEFAULT_NAME from ..models.config_data import DATA_KEYS, ConfigData from ..models.exceptions import LoginError from .config_manager import ConfigManager @@ -62,36 +62,48 @@ async def async_step(self, user_input: dict | None = None): ) else: + error_key: str | None = None + try: await self._config_manager.initialize(user_input) api = RestAPI(self._hass, self._config_manager) - await api.validate() + reset_password_flow = user_input.get(CONF_RESET_PASSWORD, False) - if api.status == ConnectivityStatus.TemporaryConnected: - _LOGGER.debug("User inputs are valid") + if reset_password_flow: + await api.reset_password() + user_input = {} - if self._entry is None: - data = copy(user_input) + else: + await api.validate() - else: - data = await self.remap_entry_data(user_input) + if api.status == ConnectivityStatus.TEMPORARY_CONNECTED: + _LOGGER.debug("User inputs are valid") - await PasswordManager.encrypt(self._hass, data) + if self._entry is None: + data = copy(user_input) - title = data.get(CONF_TITLE, DEFAULT_NAME) + else: + data = await self.remap_entry_data(user_input) - if CONF_TITLE in data: - data.pop(CONF_TITLE) + await PasswordManager.encrypt(self._hass, data) - return self._flow_handler.async_create_entry(title=title, data=data) + title = data.get(CONF_TITLE, DEFAULT_NAME) - else: - error_key = ConnectivityStatus.get_ha_error(api.status) + new_user_data = { + key: data[key] for key in data if key in DATA_KEYS + } + + return self._flow_handler.async_create_entry( + title=title, data=new_user_data + ) + + else: + error_key = ConnectivityStatus.get_ha_error(api.status) except LoginError: - error_key = "invalid_admin_credentials" + error_key = "invalid_credentials" except InvalidToken: error_key = "corrupted_encryption_key" @@ -108,23 +120,23 @@ async def async_step(self, user_input: dict | None = None): ) async def remap_entry_data(self, options: dict[str, Any]) -> dict[str, Any]: - config_options = {} - config_data = {} - entry = self._entry entry_data = entry.data - title = DEFAULT_NAME + title = options.get(CONF_TITLE, DEFAULT_NAME) - for key in options: - if key in DATA_KEYS: - config_data[key] = options.get(key, entry_data.get(key)) + config_data = { + key: options.get(key, entry_data.get(key)) + for key in options + if key in DATA_KEYS + } - elif key == CONF_TITLE: - title = options.get(key, DEFAULT_NAME) + options_excluded_keys = [CONF_TITLE, CONF_RESET_PASSWORD] + options_excluded_keys.extend(DATA_KEYS) - else: - config_options[key] = options.get(key) + config_options = { + key: options[key] for key in options if key not in options_excluded_keys + } await PasswordManager.encrypt(self._hass, config_data) diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index 95abf6c..c84a7d5 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -27,6 +27,7 @@ API_REQUEST_SERIAL_PASSWORD, API_RESPONSE_ALERT, API_RESPONSE_DATA, + API_RESPONSE_IS_EMAIL_EXISTS, API_RESPONSE_STATUS, API_RESPONSE_STATUS_FAILURE, API_RESPONSE_STATUS_SUCCESS, @@ -35,6 +36,8 @@ BLOCK_SIZE, DATA_ROBOT_DETAILS, DEFAULT_NAME, + EMAIL_VALIDATION_URL, + FORGOT_PASSWORD_URL, LOGIN_HEADERS, LOGIN_URL, MAXIMUM_ATTEMPTS_GET_AWS_TOKEN, @@ -137,7 +140,7 @@ async def terminate(self): if self._session is not None: await self._session.close() - self._set_status(ConnectivityStatus.Disconnected, "terminate requested") + self._set_status(ConnectivityStatus.DISCONNECTED, "terminate requested") async def _initialize_session(self): try: @@ -155,7 +158,7 @@ async def _initialize_session(self): f"Failed to initialize session, Error: {str(ex)}, Line: {line_number}" ) - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) async def validate(self): await self._initialize_session() @@ -216,7 +219,7 @@ async def _async_get(self, url, headers: dict): return result async def update(self): - if self._status == ConnectivityStatus.Connected: + if self._status == ConnectivityStatus.CONNECTED: _LOGGER.debug("Connected. Refresh details") await self._load_details() @@ -232,18 +235,89 @@ async def update(self): async def _login(self): await self._service_login() - if self._status == ConnectivityStatus.TemporaryConnected: + if self._status == ConnectivityStatus.TEMPORARY_CONNECTED: await self._generate_token() - elif self._status == ConnectivityStatus.InvalidCredentials: + elif self._status in [ + ConnectivityStatus.INVALID_CREDENTIALS, + ConnectivityStatus.INVALID_ACCOUNT, + ]: return else: - self._set_status(ConnectivityStatus.Failed, "general failure of login") + self._set_status(ConnectivityStatus.FAILED, "general failure of login") + + async def reset_password(self): + _LOGGER.debug("Starting reset password process") + + username = self.config_data.username + + request_data = f"{API_REQUEST_SERIAL_EMAIL}={username}" + + payload = await self._async_post( + FORGOT_PASSWORD_URL, LOGIN_HEADERS, request_data + ) + + if payload is None: + _LOGGER.error("Empty response of reset password") + + else: + data = payload.get(API_RESPONSE_DATA) + + if data is None: + _LOGGER.error("Empty response payload of reset password") + + else: + _LOGGER.info(f"Reset password response: {data}") + + async def _email_validation(self) -> bool: + _LOGGER.debug("Validating account email") + + if self._status != ConnectivityStatus.INVALID_ACCOUNT: + username = self.config_data.username + + request_data = f"{API_REQUEST_SERIAL_EMAIL}={username}" + + payload = await self._async_post( + EMAIL_VALIDATION_URL, LOGIN_HEADERS, request_data + ) + + if payload is None: + self._set_status( + ConnectivityStatus.INVALID_ACCOUNT, + "empty response of email validation", + ) + + else: + data = payload.get(API_RESPONSE_DATA) + + if data is None: + self._set_status( + ConnectivityStatus.INVALID_ACCOUNT, + "empty response payload of email validation", + ) + + else: + status = data.get(API_RESPONSE_IS_EMAIL_EXISTS, False) + + if not status: + self._set_status( + ConnectivityStatus.INVALID_ACCOUNT, + f"account [{username}] is not valid", + ) + + is_valid_account = self._status != ConnectivityStatus.INVALID_ACCOUNT + + return is_valid_account async def _service_login(self): try: - self._set_status(ConnectivityStatus.Connecting) + is_valid_account = await self._email_validation() + + if not is_valid_account: + return + + self._set_status(ConnectivityStatus.CONNECTING) username = self.config_data.username password = self.config_data.password @@ -253,14 +327,14 @@ async def _service_login(self): payload = await self._async_post(LOGIN_URL, LOGIN_HEADERS, request_data) if payload is None: - self._set_status(ConnectivityStatus.Failed, "empty response of login") + self._set_status(ConnectivityStatus.FAILED, "empty response of login") else: data = payload.get(API_RESPONSE_DATA) if data is None: self._set_status( - ConnectivityStatus.InvalidCredentials, + ConnectivityStatus.INVALID_CREDENTIALS, "empty response payload of login", ) @@ -281,7 +355,7 @@ async def _service_login(self): message = f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) async def _set_actual_motor_unit_serial(self): try: @@ -310,7 +384,7 @@ async def _set_actual_motor_unit_serial(self): API_RESPONSE_UNIT_SERIAL_NUMBER ) - self._set_status(ConnectivityStatus.TemporaryConnected, message) + self._set_status(ConnectivityStatus.TEMPORARY_CONNECTED, message) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() @@ -318,7 +392,7 @@ async def _set_actual_motor_unit_serial(self): message = f"Failed to login into {DEFAULT_NAME} service, Error: {str(ex)}, Line: {line_number}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) async def _generate_token(self): try: @@ -347,7 +421,7 @@ async def _generate_token(self): for field in API_TOKEN_FIELDS: self.data[field] = data.get(field) - self._set_status(ConnectivityStatus.Connected) + self._set_status(ConnectivityStatus.CONNECTED) if get_token_attempts > 0: _LOGGER.debug( @@ -362,7 +436,7 @@ async def _generate_token(self): if get_token_attempts + 1 >= MAXIMUM_ATTEMPTS_GET_AWS_TOKEN: message = f"Failed to retrieve AWS token after {get_token_attempts} attempts, Error: {alert}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) get_token_attempts += 1 @@ -372,10 +446,10 @@ async def _generate_token(self): message = f"Failed to retrieve AWS token from service, Error: {str(ex)}, Line: {line_number}" - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) async def _load_details(self): - if self._status != ConnectivityStatus.Connected: + if self._status != ConnectivityStatus.CONNECTED: return try: @@ -495,10 +569,10 @@ def _handle_client_error( ) if crex.status in [404, 405]: - self._set_status(ConnectivityStatus.NotFound, message) + self._set_status(ConnectivityStatus.API_NOT_FOUND, message) else: - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def _handle_server_timeout(self, endpoint: str, method: str): message = ( @@ -507,7 +581,7 @@ def _handle_server_timeout(self, endpoint: str, method: str): f"Method: {method}" ) - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def _handle_general_request_failure( self, endpoint: str, method: str, ex: Exception @@ -523,7 +597,7 @@ def _handle_general_request_failure( f"Line: {line_number}" ) - self._set_status(ConnectivityStatus.Failed, message) + self._set_status(ConnectivityStatus.FAILED, message) def set_local_async_dispatcher_send(self, callback): self._local_async_dispatcher_send = callback diff --git a/custom_components/mydolphin_plus/models/config_data.py b/custom_components/mydolphin_plus/models/config_data.py index 0a28c12..a2d5c7a 100644 --- a/custom_components/mydolphin_plus/models/config_data.py +++ b/custom_components/mydolphin_plus/models/config_data.py @@ -3,7 +3,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from ..common.consts import CONF_TITLE, DEFAULT_NAME +from ..common.consts import CONF_RESET_PASSWORD, CONF_TITLE, DEFAULT_NAME DATA_KEYS = [CONF_USERNAME, CONF_PASSWORD] @@ -55,6 +55,9 @@ def default_schema(user_input: dict | None) -> Schema: ): str, vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME)): str, vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)): str, + vol.Optional( + CONF_RESET_PASSWORD, default=user_input.get(CONF_RESET_PASSWORD, False) + ): bool, } schema = vol.Schema(new_user_input) diff --git a/custom_components/mydolphin_plus/strings.json b/custom_components/mydolphin_plus/strings.json index 11b21c2..1838b2a 100644 --- a/custom_components/mydolphin_plus/strings.json +++ b/custom_components/mydolphin_plus/strings.json @@ -6,12 +6,14 @@ "description": "Set up your MyDolphin Plus details", "data": { "username": "Username", - "password": "Password" + "password": "Password", + "reset_password": "Reset account password (Workaround for OTP)" } } }, "error": { - "invalid_admin_credentials": "Invalid administrator credentials", + "invalid_credentials": "Invalid credentials", + "invalid_account": "Invalid account", "invalid_server_details": "Invalid server details", "already_configured": "Integration already configured with the name", "missing_permanent_api_key": "Missing permanent API key", @@ -25,14 +27,17 @@ "description": "Set up username and password.", "data": { "username": "Username", - "password": "Password" + "password": "Password", + "reset_password": "Reset account password (Workaround for OTP)" } } }, "error": { - "invalid_admin_credentials": "Invalid administrator credentials", + "invalid_credentials": "Invalid credentials", + "invalid_account": "Invalid account", "invalid_server_details": "Invalid server details", - "already_configured": "integration already configured with the name", + "already_configured": "Integration already configured with the name", + "missing_permanent_api_key": "Missing permanent API key", "corrupted_encryption_key": "Encryption key got corrupted, please remove the integration and re-add it" } }, @@ -130,10 +135,9 @@ "state": { "on": "On", "off": "Off", - "hold_delay": "Idle (Delay)", - "hold_weekly": "Idle (Weekly)", + "holddelay": "Idle (Delay)", + "holdweekly": "Idle (Weekly)", "programming": "Programming", - "not_connected": "Disconnected", "cleaning": "Cleaning", "init": "Checking Environment" } diff --git a/custom_components/mydolphin_plus/translations/en.json b/custom_components/mydolphin_plus/translations/en.json index cd8f105..3f9bfd2 100644 --- a/custom_components/mydolphin_plus/translations/en.json +++ b/custom_components/mydolphin_plus/translations/en.json @@ -1,39 +1,23 @@ { "config": { - "step": { - "user": { - "title": "Set up MyDolphin Plus", - "description": "Set up your MyDolphin Plus details", - "data": { - "username": "Username", - "password": "Password" - } - } - }, "error": { - "invalid_admin_credentials": "Invalid administrator credentials", - "invalid_server_details": "Invalid server details", "already_configured": "Integration already configured with the name", - "missing_permanent_api_key": "Missing permanent API key", - "corrupted_encryption_key": "Encryption key got corrupted, please remove the integration and re-add it" - } - }, - "options": { + "corrupted_encryption_key": "Encryption key got corrupted, please remove the integration and re-add it", + "invalid_account": "Invalid account", + "invalid_credentials": "Invalid credentials", + "invalid_server_details": "Invalid server details", + "missing_permanent_api_key": "Missing permanent API key" + }, "step": { - "mydolphin_plus_additional_settings": { - "title": "Options for MyDolphin Plus.", - "description": "Set up username and password.", + "user": { "data": { - "username": "Username", - "password": "Password" - } + "password": "Password", + "reset_password": "Reset account password (Workaround for OTP)", + "username": "Username" + }, + "description": "Set up your MyDolphin Plus details", + "title": "Set up MyDolphin Plus" } - }, - "error": { - "invalid_admin_credentials": "Invalid administrator credentials", - "invalid_server_details": "Invalid server details", - "already_configured": "integration already configured with the name", - "corrupted_encryption_key": "Encryption key got corrupted, please remove the integration and re-add it" } }, "entity": { @@ -51,37 +35,49 @@ } }, "number": { - "led_intensity": { - "name": "LED Intensity" - }, "cycle_time_all": { "name": "Cycle Time Regular" }, - "cycle_time_short": { - "name": "Cycle Time Fast mode" - }, "cycle_time_floor": { "name": "Cycle Time Floor only" }, - "cycle_time_water": { - "name": "Cycle Time Water line" + "cycle_time_pickup": { + "name": "Cycle Time Pickup" + }, + "cycle_time_short": { + "name": "Cycle Time Fast mode" }, "cycle_time_ultra": { "name": "Cycle Time Ultra clean" }, - "cycle_time_pickup": { - "name": "Cycle Time Pickup" + "cycle_time_water": { + "name": "Cycle Time Water line" + }, + "led_intensity": { + "name": "LED Intensity" + } + }, + "select": { + "led_mode": { + "name": "LED Mode", + "state": { + "1": "Blinking", + "2": "Always on", + "3": "Disco" + } } }, "sensor": { - "rssi": { - "name": "RSSI" - }, - "network_name": { - "name": "Network Name" - }, - "robot_type": { - "name": "Robot Type" + "clean_mode": { + "name": "Clean Mode", + "state": { + "all": "Regular", + "floor": "Floor only", + "pickup": "Pickup", + "short": "Fast mode", + "ultra": "Ultra clean", + "water": "Water line" + } }, "cycle_count": { "name": "Cycle Count" @@ -92,76 +88,24 @@ "filter_status": { "name": "Filter Status", "state": { - "unknown": "Unknown", - "empty": "Empty", - "partially_full": "Partially full", - "getting_full": "Getting full", "almost_full": "Almost full", - "full": "Full", - "fault": "Fault", - "not_available": "Not available" - } - }, - "robot_status": { - "name": "Robot Status", - "state": { - "finished": "Finished", + "empty": "Empty", "fault": "Fault", - "notconnected": "Disconnected", - "programming": "Programming", - "init": "Initialized", - "scanning": "Scanning" - } - }, - "power_supply_status": { - "name": "Power Supply Status", - "state": { - "on": "On", - "off": "Off", - "holddelay": "Idle (Delay)", - "holdweekly": "Idle (Weekly)", - "programming": "Programming", - "error": "Error", - "cleaning": "Cleaning" - } - }, - "status": { - "name": "Status", - "state": { - "on": "On", - "off": "Off", - "holddelay": "Idle (Delay)", - "holdweekly": "Idle (Weekly)", - "programming": "Programming", - "notconnected": "Disconnected", - "cleaning": "Cleaning", - "init": "Checking Environment" + "full": "Full", + "getting_full": "Getting full", + "not_available": "Not available", + "partially_full": "Partially full", + "unknown": "Unknown" } }, - "clean_mode": { - "name": "Clean Mode", - "state": { - "all": "Regular", - "short": "Fast mode", - "floor": "Floor only", - "water": "Water line", - "ultra": "Ultra clean", - "pickup": "Pickup" - } + "network_name": { + "name": "Network Name" }, - "robot_error": { - "name": "Robot Error", + "power_supply_error": { + "name": "Power Supply Error", "state": { "0": "Ok", "1": "DC in voltage", - "2": "Out of water", - "3": "Impeller overload", - "4": "Impeller 1 under load", - "5": "Impeller overload", - "6": "Impeller 2 under load", - "7": "Drive overload", - "8": "Drive 1 under load", - "9": "Drive overload", "10": "Drive 2 under load", "11": "Wall/floor sensor", "12": "DC in voltage 23V", @@ -172,38 +116,50 @@ "17": "Impeller 1 Driver failure", "18": "Impeller 2 Driver failure", "19": "Drive 1 Driver failure", + "2": "Out of water", "20": "Drive 2 Driver failure", - "21": "Servo over load", + "21": "Servo over load", "22": "Impeller 1 Motor failure", "23": "Impeller 2 Motor failure", "24": "Drive 1 Motor failure", "25": "Drive 2 Motor failure", - "255": "Ok" + "255": "Ok", + "3": "Impeller overload", + "4": "Impeller 1 under load", + "5": "Impeller overload", + "6": "Impeller 2 under load", + "7": "Drive overload", + "8": "Drive 1 under load", + "9": "Drive overload" }, "state_attributes": { "instructions": { "state": { - "3": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn’t help, contact your dealer", - "5": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn’t help, contact your dealer", - "7": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n3. Plug in the power supply and try to operate again.\n4. If the above doesn’t help, please contact your dealer", - "9": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n 3. Plug in the power supply and try to operate again.\n4. If the above doesn’t help, please contact your dealer" + "3": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn\u00e2\u20ac\u2122t help, contact your dealer", + "5": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn\u00e2\u20ac\u2122t help, contact your dealer", + "7": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n3. Plug in the power supply and try to operate again.\n4. If the above doesn\u00e2\u20ac\u2122t help, please contact your dealer", + "9": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n 3. Plug in the power supply and try to operate again.\n4. If the above doesn\u00e2\u20ac\u2122t help, please contact your dealer" } } } }, - "power_supply_error": { - "name": "Power Supply Error", + "power_supply_status": { + "name": "Power Supply Status", + "state": { + "cleaning": "Cleaning", + "error": "Error", + "holddelay": "Idle (Delay)", + "holdweekly": "Idle (Weekly)", + "off": "Off", + "on": "On", + "programming": "Programming" + } + }, + "robot_error": { + "name": "Robot Error", "state": { "0": "Ok", "1": "DC in voltage", - "2": "Out of water", - "3": "Impeller overload", - "4": "Impeller 1 under load", - "5": "Impeller overload", - "6": "Impeller 2 under load", - "7": "Drive overload", - "8": "Drive 1 under load", - "9": "Drive overload", "10": "Drive 2 under load", "11": "Wall/floor sensor", "12": "DC in voltage 23V", @@ -214,61 +170,109 @@ "17": "Impeller 1 Driver failure", "18": "Impeller 2 Driver failure", "19": "Drive 1 Driver failure", + "2": "Out of water", "20": "Drive 2 Driver failure", "21": "Servo over load", "22": "Impeller 1 Motor failure", "23": "Impeller 2 Motor failure", "24": "Drive 1 Motor failure", "25": "Drive 2 Motor failure", - "255": "Ok" + "255": "Ok", + "3": "Impeller overload", + "4": "Impeller 1 under load", + "5": "Impeller overload", + "6": "Impeller 2 under load", + "7": "Drive overload", + "8": "Drive 1 under load", + "9": "Drive overload" }, "state_attributes": { "instructions": { "state": { - "3": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\\n5. If the above doesn’t help, contact your dealer", - "5": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\\n5. If the above doesn’t help, contact your dealer", - "7": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n3. Plug in the power supply and try to operate again.\n4. If the above doesn’t help, please contact your dealer", - "9": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n 3. Plug in the power supply and try to operate again.\n4. If the above doesn’t help, please contact your dealer" + "3": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn\u00e2\u20ac\u2122t help, contact your dealer", + "5": "Please follow these steps:\n1. Unplug the power supply.\n2. Clean the debris from the impeller opening.\n3. Dismantle the impeller compartment if the debris is inaccessible.\n4. Re-assemble the robot, plug in the power supply, and try to operate again.\n5. If the above doesn\u00e2\u20ac\u2122t help, contact your dealer", + "7": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n3. Plug in the power supply and try to operate again.\n4. If the above doesn\u00e2\u20ac\u2122t help, please contact your dealer", + "9": "Please follow these steps:\n1. Unplug the power supply.\n2. Remove any object or blockage from the driving system.\n 3. Plug in the power supply and try to operate again.\n4. If the above doesn\u00e2\u20ac\u2122t help, please contact your dealer" } } } - } - }, - "select": { - "led_mode": { - "name": "LED Mode", + }, + "robot_status": { + "name": "Robot Status", "state": { - "1": "Blinking", - "2": "Always on", - "3": "Disco" + "fault": "Fault", + "finished": "Finished", + "init": "Initialized", + "notconnected": "Disconnected", + "programming": "Programming", + "scanning": "Scanning" + } + }, + "robot_type": { + "name": "Robot Type" + }, + "rssi": { + "name": "RSSI" + }, + "status": { + "name": "Status", + "state": { + "cleaning": "Cleaning", + "holddelay": "Idle (Delay)", + "holdweekly": "Idle (Weekly)", + "init": "Checking Environment", + "off": "Off", + "on": "On", + "programming": "Programming" } } }, "vacuum": { "vacuum": { "state": { - "on": "On", - "off": "Off", + "cleaning": "Cleaning", "holddelay": "Idle (Delay)", "holdweekly": "Idle (Weekly)", - "programming": "Programming", + "init": "Checking Environment", "notconnected": "Disconnected", - "cleaning": "Cleaning", - "init": "Checking Environment" + "off": "Off", + "on": "On", + "programming": "Programming" }, "state_attributes": { "fan_speed": { "state": { "all": "Regular", - "short": "Fast mode", "floor": "Floor only", - "water": "Water line", + "pickup": "Pickup", + "short": "Fast mode", "ultra": "Ultra clean", - "pickup": "Pickup" + "water": "Water line" } } } } } + }, + "options": { + "error": { + "already_configured": "Integration already configured with the name", + "corrupted_encryption_key": "Encryption key got corrupted, please remove the integration and re-add it", + "invalid_account": "Invalid account", + "invalid_credentials": "Invalid credentials", + "invalid_server_details": "Invalid server details", + "missing_permanent_api_key": "Missing permanent API key" + }, + "step": { + "mydolphin_plus_additional_settings": { + "data": { + "password": "Password", + "reset_password": "Reset account password (Workaround for OTP)", + "username": "Username" + }, + "description": "Set up username and password.", + "title": "Options for MyDolphin Plus." + } + } } } diff --git a/custom_components/mydolphin_plus/translations/it.json b/custom_components/mydolphin_plus/translations/it.json index 1716ace..2cd33ce 100644 --- a/custom_components/mydolphin_plus/translations/it.json +++ b/custom_components/mydolphin_plus/translations/it.json @@ -1,39 +1,23 @@ { "config": { - "step": { - "user": { - "title": "Configurazione MyDolphin Plus", - "description": "Configura i dettagli del tuo MyDolphin Plus", - "data": { - "username": "Username", - "password": "Password" - } - } - }, "error": { - "invalid_admin_credentials": "Credenziali amministratore non valide", - "invalid_server_details": "Dettagli server non validi", - "already_configured": "Un'integrazione con lo stesso nome \u00e8 gi\u00e0 stata configurata", - "missing_permanent_api_key": "Chiave API permanente mancante", - "corrupted_encryption_key": "Chiave di criptazione corrotta, rimuovere l'integrazione ed aggiungerla di nuovo" - } - }, - "options": { + "already_configured": "Integrazione gi\u00e0 configurata con il nome", + "corrupted_encryption_key": "La chiave di crittografia \u00e8 stata danneggiata, rimuovi l'integrazione e lo aggiunge", + "invalid_account": "Account non valido", + "invalid_credentials": "Credenziali non valide", + "invalid_server_details": "Dettagli del server non validi", + "missing_permanent_api_key": "Chiave API permanente mancante" + }, "step": { - "mydolphin_plus_additional_settings": { - "title": "Opzioni di MyDolphin Plus.", - "description": "Impostazione username e password.", + "user": { "data": { - "username": "Username", - "password": "Password" - } + "password": "Parola d'ordine", + "reset_password": "Reimpostare la password dell'account (soluzione alternativa per OTP)", + "username": "Nome utente" + }, + "description": "Imposta i tuoi dettagli MyDolphin Plus", + "title": "Imposta mydolphin plus" } - }, - "error": { - "invalid_admin_credentials": "Credenziali amministratore non valide", - "invalid_server_details": "Dettagli server non validi", - "already_configured": "Un'integrazione con lo stesso nome \u00e8 gi\u00e0 stata configurata", - "corrupted_encryption_key": "Chiave di criptazione corrotta, rimuovere l'integrazione ed aggiungerla di nuovo" } }, "entity": { @@ -42,233 +26,253 @@ "name": "Broker AWS" }, "weekly_scheduler": { - "name": "Programma settimanale" + "name": "Scheduler settimanale" } }, "light": { "led": { - "name": "LED" + "name": "GUIDATA GUIDATO" } }, "number": { - "led_intensity": { - "name": "Intensit\u00e0 LED" - }, "cycle_time_all": { - "name": "Tempo ciclo Normale" - }, - "cycle_time_short": { - "name": "Tempo ciclo Rapido" + "name": "Tempo di ciclo regolare" }, "cycle_time_floor": { - "name": "Tempo ciclo solo pavimento" + "name": "Solo il tempo di ciclo" }, - "cycle_time_water": { - "name": "Tempo ciclo Pelo Acqua" + "cycle_time_pickup": { + "name": "Pickup del tempo di ciclo" + }, + "cycle_time_short": { + "name": "Cycle Time Fast Mode" }, "cycle_time_ultra": { - "name": "Tempo ciclo Ultra pulito" + "name": "Cycle Time Ultra Clean" }, - "cycle_time_pickup": { - "name": "Tempo ciclo Raccolta" + "cycle_time_water": { + "name": "Cycle Time Water Line" + }, + "led_intensity": { + "name": "Intensit\u00e0 a LED" + } + }, + "select": { + "led_mode": { + "name": "Modalit\u00e0 LED", + "state": { + "1": "Palla", + "2": "Sempre acceso", + "3": "Discoteca" + } } }, "sensor": { - "rssi": { - "name": "RSSI" - }, - "network_name": { - "name": "Nome rete" - }, - "robot_type": { - "name": "Modello Robot" + "clean_mode": { + "name": "Modalit\u00e0 pulita", + "state": { + "all": "Regolare", + "floor": "Solo piano", + "pickup": "Raccolta", + "short": "Modalit\u00e0 veloce", + "ultra": "Ultra pulito", + "water": "Linea di galleggiamento" + } }, "cycle_count": { - "name": "Numero di cicli" + "name": "Conteggio dei cicli" }, "cycle_time_left": { - "name": "Tempo rimanente ciclo" + "name": "Tempo di ciclo rimasto" }, "filter_status": { - "name": "Stato filtro", + "name": "Stato del filtro", "state": { - "unknown": "Sconosciuto", - "empty": "Vuoto", + "almost_full": "Pressoch\u00e8 pieno", + "empty": "Vuota Vuoto", + "fault": "Colpa", + "full": "Piena Pieno", + "getting_full": "Diventare pieno", + "not_available": "Non disponibile", "partially_full": "Parzialmente pieno", - "getting_full": "Verso il pieno", - "almost_full": "Quasi pieno", - "full": "Pieno", - "fault": "Problema", - "not_available": "Non disponibile" - } - }, - "robot_status": { - "name": "Stato del Robot", - "state": { - "finished": "Completato", - "fault": "Problema", - "notconnected": "Disconnesso", - "programming": "In programmazione", - "init": "Inizializzazione", - "scanning": "Ricognizione" - } - }, - "power_supply_status": { - "name": "Stato unit\u00e0 principale", - "state": { - "on": "On", - "off": "Off", - "holddelay": "Fermo (in attesa)", - "holdweekly": "Fermo (Settimanale)", - "programming": "In programmazione", - "error": "Errore", - "cleaning": "In pulizia" - } - }, - "status": { - "name": "Stato", - "state": { - "on": "On", - "off": "Off", - "holddelay": "Fermo (in attesa)", - "holdweekly": "Fermo (Settimanale)", - "programming": "In programmazione", - "notconnected": "Disconnesso", - "cleaning": "In pulizia", - "init": "*Checking Environment*" + "unknown": "Sconosciuta Sconosciuto" } }, - "clean_mode": { - "name": "Modo pulizia", - "state": { - "all": "Fondo e pareti", - "short": "Veloce", - "floor": "Solo fondo", - "water": "Pelo acqua", - "ultra": "Ultra pulito", - "pickup": "*Pickup*" - } + "network_name": { + "name": "Nome della rete" }, - "robot_error": { - "name": "Errore Robot", + "power_supply_error": { + "name": "Errore di alimentazione", "state": { "0": "OK", - "1": "Voltaggio alimentazione DC", - "2": "Fuori dall'acqua", - "3": "Sovraccarico rotore", - "4": "Rotore 1 sotto carico", - "5": "Sovraccarico rotore", - "6": "Rotore 2 sotto carico", - "7": "Sovraccarico trazione", - "8": "Trazione 1 sotto carico", - "9": "Sovraccarico trazione", - "10": "Trazione 2 sotto carico", - "11": "Sensore fondo/parete", - "12": "voltaggio ingresso 23V DC", - "13": "Sensore fondo/parete 2", + "1": "DC in tensione", + "10": "Drive 2 sotto carico", + "11": "Sensore muro/pavimento", + "12": "DC in tensione 23V", + "13": "Sensore del pavimento murale 2", "14": "Robot bloccato", - "15": "Surriscaldamento Alimentazione", - "16": "Sovraccarico Alimentazione", - "17": "Guasto motore Rotore 1", - "18": "Guasto motore Rotore 2", - "19": "Guasto motore Trazione 1", - "20": "Guasto motore Trazione 2", - "21": "Sovraccarico Servo motore", - "22": "Guasto motore Rotore 1", - "23": "Guasto motore Rotore 2", - "24": "Guasto motore Trazione 1", - "25": "Guasto motore Trazione 2", - "255": "OK" + "15": "Supertutto dell'alimentazione", + "16": "Sovraccarico di alimentazione", + "17": "Specatore 1 guasto del driver", + "18": "Giorganizzazione della girante 2", + "19": "Drive 1 Errore del driver", + "2": "Fuori dall'acqua", + "20": "Drive 2 Errore del driver", + "21": "Servo over cary", + "22": "Gioriglione motorio della girante 1", + "23": "Gioriglione motorio della girante 2", + "24": "Drive 1 errore del motore", + "25": "Drive 2 errori del motore", + "255": "OK", + "3": "Sovraccarico della girante", + "4": "Girante 1 sotto carico", + "5": "Sovraccarico della girante", + "6": "Girante 2 sotto carico", + "7": "Guidare il sovraccarico", + "8": "Drive 1 sotto carico", + "9": "Guidare il sovraccarico" }, "state_attributes": { "instructions": { "state": { - "3": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Pulire i detriti dall'ingresso del rotore.\n3. Smontare il compartimento del rotore se non è possibile raggiungere i detriti.\n4. Riassemblare il robot, ricollegare l'alimentazione e riprovare.\n5. Se non fosse sufficiente, contattate il vostro installatore", - "5": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Pulire i detriti dall'ingresso del rotore.\n3. Smontare il compartimento del rotore se non è possibile raggiungere i detriti.\n4. Riassemblare il robot, ricollegare l'alimentazione e riprovare.\n5. Se non fosse sufficiente, contattate il vostro installatore", - "7": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Rimuovere oggetti o altri impedimenti dal sistema di trazione.\n3. Ricollegare l'alimentazione e riprovare.\n4. Se non fosse sufficiente, contattate il vostro installatore", - "9": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Rimuovere oggetti o altri impedimenti dal sistema di trazione.\n3. Ricollegare l'alimentazione e riprovare.\n4. Se non fosse sufficiente, contattate il vostro installatore" + "3": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Pulire i detriti dall'apertura della girante.\n 3. Smontare il compartimento della girante se i detriti sono inaccessibili.\n 4. Riassemblare il robot, collegare l'alimentazione e provare a funzionare di nuovo.\n 5. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "5": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Pulire i detriti dall'apertura della girante.\n 3. Smontare il compartimento della girante se i detriti sono inaccessibili.\n 4. Riassemblare il robot, collegare l'alimentazione e provare a funzionare di nuovo.\n 5. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "7": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Rimuovere qualsiasi oggetto o blocco dal sistema di guida.\n 3. Collegare l'alimentazione e provare a funzionare di nuovo.\n 4. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "9": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Rimuovere qualsiasi oggetto o blocco dal sistema di guida.\n 3. Collegare l'alimentazione e provare a funzionare di nuovo.\n 4. Se quanto sopra non aiuta, contatta il tuo rivenditore" } } } }, - "power_supply_error": { - "name": "Errore Alimentazione", + "power_supply_status": { + "name": "Stato di alimentazione", + "state": { + "cleaning": "Pulizia", + "error": "Errore", + "holddelay": "Inattivo (ritardo)", + "holdweekly": "Idle (settimanale)", + "off": "Spento", + "on": "SU", + "programming": "Programmazione" + } + }, + "robot_error": { + "name": "Errore robot", "state": { "0": "OK", - "1": "Voltaggio ingresso DC", - "2": "Fuori dall'acqua", - "3": "Sovraccarico rotore", - "4": "Rotore 1 sotto carico", - "5": "Sovraccarico rotore", - "6": "Rotore 2 sotto carico", - "7": "Sovraccarico trazione", - "8": "Trazione 1 sotto carico", - "9": "Sovraccarico trazione", - "10": "Trazione 2 sotto carico", - "11": "Sensore fondo/parete", - "12": "voltaggio ingresso 23V DC", - "13": "Sensore fondo/parete 2", + "1": "DC in tensione", + "10": "Drive 2 sotto carico", + "11": "Sensore muro/pavimento", + "12": "DC in tensione 23V", + "13": "Sensore del pavimento murale 2", "14": "Robot bloccato", - "15": "Surriscaldamento Alimentazione", - "16": "Sovraccarico Alimentazione", - "17": "Guasto motore Rotore 1", - "18": "Guasto motore Rotore 2", - "19": "Guasto motore Trazione 1", - "20": "Guasto motore Trazione 2", - "21": "Sovraccarico Servo motore", - "22": "Guasto motore Rotore 1", - "23": "Guasto motore Rotore 2", - "24": "Guasto motore Trazione 1", - "25": "Guasto motore Trazione 2", - "255": "OK" + "15": "Supertutto dell'alimentazione", + "16": "Sovraccarico di alimentazione", + "17": "Specatore 1 guasto del driver", + "18": "Giorganizzazione della girante 2", + "19": "Drive 1 Errore del driver", + "2": "Fuori dall'acqua", + "20": "Drive 2 Errore del driver", + "21": "Servo over cary", + "22": "Gioriglione motorio della girante 1", + "23": "Gioriglione motorio della girante 2", + "24": "Drive 1 errore del motore", + "25": "Drive 2 errori del motore", + "255": "OK", + "3": "Sovraccarico della girante", + "4": "Girante 1 sotto carico", + "5": "Sovraccarico della girante", + "6": "Girante 2 sotto carico", + "7": "Guidare il sovraccarico", + "8": "Drive 1 sotto carico", + "9": "Guidare il sovraccarico" }, "state_attributes": { "instructions": { "state": { - "3": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Pulire i detriti dall'ingresso del rotore.\n3. Smontare il compartimento del rotore se non è possibile raggiungere i detriti.\n4. Riassemblare il robot, ricollegare l'alimentazione e riprovare.\n5. Se non fosse sufficiente, contattate il vostro installatore", - "5": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Pulire i detriti dall'ingresso del rotore.\n3. Smontare il compartimento del rotore se non è possibile raggiungere i detriti.\n4. Riassemblare il robot, ricollegare l'alimentazione e riprovare.\n5. Se non fosse sufficiente, contattate il vostro installatore", - "7": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Rimuovere oggetti o altri impedimenti dal sistema di trazione.\n3. Ricollegare l'alimentazione e riprovare.\n4. Se non fosse sufficiente, contattate il vostro installatore", - "9": "Eseguire le seguenti istruzioni:\n1. Staccare l'alimentazione.\n2. Rimuovere oggetti o altri impedimenti dal sistema di trazione.\n3. Ricollegare l'alimentazione e riprovare.\n4. Se non fosse sufficiente, contattate il vostro installatore" + "3": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Pulire i detriti dall'apertura della girante.\n 3. Smontare il compartimento della girante se i detriti sono inaccessibili.\n 4. Riassemblare il robot, collegare l'alimentazione e provare a funzionare di nuovo.\n 5. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "5": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Pulire i detriti dall'apertura della girante.\n 3. Smontare il compartimento della girante se i detriti sono inaccessibili.\n 4. Riassemblare il robot, collegare l'alimentazione e provare a funzionare di nuovo.\n 5. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "7": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Rimuovere qualsiasi oggetto o blocco dal sistema di guida.\n 3. Collegare l'alimentazione e provare a funzionare di nuovo.\n 4. Se quanto sopra non aiuta, contatta il tuo rivenditore", + "9": "Segui questi passaggi:\n 1. Scollegare l'alimentazione.\n 2. Rimuovere qualsiasi oggetto o blocco dal sistema di guida.\n 3. Collegare l'alimentazione e provare a funzionare di nuovo.\n 4. Se quanto sopra non aiuta, contatta il tuo rivenditore" } } } - } - }, - "select": { - "led_mode": { - "name": "Modo LED", + }, + "robot_status": { + "name": "Stato robot", "state": { - "1": "Lampeggio", - "2": "Sempre acceso", - "3": "Disco" + "fault": "Colpa", + "finished": "Finita Finito", + "init": "Inizializzato", + "notconnected": "Disconnessa Disconnesso", + "programming": "Programmazione", + "scanning": "Scansione" + } + }, + "robot_type": { + "name": "Tipo di robot" + }, + "rssi": { + "name": "RSSI" + }, + "status": { + "name": "Stato", + "state": { + "cleaning": "Pulizia", + "holddelay": "Inattivo (ritardo)", + "holdweekly": "Idle (settimanale)", + "init": "Verifica dell'ambiente", + "off": "Spento", + "on": "SU", + "programming": "Programmazione" } } }, "vacuum": { "vacuum": { + "state": { + "cleaning": "Pulizia", + "holddelay": "Inattivo (ritardo)", + "holdweekly": "Idle (settimanale)", + "init": "Verifica dell'ambiente", + "notconnected": "Disconnessa Disconnesso", + "off": "Spento", + "on": "SU", + "programming": "Programmazione" + }, "state_attributes": { "fan_speed": { "state": { - "all": "Fondo e pareti", - "short": "Veloce", - "floor": "Solo fondo", - "water": "Pelo acqua", + "all": "Regolare", + "floor": "Solo piano", + "pickup": "Raccolta", + "short": "Modalit\u00e0 veloce", "ultra": "Ultra pulito", - "pickup": "Recupero" + "water": "Linea di galleggiamento" } } - }, - "state": { - "on": "Acceso", - "off": "Spento", - "holddelay": "In attesa (Ritardato)", - "holdweekly": "In attesa (Settimanale)", - "programming": "In programmazione", - "notconnected": "Disconnesso", - "cleaning": "In Pulizia", - "init": "Inizializzazione" } } } + }, + "options": { + "error": { + "already_configured": "Integrazione gi\u00e0 configurata con il nome", + "corrupted_encryption_key": "La chiave di crittografia \u00e8 stata danneggiata, rimuovi l'integrazione e lo aggiunge", + "invalid_account": "Account non valido", + "invalid_credentials": "Credenziali non valide", + "invalid_server_details": "Dettagli del server non validi", + "missing_permanent_api_key": "Chiave API permanente mancante" + }, + "step": { + "mydolphin_plus_additional_settings": { + "data": { + "password": "Parola d'ordine", + "reset_password": "Reimpostare la password dell'account (soluzione alternativa per OTP)", + "username": "Nome utente" + }, + "description": "Imposta nome utente e password.", + "title": "Opzioni per MyDolphin Plus." + } + } } } diff --git a/requirements.txt b/requirements.txt index ae6621d..94df1a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,10 @@ awsiotsdk awscrt awsiot +flatten_json + +python-lokalise-api~=1.6 +python-dotenv~=0.20 +googletrans==4.0.0rc1 +translators~= 5.4 +deep-translator~=1.9 diff --git a/tests/api_test.py b/tests/api_test.py index e6a1baa..061b6e1 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -78,7 +78,7 @@ async def initialize(self): while True: - if self._aws_client.status == ConnectivityStatus.Connected: + if self._aws_client.status == ConnectivityStatus.CONNECTED: data = json.dumps(self._aws_client.data) _LOGGER.info(data) @@ -91,14 +91,14 @@ async def terminate(self): await self._aws_client.terminate() async def _on_api_status_changed(self, status: ConnectivityStatus): - if status == ConnectivityStatus.Connected: + if status == ConnectivityStatus.CONNECTED: await self._api.update() await self._aws_client.update_api_data(self._api.data) await self._aws_client.initialize() - elif status == ConnectivityStatus.Failed: + elif status == ConnectivityStatus.FAILED: await self._aws_client.terminate() await sleep(API_RECONNECT_INTERVAL.total_seconds()) @@ -106,14 +106,14 @@ async def _on_api_status_changed(self, status: ConnectivityStatus): await self._api.initialize(self._config_manager.aws_token_encrypted_key) async def _on_aws_status_changed(self, status: ConnectivityStatus): - if status == ConnectivityStatus.Failed: + if status == ConnectivityStatus.FAILED: await self._api.initialize(None) await sleep(WS_RECONNECT_INTERVAL.total_seconds()) await self._api.initialize(self._config_manager.aws_token_encrypted_key) - if status == ConnectivityStatus.Connected: + if status == ConnectivityStatus.CONNECTED: await self._aws_client.update() diff --git a/tests/translation_compare.py b/tests/translation_compare.py index 6a53a75..8c313cd 100644 --- a/tests/translation_compare.py +++ b/tests/translation_compare.py @@ -1,96 +1,169 @@ +import asyncio import json +import logging import os +from pathlib import Path +import sys -# Requires running cmd: "pip install flatten_json" -from flatten_json import flatten +from flatten_json import flatten, unflatten +import translators as ts -from custom_components.mydolphin_plus import DOMAIN +DOMAIN = "mydolphin_plus" -SUPPORTED_LANGUAGES = ["en", "it"] +DEBUG = str(os.environ.get("DEBUG", False)).lower() == str(True).lower() +log_level = logging.DEBUG -def _get_json_file(path): - full_path = os.path.join( - os.path.dirname(__file__), - f"..\\custom_components\\{DOMAIN}\\{path}" - ) +root = logging.getLogger() +root.setLevel(log_level) - with open(full_path, encoding="utf-8") as json_file: - json_str = json_file.read() - content = json.loads(json_str) +logging.getLogger("urllib3").setLevel(logging.WARNING) - return content +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(log_level) +formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") +stream_handler.setFormatter(formatter) +root.addHandler(stream_handler) +_LOGGER = logging.getLogger(__name__) -def _set_json_file(path, content): - full_path = os.path.join( - os.path.dirname(__file__), - f"..\\custom_components\\{DOMAIN}\\{path}" - ) +SOURCE_LANGUAGE = "en" +DESTINATION_LANGUAGES = { + "en": "en", + "it": "it" +} - data = json.dumps(content, indent=4) +TRANSLATION_PROVIDER = "google" +FLAT_SEPARATOR = "." - with open(full_path, "w", encoding="utf-8") as json_file: - json_file.write(data) +class TranslationGenerator: + def __init__(self): + self._source_translations = self._get_source_translations() -def _get_gaps(lang_name: str): - strings_json = _get_json_file("strings.json") - lang_json = _get_json_file(f"translations\\{lang_name}.json") + self._destinations = DESTINATION_LANGUAGES - strings_keys = flatten(strings_json, separator=".") - lang_keys = flatten(lang_json, separator=".") + async def initialize(self): + values = flatten(self._source_translations, FLAT_SEPARATOR) + value_keys = list(values.keys()) + last_key = value_keys[len(value_keys) - 1] - added_gaps = [key for key in strings_keys if key not in lang_keys] - removed_gaps = [key for key in lang_keys if key not in strings_keys] + _LOGGER.info( + f"Process will translate {len(values)} sentences " + f"to {len(list(self._destinations.keys()))} languages" + ) - if len(added_gaps) > 0: - for key in strings_keys: - if key not in lang_keys: - key_parts = key.split(".") - data_to_handle = lang_json - data_source = strings_json + for lang in self._destinations: + original_values = values.copy() + translated_data = self._get_translations(lang) + translated_values = flatten(translated_data, FLAT_SEPARATOR) - for i in range(0, len(key_parts)): - key_item = key_parts[i] + provider_lang = self._destinations[lang] + lang_cache = {} - if i == len(key_parts) - 1: - data_to_handle[key_item] = f"*{data_source[key_item]}*" + lang_title = provider_lang.upper() - else: - if key_item not in data_to_handle: - data_to_handle[key_item] = {} - data_to_handle = data_to_handle[key_item] - data_source = data_source[key_item] + for key in original_values: + english_value = original_values[key] - _set_json_file(f"translations\\{lang_name}.json", lang_json) + if not isinstance(english_value, str): + continue - gaps = { - "added": added_gaps, - "removed": removed_gaps - } + if key in translated_values: + translated_value = translated_values[key] - return gaps + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"translation of '{english_value}' already exists - '{translated_value}'" + ) + continue -def _compare(): - for lang_name in SUPPORTED_LANGUAGES: - gaps = _get_gaps(lang_name) - added_keys = gaps.get("added") - removed_keys = gaps.get("removed") + if english_value in lang_cache: + translated_value = lang_cache[english_value] - if len(added_keys) + len(removed_keys) > 0: - print(f"Translations for '{lang_name}' is not up to date.") + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"translation of '{english_value}' available in cache - {translated_value}" + ) - if len(added_keys) > 0: - print(f"New keys:") - for key in added_keys: - print(f" - {key}") + elif lang == SOURCE_LANGUAGE: + translated_value = english_value - if len(removed_keys) > 0: - print(f"Removed keys:") - for key in removed_keys: - print(f" - {key}") + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"source and destination languages are the same - {translated_value}" + ) + else: + original_english_value = english_value -_compare() + sleep_seconds = 10 if last_key == key else 0 + + translated_value = ts.translate_text( + english_value, + translator=TRANSLATION_PROVIDER, + to_language=provider_lang, + sleep_seconds=sleep_seconds + ) + + lang_cache[english_value] = translated_value + + _LOGGER.debug(f"Translating '{original_english_value}' to {lang_title}: {translated_value}") + + translated_values[key] = translated_value + + translated_data = unflatten(translated_values, FLAT_SEPARATOR) + + self._save_translations(lang, translated_data) + + @staticmethod + def _get_source_translations() -> dict: + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", DOMAIN, "strings.json") + + with open(file_path) as f: + data = json.load(f) + + return data + + @staticmethod + def _get_translations(lang: str): + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", DOMAIN, "translations", f"{lang}.json") + + if os.path.exists(file_path): + with open(file_path) as file: + data = json.load(file) + else: + data = {} + + return data + + @staticmethod + def _save_translations(lang: str, data: dict): + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", DOMAIN, "translations", f"{lang}.json") + + with open(file_path, "w+", encoding="utf-8") as file: + file.write(json.dumps(data, indent=4)) + + _LOGGER.info(f"Translation for {lang.upper()} stored") + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + + instance = TranslationGenerator() + + try: + loop.run_until_complete(instance.initialize()) + + except KeyboardInterrupt: + _LOGGER.info("Aborted") + + except Exception as rex: + _LOGGER.error(f"Error: {rex}") From 146a4caeb5c51cceb7a0509770aaff577d322b1f Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sat, 6 Jul 2024 09:52:21 +0300 Subject: [PATCH 09/12] initialize session for reset password flow --- custom_components/mydolphin_plus/managers/rest_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index c84a7d5..ca83c1b 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -250,6 +250,9 @@ async def _login(self): async def reset_password(self): _LOGGER.debug("Starting reset password process") + if self._session is None: + await self._initialize_session() + username = self.config_data.username request_data = f"{API_REQUEST_SERIAL_EMAIL}={username}" From 0e3c1f9aead67e32063457796045fb023eb6ada3 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sat, 6 Jul 2024 10:01:08 +0300 Subject: [PATCH 10/12] add is email exists before reset password --- .../mydolphin_plus/managers/rest_api.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/custom_components/mydolphin_plus/managers/rest_api.py b/custom_components/mydolphin_plus/managers/rest_api.py index ca83c1b..d5eadeb 100644 --- a/custom_components/mydolphin_plus/managers/rest_api.py +++ b/custom_components/mydolphin_plus/managers/rest_api.py @@ -253,25 +253,28 @@ async def reset_password(self): if self._session is None: await self._initialize_session() - username = self.config_data.username + is_valid_email = await self._email_validation() - request_data = f"{API_REQUEST_SERIAL_EMAIL}={username}" - - payload = await self._async_post( - FORGOT_PASSWORD_URL, LOGIN_HEADERS, request_data - ) + if is_valid_email: + username = self.config_data.username - if payload is None: - _LOGGER.error("Empty response of reset password") + request_data = f"{API_REQUEST_SERIAL_EMAIL}={username}" - else: - data = payload.get(API_RESPONSE_DATA) + payload = await self._async_post( + FORGOT_PASSWORD_URL, LOGIN_HEADERS, request_data + ) - if data is None: - _LOGGER.error("Empty response payload of reset password") + if payload is None: + _LOGGER.error("Empty response of reset password") else: - _LOGGER.info(f"Reset password response: {data}") + data = payload.get(API_RESPONSE_DATA) + + if data is None: + _LOGGER.error("Empty response payload of reset password") + + else: + _LOGGER.info(f"Reset password response: {data}") async def _email_validation(self) -> bool: _LOGGER.debug("Validating account email") From 393b656c1c328ecde59196300647e0577119fb13 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Sun, 7 Jul 2024 08:30:01 +0300 Subject: [PATCH 11/12] better handling clock icons --- .../mydolphin_plus/common/consts.py | 1 + .../mydolphin_plus/managers/coordinator.py | 52 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/custom_components/mydolphin_plus/common/consts.py b/custom_components/mydolphin_plus/common/consts.py index 525ed7f..95a6642 100644 --- a/custom_components/mydolphin_plus/common/consts.py +++ b/custom_components/mydolphin_plus/common/consts.py @@ -236,6 +236,7 @@ JOYSTICK_LEFT, ] +CLOCK_HOURS_NONE = "mdi:timer-sand-paused" CLOCK_HOURS_ICON = "mdi:clock-time-" CLOCK_HOURS_TEXT = [ "twelve", diff --git a/custom_components/mydolphin_plus/managers/coordinator.py b/custom_components/mydolphin_plus/managers/coordinator.py index 44e7077..9307ba9 100644 --- a/custom_components/mydolphin_plus/managers/coordinator.py +++ b/custom_components/mydolphin_plus/managers/coordinator.py @@ -37,6 +37,7 @@ ATTR_START_TIME, ATTR_STATUS, CLOCK_HOURS_ICON, + CLOCK_HOURS_NONE, CLOCK_HOURS_TEXT, CONF_DIRECTION, CONFIGURATION_URL, @@ -610,21 +611,28 @@ def _get_cycle_time_data(self, _entity_description) -> dict | None: cycle_time_minutes = cleaning_mode.get( DATA_CYCLE_INFO_CLEANING_MODE_DURATION, 0 ) - cycle_time = timedelta(minutes=cycle_time_minutes) - cycle_time_hours = int(cycle_time / timedelta(hours=1)) - cycle_start_time_ts = cycle_info.get( - DATA_CYCLE_INFO_CLEANING_MODE_START_TIME, 0 - ) - cycle_start_time = self._get_date_time_from_timestamp(cycle_start_time_ts) + attributes = {} + + if cycle_time_minutes == 0: + cycle_time_hours = None + + else: + cycle_time = timedelta(minutes=cycle_time_minutes) + cycle_time_hours = int(cycle_time / timedelta(hours=1)) + + cycle_start_time_ts = cycle_info.get( + DATA_CYCLE_INFO_CLEANING_MODE_START_TIME, 0 + ) + cycle_start_time = self._get_date_time_from_timestamp(cycle_start_time_ts) + + attributes[ATTR_START_TIME] = cycle_start_time icon = self._get_hour_icon(cycle_time_hours) result = { ATTR_STATE: cycle_time_minutes, - ATTR_ATTRIBUTES: { - ATTR_START_TIME: cycle_start_time, - }, + ATTR_ATTRIBUTES: attributes, ATTR_ICON: icon, } @@ -659,8 +667,12 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: ): seconds_left = expected_cycle_end_time_ts - now_ts - state = timedelta(seconds=seconds_left).total_seconds() - state_hours = int((expected_cycle_end_time - now) / timedelta(hours=1)) + if seconds_left > 0: + state = timedelta(seconds=seconds_left).total_seconds() + state_hours = int((expected_cycle_end_time - now) / timedelta(hours=1)) + + else: + state_hours = None icon = self._get_hour_icon(state_hours) @@ -855,14 +867,18 @@ def _get_date_time_from_timestamp(timestamp): return result @staticmethod - def _get_hour_icon(current_hour: int) -> str: - if current_hour > 11: - current_hour = current_hour - 12 + def _get_hour_icon(current_hour: int | None) -> str: + if current_hour is None: + icon = CLOCK_HOURS_NONE + + else: + if current_hour > 11: + current_hour = current_hour - 12 - if current_hour >= len(CLOCK_HOURS_TEXT): - current_hour = 0 + if current_hour >= len(CLOCK_HOURS_TEXT): + current_hour = 0 - hour_text = CLOCK_HOURS_TEXT[current_hour] - icon = "".join([CLOCK_HOURS_ICON, hour_text]) + hour_text = CLOCK_HOURS_TEXT[current_hour] + icon = "".join([CLOCK_HOURS_ICON, hour_text]) return icon From 0c14f02e79743df5c347a0a24cba3b4f3ff7c814 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Mon, 8 Jul 2024 09:16:53 +0300 Subject: [PATCH 12/12] fix missing state --- custom_components/mydolphin_plus/managers/coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/mydolphin_plus/managers/coordinator.py b/custom_components/mydolphin_plus/managers/coordinator.py index 9307ba9..9e4c257 100644 --- a/custom_components/mydolphin_plus/managers/coordinator.py +++ b/custom_components/mydolphin_plus/managers/coordinator.py @@ -660,7 +660,10 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: expected_cycle_end_time_ts ) + state = 0 seconds_left = 0 + state_hours = None + if ( calculated_state == CalculatedState.CLEANING and expected_cycle_end_time_ts > now_ts @@ -671,9 +674,6 @@ def _get_cycle_time_left_data(self, _entity_description) -> dict | None: state = timedelta(seconds=seconds_left).total_seconds() state_hours = int((expected_cycle_end_time - now) / timedelta(hours=1)) - else: - state_hours = None - icon = self._get_hour_icon(state_hours) result = {