diff --git a/custom_components/garmin_connect/__init__.py b/custom_components/garmin_connect/__init__.py index 840a33e..5bd2424 100644 --- a/custom_components/garmin_connect/__init__.py +++ b/custom_components/garmin_connect/__init__.py @@ -12,6 +12,8 @@ from garminconnect import ( GarminConnectConnectionError, GarminConnectTooManyRequestsError, ) +import requests + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant @@ -113,6 +115,13 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): return False except GarminConnectConnectionError as 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" + ) "Connection error occurred during login: %s", err) raise ConfigEntryNotReady from err except requests.exceptions.HTTPError as err: @@ -160,6 +169,8 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): sleep_time_seconds = None hrv_data = {} hrv_status = {"status": "unknown"} + endurance_data = {} + endurance_status = {"overallScore": None} next_alarms = [] today = datetime.now(ZoneInfo(self.time_zone)).date() @@ -257,6 +268,27 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): hrv_data = await self.hass.async_add_executor_job( self.api.get_hrv_data, today.isoformat() ) + _LOGGER.debug("HRV data fetched: %s", hrv_data) + + # Endurance data + endurance_data = await self.hass.async_add_executor_job( + self.api.get_endurance_score, today.isoformat() + ) + _LOGGER.debug("Endurance data fetched: %s", endurance_data) + + except ( + GarminConnectAuthenticationError, + GarminConnectTooManyRequestsError, + GarminConnectConnectionError, + ) as error: + _LOGGER.debug("Trying to relogin to Garmin Connect") + if not await self.async_login(): + raise UpdateFailed(error) from error + + # Gear data + try: + gear = await self.hass.async_add_executor_job( + self.api.get_gear, summary[Gear.USERPROFILE_ID] if hrv_data: _LOGGER.debug("HRV data fetched: %s", hrv_data) else: @@ -322,7 +354,8 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): # Gear stats data like distance, time, etc. tasks: list[Awaitable] = [ self.hass.async_add_executor_job( - self.api.get_gear_stats, gear_item[Gear.UUID]) + self.api.get_gear_stats, gear_item[Gear.UUID] + ) for gear_item in gear ] gear_stats = await asyncio.gather(*tasks) @@ -394,6 +427,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER.debug( "Error occurred while processing HRV summary status data") + # 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"], @@ -405,6 +446,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): "sleepScore": sleep_score, "sleepTimeSeconds": sleep_time_seconds, "hrvStatus": hrv_status, + "enduranceScore": endurance_status, **fitnessage_data, **hydration_data, } diff --git a/custom_components/garmin_connect/const.py b/custom_components/garmin_connect/const.py index f797142..aaaa533 100644 --- a/custom_components/garmin_connect/const.py +++ b/custom_components/garmin_connect/const.py @@ -657,6 +657,13 @@ GARMIN_ENTITY_LIST = { None, True, ], + "enduranceScore": [ + "Endurance Score", + None, + "mdi:run", + None, + SensorStateClass.MEASUREMENT, + False, "chronologicalAge": [ "Chronological Age", UnitOfTime.YEARS, diff --git a/custom_components/garmin_connect/sensor.py b/custom_components/garmin_connect/sensor.py index 250474a..1de6c08 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 = [] @@ -208,6 +213,9 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): elif self._type == "hrvStatus": value = self.coordinator.data[self._type]["status"].capitalize() + elif self._type == "enduranceScore": + value = self.coordinator.data[self._type]["overallScore"] + elif "Duration" in self._type or "Seconds" in self._type: value = round(value // 60, 2) @@ -270,6 +278,10 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): attributes = {**attributes, **self.coordinator.data[self._type]} del attributes["status"] + if self._type == "enduranceScore": + attributes = {**attributes, **self.coordinator.data[self._type]} + del attributes["overallScore"] + return attributes @property @@ -288,10 +300,12 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: - """ - Indicates whether the sensor entity is available based on coordinator data presence and the sensor type. - """ - return super().available and self.coordinator.data and self._type in self.coordinator.data + """Return True if entity is available.""" + return ( + super().available + and self.coordinator.data + and self._type in self.coordinator.data + ) async def add_body_composition(self, **kwargs): """ @@ -319,7 +333,8 @@ 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") + "Failed to login to Garmin Connect, unable to update" + ) """Record a weigh in/body composition.""" await self.hass.async_add_executor_job( @@ -362,11 +377,17 @@ 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") + "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, ) @@ -429,7 +450,8 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): gear_defaults = self._gear_defaults() activity_types = self.coordinator.data["activityTypes"] default_for_activity = self._activity_names_for_gear_defaults( - gear_defaults, activity_types) + gear_defaults, activity_types + ) if not self.coordinator.data or not gear or not stats: return {} @@ -457,7 +479,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: @@ -521,7 +545,8 @@ 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") + "Failed to login to Garmin Connect, unable to update" + ) """Update Garmin Gear settings.""" activity_type_id = next( @@ -539,7 +564,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( @@ -557,5 +583,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, )