You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			1281 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
		
		
			
		
	
	
			1281 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
| 
											11 months ago
										 | # -*- 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 json | ||
|  | import secrets | ||
|  | import traceback | ||
|  | from typing import Optional | ||
|  | 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 | ||
|  | import homeassistant.helpers.config_validation as cv | ||
|  | 
 | ||
|  | from .miot.const import ( | ||
|  |     DEFAULT_CLOUD_SERVER, | ||
|  |     DEFAULT_CTRL_MODE, | ||
|  |     DEFAULT_INTEGRATION_LANGUAGE, | ||
|  |     DEFAULT_NICK_NAME, | ||
|  |     DOMAIN, | ||
|  |     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, 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 | ||
|  |     VERSION = 1 | ||
|  |     MINOR_VERSION = 1 | ||
|  |     _main_loop: asyncio.AbstractEventLoop | ||
|  |     _mips_service: Optional[MipsService] | ||
|  |     _miot_storage: Optional[MIoTStorage] | ||
|  |     _miot_network: Optional[MIoTNetwork] | ||
|  |     _miot_i18n: Optional[MIoTI18n] | ||
|  | 
 | ||
|  |     _integration_language: Optional[str] | ||
|  |     _storage_path: Optional[str] | ||
|  |     _virtual_did: Optional[str] | ||
|  |     _uid: Optional[str] | ||
|  |     _uuid: Optional[str] | ||
|  |     _ctrl_mode: Optional[str] | ||
|  |     _area_name_rule: Optional[str] | ||
|  |     _action_debug: bool | ||
|  |     _hide_non_standard_entities: bool | ||
|  |     _auth_info: Optional[dict] | ||
|  |     _nick_name: Optional[str] | ||
|  |     _home_selected: Optional[dict] | ||
|  |     _home_info_buffer: Optional[dict[str, str | dict[str, dict]]] | ||
|  |     _home_list: Optional[dict] | ||
|  | 
 | ||
|  |     _cloud_server: Optional[str] | ||
|  |     _oauth_redirect_url: Optional[str] | ||
|  |     _miot_oauth: Optional[MIoTOauthClient] | ||
|  |     _miot_http: Optional[MIoTHttpClient] | ||
|  |     _user_cert_state: bool | ||
|  | 
 | ||
|  |     _oauth_auth_url: Optional[str] | ||
|  |     _task_oauth: Optional[asyncio.Task[None]] | ||
|  |     _config_error_reason: Optional[str] | ||
|  | 
 | ||
|  |     _fut_oauth_code: Optional[asyncio.Future] | ||
|  | 
 | ||
|  |     def __init__(self) -> None: | ||
|  |         self._main_loop = asyncio.get_running_loop() | ||
|  |         self._mips_service = None | ||
|  |         self._miot_storage = None | ||
|  |         self._miot_network = None | ||
|  |         self._miot_i18n = None | ||
|  | 
 | ||
|  |         self._integration_language = None | ||
|  |         self._storage_path = None | ||
|  |         self._virtual_did = None | ||
|  |         self._uid = None | ||
|  |         self._uuid = None   # MQTT client id | ||
|  |         self._ctrl_mode = None | ||
|  |         self._area_name_rule = None | ||
|  |         self._action_debug = False | ||
|  |         self._hide_non_standard_entities = False | ||
|  |         self._auth_info = None | ||
|  |         self._nick_name = None | ||
|  |         self._home_selected = {} | ||
|  |         self._home_info_buffer = None | ||
|  |         self._home_list = None | ||
|  | 
 | ||
|  |         self._cloud_server = None | ||
|  |         self._oauth_redirect_url = None | ||
|  |         self._miot_oauth = None | ||
|  |         self._miot_http = None | ||
|  |         self._user_cert_state = False | ||
|  | 
 | ||
|  |         self._oauth_auth_url = None | ||
|  |         self._task_oauth = None | ||
|  |         self._config_error_reason = None | ||
|  |         self._fut_oauth_code = None | ||
|  | 
 | ||
|  |     async def async_step_user(self, user_input=None): | ||
|  |         self.hass.data.setdefault(DOMAIN, {}) | ||
|  |         loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() | ||
|  | 
 | ||
|  |         if self._virtual_did is None: | ||
|  |             self._virtual_did = str(secrets.randbits(64)) | ||
|  |             self.hass.data[DOMAIN].setdefault(self._virtual_did, {}) | ||
|  |         if self._storage_path is None: | ||
|  |             self._storage_path = self.hass.config.path('.storage', DOMAIN) | ||
|  |         # MIoT network | ||
|  |         self._miot_network = self.hass.data[DOMAIN].get('miot_network', None) | ||
|  |         if self._miot_network is None: | ||
|  |             self._miot_network = MIoTNetwork(loop=loop) | ||
|  |             self.hass.data[DOMAIN]['miot_network'] = self._miot_network | ||
|  |             await self._miot_network.init_async( | ||
|  |                 refresh_interval=NETWORK_REFRESH_INTERVAL) | ||
|  |             _LOGGER.info('async_step_user, create miot network') | ||
|  |         # Mips server | ||
|  |         self._mips_service = self.hass.data[DOMAIN].get('mips_service', None) | ||
|  |         if self._mips_service is None: | ||
|  |             aiozc: HaAsyncZeroconf = await zeroconf.async_get_async_instance( | ||
|  |                 self.hass) | ||
|  |             self._mips_service = MipsService(aiozc=aiozc, loop=loop) | ||
|  |             self.hass.data[DOMAIN]['mips_service'] = self._mips_service | ||
|  |             await self._mips_service.init_async() | ||
|  |             _LOGGER.info('async_step_user, create mips service') | ||
|  |         # MIoT storage | ||
|  |         self._miot_storage = self.hass.data[DOMAIN].get('miot_storage', None) | ||
|  |         if self._miot_storage is None: | ||
|  |             self._miot_storage = MIoTStorage( | ||
|  |                 root_path=self._storage_path, loop=loop) | ||
|  |             self.hass.data[DOMAIN]['miot_storage'] = self._miot_storage | ||
|  |             _LOGGER.info( | ||
|  |                 'async_step_user, create miot storage, %s', self._storage_path) | ||
|  | 
 | ||
|  |         # Check network | ||
|  |         if not await self._miot_network.get_network_status_async(timeout=5): | ||
|  |             raise AbortFlow(reason='network_connect_error', | ||
|  |                             description_placeholders={}) | ||
|  | 
 | ||
|  |         return await self.async_step_eula(user_input) | ||
|  | 
 | ||
|  |     async def async_step_eula(self, user_input=None): | ||
|  |         if user_input: | ||
|  |             if user_input.get('eula', None) is True: | ||
|  |                 return await self.async_step_auth_config() | ||
|  |             return await self.__display_eula('eula_not_agree') | ||
|  |         return await self.__display_eula('') | ||
|  | 
 | ||
|  |     async def __display_eula(self, reason: str): | ||
|  |         return self.async_show_form( | ||
|  |             step_id='eula', | ||
|  |             data_schema=vol.Schema({ | ||
|  |                 vol.Required('eula', default=False): bool, | ||
|  |             }), | ||
|  |             last_step=False, | ||
|  |             errors={'base': reason}, | ||
|  |         ) | ||
|  | 
 | ||
|  |     async def async_step_auth_config(self, user_input=None): | ||
|  |         if user_input: | ||
|  |             self._cloud_server = user_input.get( | ||
|  |                 'cloud_server', DEFAULT_CLOUD_SERVER) | ||
|  |             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 = ( | ||
|  |                 f'{user_input.get("oauth_redirect_url")}{webhook_path}') | ||
|  |             return await self.async_step_oauth(user_input) | ||
|  |         # 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=DEFAULT_CLOUD_SERVER): vol.In(CLOUD_SERVERS), | ||
|  |                 vol.Required( | ||
|  |                     'integration_language', | ||
|  |                     default=default_language): vol.In(INTEGRATION_LANGUAGES), | ||
|  |                 vol.Required( | ||
|  |                     'oauth_redirect_url', | ||
|  |                     default=OAUTH_REDIRECT_URL): vol.In([OAUTH_REDIRECT_URL]), | ||
|  |             }), | ||
|  |             last_step=False, | ||
|  |         ) | ||
|  | 
 | ||
