Support homekit discovery for roku (#44625)

* support homekit discovery for roku

* Update config_flow.py

* Update config_flow.py

* Update test_config_flow.py

* Update __init__.py

* Update __init__.py

* Update strings.json

* Update manifest.json

* Update __init__.py

* Update test_config_flow.py

* Update __init__.py

* Update manifest.json

* Update config_flow.py

* Update config_flow.py

* Update __init__.py

* Update test_config_flow.py

* Update __init__.py

* Update manifest.json

* Update __init__.py

* Update zeroconf.py

* Update config_flow.py

* Update test_config_flow.py

* Update config_flow.py

* Update test_config_flow.py

* Update __init__.py

* Update config_flow.py

* Update test_config_flow.py

* Update manifest.json

* Update zeroconf.py
pull/44641/head
Chris Talkington 2020-12-29 20:43:02 -06:00 committed by GitHub
parent 35a19a4d02
commit 12aa537eb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 8 deletions

View File

@ -85,6 +85,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=info["title"], data=user_input)
async def async_step_homekit(self, discovery_info):
"""Handle a flow initialized by homekit discovery."""
# If we already have the host configured do
# not open connections to it if we can avoid it.
if self._host_already_configured(discovery_info[CONF_HOST]):
return self.async_abort(reason="already_configured")
self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]})
try:
info = await validate_input(self.hass, self.discovery_info)
except RokuError:
_LOGGER.debug("Roku Error", exc_info=True)
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)
await self.async_set_unique_id(info["serial_number"])
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]},
)
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": info["title"]}})
self.discovery_info.update({CONF_NAME: info["title"]})
return await self.async_step_discovery_confirm()
async def async_step_ssdp(
self, discovery_info: Optional[Dict] = None
) -> Dict[str, Any]:
@ -110,16 +140,16 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unknown error trying to connect")
return self.async_abort(reason=ERROR_UNKNOWN)
return await self.async_step_ssdp_confirm()
return await self.async_step_discovery_confirm()
async def async_step_ssdp_confirm(
async def async_step_discovery_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
if user_input is None:
return self.async_show_form(
step_id="ssdp_confirm",
step_id="discovery_confirm",
description_placeholders={"name": self.discovery_info[CONF_NAME]},
errors={},
)
@ -128,3 +158,12 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
title=self.discovery_info[CONF_NAME],
data=self.discovery_info,
)
def _host_already_configured(self, host):
"""See if we already have a hub with the host address configured."""
existing_hosts = {
entry.data[CONF_HOST]
for entry in self._async_current_entries()
if CONF_HOST in entry.data
}
return host in existing_hosts

View File

@ -3,6 +3,14 @@
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.6.0"],
"homekit": {
"models": [
"3810X",
"4660X",
"7820X",
"C105X"
]
},
"ssdp": [
{
"st": "roku:ecp",

View File

@ -8,7 +8,7 @@
"host": "[%key:common::config_flow::data::host%]"
}
},
"ssdp_confirm": {
"discovery_confirm": {
"title": "Roku",
"description": "Do you want to set up {name}?",
"data": {}

View File

@ -157,10 +157,14 @@ ZEROCONF = {
}
HOMEKIT = {
"3810X": "roku",
"4660X": "roku",
"7820X": "roku",
"819LMB": "myq",
"AC02": "tado",
"Abode": "abode",
"BSB002": "hue",
"C105X": "roku",
"Healty Home Coach": "netatmo",
"Iota": "abode",
"LIFX": "lifx",

View File

@ -8,14 +8,16 @@ from homeassistant.components.ssdp import (
ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_SERIAL,
)
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME
from homeassistant.helpers.typing import HomeAssistantType
from tests.common import MockConfigEntry, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
HOST = "192.168.1.160"
NAME = "Roku 3"
NAME_ROKUTV = '58" Onn Roku TV'
HOST = "192.168.1.160"
SSDP_LOCATION = "http://192.168.1.160/"
UPNP_FRIENDLY_NAME = "My Roku 3"
UPNP_SERIAL = "1GU48T017973"
@ -26,6 +28,16 @@ MOCK_SSDP_DISCOVERY_INFO = {
ATTR_UPNP_SERIAL: UPNP_SERIAL,
}
HOMEKIT_HOST = "192.168.1.161"
MOCK_HOMEKIT_DISCOVERY_INFO = {
CONF_NAME: "onn._hap._tcp.local.",
CONF_HOST: HOMEKIT_HOST,
"properties": {
CONF_ID: "2d:97:da:ee:dc:99",
},
}
def mock_connection(
aioclient_mock: AiohttpClientMocker,

View File

@ -1,6 +1,6 @@
"""Test the Roku config flow."""
from homeassistant.components.roku.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@ -12,8 +12,11 @@ from homeassistant.setup import async_setup_component
from tests.async_mock import patch
from tests.components.roku import (
HOMEKIT_HOST,
HOST,
MOCK_HOMEKIT_DISCOVERY_INFO,
MOCK_SSDP_DISCOVERY_INFO,
NAME_ROKUTV,
UPNP_FRIENDLY_NAME,
mock_connection,
setup_integration,
@ -128,6 +131,92 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None:
assert len(mock_validate_input.mock_calls) == 1
async def test_homekit_cannot_connect(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort homekit flow on connection error."""
mock_connection(
aioclient_mock,
host=HOMEKIT_HOST,
error=True,
)
discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_HOMEKIT},
data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
async def test_homekit_unknown_error(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort homekit flow on unknown error."""
discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy()
with patch(
"homeassistant.components.roku.config_flow.Roku.update",
side_effect=Exception,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_HOMEKIT},
data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
async def test_homekit_discovery(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the homekit discovery flow."""
mock_connection(aioclient_mock, device="rokutv", host=HOMEKIT_HOST)
discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "discovery_confirm"
assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV}
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 hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input={}
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == NAME_ROKUTV
assert result["data"]
assert result["data"][CONF_HOST] == HOMEKIT_HOST
assert result["data"][CONF_NAME] == NAME_ROKUTV
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
# test abort on existing host
discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_ssdp_cannot_connect(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
@ -176,7 +265,7 @@ async def test_ssdp_discovery(
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "ssdp_confirm"
assert result["step_id"] == "discovery_confirm"
assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME}
with patch(