From d396f2723b966e9bbd19001acf0c8ea38121aebe Mon Sep 17 00:00:00 2001 From: jtholen001 <30531217+jtholen001@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:41:46 -0800 Subject: [PATCH 1/3] Setup master token login --- .../gkeep_list_sync/config_flow.py | 17 ++++++++++++----- custom_components/gkeep_list_sync/const.py | 1 + custom_components/gkeep_list_sync/manifest.json | 2 +- custom_components/gkeep_list_sync/strings.json | 3 ++- .../gkeep_list_sync/translations/en.json | 3 ++- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/custom_components/gkeep_list_sync/config_flow.py b/custom_components/gkeep_list_sync/config_flow.py index f7648b7..eb6d20e 100644 --- a/custom_components/gkeep_list_sync/config_flow.py +++ b/custom_components/gkeep_list_sync/config_flow.py @@ -29,6 +29,7 @@ CONF_LIST_ID, DEFAULT_LIST_TITLE, MISSING_LIST, + MASTER_TOKEN ) _LOGGER = logging.getLogger(__name__) @@ -53,6 +54,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, config_entry.data.get(CONF_USERNAME), config_entry.data.get(CONF_ACCESS_TOKEN), ) + elif data[MASTER_TOKEN] is not None: + config[CONF_USERNAME] = data[CONF_USERNAME] + await hass.async_add_executor_job( + keep.resume, + data[CONF_USERNAME], + data[MASTER_TOKEN] + ) else: config[CONF_USERNAME] = data[CONF_USERNAME] await hass.async_add_executor_job( @@ -86,7 +94,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Keep List Sync.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize the Google Keep config flow.""" @@ -121,14 +129,13 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Flo CONF_USERNAME, default=self.username, ): str, - vol.Required( - CONF_PASSWORD, - ): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(MASTER_TOKEN): str, vol.Required( CONF_LIST_TITLE, default=self.list_title, ): str, - } + }, ) # Schema for List name re-auth diff --git a/custom_components/gkeep_list_sync/const.py b/custom_components/gkeep_list_sync/const.py index 2be14ec..dccbd00 100644 --- a/custom_components/gkeep_list_sync/const.py +++ b/custom_components/gkeep_list_sync/const.py @@ -7,3 +7,4 @@ MISSING_LIST = "missing_list" SHOPPING_LIST_DOMAIN = "shopping_list" SERVICE = "sync_list" +MASTER_TOKEN = "master_token" diff --git a/custom_components/gkeep_list_sync/manifest.json b/custom_components/gkeep_list_sync/manifest.json index 56dd973..f8a40fe 100644 --- a/custom_components/gkeep_list_sync/manifest.json +++ b/custom_components/gkeep_list_sync/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://github.com/fcastilloec/gkeep-list-sync/blob/main/README.md", "issue_tracker": "https://github.com/fcastilloec/gkeep-list-sync/issues", - "requirements": ["gkeepapi==0.14.2", "urllib3<2", "gpsoauth==1.0.2"], + "requirements": ["gkeepapi==0.15.1", "urllib3<2", "gpsoauth==1.0.2"], "dependencies": ["shopping_list"], "codeowners": ["@fcastilloec"], "integration_type": "service" diff --git a/custom_components/gkeep_list_sync/strings.json b/custom_components/gkeep_list_sync/strings.json index b6b0d93..bc706f5 100644 --- a/custom_components/gkeep_list_sync/strings.json +++ b/custom_components/gkeep_list_sync/strings.json @@ -5,7 +5,8 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "list_title": "List title" + "list_title": "List title", + "master_token": "Master Token" } } }, diff --git a/custom_components/gkeep_list_sync/translations/en.json b/custom_components/gkeep_list_sync/translations/en.json index 5d681b2..743b600 100644 --- a/custom_components/gkeep_list_sync/translations/en.json +++ b/custom_components/gkeep_list_sync/translations/en.json @@ -15,7 +15,8 @@ "data": { "list_title": "List title", "password": "Password", - "username": "Username" + "username": "Username", + "master_token": "Master Token" } } } From 74a44188bef86deb62d312d666ca18da05440fac Mon Sep 17 00:00:00 2001 From: jtholen001 <30531217+jtholen001@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:49:49 -0800 Subject: [PATCH 2/3] multiple accounts and cleaning up --- custom_components/gkeep_list_sync/__init__.py | 12 ++++++----- .../gkeep_list_sync/config_flow.py | 21 ++++++++----------- custom_components/gkeep_list_sync/const.py | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/custom_components/gkeep_list_sync/__init__.py b/custom_components/gkeep_list_sync/__init__.py index c7e45da..332d5bc 100644 --- a/custom_components/gkeep_list_sync/__init__.py +++ b/custom_components/gkeep_list_sync/__init__.py @@ -16,7 +16,7 @@ CONF_LIST_ID, SHOPPING_LIST_DOMAIN, MISSING_LIST, - SERVICE, + SERVICE_NAME_BASE, ) _LOGGER = logging.getLogger(__name__) @@ -83,8 +83,8 @@ async def handle_sync_list(call) -> None: # pylint: disable=unused-argument # Sync again to delete already added items await hass.async_add_executor_job(keep.sync) - # Register the service - hass.services.async_register(DOMAIN, SERVICE, handle_sync_list) + # Register the service - Allow for as many services as we have usernames + hass.services.async_register(DOMAIN, get_service_name(config_entry), handle_sync_list) return True @@ -92,6 +92,8 @@ async def handle_sync_list(call) -> None: # pylint: disable=unused-argument async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: # pylint: disable=unused-argument """Unload a config entry.""" _LOGGER.debug("Unload integration") - hass.services.async_remove(DOMAIN, SERVICE) - del hass.data[DOMAIN] + hass.services.async_remove(DOMAIN, get_service_name(config_entry)) return True + +def get_service_name(config_entry: ConfigEntry) -> str: + return SERVICE_NAME_BASE + "_" + config_entry.data.get(CONF_USERNAME).partition("@")[0] diff --git a/custom_components/gkeep_list_sync/config_flow.py b/custom_components/gkeep_list_sync/config_flow.py index eb6d20e..8037c1a 100644 --- a/custom_components/gkeep_list_sync/config_flow.py +++ b/custom_components/gkeep_list_sync/config_flow.py @@ -54,13 +54,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, config_entry.data.get(CONF_USERNAME), config_entry.data.get(CONF_ACCESS_TOKEN), ) - elif data[MASTER_TOKEN] is not None: + elif data.get(MASTER_TOKEN) is not None: config[CONF_USERNAME] = data[CONF_USERNAME] await hass.async_add_executor_job( keep.resume, data[CONF_USERNAME], data[MASTER_TOKEN] ) + elif data.get(MASTER_TOKEN) is None and data.get(CONF_PASSWORD) is None: + _LOGGER.error("A password or master token is needed to setup a new service for gkeep-list-sync") + raise(InvalidConfig()) else: config[CONF_USERNAME] = data[CONF_USERNAME] await hass.async_add_executor_job( @@ -107,21 +110,13 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Flo """Handle the initial step.""" errors = {} + config_entry: config_entries.ConfigEntry = None + # Check that the dependency is loaded if not self.hass.data.get(SHOPPING_LIST_DOMAIN): _LOGGER.error("Shopping List integration needs to be setup") return self.async_abort(reason="dependency_not_found") - # Get any current configuration entries - if self._async_current_entries(): - config_entry = self._async_current_entries()[0] - else: - config_entry = None - - # Check that single instance of the config is allowed - if config_entry and not self.reauth: - return self.async_abort(reason="single_instance_allowed") - # Schema for initial setup of loging re-auth all_schema = vol.Schema( { @@ -186,6 +181,8 @@ async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: self.missing_list = self.hass.data[DOMAIN][MISSING_LIST] return await self.async_step_user() - class CannotLogin(HomeAssistantError): """Error to indicate we cannot login.""" + +class InvalidConfig(HomeAssistantError): + """Error to indicate the user entered invalid config""" diff --git a/custom_components/gkeep_list_sync/const.py b/custom_components/gkeep_list_sync/const.py index dccbd00..e4ed7a1 100644 --- a/custom_components/gkeep_list_sync/const.py +++ b/custom_components/gkeep_list_sync/const.py @@ -6,5 +6,5 @@ DEFAULT_LIST_TITLE = "Groceries" MISSING_LIST = "missing_list" SHOPPING_LIST_DOMAIN = "shopping_list" -SERVICE = "sync_list" +SERVICE_NAME_BASE = "sync_list" MASTER_TOKEN = "master_token" From 218f1ff0af82b1815e0903a62d40f585ea623f7d Mon Sep 17 00:00:00 2001 From: jtholen001 <30531217+jtholen001@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:05:59 -0800 Subject: [PATCH 3/3] Update README.md --- README.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 004c3e8..3734dc3 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,44 @@ This is meant to be used with Google Assistant to easily add items to Home Assis 5. Restart Home Assistant. 6. In the Home Assistant Configuration, add the new integration by searching for its name. +When creating a new service you will be presented with 3 fields to fill in: +- username (requred) +- password (optional) +- master token (optional) + +You will need to fill out either password or master token as a way of authenticating to your google account via the keep api. App passwords are highly recommended, but if you are seeing authentication errors see the Authentication Options section for other methods. + ## Usage -The integration adds a service call that can be used on any automation to synchronize Google Keep List with Home Assistant Shopping List. +The integration adds one service per service entry in the integration page that can be called by any automation or script to synchronize Google Keep List with Home Assistant Shopping List. -The service goes through the following steps: +Each service will go through the following steps: -1. Reads all unchecked items from the specified Google Keep list. Checked items are ignored. +1. Read all unchecked items from the specified Google Keep list. Checked items are ignored. 2. Adds each item to Home Assistant Shopping List integration. 3. Delete the item from Google Keep list. This prevent double adding an item if the service is called again. + + +## Authentication Options +It is recommended to login with an app password as this can be easily revoked if it were to become compromised. That being said, some accounts seem to have issues logging in with passwords (both the regular password and app passwords) depending on which device you are attempting to get a token from. A workaround that seems to work for these "problematic" accounts is to create a docker container and run a script to get the tokens which we can then pass into the keep api. Although this is the least secure by far (master tokens never expire) it should allow for any google account to use this integration + +Steps to acquire a master token: (**only do this if username and password are NOT working**) +1. Install docker container if you do not already have it +2. In your preferred cli run the following commands + +``` +$ docker pull breph/ha-google-home_get-token:latest +$ docker run -it -d breph/ha-google-home_get-token +# use the returned container id inside the next command +$ docker exec -it bash + +# inside container +root@:/# python3 get_tokens.py` +``` + +3. From the printed output you should get the master token for the account you entered, it will start with: aas_et/. Copy the whole token without any additional spaces into the master token field when setting up a new service in the integration ui. + + +## Shoutouts/Credits +[@fcastilloec](https://github.com/fcastilloec) For creating this project and publishing it for us to use +[@lhy](https://github.com/LeeHanYeong) For the docker container to reliably get a master token over on [ha-google-home](https://github.com/leikoilja/ha-google-home/issues/599#issuecomment-1760800334)