Fix via_device race in google_health (#177933)

This commit is contained in:
Erik Montnemery
2026-08-01 16:47:59 +02:00
committed by GitHub
parent 203b8acf57
commit d30a56fad9
4 changed files with 59 additions and 6 deletions
@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import aiohttp_client, device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
@@ -115,6 +115,16 @@ async def async_setup_entry(
sleep_coordinator=sleep_coordinator,
)
# Register the account device up front so the per-device sensors can resolve
# it as their via_device parent even when only the device scope is granted
# (the account-level sensors that would otherwise create it are gated on
# different scopes).
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Google",
)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
@@ -23,7 +23,11 @@ from homeassistant.const import (
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
async_get_device_id_by_identifier,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -401,7 +405,11 @@ class GoogleHealthDeviceSensor(
or (device.device_type.title() if device.device_type else "Device"),
model=device.device_type.title() if device.device_type else None,
sw_version=device.device_version,
via_device=(DOMAIN, entry_id),
via_device_id=async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, entry_id),
config_entry_id=entry_id,
),
)
if device.mac_address:
+2 -2
View File
@@ -78,9 +78,9 @@ def mock_expires_at() -> int:
@pytest.fixture
def scopes() -> list[str]:
def scopes(request: pytest.FixtureRequest) -> list[str]:
"""Fixture with scopes to set up."""
return OAUTH_SCOPES
return getattr(request, "param", OAUTH_SCOPES)
@pytest.fixture(name="token_entry")
+36 -1
View File
@@ -4,12 +4,14 @@ from collections.abc import Awaitable, Callable
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from google_health_api.const import HealthApiScope
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.google_health.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.unit_system import (
METRIC_SYSTEM,
US_CUSTOMARY_SYSTEM,
@@ -140,3 +142,36 @@ async def test_sensor_unit_conversions(
assert state is not None
assert float(state.state) == expected_state
assert state.attributes.get("unit_of_measurement") == expected_unit
@pytest.mark.parametrize(
"scopes",
[[HealthApiScope.PROFILE_READ, HealthApiScope.SETTINGS_READ]],
indirect=True,
)
@pytest.mark.usefixtures("mock_google_health_client")
async def test_device_sensor_via_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
integration_setup: Callable[[], Awaitable[bool]],
) -> None:
"""Test a paired device is linked to the account device via via_device_id.
Only the profile and settings scopes are granted so the account device
can only come from the up-front registration, not from account-level
sensors that scopes outside of this test would also create.
"""
with patch("homeassistant.components.google_health._PLATFORMS", [Platform.SENSOR]):
assert await integration_setup()
account_device = device_registry.async_get_device_by_identifier(
(DOMAIN, config_entry.entry_id), config_entry.entry_id
)
assert account_device is not None
paired_device = device_registry.async_get_device_by_identifier(
(DOMAIN, "watch_123"), config_entry.entry_id
)
assert paired_device is not None
assert paired_device.via_device_id == account_device.id