1 """Config flow to configure esphome component."""
3 from __future__
import annotations
5 from collections
import OrderedDict
6 from collections.abc
import Mapping
9 from typing
import Any, cast
11 from aioesphomeapi
import (
16 InvalidEncryptionKeyAPIError,
17 RequiresEncryptionAPIError,
21 import voluptuous
as vol
39 CONF_ALLOW_SERVICE_CALLS,
42 DEFAULT_ALLOW_SERVICE_CALLS,
43 DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
46 from .dashboard
import async_get_or_create_dashboard_manager, async_set_dashboard_info
48 ERROR_REQUIRES_ENCRYPTION_KEY =
"requires_encryption_key"
49 ERROR_INVALID_ENCRYPTION_KEY =
"invalid_psk"
50 ESPHOME_URL =
"https://esphome.io/"
51 _LOGGER = logging.getLogger(__name__)
53 ZERO_NOISE_PSK =
"MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
57 """Handle a esphome config flow."""
61 _reauth_entry: ConfigEntry
64 """Initialize flow."""
65 self.
_host_host: str |
None =
None
66 self.
__name__name: str |
None =
None
67 self.
_port_port: int |
None =
None
68 self.
_password_password: str |
None =
None
76 self, user_input: dict[str, Any] |
None =
None, error: str |
None =
None
77 ) -> ConfigFlowResult:
78 if user_input
is not None:
79 self.
_host_host = user_input[CONF_HOST]
80 self.
_port_port = user_input[CONF_PORT]
83 fields: dict[Any, type] = OrderedDict()
84 fields[vol.Required(CONF_HOST, default=self.
_host_host
or vol.UNDEFINED)] = str
85 fields[vol.Optional(CONF_PORT, default=self.
_port_port
or 6053)] = int
89 errors[
"base"] = error
93 data_schema=vol.Schema(fields),
95 description_placeholders={
"esphome_url": ESPHOME_URL},
99 self, user_input: dict[str, Any] |
None =
None
100 ) -> ConfigFlowResult:
101 """Handle a flow initialized by the user."""
105 self, entry_data: Mapping[str, Any]
106 ) -> ConfigFlowResult:
107 """Handle a flow initialized by a reauth event."""
109 self.
_host_host = entry_data[CONF_HOST]
110 self.
_port_port = entry_data[CONF_PORT]
131 self, user_input: dict[str, Any] |
None =
None
132 ) -> ConfigFlowResult:
133 """Handle reauthorization flow."""
141 if user_input
is not None:
146 errors[
"base"] = error
149 step_id=
"reauth_confirm",
150 data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
157 return self.
__name__name
or "ESPHome"
160 def _name(self, value: str) ->
None:
162 self.context[
"title_placeholders"] = {
"name": self.
_name_name_name_name}
165 """Try to fetch device info and return any errors."""
170 response = ERROR_REQUIRES_ENCRYPTION_KEY
179 if response == ERROR_REQUIRES_ENCRYPTION_KEY:
196 if response == ERROR_INVALID_ENCRYPTION_KEY:
198 response = ERROR_REQUIRES_ENCRYPTION_KEY
200 if response == ERROR_REQUIRES_ENCRYPTION_KEY:
202 if response
is not None:
216 self, user_input: dict[str, Any] |
None =
None
217 ) -> ConfigFlowResult:
218 """Handle user-confirmation of discovered node."""
219 if user_input
is not None:
222 step_id=
"discovery_confirm", description_placeholders={
"name": self.
_name_name_name_name}
226 self, discovery_info: zeroconf.ZeroconfServiceInfo
227 ) -> ConfigFlowResult:
228 """Handle zeroconf discovery."""
229 mac_address: str |
None = discovery_info.properties.get(
"mac")
233 if mac_address
is None:
240 device_name = discovery_info.hostname.removesuffix(
".local.")
242 self.
_name_name_name_name = discovery_info.properties.get(
"friendly_name", device_name)
244 self.
_host_host = discovery_info.host
245 self.
_port_port = discovery_info.port
246 self.
_noise_required_noise_required = bool(discovery_info.properties.get(
"api_encryption"))
251 updates={CONF_HOST: self.
_host_host, CONF_PORT: self.
_port_port}
257 self, discovery_info: MqttServiceInfo
258 ) -> ConfigFlowResult:
259 """Handle MQTT discovery."""
260 if not discovery_info.payload:
264 if "mac" not in device_info:
268 if "port" not in device_info:
271 if "ip" not in device_info:
275 unformatted_mac = cast(str, device_info[
"mac"])
278 device_name = cast(str, device_info[
"name"])
281 self.
_name_name_name_name = cast(str, device_info.get(
"friendly_name", device_name))
282 self.
_host_host = cast(str, device_info[
"ip"])
283 self.
_port_port = cast(int, device_info[
"port"])
290 updates={CONF_HOST: self.
_host_host, CONF_PORT: self.
_port_port}
296 self, discovery_info: dhcp.DhcpServiceInfo
297 ) -> ConfigFlowResult:
298 """Handle DHCP discovery."""
306 self, discovery_info: HassioServiceInfo
307 ) -> ConfigFlowResult:
308 """Handle Supervisor service discovery."""
312 discovery_info.config[
"host"],
313 discovery_info.config[
"port"],
320 CONF_HOST: self.
_host_host,
321 CONF_PORT: self.
_port_port,
323 CONF_PASSWORD: self.
_password_password
or "",
324 CONF_NOISE_PSK: self.
_noise_psk_noise_psk
or "",
328 CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
339 options=config_options,
343 self, user_input: dict[str, Any] |
None =
None
344 ) -> ConfigFlowResult:
345 """Handle getting psk for transport encryption."""
347 if user_input
is not None:
348 self.
_noise_psk_noise_psk = user_input[CONF_NOISE_PSK]
352 errors[
"base"] = error
355 step_id=
"encryption_key",
356 data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
362 self, user_input: dict[str, Any] |
None =
None, error: str |
None =
None
363 ) -> ConfigFlowResult:
364 """Handle getting password for authentication."""
365 if user_input
is not None:
366 self.
_password_password = user_input[CONF_PASSWORD]
373 if error
is not None:
374 errors[
"base"] = error
377 step_id=
"authenticate",
378 data_schema=vol.Schema({vol.Required(
"password"): str}),
384 """Fetch device info from API and return any errors."""
385 zeroconf_instance = await zeroconf.async_get_instance(self.hass)
386 assert self.
_host_host
is not None
387 assert self.
_port_port
is not None
392 zeroconf_instance=zeroconf_instance,
399 except RequiresEncryptionAPIError:
400 return ERROR_REQUIRES_ENCRYPTION_KEY
401 except InvalidEncryptionKeyAPIError
as ex:
405 return ERROR_INVALID_ENCRYPTION_KEY
406 except ResolveAPIError:
407 return "resolve_error"
408 except APIConnectionError:
409 return "connection_error"
411 await cli.disconnect(force=
True)
419 updates={CONF_HOST: self.
_host_host, CONF_PORT: self.
_port_port}
425 """Try logging in to device and return any errors."""
426 zeroconf_instance = await zeroconf.async_get_instance(self.hass)
427 assert self.
_host_host
is not None
428 assert self.
_port_port
is not None
433 zeroconf_instance=zeroconf_instance,
438 await cli.connect(login=
True)
439 except InvalidAuthAPIError:
440 return "invalid_auth"
441 except APIConnectionError:
442 return "connection_error"
444 await cli.disconnect(force=
True)
449 """Try to retrieve the encryption key from the dashboard.
451 Return boolean if a key was retrieved.
457 or (dashboard := manager.async_get())
is None
461 await dashboard.async_request_refresh()
462 if not dashboard.last_update_success:
465 device = dashboard.data.get(self.
_device_name_device_name)
471 noise_psk = await dashboard.api.get_encryption_key(device[
"configuration"])
472 except aiohttp.ClientError
as err:
473 _LOGGER.error(
"Error talking to the dashboard: %s", err)
475 except json.JSONDecodeError:
476 _LOGGER.exception(
"Error parsing response from dashboard")
485 config_entry: ConfigEntry,
486 ) -> OptionsFlowHandler:
487 """Get the options flow for this handler."""
492 """Handle a option flow for esphome."""
495 self, user_input: dict[str, Any] |
None =
None
496 ) -> ConfigFlowResult:
497 """Handle options flow."""
498 if user_input
is not None:
501 data_schema = vol.Schema(
504 CONF_ALLOW_SERVICE_CALLS,
506 CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
511 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=data_schema)
ConfigFlowResult _async_try_fetch_device_info(self)
str|None fetch_device_info(self)
ConfigFlowResult _async_get_entry(self)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult _async_step_user_base(self, dict[str, Any]|None user_input=None, str|None error=None)
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
ConfigFlowResult async_step_mqtt(self, MqttServiceInfo discovery_info)
None _name(self, str value)
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
ConfigFlowResult _async_authenticate_or_add(self)
ConfigFlowResult async_step_encryption_key(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_authenticate(self, dict[str, Any]|None user_input=None, str|None error=None)
bool _retrieve_encryption_key_from_dashboard(self)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_hassio(self, HassioServiceInfo discovery_info)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry _get_reauth_entry(self)
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_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)
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)
ConfigEntry config_entry(self)
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)
ESPHomeDashboardManager async_get_or_create_dashboard_manager(HomeAssistant hass)
None async_set_dashboard_info(HomeAssistant hass, str addon_slug, str host, int port)
JsonObjectType json_loads_object(bytes|bytearray|memoryview|str obj)