Skip to content

Commit

Permalink
Added User Points, User Level and Last Activity sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberjunky committed Jan 21, 2025
1 parent 06ee9e1 commit dfc4b5c
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 23 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Disabled by default:

```text
Badges
User Points
User Level
Consumed KiloCalories
Remaining KiloCalories
Net Remaining KiloCalories
Expand Down Expand Up @@ -134,6 +136,8 @@ Muscle Mass
Physique Rating
Visceral Fat
Metabolic Age
Last Activities
Last Activity
```

## Screenshots
Expand Down
41 changes: 35 additions & 6 deletions custom_components/garmin_connect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
DAY_TO_NUMBER,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
LEVEL_POINTS,
Gear,
)

Expand Down Expand Up @@ -106,6 +107,7 @@ async def _async_update_data(self) -> dict:
gear_stats = {}
gear_defaults = {}
activity_types = {}
last_activities = []
sleep_data = {}
sleep_score = None
sleep_time_seconds = None
Expand All @@ -116,41 +118,64 @@ async def _async_update_data(self) -> dict:
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()
)
Expand All @@ -164,6 +189,7 @@ async def _async_update_data(self) -> dict:
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]
Expand All @@ -184,18 +210,21 @@ async def _async_update_data(self) -> dict:
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"]
Expand All @@ -208,9 +237,9 @@ async def _async_update_data(self) -> dict:
**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,
Expand Down
10 changes: 10 additions & 0 deletions custom_components/garmin_connect/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,17 @@
],
"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,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/garmin_connect/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
30 changes: 16 additions & 14 deletions custom_components/garmin_connect/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ def native_value(self):
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()

Expand Down Expand Up @@ -237,12 +240,15 @@ def extra_state_attributes(self):
}

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":
Expand Down Expand Up @@ -291,8 +297,7 @@ async def add_body_composition(self, **kwargs):

"""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(
Expand Down Expand Up @@ -322,8 +327,7 @@ async def add_blood_pressure(self, **kwargs):

"""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(
Expand Down Expand Up @@ -384,9 +388,8 @@ def extra_state_attributes(self):
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 {}
Expand Down Expand Up @@ -437,7 +440,7 @@ def available(self) -> bool:

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

Expand All @@ -452,7 +455,7 @@ def _gear_defaults(self):
return list(
filter(
lambda d: d[Gear.UUID] == self.uuid and d["defaultGear"] is True,
self.coordinator.data["gear_defaults"],
self.coordinator.data["gearDefaults"],
)
)

Expand All @@ -463,14 +466,13 @@ async def set_active_gear(self, **kwargs):

"""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:
Expand Down
2 changes: 1 addition & 1 deletion requirements_base.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
colorlog==6.9.0
setuptools==75.8.0
setuptools==75.8.0
2 changes: 1 addition & 1 deletion requirements_core_min.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# homeassistant==2024.4.1
homeassistant==2025.1.2
homeassistant==2025.1.2

0 comments on commit dfc4b5c

Please sign in to comment.