From 2113189b9fd3174afeffd5ca5fc79fc0776d8882 Mon Sep 17 00:00:00 2001 From: Richard Holmes Date: Sat, 11 Nov 2023 19:44:06 +0000 Subject: [PATCH 1/2] Update Template --- .devcontainer-template.json | 19 +++++++++++-------- .github/dependabot.yml | 4 ++-- requirements.txt | 8 ++++---- requirements_dev.txt | 8 ++++---- scripts/setup | 2 +- setup.cfg | 3 ++- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.devcontainer-template.json b/.devcontainer-template.json index e7919df..ce92979 100644 --- a/.devcontainer-template.json +++ b/.devcontainer-template.json @@ -18,25 +18,28 @@ "vscode": { "extensions": [ "ms-python.python", - "github.vscode-pull-request-github", - "ryanluker.vscode-coverage-gutters", "ms-python.vscode-pylance", - "mikestead.dotenv", + "ms-python.black-formatter", + "charliermarsh.ruff", "github.vscode-pull-request-github", - "charliermarsh.ruff" + "mikestead.dotenv", + "ryanluker.vscode-coverage-gutters" ], "settings": { "files.eol": "\n", + "files.associations": { + "*.yaml": "home-assistant" + }, "editor.tabSize": 4, "python.pythonPath": "/usr/bin/python3", "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, + "extensions.ignoreRecommendations": true, "files.trimTrailingWhitespace": true } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04f2d40..39b7e94 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,12 +4,12 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" - package-ecosystem: "pip" directory: "/" schedule: - interval: "weekly" + interval: "monthly" ignore: # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json - dependency-name: "homeassistant" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ee7c763..ba1f270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -pip>=21.0,<23.3 -bumpver==2023.1126 +pip>=21.0,<23.4 +bumpver==2023.1129 colorlog==6.7.0 -ruff==0.0.291 +ruff==0.1.1 chardet==5.2.0 -homeassistant==2023.6.1 +homeassistant==2023.11.2 diff --git a/requirements_dev.txt b/requirements_dev.txt index 2a05164..76bb1b6 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,6 +1,6 @@ -pip>=21.0,<23.3 -bumpver==2023.1126 +pip>=21.0,<23.4 +bumpver==2023.1129 colorlog==6.7.0 -ruff==0.0.291 +ruff==0.1.1 chardet==5.2.0 -pytest-homeassistant-custom-component==0.13.59 +pytest-homeassistant-custom-component==0.13.76 diff --git a/scripts/setup b/scripts/setup index f4e53b2..06f08cb 100755 --- a/scripts/setup +++ b/scripts/setup @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -e +export TERM="xterm-256color" cd "$(dirname "$0")/.." @@ -28,4 +29,3 @@ if [ ${BUILD_TYPE} == "run" ]; then clear python3 -m pip install --requirement requirements.txt fi - diff --git a/setup.cfg b/setup.cfg index 8b7c48b..de564e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,8 @@ minversion = 6.0 testpaths = tests addopts = --strict-markers - --cov=custom_components/ambrogio_robot, + --cov=custom_components/ambrogio_robot + --cov-report xml [isort] # https://github.com/timothycrosley/isort From b9595e0abbb057def48966b476d66354856d7850 Mon Sep 17 00:00:00 2001 From: Richard Holmes Date: Wed, 3 Jan 2024 20:48:57 +0000 Subject: [PATCH 2/2] Add Oauth/ User Password and Manual for ConfigFlow --- .devcontainer-template.json | 1 + configuration-template.yaml | 5 + custom_components/ambrogio_robot/__init__.py | 34 ++- .../ambrogio_robot/api/__init__.py | 1 + .../ambrogio_robot/{ => api}/api.py | 2 +- .../{api_firebase.py => api/firebase.py} | 14 +- .../ambrogio_robot/config_flow.py | 220 ++++++++++++------ custom_components/ambrogio_robot/const.py | 8 +- .../ambrogio_robot/coordinator.py | 2 +- .../frontend/firebase_auth.html | 80 +++++++ .../frontend/resources/apple-24.png | Bin 0 -> 418 bytes .../frontend/resources/button_apple.css | 41 ++++ .../frontend/resources/button_google.css | 41 ++++ .../ambrogio_robot/manifest.json | 4 +- .../ambrogio_robot/translations/en.json | 30 ++- tests/test_api_firebase.py | 2 +- tests/test_coordinator.py | 7 + 17 files changed, 397 insertions(+), 95 deletions(-) create mode 100644 custom_components/ambrogio_robot/api/__init__.py rename custom_components/ambrogio_robot/{ => api}/api.py (99%) rename custom_components/ambrogio_robot/{api_firebase.py => api/firebase.py} (89%) create mode 100644 custom_components/ambrogio_robot/frontend/firebase_auth.html create mode 100644 custom_components/ambrogio_robot/frontend/resources/apple-24.png create mode 100644 custom_components/ambrogio_robot/frontend/resources/button_apple.css create mode 100644 custom_components/ambrogio_robot/frontend/resources/button_google.css create mode 100644 tests/test_coordinator.py diff --git a/.devcontainer-template.json b/.devcontainer-template.json index ce92979..4c7159c 100644 --- a/.devcontainer-template.json +++ b/.devcontainer-template.json @@ -35,6 +35,7 @@ "python.analysis.autoSearchPaths": false, "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + "python.experiments.enabled": false, "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnPaste": false, "editor.formatOnSave": true, diff --git a/configuration-template.yaml b/configuration-template.yaml index 6ae122b..25f212b 100644 --- a/configuration-template.yaml +++ b/configuration-template.yaml @@ -1,6 +1,11 @@ # https://www.home-assistant.io/integrations/default_config/ default_config: +# Enable to initiate debugging in Home Assistant Runtime +debugpy: + start: false + wait: false + # https://www.home-assistant.io/integrations/logger/ logger: default: warning diff --git a/custom_components/ambrogio_robot/__init__.py b/custom_components/ambrogio_robot/__init__.py index c08f2f0..67bd324 100644 --- a/custom_components/ambrogio_robot/__init__.py +++ b/custom_components/ambrogio_robot/__init__.py @@ -15,14 +15,15 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .api_firebase import AmbrogioRobotFirebaseAPI, AmbrogioRobotException +from .api.firebase import AmbrogioRobotFirebaseAPI, AmbrogioRobotException from .const import ( API_KEY, DOMAIN, ) -from .api import AmbrogioRobotApiClient +from .api.api import AmbrogioRobotApiClient from .coordinator import AmbrogioDataUpdateCoordinator PLATFORMS: list[Platform] = [ @@ -41,12 +42,39 @@ async def async_setup( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + # If using a requests-based API lib + # hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api.ConfigEntryAuth( + # hass, session + # ) + + # If using an aiohttp-based API lib + # hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api.AsyncConfigEntryAuth( + # aiohttp_client.async_get_clientsession(hass), session + # + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_setup_entry2(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) # Get or update the list of robots from Firebase try: - api_firebase = AmbrogioRobotFirebaseAPI(async_get_clientsession(hass)) + api_firebase = AmbrogioRobotFirebaseAPI( + aiohttp_client.async_get_clientsession(hass) + ) tokens = await api_firebase.verify_password( entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD] ) diff --git a/custom_components/ambrogio_robot/api/__init__.py b/custom_components/ambrogio_robot/api/__init__.py new file mode 100644 index 0000000..f5462e7 --- /dev/null +++ b/custom_components/ambrogio_robot/api/__init__.py @@ -0,0 +1 @@ +"""API Module.""" diff --git a/custom_components/ambrogio_robot/api.py b/custom_components/ambrogio_robot/api/api.py similarity index 99% rename from custom_components/ambrogio_robot/api.py rename to custom_components/ambrogio_robot/api/api.py index 9602d62..7b543f4 100644 --- a/custom_components/ambrogio_robot/api.py +++ b/custom_components/ambrogio_robot/api/api.py @@ -8,7 +8,7 @@ import aiohttp import async_timeout -from .const import LOGGER +from ..const import LOGGER class AmbrogioRobotApiClientError(Exception): diff --git a/custom_components/ambrogio_robot/api_firebase.py b/custom_components/ambrogio_robot/api/firebase.py similarity index 89% rename from custom_components/ambrogio_robot/api_firebase.py rename to custom_components/ambrogio_robot/api/firebase.py index 0eded2e..04e9446 100644 --- a/custom_components/ambrogio_robot/api_firebase.py +++ b/custom_components/ambrogio_robot/api/firebase.py @@ -8,14 +8,14 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) -GOOGLEAPIS_URL = "https://www.googleapis.com" -VERIFY_PASSWORD = "/identitytoolkit/v3/relyingparty/verifyPassword" +GOOGLEAPIS_URL = "https://identitytoolkit.googleapis.com" +VERIFY_PASSWORD = "/v1/accounts:signInWithPassword" FIREBASE_URL = "wss://centrosistemi-ambrogioremote.firebaseio.com" FIREBASE_DB = "centrosistemi-ambrogioremote" FIREBASE_VER = "5" -APP_ID = "AIzaSyCUGSbVrwZ3X7BHU6oiUSmdzQwx-QXypUI" +API_KEY = "AIzaSyCUGSbVrwZ3X7BHU6oiUSmdzQwx-QXypUI" class AmbrogioRobotException(Exception): @@ -49,7 +49,7 @@ async def verify_password(self, email: str, password: str) -> dict: response = await self._session.post( urljoin( GOOGLEAPIS_URL, - f"{VERIFY_PASSWORD}?key={APP_ID}", + f"{VERIFY_PASSWORD}?key={API_KEY}", ), data=json.dumps(auth_data), headers={ @@ -64,11 +64,7 @@ async def verify_password(self, email: str, password: str) -> dict: response_json["error"]["code"], response_json["error"]["message"] ) - valid_data = { - "api_token": response_json["localId"], - "access_token": response_json["idToken"], - } - return valid_data + return response_json async def get_robots(self, access_token: str, api_token: str) -> dict: """Get the Garage Robots.""" diff --git a/custom_components/ambrogio_robot/config_flow.py b/custom_components/ambrogio_robot/config_flow.py index bfa109b..9ef4886 100644 --- a/custom_components/ambrogio_robot/config_flow.py +++ b/custom_components/ambrogio_robot/config_flow.py @@ -1,34 +1,41 @@ """Adds config flow for Ambrogio.""" import logging +import urllib.parse import voluptuous as vol -from homeassistant.const import ( - CONF_EMAIL, - CONF_ERROR, - CONF_PASSWORD, -) +from aiohttp import web_response + +from homeassistant.components import http +from homeassistant.components.http.view import HomeAssistantView from homeassistant.config_entries import ( CONN_CLASS_CLOUD_POLL, ConfigFlow, - ConfigEntry, FlowResult, - OptionsFlow, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( + CONF_EMAIL, + CONF_ERROR, + CONF_PASSWORD, + CONF_NAME, +) from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .api_firebase import AmbrogioRobotFirebaseAPI, AmbrogioRobotException +from .api.firebase import AmbrogioRobotException, AmbrogioRobotFirebaseAPI from .const import ( + API_KEY, + CONF_AUTH_PROVIDER, + CONF_REFRESH_TOKEN, + CONF_UID, DOMAIN, - UPDATE_INTERVAL_DEFAULT, - UPDATE_INTERVAL_WORKING, - CONF_SCAN_INTERVAL_DEFAULT, - CONF_SCAN_INTERVAL_WORKING, ) _LOGGER: logging.Logger = logging.getLogger(__name__) +HEADER_FRONTEND_BASE = "HA-Frontend-Base" +AUTH_CALLBACK_PATH = "/auth/ambrogio_robot/callback" +AUTH_CALLBACK_NAME = "auth:ambrogio_robot:callback" + class AmbrogioFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Ambrogio.""" @@ -36,28 +43,54 @@ class AmbrogioFlowHandler(ConfigFlow, domain=DOMAIN): # Used to call the migration method if the verison changes. VERSION = 1 CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + auth_data: dict = {} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: - """Handle the flow for setup.""" + """Show the Setup Menu.""" + + return self.async_show_menu( + step_id="user", + menu_options={ + "user_pass", + "oauth", + "manual", + }, + ) + + async def async_step_user_pass(self, user_input: dict | None = None) -> FlowResult: + """Handle Firebase user/password signin.""" _errors: dict[str, str] = {} if user_input is not None: - result = await authenticate( - self.hass, user_input[CONF_EMAIL], user_input[CONF_PASSWORD] - ) - if CONF_ERROR in result: - _errors["base"] = result[CONF_ERROR] + api = AmbrogioRobotFirebaseAPI(async_get_clientsession(self.hass)) + try: + response_json = await api.verify_password( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except AmbrogioRobotException as exp: + _LOGGER.error("Google APIS Auth Failed: %s", exp) + + if CONF_ERROR in response_json: + _errors["base"] = response_json[CONF_ERROR] else: + # Get all the valid data from the response. + self.auth_data = { + CONF_NAME: response_json["email"], + CONF_UID: response_json["localId"], + CONF_AUTH_PROVIDER: "user_password", + CONF_REFRESH_TOKEN: response_json["refreshToken"], + } + # Setup the Unique ID and check if already configured - await self.async_set_unique_id(user_input[CONF_EMAIL]) + await self.async_set_unique_id(self.auth_data[CONF_NAME]) self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input + title=self.auth_data[CONF_NAME], data=self.auth_data ) return self.async_show_form( - step_id="user", + step_id="user_pass", data_schema=vol.Schema( { vol.Required( @@ -82,72 +115,117 @@ async def async_step_user(self, user_input: dict | None = None) -> FlowResult: last_step=True, ) - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: - """Return the option flow handler.""" - return AmbrogioOptionsFlowHandler(config_entry) - + async def async_step_oauth(self, user_input: dict | None = None) -> FlowResult: + """Handle Google/ Apple OAuth signin.""" + if not user_input: + self.hass.http.register_view(AmbrogioAuthorizationCallbackView) + if (req := http.current_request.get()) is None: + raise RuntimeError("No current request in context") + if (hass_url := req.headers.get(HEADER_FRONTEND_BASE)) is None: + raise RuntimeError("No header in request") + + self.hass.http.register_static_path( + "/ambrogio_robot", + self.hass.config.path( + "custom_components/ambrogio_robot/frontend/resources" + ), + ) + self.hass.http.register_static_path( + "/ambrogio_robot/oauth", + self.hass.config.path( + "custom_components/ambrogio_robot/frontend/firebase_auth.html" + ), + ) -class AmbrogioOptionsFlowHandler(OptionsFlow): - """Ambrogio config options flow handler.""" + forward_url = f"{hass_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" + AUTH_URL = "/ambrogio_robot/oauth?{}" + parameters = { + "forwardUrl": forward_url, + "apiKey": API_KEY, + } + url = AUTH_URL.format(urllib.parse.urlencode(parameters)) + + return self.async_external_step( + step_id="oauth", + url=url, + ) - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize HACS options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) + self.auth_data = user_input + return self.async_external_step_done(next_step_id="oauth_finish") - async def async_step_init( - self, - user_input: dict[str, any] | None = None, # pylint: disable=unused-argument + async def async_step_oauth_finish( + self, user_input: dict | None = None ) -> FlowResult: - """Manage the options.""" - return await self.async_step_user() + """Handle the flow for the OAuth Finish process.""" + # Check this is unique + await self.async_set_unique_id(self.auth_data[CONF_NAME]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.auth_data[CONF_NAME], data=self.auth_data + ) + + async def async_step_manual(self, user_input: dict | None = None) -> FlowResult: + """Handle Manual Configuration.""" + _errors: dict[str, str] = {} - async def async_step_user( - self, user_input: dict[str, any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" if user_input is not None: - self.options.update(user_input) - return await self._update_options() + self.auth_data = { + CONF_NAME: user_input[CONF_NAME], + CONF_UID: user_input[CONF_UID], + CONF_AUTH_PROVIDER: "manual", + } + + _errors["base"] = "not_implemented" return self.async_show_form( - step_id="user", + step_id="manual", data_schema=vol.Schema( { vol.Required( - CONF_SCAN_INTERVAL_DEFAULT, - default=self.options.get( - CONF_SCAN_INTERVAL_DEFAULT, UPDATE_INTERVAL_DEFAULT + CONF_NAME, + default=(user_input or {}).get(CONF_NAME), + ): selector.TextSelector( + selector.TextSelectorConfig( + type=selector.TextSelectorType.TEXT ), - ): vol.All(vol.Coerce(int), vol.Range(min=120, max=600)), + ), vol.Required( - CONF_SCAN_INTERVAL_WORKING, - default=self.options.get( - CONF_SCAN_INTERVAL_WORKING, UPDATE_INTERVAL_WORKING + CONF_UID, + default=(user_input or {}).get(CONF_UID), + ): selector.TextSelector( + selector.TextSelectorConfig( + type=selector.TextSelectorType.TEXT ), - ): vol.All(vol.Coerce(int), vol.Range(min=10, max=120)), + ), } ), - ) - - async def _update_options(self) -> FlowResult: - """Update config entry options.""" - return self.async_create_entry( - title=self.config_entry.data.get(CONF_EMAIL), data=self.options + errors=_errors, + last_step=True, ) -async def authenticate( - hass: HomeAssistant, email: str, password: str -) -> dict[str, any]: - """Authenticate and Setup Robots based on Google Login.""" - api = AmbrogioRobotFirebaseAPI(async_get_clientsession(hass)) - try: - tokens = await api.verify_password(email, password) - except AmbrogioRobotException as exp: - _LOGGER.error("Google APIS Auth Failed: %s", exp) - return {CONF_ERROR: str(exp.message).lower()} +class AmbrogioAuthorizationCallbackView(HomeAssistantView): + """Handle Callback from external auth.""" + + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + requires_auth = False + + async def get(self, request): + """Receive authorization confirmation.""" + hass = request.app["hass"] + await hass.config_entries.flow.async_configure( + flow_id=request.query["flow_id"], + user_input={ + CONF_NAME: request.query["email"], + CONF_AUTH_PROVIDER: request.query["provider"], + CONF_UID: request.query["uid"], + CONF_REFRESH_TOKEN: request.query["refreshToken"], + }, + ) - return tokens + return web_response.Response( + headers={"content-type": "text/html"}, + text="Success! This window can be closed", + ) diff --git a/custom_components/ambrogio_robot/const.py b/custom_components/ambrogio_robot/const.py index b984704..46a7cc0 100644 --- a/custom_components/ambrogio_robot/const.py +++ b/custom_components/ambrogio_robot/const.py @@ -19,7 +19,8 @@ MANUFACTURER = "Zucchetti Centro Sistemi" ATTRIBUTION = "Data provided gently by Telit IoT Platform" -API_KEY = "DJMYYngGNEit40vA" +API_KEY = "AIzaSyCUGSbVrwZ3X7BHU6oiUSmdzQwx-QXypUI" +# API_KEY = "DJMYYngGNEit40vA" API_DATETIME_FORMAT_DEFAULT = "%Y-%m-%dT%H:%M:%S.%f%z" API_DATETIME_FORMAT_FALLBACK = "%Y-%m-%dT%H:%M:%S%z" API_ACK_TIMEOUT = 30 @@ -27,6 +28,11 @@ UPDATE_INTERVAL_DEFAULT = 300 UPDATE_INTERVAL_WORKING = 60 +CONF_AUTH_PROVIDER = "provider" +CONF_EXPIRATION_TIME = "expiration_time" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_UID = "uid" + CONF_CONFIRM = "confirm" CONF_ROBOT_NAME = "robot_name" CONF_ROBOT_IMEI = "robot_imei" diff --git a/custom_components/ambrogio_robot/coordinator.py b/custom_components/ambrogio_robot/coordinator.py index 8e4aa5e..b133eb2 100644 --- a/custom_components/ambrogio_robot/coordinator.py +++ b/custom_components/ambrogio_robot/coordinator.py @@ -26,7 +26,7 @@ ) from homeassistant.exceptions import ConfigEntryAuthFailed -from .api import ( +from .api.api import ( AmbrogioRobotApiClient, AmbrogioRobotApiClientAuthenticationError, AmbrogioRobotApiClientError, diff --git a/custom_components/ambrogio_robot/frontend/firebase_auth.html b/custom_components/ambrogio_robot/frontend/firebase_auth.html new file mode 100644 index 0000000..359fcb1 --- /dev/null +++ b/custom_components/ambrogio_robot/frontend/firebase_auth.html @@ -0,0 +1,80 @@ + +
+ Ambrogio Authentication for HomeAssistant + + + +
+ + + +

+ +

+
+
+ + + + + \ No newline at end of file diff --git a/custom_components/ambrogio_robot/frontend/resources/apple-24.png b/custom_components/ambrogio_robot/frontend/resources/apple-24.png new file mode 100644 index 0000000000000000000000000000000000000000..021b7bb820fe09b5ac5a16b17c9fb57ad42c0e33 GIT binary patch literal 418 zcmV;T0bTxyP)ot>VIToR0~?qVG9SWtvZu1@SimJd zgjU>R6Em2=YO>zW#JO-D^)h$^TS5!kIq~M_0soFxQemN1Gg)^UyS`icL;ZhbR~6nU z%6>5t{)my^&}=EwMa~suJKuB5Ubw2k6DAKNP{Uk&qmhZGSXmDxZ#Cr zikqVJ#?KTP)RPUdCeK((q1hI_W4o-xis~y{92G81Oa*reCMwl>1pGsI5lYpI(+&;x z{qbC<8pUl6;6-R*-lKSh;VSS{V>l3|;tVUvw;|!DvxW;iV6O=N1&!&-dPB<$5C8xG M07*qoM6N<$g2WHAxBvhE literal 0 HcmV?d00001 diff --git a/custom_components/ambrogio_robot/frontend/resources/button_apple.css b/custom_components/ambrogio_robot/frontend/resources/button_apple.css new file mode 100644 index 0000000..8c8d2dc --- /dev/null +++ b/custom_components/ambrogio_robot/frontend/resources/button_apple.css @@ -0,0 +1,41 @@ +.login-with-apple-btn { + transition: background-color .3s, box-shadow .3s; + + padding: 12px 16px 12px 42px; + border: none; + border-radius: 3px; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25); + + color: black; + font-size: 15px; + font-weight: 700; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + + background-image: url('apple-24.png'); + background-color: white; + background-repeat: no-repeat; + background-position: 6px 7px; + + &:hover { + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 2px 4px rgba(0, 0, 0, .25); + } + + &:active { + background-color: #eeeeee; + } + + &:focus { + outline: none; + box-shadow: + 0 -1px 0 rgba(0, 0, 0, .04), + 0 2px 4px rgba(0, 0, 0, .25), + 0 0 0 3px #c8dafc; + } + + &:disabled { + filter: grayscale(100%); + background-color: #ebebeb; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25); + cursor: not-allowed; + } +} \ No newline at end of file diff --git a/custom_components/ambrogio_robot/frontend/resources/button_google.css b/custom_components/ambrogio_robot/frontend/resources/button_google.css new file mode 100644 index 0000000..9bec78a --- /dev/null +++ b/custom_components/ambrogio_robot/frontend/resources/button_google.css @@ -0,0 +1,41 @@ +.login-with-google-btn { + transition: background-color .3s, box-shadow .3s; + + padding: 12px 16px 12px 42px; + border: none; + border-radius: 3px; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25); + + color: #757575; + font-size: 14px; + font-weight: 500; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTcuNiA5LjJsLS4xLTEuOEg5djMuNGg0LjhDMTMuNiAxMiAxMyAxMyAxMiAxMy42djIuMmgzYTguOCA4LjggMCAwIDAgMi42LTYuNnoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik05IDE4YzIuNCAwIDQuNS0uOCA2LTIuMmwtMy0yLjJhNS40IDUuNCAwIDAgMS04LTIuOUgxVjEzYTkgOSAwIDAgMCA4IDV6IiBmaWxsPSIjMzRBODUzIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNNCAxMC43YTUuNCA1LjQgMCAwIDEgMC0zLjRWNUgxYTkgOSAwIDAgMCAwIDhsMy0yLjN6IiBmaWxsPSIjRkJCQzA1IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNOSAzLjZjMS4zIDAgMi41LjQgMy40IDEuM0wxNSAyLjNBOSA5IDAgMCAwIDEgNWwzIDIuNGE1LjQgNS40IDAgMCAxIDUtMy43eiIgZmlsbD0iI0VBNDMzNSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTAgMGgxOHYxOEgweiIvPjwvZz48L3N2Zz4=); + background-color: white; + background-repeat: no-repeat; + background-position: 12px 11px; + + &:hover { + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 2px 4px rgba(0, 0, 0, .25); + } + + &:active { + background-color: #eeeeee; + } + + &:focus { + outline: none; + box-shadow: + 0 -1px 0 rgba(0, 0, 0, .04), + 0 2px 4px rgba(0, 0, 0, .25), + 0 0 0 3px #c8dafc; + } + + &:disabled { + filter: grayscale(100%); + background-color: #ebebeb; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25); + cursor: not-allowed; + } +} \ No newline at end of file diff --git a/custom_components/ambrogio_robot/manifest.json b/custom_components/ambrogio_robot/manifest.json index 8dbd42a..80ffdc3 100644 --- a/custom_components/ambrogio_robot/manifest.json +++ b/custom_components/ambrogio_robot/manifest.json @@ -9,6 +9,8 @@ "documentation": "https://github.com/sHedC/homeassistant-ambrogio", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/sHedC/homeassistant-ambrogio/issues", - "requirements": [], + "requirements": [ + "firebase==4.0.1" + ], "version": "0.0.0" } \ No newline at end of file diff --git a/custom_components/ambrogio_robot/translations/en.json b/custom_components/ambrogio_robot/translations/en.json index fc9501b..177569f 100644 --- a/custom_components/ambrogio_robot/translations/en.json +++ b/custom_components/ambrogio_robot/translations/en.json @@ -2,20 +2,36 @@ "config": { "step": { "user": { - "title": "Cloud Service Login", - "description": "Garage Name is the collective name for the robots under a single account, Access Token and IMEI can be found in the mobile app. For additional information go to https://github.com/sHedC/homeassistant-ambrogio. More mowers can be added after setup using the device Options.", + "title": "Choose Login", + "description": "Login using Ambrogio Username/ Password, OAuth with Google/Apple or Manually Configure.", + "menu_options": { + "user_pass": "Login with Ambrogio User/ Password", + "oauth": "Login with Google/ Apple OAuth", + "manual": "Configure Manually" + } + }, + "user_pass": { + "title": "Ambrogio Direct Login", + "description": "Enter the Email Address and Login Password for the Ambrogio direct login.", "data": { - "api_token": "API Token (This is the current known token)", - "access_token": "User Access Token", - "robot_name": "Robot Name, you know you have one", - "robot_imei": "Robots IMEI Number" + "email": "Email Address", + "password": "Password" + } + }, + "manual": { + "title": "Ambrogio Manual Setup", + "description": "Enter a Garage Name, the Account Key comes from the Android App. Once setup, use configure options to setup mowers.", + "data": { + "name": "Garage Name", + "uid": "Account Key" } } }, "error": { "invalid_email": "E-Mail is not valid.", "invalid_password": "Password is not valid.", - "robot_not_found": "Robot Mower is not found or connected properly." + "robot_not_found": "Robot Mower is not found or connected properly.", + "not_implemented": "Not Yet Implemented." }, "abort": { "already_configured": "Device is already configured" diff --git a/tests/test_api_firebase.py b/tests/test_api_firebase.py index e74b8a3..524212b 100644 --- a/tests/test_api_firebase.py +++ b/tests/test_api_firebase.py @@ -13,7 +13,7 @@ ) from custom_components.ambrogio_robot import DOMAIN -from custom_components.ambrogio_robot.api_firebase import ( +from custom_components.ambrogio_robot.api.firebase import ( AmbrogioRobotFirebaseAPI, AmbrogioRobotException, ) diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py new file mode 100644 index 0000000..1ecdc19 --- /dev/null +++ b/tests/test_coordinator.py @@ -0,0 +1,7 @@ +"""Test the Data Update Coordinator.""" +from homeassistant.core import HomeAssistant + + +async def test_setup(hass: HomeAssistant): + """Test Coordinator Setup.""" + # TODO: Build and Test Diagnostics