diff --git a/README.md b/README.md index 8aac28f..a95f5ee 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Disabled by default: ```text Badges +User Points +User Level Consumed KiloCalories Remaining KiloCalories Net Remaining KiloCalories @@ -134,6 +136,8 @@ Muscle Mass Physique Rating Visceral Fat Metabolic Age +Last Activities +Last Activity ``` ## Screenshots diff --git a/custom_components/garmin_connect/__init__.py b/custom_components/garmin_connect/__init__.py index 39166fe..eb0b95c 100644 --- a/custom_components/garmin_connect/__init__.py +++ b/custom_components/garmin_connect/__init__.py @@ -23,6 +23,7 @@ from .const import ( DAY_TO_NUMBER, DEFAULT_UPDATE_INTERVAL, DOMAIN, + LEVEL_POINTS, Gear, ) @@ -106,6 +107,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): gear_stats = {} gear_defaults = {} activity_types = {} + last_activities = [] sleep_data = {} sleep_score = None sleep_time_seconds = None @@ -116,41 +118,64 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): today = datetime.now(ZoneInfo(self.time_zone)).date() try: + # User summary summary = await self.hass.async_add_executor_job( self.api.get_user_summary, today.isoformat() ) _LOGGER.debug("Summary data fetched: %s", summary) + # Body composition body = await self.hass.async_add_executor_job( self.api.get_body_composition, today.isoformat() ) _LOGGER.debug("Body data fetched: %s", body) - activities = await self.hass.async_add_executor_job( + # Last activities + last_activities = await self.hass.async_add_executor_job( self.api.get_activities_by_date, (today - timedelta(days=7)).isoformat(), (today + timedelta(days=1)).isoformat(), ) - _LOGGER.debug("Activities data fetched: %s", activities) - summary["lastActivities"] = activities + _LOGGER.debug("Activities data fetched: %s", last_activities) + summary["lastActivities"] = last_activities + summary["lastActivity"] = last_activities[0] if last_activities else {} + # Badges badges = await self.hass.async_add_executor_job(self.api.get_earned_badges) _LOGGER.debug("Badges data fetched: %s", badges) summary["badges"] = badges + # Calculate user points and user level + user_points = 0 + for badge in badges: + user_points += badge["badgePoints"] * badge["badgeEarnedNumber"] + + summary["userPoints"] = user_points + + user_level = 0 + for level, points in LEVEL_POINTS.items(): + if user_points >= points: + user_level = level + + summary["userLevel"] = user_level + + # Alarms alarms = await self.hass.async_add_executor_job(self.api.get_device_alarms) _LOGGER.debug("Alarms data fetched: %s", alarms) 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) _LOGGER.debug("Activity types data fetched: %s", activity_types) + # Sleep data sleep_data = await self.hass.async_add_executor_job( self.api.get_sleep_data, today.isoformat() ) _LOGGER.debug("Sleep data fetched: %s", sleep_data) + # HRV data hrv_data = await self.hass.async_add_executor_job( self.api.get_hrv_data, today.isoformat() ) @@ -164,6 +189,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): 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] @@ -184,18 +210,21 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): except (KeyError, TypeError, ValueError, ConnectionError) as err: _LOGGER.debug("Gear data is not available: %s", err) + # Sleep score data try: sleep_score = sleep_data["dailySleepDTO"]["sleepScores"]["overall"]["value"] _LOGGER.debug("Sleep score data: %s", sleep_score) except KeyError: _LOGGER.debug("Sleep score data is not available") + # Sleep time seconds data try: sleep_time_seconds = sleep_data["dailySleepDTO"]["sleepTimeSeconds"] _LOGGER.debug("Sleep time seconds data: %s", sleep_time_seconds) except KeyError: _LOGGER.debug("Sleep time seconds data is not available") + # HRV data try: if hrv_data and "hrvSummary" in hrv_data: hrv_status = hrv_data["hrvSummary"] @@ -208,9 +237,9 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): **body["totalAverage"], "nextAlarm": next_alarms, "gear": gear, - "gear_stats": gear_stats, - "activity_types": activity_types, - "gear_defaults": gear_defaults, + "gearStats": gear_stats, + "activityTypes": activity_types, + "gearDefaults": gear_defaults, "sleepScore": sleep_score, "sleepTimeSeconds": sleep_time_seconds, "hrvStatus": hrv_status, diff --git a/custom_components/garmin_connect/const.py b/custom_components/garmin_connect/const.py index f6ac84b..36efc82 100644 --- a/custom_components/garmin_connect/const.py +++ b/custom_components/garmin_connect/const.py @@ -622,7 +622,17 @@ GARMIN_ENTITY_LIST = { ], "nextAlarm": ["Next Alarm Time", None, "mdi:alarm", SensorDeviceClass.TIMESTAMP, None, True], "lastActivities": ["Last Activities", None, "mdi:numeric", None, SensorStateClass.TOTAL, False], + "lastActivity": ["Last Activity", None, "mdi:walk", None, None, False], "badges": ["Badges", None, "mdi:medal", None, SensorStateClass.TOTAL, False], + "userPoints": ["User Points", None, "mdi:counter", None, SensorStateClass.TOTAL, False], + "userLevel": [ + "User Level", + None, + "mdi:star-four-points-circle", + None, + SensorStateClass.TOTAL, + False, + ], "sleepScore": [ "Sleep Score", None, diff --git a/custom_components/garmin_connect/manifest.json b/custom_components/garmin_connect/manifest.json index f3aade9..3d6dac7 100644 --- a/custom_components/garmin_connect/manifest.json +++ b/custom_components/garmin_connect/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues", "requirements": ["garminconnect>=0.2.24"], - "version": "0.2.28" + "version": "0.2.29" } diff --git a/custom_components/garmin_connect/sensor.py b/custom_components/garmin_connect/sensor.py index 60e2dac..6b6c86e 100644 --- a/custom_components/garmin_connect/sensor.py +++ b/custom_components/garmin_connect/sensor.py @@ -198,6 +198,9 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): if self._type == "lastActivities" or self._type == "badges": value = len(self.coordinator.data[self._type]) + if self._type == "lastActivity": + value = self.coordinator.data[self._type]["activityName"] + elif self._type == "hrvStatus": value = self.coordinator.data[self._type]["status"].capitalize() @@ -237,12 +240,15 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity): } if self._type == "lastActivities": - attributes["last_Activities"] = self.coordinator.data[self._type] + attributes["last_activities"] = self.coordinator.data[self._type] + + if self._type == "lastActivity": + attributes = {**attributes, **self.coordinator.data[self._type]} # Only show the last 10 badges for performance reasons if self._type == "badges": badges = self.coordinator.data.get(self._type, []) - sorted_badges = sorted(badges, key=lambda x: x['badgeEarnedDate']) + sorted_badges = sorted(badges, key=lambda x: x["badgeEarnedDate"]) attributes["badges"] = sorted_badges[-10:] if self._type == "nextAlarm": @@ -291,8 +297,7 @@ 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( @@ -322,8 +327,7 @@ 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( @@ -384,9 +388,8 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): gear = self._gear() stats = self._stats() gear_defaults = self._gear_defaults() - activity_types = self.coordinator.data["activity_types"] - default_for_activity = self._activity_names_for_gear_defaults( - gear_defaults, activity_types) + activity_types = self.coordinator.data["activityTypes"] + 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 {} @@ -437,7 +440,7 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): def _stats(self): """Get gear statistics from garmin""" - for gear_stats_item in self.coordinator.data["gear_stats"]: + for gear_stats_item in self.coordinator.data["gearStats"]: if gear_stats_item[Gear.UUID] == self._uuid: return gear_stats_item @@ -452,7 +455,7 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity): return list( filter( lambda d: d[Gear.UUID] == self.uuid and d["defaultGear"] is True, - self.coordinator.data["gear_defaults"], + self.coordinator.data["gearDefaults"], ) ) @@ -463,14 +466,13 @@ 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( filter( lambda a: a[Gear.TYPE_KEY] == activity_type, - self.coordinator.data["activity_types"], + self.coordinator.data["activityTypes"], ) )[Gear.TYPE_ID] if setting != ServiceSetting.ONLY_THIS_AS_DEFAULT: diff --git a/requirements_base.txt b/requirements_base.txt index 8735d02..3fe5c76 100644 --- a/requirements_base.txt +++ b/requirements_base.txt @@ -1,2 +1,2 @@ colorlog==6.9.0 -setuptools==75.8.0 \ No newline at end of file +setuptools==75.8.0 diff --git a/requirements_core_min.txt b/requirements_core_min.txt index b74b768..98ea4bc 100644 --- a/requirements_core_min.txt +++ b/requirements_core_min.txt @@ -1,2 +1,2 @@ # homeassistant==2024.4.1 -homeassistant==2025.1.2 \ No newline at end of file +homeassistant==2025.1.2