Bump python-google-nest-sdm to 7.0.0 (#134016)

Update python-google-nest-sdm to 7.0.0
This commit is contained in:
Allen Porter 2024-12-25 21:03:44 -08:00 committed by GitHub
parent 299250ebec
commit c75222e63c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 329 additions and 377 deletions

View File

@ -55,6 +55,7 @@ from homeassistant.helpers.typing import ConfigType
from . import api from . import api
from .const import ( from .const import (
CONF_CLOUD_PROJECT_ID,
CONF_PROJECT_ID, CONF_PROJECT_ID,
CONF_SUBSCRIBER_ID, CONF_SUBSCRIBER_ID,
CONF_SUBSCRIBER_ID_IMPORTED, CONF_SUBSCRIBER_ID_IMPORTED,
@ -214,33 +215,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
update_callback = SignalUpdateCallback(hass, async_config_reload, entry) update_callback = SignalUpdateCallback(hass, async_config_reload, entry)
subscriber.set_update_callback(update_callback.async_handle_event) subscriber.set_update_callback(update_callback.async_handle_event)
try: try:
await subscriber.start_async() unsub = await subscriber.start_async()
except AuthException as err: except AuthException as err:
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(
f"Subscriber authentication error: {err!s}" f"Subscriber authentication error: {err!s}"
) from err ) from err
except ConfigurationException as err: except ConfigurationException as err:
_LOGGER.error("Configuration error: %s", err) _LOGGER.error("Configuration error: %s", err)
subscriber.stop_async()
return False return False
except SubscriberException as err: except SubscriberException as err:
subscriber.stop_async()
raise ConfigEntryNotReady(f"Subscriber error: {err!s}") from err raise ConfigEntryNotReady(f"Subscriber error: {err!s}") from err
try: try:
device_manager = await subscriber.async_get_device_manager() device_manager = await subscriber.async_get_device_manager()
except ApiException as err: except ApiException as err:
subscriber.stop_async() unsub()
raise ConfigEntryNotReady(f"Device manager error: {err!s}") from err raise ConfigEntryNotReady(f"Device manager error: {err!s}") from err
@callback @callback
def on_hass_stop(_: Event) -> None: def on_hass_stop(_: Event) -> None:
"""Close connection when hass stops.""" """Close connection when hass stops."""
subscriber.stop_async() unsub()
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
) )
entry.async_on_unload(unsub)
entry.runtime_data = NestData( entry.runtime_data = NestData(
subscriber=subscriber, subscriber=subscriber,
device_manager=device_manager, device_manager=device_manager,
@ -253,12 +254,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if DATA_SDM not in entry.data:
# Legacy API
return True
_LOGGER.debug("Stopping nest subscriber")
subscriber = entry.runtime_data.subscriber
subscriber.stop_async()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@ -272,24 +267,25 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
or CONF_SUBSCRIBER_ID_IMPORTED in entry.data or CONF_SUBSCRIBER_ID_IMPORTED in entry.data
): ):
return return
if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None:
subscriber = await api.new_subscriber(hass, entry) subscription_name = entry.data[CONF_SUBSCRIBER_ID]
if not subscriber: admin_client = api.new_pubsub_admin_client(
return hass,
_LOGGER.debug("Deleting subscriber '%s'", subscriber.subscriber_id) access_token=entry.data["token"]["access_token"],
cloud_project_id=entry.data[CONF_CLOUD_PROJECT_ID],
)
_LOGGER.debug("Deleting subscription '%s'", subscription_name)
try: try:
await subscriber.delete_subscription() await admin_client.delete_subscription(subscription_name)
except (AuthException, SubscriberException) as err: except ApiException as err:
_LOGGER.warning( _LOGGER.warning(
( (
"Unable to delete subscription '%s'; Will be automatically cleaned up" "Unable to delete subscription '%s'; Will be automatically cleaned up"
" by cloud console: %s" " by cloud console: %s"
), ),
subscriber.subscriber_id, subscription_name,
err, err,
) )
finally:
subscriber.stop_async()
class NestEventViewBase(HomeAssistantView, ABC): class NestEventViewBase(HomeAssistantView, ABC):

View File

@ -19,5 +19,5 @@
"documentation": "https://www.home-assistant.io/integrations/nest", "documentation": "https://www.home-assistant.io/integrations/nest",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["google_nest_sdm"], "loggers": ["google_nest_sdm"],
"requirements": ["google-nest-sdm==6.1.5"] "requirements": ["google-nest-sdm==7.0.0"]
} }

View File

@ -1024,7 +1024,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2 google-generativeai==0.8.2
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==6.1.5 google-nest-sdm==7.0.0
# homeassistant.components.google_photos # homeassistant.components.google_photos
google-photos-library-api==0.12.1 google-photos-library-api==0.12.1

View File

@ -874,7 +874,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2 google-generativeai==0.8.2
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==6.1.5 google-nest-sdm==7.0.0
# homeassistant.components.google_photos # homeassistant.components.google_photos
google-photos-library-api==0.12.1 google-photos-library-api==0.12.1

View File