|  |     async def async_step_oauth(self, user_input=None): | ||
|  |         # 1: Init miot_oauth, generate auth url | ||
|  |         try: | ||
|  |             if self._miot_oauth is None: | ||
|  |                 _LOGGER.info( | ||
|  |                     'async_step_oauth, redirect_url: %s', | ||
|  |                     self._oauth_redirect_url) | ||
|  |                 miot_oauth = MIoTOauthClient( | ||
|  |                     client_id=OAUTH2_CLIENT_ID, | ||
|  |                     redirect_url=self._oauth_redirect_url, | ||
|  |                     cloud_server=self._cloud_server | ||
|  |                 ) | ||
|  |                 state = str(secrets.randbits(64)) | ||
|  |                 self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state | ||
|  |                 self._oauth_auth_url = miot_oauth.gen_auth_url( | ||
|  |                     redirect_url=self._oauth_redirect_url, state=state) | ||
|  |                 _LOGGER.info( | ||
|  |                     'async_step_oauth, oauth_url: %s', self._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._fut_oauth_code = self.hass.data[DOMAIN][ | ||
|  |                     self._virtual_did].get('fut_oauth_code', None) | ||
|  |                 if self._fut_oauth_code is None: | ||
|  |                     self._fut_oauth_code = self._main_loop.create_future() | ||
|  |                     self.hass.data[DOMAIN][self._virtual_did][ | ||
|  |                         'fut_oauth_code'] = self._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._task_oauth is None: | ||
|  |             self._task_oauth = self.hass.async_create_task( | ||
|  |                 self.__check_oauth_async()) | ||
|  |         if self._task_oauth.done(): | ||
|  |             if (error := self._task_oauth.exception()): | ||
|  |                 _LOGGER.error('task_oauth exception, %s', error) | ||
|  |                 self._config_error_reason = str(error) | ||
|  |                 return self.async_show_progress_done(next_step_id='oauth_error') | ||
|  |             return self.async_show_progress_done(next_step_id='devices_filter') | ||
|  |         return self.async_show_progress( | ||
|  |             step_id='oauth', | ||
|  |             progress_action='oauth', | ||
|  |             description_placeholders={ | ||
|  |                 'link_left': | ||
|  |                     f'<a href="{self._oauth_auth_url}" target="_blank">', | ||
|  |                 'link_right': '</a>' | ||
|  |             }, | ||
|  |             progress_task=self._task_oauth, | ||
|  |         ) | ||
|  | 
 | ||
|  |     async def __check_oauth_async(self) -> None: | ||
|  |         # TASK 1: Get oauth code | ||
|  |         oauth_code: Optional[str] = await self._fut_oauth_code | ||
|  | 
 | ||
|  |         # TASK 2: Get access_token and user_info from miot_oauth | ||
|  |         if not self._auth_info: | ||
|  |             try: | ||
|  |                 auth_info = await self._miot_oauth.get_access_token_async( | ||
|  |                     code=oauth_code) | ||
|  |                 self._miot_http = MIoTHttpClient( | ||
|  |                     cloud_server=self._cloud_server, | ||
|  |                     client_id=OAUTH2_CLIENT_ID, | ||
|  |                     access_token=auth_info['access_token']) | ||
|  |                 self._auth_info = auth_info | ||
|  |                 # Gen uuid | ||
|  |                 self._uuid = hashlib.sha256( | ||
|  |                     f'{self._virtual_did}.{auth_info["access_token"]}'.encode( | ||
|  |                         'utf-8') | ||
|  |                 ).hexdigest()[:32] | ||
|  |                 try: | ||
|  |                     self._nick_name = ( | ||
|  |                         await self._miot_http.get_user_info_async() or {} | ||
|  |                     ).get('miliaoNick', DEFAULT_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: | ||
|  |             self._home_info_buffer = ( | ||
|  |                 await self._miot_http.get_devices_async()) | ||
|  |             _LOGGER.info('get_homeinfos response: %s', self._home_info_buffer) | ||
|  |             self._uid = self._home_info_buffer['uid'] | ||
|  |             if self._uid == self._nick_name: | ||
|  |                 self._nick_name = DEFAULT_NICK_NAME | ||
|  |         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._home_info_buffer[ | ||
|  |                 '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._home_info_buffer['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._home_list = 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._user_cert_state: | ||
|  |                 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 await miot_cert.update_user_cert_async( | ||
|  |                                 cert=crt_str): | ||
|  |                             raise MIoTError('update_user_cert_async failed') | ||
|  |                         self._user_cert_state = 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) | ||
|  |         _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._config_error_reason is None: | ||
|  |             return await self.async_step_oauth() | ||
|  |         if self._config_error_reason.startswith('Flow aborted: '): | ||
|  |             raise AbortFlow( | ||
|  |                 reason=self._config_error_reason.replace('Flow aborted: ', '')) | ||
|  |         error_reason = self._config_error_reason | ||
|  |         self._config_error_reason = 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_devices_filter(self, user_input=None): | ||
|  |         _LOGGER.debug('async_step_devices_filter') | ||
|  |         try: | ||
|  |             if user_input is None: | ||
|  |                 return await self.display_device_filter_form('') | ||
|  | 
 | ||
|  |             home_selected: list = user_input.get('home_infos', []) | ||
|  |             if not home_selected: | ||
|  |                 return await self.display_device_filter_form( | ||
|  |                     'no_family_selected') | ||
|  |             self._ctrl_mode = user_input.get('ctrl_mode') | ||
|  |             for home_id, home_info in self._home_info_buffer[ | ||
|  |                     '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._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) | ||
|  |             # Storage device list | ||
|  |             devices_list: dict[str, dict] = { | ||
|  |                 did: dev_info | ||
|  |                 for did, dev_info in self._home_info_buffer['devices'].items() | ||
|  |                 if dev_info['home_id'] in home_selected} | ||
|  |             if not devices_list: | ||
|  |                 return await self.display_device_filter_form('no_devices') | ||
|  |             devices_list_sort = 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=devices_list_sort): | ||
|  |                 _LOGGER.error( | ||
|  |                     'save devices async failed, %s, %s', | ||
|  |                     self._uid, self._cloud_server) | ||
|  |                 return await self.display_device_filter_form( | ||
|  |                     'devices_storage_failed') | ||
|  |             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') | ||
|  |             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, | ||
|  |                     'ctrl_mode': self._ctrl_mode, | ||
|  |                     'home_selected': self._home_selected, | ||
|  |                     'area_name_rule': self._area_name_rule, | ||
|  |                     'action_debug': self._action_debug, | ||
|  |                     'hide_non_standard_entities': | ||
|  |                         self._hide_non_standard_entities, | ||
|  |                 }) | ||
|  |         except Exception as err: | ||
|  |             _LOGGER.error( | ||
|  |                 'async_step_devices_filter, %s, %s', | ||
|  |                 err, traceback.format_exc()) | ||
|  |             raise AbortFlow( | ||
|  |                 reason='config_flow_error', | ||
|  |                 description_placeholders={ | ||
|  |                     'error': f'config_flow error, {err}'} | ||
|  |             ) from err | ||
|  | 
 | ||
