Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Plugwise integration."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, Self
6 
7 from plugwise import Smile
8 from plugwise.exceptions import (
9  ConnectionFailedError,
10  InvalidAuthentication,
11  InvalidSetupError,
12  InvalidXMLError,
13  ResponseError,
14  UnsupportedDeviceError,
15 )
16 import voluptuous as vol
17 
18 from homeassistant.components.zeroconf import ZeroconfServiceInfo
19 from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
20 from homeassistant.const import (
21  ATTR_CONFIGURATION_URL,
22  CONF_BASE,
23  CONF_HOST,
24  CONF_NAME,
25  CONF_PASSWORD,
26  CONF_PORT,
27  CONF_USERNAME,
28 )
29 from homeassistant.core import HomeAssistant
30 from homeassistant.helpers.aiohttp_client import async_get_clientsession
31 
32 from .const import (
33  DEFAULT_PORT,
34  DEFAULT_USERNAME,
35  DOMAIN,
36  FLOW_SMILE,
37  FLOW_STRETCH,
38  SMILE,
39  STRETCH,
40  STRETCH_USERNAME,
41  ZEROCONF_MAP,
42 )
43 
44 
45 def base_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
46  """Generate base schema for gateways."""
47  schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
48 
49  if not discovery_info:
50  schema = schema.extend(
51  {
52  vol.Required(CONF_HOST): str,
53  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
54  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
55  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
56  ),
57  }
58  )
59 
60  return schema
61 
62 
63 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
64  """Validate whether the user input allows us to connect to the gateway.
65 
66  Data has the keys from base_schema() with values provided by the user.
67  """
68  websession = async_get_clientsession(hass, verify_ssl=False)
69  api = Smile(
70  host=data[CONF_HOST],
71  password=data[CONF_PASSWORD],
72  port=data[CONF_PORT],
73  username=data[CONF_USERNAME],
74  websession=websession,
75  )
76  await api.connect()
77  return api
78 
79 
80 class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
81  """Handle a config flow for Plugwise Smile."""
82 
83  VERSION = 1
84 
85  discovery_info: ZeroconfServiceInfo | None = None
86  product: str = "Unknown Smile"
87  _username: str = DEFAULT_USERNAME
88 
90  self, discovery_info: ZeroconfServiceInfo
91  ) -> ConfigFlowResult:
92  """Prepare configuration for a discovered Plugwise Smile."""
93  self.discovery_infodiscovery_info = discovery_info
94  _properties = discovery_info.properties
95 
96  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
97  if config_entry := await self.async_set_unique_idasync_set_unique_id(unique_id):
98  try:
99  await validate_input(
100  self.hass,
101  {
102  CONF_HOST: discovery_info.host,
103  CONF_PORT: discovery_info.port,
104  CONF_USERNAME: config_entry.data[CONF_USERNAME],
105  CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
106  },
107  )
108  except Exception: # noqa: BLE001
109  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
110  else:
111  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
112  {
113  CONF_HOST: discovery_info.host,
114  CONF_PORT: discovery_info.port,
115  }
116  )
117 
118  if DEFAULT_USERNAME not in unique_id:
119  self._username_username = STRETCH_USERNAME
120  self.productproduct = _product = _properties.get("product", "Unknown Smile")
121  _version = _properties.get("version", "n/a")
122  _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
123 
124  # This is an Anna, but we already have config entries.
125  # Assuming that the user has already configured Adam, aborting discovery.
126  if self._async_current_entries_async_current_entries() and _product == "smile_thermo":
127  return self.async_abortasync_abortasync_abort(reason="anna_with_adam")
128 
129  # If we have discovered an Adam or Anna, both might be on the network.
130  # In that case, we need to cancel the Anna flow, as the Adam should
131  # be added.
132  if self.hass.config_entries.flow.async_has_matching_flow(self):
133  return self.async_abortasync_abortasync_abort(reason="anna_with_adam")
134 
135  self.context.update(
136  {
137  "title_placeholders": {CONF_NAME: _name},
138  ATTR_CONFIGURATION_URL: (
139  f"http://{discovery_info.host}:{discovery_info.port}"
140  ),
141  }
142  )
143  return await self.async_step_userasync_step_user()
144 
145  def is_matching(self, other_flow: Self) -> bool:
146  """Return True if other_flow is matching this flow."""
147  # This is an Anna, and there is already an Adam flow in progress
148  if self.productproduct == "smile_thermo" and other_flow.product == "smile_open_therm":
149  return True
150 
151  # This is an Adam, and there is already an Anna flow in progress
152  if self.productproduct == "smile_open_therm" and other_flow.product == "smile_thermo":
153  self.hass.config_entries.flow.async_abort(other_flow.flow_id)
154 
155  return False
156 
157  async def async_step_user(
158  self, user_input: dict[str, Any] | None = None
159  ) -> ConfigFlowResult:
160  """Handle the initial step when using network/gateway setups."""
161  errors: dict[str, str] = {}
162 
163  if user_input is not None:
164  if self.discovery_infodiscovery_info:
165  user_input[CONF_HOST] = self.discovery_infodiscovery_info.host
166  user_input[CONF_PORT] = self.discovery_infodiscovery_info.port
167  user_input[CONF_USERNAME] = self._username_username
168 
169  try:
170  api = await validate_input(self.hass, user_input)
171  except ConnectionFailedError:
172  errors[CONF_BASE] = "cannot_connect"
173  except InvalidAuthentication:
174  errors[CONF_BASE] = "invalid_auth"
175  except InvalidSetupError:
176  errors[CONF_BASE] = "invalid_setup"
177  except (InvalidXMLError, ResponseError):
178  errors[CONF_BASE] = "response_error"
179  except UnsupportedDeviceError:
180  errors[CONF_BASE] = "unsupported"
181  except Exception: # noqa: BLE001
182  errors[CONF_BASE] = "unknown"
183  else:
184  await self.async_set_unique_idasync_set_unique_id(
185  api.smile_hostname or api.gateway_id, raise_on_progress=False
186  )
187  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
188 
189  return self.async_create_entryasync_create_entryasync_create_entry(title=api.smile_name, data=user_input)
190 
191  return self.async_show_formasync_show_formasync_show_form(
192  step_id=SOURCE_USER,
193  data_schema=base_schema(self.discovery_infodiscovery_info),
194  errors=errors,
195  )
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:91
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_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool is_matching(self, Self other_flow)
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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
Smile validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:63
vol.Schema base_schema(ZeroconfServiceInfo|None discovery_info)
Definition: config_flow.py:45
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)