# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Xiaomi Corporation.

The ownership and intellectual property rights of Xiaomi Home Assistant
Integration and related Xiaomi cloud service API interface provided under this
license, including source code and object code (collectively, "Licensed Work"),
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
hereby grants you a personal, limited, non-exclusive, non-transferable,
non-sublicensable, and royalty-free license to reproduce, use, modify, and
distribute the Licensed Work only for your use of Home Assistant for
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
you to use the Licensed Work for any other purpose, including but not limited
to use Licensed Work to develop applications (APP), Web services, and other
forms of software.

You may reproduce and distribute copies of the Licensed Work, with or without
modifications, whether in source or object form, provided that you must give
any other recipients of the Licensed Work a copy of this License and retain all
copyright and disclaimers.

Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied, including, without
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
for any direct, indirect, special, incidental, or consequential damages or
losses arising from the use or inability to use the Licensed Work.

Xiaomi reserves all rights not expressly granted to you in this License.
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
does not authorize you in any form to use the trademarks, copyrights, or other
forms of intellectual property rights of Xiaomi and its affiliates, including,
without limitation, without obtaining other written permission from Xiaomi, you
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
may make the public associate with Xiaomi in any form to publicize or promote
the software or hardware devices that use the Licensed Work.

Xiaomi has the right to immediately terminate all your authorization under this
License in the event:
1. You assert patent invalidation, litigation, or other claims against patents
or other intellectual property rights of Xiaomi or its affiliates; or,
2. You make, have made, manufacture, sell, or offer to sell products that knock
off Xiaomi or its affiliates' products.

