Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions homeassistant/components/google_health/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Coordinators for Google Health."""

import asyncio
from dataclasses import dataclass
from datetime import timedelta
import logging
Expand All @@ -12,9 +13,13 @@
HealthAuthException,
)
from google_health_api.model import (
ActiveEnergyBurnedRollupValue,
BodyFat,
DailyRestingHeartRate,
DistanceRollupValue,
FloorsRollupValue,
StepsRollupValue,
TotalCaloriesRollupValue,
Weight,
)

Expand All @@ -40,6 +45,9 @@ class GoogleHealthActivityData:

steps: StepsRollupValue | None = None
distance: DistanceRollupValue | None = None
active_energy_burned: ActiveEnergyBurnedRollupValue | None = None
total_calories: TotalCaloriesRollupValue | None = None
floors: FloorsRollupValue | None = None


@dataclass
Expand All @@ -48,6 +56,7 @@ class GoogleHealthBodyData:

weight: Weight | None = None
resting_heart_rate: DailyRestingHeartRate | None = None
body_fat: BodyFat | None = None


class GoogleHealthDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
Expand Down Expand Up @@ -116,20 +125,42 @@ def __init__(

@override
async def _async_fetch_data(self) -> GoogleHealthActivityData:
"""Fetch steps and distance rollup for today.
"""Fetch activity rollups for today.

Queries the daily rollup endpoints using Home Assistant's local time zone
to aggregate step and distance counts over the current civil day. If no
data points exist for today yet, the API returns None, which the sensors
default to 0.
Queries the daily rollup endpoints in parallel using Home Assistant's
local time zone to aggregate steps, distance, active calories, total
calories, and floors. If no data points exist for today yet, the API
returns None, which the sensors default to 0.
"""
steps_rollup = await self.api.steps.today(self.hass.config.time_zone)
distance_rollup = await self.api.distance.today(self.hass.config.time_zone)
(
steps_rollup,
distance_rollup,
active_energy_rollup,
total_calories_rollup,
floors_rollup,
) = await asyncio.gather(
self.api.steps.today(self.hass.config.time_zone),
self.api.distance.today(self.hass.config.time_zone),
self.api.active_energy_burned.today(self.hass.config.time_zone),
self.api.total_calories.today(self.hass.config.time_zone),
self.api.floors.today(self.hass.config.time_zone),
)

steps = steps_rollup.data if steps_rollup else None
distance = distance_rollup.data if distance_rollup else None

return GoogleHealthActivityData(steps=steps, distance=distance)
active_energy_burned = (
active_energy_rollup.data if active_energy_rollup else None
)
total_calories = total_calories_rollup.data if total_calories_rollup else None
floors = floors_rollup.data if floors_rollup else None

return GoogleHealthActivityData(
steps=steps,
distance=distance,
active_energy_burned=active_energy_burned,
total_calories=total_calories,
floors=floors,
)


class GoogleHealthBodyCoordinator(
Expand All @@ -155,13 +186,14 @@ def __init__(

@override
async def _async_fetch_data(self) -> GoogleHealthBodyData:
"""Fetch latest body weight and resting heart rate."""
"""Fetch latest body weight, resting heart rate, and body fat in parallel."""
# The Google Health API returns data points sorted by interval start time
# in descending order (newest first). Querying with page_size=1 and grabbing
# the first element is sufficient to fetch the most recent measurement.
weight_result = await self.api.weight.list(page_size=DEFAULT_PAGE_SIZE)
hr_result = await self.api.daily_resting_heart_rate.list(
page_size=DEFAULT_PAGE_SIZE
weight_result, hr_result, body_fat_result = await asyncio.gather(
self.api.weight.list(page_size=DEFAULT_PAGE_SIZE),
self.api.daily_resting_heart_rate.list(page_size=DEFAULT_PAGE_SIZE),
self.api.body_fat.list(page_size=DEFAULT_PAGE_SIZE),
)

weight = (
Expand All @@ -170,7 +202,12 @@ async def _async_fetch_data(self) -> GoogleHealthBodyData:
resting_heart_rate = (
hr_result.data_points[0].data if hr_result.data_points else None
)
body_fat = (
body_fat_result.data_points[0].data if body_fat_result.data_points else None
)

return GoogleHealthBodyData(
weight=weight, resting_heart_rate=resting_heart_rate
weight=weight,
resting_heart_rate=resting_heart_rate,
body_fat=body_fat,
)
37 changes: 36 additions & 1 deletion homeassistant/components/google_health/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfLength, UnitOfMass
from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfLength, UnitOfMass
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
Expand Down Expand Up @@ -56,6 +56,32 @@ class GoogleHealthSensorEntityDescription[
data.distance.millimeters_sum / 1000.0 if data and data.distance else 0.0
),
),
GoogleHealthSensorEntityDescription[GoogleHealthActivityCoordinator, float](
key="active_calories",
translation_key="active_calories",
native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda data: (
data.active_energy_burned.kcal_sum
if data and data.active_energy_burned
else 0.0
),
),
GoogleHealthSensorEntityDescription[GoogleHealthActivityCoordinator, float](
key="total_calories",
translation_key="total_calories",
native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda data: (
data.total_calories.kcal_sum if data and data.total_calories else 0.0
),
),
GoogleHealthSensorEntityDescription[GoogleHealthActivityCoordinator, int](
key="floors",
translation_key="floors",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda data: data.floors.count_sum if data and data.floors else 0,
),
]

BODY_SENSORS: list[
Expand All @@ -81,6 +107,15 @@ class GoogleHealthSensorEntityDescription[
else None
),
),
GoogleHealthSensorEntityDescription[GoogleHealthBodyCoordinator, float | None](
key="body_fat",
translation_key="body_fat",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: (
data.body_fat.percentage if data and data.body_fat else None
),
),
]


Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/google_health/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,24 @@
},
"entity": {
"sensor": {
"active_calories": {
"name": "Active calories"
},
"body_fat": {
"name": "Body fat"
},
"floors": {
"name": "Floors"
},
"resting_heart_rate": {
"name": "Resting heart rate"
},
"steps": {
"name": "Steps",
"unit_of_measurement": "steps"
},
"total_calories": {
"name": "Total calories"
}
}
},
Expand Down
Loading
Loading