|  |     async def display_device_filter_form(self, reason: str): | ||
|  |         return self.async_show_form( | ||
|  |             step_id='devices_filter', | ||
|  |             data_schema=vol.Schema({ | ||
|  |                 vol.Required('ctrl_mode', default=DEFAULT_CTRL_MODE): vol.In( | ||
|  |                     self._miot_i18n.translate(key='config.control_mode')), | ||
|  |                 vol.Required('home_infos'): cv.multi_select(self._home_list), | ||
|  |                 vol.Required('area_name_rule', default='room'): vol.In( | ||
|  |                     self._miot_i18n.translate(key='config.room_name_rule')), | ||
|  |                 vol.Required('action_debug', default=self._action_debug): bool, | ||
|  |                 vol.Required( | ||
|  |                     'hide_non_standard_entities', | ||
|  |                     default=self._hide_non_standard_entities): bool, | ||
|  |             }), | ||
|  |             errors={'base': reason}, | ||
|  |             description_placeholders={ | ||
|  |                 'nick_name': self._nick_name, | ||
|  |             }, | ||
|  |             last_step=False, | ||
|  |         ) | ||
|  | 
 | ||
|  |     @ 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 | ||
|  |     _config_entry: config_entries.ConfigEntry | ||
|  |     _main_loop: asyncio.AbstractEventLoop | ||
|  |     _miot_client: Optional[MIoTClient] | ||
|  | 
 | ||
