Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Logitech Harmony Hub integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 from typing import Any
8 from urllib.parse import urlparse
9 
10 from aioharmony.hubconnector_websocket import HubConnector
11 import aiohttp
12 import voluptuous as vol
13 
14 from homeassistant.components import ssdp
16  ATTR_ACTIVITY,
17  ATTR_DELAY_SECS,
18  DEFAULT_DELAY_SECS,
19 )
20 from homeassistant.config_entries import (
21  ConfigEntry,
22  ConfigFlow,
23  ConfigFlowResult,
24  OptionsFlow,
25 )
26 from homeassistant.const import CONF_HOST, CONF_NAME
27 from homeassistant.core import callback
28 from homeassistant.exceptions import HomeAssistantError
29 
30 from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID
31 from .util import (
32  find_best_name_for_remote,
33  find_unique_id_for_remote,
34  get_harmony_client_if_available,
35 )
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 DATA_SCHEMA = vol.Schema(
40  {vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}, extra=vol.ALLOW_EXTRA
41 )
42 
43 
44 async def validate_input(data: dict[str, Any]) -> dict[str, Any]:
45  """Validate the user input allows us to connect.
46 
47  Data has the keys from DATA_SCHEMA with values provided by the user.
48  """
49  harmony = await get_harmony_client_if_available(data[CONF_HOST])
50  if not harmony:
51  raise CannotConnect
52 
53  return {
54  CONF_NAME: find_best_name_for_remote(data, harmony),
55  CONF_HOST: data[CONF_HOST],
56  UNIQUE_ID: find_unique_id_for_remote(harmony),
57  }
58 
59 
60 class HarmonyConfigFlow(ConfigFlow, domain=DOMAIN):
61  """Handle a config flow for Logitech Harmony Hub."""
62 
63  VERSION = 1
64 
65  def __init__(self) -> None:
66  """Initialize the Harmony config flow."""
67  self.harmony_configharmony_config: dict[str, Any] = {}
68 
69  async def async_step_user(
70  self, user_input: dict[str, Any] | None = None
71  ) -> ConfigFlowResult:
72  """Handle the initial step."""
73  errors: dict[str, str] = {}
74  if user_input is not None:
75  try:
76  validated = await validate_input(user_input)
77  except CannotConnect:
78  errors["base"] = "cannot_connect"
79  except Exception:
80  _LOGGER.exception("Unexpected exception")
81  errors["base"] = "unknown"
82 
83  if "base" not in errors:
84  await self.async_set_unique_idasync_set_unique_id(validated[UNIQUE_ID])
85  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
86  return await self._async_create_entry_from_valid_input_async_create_entry_from_valid_input(
87  validated, user_input
88  )
89 
90  # Return form
91  return self.async_show_formasync_show_formasync_show_form(
92  step_id="user", data_schema=DATA_SCHEMA, errors=errors
93  )
94 
95  async def async_step_ssdp(
96  self, discovery_info: ssdp.SsdpServiceInfo
97  ) -> ConfigFlowResult:
98  """Handle a discovered Harmony device."""
99  _LOGGER.debug("SSDP discovery_info: %s", discovery_info)
100 
101  parsed_url = urlparse(discovery_info.ssdp_location)
102  friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
103 
104  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: parsed_url.hostname})
105 
106  self.context["title_placeholders"] = {"name": friendly_name}
107 
108  self.harmony_configharmony_config = {
109  CONF_HOST: parsed_url.hostname,
110  CONF_NAME: friendly_name,
111  }
112 
113  connector = HubConnector(parsed_url.hostname, asyncio.Queue())
114  try:
115  remote_id = await connector.get_remote_id()
116  except aiohttp.ClientError:
117  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
118  finally:
119  await connector.async_close_session()
120 
121  unique_id = str(remote_id)
122  await self.async_set_unique_idasync_set_unique_id(str(unique_id))
123  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
124  updates={CONF_HOST: self.harmony_configharmony_config[CONF_HOST]}
125  )
126  self.harmony_configharmony_config[UNIQUE_ID] = unique_id
127  return await self.async_step_linkasync_step_link()
128 
129  async def async_step_link(
130  self, user_input: dict[str, Any] | None = None
131  ) -> ConfigFlowResult:
132  """Attempt to link with the Harmony."""
133  errors: dict[str, str] = {}
134 
135  if user_input is not None:
136  # Everything was validated in async_step_ssdp
137  # all we do now is create.
138  return await self._async_create_entry_from_valid_input_async_create_entry_from_valid_input(
139  self.harmony_configharmony_config, {}
140  )
141 
142  self._set_confirm_only_set_confirm_only()
143  return self.async_show_formasync_show_formasync_show_form(
144  step_id="link",
145  errors=errors,
146  description_placeholders={
147  CONF_HOST: self.harmony_configharmony_config[CONF_NAME],
148  CONF_NAME: self.harmony_configharmony_config[CONF_HOST],
149  },
150  )
151 
152  @staticmethod
153  @callback
155  config_entry: ConfigEntry,
156  ) -> OptionsFlowHandler:
157  """Get the options flow for this handler."""
158  return OptionsFlowHandler()
159 
161  self, validated: dict[str, Any], user_input: dict[str, Any]
162  ) -> ConfigFlowResult:
163  """Single path to create the config entry from validated input."""
164 
165  data = {
166  CONF_NAME: validated[CONF_NAME],
167  CONF_HOST: validated[CONF_HOST],
168  }
169  # Options from yaml are preserved, we will pull them out when
170  # we setup the config entry
171  data.update(_options_from_user_input(user_input))
172 
173  return self.async_create_entryasync_create_entryasync_create_entry(title=validated[CONF_NAME], data=data)
174 
175 
176 def _options_from_user_input(user_input: dict[str, Any]) -> dict[str, Any]:
177  options: dict[str, Any] = {}
178  if ATTR_ACTIVITY in user_input:
179  options[ATTR_ACTIVITY] = user_input[ATTR_ACTIVITY]
180  if ATTR_DELAY_SECS in user_input:
181  options[ATTR_DELAY_SECS] = user_input[ATTR_DELAY_SECS]
182  return options
183 
184 
186  """Handle a option flow for Harmony."""
187 
188  async def async_step_init(
189  self, user_input: dict[str, Any] | None = None
190  ) -> ConfigFlowResult:
191  """Handle options flow."""
192  if user_input is not None:
193  return self.async_create_entryasync_create_entry(title="", data=user_input)
194 
195  remote = self.config_entryconfig_entryconfig_entry.runtime_data
196  data_schema = vol.Schema(
197  {
198  vol.Optional(
199  ATTR_DELAY_SECS,
200  default=self.config_entryconfig_entryconfig_entry.options.get(
201  ATTR_DELAY_SECS, DEFAULT_DELAY_SECS
202  ),
203  ): vol.Coerce(float),
204  vol.Optional(
205  ATTR_ACTIVITY,
206  default=self.config_entryconfig_entryconfig_entry.options.get(
207  ATTR_ACTIVITY, PREVIOUS_ACTIVE_ACTIVITY
208  ),
209  ): vol.In([PREVIOUS_ACTIVE_ACTIVITY, *remote.activity_names]),
210  }
211  )
212  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
213 
214 
216  """Error to indicate we cannot connect."""
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:97
ConfigFlowResult _async_create_entry_from_valid_input(self, dict[str, Any] validated, dict[str, Any] user_input)
Definition: config_flow.py:162
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:131
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:156
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:71
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:190
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
str
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
dict[str, Any] validate_input(dict[str, Any] data)
Definition: config_flow.py:44
dict[str, Any] _options_from_user_input(dict[str, Any] user_input)
Definition: config_flow.py:176
def find_unique_id_for_remote(HarmonyAPI harmony)
Definition: util.py:9
def find_best_name_for_remote(dict data, HarmonyAPI harmony)
Definition: util.py:18
HarmonyAPI|None get_harmony_client_if_available(str ip_address)
Definition: util.py:29