@ -5,17 +5,14 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable, Generator from collections.abc import Awaitable, Callable, Generator
import copy import copy
from dataclasses import dataclass from dataclasses import dataclass
import re
from typing import Any from typing import Any
from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.streaming_manager import Message
from google_nest_sdm.device import Device
from google_nest_sdm.device_manager import DeviceManager
from google_nest_sdm.event import EventMessage
from google_nest_sdm.event_media import CachePolicy
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.application_credentials import ClientCredential
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import API_URL
# Typing helpers # Typing helpers
type PlatformSetup = Callable[[], Awaitable[None]] type PlatformSetup = Callable[[], Awaitable[None]]
@ -66,68 +63,22 @@ TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig(
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
) )
DEVICE_ID = "enterprises/project-id/devices/device-id"
class FakeSubscriber(GoogleNestSubscriber):
"""Fake subscriber that supplies a FakeDeviceManager."""
stop_calls = 0
def __init__(self) -> None: # pylint: disable=super-init-not-called
"""Initialize Fake Subscriber."""
self._device_manager = DeviceManager()
self._subscriber_name = "fake-name"
def set_update_callback(self, target: Callable[[EventMessage], Awaitable[None]]):
"""Capture the callback set by Home Assistant."""
self._device_manager.set_update_callback(target)
async def create_subscription(self):
"""Create the subscription."""
return
async def delete_subscription(self):
"""Delete the subscription."""
return
async def start_async(self):
"""Return the fake device manager."""
return self._device_manager
async def async_get_device_manager(self) -> DeviceManager:
"""Return the fake device manager."""
return self._device_manager
@property
def cache_policy(self) -> CachePolicy:
"""Return the cache policy."""
return self._device_manager.cache_policy
def stop_async(self):
"""No-op to stop the subscriber."""
self.stop_calls += 1
async def async_receive_event(self, event_message: EventMessage):
"""Simulate a received pubsub message, invoked by tests."""
# Update device state, then invoke HomeAssistant to refresh
await self._device_manager.async_handle_event(event_message)
DEVICE_ID = "enterprise/project-id/devices/device-id"
DEVICE_COMMAND = f"{DEVICE_ID}:executeCommand" DEVICE_COMMAND = f"{DEVICE_ID}:executeCommand"
DEVICE_URL_MATCH = re.compile(
f"{API_URL}/enterprises/project-id/devices/[^:]+:executeCommand"
)
TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..."
TEST_CLIP_URL = "https://domain/clip/XyZ.mp4"
class CreateDevice: class CreateDevice:
"""Fixture used for creating devices.""" """Fixture used for creating devices."""
def __init__( def __init__(self) -> None:
self,
device_manager: DeviceManager,
auth: AbstractAuth,
) -> None:
"""Initialize CreateDevice.""" """Initialize CreateDevice."""
self.device_manager = device_manager
self.auth = auth
self.data = {"traits": {}} self.data = {"traits": {}}
self.devices = []
def create( def create(
self, self,
@ -138,4 +89,9 @@ class CreateDevice:
data = copy.deepcopy(self.data) data = copy.deepcopy(self.data)
data.update(raw_data if raw_data else {}) data.update(raw_data if raw_data else {})
data["traits"].update(raw_traits if raw_traits else {}) data["traits"].update(raw_traits if raw_traits else {})
self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth)) self.devices.append(data)
def create_nest_event(data: dict[str, Any]) -> Message:
"""Create a pub/sub event message for testing."""
return Message.from_data(data)

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from asyncio import AbstractEventLoop
from collections.abc import Generator from collections.abc import Generator
import copy import copy
import shutil import shutil
@ -13,100 +12,131 @@ import uuid
import aiohttp import aiohttp
from google_nest_sdm import diagnostics from google_nest_sdm import diagnostics
from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.streaming_manager import StreamingManager
import pytest import pytest
from yarl import URL
from homeassistant.components.application_credentials import ( from homeassistant.components.application_credentials import (
async_import_client_credential, async_import_client_credential,
) )
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID, SDM_SCOPES from homeassistant.components.nest.const import API_URL, CONF_SUBSCRIBER_ID, SDM_SCOPES
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .common import ( from .common import (
DEVICE_ID, DEVICE_ID,
DEVICE_URL_MATCH,
PROJECT_ID, PROJECT_ID,
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CLIP_URL,
TEST_CONFIG_APP_CREDS, TEST_CONFIG_APP_CREDS,
TEST_IMAGE_URL,
CreateDevice, CreateDevice,
FakeSubscriber,
NestTestConfig, NestTestConfig,
PlatformSetup, PlatformSetup,
YieldFixture, YieldFixture,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
FAKE_TOKEN = "some-token" FAKE_TOKEN = "some-token"
FAKE_REFRESH_TOKEN = "some-refresh-token" FAKE_REFRESH_TOKEN = "some-refresh-token"
class FakeAuth(AbstractAuth): class FakeAuth:
"""A fake implementation of the auth class that records requests. """A fixture for request handling that records requests.
This class captures the outgoing requests, and can also be used by This class is used with AiohttpClientMocker to capture outgoing requests
tests to set up fake responses. This class is registered as a response and can also be used by tests to set up fake responses.
handler for a fake aiohttp_server and can simulate successes or failures
from the API.
""" """
def __init__(self) -> None: def __init__(
self,
aioclient_mock: AiohttpClientMocker,
device_factory: CreateDevice,
project_id: str,
) -> None:
"""Initialize FakeAuth.""" """Initialize FakeAuth."""
super().__init__(None, None) # Tests can factory fixture to create fake device responses.
# Tests can set fake responses here. self.device_factory = device_factory
self.responses = [] # Tests can set fake structure responses here.
self.structures: list[dict[str, Any]] = []
# Tests can set fake command responses here.
self.responses: list[aiohttp.web.Response] = []
# The last request is recorded here. # The last request is recorded here.
self.method = None self.method = None
self.url = None self.url = None
self.json = None self.json = None
self.headers = None self.headers = None
self.captured_requests = [] self.captured_requests = []
# Set up by fixture
self.client = None
async def async_get_access_token(self) -> str: # API makes a call to request structures to initiate pubsub feed, but the
"""Return a valid access token.""" # integration does not use this.
return "" aioclient_mock.get(
f"{API_URL}/enterprises/{project_id}/structures",
side_effect=self.request_structures,
)
aioclient_mock.get(
f"{API_URL}/enterprises/{project_id}/devices",
side_effect=self.request_devices,
)
aioclient_mock.post(DEVICE_URL_MATCH, side_effect=self.request)
aioclient_mock.get(TEST_IMAGE_URL, side_effect=self.request)
aioclient_mock.get(TEST_CLIP_URL, side_effect=self.request)
async def request(self, method, url, **kwargs): async def request_structures(
self, method: str, url: str, data: dict[str, Any]
) -> AiohttpClientMockResponse:
"""Handle requests to create devices."""
return AiohttpClientMockResponse(
method, url, json={"structures": self.structures}
)
async def request_devices(
self, method: str, url: str, data: dict[str, Any]
) -> AiohttpClientMockResponse:
"""Handle requests to create devices."""
return AiohttpClientMockResponse(
method, url, json={"devices": self.device_factory.devices}
)
async def request(
self, method: str, url: URL, data: dict[str, Any]
) -> AiohttpClientMockResponse:
"""Capure the request arguments for tests to assert on.""" """Capure the request arguments for tests to assert on."""
self.method = method self.method = method
self.url = url str_url = str(url)
self.json = kwargs.get("json") self.url = str_url[len(API_URL) + 1 :]
self.headers = kwargs.get("headers") self.json = data
self.captured_requests.append((method, url, self.json, self.headers)) self.captured_requests.append((method, url, self.json))
return await self.client.get("/")
async def response_handler(self, request):
"""Handle fake responess for aiohttp_server."""
if len(self.responses) > 0: if len(self.responses) > 0:
return self.responses.pop(0) response = self.responses.pop(0)
return aiohttp.web.json_response() return AiohttpClientMockResponse(
method, url, response=response.body, status=response.status
)
return AiohttpClientMockResponse(method, url)
@pytest.fixture(name="device_access_project_id")
def mock_device_access_project_id() -> str:
"""Fixture to configure the device access console project id used in tests."""
return PROJECT_ID
@pytest.fixture @pytest.fixture
def aiohttp_client( async def auth(
event_loop: AbstractEventLoop, aioclient_mock: AiohttpClientMocker,
aiohttp_client: ClientSessionGenerator, create_device: CreateDevice,
socket_enabled: None, device_access_project_id: str,
) -> ClientSessionGenerator: ) -> FakeAuth:
"""Return aiohttp_client and allow opening sockets."""
return aiohttp_client
@pytest.fixture
async def auth(aiohttp_client: ClientSessionGenerator) -> FakeAuth:
"""Fixture for an AbstractAuth.""" """Fixture for an AbstractAuth."""
auth = FakeAuth() return FakeAuth(aioclient_mock, create_device, device_access_project_id)
app = aiohttp.web.Application()
app.router.add_get("/", auth.response_handler)
app.router.add_post("/", auth.response_handler)
auth.client = await aiohttp_client(app)
return auth
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -119,20 +149,32 @@ def cleanup_media_storage(hass: HomeAssistant) -> Generator[None]:
@pytest.fixture @pytest.fixture
def subscriber() -> YieldFixture[FakeSubscriber]: def subscriber_side_effect() -> Any | None:
"""Set up the FakeSusbcriber.""" """Fixture to inject failures into FakeSubscriber start."""
subscriber = FakeSubscriber() return None
@pytest.fixture(autouse=True)
def subscriber(subscriber_side_effect: Any | None) -> Generator[AsyncMock]:
"""Fixture to allow tests to emulate the pub/sub subscriber receiving messages."""
with patch( with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber", "google_nest_sdm.google_nest_subscriber.StreamingManager", spec=StreamingManager
return_value=subscriber, ) as mock_manager:
): # Use side_effect to capture the callback
yield subscriber def mock_init(**kwargs: Any) -> AsyncMock:
mock_manager.async_receive_event = kwargs["callback"]
if subscriber_side_effect is not None:
mock_manager.start.side_effect = subscriber_side_effect
return mock_manager
mock_manager.side_effect = mock_init
yield mock_manager
@pytest.fixture @pytest.fixture
def mock_subscriber() -> YieldFixture[AsyncMock]: def mock_subscriber() -> YieldFixture[AsyncMock]:
"""Fixture for injecting errors into the subscriber.""" """Fixture for injecting errors into the subscriber."""
mock_subscriber = AsyncMock(FakeSubscriber) mock_subscriber = AsyncMock(GoogleNestSubscriber)
with patch( with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber", "homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=mock_subscriber, return_value=mock_subscriber,
@ -140,12 +182,6 @@ def mock_subscriber() -> YieldFixture[AsyncMock]:
yield mock_subscriber yield mock_subscriber
@pytest.fixture
async def device_manager(subscriber: FakeSubscriber) -> DeviceManager:
"""Set up the DeviceManager."""
return await subscriber.async_get_device_manager()
@pytest.fixture @pytest.fixture
async def device_id() -> str: async def device_id() -> str:
"""Fixture to set default device id used when creating devices.""" """Fixture to set default device id used when creating devices."""
@ -166,14 +202,12 @@ async def device_traits() -> dict[str, Any]:
@pytest.fixture @pytest.fixture
async def create_device( async def create_device(
device_manager: DeviceManager,
auth: FakeAuth,
device_id: str, device_id: str,
device_type: str, device_type: str,
device_traits: dict[str, Any], device_traits: dict[str, Any],
) -> CreateDevice: ) -> CreateDevice:
"""Fixture for creating devices.""" """Fixture for creating devices."""
factory = CreateDevice(device_manager, auth) factory = CreateDevice()
factory.data.update( factory.data.update(
{ {
"name": device_id, "name": device_id,
@ -262,6 +296,7 @@ async def setup_base_platform(
hass: HomeAssistant, hass: HomeAssistant,
platforms: list[str], platforms: list[str],
config_entry: MockConfigEntry | None, config_entry: MockConfigEntry | None,
auth: FakeAuth,
) -> YieldFixture[PlatformSetup]: ) -> YieldFixture[PlatformSetup]:
"""Fixture to setup the integration platform.""" """Fixture to setup the integration platform."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -278,7 +313,8 @@ async def setup_base_platform(
@pytest.fixture @pytest.fixture
async def setup_platform( async def setup_platform(
setup_base_platform: PlatformSetup, subscriber: FakeSubscriber setup_base_platform: PlatformSetup,
subscriber: AsyncMock,
) -> PlatformSetup: ) -> PlatformSetup:
"""Fixture to setup the integration platform and subscriber.""" """Fixture to setup the integration platform and subscriber."""
return setup_base_platform return setup_base_platform

View File

@ -31,6 +31,9 @@
}), }),
}), }),
]), ]),
'subscriber': dict({
'start': 1,
}),
}) })
# --- # ---
# name: test_device_diagnostics # name: test_device_diagnostics
@ -85,5 +88,8 @@
}), }),
}), }),
]), ]),
'subscriber': dict({
'start': 1,
}),
}) })
# --- # ---

View File

@ -9,16 +9,16 @@ The tests below exercise both cases during integration setup.
""" """
import time import time
from unittest.mock import patch from unittest.mock import AsyncMock, Mock, patch
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from google.oauth2.credentials import Credentials
import pytest import pytest
from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, FakeSubscriber, PlatformSetup from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, PlatformSetup
from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
@ -27,7 +27,7 @@ FAKE_UPDATED_TOKEN = "fake-updated-token"
@pytest.fixture @pytest.fixture
def subscriber() -> FakeSubscriber | None: def subscriber() -> Mock | None:
"""Disable default subscriber since tests use their own patch.""" """Disable default subscriber since tests use their own patch."""
return None return None
@ -54,16 +54,16 @@ async def test_auth(
# Prepare to capture credentials for Subscriber # Prepare to capture credentials for Subscriber
captured_creds = None captured_creds = None
async def async_new_subscriber( def async_new_subscriber(
creds, subscription_name, event_loop, async_callback credentials: Credentials,
) -> GoogleNestSubscriber | None: ) -> Mock:
"""Capture credentials for tests.""" """Capture credentials for tests."""
nonlocal captured_creds nonlocal captured_creds
captured_creds = creds captured_creds = credentials
return None # GoogleNestSubscriber return AsyncMock()
with patch( with patch(
"google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", "google_nest_sdm.subscriber_client.pubsub_v1.SubscriberAsyncClient",
side_effect=async_new_subscriber, side_effect=async_new_subscriber,
) as new_subscriber_mock: ) as new_subscriber_mock:
await setup_platform() await setup_platform()
@ -122,16 +122,16 @@ async def test_auth_expired_token(
# Prepare to capture credentials for Subscriber # Prepare to capture credentials for Subscriber
captured_creds = None captured_creds = None
async def async_new_subscriber( def async_new_subscriber(
creds, subscription_name, event_loop, async_callback credentials: Credentials,
) -> GoogleNestSubscriber | None: ) -> Mock:
"""Capture credentials for tests.""" """Capture credentials for tests."""
nonlocal captured_creds nonlocal captured_creds
captured_creds = creds captured_creds = credentials
return None # GoogleNestSubscriber return AsyncMock()
with patch( with patch(
"google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", "google_nest_sdm.subscriber_client.pubsub_v1.SubscriberAsyncClient",
side_effect=async_new_subscriber, side_effect=async_new_subscriber,
) as new_subscriber_mock: ) as new_subscriber_mock:
await setup_platform() await setup_platform()

View File

@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup from .common import DEVICE_ID, CreateDevice, PlatformSetup
from .conftest import FakeAuth from .conftest import FakeAuth
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -520,13 +520,10 @@ async def test_camera_removed(
hass: HomeAssistant, hass: HomeAssistant,
auth: FakeAuth, auth: FakeAuth,
camera_device: None, camera_device: None,
subscriber: FakeSubscriber,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
) -> None: ) -> None:
"""Test case where entities are removed and stream tokens revoked.""" """Test case where entities are removed and stream tokens revoked."""
await setup_platform() await setup_platform()
# Simplify test setup
subscriber.cache_policy.fetch = False
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
cam = hass.states.get("camera.my_camera") cam = hass.states.get("camera.my_camera")
@ -780,7 +777,7 @@ async def test_camera_web_rtc_offer_failure(
assert response["event"] == { assert response["event"] == {
"type": "error", "type": "error",
"code": "webrtc_offer_failed", "code": "webrtc_offer_failed",
"message": "Nest API error: Bad Request response from API (400)", "message": "Nest API error: response from API (400)",
} }

View File

@ -7,10 +7,9 @@ pubsub subscriber.
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import AsyncMock
import aiohttp import aiohttp
from google_nest_sdm.auth import AbstractAuth
from google_nest_sdm.event import EventMessage
import pytest import pytest
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -45,8 +44,8 @@ from .common import (
DEVICE_COMMAND, DEVICE_COMMAND,
DEVICE_ID, DEVICE_ID,
CreateDevice, CreateDevice,
FakeSubscriber,
PlatformSetup, PlatformSetup,
create_nest_event,
) )
from .conftest import FakeAuth from .conftest import FakeAuth
@ -72,14 +71,13 @@ def device_traits() -> dict[str, Any]:
@pytest.fixture @pytest.fixture
async def create_event( async def create_event(
hass: HomeAssistant, hass: HomeAssistant,
auth: AbstractAuth, subscriber: AsyncMock,
subscriber: FakeSubscriber,
) -> CreateEvent: ) -> CreateEvent:
"""Fixture to send a pub/sub event.""" """Fixture to send a pub/sub event."""
async def create_event(traits: dict[str, Any]) -> None: async def create_event(traits: dict[str, Any]) -> None:
await subscriber.async_receive_event( await subscriber.async_receive_event(
EventMessage.create_event( create_nest_event(
{ {
"eventId": EVENT_ID, "eventId": EVENT_ID,
"timestamp": "2019-01-01T00:00:01Z", "timestamp": "2019-01-01T00:00:01Z",
@ -88,7 +86,6 @@ async def create_event(
"traits": traits, "traits": traits,
}, },
}, },
auth=auth,
) )
) )
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -7,7 +7,6 @@ from typing import Any
from unittest.mock import patch from unittest.mock import patch
from google_nest_sdm.exceptions import AuthException from google_nest_sdm.exceptions import AuthException
from google_nest_sdm.structure import Structure
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
@ -25,9 +24,9 @@ from .common import (
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS, TEST_CONFIG_APP_CREDS,
TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber,
NestTestConfig, NestTestConfig,
) )
from .conftest import FakeAuth, PlatformSetup
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
@ -58,6 +57,11 @@ def mock_rand_topic_name_fixture() -> None:
yield yield
@pytest.fixture(autouse=True)
def mock_request_setup(auth: FakeAuth) -> None:
"""Fixture to ensure fake requests are setup."""
class OAuthFixture: class OAuthFixture:
"""Simulate the oauth flow used by the config flow.""" """Simulate the oauth flow used by the config flow."""
@ -257,12 +261,6 @@ def mock_subscriptions() -> list[tuple[str, str]]:
return [] return []
@pytest.fixture(name="device_access_project_id")
def mock_device_access_project_id() -> str:
"""Fixture to configure the device access console project id used in tests."""
return PROJECT_ID
@pytest.fixture(name="cloud_project_id") @pytest.fixture(name="cloud_project_id")
def mock_cloud_project_id() -> str: def mock_cloud_project_id() -> str:
"""Fixture to configure the cloud console project id used in tests.""" """Fixture to configure the cloud console project id used in tests."""
@ -350,8 +348,7 @@ def mock_pubsub_api_responses(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_app_credentials( async def test_app_credentials(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Check full flow.""" """Check full flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -388,7 +385,7 @@ async def test_app_credentials(
("sdm_managed_topic", "device_access_project_id", "cloud_project_id"), ("sdm_managed_topic", "device_access_project_id", "cloud_project_id"),
[(True, "new-project-id", "new-cloud-project-id")], [(True, "new-project-id", "new-cloud-project-id")],
) )
async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> None: async def test_config_flow_restart(hass: HomeAssistant, oauth: OAuthFixture) -> None:
"""Check with auth implementation is re-initialized when aborting the flow.""" """Check with auth implementation is re-initialized when aborting the flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -443,8 +440,7 @@ async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> No
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_config_flow_wrong_project_id( async def test_config_flow_wrong_project_id(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Check the case where the wrong project ids are entered.""" """Check the case where the wrong project ids are entered."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -500,8 +496,7 @@ async def test_config_flow_wrong_project_id(
) )
async def test_config_flow_pubsub_configuration_error( async def test_config_flow_pubsub_configuration_error(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
mock_subscriber,
) -> None: ) -> None:
"""Check full flow fails with configuration error.""" """Check full flow fails with configuration error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -546,7 +541,8 @@ async def test_config_flow_pubsub_configuration_error(
[(True, HTTPStatus.INTERNAL_SERVER_ERROR)], [(True, HTTPStatus.INTERNAL_SERVER_ERROR)],
) )
async def test_config_flow_pubsub_subscriber_error( async def test_config_flow_pubsub_subscriber_error(
hass: HomeAssistant, oauth, mock_subscriber hass: HomeAssistant,
oauth: OAuthFixture,
) -> None: ) -> None:
"""Check full flow with a subscriber error.""" """Check full flow with a subscriber error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -697,7 +693,8 @@ async def test_reauth_multiple_config_entries(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_pubsub_subscription_strip_whitespace( async def test_pubsub_subscription_strip_whitespace(
hass: HomeAssistant, oauth, subscriber hass: HomeAssistant,
oauth: OAuthFixture,
) -> None: ) -> None:
"""Check that project id has whitespace stripped on entry.""" """Check that project id has whitespace stripped on entry."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -776,10 +773,9 @@ async def test_pubsub_subscription_auth_failure(
) )
async def test_pubsub_subscriber_config_entry_reauth( async def test_pubsub_subscriber_config_entry_reauth(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
setup_platform, setup_platform: PlatformSetup,
subscriber, config_entry: MockConfigEntry,
config_entry,
) -> None: ) -> None:
"""Test the pubsub subscriber id is preserved during reauth.""" """Test the pubsub subscriber id is preserved during reauth."""
await setup_platform() await setup_platform()
@ -805,22 +801,21 @@ async def test_pubsub_subscriber_config_entry_reauth(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_config_entry_title_from_home( async def test_config_entry_title_from_home(
hass: HomeAssistant, oauth, subscriber hass: HomeAssistant,
oauth: OAuthFixture,
auth: FakeAuth,
) -> None: ) -> None:
"""Test that the Google Home name is used for the config entry title.""" """Test that the Google Home name is used for the config entry title."""
device_manager = await subscriber.async_get_device_manager() auth.structures.append(
device_manager.add_structure( {
Structure.MakeStructure( "name": f"enterprise/{PROJECT_ID}/structures/some-structure-id",
{ "traits": {
"name": f"enterprise/{PROJECT_ID}/structures/some-structure-id", "sdm.structures.traits.Info": {
"traits": { "customName": "Example Home",
"sdm.structures.traits.Info": {
"customName": "Example Home",
},
}, },
} },
) }
) )
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -848,13 +843,13 @@ async def test_config_entry_title_from_home(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_config_entry_title_multiple_homes( async def test_config_entry_title_multiple_homes(
hass: HomeAssistant, oauth, subscriber hass: HomeAssistant,
oauth: OAuthFixture,
auth: FakeAuth,
) -> None: ) -> None:
"""Test handling of multiple Google Homes authorized.""" """Test handling of multiple Google Homes authorized."""
auth.structures.extend(
device_manager = await subscriber.async_get_device_manager() [
device_manager.add_structure(
Structure.MakeStructure(
{ {
"name": f"enterprise/{PROJECT_ID}/structures/id-1", "name": f"enterprise/{PROJECT_ID}/structures/id-1",
"traits": { "traits": {
@ -862,11 +857,7 @@ async def test_config_entry_title_multiple_homes(
"customName": "Example Home #1", "customName": "Example Home #1",
}, },
}, },
} },
)
)
device_manager.add_structure(
Structure.MakeStructure(
{ {
"name": f"enterprise/{PROJECT_ID}/structures/id-2", "name": f"enterprise/{PROJECT_ID}/structures/id-2",
"traits": { "traits": {
@ -874,8 +865,8 @@ async def test_config_entry_title_multiple_homes(
"customName": "Example Home #2", "customName": "Example Home #2",
}, },
}, },
} },
) ]
) )
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -923,18 +914,17 @@ async def test_title_failure_fallback(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_structure_missing_trait(hass: HomeAssistant, oauth, subscriber) -> None: async def test_structure_missing_trait(
hass: HomeAssistant, oauth: OAuthFixture, auth: FakeAuth
) -> None:
"""Test handling the case where a structure has no name set.""" """Test handling the case where a structure has no name set."""
device_manager = await subscriber.async_get_device_manager() auth.structures.append(
device_manager.add_structure( {
Structure.MakeStructure( "name": f"enterprise/{PROJECT_ID}/structures/id-1",
{ # Missing Info trait
"name": f"enterprise/{PROJECT_ID}/structures/id-1", "traits": {},
# Missing Info trait }
"traits": {},
}
)
) )
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -973,8 +963,7 @@ async def test_dhcp_discovery(
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_dhcp_discovery_with_creds( async def test_dhcp_discovery_with_creds(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Exercise discovery dhcp with no config present (can't run).""" """Exercise discovery dhcp with no config present (can't run)."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -1029,7 +1018,6 @@ async def test_dhcp_discovery_with_creds(
async def test_token_error( async def test_token_error(
hass: HomeAssistant, hass: HomeAssistant,
oauth: OAuthFixture, oauth: OAuthFixture,
subscriber: FakeSubscriber,
status_code: HTTPStatus, status_code: HTTPStatus,
error_reason: str, error_reason: str,
) -> None: ) -> None:
@ -1064,8 +1052,7 @@ async def test_token_error(
) )
async def test_existing_topic_and_subscription( async def test_existing_topic_and_subscription(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Test selecting existing user managed topic and subscription.""" """Test selecting existing user managed topic and subscription."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -1103,8 +1090,7 @@ async def test_existing_topic_and_subscription(
async def test_no_eligible_topics( async def test_no_eligible_topics(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Test the case where there are no eligible pub/sub topics.""" """Test the case where there are no eligible pub/sub topics."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -1127,8 +1113,7 @@ async def test_no_eligible_topics(
) )
async def test_list_topics_failure( async def test_list_topics_failure(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Test selecting existing user managed topic and subscription.""" """Test selecting existing user managed topic and subscription."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -1151,8 +1136,7 @@ async def test_list_topics_failure(
) )
async def test_list_subscriptions_failure( async def test_list_subscriptions_failure(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth: OAuthFixture,
subscriber,
) -> None: ) -> None:
"""Test selecting existing user managed topic and subscription.""" """Test selecting existing user managed topic and subscription."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@ -1,8 +1,8 @@
"""The tests for Nest device triggers.""" """The tests for Nest device triggers."""
from typing import Any from typing import Any
from unittest.mock import AsyncMock
from google_nest_sdm.event import EventMessage
import pytest import pytest
from pytest_unordered import unordered from pytest_unordered import unordered
@ -18,7 +18,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event
from tests.common import async_get_device_automations from tests.common import async_get_device_automations
@ -447,7 +447,7 @@ async def test_subscriber_automation(
service_calls: list[ServiceCall], service_calls: list[ServiceCall],
create_device: CreateDevice, create_device: CreateDevice,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
subscriber: FakeSubscriber, subscriber: AsyncMock,
) -> None: ) -> None:
"""Test end to end subscriber triggers automation.""" """Test end to end subscriber triggers automation."""
create_device.create( create_device.create(
@ -465,7 +465,7 @@ async def test_subscriber_automation(
assert await setup_automation(hass, device_entry.id, "camera_motion") assert await setup_automation(hass, device_entry.id, "camera_motion")
# Simulate a pubsub message received by the subscriber with a motion event # Simulate a pubsub message received by the subscriber with a motion event
event = EventMessage.create_event( event = create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": "2019-01-01T00:00:01Z", "timestamp": "2019-01-01T00:00:01Z",
@ -479,7 +479,6 @@ async def test_subscriber_automation(
}, },
}, },
}, },
auth=None,
) )
await subscriber.async_receive_event(event) await subscriber.async_receive_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -2,7 +2,7 @@
import datetime import datetime
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from google_nest_sdm.event import EventMessage, EventType from google_nest_sdm.event import EventMessage, EventType
@ -13,7 +13,7 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import DEVICE_ID, CreateDevice, FakeSubscriber from .common import DEVICE_ID, TEST_CLIP_URL, CreateDevice, create_nest_event
from .conftest import PlatformSetup from .conftest import PlatformSetup
EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
@ -31,14 +31,6 @@ def platforms() -> list[Platform]:
return [Platform.EVENT] return [Platform.EVENT]
@pytest.fixture(autouse=True)
def enable_prefetch(subscriber: FakeSubscriber) -> None:
"""Fixture to enable media fetching for tests to exercise."""
subscriber.cache_policy.fetch = True
with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5):
yield
@pytest.fixture @pytest.fixture
def device_type() -> str: def device_type() -> str:
"""Fixture for the type of device under test.""" """Fixture for the type of device under test."""
@ -80,7 +72,7 @@ def create_event_messages(
events: dict[str, Any], parameters: dict[str, Any] | None = None events: dict[str, Any], parameters: dict[str, Any] | None = None
) -> EventMessage: ) -> EventMessage:
"""Create an EventMessage for events.""" """Create an EventMessage for events."""
return EventMessage.create_event( return create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": utcnow().isoformat(timespec="seconds"), "timestamp": utcnow().isoformat(timespec="seconds"),
@ -90,7 +82,6 @@ def create_event_messages(
}, },
**(parameters if parameters else {}), **(parameters if parameters else {}),
}, },
auth=None,
) )
@ -152,7 +143,7 @@ def create_event_messages(
) )
async def test_receive_events( async def test_receive_events(
hass: HomeAssistant, hass: HomeAssistant,
subscriber: FakeSubscriber, subscriber: AsyncMock,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
create_device: CreateDevice, create_device: CreateDevice,
trait_types: list[TraitType], trait_types: list[TraitType],
@ -192,7 +183,7 @@ async def test_receive_events(
@pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)]) @pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)])
async def test_ignore_unrelated_event( async def test_ignore_unrelated_event(
hass: HomeAssistant, hass: HomeAssistant,
subscriber: FakeSubscriber, subscriber: AsyncMock,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
create_device: CreateDevice, create_device: CreateDevice,
trait_type: TraitType, trait_type: TraitType,
@ -222,7 +213,7 @@ async def test_ignore_unrelated_event(
@pytest.mark.freeze_time("2024-08-24T12:00:00Z") @pytest.mark.freeze_time("2024-08-24T12:00:00Z")
async def test_event_threads( async def test_event_threads(
hass: HomeAssistant, hass: HomeAssistant,
subscriber: FakeSubscriber, subscriber: AsyncMock,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
create_device: CreateDevice, create_device: CreateDevice,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
@ -275,7 +266,7 @@ async def test_event_threads(
}, },
EventType.CAMERA_CLIP_PREVIEW: { EventType.CAMERA_CLIP_PREVIEW: {
"eventSessionId": EVENT_SESSION_ID, "eventSessionId": EVENT_SESSION_ID,
"previewUrl": "http://example", "previewUrl": TEST_CLIP_URL,
}, },
}, },
parameters={"eventThreadState": "ENDED"}, parameters={"eventThreadState": "ENDED"},
@ -306,7 +297,7 @@ async def test_event_threads(
}, },
EventType.CAMERA_CLIP_PREVIEW: { EventType.CAMERA_CLIP_PREVIEW: {
"eventSessionId": EVENT_SESSION_ID2, "eventSessionId": EVENT_SESSION_ID2,
"previewUrl": "http://example", "previewUrl": TEST_CLIP_URL,
}, },
}, },
parameters={"eventThreadState": "ENDED"}, parameters={"eventThreadState": "ENDED"},