|  |     _miot_network: Optional[MIoTNetwork] | ||
|  |     _miot_storage: Optional[MIoTStorage] | ||
|  |     _mips_service: Optional[MipsService] | ||
|  |     _miot_oauth: Optional[MIoTOauthClient] | ||
|  |     _miot_http: Optional[MIoTHttpClient] | ||
|  |     _miot_i18n: Optional[MIoTI18n] | ||
|  |     _miot_lan: Optional[MIoTLan] | ||
|  | 
 | ||
|  |     _entry_data: dict | ||
|  |     _virtual_did: Optional[str] | ||
|  |     _uid: Optional[str] | ||
|  |     _storage_path: Optional[str] | ||
|  |     _cloud_server: Optional[str] | ||
|  |     _oauth_redirect_url: Optional[str] | ||
|  |     _integration_language: Optional[str] | ||
|  |     _ctrl_mode: Optional[str] | ||
|  |     _nick_name: Optional[str] | ||
|  |     _home_selected_list: Optional[list] | ||
|  |     _action_debug: bool | ||
|  |     _hide_non_standard_entities: bool | ||
|  | 
 | ||
|  |     _auth_info: Optional[dict] | ||
|  |     _home_selected_dict: Optional[dict] | ||
|  |     _home_info_buffer: Optional[dict[str, str | dict[str, dict]]] | ||
|  |     _home_list: Optional[dict] | ||
|  |     _device_list: Optional[dict[str, dict]] | ||
|  |     _devices_add: list[str] | ||
|  |     _devices_remove: list[str] | ||
|  | 
 | ||
