Added HRV and weekly steps sensors

This commit is contained in:
Ron Klinkien
2026-01-02 19:09:03 +01:00
parent d45400e3bc
commit 10cc9608b9
5 changed files with 218 additions and 5 deletions

View File

@@ -8,7 +8,7 @@
# Garmin Connect # 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 ## Supported Features
@@ -29,6 +29,8 @@ Sensor values depend on your Garmin devices and connected apps.
|--------|-------------| |--------|-------------|
| Total Steps | Daily step count | | Total Steps | Daily step count |
| Daily Step Goal | Your configured step target | | 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 | | Total Distance | Distance walked/run in meters |
| Floors Ascended/Descended | Floors climbed | | 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 | | Resting Heart Rate | Daily resting HR |
| Min/Max Heart Rate | Daily HR range | | Min/Max Heart Rate | Daily HR range |
| Last 7 Days Avg HR | Weekly average | | Last 7 Days Avg HR | Weekly average |
| HRV Weekly/Nightly Avg | Heart rate variability metrics |
| HRV Baseline | Personal HRV baseline |
### Stress & Recovery ### Stress & Recovery
@@ -60,6 +64,9 @@ Sensor values depend on your Garmin devices and connected apps.
|--------|-------------| |--------|-------------|
| Sleep Score | Overall sleep quality score | | Sleep Score | Overall sleep quality score |
| Sleep/Awake Duration | Time asleep and awake | | 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 ### Body Battery

View File

@@ -96,9 +96,18 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
gear_defaults = {} gear_defaults = {}
activity_types = {} activity_types = {}
last_activities = [] 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_data = {}
sleep_score = None sleep_score = None
sleep_time_seconds = None sleep_time_seconds = None
deep_sleep_seconds = None
light_sleep_seconds = None
rem_sleep_seconds = None
awake_sleep_seconds = None
hrv_data = {} hrv_data = {}
hrv_status = {"status": "unknown"} hrv_status = {"status": "unknown"}
endurance_data = {} endurance_data = {}
@@ -113,6 +122,29 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
self.api.get_user_summary, today.isoformat() 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( body = await self.hass.async_add_executor_job(
self.api.get_body_composition, today.isoformat() self.api.get_body_composition, today.isoformat()
) )
@@ -163,13 +195,11 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
menstrual_data = await self.hass.async_add_executor_job( menstrual_data = await self.hass.async_add_executor_job(
self.api.get_menstrual_data_for_date, today.isoformat() 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 # API returns None when not enabled - convert to empty dict
if menstrual_data is None: if menstrual_data is None:
menstrual_data = {} menstrual_data = {}
except Exception as err: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
# Menstrual data not available for this user # Menstrual data not available for this user
_LOGGER.debug("Menstrual data error: %s", err)
menstrual_data = {} menstrual_data = {}
except ( except (
@@ -247,6 +277,26 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
except KeyError: except KeyError:
pass 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: try:
if hrv_data and "hrvSummary" in hrv_data: if hrv_data and "hrvSummary" in hrv_data:
hrv_status = hrv_data["hrvSummary"] hrv_status = hrv_data["hrvSummary"]
@@ -269,6 +319,14 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
"gearDefaults": gear_defaults, "gearDefaults": gear_defaults,
"sleepScore": sleep_score, "sleepScore": sleep_score,
"sleepTimeSeconds": sleep_time_seconds, "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, "hrvStatus": hrv_status,
"enduranceScore": endurance_status, "enduranceScore": endurance_status,
**fitnessage_data, **fitnessage_data,

View File

@@ -49,6 +49,36 @@ ACTIVITY_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
native_unit_of_measurement="steps", native_unit_of_measurement="steps",
icon="mdi:target", 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( GarminConnectSensorEntityDescription(
key="totalDistanceMeters", key="totalDistanceMeters",
translation_key="total_distance", 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( attributes_fn=lambda data: {k: v for k, v in data.get(
"hrvStatus", {}).items() if k != "status"} if data.get("hrvStatus") else {}, "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 # Stress Sensors
@@ -304,6 +368,46 @@ SLEEP_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
icon="mdi:sleep", 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 # Body Battery Sensors

View File

@@ -43,6 +43,18 @@
"daily_step_goal": { "daily_step_goal": {
"name": "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": { "total_distance": {
"name": "Total distance" "name": "Total distance"
}, },
@@ -103,6 +115,18 @@
"hrv_status": { "hrv_status": {
"name": "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": { "avg_stress_level": {
"name": "Avg stress level" "name": "Avg stress level"
}, },
@@ -172,6 +196,18 @@
"sleep_score": { "sleep_score": {
"name": "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": { "body_battery_most_recent": {
"name": "Body battery (most recent)" "name": "Body battery (most recent)"
}, },

View File

@@ -25,12 +25,14 @@ You need a Garmin Connect account with at least one Garmin device that syncs dat
## Sensors ## 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 ### Activity & Steps
- **Total Steps** - Daily step count - **Total Steps** - Daily step count
- **Daily Step Goal** - Your configured step target - **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 - **Total Distance** - Distance walked/run
- **Floors Ascended/Descended** - Floors climbed - **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 - **Resting Heart Rate** - Daily resting HR
- **Min/Max Heart Rate** - Daily HR range - **Min/Max Heart Rate** - Daily HR range
- **Last 7 Days Avg HR** - Weekly average - **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 ### Stress & Recovery
@@ -56,6 +61,9 @@ This integration provides **100+ sensors** covering various health and fitness m
- **Sleep Score** - Overall sleep quality score - **Sleep Score** - Overall sleep quality score
- **Sleep Duration** - Time asleep - **Sleep Duration** - Time asleep
- **Awake Duration** - Time awake during sleep - **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 ### Body Battery