mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
Improve nest SDM integration error handling (#43271)
This commit is contained in:
parent
a3061ebd8d
commit
2d14f07396
@ -6,6 +6,7 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from google_nest_sdm.event import EventCallback, EventMessage
|
from google_nest_sdm.event import EventCallback, EventMessage
|
||||||
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
||||||
from nest import Nest
|
from nest import Nest
|
||||||
from nest.nest import APIError, AuthorizationError
|
from nest.nest import APIError, AuthorizationError
|
||||||
@ -25,6 +26,7 @@ from homeassistant.const import (
|
|||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
aiohttp_client,
|
aiohttp_client,
|
||||||
config_entry_oauth2_flow,
|
config_entry_oauth2_flow,
|
||||||
@ -208,7 +210,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
|
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
|
||||||
)
|
)
|
||||||
subscriber.set_update_callback(SignalUpdateCallback(hass))
|
subscriber.set_update_callback(SignalUpdateCallback(hass))
|
||||||
asyncio.create_task(subscriber.start_async())
|
|
||||||
|
try:
|
||||||
|
await subscriber.start_async()
|
||||||
|
except GoogleNestException as err:
|
||||||
|
_LOGGER.error("Subscriber error: %s", err)
|
||||||
|
subscriber.stop_async()
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
|
try:
|
||||||
|
await subscriber.async_get_device_manager()
|
||||||
|
except GoogleNestException as err:
|
||||||
|
_LOGGER.error("Device Manager error: %s", err)
|
||||||
|
subscriber.stop_async()
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = subscriber
|
hass.data[DOMAIN][entry.entry_id] = subscriber
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
|
@ -4,14 +4,15 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientError
|
|
||||||
from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait
|
from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait
|
||||||
from google_nest_sdm.device import Device
|
from google_nest_sdm.device import Device
|
||||||
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
from haffmpeg.tools import IMAGE_JPEG
|
from haffmpeg.tools import IMAGE_JPEG
|
||||||
|
|
||||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||||
from homeassistant.components.ffmpeg import async_get_image
|
from homeassistant.components.ffmpeg import async_get_image
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
@ -32,7 +33,10 @@ async def async_setup_sdm_entry(
|
|||||||
"""Set up the cameras."""
|
"""Set up the cameras."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
|
except GoogleNestException as err:
|
||||||
|
raise PlatformNotReady from err
|
||||||
|
|
||||||
# Fetch initial data so we have data when entities subscribe.
|
# Fetch initial data so we have data when entities subscribe.
|
||||||
|
|
||||||
@ -130,7 +134,7 @@ class NestCamera(Camera):
|
|||||||
self._stream_refresh_unsub = None
|
self._stream_refresh_unsub = None
|
||||||
try:
|
try:
|
||||||
self._stream = await self._stream.extend_rtsp_stream()
|
self._stream = await self._stream.extend_rtsp_stream()
|
||||||
except ClientError as err:
|
except GoogleNestException as err:
|
||||||
_LOGGER.debug("Failed to extend stream: %s", err)
|
_LOGGER.debug("Failed to extend stream: %s", err)
|
||||||
# Next attempt to catch a url will get a new one
|
# Next attempt to catch a url will get a new one
|
||||||
self._stream = None
|
self._stream = None
|
||||||
|
@ -4,6 +4,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from google_nest_sdm.device import Device
|
from google_nest_sdm.device import Device
|
||||||
from google_nest_sdm.device_traits import FanTrait, TemperatureTrait
|
from google_nest_sdm.device_traits import FanTrait, TemperatureTrait
|
||||||
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
from google_nest_sdm.thermostat_traits import (
|
from google_nest_sdm.thermostat_traits import (
|
||||||
ThermostatEcoTrait,
|
ThermostatEcoTrait,
|
||||||
ThermostatHvacTrait,
|
ThermostatHvacTrait,
|
||||||
@ -34,6 +35,7 @@ from homeassistant.components.climate.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
@ -80,7 +82,10 @@ async def async_setup_sdm_entry(
|
|||||||
"""Set up the client entities."""
|
"""Set up the client entities."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
|
except GoogleNestException as err:
|
||||||
|
raise PlatformNotReady from err
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for device in device_manager.devices.values():
|
for device in device_manager.devices.values():
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"python-nest==4.1.0",
|
"python-nest==4.1.0",
|
||||||
"google-nest-sdm==0.1.14"
|
"google-nest-sdm==0.1.15"
|
||||||
],
|
],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@awarecan",
|
"@awarecan",
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""Support for Google Nest SDM sensors."""
|
"""Support for Google Nest SDM sensors."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from google_nest_sdm.device import Device
|
from google_nest_sdm.device import Device
|
||||||
from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait
|
from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait
|
||||||
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -12,6 +14,7 @@ from homeassistant.const import (
|
|||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
)
|
)
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
@ -19,6 +22,9 @@ from homeassistant.helpers.typing import HomeAssistantType
|
|||||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
||||||
from .device_info import DeviceInfo
|
from .device_info import DeviceInfo
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEVICE_TYPE_MAP = {
|
DEVICE_TYPE_MAP = {
|
||||||
"sdm.devices.types.CAMERA": "Camera",
|
"sdm.devices.types.CAMERA": "Camera",
|
||||||
"sdm.devices.types.DISPLAY": "Display",
|
"sdm.devices.types.DISPLAY": "Display",
|
||||||
@ -33,7 +39,11 @@ async def async_setup_sdm_entry(
|
|||||||
"""Set up the sensors."""
|
"""Set up the sensors."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
|
except GoogleNestException as err:
|
||||||
|
_LOGGER.warning("Failed to get devices: %s", err)
|
||||||
|
raise PlatformNotReady from err
|
||||||
|
|
||||||
# Fetch initial data so we have data when entities subscribe.
|
# Fetch initial data so we have data when entities subscribe.
|
||||||
|
|
||||||
|
@ -687,7 +687,7 @@ google-cloud-pubsub==2.1.0
|
|||||||
google-cloud-texttospeech==0.4.0
|
google-cloud-texttospeech==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.nest
|
# homeassistant.components.nest
|
||||||
google-nest-sdm==0.1.14
|
google-nest-sdm==0.1.15
|
||||||
|
|
||||||
# homeassistant.components.google_travel_time
|
# homeassistant.components.google_travel_time
|
||||||
googlemaps==2.5.1
|
googlemaps==2.5.1
|
||||||
|
@ -358,7 +358,7 @@ google-api-python-client==1.6.4
|
|||||||
google-cloud-pubsub==2.1.0
|
google-cloud-pubsub==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.nest
|
# homeassistant.components.nest
|
||||||
google-nest-sdm==0.1.14
|
google-nest-sdm==0.1.15
|
||||||
|
|
||||||
# homeassistant.components.gree
|
# homeassistant.components.gree
|
||||||
greeclimate==0.10.3
|
greeclimate==0.10.3
|
||||||
|
@ -6,10 +6,8 @@ pubsub subscriber.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectionError
|
import aiohttp
|
||||||
from google_nest_sdm.auth import AbstractAuth
|
|
||||||
from google_nest_sdm.device import Device
|
from google_nest_sdm.device import Device
|
||||||
|
|
||||||
from homeassistant.components import camera
|
from homeassistant.components import camera
|
||||||
@ -41,47 +39,6 @@ DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS"
|
|||||||
DOMAIN = "nest"
|
DOMAIN = "nest"
|
||||||
|
|
||||||
|
|
||||||
class FakeResponse:
|
|
||||||
"""A fake web response used for returning results of commands."""
|
|
||||||
|
|
||||||
def __init__(self, json=None, error=None):
|
|
||||||
"""Initialize the FakeResponse."""
|
|
||||||
self._json = json
|
|
||||||
self._error = error
|
|
||||||
|
|
||||||
def raise_for_status(self):
|
|
||||||
"""Mimics a successful response status."""
|
|
||||||
if self._error:
|
|
||||||
raise self._error
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def json(self):
|
|
||||||
"""Return a dict with the response."""
|
|
||||||
assert self._json
|
|
||||||
return self._json
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAuth(AbstractAuth):
|
|
||||||
"""Fake authentication object that returns fake responses."""
|
|
||||||
|
|
||||||
def __init__(self, responses: List[FakeResponse]):
|
|
||||||
"""Initialize the FakeAuth."""
|
|
||||||
super().__init__(None, "")
|
|
||||||
self._responses = responses
|
|
||||||
|
|
||||||
async def async_get_access_token(self):
|
|
||||||
"""Return a fake access token."""
|
|
||||||
return "some-token"
|
|
||||||
|
|
||||||
async def creds(self):
|
|
||||||
"""Return a fake creds."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def request(self, method: str, url: str, **kwargs):
|
|
||||||
"""Pass through the FakeResponse."""
|
|
||||||
return self._responses.pop(0)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_camera(hass, traits={}, auth=None):
|
async def async_setup_camera(hass, traits={}, auth=None):
|
||||||
"""Set up the platform and prerequisites."""
|
"""Set up the platform and prerequisites."""
|
||||||
devices = {}
|
devices = {}
|
||||||
@ -145,21 +102,25 @@ async def test_camera_device(hass):
|
|||||||
assert device.identifiers == {("nest", DEVICE_ID)}
|
assert device.identifiers == {("nest", DEVICE_ID)}
|
||||||
|
|
||||||
|
|
||||||
async def test_camera_stream(hass, aiohttp_client):
|
async def test_camera_stream(hass, auth):
|
||||||
"""Test a basic camera and fetch its live stream."""
|
"""Test a basic camera and fetch its live stream."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
expiration = now + datetime.timedelta(seconds=100)
|
expiration = now + datetime.timedelta(seconds=100)
|
||||||
response = FakeResponse(
|
auth.responses = [
|
||||||
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamUrls": {"rtspUrl": "rtsp://some/url?auth=g.0.streamingToken"},
|
"streamUrls": {
|
||||||
|
"rtspUrl": "rtsp://some/url?auth=g.0.streamingToken"
|
||||||
|
},
|
||||||
"streamExtensionToken": "g.1.extensionToken",
|
"streamExtensionToken": "g.1.extensionToken",
|
||||||
"streamToken": "g.0.streamingToken",
|
"streamToken": "g.0.streamingToken",
|
||||||
"expiresAt": expiration.isoformat(timespec="seconds"),
|
"expiresAt": expiration.isoformat(timespec="seconds"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await async_setup_camera(hass, DEVICE_TRAITS, auth=FakeAuth([response]))
|
]
|
||||||
|
await async_setup_camera(hass, DEVICE_TRAITS, auth=auth)
|
||||||
|
|
||||||
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")
|
||||||
@ -179,15 +140,15 @@ async def test_camera_stream(hass, aiohttp_client):
|
|||||||
assert image.content == b"image bytes"
|
assert image.content == b"image bytes"
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_expired_stream_token(hass, aiohttp_client):
|
async def test_refresh_expired_stream_token(hass, auth):
|
||||||
"""Test a camera stream expiration and refresh."""
|
"""Test a camera stream expiration and refresh."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
stream_1_expiration = now + datetime.timedelta(seconds=90)
|
stream_1_expiration = now + datetime.timedelta(seconds=90)
|
||||||
stream_2_expiration = now + datetime.timedelta(seconds=180)
|
stream_2_expiration = now + datetime.timedelta(seconds=180)
|
||||||
stream_3_expiration = now + datetime.timedelta(seconds=360)
|
stream_3_expiration = now + datetime.timedelta(seconds=360)
|
||||||
responses = [
|
auth.responses = [
|
||||||
# Stream URL #1
|
# Stream URL #1
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamUrls": {
|
"streamUrls": {
|
||||||
@ -200,7 +161,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
# Stream URL #2
|
# Stream URL #2
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamExtensionToken": "g.2.extensionToken",
|
"streamExtensionToken": "g.2.extensionToken",
|
||||||
@ -210,7 +171,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
# Stream URL #3
|
# Stream URL #3
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamExtensionToken": "g.3.extensionToken",
|
"streamExtensionToken": "g.3.extensionToken",
|
||||||
@ -223,7 +184,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
|
|||||||
await async_setup_camera(
|
await async_setup_camera(
|
||||||
hass,
|
hass,
|
||||||
DEVICE_TRAITS,
|
DEVICE_TRAITS,
|
||||||
auth=FakeAuth(responses),
|
auth=auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 1
|
assert len(hass.states.async_all()) == 1
|
||||||
@ -259,12 +220,12 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
|
|||||||
assert stream_source == "rtsp://some/url?auth=g.3.streamingToken"
|
assert stream_source == "rtsp://some/url?auth=g.3.streamingToken"
|
||||||
|
|
||||||
|
|
||||||
async def test_camera_removed(hass, aiohttp_client):
|
async def test_camera_removed(hass, auth):
|
||||||
"""Test case where entities are removed and stream tokens expired."""
|
"""Test case where entities are removed and stream tokens expired."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
expiration = now + datetime.timedelta(seconds=100)
|
expiration = now + datetime.timedelta(seconds=100)
|
||||||
responses = [
|
auth.responses = [
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamUrls": {
|
"streamUrls": {
|
||||||
@ -276,12 +237,12 @@ async def test_camera_removed(hass, aiohttp_client):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
FakeResponse({"results": {}}),
|
aiohttp.web.json_response({"results": {}}),
|
||||||
]
|
]
|
||||||
await async_setup_camera(
|
await async_setup_camera(
|
||||||
hass,
|
hass,
|
||||||
DEVICE_TRAITS,
|
DEVICE_TRAITS,
|
||||||
auth=FakeAuth(responses),
|
auth=auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 1
|
assert len(hass.states.async_all()) == 1
|
||||||
@ -297,13 +258,13 @@ async def test_camera_removed(hass, aiohttp_client):
|
|||||||
assert len(hass.states.async_all()) == 0
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_expired_stream_failure(hass, aiohttp_client):
|
async def test_refresh_expired_stream_failure(hass, auth):
|
||||||
"""Tests a failure when refreshing the stream."""
|
"""Tests a failure when refreshing the stream."""
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
stream_1_expiration = now + datetime.timedelta(seconds=90)
|
stream_1_expiration = now + datetime.timedelta(seconds=90)
|
||||||
stream_2_expiration = now + datetime.timedelta(seconds=180)
|
stream_2_expiration = now + datetime.timedelta(seconds=180)
|
||||||
responses = [
|
auth.responses = [
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamUrls": {
|
"streamUrls": {
|
||||||
@ -316,9 +277,9 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
# Extending the stream fails with arbitrary error
|
# Extending the stream fails with arbitrary error
|
||||||
FakeResponse(error=ClientConnectionError()),
|
aiohttp.web.Response(status=500),
|
||||||
# Next attempt to get a stream fetches a new url
|
# Next attempt to get a stream fetches a new url
|
||||||
FakeResponse(
|
aiohttp.web.json_response(
|
||||||
{
|
{
|
||||||
"results": {
|
"results": {
|
||||||
"streamUrls": {
|
"streamUrls": {
|
||||||
@ -334,7 +295,7 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client):
|
|||||||
await async_setup_camera(
|
await async_setup_camera(
|
||||||
hass,
|
hass,
|
||||||
DEVICE_TRAITS,
|
DEVICE_TRAITS,
|
||||||
auth=FakeAuth(responses),
|
auth=auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 1
|
assert len(hass.states.async_all()) == 1
|
||||||
|
@ -364,25 +364,8 @@ async def test_thermostat_eco_heat_only(hass):
|
|||||||
assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
|
assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
|
||||||
|
|
||||||
|
|
||||||
class FakeAuth:
|
async def test_thermostat_set_hvac_mode(hass, auth):
|
||||||
"""A fake implementation of the auth class that records requests."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize FakeAuth."""
|
|
||||||
self.method = None
|
|
||||||
self.url = None
|
|
||||||
self.json = None
|
|
||||||
|
|
||||||
async def request(self, method, url, json):
|
|
||||||
"""Capure the request arguments for tests to assert on."""
|
|
||||||
self.method = method
|
|
||||||
self.url = url
|
|
||||||
self.json = json
|
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_hvac_mode(hass):
|
|
||||||
"""Test a thermostat changing hvac modes."""
|
"""Test a thermostat changing hvac modes."""
|
||||||
auth = FakeAuth()
|
|
||||||
subscriber = await setup_climate(
|
subscriber = await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -467,9 +450,8 @@ async def test_thermostat_set_hvac_mode(hass):
|
|||||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_eco_preset(hass):
|
async def test_thermostat_set_eco_preset(hass, auth):
|
||||||
"""Test a thermostat put into eco mode."""
|
"""Test a thermostat put into eco mode."""
|
||||||
auth = FakeAuth()
|
|
||||||
subscriber = await setup_climate(
|
subscriber = await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -553,9 +535,8 @@ async def test_thermostat_set_eco_preset(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_cool(hass):
|
async def test_thermostat_set_cool(hass, auth):
|
||||||
"""Test a thermostat in cool mode with a temperature change."""
|
"""Test a thermostat in cool mode with a temperature change."""
|
||||||
auth = FakeAuth()
|
|
||||||
await setup_climate(
|
await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -587,9 +568,8 @@ async def test_thermostat_set_cool(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_heat(hass):
|
async def test_thermostat_set_heat(hass, auth):
|
||||||
"""Test a thermostat heating mode with a temperature change."""
|
"""Test a thermostat heating mode with a temperature change."""
|
||||||
auth = FakeAuth()
|
|
||||||
await setup_climate(
|
await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -621,9 +601,8 @@ async def test_thermostat_set_heat(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_heat_cool(hass):
|
async def test_thermostat_set_heat_cool(hass, auth):
|
||||||
"""Test a thermostat in heatcool mode with a temperature change."""
|
"""Test a thermostat in heatcool mode with a temperature change."""
|
||||||
auth = FakeAuth()
|
|
||||||
await setup_climate(
|
await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -732,9 +711,8 @@ async def test_thermostat_fan_on(hass):
|
|||||||
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
|
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_set_fan(hass):
|
async def test_thermostat_set_fan(hass, auth):
|
||||||
"""Test a thermostat enabling the fan."""
|
"""Test a thermostat enabling the fan."""
|
||||||
auth = FakeAuth()
|
|
||||||
await setup_climate(
|
await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
@ -805,9 +783,8 @@ async def test_thermostat_fan_empty(hass):
|
|||||||
assert ATTR_FAN_MODES not in thermostat.attributes
|
assert ATTR_FAN_MODES not in thermostat.attributes
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_target_temp(hass):
|
async def test_thermostat_target_temp(hass, auth):
|
||||||
"""Test a thermostat changing hvac modes and affected on target temps."""
|
"""Test a thermostat changing hvac modes and affected on target temps."""
|
||||||
auth = FakeAuth()
|
|
||||||
subscriber = await setup_climate(
|
subscriber = await setup_climate(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
57
tests/components/nest/conftest.py
Normal file
57
tests/components/nest/conftest.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Common libraries for test setup."""
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from google_nest_sdm.auth import AbstractAuth
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAuth(AbstractAuth):
|
||||||
|
"""A fake implementation of the auth class 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Tests can set fake responses here.
|
||||||
|
responses = []
|
||||||
|
# The last request is recorded here.
|
||||||
|
method = None
|
||||||
|
url = None
|
||||||
|
json = None
|
||||||
|
|
||||||
|
# Set up by fixture
|
||||||
|
client = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize FakeAuth."""
|
||||||
|
super().__init__(None, None)
|
||||||
|
|
||||||
|
async def async_get_access_token(self) -> str:
|
||||||
|
"""Return a valid access token."""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def request(self, method, url, json):
|
||||||
|
"""Capure the request arguments for tests to assert on."""
|
||||||
|
self.method = method
|
||||||
|
self.url = url
|
||||||
|
self.json = json
|
||||||
|
return await self.client.get("/")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def auth(aiohttp_client):
|
||||||
|
"""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
|
Loading…
x
Reference in New Issue
Block a user