Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Somfy MyLink integration."""
2 
3 from __future__ import annotations
4 
5 from copy import deepcopy
6 import logging
7 from typing import Any
8 
9 from somfy_mylink_synergy import SomfyMyLinkSynergy
10 import voluptuous as vol
11 
12 from homeassistant.components import dhcp
13 from homeassistant.config_entries import (
14  ConfigEntry,
15  ConfigEntryState,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import CONF_HOST, CONF_PORT
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.exceptions import HomeAssistantError
23 from homeassistant.helpers.device_registry import format_mac
24 
25 from .const import (
26  CONF_REVERSE,
27  CONF_REVERSED_TARGET_IDS,
28  CONF_SYSTEM_ID,
29  CONF_TARGET_ID,
30  CONF_TARGET_NAME,
31  DEFAULT_PORT,
32  DOMAIN,
33  MYLINK_STATUS,
34 )
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 
39 async def validate_input(hass: HomeAssistant, data):
40  """Validate the user input allows us to connect.
41 
42  Data has the keys from schema with values provided by the user.
43  """
44  somfy_mylink = SomfyMyLinkSynergy(
45  data[CONF_SYSTEM_ID], data[CONF_HOST], data[CONF_PORT]
46  )
47 
48  try:
49  status_info = await somfy_mylink.status_info()
50  except TimeoutError as ex:
51  raise CannotConnect from ex
52 
53  if not status_info or "error" in status_info:
54  _LOGGER.debug("Auth error: %s", status_info)
55  raise InvalidAuth
56 
57  return {"title": f"MyLink {data[CONF_HOST]}"}
58 
59 
60 class SomfyConfigFlow(ConfigFlow, domain=DOMAIN):
61  """Handle a config flow for Somfy MyLink."""
62 
63  VERSION = 1
64 
65  def __init__(self) -> None:
66  """Initialize the somfy_mylink flow."""
67  self.hosthost: str | None = None
68  self.macmac: str | None = None
69  self.ip_addressip_address: str | None = None
70 
71  async def async_step_dhcp(
72  self, discovery_info: dhcp.DhcpServiceInfo
73  ) -> ConfigFlowResult:
74  """Handle dhcp discovery."""
75  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: discovery_info.ip})
76 
77  formatted_mac = format_mac(discovery_info.macaddress)
78  await self.async_set_unique_idasync_set_unique_id(format_mac(formatted_mac))
79  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
80  self.hosthost = discovery_info.hostname
81  self.macmac = formatted_mac
82  self.ip_addressip_address = discovery_info.ip
83  self.context["title_placeholders"] = {"ip": self.ip_addressip_address, "mac": self.macmac}
84  return await self.async_step_userasync_step_userasync_step_user()
85 
86  async def async_step_user(
87  self, user_input: dict[str, Any] | None = None
88  ) -> ConfigFlowResult:
89  """Handle the initial step."""
90  errors = {}
91 
92  if user_input is not None:
93  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
94 
95  try:
96  info = await validate_input(self.hass, user_input)
97  except CannotConnect:
98  errors["base"] = "cannot_connect"
99  except InvalidAuth:
100  errors["base"] = "invalid_auth"
101  except Exception:
102  _LOGGER.exception("Unexpected exception")
103  errors["base"] = "unknown"
104  else:
105  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=user_input)
106 
107  return self.async_show_formasync_show_formasync_show_form(
108  step_id="user",
109  data_schema=vol.Schema(
110  {
111  vol.Required(CONF_HOST, default=self.ip_addressip_address): str,
112  vol.Required(CONF_SYSTEM_ID): str,
113  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
114  }
115  ),
116  errors=errors,
117  )
118 
119  @staticmethod
120  @callback
122  config_entry: ConfigEntry,
123  ) -> OptionsFlowHandler:
124  """Get the options flow for this handler."""
125  return OptionsFlowHandler(config_entry)
126 
127 
129  """Handle a option flow for somfy_mylink."""
130 
131  def __init__(self, config_entry: ConfigEntry) -> None:
132  """Initialize options flow."""
133  self.optionsoptions = deepcopy(dict(config_entry.options))
134  self._target_id_target_id: str | None = None
135 
136  @callback
138  """Return the list of targets."""
139  return self.hass.data[DOMAIN][self.config_entryconfig_entryconfig_entry.entry_id][MYLINK_STATUS][
140  "result"
141  ]
142 
143  @callback
144  def _async_get_target_name(self, target_id) -> str:
145  """Find the name of a target in the api data."""
146  mylink_targets = self._async_callback_targets_async_callback_targets()
147  for cover in mylink_targets:
148  if cover["targetID"] == target_id:
149  return cover["name"]
150  raise KeyError
151 
152  async def async_step_init(
153  self, user_input: dict[str, Any] | None = None
154  ) -> ConfigFlowResult:
155  """Handle options flow."""
156 
157  if self.config_entryconfig_entryconfig_entry.state is not ConfigEntryState.LOADED:
158  _LOGGER.error("MyLink must be connected to manage device options")
159  return self.async_abortasync_abort(reason="cannot_connect")
160 
161  if user_input is not None:
162  if target_id := user_input.get(CONF_TARGET_ID):
163  return await self.async_step_target_configasync_step_target_config(None, target_id)
164 
165  return self.async_create_entryasync_create_entry(title="", data=self.optionsoptions)
166 
167  cover_dict = {None: None}
168  mylink_targets = self._async_callback_targets_async_callback_targets()
169  if mylink_targets:
170  for cover in mylink_targets:
171  cover_dict[cover["targetID"]] = cover["name"]
172 
173  data_schema = vol.Schema({vol.Optional(CONF_TARGET_ID): vol.In(cover_dict)})
174 
175  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema, errors={})
176 
178  self, user_input: dict[str, bool] | None = None, target_id: str | None = None
179  ) -> ConfigFlowResult:
180  """Handle options flow for target."""
181  reversed_target_ids: dict[str | None, bool] = self.optionsoptions.setdefault(
182  CONF_REVERSED_TARGET_IDS, {}
183  )
184 
185  if user_input is not None:
186  if user_input[CONF_REVERSE] != reversed_target_ids.get(self._target_id_target_id):
187  reversed_target_ids[self._target_id_target_id] = user_input[CONF_REVERSE]
188  return await self.async_step_initasync_step_init()
189 
190  self._target_id_target_id = target_id
191 
192  return self.async_show_formasync_show_form(
193  step_id="target_config",
194  data_schema=vol.Schema(
195  {
196  vol.Optional(
197  CONF_REVERSE,
198  default=reversed_target_ids.get(target_id, False),
199  ): bool
200  }
201  ),
202  description_placeholders={
203  CONF_TARGET_NAME: self._async_get_target_name_async_get_target_name(target_id),
204  },
205  errors={},
206  )
207 
208 
210  """Error to indicate we cannot connect."""
211 
212 
213 class InvalidAuth(HomeAssistantError):
214  """Error to indicate there is invalid auth."""
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_step_user(self, dict[str, Any]|None user_input=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)