Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Hunter Douglas PowerView integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import TYPE_CHECKING, Any, Self
7 
8 import voluptuous as vol
9 
10 from homeassistant.components import dhcp, zeroconf
11 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
12 from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME
13 from homeassistant.core import HomeAssistant
14 from homeassistant.exceptions import HomeAssistantError
15 
16 from .const import DOMAIN, HUB_EXCEPTIONS
17 from .util import async_connect_hub
18 
19 _LOGGER = logging.getLogger(__name__)
20 
21 HAP_SUFFIX = "._hap._tcp.local."
22 POWERVIEW_G2_SUFFIX = "._powerview._tcp.local."
23 POWERVIEW_G3_SUFFIX = "._PowerView-G3._tcp.local."
24 
25 
26 async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str]:
27  """Validate the user input allows us to connect.
28 
29  Data has the keys from DATA_SCHEMA with values provided by the user.
30  """
31  api = await async_connect_hub(hass, hub_address)
32  hub = api.hub
33  device_info = api.device_info
34  if hub.role != "Primary":
35  raise UnsupportedDevice(
36  f"{hub.name} ({hub.hub_address}) is the {hub.role} Hub. "
37  "Only the Primary can manage shades"
38  )
39 
40  _LOGGER.debug("Connection made using api version: %s", hub.api_version)
41 
42  # Return info that you want to store in the config entry.
43  return {
44  "title": device_info.name,
45  "unique_id": device_info.serial_number,
46  CONF_API_VERSION: hub.api_version,
47  }
48 
49 
50 class PowerviewConfigFlow(ConfigFlow, domain=DOMAIN):
51  """Handle a config flow for Hunter Douglas PowerView."""
52 
53  VERSION = 1
54  MINOR_VERSION = 2
55 
56  def __init__(self) -> None:
57  """Initialize the powerview config flow."""
58  self.powerview_configpowerview_config: dict = {}
59  self.discovered_ipdiscovered_ip: str | None = None
60  self.discovered_namediscovered_name: str | None = None
61  self.data_schema: dict = {vol.Required(CONF_HOST): str}
62 
63  async def async_step_user(
64  self, user_input: dict[str, Any] | None = None
65  ) -> ConfigFlowResult:
66  """Handle the initial step."""
67  errors: dict[str, str] = {}
68 
69  if user_input is not None:
70  info, error = await self._async_validate_or_error_async_validate_or_error(user_input[CONF_HOST])
71 
72  if info and not error:
73  self.powerview_configpowerview_config = {
74  CONF_HOST: user_input[CONF_HOST],
75  CONF_NAME: info["title"],
76  CONF_API_VERSION: info[CONF_API_VERSION],
77  }
78  await self.async_set_unique_idasync_set_unique_id(info["unique_id"])
79  return self.async_create_entryasync_create_entryasync_create_entry(
80  title=self.powerview_configpowerview_config[CONF_NAME],
81  data={
82  CONF_HOST: self.powerview_configpowerview_config[CONF_HOST],
83  CONF_API_VERSION: self.powerview_configpowerview_config[CONF_API_VERSION],
84  },
85  )
86 
87  if TYPE_CHECKING:
88  assert error is not None
89  errors["base"] = error
90 
91  return self.async_show_formasync_show_formasync_show_form(
92  step_id="user", data_schema=vol.Schema(self.data_schema), errors=errors
93  )
94 
96  self, host: str
97  ) -> tuple[dict[str, str], None] | tuple[None, str]:
98  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
99 
100  try:
101  info = await validate_input(self.hass, host)
102  except HUB_EXCEPTIONS:
103  return None, "cannot_connect"
104  except UnsupportedDevice:
105  return None, "unsupported_device"
106  except Exception:
107  _LOGGER.exception("Unexpected exception")
108  return None, "unknown"
109 
110  return info, None
111 
112  async def async_step_dhcp(
113  self, discovery_info: dhcp.DhcpServiceInfo
114  ) -> ConfigFlowResult:
115  """Handle DHCP discovery."""
116  self.discovered_ipdiscovered_ip = discovery_info.ip
117  self.discovered_namediscovered_name = discovery_info.hostname
118  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
119 
121  self, discovery_info: zeroconf.ZeroconfServiceInfo
122  ) -> ConfigFlowResult:
123  """Handle zeroconf discovery."""
124  self.discovered_ipdiscovered_ip = discovery_info.host
125  name = discovery_info.name.removesuffix(POWERVIEW_G2_SUFFIX)
126  name = name.removesuffix(POWERVIEW_G3_SUFFIX)
127  self.discovered_namediscovered_name = name
128  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
129 
131  self, discovery_info: zeroconf.ZeroconfServiceInfo
132  ) -> ConfigFlowResult:
133  """Handle HomeKit discovery."""
134  self.discovered_ipdiscovered_ip = discovery_info.host
135  name = discovery_info.name.removesuffix(HAP_SUFFIX)
136  self.discovered_namediscovered_name = name
137  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
138 
139  async def async_step_discovery_confirm(self) -> ConfigFlowResult:
140  """Confirm dhcp or homekit discovery."""
141  # If we already have the host configured do
142  # not open connections to it if we can avoid it.
143  assert self.discovered_ipdiscovered_ip and self.discovered_namediscovered_name is not None
144  if self.hass.config_entries.flow.async_has_matching_flow(self):
145  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
146 
147  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: self.discovered_ipdiscovered_ip})
148  info, error = await self._async_validate_or_error_async_validate_or_error(self.discovered_ipdiscovered_ip)
149  if error:
150  return self.async_abortasync_abortasync_abort(reason=error)
151  assert info is not None
152 
153  api_version = info[CONF_API_VERSION]
154  if not self.discovered_namediscovered_name:
155  self.discovered_namediscovered_name = f"Powerview Generation {api_version}"
156 
157  await self.async_set_unique_idasync_set_unique_id(info["unique_id"], raise_on_progress=False)
158  self._abort_if_unique_id_configured_abort_if_unique_id_configured({CONF_HOST: self.discovered_ipdiscovered_ip})
159 
160  self.powerview_configpowerview_config = {
161  CONF_HOST: self.discovered_ipdiscovered_ip,
162  CONF_NAME: self.discovered_namediscovered_name,
163  CONF_API_VERSION: api_version,
164  }
165  return await self.async_step_linkasync_step_link()
166 
167  def is_matching(self, other_flow: Self) -> bool:
168  """Return True if other_flow is matching this flow."""
169  return other_flow.discovered_ip == self.discovered_ipdiscovered_ip
170 
171  async def async_step_link(
172  self, user_input: dict[str, Any] | None = None
173  ) -> ConfigFlowResult:
174  """Attempt to link with Powerview."""
175  if user_input is not None:
176  return self.async_create_entryasync_create_entryasync_create_entry(
177  title=self.powerview_configpowerview_config[CONF_NAME],
178  data={
179  CONF_HOST: self.powerview_configpowerview_config[CONF_HOST],
180  CONF_API_VERSION: self.powerview_configpowerview_config[CONF_API_VERSION],
181  },
182  )
183 
184  self._set_confirm_only_set_confirm_only()
185  self.context["title_placeholders"] = self.powerview_configpowerview_config
186  return self.async_show_formasync_show_formasync_show_form(
187  step_id="link", description_placeholders=self.powerview_configpowerview_config
188  )
189 
190 
192  """Error to indicate the device is not supported."""
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:65
tuple[dict[str, str], None]|tuple[None, str] _async_validate_or_error(self, str host)
Definition: config_flow.py:97
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:132
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:173
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:114
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:122
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)
_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, str] validate_input(HomeAssistant hass, str hub_address)
Definition: config_flow.py:26
PowerviewAPI async_connect_hub(HomeAssistant hass, str address, int|None api_version=None)
Definition: util.py:26