diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index 4cb8d56..a3b05cf 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -77,14 +77,14 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a config entry.""" - device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][ - config_entry.entry_id] + device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][config_entry.entry_id] new_entities = [] for miot_device in device_list: for data in miot_device.entity_list.get("light", []): new_entities.append( - Light(miot_device=miot_device, entity_data=data, hass=hass)) + Light(miot_device=miot_device, entity_data=data, hass=hass) + ) if new_entities: async_add_entities(new_entities) @@ -104,8 +104,9 @@ class Light(MIoTServiceEntity, LightEntity): _brightness_scale: Optional[tuple[int, int]] _mode_map: Optional[dict[Any, Any]] - def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData, - hass: HomeAssistant) -> None: + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData, hass: HomeAssistant + ) -> None: """Initialize the Light.""" super().__init__(miot_device=miot_device, entity_data=entity_data) self.hass = hass @@ -144,8 +145,7 @@ class Light(MIoTServiceEntity, LightEntity): self._attr_supported_features |= LightEntityFeature.EFFECT self._prop_mode = prop else: - _LOGGER.info("invalid brightness format, %s", - self.entity_id) + _LOGGER.info("invalid brightness format, %s", self.entity_id) continue # color-temperature if prop.name == "color-temperature": @@ -172,9 +172,13 @@ class Light(MIoTServiceEntity, LightEntity): mode_list = prop.value_list.to_map() elif prop.value_range: mode_list = {} - if (int((prop.value_range.max_ - prop.value_range.min_) / - prop.value_range.step) - > self._VALUE_RANGE_MODE_COUNT_MAX): + if ( + int( + (prop.value_range.max_ - prop.value_range.min_) + / prop.value_range.step + ) + > self._VALUE_RANGE_MODE_COUNT_MAX + ): _LOGGER.error( "too many mode values, %s, %s, %s", self.entity_id, @@ -183,9 +187,9 @@ class Light(MIoTServiceEntity, LightEntity): ) else: for value in range( - prop.value_range.min_, - prop.value_range.max_, - prop.value_range.step, + prop.value_range.min_, + prop.value_range.max_, + prop.value_range.step, ): mode_list[value] = f"mode {value}" if mode_list: @@ -241,8 +245,9 @@ class Light(MIoTServiceEntity, LightEntity): @property def effect(self) -> Optional[str]: """Return the current mode.""" - return self.get_map_value(map_=self._mode_map, - key=self.get_prop_value(prop=self._prop_mode)) + return self.get_map_value( + map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode) + ) async def async_turn_on(self, **kwargs) -> None: """Turn the light on. @@ -258,24 +263,24 @@ class Light(MIoTServiceEntity, LightEntity): set_properties_list: List[Dict[str, Any]] = [] if self._prop_on: value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 - set_properties_list.append({ - "prop": self._prop_on, - "value": value_on - }) + set_properties_list.append({"prop": self._prop_on, "value": value_on}) + # brightness if ATTR_BRIGHTNESS in kwargs: - brightness = brightness_to_value(self._brightness_scale, - kwargs[ATTR_BRIGHTNESS]) - set_properties_list.append({ - "prop": self._prop_brightness, - "value": brightness - }) + brightness = brightness_to_value( + self._brightness_scale, kwargs[ATTR_BRIGHTNESS] + ) + set_properties_list.append( + {"prop": self._prop_brightness, "value": brightness} + ) # color-temperature if ATTR_COLOR_TEMP_KELVIN in kwargs: - set_properties_list.append({ - "prop": self._prop_color_temp, - "value": kwargs[ATTR_COLOR_TEMP_KELVIN], - }) + set_properties_list.append( + { + "prop": self._prop_color_temp, + "value": kwargs[ATTR_COLOR_TEMP_KELVIN], + } + ) self._attr_color_mode = ColorMode.COLOR_TEMP # rgb color if ATTR_RGB_COLOR in kwargs: @@ -283,46 +288,42 @@ class Light(MIoTServiceEntity, LightEntity): g = kwargs[ATTR_RGB_COLOR][1] b = kwargs[ATTR_RGB_COLOR][2] rgb = (r << 16) | (g << 8) | b - set_properties_list.append({ - "prop": self._prop_color, - "value": rgb - }) + set_properties_list.append({"prop": self._prop_color, "value": rgb}) self._attr_color_mode = ColorMode.RGB # mode if ATTR_EFFECT in kwargs: - set_properties_list.append({ - "prop": - self._prop_mode, - "value": - self.get_map_key(map_=self._mode_map, - value=kwargs[ATTR_EFFECT]), - }) + set_properties_list.append( + { + "prop": self._prop_mode, + "value": self.get_map_key( + map_=self._mode_map, value=kwargs[ATTR_EFFECT] + ), + } + ) await self.set_properties_async(set_properties_list) self.async_write_ha_state() elif command_send_mode and command_send_mode.state == "Send Turn On First": set_properties_list: List[Dict[str, Any]] = [] if self._prop_on: value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 - set_properties_list.append({ - "prop": self._prop_on, - "value": value_on - }) - await self.set_property_async(prop=self._prop_on, - value=value_on) + set_properties_list.append({"prop": self._prop_on, "value": value_on}) + await self.set_property_async(prop=self._prop_on, value=value_on) # brightness if ATTR_BRIGHTNESS in kwargs: - brightness = brightness_to_value(self._brightness_scale, - kwargs[ATTR_BRIGHTNESS]) - set_properties_list.append({ - "prop": self._prop_brightness, - "value": brightness - }) + brightness = brightness_to_value( + self._brightness_scale, kwargs[ATTR_BRIGHTNESS] + ) + set_properties_list.append( + {"prop": self._prop_brightness, "value": brightness} + ) # color-temperature if ATTR_COLOR_TEMP_KELVIN in kwargs: - set_properties_list.append({ - "prop": self._prop_color_temp, - "value": kwargs[ATTR_COLOR_TEMP_KELVIN], - }) + set_properties_list.append( + { + "prop": self._prop_color_temp, + "value": kwargs[ATTR_COLOR_TEMP_KELVIN], + } + ) self._attr_color_mode = ColorMode.COLOR_TEMP # rgb color if ATTR_RGB_COLOR in kwargs: @@ -330,35 +331,33 @@ class Light(MIoTServiceEntity, LightEntity): g = kwargs[ATTR_RGB_COLOR][1] b = kwargs[ATTR_RGB_COLOR][2] rgb = (r << 16) | (g << 8) | b - set_properties_list.append({ - "prop": self._prop_color, - "value": rgb - }) + set_properties_list.append({"prop": self._prop_color, "value": rgb}) self._attr_color_mode = ColorMode.RGB # mode if ATTR_EFFECT in kwargs: - set_properties_list.append({ - "prop": - self._prop_mode, - "value": - self.get_map_key(map_=self._mode_map, - value=kwargs[ATTR_EFFECT]), - }) + set_properties_list.append( + { + "prop": self._prop_mode, + "value": self.get_map_key( + map_=self._mode_map, value=kwargs[ATTR_EFFECT] + ), + } + ) await self.set_properties_async(set_properties_list) self.async_write_ha_state() else: if self._prop_on: value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 - await self.set_property_async(prop=self._prop_on, - value=value_on) + await self.set_property_async(prop=self._prop_on, value=value_on) # brightness if ATTR_BRIGHTNESS in kwargs: - brightness = brightness_to_value(self._brightness_scale, - kwargs[ATTR_BRIGHTNESS]) - await self.set_property_async(prop=self._prop_brightness, - value=brightness, - write_ha_state=False) + brightness = brightness_to_value( + self._brightness_scale, kwargs[ATTR_BRIGHTNESS] + ) + await self.set_property_async( + prop=self._prop_brightness, value=brightness, write_ha_state=False + ) # color-temperature if ATTR_COLOR_TEMP_KELVIN in kwargs: await self.set_property_async( @@ -373,16 +372,17 @@ class Light(MIoTServiceEntity, LightEntity): g = kwargs[ATTR_RGB_COLOR][1] b = kwargs[ATTR_RGB_COLOR][2] rgb = (r << 16) | (g << 8) | b - await self.set_property_async(prop=self._prop_color, - value=rgb, - write_ha_state=False) + await self.set_property_async( + prop=self._prop_color, value=rgb, write_ha_state=False + ) self._attr_color_mode = ColorMode.RGB # mode if ATTR_EFFECT in kwargs: await self.set_property_async( prop=self._prop_mode, - value=self.get_map_key(map_=self._mode_map, - value=kwargs[ATTR_EFFECT]), + value=self.get_map_key( + map_=self._mode_map, value=kwargs[ATTR_EFFECT] + ), write_ha_state=False, ) self.async_write_ha_state() diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index acb814a..7864ee9 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -214,8 +214,7 @@ class MIoTClient: 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._ctrl_mode = CtrlMode.load(entry_data.get("ctrl_mode", DEFAULT_CTRL_MODE)) self._network = network self._storage = storage self._mips_service = mips_service @@ -252,25 +251,30 @@ class MIoTClient: self._show_devices_changed_notify_timer = None self._display_devs_notify = entry_data.get( - "display_devices_changed_notify", ["add", "del", "offline"]) + "display_devices_changed_notify", ["add", "del", "offline"] + ) self._display_notify_content_hash = None self._display_binary_text = "text" in entry_data.get( - "display_binary_mode", ["text"]) + "display_binary_mode", ["text"] + ) self._display_binary_bool = "bool" in entry_data.get( - "display_binary_mode", ["text"]) + "display_binary_mode", ["text"] + ) 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) + 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)) # MIoT i18n client self._i18n = MIoTI18n( - lang=self._entry_data.get("integration_language", - DEFAULT_INTEGRATION_LANGUAGE), + lang=self._entry_data.get( + "integration_language", DEFAULT_INTEGRATION_LANGUAGE + ), loop=self._main_loop, ) await self._i18n.init_async() @@ -292,9 +296,9 @@ class MIoTClient: loop=self._main_loop, ) # MIoT cert client - self._cert = MIoTCert(storage=self._storage, - uid=self._uid, - cloud_server=self.cloud_server) + 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"], @@ -313,8 +317,7 @@ class MIoTClient: key=f"{self._uid}-{self._cloud_server}", handler=self.__on_network_status_changed, ) - await self.__on_network_status_changed( - status=self._network.network_status) + 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: @@ -328,13 +331,14 @@ class MIoTClient: handler=self.__on_mips_service_state_change, ) service_data = self._mips_service.get_services( - group_id=info["group_id"]).get(info["group_id"], None) + group_id=info["group_id"] + ).get(info["group_id"], None) if not service_data: - _LOGGER.info("central mips service not scanned, %s", - home_id) + _LOGGER.info("central mips service not scanned, %s", home_id) continue - _LOGGER.info("central mips service scanned, %s, %s", - home_id, service_data) + _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"], @@ -350,12 +354,13 @@ class MIoTClient: 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) + 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) + 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, @@ -363,21 +368,22 @@ class MIoTClient: 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}") + 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}") + key=f"{self._uid}-{self._cloud_server}" + ) self._miot_lan.delete_devices( - devices=list(self._device_list_cache.keys())) + 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) + 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}") + 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() @@ -385,8 +391,7 @@ class MIoTClient: 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.unsub_mips_state(key=f"{self._uid}-{self._cloud_server}") self._mips_cloud.deinit() # Cancel refresh cloud devices if self._refresh_cloud_devices_timer: @@ -396,25 +401,27 @@ class MIoTClient: # 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}") + 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.deinit() if self._mips_local_state_changed_timers: - for timer_item in self._mips_local_state_changed_timers.values( - ): + 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}") + 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}") + key=f"{self._uid}-{self._cloud_server}" + ) self._miot_lan.delete_devices( - devices=list(self._device_list_cache.keys())) + 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) + key=f"{self._uid}-{self._cloud_server}", vote=False + ) # Cancel refresh auth info if self._refresh_token_timer: self._refresh_token_timer.cancel() @@ -429,8 +436,7 @@ class MIoTClient: await self._oauth.deinit_async() await self._http.deinit_async() # Remove notify - self._persistence_notify(self.__gen_notify_key("dev_list_changed"), - None, None) + 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") @@ -510,8 +516,9 @@ class MIoTClient: if value: self.__request_show_devices_changed_notify() else: - self._persistence_notify(self.__gen_notify_key("dev_list_changed"), - None, None) + self._persistence_notify( + self.__gen_notify_key("dev_list_changed"), None, None + ) @property def device_list(self) -> dict: @@ -531,37 +538,42 @@ class MIoTClient: # 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): + 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): + 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"]) + 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"]) + access_token=valid_auth_info["access_token"] + ) # Update mips cloud token self._mips_cloud.update_access_token( - access_token=valid_auth_info["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}, + 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) + _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") @@ -577,8 +589,7 @@ class MIoTClient: return True except Exception as err: self.__show_client_error_notify( - message=self._i18n.translate( - "miot.client.invalid_oauth_info"), # type: ignore + message=self._i18n.translate("miot.client.invalid_oauth_info"), # type: ignore notify_key="oauth_info", ) _LOGGER.error( @@ -596,8 +607,10 @@ class MIoTClient: 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) + 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: @@ -605,15 +618,17 @@ class MIoTClient: 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"]) + 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) + 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") @@ -628,52 +643,52 @@ class MIoTClient: return True except MIoTClientError as error: self.__show_client_error_notify( - message=self._i18n.translate( - "miot.client.invalid_cert_info"), # type: ignore + message=self._i18n.translate("miot.client.invalid_cert_info"), # type: ignore notify_key="user_cert", ) - _LOGGER.error("refresh user cert error, %s, %s", error, - traceback.format_exc()) + _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: + 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 = 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): + 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) + _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) + 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 = 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.%d.%d, %s -> %s", did, siid, - piid, value, result) - rc = (result or - {}).get("code", - MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) + result = await self._miot_lan.set_prop_async( + did=did, siid=siid, piid=piid, value=value + ) + _LOGGER.debug( + "lan set prop, %s.%d.%d, %s -> %s", did, siid, piid, value, 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)) @@ -681,12 +696,9 @@ class MIoTClient: # Cloud control device_cloud = self._device_list_cloud.get(did, None) if device_cloud and device_cloud.get("online", False): - result = await self._http.set_prop_async(params=[{ - "did": did, - "siid": siid, - "piid": piid, - "value": value - }]) + result = 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, @@ -696,22 +708,22 @@ class MIoTClient: result, ) if result and len(result) == 1: - rc = result[0].get("code", - MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) + 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])) + 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')}") + f"{self._i18n.translate('error.common.-10007')}" + ) async def set_props_async( self, @@ -727,40 +739,46 @@ class MIoTClient: if self._ctrl_mode == CtrlMode.AUTO: # Gateway control 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): + 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) + _LOGGER.error("no gw route, %s, try control throw cloud", device_gw) else: - result = await mips.set_props_async(did=did, - props_list=props_list) - rc = {(r or - {}).get("code", - MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) - for r in result} + result = await mips.set_props_async(did=did, props_list=props_list) + rc = { + (r or {}).get( + "code", MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value + ) + for r in result + } if all(t in [0, 1] for t in rc): return True else: raise MIoTClientError( - self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0])) + self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0]) + ) # 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.set_props_async( - did=did, props_list=props_list) + did=did, props_list=props_list + ) _LOGGER.debug("lan set prop, %s -> %s", props_list, result) - rc = {(r or - {}).get("code", - MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) - for r in result} + rc = { + (r or {}).get("code", MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) + for r in result + } if all(t in [0, 1] for t in rc): return True else: raise MIoTClientError( - self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0])) + self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0]) + ) # Cloud control device_cloud = self._device_list_cloud.get(did, None) if device_cloud and device_cloud.get("online", False): @@ -771,10 +789,10 @@ class MIoTClient: result, ) if result and len(result) == len(props_list): - rc = {(r or - {}).get("code", - MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) - for r in result} + rc = { + (r or {}).get("code", MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) + for r in result + } if all(t in [0, 1] for t in rc): return True @@ -782,16 +800,18 @@ class MIoTClient: # 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])) + await self.__refresh_cloud_device_with_dids_async(dids=[did]) + ) else: raise MIoTClientError( - self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0])) + self.__get_exec_error_with_rc(rc=(rc - {0, 1})[0]) + ) # Show error message raise MIoTClientError( f"{self._i18n.translate('miot.client.device_exec_error')}, " - f"{self._i18n.translate('error.common.-10007')}") + 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: @@ -803,8 +823,8 @@ class MIoTClient: 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())) + 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: @@ -815,96 +835,93 @@ class MIoTClient: # 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) + 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()) + _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): + 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) + 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) + 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: + 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 = 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): + 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) + 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) + 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 and device_cloud.get("online", False): - result: dict = await self._http.action_async(did=did, - siid=siid, - aiid=aiid, - in_list=in_list) + 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) + 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])) + await self.__refresh_cloud_device_with_dids_async(dids=[did]) + ) raise MIoTClientError(self.__get_exec_error_with_rc(rc=rc)) # TODO: Show error message _LOGGER.error("client action failed, %s.%d.%d", did, siid, aiid) @@ -922,16 +939,15 @@ class MIoTClient: raise MIoTClientError(f"did not exist, {did}") topic = f"{did}/p/{'#' 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) + 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: Optional[int] = None, - piid: Optional[int] = None) -> bool: + def unsub_prop( + self, did: str, siid: Optional[int] = None, piid: Optional[int] = None + ) -> bool: topic = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}" if self._sub_tree.get(topic=topic): del self._sub_tree[topic] @@ -949,16 +965,15 @@ class MIoTClient: if did not in self._device_list_cache: raise MIoTClientError(f"did not exist, {did}") topic = f"{did}/e/{'#' 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) + 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: Optional[int] = None, - eiid: Optional[int] = None) -> bool: + def unsub_event( + self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None + ) -> bool: topic = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" if self._sub_tree.get(topic=topic): del self._sub_tree[topic] @@ -974,9 +989,9 @@ class MIoTClient: """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) + self._sub_device_state[did] = MipsDeviceState( + did=did, handler=handler, handler_ctx=handler_ctx + ) _LOGGER.debug("client sub device state, %s", did) return True @@ -1009,14 +1024,13 @@ class MIoTClient: break def __get_exec_error_with_rc(self, rc: int) -> str: - err_msg: str = self._i18n.translate( - key=f"error.common.{rc}") # type: ignore + err_msg: str = self._i18n.translate(key=f"error.common.{rc}") # type: ignore 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) + f"{self._i18n.translate(key='miot.client.device_exec_error')}, " + err_msg + ) @final def __gen_notify_key(self, name: str) -> str: @@ -1029,8 +1043,7 @@ class MIoTClient: 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() - ), + lambda: self._main_loop.create_task(self.refresh_oauth_info_async()), ) @final @@ -1083,18 +1096,24 @@ class MIoTClient: 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)): + 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)): + 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)): + 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 @@ -1105,8 +1124,7 @@ class MIoTClient: # Sub new self.__sub_from(from_new, did) self._sub_source_list[did] = from_new - _LOGGER.info("device sub changed, %s, from %s to %s", did, from_old, - 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: @@ -1129,11 +1147,10 @@ class MIoTClient: 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) + 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: @@ -1141,9 +1158,11 @@ class MIoTClient: 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"]): + 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) @@ -1165,13 +1184,11 @@ class MIoTClient: 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.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: + 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 @@ -1179,7 +1196,8 @@ class MIoTClient: # 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) + did=did, handler=self.__on_cloud_device_state_changed + ) else: # Disconnect for did, info in self._device_list_cloud.items(): @@ -1192,7 +1210,8 @@ class MIoTClient: continue self.__update_device_msg_sub(did=did) state_old: Optional[bool] = self._device_list_cache[did].get( - "online", None) + "online", None + ) state_new: Optional[bool] = self.__check_device_state( False, self._device_list_gateway.get(did, {}).get("online", False), @@ -1207,13 +1226,11 @@ class MIoTClient: self.__request_show_devices_changed_notify() @final - async def __on_mips_local_state_changed(self, group_id: str, - state: bool) -> None: + 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 = self._mips_local.get(group_id, None) if not mips: - _LOGGER.error("local mips state changed, mips not exist, %s", - group_id) + _LOGGER.error("local mips state changed, mips not exist, %s", group_id) return if state: # Connected @@ -1234,8 +1251,9 @@ class MIoTClient: # 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_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, @@ -1251,19 +1269,17 @@ class MIoTClient: @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) + _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(): - await self.__on_lan_device_state_changed(did=did, - state=info, - ctx=None) + for did, info in (await self._miot_lan.get_dev_list_async()).items(): + await 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={ @@ -1273,9 +1289,11 @@ class MIoTClient: "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] - }) + 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): @@ -1284,8 +1302,9 @@ class MIoTClient: 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_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), @@ -1301,8 +1320,9 @@ class MIoTClient: self.__request_show_devices_changed_notify() @final - def __on_cloud_device_state_changed(self, did: str, state: MIoTDeviceState, - ctx: Any) -> None: + 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: @@ -1314,8 +1334,7 @@ class MIoTClient: 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_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), @@ -1328,17 +1347,16 @@ class MIoTClient: if sub and sub.handler: sub.handler( did, - MIoTDeviceState.ONLINE - if state_new else MIoTDeviceState.OFFLINE, + 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) + 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: @@ -1348,23 +1366,25 @@ class MIoTClient: 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 + 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: + 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): + "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 @@ -1373,8 +1393,7 @@ class MIoTClient: 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_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), @@ -1387,8 +1406,7 @@ class MIoTClient: if sub and sub.handler: sub.handler( did, - MIoTDeviceState.ONLINE - if state_new else MIoTDeviceState.OFFLINE, + MIoTDeviceState.ONLINE if state_new else MIoTDeviceState.OFFLINE, sub.handler_ctx, ) self.__request_show_devices_changed_notify() @@ -1400,7 +1418,9 @@ class MIoTClient: try: subs: list[MIoTClientSub] = list( self._sub_tree.iter_match( - f"{params['did']}/p/{params['siid']}/{params['piid']}")) + 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 @@ -1411,15 +1431,18 @@ class MIoTClient: try: subs: list[MIoTClientSub] = list( self._sub_tree.iter_match( - f"{params['did']}/e/{params['siid']}/{params['eiid']}")) + 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]: + 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 @@ -1431,19 +1454,16 @@ class MIoTClient: 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) # type: ignore + domain="miot_devices", name=f"{self._uid}_{self._cloud_server}", type_=dict + ) # type: ignore if not cache_list: self.__show_client_error_notify( - message=self._i18n.translate( - "miot.client.invalid_device_cache"), # type: ignore + message=self._i18n.translate("miot.client.invalid_device_cache"), # type: ignore notify_key="device_cache", ) raise MIoTClientError("load device list from cache error") else: - self.__show_client_error_notify(message=None, - notify_key="device_cache") + 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(): @@ -1459,14 +1479,14 @@ class MIoTClient: "group_id": info.get("group_id", None), "online": False, "push_available": False, - } for did, info in self._device_list_cache.items() + } + 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: + self, cloud_list: dict[str, dict], filter_dids: Optional[list[str]] = None + ) -> None: """Update cloud devices. NOTICE: This function will operate the cloud_list """ @@ -1474,8 +1494,9 @@ class MIoTClient: 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_old: Optional[bool] = self._device_list_cloud.get(did, {}).get( + "online", None + ) cloud_state_new: Optional[bool] = None device_new = cloud_list.pop(did, None) if device_new: @@ -1506,37 +1527,34 @@ class MIoTClient: if sub and sub.handler: sub.handler( did, - MIoTDeviceState.ONLINE - if state_new else MIoTDeviceState.OFFLINE, + 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, + 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) + _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())) + 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"), # type: ignore + message=self._i18n.translate("miot.client.device_cloud_error"), # type: ignore notify_key="device_cloud", ) return else: - self.__show_client_error_notify(message=None, - notify_key="device_cloud") + 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 @@ -1549,41 +1567,45 @@ class MIoTClient: "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] - }) + 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: + async def __refresh_cloud_device_with_dids_async(self, dids: list[str]) -> None: _LOGGER.debug("refresh cloud device with dids, %s", dids) cloud_list = 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) + 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) + _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()), + 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())) + 6, lambda: self._main_loop.create_task(self.__refresh_cloud_devices_async()) + ) @final async def __update_devices_from_gw_async( @@ -1601,15 +1623,12 @@ class MIoTClient: if did not in filter_dids: continue device_old = self._device_list_gateway.get(did, None) - gw_state_old = device_old.get("online", - False) if device_old else False + gw_state_old = device_old.get("online", False) if device_old else False gw_state_new: bool = False device_new = gw_list.pop(did, None) if device_new: # Update gateway device info - self._device_list_gateway[did] = { - **device_new, "group_id": group_id - } + self._device_list_gateway[did] = {**device_new, "group_id": group_id} gw_state_new = device_new.get("online", False) else: # Device offline @@ -1633,8 +1652,7 @@ class MIoTClient: if sub and sub.handler: sub.handler( did, - MIoTDeviceState.ONLINE - if state_new else MIoTDeviceState.OFFLINE, + MIoTDeviceState.ONLINE if state_new else MIoTDeviceState.OFFLINE, sub.handler_ctx, ) # New devices or device home info changed @@ -1642,14 +1660,11 @@ class MIoTClient: 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) + 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) + _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_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), @@ -1662,14 +1677,12 @@ class MIoTClient: if sub and sub.handler: sub.handler( did, - MIoTDeviceState.ONLINE - if state_new else MIoTDeviceState.OFFLINE, + 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: + 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 @@ -1683,8 +1696,9 @@ class MIoTClient: 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) + _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 @@ -1692,20 +1706,19 @@ class MIoTClient: gw_list=gw_list, group_id=group_id, filter_dids=[ - did for did, info in self._device_list_gateway.items() + 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: + 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) + 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) @@ -1714,18 +1727,18 @@ class MIoTClient: self._main_loop.call_later( 0, lambda: self._main_loop.create_task( - self.__refresh_gw_devices_with_group_id_async( - group_id=group_id)), - )) + 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)), - ) + 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: @@ -1743,24 +1756,32 @@ class MIoTClient: request_list[key] = value try: results = await self._http.get_props_async( - params=list(request_list.values())) + 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): + 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) + f"{result['did']}|{result['siid']}|{result['piid']}", None + ) self.__on_prop_msg(params=result, ctx=None) if request_list: - _LOGGER.info("refresh props failed, cloud, %s", - list(request_list.keys())) + _LOGGER.info( + "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()) + _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 @@ -1788,14 +1809,11 @@ class MIoTClient: continue request_list[did] = { **params, - "fut": - mips_gw.get_prop_async(did=did, - siid=params["siid"], - piid=params["piid"], - timeout_ms=6000), + "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()]) + 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 @@ -1835,14 +1853,11 @@ class MIoTClient: continue request_list[did] = { **params, - "fut": - self._miot_lan.get_prop_async(did=did, - siid=params["siid"], - piid=params["piid"], - timeout_ms=6000), + "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()]) + 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 @@ -1870,15 +1885,16 @@ class MIoTClient: 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()): + 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()), + lambda: self._main_loop.create_task(self.__refresh_props_handler()), ) else: self._refresh_props_timer = None @@ -1894,40 +1910,35 @@ class MIoTClient: _LOGGER.info("refresh props failed, retry count exceed") return self._refresh_props_retry_count += 1 - _LOGGER.info("refresh props failed, retry, %s", - self._refresh_props_retry_count) + _LOGGER.info("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())) + 3, lambda: self._main_loop.create_task(self.__refresh_props_handler()) + ) @final - def __show_client_error_notify(self, - message: Optional[str], - notify_key: str = "") -> None: + def __show_client_error_notify( + self, message: Optional[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"), # type: ignore + self._i18n.translate(key="miot.client.xiaomi_home_error_title"), # type: ignore 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, + "nick_name": self._entry_data.get( + "nick_name", DEFAULT_NICK_NAME + ), + "uid": self._uid, + "cloud_server": self._cloud_server, + "message": message, }, ), ) # type: ignore else: self._persistence_notify( - f"{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error", - None, None) + f"{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error", None, None + ) @final def __show_devices_changed_notify(self) -> None: @@ -1946,14 +1957,16 @@ class MIoTClient: # New devices if "add" in self._display_devs_notify: for did, info in { - **self._device_list_gateway, - **self._device_list_cloud, + **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") + 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 @@ -1969,8 +1982,10 @@ class MIoTClient: 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") + message_del += ( + f"- {info.get('name', 'unknown')} ({did}, " + f"{info.get('room_name', 'unknown')})\n" + ) continue if "offline" in self._display_devs_notify: # Device offline @@ -1978,62 +1993,51 @@ class MIoTClient: 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_offline += ( + f"- {info.get('name', 'unknown')} ({did}, " + f"{info.get('room_name', 'unknown')})\n" + ) message = "" if "add" in self._display_devs_notify and count_add: message += self._i18n.translate( key="miot.client.device_list_add", - replace={ - "count": count_add, - "message": message_add - }, + replace={"count": count_add, "message": message_add}, ) # type: ignore if "del" in self._display_devs_notify and count_del: message += self._i18n.translate( key="miot.client.device_list_del", - replace={ - "count": count_del, - "message": message_del - }, + replace={"count": count_del, "message": message_del}, ) # type: ignore if "offline" in self._display_devs_notify and count_offline: message += self._i18n.translate( key="miot.client.device_list_offline", - replace={ - "count": count_offline, - "message": message_offline - }, + replace={"count": count_offline, "message": message_offline}, ) # type: ignore if message != "": msg_hash = hash(message) if msg_hash == self._display_notify_content_hash: # Notify content no change, return - _LOGGER.debug( - "device list changed notify content no change, return") + _LOGGER.debug("device list changed notify content no change, return") return network_status = self._i18n.translate( - key="miot.client.network_status_online" if self._network. - network_status else "miot.client.network_status_offline") + 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"), # type: ignore + self._i18n.translate("miot.client.device_list_changed_title"), # type: ignore 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, + "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, }, ), ) # type: ignore @@ -2045,12 +2049,12 @@ class MIoTClient: count_offline, ) else: - self._persistence_notify(self.__gen_notify_key("dev_list_changed"), - None, None) + 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: + def __request_show_devices_changed_notify(self, delay_sec: float = 6) -> None: if not self._display_devs_notify: return if not self._mips_cloud and not self._mips_local and not self._miot_lan: @@ -2058,7 +2062,8 @@ class MIoTClient: 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) + delay_sec, self.__show_devices_changed_notify + ) @staticmethod @@ -2106,8 +2111,7 @@ async def get_miot_instance_async( await network.init_async() _LOGGER.info("create miot_network instance") # MIoT service - mips_service: Optional[MipsService] = hass.data[DOMAIN].get( - "mips_service", None) + 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(aiozc=aiozc, loop=loop) diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index 395f345..bbd3357 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -103,8 +103,7 @@ class MIoTOauthClient: else: self._oauth_host = f"{cloud_server}.{DEFAULT_OAUTH2_API_HOST}" self._device_id = f"ha.{uuid}" - self._state = hashlib.sha1( - f"d={self._device_id}".encode("utf-8")).hexdigest() + self._state = hashlib.sha1(f"d={self._device_id}".encode("utf-8")).hexdigest() self._session = aiohttp.ClientSession(loop=self._main_loop) @property @@ -167,24 +166,31 @@ class MIoTOauthClient: timeout=MIHOME_HTTP_API_TIMEOUT, ) if http_res.status == 401: - raise MIoTOauthError("unauthorized(401)", - MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED) + raise MIoTOauthError( + "unauthorized(401)", MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED + ) if http_res.status != 200: raise MIoTOauthError(f"invalid http status code, {http_res.status}") res_str = await http_res.text() res_obj = json.loads(res_str) - if (not res_obj or res_obj.get("code", None) != 0 or - "result" not in res_obj or - not all(key in res_obj["result"] for key in - ["access_token", "refresh_token", "expires_in"])): + if ( + not res_obj + or res_obj.get("code", None) != 0 + or "result" not in res_obj + or not all( + key in res_obj["result"] + for key in ["access_token", "refresh_token", "expires_in"] + ) + ): raise MIoTOauthError(f"invalid http response, {res_str}") return { **res_obj["result"], - "expires_ts": - int(time.time() + (res_obj["result"].get("expires_in", 0) * - TOKEN_EXPIRES_TS_RATIO)), + "expires_ts": int( + time.time() + + (res_obj["result"].get("expires_in", 0) * TOKEN_EXPIRES_TS_RATIO) + ), } async def get_access_token_async(self, code: str) -> dict: @@ -205,7 +211,8 @@ class MIoTOauthClient: "redirect_uri": self._redirect_url, "code": code, "device_id": self._device_id, - }) + } + ) async def refresh_access_token_async(self, refresh_token: str) -> dict: """get access token by refresh token. @@ -224,7 +231,8 @@ class MIoTOauthClient: "client_id": self._client_id, "redirect_uri": self._redirect_url, "refresh_token": refresh_token, - }) + } + ) class MIoTHttpClient: @@ -259,14 +267,16 @@ class MIoTHttpClient: self._get_prop_timer = None self._get_prop_list = {} - if (not isinstance(cloud_server, str) or - not isinstance(client_id, str) or - not isinstance(access_token, str)): + if ( + not isinstance(cloud_server, str) + or not isinstance(client_id, str) + or not isinstance(access_token, str) + ): raise MIoTHttpError("invalid params") - self.update_http_header(cloud_server=cloud_server, - client_id=client_id, - access_token=access_token) + self.update_http_header( + cloud_server=cloud_server, client_id=client_id, access_token=access_token + ) self._session = aiohttp.ClientSession(loop=self._main_loop) @@ -309,10 +319,8 @@ class MIoTHttpClient: # pylint: disable=unused-private-member async def __mihome_api_get_async( - self, - url_path: str, - params: dict, - timeout: int = MIHOME_HTTP_API_TIMEOUT) -> dict: + self, url_path: str, params: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT + ) -> dict: http_res = await self._session.get( url=f"{self._base_url}{url_path}", params=params, @@ -333,16 +341,16 @@ class MIoTHttpClient: if res_obj.get("code", None) != 0: raise MIoTHttpError( f"invalid response code, {res_obj.get('code', None)}, " - f"{res_obj.get('message', '')}") - _LOGGER.debug("mihome api get, %s%s, %s -> %s", self._base_url, - url_path, params, res_obj) + f"{res_obj.get('message', '')}" + ) + _LOGGER.debug( + "mihome api get, %s%s, %s -> %s", self._base_url, url_path, params, res_obj + ) return res_obj async def __mihome_api_post_async( - self, - url_path: str, - data: dict, - timeout: int = MIHOME_HTTP_API_TIMEOUT) -> dict: + self, url_path: str, data: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT + ) -> dict: http_res = await self._session.post( url=f"{self._base_url}{url_path}", json=data, @@ -363,26 +371,29 @@ class MIoTHttpClient: if res_obj.get("code", None) != 0: raise MIoTHttpError( f"invalid response code, {res_obj.get('code', None)}, " - f"{res_obj.get('message', '')}") - _LOGGER.debug("mihome api post, %s%s, %s -> %s", self._base_url, - url_path, data, res_obj) + f"{res_obj.get('message', '')}" + ) + _LOGGER.debug( + "mihome api post, %s%s, %s -> %s", self._base_url, url_path, data, res_obj + ) return res_obj async def get_user_info_async(self) -> dict: http_res = await self._session.get( url="https://open.account.xiaomi.com/user/profile", - params={ - "clientId": self._client_id, - "token": self._access_token - }, + params={"clientId": self._client_id, "token": self._access_token}, headers={"content-type": "application/x-www-form-urlencoded"}, timeout=MIHOME_HTTP_API_TIMEOUT, ) res_str = await http_res.text() res_obj = json.loads(res_str) - if (not res_obj or res_obj.get("code", None) != 0 or - "data" not in res_obj or "miliaoNick" not in res_obj["data"]): + if ( + not res_obj + or res_obj.get("code", None) != 0 + or "data" not in res_obj + or "miliaoNick" not in res_obj["data"] + ): raise MIoTOauthError(f"invalid http response, {http_res.text}") return res_obj["data"] @@ -403,8 +414,7 @@ class MIoTHttpClient: return cert - async def __get_dev_room_page_async(self, - max_id: Optional[str] = None) -> dict: + async def __get_dev_room_page_async(self, max_id: Optional[str] = None) -> dict: res_obj = await self.__mihome_api_post_async( url_path="/app/v2/homeroom/get_dev_room_page", data={ @@ -425,34 +435,39 @@ class MIoTHttpClient: } for room in home.get("roomlist", []): if "id" not in room: - _LOGGER.error("get dev room page error, invalid room, %s", - room) + _LOGGER.error("get dev room page error, invalid room, %s", room) continue home_list[str(home["id"])]["room_info"][str(room["id"])] = { "dids": room.get("dids", None) or [] } if res_obj["result"].get("has_more", False) and isinstance( - res_obj["result"].get("max_id", None), str): + res_obj["result"].get("max_id", None), str + ): next_list = await self.__get_dev_room_page_async( - max_id=res_obj["result"]["max_id"]) + max_id=res_obj["result"]["max_id"] + ) for home_id, info in next_list.items(): home_list.setdefault(home_id, {"dids": [], "room_info": {}}) home_list[home_id]["dids"].extend(info["dids"]) for room_id, info in info["room_info"].items(): - home_list[home_id]["room_info"].setdefault( - room_id, {"dids": []}) + home_list[home_id]["room_info"].setdefault(room_id, {"dids": []}) home_list[home_id]["room_info"][room_id]["dids"].extend( - info["dids"]) + info["dids"] + ) return home_list async def get_separated_shared_devices_async(self) -> dict[str, dict]: separated_shared_devices: dict = {} device_list: dict[str, dict] = await self.__get_device_list_page_async( - dids=[], start_did=None) + dids=[], start_did=None + ) for did, value in device_list.items(): - if (value["owner"] is not None and ("userid" in value["owner"]) and - ("nickname" in value["owner"])): + if ( + value["owner"] is not None + and ("userid" in value["owner"]) + and ("nickname" in value["owner"]) + ): separated_shared_devices.setdefault(did, value["owner"]) return separated_shared_devices @@ -480,53 +495,45 @@ class MIoTHttpClient: if uid is None and device_source == "homelist": uid = str(home["uid"]) home_infos[device_source][home["id"]] = { - "home_id": - home["id"], - "home_name": - home["name"], - "city_id": - home.get("city_id", None), - "longitude": - home.get("longitude", None), - "latitude": - home.get("latitude", None), - "address": - home.get("address", None), - "dids": - home.get("dids", []), + "home_id": home["id"], + "home_name": home["name"], + "city_id": home.get("city_id", None), + "longitude": home.get("longitude", None), + "latitude": home.get("latitude", None), + "address": home.get("address", None), + "dids": home.get("dids", []), "room_info": { room["id"]: { "room_id": room["id"], "room_name": room["name"], "dids": room.get("dids", []), - } for room in home.get("roomlist", []) if "id" in room + } + for room in home.get("roomlist", []) + if "id" in room }, - "group_id": - calc_group_id(uid=home["uid"], home_id=home["id"]), - "uid": - str(home["uid"]), + "group_id": calc_group_id(uid=home["uid"], home_id=home["id"]), + "uid": str(home["uid"]), } home_infos["uid"] = uid if res_obj["result"].get("has_more", False) and isinstance( - res_obj["result"].get("max_id", None), str): + res_obj["result"].get("max_id", None), str + ): more_list = await self.__get_dev_room_page_async( - max_id=res_obj["result"]["max_id"]) + max_id=res_obj["result"]["max_id"] + ) for device_source in ["homelist", "share_home_list"]: for home_id, info in more_list.items(): if home_id not in home_infos[device_source]: _LOGGER.info("unknown home, %s, %s", home_id, info) continue - home_infos[device_source][home_id]["dids"].extend( - info["dids"]) + home_infos[device_source][home_id]["dids"].extend(info["dids"]) for room_id, info in info["room_info"].items(): - home_infos[device_source][home_id][ - "room_info"].setdefault(room_id, { - "room_id": room_id, - "room_name": "", - "dids": [] - }) - home_infos[device_source][home_id]["room_info"][ - room_id]["dids"].extend(info["dids"]) + home_infos[device_source][home_id]["room_info"].setdefault( + room_id, {"room_id": room_id, "room_name": "", "dids": []} + ) + home_infos[device_source][home_id]["room_info"][room_id][ + "dids" + ].extend(info["dids"]) return { "uid": uid, @@ -538,15 +545,15 @@ class MIoTHttpClient: return (await self.get_homeinfos_async()).get("uid", None) async def __get_device_list_page_async( - self, - dids: list[str], - start_did: Optional[str] = None) -> dict[str, dict]: + self, dids: list[str], start_did: Optional[str] = None + ) -> dict[str, dict]: req_data: dict = {"limit": 200, "get_split_device": True, "dids": dids} if start_did: req_data["start_did"] = start_did device_infos: dict = {} res_obj = await self.__mihome_api_post_async( - url_path="/app/v2/home/device_list_page", data=req_data) + url_path="/app/v2/home/device_list_page", data=req_data + ) if "result" not in res_obj: raise MIoTHttpError("invalid response result") res_obj = res_obj["result"] @@ -568,69 +575,56 @@ class MIoTHttpClient: _LOGGER.info("ignore miwifi.* device, cloud, %s", did) continue device_infos[did] = { - "did": - did, - "uid": - device.get("uid", None), - "name": - name, - "urn": - urn, - "model": - model, - "connect_type": - device.get("pid", -1), - "token": - device.get("token", None), - "online": - device.get("isOnline", False), - "icon": - device.get("icon", None), - "parent_id": - device.get("parent_id", None), - "manufacturer": - model.split(".")[0], + "did": did, + "uid": device.get("uid", None), + "name": name, + "urn": urn, + "model": model, + "connect_type": device.get("pid", -1), + "token": device.get("token", None), + "online": device.get("isOnline", False), + "icon": device.get("icon", None), + "parent_id": device.get("parent_id", None), + "manufacturer": model.split(".")[0], # 2: xiao-ai, 1: general speaker - "voice_ctrl": - device.get("voice_ctrl", 0), - "rssi": - device.get("rssi", None), - "owner": - device.get("owner", None), - "pid": - device.get("pid", None), - "local_ip": - device.get("local_ip", None), - "ssid": - device.get("ssid", None), - "bssid": - device.get("bssid", None), - "order_time": - device.get("orderTime", 0), - "fw_version": - device.get("extra", {}).get("fw_version", "unknown"), + "voice_ctrl": device.get("voice_ctrl", 0), + "rssi": device.get("rssi", None), + "owner": device.get("owner", None), + "pid": device.get("pid", None), + "local_ip": device.get("local_ip", None), + "ssid": device.get("ssid", None), + "bssid": device.get("bssid", None), + "order_time": device.get("orderTime", 0), + "fw_version": device.get("extra", {}).get("fw_version", "unknown"), } if isinstance(device.get("extra", None), dict) and device["extra"]: device_infos[did]["fw_version"] = device["extra"].get( - "fw_version", None) + "fw_version", None + ) device_infos[did]["mcu_version"] = device["extra"].get( - "mcu_version", None) - device_infos[did]["platform"] = device["extra"].get( - "platform", None) + "mcu_version", None + ) + device_infos[did]["platform"] = device["extra"].get("platform", None) next_start_did = res_obj.get("next_start_did", None) if res_obj.get("has_more", False) and next_start_did: - device_infos.update(await self.__get_device_list_page_async( - dids=dids, start_did=next_start_did)) + device_infos.update( + await self.__get_device_list_page_async( + dids=dids, start_did=next_start_did + ) + ) return device_infos async def get_devices_with_dids_async( - self, dids: list[str]) -> Optional[dict[str, dict]]: - results: list[dict[str, dict]] = await asyncio.gather(*[ - self.__get_device_list_page_async(dids=dids[index:index + 150]) - for index in range(0, len(dids), 150) - ]) + self, dids: list[str] + ) -> Optional[dict[str, dict]]: + results: list[dict[str, dict]] = await asyncio.gather( + *[ + self.__get_device_list_page_async(dids=dids[index : index + 150]) + for index in range(0, len(dids), 150) + ] + ) devices = {} for result in results: if result is None: @@ -638,16 +632,15 @@ class MIoTHttpClient: devices.update(result) return devices - async def get_devices_async(self, - home_ids: Optional[list[str]] = None - ) -> dict[str, dict]: + async def get_devices_async( + self, home_ids: Optional[list[str]] = None + ) -> dict[str, dict]: homeinfos = await self.get_homeinfos_async() homes: dict[str, dict[str, Any]] = {} devices: dict[str, dict] = {} for device_type in ["home_list", "share_home_list"]: homes.setdefault(device_type, {}) - for home_id, home_info in (homeinfos.get(device_type, None) or - {}).items(): + for home_id, home_info in (homeinfos.get(device_type, None) or {}).items(): if isinstance(home_ids, list) and home_id not in home_ids: continue home_name: str = home_info["home_name"] @@ -661,30 +654,34 @@ class MIoTHttpClient: "room_info": {}, }, ) - devices.update({ - did: { - "home_id": home_id, - "home_name": home_name, - "room_id": home_id, - "room_name": home_name, - "group_id": group_id, - } for did in home_info.get("dids", []) - }) - for room_id, room_info in home_info.get("room_info").items(): - room_name: str = room_info.get("room_name", "") - homes[device_type][home_id]["room_info"][ - room_id] = room_name - devices.update({ + devices.update( + { did: { "home_id": home_id, "home_name": home_name, - "room_id": room_id, - "room_name": room_name, + "room_id": home_id, + "room_name": home_name, "group_id": group_id, - } for did in room_info.get("dids", []) - }) - separated_shared_devices: dict = await self.get_separated_shared_devices_async( - ) + } + for did in home_info.get("dids", []) + } + ) + for room_id, room_info in home_info.get("room_info").items(): + room_name: str = room_info.get("room_name", "") + homes[device_type][home_id]["room_info"][room_id] = room_name + devices.update( + { + did: { + "home_id": home_id, + "home_name": home_name, + "room_id": room_id, + "room_name": room_name, + "group_id": group_id, + } + for did in room_info.get("dids", []) + } + ) + separated_shared_devices: dict = await self.get_separated_shared_devices_async() if separated_shared_devices: homes.setdefault("separated_shared_list", {}) for did, owner in separated_shared_devices.items(): @@ -695,20 +692,20 @@ class MIoTHttpClient: "home_name": owner["nickname"], "uid": owner_id, "group_id": "NotSupport", - "room_info": { - "shared_device": "shared_device" - }, + "room_info": {"shared_device": "shared_device"}, }, ) - devices.update({ - did: { - "home_id": owner_id, - "home_name": owner["nickname"], - "room_id": "shared_device", - "room_name": "shared_device", - "group_id": "NotSupport", + devices.update( + { + did: { + "home_id": owner_id, + "home_name": owner["nickname"], + "room_id": "shared_device", + "room_name": "shared_device", + "group_id": "NotSupport", + } } - }) + ) dids = sorted(list(devices.keys())) results = await self.get_devices_with_dids_async(dids=dids) if results is None: @@ -727,8 +724,7 @@ class MIoTHttpClient: parent_did = did.replace(match_str.group(), "") if parent_did in devices: devices[parent_did].setdefault("sub_devices", {}) - devices[parent_did]["sub_devices"][match_str.group() - [1:]] = device + devices[parent_did]["sub_devices"][match_str.group()[1:]] = device else: _LOGGER.error("unknown sub devices, %s, %s", did, parent_did) return {"uid": homeinfos["uid"], "homes": homes, "devices": devices} @@ -740,21 +736,16 @@ class MIoTHttpClient: """ res_obj = await self.__mihome_api_post_async( url_path="/app/v2/miotspec/prop/get", - data={ - "datasource": 1, - "params": params - }, + data={"datasource": 1, "params": params}, ) if "result" not in res_obj: raise MIoTHttpError("invalid response result") return res_obj["result"] async def __get_prop_async(self, did: str, siid: int, piid: int) -> Any: - results = await self.get_props_async(params=[{ - "did": did, - "siid": siid, - "piid": piid - }]) + results = await self.get_props_async( + params=[{"did": did, "siid": siid, "piid": piid}] + ) if not results: return None result = results[0] @@ -782,8 +773,7 @@ class MIoTHttpClient: results = await self.get_props_async(props_buffer) for result in results: - if not all( - key in result for key in ["did", "siid", "piid", "value"]): + if not all(key in result for key in ["did", "siid", "piid", "value"]): continue key = f"{result['did']}.{result['siid']}.{result['piid']}" prop_obj = self._get_prop_list.pop(key, None) @@ -810,11 +800,9 @@ class MIoTHttpClient: self._get_prop_timer = None return True - async def get_prop_async(self, - did: str, - siid: int, - piid: int, - immediately: bool = False) -> Any: + async def get_prop_async( + self, did: str, siid: int, piid: int, immediately: bool = False + ) -> Any: if immediately: return await self.__get_prop_async(did, siid, piid) key: str = f"{did}.{siid}.{piid}" @@ -823,11 +811,7 @@ class MIoTHttpClient: return await prop_obj["fut"] fut = self._main_loop.create_future() self._get_prop_list[key] = { - "param": { - "did": did, - "siid": siid, - "piid": piid - }, + "param": {"did": did, "siid": siid, "piid": piid}, "fut": fut, } if self._get_prop_timer is None: @@ -843,9 +827,8 @@ class MIoTHttpClient: params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] """ res_obj = await self.__mihome_api_post_async( - url_path="/app/v2/miotspec/prop/set", - data={"params": params}, - timeout=15) + url_path="/app/v2/miotspec/prop/set", data={"params": params}, timeout=15 + ) if "result" not in res_obj: raise MIoTHttpError("invalid response result") @@ -856,16 +839,16 @@ class MIoTHttpClient: params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] """ res_obj = await self.__mihome_api_post_async( - url_path="/app/v2/miotspec/prop/set", - data={"params": params}, - timeout=15) + url_path="/app/v2/miotspec/prop/set", data={"params": params}, timeout=15 + ) if "result" not in res_obj: raise MIoTHttpError("invalid response result") return res_obj["result"] - async def action_async(self, did: str, siid: int, aiid: int, - in_list: list[dict]) -> dict: + async def action_async( + self, did: str, siid: int, aiid: int, in_list: list[dict] + ) -> dict: """ params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []} """ diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index ae7dbc9..c754205 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -116,8 +116,7 @@ class MIoTEntityData: events: set[MIoTSpecEvent] actions: set[MIoTSpecAction] - def __init__(self, platform: str, - spec: MIoTSpecInstance | MIoTSpecService) -> None: + def __init__(self, platform: str, spec: MIoTSpecInstance | MIoTSpecService) -> None: self.platform = platform self.spec = spec self.device_class = None @@ -151,8 +150,7 @@ class MIoTDevice: _suggested_area: Optional[str] _sub_id: int - _device_state_sub_list: dict[str, dict[str, Callable[[str, MIoTDeviceState], - None]]] + _device_state_sub_list: dict[str, dict[str, Callable[[str, MIoTDeviceState], None]]] _value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]] _entity_list: dict[str, list[MIoTEntityData]] @@ -184,8 +182,7 @@ class MIoTDevice: self._room_name = device_info.get("room_name", None) match self.miot_client.area_name_rule: case "home_room": - self._suggested_area = f"{self._home_name} {self._room_name}".strip( - ) + self._suggested_area = f"{self._home_name} {self._room_name}".strip() case "home": self._suggested_area = self._home_name.strip() case "room": @@ -208,14 +205,15 @@ class MIoTDevice: sub_info = sub_devices.get(f"s{service.iid}", None) if sub_info is None: continue - _LOGGER.debug("miot device, update service sub info, %s, %s", - self.did, sub_info) + _LOGGER.debug( + "miot device, update service sub info, %s, %s", self.did, sub_info + ) service.description_trans = sub_info.get( - "name", service.description_trans) + "name", service.description_trans + ) # Sub device state - self.miot_client.sub_device_state(self._did, - self.__on_device_state_changed) + self.miot_client.sub_device_state(self._did, self.__on_device_state_changed) _LOGGER.debug("miot device init %s", device_info) @@ -240,14 +238,13 @@ class MIoTDevice: return self._action_list async def action_async(self, siid: int, aiid: int, in_list: list) -> list: - return await self.miot_client.action_async(did=self._did, - siid=siid, - aiid=aiid, - in_list=in_list) + return await self.miot_client.action_async( + did=self._did, siid=siid, aiid=aiid, in_list=in_list + ) def sub_device_state( - self, key: str, handler: Callable[[str, MIoTDeviceState], - None]) -> int: + self, key: str, handler: Callable[[str, MIoTDeviceState], None] + ) -> int: sub_id = self.__gen_sub_id() if key in self._device_state_sub_list: self._device_state_sub_list[key][str(sub_id)] = handler @@ -262,8 +259,9 @@ class MIoTDevice: if not sub_list: self._device_state_sub_list.pop(key, None) - def sub_property(self, handler: Callable[[dict, Any], None], siid: int, - piid: int) -> int: + def sub_property( + self, handler: Callable[[dict, Any], None], siid: int, piid: int + ) -> int: key: str = f"p.{siid}.{piid}" def _on_prop_changed(params: dict, ctx: Any) -> None: @@ -275,10 +273,9 @@ class MIoTDevice: self._value_sub_list[key][str(sub_id)] = handler else: self._value_sub_list[key] = {str(sub_id): handler} - self.miot_client.sub_prop(did=self._did, - handler=_on_prop_changed, - siid=siid, - piid=piid) + self.miot_client.sub_prop( + did=self._did, handler=_on_prop_changed, siid=siid, piid=piid + ) return sub_id def unsub_property(self, siid: int, piid: int, sub_id: int) -> None: @@ -291,8 +288,9 @@ class MIoTDevice: self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid) self._value_sub_list.pop(key, None) - def sub_event(self, handler: Callable[[dict, Any], None], siid: int, - eiid: int) -> int: + def sub_event( + self, handler: Callable[[dict, Any], None], siid: int, eiid: int + ) -> int: key: str = f"e.{siid}.{eiid}" def _on_event_occurred(params: dict, ctx: Any) -> None: @@ -304,10 +302,9 @@ class MIoTDevice: self._value_sub_list[key][str(sub_id)] = handler else: self._value_sub_list[key] = {str(sub_id): handler} - self.miot_client.sub_event(did=self._did, - handler=_on_event_occurred, - siid=siid, - eiid=eiid) + self.miot_client.sub_event( + did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid + ) return sub_id def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None: @@ -332,7 +329,8 @@ class MIoTDevice: suggested_area=self._suggested_area, configuration_url=( f"https://home.mi.com/webapp/content/baike/product/index.html?" - f"model={self._model}"), + f"model={self._model}" + ), ) @property @@ -342,35 +340,46 @@ class MIoTDevice: @property def did_tag(self) -> str: - return slugify_did(cloud_server=self.miot_client.cloud_server, - did=self._did) + return slugify_did(cloud_server=self.miot_client.cloud_server, did=self._did) def gen_device_entity_id(self, ha_domain: str) -> str: - return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" - f"{self._model_strs[-1][:20]}") + return ( + f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" + f"{self._model_strs[-1][:20]}" + ) - def gen_service_entity_id(self, ha_domain: str, siid: int, - description: str) -> str: - return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" - f"{self._model_strs[-1][:20]}_s_{siid}_{description}") + def gen_service_entity_id(self, ha_domain: str, siid: int, description: str) -> str: + return ( + f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" + f"{self._model_strs[-1][:20]}_s_{siid}_{description}" + ) - def gen_prop_entity_id(self, ha_domain: str, spec_name: str, siid: int, - piid: int) -> str: - return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" - f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" - f"_p_{siid}_{piid}") + def gen_prop_entity_id( + self, ha_domain: str, spec_name: str, siid: int, piid: int + ) -> str: + return ( + f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" + f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" + f"_p_{siid}_{piid}" + ) - def gen_event_entity_id(self, ha_domain: str, spec_name: str, siid: int, - eiid: int) -> str: - return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" - f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" - f"_e_{siid}_{eiid}") + def gen_event_entity_id( + self, ha_domain: str, spec_name: str, siid: int, eiid: int + ) -> str: + return ( + f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" + f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" + f"_e_{siid}_{eiid}" + ) - def gen_action_entity_id(self, ha_domain: str, spec_name: str, siid: int, - aiid: int) -> str: - return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" - f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" - f"_a_{siid}_{aiid}") + def gen_action_entity_id( + self, ha_domain: str, spec_name: str, siid: int, aiid: int + ) -> str: + return ( + f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" + f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" + f"_a_{siid}_{aiid}" + ) @property def name(self) -> str: @@ -407,7 +416,8 @@ class MIoTDevice: self._action_list[action.platform].append(action) def parse_miot_device_entity( - self, spec_instance: MIoTSpecInstance) -> Optional[MIoTEntityData]: + self, spec_instance: MIoTSpecInstance + ) -> Optional[MIoTEntityData]: if spec_instance.name not in SPEC_DEVICE_TRANS_MAP: return None spec_name: str = spec_instance.name @@ -417,8 +427,9 @@ class MIoTDevice: return None # 1. The device shall have all required services. required_services = SPEC_DEVICE_TRANS_MAP[spec_name]["required"].keys() - if not {service.name for service in spec_instance.services - }.issuperset(required_services): + if not {service.name for service in spec_instance.services}.issuperset( + required_services + ): return None optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].keys() @@ -434,56 +445,74 @@ class MIoTDevice: # 2. The service shall have all required properties, actions. if service.name in required_services: required_properties = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( - service.name, {}).get("required", - {}).get("properties", {})) + SPEC_DEVICE_TRANS_MAP[spec_name]["required"] + .get(service.name, {}) + .get("required", {}) + .get("properties", {}) + ) optional_properties = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( - service.name, {}).get("optional", - {}).get("properties", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["required"] + .get(service.name, {}) + .get("optional", {}) + .get("properties", set({})) + ) required_actions = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( - service.name, {}).get("required", - {}).get("actions", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["required"] + .get(service.name, {}) + .get("required", {}) + .get("actions", set({})) + ) optional_actions = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( - service.name, {}).get("optional", - {}).get("actions", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["required"] + .get(service.name, {}) + .get("optional", {}) + .get("actions", set({})) + ) elif service.name in optional_services: required_properties = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( - service.name, {}).get("required", - {}).get("properties", {})) + SPEC_DEVICE_TRANS_MAP[spec_name]["optional"] + .get(service.name, {}) + .get("required", {}) + .get("properties", {}) + ) optional_properties = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( - service.name, {}).get("optional", - {}).get("properties", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["optional"] + .get(service.name, {}) + .get("optional", {}) + .get("properties", set({})) + ) required_actions = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( - service.name, {}).get("required", - {}).get("actions", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["optional"] + .get(service.name, {}) + .get("required", {}) + .get("actions", set({})) + ) optional_actions = ( - SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( - service.name, {}).get("optional", - {}).get("actions", set({}))) + SPEC_DEVICE_TRANS_MAP[spec_name]["optional"] + .get(service.name, {}) + .get("optional", {}) + .get("actions", set({})) + ) else: continue - if not {prop.name for prop in service.properties if prop.access - }.issuperset(set(required_properties.keys())): + if not {prop.name for prop in service.properties if prop.access}.issuperset( + set(required_properties.keys()) + ): return None - if not {action.name for action in service.actions - }.issuperset(required_actions): + if not {action.name for action in service.actions}.issuperset( + required_actions + ): return None # 3. The required property shall have all required access mode. for prop in service.properties: if prop.name in required_properties: - if not set(prop.access).issuperset( - required_properties[prop.name]): + if not set(prop.access).issuperset(required_properties[prop.name]): return None # property for prop in service.properties: - if prop.name in set.union(set(required_properties.keys()), - optional_properties): + if prop.name in set.union( + set(required_properties.keys()), optional_properties + ): if prop.unit: prop.external_unit = self.unit_convert(prop.unit) # prop.icon = self.icon_convert(prop.unit) @@ -500,7 +529,8 @@ class MIoTDevice: return entity_data def parse_miot_service_entity( - self, miot_service: MIoTSpecService) -> Optional[MIoTEntityData]: + self, miot_service: MIoTSpecService + ) -> Optional[MIoTEntityData]: if miot_service.platform or miot_service.name not in SPEC_SERVICE_TRANS_MAP: return None service_name = miot_service.name @@ -510,25 +540,28 @@ class MIoTDevice: return None # Required properties, required access mode required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][ - "required"].get("properties", {}) - if not {prop.name for prop in miot_service.properties if prop.access - }.issuperset(set(required_properties.keys())): + "required" + ].get("properties", {}) + if not { + prop.name for prop in miot_service.properties if prop.access + }.issuperset(set(required_properties.keys())): return None for prop in miot_service.properties: if prop.name in required_properties: - if not set(prop.access).issuperset( - required_properties[prop.name]): + if not set(prop.access).issuperset(required_properties[prop.name]): return None # Required actions # Required events platform = SPEC_SERVICE_TRANS_MAP[service_name]["entity"] entity_data = MIoTEntityData(platform=platform, spec=miot_service) # Optional properties - optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][ - "optional"].get("properties", set({})) + optional_properties = SPEC_SERVICE_TRANS_MAP[service_name]["optional"].get( + "properties", set({}) + ) for prop in miot_service.properties: - if prop.name in set.union(set(required_properties.keys()), - optional_properties): + if prop.name in set.union( + set(required_properties.keys()), optional_properties + ): if prop.unit: prop.external_unit = self.unit_convert(prop.unit) # prop.icon = self.icon_convert(prop.unit) @@ -539,13 +572,16 @@ class MIoTDevice: miot_service.platform = platform # entity_category if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get( - "entity_category", None): + "entity_category", None + ): miot_service.entity_category = entity_category return entity_data def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: - if (miot_prop.platform or - miot_prop.name not in SPEC_PROP_TRANS_MAP["properties"]): + if ( + miot_prop.platform + or miot_prop.name not in SPEC_PROP_TRANS_MAP["properties"] + ): return False prop_name = miot_prop.name if isinstance(SPEC_PROP_TRANS_MAP["properties"][prop_name], str): @@ -559,20 +595,27 @@ class MIoTDevice: prop_access.add("write") if prop_access != (SPEC_PROP_TRANS_MAP["entities"][platform]["access"]): return False - if (miot_prop.format_.__name__ - not in SPEC_PROP_TRANS_MAP["entities"][platform]["format"]): + if ( + miot_prop.format_.__name__ + not in SPEC_PROP_TRANS_MAP["entities"][platform]["format"] + ): return False miot_prop.device_class = SPEC_PROP_TRANS_MAP["properties"][prop_name][ - "device_class"] + "device_class" + ] # Optional params if "state_class" in SPEC_PROP_TRANS_MAP["properties"][prop_name]: - miot_prop.state_class = SPEC_PROP_TRANS_MAP["properties"][ - prop_name]["state_class"] - if (not miot_prop.external_unit and "unit_of_measurement" - in SPEC_PROP_TRANS_MAP["properties"][prop_name]): + miot_prop.state_class = SPEC_PROP_TRANS_MAP["properties"][prop_name][ + "state_class" + ] + if ( + not miot_prop.external_unit + and "unit_of_measurement" in SPEC_PROP_TRANS_MAP["properties"][prop_name] + ): # Priority: spec_modify.unit > unit_convert > specv2entity.unit - miot_prop.external_unit = SPEC_PROP_TRANS_MAP["properties"][ - prop_name]["unit_of_measurement"] + miot_prop.external_unit = SPEC_PROP_TRANS_MAP["properties"][prop_name][ + "unit_of_measurement" + ] # Priority: default.icon when device_class is set > spec_modify.icon # > icon_convert miot_prop.platform = platform @@ -581,14 +624,12 @@ class MIoTDevice: def spec_transform(self) -> None: """Parse service, property, event, action from device spec.""" # STEP 1: device conversion - device_entity = self.parse_miot_device_entity( - spec_instance=self.spec_instance) + device_entity = self.parse_miot_device_entity(spec_instance=self.spec_instance) if device_entity: self.append_entity(entity_data=device_entity) # STEP 2: service conversion for service in self.spec_instance.services: - service_entity = self.parse_miot_service_entity( - miot_service=service) + service_entity = self.parse_miot_service_entity(miot_service=service) if service_entity: self.append_entity(entity_data=service_entity) # STEP 3.1: property conversion @@ -772,14 +813,14 @@ class MIoTDevice: if spec_unit in {"percentage"}: return "mdi:percent" if spec_unit in { - "weeks", - "days", - "hour", - "hours", - "minutes", - "seconds", - "ms", - "μs", + "weeks", + "days", + "hour", + "hours", + "minutes", + "seconds", + "ms", + "μs", }: return "mdi:clock" if spec_unit in {"celsius"}: @@ -836,13 +877,13 @@ class MIoTDevice: self._sub_id += 1 return self._sub_id - def __on_device_state_changed(self, did: str, state: MIoTDeviceState, - ctx: Any) -> None: + def __on_device_state_changed( + self, did: str, state: MIoTDeviceState, ctx: Any + ) -> None: self._online = state == MIoTDeviceState.ONLINE for key, sub_list in self._device_state_sub_list.items(): for handler in sub_list.values(): - self.miot_client.main_loop.call_soon_threadsafe( - handler, key, state) + self.miot_client.main_loop.call_soon_threadsafe(handler, key, state) class MIoTServiceEntity(Entity): @@ -859,13 +900,11 @@ class MIoTServiceEntity(Entity): _value_sub_ids: dict[str, int] _event_occurred_handler: Optional[Callable[[MIoTSpecEvent, dict], None]] - _prop_changed_subs: dict[MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], - None]] + _prop_changed_subs: dict[MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]] _pending_write_ha_state_timer: Optional[asyncio.TimerHandle] - def __init__(self, miot_device: MIoTDevice, - entity_data: MIoTEntityData) -> None: + def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData) -> None: if miot_device is None or entity_data is None or entity_data.spec is None: raise MIoTDeviceError("init error, invalid params") self.miot_device = miot_device @@ -886,7 +925,8 @@ class MIoTServiceEntity(Entity): ) self._attr_name = ( f"{'* ' if self.entity_data.spec.proprietary else ' '}" - f"{self.entity_data.spec.description_trans}") + f"{self.entity_data.spec.description_trans}" + ) self._attr_entity_category = entity_data.spec.entity_category # Set entity attr self._attr_unique_id = self.entity_id @@ -906,8 +946,7 @@ class MIoTServiceEntity(Entity): ) @property - def event_occurred_handler( - self) -> Optional[Callable[[MIoTSpecEvent, dict], None]]: + def event_occurred_handler(self) -> Optional[Callable[[MIoTSpecEvent, dict], None]]: return self._event_occurred_handler @event_occurred_handler.setter @@ -915,8 +954,8 @@ class MIoTServiceEntity(Entity): self._event_occurred_handler = func def sub_prop_changed( - self, prop: MIoTSpecProperty, - handler: Callable[[MIoTSpecProperty, Any], None]) -> None: + self, prop: MIoTSpecProperty, handler: Callable[[MIoTSpecProperty, Any], None] + ) -> None: if not prop or not handler: _LOGGER.error("sub_prop_changed error, invalid prop/handler") return @@ -934,7 +973,8 @@ class MIoTServiceEntity(Entity): if isinstance(self.entity_data.spec, MIoTSpecService): state_id = f"s.{self.entity_data.spec.iid}" self._state_sub_id = self.miot_device.sub_device_state( - key=state_id, handler=self.__on_device_state_changed) + key=state_id, handler=self.__on_device_state_changed + ) # Sub prop for prop in self.entity_data.props: if not prop.notifiable and not prop.readable: @@ -949,9 +989,8 @@ class MIoTServiceEntity(Entity): for event in self.entity_data.events: key = f"e.{event.service.iid}.{event.iid}" self._value_sub_ids[key] = self.miot_device.sub_event( - handler=self.__on_event_occurred, - siid=event.service.iid, - eiid=event.iid) + handler=self.__on_event_occurred, siid=event.service.iid, eiid=event.iid + ) # Refresh value if self._attr_available: @@ -964,34 +1003,30 @@ class MIoTServiceEntity(Entity): state_id = "s.0" if isinstance(self.entity_data.spec, MIoTSpecService): state_id = f"s.{self.entity_data.spec.iid}" - self.miot_device.unsub_device_state(key=state_id, - sub_id=self._state_sub_id) + self.miot_device.unsub_device_state(key=state_id, sub_id=self._state_sub_id) # Unsub prop for prop in self.entity_data.props: if not prop.notifiable and not prop.readable: continue - sub_id = self._value_sub_ids.pop(f"p.{prop.service.iid}.{prop.iid}", - None) + sub_id = self._value_sub_ids.pop(f"p.{prop.service.iid}.{prop.iid}", None) if sub_id: - self.miot_device.unsub_property(siid=prop.service.iid, - piid=prop.iid, - sub_id=sub_id) + self.miot_device.unsub_property( + siid=prop.service.iid, piid=prop.iid, sub_id=sub_id + ) # Unsub event for event in self.entity_data.events: - sub_id = self._value_sub_ids.pop( - f"e.{event.service.iid}.{event.iid}", None) + sub_id = self._value_sub_ids.pop(f"e.{event.service.iid}.{event.iid}", None) if sub_id: - self.miot_device.unsub_event(siid=event.service.iid, - eiid=event.iid, - sub_id=sub_id) + self.miot_device.unsub_event( + siid=event.service.iid, eiid=event.iid, sub_id=sub_id + ) def get_map_value(self, map_: Optional[dict[int, Any]], key: int) -> Any: if map_ is None: return None return map_.get(key, None) - def get_map_key(self, map_: Optional[dict[int, Any]], - value: Any) -> Optional[int]: + def get_map_key(self, map_: Optional[dict[int, Any]], value: Any) -> Optional[int]: if map_ is None: return None for key, value_ in map_.items(): @@ -1009,8 +1044,7 @@ class MIoTServiceEntity(Entity): return None return self._prop_value_map.get(prop, None) - def set_prop_value(self, prop: Optional[MIoTSpecProperty], - value: Any) -> None: + def set_prop_value(self, prop: Optional[MIoTSpecProperty], value: Any) -> None: if not prop: _LOGGER.error( "set_prop_value error, property is None, %s, %s", @@ -1033,11 +1067,15 @@ class MIoTServiceEntity(Entity): ) value = prop.value_format(value) if prop not in self.entity_data.props: - raise RuntimeError(f"set property failed, unknown property, " - f"{self.entity_id}, {self.name}, {prop.name}") + raise RuntimeError( + f"set property failed, unknown property, " + f"{self.entity_id}, {self.name}, {prop.name}" + ) if not prop.writable: - raise RuntimeError(f"set property failed, not writable, " - f"{self.entity_id}, {self.name}, {prop.name}") + raise RuntimeError( + f"set property failed, not writable, " + f"{self.entity_id}, {self.name}, {prop.name}" + ) try: await self.miot_device.miot_client.set_prop_async( did=self.miot_device.did, @@ -1047,7 +1085,8 @@ class MIoTServiceEntity(Entity): ) except MIoTClientError as e: raise RuntimeError( - f"{e}, {self.entity_id}, {self.name}, {prop.name}") from e + f"{e}, {self.entity_id}, {self.name}, {prop.name}" + ) from e if update_value: self._prop_value_map[prop] = value if write_ha_state: @@ -1064,32 +1103,40 @@ class MIoTServiceEntity(Entity): prop = set_property.get("prop") value = set_property.get("value") if not prop: - raise RuntimeError(f"set property failed, property is None, " - f"{self.entity_id}, {self.name}") + raise RuntimeError( + f"set property failed, property is None, " + f"{self.entity_id}, {self.name}" + ) set_property["value"] = prop.value_format(value) if prop not in self.entity_data.props: raise RuntimeError( f"set property failed, unknown property, " - f"{self.entity_id}, {self.name}, {prop.name}") + f"{self.entity_id}, {self.name}, {prop.name}" + ) if not prop.writable: raise RuntimeError( f"set property failed, not writable, " - f"{self.entity_id}, {self.name}, {prop.name}") + f"{self.entity_id}, {self.name}, {prop.name}" + ) try: - await self.miot_device.miot_client.set_props_async([{ - "did": self.miot_device.did, - "siid": set_property["prop"].service.iid, - "piid": set_property["prop"].iid, - "value": set_property["value"], - } for set_property in set_properties_list]) + await self.miot_device.miot_client.set_props_async( + [ + { + "did": self.miot_device.did, + "siid": set_property["prop"].service.iid, + "piid": set_property["prop"].iid, + "value": set_property["value"], + } + for set_property in set_properties_list + ] + ) except MIoTClientError as e: raise RuntimeError( f"{e}, {self.entity_id}, {self.name}, {'/'.join([set_property['prop'].name for set_property in set_properties_list])}" ) from e if update_value: for set_property in set_properties_list: - self._prop_value_map[ - set_property["prop"]] = set_property["value"] + self._prop_value_map[set_property["prop"]] = set_property["value"] if write_ha_state: self.async_write_ha_state() return True @@ -1118,22 +1165,23 @@ class MIoTServiceEntity(Entity): prop.name, ) return None - result = prop.value_format(await - self.miot_device.miot_client.get_prop_async( - did=self.miot_device.did, - siid=prop.service.iid, - piid=prop.iid)) + result = prop.value_format( + await self.miot_device.miot_client.get_prop_async( + did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid + ) + ) if result != self._prop_value_map[prop]: self._prop_value_map[prop] = result self.async_write_ha_state() return result - async def action_async(self, - action: MIoTSpecAction, - in_list: Optional[list] = None) -> bool: + async def action_async( + self, action: MIoTSpecAction, in_list: Optional[list] = None + ) -> bool: if not action: raise RuntimeError( - f"action failed, action is None, {self.entity_id}, {self.name}") + f"action failed, action is None, {self.entity_id}, {self.name}" + ) try: await self.miot_device.miot_client.action_async( did=self.miot_device.did, @@ -1143,7 +1191,8 @@ class MIoTServiceEntity(Entity): ) except MIoTClientError as e: raise RuntimeError( - f"{e}, {self.entity_id}, {self.name}, {action.name}") from e + f"{e}, {self.entity_id}, {self.name}, {action.name}" + ) from e return True def __on_properties_changed(self, params: dict, ctx: Any) -> None: @@ -1164,8 +1213,7 @@ class MIoTServiceEntity(Entity): if self._event_occurred_handler is None: return for event in self.entity_data.events: - if event.iid != params["eiid"] or event.service.iid != params[ - "siid"]: + if event.iid != params["eiid"] or event.service.iid != params["siid"]: continue trans_arg = {} for item in params["arguments"]: @@ -1176,8 +1224,7 @@ class MIoTServiceEntity(Entity): self._event_occurred_handler(event, trans_arg) break - def __on_device_state_changed(self, key: str, - state: MIoTDeviceState) -> None: + def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None: state_new = state == MIoTDeviceState.ONLINE if state_new == self._attr_available: return @@ -1192,11 +1239,13 @@ class MIoTServiceEntity(Entity): if not prop.readable: continue self.miot_device.miot_client.request_refresh_prop( - did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid) + did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid + ) if self._pending_write_ha_state_timer: self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer = self._main_loop.call_later( - 1, self.__write_ha_state_handler) + 1, self.__write_ha_state_handler + ) def __write_ha_state_handler(self) -> None: self._pending_write_ha_state_timer = None @@ -1237,17 +1286,16 @@ class MIoTPropertyEntity(Entity): self._pending_write_ha_state_timer = None # Gen entity_id self.entity_id = self.miot_device.gen_prop_entity_id( - ha_domain=DOMAIN, - spec_name=spec.name, - siid=spec.service.iid, - piid=spec.iid) + ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, piid=spec.iid + ) # Set entity attr self._attr_unique_id = self.entity_id self._attr_should_poll = False self._attr_has_entity_name = True self._attr_name = ( f"{'* ' if self.spec.proprietary else ' '}" - f"{self.service.description_trans} {spec.description_trans}") + f"{self.service.description_trans} {spec.description_trans}" + ) self._attr_available = miot_device.online _LOGGER.info( @@ -1273,9 +1321,8 @@ class MIoTPropertyEntity(Entity): ) # Sub value changed self._value_sub_id = self.miot_device.sub_property( - handler=self.__on_value_changed, - siid=self.service.iid, - piid=self.spec.iid) + handler=self.__on_value_changed, siid=self.service.iid, piid=self.spec.iid + ) # Refresh value if self._attr_available: self.__request_refresh_prop() @@ -1285,11 +1332,11 @@ class MIoTPropertyEntity(Entity): self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer = None self.miot_device.unsub_device_state( - key=f"{self.service.iid}.{self.spec.iid}", - sub_id=self._state_sub_id) - self.miot_device.unsub_property(siid=self.service.iid, - piid=self.spec.iid, - sub_id=self._value_sub_id) + key=f"{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id + ) + self.miot_device.unsub_property( + siid=self.service.iid, piid=self.spec.iid, sub_id=self._value_sub_id + ) def get_vlist_description(self, value: Any) -> Optional[str]: if not self._value_list: @@ -1322,14 +1369,15 @@ class MIoTPropertyEntity(Entity): async def get_property_async(self) -> Any: if not self.spec.readable: - _LOGGER.error("get property failed, not readable, %s, %s", - self.entity_id, self.name) + _LOGGER.error( + "get property failed, not readable, %s, %s", self.entity_id, self.name + ) return None return self.spec.value_format( await self.miot_device.miot_client.get_prop_async( - did=self.miot_device.did, - siid=self.spec.service.iid, - piid=self.spec.iid)) + did=self.miot_device.did, siid=self.spec.service.iid, piid=self.spec.iid + ) + ) def __on_value_changed(self, params: dict, ctx: Any) -> None: _LOGGER.debug("property changed, %s", params) @@ -1338,8 +1386,7 @@ class MIoTPropertyEntity(Entity): if not self._pending_write_ha_state_timer: self.async_write_ha_state() - def __on_device_state_changed(self, key: str, - state: MIoTDeviceState) -> None: + def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None: self._attr_available = state == MIoTDeviceState.ONLINE if not self._attr_available: self.async_write_ha_state() @@ -1350,13 +1397,13 @@ class MIoTPropertyEntity(Entity): def __request_refresh_prop(self) -> None: if self.spec.readable: self.miot_device.miot_client.request_refresh_prop( - did=self.miot_device.did, - siid=self.service.iid, - piid=self.spec.iid) + did=self.miot_device.did, siid=self.service.iid, piid=self.spec.iid + ) if self._pending_write_ha_state_timer: self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer = self._main_loop.call_later( - 1, self.__write_ha_state_handler) + 1, self.__write_ha_state_handler + ) def __write_ha_state_handler(self) -> None: self._pending_write_ha_state_timer = None @@ -1387,17 +1434,16 @@ class MIoTEventEntity(Entity): self._main_loop = miot_device.miot_client.main_loop # Gen entity_id self.entity_id = self.miot_device.gen_event_entity_id( - ha_domain=DOMAIN, - spec_name=spec.name, - siid=spec.service.iid, - eiid=spec.iid) + ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, eiid=spec.iid + ) # Set entity attr self._attr_unique_id = self.entity_id self._attr_should_poll = False self._attr_has_entity_name = True self._attr_name = ( f"{'* ' if self.spec.proprietary else ' '}" - f"{self.service.description_trans} {spec.description_trans}") + f"{self.service.description_trans} {spec.description_trans}" + ) self._attr_available = miot_device.online self._attr_event_types = [spec.description_trans] @@ -1428,23 +1474,21 @@ class MIoTEventEntity(Entity): ) # Sub value changed self._value_sub_id = self.miot_device.sub_event( - handler=self.__on_event_occurred, - siid=self.service.iid, - eiid=self.spec.iid) + handler=self.__on_event_occurred, siid=self.service.iid, eiid=self.spec.iid + ) async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_device_state( - key=f"event.{self.service.iid}.{self.spec.iid}", - sub_id=self._state_sub_id) - self.miot_device.unsub_event(siid=self.service.iid, - eiid=self.spec.iid, - sub_id=self._value_sub_id) + key=f"event.{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id + ) + self.miot_device.unsub_event( + siid=self.service.iid, eiid=self.spec.iid, sub_id=self._value_sub_id + ) @abstractmethod - def on_event_occurred(self, - name: str, - arguments: dict[str, Any] | None = None) -> None: - ... + def on_event_occurred( + self, name: str, arguments: dict[str, Any] | None = None + ) -> None: ... def __on_event_occurred(self, params: dict, ctx: Any) -> None: _LOGGER.debug("event occurred, %s", params) @@ -1455,8 +1499,9 @@ class MIoTEventEntity(Entity): continue if "piid" in item: trans_arg[self._arguments_map[item["piid"]]] = item["value"] - elif isinstance(item["value"], list) and len( - item["value"]) == len(self.spec.argument): + elif isinstance(item["value"], list) and len(item["value"]) == len( + self.spec.argument + ): # Dirty fix for cloud multi-arguments trans_arg = { prop.description_trans: item["value"][index] @@ -1470,12 +1515,10 @@ class MIoTEventEntity(Entity): params, error, ) - self.on_event_occurred(name=self.spec.description_trans, - arguments=trans_arg) + self.on_event_occurred(name=self.spec.description_trans, arguments=trans_arg) self.async_write_ha_state() - def __on_device_state_changed(self, key: str, - state: MIoTDeviceState) -> None: + def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None: state_new = state == MIoTDeviceState.ONLINE if state_new == self._attr_available: return @@ -1507,17 +1550,16 @@ class MIoTActionEntity(Entity): self._state_sub_id = 0 # Gen entity_id self.entity_id = self.miot_device.gen_action_entity_id( - ha_domain=DOMAIN, - spec_name=spec.name, - siid=spec.service.iid, - aiid=spec.iid) + ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, aiid=spec.iid + ) # Set entity attr self._attr_unique_id = self.entity_id self._attr_should_poll = False self._attr_has_entity_name = True self._attr_name = ( f"{'* ' if self.spec.proprietary else ' '}" - f"{self.service.description_trans} {spec.description_trans}") + f"{self.service.description_trans} {spec.description_trans}" + ) self._attr_available = miot_device.online _LOGGER.debug( @@ -1541,11 +1583,10 @@ class MIoTActionEntity(Entity): async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_device_state( - key=f"a.{self.service.iid}.{self.spec.iid}", - sub_id=self._state_sub_id) + key=f"a.{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id + ) - async def action_async(self, - in_list: Optional[list] = None) -> Optional[list]: + async def action_async(self, in_list: Optional[list] = None) -> Optional[list]: try: return await self.miot_device.miot_client.action_async( did=self.miot_device.did, @@ -1556,8 +1597,7 @@ class MIoTActionEntity(Entity): except MIoTClientError as e: raise RuntimeError(f"{e}, {self.entity_id}, {self.name}") from e - def __on_device_state_changed(self, key: str, - state: MIoTDeviceState) -> None: + def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None: state_new = state == MIoTDeviceState.ONLINE if state_new == self._attr_available: return diff --git a/custom_components/xiaomi_home/miot/miot_lan.py b/custom_components/xiaomi_home/miot/miot_lan.py index 1194a19..70f034a 100644 --- a/custom_components/xiaomi_home/miot/miot_lan.py +++ b/custom_components/xiaomi_home/miot/miot_lan.py @@ -162,18 +162,17 @@ class _MIoTLanDevice: # All functions SHOULD be called from the internal loop - def __init__(self, - manager: "MIoTLan", - did: str, - token: str, - ip: Optional[str] = None) -> None: + def __init__( + self, manager: "MIoTLan", did: str, token: str, ip: Optional[str] = None + ) -> None: self._manager: MIoTLan = manager self.did = did self.token = bytes.fromhex(token) aes_key: bytes = self.__md5(self.token) aex_iv: bytes = self.__md5(aes_key + self.token) - self.cipher = Cipher(algorithms.AES128(aes_key), modes.CBC(aex_iv), - default_backend()) + self.cipher = Cipher( + algorithms.AES128(aes_key), modes.CBC(aex_iv), default_backend() + ) self.ip = ip self.offset = 0 self.subscribed = False @@ -200,8 +199,7 @@ class _MIoTLanDevice: self.ip = ip if self._if_name != if_name: self._if_name = if_name - _LOGGER.info("device if_name change, %s, %s", self._if_name, - self.did) + _LOGGER.info("device if_name change, %s, %s", self._if_name, self.did) self.__update_keep_alive(state=_MIoTLanDeviceState.FRESH) @property @@ -215,18 +213,16 @@ class _MIoTLanDevice: self._online = online self._manager.broadcast_device_state( did=self.did, - state={ - "online": self._online, - "push_available": self.subscribed - }, + state={"online": self._online, "push_available": self.subscribed}, ) @property def if_name(self) -> Optional[str]: return self._if_name - def gen_packet(self, out_buffer: bytearray, clear_data: dict, did: str, - offset: int) -> int: + def gen_packet( + self, out_buffer: bytearray, clear_data: dict, did: str, offset: int + ) -> int: clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode("utf-8") padder = padding.PKCS7(algorithms.AES128.block_size).padder() padded_data = padder.update(clear_bytes) + padder.finalize() @@ -235,8 +231,9 @@ class _MIoTLanDevice: encryptor = self.cipher.encryptor() encrypted_data = encryptor.update(padded_data) + encryptor.finalize() data_len: int = len(encrypted_data) + self.OT_HEADER_LEN - out_buffer[:32] = struct.pack(">HHQI16s", self.OT_HEADER, data_len, - int(did), offset, self.token) + out_buffer[:32] = struct.pack( + ">HHQI16s", self.OT_HEADER, data_len, int(did), offset, self.token + ) out_buffer[32:data_len] = encrypted_data msg_md5: bytes = self.__md5(out_buffer[0:data_len]) out_buffer[16:32] = msg_md5 @@ -250,11 +247,11 @@ class _MIoTLanDevice: if md5_orig != md5_calc: raise ValueError(f"invalid md5, {md5_orig}, {md5_calc}") decryptor = self.cipher.decryptor() - decrypted_padded_data = (decryptor.update(encrypted_data[32:data_len]) + - decryptor.finalize()) + decrypted_padded_data = ( + decryptor.update(encrypted_data[32:data_len]) + decryptor.finalize() + ) unpadder = padding.PKCS7(algorithms.AES128.block_size).unpadder() - decrypted_data = unpadder.update( - decrypted_padded_data) + unpadder.finalize() + decrypted_data = unpadder.update(decrypted_padded_data) + unpadder.finalize() # Some device will add a redundant \0 at the end of JSON string decrypted_data = decrypted_data.rstrip(b"\x00") return json.loads(decrypted_data) @@ -305,10 +302,7 @@ class _MIoTLanDevice: self.subscribed = False self._manager.broadcast_device_state( did=self.did, - state={ - "online": self._online, - "push_available": self.subscribed - }, + state={"online": self._online, "push_available": self.subscribed}, ) def on_delete(self) -> None: @@ -321,35 +315,42 @@ class _MIoTLanDevice: _LOGGER.debug("miot lan device delete, %s", self.did) def update_info(self, info: dict) -> None: - if ("token" in info and len(info["token"]) == 32 and - info["token"].upper() != self.token.hex().upper()): + if ( + "token" in info + and len(info["token"]) == 32 + and info["token"].upper() != self.token.hex().upper() + ): # Update token self.token = bytes.fromhex(info["token"]) aes_key: bytes = self.__md5(self.token) aex_iv: bytes = self.__md5(aes_key + self.token) - self.cipher = Cipher(algorithms.AES128(aes_key), modes.CBC(aex_iv), - default_backend()) + self.cipher = Cipher( + algorithms.AES128(aes_key), modes.CBC(aex_iv), default_backend() + ) _LOGGER.debug("update token, %s", self.did) def __subscribe_handler(self, msg: dict, sub_ts: int) -> None: - if ("result" not in msg or "code" not in msg["result"] or - msg["result"]["code"] != 0): + if ( + "result" not in msg + or "code" not in msg["result"] + or msg["result"]["code"] != 0 + ): _LOGGER.error("subscribe device error, %s, %s", self.did, msg) return self.subscribed = True self.sub_ts = sub_ts self._manager.broadcast_device_state( did=self.did, - state={ - "online": self._online, - "push_available": self.subscribed - }, + state={"online": self._online, "push_available": self.subscribed}, ) _LOGGER.info("subscribe success, %s, %s", self._if_name, self.did) def __unsubscribe_handler(self, msg: dict, ctx: Any) -> None: - if ("result" not in msg or "code" not in msg["result"] or - msg["result"]["code"] != 0): + if ( + "result" not in msg + or "code" not in msg["result"] + or msg["result"]["code"] != 0 + ): _LOGGER.error("unsubscribe device error, %s, %s", self.did, msg) return _LOGGER.info("unsubscribe success, %s, %s", self._if_name, self.did) @@ -372,8 +373,11 @@ class _MIoTLanDevice: self.__update_keep_alive, _MIoTLanDeviceState.PING1, ) - case (_MIoTLanDeviceState.PING1 | _MIoTLanDeviceState.PING2 | - _MIoTLanDeviceState.PING3): + case ( + _MIoTLanDeviceState.PING1 + | _MIoTLanDeviceState.PING2 + | _MIoTLanDeviceState.PING3 + ): # Set the timer first to avoid Any early returns self._ka_timer = self._manager.internal_loop.call_later( self.FAST_PING_INTERVAL, @@ -411,16 +415,16 @@ class _MIoTLanDevice: if not online: self.online = False else: - if len(self._online_offline_history - ) < self.NETWORK_UNSTABLE_CNT_TH or ( - ts_now - self._online_offline_history[0]["ts"] - > self.NETWORK_UNSTABLE_TIME_TH): + if len(self._online_offline_history) < self.NETWORK_UNSTABLE_CNT_TH or ( + ts_now - self._online_offline_history[0]["ts"] + > self.NETWORK_UNSTABLE_TIME_TH + ): self.online = True else: _LOGGER.info("unstable device detected, %s", self.did) self._online_offline_timer = self._manager.internal_loop.call_later( - self.NETWORK_UNSTABLE_RESUME_TH, - self.__online_resume_handler) + self.NETWORK_UNSTABLE_RESUME_TH, self.__online_resume_handler + ) def __online_resume_handler(self) -> None: _LOGGER.info("unstable resume threshold past, %s", self.did) @@ -500,19 +504,21 @@ class MIoTLan: self._net_ifs = set(net_ifs) self._network = network self._network.sub_network_info( - key="miot_lan", - handler=self.__on_network_info_change_external_async) + key="miot_lan", handler=self.__on_network_info_change_external_async + ) self._mips_service = mips_service self._mips_service.sub_service_change( - key="miot_lan", group_id="*", handler=self.__on_mips_service_change) + key="miot_lan", group_id="*", handler=self.__on_mips_service_change + ) self._enable_subscribe = enable_subscribe - self._virtual_did = (str(virtual_did) if - (virtual_did is not None) else str( - secrets.randbits(64))) + self._virtual_did = ( + str(virtual_did) if (virtual_did is not None) else str(secrets.randbits(64)) + ) # Init socket probe message probe_bytes = bytearray(self.OT_PROBE_LEN) probe_bytes[:20] = ( - b"!1\x00\x20\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffMDID") + b"!1\x00\x20\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffMDID" + ) probe_bytes[20:28] = struct.pack(">Q", int(self._virtual_did)) probe_bytes[28:32] = b"\x00\x00\x00\x00" self._probe_msg = bytes(probe_bytes) @@ -537,16 +543,17 @@ class MIoTLan: self._init_lock = asyncio.Lock() self._init_done = False - if len(self._mips_service.get_services()) == 0 and len( - self._net_ifs) > 0: + if len(self._mips_service.get_services()) == 0 and len(self._net_ifs) > 0: _LOGGER.info("no central hub gateway service, init miot lan") self._main_loop.call_later( - 0, lambda: self._main_loop.create_task(self.init_async())) + 0, lambda: self._main_loop.create_task(self.init_async()) + ) def __assert_service_ready(self) -> None: if not self._init_done: - raise MIoTLanError("MIoT lan is not ready", - MIoTErrorCode.CODE_LAN_UNAVAILABLE) + raise MIoTLanError( + "MIoT lan is not ready", MIoTErrorCode.CODE_LAN_UNAVAILABLE + ) @property def virtual_did(self) -> str: @@ -585,8 +592,8 @@ class MIoTLan: return try: self._profile_models = await self._main_loop.run_in_executor( - None, load_yaml_file, - gen_absolute_path(self.PROFILE_MODELS_FILE)) + None, load_yaml_file, gen_absolute_path(self.PROFILE_MODELS_FILE) + ) except Exception as err: # pylint: disable=broad-exception-caught _LOGGER.error("load profile models error, %s", err) self._profile_models = {} @@ -599,14 +606,16 @@ class MIoTLan: self._init_done = True for handler in list(self._lan_state_sub_map.values()): self._main_loop.create_task(handler(True)) - _LOGGER.info("miot lan init, %s ,%s", self._net_ifs, - self._available_net_ifs) + _LOGGER.info( + "miot lan init, %s ,%s", self._net_ifs, self._available_net_ifs + ) def __internal_loop_thread(self) -> None: _LOGGER.info("miot lan thread start") self.__init_socket() self._scan_timer = self._internal_loop.call_later( - int(3 * random.random()), self.__scan_devices) + int(3 * random.random()), self.__scan_devices + ) self._internal_loop.run_forever() _LOGGER.info("miot lan thread exit") @@ -673,8 +682,8 @@ class MIoTLan: self._enable_subscribe = enable_subscribe return self._internal_loop.call_soon_threadsafe( - self.__update_subscribe_option, - {"enable_subscribe": enable_subscribe}) + self.__update_subscribe_option, {"enable_subscribe": enable_subscribe} + ) def update_devices(self, devices: dict[str, dict]) -> bool: _LOGGER.info("update devices, %s", devices) @@ -690,8 +699,7 @@ class MIoTLan: self._internal_loop.call_soon_threadsafe(self.__delete_devices, devices) return True - def sub_lan_state(self, key: str, handler: Callable[[bool], - Coroutine]) -> None: + def sub_lan_state(self, key: str, handler: Callable[[bool], Coroutine]) -> None: self._lan_state_sub_map[key] = handler def unsub_lan_state(self, key: str) -> None: @@ -708,9 +716,7 @@ class MIoTLan: return False self._internal_loop.call_soon_threadsafe( self.__sub_device_state, - _MIoTLanSubDeviceData(key=key, - handler=handler, - handler_ctx=handler_ctx), + _MIoTLanSubDeviceData(key=key, handler=handler, handler_ctx=handler_ctx), ) return True @@ -719,7 +725,8 @@ class MIoTLan: if not self._init_done: return False self._internal_loop.call_soon_threadsafe( - self.__unsub_device_state, _MIoTLanUnsubDeviceData(key=key)) + self.__unsub_device_state, _MIoTLanUnsubDeviceData(key=key) + ) return True @final @@ -738,24 +745,24 @@ class MIoTLan: key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}" self._internal_loop.call_soon_threadsafe( self.__sub_broadcast, - _MIoTLanRegisterBroadcastData(key=key, - handler=handler, - handler_ctx=handler_ctx), + _MIoTLanRegisterBroadcastData( + key=key, handler=handler, handler_ctx=handler_ctx + ), ) return True @final - def unsub_prop(self, - did: str, - siid: Optional[int] = None, - piid: Optional[int] = None) -> bool: + def unsub_prop( + self, did: str, siid: Optional[int] = None, piid: Optional[int] = None + ) -> bool: if not self._init_done: return False if not self._enable_subscribe: return False key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}" self._internal_loop.call_soon_threadsafe( - self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)) + self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key) + ) return True @final @@ -774,90 +781,80 @@ class MIoTLan: key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" self._internal_loop.call_soon_threadsafe( self.__sub_broadcast, - _MIoTLanRegisterBroadcastData(key=key, - handler=handler, - handler_ctx=handler_ctx), + _MIoTLanRegisterBroadcastData( + key=key, handler=handler, handler_ctx=handler_ctx + ), ) return True @final - def unsub_event(self, - did: str, - siid: Optional[int] = None, - eiid: Optional[int] = None) -> bool: + def unsub_event( + self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None + ) -> bool: if not self._init_done: return False if not self._enable_subscribe: return False key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" self._internal_loop.call_soon_threadsafe( - self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)) + self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key) + ) return True @final - async def get_prop_async(self, - did: str, - siid: int, - piid: int, - timeout_ms: int = 10000) -> Any: + async def get_prop_async( + self, did: str, siid: int, piid: int, timeout_ms: int = 10000 + ) -> Any: self.__assert_service_ready() result_obj = await self.__call_api_async( did=did, msg={ "method": "get_properties", - "params": [{ - "did": did, - "siid": siid, - "piid": piid - }], + "params": [{"did": did, "siid": siid, "piid": piid}], }, timeout_ms=timeout_ms, ) - if (result_obj and "result" in result_obj and - len(result_obj["result"]) == 1 and - "did" in result_obj["result"][0] and - result_obj["result"][0]["did"] == did): + if ( + result_obj + and "result" in result_obj + and len(result_obj["result"]) == 1 + and "did" in result_obj["result"][0] + and result_obj["result"][0]["did"] == did + ): return result_obj["result"][0].get("value", None) return None @final - async def set_prop_async(self, - did: str, - siid: int, - piid: int, - value: Any, - timeout_ms: int = 10000) -> dict: + async def set_prop_async( + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 + ) -> dict: self.__assert_service_ready() result_obj = await self.__call_api_async( did=did, msg={ - "method": - "set_properties", - "params": [{ - "did": did, - "siid": siid, - "piid": piid, - "value": value - }], + "method": "set_properties", + "params": [{"did": did, "siid": siid, "piid": piid, "value": value}], }, timeout_ms=timeout_ms, ) if result_obj: - if ("result" in result_obj and len(result_obj["result"]) == 1 and - "did" in result_obj["result"][0] and - result_obj["result"][0]["did"] == did and - "code" in result_obj["result"][0]): + if ( + "result" in result_obj + and len(result_obj["result"]) == 1 + and "did" in result_obj["result"][0] + and result_obj["result"][0]["did"] == did + and "code" in result_obj["result"][0] + ): return result_obj["result"][0] if "code" in result_obj: return result_obj raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR) @final - async def set_props_async(self, - did: str, - props_list: List[Dict[str, Any]], - timeout_ms: int = 10000) -> dict: + async def set_props_async( + self, did: str, props_list: List[Dict[str, Any]], timeout_ms: int = 10000 + ) -> dict: self.__assert_service_ready() result_obj = await self.__call_api_async( did=did, @@ -868,10 +865,12 @@ class MIoTLan: timeout_ms=timeout_ms, ) if result_obj: - if ("result" in result_obj and - len(result_obj["result"]) == len(props_list) and - result_obj["result"][0].get("did") == did and - all("code" in item for item in result_obj["result"])): + if ( + "result" in result_obj + and len(result_obj["result"]) == len(props_list) + and result_obj["result"][0].get("did") == did + and all("code" in item for item in result_obj["result"]) + ): return result_obj["result"] if "error" in result_obj: return result_obj["error"] @@ -881,23 +880,15 @@ class MIoTLan: } @final - async def action_async(self, - did: str, - siid: int, - aiid: int, - in_list: list, - timeout_ms: int = 10000) -> dict: + async def action_async( + self, did: str, siid: int, aiid: int, in_list: list, timeout_ms: int = 10000 + ) -> dict: self.__assert_service_ready() result_obj = await self.__call_api_async( did=did, msg={ "method": "action", - "params": { - "did": did, - "siid": siid, - "aiid": aiid, - "in": in_list - }, + "params": {"did": did, "siid": siid, "aiid": aiid, "in": in_list}, }, timeout_ms=timeout_ms, ) @@ -909,8 +900,7 @@ class MIoTLan: raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR) @final - async def get_dev_list_async(self, - timeout_ms: int = 10000) -> dict[str, dict]: + async def get_dev_list_async(self, timeout_ms: int = 10000) -> dict[str, dict]: if not self._init_done: return {} @@ -920,30 +910,28 @@ class MIoTLan: fut: asyncio.Future = self._main_loop.create_future() self._internal_loop.call_soon_threadsafe( self.__get_dev_list, - _MIoTLanGetDevListData(handler=get_device_list_handler, - handler_ctx=fut, - timeout_ms=timeout_ms), + _MIoTLanGetDevListData( + handler=get_device_list_handler, handler_ctx=fut, timeout_ms=timeout_ms + ), ) return await fut - async def __call_api_async(self, - did: str, - msg: dict, - timeout_ms: int = 10000) -> dict: - + async def __call_api_async( + self, did: str, msg: dict, timeout_ms: int = 10000 + ) -> dict: def call_api_handler(msg: dict, fut: asyncio.Future): self._main_loop.call_soon_threadsafe(fut.set_result, msg) fut: asyncio.Future = self._main_loop.create_future() - self._internal_loop.call_soon_threadsafe(self.__call_api, did, msg, - call_api_handler, fut, - timeout_ms) + self._internal_loop.call_soon_threadsafe( + self.__call_api, did, msg, call_api_handler, fut, timeout_ms + ) return await fut async def __on_network_info_change_external_async( - self, status: InterfaceStatus, info: NetworkInfo) -> None: - _LOGGER.info("on network info change, status: %s, info: %s", status, - info) + self, status: InterfaceStatus, info: NetworkInfo + ) -> None: + _LOGGER.info("on network info change, status: %s, info: %s", status, info) available_net_ifs = set() for if_name in list(self._network.network_info.keys()): available_net_ifs.add(if_name) @@ -965,11 +953,10 @@ class MIoTLan: _MIoTLanNetworkUpdateData(status=status, if_name=info.name), ) - async def __on_mips_service_change(self, group_id: str, - state: MipsServiceState, - data: dict) -> None: - _LOGGER.info("on mips service change, %s, %s, %s", group_id, state, - data) + async def __on_mips_service_change( + self, group_id: str, state: MipsServiceState, data: dict + ) -> None: + _LOGGER.info("on mips service change, %s, %s, %s", group_id, state, data) if len(self._mips_service.get_services()) > 0: _LOGGER.info("find central service, deinit miot lan") await self.deinit_async() @@ -982,10 +969,9 @@ class MIoTLan: def ping(self, if_name: Optional[str], target_ip: str) -> None: if not target_ip: return - self.__sendto(if_name=if_name, - data=self._probe_msg, - address=target_ip, - port=self.OT_PORT) + self.__sendto( + if_name=if_name, data=self._probe_msg, address=target_ip, port=self.OT_PORT + ) def send2device( self, @@ -1034,27 +1020,22 @@ class MIoTLan: handler_ctx: Any = None, timeout_ms: Optional[int] = None, ) -> None: - def request_timeout_handler(req_data: _MIoTLanRequestData): self._pending_requests.pop(req_data.msg_id, None) if req_data and req_data.handler: req_data.handler( - { - "code": MIoTErrorCode.CODE_TIMEOUT.value, - "error": "timeout" - }, + {"code": MIoTErrorCode.CODE_TIMEOUT.value, "error": "timeout"}, req_data.handler_ctx, ) timer: Optional[asyncio.TimerHandle] = None - request_data = _MIoTLanRequestData(msg_id=msg_id, - handler=handler, - handler_ctx=handler_ctx, - timeout=timer) + request_data = _MIoTLanRequestData( + msg_id=msg_id, handler=handler, handler_ctx=handler_ctx, timeout=timer + ) if timeout_ms: - timer = self._internal_loop.call_later(timeout_ms / 1000, - request_timeout_handler, - request_data) + timer = self._internal_loop.call_later( + timeout_ms / 1000, request_timeout_handler, request_data + ) request_data.timeout = timer self._pending_requests[msg_id] = request_data self.__sendto(if_name=if_name, data=msg, address=ip, port=self.OT_PORT) @@ -1085,10 +1066,7 @@ class MIoTLan: try: self.send2device( did=did, - msg={ - "from": "ha.xiaomi_home", - **msg - }, + msg={"from": "ha.xiaomi_home", **msg}, handler=handler, handler_ctx=handler_ctx, timeout_ms=timeout_ms, @@ -1096,10 +1074,7 @@ class MIoTLan: except Exception as err: # pylint: disable=broad-exception-caught _LOGGER.error("send2device error, %s", err) handler( - { - "code": MIoTErrorCode.CODE_INTERNAL_ERROR.value, - "error": str(err) - }, + {"code": MIoTErrorCode.CODE_INTERNAL_ERROR.value, "error": str(err)}, handler_ctx, ) @@ -1120,10 +1095,9 @@ class MIoTLan: def __get_dev_list(self, data: _MIoTLanGetDevListData) -> None: dev_list = { - device.did: { - "online": device.online, - "push_available": device.subscribed - } for device in self._lan_devices.values() if device.online + device.did: {"online": device.online, "push_available": device.subscribed} + for device in self._lan_devices.values() + if device.online } data.handler(dev_list, data.handler_ctx) @@ -1136,8 +1110,9 @@ class MIoTLan: if "model" not in info or info["model"] in self._profile_models: # Do not support the local control of # Profile device for the time being - _LOGGER.info("model not support local ctrl, %s, %s", did, - info.get("model")) + _LOGGER.info( + "model not support local ctrl, %s, %s", did, info.get("model") + ) continue if did not in self._lan_devices: if "token" not in info: @@ -1146,10 +1121,9 @@ class MIoTLan: if len(info["token"]) != 32: _LOGGER.error("invalid device token, %s, %s", did, info) continue - self._lan_devices[did] = _MIoTLanDevice(manager=self, - did=did, - token=info["token"], - ip=info.get("ip", None)) + self._lan_devices[did] = _MIoTLanDevice( + manager=self, did=did, token=info["token"], ip=info.get("ip", None) + ) else: self._lan_devices[did].update_info(info) @@ -1220,17 +1194,15 @@ class MIoTLan: return # Create socket try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, - socket.IPPROTO_UDP) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Set SO_BINDTODEVICE - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, - if_name.encode()) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, if_name.encode()) sock.bind(("", self._local_port or 0)) - self._internal_loop.add_reader(sock.fileno(), - self.__socket_read_handler, - (if_name, sock)) + self._internal_loop.add_reader( + sock.fileno(), self.__socket_read_handler, (if_name, sock) + ) self._broadcast_socks[if_name] = sock self._local_port = self._local_port or sock.getsockname()[1] _LOGGER.info("created socket, %s, %s", if_name, self._local_port) @@ -1252,9 +1224,9 @@ class MIoTLan: def __socket_read_handler(self, ctx: tuple[str, socket.socket]) -> None: try: - data_len, addr = ctx[1].recvfrom_into(self._read_buffer, - self.OT_MSG_LEN, - socket.MSG_DONTWAIT) + data_len, addr = ctx[1].recvfrom_into( + self._read_buffer, self.OT_MSG_LEN, socket.MSG_DONTWAIT + ) if data_len < 0: # Socket error _LOGGER.error("socket read error, %s, %s", ctx[0], data_len) @@ -1262,13 +1234,15 @@ class MIoTLan: if addr[1] != self.OT_PORT: # Not ot msg return - self.__raw_message_handler(self._read_buffer[:data_len], data_len, - addr[0], ctx[0]) + self.__raw_message_handler( + self._read_buffer[:data_len], data_len, addr[0], ctx[0] + ) except Exception as err: # pylint: disable=broad-exception-caught _LOGGER.error("socket read handler error, %s", err) - def __raw_message_handler(self, data: bytearray, data_len: int, ip: str, - if_name: str) -> None: + def __raw_message_handler( + self, data: bytearray, data_len: int, ip: str, if_name: str + ) -> None: if data[:2] != self.OT_HEADER: return # Keep alive message @@ -1282,14 +1256,22 @@ class MIoTLan: if data_len == self.OT_PROBE_LEN or device.subscribed: device.keep_alive(ip=ip, if_name=if_name) # Manage device subscribe status - if (self._enable_subscribe and data_len == self.OT_PROBE_LEN and - data[16:20] == b"MSUB" and data[24:27] == b"PUB"): - device.supported_wildcard_sub = (int( - data[28]) == self.OT_SUPPORT_WILDCARD_SUB) + if ( + self._enable_subscribe + and data_len == self.OT_PROBE_LEN + and data[16:20] == b"MSUB" + and data[24:27] == b"PUB" + ): + device.supported_wildcard_sub = ( + int(data[28]) == self.OT_SUPPORT_WILDCARD_SUB + ) sub_ts = struct.unpack(">I", data[20:24])[0] sub_type = int(data[27]) - if (device.supported_wildcard_sub and sub_type in [0, 1, 4] and - sub_ts != device.sub_ts): + if ( + device.supported_wildcard_sub + and sub_type in [0, 1, 4] + and sub_ts != device.sub_ts + ): device.subscribed = False device.subscribe() if data_len > self.OT_PROBE_LEN: @@ -1306,52 +1288,49 @@ class MIoTLan: _LOGGER.warning("invalid message, no id, %s, %s", did, msg) return # Reply - req: Optional[_MIoTLanRequestData] = self._pending_requests.pop( - msg["id"], None) + req: Optional[_MIoTLanRequestData] = self._pending_requests.pop(msg["id"], None) if req: if req.timeout: req.timeout.cancel() req.timeout = None if req.handler is not None: - self._main_loop.call_soon_threadsafe(req.handler, msg, - req.handler_ctx) + self._main_loop.call_soon_threadsafe(req.handler, msg, req.handler_ctx) return # Handle up link message if "method" not in msg or "params" not in msg: - _LOGGER.debug("invalid message, no method or params, %s, %s", did, - msg) + _LOGGER.debug("invalid message, no method or params, %s, %s", did, msg) return # Filter dup message if self.__filter_dup_message(did, msg["id"]): - self.send2device(did=did, - msg={ - "id": msg["id"], - "result": { - "code": 0 - } - }) + self.send2device(did=did, msg={"id": msg["id"], "result": {"code": 0}}) return _LOGGER.debug("lan message, %s, %s", did, msg) if msg["method"] == "properties_changed": for param in msg["params"]: if "siid" not in param and "piid" not in param: - _LOGGER.debug("invalid message, no siid or piid, %s, %s", - did, msg) + _LOGGER.debug("invalid message, no siid or piid, %s, %s", did, msg) continue key = f"{did}/p/{param['siid']}/{param['piid']}" subs: list[_MIoTLanRegisterBroadcastData] = list( - self._device_msg_matcher.iter_match(key)) + self._device_msg_matcher.iter_match(key) + ) for sub in subs: self._main_loop.call_soon_threadsafe( - sub.handler, param, sub.handler_ctx) - elif (msg["method"] == "event_occured" and "siid" in msg["params"] and - "eiid" in msg["params"]): + sub.handler, param, sub.handler_ctx + ) + elif ( + msg["method"] == "event_occured" + and "siid" in msg["params"] + and "eiid" in msg["params"] + ): key = f"{did}/e/{msg['params']['siid']}/{msg['params']['eiid']}" subs: list[_MIoTLanRegisterBroadcastData] = list( - self._device_msg_matcher.iter_match(key)) + self._device_msg_matcher.iter_match(key) + ) for sub in subs: - self._main_loop.call_soon_threadsafe(sub.handler, msg["params"], - sub.handler_ctx) + self._main_loop.call_soon_threadsafe( + sub.handler, msg["params"], sub.handler_ctx + ) else: _LOGGER.debug("invalid message, unknown method, %s, %s", did, msg) # Reply @@ -1362,12 +1341,13 @@ class MIoTLan: if filter_id in self._reply_msg_buffer: return True self._reply_msg_buffer[filter_id] = self._internal_loop.call_later( - 5, lambda filter_id: self._reply_msg_buffer.pop(filter_id, None), - filter_id) + 5, lambda filter_id: self._reply_msg_buffer.pop(filter_id, None), filter_id + ) return False - def __sendto(self, if_name: Optional[str], data: bytes, address: str, - port: int) -> None: + def __sendto( + self, if_name: Optional[str], data: bytes, address: str, port: int + ) -> None: if if_name is None: # Broadcast for if_n, sock in self._broadcast_socks.items(): @@ -1394,12 +1374,14 @@ class MIoTLan: pass scan_time = self.__get_next_scan_time() self._scan_timer = self._internal_loop.call_later( - scan_time, self.__scan_devices) + scan_time, self.__scan_devices + ) _LOGGER.debug("next scan time: %ss", scan_time) def __get_next_scan_time(self) -> float: if not self._last_scan_interval: self._last_scan_interval = self.OT_PROBE_INTERVAL_MIN - self._last_scan_interval = min(self._last_scan_interval * 2, - self.OT_PROBE_INTERVAL_MAX) + self._last_scan_interval = min( + self._last_scan_interval * 2, self.OT_PROBE_INTERVAL_MAX + ) return self._last_scan_interval diff --git a/custom_components/xiaomi_home/miot/miot_mips.py b/custom_components/xiaomi_home/miot/miot_mips.py index b8e3b37..f641cc3 100644 --- a/custom_components/xiaomi_home/miot/miot_mips.py +++ b/custom_components/xiaomi_home/miot/miot_mips.py @@ -101,17 +101,14 @@ class _MipsMessage: data_end = 0 while data_start < data_len: data_end = data_start + 5 - unpack_len, unpack_type = struct.unpack(" None: self._password = password if self._mqtt: - self._mqtt.username_pw_set(username=self._username, - password=self._password) + self._mqtt.username_pw_set(username=self._username, password=self._password) def log_debug(self, msg, *args, **kwargs) -> None: if self._logger: @@ -416,8 +412,7 @@ class _MipsClient(ABC): def enable_logger(self, logger: Optional[logging.Logger] = None) -> None: self._logger = logger - def enable_mqtt_logger(self, - logger: Optional[logging.Logger] = None) -> None: + def enable_mqtt_logger(self, logger: Optional[logging.Logger] = None) -> None: self._mqtt_logger = logger if self._mqtt: if logger: @@ -426,8 +421,9 @@ class _MipsClient(ABC): self._mqtt.disable_logger() @final - def sub_mips_state(self, key: str, handler: Callable[[str, bool], - Coroutine]) -> bool: + def sub_mips_state( + self, key: str, handler: Callable[[str, bool], Coroutine] + ) -> bool: """Subscribe mips state. NOTICE: callback to main loop thread This will be called before the client is connected. @@ -459,15 +455,12 @@ class _MipsClient(ABC): siid: Optional[int] = None, piid: Optional[int] = None, handler_ctx: Any = None, - ) -> bool: - ... + ) -> bool: ... @abstractmethod - def unsub_prop(self, - did: str, - siid: Optional[int] = None, - piid: Optional[int] = None) -> bool: - ... + def unsub_prop( + self, did: str, siid: Optional[int] = None, piid: Optional[int] = None + ) -> bool: ... @abstractmethod def sub_event( @@ -477,59 +470,41 @@ class _MipsClient(ABC): siid: Optional[int] = None, eiid: Optional[int] = None, handler_ctx: Any = None, - ) -> bool: - ... + ) -> bool: ... @abstractmethod - def unsub_event(self, - did: str, - siid: Optional[int] = None, - eiid: Optional[int] = None) -> bool: - ... + def unsub_event( + self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None + ) -> bool: ... @abstractmethod - async def get_dev_list_async(self, - payload: Optional[str] = None, - timeout_ms: int = 10000) -> dict[str, dict]: - ... + async def get_dev_list_async( + self, payload: Optional[str] = None, timeout_ms: int = 10000 + ) -> dict[str, dict]: ... @abstractmethod - async def get_prop_async(self, - did: str, - siid: int, - piid: int, - timeout_ms: int = 10000) -> Any: - ... + async def get_prop_async( + self, did: str, siid: int, piid: int, timeout_ms: int = 10000 + ) -> Any: ... @abstractmethod - async def set_prop_async(self, - did: str, - siid: int, - piid: int, - value: Any, - timeout_ms: int = 10000) -> dict: - ... + async def set_prop_async( + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 + ) -> dict: ... @abstractmethod - async def action_async(self, - did: str, - siid: int, - aiid: int, - in_list: list, - timeout_ms: int = 10000) -> dict: - ... + async def action_async( + self, did: str, siid: int, aiid: int, in_list: list, timeout_ms: int = 10000 + ) -> dict: ... @abstractmethod - def _on_mips_message(self, topic: str, payload: bytes) -> None: - ... + def _on_mips_message(self, topic: str, payload: bytes) -> None: ... @abstractmethod - def _on_mips_connect(self, rc: int, props: dict) -> None: - ... + def _on_mips_connect(self, rc: int, props: dict) -> None: ... @abstractmethod - def _on_mips_disconnect(self, rc: int, props: dict) -> None: - ... + def _on_mips_disconnect(self, rc: int, props: dict) -> None: ... @final def _mips_sub_internal(self, topic: str) -> None: @@ -544,7 +519,8 @@ class _MipsClient(ABC): self._mips_sub_pending_map[topic] = 0 if not self._mips_sub_pending_timer: self._mips_sub_pending_timer = self._internal_loop.call_later( - 0.01, self.__mips_sub_internal_pending_handler, topic) + 0.01, self.__mips_sub_internal_pending_handler, topic + ) except Exception as err: # pylint: disable=broad-exception-caught # Catch all exception self.log_error(f"mips sub internal error, {topic}. {err}") @@ -560,11 +536,9 @@ class _MipsClient(ABC): try: result, mid = self._mqtt.unsubscribe(topic=topic) if result == MQTT_ERR_SUCCESS: - self.log_debug( - f"mips unsub internal success, {result}, {mid}, {topic}") + self.log_debug(f"mips unsub internal success, {result}, {mid}, {topic}") return - self.log_error( - f"mips unsub internal error, {result}, {mid}, {topic}") + self.log_error(f"mips unsub internal error, {result}, {mid}, {topic}") except Exception as err: # pylint: disable=broad-exception-caught # Catch all exception self.log_error(f"mips unsub internal error, {topic}, {err}") @@ -585,9 +559,7 @@ class _MipsClient(ABC): if not self._mqtt or not self._mqtt.is_connected(): return False try: - handle = self._mqtt.publish(topic=topic, - payload=payload, - qos=self.MIPS_QOS) + handle = self._mqtt.publish(topic=topic, payload=payload, qos=self.MIPS_QOS) # self.log_debug(f'_mips_publish_internal, {topic}, {payload}') if wait_for_publish is True: handle.wait_for_publish(timeout_ms / 1000.0) @@ -612,7 +584,8 @@ class _MipsClient(ABC): self.__mqtt_loop_handler() if self._mqtt: self._mqtt_timer = self._internal_loop.call_later( - self.MQTT_INTERVAL_S, self.__mqtt_timer_handler) + self.MQTT_INTERVAL_S, self.__mqtt_timer_handler + ) def __mqtt_loop_handler(self) -> None: try: @@ -623,8 +596,7 @@ class _MipsClient(ABC): if self._mqtt: self._mqtt.loop_misc() if self._mqtt and self._mqtt.want_write(): - self._internal_loop.add_writer(self._mqtt_fd, - self.__mqtt_write_handler) + self._internal_loop.add_writer(self._mqtt_fd, self.__mqtt_write_handler) except Exception as err: # pylint: disable=broad-exception-caught # Catch all exception self.log_error(f"__mqtt_loop_handler, {err}") @@ -638,8 +610,7 @@ class _MipsClient(ABC): self._mqtt.enable_logger(logger=self._mqtt_logger) # Set mqtt config if self._username: - self._mqtt.username_pw_set(username=self._username, - password=self._password) + self._mqtt.username_pw_set(username=self._username, password=self._password) if self._ca_file and self._cert_file and self._key_file: self._mqtt.tls_set( tls_version=ssl.PROTOCOL_TLS_CLIENT, @@ -674,7 +645,8 @@ class _MipsClient(ABC): if item.handler is None: continue self.main_loop.call_soon_threadsafe( - self.main_loop.create_task, item.handler(item.key, True)) + self.main_loop.create_task, item.handler(item.key, True) + ) # Resolve future self.main_loop.call_soon_threadsafe(self._event_connect.set) self.main_loop.call_soon_threadsafe(self._event_disconnect.clear) @@ -686,8 +658,9 @@ class _MipsClient(ABC): def __on_disconnect(self, client, user_data, rc, props) -> None: if self._mqtt_state: - (self.log_info - if rc == 0 else self.log_error)(f"mips disconnect, {rc}, {props}") + (self.log_info if rc == 0 else self.log_error)( + f"mips disconnect, {rc}, {props}" + ) self._mqtt_state = False if self._mqtt_timer: self._mqtt_timer.cancel() @@ -708,8 +681,8 @@ class _MipsClient(ABC): if item.handler is None: continue self.main_loop.call_soon_threadsafe( - self.main_loop.create_task, - item.handler(item.key, False)) + self.main_loop.create_task, item.handler(item.key, False) + ) # Try to reconnect self.__mips_try_reconnect() @@ -717,14 +690,12 @@ class _MipsClient(ABC): self.main_loop.call_soon_threadsafe(self._event_disconnect.set) self.main_loop.call_soon_threadsafe(self._event_connect.clear) - def __on_message(self, client: Client, user_data: Any, - msg: MQTTMessage) -> None: + def __on_message(self, client: Client, user_data: Any, msg: MQTTMessage) -> None: self._on_mips_message(topic=msg.topic, payload=msg.payload) def __mips_sub_internal_pending_handler(self, ctx: Any) -> None: if not self._mqtt or not self._mqtt.is_connected(): - _LOGGER.error( - "mips sub internal pending, but mqtt is None or disconnected") + _LOGGER.error("mips sub internal pending, but mqtt is None or disconnected") return subbed_count = 1 for topic in list(self._mips_sub_pending_map.keys()): @@ -743,12 +714,13 @@ class _MipsClient(ABC): continue self._mips_sub_pending_map[topic] = count + 1 self.log_error( - f"retry mips sub internal, {count}, {topic}, {result}, {mid}") + f"retry mips sub internal, {count}, {topic}, {result}, {mid}" + ) if len(self._mips_sub_pending_map): self._mips_sub_pending_timer = self._internal_loop.call_later( - self.MIPS_SUB_INTERVAL, - self.__mips_sub_internal_pending_handler, None) + self.MIPS_SUB_INTERVAL, self.__mips_sub_internal_pending_handler, None + ) else: self._mips_sub_pending_timer = None @@ -782,19 +754,17 @@ class _MipsClient(ABC): if result == MQTT_ERR_SUCCESS: socket = self._mqtt.socket() if socket is None: - self.log_error( - "__mips_connect, connect success, but socket is None") + self.log_error("__mips_connect, connect success, but socket is None") self.__mips_try_reconnect() return self._mqtt_fd = socket.fileno() self.log_debug(f"__mips_connect, _mqtt_fd, {self._mqtt_fd}") - self._internal_loop.add_reader(self._mqtt_fd, - self.__mqtt_read_handler) + self._internal_loop.add_reader(self._mqtt_fd, self.__mqtt_read_handler) if self._mqtt.want_write(): - self._internal_loop.add_writer(self._mqtt_fd, - self.__mqtt_write_handler) + self._internal_loop.add_writer(self._mqtt_fd, self.__mqtt_write_handler) self._mqtt_timer = self._internal_loop.call_later( - self.MQTT_INTERVAL_S, self.__mqtt_timer_handler) + self.MQTT_INTERVAL_S, self.__mqtt_timer_handler + ) else: self.log_error(f"__mips_connect error result, {result}") self.__mips_try_reconnect() @@ -810,7 +780,8 @@ class _MipsClient(ABC): interval = self.__get_next_reconnect_time() self.log_error("mips try reconnect after %ss", interval) self._mips_reconnect_timer = self._internal_loop.call_later( - interval, self.__mips_connect) + interval, self.__mips_connect + ) def __mips_start_connect_tries(self) -> None: self._mips_reconnect_tag = True @@ -843,8 +814,8 @@ class _MipsClient(ABC): self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN else: self._mips_reconnect_interval = min( - self._mips_reconnect_interval * 2, - self.MIPS_RECONNECT_INTERVAL_MAX) + self._mips_reconnect_interval * 2, self.MIPS_RECONNECT_INTERVAL_MAX + ) return self._mips_reconnect_interval @@ -899,7 +870,8 @@ class MipsCloudClient(_MipsClient): topic: str = ( f"device/{did}/up/properties_changed/" - f"{'#' if siid is None or piid is None else f'{siid}/{piid}'}") + f"{'#' if siid is None or piid is None else f'{siid}/{piid}'}" + ) def on_prop_msg(topic: str, payload: str, ctx: Any) -> None: try: @@ -907,30 +879,32 @@ class MipsCloudClient(_MipsClient): except json.JSONDecodeError: self.log_error(f"on_prop_msg, invalid msg, {topic}, {payload}") return - if (not isinstance(msg.get("params", None), dict) or - "siid" not in msg["params"] or - "piid" not in msg["params"] or - "value" not in msg["params"]): + if ( + not isinstance(msg.get("params", None), dict) + or "siid" not in msg["params"] + or "piid" not in msg["params"] + or "value" not in msg["params"] + ): self.log_error(f"on_prop_msg, invalid msg, {topic}, {payload}") return if handler: self.log_debug("on properties_changed, %s", payload) handler(msg["params"], ctx) - return self.__reg_broadcast_external(topic=topic, - handler=on_prop_msg, - handler_ctx=handler_ctx) + return self.__reg_broadcast_external( + topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx + ) @final - def unsub_prop(self, - did: str, - siid: Optional[int] = None, - piid: Optional[int] = None) -> bool: + def unsub_prop( + self, did: str, siid: Optional[int] = None, piid: Optional[int] = None + ) -> bool: if not isinstance(did, str): raise MIoTMipsError("invalid params") topic: str = ( f"device/{did}/up/properties_changed/" - f"{'#' if siid is None or piid is None else f'{siid}/{piid}'}") + f"{'#' if siid is None or piid is None else f'{siid}/{piid}'}" + ) return self.__unreg_broadcast_external(topic=topic) @final @@ -947,7 +921,8 @@ class MipsCloudClient(_MipsClient): # Spelling error: event_occured topic: str = ( f"device/{did}/up/event_occured/" - f"{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}") + f"{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" + ) def on_event_msg(topic: str, payload: str, ctx: Any) -> None: try: @@ -955,10 +930,12 @@ class MipsCloudClient(_MipsClient): except json.JSONDecodeError: self.log_error(f"on_event_msg, invalid msg, {topic}, {payload}") return - if (not isinstance(msg.get("params", None), dict) or - "siid" not in msg["params"] or - "eiid" not in msg["params"] or - "arguments" not in msg["params"]): + if ( + not isinstance(msg.get("params", None), dict) + or "siid" not in msg["params"] + or "eiid" not in msg["params"] + or "arguments" not in msg["params"] + ): self.log_error(f"on_event_msg, invalid msg, {topic}, {payload}") return if handler: @@ -966,21 +943,21 @@ class MipsCloudClient(_MipsClient): msg["params"]["from"] = "cloud" handler(msg["params"], ctx) - return self.__reg_broadcast_external(topic=topic, - handler=on_event_msg, - handler_ctx=handler_ctx) + return self.__reg_broadcast_external( + topic=topic, handler=on_event_msg, handler_ctx=handler_ctx + ) @final - def unsub_event(self, - did: str, - siid: Optional[int] = None, - eiid: Optional[int] = None) -> bool: + def unsub_event( + self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None + ) -> bool: if not isinstance(did, str): raise MIoTMipsError("invalid params") # Spelling error: event_occured topic: str = ( f"device/{did}/up/event_occured/" - f"{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}") + f"{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" + ) return self.__unreg_broadcast_external(topic=topic) @final @@ -1003,21 +980,21 @@ class MipsCloudClient(_MipsClient): self.log_error(f"on_state_msg, recv unknown msg, {payload}") return if msg["device_id"] != did: - self.log_error( - f"on_state_msg, err msg, {did}!={msg['device_id']}") + self.log_error(f"on_state_msg, err msg, {did}!={msg['device_id']}") return if handler: self.log_debug("cloud, device state changed, %s", payload) handler( did, MIoTDeviceState.ONLINE - if msg["event"] == "online" else MIoTDeviceState.OFFLINE, + if msg["event"] == "online" + else MIoTDeviceState.OFFLINE, ctx, ) - return self.__reg_broadcast_external(topic=topic, - handler=on_state_msg, - handler_ctx=handler_ctx) + return self.__reg_broadcast_external( + topic=topic, handler=on_state_msg, handler_ctx=handler_ctx + ) @final def unsub_device_state(self, did: str) -> bool: @@ -1026,32 +1003,24 @@ class MipsCloudClient(_MipsClient): topic: str = f"device/{did}/state/#" return self.__unreg_broadcast_external(topic=topic) - async def get_dev_list_async(self, - payload: Optional[str] = None, - timeout_ms: int = 10000) -> dict[str, dict]: + async def get_dev_list_async( + self, payload: Optional[str] = None, timeout_ms: int = 10000 + ) -> dict[str, dict]: raise NotImplementedError("please call in http client") - async def get_prop_async(self, - did: str, - siid: int, - piid: int, - timeout_ms: int = 10000) -> Any: + async def get_prop_async( + self, did: str, siid: int, piid: int, timeout_ms: int = 10000 + ) -> Any: raise NotImplementedError("please call in http client") - async def set_prop_async(self, - did: str, - siid: int, - piid: int, - value: Any, - timeout_ms: int = 10000) -> dict: + async def set_prop_async( + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 + ) -> dict: raise NotImplementedError("please call in http client") - async def action_async(self, - did: str, - siid: int, - aiid: int, - in_list: list, - timeout_ms: int = 10000) -> dict: + async def action_async( + self, did: str, siid: int, aiid: int, in_list: list, timeout_ms: int = 10000 + ) -> dict: raise NotImplementedError("please call in http client") def __reg_broadcast_external( @@ -1060,8 +1029,9 @@ class MipsCloudClient(_MipsClient): handler: Callable[[str, str, Any], None], handler_ctx: Any = None, ) -> bool: - self._internal_loop.call_soon_threadsafe(self.__reg_broadcast, topic, - handler, handler_ctx) + self._internal_loop.call_soon_threadsafe( + self.__reg_broadcast, topic, handler, handler_ctx + ) return True def __unreg_broadcast_external(self, topic: str) -> bool: @@ -1075,9 +1045,9 @@ class MipsCloudClient(_MipsClient): handler_ctx: Any = None, ) -> None: if not self._msg_matcher.get(topic=topic): - sub_bc: _MipsBroadcast = _MipsBroadcast(topic=topic, - handler=handler, - handler_ctx=handler_ctx) + sub_bc: _MipsBroadcast = _MipsBroadcast( + topic=topic, handler=handler, handler_ctx=handler_ctx + ) self._msg_matcher[topic] = sub_bc self._mips_sub_internal(topic=topic) else: @@ -1102,8 +1072,7 @@ class MipsCloudClient(_MipsClient): NOTICE thread safe, this function will be called at the **mips** thread """ # broadcast - bc_list: list[_MipsBroadcast] = list( - self._msg_matcher.iter_match(topic)) + bc_list: list[_MipsBroadcast] = list(self._msg_matcher.iter_match(topic)) if not bc_list: return # The message from the cloud is not packed. @@ -1113,8 +1082,9 @@ class MipsCloudClient(_MipsClient): if item.handler is None: continue # NOTICE: call threadsafe - self.main_loop.call_soon_threadsafe(item.handler, topic, - payload_str, item.handler_ctx) + self.main_loop.call_soon_threadsafe( + item.handler, topic, payload_str, item.handler_ctx + ) class MipsLocalClient(_MipsClient): @@ -1210,30 +1180,36 @@ class MipsLocalClient(_MipsClient): ) -> bool: topic: str = ( f"appMsg/notify/iot/{did}/property/" - f"{'#' if siid is None or piid is None else f'{siid}.{piid}'}") + f"{'#' if siid is None or piid is None else f'{siid}.{piid}'}" + ) def on_prop_msg(topic: str, payload: str, ctx: Any): msg: dict = json.loads(payload) - if (msg is None or "did" not in msg or "siid" not in msg or - "piid" not in msg or "value" not in msg): + if ( + msg is None + or "did" not in msg + or "siid" not in msg + or "piid" not in msg + or "value" not in msg + ): # self.log_error(f'on_prop_msg, recv unknown msg, {payload}') return if handler: self.log_debug("local, on properties_changed, %s", payload) handler(msg, ctx) - return self.__reg_broadcast_external(topic=topic, - handler=on_prop_msg, - handler_ctx=handler_ctx) + return self.__reg_broadcast_external( + topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx + ) @final - def unsub_prop(self, - did: str, - siid: Optional[int] = None, - piid: Optional[int] = None) -> bool: + def unsub_prop( + self, did: str, siid: Optional[int] = None, piid: Optional[int] = None + ) -> bool: topic: str = ( f"appMsg/notify/iot/{did}/property/" - f"{'#' if siid is None or piid is None else f'{siid}.{piid}'}") + f"{'#' if siid is None or piid is None else f'{siid}.{piid}'}" + ) return self.__unreg_broadcast_external(topic=topic) @final @@ -1247,14 +1223,18 @@ class MipsLocalClient(_MipsClient): ) -> bool: topic: str = ( f"appMsg/notify/iot/{did}/event/" - f"{'#' if siid is None or eiid is None else f'{siid}.{eiid}'}") + f"{'#' if siid is None or eiid is None else f'{siid}.{eiid}'}" + ) def on_event_msg(topic: str, payload: str, ctx: Any): msg: dict = json.loads(payload) - if (msg is None or "did" not in msg or "siid" not in msg or - "eiid" not in msg - # or 'arguments' not in msg - ): + if ( + msg is None + or "did" not in msg + or "siid" not in msg + or "eiid" not in msg + # or 'arguments' not in msg + ): self.log_info("unknown event msg, %s", payload) return if "arguments" not in msg: @@ -1264,55 +1244,46 @@ class MipsLocalClient(_MipsClient): self.log_debug("local, on event_occurred, %s", payload) handler(msg, ctx) - return self.__reg_broadcast_external(topic=topic, - handler=on_event_msg, - handler_ctx=handler_ctx) + return self.__reg_broadcast_external( + topic=topic, handler=on_event_msg, handler_ctx=handler_ctx + ) @final - def unsub_event(self, - did: str, - siid: Optional[int] = None, - eiid: Optional[int] = None) -> bool: + def unsub_event( + self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None + ) -> bool: topic: str = ( f"appMsg/notify/iot/{did}/event/" - f"{'#' if siid is None or eiid is None else f'{siid}.{eiid}'}") + f"{'#' if siid is None or eiid is None else f'{siid}.{eiid}'}" + ) return self.__unreg_broadcast_external(topic=topic) @final - async def get_prop_safe_async(self, - did: str, - siid: int, - piid: int, - timeout_ms: int = 10000) -> Any: + async def get_prop_safe_async( + self, did: str, siid: int, piid: int, timeout_ms: int = 10000 + ) -> Any: self._get_prop_queue.setdefault(did, []) fut: asyncio.Future = self.main_loop.create_future() - self._get_prop_queue[did].append({ - "param": json.dumps({ - "did": did, - "siid": siid, - "piid": piid - }), - "fut": fut, - "timeout_ms": timeout_ms, - }) + self._get_prop_queue[did].append( + { + "param": json.dumps({"did": did, "siid": siid, "piid": piid}), + "fut": fut, + "timeout_ms": timeout_ms, + } + ) if self._get_prop_timer is None: self._get_prop_timer = self.main_loop.call_later( - 0.1, self.main_loop.create_task, self.__get_prop_timer_handle()) + 0.1, self.main_loop.create_task, self.__get_prop_timer_handle() + ) return await fut @final - async def get_prop_async(self, - did: str, - siid: int, - piid: int, - timeout_ms: int = 10000) -> Any: + async def get_prop_async( + self, did: str, siid: int, piid: int, timeout_ms: int = 10000 + ) -> Any: result_obj = await self.__request_async( topic="proxy/get", - payload=json.dumps({ - "did": did, - "siid": siid, - "piid": piid - }), + payload=json.dumps({"did": did, "siid": siid, "piid": piid}), timeout_ms=timeout_ms, ) if not isinstance(result_obj, dict) or "value" not in result_obj: @@ -1320,35 +1291,28 @@ class MipsLocalClient(_MipsClient): return result_obj["value"] @final - async def set_prop_async(self, - did: str, - siid: int, - piid: int, - value: Any, - timeout_ms: int = 10000) -> dict: + async def set_prop_async( + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 + ) -> dict: payload_obj: dict = { "did": did, "rpc": { - "id": - self.__gen_mips_id, - "method": - "set_properties", - "params": [{ - "did": did, - "siid": siid, - "piid": piid, - "value": value - }], + "id": self.__gen_mips_id, + "method": "set_properties", + "params": [{"did": did, "siid": siid, "piid": piid, "value": value}], }, } - result_obj = await self.__request_async(topic="proxy/rpcReq", - payload=json.dumps(payload_obj), - timeout_ms=timeout_ms) + result_obj = await self.__request_async( + topic="proxy/rpcReq", payload=json.dumps(payload_obj), timeout_ms=timeout_ms + ) if result_obj: - if ("result" in result_obj and len(result_obj["result"]) == 1 and - "did" in result_obj["result"][0] and - result_obj["result"][0]["did"] == did and - "code" in result_obj["result"][0]): + if ( + "result" in result_obj + and len(result_obj["result"]) == 1 + and "did" in result_obj["result"][0] + and result_obj["result"][0]["did"] == did + and "code" in result_obj["result"][0] + ): return result_obj["result"][0] if "error" in result_obj: return result_obj["error"] @@ -1358,10 +1322,9 @@ class MipsLocalClient(_MipsClient): } @final - async def set_props_async(self, - did: str, - props_list: List[Dict[str, Any]], - timeout_ms: int = 10000) -> dict: + async def set_props_async( + self, did: str, props_list: List[Dict[str, Any]], timeout_ms: int = 10000 + ) -> dict: payload_obj: dict = { "did": did, "rpc": { @@ -1370,14 +1333,16 @@ class MipsLocalClient(_MipsClient): "params": props_list, }, } - result_obj = await self.__request_async(topic="proxy/rpcReq", - payload=json.dumps(payload_obj), - timeout_ms=timeout_ms) + result_obj = await self.__request_async( + topic="proxy/rpcReq", payload=json.dumps(payload_obj), timeout_ms=timeout_ms + ) if result_obj: - if ("result" in result_obj and - len(result_obj["result"]) == len(props_list) and - result_obj["result"][0].get("did") == did and - all("code" in item for item in result_obj["result"])): + if ( + "result" in result_obj + and len(result_obj["result"]) == len(props_list) + and result_obj["result"][0].get("did") == did + and all("code" in item for item in result_obj["result"]) + ): return result_obj["result"] if "error" in result_obj: return result_obj["error"] @@ -1387,28 +1352,20 @@ class MipsLocalClient(_MipsClient): } @final - async def action_async(self, - did: str, - siid: int, - aiid: int, - in_list: list, - timeout_ms: int = 10000) -> dict: + async def action_async( + self, did: str, siid: int, aiid: int, in_list: list, timeout_ms: int = 10000 + ) -> dict: payload_obj: dict = { "did": did, "rpc": { "id": self.__gen_mips_id, "method": "action", - "params": { - "did": did, - "siid": siid, - "aiid": aiid, - "in": in_list - }, + "params": {"did": did, "siid": siid, "aiid": aiid, "in": in_list}, }, } - result_obj = await self.__request_async(topic="proxy/rpcReq", - payload=json.dumps(payload_obj), - timeout_ms=timeout_ms) + result_obj = await self.__request_async( + topic="proxy/rpcReq", payload=json.dumps(payload_obj), timeout_ms=timeout_ms + ) if result_obj: if "result" in result_obj and "code" in result_obj["result"]: return result_obj["result"] @@ -1420,12 +1377,12 @@ class MipsLocalClient(_MipsClient): } @final - async def get_dev_list_async(self, - payload: Optional[str] = None, - timeout_ms: int = 10000) -> dict[str, dict]: - result_obj = await self.__request_async(topic="proxy/getDevList", - payload=payload or "{}", - timeout_ms=timeout_ms) + async def get_dev_list_async( + self, payload: Optional[str] = None, timeout_ms: int = 10000 + ) -> dict[str, dict]: + result_obj = await self.__request_async( + topic="proxy/getDevList", payload=payload or "{}", timeout_ms=timeout_ms + ) if not result_obj or "devList" not in result_obj: raise MIoTMipsError("invalid result") device_list = {} @@ -1455,20 +1412,18 @@ class MipsLocalClient(_MipsClient): return device_list @final - async def get_action_group_list_async(self, - timeout_ms: int = 10000) -> list[str]: + async def get_action_group_list_async(self, timeout_ms: int = 10000) -> list[str]: result_obj = await self.__request_async( - topic="proxy/getMijiaActionGroupList", - payload="{}", - timeout_ms=timeout_ms) + topic="proxy/getMijiaActionGroupList", payload="{}", timeout_ms=timeout_ms + ) if not result_obj or "result" not in result_obj: raise MIoTMipsError("invalid result") return result_obj["result"] @final - async def exec_action_group_list_async(self, - ag_id: str, - timeout_ms: int = 10000) -> dict: + async def exec_action_group_list_async( + self, ag_id: str, timeout_ms: int = 10000 + ) -> dict: result_obj = await self.__request_async( topic="proxy/execMijiaActionGroup", payload=f'{{"id":"{ag_id}"}}', @@ -1486,15 +1441,14 @@ class MipsLocalClient(_MipsClient): @final @property - def on_dev_list_changed( - self) -> Optional[Callable[[Any, list[str]], Coroutine]]: + def on_dev_list_changed(self) -> Optional[Callable[[Any, list[str]], Coroutine]]: return self._on_dev_list_changed @final @on_dev_list_changed.setter def on_dev_list_changed( - self, func: Optional[Callable[[Any, list[str]], - Coroutine]]) -> None: + self, func: Optional[Callable[[Any, list[str]], Coroutine]] + ) -> None: """run in main loop.""" self._on_dev_list_changed = func @@ -1513,32 +1467,35 @@ class MipsLocalClient(_MipsClient): timer=None, ) pub_topic: str = f"master/{topic}" - result = self.__mips_publish(topic=pub_topic, - payload=payload, - mid=req.mid, - ret_topic=self._reply_topic) + result = self.__mips_publish( + topic=pub_topic, payload=payload, mid=req.mid, ret_topic=self._reply_topic + ) self.log_debug( - f"mips local call api, {result}, {req.mid}, {pub_topic}, {payload}") + f"mips local call api, {result}, {req.mid}, {pub_topic}, {payload}" + ) def on_request_timeout(req: _MipsRequest): self.log_error( - f"on mips request timeout, {req.mid}, {pub_topic}, {payload}") + f"on mips request timeout, {req.mid}, {pub_topic}, {payload}" + ) self._request_map.pop(str(req.mid), None) - req.on_reply('{"error":{"code":-10006, "message":"timeout"}}', - req.on_reply_ctx) + req.on_reply( + '{"error":{"code":-10006, "message":"timeout"}}', req.on_reply_ctx + ) - req.timer = self._internal_loop.call_later(timeout_ms / 1000, - on_request_timeout, req) + req.timer = self._internal_loop.call_later( + timeout_ms / 1000, on_request_timeout, req + ) self._request_map[str(req.mid)] = req - def __reg_broadcast(self, topic: str, handler: Callable[[str, str, Any], - None], - handler_ctx: Any) -> None: + def __reg_broadcast( + self, topic: str, handler: Callable[[str, str, Any], None], handler_ctx: Any + ) -> None: sub_topic: str = f"{self._did}/{topic}" if not self._msg_matcher.get(sub_topic): - sub_bc: _MipsBroadcast = _MipsBroadcast(topic=sub_topic, - handler=handler, - handler_ctx=handler_ctx) + sub_bc: _MipsBroadcast = _MipsBroadcast( + topic=sub_topic, handler=handler, handler_ctx=handler_ctx + ) self._msg_matcher[sub_topic] = sub_bc self._mips_sub_internal(topic=f"master/{topic}") else: @@ -1550,7 +1507,8 @@ class MipsLocalClient(_MipsClient): if self._msg_matcher.get(unsub_topic): del self._msg_matcher[unsub_topic] self._mips_unsub_internal( - topic=re.sub(f"^{self._did}", "master", unsub_topic)) + topic=re.sub(f"^{self._did}", "master", unsub_topic) + ) @final def _on_mips_connect(self, rc: int, props: dict) -> None: @@ -1563,8 +1521,7 @@ class MipsLocalClient(_MipsClient): # Sub api topic. # Sub broadcast topic for topic, _ in list(self._msg_matcher.iter_all_nodes()): - self._mips_sub_internal( - topic=re.sub(f"^{self._did}", "master", topic)) + self._mips_sub_internal(topic=re.sub(f"^{self._did}", "master", topic)) @final def _on_mips_disconnect(self, rc: int, props: dict) -> None: @@ -1578,20 +1535,18 @@ class MipsLocalClient(_MipsClient): # Reply if topic == self._reply_topic: self.log_debug(f"on request reply, {mips_msg}") - req: Optional[_MipsRequest] = self._request_map.pop( - str(mips_msg.mid), None) + req: Optional[_MipsRequest] = self._request_map.pop(str(mips_msg.mid), None) if req: # Cancel timer if req.timer: req.timer.cancel() if req.on_reply: self.main_loop.call_soon_threadsafe( - req.on_reply, mips_msg.payload or "{}", - req.on_reply_ctx) + req.on_reply, mips_msg.payload or "{}", req.on_reply_ctx + ) return # Broadcast - bc_list: list[_MipsBroadcast] = list( - self._msg_matcher.iter_match(topic=topic)) + bc_list: list[_MipsBroadcast] = list(self._msg_matcher.iter_match(topic=topic)) if bc_list: self.log_debug(f"on broadcast, {topic}, {mips_msg}") for item in bc_list or []: @@ -1599,7 +1554,7 @@ class MipsLocalClient(_MipsClient): continue self.main_loop.call_soon_threadsafe( item.handler, - topic[topic.find("/") + 1:], + topic[topic.find("/") + 1 :], mips_msg.payload or "{}", item.handler_ctx, ) @@ -1621,8 +1576,7 @@ class MipsLocalClient(_MipsClient): ) return - self.log_debug( - f"mips local client, recv unknown msg, {topic} -> {mips_msg}") + self.log_debug(f"mips local client, recv unknown msg, {topic} -> {mips_msg}") @property def __gen_mips_id(self) -> int: @@ -1662,16 +1616,17 @@ class MipsLocalClient(_MipsClient): ) -> bool: if topic is None or payload is None or on_reply is None: raise MIoTMipsError("invalid params") - self._internal_loop.call_soon_threadsafe(self.__request, topic, payload, - on_reply, on_reply_ctx, - timeout_ms) + self._internal_loop.call_soon_threadsafe( + self.__request, topic, payload, on_reply, on_reply_ctx, timeout_ms + ) return True - def __reg_broadcast_external(self, topic: str, - handler: Callable[[str, str, Any], None], - handler_ctx: Any) -> bool: - self._internal_loop.call_soon_threadsafe(self.__reg_broadcast, topic, - handler, handler_ctx) + def __reg_broadcast_external( + self, topic: str, handler: Callable[[str, str, Any], None], handler_ctx: Any + ) -> bool: + self._internal_loop.call_soon_threadsafe( + self.__reg_broadcast, topic, handler, handler_ctx + ) return True def __unreg_broadcast_external(self, topic) -> bool: @@ -1679,10 +1634,9 @@ class MipsLocalClient(_MipsClient): return True @final - async def __request_async(self, - topic: str, - payload: str, - timeout_ms: int = 10000) -> dict: + async def __request_async( + self, topic: str, payload: str, timeout_ms: int = 10000 + ) -> dict: fut_handler: asyncio.Future = self.main_loop.create_future() def on_msg_reply(payload: str, ctx: Any): @@ -1691,11 +1645,11 @@ class MipsLocalClient(_MipsClient): self.main_loop.call_soon_threadsafe(fut.set_result, payload) if not self.__request_external( - topic=topic, - payload=payload, - on_reply=on_msg_reply, - on_reply_ctx=fut_handler, - timeout_ms=timeout_ms, + topic=topic, + payload=payload, + on_reply=on_msg_reply, + on_reply_ctx=fut_handler, + timeout_ms=timeout_ms, ): # Request error fut_handler.set_result("internal request error") @@ -1714,9 +1668,8 @@ class MipsLocalClient(_MipsClient): item = self._get_prop_queue[did].pop() _LOGGER.debug("get prop, %s, %s", did, item) result_obj = await self.__request_async( - topic="proxy/get", - payload=item["param"], - timeout_ms=item["timeout_ms"]) + topic="proxy/get", payload=item["param"], timeout_ms=item["timeout_ms"] + ) if result_obj is None or "value" not in result_obj: item["fut"].set_result(None) else: @@ -1727,8 +1680,7 @@ class MipsLocalClient(_MipsClient): if self._get_prop_queue: self._get_prop_timer = self.main_loop.call_later( - 0.1, - lambda: self.main_loop.create_task(self.__get_prop_timer_handle( - ))) + 0.1, lambda: self.main_loop.create_task(self.__get_prop_timer_handle()) + ) else: self._get_prop_timer = None diff --git a/custom_components/xiaomi_home/select.py b/custom_components/xiaomi_home/select.py index 2c78d61..735c99a 100644 --- a/custom_components/xiaomi_home/select.py +++ b/custom_components/xiaomi_home/select.py @@ -70,8 +70,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a config entry.""" - device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][ - config_entry.entry_id] + device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][config_entry.entry_id] new_entities = [] for miot_device in device_list: @@ -86,13 +85,13 @@ async def async_setup_entry( for miot_device in device_list: if "device:light" in miot_device.spec_instance.urn: if miot_device.entity_list.get("light", []): - device_id = list( - miot_device.device_info.get("identifiers"))[0][1] + device_id = list(miot_device.device_info.get("identifiers"))[0][1] light_entity_id = miot_device.gen_device_entity_id(DOMAIN) new_light_select_entities.append( - LightCommandSendMode(hass=hass, - light_entity_id=light_entity_id, - device_id=device_id)) + LightCommandSendMode( + hass=hass, light_entity_id=light_entity_id, device_id=device_id + ) + ) if new_light_select_entities: async_add_entities(new_light_select_entities) @@ -109,8 +108,7 @@ class Select(MIoTPropertyEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.set_property_async(value=self.get_vlist_value( - description=option)) + await self.set_property_async(value=self.get_vlist_value(description=option)) @property def current_option(self) -> Optional[str]: @@ -123,20 +121,18 @@ class LightCommandSendMode(SelectEntity, RestoreEntity): then send other color temperatures and brightness or send them all at the same time. The default is to send one by one.""" - def __init__(self, hass: HomeAssistant, light_entity_id: str, - device_id: str): + def __init__(self, hass: HomeAssistant, light_entity_id: str, device_id: str): super().__init__() self.hass = hass self._device_id = device_id self._attr_name = "Command Send Mode" self._attr_unique_id = f"{light_entity_id}_command_send_mode" - self._attr_options = [ - "Send One by One", "Send Turn On First", "Send Together" - ] + self._attr_options = ["Send One by One", "Send Turn On First", "Send Together"] self._attr_device_info = {"identifiers": {(DOMAIN, device_id)}} self._attr_current_option = self._attr_options[0] # 默认选项 - self._attr_entity_category = (EntityCategory.CONFIG - ) # **重点:告诉 HA 这是配置类实体** + self._attr_entity_category = ( + EntityCategory.CONFIG + ) # **重点:告诉 HA 这是配置类实体** async def async_select_option(self, option: str): """处理用户选择的选项。""" @@ -147,8 +143,9 @@ class LightCommandSendMode(SelectEntity, RestoreEntity): async def async_added_to_hass(self): """在实体添加到 Home Assistant 时恢复上次的状态。""" await super().async_added_to_hass() - if (last_state := await self.async_get_last_state() - ) and last_state.state in self._attr_options: + if ( + last_state := await self.async_get_last_state() + ) and last_state.state in self._attr_options: self._attr_current_option = last_state.state @property