Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Roku."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 from urllib.parse import urlparse
8 
9 from rokuecp import Roku, RokuError
10 import voluptuous as vol
11 
12 from homeassistant.components import ssdp, zeroconf
13 from homeassistant.config_entries import (
14  ConfigEntry,
15  ConfigFlow,
16  ConfigFlowResult,
17  OptionsFlow,
18 )
19 from homeassistant.const import CONF_HOST, CONF_NAME
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.helpers.aiohttp_client import async_get_clientsession
22 
23 from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
24 
25 DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
26 
27 ERROR_CANNOT_CONNECT = "cannot_connect"
28 ERROR_UNKNOWN = "unknown"
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 
33 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
34  """Validate the user input allows us to connect.
35 
36  Data has the keys from DATA_SCHEMA with values provided by the user.
37  """
38  session = async_get_clientsession(hass)
39  roku = Roku(data[CONF_HOST], session=session)
40  device = await roku.update()
41 
42  return {
43  "title": device.info.name,
44  "serial_number": device.info.serial_number,
45  }
46 
47 
48 class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
49  """Handle a Roku config flow."""
50 
51  VERSION = 1
52 
53  discovery_info: dict[str, Any]
54 
55  def __init__(self) -> None:
56  """Set up the instance."""
57  self.discovery_infodiscovery_info = {}
58 
59  @callback
60  def _show_form(self, errors: dict[str, Any] | None = None) -> ConfigFlowResult:
61  """Show the form to the user."""
62  return self.async_show_formasync_show_formasync_show_form(
63  step_id="user",
64  data_schema=DATA_SCHEMA,
65  errors=errors or {},
66  )
67 
68  async def async_step_user(
69  self, user_input: dict[str, Any] | None = None
70  ) -> ConfigFlowResult:
71  """Handle a flow initialized by the user."""
72  if not user_input:
73  return self._show_form_show_form()
74 
75  errors = {}
76 
77  try:
78  info = await validate_input(self.hass, user_input)
79  except RokuError:
80  _LOGGER.debug("Roku Error", exc_info=True)
81  errors["base"] = ERROR_CANNOT_CONNECT
82  return self._show_form_show_form(errors)
83  except Exception:
84  _LOGGER.exception("Unknown error trying to connect")
85  return self.async_abortasync_abortasync_abort(reason=ERROR_UNKNOWN)
86 
87  await self.async_set_unique_idasync_set_unique_id(info["serial_number"])
88  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
89 
90  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=user_input)
91 
92  async def async_step_homekit(
93  self, discovery_info: zeroconf.ZeroconfServiceInfo
94  ) -> ConfigFlowResult:
95  """Handle a flow initialized by homekit discovery."""
96 
97  # If we already have the host configured do
98  # not open connections to it if we can avoid it.
99  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: discovery_info.host})
100 
101  self.discovery_infodiscovery_info.update({CONF_HOST: discovery_info.host})
102 
103  try:
104  info = await validate_input(self.hass, self.discovery_infodiscovery_info)
105  except RokuError:
106  _LOGGER.debug("Roku Error", exc_info=True)
107  return self.async_abortasync_abortasync_abort(reason=ERROR_CANNOT_CONNECT)
108  except Exception:
109  _LOGGER.exception("Unknown error trying to connect")
110  return self.async_abortasync_abortasync_abort(reason=ERROR_UNKNOWN)
111 
112  await self.async_set_unique_idasync_set_unique_id(info["serial_number"])
113  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
114  updates={CONF_HOST: discovery_info.host},
115  )
116 
117  self.context.update({"title_placeholders": {"name": info["title"]}})
118  self.discovery_infodiscovery_info.update({CONF_NAME: info["title"]})
119 
120  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
121 
122  async def async_step_ssdp(
123  self, discovery_info: ssdp.SsdpServiceInfo
124  ) -> ConfigFlowResult:
125  """Handle a flow initialized by discovery."""
126  host = urlparse(discovery_info.ssdp_location).hostname
127  name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
128  serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
129 
130  await self.async_set_unique_idasync_set_unique_id(serial_number)
131  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
132 
133  self.context.update({"title_placeholders": {"name": name}})
134 
135  self.discovery_infodiscovery_info.update({CONF_HOST: host, CONF_NAME: name})
136 
137  try:
138  await validate_input(self.hass, self.discovery_infodiscovery_info)
139  except RokuError:
140  _LOGGER.debug("Roku Error", exc_info=True)
141  return self.async_abortasync_abortasync_abort(reason=ERROR_CANNOT_CONNECT)
142  except Exception:
143  _LOGGER.exception("Unknown error trying to connect")
144  return self.async_abortasync_abortasync_abort(reason=ERROR_UNKNOWN)
145 
146  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
147 
149  self, user_input: dict | None = None
150  ) -> ConfigFlowResult:
151  """Handle user-confirmation of discovered device."""
152  if user_input is None:
153  return self.async_show_formasync_show_formasync_show_form(
154  step_id="discovery_confirm",
155  description_placeholders={"name": self.discovery_infodiscovery_info[CONF_NAME]},
156  errors={},
157  )
158 
159  return self.async_create_entryasync_create_entryasync_create_entry(
160  title=self.discovery_infodiscovery_info[CONF_NAME],
161  data=self.discovery_infodiscovery_info,
162  )
163 
164  @staticmethod
165  @callback
167  config_entry: ConfigEntry,
168  ) -> RokuOptionsFlowHandler:
169  """Create the options flow."""
170  return RokuOptionsFlowHandler()
171 
172 
174  """Handle Roku options."""
175 
176  async def async_step_init(
177  self, user_input: dict[str, Any] | None = None
178  ) -> ConfigFlowResult:
179  """Manage Roku options."""
180  if user_input is not None:
181  return self.async_create_entryasync_create_entry(title="", data=user_input)
182 
183  return self.async_show_formasync_show_form(
184  step_id="init",
185  data_schema=vol.Schema(
186  {
187  vol.Optional(
188  CONF_PLAY_MEDIA_APP_ID,
189  default=self.config_entryconfig_entryconfig_entry.options.get(
190  CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
191  ),
192  ): str,
193  }
194  ),
195  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:70
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:94
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:124
ConfigFlowResult async_step_discovery_confirm(self, dict|None user_input=None)
Definition: config_flow.py:150
ConfigFlowResult _show_form(self, dict[str, Any]|None errors=None)
Definition: config_flow.py:60
RokuOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:168
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:178
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)
_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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
dict[str, Any] validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:33
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)