|  |     _oauth_auth_url: Optional[str] | ||
|  |     _task_oauth: Optional[asyncio.Task[None]] | ||
|  |     _config_error_reason: Optional[str] | ||
|  |     _fut_oauth_code: Optional[asyncio.Future] | ||
|  |     # Config options | ||
|  |     _lang_new: Optional[str] | ||
|  |     _nick_name_new: Optional[str] | ||
|  |     _action_debug_new: bool | ||
|  |     _hide_non_standard_entities_new: bool | ||
|  |     _update_user_info: bool | ||
|  |     _update_devices: bool | ||
|  |     _update_trans_rules: bool | ||
|  |     _update_lan_ctrl_config: bool | ||
|  |     _trans_rules_count: int | ||
|  |     _trans_rules_count_success: int | ||
|  | 
 | ||
|  |     _need_reload: bool | ||
|  | 
 | ||
|  |     def __init__(self, config_entry: config_entries.ConfigEntry): | ||
|  |         self._config_entry = config_entry | ||
|  |         self._main_loop = None | ||
|  |         self._miot_client = None | ||
|  | 
 | ||
|  |         self._miot_network = None | ||
|  |         self._miot_storage = None | ||
|  |         self._mips_service = None | ||
|  |         self._miot_oauth = None | ||
|  |         self._miot_http = None | ||
|  |         self._miot_i18n = None | ||
|  |         self._miot_lan = None | ||
|  | 
 | ||