View File

@ -9,26 +9,38 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
import datetime import datetime
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import AsyncMock
from google_nest_sdm.device import Device import aiohttp
from google_nest_sdm.event import EventMessage
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import CreateDevice from .common import (
DEVICE_ID,
TEST_CLIP_URL,
TEST_IMAGE_URL,
CreateDevice,
PlatformSetup,
create_nest_event,
)
from tests.common import async_capture_events from tests.common import async_capture_events
DOMAIN = "nest" DOMAIN = "nest"
DEVICE_ID = "some-device-id"
PLATFORM = "camera" PLATFORM = "camera"
NEST_EVENT = "nest_event" NEST_EVENT = "nest_event"
EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
GENERATE_IMAGE_URL_RESPONSE = {
"results": {
"url": TEST_IMAGE_URL,
"token": "g.0.eventToken",
},
}
IMAGE_BYTES_FROM_EVENT = b"test url image bytes"
EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} EVENT_KEYS = {"device_id", "type", "timestamp", "zones"}
@ -104,7 +116,7 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None):
"""Create an EventMessage for events.""" """Create an EventMessage for events."""
if not timestamp: if not timestamp:
timestamp = utcnow() timestamp = utcnow()
return EventMessage.create_event( return create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": timestamp.isoformat(timespec="seconds"), "timestamp": timestamp.isoformat(timespec="seconds"),
@ -113,7 +125,6 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None):
"events": events, "events": events,
}, },
}, },
auth=None,
) )
@ -167,7 +178,7 @@ async def test_event(
entry = entity_registry.async_get("camera.front") entry = entity_registry.async_get("camera.front")
assert entry is not None assert entry is not None
assert entry.unique_id == "some-device-id-camera" assert entry.unique_id == f"{DEVICE_ID}-camera"
assert entry.domain == "camera" assert entry.domain == "camera"
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
@ -175,6 +186,11 @@ async def test_event(
assert device.model == expected_model assert device.model == expected_model
assert device.identifiers == {("nest", DEVICE_ID)} assert device.identifiers == {("nest", DEVICE_ID)}
auth.responses = [
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
]
timestamp = utcnow() timestamp = utcnow()
await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp)) await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp))
await hass.async_block_till_done() await hass.async_block_till_done()
@ -300,12 +316,11 @@ async def test_event_message_without_device_event(
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
await setup_platform() await setup_platform()
timestamp = utcnow() timestamp = utcnow()
event = EventMessage.create_event( event = create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": timestamp.isoformat(timespec="seconds"), "timestamp": timestamp.isoformat(timespec="seconds"),
}, },
auth=None,
) )
await subscriber.async_receive_event(event) await subscriber.async_receive_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -339,7 +354,7 @@ async def test_doorbell_event_thread(
}, },
"sdm.devices.events.CameraClipPreview.ClipPreview": { "sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID, "eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1", "previewUrl": TEST_CLIP_URL,
}, },
}, },
}, },
@ -356,9 +371,7 @@ async def test_doorbell_event_thread(
"eventThreadState": "STARTED", "eventThreadState": "STARTED",
} }
) )
await subscriber.async_receive_event( await subscriber.async_receive_event(create_nest_event(message_data_1))
EventMessage.create_event(message_data_1, auth=None)
)
# Publish message #2 that sends a no-op update to end the event thread # Publish message #2 that sends a no-op update to end the event thread
timestamp2 = timestamp1 + datetime.timedelta(seconds=1) timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
@ -369,9 +382,7 @@ async def test_doorbell_event_thread(
"eventThreadState": "ENDED", "eventThreadState": "ENDED",
} }
) )
await subscriber.async_receive_event( await subscriber.async_receive_event(create_nest_event(message_data_2))
EventMessage.create_event(message_data_2, auth=None)
)
await hass.async_block_till_done() await hass.async_block_till_done()
# The event is only published once # The event is only published once
@ -415,7 +426,7 @@ async def test_doorbell_event_session_update(
}, },
"sdm.devices.events.CameraClipPreview.ClipPreview": { "sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID, "eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1", "previewUrl": TEST_CLIP_URL,
}, },
}, },
timestamp=timestamp1, timestamp=timestamp1,
@ -437,7 +448,7 @@ async def test_doorbell_event_session_update(
}, },
"sdm.devices.events.CameraClipPreview.ClipPreview": { "sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID, "eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1", "previewUrl": TEST_CLIP_URL,
}, },
}, },
timestamp=timestamp2, timestamp=timestamp2,
@ -459,7 +470,11 @@ async def test_doorbell_event_session_update(
async def test_structure_update_event( async def test_structure_update_event(
hass: HomeAssistant, entity_registry: er.EntityRegistry, subscriber, setup_platform hass: HomeAssistant,
entity_registry: er.EntityRegistry,
subscriber: AsyncMock,
setup_platform: PlatformSetup,
create_device: CreateDevice,
) -> None: ) -> None:
"""Test a pubsub message for a new device being added.""" """Test a pubsub message for a new device being added."""
events = async_capture_events(hass, NEST_EVENT) events = async_capture_events(hass, NEST_EVENT)
@ -468,8 +483,8 @@ async def test_structure_update_event(
# Entity for first device is registered # Entity for first device is registered
assert entity_registry.async_get("camera.front") assert entity_registry.async_get("camera.front")
new_device = Device.MakeDevice( create_device.create(
{ raw_data={
"name": "device-id-2", "name": "device-id-2",
"type": "sdm.devices.types.CAMERA", "type": "sdm.devices.types.CAMERA",
"traits": { "traits": {
@ -479,16 +494,13 @@ async def test_structure_update_event(
"sdm.devices.traits.CameraLiveStream": {}, "sdm.devices.traits.CameraLiveStream": {},
}, },
}, },
auth=None,
) )
device_manager = await subscriber.async_get_device_manager()
device_manager.add_device(new_device)
# Entity for new devie has not yet been loaded # Entity for new devie has not yet been loaded
assert not entity_registry.async_get("camera.back") assert not entity_registry.async_get("camera.back")
# Send a message that triggers the device to be loaded # Send a message that triggers the device to be loaded
message = EventMessage.create_event( message = create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": utcnow().isoformat(timespec="seconds"), "timestamp": utcnow().isoformat(timespec="seconds"),
@ -498,17 +510,10 @@ async def test_structure_update_event(
"object": "enterprise/example/devices/some-device-id2", "object": "enterprise/example/devices/some-device-id2",
}, },
}, },
auth=None,
) )
with (
patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), await subscriber.async_receive_event(message)
patch( await hass.async_block_till_done()
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
),
):
await subscriber.async_receive_event(message)
await hass.async_block_till_done()
# No home assistant events published # No home assistant events published
assert not events assert not events

