mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Bump python-google-nest-sdm to 7.0.0 (#134016)
Update python-google-nest-sdm to 7.0.0
This commit is contained in:
parent
299250ebec
commit
c75222e63c
@ -55,6 +55,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
from .const import (
|
||||
CONF_CLOUD_PROJECT_ID,
|
||||
CONF_PROJECT_ID,
|
||||
CONF_SUBSCRIBER_ID,
|
||||
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)
|
||||
subscriber.set_update_callback(update_callback.async_handle_event)
|
||||
try:
|
||||
await subscriber.start_async()
|
||||
unsub = await subscriber.start_async()
|
||||
except AuthException as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Subscriber authentication error: {err!s}"
|
||||
) from err
|
||||
except ConfigurationException as err:
|
||||
_LOGGER.error("Configuration error: %s", err)
|
||||
subscriber.stop_async()
|
||||
return False
|
||||
except SubscriberException as err:
|
||||
subscriber.stop_async()
|
||||
raise ConfigEntryNotReady(f"Subscriber error: {err!s}") from err
|
||||
|
||||
try:
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
except ApiException as err:
|
||||
subscriber.stop_async()
|
||||
unsub()
|
||||
raise ConfigEntryNotReady(f"Device manager error: {err!s}") from err
|
||||
|
||||
@callback
|
||||
def on_hass_stop(_: Event) -> None:
|
||||
"""Close connection when hass stops."""
|
||||
subscriber.stop_async()
|
||||
unsub()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||
)
|
||||
|
||||
entry.async_on_unload(unsub)
|
||||
entry.runtime_data = NestData(
|
||||
subscriber=subscriber,
|
||||
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:
|
||||
"""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)
|
||||
|
||||
|
||||
@ -272,24 +267,25 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
or CONF_SUBSCRIBER_ID_IMPORTED in entry.data
|
||||
):
|
||||
return
|
||||
|
||||
subscriber = await api.new_subscriber(hass, entry)
|
||||
if not subscriber:
|
||||
return
|
||||
_LOGGER.debug("Deleting subscriber '%s'", subscriber.subscriber_id)
|
||||
if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None:
|
||||
subscription_name = entry.data[CONF_SUBSCRIBER_ID]
|
||||
admin_client = api.new_pubsub_admin_client(
|
||||
hass,
|
||||
access_token=entry.data["token"]["access_token"],
|
||||
cloud_project_id=entry.data[CONF_CLOUD_PROJECT_ID],
|
||||
)
|
||||
_LOGGER.debug("Deleting subscription '%s'", subscription_name)
|
||||
try:
|
||||
await subscriber.delete_subscription()
|
||||
except (AuthException, SubscriberException) as err:
|
||||
await admin_client.delete_subscription(subscription_name)
|
||||
except ApiException as err:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Unable to delete subscription '%s'; Will be automatically cleaned up"
|
||||
" by cloud console: %s"
|
||||
),
|
||||
subscriber.subscriber_id,
|
||||
subscription_name,
|
||||
err,
|
||||
)
|
||||
finally:
|
||||
subscriber.stop_async()
|
||||
|
||||
|
||||
class NestEventViewBase(HomeAssistantView, ABC):
|
||||
|
@ -19,5 +19,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"requirements": ["google-nest-sdm==6.1.5"]
|
||||
"requirements": ["google-nest-sdm==7.0.0"]
|
||||
}
|
||||
|
@ -1024,7 +1024,7 @@ google-cloud-texttospeech==2.17.2
|
||||
google-generativeai==0.8.2
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==6.1.5
|
||||
google-nest-sdm==7.0.0
|
||||
|
||||
# homeassistant.components.google_photos
|
||||
google-photos-library-api==0.12.1
|
||||
|
@ -874,7 +874,7 @@ google-cloud-texttospeech==2.17.2
|
||||
google-generativeai==0.8.2
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==6.1.5
|
||||
google-nest-sdm==7.0.0
|
||||
|
||||
# homeassistant.components.google_photos
|
||||
google-photos-library-api==0.12.1
|
||||
|
@ -5,17 +5,14 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from google_nest_sdm.auth import AbstractAuth
|
||||
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 google_nest_sdm.streaming_manager import Message
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.components.nest import DOMAIN
|
||||
from homeassistant.components.nest.const import API_URL
|
||||
|
||||
# Typing helpers
|
||||
type PlatformSetup = Callable[[], Awaitable[None]]
|
||||
@ -66,68 +63,22 @@ TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig(
|
||||
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
|
||||
)
|
||||
|
||||
|
||||
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_ID = "enterprises/project-id/devices/device-id"
|
||||
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:
|
||||
"""Fixture used for creating devices."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_manager: DeviceManager,
|
||||
auth: AbstractAuth,
|
||||
) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize CreateDevice."""
|
||||
self.device_manager = device_manager
|
||||
self.auth = auth
|
||||
self.data = {"traits": {}}
|
||||
self.devices = []
|
||||
|
||||
def create(
|
||||
self,
|
||||
@ -138,4 +89,9 @@ class CreateDevice:
|
||||
data = copy.deepcopy(self.data)
|
||||
data.update(raw_data if raw_data 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)
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import AbstractEventLoop
|
||||
from collections.abc import Generator
|
||||
import copy
|
||||
import shutil
|
||||
@ -13,100 +12,131 @@ import uuid
|
||||
|
||||
import aiohttp
|
||||
from google_nest_sdm import diagnostics
|
||||
from google_nest_sdm.auth import AbstractAuth
|
||||
from google_nest_sdm.device_manager import DeviceManager
|
||||
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
||||
from google_nest_sdm.streaming_manager import StreamingManager
|
||||
import pytest
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
async_import_client_credential,
|
||||
)
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import (
|
||||
DEVICE_ID,
|
||||
DEVICE_URL_MATCH,
|
||||
PROJECT_ID,
|
||||
SUBSCRIBER_ID,
|
||||
TEST_CLIP_URL,
|
||||
TEST_CONFIG_APP_CREDS,
|
||||
TEST_IMAGE_URL,
|
||||
CreateDevice,
|
||||
FakeSubscriber,
|
||||
NestTestConfig,
|
||||
PlatformSetup,
|
||||
YieldFixture,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||
|
||||
FAKE_TOKEN = "some-token"
|
||||
FAKE_REFRESH_TOKEN = "some-refresh-token"
|
||||
|
||||
|
||||
class FakeAuth(AbstractAuth):
|
||||
"""A fake implementation of the auth class that records requests.
|
||||
class FakeAuth:
|
||||
"""A fixture for request handling that records requests.
|
||||
|
||||
This class captures the outgoing requests, and can also be used by
|
||||
tests to set up fake responses. This class is registered as a response
|
||||
handler for a fake aiohttp_server and can simulate successes or failures
|
||||
from the API.
|
||||
This class is used with AiohttpClientMocker to capture outgoing requests
|
||||
and can also be used by tests to set up fake responses.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_factory: CreateDevice,
|
||||
project_id: str,
|
||||
) -> None:
|
||||
"""Initialize FakeAuth."""
|
||||
super().__init__(None, None)
|
||||
# Tests can set fake responses here.
|
||||
self.responses = []
|
||||
# Tests can factory fixture to create fake device responses.
|
||||
self.device_factory = device_factory
|
||||
# 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.
|
||||
self.method = None
|
||||
self.url = None
|
||||
self.json = None
|
||||
self.headers = None
|
||||
self.captured_requests = []
|
||||
# Set up by fixture
|
||||
self.client = None
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
return ""
|
||||
# API makes a call to request structures to initiate pubsub feed, but the
|
||||
# integration does not use this.
|
||||
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."""
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.json = kwargs.get("json")
|
||||
self.headers = kwargs.get("headers")
|
||||
self.captured_requests.append((method, url, self.json, self.headers))
|
||||
return await self.client.get("/")
|
||||
str_url = str(url)
|
||||
self.url = str_url[len(API_URL) + 1 :]
|
||||
self.json = data
|
||||
self.captured_requests.append((method, url, self.json))
|
||||
|
||||
async def response_handler(self, request):
|
||||
"""Handle fake responess for aiohttp_server."""
|
||||
if len(self.responses) > 0:
|
||||
return self.responses.pop(0)
|
||||
return aiohttp.web.json_response()
|
||||
response = self.responses.pop(0)
|
||||
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
|
||||
def aiohttp_client(
|
||||
event_loop: AbstractEventLoop,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
socket_enabled: None,
|
||||
) -> ClientSessionGenerator:
|
||||
"""Return aiohttp_client and allow opening sockets."""
|
||||
return aiohttp_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def auth(aiohttp_client: ClientSessionGenerator) -> FakeAuth:
|
||||
async def auth(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
create_device: CreateDevice,
|
||||
device_access_project_id: str,
|
||||
) -> FakeAuth:
|
||||
"""Fixture for an AbstractAuth."""
|
||||
auth = FakeAuth()
|
||||
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
|
||||
return FakeAuth(aioclient_mock, create_device, device_access_project_id)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -119,20 +149,32 @@ def cleanup_media_storage(hass: HomeAssistant) -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subscriber() -> YieldFixture[FakeSubscriber]:
|
||||
"""Set up the FakeSusbcriber."""
|
||||
subscriber = FakeSubscriber()
|
||||
def subscriber_side_effect() -> Any | None:
|
||||
"""Fixture to inject failures into FakeSubscriber start."""
|
||||
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(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber",
|
||||
return_value=subscriber,
|
||||
):
|
||||
yield subscriber
|
||||
"google_nest_sdm.google_nest_subscriber.StreamingManager", spec=StreamingManager
|
||||
) as mock_manager:
|
||||
# Use side_effect to capture the callback
|
||||
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
|
||||
def mock_subscriber() -> YieldFixture[AsyncMock]:
|
||||
"""Fixture for injecting errors into the subscriber."""
|
||||
mock_subscriber = AsyncMock(FakeSubscriber)
|
||||
mock_subscriber = AsyncMock(GoogleNestSubscriber)
|
||||
with patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber",
|
||||
return_value=mock_subscriber,
|
||||
@ -140,12 +182,6 @@ def mock_subscriber() -> YieldFixture[AsyncMock]:
|
||||
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
|
||||
async def device_id() -> str:
|
||||
"""Fixture to set default device id used when creating devices."""
|
||||
@ -166,14 +202,12 @@ async def device_traits() -> dict[str, Any]:
|
||||
|
||||
@pytest.fixture
|
||||
async def create_device(
|
||||
device_manager: DeviceManager,
|
||||
auth: FakeAuth,
|
||||
device_id: str,
|
||||
device_type: str,
|
||||
device_traits: dict[str, Any],
|
||||
) -> CreateDevice:
|
||||
"""Fixture for creating devices."""
|
||||
factory = CreateDevice(device_manager, auth)
|
||||
factory = CreateDevice()
|
||||
factory.data.update(
|
||||
{
|
||||
"name": device_id,
|
||||
@ -262,6 +296,7 @@ async def setup_base_platform(
|
||||
hass: HomeAssistant,
|
||||
platforms: list[str],
|
||||
config_entry: MockConfigEntry | None,
|
||||
auth: FakeAuth,
|
||||
) -> YieldFixture[PlatformSetup]:
|
||||
"""Fixture to setup the integration platform."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@ -278,7 +313,8 @@ async def setup_base_platform(
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_platform(
|
||||
setup_base_platform: PlatformSetup, subscriber: FakeSubscriber
|
||||
setup_base_platform: PlatformSetup,
|
||||
subscriber: AsyncMock,
|
||||
) -> PlatformSetup:
|
||||
"""Fixture to setup the integration platform and subscriber."""
|
||||
return setup_base_platform
|
||||
|
@ -31,6 +31,9 @@
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
'subscriber': dict({
|
||||
'start': 1,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_device_diagnostics
|
||||
@ -85,5 +88,8 @@
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
'subscriber': dict({
|
||||
'start': 1,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
@ -9,16 +9,16 @@ The tests below exercise both cases during integration setup.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@ -27,7 +27,7 @@ FAKE_UPDATED_TOKEN = "fake-updated-token"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subscriber() -> FakeSubscriber | None:
|
||||
def subscriber() -> Mock | None:
|
||||
"""Disable default subscriber since tests use their own patch."""
|
||||
return None
|
||||
|
||||
@ -54,16 +54,16 @@ async def test_auth(
|
||||
# Prepare to capture credentials for Subscriber
|
||||
captured_creds = None
|
||||
|
||||
async def async_new_subscriber(
|
||||
creds, subscription_name, event_loop, async_callback
|
||||
) -> GoogleNestSubscriber | None:
|
||||
def async_new_subscriber(
|
||||
credentials: Credentials,
|
||||
) -> Mock:
|
||||
"""Capture credentials for tests."""
|
||||
nonlocal captured_creds
|
||||
captured_creds = creds
|
||||
return None # GoogleNestSubscriber
|
||||
captured_creds = credentials
|
||||
return AsyncMock()
|
||||
|
||||
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,
|
||||
) as new_subscriber_mock:
|
||||
await setup_platform()
|
||||
@ -122,16 +122,16 @@ async def test_auth_expired_token(
|
||||
# Prepare to capture credentials for Subscriber
|
||||
captured_creds = None
|
||||
|
||||
async def async_new_subscriber(
|
||||
creds, subscription_name, event_loop, async_callback
|
||||
) -> GoogleNestSubscriber | None:
|
||||
def async_new_subscriber(
|
||||
credentials: Credentials,
|
||||
) -> Mock:
|
||||
"""Capture credentials for tests."""
|
||||
nonlocal captured_creds
|
||||
captured_creds = creds
|
||||
return None # GoogleNestSubscriber
|
||||
captured_creds = credentials
|
||||
return AsyncMock()
|
||||
|
||||
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,
|
||||
) as new_subscriber_mock:
|
||||
await setup_platform()
|
||||
|
@ -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.util.dt import utcnow
|
||||
|
||||
from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup
|
||||
from .common import DEVICE_ID, CreateDevice, PlatformSetup
|
||||
from .conftest import FakeAuth
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
@ -520,13 +520,10 @@ async def test_camera_removed(
|
||||
hass: HomeAssistant,
|
||||
auth: FakeAuth,
|
||||
camera_device: None,
|
||||
subscriber: FakeSubscriber,
|
||||
setup_platform: PlatformSetup,
|
||||
) -> None:
|
||||
"""Test case where entities are removed and stream tokens revoked."""
|
||||
await setup_platform()
|
||||
# Simplify test setup
|
||||
subscriber.cache_policy.fetch = False
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
cam = hass.states.get("camera.my_camera")
|
||||
@ -780,7 +777,7 @@ async def test_camera_web_rtc_offer_failure(
|
||||
assert response["event"] == {
|
||||
"type": "error",
|
||||
"code": "webrtc_offer_failed",
|
||||
"message": "Nest API error: Bad Request response from API (400)",
|
||||
"message": "Nest API error: response from API (400)",
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,10 +7,9 @@ pubsub subscriber.
|
||||
from collections.abc import Awaitable, Callable
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import aiohttp
|
||||
from google_nest_sdm.auth import AbstractAuth
|
||||
from google_nest_sdm.event import EventMessage
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@ -45,8 +44,8 @@ from .common import (
|
||||
DEVICE_COMMAND,
|
||||
DEVICE_ID,
|
||||
CreateDevice,
|
||||
FakeSubscriber,
|
||||
PlatformSetup,
|
||||
create_nest_event,
|
||||
)
|
||||
from .conftest import FakeAuth
|
||||
|
||||
@ -72,14 +71,13 @@ def device_traits() -> dict[str, Any]:
|
||||
@pytest.fixture
|
||||
async def create_event(
|
||||
hass: HomeAssistant,
|
||||
auth: AbstractAuth,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
) -> CreateEvent:
|
||||
"""Fixture to send a pub/sub event."""
|
||||
|
||||
async def create_event(traits: dict[str, Any]) -> None:
|
||||
await subscriber.async_receive_event(
|
||||
EventMessage.create_event(
|
||||
create_nest_event(
|
||||
{
|
||||
"eventId": EVENT_ID,
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
@ -88,7 +86,6 @@ async def create_event(
|
||||
"traits": traits,
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -7,7 +7,6 @@ from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from google_nest_sdm.exceptions import AuthException
|
||||
from google_nest_sdm.structure import Structure
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -25,9 +24,9 @@ from .common import (
|
||||
SUBSCRIBER_ID,
|
||||
TEST_CONFIG_APP_CREDS,
|
||||
TEST_CONFIGFLOW_APP_CREDS,
|
||||
FakeSubscriber,
|
||||
NestTestConfig,
|
||||
)
|
||||
from .conftest import FakeAuth, PlatformSetup
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@ -58,6 +57,11 @@ def mock_rand_topic_name_fixture() -> None:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_request_setup(auth: FakeAuth) -> None:
|
||||
"""Fixture to ensure fake requests are setup."""
|
||||
|
||||
|
||||
class OAuthFixture:
|
||||
"""Simulate the oauth flow used by the config flow."""
|
||||
|
||||
@ -257,12 +261,6 @@ def mock_subscriptions() -> list[tuple[str, str]]:
|
||||
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")
|
||||
def mock_cloud_project_id() -> str:
|
||||
"""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)])
|
||||
async def test_app_credentials(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Check full flow."""
|
||||
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"),
|
||||
[(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."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
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)])
|
||||
async def test_config_flow_wrong_project_id(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Check the case where the wrong project ids are entered."""
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
mock_subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Check full flow fails with configuration error."""
|
||||
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)],
|
||||
)
|
||||
async def test_config_flow_pubsub_subscriber_error(
|
||||
hass: HomeAssistant, oauth, mock_subscriber
|
||||
hass: HomeAssistant,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Check full flow with a subscriber error."""
|
||||
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)])
|
||||
async def test_pubsub_subscription_strip_whitespace(
|
||||
hass: HomeAssistant, oauth, subscriber
|
||||
hass: HomeAssistant,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Check that project id has whitespace stripped on entry."""
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
setup_platform,
|
||||
subscriber,
|
||||
config_entry,
|
||||
oauth: OAuthFixture,
|
||||
setup_platform: PlatformSetup,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the pubsub subscriber id is preserved during reauth."""
|
||||
await setup_platform()
|
||||
@ -805,22 +801,21 @@ async def test_pubsub_subscriber_config_entry_reauth(
|
||||
|
||||
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
|
||||
async def test_config_entry_title_from_home(
|
||||
hass: HomeAssistant, oauth, subscriber
|
||||
hass: HomeAssistant,
|
||||
oauth: OAuthFixture,
|
||||
auth: FakeAuth,
|
||||
) -> None:
|
||||
"""Test that the Google Home name is used for the config entry title."""
|
||||
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
device_manager.add_structure(
|
||||
Structure.MakeStructure(
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/some-structure-id",
|
||||
"traits": {
|
||||
"sdm.structures.traits.Info": {
|
||||
"customName": "Example Home",
|
||||
},
|
||||
auth.structures.append(
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/some-structure-id",
|
||||
"traits": {
|
||||
"sdm.structures.traits.Info": {
|
||||
"customName": "Example Home",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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)])
|
||||
async def test_config_entry_title_multiple_homes(
|
||||
hass: HomeAssistant, oauth, subscriber
|
||||
hass: HomeAssistant,
|
||||
oauth: OAuthFixture,
|
||||
auth: FakeAuth,
|
||||
) -> None:
|
||||
"""Test handling of multiple Google Homes authorized."""
|
||||
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
device_manager.add_structure(
|
||||
Structure.MakeStructure(
|
||||
auth.structures.extend(
|
||||
[
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/id-1",
|
||||
"traits": {
|
||||
@ -862,11 +857,7 @@ async def test_config_entry_title_multiple_homes(
|
||||
"customName": "Example Home #1",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
device_manager.add_structure(
|
||||
Structure.MakeStructure(
|
||||
},
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/id-2",
|
||||
"traits": {
|
||||
@ -874,8 +865,8 @@ async def test_config_entry_title_multiple_homes(
|
||||
"customName": "Example Home #2",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
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)])
|
||||
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."""
|
||||
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
device_manager.add_structure(
|
||||
Structure.MakeStructure(
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/id-1",
|
||||
# Missing Info trait
|
||||
"traits": {},
|
||||
}
|
||||
)
|
||||
auth.structures.append(
|
||||
{
|
||||
"name": f"enterprise/{PROJECT_ID}/structures/id-1",
|
||||
# Missing Info trait
|
||||
"traits": {},
|
||||
}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -973,8 +963,7 @@ async def test_dhcp_discovery(
|
||||
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
|
||||
async def test_dhcp_discovery_with_creds(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Exercise discovery dhcp with no config present (can't run)."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -1029,7 +1018,6 @@ async def test_dhcp_discovery_with_creds(
|
||||
async def test_token_error(
|
||||
hass: HomeAssistant,
|
||||
oauth: OAuthFixture,
|
||||
subscriber: FakeSubscriber,
|
||||
status_code: HTTPStatus,
|
||||
error_reason: str,
|
||||
) -> None:
|
||||
@ -1064,8 +1052,7 @@ async def test_token_error(
|
||||
)
|
||||
async def test_existing_topic_and_subscription(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Test selecting existing user managed topic and subscription."""
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Test the case where there are no eligible pub/sub topics."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -1127,8 +1113,7 @@ async def test_no_eligible_topics(
|
||||
)
|
||||
async def test_list_topics_failure(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Test selecting existing user managed topic and subscription."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -1151,8 +1136,7 @@ async def test_list_topics_failure(
|
||||
)
|
||||
async def test_list_subscriptions_failure(
|
||||
hass: HomeAssistant,
|
||||
oauth,
|
||||
subscriber,
|
||||
oauth: OAuthFixture,
|
||||
) -> None:
|
||||
"""Test selecting existing user managed topic and subscription."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""The tests for Nest device triggers."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_nest_sdm.event import EventMessage
|
||||
import pytest
|
||||
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.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
|
||||
|
||||
@ -447,7 +447,7 @@ async def test_subscriber_automation(
|
||||
service_calls: list[ServiceCall],
|
||||
create_device: CreateDevice,
|
||||
setup_platform: PlatformSetup,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
) -> None:
|
||||
"""Test end to end subscriber triggers automation."""
|
||||
create_device.create(
|
||||
@ -465,7 +465,7 @@ async def test_subscriber_automation(
|
||||
assert await setup_automation(hass, device_entry.id, "camera_motion")
|
||||
|
||||
# Simulate a pubsub message received by the subscriber with a motion event
|
||||
event = EventMessage.create_event(
|
||||
event = create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
@ -479,7 +479,6 @@ async def test_subscriber_automation(
|
||||
},
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
await subscriber.async_receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from google_nest_sdm.event import EventMessage, EventType
|
||||
@ -13,7 +13,7 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
|
||||
EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
|
||||
@ -31,14 +31,6 @@ def platforms() -> list[Platform]:
|
||||
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
|
||||
def device_type() -> str:
|
||||
"""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
|
||||
) -> EventMessage:
|
||||
"""Create an EventMessage for events."""
|
||||
return EventMessage.create_event(
|
||||
return create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": utcnow().isoformat(timespec="seconds"),
|
||||
@ -90,7 +82,6 @@ def create_event_messages(
|
||||
},
|
||||
**(parameters if parameters else {}),
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
|
||||
|
||||
@ -152,7 +143,7 @@ def create_event_messages(
|
||||
)
|
||||
async def test_receive_events(
|
||||
hass: HomeAssistant,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
setup_platform: PlatformSetup,
|
||||
create_device: CreateDevice,
|
||||
trait_types: list[TraitType],
|
||||
@ -192,7 +183,7 @@ async def test_receive_events(
|
||||
@pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)])
|
||||
async def test_ignore_unrelated_event(
|
||||
hass: HomeAssistant,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
setup_platform: PlatformSetup,
|
||||
create_device: CreateDevice,
|
||||
trait_type: TraitType,
|
||||
@ -222,7 +213,7 @@ async def test_ignore_unrelated_event(
|
||||
@pytest.mark.freeze_time("2024-08-24T12:00:00Z")
|
||||
async def test_event_threads(
|
||||
hass: HomeAssistant,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
setup_platform: PlatformSetup,
|
||||
create_device: CreateDevice,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@ -275,7 +266,7 @@ async def test_event_threads(
|
||||
},
|
||||
EventType.CAMERA_CLIP_PREVIEW: {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "http://example",
|
||||
"previewUrl": TEST_CLIP_URL,
|
||||
},
|
||||
},
|
||||
parameters={"eventThreadState": "ENDED"},
|
||||
@ -306,7 +297,7 @@ async def test_event_threads(
|
||||
},
|
||||
EventType.CAMERA_CLIP_PREVIEW: {
|
||||
"eventSessionId": EVENT_SESSION_ID2,
|
||||
"previewUrl": "http://example",
|
||||
"previewUrl": TEST_CLIP_URL,
|
||||
},
|
||||
},
|
||||
parameters={"eventThreadState": "ENDED"},
|
||||
|
@ -9,26 +9,38 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.event import EventMessage
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
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
|
||||
|
||||
DOMAIN = "nest"
|
||||
DEVICE_ID = "some-device-id"
|
||||
PLATFORM = "camera"
|
||||
NEST_EVENT = "nest_event"
|
||||
EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
|
||||
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"}
|
||||
|
||||
@ -104,7 +116,7 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None):
|
||||
"""Create an EventMessage for events."""
|
||||
if not timestamp:
|
||||
timestamp = utcnow()
|
||||
return EventMessage.create_event(
|
||||
return create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": timestamp.isoformat(timespec="seconds"),
|
||||
@ -113,7 +125,6 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None):
|
||||
"events": events,
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
|
||||
|
||||
@ -167,7 +178,7 @@ async def test_event(
|
||||
|
||||
entry = entity_registry.async_get("camera.front")
|
||||
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"
|
||||
|
||||
device = device_registry.async_get(entry.device_id)
|
||||
@ -175,6 +186,11 @@ async def test_event(
|
||||
assert device.model == expected_model
|
||||
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()
|
||||
await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp))
|
||||
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)
|
||||
await setup_platform()
|
||||
timestamp = utcnow()
|
||||
event = EventMessage.create_event(
|
||||
event = create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": timestamp.isoformat(timespec="seconds"),
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
await subscriber.async_receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
@ -339,7 +354,7 @@ async def test_doorbell_event_thread(
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "image-url-1",
|
||||
"previewUrl": TEST_CLIP_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -356,9 +371,7 @@ async def test_doorbell_event_thread(
|
||||
"eventThreadState": "STARTED",
|
||||
}
|
||||
)
|
||||
await subscriber.async_receive_event(
|
||||
EventMessage.create_event(message_data_1, auth=None)
|
||||
)
|
||||
await subscriber.async_receive_event(create_nest_event(message_data_1))
|
||||
|
||||
# Publish message #2 that sends a no-op update to end the event thread
|
||||
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
|
||||
@ -369,9 +382,7 @@ async def test_doorbell_event_thread(
|
||||
"eventThreadState": "ENDED",
|
||||
}
|
||||
)
|
||||
await subscriber.async_receive_event(
|
||||
EventMessage.create_event(message_data_2, auth=None)
|
||||
)
|
||||
await subscriber.async_receive_event(create_nest_event(message_data_2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The event is only published once
|
||||
@ -415,7 +426,7 @@ async def test_doorbell_event_session_update(
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "image-url-1",
|
||||
"previewUrl": TEST_CLIP_URL,
|
||||
},
|
||||
},
|
||||
timestamp=timestamp1,
|
||||
@ -437,7 +448,7 @@ async def test_doorbell_event_session_update(
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "image-url-1",
|
||||
"previewUrl": TEST_CLIP_URL,
|
||||
},
|
||||
},
|
||||
timestamp=timestamp2,
|
||||
@ -459,7 +470,11 @@ async def test_doorbell_event_session_update(
|
||||
|
||||
|
||||
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:
|
||||
"""Test a pubsub message for a new device being added."""
|
||||
events = async_capture_events(hass, NEST_EVENT)
|
||||
@ -468,8 +483,8 @@ async def test_structure_update_event(
|
||||
# Entity for first device is registered
|
||||
assert entity_registry.async_get("camera.front")
|
||||
|
||||
new_device = Device.MakeDevice(
|
||||
{
|
||||
create_device.create(
|
||||
raw_data={
|
||||
"name": "device-id-2",
|
||||
"type": "sdm.devices.types.CAMERA",
|
||||
"traits": {
|
||||
@ -479,16 +494,13 @@ async def test_structure_update_event(
|
||||
"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
|
||||
assert not entity_registry.async_get("camera.back")
|
||||
|
||||
# Send a message that triggers the device to be loaded
|
||||
message = EventMessage.create_event(
|
||||
message = create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": utcnow().isoformat(timespec="seconds"),
|
||||
@ -498,17 +510,10 @@ async def test_structure_update_event(
|
||||
"object": "enterprise/example/devices/some-device-id2",
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
with (
|
||||
patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]),
|
||||
patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber",
|
||||
return_value=subscriber,
|
||||
),
|
||||
):
|
||||
await subscriber.async_receive_event(message)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await subscriber.async_receive_event(message)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# No home assistant events published
|
||||
assert not events
|
||||
|
@ -9,9 +9,9 @@ relevant modes.
|
||||
"""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from google_nest_sdm.exceptions import (
|
||||
ApiException,
|
||||
@ -29,11 +29,11 @@ from .common import (
|
||||
PROJECT_ID,
|
||||
SUBSCRIBER_ID,
|
||||
TEST_CONFIG_NEW_SUBSCRIPTION,
|
||||
FakeSubscriber,
|
||||
PlatformSetup,
|
||||
YieldFixture,
|
||||
)
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
PLATFORM = "sensor"
|
||||
|
||||
|
||||
@ -61,25 +61,6 @@ def warning_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(
|
||||
hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform
|
||||
) -> None:
|
||||
@ -125,10 +106,9 @@ async def test_setup_configuration_failure(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()])
|
||||
async def test_setup_susbcriber_failure(
|
||||
async def test_setup_subscriber_failure(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
failing_subscriber,
|
||||
setup_base_platform,
|
||||
) -> None:
|
||||
"""Test configuration error."""
|
||||
@ -145,7 +125,6 @@ async def test_setup_device_manager_failure(
|
||||
) -> None:
|
||||
"""Test device manager api failure."""
|
||||
with (
|
||||
patch("homeassistant.components.nest.api.GoogleNestSubscriber.start_async"),
|
||||
patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager",
|
||||
side_effect=ApiException(),
|
||||
@ -165,7 +144,6 @@ async def test_subscriber_auth_failure(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
setup_base_platform,
|
||||
failing_subscriber,
|
||||
) -> None:
|
||||
"""Test subscriber throws an authentication error."""
|
||||
await setup_base_platform()
|
||||
@ -184,7 +162,6 @@ async def test_subscriber_configuration_failure(
|
||||
hass: HomeAssistant,
|
||||
error_caplog: pytest.LogCaptureFixture,
|
||||
setup_base_platform,
|
||||
failing_subscriber,
|
||||
) -> None:
|
||||
"""Test configuration error."""
|
||||
await setup_base_platform()
|
||||
@ -210,14 +187,12 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None:
|
||||
|
||||
async def test_remove_entry(
|
||||
hass: HomeAssistant,
|
||||
setup_base_platform,
|
||||
setup_base_platform: PlatformSetup,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
subscriber: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful unload of a ConfigEntry."""
|
||||
with patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber",
|
||||
return_value=FakeSubscriber(),
|
||||
):
|
||||
await setup_base_platform()
|
||||
await setup_base_platform()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
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("project_id") == PROJECT_ID
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id"),
|
||||
patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription",
|
||||
) as delete,
|
||||
):
|
||||
assert await hass.config_entries.async_remove(entry.entry_id)
|
||||
assert delete.called
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.delete(
|
||||
f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}",
|
||||
json={},
|
||||
)
|
||||
|
||||
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)
|
||||
assert not entries
|
||||
@ -243,7 +222,7 @@ async def test_remove_entry(
|
||||
async def test_home_assistant_stop(
|
||||
hass: HomeAssistant,
|
||||
setup_platform: PlatformSetup,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful subscriber shutdown when HomeAssistant stops."""
|
||||
await setup_platform()
|
||||
@ -253,31 +232,37 @@ async def test_home_assistant_stop(
|
||||
entry = entries[0]
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert not subscriber.stop.called
|
||||
await hass.async_stop()
|
||||
assert subscriber.stop_calls == 1
|
||||
assert subscriber.stop.called
|
||||
|
||||
|
||||
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:
|
||||
"""Test a failure when deleting the subscription."""
|
||||
with patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber",
|
||||
return_value=FakeSubscriber(),
|
||||
):
|
||||
await setup_base_platform()
|
||||
await setup_base_platform()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription",
|
||||
side_effect=SubscriberException(),
|
||||
) as delete:
|
||||
assert await hass.config_entries.async_remove(entry.entry_id)
|
||||
assert delete.called
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.delete(
|
||||
f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
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)
|
||||
assert not entries
|
||||
|
@ -13,7 +13,6 @@ from unittest.mock import patch
|
||||
|
||||
import aiohttp
|
||||
import av
|
||||
from google_nest_sdm.event import EventMessage
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
@ -31,7 +30,14 @@ from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.setup import async_setup_component
|
||||
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.typing import ClientSessionGenerator
|
||||
@ -70,7 +76,6 @@ BATTERY_CAMERA_TRAITS = {
|
||||
PERSON_EVENT = "sdm.devices.events.CameraPerson.Person"
|
||||
MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion"
|
||||
|
||||
TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..."
|
||||
GENERATE_IMAGE_URL_RESPONSE = {
|
||||
"results": {
|
||||
"url": TEST_IMAGE_URL,
|
||||
@ -162,12 +167,6 @@ def mp4() -> io.BytesIO:
|
||||
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
|
||||
def cache_size() -> int:
|
||||
"""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."""
|
||||
if device_id is None:
|
||||
device_id = DEVICE_ID
|
||||
return EventMessage.create_event(
|
||||
return create_nest_event(
|
||||
{
|
||||
"eventId": f"{EVENT_ID}-{timestamp}",
|
||||
"timestamp": timestamp.isoformat(timespec="seconds"),
|
||||
@ -209,7 +208,6 @@ def create_event_message(event_data, timestamp, device_id=None):
|
||||
"events": event_data,
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
|
||||
|
||||
@ -224,7 +222,7 @@ def create_battery_event_data(
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"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
|
||||
|
||||
|
||||
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."""
|
||||
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}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
|
||||
async def test_media_store_load_filesystem_error(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
@ -5,8 +5,8 @@ pubsub subscriber.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_nest_sdm.event import EventMessage
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
@ -198,7 +198,7 @@ async def test_device_name_from_structure(
|
||||
|
||||
async def test_event_updates_sensor(
|
||||
hass: HomeAssistant,
|
||||
subscriber: FakeSubscriber,
|
||||
subscriber: AsyncMock,
|
||||
create_device: CreateDevice,
|
||||
setup_platform: PlatformSetup,
|
||||
) -> None:
|
||||
@ -217,7 +217,7 @@ async def test_event_updates_sensor(
|
||||
assert temperature.state == "25.1"
|
||||
|
||||
# Simulate a pubsub message received by the subscriber with a trait update
|
||||
event = EventMessage.create_event(
|
||||
event = create_nest_event(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"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 hass.async_block_till_done() # Process dispatch/update signal
|
||||
|
Loading…
x
Reference in New Issue
Block a user