Fix Google Health sleep sensors (#179322)

Co-authored-by: Home Assistant Developer <hello@home-assistant.io>
This commit is contained in:
Allen Porter
2026-08-16 11:18:09 -07:00
committed by GitHub
co-authored by Home Assistant Developer
parent b9f0b1961e
commit 4af0d7ea28
5 changed files with 113 additions and 9 deletions
@@ -42,6 +42,7 @@ POLLING_INTERVAL = timedelta(minutes=15)
BODY_POLLING_INTERVAL = timedelta(hours=1)
DEVICE_POLLING_INTERVAL = timedelta(hours=1)
DEFAULT_PAGE_SIZE = 1
SLEEP_PAGE_SIZE = 10
@dataclass
@@ -254,6 +255,7 @@ class GoogleHealthDeviceCoordinator(
class GoogleHealthSleepData:
"""Class to hold sleep data."""
# The most recent sleep session with summary data
sleep: Sleep | None = None
@@ -281,9 +283,15 @@ class GoogleHealthSleepCoordinator(
@override
async def _async_fetch_data(self) -> GoogleHealthSleepData:
"""Fetch latest sleep session."""
sleep_result = await self.api.sleep.list(page_size=DEFAULT_PAGE_SIZE)
sleep = sleep_result.data_points[0].data if sleep_result.data_points else None
return GoogleHealthSleepData(sleep=sleep)
sleep_result = await self.api.sleep.list(page_size=SLEEP_PAGE_SIZE)
# Find the first session with a sleep summary
for data_point in sleep_result.data_points or ():
if data_point.data and data_point.data.summary:
return GoogleHealthSleepData(sleep=data_point.data)
# No sessions or current session still in progress
return GoogleHealthSleepData()
@dataclass
@@ -0,0 +1,31 @@
{
"dataPoints": [
{
"name": "users/me/dataTypes/sleep/dataPoints/session-active-12345",
"sleep": {
"interval": {
"startTime": "2026-08-16T22:00:00Z",
"startUtcOffset": "-07:00"
}
}
},
{
"name": "users/me/dataTypes/sleep/dataPoints/session-12345",
"sleep": {
"interval": {
"startTime": "2026-08-15T22:00:00Z",
"endTime": "2026-08-16T06:00:00Z",
"startUtcOffset": "-07:00",
"endUtcOffset": "-07:00"
},
"summary": {
"minutesToFallAsleep": 15,
"minutesAfterWakeUp": 10,
"minutesAsleep": 420,
"minutesInSleepPeriod": 480,
"minutesAwake": 60
}
}
}
]
}
@@ -201,7 +201,7 @@ async def test_config_flow_get_identity_error(
mock_google_health_client: AsyncMock,
) -> None:
"""Test config flow aborts if get_identity raises an API error."""
mock_google_health_client.get_identity.side_effect = GoogleHealthApiError
mock_google_health_client.get_identity.side_effect = GoogleHealthApiError("Error")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -243,7 +243,9 @@ async def test_config_flow_api_not_enabled(
mock_google_health_client: AsyncMock,
) -> None:
"""Test config flow aborts if the Google Health API is not enabled."""
mock_google_health_client.get_identity.side_effect = HealthApiForbiddenException
mock_google_health_client.get_identity.side_effect = HealthApiForbiddenException(
"Forbidden"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
+6 -2
View File
@@ -54,7 +54,9 @@ async def test_setup_api_error(
integration_setup: Callable[[], Awaitable[bool]],
) -> None:
"""Test setup error retry handling when API fails."""
mock_google_health_client.steps.today.side_effect = GoogleHealthApiError
mock_google_health_client.steps.today.side_effect = GoogleHealthApiError(
"API error"
)
assert not await integration_setup()
assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY
@@ -67,7 +69,9 @@ async def test_setup_auth_error(
integration_setup: Callable[[], Awaitable[bool]],
) -> None:
"""Test setup error when API returns auth or forbidden errors."""
mock_google_health_client.steps.today.side_effect = HealthApiForbiddenException
mock_google_health_client.steps.today.side_effect = HealthApiForbiddenException(
"Forbidden"
)
assert not await integration_setup()
assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR
+61 -2
View File
@@ -6,7 +6,11 @@ from unittest.mock import AsyncMock, MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from google_health_api.const import HealthApiScope
from google_health_api.model import ListPairedDevicesResult, _ListPairedDevicesModel
from google_health_api.model import (
SLEEP,
ListPairedDevicesResult,
_ListPairedDevicesModel,
)
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -21,7 +25,7 @@ from homeassistant.util.unit_system import (
UnitSystem,
)
from .conftest import paired_devices_fixture
from .conftest import _list_fixture, paired_devices_fixture
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -101,6 +105,61 @@ async def test_sensor_empty_sleep(
assert time_asleep_state.state == "unknown"
async def test_sensor_in_progress_sleep(
hass: HomeAssistant,
mock_google_health_client: AsyncMock,
integration_setup: Callable[[], Awaitable[bool]],
) -> None:
"""Test sleep sensors return latest completed session when an active session is in progress."""
mock_google_health_client.sleep.list.return_value = _list_fixture(
"sleep_in_progress.json", SLEEP
)
assert await integration_setup()
time_asleep_state = hass.states.get("sensor.google_health_time_asleep")
assert time_asleep_state is not None
assert time_asleep_state.state == "420"
time_awake_state = hass.states.get("sensor.google_health_time_awake")
assert time_awake_state is not None
assert time_awake_state.state == "60"
time_in_bed_state = hass.states.get("sensor.google_health_time_in_bed")
assert time_in_bed_state is not None
assert time_in_bed_state.state == "480"
time_to_fall_asleep_state = hass.states.get(
"sensor.google_health_time_to_fall_asleep"
)
assert time_to_fall_asleep_state is not None
assert time_to_fall_asleep_state.state == "15"
time_after_wakeup_state = hass.states.get(
"sensor.google_health_time_after_waking_up"
)
assert time_after_wakeup_state is not None
assert time_after_wakeup_state.state == "10"
async def test_sensor_only_in_progress_sleep(
hass: HomeAssistant,
mock_google_health_client: AsyncMock,
integration_setup: Callable[[], Awaitable[bool]],
) -> None:
"""Test sleep sensors when only an in-progress session with no summary exists."""
full_fixture = _list_fixture("sleep_in_progress.json", SLEEP)
mock_google_health_client.sleep.list.return_value = MagicMock(
data_points=full_fixture.data_points[:1]
)
assert await integration_setup()
time_asleep_state = hass.states.get("sensor.google_health_time_asleep")
assert time_asleep_state is not None
assert time_asleep_state.state == "unknown"
@pytest.mark.parametrize(
("unit_system", "expected_sensors"),
[