|  |         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._oauth_redirect_url = self._entry_data['oauth_redirect_url'] | ||
|  |         self._ctrl_mode = self._entry_data['ctrl_mode'] | ||
|  |         self._integration_language = self._entry_data['integration_language'] | ||
|  |         self._nick_name = self._entry_data['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._home_selected_list = list( | ||
|  |             self._entry_data['home_selected'].keys()) | ||
|  | 
 | ||
|  |         self._auth_info = None | ||
|  |         self._home_selected_dict = {} | ||
|  |         self._home_info_buffer = None | ||
|  |         self._home_list = None | ||
|  |         self._device_list = None | ||
|  |         self._devices_add = [] | ||
|  |         self._devices_remove = [] | ||
|  | 
 | ||
|  |         self._oauth_auth_url = None | ||
|  |         self._task_oauth = None | ||
|  |         self._config_error_reason = None | ||
|  |         self._fut_oauth_code = None | ||
|  | 
 | ||
|  |         self._lang_new = None | ||
|  |         self._nick_name_new = None | ||
|  |         self._action_debug_new = False | ||
|  |         self._hide_non_standard_entities_new = False | ||
|  |         self._update_user_info = False | ||
|  |         self._update_devices = False | ||
|  |         self._update_trans_rules = False | ||
|  |         self._update_lan_ctrl_config = False | ||
|  |         self._trans_rules_count = 0 | ||
|  |         self._trans_rules_count_success = 0 | ||
|  | 
 | ||
|  |         self._need_reload = False | ||
|  | 
 | ||
