From 359eaea923545f07ad4b00cf47ffe347b19215dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Paris?= Date: Thu, 30 Oct 2025 14:22:35 +0000 Subject: [PATCH] endurance score: fix bug when garmin api returns blank --- custom_components/garmin_connect/__init__.py | 38 +++++++++++--- custom_components/garmin_connect/sensor.py | 52 +++++++++++++++----- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/custom_components/garmin_connect/__init__.py b/custom_components/garmin_connect/__init__.py index da0a261..9132fc7 100644 --- a/custom_components/garmin_connect/__init__.py +++ b/custom_components/garmin_connect/__init__.py @@ -12,12 +12,13 @@ from garminconnect import ( GarminConnectConnectionError, GarminConnectTooManyRequestsError, ) +import requests + 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.update_coordinator import DataUpdateCoordinator, UpdateFailed -import requests from .const import ( DATA_COORDINATOR, @@ -76,9 +77,13 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): self.time_zone = self.hass.config.time_zone _LOGGER.debug("Time zone: %s", self.time_zone) - self.api = Garmin(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], self.in_china) + self.api = Garmin( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], self.in_china + ) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL) + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL + ) async def async_login(self) -> bool: """Login to Garmin Connect.""" @@ -91,10 +96,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER.error("Error occurred during Garmin Connect login request: %s", err) return False except GarminConnectConnectionError as err: - _LOGGER.error("Connection error occurred during Garmin Connect login request: %s", err) + _LOGGER.error( + "Connection error occurred during Garmin Connect login request: %s", err + ) raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error occurred during Garmin Connect login request") + _LOGGER.exception( + "Unknown error occurred during Garmin Connect login request" + ) return False return True @@ -115,6 +124,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): hrv_data = {} hrv_status = {"status": "unknown"} endurance_data = {} + endurance_status = {"overallScore": None} next_alarms = [] today = datetime.now(ZoneInfo(self.time_zone)).date() @@ -168,7 +178,9 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): next_alarms = calculate_next_active_alarms(alarms, self.time_zone) # Activity types - activity_types = await self.hass.async_add_executor_job(self.api.get_activity_types) + activity_types = await self.hass.async_add_executor_job( + self.api.get_activity_types + ) _LOGGER.debug("Activity types data fetched: %s", activity_types) # Sleep data @@ -215,7 +227,9 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): # Gear stats data try: tasks: list[Awaitable] = [ - self.hass.async_add_executor_job(self.api.get_gear_stats, gear_item[Gear.UUID]) + self.hass.async_add_executor_job( + self.api.get_gear_stats, gear_item[Gear.UUID] + ) for gear_item in gear ] gear_stats = await asyncio.gather(*tasks) @@ -251,6 +265,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): except KeyError: _LOGGER.debug("HRV data is not available") + # Endurance status + try: + if endurance_data and "overallScore" in endurance_data: + endurance_status = endurance_data + _LOGGER.debug("Endurance score: %s", endurance_status) + except KeyError: + _LOGGER.debug("Endurance data is not available") + return { **summary, **body["totalAverage"], @@ -262,7 +284,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): "sleepScore": sleep_score, "sleepTimeSeconds": sleep_time_seconds, "hrvStatus": hrv_status, - "enduranceScore": endurance_data, + "enduranceScore": endurance_status, } diff --git a/custom_components/garmin_connect/sensor.py b/custom_components/garmin_connect/sensor.py index 05e2af7..82ffe21 100644 --- a/custom_components/garmin_connect/sensor.py +++ b/custom_components/garmin_connect/sensor.py @@ -7,6 +7,8 @@ import logging from numbers import Number from zoneinfo import ZoneInfo +import voluptuous as vol + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -22,7 +24,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -import voluptuous as vol from .const import ( DATA_COORDINATOR, @@ -36,9 +37,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: """Set up Garmin Connect sensor based on a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[GARMIN_DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: DataUpdateCoordinator = hass.data[GARMIN_DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] unique_id = entry.data[CONF_ID] entities = [] @@ -287,7 +292,11 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self.coordinator.data and self._type in self.coordinator.data + return ( + super().available + and self.coordinator.data + and self._type in self.coordinator.data + ) async def add_body_composition(self, **kwargs): """Handle the service call to add body composition.""" @@ -307,7 +316,9 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): """Check for login.""" if not await self.coordinator.async_login(): - raise IntegrationError("Failed to login to Garmin Connect, unable to update") + raise IntegrationError( + "Failed to login to Garmin Connect, unable to update" + ) """Record a weigh in/body composition.""" await self.hass.async_add_executor_job( @@ -337,11 +348,18 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): """Check for login.""" if not await self.coordinator.async_login(): - raise IntegrationError("Failed to login to Garmin Connect, unable to update") + raise IntegrationError( + "Failed to login to Garmin Connect, unable to update" + ) """Record a blood pressure measurement.""" await self.hass.async_add_executor_job( - self.coordinator.api.set_blood_pressure, systolic, diastolic, pulse, timestamp, notes + self.coordinator.api.set_blood_pressure, + systolic, + diastolic, + pulse, + timestamp, + notes, ) @@ -399,7 +417,9 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): stats = self._stats() gear_defaults = self._gear_defaults() activity_types = self.coordinator.data["activityTypes"] - default_for_activity = self._activity_names_for_gear_defaults(gear_defaults, activity_types) + default_for_activity = self._activity_names_for_gear_defaults( + gear_defaults, activity_types + ) if not self.coordinator.data or not gear or not stats: return {} @@ -427,7 +447,9 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): def _activity_names_for_gear_defaults(self, gear_defaults, activity_types): """Get activity names for gear defaults.""" activity_type_ids = [d["activityTypePk"] for d in gear_defaults] - return [a["typeKey"] for a in activity_types if a["typeId"] in activity_type_ids] + return [ + a["typeKey"] for a in activity_types if a["typeId"] in activity_type_ids + ] @property def device_info(self) -> DeviceInfo: @@ -476,7 +498,9 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): """Check for login.""" if not await self.coordinator.async_login(): - raise IntegrationError("Failed to login to Garmin Connect, unable to update") + raise IntegrationError( + "Failed to login to Garmin Connect, unable to update" + ) """Update Garmin Gear settings.""" activity_type_id = next( @@ -494,7 +518,8 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): ) else: old_default_state = await self.hass.async_add_executor_job( - self.coordinator.api.get_gear_defaults, self.coordinator.data[Gear.USERPROFILE_ID] + self.coordinator.api.get_gear_defaults, + self.coordinator.data[Gear.USERPROFILE_ID], ) to_deactivate = list( filter( @@ -512,5 +537,8 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): False, ) await self.hass.async_add_executor_job( - self.coordinator.api.set_gear_default, activity_type_id, self._uuid, True + self.coordinator.api.set_gear_default, + activity_type_id, + self._uuid, + True, )