Config flow for Xiaomi Home.
"""
import asyncio
import hashlib
import ipaddress
import json
import secrets
import traceback
from typing import Optional, Set, Tuple
from urllib.parse import urlparse
from aiohttp import web
from aiohttp.hdrs import METH_GET
import voluptuous as vol
import logging

from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import HaAsyncZeroconf
from homeassistant.components.webhook import (
    async_register as webhook_async_register,
    async_unregister as webhook_async_unregister,
    async_generate_path as webhook_async_generate_path
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.instance_id import async_get
import homeassistant.helpers.config_validation as cv

from .miot.const import (
    DEFAULT_CLOUD_SERVER,
    DEFAULT_CTRL_MODE,
    DEFAULT_INTEGRATION_LANGUAGE,
    DEFAULT_NICK_NAME,
    DEFAULT_OAUTH2_API_HOST,
    DOMAIN,
    OAUTH2_AUTH_URL,
    OAUTH2_CLIENT_ID,
    CLOUD_SERVERS,
    OAUTH_REDIRECT_URL,
    INTEGRATION_LANGUAGES,
    SUPPORT_CENTRAL_GATEWAY_CTRL,
    NETWORK_REFRESH_INTERVAL,
    MIHOME_CERT_EXPIRE_MARGIN
)
from .miot.miot_cloud import MIoTHttpClient, MIoTOauthClient
from .miot.miot_storage import MIoTStorage, MIoTCert
from .miot.miot_mdns import MipsService
from .miot.web_pages import oauth_redirect_page
from .miot.miot_error import (
    MIoTConfigError, MIoTError, MIoTErrorCode, MIoTOauthError)
from .miot.miot_i18n import MIoTI18n
from .miot.miot_network import MIoTNetwork
from .miot.miot_client import MIoTClient, get_miot_instance_async
from .miot.miot_spec import MIoTSpecParser
from .miot.miot_lan import MIoTLan

_LOGGER = logging.getLogger(__name__)


class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    """Xiaomi Home config flow."""
    # pylint: disable=unused-argument, inconsistent-quotes
    VERSION = 1
    MINOR_VERSION = 1
    DEFAULT_AREA_NAME_RULE = 'room'
    _main_loop: asyncio.AbstractEventLoop
    _miot_network: MIoTNetwork
    _mips_service: MipsService
    _miot_storage: MIoTStorage
    _miot_i18n: MIoTI18n
    _miot_oauth: Optional[MIoTOauthClient]
    _miot_http: Optional[MIoTHttpClient]

    _storage_path: str
    _virtual_did: str
    _uid: str
    _uuid: str
    _ctrl_mode: str
    _area_name_rule: str
    _action_debug: bool
    _hide_non_standard_entities: bool
    _display_binary_mode: list[str]
    _display_devices_changed_notify: list[str]

    _cloud_server: str
    _integration_language: str
    _auth_info: dict
    _nick_name: str
    _home_selected: dict
    _devices_filter: dict
    _device_list_sorted: dict
    _oauth_redirect_url_full: str

    # Config cache
    _cc_home_info: dict
    _cc_home_list_show: dict
    _cc_network_detect_addr: str
    _cc_oauth_auth_url: str
    _cc_user_cert_done: bool
    _cc_task_oauth: Optional[asyncio.Task[None]]
    _cc_config_rc: Optional[str]
    _cc_fut_oauth_code: Optional[asyncio.Future]
    _opt_check_network_deps: bool

    def __init__(self) -> None:
        self._main_loop = asyncio.get_running_loop()
        self._cloud_server = DEFAULT_CLOUD_SERVER
        self._integration_language = DEFAULT_INTEGRATION_LANGUAGE
        self._storage_path = ''
        self._virtual_did = ''
        self._uid = ''
        self._uuid = ''   # MQTT client id
        self._ctrl_mode = DEFAULT_CTRL_MODE
        self._area_name_rule = self.DEFAULT_AREA_NAME_RULE
        self._action_debug = False
        self._hide_non_standard_entities = False
        self._display_binary_mode = ['bool']
        self._display_devices_changed_notify = ['add', 'del', 'offline']
        self._auth_info = {}
        self._nick_name = DEFAULT_NICK_NAME
        self._home_selected = {}
        self._devices_filter = {}
        self._device_list_sorted = {}
        self._oauth_redirect_url_full = ''
        self._miot_oauth = None
        self._miot_http = None

        self._cc_home_info = {}
        self._cc_home_list_show = {}
        self._cc_network_detect_addr = ''
        self._cc_oauth_auth_url = ''
        self._cc_user_cert_done = False
        self._cc_task_oauth = None
        self._cc_config_rc = None
        self._cc_fut_oauth_code = None
        self._opt_check_network_deps = False

    async def async_step_user(
        self, user_input: Optional[dict] = None
    ):
        self.hass.data.setdefault(DOMAIN, {})
        if not self._virtual_did:
            self._virtual_did = str(secrets.randbits(64))
            self.hass.data[DOMAIN].setdefault(self._virtual_did, {})
        if not self._storage_path:
            self._storage_path = self.hass.config.path('.storage', DOMAIN)
        # MIoT storage
        self._miot_storage = self.hass.data[DOMAIN].get('miot_storage', None)
        if not self._miot_storage:
            self._miot_storage = MIoTStorage(
                root_path=self._storage_path, loop=self._main_loop)
            self.hass.data[DOMAIN]['miot_storage'] = self._miot_storage
            _LOGGER.info(
                'async_step_user, create miot storage, %s', self._storage_path)
        # MIoT network
        network_detect_addr = (await self._miot_storage.load_user_config_async(
            uid='global_config', cloud_server='all',
            keys=['network_detect_addr'])).get('network_detect_addr', {})
        self._cc_network_detect_addr = ','.join(
            network_detect_addr.get('ip', [])
            + network_detect_addr.get('url', []))
        self._miot_network = self.hass.data[DOMAIN].get('miot_network', None)
        if not self._miot_network:
            self._miot_network = MIoTNetwork(
                ip_addr_list=network_detect_addr.get('ip', []),
                url_addr_list=network_detect_addr.get('url', []),
                refresh_interval=NETWORK_REFRESH_INTERVAL,
                loop=self._main_loop)
            self.hass.data[DOMAIN]['miot_network'] = self._miot_network
            await self._miot_network.init_async()
            _LOGGER.info('async_step_user, create miot network')
        # MIPS service
        self._mips_service = self.hass.data[DOMAIN].get('mips_service', None)
        if not self._mips_service:
            aiozc: HaAsyncZeroconf = await zeroconf.async_get_async_instance(
                self.hass)
            self._mips_service = MipsService(aiozc=aiozc, loop=self._main_loop)
            self.hass.data[DOMAIN]['mips_service'] = self._mips_service
            await self._mips_service.init_async()
            _LOGGER.info('async_step_user, create mips service')

        return await self.async_step_eula(user_input)

    async def async_step_eula(
        self, user_input: Optional[dict] = None
    ):
        if user_input:
            if user_input.get('eula', None) is True:
                return await self.async_step_auth_config()
            return await self.__show_eula_form('eula_not_agree')
        return await self.__show_eula_form('')

    async def __show_eula_form(self, reason: str):
        return self.async_show_form(
            step_id='eula',
            data_schema=vol.Schema({
                vol.Required('eula', default=False): bool,  # type: ignore
            }),
            last_step=False,
            errors={'base': reason},
        )

    async def async_step_auth_config(
        self, user_input: Optional[dict] = None
    ):
        if user_input:
            self._cloud_server = user_input.get(
                'cloud_server', self._cloud_server)
            # Gen instance uuid
            ha_uuid = await async_get(self.hass)
            if not ha_uuid:
                raise AbortFlow(reason='ha_uuid_get_failed')
            self._uuid = hashlib.sha256(
                f'{ha_uuid}.{self._virtual_did}.{self._cloud_server}'.encode(
                    'utf-8')).hexdigest()[:32]
            self._integration_language = user_input.get(
                'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
            self._miot_i18n = MIoTI18n(
                lang=self._integration_language, loop=self._main_loop)
            await self._miot_i18n.init_async()
            webhook_path = webhook_async_generate_path(
                webhook_id=self._virtual_did)
            self._oauth_redirect_url_full = (
                f'{user_input.get("oauth_redirect_url")}{webhook_path}')

            if user_input.get('network_detect_config', False):
                return await self.async_step_network_detect_config()
            return await self.async_step_oauth(user_input)
        return await self.__show_auth_config_form('')

    async def __show_auth_config_form(self, reason: str):
        # Generate default language from HomeAssistant config (not user config)
        default_language: str = self.hass.config.language
        if default_language not in INTEGRATION_LANGUAGES:
            if default_language.split('-', 1)[0] not in INTEGRATION_LANGUAGES:
                default_language = DEFAULT_INTEGRATION_LANGUAGE
            else:
                default_language = default_language.split('-', 1)[0]
        return self.async_show_form(
            step_id='auth_config',
            data_schema=vol.Schema({
                vol.Required(
                    'cloud_server',
                    default=self._cloud_server  # type: ignore
                ):  vol.In(CLOUD_SERVERS),
                vol.Required(
                    'integration_language',
                    default=default_language  # type: ignore
                ):   vol.In(INTEGRATION_LANGUAGES),
                vol.Required(
                    'oauth_redirect_url',
                    default=OAUTH_REDIRECT_URL  # type: ignore
                ): vol.In([OAUTH_REDIRECT_URL]),
                vol.Required(
                    'network_detect_config',
                    default=False  # type: ignore
                ): bool,
            }),
            errors={'base': reason},
            last_step=False,
        )

    async def async_step_network_detect_config(
        self, user_input: Optional[dict] = None
    ):
        if not user_input:
            return await self.__show_network_detect_config_form(reason='')
        self._cc_network_detect_addr = user_input.get(
            'network_detect_addr', self._cc_network_detect_addr)

        ip_list, url_list, invalid_list = _handle_network_detect_addr(
            addr_str=self._cc_network_detect_addr)
        if invalid_list:
            return await self.__show_network_detect_config_form(
                reason='invalid_network_addr')
        if ip_list or url_list:
            if ip_list and not await self._miot_network.ping_multi_async(
                    ip_list=ip_list):
                return await self.__show_network_detect_config_form(
                    reason='invalid_ip_addr')
            if url_list and not await self._miot_network.http_multi_async(
                    url_list=url_list):
                return await self.__show_network_detect_config_form(
                    reason='invalid_http_addr')
        else:
            if not await self._miot_network.get_network_status_async():
                return await self.__show_network_detect_config_form(
                    reason='invalid_default_addr')
        network_detect_addr: dict = {'ip': ip_list, 'url': url_list}
        # Save
        if await self._miot_storage.update_user_config_async(
            uid='global_config', cloud_server='all', config={
                'network_detect_addr': network_detect_addr}):
            _LOGGER.info(
                'update network_detect_addr, %s', network_detect_addr)
        await self._miot_network.update_addr_list_async(
            ip_addr_list=ip_list, url_addr_list=url_list)
        # Check network deps
        self._opt_check_network_deps = user_input.get(
            'check_network_deps', self._opt_check_network_deps)
        if self._opt_check_network_deps:
            # OAuth2
            if not await self._miot_network.http_multi_async(
                    url_list=[OAUTH2_AUTH_URL]):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_oauth2_host')
            # HTTP API
            http_host = (
                DEFAULT_OAUTH2_API_HOST
                if self._cloud_server == DEFAULT_CLOUD_SERVER
                else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')
            if not await self._miot_network.http_multi_async(
                    url_list=[
                        f'https://{http_host}/app/v2/ha/oauth/get_token']):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_http_host')
            # SPEC API
            if not await self._miot_network.http_multi_async(
                    url_list=[
                        'https://miot-spec.org/miot-spec-v2/template/list/'
                        'device']):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_spec_host')
            # MQTT Broker
            # pylint: disable=import-outside-toplevel
            try:
                from paho.mqtt import client
                mqtt_client = client.Client(
                    client_id=f'ha.{self._uid}',
                    protocol=client.MQTTv5)  # type: ignore
                if mqtt_client.connect(
                        host=f'{self._cloud_server}-ha.mqtt.io.mi.com',
                        port=8883) != 0:
                    raise RuntimeError('mqtt connect error')
                mqtt_client.disconnect()
            except Exception as err:  # pylint: disable=broad-exception-caught
                _LOGGER.error('try connect mqtt broker error, %s', err)
                return await self.__show_network_detect_config_form(
                    reason='unreachable_mqtt_broker')

        return await self.async_step_oauth()

    async def __show_network_detect_config_form(self, reason: str):
        if not self._cc_network_detect_addr:
            addr_list: dict = (await self._miot_storage.load_user_config_async(
                'global_config', 'all', ['network_detect_addr'])).get(
                    'network_detect_addr', {})
            self._cc_network_detect_addr = ','.join(
                addr_list.get('ip', [])+addr_list.get('url', []))
        return self.async_show_form(
            step_id='network_detect_config',
            data_schema=vol.Schema({
                vol.Optional(
                    'network_detect_addr',
                    default=self._cc_network_detect_addr  # type: ignore
                ): str,
                vol.Optional(
                    'check_network_deps',
                    default=self._opt_check_network_deps  # type: ignore
                ): bool,
            }),
            errors={'base': reason},
            description_placeholders={
                'cloud_server': self._cloud_server,
                'http_host': (
                    DEFAULT_OAUTH2_API_HOST
                    if self._cloud_server == DEFAULT_CLOUD_SERVER
                    else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')},
            last_step=False
        )

    async def async_step_oauth(
        self, user_input: Optional[dict] = None
    ):
        # 1: Init miot_oauth, generate auth url
        try:
            if not self._miot_oauth:
                _LOGGER.info(
                    'async_step_oauth, redirect_url: %s',
                    self._oauth_redirect_url_full)
                miot_oauth = MIoTOauthClient(
                    client_id=OAUTH2_CLIENT_ID,
                    redirect_url=self._oauth_redirect_url_full,
                    cloud_server=self._cloud_server,
                    uuid=self._uuid,
                    loop=self._main_loop)
                self._cc_oauth_auth_url = miot_oauth.gen_auth_url(
                    redirect_url=self._oauth_redirect_url_full)
                self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (
                    miot_oauth.state)
                self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (
                    self._miot_i18n)
                _LOGGER.info(
                    'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)
                webhook_async_unregister(
                    self.hass, webhook_id=self._virtual_did)
                webhook_async_register(
                    self.hass,
                    domain=DOMAIN,
                    name='oauth redirect url webhook',
                    webhook_id=self._virtual_did,
                    handler=_handle_oauth_webhook,
                    allowed_methods=(METH_GET,),
                )
                self._cc_fut_oauth_code = self.hass.data[DOMAIN][
                    self._virtual_did].get('fut_oauth_code', None)
                if not self._cc_fut_oauth_code:
                    self._cc_fut_oauth_code = self._main_loop.create_future()
                    self.hass.data[DOMAIN][self._virtual_did][
                        'fut_oauth_code'] = self._cc_fut_oauth_code
                _LOGGER.info(
                    'async_step_oauth, webhook.async_register: %s',
                    self._virtual_did)
                self._miot_oauth = miot_oauth
        except Exception as err:  # pylint: disable=broad-exception-caught
            _LOGGER.error(
                'async_step_oauth, %s, %s', err, traceback.format_exc())
            return self.async_show_progress_done(next_step_id='oauth_error')

        # 2: show OAuth2 loading page
        if self._cc_task_oauth is None:
            self._cc_task_oauth = self.hass.async_create_task(
                self.__check_oauth_async())
        if self._cc_task_oauth.done():
            if (error := self._cc_task_oauth.exception()):
                _LOGGER.error('task_oauth exception, %s', error)
                self._cc_config_rc = str(error)
                return self.async_show_progress_done(next_step_id='oauth_error')
            if self._miot_oauth:
                await self._miot_oauth.deinit_async()
                self._miot_oauth = None
            return self.async_show_progress_done(next_step_id='homes_select')
        # pylint: disable=unexpected-keyword-arg
        return self.async_show_progress(
            step_id='oauth',
            progress_action='oauth',
            description_placeholders={
                'link_left':
                    f'<a href="{self._cc_oauth_auth_url}" target="_blank">',
                'link_right': '</a>'
            },
            progress_task=self._cc_task_oauth,  # type: ignore
        )

    async def __check_oauth_async(self) -> None:
        # TASK 1: Get oauth code
        if not self._cc_fut_oauth_code:
            raise MIoTConfigError('oauth_code_fut_error')
        oauth_code: Optional[str] = await self._cc_fut_oauth_code
        if not oauth_code:
            raise MIoTConfigError('oauth_code_error')
        # TASK 2: Get access_token and user_info from miot_oauth
        if not self._auth_info:
            try:
                if not self._miot_oauth:
                    raise MIoTConfigError('oauth_client_error')
                auth_info = await self._miot_oauth.get_access_token_async(
                    code=oauth_code)
                if not self._miot_http:
                    self._miot_http = MIoTHttpClient(
                        cloud_server=self._cloud_server,
                        client_id=OAUTH2_CLIENT_ID,
                        access_token=auth_info['access_token'])
                else:
                    self._miot_http.update_http_header(
                        cloud_server=self._cloud_server,
                        client_id=OAUTH2_CLIENT_ID,
                        access_token=auth_info['access_token'])
                self._auth_info = auth_info
                try:
                    self._nick_name = (
                        await self._miot_http.get_user_info_async() or {}
                    ).get('miliaoNick', self._nick_name)
                except (MIoTOauthError, json.JSONDecodeError):
                    self._nick_name = DEFAULT_NICK_NAME
                    _LOGGER.error('get nick name failed')
            except Exception as err:
                _LOGGER.error(
                    'get_access_token, %s, %s', err, traceback.format_exc())
                raise MIoTConfigError('get_token_error') from err

        # TASK 3: Get home info
        try:
            if not self._miot_http:
                raise MIoTConfigError('http_client_error')
            self._cc_home_info = (
                await self._miot_http.get_devices_async())
            _LOGGER.info('get_homeinfos response: %s', self._cc_home_info)
            self._uid = self._cc_home_info['uid']
            if self._uid == self._nick_name:
                self._nick_name = DEFAULT_NICK_NAME
            # Save auth_info
            if not (await self._miot_storage.update_user_config_async(
                    uid=self._uid, cloud_server=self._cloud_server, config={
                        'auth_info': self._auth_info
                    })):
                raise MIoTError('miot_storage.update_user_config_async error')
        except Exception as err:
            _LOGGER.error(
                'get_homeinfos error, %s, %s', err, traceback.format_exc())
            raise MIoTConfigError('get_homeinfo_error') from err

        # TASK 4: Abort if unique_id configured
        # Each MiHome account can only configure one instance
        await self.async_set_unique_id(f'{self._cloud_server}{self._uid}')
        self._abort_if_unique_id_configured()

        # TASK 5: Query mdns info
        mips_list = None
        if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:
            try:
                mips_list = self._mips_service.get_services()
            except Exception as err:
                _LOGGER.error(
                    'async_update_services error, %s, %s',
                    err, traceback.format_exc())
                raise MIoTConfigError('mdns_discovery_error') from err

        # TASK 6: Generate devices filter
        home_list = {}
        tip_devices = self._miot_i18n.translate(key='config.other.devices')
        # home list
        for home_id, home_info in self._cc_home_info[
                'homes']['home_list'].items():
            # i18n
            tip_central = ''
            group_id = home_info.get('group_id', None)
            dev_list = {
                device['did']: device
                for device in list(self._cc_home_info['devices'].values())
                if device.get('home_id', None) == home_id}
            if (
                mips_list
                and group_id in mips_list
                and mips_list[group_id].get('did', None) in dev_list
            ):
                # i18n
                tip_central = self._miot_i18n.translate(
                    key='config.other.found_central_gateway')
                home_info['central_did'] = mips_list[group_id].get('did', None)
            home_list[home_id] = (
                f'{home_info["home_name"]} '
                f'[ {len(dev_list)} {tip_devices} {tip_central} ]')

        self._cc_home_list_show = dict(sorted(home_list.items()))

        # TASK 7: Get user's MiHome certificate
        if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:
            miot_cert = MIoTCert(
                storage=self._miot_storage,
                uid=self._uid, cloud_server=self._cloud_server)
            if not self._cc_user_cert_done:
                try:
                    if await miot_cert.user_cert_remaining_time_async(
                            did=self._virtual_did) < MIHOME_CERT_EXPIRE_MARGIN:
                        user_key = await miot_cert.load_user_key_async()
                        if user_key is None:
                            user_key = miot_cert.gen_user_key()
                            if not await miot_cert.update_user_key_async(
                                    key=user_key):
                                raise MIoTError('update_user_key_async failed')
                        csr_str = miot_cert.gen_user_csr(
                            user_key=user_key, did=self._virtual_did)
                        crt_str = await self._miot_http.get_central_cert_async(
                            csr_str)
                        if not crt_str:
                            raise MIoTError('get_central_cert_async failed')
                        if not await miot_cert.update_user_cert_async(
                                cert=crt_str):
                            raise MIoTError('update_user_cert_async failed')
                        self._cc_user_cert_done = True
                        _LOGGER.info(
                            'get mihome cert success, %s, %s',
                            self._uid, self._virtual_did)
                except Exception as err:
                    _LOGGER.error(
                        'get user cert error, %s, %s',
                        err, traceback.format_exc())
                    raise MIoTConfigError('get_cert_error') from err

        # Auth success, unregister oauth webhook
        webhook_async_unregister(self.hass, webhook_id=self._virtual_did)
        if self._miot_http:
            await self._miot_http.deinit_async()
            self._miot_http = None
        _LOGGER.info(
            '__check_oauth_async, webhook.async_unregister: %s',
            self._virtual_did)

    # Show setup error message
    async def async_step_oauth_error(self, user_input=None):
        if self._cc_config_rc is None:
            return await self.async_step_oauth()
        if self._cc_config_rc.startswith('Flow aborted: '):
            raise AbortFlow(
                reason=self._cc_config_rc.replace('Flow aborted: ', ''))
        error_reason = self._cc_config_rc
        self._cc_config_rc = None
        return self.async_show_form(
            step_id='oauth_error',
            data_schema=vol.Schema({}),
            last_step=False,
            errors={'base': error_reason},
        )

    async def async_step_homes_select(
        self, user_input: Optional[dict] = None
    ):
        _LOGGER.debug('async_step_homes_select')
        try:
            if not user_input:
                return await self.__show_homes_select_form('')

            home_selected: list = user_input.get('home_infos', [])
            if not home_selected:
                return await self.__show_homes_select_form(
                    'no_family_selected')
            for home_id, home_info in self._cc_home_info[
                    'homes']['home_list'].items():
                if home_id in home_selected:
                    self._home_selected[home_id] = home_info
            self._area_name_rule = user_input.get(
                'area_name_rule', self._area_name_rule)
            # Storage device list
            devices_list: dict[str, dict] = {
                did: dev_info
                for did, dev_info in self._cc_home_info['devices'].items()
                if dev_info['home_id'] in home_selected}
            if not devices_list:
                return await self.__show_homes_select_form('no_devices')
            self._device_list_sorted = dict(sorted(
                devices_list.items(), key=lambda item:
                    item[1].get('home_id', '')+item[1].get('room_id', '')))

            if not await self._miot_storage.save_async(
                    domain='miot_devices',
                    name=f'{self._uid}_{self._cloud_server}',
                    data=self._device_list_sorted):
                _LOGGER.error(
                    'save devices async failed, %s, %s',
                    self._uid, self._cloud_server)
                return await self.__show_homes_select_form(
                    'devices_storage_failed')
            if user_input.get('advanced_options', False):
                return await self.async_step_advanced_options()
            return await self.config_flow_done()
        except Exception as err:
            _LOGGER.error(
                'async_step_homes_select, %s, %s',
                err, traceback.format_exc())
            raise AbortFlow(
                reason='config_flow_error',
                description_placeholders={
                    'error': f'config_flow error, {err}'}
            ) from err

    async def __show_homes_select_form(self, reason: str):
        return self.async_show_form(
            step_id='homes_select',
            data_schema=vol.Schema({
                vol.Required('home_infos'): cv.multi_select(
                    self._cc_home_list_show),
                vol.Required(
                    'area_name_rule',
                    default=self._area_name_rule  # type: ignore
                ): vol.In(self._miot_i18n.translate(
                    key='config.room_name_rule')),
                vol.Required(
                    'advanced_options', default=False  # type: ignore
                ): bool,
            }),
            errors={'base': reason},
            description_placeholders={
                'nick_name': self._nick_name,
            },
            last_step=False,
        )

    async def async_step_advanced_options(
        self, user_input: Optional[dict] = None
    ):
        if user_input:
            self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
            self._action_debug = user_input.get(
                'action_debug', self._action_debug)
            self._hide_non_standard_entities = user_input.get(
                'hide_non_standard_entities', self._hide_non_standard_entities)
            self._display_binary_mode = user_input.get(
                'display_binary_mode', self._display_binary_mode)
            self._display_devices_changed_notify = user_input.get(
                'display_devices_changed_notify',
                self._display_devices_changed_notify)
            # Device filter
            if user_input.get('devices_filter', False):
                return await self.async_step_devices_filter()
            return await self.config_flow_done()
        return self.async_show_form(
            step_id='advanced_options',
            data_schema=vol.Schema({
                vol.Required(
                    'devices_filter', default=False): bool,  # type: ignore
                vol.Required(
                    'ctrl_mode', default=self._ctrl_mode  # type: ignore
                ): vol.In(self._miot_i18n.translate(key='config.control_mode')),
                vol.Required(
                    'action_debug', default=self._action_debug  # type: ignore
                ): bool,
                vol.Required(
                    'hide_non_standard_entities',
                    default=self._hide_non_standard_entities  # type: ignore
                ): bool,
                vol.Required(
                    'display_binary_mode',
                    default=self._display_binary_mode  # type: ignore
                ): cv.multi_select(
                    self._miot_i18n.translate(
                        key='config.binary_mode')),  # type: ignore
                vol.Required(
                    'display_devices_changed_notify',
                    default=self._display_devices_changed_notify  # type: ignore
                ): cv.multi_select(
                    self._miot_i18n.translate(
                        key='config.device_state')),  # type: ignore
            }),
            last_step=False,
        )

    async def async_step_devices_filter(
        self, user_input: Optional[dict] = None
    ):
        if user_input:
            # Room filter
            include_items: dict = {}
            exclude_items: dict = {}
            room_list_in: list = user_input.get('room_list', [])
            if room_list_in:
                if user_input.get(
                        'room_filter_mode', 'exclude') == 'include':
                    include_items['room_id'] = room_list_in
                else:
                    exclude_items['room_id'] = room_list_in
            # Connect Type filter
            type_list_in: list = user_input.get('type_list', [])
            if type_list_in:
                if user_input.get(
                        'type_filter_mode', 'exclude') == 'include':
                    include_items['connect_type'] = type_list_in
                else:
                    exclude_items['connect_type'] = type_list_in
            # Model filter
            model_list_in: list = user_input.get('model_list', [])
            if model_list_in:
                if user_input.get(
                        'model_filter_mode', 'exclude') == 'include':
                    include_items['model'] = model_list_in
                else:
                    exclude_items['model'] = model_list_in
            # Device filter
            device_list_in: list = user_input.get('device_list', [])
            if device_list_in:
                if user_input.get(
                        'devices_filter_mode', 'exclude') == 'include':
                    include_items['did'] = device_list_in
                else:
                    exclude_items['did'] = device_list_in
            device_filter_list = _handle_devices_filter(
                devices=self._device_list_sorted,
                logic_or=(user_input.get('statistics_logic', 'or') == 'or'),
                item_in=include_items, item_ex=exclude_items)
            if not device_filter_list:
                return await self.__show_devices_filter_form(
                    reason='no_filter_devices')
            self._device_list_sorted = dict(sorted(
                device_filter_list.items(), key=lambda item:
                    item[1].get('home_id', '')+item[1].get('room_id', '')))
            # Save devices
            if not await self._miot_storage.save_async(
                    domain='miot_devices',
                    name=f'{self._uid}_{self._cloud_server}',
                    data=self._device_list_sorted):
                _LOGGER.error(
                    'save devices async failed, %s, %s',
                    self._uid, self._cloud_server)
                raise AbortFlow(
                    reason='storage_error', description_placeholders={
                        'error': 'save user devices error'})
            self._devices_filter = {
                'room_list': {
                    'items': room_list_in,
                    'mode': user_input.get('room_filter_mode', 'exclude')},
                'type_list': {
                    'items': type_list_in,
                    'mode': user_input.get('type_filter_mode', 'exclude')},
                'model_list': {
                    'items': model_list_in,
                    'mode': user_input.get('model_filter_mode', 'exclude')},
                'device_list': {
                    'items': device_list_in,
                    'mode': user_input.get('devices_filter_mode', 'exclude')},
                'statistics_logic': user_input.get('statistics_logic', 'or'),
            }
            return await self.config_flow_done()
        return await self.__show_devices_filter_form(reason='')

    async def __show_devices_filter_form(self, reason: str):
        tip_devices: str = self._miot_i18n.translate(
            key='config.other.devices')  # type: ignore
        tip_without_room: str = self._miot_i18n.translate(
            key='config.other.without_room')  # type: ignore
        trans_statistics_logic: dict = self._miot_i18n.translate(
            key='config.statistics_logic')  # type: ignore
        trans_filter_mode: dict = self._miot_i18n.translate(
            key='config.filter_mode')  # type: ignore
        trans_connect_type: dict = self._miot_i18n.translate(
            key='config.connect_type')  # type: ignore

        room_device_count: dict = {}
        model_device_count: dict = {}
        connect_type_count: dict = {}
        device_list: dict = {}
        for did, info in self._device_list_sorted.items():
            device_list[did] = (
                f'[ {info["home_name"]} {info["room_name"]} ] ' +
                f'{info["name"]}, {did}')
            room_device_count.setdefault(info['room_id'], 0)
            room_device_count[info['room_id']] += 1
            model_device_count.setdefault(info['model'], 0)
            model_device_count[info['model']] += 1
            connect_type_count.setdefault(str(info['connect_type']), 0)
            connect_type_count[str(info['connect_type'])] += 1
        model_list: dict = {}
        for model, count in model_device_count.items():
            model_list[model] = f'{model} [ {count} {tip_devices} ]'
        type_list: dict = {
            k: f'{trans_connect_type.get(k, f"Connect Type ({k})")} '
            f'[ {v} {tip_devices} ]'
            for k, v in connect_type_count.items()}
        room_list: dict = {}
        for home_id, home_info in self._home_selected.items():
            for room_id, room_name in home_info['room_info'].items():
                if room_id not in room_device_count:
                    continue
                room_list[room_id] = (
                    f'{home_info["home_name"]} {room_name}'
                    f' [ {room_device_count[room_id]}{tip_devices} ]')
            if home_id in room_device_count:
                room_list[home_id] = (
                    f'{home_info["home_name"]} {tip_without_room}'
                    f' [ {room_device_count[home_id]}{tip_devices} ]')
        return self.async_show_form(
            step_id='devices_filter',
            data_schema=vol.Schema({
                vol.Required(
                    'room_filter_mode', default='exclude'  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('room_list'): cv.multi_select(room_list),
                vol.Required(
                    'type_filter_mode', default='exclude'  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('type_list'): cv.multi_select(type_list),
                vol.Required(
                    'model_filter_mode', default='exclude'  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('model_list'): cv.multi_select(dict(sorted(
                    model_list.items(), key=lambda item: item[0]))),
                vol.Required(
                    'devices_filter_mode', default='exclude'  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('device_list'): cv.multi_select(dict(sorted(
                    device_list.items(), key=lambda device: device[1]))),
                vol.Required(
                    'statistics_logic', default='or'  # type: ignore
                ): vol.In(trans_statistics_logic),
            }),
            errors={'base': reason},
            last_step=False
        )

    async def config_flow_done(self):
        return self.async_create_entry(
            title=(
                f'{self._nick_name}: {self._uid} '
                f'[{CLOUD_SERVERS[self._cloud_server]}]'),
            data={
                'virtual_did': self._virtual_did,
                'uuid': self._uuid,
                'integration_language': self._integration_language,
                'storage_path': self._storage_path,
                'uid': self._uid,
                'nick_name': self._nick_name,
                'cloud_server': self._cloud_server,
                'oauth_redirect_url': self._oauth_redirect_url_full,
                'ctrl_mode': self._ctrl_mode,
                'home_selected': self._home_selected,
                'devices_filter': self._devices_filter,
                'area_name_rule': self._area_name_rule,
                'action_debug': self._action_debug,
                'hide_non_standard_entities':
                    self._hide_non_standard_entities,
                'display_binary_mode': self._display_binary_mode,
                'display_devices_changed_notify':
                    self._display_devices_changed_notify
            })

    @ staticmethod
    @ callback
    def async_get_options_flow(
            config_entry: config_entries.ConfigEntry,
    ) -> config_entries.OptionsFlow:
        return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
    """Xiaomi MiHome options flow."""
    # pylint: disable=unused-argument
    # pylint: disable=inconsistent-quotes
    _config_entry: config_entries.ConfigEntry
    _main_loop: asyncio.AbstractEventLoop
    _miot_client: MIoTClient

    _miot_network: MIoTNetwork
    _miot_storage: MIoTStorage
    _mips_service: MipsService
    _miot_oauth: MIoTOauthClient
    _miot_http: MIoTHttpClient
    _miot_i18n: MIoTI18n
    _miot_lan: MIoTLan

    _entry_data: dict
    _virtual_did: str
    _uid: str
    _storage_path: str
    _cloud_server: str

    _integration_language: str
    _ctrl_mode: str
    _nick_name: str
    _home_selected_list: list
    _devices_filter: dict
    _action_debug: bool
    _hide_non_standard_entities: bool
    _display_binary_mode: list[str]
    _display_devs_notify: list[str]

    _oauth_redirect_url_full: str
    _auth_info: dict
    _home_selected: dict
    _device_list_sorted: dict
    _devices_add: list[str]
    _devices_remove: list[str]

    # Config options
    _lang_new: str
    _nick_name_new: Optional[str]
    _action_debug_new: bool
    _hide_non_standard_entities_new: bool
    _display_binary_mode_new: list[str]
    _update_user_info: bool
    _update_devices: bool
    _update_trans_rules: bool
    _opt_lan_ctrl_cfg: bool
    _opt_network_detect_cfg: bool
    _opt_check_network_deps: bool

    _trans_rules_count: int
    _trans_rules_count_success: int

    _need_reload: bool

    # Config cache
    _cc_home_info: dict
    _cc_home_list_show: dict
    _cc_oauth_auth_url: Optional[str]
    _cc_task_oauth: Optional[asyncio.Task[None]]
    _cc_config_rc: Optional[str]
    _cc_fut_oauth_code: Optional[asyncio.Future]
    _cc_devices_local: dict
    _cc_network_detect_addr: str

    def __init__(self, config_entry: config_entries.ConfigEntry):
        self._config_entry = config_entry
        self._main_loop = asyncio.get_event_loop()

        self._entry_data = dict(config_entry.data)
        self._virtual_did = self._entry_data['virtual_did']
        self._uid = self._entry_data['uid']
        self._storage_path = self._entry_data['storage_path']
        self._cloud_server = self._entry_data['cloud_server']
        self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)
        self._integration_language = self._entry_data.get(
            'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
        self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)
        self._action_debug = self._entry_data.get('action_debug', False)
        self._hide_non_standard_entities = self._entry_data.get(
            'hide_non_standard_entities', False)
        self._display_binary_mode = self._entry_data.get(
            'display_binary_mode', ['text'])
        self._display_devs_notify = self._entry_data.get(
            'display_devices_changed_notify', ['add', 'del', 'offline'])
        self._home_selected_list = list(
            self._entry_data['home_selected'].keys())
        self._devices_filter = self._entry_data.get('devices_filter', {})

        self._oauth_redirect_url_full = ''
        self._auth_info = {}
        self._home_selected = {}
        self._device_list_sorted = {}

        self._devices_add = []
        self._devices_remove = []

        self._lang_new = self._integration_language
        self._nick_name_new = None
        self._action_debug_new = False
        self._hide_non_standard_entities_new = False
        self._display_binary_mode_new = []
        self._update_user_info = False
        self._update_devices = False
        self._update_trans_rules = False
        self._opt_lan_ctrl_cfg = False
        self._opt_network_detect_cfg = False
        self._opt_check_network_deps = False
        self._trans_rules_count = 0
        self._trans_rules_count_success = 0

        self._need_reload = False

        self._cc_home_info = {}
        self._cc_home_list_show = {}
        self._cc_oauth_auth_url = None
        self._cc_task_oauth = None
        self._cc_config_rc = None
        self._cc_fut_oauth_code = None
        self._cc_devices_local = {}
        self._cc_network_detect_addr = ''

        _LOGGER.info(
            'options init, %s, %s, %s, %s', config_entry.entry_id,
            config_entry.unique_id, config_entry.data, config_entry.options)

    async def async_step_init(self, user_input=None):
        self.hass.data.setdefault(DOMAIN, {})
        self.hass.data[DOMAIN].setdefault(self._virtual_did, {})
        try:
            # MIoT client
            self._miot_client = await get_miot_instance_async(
                hass=self.hass, entry_id=self._config_entry.entry_id)
            if not self._miot_client:
                raise MIoTConfigError('invalid miot client')
            # MIoT network
            self._miot_network = self._miot_client.miot_network
            if not self._miot_network:
                raise MIoTConfigError('invalid miot network')
            # MIoT storage
            self._miot_storage = self._miot_client.miot_storage
            if not self._miot_storage:
                raise MIoTConfigError('invalid miot storage')
            # Mips service
            self._mips_service = self._miot_client.mips_service
            if not self._mips_service:
                raise MIoTConfigError('invalid mips service')
            # MIoT oauth
            self._miot_oauth = self._miot_client.miot_oauth
            if not self._miot_oauth:
                raise MIoTConfigError('invalid miot oauth')
            # MIoT http
            self._miot_http = self._miot_client.miot_http
            if not self._miot_http:
                raise MIoTConfigError('invalid miot http')
            self._miot_i18n = self._miot_client.miot_i18n
            if not self._miot_i18n:
                raise MIoTConfigError('invalid miot i18n')
            self._miot_lan = self._miot_client.miot_lan
            if not self._miot_lan:
                raise MIoTConfigError('invalid miot lan')
            # Check token
            if not await self._miot_client.refresh_oauth_info_async():
                # Check network
                if not await self._miot_network.get_network_status_async():
                    raise AbortFlow(
                        reason='network_connect_error',
                        description_placeholders={})
                self._need_reload = True
                return await self.async_step_auth_config()
            return await self.async_step_config_options()
        except MIoTConfigError as err:
            raise AbortFlow(
                reason='options_flow_error',
                description_placeholders={'error': str(err)}
            ) from err
        except AbortFlow as err:
            raise err
        except Exception as err:
            _LOGGER.error(
                'async_step_init error, %s, %s',
                err, traceback.format_exc())
            raise AbortFlow(
                reason='re_add',
                description_placeholders={'error': str(err)},
            ) from err

    async def async_step_auth_config(self, user_input=None):
        if user_input:
            webhook_path = webhook_async_generate_path(
                webhook_id=self._virtual_did)
            self._oauth_redirect_url_full = (
                f'{user_input.get("oauth_redirect_url")}{webhook_path}')
            return await self.async_step_oauth(user_input)
        return self.async_show_form(
            step_id='auth_config',
            data_schema=vol.Schema({
                vol.Required(
                    'oauth_redirect_url',
                    default=OAUTH_REDIRECT_URL  # type: ignore
                ): vol.In([OAUTH_REDIRECT_URL]),
            }),
            description_placeholders={
                'cloud_server': CLOUD_SERVERS[self._cloud_server],
            },
            last_step=False,
        )

    async def async_step_oauth(self, user_input=None):
        try:
            if self._cc_task_oauth is None:
                self._cc_oauth_auth_url = self._miot_oauth.gen_auth_url(
                    redirect_url=self._oauth_redirect_url_full)
                self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (
                    self._miot_oauth.state)
                self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (
                    self._miot_i18n)
                _LOGGER.info(
                    'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)
                webhook_async_unregister(
                    self.hass, webhook_id=self._virtual_did)
                webhook_async_register(
                    self.hass,
                    domain=DOMAIN,
                    name='oauth redirect url webhook',
                    webhook_id=self._virtual_did,
                    handler=_handle_oauth_webhook,
                    allowed_methods=(METH_GET,),
                )
                self._cc_fut_oauth_code = self.hass.data[DOMAIN][
                    self._virtual_did].get('fut_oauth_code', None)
                if self._cc_fut_oauth_code is None:
                    self._cc_fut_oauth_code = self._main_loop.create_future()
                    self.hass.data[DOMAIN][self._virtual_did][
                        'fut_oauth_code'] = self._cc_fut_oauth_code
                self._cc_task_oauth = self.hass.async_create_task(
                    self.__check_oauth_async())
                _LOGGER.info(
                    'async_step_oauth, webhook.async_register: %s',
                    self._virtual_did)

            if self._cc_task_oauth.done():
                if (error := self._cc_task_oauth.exception()):
                    _LOGGER.error('task_oauth exception, %s', error)
                    self._cc_config_rc = str(error)
                    self._cc_task_oauth = None
                    return self.async_show_progress_done(
                        next_step_id='oauth_error')
                return self.async_show_progress_done(
                    next_step_id='config_options')
        except Exception as err:  # pylint: disable=broad-exception-caught
            _LOGGER.error(
                'async_step_oauth error, %s, %s',
                err, traceback.format_exc())
            self._cc_config_rc = str(err)
            return self.async_show_progress_done(next_step_id='oauth_error')
        # pylint: disable=unexpected-keyword-arg
        return self.async_show_progress(
            step_id='oauth',
            progress_action='oauth',
            description_placeholders={
                'link_left':
                    f'<a href="{self._cc_oauth_auth_url}" target="_blank">',
                'link_right': '</a>'
            },
            progress_task=self._cc_task_oauth,  # type: ignore
        )

    async def __check_oauth_async(self) -> None:
        # Get oauth code
        if not self._cc_fut_oauth_code:
            raise MIoTConfigError('oauth_code_fut_error')
        oauth_code: str = await self._cc_fut_oauth_code
        if not oauth_code:
            raise MIoTConfigError('oauth_code_error')
        _LOGGER.debug('options flow __check_oauth_async, %s', oauth_code)
        # Get access_token and user_info from miot_oauth
        if not self._auth_info:
            auth_info: dict = {}
            try:
                auth_info = await self._miot_oauth.get_access_token_async(
                    code=oauth_code)
            except Exception as err:
                _LOGGER.error(
                    'get_access_token, %s, %s', err, traceback.format_exc())
                raise MIoTConfigError('get_token_error') from err
            # Check uid
            m_http: MIoTHttpClient = MIoTHttpClient(
                cloud_server=self._cloud_server,
                client_id=OAUTH2_CLIENT_ID,
                access_token=auth_info['access_token'],
                loop=self._main_loop)
            if await m_http.get_uid_async() != self._uid:
                raise AbortFlow('inconsistent_account')
            del m_http
            self._miot_http.update_http_header(
                access_token=auth_info['access_token'])
            if not await self._miot_storage.update_user_config_async(
                    uid=self._uid,
                    cloud_server=self._cloud_server,
                    config={'auth_info': auth_info}):
                raise AbortFlow('storage_error')
            self._auth_info = auth_info

        # Auth success, unregister oauth webhook
        webhook_async_unregister(self.hass, webhook_id=self._virtual_did)
        _LOGGER.info(
            '__check_oauth_async, webhook.async_unregister: %s',
            self._virtual_did)

    # Show setup error message
    async def async_step_oauth_error(self, user_input=None):
        if self._cc_config_rc is None:
            return await self.async_step_oauth()
        if self._cc_config_rc.startswith('Flow aborted: '):
            raise AbortFlow(
                reason=self._cc_config_rc.replace('Flow aborted: ', ''))
        error_reason = self._cc_config_rc
        self._cc_config_rc = None
        return self.async_show_form(
            step_id='oauth_error',
            data_schema=vol.Schema({}),
            last_step=False,
            errors={'base': error_reason},
        )

    async def async_step_config_options(self, user_input=None):
        if not user_input:
            return self.async_show_form(
                step_id='config_options',
                data_schema=vol.Schema({
                    # Integration configure
                    vol.Required(
                        'integration_language',
                        default=self._integration_language  # type: ignore
                    ): vol.In(INTEGRATION_LANGUAGES),
                    vol.Required(
                        'update_user_info',
                        default=self._update_user_info  # type: ignore
                    ): bool,
                    vol.Required(
                        'network_detect_config',
                        default=self._opt_network_detect_cfg  # type: ignore
                    ): bool,
                    # Device info configure
                    vol.Required(
                        'update_devices',
                        default=self._update_devices  # type: ignore
                    ): bool,
                    vol.Required(
                        'display_devices_changed_notify',
                        default=self._display_devs_notify  # type: ignore
                    ): cv.multi_select(
                        self._miot_i18n.translate(
                            'config.device_state')),  # type: ignore
                    vol.Required(
                        'update_lan_ctrl_config',
                        default=self._opt_lan_ctrl_cfg  # type: ignore
                    ): bool,
                    # Entity info configure
                    vol.Required(
                        'action_debug',
                        default=self._action_debug  # type: ignore
                    ): bool,
                    vol.Required(
                        'hide_non_standard_entities',
                        default=self._hide_non_standard_entities  # type: ignore
                    ): bool,
                    vol.Required(
                        'display_binary_mode',
                        default=self._display_binary_mode  # type: ignore
                    ): cv.multi_select(
                        self._miot_i18n.translate(
                            'config.binary_mode')),  # type: ignore
                    vol.Required(
                        'update_trans_rules',
                        default=self._update_trans_rules  # type: ignore
                    ): bool,
                }),
                errors={},
                description_placeholders={
                    'nick_name': self._nick_name,
                    'uid': self._uid,
                    'cloud_server': CLOUD_SERVERS[self._cloud_server],
                    'instance_id': f'ha.{self._entry_data["uuid"]}'
                },
                last_step=False,
            )
        # Check network
        if not await self._miot_network.get_network_status_async():
            raise AbortFlow(
                reason='network_connect_error', description_placeholders={})
        self._lang_new = user_input.get(
            'integration_language', self._integration_language)
        self._update_user_info = user_input.get(
            'update_user_info', self._update_user_info)
        self._update_devices = user_input.get(
            'update_devices', self._update_devices)
        self._action_debug_new = user_input.get(
            'action_debug', self._action_debug)
        self._hide_non_standard_entities_new = user_input.get(
            'hide_non_standard_entities', self._hide_non_standard_entities)
        self._display_binary_mode_new = user_input.get(
            'display_binary_mode', self._display_binary_mode)
        self._display_devs_notify = user_input.get(
            'display_devices_changed_notify', self._display_devs_notify)
        self._update_trans_rules = user_input.get(
            'update_trans_rules', self._update_trans_rules)
        self._opt_lan_ctrl_cfg = user_input.get(
            'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)
        self._opt_network_detect_cfg = user_input.get(
            'network_detect_config', self._opt_network_detect_cfg)

        return await self.async_step_update_user_info()

    async def async_step_update_user_info(self, user_input=None):
        if not self._update_user_info:
            return await self.async_step_homes_select()
        if not user_input:
            nick_name_new = (
                await self._miot_http.get_user_info_async() or {}).get(
                    'miliaoNick', DEFAULT_NICK_NAME)
            return self.async_show_form(
                step_id='update_user_info',
                data_schema=vol.Schema({
                    vol.Required('nick_name', default=nick_name_new): str
                }),
                description_placeholders={
                    'nick_name': self._nick_name
                },
                last_step=False
            )

        self._nick_name_new = user_input.get('nick_name')
        return await self.async_step_homes_select()

    async def async_step_homes_select(
        self, user_input: Optional[dict] = None
    ):
        if not self._update_devices:
            return await self.async_step_update_trans_rules()
        if not user_input:
            # Query mdns info
            try:
                mips_list = self._mips_service.get_services()
            except Exception as err:
                _LOGGER.error(
                    'async_update_services error, %s, %s',
                    err, traceback.format_exc())
                raise MIoTConfigError('mdns_discovery_error') from err

            # Get home info
            try:
                self._cc_home_info = (
                    await self._miot_http.get_devices_async())
            except Exception as err:
                _LOGGER.error(
                    'get_homeinfos error, %s, %s', err, traceback.format_exc())
                raise MIoTConfigError('get_homeinfo_error') from err
            # Generate devices filter
            home_list = {}
            tip_devices = self._miot_i18n.translate(key='config.other.devices')
            # home list
            for home_id, home_info in self._cc_home_info[
                    'homes']['home_list'].items():
                # i18n
                tip_central = ''
                group_id = home_info.get('group_id', None)
                did_list = {
                    device['did']: device for device in list(
                        self._cc_home_info['devices'].values())
                    if device.get('home_id', None) == home_id}
                if (
                    group_id in mips_list
                    and mips_list[group_id].get('did', None) in did_list
                ):
                    # i18n
                    tip_central = self._miot_i18n.translate(
                        key='config.other.found_central_gateway')
                    home_info['central_did'] = mips_list[group_id].get(
                        'did', None)
                home_list[home_id] = (
                    f'{home_info["home_name"]} '
                    f'[ {len(did_list)} {tip_devices} {tip_central} ]')
            # Remove deleted item
            self._home_selected_list = [
                home_id for home_id in self._home_selected_list
                if home_id in home_list]
            self._cc_home_list_show = dict(sorted(home_list.items()))
            # Get local devices
            self._cc_devices_local = (
                await self._miot_storage.load_async(
                    domain='miot_devices',
                    name=f'{self._uid}_{self._cloud_server}',
                    type_=dict)) or {}  # type: ignore

            return await self.__show_homes_select_form('')

        self._home_selected_list = user_input.get('home_infos', [])
        if not self._home_selected_list:
            return await self.__show_homes_select_form('no_family_selected')
        self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
        self._home_selected = {}
        for home_id, home_info in self._cc_home_info[
                'homes']['home_list'].items():
            if home_id in self._home_selected_list:
                self._home_selected[home_id] = home_info
        # Get device list
        device_list: dict = {
            did: dev_info
            for did, dev_info in self._cc_home_info['devices'].items()
            if dev_info['home_id'] in self._home_selected_list}
        if not device_list:
            return await self.__show_homes_select_form('no_devices')
        self._device_list_sorted = dict(sorted(
            device_list.items(), key=lambda item:
                item[1].get('home_id', '')+item[1].get('room_id', '')))

        if user_input.get('devices_filter', False):
            return await self.async_step_devices_filter()
        return await self.update_devices_done_async()

    async def __show_homes_select_form(self, reason: str):
        devices_local_count: str = str(len(self._cc_devices_local))
        return self.async_show_form(
            step_id='homes_select',
            data_schema=vol.Schema({
                vol.Required(
                    'home_infos',
                    default=self._home_selected_list  # type: ignore
                ): cv.multi_select(self._cc_home_list_show),
                vol.Required(
                    'devices_filter', default=False  # type: ignore
                ): bool,
                vol.Required(
                    'ctrl_mode', default=self._ctrl_mode  # type: ignore
                ): vol.In(self._miot_i18n.translate(key='config.control_mode')),
            }),
            errors={'base': reason},
            description_placeholders={
                'local_count': devices_local_count
            },
            last_step=False
        )

    async def async_step_devices_filter(
        self, user_input: Optional[dict] = None
    ):
        if user_input:
            # Room filter
            include_items: dict = {}
            exclude_items: dict = {}
            room_list_in: list = user_input.get('room_list', [])
            room_filter_mode: str = user_input.get(
                'room_filter_mode', 'exclude')
            if room_list_in:
                if room_filter_mode == 'include':
                    include_items['room_id'] = room_list_in
                else:
                    exclude_items['room_id'] = room_list_in
            # Connect Type filter
            type_list_in: list = user_input.get('type_list', [])
            type_filter_mode: str = user_input.get(
                'type_filter_mode', 'exclude')
            if type_list_in:
                if type_filter_mode == 'include':
                    include_items['connect_type'] = type_list_in
                else:
                    exclude_items['connect_type'] = type_list_in
            # Model filter
            model_list_in: list = user_input.get('model_list', [])
            model_filter_mode: str = user_input.get(
                'model_filter_mode', 'exclude')
            if model_list_in:
                if model_filter_mode == 'include':
                    include_items['model'] = model_list_in
                else:
                    exclude_items['model'] = model_list_in
            # Device filter
            device_list_in: list = user_input.get('device_list', [])
            device_filter_mode: str = user_input.get(
                'devices_filter_mode', 'exclude')
            if device_list_in:
                if device_filter_mode == 'include':
                    include_items['did'] = device_list_in
                else:
                    exclude_items['did'] = device_list_in
            statistics_logic: str = user_input.get('statistics_logic', 'or')
            device_filter_list = _handle_devices_filter(
                devices=self._device_list_sorted,
                logic_or=(statistics_logic == 'or'),
                item_in=include_items, item_ex=exclude_items)
            if not device_filter_list:
                return await self.__show_devices_filter_form(
                    reason='no_filter_devices')
            self._device_list_sorted = dict(sorted(
                device_filter_list.items(), key=lambda item:
                    item[1].get('home_id', '')+item[1].get('room_id', '')))
            self._devices_filter = {
                'room_list': {
                    'items': room_list_in, 'mode': room_filter_mode},
                'type_list': {
                    'items': type_list_in, 'mode': type_filter_mode},
                'model_list': {
                    'items': model_list_in, 'mode': model_filter_mode},
                'device_list': {
                    'items': device_list_in, 'mode': device_filter_mode},
                'statistics_logic': statistics_logic}
            return await self.update_devices_done_async()
        return await self.__show_devices_filter_form(reason='')

    async def __show_devices_filter_form(self, reason: str):
        tip_devices: str = self._miot_i18n.translate(
            key='config.other.devices')  # type: ignore
        tip_without_room: str = self._miot_i18n.translate(
            key='config.other.without_room')  # type: ignore
        trans_statistics_logic: dict = self._miot_i18n.translate(
            key='config.statistics_logic')  # type: ignore
        trans_filter_mode: dict = self._miot_i18n.translate(
            key='config.filter_mode')  # type: ignore
        trans_connect_type: dict = self._miot_i18n.translate(
            key='config.connect_type')  # type: ignore

        room_device_count: dict = {}
        model_device_count: dict = {}
        connect_type_count: dict = {}
        device_list: dict = {}
        for did, info in self._device_list_sorted.items():
            device_list[did] = (
                f'[ {info["home_name"]} {info["room_name"]} ] ' +
                f'{info["name"]}, {did}')
            room_device_count.setdefault(info['room_id'], 0)
            room_device_count[info['room_id']] += 1
            model_device_count.setdefault(info['model'], 0)
            model_device_count[info['model']] += 1
            connect_type_count.setdefault(str(info['connect_type']), 0)
            connect_type_count[str(info['connect_type'])] += 1
        model_list: dict = {}
        for model, count in model_device_count.items():
            model_list[model] = f'{model} [ {count} {tip_devices} ]'
        type_list: dict = {
            k: f'{trans_connect_type.get(k, f"Connect Type ({k})")} '
            f'[ {v} {tip_devices} ]'
            for k, v in connect_type_count.items()}
        room_list: dict = {}
        for home_id, home_info in self._home_selected.items():
            for room_id, room_name in home_info['room_info'].items():
                if room_id not in room_device_count:
                    continue
                room_list[room_id] = (
                    f'{home_info["home_name"]} {room_name}'
                    f' [ {room_device_count[room_id]}{tip_devices} ]')
            if home_id in room_device_count:
                room_list[home_id] = (
                    f'{home_info["home_name"]} {tip_without_room}'
                    f' [ {room_device_count[home_id]}{tip_devices} ]')
        return self.async_show_form(
            step_id='devices_filter',
            data_schema=vol.Schema({
                vol.Required(
                    'room_filter_mode', default=self._devices_filter.get(
                        'room_list', {}).get('mode', 'exclude')  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('room_list', default=[
                    room_id for room_id in self._devices_filter.get(
                        'room_list', {}).get('items', [])
                    if room_id in room_list]  # type: ignore
                ): cv.multi_select(room_list),
                vol.Required(
                    'type_filter_mode', default=self._devices_filter.get(
                        'type_list', {}).get('mode', 'exclude')  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('type_list', default=[
                    type_ for type_ in self._devices_filter.get(
                        'type_list', {}).get('items', [])
                    if type_ in type_list]  # type: ignore
                ): cv.multi_select(type_list),
                vol.Required(
                    'model_filter_mode',
                    default=self._devices_filter.get('model_list', {}).get(
                        'mode', 'exclude')  # type: ignore
                ): vol.In(trans_filter_mode),
                vol.Optional('model_list', default=[
                    model for model in self._devices_filter.get(
                        'model_list', {}).get('items', [])
                    if model in model_list]  # type: ignore
                ): cv.multi_select(dict(sorted(
                    model_list.items(), key=lambda item: item[0]))),
                vol.Required(
                    'devices_filter_mode', default=self._devices_filter.get(
                        'device_list', {}).get(
                            'mode', 'exclude')  # type: ignore
                ):  vol.In(trans_filter_mode),
                vol.Optional('device_list', default=[
                    did for did in self._devices_filter.get(
                        'device_list', {}).get('items', [])
                    if did in device_list]  # type: ignore
                ): cv.multi_select(dict(sorted(
                    device_list.items(), key=lambda device: device[1]))),
                vol.Required(
                    'statistics_logic', default=self._devices_filter.get(
                        'statistics_logic', 'or')
                ): vol.In(trans_statistics_logic),
            }),
            errors={'base': reason},
            last_step=False
        )

    async def update_devices_done_async(self):
        # Statistics devices changed
        self._devices_add = []
        self._devices_remove = []

        self._devices_add = [
            did for did in list(self._device_list_sorted.keys())
            if did not in self._cc_devices_local]
        self._devices_remove = [
            did for did in self._cc_devices_local.keys()
            if did not in self._device_list_sorted]
        _LOGGER.debug(
            'devices update, add->%s, remove->%s',
            self._devices_add, self._devices_remove)
        return await self.async_step_update_trans_rules()

    async def async_step_update_trans_rules(self, user_input=None):
        if not self._update_trans_rules:
            return await self.async_step_update_lan_ctrl_config()
        urn_list: list[str] = list({
            info['urn']
            for info in list(self._miot_client.device_list.values())
            if 'urn' in info})
        self._trans_rules_count = len(urn_list)
        if not user_input:
            return self.async_show_form(
                step_id='update_trans_rules',
                data_schema=vol.Schema({
                    vol.Required(
                        'confirm', default=False  # type: ignore
                    ): bool
                }),
                description_placeholders={
                    'urn_count': str(self._trans_rules_count),
                },
                last_step=False
            )
        if user_input.get('confirm', False):
            # Update trans rules
            if urn_list:
                spec_parser: MIoTSpecParser = MIoTSpecParser(
                    lang=self._lang_new, storage=self._miot_storage)
                await spec_parser.init_async()
                self._trans_rules_count_success = (
                    await spec_parser.refresh_async(urn_list=urn_list))
                await spec_parser.deinit_async()
        else:
            # SKIP update trans rules
            self._update_trans_rules = False

        return await self.async_step_update_lan_ctrl_config()

    async def async_step_update_lan_ctrl_config(self, user_input=None):
        if not self._opt_lan_ctrl_cfg:
            return await self.async_step_network_detect_config()
        if not user_input:
            notice_net_dup: str = ''
            lan_ctrl_config = await self._miot_storage.load_user_config_async(
                'global_config', 'all', ['net_interfaces', 'enable_subscribe'])
            selected_if = lan_ctrl_config.get('net_interfaces', [])
            enable_subscribe = lan_ctrl_config.get('enable_subscribe', False)
            net_unavailable = self._miot_i18n.translate(
                key='config.lan_ctrl_config.net_unavailable')
            net_if = {
                if_name: f'{if_name}: {net_unavailable}'
                for if_name in selected_if}
            net_info = await self._miot_network.get_network_info_async()
            net_segs = set()
            for if_name, info in net_info.items():
                net_if[if_name] = (
                    f'{if_name} ({info.ip}/{info.netmask})')
                net_segs.add(info.net_seg)
            if len(net_segs) != len(net_info):
                notice_net_dup: str = self._miot_i18n.translate(
                    key='config.lan_ctrl_config.notice_net_dup')  # type: ignore
            return self.async_show_form(
                step_id='update_lan_ctrl_config',
                data_schema=vol.Schema({
                    vol.Required(
                        'net_interfaces', default=selected_if
                    ): cv.multi_select(net_if),
                    vol.Required(
                        'enable_subscribe', default=enable_subscribe): bool
                }),
                description_placeholders={
                    'notice_net_dup': notice_net_dup,
                },
                last_step=False
            )

        selected_if_new: list = user_input.get('net_interfaces', [])
        enable_subscribe_new: bool = user_input.get('enable_subscribe', False)
        lan_ctrl_config = await self._miot_storage.load_user_config_async(
            'global_config', 'all', ['net_interfaces', 'enable_subscribe'])
        selected_if = lan_ctrl_config.get('net_interfaces', [])
        enable_subscribe = lan_ctrl_config.get('enable_subscribe', False)
        if (
            set(selected_if_new) != set(selected_if)
            or enable_subscribe_new != enable_subscribe
        ):
            if not await self._miot_storage.update_user_config_async(
                    'global_config', 'all', {
                        'net_interfaces': selected_if_new,
                        'enable_subscribe': enable_subscribe_new}
            ):
                raise AbortFlow(
                    reason='storage_error',
                    description_placeholders={
                        'error': 'Update net config error'})
            await self._miot_lan.update_net_ifs_async(net_ifs=selected_if_new)
            await self._miot_lan.update_subscribe_option(
                enable_subscribe=enable_subscribe_new)

        return await self.async_step_network_detect_config()

    async def async_step_network_detect_config(
        self, user_input: Optional[dict] = None
    ):
        if not self._opt_network_detect_cfg:
            return await self.async_step_config_confirm()
        if not user_input:
            return await self.__show_network_detect_config_form(reason='')
        self._cc_network_detect_addr = user_input.get(
            'network_detect_addr', self._cc_network_detect_addr)

        ip_list, url_list, invalid_list = _handle_network_detect_addr(
            addr_str=self._cc_network_detect_addr)
        if invalid_list:
            return await self.__show_network_detect_config_form(
                reason='invalid_network_addr')
        if ip_list or url_list:
            if ip_list and not await self._miot_network.ping_multi_async(
                    ip_list=ip_list):
                return await self.__show_network_detect_config_form(
                    reason='invalid_ip_addr')
            if url_list and not await self._miot_network.http_multi_async(
                    url_list=url_list):
                return await self.__show_network_detect_config_form(
                    reason='invalid_http_addr')
        else:
            if not await self._miot_network.get_network_status_async():
                return await self.__show_network_detect_config_form(
                    reason='invalid_default_addr')
        network_detect_addr: dict = {'ip': ip_list, 'url': url_list}
        # Save
        if await self._miot_storage.update_user_config_async(
            uid='global_config', cloud_server='all', config={
                'network_detect_addr': network_detect_addr}):
            _LOGGER.info(
                'update network_detect_addr, %s', network_detect_addr)
        await self._miot_network.update_addr_list_async(
            ip_addr_list=ip_list, url_addr_list=url_list)
        # Check network deps
        self._opt_check_network_deps = user_input.get(
            'check_network_deps', False)
        if self._opt_check_network_deps:
            # OAuth2
            if not await self._miot_network.http_multi_async(
                    url_list=[OAUTH2_AUTH_URL]):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_oauth2_host')
            # HTTP API
            http_host = (
                DEFAULT_OAUTH2_API_HOST
                if self._cloud_server == DEFAULT_CLOUD_SERVER
                else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')
            if not await self._miot_network.http_multi_async(
                    url_list=[
                        f'https://{http_host}/app/v2/ha/oauth/get_token']):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_http_host')
            # SPEC API
            if not await self._miot_network.http_multi_async(
                    url_list=[
                        'https://miot-spec.org/miot-spec-v2/template/list/'
                        'device']):
                return await self.__show_network_detect_config_form(
                    reason='unreachable_spec_host')
            # MQTT Broker
            # pylint: disable=import-outside-toplevel
            try:
                from paho.mqtt import client
                mqtt_client = client.Client(
                    client_id=f'ha.{self._uid}',
                    protocol=client.MQTTv5)  # type: ignore
                if mqtt_client.connect(
                        host=f'{self._cloud_server}-ha.mqtt.io.mi.com',
                        port=8883) != 0:
                    raise RuntimeError('mqtt connect error')
                mqtt_client.disconnect()
            except Exception as err:  # pylint: disable=broad-exception-caught
                _LOGGER.error('try connect mqtt broker error, %s', err)
                return await self.__show_network_detect_config_form(
                    reason='unreachable_mqtt_broker')

        return await self.async_step_config_confirm()

    async def __show_network_detect_config_form(self, reason: str):
        if not self._cc_network_detect_addr:
            addr_list: dict = (await self._miot_storage.load_user_config_async(
                'global_config', 'all', ['network_detect_addr'])).get(
                    'network_detect_addr', {})
            self._cc_network_detect_addr = ','.join(
                addr_list.get('ip', [])+addr_list.get('url', []))
        return self.async_show_form(
            step_id='network_detect_config',
            data_schema=vol.Schema({
                vol.Optional(
                    'network_detect_addr',
                    default=self._cc_network_detect_addr  # type: ignore
                ): str,
                vol.Optional(
                    'check_network_deps',
                    default=self._opt_check_network_deps  # type: ignore
                ): bool,
            }),
            errors={'base': reason},
            description_placeholders={
                'cloud_server': self._cloud_server,
                'http_host': (
                    DEFAULT_OAUTH2_API_HOST
                    if self._cloud_server == DEFAULT_CLOUD_SERVER
                    else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')},
            last_step=False
        )

    async def async_step_config_confirm(self, user_input=None):
        if not user_input or not user_input.get('confirm', False):
            enable_text = self._miot_i18n.translate(
                key='config.option_status.enable')
            disable_text = self._miot_i18n.translate(
                key='config.option_status.disable')
            trans_devs_display: dict = self._miot_i18n.translate(
                key='config.device_state')  # type: ignore
            return self.async_show_form(
                step_id='config_confirm',
                data_schema=vol.Schema({
                    vol.Required(
                        'confirm', default=False): bool  # type: ignore
                }),
                description_placeholders={
                    'nick_name': self._nick_name,
                    'lang_new': INTEGRATION_LANGUAGES[self._lang_new],
                    'nick_name_new': self._nick_name_new,
                    'devices_add': len(self._devices_add),
                    'devices_remove': len(self._devices_remove),
                    'trans_rules_count': self._trans_rules_count,
                    'trans_rules_count_success':
                        self._trans_rules_count_success,
                    'action_debug': (
                        enable_text if self._action_debug_new
                        else disable_text),
                    'hide_non_standard_entities': (
                        enable_text if self._hide_non_standard_entities_new
                        else disable_text),
                    'display_devices_changed_notify': (' '.join(
                        trans_devs_display[key]
                        for key in self._display_devs_notify
                        if key in trans_devs_display)
                        if self._display_devs_notify
                        else self._miot_i18n.translate(
                            key='config.other.no_display'))
                },  # type: ignore
                errors={'base': 'not_confirm'} if user_input else {},
                last_step=True
            )

        if self._lang_new != self._integration_language:
            self._entry_data['integration_language'] = self._lang_new
            self._need_reload = True
        if self._update_user_info:
            self._entry_data['nick_name'] = self._nick_name_new
        if self._update_devices:
            self._entry_data['ctrl_mode'] = self._ctrl_mode
            self._entry_data['home_selected'] = self._home_selected
            self._entry_data['devices_filter'] = self._devices_filter
            if not await self._miot_storage.save_async(
                    domain='miot_devices',
                    name=f'{self._uid}_{self._cloud_server}',
                    data=self._device_list_sorted):
                _LOGGER.error(
                    'save devices async failed, %s, %s',
                    self._uid, self._cloud_server)
                raise AbortFlow(
                    reason='storage_error', description_placeholders={
                        'error': 'save user devices error'})
            self._need_reload = True
        if self._update_trans_rules:
            self._need_reload = True
        if self._action_debug_new != self._action_debug:
            self._entry_data['action_debug'] = self._action_debug_new
            self._need_reload = True
        if (
            self._hide_non_standard_entities_new !=
            self._hide_non_standard_entities
        ):
            self._entry_data['hide_non_standard_entities'] = (
                self._hide_non_standard_entities_new)
            self._need_reload = True
        if set(self._display_binary_mode) != set(self._display_binary_mode_new):
            self._entry_data['display_binary_mode'] = (
                self._display_binary_mode_new)
            self._need_reload = True
        # Update display_devices_changed_notify
        self._entry_data['display_devices_changed_notify'] = (
            self._display_devs_notify)
        self._miot_client.display_devices_changed_notify = (
            self._display_devs_notify)
        if (
                self._devices_remove
                and not await self._miot_storage.update_user_config_async(
                    uid=self._uid,
                    cloud_server=self._cloud_server,
                    config={'devices_remove': self._devices_remove})
        ):
            raise AbortFlow(
                reason='storage_error',
                description_placeholders={'error': 'Update user config error'})
        entry_title = (
            f'{self._nick_name_new or self._nick_name}: '
            f'{self._uid} [{CLOUD_SERVERS[self._cloud_server]}]')
        # Update entry config
        self.hass.config_entries.async_update_entry(
            self._config_entry, title=entry_title, data=self._entry_data)
        # Reload later
        if self._need_reload:
            self._main_loop.call_later(
                0, lambda: self._main_loop.create_task(
                    self.hass.config_entries.async_reload(
                        entry_id=self._config_entry.entry_id)))
        return self.async_create_entry(title='', data={})


async def _handle_oauth_webhook(hass, webhook_id, request):
    """Webhook to handle oauth2 callback."""
    # pylint: disable=inconsistent-quotes
    i18n: MIoTI18n = hass.data[DOMAIN][webhook_id].get('i18n', None)
    try:
        data = dict(request.query)
        if data.get('code', None) is None or data.get('state', None) is None:
            raise MIoTConfigError(
                'invalid oauth code or state',
                MIoTErrorCode.CODE_CONFIG_INVALID_INPUT)

        if data['state'] != hass.data[DOMAIN][webhook_id]['oauth_state']:
            raise MIoTConfigError(
                f'inconsistent state, '
                f'{hass.data[DOMAIN][webhook_id]["oauth_state"]}!='
                f'{data["state"]}', MIoTErrorCode.CODE_CONFIG_INVALID_STATE)

        fut_oauth_code: asyncio.Future = hass.data[DOMAIN][webhook_id].pop(
            'fut_oauth_code', None)
        fut_oauth_code.set_result(data['code'])
        _LOGGER.info('webhook code: %s', data['code'])

        success_trans: dict = {}
        if i18n:
            success_trans = i18n.translate(
                'oauth2.success') or {}  # type: ignore
        # Delete
        del hass.data[DOMAIN][webhook_id]['oauth_state']
        del hass.data[DOMAIN][webhook_id]['i18n']
        return web.Response(
            body=await oauth_redirect_page(
                title=success_trans.get('title', 'Success'),
                content=success_trans.get(
                    'content', (
                        'Please close this page and return to the account '
                        'authentication page to click NEXT')),
                button=success_trans.get('button', 'Close Page'),
                success=True,
            ), content_type='text/html')

    except Exception as err:  # pylint: disable=broad-exception-caught
        fail_trans: dict = {}
        err_msg: str = str(err)
        if i18n:
            if isinstance(err, MIoTConfigError):
                err_msg = i18n.translate(
                    f'oauth2.error_msg.{err.code.value}'
                ) or err.message  # type: ignore
            fail_trans = i18n.translate('oauth2.fail') or {}  # type: ignore
        return web.Response(
            body=await oauth_redirect_page(
                title=fail_trans.get('title', 'Authentication Failed'),
                content=str(fail_trans.get('content', (
                    '{error_msg}, Please close this page and return to the '
                    'account authentication page to click the authentication '
                    'link again.'))).replace('{error_msg}', err_msg),
                button=fail_trans.get('button', 'Close Page'),
                success=False),
            content_type='text/html')


def _handle_devices_filter(
    devices: dict, logic_or: bool, item_in: dict, item_ex: dict
) -> dict:
    """Private method to filter devices."""
    include_set: Set = set([])
    if not item_in:
        include_set = set(devices.keys())
    else:
        filter_item: list[set] = []
        for key, value in item_in.items():
            filter_item.append(set([
                did for did, info in devices.items()
                if str(info[key]) in value]))
        include_set = (
            set.union(*filter_item)
            if logic_or else set.intersection(*filter_item))
    if not include_set:
        return {}
    if item_ex:
        filter_item: list[set] = []
        for key, value in item_ex.items():
            filter_item.append(set([
                did for did, info in devices.items()
                if str(info[key]) in value]))
        exclude_set: Set = (
            set.union(*filter_item)
            if logic_or else set.intersection(*filter_item))
        if exclude_set:
            include_set = include_set-exclude_set
    if not include_set:
        return {}
    return {
        did: info for did, info in devices.items() if did in include_set}


def _handle_network_detect_addr(
    addr_str: str
) -> Tuple[list[str], list[str], list[str]]:
    ip_list: list[str] = []
    url_list: list[str] = []
    invalid_list: list[str] = []
    if addr_str:
        for addr in addr_str.split(','):
            addr = addr.strip()
            if not addr:
                continue
            # pylint: disable=broad-exception-caught
            try:
                ipaddress.ip_address(addr)
                ip_list.append(addr)
                continue
            except Exception:
                pass
            try:
                result = urlparse(addr)
                if (
                    result.netloc
                    and result.scheme.startswith('http')
                ):
                    url_list.append(addr)
                    continue
            except Exception:
                pass
            invalid_list.append(addr)
    return ip_list, url_list, invalid_list