async def _async_get_and_check_device_info(self): """Try to get the device info.""" _port, _method, info = await async_get_device_info( self.hass, self._bridge, self._host) if not info: if not _method: LOGGER.debug( "Samsung host %s is not supported by either %s or %s methods", self._host, METHOD_LEGACY, METHOD_WEBSOCKET, ) raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) device_type = dev_info.get("type") if device_type != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") self._name = name.replace("[TV] ", "") if name else device_type self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if mac := mac_from_device_info(info): self._mac = mac
async def _async_set_unique_id_or_update( self, isy_mac: str, ip_address: str, port: int | None ) -> None: """Abort and update the ip address on change.""" existing_entry = await self.async_set_unique_id(isy_mac) if not existing_entry: return if existing_entry.source == config_entries.SOURCE_IGNORE: raise data_entry_flow.AbortFlow("already_configured") parsed_url = urlparse(existing_entry.data[CONF_HOST]) if parsed_url.hostname != ip_address: new_netloc = ip_address if port: new_netloc = f"{ip_address}:{port}" elif parsed_url.port: new_netloc = f"{ip_address}:{parsed_url.port}" self.hass.config_entries.async_update_entry( existing_entry, data={ **existing_entry.data, CONF_HOST: urlunparse( ( parsed_url.scheme, new_netloc, parsed_url.path, parsed_url.query, parsed_url.fragment, None, ) ), }, ) raise data_entry_flow.AbortFlow("already_configured")
def _try_connect(self): """Try to connect and check auth.""" for method in SUPPORTED_METHODS: self._bridge = SamsungTVBridge.get_bridge(method, self._host) result = self._bridge.try_connect() if result == RESULT_SUCCESS: return if result != RESULT_CANNOT_CONNECT: raise data_entry_flow.AbortFlow(result) LOGGER.debug("No working config found") raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT)
def _abort_if_unique_id_configured(self) -> None: """Abort if the unique ID is already configured.""" if self.unique_id is None: return if self.unique_id in self._async_current_ids(): raise data_entry_flow.AbortFlow("already_configured")
async def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" assert self._host is not None if (entry := self._async_update_existing_host_entry()) and entry.unique_id: # If we have the unique id and the mac we abort # as we do not need anything else raise data_entry_flow.AbortFlow("already_configured")
async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" supported_types = { device_type for _, device_types in DOMAINS_AND_TYPES for device_type in device_types } if device.type not in supported_types: LOGGER.error( "Unsupported device: %s. If it worked before, please open " "an issue at https://github.com/home-assistant/core/issues", hex(device.devtype), ) raise data_entry_flow.AbortFlow("not_supported") await self.async_set_unique_id(device.mac.hex(), raise_on_progress=raise_on_progress) self.device = device # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": device.name, "model": device.model, "host": device.host[0], }
async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" _LOGGER.debug("Async_Step_Zeroconf start") if discovery_info is None: return self.async_abort(reason="cannot_connect") _LOGGER.debug("Async_Step_Zeroconf discovery info %s" % discovery_info) # if it's not a MLGW or BLGW device, then abort if not discovery_info.get("name"): return self.async_abort(reason="not_mlgw_device") if not (discovery_info["name"].startswith("MLGW") or discovery_info["name"].startswith("BLGW")): return self.async_abort(reason="not_mlgw_device") # Hostname is format: mlgw.local. self.host = discovery_info["hostname"].rstrip(".") _LOGGER.debug("Async_Step_Zeroconf Hostname %s" % self.host) try: sn = await mlgw_get_xmpp_serial(self.host) except Exception as e: _LOGGER.debug("Exception %s" % str(e)) return self.async_abort(reason="cannot_connect") if sn is not None: await self.async_set_unique_id(sn) self._abort_if_unique_id_configured() if sn is None: raise data_entry_flow.AbortFlow("no_serial_number") _LOGGER.debug("Async_Step_Zeroconf Awaiting Confirmation %s sn: %s" % (self.host, sn)) return await self.async_step_zeroconf_confirm()
async def async_set_unique_id( self, unique_id: Optional[str] = None, *, raise_on_progress: bool = True ) -> Optional[ConfigEntry]: """Set a unique ID for the config flow. Returns optionally existing config entry with same ID. """ if unique_id is None: self.context["unique_id"] = None # pylint: disable=no-member return None if raise_on_progress: for progress in self._async_in_progress(): if progress["context"].get("unique_id") == unique_id: raise data_entry_flow.AbortFlow("already_in_progress") self.context["unique_id"] = unique_id # pylint: disable=no-member # Abort discoveries done using the default discovery unique id assert self.hass is not None if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID: for progress in self._async_in_progress(): if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID: self.hass.config_entries.flow.async_abort(progress["flow_id"]) for entry in self._async_current_entries(): if entry.unique_id == unique_id: return entry return None
def _abort_if_unique_id_configured( self, updates: Optional[Dict[Any, Any]] = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" assert self.hass if self.unique_id is None: return for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: if updates is not None: changed = self.hass.config_entries.async_update_entry( entry, data={**entry.data, **updates} ) if ( changed and reload_on_update and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY) ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) raise data_entry_flow.AbortFlow("already_configured")
def _abort_if_unique_id_configured( self, updates: Optional[Dict[Any, Any]] = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" if self.unique_id is None: return for entry in self._async_current_entries(include_ignore=True): if entry.unique_id == self.unique_id: if updates is not None: changed = self.hass.config_entries.async_update_entry( entry, data={ **entry.data, **updates }) if (changed and reload_on_update and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY)): self.hass.async_create_task( self.hass.config_entries.async_reload( entry.entry_id)) # Allow ignored entries to be configured on manual user step if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: continue raise data_entry_flow.AbortFlow("already_configured")
async def _async_set_name_host_from_input(self, user_input): try: self._host = await self.hass.async_add_executor_job( socket.gethostbyname, user_input[CONF_HOST]) except socket.gaierror as err: raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err self._name = user_input.get(CONF_NAME, self._host) self._title = self._name
async def _async_get_and_check_device_info(self) -> bool: """Try to get the device info.""" _port, _method, info = await async_get_device_info( self.hass, self._bridge, self._host) if not info: if not _method: LOGGER.debug( "Samsung host %s is not supported by either %s or %s methods", self._host, METHOD_LEGACY, METHOD_WEBSOCKET, ) raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) if (device_type := dev_info.get("type")) != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
async def _async_start_discovery_for_host(self, host): """Start discovery for a host.""" if entry := self._async_update_existing_host_entry(host): if entry.unique_id: # Let the flow continue to fill the missing # unique id as we may be able to obtain it # in the next step raise data_entry_flow.AbortFlow("already_configured")
async def _async_set_unique_id_from_udn(self, raise_on_progress=True): """Set the unique id from the udn.""" assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) if (entry := self._async_update_existing_host_entry() ) and _entry_is_complete(entry): raise data_entry_flow.AbortFlow("already_configured")
async def _async_set_device_unique_id(self, raise_on_progress: bool = True ) -> None: """Set device unique_id.""" if not await self._async_get_and_check_device_info(): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) await self._async_set_unique_id_from_udn(raise_on_progress) self._async_update_and_abort_for_matching_unique_id()
async def _async_get_and_check_device_info(self): """Try to get the device info.""" info = await async_get_device_info(self.hass, self._bridge, self._host) if not info: raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) dev_info = info.get("device", {}) device_type = dev_info.get("type") if device_type != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") self._name = name.replace("[TV] ", "") if name else device_type self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if dev_info.get("networkType") == "wireless" and dev_info.get( "wifiMac"): self._mac = format_mac(dev_info.get("wifiMac")) self._device_info = info
async def async_step_import(self, user_input=None): """Import a config entry.""" if self._async_current_entries(): for entry in self._async_current_entries(include_ignore=True): if user_input is not None: self.hass.config_entries.async_update_entry( entry, data=user_input ) raise data_entry_flow.AbortFlow("already_configured") else: return self.async_create_entry(title="RCSLink Config", data=user_input)
def _async_check_and_update_in_progress(self, host: str, unique_id: str) -> None: """Check for in-progress flows and update them with identifiers if needed.""" for flow in self._async_in_progress(include_uninitialized=True): context = flow["context"] if (context.get("source") != config_entries.SOURCE_ZEROCONF or context.get(CONF_ADDRESS) != host): continue if ("all_identifiers" in context and unique_id not in context["all_identifiers"]): # Add potentially new identifiers from this device to the existing flow context["all_identifiers"].append(unique_id) raise data_entry_flow.AbortFlow("already_in_progress")
def _abort_if_unique_id_configured(self, updates: Dict[Any, Any] = None) -> None: """Abort if the unique ID is already configured.""" assert self.hass if self.unique_id is None: return for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: if updates is not None and not updates.items() <= entry.data.items(): self.hass.config_entries.async_update_entry( entry, data={**entry.data, **updates} ) raise data_entry_flow.AbortFlow("already_configured")
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) await self._async_set_unique_id_from_udn() await self._async_start_discovery_for_host( urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname) self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] if not self._manufacturer or not self._manufacturer.lower().startswith( "samsung"): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._name = self._title = self._model = discovery_info.get( ATTR_UPNP_MODEL_NAME) self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm()
async def _async_handle_discovery_without_unique_id(self) -> None: """Mark this flow discovered, without a unique identifier. If a flow initiated by discovery, doesn't have a unique ID, this can be used alternatively. It will ensure only 1 flow is started and only when the handler has no existing config entries. It ensures that the discovery can be ignored by the user. """ if self.unique_id is not None: return # Abort if the handler has config entries already if self._async_current_entries(): raise data_entry_flow.AbortFlow("already_configured") # Use an special unique id to differentiate await self.async_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID) self._abort_if_unique_id_configured() # Abort if any other flow for this handler is already in progress if self._async_in_progress(): raise data_entry_flow.AbortFlow("already_in_progress")
def _async_check_configured_entry(self) -> None: """Check if entry is configured, update unique_id if needed.""" for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] != self._host: continue if self._uuid and not entry.unique_id: _LOGGER.debug( "Updating unique_id for host %s, unique_id: %s", self._host, self._uuid, ) self.hass.config_entries.async_update_entry(entry, unique_id=self._uuid) raise data_entry_flow.AbortFlow("already_configured")
async def _async_discovery_handler(self, ip_address): """Start the user flow from any discovery.""" self.context[CONF_IP_ADDRESS] = ip_address self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address}) self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) self._ip_address = ip_address for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_IP_ADDRESS) == self._ip_address: raise data_entry_flow.AbortFlow("already_in_progress") self._device_type = DEVICE_TYPE_ISMARTGATE return await self.async_step_user()
async def async_set_unique_id( self, unique_id: str, *, raise_on_progress: bool = True ) -> Optional[ConfigEntry]: """Set a unique ID for the config flow. Returns optionally existing config entry with same ID. """ if raise_on_progress: for progress in self._async_in_progress(): if progress["context"].get("unique_id") == unique_id: raise data_entry_flow.AbortFlow("already_in_progress") self.context["unique_id"] = unique_id # pylint: disable=no-member for entry in self._async_current_entries(): if entry.unique_id == unique_id: return entry return None
async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" if device.type not in DEVICE_TYPES: _LOGGER.error( "Unsupported device: %s. If it worked before, please open " "an issue at https://github.com/home-assistant/core/issues", hex(device.devtype), ) raise data_entry_flow.AbortFlow("not_supported") await self.async_set_unique_id(device.mac.hex(), raise_on_progress=raise_on_progress) self.device = device self.context["title_placeholders"] = { "name": device.name, "model": device.model, "host": device.host[0], }
async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery of a Squeezebox player.""" _LOGGER.debug("Reached dhcp discovery of a player with info: %s", discovery_info) await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) self._abort_if_unique_id_configured() _LOGGER.debug("Configuring dhcp player with unique id: %s", self.unique_id) registry = async_get(self.hass) # if we have detected this player, do nothing. if not, there must be a server out there for us to configure, so start the normal user flow (which tries to autodetect server) if registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.unique_id) is not None: # this player is already known, so do nothing other than mark as configured raise data_entry_flow.AbortFlow("already_configured") # if the player is unknown, then we likely need to configure its server return await self.async_step_user()
async def async_found_zeroconf_device(self, user_input=None): """Handle device found after Zeroconf discovery.""" # Suppose we have a device with three services: A, B and C. Let's assume # service A is discovered by Zeroconf, triggering a device scan that also finds # service B but *not* C. An identifier is picked from one of the services and # used as unique_id. The select process is deterministic (let's say in order A, # B and C) but in practice that doesn't matter. So, a flow is set up for the # device with unique_id set to "A" for services A and B. # # Now, service C is found and the same thing happens again but only service B # is found. In this case, unique_id will be set to "B" which is problematic # since both flows really represent the same device. They will however end up # as two separate flows. # # To solve this, all identifiers found during a device scan is stored as # "all_identifiers" in the flow context. When a new service is discovered, the # code below will check these identifiers for all active flows and abort if a # match is found. Before aborting, the original flow is updated with any # potentially new identifiers. In the example above, when service C is # discovered, the identifier of service C will be inserted into # "all_identifiers" of the original flow (making the device complete). for flow in self._async_in_progress(): for identifier in self.atv.all_identifiers: if identifier not in flow["context"].get("all_identifiers", []): continue # Add potentially new identifiers from this device to the existing flow identifiers = set(flow["context"]["all_identifiers"]) identifiers.update(self.atv.all_identifiers) flow["context"]["all_identifiers"] = list(identifiers) raise data_entry_flow.AbortFlow("already_in_progress") self.context["all_identifiers"] = self.atv.all_identifiers # Also abort if an integration with this identifier already exists await self.async_set_unique_id(self.device_identifier) self._abort_if_unique_id_configured() self.context["identifier"] = self.unique_id return await self.async_step_confirm()
def _abort_if_manufacturer_is_not_samsung(self) -> None: if not self._manufacturer or not self._manufacturer.lower().startswith( "samsung"): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" VERSION = 2 def __init__(self): """Initialize flow.""" self._reauth_entry = None self._host = None self._mac = None self._udn = None self._manufacturer = None self._model = None self._name = None self._title = None self._id = None self._bridge = None self._device_info = None def _get_entry_from_bridge(self): """Get device entry.""" data = { CONF_HOST: self._host, CONF_MAC: self._mac, CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, CONF_METHOD: self._bridge.method, CONF_MODEL: self._model, CONF_NAME: self._name, CONF_PORT: self._bridge.port, } if self._bridge.token: data[CONF_TOKEN] = self._bridge.token return self.async_create_entry( title=self._title, data=data, ) async def _async_set_device_unique_id(self, raise_on_progress=True): """Set device unique_id.""" await self._async_get_and_check_device_info() await self._async_set_unique_id_from_udn(raise_on_progress) async def _async_set_unique_id_from_udn(self, raise_on_progress=True): """Set the unique id from the udn.""" assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) self._async_update_existing_host_entry(self._host) updates = {CONF_HOST: self._host} if self._mac: updates[CONF_MAC] = self._mac self._abort_if_unique_id_configured(updates=updates) def _try_connect(self): """Try to connect and check auth.""" for method in SUPPORTED_METHODS: self._bridge = SamsungTVBridge.get_bridge(method, self._host) result = self._bridge.try_connect() if result == RESULT_SUCCESS: return if result != RESULT_CANNOT_CONNECT: raise data_entry_flow.AbortFlow(result) LOGGER.debug("No working config found") raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT) async def _async_get_and_check_device_info(self): """Try to get the device info.""" info = await async_get_device_info(self.hass, self._bridge, self._host) if not info: raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) dev_info = info.get("device", {}) device_type = dev_info.get("type") if device_type != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") self._name = name.replace("[TV] ", "") if name else device_type self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if dev_info.get("networkType") == "wireless" and dev_info.get( "wifiMac"): self._mac = format_mac(dev_info.get("wifiMac")) self._device_info = info async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" # We need to import even if we cannot validate # since the TV may be off at startup await self._async_set_name_host_from_input(user_input) self._async_abort_entries_match({CONF_HOST: self._host}) if user_input.get(CONF_PORT) in WEBSOCKET_PORTS: user_input[CONF_METHOD] = METHOD_WEBSOCKET else: user_input[CONF_METHOD] = METHOD_LEGACY user_input[CONF_PORT] = LEGACY_PORT user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER return self.async_create_entry( title=self._title, data=user_input, ) async def _async_set_name_host_from_input(self, user_input): try: self._host = await self.hass.async_add_executor_job( socket.gethostbyname, user_input[CONF_HOST]) except socket.gaierror as err: raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err self._name = user_input.get(CONF_NAME, self._host) self._title = self._name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) await self.hass.async_add_executor_job(self._try_connect) self._async_abort_entries_match({CONF_HOST: self._host}) if self._bridge.method != METHOD_LEGACY: # Legacy bridge does not provide device info await self._async_set_device_unique_id(raise_on_progress=False) return self._get_entry_from_bridge() return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) @callback def _async_update_existing_host_entry(self, host): for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] != host: continue entry_kw_args = {} if self.unique_id and entry.unique_id is None: entry_kw_args["unique_id"] = self.unique_id if self._mac and not entry.data.get(CONF_MAC): data_copy = dict(entry.data) data_copy[CONF_MAC] = self._mac entry_kw_args["data"] = data_copy if entry_kw_args: self.hass.config_entries.async_update_entry( entry, **entry_kw_args) return entry return None async def _async_start_discovery(self): """Start discovery.""" assert self._host is not None if entry := self._async_update_existing_host_entry(self._host): if entry.unique_id: # Let the flow continue to fill the missing # unique id as we may be able to obtain it # in the next step raise data_entry_flow.AbortFlow("already_configured") self.context[CONF_HOST] = self._host for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self._host: raise data_entry_flow.AbortFlow("already_in_progress")
async def async_step_init(self, user_input=None): raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"})