Skip to content

Commit

Permalink
Merge pull request #213 from sh00t2kill/email-validation
Browse files Browse the repository at this point in the history
Add reset account password flow from setup or configure
  • Loading branch information
elad-bar authored Jul 5, 2024
2 parents e0a1a12 + ea0c9a7 commit ec7812f
Show file tree
Hide file tree
Showing 15 changed files with 694 additions and 504 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 21 additions & 19 deletions custom_components/mydolphin_plus/common/connectivity_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,44 @@


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

@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)

return error_id


IGNORED_TRANSITIONS = {ConnectivityStatus.Disconnected: [ConnectivityStatus.Failed]}
IGNORED_TRANSITIONS = {ConnectivityStatus.DISCONNECTED: [ConnectivityStatus.FAILED]}
5 changes: 5 additions & 0 deletions custom_components/mydolphin_plus/common/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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/"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mydolphin_plus/diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Diagnostics support for Tuya."""
"""Diagnostics support for MyDolphin."""
from __future__ import annotations

import logging
Expand Down
20 changes: 10 additions & 10 deletions custom_components/mydolphin_plus/managers/aws_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -330,23 +330,23 @@ 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(
callback_data, mqtt.OnConnectionFailureData
):
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(
callback_data, mqtt.OnConnectionClosedData
):
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}"
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 8 additions & 8 deletions custom_components/mydolphin_plus/managers/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()

Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
66 changes: 39 additions & 27 deletions custom_components/mydolphin_plus/managers/flow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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)

Expand Down
Loading

0 comments on commit ec7812f

Please sign in to comment.