From a0b3d0863b00cfe911d52c16980ea8a34fdacae0 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 31 May 2021 23:38:33 +0200 Subject: [PATCH] Fix Garmin Connect integration with python-garminconnect-aio (#50865) --- .../components/garmin_connect/__init__.py | 51 ++++++++----------- .../components/garmin_connect/config_flow.py | 19 ++++--- .../components/garmin_connect/const.py | 5 +- .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 40 ++++++++++----- 8 files changed, 67 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index af08a86abb2..bc3a1f0aad0 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,8 +1,8 @@ """The Garmin Connect integration.""" -from datetime import date, timedelta +from datetime import date import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,25 +13,27 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -MIN_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garmin Connect from a config entry.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - garmin_client = Garmin(username, password) + websession = async_get_clientsession(hass) + username: str = entry.data[CONF_USERNAME] + password: str = entry.data[CONF_PASSWORD] + + garmin_client = Garmin(websession, username, password) try: - await hass.async_add_executor_job(garmin_client.login) + await garmin_client.login() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -73,38 +75,29 @@ class GarminConnectData: self.client = client self.data = None - async def _get_combined_alarms_of_all_devices(self): - """Combine the list of active alarms from all garmin devices.""" - alarms = [] - devices = await self.hass.async_add_executor_job(self.client.get_devices) - for device in devices: - device_settings = await self.hass.async_add_executor_job( - self.client.get_device_settings, device["deviceId"] - ) - alarms += device_settings["alarms"] - return alarms - - @Throttle(MIN_SCAN_INTERVAL) + @Throttle(DEFAULT_UPDATE_INTERVAL) async def async_update(self): - """Update data via library.""" + """Update data via API wrapper.""" today = date.today() try: - self.data = await self.hass.async_add_executor_job( - self.client.get_stats_and_body, today.isoformat() - ) - self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() + summary = await self.client.get_user_summary(today.isoformat()) + body = await self.client.get_body_composition(today.isoformat()) + + self.data = { + **summary, + **body["totalAverage"], + } + self.data["nextAlarm"] = await self.client.get_device_alarms() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, GarminConnectConnectionError, ) as err: _LOGGER.error( - "Error occurred during Garmin Connect get activity request: %s", err + "Error occurred during Garmin Connect update requests: %s", err ) - return except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unknown error occurred during Garmin Connect get activity request" + "Unknown error occurred during Garmin Connect update requests" ) - return diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 218a98ba9a4..8e26e2bf608 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -37,11 +38,15 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - garmin_client = Garmin(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + websession = async_get_clientsession(self.hass) + + garmin_client = Garmin( + websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) errors = {} try: - await self.hass.async_add_executor_job(garmin_client.login) + username = await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -56,15 +61,13 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return await self._show_setup_form(errors) - unique_id = garmin_client.get_full_name() - - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(username) self._abort_if_unique_id_configured() return self.async_create_entry( - title=unique_id, + title=username, data={ - CONF_ID: unique_id, + CONF_ID: username, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 991ac90526a..19ed4ca4d94 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -1,4 +1,6 @@ """Constants for the Garmin Connect integration.""" +from datetime import timedelta + from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, LENGTH_METERS, @@ -8,7 +10,8 @@ from homeassistant.const import ( ) DOMAIN = "garmin_connect" -ATTRIBUTION = "Data provided by garmin.com" +ATTRIBUTION = "connect.garmin.com" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) GARMIN_ENTITY_LIST = { "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 913e85de954..2495249e4a4 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.19"], + "requirements": ["garminconnect_aio==0.1.1"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 0d946d5e88e..eb1690c9765 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, diff --git a/requirements_all.txt b/requirements_all.txt index d6d871d3126..298934a40e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9961342391d..9c389bc742b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index f3784d5e2e2..2ad36ffa29c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry MOCK_CONF = { - CONF_ID: "First Lastname", + CONF_ID: "my@email.address", CONF_USERNAME: "my@email.address", CONF_PASSWORD: "mypassw0rd", } @@ -23,27 +23,33 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): - """Mock Garmin.""" + """Mock Garmin Connect.""" with patch( "homeassistant.components.garmin_connect.config_flow.Garmin", ) as garmin: - garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value async def test_show_form(hass): """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER -async def test_step_user(hass, mock_garmin_connect): +async def test_step_user(hass): """Test registering an integration and finishing flow works.""" with patch( + "homeassistant.components.garmin_connect.Garmin.login", + return_value="my@email.address", + ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -95,12 +101,18 @@ async def test_unknown_error(hass, mock_garmin_connect): assert result["errors"] == {"base": "unknown"} -async def test_abort_if_already_setup(hass, mock_garmin_connect): +async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ).add_to_hass(hass) + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured"