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/ring/camera.py
homeassistant/components/ripple/sensor.py homeassistant/components/ripple/sensor.py
homeassistant/components/rocketchat/notify.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/roomba/vacuum.py
homeassistant/components/route53/* homeassistant/components/route53/*
homeassistant/components/rova/sensor.py 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.""" """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 from roku import Roku, RokuException
import voluptuous as vol 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.const import CONF_HOST
from homeassistant.helpers import discovery from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import 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 .const import DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -34,77 +27,67 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
# Currently no attributes but it might change later PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
ROKU_SCAN_SCHEMA = vol.Schema({}) SCAN_INTERVAL = timedelta(seconds=30)
def setup(hass, config): def get_roku_data(host: str) -> dict:
"""Set up the Roku component.""" """Retrieve a Roku instance and version info for the device."""
hass.data[DATA_ROKU] = {} roku = Roku(host)
roku_device_info = roku.device_info
def service_handler(service): return {
"""Handle service calls.""" DATA_CLIENT: roku,
if service.service == SERVICE_SCAN: DATA_DEVICE_INFO: roku_device_info,
scan_for_rokus(hass) }
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, []): if DOMAIN in config:
_setup_roku(hass, config, conf) for entry_config in config[DOMAIN]:
hass.async_create_task(
hass.services.register( hass.config_entries.flow.async_init(
DOMAIN, SERVICE_SCAN, service_handler, schema=ROKU_SCAN_SCHEMA DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config,
) )
)
return True return True
def scan_for_rokus(hass): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Scan for devices and present a notification of the ones found.""" """Set up Roku from a config entry."""
try:
rokus = Roku.discover() roku_data = await hass.async_add_executor_job(
get_roku_data, entry.data[CONF_HOST],
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,
)
) )
if not devices: except (SocketGIAError, RequestException, RokuException) as exception:
devices = ["No device(s) found"] raise ConfigEntryNotReady from exception
hass.components.persistent_notification.create( hass.data[DOMAIN][entry.entry_id] = roku_data
"The following devices were found:<br /><br />" + "<br /><br />".join(devices),
title=NOTIFICATION_SCAN_TITLE, for component in PLATFORMS:
notification_id=NOTIFICATION_SCAN_ID, 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): return unload_ok
"""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)

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.""" """Constants for the Roku integration."""
DOMAIN = "roku"
DATA_CLIENT = "client"
DATA_DEVICE_INFO = "device_info"
DEFAULT_PORT = 8060 DEFAULT_PORT = 8060
DEFAULT_MANUFACTURER = "Roku"

View File

@ -4,6 +4,14 @@
"documentation": "https://www.home-assistant.io/integrations/roku", "documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["roku==4.0.0"], "requirements": ["roku==4.0.0"],
"dependencies": [], "dependencies": [],
"after_dependencies": ["discovery"], "ssdp": [
"codeowners": ["@ctalkington"] {
"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.""" """Support for the Roku media player."""
import logging from requests.exceptions import (
ConnectionError as RequestsConnectionError,
import requests.exceptions ReadTimeout as RequestsReadTimeout,
from roku import Roku )
from roku import RokuException
from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
@ -16,17 +17,9 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
) )
from homeassistant.const import ( from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY
CONF_HOST,
STATE_HOME,
STATE_IDLE,
STATE_PLAYING,
STATE_STANDBY,
)
from .const import DEFAULT_PORT from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
SUPPORT_ROKU = ( SUPPORT_ROKU = (
SUPPORT_PREVIOUS_TRACK SUPPORT_PREVIOUS_TRACK
@ -40,23 +33,19 @@ SUPPORT_ROKU = (
) )
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Roku platform.""" """Set up the Roku config entry."""
if not discovery_info: roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
return async_add_entities([RokuDevice(roku)], True)
host = discovery_info[CONF_HOST]
async_add_entities([RokuDevice(host)], True)
class RokuDevice(MediaPlayerDevice): class RokuDevice(MediaPlayerDevice):
"""Representation of a Roku device on the network.""" """Representation of a Roku device on the network."""
def __init__(self, host): def __init__(self, roku):
"""Initialize the Roku device.""" """Initialize the Roku device."""
self.roku = roku
self.roku = Roku(host) self.ip_address = roku.host
self.ip_address = host
self.channels = [] self.channels = []
self.current_app = None self.current_app = None
self._available = False self._available = False
@ -77,7 +66,7 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None self.current_app = None
self._available = True self._available = True
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): except (RequestsConnectionError, RequestsReadTimeout, RokuException):
self._available = False self._available = False
pass pass
@ -130,6 +119,17 @@ class RokuDevice(MediaPlayerDevice):
"""Return a unique, Home Assistant friendly identifier for this entity.""" """Return a unique, Home Assistant friendly identifier for this entity."""
return self._device_info.serial_num 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 @property
def media_content_type(self): def media_content_type(self):
"""Content type of current playing media.""" """Content type of current playing media."""

View File

@ -1,34 +1,48 @@
"""Support for the Roku remote.""" """Support for the Roku remote."""
import requests.exceptions from typing import Callable, List
from roku import Roku
from homeassistant.components import remote from requests.exceptions import (
from homeassistant.const import CONF_HOST 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): async def async_setup_entry(
"""Set up the Roku remote platform.""" hass: HomeAssistantType,
if not discovery_info: entry: ConfigEntry,
return async_add_entities: Callable[[List, bool], None],
) -> bool:
host = discovery_info[CONF_HOST] """Load Roku remote based on a config entry."""
async_add_entities([RokuRemote(host)], True) 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.""" """Device that sends commands to an Roku."""
def __init__(self, host): def __init__(self, roku):
"""Initialize the Roku device.""" """Initialize the Roku device."""
self.roku = roku
self.roku = Roku(host) self._available = False
self._device_info = {} self._device_info = {}
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
if not self.enabled:
return
try: try:
self._device_info = self.roku.device_info 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 pass
@property @property
@ -38,11 +52,27 @@ class RokuRemote(remote.RemoteDevice):
return self._device_info.user_device_name return self._device_info.user_device_name
return f"Roku {self._device_info.serial_num}" 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 @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return a unique ID."""
return self._device_info.serial_num 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 @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """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", "rachio",
"rainmachine", "rainmachine",
"ring", "ring",
"roku",
"samsungtv", "samsungtv",
"sense", "sense",
"sentry", "sentry",

View File

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

View File

@ -628,6 +628,9 @@ rflink==0.0.52
# homeassistant.components.ring # homeassistant.components.ring
ring_doorbell==0.6.0 ring_doorbell==0.6.0
# homeassistant.components.roku
roku==4.0.0
# homeassistant.components.yamaha # homeassistant.components.yamaha
rxv==0.6.0 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