|  |         _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: | ||
|  |             # main loop | ||
|  |             self._main_loop = asyncio.get_running_loop() | ||
|  |             # MIoT client | ||
|  |             self._miot_client: MIoTClient = 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( | ||
|  |                         timeout=3): | ||
|  |                     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 = ( | ||
|  |                 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): 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._task_oauth is None: | ||
|  |                 state = str(secrets.randbits(64)) | ||
|  |                 self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state | ||
|  |                 self._miot_oauth.set_redirect_url( | ||
|  |                     redirect_url=self._oauth_redirect_url) | ||
|  |                 self._oauth_auth_url = self._miot_oauth.gen_auth_url( | ||
|  |                     redirect_url=self._oauth_redirect_url, state=state) | ||
|  |                 _LOGGER.info( | ||
|  |                     'async_step_oauth, oauth_url: %s', | ||
|  |                     self._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._fut_oauth_code = self.hass.data[DOMAIN][ | ||
|  |                     self._virtual_did].get('fut_oauth_code', None) | ||
|  |                 if self._fut_oauth_code is None: | ||
|  |                     self._fut_oauth_code = self._main_loop.create_future() | ||
|  |                     self.hass.data[DOMAIN][self._virtual_did][ | ||
|  |                         'fut_oauth_code'] = self._fut_oauth_code | ||
|  |                 self._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._task_oauth.done(): | ||
|  |                 if (error := self._task_oauth.exception()): | ||
|  |                     _LOGGER.error('task_oauth exception, %s', error) | ||
|  |                     self._config_error_reason = str(error) | ||
|  |                     self._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._config_error_reason = str(err) | ||
|  |             return self.async_show_progress_done(next_step_id='oauth_error') | ||
|  | 
 | ||
|  |         return self.async_show_progress( | ||
|  |             step_id='oauth', | ||
|  |             progress_action='oauth', | ||
|  |             description_placeholders={ | ||
|  |                 'link_left': | ||
|  |                     f'<a href="{self._oauth_auth_url}" target="_blank">', | ||
|  |                 'link_right': '</a>' | ||
|  |             }, | ||
|  |             progress_task=self._task_oauth, | ||
|  |         ) | ||
|  | 
 | ||
|  |     async def __check_oauth_async(self) -> None: | ||
|  |         # Get oauth code | ||
|  |         oauth_code: Optional[str] = await self._fut_oauth_code | ||
|  |         _LOGGER.debug('options flow __check_oauth_async, %s', oauth_code) | ||
|  |         # Get access_token and user_info from miot_oauth | ||
|  |         if self._auth_info is None: | ||
|  |             auth_info: dict = None | ||
|  |             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._config_error_reason is None: | ||
|  |             return await self.async_step_oauth() | ||
|  |         if self._config_error_reason.startswith('Flow aborted: '): | ||
|  |             raise AbortFlow( | ||
|  |                 reason=self._config_error_reason.replace('Flow aborted: ', '')) | ||
|  |         error_reason = self._config_error_reason | ||
|  |         self._config_error_reason = 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({ | ||
|  |                     vol.Required( | ||
|  |                         'integration_language', | ||
|  |                         default=self._integration_language | ||
|  |                     ): vol.In(INTEGRATION_LANGUAGES), | ||
|  |                     vol.Required( | ||
|  |                         'update_user_info', | ||
|  |                         default=self._update_user_info): bool, | ||
|  |                     vol.Required( | ||
|  |                         'update_devices', default=self._update_devices): bool, | ||
|  |                     vol.Required( | ||
|  |                         'action_debug', default=self._action_debug): bool, | ||
|  |                     vol.Required( | ||
|  |                         'hide_non_standard_entities', | ||
|  |                         default=self._hide_non_standard_entities): bool, | ||
|  |                     vol.Required( | ||
|  |                         'update_trans_rules', | ||
|  |                         default=self._update_trans_rules): bool, | ||
|  |                     vol.Required( | ||
|  |                         'update_lan_ctrl_config', | ||
|  |                         default=self._update_lan_ctrl_config): bool | ||
|  |                 }), | ||
|  |                 errors={}, | ||
|  |                 description_placeholders={ | ||
|  |                     'nick_name': self._nick_name, | ||
|  |                     'uid': self._uid, | ||
|  |                     'cloud_server': CLOUD_SERVERS[self._cloud_server] | ||
|  |                 }, | ||
|  |                 last_step=False, | ||
|  |             ) | ||
|  |         # Check network | ||
|  |         if not await self._miot_network.get_network_status_async(timeout=3): | ||
|  |             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._update_trans_rules = user_input.get( | ||
|  |             'update_trans_rules', self._update_trans_rules) | ||
|  |         self._update_lan_ctrl_config = user_input.get( | ||
|  |             'update_lan_ctrl_config', self._update_lan_ctrl_config) | ||
|  | 
 | ||
|  |         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_devices_filter() | ||
|  |         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_devices_filter() | ||
|  | 
 | ||
