From 10cc9608b939f4e382bf669e45d50a52edb0634c Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Fri, 2 Jan 2026 19:09:03 +0100 Subject: [PATCH] Added HRV and weekly steps sensors --- README.md | 9 +- .../garmin_connect/coordinator.py | 64 ++++++++++- .../garmin_connect/sensor_descriptions.py | 104 ++++++++++++++++++ .../garmin_connect/translations/en.json | 36 ++++++ docs/garmin_connect.markdown | 10 +- 5 files changed, 218 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 200b3ad..9e076f1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # Garmin Connect -Integrate your Garmin Connect fitness data with Home Assistant. Access **100+ sensors** covering health metrics, activities, body composition, and gear tracking—plus add measurements directly to Garmin Connect via services. +Integrate your Garmin Connect fitness data with Home Assistant. Access **110+ sensors** covering health metrics, activities, body composition, and gear tracking—plus add measurements directly to Garmin Connect via services. ## Supported Features @@ -29,6 +29,8 @@ Sensor values depend on your Garmin devices and connected apps. |--------|-------------| | Total Steps | Daily step count | | Daily Step Goal | Your configured step target | +| Yesterday Steps/Distance | Previous day's complete totals | +| Weekly Step/Distance Avg | 7-day averages | | Total Distance | Distance walked/run in meters | | Floors Ascended/Descended | Floors climbed | @@ -46,6 +48,8 @@ Sensor values depend on your Garmin devices and connected apps. | Resting Heart Rate | Daily resting HR | | Min/Max Heart Rate | Daily HR range | | Last 7 Days Avg HR | Weekly average | +| HRV Weekly/Nightly Avg | Heart rate variability metrics | +| HRV Baseline | Personal HRV baseline | ### Stress & Recovery @@ -60,6 +64,9 @@ Sensor values depend on your Garmin devices and connected apps. |--------|-------------| | Sleep Score | Overall sleep quality score | | Sleep/Awake Duration | Time asleep and awake | +| Deep Sleep | Time in deep sleep | +| Light Sleep | Time in light sleep | +| REM Sleep | Time in REM sleep | ### Body Battery diff --git a/custom_components/garmin_connect/coordinator.py b/custom_components/garmin_connect/coordinator.py index 67c23b1..98d341c 100644 --- a/custom_components/garmin_connect/coordinator.py +++ b/custom_components/garmin_connect/coordinator.py @@ -96,9 +96,18 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): gear_defaults = {} activity_types = {} last_activities = [] + daily_steps: list[dict[str, Any]] = [] + yesterday_steps = None + yesterday_distance = None + weekly_step_avg = None + weekly_distance_avg = None sleep_data = {} sleep_score = None sleep_time_seconds = None + deep_sleep_seconds = None + light_sleep_seconds = None + rem_sleep_seconds = None + awake_sleep_seconds = None hrv_data = {} hrv_status = {"status": "unknown"} endurance_data = {} @@ -113,6 +122,29 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): self.api.get_user_summary, today.isoformat() ) + # Fetch last 7 days steps for weekly average and yesterday's final count + week_ago = (today - timedelta(days=7)).isoformat() + yesterday = (today - timedelta(days=1)).isoformat() + daily_steps = await self.hass.async_add_executor_job( + self.api.get_daily_steps, week_ago, yesterday + ) + + # Process daily steps for yesterday values and weekly averages + if daily_steps: + # Yesterday is the last item in the list + if daily_steps: + yesterday_data = daily_steps[-1] + yesterday_steps = yesterday_data.get("totalSteps") + yesterday_distance = yesterday_data.get("totalDistance") + + # Calculate weekly averages + total_steps = sum(d.get("totalSteps", 0) for d in daily_steps) + total_distance = sum(d.get("totalDistance", 0) for d in daily_steps) + days_count = len(daily_steps) + if days_count > 0: + weekly_step_avg = round(total_steps / days_count) + weekly_distance_avg = round(total_distance / days_count) + body = await self.hass.async_add_executor_job( self.api.get_body_composition, today.isoformat() ) @@ -163,13 +195,11 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): menstrual_data = await self.hass.async_add_executor_job( self.api.get_menstrual_data_for_date, today.isoformat() ) - _LOGGER.debug("Menstrual data: %s", menstrual_data) # API returns None when not enabled - convert to empty dict if menstrual_data is None: menstrual_data = {} - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except # Menstrual data not available for this user - _LOGGER.debug("Menstrual data error: %s", err) menstrual_data = {} except ( @@ -247,6 +277,26 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): except KeyError: pass + try: + deep_sleep_seconds = sleep_data["dailySleepDTO"]["deepSleepSeconds"] + except KeyError: + pass + + try: + light_sleep_seconds = sleep_data["dailySleepDTO"]["lightSleepSeconds"] + except KeyError: + pass + + try: + rem_sleep_seconds = sleep_data["dailySleepDTO"]["remSleepSeconds"] + except KeyError: + pass + + try: + awake_sleep_seconds = sleep_data["dailySleepDTO"]["awakeSleepSeconds"] + except KeyError: + pass + try: if hrv_data and "hrvSummary" in hrv_data: hrv_status = hrv_data["hrvSummary"] @@ -269,6 +319,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): "gearDefaults": gear_defaults, "sleepScore": sleep_score, "sleepTimeSeconds": sleep_time_seconds, + "deepSleepSeconds": deep_sleep_seconds, + "lightSleepSeconds": light_sleep_seconds, + "remSleepSeconds": rem_sleep_seconds, + "awakeSleepSeconds": awake_sleep_seconds, + "yesterdaySteps": yesterday_steps, + "yesterdayDistance": yesterday_distance, + "weeklyStepAvg": weekly_step_avg, + "weeklyDistanceAvg": weekly_distance_avg, "hrvStatus": hrv_status, "enduranceScore": endurance_status, **fitnessage_data, diff --git a/custom_components/garmin_connect/sensor_descriptions.py b/custom_components/garmin_connect/sensor_descriptions.py index d859bc3..6ffae42 100644 --- a/custom_components/garmin_connect/sensor_descriptions.py +++ b/custom_components/garmin_connect/sensor_descriptions.py @@ -49,6 +49,36 @@ ACTIVITY_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( native_unit_of_measurement="steps", icon="mdi:target", ), + GarminConnectSensorEntityDescription( + key="yesterdaySteps", + translation_key="yesterday_steps", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="steps", + icon="mdi:walk", + ), + GarminConnectSensorEntityDescription( + key="weeklyStepAvg", + translation_key="weekly_step_avg", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="steps", + icon="mdi:chart-line", + ), + GarminConnectSensorEntityDescription( + key="yesterdayDistance", + translation_key="yesterday_distance", + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfLength.METERS, + icon="mdi:map-marker-distance", + ), + GarminConnectSensorEntityDescription( + key="weeklyDistanceAvg", + translation_key="weekly_distance_avg", + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.METERS, + icon="mdi:chart-line", + ), GarminConnectSensorEntityDescription( key="totalDistanceMeters", translation_key="total_distance", @@ -168,6 +198,40 @@ HEART_RATE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( attributes_fn=lambda data: {k: v for k, v in data.get( "hrvStatus", {}).items() if k != "status"} if data.get("hrvStatus") else {}, ), + GarminConnectSensorEntityDescription( + key="hrvWeeklyAvg", + translation_key="hrv_weekly_avg", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="ms", + icon="mdi:heart-pulse", + value_fn=lambda data: data.get("hrvStatus", {}).get("weeklyAvg"), + ), + GarminConnectSensorEntityDescription( + key="hrvLastNightAvg", + translation_key="hrv_last_night_avg", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="ms", + icon="mdi:heart-pulse", + value_fn=lambda data: data.get("hrvStatus", {}).get("lastNightAvg"), + ), + GarminConnectSensorEntityDescription( + key="hrvLastNight5MinHigh", + translation_key="hrv_last_night_5min_high", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="ms", + icon="mdi:heart-pulse", + value_fn=lambda data: data.get("hrvStatus", {}).get("lastNight5MinHigh"), + ), + GarminConnectSensorEntityDescription( + key="hrvBaseline", + translation_key="hrv_baseline", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="ms", + icon="mdi:heart-pulse", + value_fn=lambda data: data.get("hrvStatus", {}).get("baseline", {}).get( + "lowUpper") if data.get("hrvStatus", {}).get("baseline") else None, + attributes_fn=lambda data: data.get("hrvStatus", {}).get("baseline", {}), + ), ) # Stress Sensors @@ -304,6 +368,46 @@ SLEEP_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, icon="mdi:sleep", ), + GarminConnectSensorEntityDescription( + key="deepSleepSeconds", + translation_key="deep_sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + value_fn=lambda data: round(data.get( + "deepSleepSeconds", 0) / 60, 2) if data.get("deepSleepSeconds") else None, + ), + GarminConnectSensorEntityDescription( + key="lightSleepSeconds", + translation_key="light_sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + value_fn=lambda data: round(data.get( + "lightSleepSeconds", 0) / 60, 2) if data.get("lightSleepSeconds") else None, + ), + GarminConnectSensorEntityDescription( + key="remSleepSeconds", + translation_key="rem_sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + value_fn=lambda data: round(data.get( + "remSleepSeconds", 0) / 60, 2) if data.get("remSleepSeconds") else None, + ), + GarminConnectSensorEntityDescription( + key="awakeSleepSeconds", + translation_key="awake_sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep-off", + value_fn=lambda data: round(data.get( + "awakeSleepSeconds", 0) / 60, 2) if data.get("awakeSleepSeconds") else None, + ), ) # Body Battery Sensors diff --git a/custom_components/garmin_connect/translations/en.json b/custom_components/garmin_connect/translations/en.json index e7d192c..13c6d7b 100644 --- a/custom_components/garmin_connect/translations/en.json +++ b/custom_components/garmin_connect/translations/en.json @@ -43,6 +43,18 @@ "daily_step_goal": { "name": "Daily step goal" }, + "yesterday_steps": { + "name": "Yesterday steps" + }, + "weekly_step_avg": { + "name": "Weekly step average" + }, + "yesterday_distance": { + "name": "Yesterday distance" + }, + "weekly_distance_avg": { + "name": "Weekly distance average" + }, "total_distance": { "name": "Total distance" }, @@ -103,6 +115,18 @@ "hrv_status": { "name": "HRV status" }, + "hrv_weekly_avg": { + "name": "HRV weekly average" + }, + "hrv_last_night_avg": { + "name": "HRV last night average" + }, + "hrv_last_night_5min_high": { + "name": "HRV last night 5-min high" + }, + "hrv_baseline": { + "name": "HRV baseline" + }, "avg_stress_level": { "name": "Avg stress level" }, @@ -172,6 +196,18 @@ "sleep_score": { "name": "Sleep score" }, + "deep_sleep": { + "name": "Deep sleep" + }, + "light_sleep": { + "name": "Light sleep" + }, + "rem_sleep": { + "name": "REM sleep" + }, + "awake_sleep": { + "name": "Awake during sleep" + }, "body_battery_most_recent": { "name": "Body battery (most recent)" }, diff --git a/docs/garmin_connect.markdown b/docs/garmin_connect.markdown index 69c7966..e233020 100644 --- a/docs/garmin_connect.markdown +++ b/docs/garmin_connect.markdown @@ -25,12 +25,14 @@ You need a Garmin Connect account with at least one Garmin device that syncs dat ## Sensors -This integration provides **100+ sensors** covering various health and fitness metrics. Sensors are grouped into the following categories: +This integration provides **110+ sensors** covering various health and fitness metrics. Sensors are grouped into the following categories: ### Activity & Steps - **Total Steps** - Daily step count - **Daily Step Goal** - Your configured step target +- **Yesterday Steps/Distance** - Previous day's complete totals +- **Weekly Step/Distance Avg** - 7-day averages - **Total Distance** - Distance walked/run - **Floors Ascended/Descended** - Floors climbed @@ -45,6 +47,9 @@ This integration provides **100+ sensors** covering various health and fitness m - **Resting Heart Rate** - Daily resting HR - **Min/Max Heart Rate** - Daily HR range - **Last 7 Days Avg HR** - Weekly average +- **HRV Weekly Average** - 7-day HRV average (ms) +- **HRV Last Night Average** - Last night's HRV (ms) +- **HRV Baseline** - Personal HRV baseline with range ### Stress & Recovery @@ -56,6 +61,9 @@ This integration provides **100+ sensors** covering various health and fitness m - **Sleep Score** - Overall sleep quality score - **Sleep Duration** - Time asleep - **Awake Duration** - Time awake during sleep +- **Deep Sleep** - Time in deep sleep +- **Light Sleep** - Time in light sleep +- **REM Sleep** - Time in REM sleep ### Body Battery