1 """Config flow for TP-Link."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
7 from typing
import TYPE_CHECKING, Any, Self
18 import voluptuous
as vol
43 async_discover_devices,
44 create_async_tplink_clientsession,
51 CONF_CONFIG_ENTRY_MINOR_VERSION,
52 CONF_CONNECTION_PARAMETERS,
53 CONF_CREDENTIALS_HASH,
59 _LOGGER = logging.getLogger(__name__)
61 STEP_AUTH_DATA_SCHEMA = vol.Schema(
62 {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
67 """Handle a config flow for tplink."""
70 MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
72 host: str |
None =
None
73 port: int |
None =
None
76 """Initialize the config flow."""
81 self, discovery_info: dhcp.DhcpServiceInfo
82 ) -> ConfigFlowResult:
83 """Handle discovery via dhcp."""
85 discovery_info.ip, dr.format_mac(discovery_info.macaddress)
89 self, discovery_info: DiscoveryInfoType
90 ) -> ConfigFlowResult:
91 """Handle integration discovery."""
93 discovery_info[CONF_HOST],
94 discovery_info[CONF_MAC],
95 discovery_info[CONF_DEVICE],
100 self, entry: ConfigEntry, host: str, device: Device |
None
102 """Return updates if the host or device config has changed."""
103 entry_data = entry.data
104 updates: dict[str, Any] = {}
105 new_connection_params =
False
106 if entry_data[CONF_HOST] != host:
107 updates[CONF_HOST] = host
109 device_conn_params_dict = device.config.connection_type.to_dict()
110 entry_conn_params_dict = entry_data.get(CONF_CONNECTION_PARAMETERS)
111 if device_conn_params_dict != entry_conn_params_dict:
112 new_connection_params =
True
113 updates[CONF_CONNECTION_PARAMETERS] = device_conn_params_dict
114 updates[CONF_USES_HTTP] = device.config.uses_http
117 updates = {**entry.data, **updates}
119 if new_connection_params:
120 updates.pop(CONF_CREDENTIALS_HASH,
None)
122 "Connection type changed for %s from %s to: %s",
124 entry_conn_params_dict,
125 device_conn_params_dict,
131 self, entry: ConfigEntry, host: str, device: Device |
None
132 ) -> ConfigFlowResult |
None:
133 """If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
134 if entry.state
not in (
135 ConfigEntryState.SETUP_ERROR,
136 ConfigEntryState.SETUP_RETRY,
143 reason=
"already_configured",
148 self, host: str, formatted_mac: str, device: Device |
None =
None
149 ) -> ConfigFlowResult:
150 """Handle any discovery."""
152 formatted_mac, raise_on_progress=
False
154 if current_entry
and (
156 current_entry, host, device
163 if self.hass.config_entries.flow.async_has_matching_flow(self):
175 raise_on_progress=
True,
176 raise_on_timeout=
True,
178 except AuthenticationError:
180 except KasaException:
186 """Return True if other_flow is matching this flow."""
187 return other_flow.host == self.
hosthost
190 self, user_input: dict[str, Any] |
None =
None
191 ) -> ConfigFlowResult:
192 """Dialog that informs the user that auth is required."""
197 if credentials
and credentials != self.
_discovered_device_discovered_device.config.credentials:
202 except AuthenticationError:
211 username = user_input[CONF_USERNAME]
212 password = user_input[CONF_PASSWORD]
213 credentials = Credentials(username, password)
218 except AuthenticationError
as ex:
219 errors[CONF_PASSWORD] =
"invalid_auth"
220 placeholders[
"error"] =
str(ex)
221 except KasaException
as ex:
222 errors[
"base"] =
"cannot_connect"
223 placeholders[
"error"] =
str(ex)
227 self.hass.async_create_task(
232 self.context[
"title_placeholders"] = placeholders
234 step_id=
"discovery_auth_confirm",
235 data_schema=STEP_AUTH_DATA_SCHEMA,
237 description_placeholders=placeholders,
241 """Make placeholders for the discovery steps."""
243 assert discovered_device
is not None
245 "name": discovered_device.alias
or mac_alias(discovered_device.mac),
246 "model": discovered_device.model,
247 "host": discovered_device.host,
251 self, user_input: dict[str, Any] |
None =
None
252 ) -> ConfigFlowResult:
253 """Confirm discovery."""
255 if user_input
is not None:
260 self.context[
"title_placeholders"] = placeholders
262 step_id=
"discovery_confirm", description_placeholders=placeholders
267 """Parse the host string for host and port."""
269 _, _, bracketed = host_str.partition(
"[")
270 host, _, port_str = bracketed.partition(
"]")
271 _, _, port_str = port_str.partition(
":")
273 host, _, port_str = host_str.partition(
":")
286 self, user_input: dict[str, Any] |
None =
None
287 ) -> ConfigFlowResult:
288 """Handle the initial step."""
289 errors: dict[str, str] = {}
290 placeholders: dict[str, str] = {}
292 if user_input
is not None:
293 if not (host := user_input[CONF_HOST]):
298 match_dict = {CONF_HOST: host}
301 match_dict[CONF_PORT] = port
310 raise_on_progress=
False,
311 raise_on_timeout=
False,
315 credentials=credentials,
316 raise_on_progress=
False,
319 except AuthenticationError:
321 except KasaException
as ex:
322 errors[
"base"] =
"cannot_connect"
323 placeholders[
"error"] =
str(ex)
331 data_schema=vol.Schema({vol.Optional(CONF_HOST, default=
""): str}),
333 description_placeholders=placeholders,
337 self, user_input: dict[str, Any] |
None =
None
338 ) -> ConfigFlowResult:
339 """Dialog that informs the user that auth is required."""
340 errors: dict[str, str] = {}
343 assert self.
hosthost
is not None
344 placeholders: dict[str, str] = {CONF_HOST: self.
hosthost}
347 username = user_input[CONF_USERNAME]
348 password = user_input[CONF_PASSWORD]
349 credentials = Credentials(username, password)
350 device: Device |
None
359 credentials=credentials,
360 raise_on_progress=
False,
363 except AuthenticationError
as ex:
364 errors[CONF_PASSWORD] =
"invalid_auth"
365 placeholders[
"error"] =
str(ex)
366 except KasaException
as ex:
367 errors[
"base"] =
"cannot_connect"
368 placeholders[
"error"] =
str(ex)
371 errors[
"base"] =
"cannot_connect"
372 placeholders[
"error"] =
"try_connect_all failed"
375 self.hass.async_create_task(
381 step_id=
"user_auth_confirm",
382 data_schema=STEP_AUTH_DATA_SCHEMA,
384 description_placeholders=placeholders,
388 self, user_input: dict[str, Any] |
None =
None
389 ) -> ConfigFlowResult:
390 """Handle the step to pick discovered device."""
391 if user_input
is not None:
392 mac = user_input[CONF_DEVICE]
402 except AuthenticationError:
404 except KasaException:
408 configured_devices = {
414 f
"{device.alias or mac_alias(device.mac)} {device.model} ({device.host}) {formatted_mac}"
417 if formatted_mac
not in configured_devices
423 step_id=
"pick_device",
424 data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
428 """Reload any in progress config flow that now have credentials."""
429 _config_entries = self.hass.config_entries
432 await _config_entries.async_reload(self.
_get_reauth_entry_get_reauth_entry().entry_id)
434 for flow
in _config_entries.flow.async_progress_by_handler(
435 DOMAIN, include_uninitialized=
True
437 context = flow[
"context"]
438 if context.get(
"source") != SOURCE_REAUTH:
440 entry_id: str = context[
"entry_id"]
441 if entry := _config_entries.async_get_entry(entry_id):
442 await _config_entries.async_reload(entry.entry_id)
443 if entry.state
is ConfigEntryState.LOADED:
444 _config_entries.flow.async_abort(flow[
"flow_id"])
448 """Create a config entry from a smart device."""
452 data: dict[str, Any] = {
453 CONF_HOST: device.host,
454 CONF_ALIAS: device.alias,
455 CONF_MODEL: device.model,
456 CONF_CONNECTION_PARAMETERS: device.config.connection_type.to_dict(),
457 CONF_USES_HTTP: device.config.uses_http,
459 if device.config.aes_keys:
460 data[CONF_AES_KEYS] = device.config.aes_keys
461 if device.credentials_hash:
462 data[CONF_CREDENTIALS_HASH] = device.credentials_hash
463 if port := device.config.port_override:
464 data[CONF_PORT] = port
466 title=f
"{device.alias} {device.model}",
473 credentials: Credentials |
None,
474 raise_on_progress: bool,
476 port: int |
None =
None,
478 """Try to connect to the device speculatively.
480 The connection parameters aren't known but discovery has failed so try
484 device = await Discover.try_connect_all(
486 credentials=credentials,
494 device = await Device.connect(
495 config=DeviceConfig(host, port_override=port)
501 dr.format_mac(device.mac),
502 raise_on_progress=raise_on_progress,
509 credentials: Credentials |
None,
510 raise_on_progress: bool,
511 raise_on_timeout: bool,
513 port: int |
None =
None,
515 """Try to discover the device and call update.
517 Will try to connect directly if discovery fails.
523 credentials=credentials,
526 except TimeoutError
as ex:
534 raise_on_progress=raise_on_progress,
545 discovered_device: Device,
546 credentials: Credentials |
None,
548 """Try to connect."""
551 config = discovered_device.config
553 config.credentials = credentials
554 config.timeout = CONNECT_TIMEOUT
561 raise_on_progress=
False,
566 self, entry_data: Mapping[str, Any]
567 ) -> ConfigFlowResult:
568 """Start the reauthentication flow if the device needs updated credentials."""
572 self, user_input: dict[str, Any] |
None =
None
573 ) -> ConfigFlowResult:
574 """Dialog that informs the user that reauth is required."""
575 errors: dict[str, str] = {}
576 placeholders: dict[str, str] = {}
578 entry_data = reauth_entry.data
579 host = entry_data[CONF_HOST]
580 port = entry_data.get(CONF_PORT)
583 username = user_input[CONF_USERNAME]
584 password = user_input[CONF_PASSWORD]
585 credentials = Credentials(username, password)
589 credentials=credentials,
590 raise_on_progress=
False,
591 raise_on_timeout=
False,
595 credentials=credentials,
596 raise_on_progress=
False,
599 except AuthenticationError
as ex:
600 errors[CONF_PASSWORD] =
"invalid_auth"
601 placeholders[
"error"] =
str(ex)
602 except KasaException
as ex:
603 errors[
"base"] =
"cannot_connect"
604 placeholders[
"error"] =
str(ex)
607 errors[
"base"] =
"cannot_connect"
608 placeholders[
"error"] =
"try_connect_all failed"
611 dr.format_mac(device.mac),
612 raise_on_progress=
False,
616 self.hass.config_entries.async_update_entry(
617 reauth_entry, data=updates
619 self.hass.async_create_task(
625 alias = entry_data.get(CONF_ALIAS)
or "unknown"
626 model = entry_data.get(CONF_MODEL)
or "unknown"
628 placeholders.update({
"name": alias,
"model": model,
"host": host})
630 self.context[
"title_placeholders"] = placeholders
632 step_id=
"reauth_confirm",
633 data_schema=STEP_AUTH_DATA_SCHEMA,
635 description_placeholders=placeholders,
ConfigFlowResult _async_handle_discovery(self, str host, str formatted_mac, Device|None device=None)
bool is_matching(self, Self other_flow)
ConfigFlowResult async_step_discovery_auth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user_auth_confirm(self, dict[str, Any]|None user_input=None)
tuple[str, int|None] _async_get_host_port(str host_str)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Device|None _async_try_discover_and_update(self, str host, Credentials|None credentials, bool raise_on_progress, bool raise_on_timeout, *int|None port=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Device _async_try_connect(self, Device discovered_device, Credentials|None credentials)
dict[str, str] _async_make_placeholders_from_discovery(self)
ConfigFlowResult _async_create_entry_from_device(self, Device device)
None _async_reload_requires_auth_entries(self)
Device|None _async_try_connect_all(self, str host, Credentials|None credentials, bool raise_on_progress, *int|None port=None)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult|None _update_config_if_entry_in_setup_error(self, ConfigEntry entry, str host, Device|None device)
dict|None _get_config_updates(self, ConfigEntry entry, str host, Device|None device)
ConfigFlowResult async_step_pick_device(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)
None _set_confirm_only(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)
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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
IssData update(pyiss.ISS iss)
dict[str, Device] async_discover_devices(HomeAssistant hass)
Credentials|None get_credentials(HomeAssistant hass)
None set_credentials(HomeAssistant hass, str username, str password)
ClientSession create_async_tplink_clientsession(HomeAssistant hass)