mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix Google Health sleep sensors (#179322)
Co-authored-by: Home Assistant Developer <hello@home-assistant.io>
This commit is contained in:
co-authored by
Home Assistant Developer
parent
b9f0b1961e
commit
4af0d7ea28
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user