Make harmony handle IP address changes (#33173)

If the IP address of the harmony hub changed it would
not be rediscovered.  We now connect to get the
unique id and then update config entries with
the correct ip if it is already setup.
This commit is contained in:
J. Nick Koston 2020-03-22 23:24:49 -05:00 committed by GitHub
parent 087b672449
commit 0b2a8bf79a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 41 deletions

View File

@ -37,11 +37,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
try: try:
device = HarmonyRemote(name, address, activity, harmony_conf_file, delay_secs) device = HarmonyRemote(
await device.connect() name, entry.unique_id, address, activity, harmony_conf_file, delay_secs
)
connected_ok = await device.connect()
except (asyncio.TimeoutError, ValueError, AttributeError): except (asyncio.TimeoutError, ValueError, AttributeError):
raise ConfigEntryNotReady raise ConfigEntryNotReady
if not connected_ok:
raise ConfigEntryNotReady
hass.data[DOMAIN][entry.entry_id] = device hass.data[DOMAIN][entry.entry_id] = device
entry.add_update_listener(_update_listener) entry.add_update_listener(_update_listener)

View File

@ -26,24 +26,32 @@ DATA_SCHEMA = vol.Schema(
) )
async def get_harmony_client_if_available(hass: core.HomeAssistant, ip_address):
"""Connect to a harmony hub and fetch info."""
harmony = HarmonyAPI(ip_address=ip_address)
try:
if not await harmony.connect():
await harmony.close()
return None
except harmony_exceptions.TimeOut:
return None
await harmony.close()
return harmony
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
harmony = HarmonyAPI(ip_address=data[CONF_HOST]) harmony = await get_harmony_client_if_available(hass, data[CONF_HOST])
if not harmony:
_LOGGER.debug("harmony:%s", harmony)
try:
if not await harmony.connect():
await harmony.close()
raise CannotConnect
except harmony_exceptions.TimeOut:
raise CannotConnect raise CannotConnect
unique_id = find_unique_id_for_remote(harmony) unique_id = find_unique_id_for_remote(harmony)
await harmony.close()
# As a last resort we get the name from the harmony client # As a last resort we get the name from the harmony client
# in the event a name was not provided. harmony.name is # in the event a name was not provided. harmony.name is
@ -74,7 +82,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
info = await validate_input(self.hass, user_input) validated = await validate_input(self.hass, user_input)
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -82,7 +90,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
if "base" not in errors: if "base" not in errors:
return await self._async_create_entry_from_valid_input(info, user_input) await self.async_set_unique_id(validated[UNIQUE_ID])
self._abort_if_unique_id_configured()
return await self._async_create_entry_from_valid_input(
validated, user_input
)
# Return form # Return form
return self.async_show_form( return self.async_show_form(
@ -104,8 +116,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_NAME: friendly_name, CONF_NAME: friendly_name,
} }
if self._host_already_configured(self.harmony_config): harmony = await get_harmony_client_if_available(
return self.async_abort(reason="already_configured") self.hass, self.harmony_config[CONF_HOST]
)
if harmony:
unique_id = find_unique_id_for_remote(harmony)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.harmony_config[CONF_HOST]}
)
self.harmony_config[UNIQUE_ID] = unique_id
return await self.async_step_link() return await self.async_step_link()
@ -114,16 +135,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: # Everything was validated in async_step_ssdp
info = await validate_input(self.hass, self.harmony_config) # all we do now is create.
except CannotConnect: return await self._async_create_entry_from_valid_input(
errors["base"] = "cannot_connect" self.harmony_config, {}
except Exception: # pylint: disable=broad-except )
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if "base" not in errors:
return await self._async_create_entry_from_valid_input(info, user_input)
return self.async_show_form( return self.async_show_form(
step_id="link", step_id="link",
@ -146,8 +162,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_create_entry_from_valid_input(self, validated, user_input): async def _async_create_entry_from_valid_input(self, validated, user_input):
"""Single path to create the config entry from validated input.""" """Single path to create the config entry from validated input."""
await self.async_set_unique_id(validated[UNIQUE_ID])
self._abort_if_unique_id_configured()
data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]} data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]}
# Options from yaml are preserved, we will pull them out when # Options from yaml are preserved, we will pull them out when
# we setup the config entry # we setup the config entry

View File

@ -34,7 +34,6 @@ from .const import (
SERVICE_CHANGE_CHANNEL, SERVICE_CHANGE_CHANNEL,
SERVICE_SYNC, SERVICE_SYNC,
) )
from .util import find_unique_id_for_remote
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -130,7 +129,7 @@ def register_services(hass):
class HarmonyRemote(remote.RemoteDevice): class HarmonyRemote(remote.RemoteDevice):
"""Remote representation used to control a Harmony device.""" """Remote representation used to control a Harmony device."""
def __init__(self, name, host, activity, out_path, delay_secs): def __init__(self, name, unique_id, host, activity, out_path, delay_secs):
"""Initialize HarmonyRemote class.""" """Initialize HarmonyRemote class."""
self._name = name self._name = name
self.host = host self.host = host
@ -141,6 +140,7 @@ class HarmonyRemote(remote.RemoteDevice):
self._config_path = out_path self._config_path = out_path
self.delay_secs = delay_secs self.delay_secs = delay_secs
self._available = False self._available = False
self._unique_id = unique_id
self._undo_dispatch_subscription = None self._undo_dispatch_subscription = None
@property @property
@ -217,7 +217,7 @@ class HarmonyRemote(remote.RemoteDevice):
@property @property
def unique_id(self): def unique_id(self):
"""Return the unique id.""" """Return the unique id."""
return find_unique_id_for_remote(self._client) return self._unique_id
@property @property
def name(self): def name(self):

View File

@ -91,19 +91,28 @@ async def test_form_ssdp(hass):
"""Test we get the form with ssdp source.""" """Test we get the form with ssdp source."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch(
"homeassistant.components.harmony.config_flow.HarmonyAPI",
return_value=harmonyapi,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_SSDP}, context={"source": config_entries.SOURCE_SSDP},
data={ data={
"friendlyName": "Harmony Hub", "friendlyName": "Harmony Hub",
"ssdp_location": "http://192.168.209.238:8088/description", "ssdp_location": "http://192.168.1.12:8088/description",
}, },
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "link" assert result["step_id"] == "link"
assert result["errors"] == {} assert result["errors"] == {}
assert result["description_placeholders"] == {
"host": "Harmony Hub",
"name": "192.168.1.12",
}
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch( with patch(
"homeassistant.components.harmony.config_flow.HarmonyAPI", "homeassistant.components.harmony.config_flow.HarmonyAPI",
return_value=harmonyapi, return_value=harmonyapi,
@ -117,7 +126,7 @@ async def test_form_ssdp(hass):
assert result2["type"] == "create_entry" assert result2["type"] == "create_entry"
assert result2["title"] == "Harmony Hub" assert result2["title"] == "Harmony Hub"
assert result2["data"] == { assert result2["data"] == {
"host": "192.168.209.238", "host": "192.168.1.12",
"name": "Harmony Hub", "name": "Harmony Hub",
} }
await hass.async_block_till_done() await hass.async_block_till_done()