Add config flow to roku (#31988)

* create a dedicated const.py

* add DEFAULT_PORT to const.py

* work on config flow conversion.

* remove discovery.

* work on config flow and add tests. other cleanup.

* work on config flow and add tests. other cleanup.

* add quality scale to manifest.

* work on config flow and add tests. other cleanup.

* review tweaks.

* Update manifest.json

* catch more specific errors

* catch more errors.

* impprt specific exceptions

* import specific exceptions

* Update __init__.py

* Update config_flow.py

* Update media_player.py

* Update remote.py

* Update media_player.py

* Update remote.py

* Update media_player.py

* Update remote.py

* Update config_flow.py

* Update config_flow.py

* Update media_player.py

* Update __init__.py

* Update __init__.py

* Update config_flow.py

* Update test_config_flow.py

* Update config_flow.py

* Update __init__.py

* Update test_config_flow.py

* Update remote.py

* Update test_init.py

* Update test_init.py

* Update media_player.py

* Update media_player.py

* Update media_player.py
This commit is contained in:
Chris Talkington 2020-03-15 23:13:04 -05:00 committed by GitHub
parent 6e95b90f42
commit cf8dfdae47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 716 additions and 125 deletions

View File

@ -592,7 +592,9 @@ omit =
homeassistant/components/ring/camera.py
homeassistant/components/ripple/sensor.py
homeassistant/components/rocketchat/notify.py
homeassistant/components/roku/*
homeassistant/components/roku/__init__.py
homeassistant/components/roku/media_player.py
homeassistant/components/roku/remote.py
homeassistant/components/roomba/vacuum.py
homeassistant/components/route53/*
homeassistant/components/rova/sensor.py

View File

@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Roku device is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"flow_title": "Roku: {name}",
"step": {
"ssdp_confirm": {
"data": {},
"description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.",
"title": "Roku"
},
"user": {
"data": {
"host": "Host or IP address"
},
"description": "Enter your Roku information.",
"title": "Roku"
}
},
"title": "Roku"
}
}

View File

@ -1,29 +1,22 @@
"""Support for Roku."""
import logging
import asyncio
from datetime import timedelta
from socket import gaierror as SocketGIAError
from typing import Dict
from requests.exceptions import RequestException
from roku import Roku, RokuException
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_ROKU
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = "roku"
SERVICE_SCAN = "roku_scan"
ATTR_ROKU = "roku"
DATA_ROKU = "data_roku"
NOTIFICATION_ID = "roku_notification"
NOTIFICATION_TITLE = "Roku Setup"
NOTIFICATION_SCAN_ID = "roku_scan_notification"
NOTIFICATION_SCAN_TITLE = "Roku Scan"
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN
CONFIG_SCHEMA = vol.Schema(
{
@ -34,77 +27,67 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
# Currently no attributes but it might change later
ROKU_SCAN_SCHEMA = vol.Schema({})
PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
SCAN_INTERVAL = timedelta(seconds=30)
def setup(hass, config):
"""Set up the Roku component."""
hass.data[DATA_ROKU] = {}
def get_roku_data(host: str) -> dict:
"""Retrieve a Roku instance and version info for the device."""
roku = Roku(host)
roku_device_info = roku.device_info
def service_handler(service):
"""Handle service calls."""
if service.service == SERVICE_SCAN:
scan_for_rokus(hass)
return {
DATA_CLIENT: roku,
DATA_DEVICE_INFO: roku_device_info,
}
def roku_discovered(service, info):
"""Set up an Roku that was auto discovered."""
_setup_roku(hass, config, {CONF_HOST: info["host"]})
discovery.listen(hass, SERVICE_ROKU, roku_discovered)
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
"""Set up the Roku integration."""
hass.data.setdefault(DOMAIN, {})
for conf in config.get(DOMAIN, []):
_setup_roku(hass, config, conf)
hass.services.register(
DOMAIN, SERVICE_SCAN, service_handler, schema=ROKU_SCAN_SCHEMA
)
if DOMAIN in config:
for entry_config in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config,
)
)
return True
def scan_for_rokus(hass):
"""Scan for devices and present a notification of the ones found."""
rokus = Roku.discover()
devices = []
for roku in rokus:
try:
r_info = roku.device_info
except RokuException: # skip non-roku device
continue
devices.append(
"Name: {0}<br />Host: {1}<br />".format(
r_info.userdevicename
if r_info.userdevicename
else f"{r_info.modelname} {r_info.serial_num}",
roku.host,
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
try:
roku_data = await hass.async_add_executor_job(
get_roku_data, entry.data[CONF_HOST],
)
if not devices:
devices = ["No device(s) found"]
except (SocketGIAError, RequestException, RokuException) as exception:
raise ConfigEntryNotReady from exception
hass.components.persistent_notification.create(
"The following devices were found:<br /><br />" + "<br /><br />".join(devices),
title=NOTIFICATION_SCAN_TITLE,
notification_id=NOTIFICATION_SCAN_ID,
hass.data[DOMAIN][entry.entry_id] = roku_data
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
def _setup_roku(hass, hass_config, roku_config):
"""Set up a Roku."""
host = roku_config[CONF_HOST]
if host in hass.data[DATA_ROKU]:
return
roku = Roku(host)
r_info = roku.device_info
hass.data[DATA_ROKU][host] = {ATTR_ROKU: r_info.serial_num}
discovery.load_platform(hass, "media_player", DOMAIN, roku_config, hass_config)
discovery.load_platform(hass, "remote", DOMAIN, roku_config, hass_config)
return unload_ok

View File

@ -0,0 +1,134 @@
"""Config flow for Roku."""
import logging
from socket import gaierror as SocketGIAError
from typing import Any, Dict, Optional
from urllib.parse import urlparse
from requests.exceptions import RequestException
from roku import Roku, RokuException
import voluptuous as vol
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_SERIAL,
)
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN # pylint: disable=unused-import
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
ERROR_CANNOT_CONNECT = "cannot_connect"
ERROR_UNKNOWN = "unknown"
_LOGGER = logging.getLogger(__name__)
def validate_input(data: Dict) -> Dict:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
roku = Roku(data["host"])
device_info = roku.device_info
return {
"title": data["host"],
"host": data["host"],
"serial_num": device_info.serial_num,
}
class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Roku config flow."""
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
@callback
def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
"""Show the form to the user."""
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors or {},
)
async def async_step_import(
self, user_input: Optional[Dict] = None
) -> Dict[str, Any]:
"""Handle configuration by yaml file."""
return await self.async_step_user(user_input)
async def async_step_user(
self, user_input: Optional[Dict] = None
) -> Dict[str, Any]:
"""Handle a flow initialized by the user."""
if not user_input:
return self._show_form()
errors = {}
try:
info = await self.hass.async_add_executor_job(validate_input, user_input)
except (SocketGIAError, RequestException, RokuException):
errors["base"] = ERROR_CANNOT_CONNECT
return self._show_form(errors)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error trying to connect.")
return self.async_abort(reason=ERROR_UNKNOWN)
await self.async_set_unique_id(info["serial_num"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
async def async_step_ssdp(
self, discovery_info: Optional[Dict] = None
) -> Dict[str, Any]:
"""Handle a flow initialized by discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
name = discovery_info[ATTR_UPNP_FRIENDLY_NAME]
serial_num = discovery_info[ATTR_UPNP_SERIAL]
await self.async_set_unique_id(serial_num)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": host}}
)
return await self.async_step_ssdp_confirm()
async def async_step_ssdp_confirm(
self, user_input: Optional[Dict] = None
) -> Dict[str, Any]:
"""Handle user-confirmation of discovered device."""
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
name = self.context.get(CONF_NAME)
if user_input is not None:
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
user_input[CONF_HOST] = self.context.get(CONF_HOST)
user_input[CONF_NAME] = name
try:
await self.hass.async_add_executor_job(validate_input, user_input)
return self.async_create_entry(title=name, data=user_input)
except (SocketGIAError, RequestException, RokuException):
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error trying to connect.")
return self.async_abort(reason=ERROR_UNKNOWN)
return self.async_show_form(
step_id="ssdp_confirm", description_placeholders={"name": name},
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -1,2 +1,8 @@
"""Constants for the Roku integration."""
DOMAIN = "roku"
DATA_CLIENT = "client"
DATA_DEVICE_INFO = "device_info"
DEFAULT_PORT = 8060
DEFAULT_MANUFACTURER = "Roku"

View File

@ -4,6 +4,14 @@
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["roku==4.0.0"],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": ["@ctalkington"]
"ssdp": [
{
"st": "roku:ecp",
"manufacturer": "Roku",
"deviceType": "urn:roku-com:device:player:1-0"
}
],
"codeowners": ["@ctalkington"],
"quality_scale": "silver",
"config_flow": true
}

View File

@ -1,8 +1,9 @@
"""Support for the Roku media player."""
import logging
import requests.exceptions
from roku import Roku
from requests.exceptions import (
ConnectionError as RequestsConnectionError,
ReadTimeout as RequestsReadTimeout,
)
from roku import RokuException
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
@ -16,17 +17,9 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
CONF_HOST,
STATE_HOME,
STATE_IDLE,
STATE_PLAYING,
STATE_STANDBY,
)
from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY
from .const import DEFAULT_PORT
_LOGGER = logging.getLogger(__name__)
from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DEFAULT_PORT, DOMAIN
SUPPORT_ROKU = (
SUPPORT_PREVIOUS_TRACK
@ -40,23 +33,19 @@ SUPPORT_ROKU = (
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Roku platform."""
if not discovery_info:
return
host = discovery_info[CONF_HOST]
async_add_entities([RokuDevice(host)], True)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Roku config entry."""
roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
async_add_entities([RokuDevice(roku)], True)
class RokuDevice(MediaPlayerDevice):
"""Representation of a Roku device on the network."""
def __init__(self, host):
def __init__(self, roku):
"""Initialize the Roku device."""
self.roku = Roku(host)
self.ip_address = host
self.roku = roku
self.ip_address = roku.host
self.channels = []
self.current_app = None
self._available = False
@ -77,7 +66,7 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None
self._available = True
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout):
except (RequestsConnectionError, RequestsReadTimeout, RokuException):
self._available = False
pass
@ -130,6 +119,17 @@ class RokuDevice(MediaPlayerDevice):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self._device_info.serial_num
@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self.name,
"identifiers": {(DOMAIN, self.unique_id)},
"manufacturer": DEFAULT_MANUFACTURER,
"model": self._device_info.model_num,
"sw_version": self._device_info.software_version,
}
@property
def media_content_type(self):
"""Content type of current playing media."""

View File

@ -1,34 +1,48 @@
"""Support for the Roku remote."""
import requests.exceptions
from roku import Roku
from typing import Callable, List
from homeassistant.components import remote
from homeassistant.const import CONF_HOST
from requests.exceptions import (
ConnectionError as RequestsConnectionError,
ReadTimeout as RequestsReadTimeout,
)
from roku import RokuException
from homeassistant.components.remote import RemoteDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DOMAIN
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Roku remote platform."""
if not discovery_info:
return
host = discovery_info[CONF_HOST]
async_add_entities([RokuRemote(host)], True)
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
async_add_entities: Callable[[List, bool], None],
) -> bool:
"""Load Roku remote based on a config entry."""
roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
async_add_entities([RokuRemote(roku)], True)
class RokuRemote(remote.RemoteDevice):
class RokuRemote(RemoteDevice):
"""Device that sends commands to an Roku."""
def __init__(self, host):
def __init__(self, roku):
"""Initialize the Roku device."""
self.roku = Roku(host)
self.roku = roku
self._available = False
self._device_info = {}
def update(self):
"""Retrieve latest state."""
if not self.enabled:
return
try:
self._device_info = self.roku.device_info
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout):
self._available = True
except (RequestsConnectionError, RequestsReadTimeout, RokuException):
self._available = False
pass
@property
@ -38,11 +52,27 @@ class RokuRemote(remote.RemoteDevice):
return self._device_info.user_device_name
return f"Roku {self._device_info.serial_num}"
@property
def available(self):
"""Return if able to retrieve information from device or not."""
return self._available
@property
def unique_id(self):
"""Return a unique ID."""
return self._device_info.serial_num
@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self.name,
"identifiers": {(DOMAIN, self.unique_id)},
"manufacturer": DEFAULT_MANUFACTURER,
"model": self._device_info.model_num,
"sw_version": self._device_info.software_version,
}
@property
def is_on(self):
"""Return true if device is on."""

View File

@ -1,2 +0,0 @@
roku_scan:
description: Scans the local network for Rokus. All found devices are presented as a persistent notification.

View File

@ -0,0 +1,27 @@
{
"config": {
"title": "Roku",
"flow_title": "Roku: {name}",
"step": {
"user": {
"title": "Roku",
"description": "Enter your Roku information.",
"data": {
"host": "Host or IP address"
}
},
"ssdp_confirm": {
"title": "Roku",
"description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.",
"data": {}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Roku device is already configured"
}
}
}

View File

@ -83,6 +83,7 @@ FLOWS = [
"rachio",
"rainmachine",
"ring",
"roku",
"samsungtv",
"sense",
"sentry",

View File

@ -47,6 +47,13 @@ SSDP = {
"manufacturer": "konnected.io"
}
],
"roku": [
{
"deviceType": "urn:roku-com:device:player:1-0",
"manufacturer": "Roku",
"st": "roku:ecp"
}
],
"samsungtv": [
{
"st": "urn:samsung.com:device:RemoteControlReceiver:1"

View File

@ -628,6 +628,9 @@ rflink==0.0.52
# homeassistant.components.ring
ring_doorbell==0.6.0
# homeassistant.components.roku
roku==4.0.0
# homeassistant.components.yamaha
rxv==0.6.0

View File

@ -0,0 +1,50 @@
"""Tests for the Roku component."""
from homeassistant.components.roku.const import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.helpers.typing import HomeAssistantType
from tests.common import MockConfigEntry
HOST = "1.2.3.4"
NAME = "Roku 3"
SSDP_LOCATION = "http://1.2.3.4/"
UPNP_FRIENDLY_NAME = "My Roku 3"
UPNP_SERIAL = "1GU48T017973"
class MockDeviceInfo(object):
"""Mock DeviceInfo for Roku."""
model_name = NAME
model_num = "4200X"
software_version = "7.5.0.09021"
serial_num = UPNP_SERIAL
user_device_name = UPNP_FRIENDLY_NAME
roku_type = "Box"
def __repr__(self):
"""Return the object representation of DeviceInfo."""
return "<DeviceInfo: %s-%s, SW v%s, Ser# %s (%s)>" % (
self.model_name,
self.model_num,
self.software_version,
self.serial_num,
self.roku_type,
)
async def setup_integration(
hass: HomeAssistantType, skip_entry_setup: bool = False
) -> MockConfigEntry:
"""Set up the Roku integration in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN, unique_id=UPNP_SERIAL, data={CONF_HOST: HOST}
)
entry.add_to_hass(hass)
if not skip_entry_setup:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@ -0,0 +1,247 @@
"""Test the Roku config flow."""
from socket import gaierror as SocketGIAError
from typing import Any, Dict, Optional
from asynctest import patch
from requests.exceptions import RequestException
from roku import RokuException
from homeassistant.components.roku.const import DOMAIN
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_SERIAL,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
from tests.components.roku import (
HOST,
SSDP_LOCATION,
UPNP_FRIENDLY_NAME,
UPNP_SERIAL,
MockDeviceInfo,
setup_integration,
)
async def async_configure_flow(
hass: HomeAssistantType, flow_id: str, user_input: Optional[Dict] = None
) -> Any:
"""Set up mock Roku integration flow."""
with patch(
"homeassistant.components.roku.config_flow.Roku.device_info",
new=MockDeviceInfo,
):
return await hass.config_entries.flow.async_configure(
flow_id=flow_id, user_input=user_input
)
async def async_init_flow(
hass: HomeAssistantType,
handler: str = DOMAIN,
context: Optional[Dict] = None,
data: Any = None,
) -> Any:
"""Set up mock Roku integration flow."""
with patch(
"homeassistant.components.roku.config_flow.Roku.device_info",
new=MockDeviceInfo,
):
return await hass.config_entries.flow.async_init(
handler=handler, context=context, data=data
)
async def test_duplicate_error(hass: HomeAssistantType) -> None:
"""Test that errors are shown when duplicates are added."""
await setup_integration(hass, skip_entry_setup=True)
result = await async_init_flow(
hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
result = await async_init_flow(
hass, context={CONF_SOURCE: SOURCE_USER}, data={CONF_HOST: HOST}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
result = await async_init_flow(
hass,
context={CONF_SOURCE: SOURCE_SSDP},
data={
ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME,
ATTR_SSDP_LOCATION: SSDP_LOCATION,
ATTR_UPNP_SERIAL: UPNP_SERIAL,
},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_form(hass: HomeAssistantType) -> None:
"""Test the user step."""
await async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.roku.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roku.async_setup_entry", return_value=True,
) as mock_setup_entry:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST})
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST
assert result["data"] == {CONF_HOST: HOST}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass: HomeAssistantType) -> None:
"""Test we handle cannot connect roku error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
with patch(
"homeassistant.components.roku.config_flow.validate_input",
side_effect=RokuException,
) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
await hass.async_block_till_done()
assert len(mock_validate_input.mock_calls) == 1
async def test_form_cannot_connect_request(hass: HomeAssistantType) -> None:
"""Test we handle cannot connect request error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
with patch(
"homeassistant.components.roku.config_flow.validate_input",
side_effect=RequestException,
) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
await hass.async_block_till_done()
assert len(mock_validate_input.mock_calls) == 1
async def test_form_cannot_connect_socket(hass: HomeAssistantType) -> None:
"""Test we handle cannot connect socket error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
with patch(
"homeassistant.components.roku.config_flow.validate_input",
side_effect=SocketGIAError,
) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
await hass.async_block_till_done()
assert len(mock_validate_input.mock_calls) == 1
async def test_form_unknown_error(hass: HomeAssistantType) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
with patch(
"homeassistant.components.roku.config_flow.validate_input",
side_effect=Exception,
) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
await hass.async_block_till_done()
assert len(mock_validate_input.mock_calls) == 1
async def test_import(hass: HomeAssistantType) -> None:
"""Test the import step."""
with patch(
"homeassistant.components.roku.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roku.async_setup_entry", return_value=True,
) as mock_setup_entry:
result = await async_init_flow(
hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST}
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST
assert result["data"] == {CONF_HOST: HOST}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_ssdp_discovery(hass: HomeAssistantType) -> None:
"""Test the ssdp discovery step."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data={
ATTR_SSDP_LOCATION: SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME,
ATTR_UPNP_SERIAL: UPNP_SERIAL,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "ssdp_confirm"
assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME}
with patch(
"homeassistant.components.roku.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roku.async_setup_entry", return_value=True,
) as mock_setup_entry:
result = await async_configure_flow(hass, result["flow_id"], {})
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == UPNP_FRIENDLY_NAME
assert result["data"] == {
CONF_HOST: HOST,
CONF_NAME: UPNP_FRIENDLY_NAME,
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,68 @@
"""Tests for the Roku integration."""
from socket import gaierror as SocketGIAError
from asynctest import patch
from requests.exceptions import RequestException
from roku import RokuException
from homeassistant.components.roku.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.helpers.typing import HomeAssistantType
from tests.components.roku import MockDeviceInfo, setup_integration
async def test_config_entry_not_ready(hass: HomeAssistantType) -> None:
"""Test the Roku configuration entry not ready."""
with patch(
"homeassistant.components.roku.Roku._call", side_effect=RokuException,
):
entry = await setup_integration(hass)
assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_config_entry_not_ready_request(hass: HomeAssistantType) -> None:
"""Test the Roku configuration entry not ready."""
with patch(
"homeassistant.components.roku.Roku._call", side_effect=RequestException,
):
entry = await setup_integration(hass)
assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_config_entry_not_ready_socket(hass: HomeAssistantType) -> None:
"""Test the Roku configuration entry not ready."""
with patch(
"homeassistant.components.roku.Roku._call", side_effect=SocketGIAError,
):
entry = await setup_integration(hass)
assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_unload_config_entry(hass: HomeAssistantType) -> None:
"""Test the Roku configuration entry unloading."""
with patch(
"homeassistant.components.roku.Roku.device_info", return_value=MockDeviceInfo,
), patch(
"homeassistant.components.roku.media_player.async_setup_entry",
return_value=True,
), patch(
"homeassistant.components.roku.remote.async_setup_entry", return_value=True,
):
entry = await setup_integration(hass)
assert hass.data[DOMAIN][entry.entry_id]
assert entry.state == ENTRY_STATE_LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.entry_id not in hass.data[DOMAIN]
assert entry.state == ENTRY_STATE_NOT_LOADED