Fix Garmin Connect integration with python-garminconnect-aio (#50865)

This commit is contained in:
Ron Klinkien 2021-05-31 23:38:33 +02:00 committed by GitHub
parent 6ba2ee5cef
commit a0b3d0863b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 67 additions and 56 deletions

View file

@ -1,8 +1,8 @@
"""The Garmin Connect integration.""" """The Garmin Connect integration."""
from datetime import date, timedelta from datetime import date
import logging import logging
from garminconnect import ( from garminconnect_aio import (
Garmin, Garmin,
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectConnectionError, GarminConnectConnectionError,
@ -13,25 +13,27 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import DOMAIN from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"] PLATFORMS = ["sensor"]
MIN_SCAN_INTERVAL = timedelta(minutes=10)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Garmin Connect from a config entry.""" """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: try:
await hass.async_add_executor_job(garmin_client.login) await garmin_client.login()
except ( except (
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError, GarminConnectTooManyRequestsError,
@ -73,38 +75,29 @@ class GarminConnectData:
self.client = client self.client = client
self.data = None self.data = None
async def _get_combined_alarms_of_all_devices(self): @Throttle(DEFAULT_UPDATE_INTERVAL)
"""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)
async def async_update(self): async def async_update(self):
"""Update data via library.""" """Update data via API wrapper."""
today = date.today() today = date.today()
try: try:
self.data = await self.hass.async_add_executor_job( summary = await self.client.get_user_summary(today.isoformat())
self.client.get_stats_and_body, today.isoformat() body = await self.client.get_body_composition(today.isoformat())
)
self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() self.data = {
**summary,
**body["totalAverage"],
}
self.data["nextAlarm"] = await self.client.get_device_alarms()
except ( except (
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError, GarminConnectTooManyRequestsError,
GarminConnectConnectionError, GarminConnectConnectionError,
) as err: ) as err:
_LOGGER.error( _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 except Exception: # pylint: disable=broad-except
_LOGGER.exception( _LOGGER.exception(
"Unknown error occurred during Garmin Connect get activity request" "Unknown error occurred during Garmin Connect update requests"
) )
return

View file

@ -1,7 +1,7 @@
"""Config flow for Garmin Connect integration.""" """Config flow for Garmin Connect integration."""
import logging import logging
from garminconnect import ( from garminconnect_aio import (
Garmin, Garmin,
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectConnectionError, GarminConnectConnectionError,
@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@ -37,11 +38,15 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None: if user_input is None:
return await self._show_setup_form() 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 = {} errors = {}
try: try:
await self.hass.async_add_executor_job(garmin_client.login) username = await garmin_client.login()
except GarminConnectConnectionError: except GarminConnectConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
return await self._show_setup_form(errors) return await self._show_setup_form(errors)
@ -56,15 +61,13 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
return await self._show_setup_form(errors) return await self._show_setup_form(errors)
unique_id = garmin_client.get_full_name() await self.async_set_unique_id(username)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=unique_id, title=username,
data={ data={
CONF_ID: unique_id, CONF_ID: username,
CONF_USERNAME: user_input[CONF_USERNAME], CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_PASSWORD: user_input[CONF_PASSWORD],
}, },

View file

@ -1,4 +1,6 @@
"""Constants for the Garmin Connect integration.""" """Constants for the Garmin Connect integration."""
from datetime import timedelta
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_TIMESTAMP,
LENGTH_METERS, LENGTH_METERS,
@ -8,7 +10,8 @@ from homeassistant.const import (
) )
DOMAIN = "garmin_connect" DOMAIN = "garmin_connect"
ATTRIBUTION = "Data provided by garmin.com" ATTRIBUTION = "connect.garmin.com"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10)
GARMIN_ENTITY_LIST = { GARMIN_ENTITY_LIST = {
"totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True],

View file

@ -2,7 +2,7 @@
"domain": "garmin_connect", "domain": "garmin_connect",
"name": "Garmin Connect", "name": "Garmin Connect",
"documentation": "https://www.home-assistant.io/integrations/garmin_connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect",
"requirements": ["garminconnect==0.1.19"], "requirements": ["garminconnect_aio==0.1.1"],
"codeowners": ["@cyberjunky"], "codeowners": ["@cyberjunky"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from garminconnect import ( from garminconnect_aio import (
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectConnectionError, GarminConnectConnectionError,
GarminConnectTooManyRequestsError, GarminConnectTooManyRequestsError,

View file

@ -635,7 +635,7 @@ gTTS==2.2.2
garages-amsterdam==2.1.1 garages-amsterdam==2.1.1
# homeassistant.components.garmin_connect # homeassistant.components.garmin_connect
garminconnect==0.1.19 garminconnect_aio==0.1.1
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.6.30 geniushub-client==0.6.30

View file

@ -341,7 +341,7 @@ gTTS==2.2.2
garages-amsterdam==2.1.1 garages-amsterdam==2.1.1
# homeassistant.components.garmin_connect # homeassistant.components.garmin_connect
garminconnect==0.1.19 garminconnect_aio==0.1.1
# homeassistant.components.geo_json_events # homeassistant.components.geo_json_events
# homeassistant.components.usgs_earthquakes_feed # homeassistant.components.usgs_earthquakes_feed

View file

@ -1,7 +1,7 @@
"""Test the Garmin Connect config flow.""" """Test the Garmin Connect config flow."""
from unittest.mock import patch from unittest.mock import patch
from garminconnect import ( from garminconnect_aio import (
GarminConnectAuthenticationError, GarminConnectAuthenticationError,
GarminConnectConnectionError, GarminConnectConnectionError,
GarminConnectTooManyRequestsError, GarminConnectTooManyRequestsError,
@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_CONF = { MOCK_CONF = {
CONF_ID: "First Lastname", CONF_ID: "my@email.address",
CONF_USERNAME: "my@email.address", CONF_USERNAME: "my@email.address",
CONF_PASSWORD: "mypassw0rd", CONF_PASSWORD: "mypassw0rd",
} }
@ -23,27 +23,33 @@ MOCK_CONF = {
@pytest.fixture(name="mock_garmin_connect") @pytest.fixture(name="mock_garmin_connect")
def mock_garmin(): def mock_garmin():
"""Mock Garmin.""" """Mock Garmin Connect."""
with patch( with patch(
"homeassistant.components.garmin_connect.config_flow.Garmin", "homeassistant.components.garmin_connect.config_flow.Garmin",
) as 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 yield garmin.return_value
async def test_show_form(hass): async def test_show_form(hass):
"""Test that the form is served with no input.""" """Test that the form is served with no input."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM 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.""" """Test registering an integration and finishing flow works."""
with patch( with patch(
"homeassistant.components.garmin_connect.Garmin.login",
return_value="my@email.address",
), patch(
"homeassistant.components.garmin_connect.async_setup_entry", return_value=True "homeassistant.components.garmin_connect.async_setup_entry", return_value=True
): ):
result = await hass.config_entries.flow.async_init( 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"} 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.""" """Test abort if already setup."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) MockConfigEntry(
entry.add_to_hass(hass) 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF DOMAIN, context={"source": "user"}, data=MOCK_CONF
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"