mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-08 20:38:00 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9df43a1f3 | ||
|
|
d6053f2977 | ||
|
|
40f4bb9b44 | ||
|
|
6fe3fb3881 | ||
|
|
2b17d44056 | ||
|
|
b2fa46400e | ||
|
|
51764dbdee | ||
|
|
a5b6a77480 | ||
|
|
e61f38f02d | ||
|
|
1946869604 | ||
|
|
d366b64013 | ||
|
|
e1371ee532 | ||
|
|
2f2f38839a | ||
|
|
d6e669494d | ||
|
|
53b21e92fb | ||
|
|
441e4a0018 | ||
|
|
8dd4995fb2 | ||
|
|
160a1b96e9 | ||
|
|
355f9d7732 | ||
|
|
1776cb4037 |
2
.github/workflows/hassfest.yaml
vendored
2
.github/workflows/hassfest.yaml
vendored
@@ -10,5 +10,5 @@ jobs:
|
||||
validate:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v5"
|
||||
- uses: "actions/checkout@v6"
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
|
||||
12
README.md
12
README.md
@@ -1,3 +1,8 @@
|
||||
[![GitHub Release][releases-shield]][releases]
|
||||
[![GitHub Activity][commits-shield]][commits]
|
||||
[![License][license-shield]](LICENSE)
|
||||
![Project Maintenance][maintenance-shield]
|
||||
|
||||
[](https://www.paypal.me/cyberjunkynl/)
|
||||
[](https://github.com/sponsors/cyberjunky)
|
||||
|
||||
@@ -281,3 +286,10 @@ If you find this library useful for your projects, please consider supporting it
|
||||
- Shows appreciation for hundreds of hours of development
|
||||
|
||||
Every contribution, no matter the size, makes a difference and is greatly appreciated! 🙏
|
||||
|
||||
[releases-shield]: https://img.shields.io/github/release/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
|
||||
[releases]: https://github.com/cyberjunky/home-assistant-garmin_connect/releases
|
||||
[commits-shield]: https://img.shields.io/github/commit-activity/y/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
|
||||
[commits]: https://github.com/cyberjunky/home-assistant-garmin_connect/commits/main
|
||||
[license-shield]: https://img.shields.io/github/license/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
|
||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-cyberjunky-blue.svg?style=for-the-badge
|
||||
|
||||
@@ -39,10 +39,15 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"Migrating Garmin Connect config entry from version %s", entry.version)
|
||||
|
||||
if entry.version == 1:
|
||||
# Check if we need to migrate (old entries have username/password, new ones have token)
|
||||
if CONF_TOKEN not in entry.data and CONF_USERNAME in entry.data and CONF_PASSWORD in entry.data:
|
||||
# Scenario 1: Has USERNAME + PASSWORD but no TOKEN (old auth method)
|
||||
# Migrate to: ID + TOKEN
|
||||
if (
|
||||
CONF_TOKEN not in entry.data
|
||||
and CONF_USERNAME in entry.data
|
||||
and CONF_PASSWORD in entry.data
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Migrating Garmin Connect config entry to token-based authentication")
|
||||
"Migrating Garmin Connect config entry from username/password to token-based authentication")
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -76,10 +81,54 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.error(
|
||||
"Failed to migrate Garmin Connect config entry. "
|
||||
"Please re-add the integration. Error: %s", err
|
||||
"Please re-add the integration. Error: %s",
|
||||
err,
|
||||
)
|
||||
return False
|
||||
|
||||
# Scenario 2: Has USERNAME + TOKEN but no ID (partially migrated)
|
||||
# Migrate to: ID + TOKEN (remove USERNAME)
|
||||
elif (
|
||||
CONF_ID not in entry.data
|
||||
and CONF_USERNAME in entry.data
|
||||
and CONF_TOKEN in entry.data
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Migrating Garmin Connect config entry: converting USERNAME to ID")
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
|
||||
# Create new data with ID instead of USERNAME
|
||||
new_data = {
|
||||
CONF_ID: username,
|
||||
CONF_TOKEN: entry.data[CONF_TOKEN],
|
||||
}
|
||||
|
||||
# Update the config entry
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
_LOGGER.info(
|
||||
"Successfully migrated Garmin Connect config entry from USERNAME to ID")
|
||||
return True
|
||||
|
||||
# Scenario 3: Missing both TOKEN and credentials (incomplete/corrupted)
|
||||
# Add placeholder ID to allow reauth flow
|
||||
elif CONF_TOKEN not in entry.data:
|
||||
if CONF_ID not in entry.data:
|
||||
_LOGGER.info(
|
||||
"Config entry missing CONF_ID, adding placeholder for reauth flow")
|
||||
new_data = {
|
||||
**entry.data,
|
||||
CONF_ID: entry.entry_id, # Use entry_id as fallback
|
||||
}
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
_LOGGER.info(
|
||||
"Garmin Connect config entry is incomplete (missing token). "
|
||||
"Reauthentication will be required to complete setup."
|
||||
)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -152,14 +201,18 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
try:
|
||||
# Check if the token exists in the entry data
|
||||
if CONF_TOKEN not in self.entry.data:
|
||||
_LOGGER.error(
|
||||
"Token not found in config entry. This may be an old config entry that needs migration. "
|
||||
"Please remove and re-add the Garmin Connect integration."
|
||||
_LOGGER.info(
|
||||
"Token not found in config entry. Reauthentication required."
|
||||
)
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Token not found, please re-add the integration")
|
||||
"Token not found in config entry. This may be an old or incomplete configuration. "
|
||||
"A reauthentication flow will be initiated. Please check your notifications."
|
||||
)
|
||||
|
||||
await self.hass.async_add_executor_job(self.api.login, self.entry.data[CONF_TOKEN])
|
||||
except ConfigEntryAuthFailed:
|
||||
# Re-raise ConfigEntryAuthFailed without logging as "unknown error"
|
||||
raise
|
||||
except GarminConnectAuthenticationError as err:
|
||||
_LOGGER.error(
|
||||
"Authentication error occurred during login: %s", err.response.text)
|
||||
@@ -332,10 +385,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
# Gear data
|
||||
try:
|
||||
# Check if userProfileId exists in summary before fetching gear data
|
||||
if Gear.USERPROFILE_ID in summary:
|
||||
gear = await self.hass.async_add_executor_job(
|
||||
self.api.get_gear, summary[Gear.USERPROFILE_ID]
|
||||
)
|
||||
_LOGGER.debug("Gear data fetched: %s", gear)
|
||||
else:
|
||||
_LOGGER.debug("No userProfileId found in summary, skipping gear data fetch")
|
||||
|
||||
# Fitness age data
|
||||
fitnessage_data = await self.hass.async_add_executor_job(
|
||||
@@ -385,15 +442,8 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
return {}
|
||||
|
||||
try:
|
||||
# Gear data like shoes, bike, etc.
|
||||
gear = await self.hass.async_add_executor_job(
|
||||
self.api.get_gear, summary[Gear.USERPROFILE_ID]
|
||||
)
|
||||
# Use gear data from the first fetch if available
|
||||
if gear:
|
||||
_LOGGER.debug("Gear data fetched: %s", gear)
|
||||
else:
|
||||
_LOGGER.debug("No gear data found")
|
||||
|
||||
# Gear stats data like distance, time, etc.
|
||||
tasks: list[Awaitable] = [
|
||||
self.hass.async_add_executor_job(
|
||||
@@ -408,6 +458,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER.debug("No gear statistics data found")
|
||||
|
||||
# Gear defaults data like shoe, bike, etc.
|
||||
if Gear.USERPROFILE_ID in summary:
|
||||
gear_defaults = await self.hass.async_add_executor_job(
|
||||
self.api.get_gear_defaults, summary[Gear.USERPROFILE_ID]
|
||||
)
|
||||
@@ -415,6 +466,10 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER.debug("Gear defaults data fetched: %s", gear_defaults)
|
||||
else:
|
||||
_LOGGER.debug("No gear defaults data found")
|
||||
else:
|
||||
_LOGGER.debug("No userProfileId found in summary, skipping gear defaults fetch")
|
||||
else:
|
||||
_LOGGER.debug("No gear data available, skipping gear stats and defaults fetch")
|
||||
except GarminConnectAuthenticationError as err:
|
||||
_LOGGER.error(
|
||||
"Authentication error occurred while fetching Gear data: %s", err.response.text)
|
||||
@@ -431,9 +486,12 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if err.response.status_code == 401:
|
||||
_LOGGER.error(
|
||||
"Authentication error while fetching Gear data: %s", err.response.text)
|
||||
elif err.response.status_code == 403:
|
||||
_LOGGER.debug(
|
||||
"Access forbidden while fetching Gear data (user may not have gear configured): %s", err.response.text)
|
||||
elif err.response.status_code == 404:
|
||||
_LOGGER.error(
|
||||
"URL not found error while fetching Gear data: %s", err.response.text)
|
||||
_LOGGER.debug(
|
||||
"Gear data not found (user may not have gear configured): %s", err.response.text)
|
||||
elif err.response.status_code == 429:
|
||||
_LOGGER.error(
|
||||
"Too many requests error while fetching Gear data: %s", err.response.text)
|
||||
|
||||
@@ -126,13 +126,15 @@ class GarminConnectConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
config_data = {
|
||||
CONF_ID: self._username,
|
||||
CONF_USERNAME: self._username,
|
||||
CONF_TOKEN: self._api.garth.dumps(),
|
||||
}
|
||||
existing_entry = await self.async_set_unique_id(self._username)
|
||||
|
||||
if existing_entry:
|
||||
return self.async_update_reload_and_abort(existing_entry, data=config_data)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data=config_data)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=cast(str, self._username), data=config_data
|
||||
@@ -180,9 +182,11 @@ class GarminConnectConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
Start the reauthorization process using existing configuration entry data.
|
||||
|
||||
Extracts the username from the entry data and advances to the reauthorization confirmation step.
|
||||
Extracts the username from the entry data (using CONF_ID if CONF_USERNAME is not available for migrated entries) and advances to the reauthorization confirmation step.
|
||||
"""
|
||||
self._username = entry_data[CONF_USERNAME]
|
||||
# For backward compatibility: try CONF_USERNAME first, fall back to CONF_ID
|
||||
self._username = entry_data.get(
|
||||
CONF_USERNAME) or entry_data.get(CONF_ID)
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
|
||||
"requirements": ["garminconnect>=0.2.31"],
|
||||
"version": "0.2.33"
|
||||
"version": "0.2.38"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
homeassistant==2025.1.4
|
||||
homeassistant==2025.12.4
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
--requirement requirements_base.txt
|
||||
codespell==2.4.1
|
||||
isort==7.0.0
|
||||
pre-commit==4.3.0
|
||||
pre-commit==4.5.1
|
||||
pre-commit-hooks==6.0.0
|
||||
pyupgrade==3.21.0
|
||||
ruff==0.14.3
|
||||
pyupgrade==3.21.2
|
||||
ruff==0.14.10
|
||||
vulture==2.14
|
||||
|
||||
Reference in New Issue
Block a user