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 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 AbortFlow("already_configured")
async def _async_try_connect_and_fetch(ip_address: str) -> dict[str, Any]: """Try to connect.""" _LOGGER.debug("config_flow _async_try_connect_and_fetch") # Make connection with device # This is to test the connection and to get info for unique_id energy_api = HomeWizardEnergy(ip_address) try: device = await energy_api.device() except DisabledError as ex: _LOGGER.error("API disabled, API must be enabled in the app") raise AbortFlow("api_not_enabled") from ex except UnsupportedError as ex: _LOGGER.error("API version unsuppored") raise AbortFlow("unsupported_api_version") from ex except Exception as ex: _LOGGER.exception( "Error connecting with Energy Device at %s", ip_address, ) raise AbortFlow("unknown_error") from ex finally: await energy_api.close() return { CONF_PRODUCT_NAME: device.product_name, CONF_PRODUCT_TYPE: device.product_type, CONF_SERIAL: device.serial, }
async def async_get_currencies(self) -> dict[str, str]: """Get the available currencies.""" if not self.currencies: client = Client("dummy-api-key", async_get_clientsession(self.hass)) try: async with async_timeout.timeout(CLIENT_TIMEOUT): self.currencies = await client.get_currencies() except OpenExchangeRatesClientError as err: raise AbortFlow("cannot_connect") from err except asyncio.TimeoutError as err: raise AbortFlow("timeout_connect") from err return self.currencies
async def _async_parse_discovery(self, discovery_info: ssdp.SsdpServiceInfo, raise_on_progress: bool = True) -> None: """Get required details from an SSDP discovery. Aborts if a device matching the SSDP USN has already been configured. """ LOGGER.debug( "_async_parse_discovery: location: %s, USN: %s", discovery_info.ssdp_location, discovery_info.ssdp_usn, ) if not discovery_info.ssdp_location or not discovery_info.ssdp_usn: raise AbortFlow("bad_ssdp") if not self._location: self._location = discovery_info.ssdp_location self._usn = discovery_info.ssdp_usn await self.async_set_unique_id(self._usn, raise_on_progress=raise_on_progress) # Abort if already configured, but update the last-known location self._abort_if_unique_id_configured(updates={CONF_URL: self._location}, reload_on_update=False) self._name = (discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or urlparse(self._location).hostname or DEFAULT_NAME)
async def async_step_discovery_confirm(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Confirm discovery.""" if user_input is not None: if (self.config[CONF_API_ENABLED]) != "1": raise AbortFlow(reason="api_not_enabled") # Check connection await self._async_try_connect_and_fetch( str(self.config[CONF_IP_ADDRESS])) return self.async_create_entry( title= f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})", data={ CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS], }, ) self._set_confirm_only() self.context["title_placeholders"] = { "name": f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})" } return self.async_show_form( step_id="discovery_confirm", description_placeholders={ CONF_PRODUCT_TYPE: self.config[CONF_PRODUCT_TYPE], CONF_SERIAL: self.config[CONF_SERIAL], CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS], }, )
async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info.name host: str = discovery_info.host bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) for entry in self._async_current_entries(): if entry.unique_id != bond_id: continue updates = {CONF_HOST: host} if entry.state == ConfigEntryState.SETUP_ERROR and ( token := await async_get_token(self.hass, host) ): updates[CONF_ACCESS_TOKEN] = token new_data = {**entry.data, **updates} if new_data != dict(entry.data): self.hass.config_entries.async_update_entry( entry, data={**entry.data, **updates} ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) raise AbortFlow("already_configured")
async def async_step_finish_addon_setup(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. Set unique id and abort if already configured. """ if not self.ws_address: discovery_info = await self._async_get_addon_discovery_info() self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" if not self.unique_id or self.context[ "source"] == config_entries.SOURCE_USB: if not self.version_info: try: self.version_info = await async_get_version_info( self.hass, self.ws_address) except CannotConnect as err: raise AbortFlow("cannot_connect") from err await self.async_set_unique_id(str(self.version_info.home_id), raise_on_progress=False) self._abort_if_unique_id_configured( updates={ CONF_URL: self.ws_address, CONF_USB_PATH: self.usb_path, CONF_S0_LEGACY_KEY: self.s0_legacy_key, CONF_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, }) return self._async_create_entry_from_vars()
async def _async_set_discovered_mac(self, device: FluxLEDDiscovery, allow_update_mac: bool) -> None: """Set the discovered mac. We only allow it to be updated if it comes from udp discovery since the dhcp mac can be one digit off from the udp discovery mac for devices with multiple network interfaces """ mac_address = device[ATTR_ID] assert mac_address is not None mac = dr.format_mac(mac_address) await self.async_set_unique_id(mac) for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( entry.unique_id and ":" in entry.unique_id and mac_matches_by_one(entry.unique_id, mac)): if (async_update_entry_from_discovery(self.hass, entry, device, None, allow_update_mac) or entry.state == config_entries.ConfigEntryState.SETUP_RETRY): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id)) else: async_dispatcher_send( self.hass, FLUX_LED_DISCOVERY_SIGNAL.format( entry_id=entry.entry_id), ) raise AbortFlow("already_configured")
async def async_step_finish_addon_setup( self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. Set unique id and abort if already configured. """ assert self.hass if not self.ws_address: discovery_info = await self._async_get_addon_discovery_info() self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" if not self.unique_id: try: version_info = await async_get_version_info(self.hass, self.ws_address) except CannotConnect as err: raise AbortFlow("cannot_connect") from err await self.async_set_unique_id( version_info.home_id, raise_on_progress=False ) self._abort_if_unique_id_configured( updates={ CONF_URL: self.ws_address, CONF_USB_PATH: self.usb_path, CONF_NETWORK_KEY: self.network_key, } ) return self._async_create_entry_from_vars()
async def _async_try_connect_and_fetch(ip_address: str) -> dict[str, Any]: """Try to connect.""" _LOGGER.debug("config_flow _async_try_connect_and_fetch") # Make connection with device # This is to test the connection and to get info for unique_id energy_api = aiohwenergy.HomeWizardEnergy(ip_address) try: with async_timeout.timeout(10): await energy_api.initialize() except aiohwenergy.DisabledError as ex: _LOGGER.error("API disabled, API must be enabled in the app") raise AbortFlow("api_not_enabled") from ex except Exception as ex: _LOGGER.exception( "Error connecting with Energy Device at %s", ip_address, ) raise AbortFlow("unknown_error") from ex finally: await energy_api.close() if energy_api.device is None: _LOGGER.error("Initialization failed") raise AbortFlow("unknown_error") # Validate metadata if energy_api.device.api_version != "v1": raise AbortFlow("unsupported_api_version") if energy_api.device.product_type not in SUPPORTED_DEVICES: _LOGGER.error( "Device (%s) not supported by integration", energy_api.device.product_type, ) raise AbortFlow("device_not_supported") return { CONF_PRODUCT_NAME: energy_api.device.product_name, CONF_PRODUCT_TYPE: energy_api.device.product_type, CONF_SERIAL: energy_api.device.serial, }
async def _async_set_addon_config(self, config: dict) -> None: """Set Z-Wave JS add-on config.""" addon_manager: AddonManager = get_addon_manager(self.hass) try: await addon_manager.async_set_addon_options(config) except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_set_config_failed") from err
async def _async_set_addon_config(self, config): """Set OpenZWave add-on config.""" options = {"options": config} try: await self.hass.components.hassio.async_set_addon_options( "core_zwave", options) except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to set OpenZWave add-on config: %s", err) raise AbortFlow("addon_set_config_failed") from err
async def _async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" addon_manager: AddonManager = get_addon_manager(self.hass) try: addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_info_failed") from err return addon_info
async def _async_get_addon_discovery_info(self) -> dict: """Return add-on discovery info.""" assert self.hass try: discovery_info: dict = ( await self.hass.components.hassio.async_get_addon_discovery_info( "core_zwave_js")) except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err) raise AbortFlow("addon_get_discovery_info_failed") from err if not discovery_info: _LOGGER.error("Failed to get Z-Wave JS add-on discovery info") raise AbortFlow("addon_missing_discovery_info") discovery_info_config: dict = discovery_info["config"] return discovery_info_config
async def _async_get_addon_info(self): """Return and cache OpenZWave add-on info.""" try: addon_info = await self.hass.components.hassio.async_get_addon_info( "core_zwave") except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to get OpenZWave add-on info: %s", err) raise AbortFlow("addon_info_failed") from err return addon_info
async def _async_get_addon_discovery_info(self) -> dict: """Return add-on discovery info.""" addon_manager: AddonManager = get_addon_manager(self.hass) try: discovery_info_config = await addon_manager.async_get_addon_discovery_info() except AddonError as err: _LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err) raise AbortFlow("addon_get_discovery_info_failed") from err return discovery_info_config
async def _async_get_addon_info(self) -> dict: """Return and cache Z-Wave JS add-on info.""" assert self.hass try: addon_info: dict = await self.hass.components.hassio.async_get_addon_info( "core_zwave_js") except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err) raise AbortFlow("addon_info_failed") from err return addon_info
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 AbortFlow("already_in_progress")
async def _validate_and_create(self, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ for entry in self.hass.config_entries.async_entries(DOMAIN): if ( entry.data[CONF_HOST] == data[CONF_HOST] and entry.data[CONF_PORT] == data[CONF_PORT] ): raise AbortFlow("already_configured") camera = FoscamCamera( data[CONF_HOST], data[CONF_PORT], data[CONF_USERNAME], data[CONF_PASSWORD], verbose=False, ) # Validate data by sending a request to the camera ret, _ = await self.hass.async_add_executor_job(camera.get_product_all_info) if ret == ERROR_FOSCAM_UNAVAILABLE: raise CannotConnect if ret == ERROR_FOSCAM_AUTH: raise InvalidAuth if ret != FOSCAM_SUCCESS: LOGGER.error( "Unexpected error code from camera %s:%s: %s", data[CONF_HOST], data[CONF_PORT], ret, ) raise InvalidResponse # Try to get camera name (only possible with admin account) ret, response = await self.hass.async_add_executor_job(camera.get_dev_info) dev_name = response.get( "devName", f"Foscam {data[CONF_HOST]}:{data[CONF_PORT]}" ) name = data.pop(CONF_NAME, dev_name) return self.async_create_entry(title=name, data=data)
async def _async_discovery_handler(self, ip_address: str) -> FlowResult: """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 AbortFlow("already_in_progress") self._device_type = DEVICE_TYPE_ISMARTGATE return await self.async_step_user()
async def _async_connect_discovered_or_abort(self) -> None: """Connect to the device and verify its responding.""" device = self._discovered_device bulb = wizlight(device.ip_address) try: bulbtype = await bulb.get_bulbtype() except WIZ_CONNECT_EXCEPTIONS as ex: _LOGGER.debug( "Failed to connect to %s during discovery: %s", device.ip_address, ex, exc_info=True, ) raise AbortFlow("cannot_connect") from ex self._name = name_from_bulb_type_and_mac(bulbtype, device.mac_address)
async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: url = user_input[CONF_URL] username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] qsw = QnapQswApi( aiohttp_client.async_get_clientsession(self.hass), ConnectionOptions(url, username, password), ) try: system_board = await qsw.validate() except LoginError: errors[CONF_PASSWORD] = "invalid_auth" except QswError: errors[CONF_URL] = "cannot_connect" else: mac = system_board.get_mac() if mac is None: raise AbortFlow("invalid_id") await self.async_set_unique_id(format_mac(mac), raise_on_progress=False) self._abort_if_unique_id_configured() title = f"QNAP {system_board.get_product()} {mac}" return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required(CONF_URL): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } ), errors=errors, )
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle DHCP discovery.""" self._discovered_url = f"http://{discovery_info.ip}" self._discovered_mac = discovery_info.macaddress _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) options = ConnectionOptions(self._discovered_url, "", "") qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) try: await qsw.get_live() except QswError as err: raise AbortFlow("cannot_connect") from err await self.async_set_unique_id(format_mac(self._discovered_mac)) self._abort_if_unique_id_configured() return await self.async_step_discovered_connection()
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 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], }
def _async_discover_devices(self) -> None: current_addresses = self._async_current_ids() for connectable in (True, False): for discovery_info in async_discovered_service_info(self.hass, connectable): address = discovery_info.address if ( format_unique_id(address) in current_addresses or address in self._discovered_advs ): continue parsed = parse_advertisement_data( discovery_info.device, discovery_info.advertisement ) if not parsed: continue model_name = parsed.data.get("modelName") if ( discovery_info.connectable and model_name in CONNECTABLE_SUPPORTED_MODEL_TYPES ) or model_name in NON_CONNECTABLE_SUPPORTED_MODEL_TYPES: self._discovered_advs[address] = parsed if not self._discovered_advs: raise AbortFlow("no_unconfigured_devices")
async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo) -> FlowResult: """Handle a discovered HomeKit accessory. This flow is triggered by the discovery component. """ # Normalize properties from discovery # homekit_python has code to do this, but not in a form we can # easily use, so do the bare minimum ourselves here instead. properties = { key.lower(): value for (key, value) in discovery_info.properties.items() } if zeroconf.ATTR_PROPERTIES_ID not in properties: # This can happen if the TXT record is received after the PTR record # we will wait for the next update in this case _LOGGER.debug( "HomeKit device %s: id not exposed; TXT record may have not yet been received", properties, ) return self.async_abort(reason="invalid_properties") # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. hkid = properties[zeroconf.ATTR_PROPERTIES_ID] normalized_hkid = normalize_hkid(hkid) model = properties["md"] name = discovery_info.name.replace("._hap._tcp.local.", "") status_flags = int(properties["sf"]) paired = not status_flags & 0x01 # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. try: config_num = int(properties["c#"]) except KeyError: _LOGGER.warning( "HomeKit device %s: c# not exposed, in violation of spec", hkid) config_num = None # Set unique-id and error out if it's already configured existing_entry = await self.async_set_unique_id( normalized_hkid, raise_on_progress=False) updated_ip_port = { "AccessoryIP": discovery_info.host, "AccessoryPort": discovery_info.port, } # If the device is already paired and known to us we should monitor c# # (config_num) for changes. If it changes, we check for new entities if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): if existing_entry: self.hass.config_entries.async_update_entry( existing_entry, data={ **existing_entry.data, **updated_ip_port }) conn = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected await conn.pairing.connection.reconnect_soon() if conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid) self.hass.async_create_task( conn.async_refresh_entity_map(config_num)) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) # Device isn't paired with us or anyone else. # But we have a 'complete' config entry for it - that is probably # invalid. Remove it automatically. existing = find_existing_host(self.hass, hkid) if not paired and existing: if self.controller is None: await self._async_setup_controller() pairing = self.controller.load_pairing( existing.data["AccessoryPairingID"], dict(existing.data)) try: await pairing.list_accessories_and_characteristics() except AuthenticationError: _LOGGER.debug( "%s (%s - %s) is unpaired. Removing invalid pairing for this device", name, model, hkid, ) await self.hass.config_entries.async_remove(existing.entry_id) else: _LOGGER.debug( "%s (%s - %s) claims to be unpaired but isn't. " "It's implementation of HomeKit is defective " "or a zeroconf relay is broadcasting stale data", name, model, hkid, ) return self.async_abort(reason="already_paired") # Set unique-id and error out if it's already configured self._abort_if_unique_id_configured(updates=updated_ip_port) for progress in self._async_in_progress(include_uninitialized=True): if progress["context"].get("unique_id") == normalized_hkid: if paired: # If the device gets paired, we want to dismiss # an existing discovery since we can no longer # pair with it self.hass.config_entries.flow.async_abort( progress["flow_id"]) else: raise AbortFlow("already_in_progress") if paired: # Device is paired but not to us - ignore it _LOGGER.debug("HomeKit device %s ignored as already paired", hkid) return self.async_abort(reason="already_paired") # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. if model in HOMEKIT_IGNORE: return self.async_abort(reason="ignored_model") # If this is a HomeKit bridge/accessory exported by *this* HA instance ignore it. if await self._hkid_is_homekit(hkid): return self.async_abort(reason="ignored_model") self.name = name self.model = model self.hkid = hkid # We want to show the pairing form - but don't call async_step_pair # directly as it has side effects (will ask the device to show a # pairing code) return self._async_step_pair_show_form()
async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a discovered integration.""" lock_cfg = ValidatedLockConfig( discovery_info["name"], discovery_info["address"], discovery_info["serial"], discovery_info["key"], discovery_info["slot"], ) address = lock_cfg.address local_name = lock_cfg.local_name hass = self.hass # We do not want to raise on progress as integration_discovery takes # precedence over other discovery flows since we already have the keys. # # After we do discovery we will abort the flows that do not have the keys # below unless the user is already setting them up. await self.async_set_unique_id(address, raise_on_progress=False) new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} self._abort_if_unique_id_configured(updates=new_data) for entry in self._async_current_entries(): if (local_name_is_unique(lock_cfg.local_name) and entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name): if hass.config_entries.async_update_entry(entry, data={ **entry.data, **new_data }): hass.async_create_task( hass.config_entries.async_reload(entry.entry_id)) raise AbortFlow(reason="already_configured") try: self._discovery_info = await async_get_service_info( hass, local_name, address) except asyncio.TimeoutError: return self.async_abort(reason="no_devices_found") # Integration discovery should abort other flows unless they # are already in the process of being set up since this discovery # will already have all the keys and the user can simply confirm. for progress in self._async_in_progress(include_uninitialized=True): context = progress["context"] if (local_name_is_unique(local_name) and context.get("local_name") == local_name) or context.get("unique_id") == address: if context.get("active"): # The user has already started interacting with this flow # and entered the keys. We abort the discovery flow since # we assume they do not want to use the discovered keys for # some reason. raise data_entry_flow.AbortFlow("already_in_progress") hass.config_entries.flow.async_abort(progress["flow_id"]) self._lock_cfg = lock_cfg self.context["title_placeholders"] = { "name": human_readable_name(lock_cfg.name, lock_cfg.local_name, self._discovery_info.address) } return await self.async_step_integration_discovery_confirm()