View File

@ -9,9 +9,9 @@ relevant modes.
""" """
from collections.abc import Generator from collections.abc import Generator
from http import HTTPStatus
import logging import logging
from typing import Any from unittest.mock import AsyncMock, patch
from unittest.mock import patch
from google_nest_sdm.exceptions import ( from google_nest_sdm.exceptions import (
ApiException, ApiException,
@ -29,11 +29,11 @@ from .common import (
PROJECT_ID, PROJECT_ID,
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_NEW_SUBSCRIPTION, TEST_CONFIG_NEW_SUBSCRIPTION,
FakeSubscriber,
PlatformSetup, PlatformSetup,
YieldFixture,
) )
from tests.test_util.aiohttp import AiohttpClientMocker
PLATFORM = "sensor" PLATFORM = "sensor"
@ -61,25 +61,6 @@ def warning_caplog(
yield caplog yield caplog
@pytest.fixture
def subscriber_side_effect() -> Any | None:
"""Fixture to inject failures into FakeSubscriber start."""
return None
@pytest.fixture
def failing_subscriber(
subscriber_side_effect: Any | None,
) -> YieldFixture[FakeSubscriber]:
"""Fixture overriding default subscriber behavior to allow failure injection."""
subscriber = FakeSubscriber()
with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber.start_async",
side_effect=subscriber_side_effect,
):
yield subscriber
async def test_setup_success( async def test_setup_success(
hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform
) -> None: ) -> None:
@ -125,10 +106,9 @@ async def test_setup_configuration_failure(
@pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()]) @pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()])
async def test_setup_susbcriber_failure( async def test_setup_subscriber_failure(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
failing_subscriber,
setup_base_platform, setup_base_platform,
) -> None: ) -> None:
"""Test configuration error.""" """Test configuration error."""
@ -145,7 +125,6 @@ async def test_setup_device_manager_failure(
) -> None: ) -> None:
"""Test device manager api failure.""" """Test device manager api failure."""
with ( with (
patch("homeassistant.components.nest.api.GoogleNestSubscriber.start_async"),
patch( patch(
"homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager",
side_effect=ApiException(), side_effect=ApiException(),
@ -165,7 +144,6 @@ async def test_subscriber_auth_failure(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
setup_base_platform, setup_base_platform,
failing_subscriber,
) -> None: ) -> None:
"""Test subscriber throws an authentication error.""" """Test subscriber throws an authentication error."""
await setup_base_platform() await setup_base_platform()
@ -184,7 +162,6 @@ async def test_subscriber_configuration_failure(
hass: HomeAssistant, hass: HomeAssistant,
error_caplog: pytest.LogCaptureFixture, error_caplog: pytest.LogCaptureFixture,
setup_base_platform, setup_base_platform,
failing_subscriber,
) -> None: ) -> None:
"""Test configuration error.""" """Test configuration error."""
await setup_base_platform() await setup_base_platform()
@ -210,14 +187,12 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None:
async def test_remove_entry( async def test_remove_entry(
hass: HomeAssistant, hass: HomeAssistant,
setup_base_platform, setup_base_platform: PlatformSetup,
aioclient_mock: AiohttpClientMocker,
subscriber: AsyncMock,
) -> None: ) -> None:
"""Test successful unload of a ConfigEntry.""" """Test successful unload of a ConfigEntry."""
with patch( await setup_base_platform()
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=FakeSubscriber(),
):
await setup_base_platform()
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1 assert len(entries) == 1
@ -227,14 +202,18 @@ async def test_remove_entry(
assert entry.data.get("subscriber_id") == SUBSCRIBER_ID assert entry.data.get("subscriber_id") == SUBSCRIBER_ID
assert entry.data.get("project_id") == PROJECT_ID assert entry.data.get("project_id") == PROJECT_ID
with ( aioclient_mock.clear_requests()
patch("homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id"), aioclient_mock.delete(
patch( f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}",
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", json={},
) as delete, )
):
assert await hass.config_entries.async_remove(entry.entry_id) assert not subscriber.stop.called
assert delete.called
assert await hass.config_entries.async_remove(entry.entry_id)
assert aioclient_mock.call_count == 1
assert subscriber.stop.called
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert not entries assert not entries
@ -243,7 +222,7 @@ async def test_remove_entry(
async def test_home_assistant_stop( async def test_home_assistant_stop(
hass: HomeAssistant, hass: HomeAssistant,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
subscriber: FakeSubscriber, subscriber: AsyncMock,
) -> None: ) -> None:
"""Test successful subscriber shutdown when HomeAssistant stops.""" """Test successful subscriber shutdown when HomeAssistant stops."""
await setup_platform() await setup_platform()
@ -253,31 +232,37 @@ async def test_home_assistant_stop(
entry = entries[0] entry = entries[0]
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
assert not subscriber.stop.called
await hass.async_stop() await hass.async_stop()
assert subscriber.stop_calls == 1 assert subscriber.stop.called
async def test_remove_entry_delete_subscriber_failure( async def test_remove_entry_delete_subscriber_failure(
hass: HomeAssistant, setup_base_platform hass: HomeAssistant,
setup_base_platform: PlatformSetup,
aioclient_mock: AiohttpClientMocker,
subscriber: AsyncMock,
) -> None: ) -> None:
"""Test a failure when deleting the subscription.""" """Test a failure when deleting the subscription."""
with patch( await setup_base_platform()
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=FakeSubscriber(),
):
await setup_base_platform()
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1 assert len(entries) == 1
entry = entries[0] entry = entries[0]
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with patch( aioclient_mock.clear_requests()
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", aioclient_mock.delete(
side_effect=SubscriberException(), f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}",
) as delete: status=HTTPStatus.NOT_FOUND,
assert await hass.config_entries.async_remove(entry.entry_id) )
assert delete.called
assert not subscriber.stop.called
assert await hass.config_entries.async_remove(entry.entry_id)
assert aioclient_mock.call_count == 1
assert subscriber.stop.called
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert not entries assert not entries

View File

@ -13,7 +13,6 @@ from unittest.mock import patch
import aiohttp import aiohttp
import av import av
from google_nest_sdm.event import EventMessage
import numpy as np import numpy as np
import pytest import pytest
@ -31,7 +30,14 @@ from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .common import DEVICE_ID, CreateDevice, FakeSubscriber from .common import (
DEVICE_ID,
TEST_CLIP_URL,
TEST_IMAGE_URL,
CreateDevice,
create_nest_event,
)
from .conftest import FakeAuth
from tests.common import MockUser, async_capture_events from tests.common import MockUser, async_capture_events
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -70,7 +76,6 @@ BATTERY_CAMERA_TRAITS = {
PERSON_EVENT = "sdm.devices.events.CameraPerson.Person" PERSON_EVENT = "sdm.devices.events.CameraPerson.Person"
MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion" MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion"
TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..."
GENERATE_IMAGE_URL_RESPONSE = { GENERATE_IMAGE_URL_RESPONSE = {
"results": { "results": {
"url": TEST_IMAGE_URL, "url": TEST_IMAGE_URL,
@ -162,12 +167,6 @@ def mp4() -> io.BytesIO:
return output return output
@pytest.fixture(autouse=True)
def enable_prefetch(subscriber: FakeSubscriber) -> None:
"""Fixture to enable media fetching for tests to exercise."""
subscriber.cache_policy.fetch = True
@pytest.fixture @pytest.fixture
def cache_size() -> int: def cache_size() -> int:
"""Fixture for overrideing cache size.""" """Fixture for overrideing cache size."""
@ -200,7 +199,7 @@ def create_event_message(event_data, timestamp, device_id=None):
"""Create an EventMessage for a single event type.""" """Create an EventMessage for a single event type."""
if device_id is None: if device_id is None:
device_id = DEVICE_ID device_id = DEVICE_ID
return EventMessage.create_event( return create_nest_event(
{ {
"eventId": f"{EVENT_ID}-{timestamp}", "eventId": f"{EVENT_ID}-{timestamp}",
"timestamp": timestamp.isoformat(timespec="seconds"), "timestamp": timestamp.isoformat(timespec="seconds"),
@ -209,7 +208,6 @@ def create_event_message(event_data, timestamp, device_id=None):
"events": event_data, "events": event_data,
}, },
}, },
auth=None,
) )
@ -224,7 +222,7 @@ def create_battery_event_data(
}, },
"sdm.devices.events.CameraClipPreview.ClipPreview": { "sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": event_session_id, "eventSessionId": event_session_id,
"previewUrl": "https://127.0.0.1/example", "previewUrl": TEST_CLIP_URL,
}, },
} }
@ -284,7 +282,9 @@ async def test_supported_device(
assert len(browse.children) == 0 assert len(browse.children) == 0
async def test_integration_unloaded(hass: HomeAssistant, auth, setup_platform) -> None: async def test_integration_unloaded(
hass: HomeAssistant, auth: FakeAuth, setup_platform
) -> None:
"""Test the media player loads, but has no devices, when config unloaded.""" """Test the media player loads, but has no devices, when config unloaded."""
await setup_platform() await setup_platform()
@ -1257,6 +1257,7 @@ async def test_media_store_save_filesystem_error(
assert response.status == HTTPStatus.NOT_FOUND, f"Response not matched: {response}" assert response.status == HTTPStatus.NOT_FOUND, f"Response not matched: {response}"
@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
async def test_media_store_load_filesystem_error( async def test_media_store_load_filesystem_error(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,

View File

@ -5,8 +5,8 @@ pubsub subscriber.
""" """
from typing import Any from typing import Any
from unittest.mock import AsyncMock
from google_nest_sdm.event import EventMessage
import pytest import pytest
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -25,7 +25,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event
@pytest.fixture @pytest.fixture
@ -198,7 +198,7 @@ async def test_device_name_from_structure(
async def test_event_updates_sensor( async def test_event_updates_sensor(
hass: HomeAssistant, hass: HomeAssistant,
subscriber: FakeSubscriber, subscriber: AsyncMock,
create_device: CreateDevice, create_device: CreateDevice,
setup_platform: PlatformSetup, setup_platform: PlatformSetup,
) -> None: ) -> None:
@ -217,7 +217,7 @@ async def test_event_updates_sensor(
assert temperature.state == "25.1" assert temperature.state == "25.1"
# Simulate a pubsub message received by the subscriber with a trait update # Simulate a pubsub message received by the subscriber with a trait update
event = EventMessage.create_event( event = create_nest_event(
{ {
"eventId": "some-event-id", "eventId": "some-event-id",
"timestamp": "2019-01-01T00:00:01Z", "timestamp": "2019-01-01T00:00:01Z",
@ -230,7 +230,6 @@ async def test_event_updates_sensor(
}, },
}, },
}, },
auth=None,
) )
await subscriber.async_receive_event(event) await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal await hass.async_block_till_done() # Process dispatch/update signal