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.pypull/44641/head
parent
35a19a4d02
commit
12aa537eb9
|
@ -85,6 +85,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
return self.async_create_entry(title=info["title"], data=user_input)
|
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(
|
async def async_step_ssdp(
|
||||||
self, discovery_info: Optional[Dict] = None
|
self, discovery_info: Optional[Dict] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
@ -110,16 +140,16 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
_LOGGER.exception("Unknown error trying to connect")
|
_LOGGER.exception("Unknown error trying to connect")
|
||||||
return self.async_abort(reason=ERROR_UNKNOWN)
|
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
|
self, user_input: Optional[Dict] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle user-confirmation of discovered device."""
|
"""Handle user-confirmation of discovered device."""
|
||||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="ssdp_confirm",
|
step_id="discovery_confirm",
|
||||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||||
errors={},
|
errors={},
|
||||||
)
|
)
|
||||||
|
@ -128,3 +158,12 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
title=self.discovery_info[CONF_NAME],
|
title=self.discovery_info[CONF_NAME],
|
||||||
data=self.discovery_info,
|
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
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
"name": "Roku",
|
"name": "Roku",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||||
"requirements": ["rokuecp==0.6.0"],
|
"requirements": ["rokuecp==0.6.0"],
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"3810X",
|
||||||
|
"4660X",
|
||||||
|
"7820X",
|
||||||
|
"C105X"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "roku:ecp",
|
"st": "roku:ecp",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ssdp_confirm": {
|
"discovery_confirm": {
|
||||||
"title": "Roku",
|
"title": "Roku",
|
||||||
"description": "Do you want to set up {name}?",
|
"description": "Do you want to set up {name}?",
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|
|
@ -157,10 +157,14 @@ ZEROCONF = {
|
||||||
}
|
}
|
||||||
|
|
||||||
HOMEKIT = {
|
HOMEKIT = {
|
||||||
|
"3810X": "roku",
|
||||||
|
"4660X": "roku",
|
||||||
|
"7820X": "roku",
|
||||||
"819LMB": "myq",
|
"819LMB": "myq",
|
||||||
"AC02": "tado",
|
"AC02": "tado",
|
||||||
"Abode": "abode",
|
"Abode": "abode",
|
||||||
"BSB002": "hue",
|
"BSB002": "hue",
|
||||||
|
"C105X": "roku",
|
||||||
"Healty Home Coach": "netatmo",
|
"Healty Home Coach": "netatmo",
|
||||||
"Iota": "abode",
|
"Iota": "abode",
|
||||||
"LIFX": "lifx",
|
"LIFX": "lifx",
|
||||||
|
|
|
@ -8,14 +8,16 @@ from homeassistant.components.ssdp import (
|
||||||
ATTR_UPNP_FRIENDLY_NAME,
|
ATTR_UPNP_FRIENDLY_NAME,
|
||||||
ATTR_UPNP_SERIAL,
|
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 homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, load_fixture
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
HOST = "192.168.1.160"
|
|
||||||
NAME = "Roku 3"
|
NAME = "Roku 3"
|
||||||
|
NAME_ROKUTV = '58" Onn Roku TV'
|
||||||
|
|
||||||
|
HOST = "192.168.1.160"
|
||||||
SSDP_LOCATION = "http://192.168.1.160/"
|
SSDP_LOCATION = "http://192.168.1.160/"
|
||||||
UPNP_FRIENDLY_NAME = "My Roku 3"
|
UPNP_FRIENDLY_NAME = "My Roku 3"
|
||||||
UPNP_SERIAL = "1GU48T017973"
|
UPNP_SERIAL = "1GU48T017973"
|
||||||
|
@ -26,6 +28,16 @@ MOCK_SSDP_DISCOVERY_INFO = {
|
||||||
ATTR_UPNP_SERIAL: UPNP_SERIAL,
|
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(
|
def mock_connection(
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Test the Roku config flow."""
|
"""Test the Roku config flow."""
|
||||||
from homeassistant.components.roku.const import DOMAIN
|
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.const import CONF_HOST, CONF_NAME, CONF_SOURCE
|
||||||
from homeassistant.data_entry_flow import (
|
from homeassistant.data_entry_flow import (
|
||||||
RESULT_TYPE_ABORT,
|
RESULT_TYPE_ABORT,
|
||||||
|
@ -12,8 +12,11 @@ from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.async_mock import patch
|
from tests.async_mock import patch
|
||||||
from tests.components.roku import (
|
from tests.components.roku import (
|
||||||
|
HOMEKIT_HOST,
|
||||||
HOST,
|
HOST,
|
||||||
|
MOCK_HOMEKIT_DISCOVERY_INFO,
|
||||||
MOCK_SSDP_DISCOVERY_INFO,
|
MOCK_SSDP_DISCOVERY_INFO,
|
||||||
|
NAME_ROKUTV,
|
||||||
UPNP_FRIENDLY_NAME,
|
UPNP_FRIENDLY_NAME,
|
||||||
mock_connection,
|
mock_connection,
|
||||||
setup_integration,
|
setup_integration,
|
||||||
|
@ -128,6 +131,92 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None:
|
||||||
assert len(mock_validate_input.mock_calls) == 1
|
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(
|
async def test_ssdp_cannot_connect(
|
||||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -176,7 +265,7 @@ async def test_ssdp_discovery(
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == RESULT_TYPE_FORM
|
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}
|
assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
|
Loading…
Reference in New Issue