|  |     async def async_step_devices_filter(self, user_input=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._home_info_buffer = ( | ||
|  |                     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._home_info_buffer[ | ||
|  |                     '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._home_info_buffer['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._home_list = dict(sorted(home_list.items())) | ||
|  |             return await self.display_device_filter_form('') | ||
|  | 
 | ||
|  |         self._home_selected_list = user_input.get('home_infos', []) | ||
|  |         if not self._home_selected_list: | ||
|  |             return await self.display_device_filter_form('no_family_selected') | ||
|  |         self._ctrl_mode = user_input.get('ctrl_mode') | ||
|  |         self._home_selected_dict = {} | ||
|  |         for home_id, home_info in self._home_info_buffer[ | ||
|  |                 'homes']['home_list'].items(): | ||
|  |             if home_id in self._home_selected_list: | ||
|  |                 self._home_selected_dict[home_id] = home_info | ||
|  |         # Get device list | ||
|  |         self._device_list: dict[str, dict] = { | ||
|  |             did: dev_info | ||
|  |             for did, dev_info in self._home_info_buffer['devices'].items() | ||
|  |             if dev_info['home_id'] in self._home_selected_list} | ||
|  |         if not self._device_list: | ||
|  |             return await self.display_device_filter_form('no_devices') | ||
|  |         # Statistics devices changed | ||
|  |         self._devices_add = [] | ||
|  |         self._devices_remove = [] | ||
|  |         local_devices = await self._miot_storage.load_async( | ||
|  |             domain='miot_devices', | ||
|  |             name=f'{self._uid}_{self._cloud_server}', | ||
|  |             type_=dict) or {} | ||
|  | 
 | ||
|  |         self._devices_add = [ | ||
|  |             did for did in self._device_list.keys() if did not in local_devices] | ||
|  |         self._devices_remove = [ | ||
|  |             did for did in local_devices.keys() if did not in self._device_list] | ||
|  |         _LOGGER.debug( | ||
|  |             'devices update, add->%s, remove->%s', | ||
|  |             self._devices_add, self._devices_remove) | ||
|  |         return await self.async_step_update_trans_rules() | ||
|  | 
 | ||
|  |     async def display_device_filter_form(self, reason: str): | ||
|  |         return self.async_show_form( | ||
|  |             step_id='devices_filter', | ||
|  |             data_schema=vol.Schema({ | ||
|  |                 vol.Required( | ||
|  |                     'ctrl_mode', default=self._ctrl_mode | ||
|  |                 ): vol.In(self._miot_i18n.translate(key='config.control_mode')), | ||
|  |                 vol.Required( | ||
|  |                     'home_infos', | ||
|  |                     default=self._home_selected_list | ||
|  |                 ): cv.multi_select(self._home_list), | ||
|  |             }), | ||
|  |             errors={'base': reason}, | ||
|  |             description_placeholders={ | ||
|  |                 'nick_name': self._nick_name | ||
|  |             }, | ||
|  |             last_step=False | ||
|  |         ) | ||
|  | 
 | ||
|  |     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): bool | ||
|  |                 }), | ||
|  |                 description_placeholders={ | ||
|  |                     'urn_count': 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._update_lan_ctrl_config: | ||
|  |             return await self.async_step_config_confirm() | ||
|  |         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 = self._miot_i18n.translate( | ||
|  |                     key='config.lan_ctrl_config.notice_net_dup') | ||
|  |             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_config_confirm() | ||
|  | 
 | ||
|  |     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') | ||
|  |             return self.async_show_form( | ||
|  |                 step_id='config_confirm', | ||
|  |                 data_schema=vol.Schema({ | ||
|  |                     vol.Required('confirm', default=False): bool | ||
|  |                 }), | ||
|  |                 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), | ||
|  |                 }, | ||
|  |                 errors={'base': 'not_confirm'} if user_input else {}, | ||
|  |                 last_step=True | ||
|  |             ) | ||
|  | 
 | ||
|  |         self._entry_data['oauth_redirect_url'] = self._oauth_redirect_url | ||
|  |         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_dict | ||
|  |             devices_list_sort = dict(sorted( | ||
|  |                 self._device_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=devices_list_sort): | ||
|  |                 _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 ( | ||
|  |                 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): | ||
|  |     try: | ||
|  |         data = dict(request.query) | ||
|  |         if data.get('code', None) is None or data.get('state', None) is None: | ||
|  |             raise MIoTConfigError('invalid oauth code') | ||
|  | 
 | ||
|  |         if data['state'] != hass.data[DOMAIN][webhook_id]['oauth_state']: | ||
|  |             raise MIoTConfigError( | ||
|  |                 f'invalid oauth state, ' | ||
|  |                 f'{hass.data[DOMAIN][webhook_id]["oauth_state"]}, ' | ||
|  |                 f'{data["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']) | ||
|  | 
 | ||
|  |         return web.Response( | ||
|  |             body=oauth_redirect_page( | ||
|  |                 hass.config.language, 'success'), content_type='text/html') | ||
|  | 
 | ||
|  |     except MIoTConfigError: | ||
|  |         return web.Response( | ||
|  |             body=oauth_redirect_page(hass.config.language, 'fail'), | ||
|  |             content_type='text/html') |