|  |  |  | # -*- coding: utf-8 -*- | 
					
						
							|  |  |  | """MIoT client instance.""" | 
					
						
							|  |  |  | from copy import deepcopy | 
					
						
							|  |  |  | from typing import Callable, Optional, final | 
					
						
							|  |  |  | import asyncio | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import time | 
					
						
							|  |  |  | import traceback | 
					
						
							|  |  |  | from dataclasses import dataclass | 
					
						
							|  |  |  | from enum import Enum, auto | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from homeassistant.core import HomeAssistant | 
					
						
							|  |  |  | from homeassistant.components import zeroconf | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # pylint: disable=relative-beyond-top-level | 
					
						
							|  |  |  | from .common import MIoTMatcher | 
					
						
							|  |  |  | from .const import ( | 
					
						
							|  |  |  |     DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN, | 
					
						
							|  |  |  |     MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL, | 
					
						
							|  |  |  |     OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL) | 
					
						
							|  |  |  | from .miot_cloud import MIoTHttpClient, MIoTOauthClient | 
					
						
							|  |  |  | from .miot_error import MIoTClientError, MIoTErrorCode | 
					
						
							|  |  |  | from .miot_mips import ( | 
					
						
							|  |  |  |     MIoTDeviceState, MipsCloudClient, MipsDeviceState, | 
					
						
							|  |  |  |     MipsLocalClient) | 
					
						
							|  |  |  | from .miot_lan import MIoTLan | 
					
						
							|  |  |  | from .miot_network import MIoTNetwork | 
					
						
							|  |  |  | from .miot_storage import MIoTCert, MIoTStorage | 
					
						
							|  |  |  | from .miot_mdns import MipsService, MipsServiceState | 
					
						
							|  |  |  | from .miot_i18n import MIoTI18n | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _LOGGER = logging.getLogger(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass | 
					
						
							|  |  |  | class MIoTClientSub: | 
					
						
							|  |  |  |     """MIoT client subscription.""" | 
					
						
							|  |  |  |     topic: str = None | 
					
						
							|  |  |  |     handler: Callable[[dict, any], None] = None | 
					
						
							|  |  |  |     handler_ctx: any = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __str__(self) -> str: | 
					
						
							|  |  |  |         return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class CtrlMode(Enum): | 
					
						
							|  |  |  |     """MIoT client control mode.""" | 
					
						
							|  |  |  |     AUTO = 0 | 
					
						
							|  |  |  |     CLOUD = auto() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def load(mode: str) -> 'CtrlMode': | 
					
						
							|  |  |  |         if mode == 'auto': | 
					
						
							|  |  |  |             return CtrlMode.AUTO | 
					
						
							|  |  |  |         if mode == 'cloud': | 
					
						
							|  |  |  |             return CtrlMode.CLOUD | 
					
						
							|  |  |  |         raise MIoTClientError(f'unknown ctrl mode, {mode}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class MIoTClient: | 
					
						
							|  |  |  |     """MIoT client instance.""" | 
					
						
							|  |  |  |     # pylint: disable=unused-argument | 
					
						
							|  |  |  |     # pylint: disable=broad-exception-caught | 
					
						
							|  |  |  |     # pylint: disable=inconsistent-quotes | 
					
						
							|  |  |  |     _main_loop: asyncio.AbstractEventLoop | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _uid: str | 
					
						
							|  |  |  |     _entry_id: str | 
					
						
							|  |  |  |     _entry_data: dict | 
					
						
							|  |  |  |     _cloud_server: str | 
					
						
							|  |  |  |     _ctrl_mode: CtrlMode | 
					
						
							|  |  |  |     # MIoT network monitor | 
					
						
							|  |  |  |     _network: MIoTNetwork | 
					
						
							|  |  |  |     # MIoT storage client | 
					
						
							|  |  |  |     _storage: MIoTStorage | 
					
						
							|  |  |  |     # MIoT mips service | 
					
						
							|  |  |  |     _mips_service: MipsService | 
					
						
							|  |  |  |     # MIoT oauth client | 
					
						
							|  |  |  |     _oauth: MIoTOauthClient | 
					
						
							|  |  |  |     # MIoT http client | 
					
						
							|  |  |  |     _http: MIoTHttpClient | 
					
						
							|  |  |  |     # MIoT i18n client | 
					
						
							|  |  |  |     _i18n: MIoTI18n | 
					
						
							|  |  |  |     # MIoT cert client | 
					
						
							|  |  |  |     _cert: MIoTCert | 
					
						
							|  |  |  |     # User config, store in the .storage/xiaomi_home | 
					
						
							|  |  |  |     _user_config: dict | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Multi local mips client, key=group_id | 
					
						
							|  |  |  |     _mips_local: dict[str, MipsLocalClient] | 
					
						
							|  |  |  |     # Cloud mips client | 
					
						
							|  |  |  |     _mips_cloud: MipsCloudClient | 
					
						
							|  |  |  |     # MIoT lan client | 
					
						
							|  |  |  |     _miot_lan: MIoTLan | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Device list load from local storage, {did: <info>} | 
					
						
							|  |  |  |     _device_list_cache: dict[str, dict] | 
					
						
							|  |  |  |     # Device list obtained from cloud, {did: <info>} | 
					
						
							|  |  |  |     _device_list_cloud: dict[str, dict] | 
					
						
							|  |  |  |     # Device list obtained from gateway, {did: <info>} | 
					
						
							|  |  |  |     _device_list_gateway: dict[str, dict] | 
					
						
							|  |  |  |     # Device list scanned from LAN, {did: <info>} | 
					
						
							|  |  |  |     _device_list_lan: dict[str, dict] | 
					
						
							|  |  |  |     # Device list update timestamp | 
					
						
							|  |  |  |     _device_list_update_ts: int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _sub_source_list: dict[str] | 
					
						
							|  |  |  |     _sub_tree: MIoTMatcher | 
					
						
							|  |  |  |     _sub_device_state: dict[str, MipsDeviceState] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _mips_local_state_changed_timers: dict[str, asyncio.TimerHandle] | 
					
						
							|  |  |  |     _refresh_token_timer: Optional[asyncio.TimerHandle] | 
					
						
							|  |  |  |     _refresh_cert_timer: Optional[asyncio.TimerHandle] | 
					
						
							|  |  |  |     _refresh_cloud_devices_timer: Optional[asyncio.TimerHandle] | 
					
						
							|  |  |  |     # Refresh prop | 
					
						
							|  |  |  |     _refresh_props_list: dict[str, dict] | 
					
						
							|  |  |  |     _refresh_props_timer: Optional[asyncio.TimerHandle] | 
					
						
							|  |  |  |     _refresh_props_retry_count: int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Persistence notify handler, params: notify_id, title, message | 
					
						
							|  |  |  |     _persistence_notify: Callable[[str, str, str], None] | 
					
						
							|  |  |  |     # Device list changed notify | 
					
						
							|  |  |  |     _show_devices_changed_notify_timer: Optional[asyncio.TimerHandle] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__( | 
					
						
							|  |  |  |             self, | 
					
						
							|  |  |  |             entry_id: str, | 
					
						
							|  |  |  |             entry_data: dict, | 
					
						
							|  |  |  |             network: MIoTNetwork, | 
					
						
							|  |  |  |             storage: MIoTStorage, | 
					
						
							|  |  |  |             mips_service: MipsService, | 
					
						
							|  |  |  |             miot_lan: MIoTLan, | 
					
						
							|  |  |  |             loop: Optional[asyncio.AbstractEventLoop] = None) -> None: | 
					
						
							|  |  |  |         # MUST run in a running event loop | 
					
						
							|  |  |  |         self._main_loop = loop or asyncio.get_running_loop() | 
					
						
							|  |  |  |         # Check params | 
					
						
							|  |  |  |         if not isinstance(entry_data, dict): | 
					
						
							|  |  |  |             raise MIoTClientError('invalid entry data') | 
					
						
							|  |  |  |         if 'uid' not in entry_data or 'cloud_server' not in entry_data: | 
					
						
							|  |  |  |             raise MIoTClientError('invalid entry data content') | 
					
						
							|  |  |  |         if not isinstance(network, MIoTNetwork): | 
					
						
							|  |  |  |             raise MIoTClientError('invalid miot network') | 
					
						
							|  |  |  |         if not isinstance(storage, MIoTStorage): | 
					
						
							|  |  |  |             raise MIoTClientError('invalid miot storage') | 
					
						
							|  |  |  |         if not isinstance(mips_service, MipsService): | 
					
						
							|  |  |  |             raise MIoTClientError('invalid mips service') | 
					
						
							|  |  |  |         self._entry_id = entry_id | 
					
						
							|  |  |  |         self._entry_data = entry_data | 
					
						
							|  |  |  |         self._uid = entry_data['uid'] | 
					
						
							|  |  |  |         self._cloud_server = entry_data['cloud_server'] | 
					
						
							|  |  |  |         self._ctrl_mode = CtrlMode.load( | 
					
						
							|  |  |  |             entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)) | 
					
						
							|  |  |  |         self._network = network | 
					
						
							|  |  |  |         self._storage = storage | 
					
						
							|  |  |  |         self._mips_service = mips_service | 
					
						
							|  |  |  |         self._oauth = None | 
					
						
							|  |  |  |         self._http = None | 
					
						
							|  |  |  |         self._i18n = None | 
					
						
							|  |  |  |         self._cert = None | 
					
						
							|  |  |  |         self._user_config = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self._mips_local = {} | 
					
						
							|  |  |  |         self._mips_cloud = None | 
					
						
							|  |  |  |         self._miot_lan = miot_lan | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self._device_list_cache = {} | 
					
						
							|  |  |  |         self._device_list_cloud = {} | 
					
						
							|  |  |  |         self._device_list_gateway = {} | 
					
						
							|  |  |  |         self._device_list_lan = {} | 
					
						
							|  |  |  |         self._device_list_update_ts = 0 | 
					
						
							|  |  |  |         self._sub_source_list = {} | 
					
						
							|  |  |  |         self._sub_tree = MIoTMatcher() | 
					
						
							|  |  |  |         self._sub_device_state = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self._mips_local_state_changed_timers = {} | 
					
						
							|  |  |  |         self._refresh_token_timer = None | 
					
						
							|  |  |  |         self._refresh_cert_timer = None | 
					
						
							|  |  |  |         self._refresh_cloud_devices_timer = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Refresh prop | 
					
						
							|  |  |  |         self._refresh_props_list = {} | 
					
						
							|  |  |  |         self._refresh_props_timer = None | 
					
						
							|  |  |  |         self._refresh_props_retry_count = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self._persistence_notify = None | 
					
						
							|  |  |  |         self._show_devices_changed_notify_timer = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def init_async(self) -> None: | 
					
						
							|  |  |  |         # Load user config and check | 
					
						
							|  |  |  |         self._user_config = await self._storage.load_user_config_async( | 
					
						
							|  |  |  |             uid=self._uid, cloud_server=self._cloud_server) | 
					
						
							|  |  |  |         if not self._user_config: | 
					
						
							|  |  |  |             # Integration need to be add again | 
					
						
							|  |  |  |             raise MIoTClientError('load_user_config_async error') | 
					
						
							|  |  |  |         _LOGGER.debug('user config, %s', json.dumps(self._user_config)) | 
					
						
							|  |  |  |         # Load cache device list | 
					
						
							|  |  |  |         await self.__load_cache_device_async() | 
					
						
							|  |  |  |         # MIoT i18n client | 
					
						
							|  |  |  |         self._i18n = MIoTI18n( | 
					
						
							|  |  |  |             lang=self._entry_data.get( | 
					
						
							|  |  |  |                 'integration_language', DEFAULT_INTEGRATION_LANGUAGE), | 
					
						
							|  |  |  |             loop=self._main_loop) | 
					
						
							|  |  |  |         await self._i18n.init_async() | 
					
						
							|  |  |  |         # MIoT oauth client instance | 
					
						
							|  |  |  |         self._oauth = MIoTOauthClient( | 
					
						
							|  |  |  |             client_id=OAUTH2_CLIENT_ID, | 
					
						
							|  |  |  |             redirect_url=self._entry_data['oauth_redirect_url'], | 
					
						
							|  |  |  |             cloud_server=self._cloud_server, | 
					
						
							|  |  |  |             loop=self._main_loop) | 
					
						
							|  |  |  |         # MIoT http client instance | 
					
						
							|  |  |  |         self._http = MIoTHttpClient( | 
					
						
							|  |  |  |             cloud_server=self._cloud_server, | 
					
						
							|  |  |  |             client_id=OAUTH2_CLIENT_ID, | 
					
						
							|  |  |  |             access_token=self._user_config['auth_info']['access_token'], | 
					
						
							|  |  |  |             loop=self._main_loop) | 
					
						
							|  |  |  |         # MIoT cert client | 
					
						
							|  |  |  |         self._cert = MIoTCert( | 
					
						
							|  |  |  |             storage=self._storage, | 
					
						
							|  |  |  |             uid=self._uid, | 
					
						
							|  |  |  |             cloud_server=self.cloud_server) | 
					
						
							|  |  |  |         # MIoT cloud mips client | 
					
						
							|  |  |  |         self._mips_cloud = MipsCloudClient( | 
					
						
							|  |  |  |             uuid=self._entry_data['uuid'], | 
					
						
							|  |  |  |             cloud_server=self._cloud_server, | 
					
						
							|  |  |  |             app_id=OAUTH2_CLIENT_ID, | 
					
						
							|  |  |  |             token=self._user_config['auth_info']['access_token'], | 
					
						
							|  |  |  |             loop=self._main_loop) | 
					
						
							|  |  |  |         self._mips_cloud.enable_logger(logger=_LOGGER) | 
					
						
							|  |  |  |         self._mips_cloud.sub_mips_state( | 
					
						
							|  |  |  |             key=f'{self._uid}-{self._cloud_server}', | 
					
						
							|  |  |  |             handler=self.__on_mips_cloud_state_changed) | 
					
						
							|  |  |  |         # Subscribe network status | 
					
						
							|  |  |  |         self._network.sub_network_status( | 
					
						
							|  |  |  |             key=f'{self._uid}-{self._cloud_server}', | 
					
						
							|  |  |  |             handler=self.__on_network_status_changed) | 
					
						
							|  |  |  |         await self.__on_network_status_changed( | 
					
						
							|  |  |  |             status=self._network.network_status) | 
					
						
							|  |  |  |         # Create multi mips local client instance according to the | 
					
						
							|  |  |  |         # number of hub gateways | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             # Central hub gateway ctrl | 
					
						
							|  |  |  |             if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL: | 
					
						
							|  |  |  |                 for home_id, info in self._entry_data['home_selected'].items(): | 
					
						
							|  |  |  |                     # Create local mips service changed listener | 
					
						
							|  |  |  |                     self._mips_service.sub_service_change( | 
					
						
							|  |  |  |                         key=f'{self._uid}-{self._cloud_server}', | 
					
						
							|  |  |  |                         group_id=info['group_id'], | 
					
						
							|  |  |  |                         handler=self.__on_mips_service_state_change) | 
					
						
							|  |  |  |                     service_data = self._mips_service.get_services( | 
					
						
							|  |  |  |                         group_id=info['group_id']).get(info['group_id'], None) | 
					
						
							|  |  |  |                     if not service_data: | 
					
						
							|  |  |  |                         _LOGGER.info( | 
					
						
							|  |  |  |                             'central mips service not scanned, %s', home_id) | 
					
						
							|  |  |  |                         continue | 
					
						
							|  |  |  |                     _LOGGER.info( | 
					
						
							|  |  |  |                         'central mips service scanned, %s, %s', | 
					
						
							|  |  |  |                         home_id, service_data) | 
					
						
							|  |  |  |                     mips = MipsLocalClient( | 
					
						
							|  |  |  |                         did=self._entry_data['virtual_did'], | 
					
						
							|  |  |  |                         group_id=info['group_id'], | 
					
						
							|  |  |  |                         host=service_data['addresses'][0], | 
					
						
							|  |  |  |                         ca_file=self._cert.ca_file, | 
					
						
							|  |  |  |                         cert_file=self._cert.cert_file, | 
					
						
							|  |  |  |                         key_file=self._cert.key_file, | 
					
						
							|  |  |  |                         port=service_data['port'], | 
					
						
							|  |  |  |                         home_name=info['home_name'], | 
					
						
							|  |  |  |                         loop=self._main_loop) | 
					
						
							|  |  |  |                     self._mips_local[info['group_id']] = mips | 
					
						
							|  |  |  |                     mips.enable_logger(logger=_LOGGER) | 
					
						
							|  |  |  |                     mips.on_dev_list_changed = self.__on_gw_device_list_changed | 
					
						
							|  |  |  |                     mips.sub_mips_state( | 
					
						
							|  |  |  |                         key=info['group_id'], | 
					
						
							|  |  |  |                         handler=self.__on_mips_local_state_changed) | 
					
						
							|  |  |  |                     mips.connect() | 
					
						
							|  |  |  |             # Lan ctrl | 
					
						
							|  |  |  |             await self._miot_lan.vote_for_lan_ctrl_async( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}', vote=True) | 
					
						
							|  |  |  |             self._miot_lan.sub_lan_state( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}', | 
					
						
							|  |  |  |                 handler=self.__on_miot_lan_state_change) | 
					
						
							|  |  |  |             if self._miot_lan.init_done: | 
					
						
							|  |  |  |                 await self.__on_miot_lan_state_change(True) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self._miot_lan.unsub_lan_state( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |             if self._miot_lan.init_done: | 
					
						
							|  |  |  |                 self._miot_lan.unsub_device_state( | 
					
						
							|  |  |  |                     key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |                 self._miot_lan.delete_devices( | 
					
						
							|  |  |  |                     devices=list(self._device_list_cache.keys())) | 
					
						
							|  |  |  |             await self._miot_lan.vote_for_lan_ctrl_async( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}', vote=False) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         _LOGGER.info('init_async, %s, %s', self._uid, self._cloud_server) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def deinit_async(self) -> None: | 
					
						
							|  |  |  |         self._network.unsub_network_status( | 
					
						
							|  |  |  |             key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |         # Cancel refresh props | 
					
						
							|  |  |  |         if self._refresh_props_timer: | 
					
						
							|  |  |  |             self._refresh_props_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_props_timer = None | 
					
						
							|  |  |  |         self._refresh_props_list.clear() | 
					
						
							|  |  |  |         self._refresh_props_retry_count = 0 | 
					
						
							|  |  |  |         # Cloud mips | 
					
						
							|  |  |  |         self._mips_cloud.unsub_mips_state( | 
					
						
							|  |  |  |             key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |         self._mips_cloud.disconnect() | 
					
						
							|  |  |  |         # Cancel refresh cloud devices | 
					
						
							|  |  |  |         if self._refresh_cloud_devices_timer: | 
					
						
							|  |  |  |             self._refresh_cloud_devices_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_cloud_devices_timer = None | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             # Central hub gateway mips | 
					
						
							|  |  |  |             if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL: | 
					
						
							|  |  |  |                 self._mips_service.unsub_service_change( | 
					
						
							|  |  |  |                     key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |                 for mips in self._mips_local.values(): | 
					
						
							|  |  |  |                     mips.on_dev_list_changed = None | 
					
						
							|  |  |  |                     mips.unsub_mips_state(key=mips.group_id) | 
					
						
							|  |  |  |                     mips.disconnect() | 
					
						
							|  |  |  |                 if self._mips_local_state_changed_timers: | 
					
						
							|  |  |  |                     for timer_item in ( | 
					
						
							|  |  |  |                             self._mips_local_state_changed_timers.values()): | 
					
						
							|  |  |  |                         timer_item.cancel() | 
					
						
							|  |  |  |                     self._mips_local_state_changed_timers.clear() | 
					
						
							|  |  |  |             self._miot_lan.unsub_lan_state( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |             if self._miot_lan.init_done: | 
					
						
							|  |  |  |                 self._miot_lan.unsub_device_state( | 
					
						
							|  |  |  |                     key=f'{self._uid}-{self._cloud_server}') | 
					
						
							|  |  |  |                 self._miot_lan.delete_devices( | 
					
						
							|  |  |  |                     devices=list(self._device_list_cache.keys())) | 
					
						
							|  |  |  |             await self._miot_lan.vote_for_lan_ctrl_async( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}', vote=False) | 
					
						
							|  |  |  |         # Cancel refresh auth info | 
					
						
							|  |  |  |         if self._refresh_token_timer: | 
					
						
							|  |  |  |             self._refresh_token_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_token_timer = None | 
					
						
							|  |  |  |         if self._refresh_cert_timer: | 
					
						
							|  |  |  |             self._refresh_cert_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_cert_timer = None | 
					
						
							|  |  |  |         # Cancel device changed notify timer | 
					
						
							|  |  |  |         if self._show_devices_changed_notify_timer: | 
					
						
							|  |  |  |             self._show_devices_changed_notify_timer.cancel() | 
					
						
							|  |  |  |             self._show_devices_changed_notify_timer = None | 
					
						
							|  |  |  |         # Remove notify | 
					
						
							|  |  |  |         self._persistence_notify( | 
					
						
							|  |  |  |             self.__gen_notify_key('dev_list_changed'), None, None) | 
					
						
							|  |  |  |         self.__show_client_error_notify( | 
					
						
							|  |  |  |             message=None, notify_key='oauth_info') | 
					
						
							|  |  |  |         self.__show_client_error_notify( | 
					
						
							|  |  |  |             message=None, notify_key='user_cert') | 
					
						
							|  |  |  |         self.__show_client_error_notify( | 
					
						
							|  |  |  |             message=None, notify_key='device_cache') | 
					
						
							|  |  |  |         self.__show_client_error_notify( | 
					
						
							|  |  |  |             message=None, notify_key='device_cloud') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         _LOGGER.info('deinit_async, %s', self._uid) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def main_loop(self) -> asyncio.AbstractEventLoop: | 
					
						
							|  |  |  |         return self._main_loop | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_network(self) -> MIoTNetwork: | 
					
						
							|  |  |  |         return self._network | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_storage(self) -> MIoTStorage: | 
					
						
							|  |  |  |         return self._storage | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def mips_service(self) -> MipsService: | 
					
						
							|  |  |  |         return self._mips_service | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_oauth(self) -> MIoTOauthClient: | 
					
						
							|  |  |  |         return self._oauth | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_http(self) -> MIoTHttpClient: | 
					
						
							|  |  |  |         return self._http | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_i18n(self) -> MIoTI18n: | 
					
						
							|  |  |  |         return self._i18n | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def miot_lan(self) -> MIoTLan: | 
					
						
							|  |  |  |         return self._miot_lan | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def user_config(self) -> dict: | 
					
						
							|  |  |  |         return self._user_config | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def area_name_rule(self) -> Optional[str]: | 
					
						
							|  |  |  |         return self._entry_data.get('area_name_rule', None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def cloud_server(self) -> str: | 
					
						
							|  |  |  |         return self._cloud_server | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def action_debug(self) -> bool: | 
					
						
							|  |  |  |         return self._entry_data.get('action_debug', False) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def hide_non_standard_entities(self) -> bool: | 
					
						
							|  |  |  |         return self._entry_data.get( | 
					
						
							|  |  |  |             'hide_non_standard_entities', False) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def device_list(self) -> dict: | 
					
						
							|  |  |  |         return self._device_list_cache | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def persistent_notify(self) -> Callable: | 
					
						
							|  |  |  |         return self._persistence_notify | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @persistent_notify.setter | 
					
						
							|  |  |  |     def persistent_notify(self, func) -> None: | 
					
						
							|  |  |  |         self._persistence_notify = func | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def refresh_oauth_info_async(self) -> bool: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             # Load auth info | 
					
						
							|  |  |  |             auth_info: Optional[dict] = None | 
					
						
							|  |  |  |             user_config: dict = await self._storage.load_user_config_async( | 
					
						
							|  |  |  |                 uid=self._uid, cloud_server=self._cloud_server, | 
					
						
							|  |  |  |                 keys=['auth_info']) | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 not user_config | 
					
						
							|  |  |  |                 or (auth_info := user_config.get('auth_info', None)) is None | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 raise MIoTClientError('load_user_config_async error') | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 'expires_ts' not in auth_info | 
					
						
							|  |  |  |                 or 'access_token' not in auth_info | 
					
						
							|  |  |  |                 or 'refresh_token' not in auth_info | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 raise MIoTClientError('invalid auth info') | 
					
						
							|  |  |  |             # Determine whether to update token | 
					
						
							|  |  |  |             refresh_time = int(auth_info['expires_ts'] - time.time()) | 
					
						
							|  |  |  |             if refresh_time <= 60: | 
					
						
							|  |  |  |                 valid_auth_info = await self._oauth.refresh_access_token_async( | 
					
						
							|  |  |  |                     refresh_token=auth_info['refresh_token']) | 
					
						
							|  |  |  |                 auth_info = valid_auth_info | 
					
						
							|  |  |  |                 # Update http token | 
					
						
							|  |  |  |                 self._http.update_http_header( | 
					
						
							|  |  |  |                     access_token=valid_auth_info['access_token']) | 
					
						
							|  |  |  |                 # Update mips cloud token | 
					
						
							|  |  |  |                 self._mips_cloud.update_access_token( | 
					
						
							|  |  |  |                     access_token=valid_auth_info['access_token']) | 
					
						
							|  |  |  |                 # Update storage | 
					
						
							|  |  |  |                 if not await self._storage.update_user_config_async( | 
					
						
							|  |  |  |                         uid=self._uid, cloud_server=self._cloud_server, | 
					
						
							|  |  |  |                         config={'auth_info': auth_info}): | 
					
						
							|  |  |  |                     raise MIoTClientError('update_user_config_async error') | 
					
						
							|  |  |  |                 _LOGGER.info( | 
					
						
							|  |  |  |                     'refresh oauth info, get new access_token, %s', | 
					
						
							|  |  |  |                     auth_info) | 
					
						
							|  |  |  |                 refresh_time = int(auth_info['expires_ts'] - time.time()) | 
					
						
							|  |  |  |                 if refresh_time <= 0: | 
					
						
							|  |  |  |                     raise MIoTClientError('invalid expires time') | 
					
						
							|  |  |  |             self.__show_client_error_notify(None, 'oauth_info') | 
					
						
							|  |  |  |             self.__request_refresh_auth_info(refresh_time) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             _LOGGER.debug( | 
					
						
							|  |  |  |                 'refresh oauth info (%s, %s) after %ds', | 
					
						
							|  |  |  |                 self._uid, self._cloud_server, refresh_time) | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         except Exception as err: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=self._i18n.translate('miot.client.invalid_oauth_info'), | 
					
						
							|  |  |  |                 notify_key='oauth_info') | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'refresh oauth info error (%s, %s), %s, %s', | 
					
						
							|  |  |  |                 self._uid, self._cloud_server, err, traceback.format_exc()) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def refresh_user_cert_async(self) -> bool: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             if self._cloud_server not in SUPPORT_CENTRAL_GATEWAY_CTRL: | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             if not await self._cert.verify_ca_cert_async(): | 
					
						
							|  |  |  |                 raise MIoTClientError('ca cert is not ready') | 
					
						
							|  |  |  |             refresh_time = ( | 
					
						
							|  |  |  |                 await self._cert.user_cert_remaining_time_async() - | 
					
						
							|  |  |  |                 MIHOME_CERT_EXPIRE_MARGIN) | 
					
						
							|  |  |  |             if refresh_time <= 60: | 
					
						
							|  |  |  |                 user_key = await self._cert.load_user_key_async() | 
					
						
							|  |  |  |                 if not user_key: | 
					
						
							|  |  |  |                     user_key = self._cert.gen_user_key() | 
					
						
							|  |  |  |                     if not await self._cert.update_user_key_async(key=user_key): | 
					
						
							|  |  |  |                         raise MIoTClientError('update_user_key_async failed') | 
					
						
							|  |  |  |                 csr_str = self._cert.gen_user_csr( | 
					
						
							|  |  |  |                     user_key=user_key, did=self._entry_data['virtual_did']) | 
					
						
							|  |  |  |                 crt_str = await self.miot_http.get_central_cert_async(csr_str) | 
					
						
							|  |  |  |                 if not await self._cert.update_user_cert_async(cert=crt_str): | 
					
						
							|  |  |  |                     raise MIoTClientError('update user cert error') | 
					
						
							|  |  |  |                 _LOGGER.info('update_user_cert_async, %s', crt_str) | 
					
						
							|  |  |  |                 # Create cert update task | 
					
						
							|  |  |  |                 refresh_time = ( | 
					
						
							|  |  |  |                     await self._cert.user_cert_remaining_time_async() - | 
					
						
							|  |  |  |                     MIHOME_CERT_EXPIRE_MARGIN) | 
					
						
							|  |  |  |                 if refresh_time <= 0: | 
					
						
							|  |  |  |                     raise MIoTClientError('invalid refresh time') | 
					
						
							|  |  |  |             self.__show_client_error_notify(None, 'user_cert') | 
					
						
							|  |  |  |             self.__request_refresh_user_cert(refresh_time) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             _LOGGER.debug( | 
					
						
							|  |  |  |                 'refresh user cert (%s, %s) after %ds', | 
					
						
							|  |  |  |                 self._uid, self._cloud_server, refresh_time) | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         except MIoTClientError as error: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=self._i18n.translate('miot.client.invalid_cert_info'), | 
					
						
							|  |  |  |                 notify_key='user_cert') | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'refresh user cert error, %s, %s', | 
					
						
							|  |  |  |                 error, traceback.format_exc()) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def set_prop_async( | 
					
						
							|  |  |  |         self, did: str, siid: int, piid: int, value: any | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  |         # Priority local control | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             # Gateway control | 
					
						
							|  |  |  |             device_gw: dict = self._device_list_gateway.get(did, None) | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 device_gw and device_gw.get('online', False) | 
					
						
							|  |  |  |                 and device_gw.get('specv2_access', False) | 
					
						
							|  |  |  |                 and 'group_id' in device_gw | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 mips = self._mips_local.get(device_gw['group_id'], None) | 
					
						
							|  |  |  |                 if mips is None: | 
					
						
							|  |  |  |                     _LOGGER.error( | 
					
						
							|  |  |  |                         'no gw route, %s, try control throw cloud', | 
					
						
							|  |  |  |                         device_gw) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     result = await mips.set_prop_async( | 
					
						
							|  |  |  |                         did=did, siid=siid, piid=piid, value=value) | 
					
						
							|  |  |  |                     rc = (result or {}).get( | 
					
						
							|  |  |  |                         'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                     if rc in [0, 1]: | 
					
						
							|  |  |  |                         return True | 
					
						
							|  |  |  |                     raise MIoTClientError( | 
					
						
							|  |  |  |                         self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  |             # Lan control | 
					
						
							|  |  |  |             device_lan: dict = self._device_list_lan.get(did, None) | 
					
						
							|  |  |  |             if device_lan and device_lan.get('online', False): | 
					
						
							|  |  |  |                 result = await self._miot_lan.set_prop_async( | 
					
						
							|  |  |  |                     did=did, siid=siid, piid=piid, value=value) | 
					
						
							|  |  |  |                 _LOGGER.debug( | 
					
						
							|  |  |  |                     'lan set prop, %s, %s, %s -> %s', did, siid, piid, result) | 
					
						
							|  |  |  |                 rc = (result or {}).get( | 
					
						
							|  |  |  |                     'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                 if rc in [0, 1]: | 
					
						
							|  |  |  |                     return True | 
					
						
							|  |  |  |                 raise MIoTClientError( | 
					
						
							|  |  |  |                     self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Cloud control | 
					
						
							|  |  |  |         device_cloud = self._device_list_cloud.get(did, None) | 
					
						
							|  |  |  |         if device_cloud and device_cloud.get('online', False): | 
					
						
							|  |  |  |             result: list = await self._http.set_prop_async( | 
					
						
							|  |  |  |                 params=[ | 
					
						
							|  |  |  |                     {'did': did, 'siid': siid, 'piid': piid, 'value': value} | 
					
						
							|  |  |  |                 ]) | 
					
						
							|  |  |  |             _LOGGER.debug( | 
					
						
							|  |  |  |                 'set prop response, %s.%d.%d, %s, result, %s', | 
					
						
							|  |  |  |                 did, siid, piid, value, result) | 
					
						
							|  |  |  |             if result and len(result) == 1: | 
					
						
							|  |  |  |                 rc = result[0].get( | 
					
						
							|  |  |  |                     'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                 if rc in [0, 1]: | 
					
						
							|  |  |  |                     return True | 
					
						
							|  |  |  |                 if rc in [-704010000, -704042011]: | 
					
						
							|  |  |  |                     # Device remove or offline | 
					
						
							|  |  |  |                     _LOGGER.error('device may be removed or offline, %s', did) | 
					
						
							|  |  |  |                     self._main_loop.create_task( | 
					
						
							|  |  |  |                         await self.__refresh_cloud_device_with_dids_async( | 
					
						
							|  |  |  |                             dids=[did])) | 
					
						
							|  |  |  |                 raise MIoTClientError( | 
					
						
							|  |  |  |                     self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Show error message | 
					
						
							|  |  |  |         raise MIoTClientError( | 
					
						
							|  |  |  |             f'{self._i18n.translate("miot.client.device_exec_error")}, ' | 
					
						
							|  |  |  |             f'{self._i18n.translate("error.common.-10007")}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def request_refresh_prop( | 
					
						
							|  |  |  |         self, did: str, siid: int, piid: int | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  |         key: str = f'{did}|{siid}|{piid}' | 
					
						
							|  |  |  |         if key in self._refresh_props_list: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._refresh_props_list[key] = { | 
					
						
							|  |  |  |             'did': did, 'siid': siid, 'piid': piid} | 
					
						
							|  |  |  |         if self._refresh_props_timer: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._refresh_props_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             0.2, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                 self.__refresh_props_handler())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def get_prop_async(self, did: str, siid: int, piid: int) -> any: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # NOTICE: Since there are too many request attributes and obtaining | 
					
						
							|  |  |  |         # them directly from the hub or device will cause device abnormalities, | 
					
						
							|  |  |  |         # so obtaining the cache from the cloud is the priority here. | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             if self._network.network_status: | 
					
						
							|  |  |  |                 result = await self._http.get_prop_async( | 
					
						
							|  |  |  |                     did=did, siid=siid, piid=piid) | 
					
						
							|  |  |  |                 if result: | 
					
						
							|  |  |  |                     return result | 
					
						
							|  |  |  |         except Exception as err:  # pylint: disable=broad-exception-caught | 
					
						
							|  |  |  |             # Catch all exceptions | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'client get prop from cloud error, %s, %s', | 
					
						
							|  |  |  |                 err, traceback.format_exc()) | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             # Central hub gateway | 
					
						
							|  |  |  |             device_gw = self._device_list_gateway.get(did, None) | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 device_gw and device_gw.get('online', False) | 
					
						
							|  |  |  |                 and device_gw.get('specv2_access', False) | 
					
						
							|  |  |  |                 and 'group_id' in device_gw | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 mips = self._mips_local.get(device_gw['group_id'], None) | 
					
						
							|  |  |  |                 if mips is None: | 
					
						
							|  |  |  |                     _LOGGER.error('no gw route, %s', device_gw) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     return await mips.get_prop_async( | 
					
						
							|  |  |  |                         did=did, siid=siid, piid=piid) | 
					
						
							|  |  |  |             # Lan | 
					
						
							|  |  |  |             device_lan = self._device_list_lan.get(did, None) | 
					
						
							|  |  |  |             if device_lan and device_lan.get('online', False): | 
					
						
							|  |  |  |                 return await self._miot_lan.get_prop_async( | 
					
						
							|  |  |  |                     did=did, siid=siid, piid=piid) | 
					
						
							|  |  |  |         # _LOGGER.error( | 
					
						
							|  |  |  |         #     'client get prop failed, no-link, %s.%d.%d', did, siid, piid) | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def action_async( | 
					
						
							|  |  |  |         self, did: str, siid: int, aiid: int, in_list: list | 
					
						
							|  |  |  |     ) -> list: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         device_gw: dict = self._device_list_gateway.get(did, None) | 
					
						
							|  |  |  |         # Priority local control | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 device_gw and device_gw.get('online', False) | 
					
						
							|  |  |  |                 and device_gw.get('specv2_access', False) | 
					
						
							|  |  |  |                 and 'group_id' in device_gw | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 mips = self._mips_local.get( | 
					
						
							|  |  |  |                     device_gw['group_id'], None) | 
					
						
							|  |  |  |                 if mips is None: | 
					
						
							|  |  |  |                     _LOGGER.error('no gw route, %s', device_gw) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     result = await mips.action_async( | 
					
						
							|  |  |  |                         did=did, siid=siid, aiid=aiid, in_list=in_list) | 
					
						
							|  |  |  |                     rc = (result or {}).get( | 
					
						
							|  |  |  |                         'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                     if rc in [0, 1]: | 
					
						
							|  |  |  |                         return result.get('out', []) | 
					
						
							|  |  |  |                     raise MIoTClientError( | 
					
						
							|  |  |  |                         self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  |             # Lan control | 
					
						
							|  |  |  |             device_lan = self._device_list_lan.get(did, None) | 
					
						
							|  |  |  |             if device_lan and device_lan.get('online', False): | 
					
						
							|  |  |  |                 result = await self._miot_lan.action_async( | 
					
						
							|  |  |  |                     did=did, siid=siid, aiid=aiid, in_list=in_list) | 
					
						
							|  |  |  |                 _LOGGER.debug( | 
					
						
							|  |  |  |                     'lan action, %s, %s, %s -> %s', did, siid, aiid, result) | 
					
						
							|  |  |  |                 rc = (result or {}).get( | 
					
						
							|  |  |  |                     'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                 if rc in [0, 1]: | 
					
						
							|  |  |  |                     return result.get('out', []) | 
					
						
							|  |  |  |                 raise MIoTClientError( | 
					
						
							|  |  |  |                     self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  |         # Cloud control | 
					
						
							|  |  |  |         device_cloud = self._device_list_cloud.get(did, None) | 
					
						
							|  |  |  |         if device_cloud.get('online', False): | 
					
						
							|  |  |  |             result: dict = await self._http.action_async( | 
					
						
							|  |  |  |                 did=did, siid=siid, aiid=aiid, in_list=in_list) | 
					
						
							|  |  |  |             if result: | 
					
						
							|  |  |  |                 rc = result.get( | 
					
						
							|  |  |  |                     'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) | 
					
						
							|  |  |  |                 if rc in [0, 1]: | 
					
						
							|  |  |  |                     return result.get('out', []) | 
					
						
							|  |  |  |                 if rc in [-704010000, -704042011]: | 
					
						
							|  |  |  |                     # Device remove or offline | 
					
						
							|  |  |  |                     _LOGGER.error('device removed or offline, %s', did) | 
					
						
							|  |  |  |                     self._main_loop.create_task( | 
					
						
							|  |  |  |                         await self.__refresh_cloud_device_with_dids_async( | 
					
						
							|  |  |  |                             dids=[did])) | 
					
						
							|  |  |  |                 raise MIoTClientError( | 
					
						
							|  |  |  |                     self.__get_exec_error_with_rc(rc=rc)) | 
					
						
							|  |  |  |         # Show error message | 
					
						
							|  |  |  |         _LOGGER.error( | 
					
						
							|  |  |  |             'client action failed, %s.%d.%d', did, siid, aiid) | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sub_prop( | 
					
						
							|  |  |  |         self, did: str, handler: Callable[[dict, any], None], | 
					
						
							|  |  |  |         siid: int = None, piid: int = None, handler_ctx: any = None | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         topic = ( | 
					
						
							|  |  |  |             f'{did}/p/' | 
					
						
							|  |  |  |             f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') | 
					
						
							|  |  |  |         self._sub_tree[topic] = MIoTClientSub( | 
					
						
							|  |  |  |             topic=topic, handler=handler, handler_ctx=handler_ctx) | 
					
						
							|  |  |  |         _LOGGER.debug('client sub prop, %s', topic) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: | 
					
						
							|  |  |  |         topic = ( | 
					
						
							|  |  |  |             f'{did}/p/' | 
					
						
							|  |  |  |             f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') | 
					
						
							|  |  |  |         if self._sub_tree.get(topic=topic): | 
					
						
							|  |  |  |             del self._sub_tree[topic] | 
					
						
							|  |  |  |         _LOGGER.debug('client unsub prop, %s', topic) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sub_event( | 
					
						
							|  |  |  |         self, did: str, handler: Callable[[dict, any], None], | 
					
						
							|  |  |  |         siid: int = None, eiid: int = None, handler_ctx: any = None | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  |         topic = ( | 
					
						
							|  |  |  |             f'{did}/e/' | 
					
						
							|  |  |  |             f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') | 
					
						
							|  |  |  |         self._sub_tree[topic] = MIoTClientSub( | 
					
						
							|  |  |  |             topic=topic, handler=handler, handler_ctx=handler_ctx) | 
					
						
							|  |  |  |         _LOGGER.debug('client sub event, %s', topic) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: | 
					
						
							|  |  |  |         topic = ( | 
					
						
							|  |  |  |             f'{did}/e/' | 
					
						
							|  |  |  |             f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') | 
					
						
							|  |  |  |         if self._sub_tree.get(topic=topic): | 
					
						
							|  |  |  |             del self._sub_tree[topic] | 
					
						
							|  |  |  |         _LOGGER.debug('client unsub event, %s', topic) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sub_device_state( | 
					
						
							|  |  |  |         self, did: str, handler: Callable[[str, MIoTDeviceState, any], None], | 
					
						
							|  |  |  |         handler_ctx: any = None | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         """Call callback handler in main loop""" | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             raise MIoTClientError(f'did not exist, {did}') | 
					
						
							|  |  |  |         self._sub_device_state[did] = MipsDeviceState( | 
					
						
							|  |  |  |             did=did, handler=handler, handler_ctx=handler_ctx) | 
					
						
							|  |  |  |         _LOGGER.debug('client sub device state, %s', did) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def unsub_device_state(self, did: str) -> bool: | 
					
						
							|  |  |  |         self._sub_device_state.pop(did, None) | 
					
						
							|  |  |  |         _LOGGER.debug('client unsub device state, %s', did) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __get_exec_error_with_rc(self, rc: int) -> str: | 
					
						
							|  |  |  |         err_msg: str = self._i18n.translate(key=f'error.common.{rc}') | 
					
						
							|  |  |  |         if not err_msg: | 
					
						
							|  |  |  |             err_msg = f'{self._i18n.translate(key="error.common.-10000")}, ' | 
					
						
							|  |  |  |             err_msg += f'code={rc}' | 
					
						
							|  |  |  |         return ( | 
					
						
							|  |  |  |             f'{self._i18n.translate(key="miot.client.device_exec_error")}, ' | 
					
						
							|  |  |  |             + err_msg) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __gen_notify_key(self, name: str) -> str: | 
					
						
							|  |  |  |         return f'{DOMAIN}-{self._uid}-{self._cloud_server}-{name}' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __request_refresh_auth_info(self, delay_sec: int) -> None: | 
					
						
							|  |  |  |         if self._refresh_token_timer: | 
					
						
							|  |  |  |             self._refresh_token_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_token_timer = None | 
					
						
							|  |  |  |         self._refresh_token_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             delay_sec, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                 self.refresh_oauth_info_async())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __request_refresh_user_cert(self, delay_sec: int) -> None: | 
					
						
							|  |  |  |         if self._refresh_cert_timer: | 
					
						
							|  |  |  |             self._refresh_cert_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_cert_timer = None | 
					
						
							|  |  |  |         self._refresh_cert_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             delay_sec, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                 self.refresh_user_cert_async())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __update_device_msg_sub(self, did: str) -> None: | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         from_old: Optional[str] = self._sub_source_list.get(did, None) | 
					
						
							|  |  |  |         from_new: Optional[str] = None | 
					
						
							|  |  |  |         if self._ctrl_mode == CtrlMode.AUTO: | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 did in self._device_list_gateway | 
					
						
							|  |  |  |                 and self._device_list_gateway[did].get('online', False) | 
					
						
							|  |  |  |                 and self._device_list_gateway[did].get('push_available', False) | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 from_new = self._device_list_gateway[did]['group_id'] | 
					
						
							|  |  |  |             elif ( | 
					
						
							|  |  |  |                 did in self._device_list_lan | 
					
						
							|  |  |  |                 and self._device_list_lan[did].get('online', False) | 
					
						
							|  |  |  |                 and self._device_list_lan[did].get('push_available', False) | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 from_new = 'lan' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |             from_new is None | 
					
						
							|  |  |  |             and did in self._device_list_cloud | 
					
						
							|  |  |  |             and self._device_list_cloud[did].get('online', False) | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             from_new = 'cloud' | 
					
						
							|  |  |  |         if from_new == from_old: | 
					
						
							|  |  |  |             # No need to update | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         # Unsub old | 
					
						
							|  |  |  |         if from_old: | 
					
						
							|  |  |  |             if from_old == 'cloud': | 
					
						
							|  |  |  |                 self._mips_cloud.unsub_prop(did=did) | 
					
						
							|  |  |  |                 self._mips_cloud.unsub_event(did=did) | 
					
						
							|  |  |  |             elif from_old == 'lan': | 
					
						
							|  |  |  |                 self._miot_lan.unsub_prop(did=did) | 
					
						
							|  |  |  |                 self._miot_lan.unsub_event(did=did) | 
					
						
							|  |  |  |             elif from_old in self._mips_local: | 
					
						
							|  |  |  |                 mips = self._mips_local[from_old] | 
					
						
							|  |  |  |                 mips.unsub_prop(did=did) | 
					
						
							|  |  |  |                 mips.unsub_event(did=did) | 
					
						
							|  |  |  |         # Sub new | 
					
						
							|  |  |  |         if from_new == 'cloud': | 
					
						
							|  |  |  |             self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg) | 
					
						
							|  |  |  |             self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg) | 
					
						
							|  |  |  |         elif from_new == 'lan': | 
					
						
							|  |  |  |             self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg) | 
					
						
							|  |  |  |             self._miot_lan.sub_event(did=did, handler=self.__on_event_msg) | 
					
						
							|  |  |  |         elif from_new in self._mips_local: | 
					
						
							|  |  |  |             mips = self._mips_local[from_new] | 
					
						
							|  |  |  |             mips.sub_prop(did=did, handler=self.__on_prop_msg) | 
					
						
							|  |  |  |             mips.sub_event(did=did, handler=self.__on_event_msg) | 
					
						
							|  |  |  |         self._sub_source_list[did] = from_new | 
					
						
							|  |  |  |         _LOGGER.info( | 
					
						
							|  |  |  |             'device sub changed, %s, from %s to %s', did, from_old, from_new) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_network_status_changed(self, status: bool) -> None: | 
					
						
							|  |  |  |         _LOGGER.info('network status changed, %s', status) | 
					
						
							|  |  |  |         if status: | 
					
						
							|  |  |  |             # Check auth_info | 
					
						
							|  |  |  |             if await self.refresh_oauth_info_async(): | 
					
						
							|  |  |  |                 # Connect to mips cloud | 
					
						
							|  |  |  |                 self._mips_cloud.connect() | 
					
						
							|  |  |  |                 # Update device list | 
					
						
							|  |  |  |                 self.__request_refresh_cloud_devices() | 
					
						
							|  |  |  |             await self.refresh_user_cert_async() | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.__request_show_devices_changed_notify(delay_sec=30) | 
					
						
							|  |  |  |             # Cancel refresh cloud devices | 
					
						
							|  |  |  |             if self._refresh_cloud_devices_timer: | 
					
						
							|  |  |  |                 self._refresh_cloud_devices_timer.cancel() | 
					
						
							|  |  |  |                 self._refresh_cloud_devices_timer = None | 
					
						
							|  |  |  |             # Disconnect cloud mips | 
					
						
							|  |  |  |             self._mips_cloud.disconnect() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_mips_service_state_change( | 
					
						
							|  |  |  |         self, group_id: str, state: MipsServiceState, data: dict | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info( | 
					
						
							|  |  |  |             'mips service state changed, %s, %s, %s', group_id, state, data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         mips = self._mips_local.get(group_id, None) | 
					
						
							|  |  |  |         if mips: | 
					
						
							|  |  |  |             if state == MipsServiceState.REMOVED: | 
					
						
							|  |  |  |                 mips.disconnect() | 
					
						
							|  |  |  |                 self._mips_local.pop(group_id, None) | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 mips.client_id == self._entry_data['virtual_did'] | 
					
						
							|  |  |  |                 and mips.host == data['addresses'][0] | 
					
						
							|  |  |  |                 and mips.port == data['port'] | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             mips.disconnect() | 
					
						
							|  |  |  |             self._mips_local.pop(group_id, None) | 
					
						
							|  |  |  |         home_name: str = '' | 
					
						
							|  |  |  |         for info in list(self._entry_data['home_selected'].values()): | 
					
						
							|  |  |  |             if info.get('group_id', None) == group_id: | 
					
						
							|  |  |  |                 home_name = info.get('home_name', '') | 
					
						
							|  |  |  |         mips = MipsLocalClient( | 
					
						
							|  |  |  |             did=self._entry_data['virtual_did'], | 
					
						
							|  |  |  |             group_id=group_id, | 
					
						
							|  |  |  |             host=data['addresses'][0], | 
					
						
							|  |  |  |             ca_file=self._cert.ca_file, | 
					
						
							|  |  |  |             cert_file=self._cert.cert_file, | 
					
						
							|  |  |  |             key_file=self._cert.key_file, | 
					
						
							|  |  |  |             port=data['port'], | 
					
						
							|  |  |  |             home_name=home_name, | 
					
						
							|  |  |  |             loop=self._main_loop) | 
					
						
							|  |  |  |         self._mips_local[group_id] = mips | 
					
						
							|  |  |  |         mips.enable_logger(logger=_LOGGER) | 
					
						
							|  |  |  |         mips.on_dev_list_changed = self.__on_gw_device_list_changed | 
					
						
							|  |  |  |         mips.sub_mips_state( | 
					
						
							|  |  |  |             key=group_id, handler=self.__on_mips_local_state_changed) | 
					
						
							|  |  |  |         mips.connect() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_mips_cloud_state_changed( | 
					
						
							|  |  |  |         self, key: str, state: bool | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info('cloud mips state changed, %s, %s', key, state) | 
					
						
							|  |  |  |         if state: | 
					
						
							|  |  |  |             # Connect | 
					
						
							|  |  |  |             self.__request_refresh_cloud_devices(immediately=True) | 
					
						
							|  |  |  |             # Sub cloud device state | 
					
						
							|  |  |  |             for did in list(self._device_list_cache.keys()): | 
					
						
							|  |  |  |                 self._mips_cloud.sub_device_state( | 
					
						
							|  |  |  |                     did=did, handler=self.__on_cloud_device_state_changed) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # Disconnect | 
					
						
							|  |  |  |             for did, info in self._device_list_cloud.items(): | 
					
						
							|  |  |  |                 cloud_state_old: Optional[bool] = info.get('online', None) | 
					
						
							|  |  |  |                 if not cloud_state_old: | 
					
						
							|  |  |  |                     # Cloud state is None or False, no need to update | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 info['online'] = False | 
					
						
							|  |  |  |                 if did not in self._device_list_cache: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |                 state_old: Optional[bool] = self._device_list_cache[did].get( | 
					
						
							|  |  |  |                     'online', None) | 
					
						
							|  |  |  |                 state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                     False, | 
					
						
							|  |  |  |                     self._device_list_gateway.get( | 
					
						
							|  |  |  |                         did, {}).get('online', False), | 
					
						
							|  |  |  |                     self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |                 if state_old == state_new: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |                 sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |                 if sub and sub.handler: | 
					
						
							|  |  |  |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |             self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_mips_local_state_changed( | 
					
						
							|  |  |  |         self, group_id: str, state: bool | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info('local mips state changed, %s, %s', group_id, state) | 
					
						
							|  |  |  |         mips: MipsLocalClient = self._mips_local.get(group_id, None) | 
					
						
							|  |  |  |         if mips is None: | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'local mips state changed, mips not exist, %s', group_id) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if state: | 
					
						
							|  |  |  |             # Connected | 
					
						
							|  |  |  |             self.__request_refresh_gw_devices_by_group_id(group_id=group_id) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # Disconnect | 
					
						
							|  |  |  |             for did, info in self._device_list_gateway.items(): | 
					
						
							|  |  |  |                 if info.get('group_id', None) != group_id: | 
					
						
							|  |  |  |                     # Not belong to this gateway | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 if not info.get('online', False): | 
					
						
							|  |  |  |                     # Device offline, no need to update | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 # Update local device info | 
					
						
							|  |  |  |                 info['online'] = False | 
					
						
							|  |  |  |                 info['push_available'] = False | 
					
						
							|  |  |  |                 if did not in self._device_list_cache: | 
					
						
							|  |  |  |                     # Device not exist | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |                 state_old: Optional[bool] = self._device_list_cache.get( | 
					
						
							|  |  |  |                     did, {}).get('online', None) | 
					
						
							|  |  |  |                 state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                     self._device_list_cloud.get(did, {}).get('online', None), | 
					
						
							|  |  |  |                     False, | 
					
						
							|  |  |  |                     self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |                 if state_old == state_new: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |                 sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |                 if sub and sub.handler: | 
					
						
							|  |  |  |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |             self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_miot_lan_state_change(self, state: bool) -> None: | 
					
						
							|  |  |  |         _LOGGER.info( | 
					
						
							|  |  |  |             'miot lan state changed, %s, %s, %s', | 
					
						
							|  |  |  |             self._uid, self._cloud_server,  state) | 
					
						
							|  |  |  |         if state: | 
					
						
							|  |  |  |             # Update device | 
					
						
							|  |  |  |             self._miot_lan.sub_device_state( | 
					
						
							|  |  |  |                 key=f'{self._uid}-{self._cloud_server}', | 
					
						
							|  |  |  |                 handler=self.__on_lan_device_state_changed) | 
					
						
							|  |  |  |             for did, info in ( | 
					
						
							|  |  |  |                     await self._miot_lan.get_dev_list_async()).items(): | 
					
						
							|  |  |  |                 self.__on_lan_device_state_changed( | 
					
						
							|  |  |  |                     did=did, state=info, ctx=None) | 
					
						
							|  |  |  |             _LOGGER.info('lan device list, %s', self._device_list_lan) | 
					
						
							|  |  |  |             self._miot_lan.update_devices(devices={ | 
					
						
							|  |  |  |                 did: { | 
					
						
							|  |  |  |                     'token': info['token'], | 
					
						
							|  |  |  |                     'connect_type': info['connect_type']} | 
					
						
							|  |  |  |                 for did, info in self._device_list_cache.items() | 
					
						
							|  |  |  |                 if 'token' in info and 'connect_type' in info | 
					
						
							|  |  |  |                 and info['connect_type'] in [0, 8, 12, 23] | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             for did, info in self._device_list_lan.items(): | 
					
						
							|  |  |  |                 if not info.get('online', False): | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 # Update local device info | 
					
						
							|  |  |  |                 info['online'] = False | 
					
						
							|  |  |  |                 info['push_available'] = False | 
					
						
							|  |  |  |                 self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |                 state_old: Optional[bool] = self._device_list_cache.get( | 
					
						
							|  |  |  |                     did, {}).get('online', None) | 
					
						
							|  |  |  |                 state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                     self._device_list_cloud.get(did, {}).get('online', None), | 
					
						
							|  |  |  |                     self._device_list_gateway.get( | 
					
						
							|  |  |  |                         did, {}).get('online', False), | 
					
						
							|  |  |  |                     False) | 
					
						
							|  |  |  |                 if state_old == state_new: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |                 sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |                 if sub and sub.handler: | 
					
						
							|  |  |  |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |             self._device_list_lan = {} | 
					
						
							|  |  |  |             self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __on_cloud_device_state_changed( | 
					
						
							|  |  |  |         self, did: str, state: MIoTDeviceState, ctx: any | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info('cloud device state changed, %s, %s', did, state) | 
					
						
							|  |  |  |         cloud_device = self._device_list_cloud.get(did, None) | 
					
						
							|  |  |  |         if not cloud_device: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         cloud_state_new: bool = state == MIoTDeviceState.ONLINE | 
					
						
							|  |  |  |         if cloud_device.get('online', False) == cloud_state_new: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         cloud_device['online'] = cloud_state_new | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |         state_old: Optional[bool] = self._device_list_cache[did].get( | 
					
						
							|  |  |  |             'online', None) | 
					
						
							|  |  |  |         state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |             cloud_state_new, | 
					
						
							|  |  |  |             self._device_list_gateway.get(did, {}).get('online', False), | 
					
						
							|  |  |  |             self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |         if state_old == state_new: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |         sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |         if sub and sub.handler: | 
					
						
							|  |  |  |             sub.handler( | 
					
						
							|  |  |  |                 did, MIoTDeviceState.ONLINE if state_new | 
					
						
							|  |  |  |                 else MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_gw_device_list_changed( | 
					
						
							|  |  |  |         self, mips: MipsLocalClient, did_list: list[str] | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info( | 
					
						
							|  |  |  |             'gateway devices list changed, %s, %s', mips.group_id, did_list) | 
					
						
							|  |  |  |         payload: dict = {'filter': {'did': did_list}} | 
					
						
							|  |  |  |         gw_list = await mips.get_dev_list_async( | 
					
						
							|  |  |  |             payload=json.dumps(payload)) | 
					
						
							|  |  |  |         if gw_list is None: | 
					
						
							|  |  |  |             _LOGGER.error('local mips get_dev_list_async failed, %s', did_list) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await self.__update_devices_from_gw_async( | 
					
						
							|  |  |  |             gw_list=gw_list, group_id=mips.group_id, filter_dids=[ | 
					
						
							|  |  |  |                 did for did in did_list | 
					
						
							|  |  |  |                 if self._device_list_gateway.get(did, {}).get( | 
					
						
							|  |  |  |                     'group_id', None) == mips.group_id]) | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __on_lan_device_state_changed( | 
					
						
							|  |  |  |         self, did: str, state: dict, ctx: any | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.info('lan device state changed, %s, %s', did, state) | 
					
						
							|  |  |  |         lan_state_new: bool = state.get('online', False) | 
					
						
							|  |  |  |         lan_sub_new: bool = state.get('push_available', False) | 
					
						
							|  |  |  |         self._device_list_lan.setdefault(did, {}) | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |             lan_state_new == self._device_list_lan[did].get('online', False) | 
					
						
							|  |  |  |             and lan_sub_new == self._device_list_lan[did].get( | 
					
						
							|  |  |  |                 'push_available', False) | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._device_list_lan[did]['online'] = lan_state_new | 
					
						
							|  |  |  |         self._device_list_lan[did]['push_available'] = lan_sub_new | 
					
						
							|  |  |  |         if did not in self._device_list_cache: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |         if lan_state_new == self._device_list_cache[did].get('online', False): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         state_old: Optional[bool] = self._device_list_cache[did].get( | 
					
						
							|  |  |  |             'online', None) | 
					
						
							|  |  |  |         state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |             self._device_list_cloud.get(did, {}).get('online', None), | 
					
						
							|  |  |  |             self._device_list_gateway.get(did, {}).get('online', False), | 
					
						
							|  |  |  |             lan_state_new) | 
					
						
							|  |  |  |         if state_old == state_new: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |         sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |         if sub and sub.handler: | 
					
						
							|  |  |  |             sub.handler( | 
					
						
							|  |  |  |                 did, MIoTDeviceState.ONLINE if state_new | 
					
						
							|  |  |  |                 else MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __on_prop_msg(self, params: dict, ctx: any) -> None: | 
					
						
							|  |  |  |         """params MUST contain did, siid, piid, value""" | 
					
						
							|  |  |  |         # BLE device has no online/offline msg | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             subs: list[MIoTClientSub] = list(self._sub_tree.iter_match( | 
					
						
							|  |  |  |                 f'{params["did"]}/p/{params["siid"]}/{params["piid"]}')) | 
					
						
							|  |  |  |             for sub in subs: | 
					
						
							|  |  |  |                 sub.handler(params, sub.handler_ctx) | 
					
						
							|  |  |  |         except Exception as err:  # pylint: disable=broad-exception-caught | 
					
						
							|  |  |  |             _LOGGER.error('on prop msg error, %s, %s', params, err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __on_event_msg(self, params: dict, ctx: any) -> None: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             subs: list[MIoTClientSub] = list(self._sub_tree.iter_match( | 
					
						
							|  |  |  |                 f'{params["did"]}/e/{params["siid"]}/{params["eiid"]}')) | 
					
						
							|  |  |  |             for sub in subs: | 
					
						
							|  |  |  |                 sub.handler(params, sub.handler_ctx) | 
					
						
							|  |  |  |         except Exception as err:  # pylint: disable=broad-exception-caught | 
					
						
							|  |  |  |             _LOGGER.error('on event msg error, %s, %s', params, err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __check_device_state( | 
					
						
							|  |  |  |         self, cloud_state: Optional[bool], gw_state: bool, lan_state: bool | 
					
						
							|  |  |  |     ) -> Optional[bool]: | 
					
						
							|  |  |  |         if cloud_state is None and not gw_state and not lan_state: | 
					
						
							|  |  |  |             # Device remove | 
					
						
							|  |  |  |             return None | 
					
						
							|  |  |  |         if cloud_state or gw_state or lan_state: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __load_cache_device_async(self) -> None: | 
					
						
							|  |  |  |         """Load device list from cache.""" | 
					
						
							|  |  |  |         cache_list: Optional[dict[str, dict]] = await self._storage.load_async( | 
					
						
							|  |  |  |             domain='miot_devices', | 
					
						
							|  |  |  |             name=f'{self._uid}_{self._cloud_server}', | 
					
						
							|  |  |  |             type_=dict) | 
					
						
							|  |  |  |         if not cache_list: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=self._i18n.translate( | 
					
						
							|  |  |  |                     'miot.client.invalid_device_cache'), | 
					
						
							|  |  |  |                 notify_key='device_cache') | 
					
						
							|  |  |  |             raise MIoTClientError('load device list from cache error') | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=None, notify_key='device_cache') | 
					
						
							|  |  |  |         # Set default online status = False | 
					
						
							|  |  |  |         self._device_list_cache = {} | 
					
						
							|  |  |  |         for did, info in cache_list.items(): | 
					
						
							|  |  |  |             if info.get('online', None): | 
					
						
							|  |  |  |                 self._device_list_cache[did] = { | 
					
						
							|  |  |  |                     **info, 'online': False} | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self._device_list_cache[did] = info | 
					
						
							|  |  |  |         self._device_list_cloud = deepcopy(self._device_list_cache) | 
					
						
							|  |  |  |         self._device_list_gateway = { | 
					
						
							|  |  |  |             did: { | 
					
						
							|  |  |  |                 'did': did, | 
					
						
							|  |  |  |                 'name': info.get('name', None), | 
					
						
							|  |  |  |                 'group_id': info.get('group_id', None), | 
					
						
							|  |  |  |                 'online': False, | 
					
						
							|  |  |  |                 'push_available': False} | 
					
						
							|  |  |  |             for did, info in self._device_list_cache.items()} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __update_devices_from_cloud_async( | 
					
						
							|  |  |  |         self, cloud_list: dict[str, dict], | 
					
						
							|  |  |  |         filter_dids: Optional[list[str]] = None | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         """Update cloud devices.
 | 
					
						
							|  |  |  |         NOTICE: This function will operate the cloud_list | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         for did, info in self._device_list_cache.items(): | 
					
						
							|  |  |  |             if filter_dids and did not in filter_dids: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             state_old: Optional[bool] = info.get('online', None) | 
					
						
							|  |  |  |             cloud_state_old: Optional[bool] = self._device_list_cloud.get( | 
					
						
							|  |  |  |                 did, {}).get('online', None) | 
					
						
							|  |  |  |             cloud_state_new: Optional[bool] = None | 
					
						
							|  |  |  |             device_new: dict = cloud_list.pop(did, None) | 
					
						
							|  |  |  |             if device_new: | 
					
						
							|  |  |  |                 cloud_state_new = device_new.get('online', None) | 
					
						
							|  |  |  |                 # Update cache device info | 
					
						
							|  |  |  |                 info.update( | 
					
						
							|  |  |  |                     {**device_new, 'online': state_old}) | 
					
						
							|  |  |  |                 # Update cloud device | 
					
						
							|  |  |  |                 self._device_list_cloud[did] = device_new | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 # Device deleted | 
					
						
							|  |  |  |                 self._device_list_cloud[did]['online'] = None | 
					
						
							|  |  |  |             if cloud_state_old == cloud_state_new: | 
					
						
							|  |  |  |                 # Cloud online status no change | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             # Update sub from | 
					
						
							|  |  |  |             self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |             state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                 cloud_state_new, | 
					
						
							|  |  |  |                 self._device_list_gateway.get(did, {}).get('online', False), | 
					
						
							|  |  |  |                 self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |             if state_old == state_new: | 
					
						
							|  |  |  |                 # Online status no change | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             info['online'] = state_new | 
					
						
							|  |  |  |             # Call device state changed callback | 
					
						
							|  |  |  |             sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |             if sub and sub.handler: | 
					
						
							|  |  |  |                 sub.handler( | 
					
						
							|  |  |  |                     did, MIoTDeviceState.ONLINE if state_new | 
					
						
							|  |  |  |                     else MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |         # New devices | 
					
						
							|  |  |  |         self._device_list_cloud.update(cloud_list) | 
					
						
							|  |  |  |         # Update storage | 
					
						
							|  |  |  |         if not await self._storage.save_async( | 
					
						
							|  |  |  |             domain='miot_devices', | 
					
						
							|  |  |  |             name=f'{self._uid}_{self._cloud_server}', | 
					
						
							|  |  |  |             data=self._device_list_cache | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             _LOGGER.error('save device list to cache failed') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_cloud_devices_async(self) -> None: | 
					
						
							|  |  |  |         _LOGGER.debug( | 
					
						
							|  |  |  |             'refresh cloud devices, %s, %s', self._uid, self._cloud_server) | 
					
						
							|  |  |  |         self._refresh_cloud_devices_timer = None | 
					
						
							|  |  |  |         result = await self._http.get_devices_async( | 
					
						
							|  |  |  |             home_ids=list(self._entry_data.get('home_selected', {}).keys())) | 
					
						
							|  |  |  |         if not result and 'devices' not in result: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=self._i18n.translate('miot.client.device_cloud_error'), | 
					
						
							|  |  |  |                 notify_key='device_cloud') | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.__show_client_error_notify( | 
					
						
							|  |  |  |                 message=None, notify_key='device_cloud') | 
					
						
							|  |  |  |         cloud_list: dict[str, dict] = result['devices'] | 
					
						
							|  |  |  |         await self.__update_devices_from_cloud_async(cloud_list=cloud_list) | 
					
						
							|  |  |  |         # Update lan device | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |             self._ctrl_mode == CtrlMode.AUTO | 
					
						
							|  |  |  |             and self._miot_lan.init_done | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             self._miot_lan.update_devices(devices={ | 
					
						
							|  |  |  |                 did: { | 
					
						
							|  |  |  |                     'token': info['token'], | 
					
						
							|  |  |  |                     'connect_type': info['connect_type']} | 
					
						
							|  |  |  |                 for did, info in self._device_list_cache.items() | 
					
						
							|  |  |  |                 if 'token' in info and 'connect_type' in info | 
					
						
							|  |  |  |                 and info['connect_type'] in [0, 8, 12, 23] | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_cloud_device_with_dids_async( | 
					
						
							|  |  |  |         self, dids: list[str] | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         _LOGGER.debug('refresh cloud device with dids, %s', dids) | 
					
						
							|  |  |  |         cloud_list: dict[str, dict] = ( | 
					
						
							|  |  |  |             await self._http.get_devices_with_dids_async(dids=dids)) | 
					
						
							|  |  |  |         if cloud_list is None: | 
					
						
							|  |  |  |             _LOGGER.error('cloud http get_dev_list_async failed, %s', dids) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await self.__update_devices_from_cloud_async( | 
					
						
							|  |  |  |             cloud_list=cloud_list, filter_dids=dids) | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __request_refresh_cloud_devices(self, immediately=False) -> None: | 
					
						
							|  |  |  |         _LOGGER.debug( | 
					
						
							|  |  |  |             'request refresh cloud devices, %s, %s', | 
					
						
							|  |  |  |             self._uid, self._cloud_server) | 
					
						
							|  |  |  |         if immediately: | 
					
						
							|  |  |  |             if self._refresh_cloud_devices_timer: | 
					
						
							|  |  |  |                 self._refresh_cloud_devices_timer.cancel() | 
					
						
							|  |  |  |             self._refresh_cloud_devices_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |                 0, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                     self.__refresh_cloud_devices_async())) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if self._refresh_cloud_devices_timer: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._refresh_cloud_devices_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             6, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                 self.__refresh_cloud_devices_async())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __update_devices_from_gw_async( | 
					
						
							|  |  |  |         self, gw_list: dict[str, dict], | 
					
						
							|  |  |  |         group_id: Optional[str] = None, | 
					
						
							|  |  |  |         filter_dids: Optional[list[str]] = None | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         """Update cloud devices.
 | 
					
						
							|  |  |  |         NOTICE: This function will operate the gw_list"""
 | 
					
						
							|  |  |  |         _LOGGER.debug('update gw devices, %s, %s', group_id, filter_dids) | 
					
						
							|  |  |  |         if not gw_list and not filter_dids: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         for did, info in self._device_list_cache.items(): | 
					
						
							|  |  |  |             if did not in filter_dids: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             device_old: dict = self._device_list_gateway.get(did, None) | 
					
						
							|  |  |  |             gw_state_old = device_old.get( | 
					
						
							|  |  |  |                 'online', False) if device_old else False | 
					
						
							|  |  |  |             gw_state_new: bool = False | 
					
						
							|  |  |  |             device_new: dict = gw_list.pop(did, None) | 
					
						
							|  |  |  |             if device_new: | 
					
						
							|  |  |  |                 # Update gateway device info | 
					
						
							|  |  |  |                 self._device_list_gateway[did] = { | 
					
						
							|  |  |  |                     **device_new, 'group_id': group_id} | 
					
						
							|  |  |  |                 gw_state_new = device_new.get('online', False) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 # Device offline | 
					
						
							|  |  |  |                 if device_old: | 
					
						
							|  |  |  |                     device_old['online'] = False | 
					
						
							|  |  |  |             # Update cache group_id | 
					
						
							|  |  |  |             info['group_id'] = group_id | 
					
						
							|  |  |  |             if gw_state_old == gw_state_new: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |             state_old: Optional[bool] = info.get('online', None) | 
					
						
							|  |  |  |             state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                 self._device_list_cloud.get(did, {}).get('online', None), | 
					
						
							|  |  |  |                 gw_state_new, | 
					
						
							|  |  |  |                 self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |             if state_old == state_new: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             info['online'] = state_new | 
					
						
							|  |  |  |             sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |             if sub and sub.handler: | 
					
						
							|  |  |  |                 sub.handler( | 
					
						
							|  |  |  |                     did, MIoTDeviceState.ONLINE if state_new | 
					
						
							|  |  |  |                     else MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  |         # New devices or device home info changed | 
					
						
							|  |  |  |         for did, info in gw_list.items(): | 
					
						
							|  |  |  |             self._device_list_gateway[did] = {**info, 'group_id': group_id} | 
					
						
							|  |  |  |             if did not in self._device_list_cache: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             group_id_old: str = self._device_list_cache[did].get( | 
					
						
							|  |  |  |                 'group_id', None) | 
					
						
							|  |  |  |             self._device_list_cache[did]['group_id'] = group_id | 
					
						
							|  |  |  |             _LOGGER.info( | 
					
						
							|  |  |  |                 'move device %s from %s to %s', did, group_id_old, group_id) | 
					
						
							|  |  |  |             self.__update_device_msg_sub(did=did) | 
					
						
							|  |  |  |             state_old: Optional[bool] = self._device_list_cache[did].get( | 
					
						
							|  |  |  |                 'online', None) | 
					
						
							|  |  |  |             state_new: Optional[bool] = self.__check_device_state( | 
					
						
							|  |  |  |                 self._device_list_cloud.get(did, {}).get('online', None), | 
					
						
							|  |  |  |                 info.get('online', False), | 
					
						
							|  |  |  |                 self._device_list_lan.get(did, {}).get('online', False)) | 
					
						
							|  |  |  |             if state_old == state_new: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             self._device_list_cache[did]['online'] = state_new | 
					
						
							|  |  |  |             sub: MipsDeviceState = self._sub_device_state.get(did, None) | 
					
						
							|  |  |  |             if sub and sub.handler: | 
					
						
							|  |  |  |                 sub.handler( | 
					
						
							|  |  |  |                     did, MIoTDeviceState.ONLINE if state_new | 
					
						
							|  |  |  |                     else MIoTDeviceState.OFFLINE, sub.handler_ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_gw_devices_with_group_id_async( | 
					
						
							|  |  |  |         self, group_id: str | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         """Refresh gateway devices by group_id""" | 
					
						
							|  |  |  |         _LOGGER.debug( | 
					
						
							|  |  |  |             'refresh gw devices with group_id, %s', group_id) | 
					
						
							|  |  |  |         # Remove timer | 
					
						
							|  |  |  |         self._mips_local_state_changed_timers.pop(group_id, None) | 
					
						
							|  |  |  |         mips: MipsLocalClient = self._mips_local.get(group_id, None) | 
					
						
							|  |  |  |         if not mips: | 
					
						
							|  |  |  |             _LOGGER.error('mips not exist, %s', group_id) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if not mips.mips_state: | 
					
						
							|  |  |  |             _LOGGER.debug('local mips disconnect, skip refresh, %s', group_id) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         gw_list: dict = await mips.get_dev_list_async() | 
					
						
							|  |  |  |         if gw_list is None: | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'refresh gw devices with group_id failed, %s, %s', | 
					
						
							|  |  |  |                 self._uid, group_id) | 
					
						
							|  |  |  |             # Retry until success | 
					
						
							|  |  |  |             self.__request_refresh_gw_devices_by_group_id( | 
					
						
							|  |  |  |                 group_id=group_id) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await self.__update_devices_from_gw_async( | 
					
						
							|  |  |  |             gw_list=gw_list, group_id=group_id, filter_dids=[ | 
					
						
							|  |  |  |                 did for did, info in self._device_list_gateway.items() | 
					
						
							|  |  |  |                 if info.get('group_id', None) == group_id]) | 
					
						
							|  |  |  |         self.__request_show_devices_changed_notify() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __request_refresh_gw_devices_by_group_id( | 
					
						
							|  |  |  |         self, group_id: str, immediately: bool = False | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         """Request refresh gateway devices by group_id""" | 
					
						
							|  |  |  |         refresh_timer = self._mips_local_state_changed_timers.get( | 
					
						
							|  |  |  |             group_id, None) | 
					
						
							|  |  |  |         if immediately: | 
					
						
							|  |  |  |             if refresh_timer: | 
					
						
							|  |  |  |                 self._mips_local_state_changed_timers.pop(group_id, None) | 
					
						
							|  |  |  |                 refresh_timer.cancel() | 
					
						
							|  |  |  |             self._mips_local_state_changed_timers[group_id] = ( | 
					
						
							|  |  |  |                 self._main_loop.call_later( | 
					
						
							|  |  |  |                     0, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                         self.__refresh_gw_devices_with_group_id_async( | 
					
						
							|  |  |  |                             group_id=group_id)))) | 
					
						
							|  |  |  |         if refresh_timer: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._mips_local_state_changed_timers[group_id] = ( | 
					
						
							|  |  |  |             self._main_loop.call_later( | 
					
						
							|  |  |  |                 3, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                     self.__refresh_gw_devices_with_group_id_async( | 
					
						
							|  |  |  |                         group_id=group_id)))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_props_from_cloud(self, patch_len: int = 150) -> bool: | 
					
						
							|  |  |  |         if not self._network.network_status: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         request_list = None | 
					
						
							|  |  |  |         if len(self._refresh_props_list) < patch_len: | 
					
						
							|  |  |  |             request_list = self._refresh_props_list | 
					
						
							|  |  |  |             self._refresh_props_list = {} | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             request_list = {} | 
					
						
							|  |  |  |             for _ in range(patch_len): | 
					
						
							|  |  |  |                 key, value = self._refresh_props_list.popitem() | 
					
						
							|  |  |  |                 request_list[key] = value | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             results = await self._http.get_props_async( | 
					
						
							|  |  |  |                 params=list(request_list.values())) | 
					
						
							|  |  |  |             if not results: | 
					
						
							|  |  |  |                 raise MIoTClientError('get_props_async failed') | 
					
						
							|  |  |  |             for result in results: | 
					
						
							|  |  |  |                 if ( | 
					
						
							|  |  |  |                     'did' not in result | 
					
						
							|  |  |  |                     or 'siid' not in result | 
					
						
							|  |  |  |                     or 'piid' not in result | 
					
						
							|  |  |  |                     or 'value' not in result | 
					
						
							|  |  |  |                 ): | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 request_list.pop( | 
					
						
							|  |  |  |                     f'{result["did"]}|{result["siid"]}|{result["piid"]}', | 
					
						
							|  |  |  |                     None) | 
					
						
							|  |  |  |                 self.__on_prop_msg(params=result, ctx=None) | 
					
						
							|  |  |  |             if request_list: | 
					
						
							|  |  |  |                 _LOGGER.error( | 
					
						
							|  |  |  |                     'refresh props failed, cloud, %s', | 
					
						
							|  |  |  |                     list(request_list.keys())) | 
					
						
							|  |  |  |                 request_list = None | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         except Exception as err:  # pylint:disable=broad-exception-caught | 
					
						
							|  |  |  |             _LOGGER.error( | 
					
						
							|  |  |  |                 'refresh props error, cloud, %s, %s', | 
					
						
							|  |  |  |                 err, traceback.format_exc()) | 
					
						
							|  |  |  |             # Add failed request back to the list | 
					
						
							|  |  |  |             self._refresh_props_list.update(request_list) | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_props_from_gw(self) -> bool: | 
					
						
							|  |  |  |         if not self._mips_local or not self._device_list_gateway: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         request_list = {} | 
					
						
							|  |  |  |         succeed_once = False | 
					
						
							|  |  |  |         for key in list(self._refresh_props_list.keys()): | 
					
						
							|  |  |  |             did = key.split('|')[0] | 
					
						
							|  |  |  |             if did in request_list: | 
					
						
							|  |  |  |                 # NOTICE: A device only requests once a cycle, continuous | 
					
						
							|  |  |  |                 # acquisition of properties can cause device exceptions. | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             params = self._refresh_props_list.pop(key) | 
					
						
							|  |  |  |             device_gw = self._device_list_gateway.get(did, None) | 
					
						
							|  |  |  |             if not device_gw: | 
					
						
							|  |  |  |                 # Device not exist | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             mips_gw = self._mips_local.get(device_gw['group_id'], None) | 
					
						
							|  |  |  |             if not mips_gw: | 
					
						
							|  |  |  |                 _LOGGER.error('mips gateway not exist, %s', key) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             request_list[did] = { | 
					
						
							|  |  |  |                 **params, | 
					
						
							|  |  |  |                 'fut': mips_gw.get_prop_async( | 
					
						
							|  |  |  |                     did=did, siid=params['siid'], piid=params['piid'], | 
					
						
							|  |  |  |                     timeout_ms=6000)} | 
					
						
							|  |  |  |         results = await asyncio.gather( | 
					
						
							|  |  |  |             *[v['fut'] for v in request_list.values()]) | 
					
						
							|  |  |  |         for (did, param), result in zip(request_list.items(), results): | 
					
						
							|  |  |  |             if result is None: | 
					
						
							|  |  |  |                 # Don't use "not result", it will be skipped when result | 
					
						
							|  |  |  |                 # is 0, false | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             self.__on_prop_msg( | 
					
						
							|  |  |  |                 params={ | 
					
						
							|  |  |  |                     'did': did, | 
					
						
							|  |  |  |                     'siid': param['siid'], | 
					
						
							|  |  |  |                     'piid': param['piid'], | 
					
						
							|  |  |  |                     'value': result}, | 
					
						
							|  |  |  |                 ctx=None) | 
					
						
							|  |  |  |             succeed_once = True | 
					
						
							|  |  |  |         if succeed_once: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         _LOGGER.error( | 
					
						
							|  |  |  |             'refresh props failed, gw, %s', list(request_list.keys())) | 
					
						
							|  |  |  |         # Add failed request back to the list | 
					
						
							|  |  |  |         self._refresh_props_list.update(request_list) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_props_from_lan(self) -> bool: | 
					
						
							|  |  |  |         if not self._miot_lan.init_done or len(self._mips_local) > 0: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         request_list = {} | 
					
						
							|  |  |  |         succeed_once = False | 
					
						
							|  |  |  |         for key in list(self._refresh_props_list.keys()): | 
					
						
							|  |  |  |             did = key.split('|')[0] | 
					
						
							|  |  |  |             if did in request_list: | 
					
						
							|  |  |  |                 # NOTICE: A device only requests once a cycle, continuous | 
					
						
							|  |  |  |                 # acquisition of properties can cause device exceptions. | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             params = self._refresh_props_list.pop(key) | 
					
						
							|  |  |  |             if did not in self._device_list_lan: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             request_list[did] = { | 
					
						
							|  |  |  |                 **params, | 
					
						
							|  |  |  |                 'fut': self._miot_lan.get_prop_async( | 
					
						
							|  |  |  |                     did=did, siid=params['siid'], piid=params['piid'], | 
					
						
							|  |  |  |                     timeout_ms=6000)} | 
					
						
							|  |  |  |         results = await asyncio.gather( | 
					
						
							|  |  |  |             *[v['fut'] for v in request_list.values()]) | 
					
						
							|  |  |  |         for (did, param), result in zip(request_list.items(), results): | 
					
						
							|  |  |  |             if result is None: | 
					
						
							|  |  |  |                 # Don't use "not result", it will be skipped when result | 
					
						
							|  |  |  |                 # is 0, false | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             self.__on_prop_msg( | 
					
						
							|  |  |  |                 params={ | 
					
						
							|  |  |  |                     'did': did, | 
					
						
							|  |  |  |                     'siid': param['siid'], | 
					
						
							|  |  |  |                     'piid': param['piid'], | 
					
						
							|  |  |  |                     'value': result}, | 
					
						
							|  |  |  |                 ctx=None) | 
					
						
							|  |  |  |             succeed_once = True | 
					
						
							|  |  |  |         if succeed_once: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         _LOGGER.error( | 
					
						
							|  |  |  |             'refresh props failed, lan, %s', list(request_list.keys())) | 
					
						
							|  |  |  |         # Add failed request back to the list | 
					
						
							|  |  |  |         self._refresh_props_list.update(request_list) | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     async def __refresh_props_handler(self) -> None: | 
					
						
							|  |  |  |         if not self._refresh_props_list: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         # Cloud, Central hub gateway, Lan control | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |             await self.__refresh_props_from_cloud() | 
					
						
							|  |  |  |             or await self.__refresh_props_from_gw() | 
					
						
							|  |  |  |             or await self.__refresh_props_from_lan() | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             self._refresh_props_retry_count = 0 | 
					
						
							|  |  |  |             if self._refresh_props_list: | 
					
						
							|  |  |  |                 self._refresh_props_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |                     0.2, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                         self.__refresh_props_handler())) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self._refresh_props_timer = None | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Try three times, and if it fails three times, empty the list. | 
					
						
							|  |  |  |         if self._refresh_props_retry_count >= 3: | 
					
						
							|  |  |  |             self._refresh_props_list = {} | 
					
						
							|  |  |  |             self._refresh_props_retry_count = 0 | 
					
						
							|  |  |  |             if self._refresh_props_timer: | 
					
						
							|  |  |  |                 self._refresh_props_timer.cancel() | 
					
						
							|  |  |  |                 self._refresh_props_timer = None | 
					
						
							|  |  |  |             _LOGGER.error('refresh props failed, retry count exceed') | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self._refresh_props_retry_count += 1 | 
					
						
							|  |  |  |         _LOGGER.error( | 
					
						
							|  |  |  |             'refresh props failed, retry, %s', self._refresh_props_retry_count) | 
					
						
							|  |  |  |         self._refresh_props_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             3, lambda: self._main_loop.create_task( | 
					
						
							|  |  |  |                 self.__refresh_props_handler())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __show_client_error_notify( | 
					
						
							|  |  |  |         self, message: str, notify_key: str = '' | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         if message: | 
					
						
							|  |  |  |             self._persistence_notify( | 
					
						
							|  |  |  |                 f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error', | 
					
						
							|  |  |  |                 self._i18n.translate( | 
					
						
							|  |  |  |                     key='miot.client.xiaomi_home_error_title'), | 
					
						
							|  |  |  |                 self._i18n.translate( | 
					
						
							|  |  |  |                     key='miot.client.xiaomi_home_error', | 
					
						
							|  |  |  |                     replace={ | 
					
						
							|  |  |  |                         'nick_name': self._entry_data.get( | 
					
						
							|  |  |  |                             'nick_name', DEFAULT_NICK_NAME), | 
					
						
							|  |  |  |                         'uid': self._uid, | 
					
						
							|  |  |  |                         'cloud_server': self._cloud_server, | 
					
						
							|  |  |  |                         'message': message | 
					
						
							|  |  |  |                     })) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self._persistence_notify( | 
					
						
							|  |  |  |                 f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error', | 
					
						
							|  |  |  |                 None, None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __show_devices_changed_notify(self) -> None: | 
					
						
							|  |  |  |         """Show device list changed notify""" | 
					
						
							|  |  |  |         self._show_devices_changed_notify_timer = None | 
					
						
							|  |  |  |         if self._persistence_notify is None: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         message_add: str = '' | 
					
						
							|  |  |  |         count_add: int = 0 | 
					
						
							|  |  |  |         message_del: str = '' | 
					
						
							|  |  |  |         count_del: int = 0 | 
					
						
							|  |  |  |         message_offline: str = '' | 
					
						
							|  |  |  |         count_offline: int = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # New devices | 
					
						
							|  |  |  |         for did, info in { | 
					
						
							|  |  |  |                 **self._device_list_gateway, **self._device_list_cloud | 
					
						
							|  |  |  |         }.items(): | 
					
						
							|  |  |  |             if did in self._device_list_cache: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             count_add += 1 | 
					
						
							|  |  |  |             message_add += ( | 
					
						
							|  |  |  |                 f'- {info.get("name", "unknown")} ({did}, ' | 
					
						
							|  |  |  |                 f'{info.get("model", "unknown")})\n') | 
					
						
							|  |  |  |         # Get unavailable and offline devices | 
					
						
							|  |  |  |         home_name_del: Optional[str] = None | 
					
						
							|  |  |  |         home_name_offline: Optional[str] = None | 
					
						
							|  |  |  |         for did, info in self._device_list_cache.items(): | 
					
						
							|  |  |  |             online: Optional[bool] = info.get('online', None) | 
					
						
							|  |  |  |             home_name_new = info.get('home_name', 'unknown') | 
					
						
							|  |  |  |             if online: | 
					
						
							|  |  |  |                 # Skip online device | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if online is None: | 
					
						
							|  |  |  |                 # Device not exist | 
					
						
							|  |  |  |                 if home_name_del != home_name_new: | 
					
						
							|  |  |  |                     message_del += f'\n[{home_name_new}]\n' | 
					
						
							|  |  |  |                     home_name_del = home_name_new | 
					
						
							|  |  |  |                 count_del += 1 | 
					
						
							|  |  |  |                 message_del += ( | 
					
						
							|  |  |  |                     f'- {info.get("name", "unknown")} ({did}, ' | 
					
						
							|  |  |  |                     f'{info.get("room_name", "unknown")})\n') | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 # Device offline | 
					
						
							|  |  |  |                 if home_name_offline != home_name_new: | 
					
						
							|  |  |  |                     message_offline += f'\n[{home_name_new}]\n' | 
					
						
							|  |  |  |                     home_name_offline = home_name_new | 
					
						
							|  |  |  |                 count_offline += 1 | 
					
						
							|  |  |  |                 message_offline += ( | 
					
						
							|  |  |  |                     f'- {info.get("name", "unknown")} ({did}, ' | 
					
						
							|  |  |  |                     f'{info.get("room_name", "unknown")})\n') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         message = '' | 
					
						
							|  |  |  |         if count_add: | 
					
						
							|  |  |  |             message += self._i18n.translate( | 
					
						
							|  |  |  |                 key='miot.client.device_list_add', | 
					
						
							|  |  |  |                 replace={ | 
					
						
							|  |  |  |                     'count': count_add, | 
					
						
							|  |  |  |                     'message': message_add}) | 
					
						
							|  |  |  |         if count_del: | 
					
						
							|  |  |  |             message += self._i18n.translate( | 
					
						
							|  |  |  |                 key='miot.client.device_list_del', | 
					
						
							|  |  |  |                 replace={ | 
					
						
							|  |  |  |                     'count': count_del, | 
					
						
							|  |  |  |                     'message': message_del}) | 
					
						
							|  |  |  |         if count_offline: | 
					
						
							|  |  |  |             message += self._i18n.translate( | 
					
						
							|  |  |  |                 key='miot.client.device_list_offline', | 
					
						
							|  |  |  |                 replace={ | 
					
						
							|  |  |  |                     'count': count_offline, | 
					
						
							|  |  |  |                     'message': message_offline}) | 
					
						
							|  |  |  |         if message != '': | 
					
						
							|  |  |  |             network_status = self._i18n.translate( | 
					
						
							|  |  |  |                 key='miot.client.network_status_online' | 
					
						
							|  |  |  |                 if self._network.network_status | 
					
						
							|  |  |  |                 else 'miot.client.network_status_offline') | 
					
						
							|  |  |  |             self._persistence_notify( | 
					
						
							|  |  |  |                 self.__gen_notify_key('dev_list_changed'), | 
					
						
							|  |  |  |                 self._i18n.translate('miot.client.device_list_changed_title'), | 
					
						
							|  |  |  |                 self._i18n.translate( | 
					
						
							|  |  |  |                     key='miot.client.device_list_changed', | 
					
						
							|  |  |  |                     replace={ | 
					
						
							|  |  |  |                         'nick_name': self._entry_data.get( | 
					
						
							|  |  |  |                             'nick_name', DEFAULT_NICK_NAME), | 
					
						
							|  |  |  |                         'uid': self._uid, | 
					
						
							|  |  |  |                         'cloud_server': self._cloud_server, | 
					
						
							|  |  |  |                         'network_status': network_status, | 
					
						
							|  |  |  |                         'message': message | 
					
						
							|  |  |  |                     })) | 
					
						
							|  |  |  |             _LOGGER.debug( | 
					
						
							|  |  |  |                 'show device list changed notify, add %s, del %s, offline %s', | 
					
						
							|  |  |  |                 count_add, count_del, count_offline) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self._persistence_notify( | 
					
						
							|  |  |  |                 self.__gen_notify_key('dev_list_changed'), None, None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @final | 
					
						
							|  |  |  |     def __request_show_devices_changed_notify( | 
					
						
							|  |  |  |         self, delay_sec: float = 6 | 
					
						
							|  |  |  |     ) -> None: | 
					
						
							|  |  |  |         if not self._mips_cloud and not self._mips_local and not self._miot_lan: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if self._show_devices_changed_notify_timer: | 
					
						
							|  |  |  |             self._show_devices_changed_notify_timer.cancel() | 
					
						
							|  |  |  |         self._show_devices_changed_notify_timer = self._main_loop.call_later( | 
					
						
							|  |  |  |             delay_sec, self.__show_devices_changed_notify) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @staticmethod | 
					
						
							|  |  |  | async def get_miot_instance_async( | 
					
						
							|  |  |  |     hass: HomeAssistant, entry_id: str, entry_data: Optional[dict] = None, | 
					
						
							|  |  |  |     persistent_notify: Optional[Callable[[str, str, str], None]] = None | 
					
						
							|  |  |  | ) -> MIoTClient: | 
					
						
							|  |  |  |     if entry_id is None: | 
					
						
							|  |  |  |         raise MIoTClientError('invalid entry_id') | 
					
						
							|  |  |  |     miot_client: MIoTClient = None | 
					
						
							|  |  |  |     if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None): | 
					
						
							|  |  |  |         _LOGGER.info('instance exist, %s', entry_id) | 
					
						
							|  |  |  |         miot_client = a | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         if entry_data is None: | 
					
						
							|  |  |  |             raise MIoTClientError('entry data is None') | 
					
						
							|  |  |  |         # Get running loop | 
					
						
							|  |  |  |         loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() | 
					
						
							|  |  |  |         if loop is None: | 
					
						
							|  |  |  |             raise MIoTClientError('loop is None') | 
					
						
							|  |  |  |         # MIoT network | 
					
						
							|  |  |  |         network: Optional[MIoTNetwork] = hass.data[DOMAIN].get( | 
					
						
							|  |  |  |             'miot_network', None) | 
					
						
							|  |  |  |         if not network: | 
					
						
							|  |  |  |             network = MIoTNetwork(loop=loop) | 
					
						
							|  |  |  |             hass.data[DOMAIN]['miot_network'] = network | 
					
						
							|  |  |  |             await network.init_async( | 
					
						
							|  |  |  |                 refresh_interval=NETWORK_REFRESH_INTERVAL) | 
					
						
							|  |  |  |             _LOGGER.info('create miot_network instance') | 
					
						
							|  |  |  |         # MIoT storage | 
					
						
							|  |  |  |         storage: Optional[MIoTStorage] = hass.data[DOMAIN].get( | 
					
						
							|  |  |  |             'miot_storage', None) | 
					
						
							|  |  |  |         if not storage: | 
					
						
							|  |  |  |             storage = MIoTStorage( | 
					
						
							|  |  |  |                 root_path=entry_data['storage_path'], loop=loop) | 
					
						
							|  |  |  |             hass.data[DOMAIN]['miot_storage'] = storage | 
					
						
							|  |  |  |             _LOGGER.info('create miot_storage instance') | 
					
						
							|  |  |  |         # MIoT service | 
					
						
							|  |  |  |         mips_service: Optional[MipsService] = hass.data[DOMAIN].get( | 
					
						
							|  |  |  |             'mips_service', None) | 
					
						
							|  |  |  |         if not mips_service: | 
					
						
							|  |  |  |             aiozc = await zeroconf.async_get_async_instance(hass) | 
					
						
							|  |  |  |             mips_service: MipsService = MipsService(aiozc=aiozc, loop=loop) | 
					
						
							|  |  |  |             hass.data[DOMAIN]['mips_service'] = mips_service | 
					
						
							|  |  |  |             await mips_service.init_async() | 
					
						
							|  |  |  |             _LOGGER.info('create mips_service instance') | 
					
						
							|  |  |  |         # MIoT lan | 
					
						
							|  |  |  |         miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get( | 
					
						
							|  |  |  |             'miot_lan', None) | 
					
						
							|  |  |  |         if not miot_lan: | 
					
						
							|  |  |  |             lan_config = (await storage.load_user_config_async( | 
					
						
							|  |  |  |                 uid='global_config', | 
					
						
							|  |  |  |                 cloud_server='all', | 
					
						
							|  |  |  |                 keys=['net_interfaces', 'enable_subscribe'])) or {} | 
					
						
							|  |  |  |             miot_lan = MIoTLan( | 
					
						
							|  |  |  |                 net_ifs=lan_config.get('net_interfaces', []), | 
					
						
							|  |  |  |                 network=network, | 
					
						
							|  |  |  |                 mips_service=mips_service, | 
					
						
							|  |  |  |                 enable_subscribe=lan_config.get('enable_subscribe', False), | 
					
						
							|  |  |  |                 loop=loop) | 
					
						
							|  |  |  |             hass.data[DOMAIN]['miot_lan'] = miot_lan | 
					
						
							|  |  |  |             _LOGGER.info('create miot_lan instance') | 
					
						
							|  |  |  |         # MIoT client | 
					
						
							|  |  |  |         miot_client = MIoTClient( | 
					
						
							|  |  |  |             entry_id=entry_id, | 
					
						
							|  |  |  |             entry_data=entry_data, | 
					
						
							|  |  |  |             network=network, | 
					
						
							|  |  |  |             storage=storage, | 
					
						
							|  |  |  |             mips_service=mips_service, | 
					
						
							|  |  |  |             miot_lan=miot_lan, | 
					
						
							|  |  |  |             loop=loop | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         miot_client.persistent_notify = persistent_notify | 
					
						
							|  |  |  |         hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client) | 
					
						
							|  |  |  |         _LOGGER.info( | 
					
						
							|  |  |  |             'new miot_client instance, %s, %s', entry_id, entry_data) | 
					
						
							|  |  |  |         await miot_client.init_async() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return miot_client |