Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for IntelliFire integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from dataclasses import dataclass
7 from typing import Any
8 
9 from aiohttp import ClientConnectionError
10 from intellifire4py.cloud_interface import IntelliFireCloudInterface
11 from intellifire4py.exceptions import LoginError
12 from intellifire4py.local_api import IntelliFireAPILocal
13 from intellifire4py.model import IntelliFireCommonFireplaceData
14 import voluptuous as vol
15 
16 from homeassistant.components.dhcp import DhcpServiceInfo
17 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
18 from homeassistant.const import (
19  CONF_API_KEY,
20  CONF_HOST,
21  CONF_IP_ADDRESS,
22  CONF_PASSWORD,
23  CONF_USERNAME,
24 )
25 
26 from .const import (
27  API_MODE_LOCAL,
28  CONF_AUTH_COOKIE,
29  CONF_CONTROL_MODE,
30  CONF_READ_MODE,
31  CONF_SERIAL,
32  CONF_USER_ID,
33  CONF_WEB_CLIENT_ID,
34  DOMAIN,
35  LOGGER,
36 )
37 
38 STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
39 
40 MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated
41 
42 
43 @dataclass
45  """Host info for discovery."""
46 
47  ip: str
48  serial: str | None
49 
50 
52  host: str, dhcp_mode: bool = False
53 ) -> str:
54  """Validate the user input allows us to connect.
55 
56  Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
57  """
58  LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host)
59  api = IntelliFireAPILocal(fireplace_ip=host)
60  await api.poll(suppress_warnings=dhcp_mode)
61  serial = api.data.serial
62 
63  LOGGER.debug("Found a fireplace: %s", serial)
64 
65  # Return the serial number which will be used to calculate a unique ID for the device/sensors
66  return serial
67 
68 
69 class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
70  """Handle a config flow for IntelliFire."""
71 
72  VERSION = 1
73  MINOR_VERSION = 2
74 
75  def __init__(self) -> None:
76  """Initialize the Config Flow Handler."""
77 
78  # DHCP Variables
79  self._dhcp_discovered_serial_dhcp_discovered_serial: str = "" # used only in discovery mode
80  self._discovered_host: DiscoveredHostInfo
81  self._dhcp_mode_dhcp_mode = False
82 
83  self._not_configured_hosts: list[DiscoveredHostInfo] = []
84  self._reauth_needed: DiscoveredHostInfo
85 
86  self._configured_serials_configured_serials: list[str] = []
87 
88  # Define a cloud api interface we can use
89  self.cloud_api_interfacecloud_api_interface = IntelliFireCloudInterface()
90 
91  async def async_step_user(
92  self, user_input: dict[str, Any] | None = None
93  ) -> ConfigFlowResult:
94  """Start the user flow."""
95 
96  current_entries = self._async_current_entries_async_current_entries(include_ignore=False)
97  self._configured_serials_configured_serials = [
98  entry.data[CONF_SERIAL] for entry in current_entries
99  ]
100 
101  return await self.async_step_cloud_apiasync_step_cloud_api()
102 
104  self, user_input: dict[str, Any] | None = None
105  ) -> ConfigFlowResult:
106  """Authenticate against IFTAPI Cloud in order to see configured devices.
107 
108  Local control of IntelliFire devices requires that the user download the correct API KEY which is only available on the cloud. Cloud control of the devices requires the user has at least once authenticated against the cloud and a set of cookie variables have been stored locally.
109 
110  """
111  errors: dict[str, str] = {}
112  LOGGER.debug("STEP: cloud_api")
113 
114  if user_input is not None:
115  try:
116  async with self.cloud_api_interfacecloud_api_interface as cloud_interface:
117  await cloud_interface.login_with_credentials(
118  username=user_input[CONF_USERNAME],
119  password=user_input[CONF_PASSWORD],
120  )
121 
122  # If login was successful pass username/password to next step
123  return await self.async_step_pick_cloud_deviceasync_step_pick_cloud_device()
124  except LoginError:
125  errors["base"] = "api_error"
126 
127  return self.async_show_formasync_show_formasync_show_form(
128  step_id="cloud_api",
129  errors=errors,
130  data_schema=vol.Schema(
131  {
132  vol.Required(CONF_USERNAME): str,
133  vol.Required(CONF_PASSWORD): str,
134  }
135  ),
136  )
137 
139  self, user_input: dict[str, Any] | None = None
140  ) -> ConfigFlowResult:
141  """Step to select a device from the cloud.
142 
143  We can only get here if we have logged in. If there is only one device available it will be auto-configured,
144  else the user will be given a choice to pick a device.
145  """
146  errors: dict[str, str] = {}
147  LOGGER.debug(
148  f"STEP: pick_cloud_device: {user_input} - DHCP_MODE[{self._dhcp_mode}"
149  )
150 
151  if self._dhcp_mode_dhcp_mode or user_input is not None:
152  if self._dhcp_mode_dhcp_mode:
153  serial = self._dhcp_discovered_serial_dhcp_discovered_serial
154  LOGGER.debug(f"DHCP Mode detected for serial [{serial}]")
155  if user_input is not None:
156  serial = user_input[CONF_SERIAL]
157 
158  # Run a unique ID Check prior to anything else
159  await self.async_set_unique_idasync_set_unique_id(serial)
160  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_SERIAL: serial})
161 
162  # If Serial is Good obtain fireplace and configure
163  fireplace = self.cloud_api_interfacecloud_api_interface.user_data.get_data_for_serial(serial)
164  if fireplace:
165  return await self._async_create_config_entry_from_common_data_async_create_config_entry_from_common_data(
166  fireplace=fireplace
167  )
168 
169  # Parse User Data to see if we auto-configure or prompt for selection:
170  user_data = self.cloud_api_interfacecloud_api_interface.user_data
171 
172  available_fireplaces: list[IntelliFireCommonFireplaceData] = [
173  fp
174  for fp in user_data.fireplaces
175  if fp.serial not in self._configured_serials_configured_serials
176  ]
177 
178  # Abort if all devices have been configured
179  if not available_fireplaces:
180  return self.async_abortasync_abortasync_abort(reason="no_available_devices")
181 
182  # If there is a single fireplace configure it
183  if len(available_fireplaces) == 1:
184  return await self._async_create_config_entry_from_common_data_async_create_config_entry_from_common_data(
185  fireplace=available_fireplaces[0]
186  )
187 
188  return self.async_show_formasync_show_formasync_show_form(
189  step_id="pick_cloud_device",
190  errors=errors,
191  data_schema=vol.Schema(
192  {
193  vol.Required(CONF_SERIAL): vol.In(
194  [fp.serial for fp in available_fireplaces]
195  )
196  }
197  ),
198  )
199 
201  self, fireplace: IntelliFireCommonFireplaceData
202  ) -> ConfigFlowResult:
203  """Construct a config entry based on an object of IntelliFireCommonFireplaceData."""
204 
205  data = {
206  CONF_IP_ADDRESS: fireplace.ip_address,
207  CONF_API_KEY: fireplace.api_key,
208  CONF_SERIAL: fireplace.serial,
209  CONF_AUTH_COOKIE: fireplace.auth_cookie,
210  CONF_WEB_CLIENT_ID: fireplace.web_client_id,
211  CONF_USER_ID: fireplace.user_id,
212  CONF_USERNAME: self.cloud_api_interfacecloud_api_interface.user_data.username,
213  CONF_PASSWORD: self.cloud_api_interfacecloud_api_interface.user_data.password,
214  }
215 
216  options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL}
217 
218  if self.sourcesourcesourcesource == SOURCE_REAUTH:
219  return self.async_update_reload_and_abortasync_update_reload_and_abort(
220  self._get_reauth_entry_get_reauth_entry(), data=data, options=options
221  )
222  return self.async_create_entryasync_create_entryasync_create_entry(
223  title=f"Fireplace {fireplace.serial}", data=data, options=options
224  )
225 
226  async def async_step_reauth(
227  self, entry_data: Mapping[str, Any]
228  ) -> ConfigFlowResult:
229  """Perform reauth upon an API authentication error."""
230  LOGGER.debug("STEP: reauth")
231 
232  # populate the expected vars
233  self._dhcp_discovered_serial_dhcp_discovered_serial = self._get_reauth_entry_get_reauth_entry().data[CONF_SERIAL]
234 
235  placeholders = {"serial": self._dhcp_discovered_serial_dhcp_discovered_serial}
236  self.context["title_placeholders"] = placeholders
237 
238  return await self.async_step_cloud_apiasync_step_cloud_api()
239 
240  async def async_step_dhcp(
241  self, discovery_info: DhcpServiceInfo
242  ) -> ConfigFlowResult:
243  """Handle DHCP Discovery."""
244  self._dhcp_mode_dhcp_mode = True
245 
246  # Run validation logic on ip
247  ip_address = discovery_info.ip
248  LOGGER.debug("STEP: dhcp for ip_address %s", ip_address)
249 
250  self._async_abort_entries_match_async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
251  try:
252  self._dhcp_discovered_serial_dhcp_discovered_serial = await _async_poll_local_fireplace_for_serial(
253  ip_address, dhcp_mode=True
254  )
255  except (ConnectionError, ClientConnectionError):
256  LOGGER.debug(
257  "DHCP Discovery has determined %s is not an IntelliFire device",
258  ip_address,
259  )
260  return self.async_abortasync_abortasync_abort(reason="not_intellifire_device")
261 
262  return await self.async_step_cloud_apiasync_step_cloud_api()
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:93
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:228
ConfigFlowResult _async_create_config_entry_from_common_data(self, IntelliFireCommonFireplaceData fireplace)
Definition: config_flow.py:202
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
Definition: config_flow.py:242
ConfigFlowResult async_step_pick_cloud_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:140
ConfigFlowResult async_step_cloud_api(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:105
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
str _async_poll_local_fireplace_for_serial(str host, bool dhcp_mode=False)
Definition: config_flow.py:53