Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Samsung TV."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from functools import partial
7 import socket
8 from typing import Any, Self
9 from urllib.parse import urlparse
10 
11 import getmac
12 from samsungtvws.encrypted.authenticator import SamsungTVEncryptedWSAsyncAuthenticator
13 import voluptuous as vol
14 
15 from homeassistant.components import dhcp, ssdp, zeroconf
16 from homeassistant.config_entries import (
17  ConfigEntry,
18  ConfigEntryState,
19  ConfigFlow,
20  ConfigFlowResult,
21 )
22 from homeassistant.const import (
23  CONF_HOST,
24  CONF_MAC,
25  CONF_METHOD,
26  CONF_MODEL,
27  CONF_NAME,
28  CONF_PORT,
29  CONF_TOKEN,
30 )
31 from homeassistant.core import callback
32 from homeassistant.data_entry_flow import AbortFlow
33 from homeassistant.helpers.aiohttp_client import async_get_clientsession
34 from homeassistant.helpers.device_registry import format_mac
35 
36 from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info
37 from .const import (
38  CONF_MANUFACTURER,
39  CONF_SESSION_ID,
40  CONF_SSDP_MAIN_TV_AGENT_LOCATION,
41  CONF_SSDP_RENDERING_CONTROL_LOCATION,
42  DEFAULT_MANUFACTURER,
43  DOMAIN,
44  LOGGER,
45  METHOD_ENCRYPTED_WEBSOCKET,
46  METHOD_LEGACY,
47  RESULT_AUTH_MISSING,
48  RESULT_CANNOT_CONNECT,
49  RESULT_INVALID_PIN,
50  RESULT_NOT_SUPPORTED,
51  RESULT_SUCCESS,
52  RESULT_UNKNOWN_HOST,
53  SUCCESSFUL_RESULTS,
54  UPNP_SVC_MAIN_TV_AGENT,
55  UPNP_SVC_RENDERING_CONTROL,
56 )
57 
58 DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str})
59 
60 
61 def _strip_uuid(udn: str) -> str:
62  return udn[5:] if udn.startswith("uuid:") else udn
63 
64 
66  entry: ConfigEntry,
67  ssdp_rendering_control_location: str | None,
68  ssdp_main_tv_agent_location: str | None,
69 ) -> bool:
70  """Return True if the config entry information is complete.
71 
72  If we do not have an ssdp location we consider it complete
73  as some TVs will not support SSDP/UPNP
74  """
75  return bool(
76  entry.unique_id
77  and entry.data.get(CONF_MAC)
78  and (
79  not ssdp_rendering_control_location
80  or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
81  )
82  and (
83  not ssdp_main_tv_agent_location
84  or entry.data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION)
85  )
86  )
87 
88 
90  current_unformatted_mac: str, formatted_mac: str
91 ) -> bool:
92  """Check if two macs are the same but formatted incorrectly."""
93  current_formatted_mac = format_mac(current_unformatted_mac)
94  return (
95  current_formatted_mac == formatted_mac
96  and current_unformatted_mac != current_formatted_mac
97  )
98 
99 
100 class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
101  """Handle a Samsung TV config flow."""
102 
103  VERSION = 2
104  MINOR_VERSION = 2
105 
106  def __init__(self) -> None:
107  """Initialize flow."""
108  self._host_host: str = ""
109  self._mac_mac: str | None = None
110  self._udn_udn: str | None = None
111  self._upnp_udn_upnp_udn: str | None = None
112  self._ssdp_rendering_control_location_ssdp_rendering_control_location: str | None = None
113  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location: str | None = None
114  self._manufacturer_manufacturer: str | None = None
115  self._model_model: str | None = None
116  self._connect_result_connect_result: str | None = None
117  self._method_method: str | None = None
118  self._name_name: str | None = None
119  self._title_title: str = ""
120  self._id: int | None = None
121  self._bridge_bridge: SamsungTVBridge | None = None
122  self._device_info_device_info: dict[str, Any] | None = None
123  self._authenticator_authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = None
124 
125  def _base_config_entry(self) -> dict[str, Any]:
126  """Generate the base config entry without the method."""
127  assert self._bridge_bridge is not None
128  return {
129  CONF_HOST: self._host_host,
130  CONF_MAC: self._mac_mac,
131  CONF_MANUFACTURER: self._manufacturer_manufacturer or DEFAULT_MANUFACTURER,
132  CONF_METHOD: self._bridge_bridge.method,
133  CONF_MODEL: self._model_model,
134  CONF_NAME: self._name_name,
135  CONF_PORT: self._bridge_bridge.port,
136  CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location_ssdp_rendering_control_location,
137  CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location,
138  }
139 
140  def _get_entry_from_bridge(self) -> ConfigFlowResult:
141  """Get device entry."""
142  assert self._bridge_bridge
143  data = self._base_config_entry_base_config_entry()
144  if self._bridge_bridge.token:
145  data[CONF_TOKEN] = self._bridge_bridge.token
146  return self.async_create_entryasync_create_entryasync_create_entry(
147  title=self._title_title,
148  data=data,
149  )
150 
151  async def _async_set_device_unique_id(self, raise_on_progress: bool = True) -> None:
152  """Set device unique_id."""
153  if not await self._async_get_and_check_device_info_async_get_and_check_device_info():
154  raise AbortFlow(RESULT_NOT_SUPPORTED)
155  await self._async_set_unique_id_from_udn_async_set_unique_id_from_udn(raise_on_progress)
156  self._async_update_and_abort_for_matching_unique_id_async_update_and_abort_for_matching_unique_id()
157 
159  self, raise_on_progress: bool = True
160  ) -> None:
161  """Set the unique id from the udn."""
162  assert self._host_host is not None
163  # Set the unique id without raising on progress in case
164  # there are two SSDP flows with for each ST
165  await self.async_set_unique_idasync_set_unique_id(self._udn_udn, raise_on_progress=False)
166  if (
167  entry := self._async_update_existing_matching_entry_async_update_existing_matching_entry()
168  ) and _entry_is_complete(
169  entry,
170  self._ssdp_rendering_control_location_ssdp_rendering_control_location,
171  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location,
172  ):
173  raise AbortFlow("already_configured")
174  # Now that we have updated the config entry, we can raise
175  # if another one is progressing
176  if raise_on_progress:
177  await self.async_set_unique_idasync_set_unique_id(self._udn_udn, raise_on_progress=True)
178 
180  """Abort and update host and mac if we have it."""
181  updates = {CONF_HOST: self._host_host}
182  if self._mac_mac:
183  updates[CONF_MAC] = self._mac_mac
184  if self._model_model:
185  updates[CONF_MODEL] = self._model_model
186  if self._ssdp_rendering_control_location_ssdp_rendering_control_location:
187  updates[CONF_SSDP_RENDERING_CONTROL_LOCATION] = (
188  self._ssdp_rendering_control_location_ssdp_rendering_control_location
189  )
190  if self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location:
191  updates[CONF_SSDP_MAIN_TV_AGENT_LOCATION] = (
192  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location
193  )
194  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates=updates, reload_on_update=False)
195 
196  async def _async_create_bridge(self) -> None:
197  """Create the bridge."""
198  result, method, _info = await self._async_get_device_info_and_method_async_get_device_info_and_method()
199  if result not in SUCCESSFUL_RESULTS:
200  LOGGER.debug("No working config found for %s", self._host_host)
201  raise AbortFlow(result)
202  assert method is not None
203  self._bridge_bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host_host)
204 
206  self,
207  ) -> tuple[str, str | None, dict[str, Any] | None]:
208  """Get device info and method only once."""
209  if self._connect_result_connect_result is None:
210  result, _, method, info = await async_get_device_info(self.hass, self._host_host)
211  self._connect_result_connect_result = result
212  self._method_method = method
213  self._device_info_device_info = info
214  if not method:
215  LOGGER.debug("Host:%s did not return device info", self._host_host)
216  return result, None, None
217  return self._connect_result_connect_result, self._method_method, self._device_info_device_info
218 
219  async def _async_get_and_check_device_info(self) -> bool:
220  """Try to get the device info."""
221  result, _method, info = await self._async_get_device_info_and_method_async_get_device_info_and_method()
222  if result not in SUCCESSFUL_RESULTS:
223  raise AbortFlow(result)
224  if not info:
225  return False
226  dev_info = info.get("device", {})
227  assert dev_info is not None
228  if (device_type := dev_info.get("type")) != "Samsung SmartTV":
229  LOGGER.debug(
230  "Host:%s has type: %s which is not supported", self._host_host, device_type
231  )
232  raise AbortFlow(RESULT_NOT_SUPPORTED)
233  self._model_model = dev_info.get("modelName")
234  name = dev_info.get("name")
235  self._name_name = name.replace("[TV] ", "") if name else device_type
236  self._title_title = f"{self._name} ({self._model})"
237  self._udn_udn = _strip_uuid(dev_info.get("udn", info["id"]))
238  if mac := mac_from_device_info(info):
239  # Samsung sometimes returns a value of "none" for the mac address
240  # this should be ignored - but also shouldn't trigger getmac
241  if mac != "none":
242  self._mac_mac = mac
243  elif mac := await self.hass.async_add_executor_job(
244  partial(getmac.get_mac_address, ip=self._host_host)
245  ):
246  self._mac_mac = mac
247  return True
248 
249  async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> None:
250  try:
251  self._host_host = await self.hass.async_add_executor_job(
252  socket.gethostbyname, user_input[CONF_HOST]
253  )
254  except socket.gaierror as err:
255  raise AbortFlow(RESULT_UNKNOWN_HOST) from err
256  self._name_name = user_input.get(CONF_NAME, self._host_host) or ""
257  self._title_title = self._name_name
258 
259  async def async_step_user(
260  self, user_input: dict[str, Any] | None = None
261  ) -> ConfigFlowResult:
262  """Handle a flow initialized by the user."""
263  if user_input is not None:
264  await self._async_set_name_host_from_input_async_set_name_host_from_input(user_input)
265  await self._async_create_bridge_async_create_bridge()
266  assert self._bridge_bridge
267  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: self._host_host})
268  if self._bridge_bridge.method != METHOD_LEGACY:
269  # Legacy bridge does not provide device info
270  await self._async_set_device_unique_id_async_set_device_unique_id(raise_on_progress=False)
271  if self._bridge_bridge.method == METHOD_ENCRYPTED_WEBSOCKET:
272  return await self.async_step_encrypted_pairingasync_step_encrypted_pairing()
273  return await self.async_step_pairingasync_step_pairing({})
274 
275  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=DATA_SCHEMA)
276 
278  self, user_input: dict[str, Any] | None = None
279  ) -> ConfigFlowResult:
280  """Handle a pairing by accepting the message on the TV."""
281  assert self._bridge_bridge is not None
282  errors: dict[str, str] = {}
283  if user_input is not None:
284  result = await self._bridge_bridge.async_try_connect()
285  if result == RESULT_SUCCESS:
286  return self._get_entry_from_bridge_get_entry_from_bridge()
287  if result != RESULT_AUTH_MISSING:
288  raise AbortFlow(result)
289  errors = {"base": RESULT_AUTH_MISSING}
290 
291  self.context["title_placeholders"] = {"device": self._title_title}
292  return self.async_show_formasync_show_formasync_show_form(
293  step_id="pairing",
294  errors=errors,
295  description_placeholders={"device": self._title_title},
296  data_schema=vol.Schema({}),
297  )
298 
300  self, user_input: dict[str, Any] | None = None
301  ) -> ConfigFlowResult:
302  """Handle a encrypted pairing."""
303  assert self._host_host is not None
304  await self._async_start_encrypted_pairing_async_start_encrypted_pairing(self._host_host)
305  assert self._authenticator_authenticator is not None
306  errors: dict[str, str] = {}
307 
308  if user_input is not None:
309  if (
310  (pin := user_input.get("pin"))
311  and (token := await self._authenticator_authenticator.try_pin(pin))
312  and (session_id := await self._authenticator_authenticator.get_session_id_and_close())
313  ):
314  return self.async_create_entryasync_create_entryasync_create_entry(
315  data={
316  **self._base_config_entry_base_config_entry(),
317  CONF_TOKEN: token,
318  CONF_SESSION_ID: session_id,
319  },
320  title=self._title_title,
321  )
322  errors = {"base": RESULT_INVALID_PIN}
323 
324  self.context["title_placeholders"] = {"device": self._title_title}
325  return self.async_show_formasync_show_formasync_show_form(
326  step_id="encrypted_pairing",
327  errors=errors,
328  description_placeholders={"device": self._title_title},
329  data_schema=vol.Schema({vol.Required("pin"): str}),
330  )
331 
332  @callback
334  self,
335  ) -> tuple[ConfigEntry | None, bool]:
336  """Get first existing matching entry (prefer unique id)."""
337  matching_host_entry: ConfigEntry | None = None
338  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
339  if (self._mac_mac and self._mac_mac == entry.data.get(CONF_MAC)) or (
340  self._upnp_udn_upnp_udn and self._upnp_udn_upnp_udn == entry.unique_id
341  ):
342  LOGGER.debug("Found entry matching unique_id for %s", self._host_host)
343  return entry, True
344 
345  if entry.data[CONF_HOST] == self._host_host:
346  LOGGER.debug("Found entry matching host for %s", self._host_host)
347  matching_host_entry = entry
348 
349  return matching_host_entry, False
350 
351  @callback
353  self,
354  ) -> ConfigEntry | None:
355  """Check existing entries and update them.
356 
357  Returns the existing entry if it was updated.
358  """
359  entry, is_unique_match = self._async_get_existing_matching_entry_async_get_existing_matching_entry()
360  if not entry:
361  return None
362  entry_kw_args: dict = {}
363  if self.unique_idunique_id and (
364  entry.unique_id is None
365  or (is_unique_match and self.unique_idunique_id != entry.unique_id)
366  ):
367  entry_kw_args["unique_id"] = self.unique_idunique_id
368  data: dict[str, Any] = dict(entry.data)
369  update_ssdp_rendering_control_location = (
370  self._ssdp_rendering_control_location_ssdp_rendering_control_location
371  and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
372  != self._ssdp_rendering_control_location_ssdp_rendering_control_location
373  )
374  update_ssdp_main_tv_agent_location = (
375  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location
376  and data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION)
377  != self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location
378  )
379  update_mac = self._mac_mac and (
380  not (data_mac := data.get(CONF_MAC))
381  or _mac_is_same_with_incorrect_formatting(data_mac, self._mac_mac)
382  )
383  update_model = self._model_model and not data.get(CONF_MODEL)
384  if (
385  update_ssdp_rendering_control_location
386  or update_ssdp_main_tv_agent_location
387  or update_mac
388  or update_model
389  ):
390  if update_ssdp_rendering_control_location:
391  data[CONF_SSDP_RENDERING_CONTROL_LOCATION] = (
392  self._ssdp_rendering_control_location_ssdp_rendering_control_location
393  )
394  if update_ssdp_main_tv_agent_location:
395  data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] = (
396  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location
397  )
398  if update_mac:
399  data[CONF_MAC] = self._mac_mac
400  if update_model:
401  data[CONF_MODEL] = self._model_model
402  entry_kw_args["data"] = data
403  if not entry_kw_args:
404  return None
405  LOGGER.debug("Updating existing config entry with %s", entry_kw_args)
406  self.hass.config_entries.async_update_entry(entry, **entry_kw_args)
407  if entry.state != ConfigEntryState.LOADED:
408  # If its loaded it already has a reload listener in place
409  # and we do not want to trigger multiple reloads
410  self.hass.async_create_task(
411  self.hass.config_entries.async_reload(entry.entry_id)
412  )
413  return entry
414 
415  @callback
417  """Start discovery."""
418  assert self._host_host is not None
419  if (entry := self._async_update_existing_matching_entry_async_update_existing_matching_entry()) and entry.unique_id:
420  # If we have the unique id and the mac we abort
421  # as we do not need anything else
422  raise AbortFlow("already_configured")
423  self._async_abort_if_host_already_in_progress_async_abort_if_host_already_in_progress()
424 
425  @callback
427  if self.hass.config_entries.flow.async_has_matching_flow(self):
428  raise AbortFlow("already_in_progress")
429 
430  def is_matching(self, other_flow: Self) -> bool:
431  """Return True if other_flow is matching this flow."""
432  return other_flow._host == self._host_host # noqa: SLF001
433 
434  @callback
436  if not self._manufacturer_manufacturer or not self._manufacturer_manufacturer.lower().startswith(
437  "samsung"
438  ):
439  raise AbortFlow(RESULT_NOT_SUPPORTED)
440 
441  async def async_step_ssdp(
442  self, discovery_info: ssdp.SsdpServiceInfo
443  ) -> ConfigFlowResult:
444  """Handle a flow initialized by ssdp discovery."""
445  LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)
446  model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or ""
447  if discovery_info.ssdp_st == UPNP_SVC_RENDERING_CONTROL:
448  self._ssdp_rendering_control_location_ssdp_rendering_control_location = discovery_info.ssdp_location
449  LOGGER.debug(
450  "Set SSDP RenderingControl location to: %s",
451  self._ssdp_rendering_control_location_ssdp_rendering_control_location,
452  )
453  elif discovery_info.ssdp_st == UPNP_SVC_MAIN_TV_AGENT:
454  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location = discovery_info.ssdp_location
455  LOGGER.debug(
456  "Set SSDP MainTvAgent location to: %s",
457  self._ssdp_main_tv_agent_location_ssdp_main_tv_agent_location,
458  )
459  self._udn_udn = self._upnp_udn_upnp_udn = _strip_uuid(
460  discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
461  )
462  if hostname := urlparse(discovery_info.ssdp_location or "").hostname:
463  self._host_host = hostname
464  self._manufacturer_manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER)
465  self._abort_if_manufacturer_is_not_samsung_abort_if_manufacturer_is_not_samsung()
466 
467  # Set defaults, in case they cannot be extracted from device_info
468  self._name_name = self._title_title = self._model_model = model_name
469  # Update from device_info (if accessible)
470  await self._async_get_and_check_device_info_async_get_and_check_device_info()
471 
472  # The UDN provided by the ssdp discovery doesn't always match the UDN
473  # from the device_info, used by the other methods so we need to
474  # ensure the device_info is loaded before setting the unique_id
475  await self._async_set_unique_id_from_udn_async_set_unique_id_from_udn()
476  self._async_update_and_abort_for_matching_unique_id_async_update_and_abort_for_matching_unique_id()
477  self._async_abort_if_host_already_in_progress_async_abort_if_host_already_in_progress()
478  if self._method_method == METHOD_LEGACY and discovery_info.ssdp_st in (
479  UPNP_SVC_RENDERING_CONTROL,
480  UPNP_SVC_MAIN_TV_AGENT,
481  ):
482  # The UDN we use for the unique id cannot be determined
483  # from device_info for legacy devices
484  return self.async_abortasync_abortasync_abort(reason="not_supported")
485  self.context["title_placeholders"] = {"device": self._title_title}
486  return await self.async_step_confirmasync_step_confirm()
487 
488  async def async_step_dhcp(
489  self, discovery_info: dhcp.DhcpServiceInfo
490  ) -> ConfigFlowResult:
491  """Handle a flow initialized by dhcp discovery."""
492  LOGGER.debug("Samsung device found via DHCP: %s", discovery_info)
493  self._mac_mac = format_mac(discovery_info.macaddress)
494  self._host_host = discovery_info.ip
495  self._async_start_discovery_with_mac_address_async_start_discovery_with_mac_address()
496  await self._async_set_device_unique_id_async_set_device_unique_id()
497  self.context["title_placeholders"] = {"device": self._title_title}
498  return await self.async_step_confirmasync_step_confirm()
499 
501  self, discovery_info: zeroconf.ZeroconfServiceInfo
502  ) -> ConfigFlowResult:
503  """Handle a flow initialized by zeroconf discovery."""
504  LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info)
505  self._mac_mac = format_mac(discovery_info.properties["deviceid"])
506  self._host_host = discovery_info.host
507  self._async_start_discovery_with_mac_address_async_start_discovery_with_mac_address()
508  await self._async_set_device_unique_id_async_set_device_unique_id()
509  self.context["title_placeholders"] = {"device": self._title_title}
510  return await self.async_step_confirmasync_step_confirm()
511 
513  self, user_input: dict[str, Any] | None = None
514  ) -> ConfigFlowResult:
515  """Handle user-confirmation of discovered node."""
516  if user_input is not None:
517  await self._async_create_bridge_async_create_bridge()
518  assert self._bridge_bridge
519  if self._bridge_bridge.method == METHOD_ENCRYPTED_WEBSOCKET:
520  return await self.async_step_encrypted_pairingasync_step_encrypted_pairing()
521  return await self.async_step_pairingasync_step_pairing({})
522 
523  return self.async_show_formasync_show_formasync_show_form(
524  step_id="confirm", description_placeholders={"device": self._title_title}
525  )
526 
527  async def async_step_reauth(
528  self, entry_data: Mapping[str, Any]
529  ) -> ConfigFlowResult:
530  """Handle configuration by re-auth."""
531  if entry_data.get(CONF_MODEL) and entry_data.get(CONF_NAME):
532  self._title_title = f"{entry_data[CONF_NAME]} ({entry_data[CONF_MODEL]})"
533  else:
534  self._title_title = entry_data.get(CONF_NAME) or entry_data[CONF_HOST]
535  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
536 
538  self, user_input: dict[str, Any] | None = None
539  ) -> ConfigFlowResult:
540  """Confirm reauth."""
541  errors = {}
542 
543  reauth_entry = self._get_reauth_entry_get_reauth_entry()
544  method = reauth_entry.data[CONF_METHOD]
545  if user_input is not None:
546  if method == METHOD_ENCRYPTED_WEBSOCKET:
547  return await self.async_step_reauth_confirm_encryptedasync_step_reauth_confirm_encrypted()
548  bridge = SamsungTVBridge.get_bridge(
549  self.hass,
550  method,
551  reauth_entry.data[CONF_HOST],
552  )
553  result = await bridge.async_try_connect()
554  if result == RESULT_SUCCESS:
555  new_data = dict(reauth_entry.data)
556  new_data[CONF_TOKEN] = bridge.token
557  return self.async_update_reload_and_abortasync_update_reload_and_abort(
558  reauth_entry,
559  data=new_data,
560  )
561  if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT):
562  return self.async_abortasync_abortasync_abort(reason=result)
563 
564  # On websocket we will get RESULT_CANNOT_CONNECT when auth is missing
565  errors = {"base": RESULT_AUTH_MISSING}
566 
567  self.context["title_placeholders"] = {"device": self._title_title}
568  return self.async_show_formasync_show_formasync_show_form(
569  step_id="reauth_confirm",
570  errors=errors,
571  description_placeholders={"device": self._title_title},
572  )
573 
574  async def _async_start_encrypted_pairing(self, host: str) -> None:
575  if self._authenticator_authenticator is None:
576  self._authenticator_authenticator = SamsungTVEncryptedWSAsyncAuthenticator(
577  host,
578  web_session=async_get_clientsession(self.hass),
579  )
580  await self._authenticator_authenticator.start_pairing()
581 
583  self, user_input: dict[str, Any] | None = None
584  ) -> ConfigFlowResult:
585  """Confirm reauth (encrypted method)."""
586  errors = {}
587 
588  reauth_entry = self._get_reauth_entry_get_reauth_entry()
589  await self._async_start_encrypted_pairing_async_start_encrypted_pairing(reauth_entry.data[CONF_HOST])
590  assert self._authenticator_authenticator is not None
591 
592  if user_input is not None:
593  if (
594  (pin := user_input.get("pin"))
595  and (token := await self._authenticator_authenticator.try_pin(pin))
596  and (session_id := await self._authenticator_authenticator.get_session_id_and_close())
597  ):
598  return self.async_update_reload_and_abortasync_update_reload_and_abort(
599  reauth_entry,
600  data_updates={
601  CONF_TOKEN: token,
602  CONF_SESSION_ID: session_id,
603  },
604  )
605 
606  errors = {"base": RESULT_INVALID_PIN}
607 
608  self.context["title_placeholders"] = {"device": self._title_title}
609  return self.async_show_formasync_show_formasync_show_form(
610  step_id="reauth_confirm_encrypted",
611  errors=errors,
612  description_placeholders={"device": self._title_title},
613  data_schema=vol.Schema({vol.Required("pin"): str}),
614  )
None _async_set_device_unique_id(self, bool raise_on_progress=True)
Definition: config_flow.py:151
None _async_set_name_host_from_input(self, dict[str, Any] user_input)
Definition: config_flow.py:249
ConfigFlowResult async_step_pairing(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:279
ConfigFlowResult async_step_reauth_confirm_encrypted(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:584
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:261
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:490
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:443
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:502
ConfigFlowResult async_step_encrypted_pairing(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:301
tuple[str, str|None, dict[str, Any]|None] _async_get_device_info_and_method(self)
Definition: config_flow.py:207
tuple[ConfigEntry|None, bool] _async_get_existing_matching_entry(self)
Definition: config_flow.py:335
None _async_set_unique_id_from_udn(self, bool raise_on_progress=True)
Definition: config_flow.py:160
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:539
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:514
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:529
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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
Device async_try_connect(str ip_address)
Definition: config_flow.py:23
tuple[str, int|None, str|None, dict[str, Any]|None] async_get_device_info(HomeAssistant hass, str host)
Definition: bridge.py:104
str|None mac_from_device_info(dict[str, Any] info)
Definition: bridge.py:89
bool _mac_is_same_with_incorrect_formatting(str current_unformatted_mac, str formatted_mac)
Definition: config_flow.py:91
bool _entry_is_complete(ConfigEntry entry, str|None ssdp_rendering_control_location, str|None ssdp_main_tv_agent_location)
Definition: config_flow.py:69
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)